< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Books.ComicBookInfo.ComicBookInfoProvider
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs
Line coverage
52%
Covered lines: 47
Uncovered lines: 42
Coverable lines: 89
Total lines: 245
Line coverage: 52.8%
Branch coverage
36%
Covered branches: 18
Total branches: 50
Branch coverage: 36%
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: 0% (0/89) Branch coverage: 0% (0/52) Total lines: 2466/16/2026 - 12:14:30 AM Line coverage: 0% (0/86) Branch coverage: 0% (0/50) Total lines: 2386/28/2026 - 12:15:35 AM Line coverage: 3.4% (3/86) Branch coverage: 0% (0/50) Total lines: 23810/29/2026 - 12:17:11 AM Line coverage: 52.8% (47/89) Branch coverage: 36% (18/50) Total lines: 245 6/11/2026 - 12:16:04 AM Line coverage: 0% (0/89) Branch coverage: 0% (0/52) Total lines: 2466/16/2026 - 12:14:30 AM Line coverage: 0% (0/86) Branch coverage: 0% (0/50) Total lines: 2386/28/2026 - 12:15:35 AM Line coverage: 3.4% (3/86) Branch coverage: 0% (0/50) Total lines: 23810/29/2026 - 12:17:11 AM Line coverage: 52.8% (47/89) Branch coverage: 36% (18/50) Total lines: 245

Coverage delta

Coverage delta 50 -50

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ReadMetadata()62.5%10866.66%
HasItemChanged(...)0%2040%
SaveMetadata(...)50%11863.63%
GetComicBookFile(...)50%4475%
ReadComicBookMetadata(...)41.66%161270%
ReadPeopleMetadata(...)0%156120%
ReadCultureInfoInto(...)100%210%
ReadStringInto(...)100%22100%
ReadTwoPartDateInto(...)100%210%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.IO;
 4using System.IO.Compression;
 5using System.Linq;
 6using System.Text.Json;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using Jellyfin.Data.Enums;
 10using Jellyfin.Extensions.Json;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Controller.Providers;
 13using MediaBrowser.Model.IO;
 14using MediaBrowser.Providers.Books.ComicBookInfo.Models;
 15using Microsoft.Extensions.Logging;
 16
 17namespace MediaBrowser.Providers.Books.ComicBookInfo;
 18
 19/// <summary>
 20/// ComicBookInfo provider.
 21/// </summary>
 22public class ComicBookInfoProvider : IComicProvider
 23{
 24    private readonly ILogger<ComicBookInfoProvider> _logger;
 25    private readonly IFileSystem _fileSystem;
 26
 27    /// <summary>
 28    /// Initializes a new instance of the <see cref="ComicBookInfoProvider"/> class.
 29    /// </summary>
 30    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 31    /// <param name="logger">Instance of the <see cref="ILogger{ComicBookInfoProvider}"/> interface.</param>
 32    public ComicBookInfoProvider(IFileSystem fileSystem, ILogger<ComicBookInfoProvider> logger)
 33    {
 3234        _fileSystem = fileSystem;
 3235        _logger = logger;
 3236    }
 37
 38    /// <inheritdoc />
 39    public async ValueTask<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, Cancell
 40    {
 541        var path = GetComicBookFile(info.Path)?.FullName;
 42
 543        if (path is null)
 44        {
 045            _logger.LogDebug("could not load comic: {Path}", info.Path);
 046            return new MetadataResult<Book> { HasMetadata = false };
 47        }
 48
 49        try
 50        {
 551            Stream stream = AsyncFile.OpenRead(path);
 552            await using (stream.ConfigureAwait(false))
 53            {
 554                var archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Read, false, null, cancellationToken).
 555                await using (archive.ConfigureAwait(false))
 56                {
 57                    // ZipArchive.Comment is an empty string, not null, when the archive has no comment
 558                    if (string.IsNullOrWhiteSpace(archive.Comment))
 59                    {
 360                        _logger.LogDebug("missing ComicBookInfo in archive comment: {Path}", info.Path);
 361                        return new MetadataResult<Book> { HasMetadata = false };
 62                    }
 63
 264                    var comicBookMetadata = JsonSerializer.Deserialize<ComicBookInfoFormat>(archive.Comment, JsonDefault
 165                    if (comicBookMetadata is null)
 66                    {
 067                        _logger.LogError("ComicBookInfo deserialization failure: {Path}", info.Path);
 068                        return new MetadataResult<Book> { HasMetadata = false };
 69                    }
 70
 171                    return SaveMetadata(comicBookMetadata);
 72                }
 73            }
 074        }
 175        catch (JsonException ex)
 76        {
 77            // the archive comment is not reserved for ComicBookInfo, so any other content is not an error
 178            _logger.LogDebug("archive comment is not valid ComicBookInfo metadata: {Path}: {Message}", info.Path, ex.Mes
 179            return new MetadataResult<Book> { HasMetadata = false };
 80        }
 081        catch (Exception ex)
 82        {
 083            _logger.LogError(ex, "failed to load ComicBookInfo metadata: {Path}", info.Path);
 084            return new MetadataResult<Book> { HasMetadata = false };
 85        }
 586    }
 87
 88    /// <inheritdoc />
 89    public bool HasItemChanged(BaseItem item)
 90    {
 091        var file = GetComicBookFile(item.Path);
 92
 093        if (file is null)
 94        {
 095            return false;
 96        }
 97
 098        return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
 99    }
 100
 101    private MetadataResult<Book> SaveMetadata(ComicBookInfoFormat comic)
 102    {
 1103        if (comic.Metadata is null)
 104        {
 0105            return new MetadataResult<Book> { HasMetadata = false };
 106        }
 107
 1108        var book = ReadComicBookMetadata(comic.Metadata);
 109
 1110        if (book is null)
 111        {
 0112            return new MetadataResult<Book> { HasMetadata = false };
 113        }
 114
 1115        var metadataResult = new MetadataResult<Book> { Item = book, HasMetadata = true };
 116
 1117        if (comic.Metadata.Language is not null)
 118        {
 0119            metadataResult.ResultLanguage = ReadCultureInfoInto(comic.Metadata.Language);
 120        }
 121
 1122        if (comic.Metadata.Credits.Count > 0)
 123        {
 0124            ReadPeopleMetadata(comic.Metadata, metadataResult);
 125        }
 126
 1127        return metadataResult;
 128    }
 129
 130    private FileSystemMetadata? GetComicBookFile(string path)
 131    {
 5132        var fileInfo = _fileSystem.GetFileSystemInfo(path);
 133
 5134        if (fileInfo.IsDirectory)
 135        {
 0136            return null;
 137        }
 138
 139        // only parse files that are known to have ComicBookInfo metadata
 5140        return fileInfo.Extension.Equals(".cbz", StringComparison.OrdinalIgnoreCase) ? fileInfo : null;
 141    }
 142
 143    private static Book? ReadComicBookMetadata(ComicBookInfoMetadata comic)
 144    {
 1145        var book = new Book();
 1146        var hasFoundMetadata = false;
 147
 1148        hasFoundMetadata |= ReadStringInto(comic.Title, title => book.Name = title);
 1149        hasFoundMetadata |= ReadStringInto(comic.Series, series => book.SeriesName = series);
 1150        hasFoundMetadata |= ReadStringInto(comic.Genre, genre => book.AddGenre(genre));
 1151        hasFoundMetadata |= ReadStringInto(comic.Comments, overview => book.Overview = overview);
 1152        hasFoundMetadata |= ReadStringInto(comic.Publisher, publisher => book.SetStudios([publisher]));
 153
 1154        if (comic.PublicationYear is not null)
 155        {
 0156            book.ProductionYear = comic.PublicationYear;
 0157            hasFoundMetadata = true;
 158        }
 159
 1160        if (comic.Issue is not null)
 161        {
 1162            book.IndexNumber = comic.Issue;
 1163            hasFoundMetadata = true;
 164        }
 165
 1166        if (comic.Tags.Count > 0)
 167        {
 0168            book.Tags = comic.Tags.ToArray();
 0169            hasFoundMetadata = true;
 170        }
 171
 1172        if (comic.PublicationYear is not null && comic.PublicationMonth is not null)
 173        {
 0174            book.PremiereDate = ReadTwoPartDateInto(comic.PublicationYear.Value, comic.PublicationMonth.Value);
 0175            hasFoundMetadata = true;
 176        }
 177
 1178        return hasFoundMetadata ? book : null;
 179    }
 180
 181    private static void ReadPeopleMetadata(ComicBookInfoMetadata comic, MetadataResult<Book> metadataResult)
 182    {
 0183        foreach (var person in comic.Credits)
 184        {
 0185            if (person.Person is null || person.Role is null)
 186            {
 187                continue;
 188            }
 189
 0190            if (person.Person.Contains(',', StringComparison.InvariantCultureIgnoreCase))
 191            {
 0192                var name = person.Person.Split(',');
 0193                person.Person = name[1].Trim(' ') + " " + name[0].Trim(' ');
 194            }
 195
 0196            if (!Enum.TryParse(person.Role, out PersonKind personKind))
 197            {
 0198                personKind = PersonKind.Unknown;
 199            }
 200
 0201            if (string.Equals("Colorer", person.Role, StringComparison.OrdinalIgnoreCase))
 202            {
 0203                personKind = PersonKind.Colorist;
 204            }
 205
 0206            metadataResult.AddPerson(new PersonInfo { Name = person.Person, Type = personKind });
 207        }
 0208    }
 209
 210    private static string? ReadCultureInfoInto(string language)
 211    {
 212        try
 213        {
 0214            return CultureInfo.GetCultureInfo(language).TwoLetterISOLanguageName;
 215        }
 0216        catch (CultureNotFoundException)
 217        {
 0218            return null;
 219        }
 0220    }
 221
 222    private static bool ReadStringInto(string? data, Action<string> commitResult)
 223    {
 5224        if (!string.IsNullOrWhiteSpace(data))
 225        {
 2226            commitResult(data);
 2227            return true;
 228        }
 229
 3230        return false;
 231    }
 232
 233    private static DateTime? ReadTwoPartDateInto(int year, int month)
 234    {
 235        try
 236        {
 237            // use first day of the month because this format doesn't include a day
 0238            return new DateTime(year, month, 1, 0, 0, 0, DateTimeKind.Unspecified);
 239        }
 0240        catch (ArgumentOutOfRangeException)
 241        {
 0242            return null;
 243        }
 0244    }
 245}