< 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
3%
Covered lines: 3
Uncovered lines: 83
Coverable lines: 86
Total lines: 238
Line coverage: 3.4%
Branch coverage
0%
Covered branches: 0
Total branches: 50
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: 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: 238 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: 238

Coverage delta

Coverage delta 4 -4

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ReadMetadata()0%7280%
HasItemChanged(...)0%2040%
SaveMetadata(...)0%7280%
GetComicBookFile(...)0%2040%
ReadComicBookMetadata(...)0%156120%
ReadPeopleMetadata(...)0%156120%
ReadCultureInfoInto(...)100%210%
ReadStringInto(...)0%620%
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    {
 2234        _fileSystem = fileSystem;
 2235        _logger = logger;
 2236    }
 37
 38    /// <inheritdoc />
 39    public async ValueTask<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, Cancell
 40    {
 041        var path = GetComicBookFile(info.Path)?.FullName;
 42
 043        if (path is null)
 44        {
 045            _logger.LogError("could not load comic: {Path}", info.Path);
 046            return new MetadataResult<Book> { HasMetadata = false };
 47        }
 48
 49        try
 50        {
 051            Stream stream = AsyncFile.OpenRead(path);
 052            await using (stream.ConfigureAwait(false))
 53            {
 054                var archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Read, false, null, cancellationToken).
 055                await using (archive.ConfigureAwait(false))
 56                {
 057                    if (archive.Comment is null)
 58                    {
 059                        _logger.LogInformation("missing ComicBookInfo in archive comment: {Path}", info.Path);
 060                        return new MetadataResult<Book> { HasMetadata = false };
 61                    }
 62
 063                    var comicBookMetadata = JsonSerializer.Deserialize<ComicBookInfoFormat>(archive.Comment, JsonDefault
 064                    if (comicBookMetadata is null)
 65                    {
 066                        _logger.LogError("ComicBookInfo deserialization failure: {Path}", info.Path);
 067                        return new MetadataResult<Book> { HasMetadata = false };
 68                    }
 69
 070                    return SaveMetadata(comicBookMetadata);
 71                }
 72            }
 073        }
 074        catch (Exception ex)
 75        {
 076            _logger.LogError(ex, "failed to load ComicBookInfo metadata: {Path}", info.Path);
 077            return new MetadataResult<Book> { HasMetadata = false };
 78        }
 079    }
 80
 81    /// <inheritdoc />
 82    public bool HasItemChanged(BaseItem item)
 83    {
 084        var file = GetComicBookFile(item.Path);
 85
 086        if (file is null)
 87        {
 088            return false;
 89        }
 90
 091        return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
 92    }
 93
 94    private MetadataResult<Book> SaveMetadata(ComicBookInfoFormat comic)
 95    {
 096        if (comic.Metadata is null)
 97        {
 098            return new MetadataResult<Book> { HasMetadata = false };
 99        }
 100
 0101        var book = ReadComicBookMetadata(comic.Metadata);
 102
 0103        if (book is null)
 104        {
 0105            return new MetadataResult<Book> { HasMetadata = false };
 106        }
 107
 0108        var metadataResult = new MetadataResult<Book> { Item = book, HasMetadata = true };
 109
 0110        if (comic.Metadata.Language is not null)
 111        {
 0112            metadataResult.ResultLanguage = ReadCultureInfoInto(comic.Metadata.Language);
 113        }
 114
 0115        if (comic.Metadata.Credits.Count > 0)
 116        {
 0117            ReadPeopleMetadata(comic.Metadata, metadataResult);
 118        }
 119
 0120        return metadataResult;
 121    }
 122
 123    private FileSystemMetadata? GetComicBookFile(string path)
 124    {
 0125        var fileInfo = _fileSystem.GetFileSystemInfo(path);
 126
 0127        if (fileInfo.IsDirectory)
 128        {
 0129            return null;
 130        }
 131
 132        // only parse files that are known to have ComicBookInfo metadata
 0133        return fileInfo.Extension.Equals(".cbz", StringComparison.OrdinalIgnoreCase) ? fileInfo : null;
 134    }
 135
 136    private static Book? ReadComicBookMetadata(ComicBookInfoMetadata comic)
 137    {
 0138        var book = new Book();
 0139        var hasFoundMetadata = false;
 140
 0141        hasFoundMetadata |= ReadStringInto(comic.Title, title => book.Name = title);
 0142        hasFoundMetadata |= ReadStringInto(comic.Series, series => book.SeriesName = series);
 0143        hasFoundMetadata |= ReadStringInto(comic.Genre, genre => book.AddGenre(genre));
 0144        hasFoundMetadata |= ReadStringInto(comic.Comments, overview => book.Overview = overview);
 0145        hasFoundMetadata |= ReadStringInto(comic.Publisher, publisher => book.SetStudios([publisher]));
 146
 0147        if (comic.PublicationYear is not null)
 148        {
 0149            book.ProductionYear = comic.PublicationYear;
 0150            hasFoundMetadata = true;
 151        }
 152
 0153        if (comic.Issue is not null)
 154        {
 0155            book.IndexNumber = comic.Issue;
 0156            hasFoundMetadata = true;
 157        }
 158
 0159        if (comic.Tags.Count > 0)
 160        {
 0161            book.Tags = comic.Tags.ToArray();
 0162            hasFoundMetadata = true;
 163        }
 164
 0165        if (comic.PublicationYear is not null && comic.PublicationMonth is not null)
 166        {
 0167            book.PremiereDate = ReadTwoPartDateInto(comic.PublicationYear.Value, comic.PublicationMonth.Value);
 0168            hasFoundMetadata = true;
 169        }
 170
 0171        return hasFoundMetadata ? book : null;
 172    }
 173
 174    private static void ReadPeopleMetadata(ComicBookInfoMetadata comic, MetadataResult<Book> metadataResult)
 175    {
 0176        foreach (var person in comic.Credits)
 177        {
 0178            if (person.Person is null || person.Role is null)
 179            {
 180                continue;
 181            }
 182
 0183            if (person.Person.Contains(',', StringComparison.InvariantCultureIgnoreCase))
 184            {
 0185                var name = person.Person.Split(',');
 0186                person.Person = name[1].Trim(' ') + " " + name[0].Trim(' ');
 187            }
 188
 0189            if (!Enum.TryParse(person.Role, out PersonKind personKind))
 190            {
 0191                personKind = PersonKind.Unknown;
 192            }
 193
 0194            if (string.Equals("Colorer", person.Role, StringComparison.OrdinalIgnoreCase))
 195            {
 0196                personKind = PersonKind.Colorist;
 197            }
 198
 0199            metadataResult.AddPerson(new PersonInfo { Name = person.Person, Type = personKind });
 200        }
 0201    }
 202
 203    private static string? ReadCultureInfoInto(string language)
 204    {
 205        try
 206        {
 0207            return CultureInfo.GetCultureInfo(language).DisplayName;
 208        }
 0209        catch (CultureNotFoundException)
 210        {
 0211            return null;
 212        }
 0213    }
 214
 215    private static bool ReadStringInto(string? data, Action<string> commitResult)
 216    {
 0217        if (!string.IsNullOrWhiteSpace(data))
 218        {
 0219            commitResult(data);
 0220            return true;
 221        }
 222
 0223        return false;
 224    }
 225
 226    private static DateTime? ReadTwoPartDateInto(int year, int month)
 227    {
 228        try
 229        {
 230            // use first day of the month because this format doesn't include a day
 0231            return new DateTime(year, month, 1, 0, 0, 0, DateTimeKind.Unspecified);
 232        }
 0233        catch (ArgumentOutOfRangeException)
 234        {
 0235            return null;
 236        }
 0237    }
 238}