Add analyzers to project

This commit is contained in:
crobibero 2020-12-14 18:11:44 -07:00
parent 5999e2299a
commit 6e9ce31d19
19 changed files with 835 additions and 900 deletions

View File

@ -4,6 +4,5 @@ namespace Jellyfin.Plugin.Bookshelf.Configuration
{
public class PluginConfiguration : BasePluginConfiguration
{
}
}
}

View File

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

View File

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

View File

@ -34,4 +34,4 @@ namespace Jellyfin.Plugin.Bookshelf
};
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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