using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using SharpCompress; namespace MediaBrowser.Providers.Books.ComicInfo; /// /// ComicInfo reader. /// public static class ComicInfoReader { /// /// Filename to check for comic metadata either next to the comic file or inside the archive. /// public const string ComicRackMetaFile = "ComicInfo.xml"; /// /// Read comic book metadata. /// /// The XDocument to read for comic metadata. /// The resulting book. public static Book? ReadComicBookMetadata(XDocument xml) { var book = new Book(); var hasFoundMetadata = false; // this value is only used internally since Jellyfin has no manga flag var isManga = false; hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Title", title => book.Name = title); hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Manga", manga => isManga = manga.Equals("Yes", StringComparison.OrdinalIgnoreCase)); hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Series", series => book.SeriesName = series); hasFoundMetadata |= ReadIntInto(xml, "ComicInfo/Number", issue => book.IndexNumber = issue); hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Summary", summary => book.Overview = summary); hasFoundMetadata |= ReadIntInto(xml, "ComicInfo/Year", year => book.ProductionYear = year); hasFoundMetadata |= ReadThreePartDateInto(xml, "ComicInfo/Year", "ComicInfo/Month", "ComicInfo/Day", dateTime => book.PremiereDate = dateTime); hasFoundMetadata |= ReadCommaSeparatedStringsInto(xml, "ComicInfo/Genre", genres => genres.ForEach(genre => book.AddGenre(genre))); hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Publisher", publisher => book.SetStudios([publisher])); hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/AlternateSeries", title => { if (isManga) { // Software like ComicTagger (https://github.com/comictagger/comictagger) will use // this field for the series name in the original language when tagging manga. book.OriginalTitle = title; } else { // Some US comics can be part of cross-over story arcs. This field is then used to // specify an alternate series. } }); return hasFoundMetadata ? book : null; } /// /// Read people metadata. /// /// The XDocument to read for people metadata. /// The metadata result to update. public static void ReadPeopleMetadata(XDocument xml, MetadataResult metadataResult) { ReadCommaSeparatedStringsInto(xml, "ComicInfo/Writer", authors => { authors.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Author })); }); ReadCommaSeparatedStringsInto(xml, "ComicInfo/Penciller", pencillers => { pencillers.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Penciller })); }); ReadCommaSeparatedStringsInto(xml, "ComicInfo/Inker", inkers => { inkers.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Inker })); }); ReadCommaSeparatedStringsInto(xml, "ComicInfo/Letterer", letterers => { letterers.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Letterer })); }); ReadCommaSeparatedStringsInto(xml, "ComicInfo/CoverArtist", artists => { artists.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.CoverArtist })); }); ReadCommaSeparatedStringsInto(xml, "ComicInfo/Colourist", colorists => { colorists.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Colorist })); }); } /// /// Read culture information. /// /// the XDocument to read for metadata. /// The path to search. /// The action to take after parsing all metadata. public static void ReadCultureInfoInto(XDocument xml, string xPath, Action commitResult) { string? culture = null; if (!ReadStringInto(xml, xPath, value => culture = value)) { return; } // culture cannot be null here as the method would have returned earlier commitResult(new CultureInfo(culture!)); } private static bool ReadStringInto(XDocument xml, string xPath, Action commitResult) { var resultElement = xml.XPathSelectElement(xPath); if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.Value)) { commitResult(resultElement.Value); return true; } return false; } private static bool ReadCommaSeparatedStringsInto(XDocument xml, string xPath, Action> commitResult) { var resultElement = xml.XPathSelectElement(xPath); if (resultElement is null || string.IsNullOrWhiteSpace(resultElement.Value)) { return false; } try { var splits = resultElement.Value.Split(",").Select(p => p.Trim()).ToArray(); if (splits.Length < 1) { return false; } commitResult(splits); return true; } catch (ArgumentNullException) { return false; } } private static bool ReadIntInto(XDocument xml, string xPath, Action commitResult) { var resultElement = xml.XPathSelectElement(xPath); if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.Value)) { return ParseInt(resultElement.Value, commitResult); } return false; } private static bool ReadThreePartDateInto(XDocument xml, string yearXPath, string monthXPath, string dayXPath, Action commitResult) { int year = 0; int month = 0; int day = 0; var parsed = false; parsed |= ReadIntInto(xml, yearXPath, num => year = num); parsed |= ReadIntInto(xml, monthXPath, num => month = num); parsed |= ReadIntInto(xml, dayXPath, num => day = num); if (!parsed) { return false; } try { var dateTime = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Unspecified); commitResult(dateTime); return true; } catch (ArgumentOutOfRangeException) { return false; } } private static bool ParseInt(string input, Action commitResult) { if (int.TryParse(input, out var parsed)) { commitResult(parsed); return true; } return false; } }