diff --git a/Jellyfin.Plugin.Bookshelf/Providers/BookProviderFromOpf.cs b/Jellyfin.Plugin.Bookshelf/Providers/BookProviderFromOpf.cs index b17e3a66..7fac5a92 100644 --- a/Jellyfin.Plugin.Bookshelf/Providers/BookProviderFromOpf.cs +++ b/Jellyfin.Plugin.Bookshelf/Providers/BookProviderFromOpf.cs @@ -46,21 +46,24 @@ public bool HasChanged(BaseItem item, IDirectoryService directoryService) public Task> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken) { var path = GetXmlFile(info.Path).FullName; - var result = new MetadataResult(); try { - var item = new Book(); - result.HasMetadata = true; - result.Item = item; - ReadOpfData(result, path, cancellationToken); + var result = ReadOpfData(path, cancellationToken); + + if (result is null) + { + return Task.FromResult(new MetadataResult { HasMetadata = false }); + } + else + { + return Task.FromResult(result); + } } catch (FileNotFoundException) { - result.HasMetadata = false; + return Task.FromResult(new MetadataResult { HasMetadata = false }); } - - return Task.FromResult(result); } private FileSystemMetadata GetXmlFile(string path) @@ -85,14 +88,15 @@ private FileSystemMetadata GetXmlFile(string path) return file.Exists ? file : _fileSystem.GetFileInfo(Path.Combine(directoryPath, CalibreOpfFile)); } - private void ReadOpfData(MetadataResult bookResult, string metaFile, CancellationToken cancellationToken) + private MetadataResult ReadOpfData(string metaFile, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var doc = new XmlDocument(); doc.Load(metaFile); - OpfReader.ReadOpfData(bookResult, doc, _logger, cancellationToken); + var utilities = new OpfReader(doc, _logger); + return utilities.ReadOpfData(cancellationToken); } } } diff --git a/Jellyfin.Plugin.Bookshelf/Providers/Epub/EpubMetadataImageProvider.cs b/Jellyfin.Plugin.Bookshelf/Providers/Epub/EpubMetadataImageProvider.cs index ca34727e..0bae7aa1 100644 --- a/Jellyfin.Plugin.Bookshelf/Providers/Epub/EpubMetadataImageProvider.cs +++ b/Jellyfin.Plugin.Bookshelf/Providers/Epub/EpubMetadataImageProvider.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Net; +using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.Bookshelf.Providers.Epub { @@ -17,8 +18,16 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub /// public class EpubMetadataImageProvider : IDynamicImageProvider { - private const string DcNamespace = @"http://purl.org/dc/elements/1.1/"; - private const string OpfNamespace = @"http://www.idpf.org/2007/opf"; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public EpubMetadataImageProvider(ILogger logger) + { + _logger = logger; + } /// public string Name => "Epub Metadata"; @@ -46,92 +55,10 @@ public Task GetImage(BaseItem item, ImageType type, Cancel return Task.FromResult(new DynamicImageResponse { HasImage = false }); } - private bool IsValidImage(string? mimeType) - { - return !string.IsNullOrEmpty(mimeType) - && !string.IsNullOrWhiteSpace(MimeTypes.ToExtension(mimeType)); - } - - private EpubCover? ReadManifestItem(XmlNode manifestNode, string opfRootDirectory) - { - var href = manifestNode.Attributes?["href"]?.Value; - var mediaType = manifestNode.Attributes?["media-type"]?.Value; - - if (string.IsNullOrEmpty(href) - || string.IsNullOrEmpty(mediaType) - || !IsValidImage(mediaType)) - { - return null; - } - - var coverPath = Path.Combine(opfRootDirectory, href); - return new EpubCover(mediaType, coverPath); - } - - private EpubCover? ReadCoverPath(XmlDocument opf, string opfRootDirectory) - { - var namespaceManager = new XmlNamespaceManager(opf.NameTable); - namespaceManager.AddNamespace("dc", DcNamespace); - namespaceManager.AddNamespace("opf", OpfNamespace); - - var coverImagePropertyNode = opf.SelectSingleNode("//opf:item[@properties='cover-image']", namespaceManager); - if (coverImagePropertyNode is not null) - { - var coverImageProperty = ReadManifestItem(coverImagePropertyNode, opfRootDirectory); - if (coverImageProperty != null) - { - return coverImageProperty; - } - } - - var coverIdNode = opf.SelectSingleNode("//opf:item[@id='cover']", namespaceManager); - if (coverIdNode is not null) - { - var coverId = ReadManifestItem(coverIdNode, opfRootDirectory); - if (coverId != null) - { - return coverId; - } - } - - var coverImageIdNode = opf.SelectSingleNode("//opf:item[@id='cover-image']", namespaceManager); - if (coverImageIdNode is not null) - { - var coverImageId = ReadManifestItem(coverImageIdNode, opfRootDirectory); - if (coverImageId != null) - { - return coverImageId; - } - } - - var metaCoverImage = opf.SelectSingleNode("//opf:meta[@name='cover']", namespaceManager); - var content = metaCoverImage?.Attributes?["content"]?.Value; - if (string.IsNullOrEmpty(content) || metaCoverImage is null) - { - return null; - } - - var coverPath = Path.Combine("Images", content); - var coverFileManifest = opf.SelectSingleNode($"//opf:item[@href='{coverPath}']", namespaceManager); - var mediaType = coverFileManifest?.Attributes?["media-type"]?.Value; - if (coverFileManifest?.Attributes is not null - && !string.IsNullOrEmpty(mediaType) && IsValidImage(mediaType)) - { - return new EpubCover(mediaType, Path.Combine(opfRootDirectory, coverPath)); - } - - var coverFileIdManifest = opf.SelectSingleNode($"//opf:item[@id='{content}']", namespaceManager); - if (coverFileIdManifest is not null) - { - return ReadManifestItem(coverFileIdManifest, opfRootDirectory); - } - - return null; - } - private async Task LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory) { - var coverRef = ReadCoverPath(opf, opfRootDirectory); + var utilities = new OpfReader(opf, _logger); + var coverRef = utilities.ReadCoverPath(opfRootDirectory); if (coverRef == null) { return new DynamicImageResponse { HasImage = false }; @@ -193,17 +120,5 @@ private Task GetFromZip(BaseItem item) return LoadCover(epub, opfDocument, opfRootDirectory); } - - private readonly struct EpubCover - { - public EpubCover(string coverMimeType, string coverPath) - { - (MimeType, Path) = (coverMimeType, coverPath); - } - - public string MimeType { get; } - - public string Path { get; } - } } } diff --git a/Jellyfin.Plugin.Bookshelf/Providers/Epub/EpubMetadataProvider.cs b/Jellyfin.Plugin.Bookshelf/Providers/Epub/EpubMetadataProvider.cs index 89a47956..045a5612 100644 --- a/Jellyfin.Plugin.Bookshelf/Providers/Epub/EpubMetadataProvider.cs +++ b/Jellyfin.Plugin.Bookshelf/Providers/Epub/EpubMetadataProvider.cs @@ -40,21 +40,22 @@ public Task> GetMetadata( CancellationToken cancellationToken) { var path = GetEpubFile(info.Path)?.FullName; - var result = new MetadataResult(); - if (path == null) + if (path is null) { - result.HasMetadata = false; + return Task.FromResult(new MetadataResult { HasMetadata = false }); + } + + var result = ReadEpubAsZip(path, cancellationToken); + + if (result is null) + { + return Task.FromResult(new MetadataResult { HasMetadata = false }); } else { - var item = new Book(); - result.HasMetadata = true; - result.Item = item; - ReadEpubAsZip(result, path, cancellationToken); + return Task.FromResult(result); } - - return Task.FromResult(result); } private FileSystemMetadata? GetEpubFile(string path) @@ -74,20 +75,20 @@ public Task> GetMetadata( return fileInfo; } - private void ReadEpubAsZip(MetadataResult result, string path, CancellationToken cancellationToken) + private MetadataResult? ReadEpubAsZip(string path, CancellationToken cancellationToken) { using var epub = ZipFile.OpenRead(path); var opfFilePath = EpubUtils.ReadContentFilePath(epub); if (opfFilePath == null) { - return; + return null; } var opf = epub.GetEntry(opfFilePath); if (opf == null) { - return; + return null; } using var opfStream = opf.Open(); @@ -95,7 +96,8 @@ private void ReadEpubAsZip(MetadataResult result, string path, Cancellatio var opfDocument = new XmlDocument(); opfDocument.Load(opfStream); - OpfReader.ReadOpfData(result, opfDocument, _logger, cancellationToken); + var utilities = new OpfReader(opfDocument, _logger); + return utilities.ReadOpfData(cancellationToken); } } } diff --git a/Jellyfin.Plugin.Bookshelf/Providers/OpfReader.cs b/Jellyfin.Plugin.Bookshelf/Providers/OpfReader.cs index 1fdc6c63..3f0caeb2 100644 --- a/Jellyfin.Plugin.Bookshelf/Providers/OpfReader.cs +++ b/Jellyfin.Plugin.Bookshelf/Providers/OpfReader.cs @@ -1,11 +1,13 @@ using System; using System.Globalization; +using System.IO; using System.Linq; using System.Threading; using System.Xml; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.Bookshelf.Providers @@ -13,69 +15,114 @@ namespace Jellyfin.Plugin.Bookshelf.Providers /// /// OPF reader. /// - public static class OpfReader + /// The type of category. + public class OpfReader { private const string DcNamespace = @"http://purl.org/dc/elements/1.1/"; private const string OpfNamespace = @"http://www.idpf.org/2007/opf"; + private readonly XmlNamespaceManager _namespaceManager; + + private readonly XmlDocument _document; + + private readonly ILogger _logger; + /// - /// Read opf data. + /// Initializes a new instance of the class. /// - /// The metadata result to update. /// The xdocument to parse. /// Instance of the interface. - /// The cancellation token. - /// The type of category. - public static void ReadOpfData( - MetadataResult bookResult, - XmlDocument doc, - ILogger logger, - CancellationToken cancellationToken) + public OpfReader(XmlDocument doc, ILogger logger) { - var book = bookResult.Item; - - cancellationToken.ThrowIfCancellationRequested(); - - var namespaceManager = new XmlNamespaceManager(doc.NameTable); - namespaceManager.AddNamespace("dc", DcNamespace); - namespaceManager.AddNamespace("opf", OpfNamespace); - - var nameNode = doc.SelectSingleNode("//dc:title", namespaceManager); + _document = doc; + _logger = logger; + _namespaceManager = new XmlNamespaceManager(_document.NameTable); + _namespaceManager.AddNamespace("dc", DcNamespace); + _namespaceManager.AddNamespace("opf", OpfNamespace); + } - if (!string.IsNullOrEmpty(nameNode?.InnerText)) + /// + /// Checks the file path for the existence of a cover. + /// + /// The root directory in which the opf metadata file is located. + /// Returns the found cover and it's type or null. + public (string MimeType, string Path)? ReadCoverPath(string opfRootDirectory) + { + var coverImage = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@properties='cover-image']"); + if (coverImage is not null) { - book.Name = nameNode.InnerText; + return coverImage; } - var overViewNode = doc.SelectSingleNode("//dc:description", namespaceManager); - - if (!string.IsNullOrEmpty(overViewNode?.InnerText)) + var coverId = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@id='cover']"); + if (coverId is not null) { - book.Overview = overViewNode.InnerText; + return coverId; } - var studioNode = doc.SelectSingleNode("//dc:publisher", namespaceManager); + var coverImageId = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@id='cover-image']"); + if (coverImageId is not null) + { + return coverImageId; + } - if (!string.IsNullOrEmpty(studioNode?.InnerText)) + var metaCoverImage = _document.SelectSingleNode("//opf:meta[@name='cover']", _namespaceManager); + var content = metaCoverImage?.Attributes?["content"]?.Value; + if (string.IsNullOrEmpty(content) || metaCoverImage is null) { - book.AddStudio(studioNode.InnerText); + return null; } - var isbnNode = doc.SelectSingleNode("//dc:identifier[@opf:scheme='ISBN']", namespaceManager); + var coverPath = Path.Combine("Images", content); + var coverFileManifest = _document.SelectSingleNode($"//opf:item[@href='{coverPath}']", _namespaceManager); + var mediaType = coverFileManifest?.Attributes?["media-type"]?.Value; + if (coverFileManifest?.Attributes is not null + && !string.IsNullOrEmpty(mediaType) && IsValidImage(mediaType)) + { + return (mediaType, Path.Combine(opfRootDirectory, coverPath)); + } - if (!string.IsNullOrEmpty(isbnNode?.InnerText)) + var coverFileIdManifest = _document.SelectSingleNode($"//opf:item[@id='{content}']", _namespaceManager); + if (coverFileIdManifest is not null) { - book.SetProviderId("ISBN", isbnNode.InnerText); + return ReadManifestItem(coverFileIdManifest, opfRootDirectory); } - var amazonNode = doc.SelectSingleNode("//dc:identifier[@opf:scheme='AMAZON']", namespaceManager); + return null; + } - if (!string.IsNullOrEmpty(amazonNode?.InnerText)) + /// + /// Read opf data. + /// + /// The cancellation token. + /// The metadata result to update. + public MetadataResult ReadOpfData( + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var book = CreateBookFromOpf(); + var bookResult = new MetadataResult { Item = book, HasMetadata = true }; + ReadStringInto("//dc:creator", author => { - book.SetProviderId("Amazon", amazonNode.InnerText); - } + var person = new PersonInfo { Name = author, Type = "Author" }; + bookResult.AddPerson(person); + }); + + return bookResult; + } + + private Book CreateBookFromOpf() + { + var book = new Book(); + + ReadStringInto("//dc:title", title => book.Name = title); + ReadStringInto("//dc:description", summary => book.Overview = summary); + ReadStringInto("//dc:publisher", publisher => book.AddStudio(publisher)); + ReadStringInto("//dc:identifier[@opf:scheme='ISBN']", isbn => book.SetProviderId("ISBN", isbn)); + ReadStringInto("//dc:identifier[@opf:scheme='AMAZON']", amazon => book.SetProviderId("Amazon", amazon)); - var genresNodes = doc.SelectNodes("//dc:subject", namespaceManager); + var genresNodes = _document.SelectNodes("//dc:subject", _namespaceManager); if (genresNodes != null && genresNodes.Count > 0) { @@ -86,56 +133,84 @@ public static void ReadOpfData( } } - var authorNode = doc.SelectSingleNode("//dc:creator", namespaceManager); + ReadInt32AttributeInto("//opf:meta[@name='calibre:series_index']", index => book.IndexNumber = index); + ReadInt32AttributeInto("//opf:meta[@name='calibre:rating']", rating => book.CommunityRating = rating); - if (!string.IsNullOrEmpty(authorNode?.InnerText)) - { - var person = new PersonInfo { Name = authorNode.InnerText, Type = "Author" }; - - bookResult.AddPerson(person); - } - - var seriesIndexNode = doc.SelectSingleNode("//opf:meta[@name='calibre:series_index']", namespaceManager); + var seriesNameNode = _document.SelectSingleNode("//opf:meta[@name='calibre:series']", _namespaceManager); - if (!string.IsNullOrEmpty(seriesIndexNode?.Attributes?["content"]?.Value)) + if (!string.IsNullOrEmpty(seriesNameNode?.Attributes?["content"]?.Value)) { try { - book.IndexNumber = Convert.ToInt32(seriesIndexNode.Attributes["content"]?.Value, CultureInfo.InvariantCulture); + book.SeriesName = seriesNameNode.Attributes["content"]?.Value; } catch (Exception) { - logger.LogError("Error parsing Calibre series index"); + _logger.LogError("Error parsing Calibre series name"); } } - var seriesNameNode = doc.SelectSingleNode("//opf:meta[@name='calibre:series']", namespaceManager); + return book; + } - if (!string.IsNullOrEmpty(seriesNameNode?.Attributes?["content"]?.Value)) + private void ReadStringInto(string xPath, Action commitResult) + { + var resultElement = _document.SelectSingleNode(xPath, _namespaceManager); + if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.InnerText)) + { + commitResult(resultElement.InnerText); + } + } + + private void ReadInt32AttributeInto(string xPath, Action commitResult) + { + var resultElement = _document.SelectSingleNode(xPath, _namespaceManager); + var resultValue = resultElement?.Attributes?["content"]?.Value; + if (!string.IsNullOrEmpty(resultValue)) { try { - book.SeriesName = seriesNameNode.Attributes["content"]?.Value; + commitResult(Convert.ToInt32(resultValue, CultureInfo.InvariantCulture)); } - catch (Exception) + catch (Exception e) { - logger.LogError("Error parsing Calibre series name"); + _logger.LogError(e, "Error converting to int32"); } } + } - var ratingNode = doc.SelectSingleNode("//opf:meta[@name='calibre:rating']", namespaceManager); + private (string MimeType, string Path)? ReadEpubCoverInto(string opfRootDirectory, string xPath) + { + var resultElement = _document.SelectSingleNode(xPath, _namespaceManager); + if (resultElement is not null) + { + var resultValue = ReadManifestItem(resultElement, opfRootDirectory); + return resultValue; + } + + return null; + } - if (!string.IsNullOrEmpty(ratingNode?.Attributes?["content"]?.Value)) + private (string MimeType, string Path)? ReadManifestItem(XmlNode manifestNode, string opfRootDirectory) + { + var href = manifestNode.Attributes?["href"]?.Value; + var mediaType = manifestNode.Attributes?["media-type"]?.Value; + + if (string.IsNullOrEmpty(href) + || string.IsNullOrEmpty(mediaType) + || !IsValidImage(mediaType)) { - try - { - book.CommunityRating = Convert.ToInt32(ratingNode.Attributes["content"]?.Value, CultureInfo.InvariantCulture); - } - catch (Exception) - { - logger.LogError("Error parsing Calibre rating node"); - } + return null; } + + var coverPath = Path.Combine(opfRootDirectory, href); + return (MimeType: mediaType, Path: coverPath); + } + + private bool IsValidImage(string? mimeType) + { + return !string.IsNullOrEmpty(mimeType) + && !string.IsNullOrWhiteSpace(MimeTypes.ToExtension(mimeType)); } } }