From 8233ac6c3e9cc898ddf6bb78d50b107d524d00f4 Mon Sep 17 00:00:00 2001 From: Patrick Farwick <9168045+MinecraftPlaye@users.noreply.github.com> Date: Sun, 16 Jan 2022 23:27:49 +0000 Subject: [PATCH] Refactor opf metadata reader for readability The image provider for ePuBs read the opf metadata file and various code segments in the OpfReader class were duplicated code. This commit refactors the opf reader for readability. Additionally, it moves opf related code from the ePuB image provider into the opf reader. --- .../Providers/BookProviderFromOpf.cs | 24 ++- .../Epub/EpubMetadataImageProvider.cs | 111 ++-------- .../Providers/Epub/EpubMetadataProvider.cs | 28 +-- .../Providers/OpfReader.cs | 201 ++++++++++++------ 4 files changed, 180 insertions(+), 184 deletions(-) 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)); } } }