< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Books.ComicImageProvider
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Books/ComicImageProvider.cs
Line coverage
11%
Covered lines: 5
Uncovered lines: 40
Coverable lines: 45
Total lines: 146
Line coverage: 11.1%
Branch coverage
0%
Covered branches: 0
Total branches: 48
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 6/11/2026 - 12:16:04 AM Line coverage: 11.1% (5/45) Branch coverage: 0% (0/48) Total lines: 146 6/11/2026 - 12:16:04 AM Line coverage: 11.1% (5/45) Branch coverage: 0% (0/48) Total lines: 146

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Name()100%210%
GetImage(...)0%620%
GetSupportedImages()100%210%
Supports(...)100%11100%
LoadCover()0%620%
FindCoverEntryInArchive(...)0%7280%
GetImageFormat(...)0%1332360%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Books/ComicImageProvider.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using Jellyfin.Extensions;
 8using MediaBrowser.Controller.Entities;
 9using MediaBrowser.Controller.Providers;
 10using MediaBrowser.Model.Drawing;
 11using MediaBrowser.Model.Entities;
 12using Microsoft.Extensions.Logging;
 13using SharpCompress.Archives;
 14
 15namespace MediaBrowser.Providers.Books;
 16
 17/// <summary>
 18/// The ComicImageProvider tries to find either an image named "cover" or, in case that
 19/// fails, just takes the first image inside the archive, hoping that it is the cover.
 20/// </summary>
 21public class ComicImageProvider : IDynamicImageProvider
 22{
 2123    private readonly string[] _comicBookExtensions = [".cb7", ".cbr", ".cbt", ".cbz"];
 2124    private readonly string[] _coverExtensions = [".png", ".jpeg", ".jpg", ".webp", ".bmp", ".gif"];
 25
 26    private readonly ILogger<ComicImageProvider> _logger;
 27
 28    /// <summary>
 29    /// Initializes a new instance of the <see cref="ComicImageProvider"/> class.
 30    /// </summary>
 31    /// <param name="logger">Instance of the <see cref="ILogger{ComicImageProvider}"/> interface.</param>
 32    public ComicImageProvider(ILogger<ComicImageProvider> logger)
 33    {
 2134        _logger = logger;
 2135    }
 36
 37    /// <inheritdoc />
 038    public string Name => "Comic Book Archive Cover Extractor";
 39
 40    /// <inheritdoc />
 41    public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
 42    {
 043        var extension = Path.GetExtension(item.Path);
 44
 045        if (_comicBookExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
 46        {
 047            return LoadCover(item);
 48        }
 49
 050        return Task.FromResult(new DynamicImageResponse { HasImage = false });
 51    }
 52
 53    /// <inheritdoc />
 54    public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
 55    {
 056        yield return ImageType.Primary;
 057    }
 58
 59    /// <inheritdoc />
 60    public bool Supports(BaseItem item)
 61    {
 5662        return item is Book;
 63    }
 64
 65    /// <summary>
 66    /// Tries to load a cover from the CBZ archive. Returns a response
 67    /// with no image if nothing is found.
 68    /// </summary>
 69    /// <param name="item">Item to check for covers.</param>
 70    private async Task<DynamicImageResponse> LoadCover(BaseItem item)
 71    {
 072        var memoryStream = new MemoryStream();
 73
 74        try
 75        {
 76            ImageFormat imageFormat;
 77
 078            using (Stream stream = File.OpenRead(item.Path))
 079            using (var archive = ArchiveFactory.Open(stream))
 80            {
 81                // throw exception to log results if no cover is found
 082                (var cover, imageFormat) = FindCoverEntryInArchive(archive) ?? throw new InvalidOperationException("no s
 83
 84                // copy the cover to memory stream
 085                await cover.OpenEntryStream().CopyToAsync(memoryStream).ConfigureAwait(false);
 086            }
 87
 88            // reset stream position after copying
 089            memoryStream.Position = 0;
 90
 091            return new DynamicImageResponse { HasImage = true, Stream = memoryStream, Format = imageFormat };
 92        }
 093        catch (Exception e)
 94        {
 095            _logger.LogError(e, "failed to load cover from {Path}", item.Path);
 096            return new DynamicImageResponse { HasImage = false };
 97        }
 098    }
 99
 100    /// <summary>
 101    /// Tries to find the entry containing the cover.
 102    /// </summary>
 103    /// <param name="archive">The archive to search.</param>
 104    /// <returns>The search result.</returns>
 105    private (IArchiveEntry CoverEntry, ImageFormat ImageFormat)? FindCoverEntryInArchive(IArchive archive)
 106    {
 107        IArchiveEntry? cover;
 108
 109        // only some comics will explicitly name their cover file
 110        // in many cases the cover will simply be the first image in the archive
 0111        foreach (var extension in _coverExtensions)
 112        {
 0113            cover = archive.Entries.FirstOrDefault(e => e.Key == "cover" + extension);
 114
 0115            if (cover is not null)
 116            {
 0117                var imageFormat = GetImageFormat(extension);
 118
 0119                return (cover, imageFormat);
 120            }
 121        }
 122
 0123        cover = archive.Entries.OrderBy(x => x.Key).FirstOrDefault(x => _coverExtensions.Contains(Path.GetExtension(x.Ke
 124
 0125        if (cover is not null)
 126        {
 0127            var imageFormat = GetImageFormat(Path.GetExtension(cover.Key ?? string.Empty));
 128
 0129            return (cover, imageFormat);
 130        }
 131
 0132        return null;
 133    }
 134
 0135    private static ImageFormat GetImageFormat(string extension) => extension.ToLowerInvariant() switch
 0136    {
 0137        ".jpg" => ImageFormat.Jpg,
 0138        ".jpeg" => ImageFormat.Jpg,
 0139        ".png" => ImageFormat.Png,
 0140        ".webp" => ImageFormat.Webp,
 0141        ".bmp" => ImageFormat.Bmp,
 0142        ".gif" => ImageFormat.Gif,
 0143        ".svg" => ImageFormat.Svg,
 0144        _ => throw new ArgumentException($"unsupported extension: {extension}"),
 0145    };
 146}