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;
}
}
}