< 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
0%
Covered lines: 0
Uncovered lines: 89
Coverable lines: 89
Total lines: 246
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 52
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: 246 6/11/2026 - 12:16:04 AM Line coverage: 0% (0/89) Branch coverage: 0% (0/52) Total lines: 246

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
ReadMetadata()0%110100%
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.Linq;
 5using System.Text.Json;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Data.Enums;
 9using Jellyfin.Extensions.Json;
 10using MediaBrowser.Controller.Entities;
 11using MediaBrowser.Controller.Providers;
 12using MediaBrowser.Model.IO;
 13using MediaBrowser.Providers.Books.ComicBookInfo.Models;
 14using Microsoft.Extensions.Logging;
 15using SharpCompress.Archives.Zip;
 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    {
 034        _fileSystem = fileSystem;
 035        _logger = logger;
 036    }
 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 = File.OpenRead(path);
 52
 53            // not yet async: https://github.com/adamhathcock/sharpcompress/pull/565
 054            await using (stream.ConfigureAwait(false))
 055            using (var archive = ZipArchive.Open(stream))
 56            {
 057                if (!archive.IsComplete)
 58                {
 059                    _logger.LogError("incomplete comic archive: {Path}", info.Path);
 060                    return new MetadataResult<Book> { HasMetadata = false };
 61                }
 62
 063                var volume = archive.Volumes.First();
 64
 065                if (volume.Comment is null)
 66                {
 067                    _logger.LogInformation("missing ComicBookInfo in archive comment: {Path}", info.Path);
 068                    return new MetadataResult<Book> { HasMetadata = false };
 69                }
 70
 071                var comicBookMetadata = JsonSerializer.Deserialize<ComicBookInfoFormat>(volume.Comment, JsonDefaults.Opt
 72
 073                if (comicBookMetadata is null)
 74                {
 075                    _logger.LogError("ComicBookInfo deserialization failure: {Path}", info.Path);
 076                    return new MetadataResult<Book> { HasMetadata = false };
 77                }
 78
 079                return SaveMetadata(comicBookMetadata);
 80            }
 081        }
 082        catch (Exception ex)
 83        {
 084            _logger.LogError(ex, "failed to load ComicBookInfo metadata: {Path}", info.Path);
 085            return new MetadataResult<Book> { HasMetadata = false };
 86        }
 087    }
 88
 89    /// <inheritdoc />
 90    public bool HasItemChanged(BaseItem item)
 91    {
 092        var file = GetComicBookFile(item.Path);
 93
 094        if (file is null)
 95        {
 096            return false;
 97        }
 98
 099        return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
 100    }
 101
 102    private MetadataResult<Book> SaveMetadata(ComicBookInfoFormat comic)
 103    {
 0104        if (comic.Metadata is null)
 105        {
 0106            return new MetadataResult<Book> { HasMetadata = false };
 107        }
 108
 0109        var book = ReadComicBookMetadata(comic.Metadata);
 110
 0111        if (book is null)
 112        {
 0113            return new MetadataResult<Book> { HasMetadata = false };
 114        }
 115
 0116        var metadataResult = new MetadataResult<Book> { Item = book, HasMetadata = true };
 117
 0118        if (comic.Metadata.Language is not null)
 119        {
 0120            metadataResult.ResultLanguage = ReadCultureInfoInto(comic.Metadata.Language);
 121        }
 122
 0123        if (comic.Metadata.Credits.Count > 0)
 124        {
 0125            ReadPeopleMetadata(comic.Metadata, metadataResult);
 126        }
 127
 0128        return metadataResult;
 129    }
 130
 131    private FileSystemMetadata? GetComicBookFile(string path)
 132    {
 0133        var fileInfo = _fileSystem.GetFileSystemInfo(path);
 134
 0135        if (fileInfo.IsDirectory)
 136        {
 0137            return null;
 138        }
 139
 140        // only parse files that are known to have ComicBookInfo metadata
 0141        return fileInfo.Extension.Equals(".cbz", StringComparison.OrdinalIgnoreCase) ? fileInfo : null;
 142    }
 143
 144    private static Book? ReadComicBookMetadata(ComicBookInfoMetadata comic)
 145    {
 0146        var book = new Book();
 0147        var hasFoundMetadata = false;
 148
 0149        hasFoundMetadata |= ReadStringInto(comic.Title, title => book.Name = title);
 0150        hasFoundMetadata |= ReadStringInto(comic.Series, series => book.SeriesName = series);
 0151        hasFoundMetadata |= ReadStringInto(comic.Genre, genre => book.AddGenre(genre));
 0152        hasFoundMetadata |= ReadStringInto(comic.Comments, overview => book.Overview = overview);
 0153        hasFoundMetadata |= ReadStringInto(comic.Publisher, publisher => book.SetStudios([publisher]));
 154
 0155        if (comic.PublicationYear is not null)
 156        {
 0157            book.ProductionYear = comic.PublicationYear;
 0158            hasFoundMetadata = true;
 159        }
 160
 0161        if (comic.Issue is not null)
 162        {
 0163            book.IndexNumber = comic.Issue;
 0164            hasFoundMetadata = true;
 165        }
 166
 0167        if (comic.Tags.Count > 0)
 168        {
 0169            book.Tags = comic.Tags.ToArray();
 0170            hasFoundMetadata = true;
 171        }
 172
 0173        if (comic.PublicationYear is not null && comic.PublicationMonth is not null)
 174        {
 0175            book.PremiereDate = ReadTwoPartDateInto(comic.PublicationYear.Value, comic.PublicationMonth.Value);
 0176            hasFoundMetadata = true;
 177        }
 178
 0179        return hasFoundMetadata ? book : null;
 180    }
 181
 182    private static void ReadPeopleMetadata(ComicBookInfoMetadata comic, MetadataResult<Book> metadataResult)
 183    {
 0184        foreach (var person in comic.Credits)
 185        {
 0186            if (person.Person is null || person.Role is null)
 187            {
 188                continue;
 189            }
 190
 0191            if (person.Person.Contains(',', StringComparison.InvariantCultureIgnoreCase))
 192            {
 0193                var name = person.Person.Split(',');
 0194                person.Person = name[1].Trim(' ') + " " + name[0].Trim(' ');
 195            }
 196
 0197            if (!Enum.TryParse(person.Role, out PersonKind personKind))
 198            {
 0199                personKind = PersonKind.Unknown;
 200            }
 201
 0202            if (string.Equals("Colorer", person.Role, StringComparison.OrdinalIgnoreCase))
 203            {
 0204                personKind = PersonKind.Colorist;
 205            }
 206
 0207            metadataResult.AddPerson(new PersonInfo { Name = person.Person, Type = personKind });
 208        }
 0209    }
 210
 211    private static string? ReadCultureInfoInto(string language)
 212    {
 213        try
 214        {
 0215            return CultureInfo.GetCultureInfo(language).DisplayName;
 216        }
 0217        catch (CultureNotFoundException)
 218        {
 0219            return null;
 220        }
 0221    }
 222
 223    private static bool ReadStringInto(string? data, Action<string> commitResult)
 224    {
 0225        if (!string.IsNullOrWhiteSpace(data))
 226        {
 0227            commitResult(data);
 0228            return true;
 229        }
 230
 0231        return false;
 232    }
 233
 234    private static DateTime? ReadTwoPartDateInto(int year, int month)
 235    {
 236        try
 237        {
 238            // use first day of the month because this format doesn't include a day
 0239            return new DateTime(year, month, 1, 0, 0, 0, DateTimeKind.Unspecified);
 240        }
 0241        catch (ArgumentOutOfRangeException)
 242        {
 0243            return null;
 244        }
 0245    }
 246}