More analyze

This commit is contained in:
Cody Robibero 2021-11-06 11:45:58 -06:00
parent 9dc2cf2a36
commit 79a51dda5e
31 changed files with 848 additions and 548 deletions

View File

@ -2,7 +2,10 @@
namespace Jellyfin.Plugin.Bookshelf.Configuration
{
/// <summary>
/// Instance of the empty plugin configuration.
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
}
}
}

View File

@ -9,7 +9,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<NoWarn>CS1591</NoWarn>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@ -25,7 +25,6 @@
<!-- Code Analyzers-->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.*" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
</ItemGroup>

View File

@ -8,20 +8,34 @@ using MediaBrowser.Model.Serialization;
namespace Jellyfin.Plugin.Bookshelf
{
/// <summary>
/// Plugin entrypoint.
/// </summary>
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
/// <inheritdoc />
public override string Name => "Bookshelf";
/// <inheritdoc />
public override Guid Id => Guid.Parse("9c4e63f1-031b-4f25-988b-4f7d78a8b53e");
public static Plugin Instance { get; private set; }
/// <summary>
/// Gets the current plugin instance.
/// </summary>
public static Plugin? Instance { get; private set; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
@ -34,4 +48,4 @@ namespace Jellyfin.Plugin.Bookshelf
};
}
}
}
}

View File

@ -1,7 +1,7 @@
using MediaBrowser.Common.Plugins;
using Jellyfin.Plugin.Bookshelf.Providers.ComicBook;
using Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo;
using Jellyfin.Plugin.Bookshelf.Providers;
using Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo;
using Jellyfin.Plugin.Bookshelf.Providers.ComicInfo;
using MediaBrowser.Common.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Plugin.Bookshelf
@ -11,6 +11,7 @@ namespace Jellyfin.Plugin.Bookshelf
/// </summary>
public class PluginServiceRegistrator : IPluginServiceRegistrator
{
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection)
{
// register the proxy local metadata provider for comic files

View File

@ -9,6 +9,9 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Bookshelf.Providers
{
/// <summary>
/// OPF book provider.
/// </summary>
public class BookProviderFromOpf : ILocalMetadataProvider<Book>, IHasItemChangeMonitor
{
private const string StandardOpfFile = "content.opf";
@ -20,20 +23,28 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
private readonly ILogger<BookProviderFromOpf> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="BookProviderFromOpf"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{BookProviderFromOpf}"/> interface.</param>
public BookProviderFromOpf(IFileSystem fileSystem, ILogger<BookProviderFromOpf> logger)
{
_fileSystem = fileSystem;
_logger = logger;
}
/// <inheritdoc />
public string Name => "Open Packaging Format";
/// <inheritdoc />
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
var file = GetXmlFile(item.Path);
return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
}
/// <inheritdoc />
public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
{
var path = GetXmlFile(info.Path).FullName;
@ -58,7 +69,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
{
var fileInfo = _fileSystem.GetFileSystemInfo(path);
var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path));
var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path)!);
var directoryPath = directoryInfo.FullName;
@ -78,8 +89,6 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
private void ReadOpfData(MetadataResult<Book> bookResult, string metaFile, CancellationToken cancellationToken)
{
var book = bookResult.Item;
cancellationToken.ThrowIfCancellationRequested();
var doc = new XmlDocument();
@ -88,4 +97,4 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
OpfReader.ReadOpfData(bookResult, doc, cancellationToken, _logger);
}
}
}
}

View File

@ -11,8 +11,7 @@ using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
namespace Jellyfin.Plugin.Bookshelf.Providers
{
/// <summary>
/// The ComicBookImageProvider tries find either a image named "cover" or,
@ -21,22 +20,28 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
/// </summary>
public class ComicBookImageProvider : IDynamicImageProvider
{
private const string CbzFileExtension = ".cbz";
private readonly ILogger<ComicBookImageProvider> _logger;
private const string CBZ_FILE_EXTENSION = ".cbz";
public string Name => "Comic Book Zip Archive Cover Extractor";
/// <summary>
/// Initializes a new instance of the <see cref="ComicBookImageProvider"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{ComicBookImageProvider}"/> interface.</param>
public ComicBookImageProvider(ILogger<ComicBookImageProvider> logger)
{
_logger = logger;
}
/// <inheritdoc />
public string Name => "Comic Book Zip Archive Cover Extractor";
/// <inheritdoc />
public async Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
//Check if the file is a .cbz file
// Check if the file is a .cbz file
var extension = Path.GetExtension(item.Path);
if (string.Equals(extension, CBZ_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
if (string.Equals(extension, CbzFileExtension, StringComparison.OrdinalIgnoreCase))
{
return await LoadCover(item);
}
@ -46,11 +51,13 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
}
}
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType> { ImageType.Primary };
yield return ImageType.Primary;
}
/// <inheritdoc />
public bool Supports(BaseItem item)
{
return item is Book;
@ -60,28 +67,27 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
/// Tries to load a cover from the .cbz archive. Returns a response
/// with no image if nothing is found.
/// </summary>
/// <param name="BaseItem">Item to load a cover for.</param>
/// <param name="item">Item to load a cover for.</param>
private async Task<DynamicImageResponse> LoadCover(BaseItem item)
{
//The image will be loaded into memory, create stream
// The image will be loaded into memory, create stream
var memoryStream = new MemoryStream();
try
{
//Open the .cbz
//This should return a valid reference or throw
// Open the .cbz
// This should return a valid reference or throw
using var archive = ZipFile.OpenRead(item.Path);
//If no cover is found, throw exception to log results
// If no cover is found, throw exception to log results
var (cover, imageFormat) = FindCoverEntryInZip(archive) ?? throw new InvalidOperationException("No supported cover found");
//Copy our cover to memory stream
// Copy our cover to memory stream
await cover.Open().CopyToAsync(memoryStream);
//Reset stream position after copying
// Reset stream position after copying
memoryStream.Position = 0;
//Return the response
// Return the response
return new DynamicImageResponse
{
HasImage = true,
@ -91,13 +97,17 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
}
catch (Exception e)
{
//Log and return nothing
_logger.LogError(e, "Failed to load cover from {0}", item.Path);
// Log and return nothing
_logger.LogError(e, "Failed to load cover from {Path}", item.Path);
return new DynamicImageResponse { HasImage = false };
}
}
/// <summary>
/// Tries to find the entry containing the cover.
/// </summary>
/// <param name="archive">The archive to search.</param>
/// <returns>The search result.</returns>
private (ZipArchiveEntry coverEntry, ImageFormat imageFormat)? FindCoverEntryInZip(ZipArchive archive)
{
foreach (ImageFormat imageFormat in Enum.GetValues(typeof(ImageFormat)))
@ -109,7 +119,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
// that it is the first jpeg entry (and page)
var cover = archive.GetEntry("cover" + extension) ?? archive.Entries.OrderBy(x => x.Name).FirstOrDefault(x => x.Name.EndsWith(extension));
//If we have found something, return immideatly
// If we have found something, return immediately
if (cover is not null)
{
return (cover, imageFormat);

View File

@ -1,17 +1,22 @@
using System.Text.Json.Serialization;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
{
/// <summary>
/// Comic book info credit dto.
/// </summary>
public class ComicBookInfoCredit
{
/// <summary>
/// Gets or sets the person name.
/// </summary>
[JsonPropertyName("person")]
public string? Person { get; set; }
/// <summary>
/// Gets or sets the role.
/// </summary>
[JsonPropertyName("role")]
public string? Role { get; set; }
[JsonPropertyName("primary")]
public string? Primary { get; set; }
}
}

View File

@ -1,16 +1,27 @@
using System.Text.Json.Serialization;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
{
/// <summary>
/// Comic book info format dto.
/// </summary>
public class ComicBookInfoFormat
{
/// <summary>
/// Gets or sets the app id.
/// </summary>
[JsonPropertyName("appID")]
public string? AppId { get; set; }
/// <summary>
/// Gets or sets the last modified timestamp.
/// </summary>
[JsonPropertyName("lastModified")]
public string? LastModified { get; set; }
/// <summary>
/// Gets or sets the metadata.
/// </summary>
[JsonPropertyName("ComicBookInfo/1.0")]
public ComicBookInfoMetadata? Metadata { get; set; }
}

View File

@ -1,55 +1,106 @@
using System;
using System.Text.Json.Serialization;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
{
/// <summary>
/// Comic book info metadata dto.
/// </summary>
public class ComicBookInfoMetadata
{
/// <summary>
/// Gets or sets the series.
/// </summary>
[JsonPropertyName("series")]
public string? Series { get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <summary>
/// Gets or sets the publisher.
/// </summary>
[JsonPropertyName("publisher")]
public string? Publisher { get; set; }
/// <summary>
/// Gets or sets the publication month.
/// </summary>
[JsonPropertyName("publicationMonth")]
public int? PublicationMonth { get; set; }
/// <summary>
/// Gets or sets the publication year.
/// </summary>
[JsonPropertyName("publicationYear")]
public int? PublicationYear { get; set; }
/// <summary>
/// Gets or sets the issue number.
/// </summary>
[JsonPropertyName("issue")]
public int? Issue { get; set; }
/// <summary>
/// Gets or sets the number of issues.
/// </summary>
[JsonPropertyName("numberOfIssues")]
public int? NumberOfIssues { get; set; }
/// <summary>
/// Gets or sets the volume number.
/// </summary>
[JsonPropertyName("volume")]
public int? Volume { get; set; }
/// <summary>
/// Gets or sets the number of volumes.
/// </summary>
[JsonPropertyName("numberOfVolumes")]
public int? NumberOfVolumes { get; set; }
/// <summary>
/// Gets or sets the rating.
/// </summary>
[JsonPropertyName("rating")]
public int? Rating { get; set; }
/// <summary>
/// Gets or sets the genre.
/// </summary>
[JsonPropertyName("genre")]
public string? Genre { get; set; }
/// <summary>
/// Gets or sets the language.
/// </summary>
[JsonPropertyName("language")]
public string? Language { get; set; }
/// <summary>
/// Gets or sets the country.
/// </summary>
[JsonPropertyName("country")]
public string? Country { get; set; }
/// <summary>
/// Gets or sets the list of credits.
/// </summary>
[JsonPropertyName("credits")]
public ComicBookInfoCredit[]? Credits { get; set; }
public ComicBookInfoCredit[] Credits { get; set; } = Array.Empty<ComicBookInfoCredit>();
/// <summary>
/// Gets or sets the list of tags.
/// </summary>
[JsonPropertyName("tags")]
public string[]? Tags { get; set; }
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the comments.
/// </summary>
[JsonPropertyName("comments")]
public string? Comments { get; set; }
}

View File

@ -4,32 +4,39 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.Zip;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using Jellyfin.Extensions.Json;
using SharpCompress.Archives.Zip;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
{
/// <summary>
/// Comic book info provider.
/// </summary>
public class ComicBookInfoProvider : IComicFileProvider
{
private readonly ILogger<ComicBookInfoProvider> _logger;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="ComicBookInfoProvider"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{ComicBookInfoProvider}"/> interface.</param>
public ComicBookInfoProvider(IFileSystem fileSystem, ILogger<ComicBookInfoProvider> logger)
{
_fileSystem = fileSystem;
_logger = logger;
}
public async Task<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
/// <inheritdoc />
public async ValueTask<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
{
var path = GetComicBookFile(info.Path)?.FullName;
@ -41,7 +48,8 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
try
{
using Stream stream = File.OpenRead(path);
await using Stream stream = File.OpenRead(path);
// not yet async: https://github.com/adamhathcock/sharpcompress/pull/565
using var archive = ZipArchive.Open(stream);
@ -50,7 +58,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
var volume = archive.Volumes.First();
if (volume.Comment is not null)
{
using var jsonStream = new MemoryStream(Encoding.UTF8.GetBytes(volume.Comment));
await using var jsonStream = new MemoryStream(Encoding.UTF8.GetBytes(volume.Comment));
var comicBookMetadata = await JsonSerializer.DeserializeAsync<ComicBookInfoFormat>(jsonStream, JsonDefaults.Options, cancellationToken);
if (comicBookMetadata is null)
@ -67,6 +75,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
return new MetadataResult<Book> { HasMetadata = false };
}
}
_logger.LogError("Could not load ComicBookInfo metadata for {Path}", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
@ -77,7 +86,8 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
}
}
public bool HasItemChanged(BaseItem item, IDirectoryService directoryService)
/// <inheritdoc />
public bool HasItemChanged(BaseItem item)
{
var file = GetComicBookFile(item.Path);
@ -110,7 +120,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
metadataResult.ResultLanguage = ReadCultureInfoAsThreeLetterIsoInto(comic.Metadata.Language);
}
if (comic.Metadata.Credits is not null && comic.Metadata.Credits.Length > 0)
if (comic.Metadata.Credits.Length > 0)
{
foreach (var person in comic.Metadata.Credits)
{
@ -132,11 +142,11 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
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(new[] { publisher }));
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(new[] { publisher }));
if (comic.PublicationYear is not null)
{
@ -179,21 +189,22 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
commitResult(data);
return true;
}
return false;
}
private DateTime? ReadTwoPartDateInto(int year, int month)
{
//Try-Catch because DateTime actually wants a real date, how boring
// Try-Catch because DateTime actually wants a real date, how boring
try
{
//The format does not provide a day, set it to be always the first day of the month
// The format does not provide a day, set it to be always the first day of the month
var dateTime = new DateTime(year, month, 1);
return dateTime;
}
catch (Exception)
{
//Nothing to do here
// Nothing to do here
return null;
}
}
@ -206,7 +217,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
}
catch (Exception)
{
//Ignored
// Ignored
return null;
}
}
@ -221,14 +232,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
}
// Only parse files that are known to have internal metadata
if (fileInfo.Extension.Equals(".cbz", StringComparison.OrdinalIgnoreCase))
{
return fileInfo;
}
else
{
return null;
}
return fileInfo.Extension.Equals(".cbz", StringComparison.OrdinalIgnoreCase) ? fileInfo : null;
}
}
}

View File

@ -1,34 +1,34 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers
{
/// <summary>
/// Comic file provider.
/// </summary>
public class ComicFileProvider : ILocalMetadataProvider<Book>, IHasItemChangeMonitor
{
protected readonly ILogger<ComicFileProvider> _logger;
private readonly IEnumerable<IComicFileProvider> _comicFileProviders;
private readonly IFileSystem _fileSystem;
private readonly IEnumerable<IComicFileProvider> _iComicFileProviders;
public string Name => "Comic Provider";
public ComicFileProvider(IFileSystem fileSystem, ILogger<ComicFileProvider> logger, IEnumerable<IComicFileProvider> iComicFileProviders)
/// <summary>
/// Initializes a new instance of the <see cref="ComicFileProvider"/> class.
/// </summary>
/// <param name="comicFileProviders">The list of comic file providers.</param>
public ComicFileProvider(IEnumerable<IComicFileProvider> comicFileProviders)
{
this._fileSystem = fileSystem;
this._logger = logger;
this._iComicFileProviders = iComicFileProviders;
_comicFileProviders = comicFileProviders;
}
/// <inheritdoc />
public string Name => "Comic Provider";
/// <inheritdoc />
public async Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
{
foreach (IComicFileProvider iComicFileProvider in _iComicFileProviders)
foreach (IComicFileProvider iComicFileProvider in _comicFileProviders)
{
var metadata = await iComicFileProvider.ReadMetadata(info, directoryService, cancellationToken);
@ -37,20 +37,22 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
return metadata;
}
}
return new MetadataResult<Book> { HasMetadata = false };
}
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
bool IHasItemChangeMonitor.HasChanged(BaseItem item, IDirectoryService directoryService)
{
foreach (IComicFileProvider iComicFileProvider in _iComicFileProviders)
foreach (IComicFileProvider iComicFileProvider in _comicFileProviders)
{
var fileChanged = iComicFileProvider.HasItemChanged(item, directoryService);
var fileChanged = iComicFileProvider.HasItemChanged(item);
if (fileChanged)
{
return fileChanged;
}
}
return false;
}
}

View File

@ -7,56 +7,42 @@ using System.Xml.XPath;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicInfo
{
/// <summary>
/// Comic info xml utilities.
/// </summary>
public class ComicInfoXmlUtilities : IComicInfoXmlUtilities
{
/// <summary>
/// Read all metadata for the Jellyfin book about the comic itself,
/// returns null if nothing was found.
/// </summary>
/// <param name="xml"> The xml document to read from</param>
/// <returns></returns>
/// <inheritdoc />
public Book? ReadComicBookMetadata(XDocument xml)
{
var book = new Book();
var hasFoundMetadata = false;
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Title", (title) => book.Name = title);
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/AlternateSeries", (title) => book.OriginalTitle = title);
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 |= ReadCommaSeperatedStringsInto(xml, "ComicInfo/Genre", (generes) =>
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Title", title => book.Name = title);
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/AlternateSeries", title => book.OriginalTitle = title);
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 |= ReadCommaSeperatedStringsInto(xml, "ComicInfo/Genre", genres =>
{
foreach (var genere in generes)
foreach (var genre in genres)
{
book.AddGenre(genere);
book.AddGenre(genre);
}
});
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Publisher", (publisher) => book.SetStudios(new[] { publisher }));
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Publisher", publisher => book.SetStudios(new[] { publisher }));
if (hasFoundMetadata)
{
return book;
}
else
{
return null;
}
return hasFoundMetadata ? book : null;
}
/// <summary>
/// Read all people related metadata about the comic itself.
/// </summary>
/// <param name="xdocument">The xml document to read from</param>
/// <param name="metadataResult">The metadata result to write the values into</param>
/// <inheritdoc />
public void ReadPeopleMetadata(XDocument xdocument, MetadataResult<Book> metadataResult)
{
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Writer", (authors) =>
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Writer", authors =>
{
foreach (var author in authors)
{
@ -64,7 +50,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
metadataResult.AddPerson(person);
}
});
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Penciller", (pencilers) =>
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Penciller", pencilers =>
{
foreach (var penciller in pencilers)
{
@ -72,7 +58,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
metadataResult.AddPerson(person);
}
});
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Inker", (inkers) =>
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Inker", inkers =>
{
foreach (var inker in inkers)
{
@ -80,7 +66,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
metadataResult.AddPerson(person);
}
});
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Letterer", (letterers) =>
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Letterer", letterers =>
{
foreach (var letterer in letterers)
{
@ -88,7 +74,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
metadataResult.AddPerson(person);
}
});
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/CoverArtist", (coverartists) =>
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/CoverArtist", coverartists =>
{
foreach (var coverartist in coverartists)
{
@ -96,7 +82,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
metadataResult.AddPerson(person);
}
});
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Colourist", (colourists) =>
ReadCommaSeperatedStringsInto(xdocument, "ComicInfo/Colourist", colourists =>
{
foreach (var colourist in colourists)
{
@ -106,29 +92,26 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
});
}
/// <summary>
/// Read language culture information and commit the result.
/// </summary>
/// <param name="xml">The xml document to read from</param>
/// <param name="xPath">The xml tag to read the information from</param>
/// <param name="commitResult">What to do with the result</param>
/// <returns></returns>
/// <inheritdoc />
public bool ReadCultureInfoInto(XDocument xml, string xPath, Action<CultureInfo> commitResult)
{
string? culture = null;
//Try to read into culture string
if (!ReadStringInto(xml, xPath, (value) => culture = value)) return false;
// Try to read into culture string
if (!ReadStringInto(xml, xPath, value => culture = value))
{
return false;
}
try
{
//Culture cannot be null here as the method would have returned earlier
// Culture cannot be null here as the method would have returned earlier
commitResult(new CultureInfo(culture!));
return true;
}
catch (Exception)
{
//Ignored
// Ignored
return false;
}
}
@ -141,6 +124,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
commitResult(resultElement.Value);
return true;
}
return false;
}
@ -163,10 +147,11 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
}
catch (Exception)
{
//Nothing to do here except acknowledging
// Nothing to do here except acknowledging
return false;
}
}
return false;
}
@ -177,6 +162,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
{
return ParseInt(resultElement.Value, commitResult);
}
return false;
}
@ -187,24 +173,26 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
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);
parsed |= ReadIntInto(xml, yearXPath, num => year = num);
parsed |= ReadIntInto(xml, monthXPath, num => month = num);
parsed |= ReadIntInto(xml, dayXPath, num => day = num);
//Apparently there were some values inside if this does not return
if (!parsed) return false;
DateTime? dateTime = null;
// Apparently there were some values inside if this does not return
if (!parsed)
{
return false;
}
//Try-Catch because DateTime actually wants a real date, how boring
// Try-Catch because DateTime actually wants a real date, how boring
try
{
dateTime = new DateTime(year, month, day);
commitResult(dateTime.Value);
var dateTime = new DateTime(year, month, day);
commitResult(dateTime);
return true;
}
catch (Exception)
{
//Nothing to do here except acknowledging
// Nothing to do here except acknowledging
return false;
}
}

View File

@ -9,8 +9,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicInfo
{
/// <summary>
/// Handles metadata for comics which is saved as an XML document. This XML document is not part
@ -18,33 +17,37 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
/// </summary>
public class ExternalComicInfoProvider : IComicFileProvider
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<ExternalComicInfoProvider> _logger;
private const string ComicRackMetaFile = "ComicInfo.xml";
private readonly IFileSystem _fileSystem;
private readonly ILogger<ExternalComicInfoProvider> _logger;
private readonly ComicInfoXmlUtilities _utilities = new ComicInfoXmlUtilities();
/// <summary>
/// Initializes a new instance of the <see cref="ExternalComicInfoProvider"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{ExternalComicInfoProvider}"/> interface.</param>
public ExternalComicInfoProvider(IFileSystem fileSystem, ILogger<ExternalComicInfoProvider> logger)
{
this._logger = logger;
this._fileSystem = fileSystem;
_logger = logger;
_fileSystem = fileSystem;
}
public async Task<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
/// <inheritdoc />
public async ValueTask<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
{
var comicInfoXml = await this.LoadXml(info, directoryService, cancellationToken);
var comicInfoXml = await LoadXml(info, directoryService, cancellationToken);
if (comicInfoXml is null)
{
this._logger.LogInformation("Could not load ComicInfo metadata for {Path} from XML file", info.Path);
_logger.LogInformation("Could not load ComicInfo metadata for {Path} from XML file", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
var book = this._utilities.ReadComicBookMetadata(comicInfoXml);
var book = _utilities.ReadComicBookMetadata(comicInfoXml);
//If we found no metadata about the book itself, abort mission
// If we found no metadata about the book itself, abort mission
if (book is null)
{
return new MetadataResult<Book> { HasMetadata = false };
@ -52,18 +55,19 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
var metadataResult = new MetadataResult<Book> { Item = book, HasMetadata = true };
//We have found metadata like the title
//let's search for the people like the author and save the found metadata
this._utilities.ReadPeopleMetadata(comicInfoXml, metadataResult);
// We have found metadata like the title
// let's search for the people like the author and save the found metadata
_utilities.ReadPeopleMetadata(comicInfoXml, metadataResult);
this._utilities.ReadCultureInfoInto(comicInfoXml, "ComicInfo/LanguageISO", (cultureInfo) => metadataResult.ResultLanguage = cultureInfo.ThreeLetterISOLanguageName);
_utilities.ReadCultureInfoInto(comicInfoXml, "ComicInfo/LanguageISO", cultureInfo => metadataResult.ResultLanguage = cultureInfo.ThreeLetterISOLanguageName);
return metadataResult;
}
public bool HasItemChanged(BaseItem item, IDirectoryService directoryService)
/// <inheritdoc />
public bool HasItemChanged(BaseItem item)
{
var file = this.GetXmlFilePath(item.Path);
var file = GetXmlFilePath(item.Path);
return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
}
@ -82,6 +86,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
using var reader = XmlReader.Create(path, new XmlReaderSettings { Async = true });
var comicInfoXml = XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken);
// Read data from XML
return await comicInfoXml;
}
@ -96,7 +101,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
{
var fileInfo = _fileSystem.GetFileSystemInfo(path);
var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path));
var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path)!);
var directoryPath = directoryInfo.FullName;

View File

@ -1,19 +1,37 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicInfo
{
/// <summary>
/// Xml utilities.
/// </summary>
public interface IComicInfoXmlUtilities
{
/// <summary>
/// Read comic book metadata.
/// </summary>
/// <param name="xml">The xdocument to read metadata from.</param>
/// <returns>The resulting book.</returns>
Book? ReadComicBookMetadata(XDocument xml);
/// <summary>
/// Read people metadata.
/// </summary>
/// <param name="xdocument">The xdocument to read metadata from.</param>
/// <param name="metadataResult">The metadata result to update.</param>
void ReadPeopleMetadata(XDocument xdocument, MetadataResult<Book> metadataResult);
/// <summary>
/// Read culture info.
/// </summary>
/// <param name="xml">the xdocument to read metadata from.</param>
/// <param name="xPath">The xpath to search.</param>
/// <param name="commitResult">The action to take after parsing.</param>
/// <returns>Read status.</returns>
bool ReadCultureInfoInto(XDocument xml, string xPath, Action<CultureInfo> commitResult);
}
}

View File

@ -8,8 +8,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
#nullable enable
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicInfo
{
/// <summary>
/// Handles metadata for comics which is saved as an XML document inside of the comic itself.
@ -17,30 +16,34 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
public class InternalComicInfoProvider : IComicFileProvider
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<InternalComicInfoProvider> _logger;
private readonly IComicInfoXmlUtilities _utilities = new ComicInfoXmlUtilities();
private readonly ComicInfoXmlUtilities _utilities = new ComicInfoXmlUtilities();
/// <summary>
/// Initializes a new instance of the <see cref="InternalComicInfoProvider"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{InternalComicInfoProvider}"/> interface.</param>
public InternalComicInfoProvider(IFileSystem fileSystem, ILogger<InternalComicInfoProvider> logger)
{
this._logger = logger;
this._fileSystem = fileSystem;
_logger = logger;
_fileSystem = fileSystem;
}
public async Task<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
/// <inheritdoc />
public async ValueTask<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
{
var comicInfoXml = await this.LoadXml(info, directoryService, cancellationToken);
var comicInfoXml = await LoadXml(info, directoryService, cancellationToken);
if (comicInfoXml is null)
{
this._logger.LogInformation("Could not load ComicInfo metadata for {Path} from XML file. No internal XML in comic archive", info.Path);
_logger.LogInformation("Could not load ComicInfo metadata for {Path} from XML file. No internal XML in comic archive", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
var book = this._utilities.ReadComicBookMetadata(comicInfoXml);
var book = _utilities.ReadComicBookMetadata(comicInfoXml);
//If we found no metadata about the book itself, abort mission
// If we found no metadata about the book itself, abort mission
if (book is null)
{
return new MetadataResult<Book> { HasMetadata = false };
@ -48,30 +51,31 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
var metadataResult = new MetadataResult<Book> { Item = book, HasMetadata = true };
//We have found metadata like the title
//let's search for the people like the author and save the found metadata
this._utilities.ReadPeopleMetadata(comicInfoXml, metadataResult);
// We have found metadata like the title
// let's search for the people like the author and save the found metadata
_utilities.ReadPeopleMetadata(comicInfoXml, metadataResult);
this._utilities.ReadCultureInfoInto(comicInfoXml, "ComicInfo/LanguageISO", (cultureInfo) => metadataResult.ResultLanguage = cultureInfo.ThreeLetterISOLanguageName);
_utilities.ReadCultureInfoInto(comicInfoXml, "ComicInfo/LanguageISO", cultureInfo => metadataResult.ResultLanguage = cultureInfo.ThreeLetterISOLanguageName);
return metadataResult;
}
public bool HasItemChanged(BaseItem item, IDirectoryService directoryService)
/// <inheritdoc />
public bool HasItemChanged(BaseItem item)
{
var file = this.GetComicBookFile(item.Path);
var file = GetComicBookFile(item.Path);
if (file is null)
{
return false;
}
return file.Exists && this._fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
}
protected async Task<XDocument?> LoadXml(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
private async Task<XDocument?> LoadXml(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
{
var path = this.GetComicBookFile(info.Path)?.FullName;
var path = GetComicBookFile(info.Path)?.FullName;
if (path is null)
{
@ -92,7 +96,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
}
// Open the xml
using var containerStream = container.Open();
await using var containerStream = container.Open();
var comicInfoXml = XDocument.LoadAsync(containerStream, LoadOptions.None, cancellationToken);
// Read data from XML
@ -100,14 +104,14 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBook
}
catch (Exception e)
{
this._logger.LogError(e, "Could not load internal xml from {Path}", path);
_logger.LogError(e, "Could not load internal xml from {Path}", path);
return null;
}
}
private FileSystemMetadata? GetComicBookFile(string path)
{
var fileInfo = this._fileSystem.GetFileSystemInfo(path);
var fileInfo = _fileSystem.GetFileSystemInfo(path);
if (fileInfo.IsDirectory)
{

View File

@ -5,25 +5,28 @@ using MediaBrowser.Model.Providers;
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
{
/// <summary>
/// Comic vine volume external id.
/// </summary>
public class ComicVineVolumeExternalId : IExternalId
{
public static string KeyName => "ComicVineVolume";
public string Key => KeyName;
/// <inheritdoc />
public string Key => "ComicVineVolume";
/// <inheritdoc />
public string ProviderName => "Comic Vine Volume";
/// <inheritdoc />
public ExternalIdMediaType? Type
=> null; // TODO: enum does not yet have the Book type
// TODO: Is there a url?
public string UrlFormatString =>
null;
/// <inheritdoc />
public string UrlFormatString => string.Empty;
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
{
return item is Book;
}
}
}
}

View File

@ -1,90 +0,0 @@
#pragma warning disable SA1300
#pragma warning disable SA1402
#pragma warning disable SA1649
using System.Collections.Generic;
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
{
public class SearchResult
{
public string error { get; set; }
public int limit { get; set; }
public int offset { get; set; }
public int number_of_page_results { get; set; }
public int number_of_total_results { get; set; }
public int status_code { get; set; }
public List<Result> results { get; set; }
public string version { get; set; }
}
public class Result
{
public string aliases { get; set; }
public string api_detail_url { get; set; }
public string cover_date { get; set; }
public string date_added { get; set; }
public string date_last_updated { get; set; }
public string deck { get; set; }
public string description { get; set; }
public bool has_staff_review { get; set; }
public int id { get; set; }
public ImageUrls image { get; set; }
public string issue_number { get; set; }
public string name { get; set; }
public string site_detail_url { get; set; }
public string store_date { get; set; }
public Volume volume { get; set; }
public string resource_type { get; set; }
}
public class ImageUrls
{
public string icon_url { get; set; }
public string medium_url { get; set; }
public string screen_url { get; set; }
public string small_url { get; set; }
public string super_url { get; set; }
public string thumb_url { get; set; }
public string tiny_url { get; set; }
}
public class Volume
{
public string api_detail_url { get; set; }
public int id { get; set; }
public string name { get; set; }
public string site_detail_url { get; set; }
}
}

View File

@ -9,34 +9,33 @@ 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.Epub
{
/// <summary>
/// Epub metadata image provider.
/// </summary>
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<EpubMetadataImageProvider> _logger;
public EpubMetadataImageProvider(ILogger<EpubMetadataImageProvider> logger)
{
_logger = logger;
}
/// <inheritdoc />
public string Name => "Epub Metadata";
/// <inheritdoc />
public bool Supports(BaseItem item)
{
return item is Book;
}
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType> { ImageType.Primary };
yield return ImageType.Primary;
}
/// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
if (string.Equals(Path.GetExtension(item.Path), ".epub", StringComparison.OrdinalIgnoreCase))
@ -47,23 +46,26 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
private bool IsValidImage(string mimeType)
private bool IsValidImage(string? mimeType)
{
return !string.IsNullOrWhiteSpace(MimeTypes.ToExtension(mimeType));
return !string.IsNullOrEmpty(mimeType)
&& !string.IsNullOrWhiteSpace(MimeTypes.ToExtension(mimeType));
}
private EpubCover? ReadManifestItem(XmlNode manifestNode, string opfRootDirectory)
{
if (manifestNode?.Attributes?["href"]?.Value != null &&
manifestNode.Attributes?["media-type"]?.Value != null &&
IsValidImage(manifestNode.Attributes["media-type"].Value))
var href = manifestNode.Attributes?["href"]?.Value;
var mediaType = manifestNode.Attributes?["media-type"]?.Value;
if (string.IsNullOrEmpty(href)
|| string.IsNullOrEmpty(mediaType)
|| !IsValidImage(mediaType))
{
var coverMimeType = manifestNode.Attributes["media-type"].Value;
var coverPath = Path.Combine(opfRootDirectory, manifestNode.Attributes["href"].Value);
return new EpubCover(coverMimeType, coverPath);
return null;
}
return null;
var coverPath = Path.Combine(opfRootDirectory, href);
return new EpubCover(mediaType, coverPath);
}
private EpubCover? ReadCoverPath(XmlDocument opf, string opfRootDirectory)
@ -72,44 +74,55 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
namespaceManager.AddNamespace("dc", DcNamespace);
namespaceManager.AddNamespace("opf", OpfNamespace);
var coverImagePropertyNode =
opf.SelectSingleNode("//opf:item[@properties='cover-image']", namespaceManager);
var coverImageProperty = ReadManifestItem(coverImagePropertyNode, opfRootDirectory);
if (coverImageProperty != null)
var coverImagePropertyNode = opf.SelectSingleNode("//opf:item[@properties='cover-image']", namespaceManager);
if (coverImagePropertyNode is not null)
{
return coverImageProperty;
var coverImageProperty = ReadManifestItem(coverImagePropertyNode, opfRootDirectory);
if (coverImageProperty != null)
{
return coverImageProperty;
}
}
var coverIdNode =
opf.SelectSingleNode("//opf:item[@id='cover']", namespaceManager);
var coverId = ReadManifestItem(coverIdNode, opfRootDirectory);
if (coverId != null)
var coverIdNode = opf.SelectSingleNode("//opf:item[@id='cover']", namespaceManager);
if (coverIdNode is not null)
{
return coverId;
var coverId = ReadManifestItem(coverIdNode, opfRootDirectory);
if (coverId != null)
{
return coverId;
}
}
var coverImageIdNode =
opf.SelectSingleNode("//opf:item[@id='cover-image']", namespaceManager);
var coverImageId = ReadManifestItem(coverImageIdNode, opfRootDirectory);
if (coverImageId != null)
var coverImageIdNode = opf.SelectSingleNode("//opf:item[@id='cover-image']", namespaceManager);
if (coverImageIdNode is not null)
{
return coverImageId;
var coverImageId = ReadManifestItem(coverImageIdNode, opfRootDirectory);
if (coverImageId != null)
{
return coverImageId;
}
}
var metaCoverImage = opf.SelectSingleNode("//opf:meta[@name='cover']", namespaceManager);
if (metaCoverImage?.Attributes?["content"]?.Value != null)
var content = metaCoverImage?.Attributes?["content"]?.Value;
if (string.IsNullOrEmpty(content) || metaCoverImage is null)
{
var metaContent = metaCoverImage.Attributes["content"].Value;
var coverPath = Path.Combine("Images", metaContent);
var coverFileManifest = opf.SelectSingleNode($"//opf:item[@href='{coverPath}']", namespaceManager);
if (coverFileManifest?.Attributes?["media-type"]?.Value != null &&
IsValidImage(coverFileManifest.Attributes["media-type"].Value))
{
var coverMimeType = coverFileManifest.Attributes["media-type"].Value;
return new EpubCover(coverMimeType, Path.Combine(opfRootDirectory, coverPath));
}
return null;
}
var coverFileIdManifest = opf.SelectSingleNode($"//opf:item[@id='{metaContent}']", namespaceManager);
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);
}
@ -161,6 +174,10 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
}
var opfRootDirectory = Path.GetDirectoryName(opfFilePath);
if (string.IsNullOrEmpty(opfRootDirectory))
{
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
var opfFile = epub.GetEntry(opfFilePath);
if (opfFile == null)
@ -188,4 +205,4 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
public string Path { get; }
}
}
}
}

View File

@ -11,19 +11,29 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
{
/// <summary>
/// Epub metadata provider.
/// </summary>
public class EpubMetadataProvider : ILocalMetadataProvider<Book>
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<EpubMetadataProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="EpubMetadataProvider"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{EpubMetadataProvider}"/> interface.</param>
public EpubMetadataProvider(IFileSystem fileSystem, ILogger<EpubMetadataProvider> logger)
{
_fileSystem = fileSystem;
_logger = logger;
}
/// <inheritdoc />
public string Name => "Epub Metadata";
/// <inheritdoc />
public Task<MetadataResult<Book>> GetMetadata(
ItemInfo info,
IDirectoryService directoryService,
@ -47,7 +57,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
return Task.FromResult(result);
}
private FileSystemMetadata GetEpubFile(string path)
private FileSystemMetadata? GetEpubFile(string path)
{
var fileInfo = _fileSystem.GetFileSystemInfo(path);
@ -88,4 +98,4 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
OpfReader.ReadOpfData(result, opfDocument, cancellationToken, _logger);
}
}
}
}

View File

@ -5,9 +5,17 @@ using System.Xml.Linq;
namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
{
/// <summary>
/// Epub utils.
/// </summary>
public static class EpubUtils
{
public static string ReadContentFilePath(ZipArchive epub)
/// <summary>
/// Attempt to read content from zip archive.
/// </summary>
/// <param name="epub">The zip archive.</param>
/// <returns>The content file path.</returns>
public static string? ReadContentFilePath(ZipArchive epub)
{
var container = epub.GetEntry(Path.Combine("META-INF", "container.xml"));
if (container == null)
@ -24,4 +32,4 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
return element?.Attribute("full-path")?.Value;
}
}
}
}

View File

@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
{
/// <summary>
/// Book result dto.
/// </summary>
public class BookResult
{
/// <summary>
/// Gets or sets the book kind.
/// </summary>
[JsonPropertyName("kind")]
public string? Kind { get; set; }
/// <summary>
/// Gets or sets the book id.
/// </summary>
[JsonPropertyName("id")]
public string? Id { get; set; }
/// <summary>
/// Gets or sets the etag.
/// </summary>
[JsonPropertyName("etag")]
public string? Etag { get; set; }
/// <summary>
/// Gets or sets the self link.
/// </summary>
[JsonPropertyName("selfLink")]
public string? SelfLink { get; set; }
/// <summary>
/// Gets or sets the volume info.
/// </summary>
[JsonPropertyName("volumeInfo")]
public VolumeInfo? VolumeInfo { get; set; }
}
}

View File

@ -1,9 +1,18 @@
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
{
/// <summary>
/// Google API urls.
/// </summary>
public static class GoogleApiUrls
{
// GoogleBooks API Endpoints
/// <summary>
/// Gets the search url.
/// </summary>
public const string SearchUrl = @"https://www.googleapis.com/books/v1/volumes?q={0}&startIndex={1}&maxResults={2}";
/// <summary>
/// Gets the details url.
/// </summary>
public const string DetailsUrl = @"https://www.googleapis.com/books/v1/volumes/{0}";
}
}
}

View File

@ -1,42 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using Jellyfin.Extensions.Json;
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
{
/// <summary>
/// Google books image provider.
/// </summary>
public class GoogleBooksImageProvider : IRemoteImageProvider
{
private IHttpClientFactory _httpClientFactory;
private ILogger<GoogleBooksImageProvider> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public GoogleBooksImageProvider(ILogger<GoogleBooksImageProvider> logger, IHttpClientFactory httpClientFactory)
/// <summary>
/// Initializes a new instance of the <see cref="GoogleBooksImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
public GoogleBooksImageProvider(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
/// <inheritdoc />
public string Name => "Google Books";
/// <inheritdoc />
public bool Supports(BaseItem item)
{
return item is Book;
}
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
yield return ImageType.Primary;
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@ -65,7 +73,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
return list;
}
private async Task<BookResult> FetchBookData(string googleBookId, CancellationToken cancellationToken)
private async Task<BookResult?> FetchBookData(string googleBookId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@ -73,50 +81,54 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
using (var response = await httpClient.GetAsync(url).ConfigureAwait(false))
{
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<BookResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
}
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<BookResult>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
}
private List<string> ProcessBookImage(BookResult bookResult)
{
var images = new List<string>();
if (!string.IsNullOrEmpty(bookResult.volumeInfo.imageLinks?.extraLarge))
if (bookResult.VolumeInfo is null)
{
images.Add(bookResult.volumeInfo.imageLinks.extraLarge);
return images;
}
else if (!string.IsNullOrEmpty(bookResult.volumeInfo.imageLinks?.large))
if (!string.IsNullOrEmpty(bookResult.VolumeInfo.ImageLinks?.ExtraLarge))
{
images.Add(bookResult.volumeInfo.imageLinks.large);
images.Add(bookResult.VolumeInfo.ImageLinks.ExtraLarge);
}
else if (!string.IsNullOrEmpty(bookResult.volumeInfo.imageLinks?.medium))
else if (!string.IsNullOrEmpty(bookResult.VolumeInfo.ImageLinks?.Large))
{
images.Add(bookResult.volumeInfo.imageLinks.medium);
images.Add(bookResult.VolumeInfo.ImageLinks.Large);
}
else if (!string.IsNullOrEmpty(bookResult.volumeInfo.imageLinks?.small))
else if (!string.IsNullOrEmpty(bookResult.VolumeInfo.ImageLinks?.Medium))
{
images.Add(bookResult.volumeInfo.imageLinks.small);
images.Add(bookResult.VolumeInfo.ImageLinks.Medium);
}
else if (!string.IsNullOrEmpty(bookResult.VolumeInfo.ImageLinks?.Small))
{
images.Add(bookResult.VolumeInfo.ImageLinks.Small);
}
// sometimes the thumbnails can be different from the larger images
if (!string.IsNullOrEmpty(bookResult.volumeInfo.imageLinks?.thumbnail))
if (!string.IsNullOrEmpty(bookResult.VolumeInfo.ImageLinks?.Thumbnail))
{
images.Add(bookResult.volumeInfo.imageLinks.thumbnail);
images.Add(bookResult.VolumeInfo.ImageLinks.Thumbnail);
}
else if (!string.IsNullOrEmpty(bookResult.volumeInfo.imageLinks?.smallThumbnail))
else if (!string.IsNullOrEmpty(bookResult.VolumeInfo.ImageLinks?.SmallThumbnail))
{
images.Add(bookResult.volumeInfo.imageLinks.smallThumbnail);
images.Add(bookResult.VolumeInfo.ImageLinks.SmallThumbnail);
}
return images;
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await httpClient.GetAsync(url).ConfigureAwait(false);
return await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -18,6 +18,9 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
{
/// <summary>
/// Google books provider.
/// </summary>
public class GoogleBooksProvider : IRemoteMetadataProvider<Book, BookInfo>
{
// convert these characters to whitespace for better matching
@ -29,7 +32,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
// first pattern provides the name and the year
// alternate option to use series index instead of year
// last resort matches the whole string as the name
private static readonly Regex[] NameMatches =
private static readonly Regex[] _nameMatches =
{
new Regex(@"(?<name>.*)\((?<year>\d{4})\)"),
new Regex(@"(?<index>\d*)\s\-\s(?<name>.*)"),
@ -53,6 +56,11 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<GoogleBooksProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="GoogleBooksProvider"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{GoogleBooksProvider}"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
public GoogleBooksProvider(
ILogger<GoogleBooksProvider> logger,
IHttpClientFactory httpClientFactory)
@ -61,21 +69,33 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
_logger = logger;
}
/// <inheritdoc />
public string Name => "Google Books";
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BookInfo item, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var list = new List<RemoteSearchResult>();
var searchResults = await GetSearchResultsInternal(item, cancellationToken);
foreach (var result in searchResults.items)
if (searchResults is null)
{
var remoteSearchResult = new RemoteSearchResult();
remoteSearchResult.Name = result.volumeInfo.title;
if (result.volumeInfo.imageLinks?.thumbnail != null)
return Enumerable.Empty<RemoteSearchResult>();
}
foreach (var result in searchResults.Items)
{
if (result.VolumeInfo is null)
{
remoteSearchResult.ImageUrl = result.volumeInfo.imageLinks.thumbnail;
continue;
}
var remoteSearchResult = new RemoteSearchResult();
remoteSearchResult.Name = result.VolumeInfo.Title;
if (result.VolumeInfo.ImageLinks?.Thumbnail != null)
{
remoteSearchResult.ImageUrl = result.VolumeInfo.ImageLinks.Thumbnail;
}
list.Add(remoteSearchResult);
@ -84,6 +104,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
return list;
}
/// <inheritdoc />
public async Task<MetadataResult<Book>> GetMetadata(BookInfo item, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@ -105,24 +126,26 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
return metadataResult;
}
metadataResult.Item = ProcessBookData(bookResult, cancellationToken);
var bookMetadataResult = ProcessBookData(bookResult, cancellationToken);
if (bookMetadataResult == null)
{
return metadataResult;
}
metadataResult.Item = bookMetadataResult;
metadataResult.QueriedById = true;
metadataResult.HasMetadata = true;
return metadataResult;
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await httpClient.GetAsync(url).ConfigureAwait(false);
return await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
}
public bool Supports(BaseItem item)
{
return item is Book;
}
private async Task<SearchResult> GetSearchResultsInternal(BookInfo item, CancellationToken cancellationToken)
private async Task<SearchResult?> GetSearchResultsInternal(BookInfo item, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@ -134,34 +157,37 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
using (var response = await httpClient.GetAsync(url).ConfigureAwait(false))
{
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<SearchResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
}
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<SearchResult>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
}
private async Task<string> FetchBookId(BookInfo item, CancellationToken cancellationToken)
private async Task<string?> FetchBookId(BookInfo item, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var searchResults = await GetSearchResultsInternal(item, cancellationToken);
if (searchResults?.items == null)
if (searchResults?.Items == null)
{
return null;
}
var comparableName = GetComparableName(item.Name);
foreach (var i in searchResults.items)
foreach (var i in searchResults.Items)
{
if (i.VolumeInfo is null)
{
continue;
}
// no match so move on to the next item
if (!GetComparableName(i.volumeInfo.title).Equals(comparableName))
if (!GetComparableName(i.VolumeInfo.Title).Equals(comparableName))
{
continue;
}
// adjust for google yyyy-mm-dd format
var resultYear = i.volumeInfo.publishedDate.Length > 4 ? i.volumeInfo.publishedDate.Substring(0, 4) : i.volumeInfo.publishedDate;
var resultYear = i.VolumeInfo.PublishedDate?.Length > 4 ? i.VolumeInfo.PublishedDate[..4] : i.VolumeInfo.PublishedDate;
if (!int.TryParse(resultYear, out var bookReleaseYear))
{
continue;
@ -173,13 +199,13 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
continue;
}
return i.id;
return i.Id;
}
return null;
}
private async Task<BookResult> FetchBookData(string googleBookId, CancellationToken cancellationToken)
private async Task<BookResult?> FetchBookData(string googleBookId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@ -187,62 +213,77 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
using (var response = await httpClient.GetAsync(url).ConfigureAwait(false))
{
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<BookResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
}
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<BookResult>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
}
private Book ProcessBookData(BookResult bookResult, CancellationToken cancellationToken)
private Book? ProcessBookData(BookResult bookResult, CancellationToken cancellationToken)
{
if (bookResult.VolumeInfo is null)
{
return null;
}
var book = new Book();
cancellationToken.ThrowIfCancellationRequested();
book.Name = bookResult.volumeInfo.title;
book.Overview = bookResult.volumeInfo.description;
book.Name = bookResult.VolumeInfo.Title;
book.Overview = bookResult.VolumeInfo.Description;
try
{
book.ProductionYear = bookResult.volumeInfo.publishedDate.Length > 4
? Convert.ToInt32(bookResult.volumeInfo.publishedDate.Substring(0, 4))
: Convert.ToInt32(bookResult.volumeInfo.publishedDate);
book.ProductionYear = bookResult.VolumeInfo.PublishedDate?.Length > 4
? Convert.ToInt32(bookResult.VolumeInfo.PublishedDate[..4])
: Convert.ToInt32(bookResult.VolumeInfo.PublishedDate);
}
catch (Exception)
{
_logger.LogError("Error parsing date");
}
if (!string.IsNullOrEmpty(bookResult.volumeInfo.publisher))
if (!string.IsNullOrEmpty(bookResult.VolumeInfo.Publisher))
{
book.Studios.Append(bookResult.volumeInfo.publisher);
book.Studios = book.Studios.Append(bookResult.VolumeInfo.Publisher).ToArray();
}
if (!string.IsNullOrEmpty(bookResult.volumeInfo.mainCatagory))
var tags = new List<string>();
if (!string.IsNullOrEmpty(bookResult.VolumeInfo.MainCategory))
{
book.Tags.Append(bookResult.volumeInfo.mainCatagory);
tags.Add(bookResult.VolumeInfo.MainCategory);
}
if (bookResult.volumeInfo.catagories != null && bookResult.volumeInfo.catagories.Count > 0)
if (bookResult.VolumeInfo.Categories is { Length: > 0 })
{
foreach (var category in bookResult.volumeInfo.catagories)
foreach (var category in bookResult.VolumeInfo.Categories)
{
book.Tags.Append(category);
tags.Add(category);
}
}
// google rates out of five so convert to ten
book.CommunityRating = bookResult.volumeInfo.averageRating * 2;
if (!string.IsNullOrEmpty(bookResult.id))
if (tags.Count > 0)
{
book.SetProviderId("GoogleBooks", bookResult.id);
tags.AddRange(book.Tags);
book.Tags = tags.ToArray();
}
// google rates out of five so convert to ten
book.CommunityRating = bookResult.VolumeInfo.AverageRating * 2;
if (!string.IsNullOrEmpty(bookResult.Id))
{
book.SetProviderId("GoogleBooks", bookResult.Id);
}
return book;
}
private string GetComparableName(string name)
private string GetComparableName(string? name)
{
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
name = name.ToLower();
name = name.Normalize(NormalizationForm.FormKD);
@ -292,7 +333,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
private void GetBookMetadata(BookInfo item)
{
foreach (var regex in NameMatches)
foreach (var regex in _nameMatches)
{
var match = regex.Match(item.Name);
if (!match.Success)
@ -302,7 +343,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
// catch return value because user may want to index books from zero
// but zero is also the return value from int.TryParse failure
var result = int.TryParse(match.Groups["index"]?.Value, out var index);
var result = int.TryParse(match.Groups["index"].Value, out var index);
if (result)
{
item.IndexNumber = index;
@ -311,7 +352,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
item.Name = match.Groups["name"].Value.Trim();
// might as well catch the return value here as well
result = int.TryParse(match.Groups["year"]?.Value, out var year);
result = int.TryParse(match.Groups["year"].Value, out var year);
if (result)
{
item.Year = year;

View File

@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
{
/// <summary>
/// Image links dto.
/// </summary>
public class ImageLinks
{
/// <summary>
/// Gets or sets the small thumbnail.
/// </summary>
/// <remarks>
/// // Only the 2 thumbnail images are available during the initial search.
/// </remarks>
[JsonPropertyName("smallThumbnail")]
public string? SmallThumbnail { get; set; }
/// <summary>
/// Gets or sets the thumbnail.
/// </summary>
[JsonPropertyName("thumbnail")]
public string? Thumbnail { get; set; }
/// <summary>
/// Gets or sets the small image.
/// </summary>
[JsonPropertyName("small")]
public string? Small { get; set; }
/// <summary>
/// Gets or sets the medium image.
/// </summary>
[JsonPropertyName("medium")]
public string? Medium { get; set; }
/// <summary>
/// Gets or sets the large image.
/// </summary>
[JsonPropertyName("large")]
public string? Large { get; set; }
/// <summary>
/// Gets or sets the extra large image.
/// </summary>
[JsonPropertyName("extraLarge")]
public string? ExtraLarge { get; set; }
}
}

View File

@ -1,67 +0,0 @@
#pragma warning disable SA1300
#pragma warning disable SA1402
#pragma warning disable SA1649
using System.Collections.Generic;
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
{
public class SearchResult
{
public string kind { get; set; }
public int totalItems { get; set; }
public List<BookResult> items { get; set; }
}
public class BookResult
{
public string Kind { get; set; }
public string id { get; set; }
public string etag { get; set; }
public string selfLink { get; set; }
public VolumeInfo volumeInfo { get; set; }
}
public class VolumeInfo
{
public string title { get; set; }
public List<string> authors { get; set; }
public string publishedDate { get; set; }
public ImageLinks imageLinks { get; set; }
public string publisher { get; set; }
public string description { get; set; }
public string mainCatagory { get; set; }
public List<string> catagories { get; set; }
public float averageRating { get; set; }
}
public class ImageLinks
{
// Only the 2 thumbnail images are available during the initial search
public string smallThumbnail { get; set; }
public string thumbnail { get; set; }
public string small { get; set; }
public string medium { get; set; }
public string large { get; set; }
public string extraLarge { get; set; }
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
{
/// <summary>
/// Search result dto.
/// </summary>
public class SearchResult
{
/// <summary>
/// Gets or sets the result kind.
/// </summary>
[JsonPropertyName("kind")]
public string? Kind { get; set; }
/// <summary>
/// Gets or sets the total item count.
/// </summary>
[JsonPropertyName("totalItems")]
public int TotalItems { get; set; }
/// <summary>
/// Gets or sets the list of items.
/// </summary>
[JsonPropertyName("items")]
public BookResult[] Items { get; set; } = Array.Empty<BookResult>();
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
{
/// <summary>
/// Volume info dto.
/// </summary>
public class VolumeInfo
{
/// <summary>
/// Gets or sets the title.
/// </summary>
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <summary>
/// Gets or sets the list of authors.
/// </summary>
[JsonPropertyName("authors")]
public string[] Authors { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the published date.
/// </summary>
[JsonPropertyName("publishedDate")]
public string? PublishedDate { get; set; }
/// <summary>
/// Gets or sets the image links.
/// </summary>
[JsonPropertyName("imageLinks")]
public ImageLinks? ImageLinks { get; set; }
/// <summary>
/// Gets or sets the publisher.
/// </summary>
[JsonPropertyName("publisher")]
public string? Publisher { get; set; }
/// <summary>
/// Gets or sets the description.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; set; }
/// <summary>
/// Gets or sets the main category.
/// </summary>
[JsonPropertyName("mainCategory")]
public string? MainCategory { get; set; }
/// <summary>
/// Gets or sets the list of categories.
/// </summary>
[JsonPropertyName("categories")]
public string[] Categories { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the average rating.
/// </summary>
[JsonPropertyName("averageRating")]
public float AverageRating { get; set; }
}
}

View File

@ -5,10 +5,25 @@ using MediaBrowser.Controller.Providers;
namespace Jellyfin.Plugin.Bookshelf.Providers
{
/// <summary>
/// Comic file provider interface.
/// </summary>
public interface IComicFileProvider
{
Task<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken);
/// <summary>
/// Read the item metadata.
/// </summary>
/// <param name="info">The item info.</param>
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The metadata result.</returns>
ValueTask<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken);
bool HasItemChanged(BaseItem item, IDirectoryService directoryService);
/// <summary>
/// Has the item changed.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>Item change status.</returns>
bool HasItemChanged(BaseItem item);
}
}

View File

@ -9,11 +9,22 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Bookshelf.Providers
{
/// <summary>
/// OPF reader.
/// </summary>
public static class OpfReader
{
private const string DcNamespace = @"http://purl.org/dc/elements/1.1/";
private const string OpfNamespace = @"http://www.idpf.org/2007/opf";
/// <summary>
/// Read opf data.
/// </summary>
/// <param name="bookResult">The metadata result to update.</param>
/// <param name="doc">The xdocument to parse.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="logger">Instance of the <see cref="ILogger{TCategoryName}"/> interface.</param>
/// <typeparam name="TCategoryName">The type of category.</typeparam>
public static void ReadOpfData<TCategoryName>(
MetadataResult<Book> bookResult,
XmlDocument doc,
@ -89,7 +100,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
{
try
{
book.IndexNumber = Convert.ToInt32(seriesIndexNode.Attributes["content"].Value);
book.IndexNumber = Convert.ToInt32(seriesIndexNode.Attributes["content"]?.Value);
}
catch (Exception)
{
@ -103,7 +114,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
{
try
{
book.SeriesName = seriesNameNode.Attributes["content"].Value;
book.SeriesName = seriesNameNode.Attributes["content"]?.Value;
}
catch (Exception)
{
@ -117,7 +128,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
{
try
{
book.CommunityRating = Convert.ToInt32(ratingNode.Attributes["content"].Value);
book.CommunityRating = Convert.ToInt32(ratingNode.Attributes["content"]?.Value);
}
catch (Exception)
{
@ -126,4 +137,4 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
}
}
}
}
}

View File

@ -1,58 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Emby.AutoOrganize" Description="Code analysis rules for Emby.AutoOrganize.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
<Rule Id="SA1202" Action="Info" />
<!-- disable warning SA1204: Static members must appear before non-static members -->
<Rule Id="SA1204" Action="Info" />
<RuleSet Name="Rules for Jellyfin" Description="Code analysis rules for Jellyfin" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
<Rule Id="SA1202" Action="Info" />
<!-- disable warning SA1204: Static members must appear before non-static members -->
<Rule Id="SA1204" Action="Info" />
<!-- disable warning SA1404: Code analysis suppression should have justification -->
<Rule Id="SA1404" Action="Info" />
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
<Rule Id="SA1101" Action="None" />
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
<Rule Id="SA1108" Action="None" />
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
<Rule Id="SA1128" Action="None" />
<!-- disable warning SA1130: Use lambda syntax -->
<Rule Id="SA1130" Action="None" />
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
<Rule Id="SA1200" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
<Rule Id="SA1512" Action="None" />
<!-- disable warning SA1600: Elements should be documented -->
<Rule Id="SA1600" Action="None" />
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
<Rule Id="SA1633" Action="None" />
</Rules>
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
<Rule Id="SA1011" Action="None" />
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
<Rule Id="SA1101" Action="None" />
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
<Rule Id="SA1108" Action="None" />
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
<Rule Id="SA1128" Action="None" />
<!-- disable warning SA1130: Use lambda syntax -->
<Rule Id="SA1130" Action="None" />
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
<Rule Id="SA1200" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
<Rule Id="SA1512" Action="None" />
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
<Rule Id="SA1515" Action="None" />
<!-- disable warning SA1600: Elements should be documented -->
<Rule Id="SA1600" Action="None" />
<!-- disable warning SA1602: Enumeration items should be documented -->
<Rule Id="SA1602" Action="None" />
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
<Rule Id="SA1633" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
<!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
If so, remove the code from the assembly.
If this class is intended to contain only static members, make it static -->
<Rule Id="CA1812" Action="Info" />
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
<Rule Id="CA1822" Action="Info" />
<Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="None" />
</Rules>
<!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
<!-- disable warning CA1716: Identifiers should not match keywords -->
<Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
<!-- disable warning CA1805: Do not initialize unnecessarily -->
<Rule Id="CA1805" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
If so, remove the code from the assembly.
If this class is intended to contain only static members, make it static -->
<Rule Id="CA1812" Action="Info" />
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
<Rule Id="CA1822" Action="Info" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="Info" />
<!-- disable warning CA5394: Do not use insecure randomness -->
<Rule Id="CA5394" Action="Info" />
<!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute -->
<Rule Id="CA1014" Action="Info" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
<!-- disable warning CA1055: URI return values should not be strings -->
<Rule Id="CA1055" Action="None" />
<!-- disable warning CA1056: URI properties should not be strings -->
<Rule Id="CA1056" Action="None" />
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
</Rules>
</RuleSet>