mirror of
https://github.com/jellyfin/jellyfin-plugin-bookshelf.git
synced 2024-11-23 05:39:51 +00:00
Add analyzers to project
This commit is contained in:
parent
5999e2299a
commit
6e9ce31d19
@ -4,6 +4,5 @@ namespace Jellyfin.Plugin.Bookshelf.Configuration
|
||||
{
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,83 +1,14 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
using System.Xml;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Class XmlExtensions
|
||||
/// Class XmlExtensions.
|
||||
/// </summary>
|
||||
public static class XmlExtensions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get int32.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public static int SafeGetInt32(this XmlDocument doc, string path)
|
||||
{
|
||||
return SafeGetInt32(doc, path, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get int32.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="defaultInt">The default int.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public static int SafeGetInt32(this XmlDocument doc, string path, int defaultInt)
|
||||
{
|
||||
XmlNode rvalNode = doc.SelectSingleNode(path);
|
||||
if (rvalNode != null && rvalNode.InnerText.Length > 0)
|
||||
{
|
||||
int rval;
|
||||
if (Int32.TryParse(rvalNode.InnerText, out rval))
|
||||
{
|
||||
return rval;
|
||||
}
|
||||
|
||||
}
|
||||
return defaultInt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _us culture
|
||||
/// </summary>
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get single.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="minValue">The min value.</param>
|
||||
/// <param name="maxValue">The max value.</param>
|
||||
/// <returns>System.Single.</returns>
|
||||
public static float SafeGetSingle(this XmlDocument doc, string path, float minValue, float maxValue)
|
||||
{
|
||||
XmlNode rvalNode = doc.SelectSingleNode(path);
|
||||
if (rvalNode != null && rvalNode.InnerText.Length > 0)
|
||||
{
|
||||
float rval;
|
||||
// float.TryParse is local aware, so it can be probamatic, force us culture
|
||||
if (float.TryParse(rvalNode.InnerText, NumberStyles.AllowDecimalPoint, UsCulture, out rval))
|
||||
{
|
||||
if (rval >= minValue && rval <= maxValue)
|
||||
{
|
||||
return rval;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return minValue;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get string.
|
||||
/// Safes the get string.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
@ -88,7 +19,7 @@ namespace Jellyfin.Plugin.Bookshelf.Extensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get string.
|
||||
/// Safes the get string.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
@ -107,132 +38,5 @@ namespace Jellyfin.Plugin.Bookshelf.Extensions
|
||||
|
||||
return defaultString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get DateTime.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.DateTime.</returns>
|
||||
public static DateTime? SafeGetDateTime(this XmlDocument doc, string path)
|
||||
{
|
||||
return SafeGetDateTime(doc, path, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get DateTime.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="defaultDate">The default date.</param>
|
||||
/// <returns>System.DateTime.</returns>
|
||||
public static DateTime? SafeGetDateTime(this XmlDocument doc, string path, DateTime? defaultDate)
|
||||
{
|
||||
var rvalNode = doc.SelectSingleNode(path);
|
||||
|
||||
if (rvalNode != null)
|
||||
{
|
||||
var text = rvalNode.InnerText;
|
||||
DateTime date;
|
||||
if (DateTime.TryParse(text, out date))
|
||||
return date.ToUniversalTime();
|
||||
}
|
||||
return defaultDate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get string.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string SafeGetString(this XmlNode doc, string path)
|
||||
{
|
||||
return SafeGetString(doc, path, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safes the get string.
|
||||
/// </summary>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="defaultValue">The default value.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string SafeGetString(this XmlNode doc, string path, string defaultValue)
|
||||
{
|
||||
var rvalNode = doc.SelectSingleNode(path);
|
||||
if (rvalNode != null)
|
||||
{
|
||||
var text = rvalNode.InnerText;
|
||||
|
||||
return !string.IsNullOrWhiteSpace(text) ? text : defaultValue;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the string safe.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string ReadStringSafe(this XmlReader reader)
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
return string.IsNullOrWhiteSpace(val) ? null : val;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the value safe.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string ReadValueSafe(this XmlReader reader)
|
||||
{
|
||||
reader.Read();
|
||||
|
||||
var val = reader.Value;
|
||||
|
||||
return string.IsNullOrWhiteSpace(val) ? null : val;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a float from the current element of an XmlReader
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>System.Single.</returns>
|
||||
public static float ReadFloatSafe(this XmlReader reader)
|
||||
{
|
||||
string valueString = reader.ReadElementContentAsString();
|
||||
|
||||
float value = 0;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(valueString))
|
||||
{
|
||||
// float.TryParse is local aware, so it can be probamatic, force us culture
|
||||
float.TryParse(valueString, NumberStyles.AllowDecimalPoint, UsCulture, out value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an int from the current element of an XmlReader
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public static int ReadIntSafe(this XmlReader reader)
|
||||
{
|
||||
string valueString = reader.ReadElementContentAsString();
|
||||
|
||||
int value = 0;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(valueString))
|
||||
{
|
||||
int.TryParse(valueString, out value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,9 @@
|
||||
<RootNamespace>Jellyfin.Plugin.Bookshelf</RootNamespace>
|
||||
<AssemblyVersion>5.0.0.0</AssemblyVersion>
|
||||
<FileVersion>5.0.0.0</FileVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -17,4 +20,14 @@
|
||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -34,4 +34,4 @@ namespace Jellyfin.Plugin.Bookshelf
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -16,9 +16,9 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
|
||||
private const string DcNamespace = @"http://purl.org/dc/elements/1.1/";
|
||||
private const string OpfNamespace = @"http://www.idpf.org/2007/opf";
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
private readonly ILogger<BookProviderFromOpf> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public BookProviderFromOpf(IFileSystem fileSystem, ILogger<BookProviderFromOpf> logger)
|
||||
{
|
||||
@ -28,28 +28,6 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
|
||||
public string Name => "Open Packaging Format";
|
||||
|
||||
private FileSystemMetadata GetXmlFile(string path)
|
||||
{
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
|
||||
var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path));
|
||||
|
||||
var directoryPath = directoryInfo.FullName;
|
||||
|
||||
var specificFile = Path.Combine(directoryPath, Path.GetFileNameWithoutExtension(path) + ".opf");
|
||||
|
||||
var file = _fileSystem.GetFileInfo(specificFile);
|
||||
|
||||
if (file.Exists)
|
||||
{
|
||||
return file;
|
||||
}
|
||||
|
||||
file = _fileSystem.GetFileInfo(Path.Combine(directoryPath, StandardOpfFile));
|
||||
|
||||
return file.Exists ? file : _fileSystem.GetFileInfo(Path.Combine(directoryPath, CalibreOpfFile));
|
||||
}
|
||||
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var file = GetXmlFile(item.Path);
|
||||
@ -76,6 +54,28 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private FileSystemMetadata GetXmlFile(string path)
|
||||
{
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
|
||||
var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path));
|
||||
|
||||
var directoryPath = directoryInfo.FullName;
|
||||
|
||||
var specificFile = Path.Combine(directoryPath, Path.GetFileNameWithoutExtension(path) + ".opf");
|
||||
|
||||
var file = _fileSystem.GetFileInfo(specificFile);
|
||||
|
||||
if (file.Exists)
|
||||
{
|
||||
return file;
|
||||
}
|
||||
|
||||
file = _fileSystem.GetFileInfo(Path.Combine(directoryPath, StandardOpfFile));
|
||||
|
||||
return file.Exists ? file : _fileSystem.GetFileInfo(Path.Combine(directoryPath, CalibreOpfFile));
|
||||
}
|
||||
|
||||
private void ReadOpfData(MetadataResult<Book> bookResult, string metaFile, CancellationToken cancellationToken)
|
||||
{
|
||||
var book = bookResult.Item;
|
||||
@ -88,4 +88,4 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
OpfReader.ReadOpfData(bookResult, doc, cancellationToken, _logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
using Jellyfin.Plugin.Bookshelf.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Jellyfin.Plugin.Bookshelf.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// http://wiki.mobileread.com/wiki/CBR/CBZ#Metadata
|
||||
/// http://wiki.mobileread.com/wiki/CBR/CBZ#Metadata.
|
||||
/// </summary>
|
||||
public class ComicProviderFromXml : ILocalMetadataProvider<Book>, IHasItemChangeMonitor
|
||||
{
|
||||
@ -18,6 +18,10 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComicProviderFromXml"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
public ComicProviderFromXml(IFileSystem fileSystem)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
@ -25,8 +29,35 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
|
||||
public string Name => "Comic Vine XML";
|
||||
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var file = GetXmlFile(item.Path);
|
||||
return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
|
||||
}
|
||||
|
||||
public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetXmlFile(info.Path).FullName;
|
||||
|
||||
var result = new MetadataResult<Book>();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Book();
|
||||
result.HasMetadata = true;
|
||||
result.Item = item;
|
||||
ReadXmlData(result, path, cancellationToken);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
result.HasMetadata = false;
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the XML data.
|
||||
/// Reads the XML data.
|
||||
/// </summary>
|
||||
/// <param name="bookResult">The book result.</param>
|
||||
/// <param name="metaFile">The meta file.</param>
|
||||
@ -43,12 +74,16 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
var name = doc.SafeGetString("ComicInfo/Title");
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
book.Name = name;
|
||||
}
|
||||
|
||||
var overview = doc.SafeGetString("ComicInfo/Summary");
|
||||
|
||||
if (!string.IsNullOrEmpty(overview))
|
||||
{
|
||||
book.Overview = overview;
|
||||
}
|
||||
|
||||
var publisher = doc.SafeGetString("ComicInfo/Publisher");
|
||||
|
||||
@ -80,32 +115,5 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
|
||||
return file.Exists ? file : _fileSystem.GetFileInfo(Path.Combine(directoryPath, ComicRackMetaFile));
|
||||
}
|
||||
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var file = GetXmlFile(item.Path);
|
||||
return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
|
||||
}
|
||||
|
||||
public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetXmlFile(info.Path).FullName;
|
||||
|
||||
var result = new MetadataResult<Book>();
|
||||
|
||||
try
|
||||
{
|
||||
var item = new Book();
|
||||
result.HasMetadata = true;
|
||||
result.Item = item;
|
||||
ReadXmlData(result, path, cancellationToken);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
result.HasMetadata = false;
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
|
||||
{
|
||||
public class ComicVineVolumeExternalId : IExternalId
|
||||
{
|
||||
public string Key
|
||||
{
|
||||
get { return KeyName; }
|
||||
}
|
||||
|
||||
public string ProviderName
|
||||
{
|
||||
get { return "Comic Vine Volume"; }
|
||||
}
|
||||
|
||||
public bool Supports(IHasProviderIds item)
|
||||
{
|
||||
return item is Book;
|
||||
}
|
||||
|
||||
public ExternalIdMediaType? Type
|
||||
=> null; // TODO: enum does not yet have the Book type
|
||||
|
||||
public string UrlFormatString
|
||||
{
|
||||
get
|
||||
{
|
||||
// TODO: Is there a url?
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static string KeyName
|
||||
{
|
||||
get
|
||||
{
|
||||
return "ComicVineVolume";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,96 +1,84 @@
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
|
||||
/*namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
|
||||
{
|
||||
//public class ComicVineImageProvider : IRemoteImageProvider
|
||||
//{
|
||||
// private readonly IHttpClient _httpClient;
|
||||
public class ComicVineImageProvider : IRemoteImageProvider
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
// public ComicVineImageProvider(IHttpClient httpClient, IJsonSerializer jsonSerializer)
|
||||
// {
|
||||
// _httpClient = httpClient;
|
||||
// _jsonSerializer = jsonSerializer;
|
||||
// }
|
||||
public ComicVineImageProvider(IHttpClient httpClient, IJsonSerializer jsonSerializer)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
}
|
||||
|
||||
// private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
// private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
// public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasMetadata item, CancellationToken cancellationToken)
|
||||
// {
|
||||
// var volumeId = item.GetProviderId(ComicVineVolumeExternalId.KeyName);
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasMetadata item, CancellationToken cancellationToken)
|
||||
{
|
||||
var volumeId = item.GetProviderId(ComicVineVolumeExternalId.KeyName);
|
||||
|
||||
// var images = new List<RemoteImageInfo>();
|
||||
var images = new List<RemoteImageInfo>();
|
||||
|
||||
// if (!string.IsNullOrEmpty(volumeId))
|
||||
// {
|
||||
// var issueNumber = ComicVineMetadataProvider.GetIssueNumberFromName(item.Name).ToString(_usCulture);
|
||||
if (!string.IsNullOrEmpty(volumeId))
|
||||
{
|
||||
var issueNumber = ComicVineMetadataProvider.GetIssueNumberFromName(item.Name).ToString(_usCulture);
|
||||
|
||||
// await ComicVineMetadataProvider.Current.EnsureCacheFile(volumeId, issueNumber, cancellationToken).ConfigureAwait(false);
|
||||
await ComicVineMetadataProvider.Current.EnsureCacheFile(volumeId, issueNumber, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// var cachePath = ComicVineMetadataProvider.Current.GetCacheFilePath(volumeId, issueNumber);
|
||||
var cachePath = ComicVineMetadataProvider.Current.GetCacheFilePath(volumeId, issueNumber);
|
||||
|
||||
// try
|
||||
// {
|
||||
// var issueInfo = _jsonSerializer.DeserializeFromFile<SearchResult>(cachePath);
|
||||
try
|
||||
{
|
||||
var issueInfo = _jsonSerializer.DeserializeFromFile<SearchResult>(cachePath);
|
||||
|
||||
// if (issueInfo.results.Count > 0)
|
||||
// {
|
||||
// var result = issueInfo.results[0].image;
|
||||
if (issueInfo.results.Count > 0)
|
||||
{
|
||||
var result = issueInfo.results[0].image;
|
||||
|
||||
// if (!string.IsNullOrEmpty(result.medium_url))
|
||||
// {
|
||||
// images.Add(new RemoteImageInfo
|
||||
// {
|
||||
// Url = result.medium_url,
|
||||
// ProviderName = Name
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// catch (FileNotFoundException)
|
||||
// {
|
||||
// }
|
||||
// catch (DirectoryNotFoundException)
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
if (!string.IsNullOrEmpty(result.medium_url))
|
||||
{
|
||||
images.Add(new RemoteImageInfo
|
||||
{
|
||||
Url = result.medium_url,
|
||||
ProviderName = Name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// return images;
|
||||
// }
|
||||
return images;
|
||||
}
|
||||
|
||||
// public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
// {
|
||||
// return _httpClient.GetResponse(new HttpRequestOptions
|
||||
// {
|
||||
// CancellationToken = cancellationToken,
|
||||
// Url = url,
|
||||
// ResourcePool = Plugin.Instance.ComicVineSemiphore
|
||||
// });
|
||||
// }
|
||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url,
|
||||
ResourcePool = Plugin.Instance.ComicVineSemiphore
|
||||
});
|
||||
}
|
||||
|
||||
// public IEnumerable<ImageType> GetSupportedImages(IHasMetadata item)
|
||||
// {
|
||||
// return new List<ImageType> { ImageType.Primary };
|
||||
// }
|
||||
public IEnumerable<ImageType> GetSupportedImages(IHasMetadata item)
|
||||
{
|
||||
return new List<ImageType> {ImageType.Primary};
|
||||
}
|
||||
|
||||
// public string Name
|
||||
// {
|
||||
// get { return "Comic Vine"; }
|
||||
// }
|
||||
public string Name
|
||||
{
|
||||
get { return "Comic Vine"; }
|
||||
}
|
||||
|
||||
// public bool Supports(IHasMetadata item)
|
||||
// {
|
||||
// return item is Book;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
public bool Supports(IHasMetadata item)
|
||||
{
|
||||
return item is Book;
|
||||
}
|
||||
}
|
||||
}*/
|
@ -1,401 +1,384 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
|
||||
/*namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
|
||||
{
|
||||
//public class ComicVineMetadataProvider : IRemoteMetadataProvider<Book, BookInfo>
|
||||
//{
|
||||
// private const string ApiKey = "cc632e23e4b370807f4de6f0e3ba0116c734c10b";
|
||||
// private const string VolumeSearchUrl =
|
||||
// @"http://api.comicvine.com/search/?api_key={0}&format=json&resources=issue&query={1}";
|
||||
public class ComicVineMetadataProvider : IRemoteMetadataProvider<Book, BookInfo>
|
||||
{
|
||||
private const string ApiKey = "cc632e23e4b370807f4de6f0e3ba0116c734c10b";
|
||||
private const string VolumeSearchUrl =
|
||||
@"http:api.comicvine.com/search/?api_key={0}&format=json&resources=issue&query={1}";
|
||||
|
||||
// private const string IssueSearchUrl =
|
||||
// @"http://api.comicvine.com/issues/?api_key={0}&format=json&filter=issue_number:{1},volume:{2}";
|
||||
private const string IssueSearchUrl =
|
||||
@"http:api.comicvine.com/issues/?api_key={0}&format=json&filter=issue_number:{1},volume:{2}";
|
||||
|
||||
// private static readonly Regex[] NameMatches =
|
||||
// {
|
||||
// new Regex(@"(?<name>.*)\((?<year>\d{4})\)"), // matches "My Comic (2001)" and gives us the name and the year
|
||||
// new Regex(@"(?<name>.*)") // last resort matches the whole string as the name
|
||||
// };
|
||||
private static readonly Regex[] NameMatches =
|
||||
{
|
||||
new Regex(@"(?<name>.*)\((?<year>\d{4})\)"), matches "My Comic (2001)" and gives us the name and the year
|
||||
new Regex(@"(?<name>.*)") last resort matches the whole string as the name
|
||||
};
|
||||
|
||||
// private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
// private readonly ILogger<ComicVineMetadataProvider> _logger;
|
||||
// private readonly IHttpClient _httpClient;
|
||||
// private readonly IJsonSerializer _jsonSerializer;
|
||||
// private readonly IFileSystem _fileSystem;
|
||||
// private readonly IApplicationPaths _appPaths;
|
||||
private readonly ILogger<ComicVineMetadataProvider> _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
// public static ComicVineMetadataProvider Current;
|
||||
|
||||
// public ComicVineMetadataProvider(ILogger<ComicVineMetadataProvider> logger, IHttpClient httpClient, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IApplicationPaths appPaths)
|
||||
// {
|
||||
// _logger = logger;
|
||||
// _httpClient = httpClient;
|
||||
// _jsonSerializer = jsonSerializer;
|
||||
// _fileSystem = fileSystem;
|
||||
// _appPaths = appPaths;
|
||||
// Current = this;
|
||||
// }
|
||||
|
||||
// public async Task<MetadataResult<Book>> GetMetadata(BookInfo info, CancellationToken cancellationToken)
|
||||
// {
|
||||
// var result = new MetadataResult<Book>();
|
||||
|
||||
// var volumeId = info.GetProviderId(ComicVineVolumeExternalId.KeyName) ??
|
||||
// await FetchComicVolumeId(info, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// if (string.IsNullOrEmpty(volumeId))
|
||||
// {
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// var issueNumber = GetIssueNumberFromName(info.Name).ToString(_usCulture);
|
||||
|
||||
// await EnsureCacheFile(volumeId, issueNumber, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// var cachePath = GetCacheFilePath(volumeId, issueNumber);
|
||||
|
||||
// try
|
||||
// {
|
||||
// var issueInfo = _jsonSerializer.DeserializeFromFile<SearchResult>(cachePath);
|
||||
|
||||
// result.Item = new Book();
|
||||
// result.Item.SetProviderId(ComicVineVolumeExternalId.KeyName, volumeId);
|
||||
// result.HasMetadata = true;
|
||||
|
||||
// ProcessIssueData(result.Item, issueInfo, cancellationToken);
|
||||
// }
|
||||
// catch (FileNotFoundException)
|
||||
// {
|
||||
// }
|
||||
// catch (DirectoryNotFoundException)
|
||||
// {
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// /// <param name="item"></param>
|
||||
// /// <param name="issue"></param>
|
||||
// /// <param name="cancellationToken"></param>
|
||||
// private void ProcessIssueData(Book item, SearchResult issue, CancellationToken cancellationToken)
|
||||
// {
|
||||
// cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// if (issue.results == null || issue.results.Count == 0)
|
||||
// return;
|
||||
|
||||
// var name = issue.results[0].issue_number;
|
||||
|
||||
// if (!string.IsNullOrEmpty(issue.results[0].name))
|
||||
// name += " - " + issue.results[0].name;
|
||||
|
||||
// item.Name = name;
|
||||
|
||||
// string sortIssueName = issue.results[0].issue_number;
|
||||
|
||||
// if (sortIssueName.Length == 1)
|
||||
// sortIssueName = "00" + sortIssueName;
|
||||
// else if (sortIssueName.Length == 2)
|
||||
// sortIssueName = "0" + sortIssueName;
|
||||
|
||||
// sortIssueName += " - " + issue.results[0].volume.name;
|
||||
|
||||
// if (!string.IsNullOrEmpty(issue.results[0].name))
|
||||
// sortIssueName += ", " + issue.results[0].name;
|
||||
|
||||
// item.ForcedSortName = sortIssueName;
|
||||
|
||||
// item.SeriesName = issue.results[0].volume.name;
|
||||
|
||||
// item.Overview = WebUtility.HtmlDecode(issue.results[0].description);
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// /// <param name="item"></param>
|
||||
// /// <param name="cancellationToken"></param>
|
||||
// /// <returns></returns>
|
||||
// private async Task<string> FetchComicVolumeId(BookInfo item, CancellationToken cancellationToken)
|
||||
// {
|
||||
// cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// /*
|
||||
// * Comics should be stored so that they represent the volume number and the parent represents the comic series.
|
||||
// */
|
||||
// var name = item.SeriesName;
|
||||
// var year = string.Empty;
|
||||
|
||||
// foreach (var re in NameMatches)
|
||||
// {
|
||||
// Match m = re.Match(name);
|
||||
// if (m.Success)
|
||||
// {
|
||||
// name = m.Groups["name"].Value.Trim();
|
||||
// year = m.Groups["year"] != null ? m.Groups["year"].Value : null;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (string.IsNullOrEmpty(year) && item.Year != null)
|
||||
// {
|
||||
// year = item.Year.ToString();
|
||||
// }
|
||||
|
||||
// var url = string.Format(VolumeSearchUrl, ApiKey, UrlEncode(name));
|
||||
// var stream = await _httpClient.Get(url, Plugin.Instance.ComicVineSemiphore, cancellationToken);
|
||||
|
||||
// if (stream == null)
|
||||
// {
|
||||
// _logger.Info("response is null");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// var searchResult = _jsonSerializer.DeserializeFromStream<SearchResult>(stream);
|
||||
|
||||
// var comparableName = GetComparableName(name);
|
||||
|
||||
// foreach (var result in searchResult.results)
|
||||
// {
|
||||
// if (result.volume.name != null &&
|
||||
// GetComparableName(result.volume.name).Equals(comparableName, StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// _logger.Info("volume name: " + GetComparableName(result.volume.name) + ", matches: " + comparableName);
|
||||
// if (!string.IsNullOrEmpty(year))
|
||||
// {
|
||||
// var resultYear = result.cover_date.Substring(0, 4);
|
||||
|
||||
// if (year == resultYear)
|
||||
// return result.volume.id.ToString(CultureInfo.InvariantCulture);
|
||||
// }
|
||||
// else
|
||||
// return result.volume.id.ToString(CultureInfo.InvariantCulture);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (result.volume.name != null)
|
||||
// _logger.Info(comparableName + " does not match " + GetComparableName(result.volume.name));
|
||||
// }
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// /// <param name="volumeId"></param>
|
||||
// /// <param name="issueNumber"></param>
|
||||
// /// <param name="cancellationToken"></param>
|
||||
// /// <returns></returns>
|
||||
// private async Task<SearchResult> GetComicIssue(string volumeId, float issueNumber, CancellationToken cancellationToken)
|
||||
// {
|
||||
// var url = string.Format(IssueSearchUrl, ApiKey, issueNumber, volumeId);
|
||||
|
||||
// var stream = await _httpClient.Get(url, Plugin.Instance.ComicVineSemiphore, cancellationToken);
|
||||
|
||||
// if (stream == null)
|
||||
// {
|
||||
// _logger.Info("response is null");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// return _jsonSerializer.DeserializeFromStream<SearchResult>(stream);
|
||||
// }
|
||||
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// /// <param name="name"></param>
|
||||
// /// <returns></returns>
|
||||
// public static float GetIssueNumberFromName(string name)
|
||||
// {
|
||||
// var result = Regex.Match(name, @"\d+\.\d").Value;
|
||||
|
||||
// if (string.IsNullOrEmpty(result))
|
||||
// result = Regex.Match(name, @"#\d+").Value;
|
||||
|
||||
// if (string.IsNullOrEmpty(result))
|
||||
// result = Regex.Match(name, @"\d+").Value;
|
||||
|
||||
// if (!string.IsNullOrEmpty(result))
|
||||
// {
|
||||
// result = result.Replace("#", "");
|
||||
// // Remove any leading zeros so that 005 becomes 5
|
||||
// result = result.TrimStart(new[] { '0' });
|
||||
|
||||
// var issueNumber = float.Parse(result);
|
||||
// return issueNumber;
|
||||
// }
|
||||
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
// private const string Remove = "\"'!`?";
|
||||
// // "Face/Off" support.
|
||||
// private const string Spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - they are different char codes)
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// /// <param name="name"></param>
|
||||
// /// <returns></returns>
|
||||
// internal static string GetComparableName(string name)
|
||||
// {
|
||||
// name = name.ToLower();
|
||||
// name = name.Normalize(NormalizationForm.FormKD);
|
||||
|
||||
// foreach (var pair in ReplaceEndNumerals)
|
||||
// {
|
||||
// if (name.EndsWith(pair.Key))
|
||||
// {
|
||||
// name = name.Remove(name.IndexOf(pair.Key, StringComparison.InvariantCulture), pair.Key.Length);
|
||||
// name = name + pair.Value;
|
||||
// }
|
||||
// }
|
||||
|
||||
// var sb = new StringBuilder();
|
||||
// foreach (var c in name)
|
||||
// {
|
||||
// if (c >= 0x2B0 && c <= 0x0333)
|
||||
// {
|
||||
// // skip char modifier and diacritics
|
||||
// }
|
||||
// else if (Remove.IndexOf(c) > -1)
|
||||
// {
|
||||
// // skip chars we are removing
|
||||
// }
|
||||
// else if (Spacers.IndexOf(c) > -1)
|
||||
// {
|
||||
// sb.Append(" ");
|
||||
// }
|
||||
// else if (c == '&')
|
||||
// {
|
||||
// sb.Append(" and ");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// sb.Append(c);
|
||||
// }
|
||||
// }
|
||||
// name = sb.ToString();
|
||||
// name = name.Replace("the", " ");
|
||||
// name = name.Replace(" - ", ": ");
|
||||
|
||||
// string prevName;
|
||||
// do
|
||||
// {
|
||||
// prevName = name;
|
||||
// name = name.Replace(" ", " ");
|
||||
// } while (name.Length != prevName.Length);
|
||||
|
||||
// return name.Trim();
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// static readonly Dictionary<string, string> ReplaceEndNumerals = new Dictionary<string, string> {
|
||||
// {" i", " 1"},
|
||||
// {" ii", " 2"},
|
||||
// {" iii", " 3"},
|
||||
// {" iv", " 4"},
|
||||
// {" v", " 5"},
|
||||
// {" vi", " 6"},
|
||||
// {" vii", " 7"},
|
||||
// {" viii", " 8"},
|
||||
// {" ix", " 9"},
|
||||
// {" x", " 10"}
|
||||
// };
|
||||
|
||||
// private static string UrlEncode(string name)
|
||||
// {
|
||||
// return WebUtility.UrlEncode(name);
|
||||
// }
|
||||
|
||||
// public string Name
|
||||
// {
|
||||
// get { return "Comic Vine"; }
|
||||
// }
|
||||
|
||||
// private readonly Task _cachedResult = Task.FromResult(true);
|
||||
|
||||
// internal Task EnsureCacheFile(string volumeId, string issueNumber, CancellationToken cancellationToken)
|
||||
// {
|
||||
// var path = GetCacheFilePath(volumeId, issueNumber);
|
||||
|
||||
// var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
|
||||
// if (fileInfo.Exists)
|
||||
// {
|
||||
// // If it's recent don't re-download
|
||||
// if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7)
|
||||
// {
|
||||
// return _cachedResult;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return DownloadIssueInfo(volumeId, issueNumber, cancellationToken);
|
||||
// }
|
||||
|
||||
// internal async Task DownloadIssueInfo(string volumeId, string issueNumber, CancellationToken cancellationToken)
|
||||
// {
|
||||
// var url = string.Format(IssueSearchUrl, ApiKey, issueNumber, volumeId);
|
||||
|
||||
// var xmlPath = GetCacheFilePath(volumeId, issueNumber);
|
||||
|
||||
// using (var stream = await _httpClient.Get(url, Plugin.Instance.ComicVineSemiphore, cancellationToken).ConfigureAwait(false))
|
||||
// {
|
||||
// Directory.CreateDirectory(Path.GetDirectoryName(xmlPath));
|
||||
|
||||
// using (var fileStream = _fileSystem.GetFileStream(xmlPath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
|
||||
// {
|
||||
// await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// internal string GetCacheFilePath(string volumeId, string issueNumber)
|
||||
// {
|
||||
// var gameDataPath = GetComicVineDataPath();
|
||||
// return Path.Combine(gameDataPath, volumeId, "issue-" + issueNumber.ToString(_usCulture) + ".json");
|
||||
// }
|
||||
|
||||
// private string GetComicVineDataPath()
|
||||
// {
|
||||
// var dataPath = Path.Combine(_appPaths.CachePath, "comicvine");
|
||||
|
||||
// return dataPath;
|
||||
// }
|
||||
|
||||
// public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
// public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BookInfo searchInfo, CancellationToken cancellationToken)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
//}
|
||||
}
|
||||
public static ComicVineMetadataProvider Current;
|
||||
|
||||
public ComicVineMetadataProvider(ILogger<ComicVineMetadataProvider> logger, IHttpClient httpClient, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IApplicationPaths appPaths)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
Current = this;
|
||||
}
|
||||
|
||||
public async Task<MetadataResult<Book>> GetMetadata(BookInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<Book>();
|
||||
|
||||
var volumeId = info.GetProviderId(ComicVineVolumeExternalId.KeyName) ??
|
||||
await FetchComicVolumeId(info, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(volumeId))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var issueNumber = GetIssueNumberFromName(info.Name).ToString(_usCulture);
|
||||
|
||||
await EnsureCacheFile(volumeId, issueNumber, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var cachePath = GetCacheFilePath(volumeId, issueNumber);
|
||||
|
||||
try
|
||||
{
|
||||
var issueInfo = _jsonSerializer.DeserializeFromFile<SearchResult>(cachePath);
|
||||
|
||||
result.Item = new Book();
|
||||
result.Item.SetProviderId(ComicVineVolumeExternalId.KeyName, volumeId);
|
||||
result.HasMetadata = true;
|
||||
|
||||
ProcessIssueData(result.Item, issueInfo, cancellationToken);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/ <summary>
|
||||
/
|
||||
/ </summary>
|
||||
/ <param name="item"></param>
|
||||
/ <param name="issue"></param>
|
||||
/ <param name="cancellationToken"></param>
|
||||
private void ProcessIssueData(Book item, SearchResult issue, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (issue.results == null || issue.results.Count == 0)
|
||||
return;
|
||||
|
||||
var name = issue.results[0].issue_number;
|
||||
|
||||
if (!string.IsNullOrEmpty(issue.results[0].name))
|
||||
name += " - " + issue.results[0].name;
|
||||
|
||||
item.Name = name;
|
||||
|
||||
string sortIssueName = issue.results[0].issue_number;
|
||||
|
||||
if (sortIssueName.Length == 1)
|
||||
sortIssueName = "00" + sortIssueName;
|
||||
else if (sortIssueName.Length == 2)
|
||||
sortIssueName = "0" + sortIssueName;
|
||||
|
||||
sortIssueName += " - " + issue.results[0].volume.name;
|
||||
|
||||
if (!string.IsNullOrEmpty(issue.results[0].name))
|
||||
sortIssueName += ", " + issue.results[0].name;
|
||||
|
||||
item.ForcedSortName = sortIssueName;
|
||||
|
||||
item.SeriesName = issue.results[0].volume.name;
|
||||
|
||||
item.Overview = WebUtility.HtmlDecode(issue.results[0].description);
|
||||
}
|
||||
|
||||
/ <summary>
|
||||
/
|
||||
/ </summary>
|
||||
/ <param name="item"></param>
|
||||
/ <param name="cancellationToken"></param>
|
||||
/ <returns></returns>
|
||||
private async Task<string> FetchComicVolumeId(BookInfo item, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
/*
|
||||
* Comics should be stored so that they represent the volume number and the parent represents the comic series.
|
||||
#1#
|
||||
var name = item.SeriesName;
|
||||
var year = string.Empty;
|
||||
|
||||
foreach (var re in NameMatches)
|
||||
{
|
||||
Match m = re.Match(name);
|
||||
if (m.Success)
|
||||
{
|
||||
name = m.Groups["name"].Value.Trim();
|
||||
year = m.Groups["year"] != null ? m.Groups["year"].Value : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(year) && item.Year != null)
|
||||
{
|
||||
year = item.Year.ToString();
|
||||
}
|
||||
|
||||
var url = string.Format(VolumeSearchUrl, ApiKey, UrlEncode(name));
|
||||
var stream = await _httpClient.Get(url, Plugin.Instance.ComicVineSemiphore, cancellationToken);
|
||||
|
||||
if (stream == null)
|
||||
{
|
||||
_logger.Info("response is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
var searchResult = _jsonSerializer.DeserializeFromStream<SearchResult>(stream);
|
||||
|
||||
var comparableName = GetComparableName(name);
|
||||
|
||||
foreach (var result in searchResult.results)
|
||||
{
|
||||
if (result.volume.name != null &&
|
||||
GetComparableName(result.volume.name).Equals(comparableName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.Info("volume name: " + GetComparableName(result.volume.name) + ", matches: " + comparableName);
|
||||
if (!string.IsNullOrEmpty(year))
|
||||
{
|
||||
var resultYear = result.cover_date.Substring(0, 4);
|
||||
|
||||
if (year == resultYear)
|
||||
return result.volume.id.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
return result.volume.id.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result.volume.name != null)
|
||||
_logger.Info(comparableName + " does not match " + GetComparableName(result.volume.name));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/ <summary>
|
||||
/
|
||||
/ </summary>
|
||||
/ <param name="volumeId"></param>
|
||||
/ <param name="issueNumber"></param>
|
||||
/ <param name="cancellationToken"></param>
|
||||
/ <returns></returns>
|
||||
private async Task<SearchResult> GetComicIssue(string volumeId, float issueNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = string.Format(IssueSearchUrl, ApiKey, issueNumber, volumeId);
|
||||
|
||||
var stream = await _httpClient.Get(url, Plugin.Instance.ComicVineSemiphore, cancellationToken);
|
||||
|
||||
if (stream == null)
|
||||
{
|
||||
_logger.Info("response is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
return _jsonSerializer.DeserializeFromStream<SearchResult>(stream);
|
||||
}
|
||||
|
||||
|
||||
/ <summary>
|
||||
/
|
||||
/ </summary>
|
||||
/ <param name="name"></param>
|
||||
/ <returns></returns>
|
||||
public static float GetIssueNumberFromName(string name)
|
||||
{
|
||||
var result = Regex.Match(name, @"\d+\.\d").Value;
|
||||
|
||||
if (string.IsNullOrEmpty(result))
|
||||
result = Regex.Match(name, @"#\d+").Value;
|
||||
|
||||
if (string.IsNullOrEmpty(result))
|
||||
result = Regex.Match(name, @"\d+").Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
result = result.Replace("#", "");
|
||||
Remove any leading zeros so that 005 becomes 5
|
||||
result = result.TrimStart(new[] { '0' });
|
||||
|
||||
var issueNumber = float.Parse(result);
|
||||
return issueNumber;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private const string Remove = "\"'!`?";
|
||||
"Face/Off" support.
|
||||
private const string Spacers = "/,.:;\\(){}[]+-_=–*"; (there are not actually two - they are different char codes)
|
||||
|
||||
/ <summary>
|
||||
/
|
||||
/ </summary>
|
||||
/ <param name="name"></param>
|
||||
/ <returns></returns>
|
||||
internal static string GetComparableName(string name)
|
||||
{
|
||||
name = name.ToLower();
|
||||
name = name.Normalize(NormalizationForm.FormKD);
|
||||
|
||||
foreach (var pair in ReplaceEndNumerals)
|
||||
{
|
||||
if (name.EndsWith(pair.Key))
|
||||
{
|
||||
name = name.Remove(name.IndexOf(pair.Key, StringComparison.InvariantCulture), pair.Key.Length);
|
||||
name = name + pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in name)
|
||||
{
|
||||
if (c >= 0x2B0 && c <= 0x0333)
|
||||
{
|
||||
skip char modifier and diacritics
|
||||
}
|
||||
else if (Remove.IndexOf(c) > -1)
|
||||
{
|
||||
skip chars we are removing
|
||||
}
|
||||
else if (Spacers.IndexOf(c) > -1)
|
||||
{
|
||||
sb.Append(" ");
|
||||
}
|
||||
else if (c == '&')
|
||||
{
|
||||
sb.Append(" and ");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
name = sb.ToString();
|
||||
name = name.Replace("the", " ");
|
||||
name = name.Replace(" - ", ": ");
|
||||
|
||||
string prevName;
|
||||
do
|
||||
{
|
||||
prevName = name;
|
||||
name = name.Replace(" ", " ");
|
||||
} while (name.Length != prevName.Length);
|
||||
|
||||
return name.Trim();
|
||||
}
|
||||
|
||||
/ <summary>
|
||||
/
|
||||
/ </summary>
|
||||
static readonly Dictionary<string, string> ReplaceEndNumerals = new Dictionary<string, string> {
|
||||
{" i", " 1"},
|
||||
{" ii", " 2"},
|
||||
{" iii", " 3"},
|
||||
{" iv", " 4"},
|
||||
{" v", " 5"},
|
||||
{" vi", " 6"},
|
||||
{" vii", " 7"},
|
||||
{" viii", " 8"},
|
||||
{" ix", " 9"},
|
||||
{" x", " 10"}
|
||||
};
|
||||
|
||||
private static string UrlEncode(string name)
|
||||
{
|
||||
return WebUtility.UrlEncode(name);
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "Comic Vine"; }
|
||||
}
|
||||
|
||||
private readonly Task _cachedResult = Task.FromResult(true);
|
||||
|
||||
internal Task EnsureCacheFile(string volumeId, string issueNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetCacheFilePath(volumeId, issueNumber);
|
||||
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
If it's recent don't re-download
|
||||
if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7)
|
||||
{
|
||||
return _cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
return DownloadIssueInfo(volumeId, issueNumber, cancellationToken);
|
||||
}
|
||||
|
||||
internal async Task DownloadIssueInfo(string volumeId, string issueNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = string.Format(IssueSearchUrl, ApiKey, issueNumber, volumeId);
|
||||
|
||||
var xmlPath = GetCacheFilePath(volumeId, issueNumber);
|
||||
|
||||
using (var stream = await _httpClient.Get(url, Plugin.Instance.ComicVineSemiphore, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(xmlPath));
|
||||
|
||||
using (var fileStream = _fileSystem.GetFileStream(xmlPath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
|
||||
{
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal string GetCacheFilePath(string volumeId, string issueNumber)
|
||||
{
|
||||
var gameDataPath = GetComicVineDataPath();
|
||||
return Path.Combine(gameDataPath, volumeId, "issue-" + issueNumber.ToString(_usCulture) + ".json");
|
||||
}
|
||||
|
||||
private string GetComicVineDataPath()
|
||||
{
|
||||
var dataPath = Path.Combine(_appPaths.CachePath, "comicvine");
|
||||
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BookInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}*/
|
@ -0,0 +1,29 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
|
||||
{
|
||||
public class ComicVineVolumeExternalId : IExternalId
|
||||
{
|
||||
public static string KeyName => "ComicVineVolume";
|
||||
|
||||
public string Key => KeyName;
|
||||
|
||||
public string ProviderName => "Comic Vine Volume";
|
||||
|
||||
public ExternalIdMediaType? Type
|
||||
=> null; // TODO: enum does not yet have the Book type
|
||||
|
||||
// TODO: Is there a url?
|
||||
public string UrlFormatString =>
|
||||
|
||||
null;
|
||||
|
||||
public bool Supports(IHasProviderIds item)
|
||||
{
|
||||
return item is Book;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,55 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
#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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -27,15 +27,6 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
|
||||
public string Name => "Epub Metadata";
|
||||
|
||||
private readonly struct EpubCover
|
||||
{
|
||||
public string MimeType { get; }
|
||||
public string Path { get; }
|
||||
|
||||
public EpubCover(string coverMimeType, string coverPath) =>
|
||||
(this.MimeType, this.Path) = (coverMimeType, coverPath);
|
||||
}
|
||||
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Book;
|
||||
@ -43,7 +34,17 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
return new List<ImageType> {ImageType.Primary};
|
||||
return new List<ImageType> { ImageType.Primary };
|
||||
}
|
||||
|
||||
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.Equals(Path.GetExtension(item.Path), ".epub", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetFromZip(item);
|
||||
}
|
||||
|
||||
return Task.FromResult(new DynamicImageResponse { HasImage = false });
|
||||
}
|
||||
|
||||
private bool IsValidImage(string mimeType)
|
||||
@ -61,10 +62,8 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
var coverPath = Path.Combine(opfRootDirectory, manifestNode.Attributes["href"].Value);
|
||||
return new EpubCover(coverMimeType, coverPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private EpubCover? ReadCoverPath(XmlDocument opf, string opfRootDirectory)
|
||||
@ -122,7 +121,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
var coverRef = ReadCoverPath(opf, opfRootDirectory);
|
||||
if (coverRef == null)
|
||||
{
|
||||
return Task.FromResult(new DynamicImageResponse {HasImage = false});
|
||||
return Task.FromResult(new DynamicImageResponse { HasImage = false });
|
||||
}
|
||||
|
||||
var cover = coverRef.Value;
|
||||
@ -130,7 +129,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
var coverFile = epub.GetEntry(cover.Path);
|
||||
if (coverFile == null)
|
||||
{
|
||||
return Task.FromResult(new DynamicImageResponse {HasImage = false});
|
||||
return Task.FromResult(new DynamicImageResponse { HasImage = false });
|
||||
}
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
@ -158,7 +157,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
var opfFilePath = EpubUtils.ReadContentFilePath(epub);
|
||||
if (opfFilePath == null)
|
||||
{
|
||||
return Task.FromResult(new DynamicImageResponse {HasImage = false});
|
||||
return Task.FromResult(new DynamicImageResponse { HasImage = false });
|
||||
}
|
||||
|
||||
var opfRootDirectory = Path.GetDirectoryName(opfFilePath);
|
||||
@ -166,7 +165,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
var opfFile = epub.GetEntry(opfFilePath);
|
||||
if (opfFile == null)
|
||||
{
|
||||
return Task.FromResult(new DynamicImageResponse {HasImage = false});
|
||||
return Task.FromResult(new DynamicImageResponse { HasImage = false });
|
||||
}
|
||||
|
||||
using var opfStream = opfFile.Open();
|
||||
@ -177,16 +176,16 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
return LoadCover(epub, opfDocument, opfRootDirectory);
|
||||
}
|
||||
|
||||
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
|
||||
private readonly struct EpubCover
|
||||
{
|
||||
if (string.Equals(Path.GetExtension(item.Path), ".epub", StringComparison.OrdinalIgnoreCase))
|
||||
public EpubCover(string coverMimeType, string coverPath)
|
||||
{
|
||||
return GetFromZip(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Task.FromResult(new DynamicImageResponse {HasImage = false});
|
||||
(MimeType, Path) = (coverMimeType, coverPath);
|
||||
}
|
||||
|
||||
public string MimeType { get; }
|
||||
|
||||
public string Path { get; }
|
||||
}
|
||||
}
|
||||
}
|
@ -13,8 +13,8 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
{
|
||||
public class EpubMetadataProvider : ILocalMetadataProvider<Book>
|
||||
{
|
||||
private readonly ILogger<EpubMetadataProvider> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<EpubMetadataProvider> _logger;
|
||||
|
||||
public EpubMetadataProvider(IFileSystem fileSystem, ILogger<EpubMetadataProvider> logger)
|
||||
{
|
||||
@ -24,6 +24,29 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
|
||||
public string Name => "Epub Metadata";
|
||||
|
||||
public Task<MetadataResult<Book>> GetMetadata(
|
||||
ItemInfo info,
|
||||
IDirectoryService directoryService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetEpubFile(info.Path)?.FullName;
|
||||
var result = new MetadataResult<Book>();
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
result.HasMetadata = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = new Book();
|
||||
result.HasMetadata = true;
|
||||
result.Item = item;
|
||||
ReadEpubAsZip(result, path, cancellationToken);
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private FileSystemMetadata GetEpubFile(string path)
|
||||
{
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
@ -64,28 +87,5 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.Epub
|
||||
|
||||
OpfReader.ReadOpfData(result, opfDocument, cancellationToken, _logger);
|
||||
}
|
||||
|
||||
public Task<MetadataResult<Book>> GetMetadata(
|
||||
ItemInfo info,
|
||||
IDirectoryService directoryService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetEpubFile(info.Path)?.FullName;
|
||||
var result = new MetadataResult<Book>();
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
result.HasMetadata = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = new Book();
|
||||
result.HasMetadata = true;
|
||||
result.Item = item;
|
||||
ReadEpubAsZip(result, path, cancellationToken);
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,4 +6,4 @@
|
||||
public const string SearchUrl = @"https://www.googleapis.com/books/v1/volumes?q={0}&startIndex={1}&maxResults={2}";
|
||||
public const string DetailsUrl = @"https://www.googleapis.com/books/v1/volumes/{0}";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@ -15,11 +15,13 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
public class GoogleBooksImageProvider : IRemoteImageProvider
|
||||
{
|
||||
private IHttpClientFactory _httpClientFactory;
|
||||
private IJsonSerializer _jsonSerializer;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private ILogger<GoogleBooksImageProvider> _logger;
|
||||
|
||||
public GoogleBooksImageProvider(ILogger<GoogleBooksImageProvider> logger, IHttpClientFactory httpClientFactory,
|
||||
public GoogleBooksImageProvider(
|
||||
ILogger<GoogleBooksImageProvider> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IJsonSerializer jsonSerializer)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
@ -67,6 +69,12 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
return await httpClient.GetAsync(url).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<BookResult> FetchBookData(string googleBookId, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@ -114,11 +122,5 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
return await httpClient.GetAsync(url).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,20 +19,43 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
public class GoogleBooksProvider : IRemoteMetadataProvider<Book, BookInfo>
|
||||
{
|
||||
// convert these characters to whitespace for better matching
|
||||
// there are two dashes with different char codes
|
||||
private const string Spacers = "/,.:;\\(){}[]+-_=–*";
|
||||
|
||||
private const string Remove = "\"'!`?";
|
||||
|
||||
// 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 = new[] {
|
||||
private static readonly Regex[] NameMatches =
|
||||
{
|
||||
new Regex(@"(?<name>.*)\((?<year>\d{4})\)"),
|
||||
new Regex(@"(?<index>\d*)\s\-\s(?<name>.*)"),
|
||||
new Regex(@"(?<name>.*)")
|
||||
};
|
||||
|
||||
private IHttpClientFactory _httpClientFactory;
|
||||
private IJsonSerializer _jsonSerializer;
|
||||
private ILogger<GoogleBooksProvider> _logger;
|
||||
private readonly Dictionary<string, string> _replaceEndNumerals = new ()
|
||||
{
|
||||
{ " i", " 1" },
|
||||
{ " ii", " 2" },
|
||||
{ " iii", " 3" },
|
||||
{ " iv", " 4" },
|
||||
{ " v", " 5" },
|
||||
{ " vi", " 6" },
|
||||
{ " vii", " 7" },
|
||||
{ " viii", " 8" },
|
||||
{ " ix", " 9" },
|
||||
{ " x", " 10" }
|
||||
};
|
||||
|
||||
public GoogleBooksProvider(ILogger<GoogleBooksProvider> logger, IHttpClientFactory httpClientFactory,
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly ILogger<GoogleBooksProvider> _logger;
|
||||
|
||||
public GoogleBooksProvider(
|
||||
ILogger<GoogleBooksProvider> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IJsonSerializer jsonSerializer)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
@ -42,11 +65,6 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
|
||||
public string Name => "Google Books";
|
||||
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Book;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BookInfo item, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@ -71,11 +89,11 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
public async Task<MetadataResult<Book>> GetMetadata(BookInfo item, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
MetadataResult<Book> metadataResult = new MetadataResult<Book>();
|
||||
var metadataResult = new MetadataResult<Book>();
|
||||
metadataResult.HasMetadata = false;
|
||||
|
||||
var googleBookId = item.GetProviderId("GoogleBooks")
|
||||
?? await FetchBookId(item, cancellationToken).ConfigureAwait(false);
|
||||
?? await FetchBookId(item, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(googleBookId))
|
||||
{
|
||||
@ -95,6 +113,17 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
return metadataResult;
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
return await httpClient.GetAsync(url).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Book;
|
||||
}
|
||||
|
||||
private async Task<SearchResult> GetSearchResultsInternal(BookInfo item, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@ -112,7 +141,6 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task<string> FetchBookId(BookInfo item, CancellationToken cancellationToken)
|
||||
@ -129,14 +157,23 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
foreach (var i in searchResults.items)
|
||||
{
|
||||
// no match so move on to the next item
|
||||
if (!GetComparableName(i.volumeInfo.title).Equals(comparableName)) continue;
|
||||
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;
|
||||
if (!int.TryParse(resultYear, out var bookReleaseYear)) continue;
|
||||
var resultYear = i.volumeInfo.publishedDate.Length > 4 ? i.volumeInfo.publishedDate.Substring(0, 4) : i.volumeInfo.publishedDate;
|
||||
if (!int.TryParse(resultYear, out var bookReleaseYear))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// allow a one year variance
|
||||
if (Math.Abs(bookReleaseYear - item.Year ?? 0) > 1) continue;
|
||||
if (Math.Abs(bookReleaseYear - item.Year ?? 0) > 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return i.id;
|
||||
}
|
||||
@ -178,31 +215,34 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(bookResult.volumeInfo.publisher))
|
||||
{
|
||||
book.Studios.Append(bookResult.volumeInfo.publisher);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(bookResult.volumeInfo.mainCatagory))
|
||||
{
|
||||
book.Tags.Append(bookResult.volumeInfo.mainCatagory);
|
||||
}
|
||||
|
||||
if (bookResult.volumeInfo.catagories != null && bookResult.volumeInfo.catagories.Count > 0)
|
||||
{
|
||||
foreach (var category in bookResult.volumeInfo.catagories)
|
||||
{
|
||||
book.Tags.Append(category);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// convert these characters to whitespace for better matching
|
||||
// there are two dashes with different char codes
|
||||
private const string Spacers = "/,.:;\\(){}[]+-_=–*";
|
||||
private const string Remove = "\"'!`?";
|
||||
|
||||
private string GetComparableName(string name)
|
||||
{
|
||||
name = name.ToLower();
|
||||
@ -241,11 +281,12 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
name = sb.ToString();
|
||||
name = name.Replace("the", " ");
|
||||
name = name.Replace(" - ", ": ");
|
||||
|
||||
Regex regex = new Regex(@"\s+");
|
||||
var regex = new Regex(@"\s+");
|
||||
name = regex.Replace(name, " ");
|
||||
|
||||
return name.Trim();
|
||||
@ -256,38 +297,28 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
foreach (var regex in NameMatches)
|
||||
{
|
||||
var match = regex.Match(item.Name);
|
||||
if (!match.Success) continue;
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (result) item.IndexNumber = index;
|
||||
if (result)
|
||||
{
|
||||
item.IndexNumber = index;
|
||||
}
|
||||
|
||||
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);
|
||||
if (result) item.Year = year;
|
||||
if (result)
|
||||
{
|
||||
item.Year = year;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, string> _replaceEndNumerals = new Dictionary<string, string> {
|
||||
{" i", " 1"},
|
||||
{" ii", " 2"},
|
||||
{" iii", " 3"},
|
||||
{" iv", " 4"},
|
||||
{" v", " 5"},
|
||||
{" vi", " 6"},
|
||||
{" vii", " 7"},
|
||||
{" viii", " 8"},
|
||||
{" ix", " 9"},
|
||||
{" x", " 10"}
|
||||
};
|
||||
|
||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
return await httpClient.GetAsync(url).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
#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; }
|
||||
}
|
||||
|
||||
@ -35,10 +53,15 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
// 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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -18,8 +18,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
MetadataResult<Book> bookResult,
|
||||
XmlDocument doc,
|
||||
CancellationToken cancellationToken,
|
||||
ILogger<TCategoryName> logger
|
||||
)
|
||||
ILogger<TCategoryName> logger)
|
||||
{
|
||||
var book = bookResult.Item;
|
||||
|
||||
@ -32,28 +31,37 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
var nameNode = doc.SelectSingleNode("//dc:title", namespaceManager);
|
||||
|
||||
if (!string.IsNullOrEmpty(nameNode?.InnerText))
|
||||
{
|
||||
book.Name = nameNode.InnerText;
|
||||
}
|
||||
|
||||
var overViewNode = doc.SelectSingleNode("//dc:description", namespaceManager);
|
||||
|
||||
if (!string.IsNullOrEmpty(overViewNode?.InnerText))
|
||||
{
|
||||
book.Overview = overViewNode.InnerText;
|
||||
|
||||
}
|
||||
|
||||
var studioNode = doc.SelectSingleNode("//dc:publisher", namespaceManager);
|
||||
|
||||
if (!string.IsNullOrEmpty(studioNode?.InnerText))
|
||||
{
|
||||
book.AddStudio(studioNode.InnerText);
|
||||
}
|
||||
|
||||
var isbnNode = doc.SelectSingleNode("//dc:identifier[@opf:scheme='ISBN']", namespaceManager);
|
||||
|
||||
if (!string.IsNullOrEmpty(isbnNode?.InnerText))
|
||||
{
|
||||
book.SetProviderId("ISBN", isbnNode.InnerText);
|
||||
}
|
||||
|
||||
var amazonNode = doc.SelectSingleNode("//dc:identifier[@opf:scheme='AMAZON']", namespaceManager);
|
||||
|
||||
if (!string.IsNullOrEmpty(amazonNode?.InnerText))
|
||||
{
|
||||
book.SetProviderId("Amazon", amazonNode.InnerText);
|
||||
}
|
||||
|
||||
var genresNodes = doc.SelectNodes("//dc:subject", namespaceManager);
|
||||
|
||||
@ -70,7 +78,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers
|
||||
|
||||
if (!string.IsNullOrEmpty(authorNode?.InnerText))
|
||||
{
|
||||
var person = new PersonInfo {Name = authorNode.InnerText, Type = "Author"};
|
||||
var person = new PersonInfo { Name = authorNode.InnerText, Type = "Author" };
|
||||
|
||||
bookResult.AddPerson(person);
|
||||
}
|
||||
|
58
jellyfin.ruleset
Normal file
58
jellyfin.ruleset
Normal file
@ -0,0 +1,58 @@
|
||||
<?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" />
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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" />
|
||||
|
||||
<!-- 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>
|
||||
</RuleSet>
|
Loading…
Reference in New Issue
Block a user