mirror of
https://github.com/jellyfin/jellyfin-plugin-bookshelf.git
synced 2025-02-17 04:17:54 +00:00
More analyze
This commit is contained in:
parent
9dc2cf2a36
commit
79a51dda5e
@ -2,7 +2,10 @@
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Instance of the empty plugin configuration.
|
||||
/// </summary>
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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>();
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
130
jellyfin.ruleset
130
jellyfin.ruleset
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user