Merge pull request #38 from MinecraftPlaye/feature-comicbookinfo-metadata

This commit is contained in:
Cody Robibero 2021-11-06 10:39:53 -06:00 committed by GitHub
commit 377bfceace
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 354 additions and 1 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>();
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}
}