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:
Nils Fürniß 2022-03-19 20:57:32 +01:00 committed by GitHub
parent 04160e9c31
commit ccde517e67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 71 additions and 257 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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