using System; using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Books.ComicBookInfo.Models; using Microsoft.Extensions.Logging; using SharpCompress.Archives.Zip; namespace MediaBrowser.Providers.Books.ComicBookInfo; /// /// ComicBookInfo provider. /// public class ComicBookInfoProvider : IComicProvider { private readonly ILogger _logger; private readonly IFileSystem _fileSystem; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. public ComicBookInfoProvider(IFileSystem fileSystem, ILogger logger) { _fileSystem = fileSystem; _logger = logger; } /// public async ValueTask> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken) { var path = GetComicBookFile(info.Path)?.FullName; if (path is null) { _logger.LogError("could not load comic: {Path}", info.Path); return new MetadataResult { HasMetadata = false }; } try { Stream stream = File.OpenRead(path); // not yet async: https://github.com/adamhathcock/sharpcompress/pull/565 await using (stream.ConfigureAwait(false)) using (var archive = ZipArchive.Open(stream)) { if (!archive.IsComplete) { _logger.LogError("incomplete comic archive: {Path}", info.Path); return new MetadataResult { HasMetadata = false }; } var volume = archive.Volumes.First(); if (volume.Comment is null) { _logger.LogInformation("missing ComicBookInfo in archive comment: {Path}", info.Path); return new MetadataResult { HasMetadata = false }; } var comicBookMetadata = JsonSerializer.Deserialize(volume.Comment, JsonDefaults.Options); if (comicBookMetadata is null) { _logger.LogError("ComicBookInfo deserialization failure: {Path}", info.Path); return new MetadataResult { HasMetadata = false }; } return SaveMetadata(comicBookMetadata); } } catch (Exception ex) { _logger.LogError(ex, "failed to load ComicBookInfo metadata: {Path}", info.Path); return new MetadataResult { HasMetadata = false }; } } /// public bool HasItemChanged(BaseItem item) { var file = GetComicBookFile(item.Path); if (file is null) { return false; } return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; } private MetadataResult SaveMetadata(ComicBookInfoFormat comic) { if (comic.Metadata is null) { return new MetadataResult { HasMetadata = false }; } var book = ReadComicBookMetadata(comic.Metadata); if (book is null) { return new MetadataResult { HasMetadata = false }; } var metadataResult = new MetadataResult { Item = book, HasMetadata = true }; if (comic.Metadata.Language is not null) { metadataResult.ResultLanguage = ReadCultureInfoInto(comic.Metadata.Language); } if (comic.Metadata.Credits.Count > 0) { ReadPeopleMetadata(comic.Metadata, metadataResult); } return metadataResult; } private FileSystemMetadata? GetComicBookFile(string path) { var fileInfo = _fileSystem.GetFileSystemInfo(path); if (fileInfo.IsDirectory) { return null; } // only parse files that are known to have ComicBookInfo metadata return fileInfo.Extension.Equals(".cbz", StringComparison.OrdinalIgnoreCase) ? fileInfo : null; } private static Book? ReadComicBookMetadata(ComicBookInfoMetadata comic) { var book = new Book(); var hasFoundMetadata = false; hasFoundMetadata |= ReadStringInto(comic.Title, title => book.Name = title); hasFoundMetadata |= ReadStringInto(comic.Series, series => book.SeriesName = series); hasFoundMetadata |= ReadStringInto(comic.Genre, genre => book.AddGenre(genre)); hasFoundMetadata |= ReadStringInto(comic.Comments, overview => book.Overview = overview); hasFoundMetadata |= ReadStringInto(comic.Publisher, publisher => book.SetStudios([publisher])); if (comic.PublicationYear is not null) { book.ProductionYear = comic.PublicationYear; hasFoundMetadata = true; } if (comic.Issue is not null) { book.IndexNumber = comic.Issue; hasFoundMetadata = true; } if (comic.Tags.Count > 0) { book.Tags = comic.Tags.ToArray(); hasFoundMetadata = true; } if (comic.PublicationYear is not null && comic.PublicationMonth is not null) { book.PremiereDate = ReadTwoPartDateInto(comic.PublicationYear.Value, comic.PublicationMonth.Value); hasFoundMetadata = true; } return hasFoundMetadata ? book : null; } private static void ReadPeopleMetadata(ComicBookInfoMetadata comic, MetadataResult metadataResult) { foreach (var person in comic.Credits) { if (person.Person is null || person.Role is null) { continue; } if (person.Person.Contains(',', StringComparison.InvariantCultureIgnoreCase)) { var name = person.Person.Split(','); person.Person = name[1].Trim(' ') + " " + name[0].Trim(' '); } if (!Enum.TryParse(person.Role, out PersonKind personKind)) { personKind = PersonKind.Unknown; } if (string.Equals("Colorer", person.Role, StringComparison.OrdinalIgnoreCase)) { personKind = PersonKind.Colorist; } metadataResult.AddPerson(new PersonInfo { Name = person.Person, Type = personKind }); } } private static string? ReadCultureInfoInto(string language) { try { return CultureInfo.GetCultureInfo(language).DisplayName; } catch (CultureNotFoundException) { return null; } } private static bool ReadStringInto(string? data, Action commitResult) { if (!string.IsNullOrWhiteSpace(data)) { commitResult(data); return true; } return false; } private static DateTime? ReadTwoPartDateInto(int year, int month) { try { // use first day of the month because this format doesn't include a day return new DateTime(year, month, 1, 0, 0, 0, DateTimeKind.Unspecified); } catch (ArgumentOutOfRangeException) { return null; } } }