mirror of
https://github.com/jellyfin/jellyfin-plugin-anidb.git
synced 2024-11-26 23:50:26 +00:00
Fix episode metadata & improve series metadata (#29)
* Improve AniDbUrlRegex * Use Empty and Improve AniDbUrlRegex * Fix PluginConfiguration merge issues * Fix AniDbSeriesProvider.cs merge issues * Remove old files
This commit is contained in:
parent
04160e9c31
commit
ccde517e67
@ -31,6 +31,7 @@ namespace Jellyfin.Plugin.AniDB.Configuration
|
||||
{
|
||||
TitlePreference = TitlePreferenceType.Localized;
|
||||
OriginalTitlePreference = TitlePreferenceType.JapaneseRomaji;
|
||||
IgnoreSeason = false;
|
||||
TitleSimilarityThreshold = 50;
|
||||
MaxGenres = 5;
|
||||
TidyGenreList = true;
|
||||
@ -44,6 +45,8 @@ namespace Jellyfin.Plugin.AniDB.Configuration
|
||||
|
||||
public TitlePreferenceType OriginalTitlePreference { get; set; }
|
||||
|
||||
public bool IgnoreSeason { get; set; }
|
||||
|
||||
public int TitleSimilarityThreshold { get; set; }
|
||||
|
||||
public int MaxGenres { get; set; }
|
||||
|
@ -31,6 +31,13 @@
|
||||
<input id="chkTitleSimilarityThreshold" name="chkTitleSimilarityThreshold" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">Set this to zero to only automatically match if the title of the show is exactly the same. A value of one means that one character can be inserted or deleted.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="chkIgnoreSeason" name="chkIgnoreSeason" type="checkbox" is="emby-checkbox" />
|
||||
<span>Ignore Season</span>
|
||||
</label>
|
||||
<div class="fieldDescription">AniDB doesn't support seasons. If checked, it will treat every season like season one, except for specials.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputeLabel inputLabelUnfocused" for="chkMaxGenres">Max Genres</label>
|
||||
<input id="chkMaxGenres" name="chkMaxGenres" type="number" is="emby-input" min="0" />
|
||||
@ -86,6 +93,7 @@
|
||||
ApiClient.getPluginConfiguration(AniDBConfigurationPage.pluginUniqueId).then(function (config) {
|
||||
document.getElementById('titleLanguage').value = config.TitlePreference;
|
||||
document.getElementById('originalTitleLanguage').value = config.OriginalTitlePreference;
|
||||
document.getElementById('chkIgnoreSeason').checked = config.IgnoreSeason;
|
||||
document.getElementById('chkTitleSimilarityThreshold').value = config.TitleSimilarityThreshold;
|
||||
document.getElementById('chkMaxGenres').value = config.MaxGenres;
|
||||
document.getElementById('chkTitleCaseGenres').checked = config.TitleCaseGenres;
|
||||
@ -104,6 +112,7 @@
|
||||
ApiClient.getPluginConfiguration(AniDBConfigurationPage.pluginUniqueId).then(function (config) {
|
||||
config.TitlePreference = document.getElementById('titleLanguage').value;
|
||||
config.OriginalTitlePreference = document.getElementById('originalTitleLanguage').value;
|
||||
config.IgnoreSeason = document.getElementById('chkIgnoreSeason').checked;
|
||||
config.TitleSimilarityThreshold = document.getElementById('chkTitleSimilarityThreshold').value;
|
||||
config.MaxGenres = document.getElementById('chkMaxGenres').value;
|
||||
config.TitleCaseGenres = document.getElementById('chkTitleCaseGenres').checked;
|
||||
|
@ -1,50 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Converter
|
||||
{
|
||||
public struct AniDbEpisodeIdentity
|
||||
{
|
||||
private static readonly Regex _regex = new Regex(@"(?<series>\d+):(?<type>[S])?(?<epno>\d+)(-(?<epnoend>\d+))?");
|
||||
|
||||
public AniDbEpisodeIdentity(string id)
|
||||
{
|
||||
this = Parse(id).Value;
|
||||
}
|
||||
|
||||
public AniDbEpisodeIdentity(string seriesId, int episodeNumber, int? episodeNumberEnd, string episodeType)
|
||||
{
|
||||
SeriesId = seriesId;
|
||||
EpisodeNumber = episodeNumber;
|
||||
EpisodeNumberEnd = episodeNumberEnd;
|
||||
EpisodeType = episodeType;
|
||||
}
|
||||
|
||||
public string SeriesId { get; private set; }
|
||||
public int EpisodeNumber { get; private set; }
|
||||
public int? EpisodeNumberEnd { get; private set; }
|
||||
public string EpisodeType { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}:{1}{2}",
|
||||
SeriesId,
|
||||
EpisodeType ?? "",
|
||||
EpisodeNumber + (EpisodeNumberEnd != null ? "-" + EpisodeNumberEnd.Value.ToString() : ""));
|
||||
}
|
||||
|
||||
public static AniDbEpisodeIdentity? Parse(string id)
|
||||
{
|
||||
var match = _regex.Match(id);
|
||||
if (match.Success)
|
||||
{
|
||||
return new AniDbEpisodeIdentity(
|
||||
match.Groups["series"].Value,
|
||||
int.Parse(match.Groups["epno"].Value),
|
||||
match.Groups["epnoend"].Success ? int.Parse(match.Groups["epnoend"].Value) : (int?)null,
|
||||
match.Groups["type"].Success ? match.Groups["type"].Value : null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Converter
|
||||
{
|
||||
public struct TvdbEpisodeIdentity
|
||||
{
|
||||
public TvdbEpisodeIdentity(string id)
|
||||
: this()
|
||||
{
|
||||
this = Parse(id).Value;
|
||||
}
|
||||
|
||||
public TvdbEpisodeIdentity(string seriesId, int? seasonIndex, int episodeNumber, int? episodeNumberEnd)
|
||||
: this()
|
||||
{
|
||||
SeriesId = seriesId;
|
||||
SeasonIndex = seasonIndex;
|
||||
EpisodeNumber = episodeNumber;
|
||||
EpisodeNumberEnd = episodeNumberEnd;
|
||||
}
|
||||
|
||||
public string SeriesId { get; private set; }
|
||||
public int? SeasonIndex { get; private set; }
|
||||
public int EpisodeNumber { get; private set; }
|
||||
public int? EpisodeNumberEnd { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}:{1}:{2}",
|
||||
SeriesId,
|
||||
SeasonIndex != null ? SeasonIndex.Value.ToString() : "A",
|
||||
EpisodeNumber + (EpisodeNumberEnd != null ? "-" + EpisodeNumberEnd.Value.ToString() : ""));
|
||||
}
|
||||
|
||||
public static TvdbEpisodeIdentity? Parse(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parts = id.Split(':');
|
||||
var series = parts[0];
|
||||
var season = parts[1] != "A" ? (int?)int.Parse(parts[1]) : null;
|
||||
|
||||
int index;
|
||||
int? indexEnd;
|
||||
|
||||
var split = parts[2].IndexOf("-", StringComparison.OrdinalIgnoreCase);
|
||||
if (split != -1)
|
||||
{
|
||||
index = int.Parse(parts[2].Substring(0, split));
|
||||
indexEnd = int.Parse(parts[2].Substring(split + 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
index = int.Parse(parts[2]);
|
||||
indexEnd = null;
|
||||
}
|
||||
|
||||
return new TvdbEpisodeIdentity(series, season, index, indexEnd);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,11 +3,10 @@ using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Jellyfin.Plugin.AniDB.Providers.AniDB.Converter;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@ -32,30 +31,38 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
public string Name => "AniDB";
|
||||
|
||||
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var result = new MetadataResult<Episode>();
|
||||
|
||||
var aniDbId = info.ProviderIds.GetOrDefault(ProviderNames.AniDb);
|
||||
if (string.IsNullOrEmpty(aniDbId))
|
||||
var animeId = info.SeriesProviderIds.GetOrDefault(ProviderNames.AniDb);
|
||||
if (string.IsNullOrEmpty(animeId))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var id = AniDbEpisodeIdentity.Parse(aniDbId);
|
||||
if (id == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var seriesFolder = await FindSeriesFolder(id.Value.SeriesId, cancellationToken);
|
||||
var seriesFolder = await FindSeriesFolder(animeId, cancellationToken);
|
||||
if (string.IsNullOrEmpty(seriesFolder))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var xml = GetEpisodeXmlFile(id.Value.EpisodeNumber, id.Value.EpisodeType, seriesFolder);
|
||||
if (!Plugin.Instance.Configuration.IgnoreSeason && info.ParentIndexNumber > 1)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
string episodeType = string.Empty;
|
||||
|
||||
if (info.ParentIndexNumber == 0)
|
||||
{
|
||||
episodeType = "S";
|
||||
}
|
||||
|
||||
var xml = GetEpisodeXmlFile(info.IndexNumber, episodeType, seriesFolder);
|
||||
if (xml == null || !xml.Exists)
|
||||
{
|
||||
return result;
|
||||
@ -71,71 +78,39 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
|
||||
await ParseEpisodeXml(xml, result.Item, info.MetadataLanguage).ConfigureAwait(false);
|
||||
|
||||
if (id.Value.EpisodeNumberEnd != null && id.Value.EpisodeNumberEnd > id.Value.EpisodeNumber)
|
||||
{
|
||||
for (var i = id.Value.EpisodeNumber + 1; i <= id.Value.EpisodeNumberEnd; i++)
|
||||
{
|
||||
var additionalXml = GetEpisodeXmlFile(i, id.Value.EpisodeType, seriesFolder);
|
||||
if (additionalXml == null || !additionalXml.Exists)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await ParseAdditionalEpisodeXml(additionalXml, result.Item, info.MetadataLanguage).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string Name => "AniDB";
|
||||
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<RemoteSearchResult>();
|
||||
|
||||
var id = AniDbEpisodeIdentity.Parse(searchInfo.ProviderIds.GetOrDefault(ProviderNames.AniDb));
|
||||
if (id == null)
|
||||
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
|
||||
{
|
||||
return list;
|
||||
return Enumerable.Empty<RemoteSearchResult>();
|
||||
}
|
||||
|
||||
await AniDbSeriesProvider.GetSeriesData(
|
||||
_configurationManager.ApplicationPaths,
|
||||
id.Value.SeriesId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
if (!metadataResult.HasMetadata)
|
||||
{
|
||||
var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
|
||||
return Enumerable.Empty<RemoteSearchResult>();
|
||||
}
|
||||
|
||||
if (metadataResult.HasMetadata)
|
||||
var item = metadataResult.Item;
|
||||
|
||||
return new[]
|
||||
{
|
||||
new RemoteSearchResult
|
||||
{
|
||||
var item = metadataResult.Item;
|
||||
|
||||
list.Add(new RemoteSearchResult
|
||||
{
|
||||
IndexNumber = item.IndexNumber,
|
||||
Name = item.Name,
|
||||
ParentIndexNumber = item.ParentIndexNumber,
|
||||
PremiereDate = item.PremiereDate,
|
||||
ProductionYear = item.ProductionYear,
|
||||
ProviderIds = item.ProviderIds,
|
||||
SearchProviderName = Name,
|
||||
IndexNumberEnd = item.IndexNumberEnd
|
||||
});
|
||||
IndexNumber = item.IndexNumber,
|
||||
Name = item.Name,
|
||||
ParentIndexNumber = item.ParentIndexNumber,
|
||||
PremiereDate = item.PremiereDate,
|
||||
ProductionYear = item.ProductionYear,
|
||||
ProviderIds = item.ProviderIds,
|
||||
SearchProviderName = Name,
|
||||
IndexNumberEnd = item.IndexNumberEnd
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Don't fail the provider because this will just keep on going and going.
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
// Don't fail the provider because this will just keep on going and going.
|
||||
}
|
||||
|
||||
return list;
|
||||
};
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
@ -144,69 +119,6 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
return imageProvider.GetImageResponse(url, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ParseAdditionalEpisodeXml(FileInfo xml, Episode episode, string metadataLanguage)
|
||||
{
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
CheckCharacters = false,
|
||||
IgnoreProcessingInstructions = true,
|
||||
IgnoreComments = true,
|
||||
ValidationType = ValidationType.None
|
||||
};
|
||||
|
||||
using (var streamReader = xml.OpenText())
|
||||
using (var reader = XmlReader.Create(streamReader, settings))
|
||||
{
|
||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||
|
||||
var titles = new List<Title>();
|
||||
|
||||
while (await reader.ReadAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "length":
|
||||
var length = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(length))
|
||||
{
|
||||
long duration;
|
||||
if (long.TryParse(length, out duration))
|
||||
{
|
||||
episode.RunTimeTicks += TimeSpan.FromMinutes(duration).Ticks;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "title":
|
||||
var language = reader.GetAttribute("xml:lang");
|
||||
var name = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
titles.Add(new Title
|
||||
{
|
||||
Language = language,
|
||||
Type = "main",
|
||||
Name = name
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var title = titles.Localize(Plugin.Instance.Configuration.TitlePreference, metadataLanguage).Name;
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
title = ", " + title;
|
||||
episode.Name += Plugin.Instance.Configuration.AniDbReplaceGraves
|
||||
? title.Replace('`', '\'')
|
||||
: title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> FindSeriesFolder(string seriesId, CancellationToken cancellationToken)
|
||||
{
|
||||
var seriesDataPath = await AniDbSeriesProvider.GetSeriesData(_configurationManager.ApplicationPaths, seriesId, cancellationToken).ConfigureAwait(false);
|
||||
@ -217,6 +129,7 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
{
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
Async = true,
|
||||
CheckCharacters = false,
|
||||
IgnoreProcessingInstructions = true,
|
||||
IgnoreComments = true,
|
||||
@ -284,12 +197,18 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
Name = name
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "summary":
|
||||
var overview = AniDbSeriesProvider.ReplaceNewLine(reader.ReadElementContentAsString());
|
||||
episode.Overview = Plugin.Instance.Configuration.AniDbReplaceGraves ? overview.Replace('`', '\'') : overview;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var title = titles.Localize(Plugin.Instance.Configuration.TitlePreference, preferredMetadataLanguage).Name;
|
||||
var title = titles.Localize(Configuration.TitlePreferenceType.Localized, preferredMetadataLanguage).Name;
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
episode.Name = Plugin.Instance.Configuration.AniDbReplaceGraves
|
||||
|
@ -31,7 +31,7 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
// AniDB has very low request rate limits, a minimum of 2 seconds between requests, and an average of 4 seconds between requests
|
||||
public static readonly RateLimiter RequestLimiter = new RateLimiter(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5));
|
||||
private static readonly int[] IgnoredTagIds = { 6, 22, 23, 60, 128, 129, 185, 216, 242, 255, 268, 269, 289 };
|
||||
private static readonly Regex AniDbUrlRegex = new Regex(@"https?://anidb.net/\w+ \[(?<name>[^\]]*)\]");
|
||||
private static readonly Regex AniDbUrlRegex = new Regex(@"https?://anidb.net/\w+(/[0-9]+)? \[(?<name>[^\]]*)\]", RegexOptions.Compiled);
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
private readonly Dictionary<string, string> _typeMappings = new Dictionary<string, string>
|
||||
@ -250,9 +250,10 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
break;
|
||||
|
||||
case "description":
|
||||
series.Overview = ReplaceLineFeedWithNewLine(
|
||||
StripAniDbLinks(
|
||||
await reader.ReadElementContentAsStringAsync().ConfigureAwait(false)));
|
||||
var description = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false);
|
||||
description = description.TrimStart('*').Trim();
|
||||
series.Overview = ReplaceNewLine(StripAniDbLinks(
|
||||
Plugin.Instance.Configuration.AniDbReplaceGraves ? description.Replace('`', '\'') : description));
|
||||
|
||||
break;
|
||||
|
||||
@ -408,9 +409,9 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
return AniDbUrlRegex.Replace(text, "${name}");
|
||||
}
|
||||
|
||||
public static string ReplaceLineFeedWithNewLine(string text)
|
||||
public static string ReplaceNewLine(string text)
|
||||
{
|
||||
return text.Replace("\n", Environment.NewLine);
|
||||
return text.Replace("\n", "<br>");
|
||||
}
|
||||
|
||||
private async Task ParseActors(MetadataResult<Series> series, XmlReader reader)
|
||||
@ -454,7 +455,9 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
|
||||
if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(role)) // && series.People.All(p => p.Name != name))
|
||||
{
|
||||
series.AddPerson(CreatePerson(name, PersonType.Actor, role));
|
||||
series.AddPerson(CreatePerson(
|
||||
Plugin.Instance.Configuration.AniDbReplaceGraves ? name.Replace('`', '\'') : name,
|
||||
PersonType.Actor, role));
|
||||
}
|
||||
}
|
||||
|
||||
@ -521,7 +524,8 @@ namespace Jellyfin.Plugin.AniDB.Providers.AniDB.Metadata
|
||||
}
|
||||
else
|
||||
{
|
||||
series.AddPerson(CreatePerson(name, type));
|
||||
series.AddPerson(CreatePerson(
|
||||
Plugin.Instance.Configuration.AniDbReplaceGraves ? name.Replace('`', '\'') : name, type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user