mirror of
https://github.com/jellyfin/jellyfin-plugin-bookshelf.git
synced 2024-11-23 05:39:51 +00:00
Merge pull request #38 from MinecraftPlaye/feature-comicbookinfo-metadata
This commit is contained in:
commit
377bfceace
@ -1,14 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Bookshelf</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="bookshelfConfigurationPage" data-role="page" class="page type-interior pluginConfigurationPage">
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<h1>Bookshelf</h1>
|
||||
<p>To set up Bookshelf, just add a media library in the server configuration and set the content type to books. This plugin supports standard OPF as well as Calibre OPF if you'd like to import the metadata from another program. ComicInfo XML files are also supported but the ComicVine provider is not yet functional. If you want to see support for a specific provider we always welcome code contributions!</p>
|
||||
<p>To set up Bookshelf, just add a media library in the server configuration and set the content type to
|
||||
books. This plugin supports standard OPF as well as Calibre OPF if you'd like to import the metadata
|
||||
from another program. If you want to see support for a specific provider we always welcome code
|
||||
contributions!
|
||||
</p>
|
||||
<h1>Supported Formats</h1>
|
||||
<ul>
|
||||
<li>epub</li>
|
||||
@ -17,8 +23,28 @@
|
||||
<li>cbz</li>
|
||||
<li>cbr</li>
|
||||
</ul>
|
||||
|
||||
The following <b>limitations</b> apply:
|
||||
<ul>
|
||||
<li>
|
||||
.cbr Comics tagged with ComicRack's ComicInfo format
|
||||
are partially supported. Any metadata bundled with the
|
||||
comic book itself will be ignored.
|
||||
</li>
|
||||
<li>
|
||||
The
|
||||
<a href="https://launchpad.net/acbf">Advanced Comic Book Format</a>
|
||||
format is not supported.
|
||||
</li>
|
||||
<li>
|
||||
The
|
||||
<a href="https://www.denvog.com/comet/comet-specification/">CoMet</a>
|
||||
format is not supported.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -10,6 +10,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.*-*" />
|
||||
<PackageReference Include="sharpcompress" Version="0.30.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using Jellyfin.Plugin.Bookshelf.Providers.ComicBook;
|
||||
using Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo;
|
||||
using Jellyfin.Plugin.Bookshelf.Providers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -18,6 +19,7 @@ namespace Jellyfin.Plugin.Bookshelf
|
||||
// register the actual implementations of the local metadata provider for comic files
|
||||
serviceCollection.AddSingleton<IComicFileProvider, ExternalComicInfoProvider>();
|
||||
serviceCollection.AddSingleton<IComicFileProvider, InternalComicInfoProvider>();
|
||||
serviceCollection.AddSingleton<IComicFileProvider, ComicBookInfoProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
#nullable enable
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
|
||||
{
|
||||
public class ComicBookInfoFormat
|
||||
{
|
||||
[JsonPropertyName("appID")]
|
||||
public string? AppId { get; set; }
|
||||
|
||||
[JsonPropertyName("lastModified")]
|
||||
public string? LastModified { get; set; }
|
||||
|
||||
[JsonPropertyName("ComicBookInfo/1.0")]
|
||||
public ComicBookInfoMetadata? Metadata { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
#nullable enable
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
|
||||
{
|
||||
public class ComicBookInfoCredit
|
||||
{
|
||||
[JsonPropertyName("person")]
|
||||
public string? Person { get; set; }
|
||||
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role { get; set; }
|
||||
|
||||
[JsonPropertyName("primary")]
|
||||
public string? Primary { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
#nullable enable
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
|
||||
{
|
||||
public class ComicBookInfoMetadata
|
||||
{
|
||||
[JsonPropertyName("series")]
|
||||
public string? Series { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonPropertyName("publisher")]
|
||||
public string? Publisher { get; set; }
|
||||
|
||||
[JsonPropertyName("publicationMonth")]
|
||||
public int? PublicationMonth { get; set; }
|
||||
|
||||
[JsonPropertyName("publicationYear")]
|
||||
public int? PublicationYear { get; set; }
|
||||
|
||||
[JsonPropertyName("issue")]
|
||||
public int? Issue { get; set; }
|
||||
|
||||
[JsonPropertyName("numberOfIssues")]
|
||||
public int? NumberOfIssues { get; set; }
|
||||
|
||||
[JsonPropertyName("volume")]
|
||||
public int? Volume { get; set; }
|
||||
|
||||
[JsonPropertyName("numberOfVolumes")]
|
||||
public int? NumberOfVolumes { get; set; }
|
||||
|
||||
[JsonPropertyName("rating")]
|
||||
public int? Rating { get; set; }
|
||||
|
||||
[JsonPropertyName("genre")]
|
||||
public string? Genre { get; set; }
|
||||
|
||||
[JsonPropertyName("language")]
|
||||
public string? Language { get; set; }
|
||||
|
||||
[JsonPropertyName("country")]
|
||||
public string? Country { get; set; }
|
||||
|
||||
[JsonPropertyName("credits")]
|
||||
public ComicBookInfoCredit[]? Credits { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
public string[]? Tags { get; set; }
|
||||
|
||||
[JsonPropertyName("comments")]
|
||||
public string? Comments { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
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 MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Jellyfin.Extensions.Json;
|
||||
|
||||
#nullable enable
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicBookInfo
|
||||
{
|
||||
public class ComicBookInfoProvider : IComicFileProvider
|
||||
{
|
||||
private readonly ILogger<ComicBookInfoProvider> _logger;
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public ComicBookInfoProvider(IFileSystem fileSystem, ILogger<ComicBookInfoProvider> logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<MetadataResult<Book>> ReadMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetComicBookFile(info.Path)?.FullName;
|
||||
|
||||
if (path is null)
|
||||
{
|
||||
_logger.LogError("Could not load Comic for {Path}", info.Path);
|
||||
return new MetadataResult<Book> { HasMetadata = false };
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using Stream stream = File.OpenRead(path);
|
||||
// not yet async: https://github.com/adamhathcock/sharpcompress/pull/565
|
||||
using var archive = ZipArchive.Open(stream);
|
||||
|
||||
if (archive.IsComplete)
|
||||
{
|
||||
var volume = archive.Volumes.First();
|
||||
if (volume.Comment is not null)
|
||||
{
|
||||
using var jsonStream = new MemoryStream(Encoding.UTF8.GetBytes(volume.Comment));
|
||||
var comicBookMetadata = await JsonSerializer.DeserializeAsync<ComicBookInfoFormat>(jsonStream, JsonDefaults.Options, cancellationToken);
|
||||
|
||||
if (comicBookMetadata is null)
|
||||
{
|
||||
_logger.LogError("Failed to load ComicBookInfo metadata from archive comment for {Path}", info.Path);
|
||||
return new MetadataResult<Book> { HasMetadata = false };
|
||||
}
|
||||
|
||||
return SaveMetadata(comicBookMetadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("{Path} does not contain any ComicBookInfo metadata", info.Path);
|
||||
return new MetadataResult<Book> { HasMetadata = false };
|
||||
}
|
||||
}
|
||||
_logger.LogError("Could not load ComicBookInfo metadata for {Path}", info.Path);
|
||||
return new MetadataResult<Book> { HasMetadata = false };
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.LogError("Failed to load ComicBookInfo metadata for {Path}", info.Path);
|
||||
return new MetadataResult<Book> { HasMetadata = false };
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasItemChanged(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var file = GetComicBookFile(item.Path);
|
||||
|
||||
if (file is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
|
||||
}
|
||||
|
||||
private MetadataResult<Book> SaveMetadata(ComicBookInfoFormat comic)
|
||||
{
|
||||
if (comic.Metadata is null)
|
||||
{
|
||||
return new MetadataResult<Book> { HasMetadata = false };
|
||||
}
|
||||
|
||||
var book = ReadComicBookMetadata(comic.Metadata);
|
||||
|
||||
if (book is null)
|
||||
{
|
||||
return new MetadataResult<Book> { HasMetadata = false };
|
||||
}
|
||||
|
||||
var metadataResult = new MetadataResult<Book> { Item = book, HasMetadata = true };
|
||||
|
||||
if (comic.Metadata.Language is not null)
|
||||
{
|
||||
metadataResult.ResultLanguage = ReadCultureInfoAsThreeLetterIsoInto(comic.Metadata.Language);
|
||||
}
|
||||
|
||||
if (comic.Metadata.Credits is not null && comic.Metadata.Credits.Length > 0)
|
||||
{
|
||||
foreach (var person in comic.Metadata.Credits)
|
||||
{
|
||||
if (person.Person is null || person.Role is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var personInfo = new PersonInfo { Name = person.Person, Type = person.Role };
|
||||
metadataResult.AddPerson(personInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return metadataResult;
|
||||
}
|
||||
|
||||
private Book? ReadComicBookMetadata(ComicBookInfoMetadata comic)
|
||||
{
|
||||
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 }));
|
||||
|
||||
if (comic.PublicationYear is not null)
|
||||
{
|
||||
book.ProductionYear = comic.PublicationYear;
|
||||
hasFoundMetadata |= true;
|
||||
}
|
||||
|
||||
if (comic.Issue is not null)
|
||||
{
|
||||
book.IndexNumber = comic.Issue;
|
||||
hasFoundMetadata |= true;
|
||||
}
|
||||
|
||||
if (comic.Tags is not null && comic.Tags.Length > 0)
|
||||
{
|
||||
book.Tags = comic.Tags;
|
||||
hasFoundMetadata |= true;
|
||||
}
|
||||
|
||||
if (comic.PublicationYear is not null && comic.PublicationMonth is not null)
|
||||
{
|
||||
book.PremiereDate = ReadTwoPartDateInto(comic.PublicationYear.Value, comic.PublicationMonth.Value);
|
||||
hasFoundMetadata |= true;
|
||||
}
|
||||
|
||||
if (hasFoundMetadata)
|
||||
{
|
||||
return book;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ReadStringInto(string? data, Action<string> commitResult)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(data))
|
||||
{
|
||||
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
|
||||
{
|
||||
//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
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? ReadCultureInfoAsThreeLetterIsoInto(string language)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new CultureInfo(language).ThreeLetterISOLanguageName;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//Ignored
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private FileSystemMetadata? GetComicBookFile(string path)
|
||||
{
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
|
||||
if (fileInfo.IsDirectory)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only parse files that are known to have internal metadata
|
||||
if (fileInfo.Extension.Equals(".cbz", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return fileInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user