Optimize anime name format

This commit is contained in:
cxfksword 2022-10-26 14:27:33 +08:00
parent ee8a090b11
commit 192865c7e1
10 changed files with 182 additions and 91 deletions

View File

@ -0,0 +1,48 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Providers;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Test
{
[TestClass]
public class EpisodeProviderTest
{
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
builder.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.SingleLine = true;
options.TimestampFormat = "hh:mm:ss ";
}));
[TestMethod]
public void TestGuessEpisodeNumber()
{
var doubanApi = new DoubanApi(loggerFactory);
var tmdbApi = new TmdbApi(loggerFactory);
var omdbApi = new OmdbApi(loggerFactory);
var httpClientFactory = new DefaultHttpClientFactory();
var libraryManagerStub = new Mock<ILibraryManager>();
var provider = new EpisodeProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, doubanApi, tmdbApi, omdbApi);
var indexNumber = provider.GuessEpisodeNumber("[POPGO][Stand_Alone_Complex][05][1080P][BluRay][x264_FLACx2_AC3x1][chs_jpn][D87C36B6].mkv");
Assert.AreEqual(indexNumber, 5);
indexNumber = provider.GuessEpisodeNumber("Fullmetal Alchemist Brotherhood.E05.1920X1080");
Assert.AreEqual(indexNumber, 5);
}
}
}

View File

@ -1,5 +1,6 @@
using Jellyfin.Plugin.MetaShark.Core;
using Emby.Naming.TV;
namespace Jellyfin.Plugin.MetaShark.Test
{

View File

@ -27,7 +27,6 @@ namespace Jellyfin.Plugin.MetaShark.Api
private readonly ILogger<TmdbApi> _logger;
private readonly IMemoryCache _memoryCache;
private readonly TMDbClient _tmDbClient;
private readonly PluginConfiguration _config;
/// <summary>
/// Initializes a new instance of the <see cref="TmdbApi"/> class.
@ -36,8 +35,8 @@ namespace Jellyfin.Plugin.MetaShark.Api
{
_logger = loggerFactory.CreateLogger<TmdbApi>();
_memoryCache = new MemoryCache(new MemoryCacheOptions());
_config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
var apiKey = string.IsNullOrEmpty(_config.TmdbApiKey) ? DEFAULT_API_KEY : _config.TmdbApiKey;
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
var apiKey = string.IsNullOrEmpty(config.TmdbApiKey) ? DEFAULT_API_KEY : config.TmdbApiKey;
_tmDbClient = new TMDbClient(apiKey);
_tmDbClient.RequestTimeout = TimeSpan.FromSeconds(10);
// Not really interested in NotFoundException
@ -54,7 +53,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb movie or null if not found.</returns>
public async Task<Movie?> GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return null;
}
@ -133,7 +132,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb tv show information or null if not found.</returns>
public async Task<TvShow?> GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return null;
}
@ -180,7 +179,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb tv season information or null if not found.</returns>
public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return null;
}
@ -229,7 +228,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb tv episode information or null if not found.</returns>
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string language, string imageLanguages, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return null;
}
@ -275,7 +274,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb person information or null if not found.</returns>
public async Task<Person?> GetPersonAsync(int personTmdbId, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return null;
}
@ -324,7 +323,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
string language,
CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return null;
}
@ -368,7 +367,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb tv show information.</returns>
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return new List<SearchTv>();
}
@ -408,7 +407,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb person information.</returns>
public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return new List<SearchPerson>();
}
@ -463,7 +462,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb movie information.</returns>
public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return new List<SearchMovie>();
}
@ -505,7 +504,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
/// <returns>The TMDb collection information.</returns>
public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
{
if (!this._config.EnableTmdb)
if (!this.IsEnable())
{
return new List<SearchCollection>();
}
@ -648,6 +647,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
return language;
}
public string GetImageLanguagesParam(string preferredLanguage)
{
var languages = new List<string>();
@ -677,5 +677,10 @@ namespace Jellyfin.Plugin.MetaShark.Api
return string.Join(',', languages);
}
private bool IsEnable()
{
return Plugin.Instance?.Configuration.EnableTmdb ?? true;
}
}
}

View File

@ -115,8 +115,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var jw = new JaroWinkler();
foreach (var item in result)
{
this.Log($"GuestSeasonByDouban name: {name} item.Name: {item.Name} score: {jw.Similarity(name, item.Name)} ");
if (jw.Similarity(name, item.Name) < 0.8)
var score = jw.Similarity(name, item.Name);
this.Log($"GuestSeasonByDouban name: {name} douban_name: {item.Name} douban_sid: {item.Sid} douban_year: {item.Year} score: {score} ");
if (score < 0.8)
{
continue;
}

View File

@ -53,7 +53,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
this.Log($"GetEpisodeImages for item: {item.Name} number: {item.IndexNumber}");
this.Log($"GetEpisodeImages of [name]: {item.Name} number: {item.IndexNumber} ParentIndexNumber: {item.ParentIndexNumber}");
var episode = (MediaBrowser.Controller.Entities.TV.Episode)item;
var series = episode.Series;
@ -62,7 +62,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (seriesTmdbId <= 0)
{
this.Log($"Got images failed because the seriesTmdbId is empty!");
this.Log($"[GetEpisodeImages] The seriesTmdbId is empty!");
return Enumerable.Empty<RemoteImageInfo>();
}
@ -71,7 +71,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
this.Log($"Got images failed because the seasonNumber or episodeNumber is empty! seasonNumber: {seasonNumber} episodeNumber: {episodeNumber}");
this.Log($"[GetEpisodeImages] The seasonNumber or episodeNumber is empty! seasonNumber: {seasonNumber} episodeNumber: {episodeNumber}");
return Enumerable.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
@ -82,7 +82,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
.ConfigureAwait(false);
if (seasonResult == null || seasonResult.Episodes.Count < episodeNumber.Value)
{
this.Log($"Not valid season data for seasonNumber: {seasonNumber} episodeNumber: {episodeNumber}");
this.Log($"[GetEpisodeImages] Can't get season data for seasonNumber: {seasonNumber} episodeNumber: {episodeNumber}");
return Enumerable.Empty<RemoteImageInfo>();
}
@ -105,7 +105,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
this.Log("[GetEpisodeImages] GetImageResponse url: {0}", url);
return this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
}

View File

@ -45,36 +45,53 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo info, CancellationToken cancellationToken)
{
this.Log($"GetSearchResults of [name]: {info.Name}");
this.Log($"GetEpisodeSearchResults of [name]: {info.Name}");
return await Task.FromResult(Enumerable.Empty<RemoteSearchResult>());
}
/// <inheritdoc />
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
this.Log($"GetEpisodeMetadata of [name]: {info.Name} number: {info.IndexNumber}");
// 重新识别时info的IndexNumber和ParentIndexNumber是从文件路径解析出来的假如命名不规范就会导致解析出错误值
// 刷新元数据不覆盖时IndexNumber和ParentIndexNumber是从当前的元数据获取
this.Log($"GetEpisodeMetadata of [name]: {info.Name} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber}");
var result = new MetadataResult<Episode>();
// 剧集信息只有tmdb有
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
var seasonNumber = info.ParentIndexNumber; // 没有season级目录时会为null
var seasonNumber = info.ParentIndexNumber;
var episodeNumber = info.IndexNumber;
var indexNumberEnd = info.IndexNumberEnd;
if (episodeNumber is null or 0)
// 修正anime命名格式导致的seasonNumber错误从season元数据读取)
var parent = _libraryManager.FindByPath(Path.GetDirectoryName(info.Path), true);
if (parent is Season season)
{
// 从文件名获取剧集的indexNumber
var fileName = Path.GetFileName(info.Path) ?? string.Empty;
episodeNumber = this.GuessEpisodeNumber(episodeNumber, fileName);
if (episodeNumber.HasValue && episodeNumber.Value > 0)
{
result.HasMetadata = true;
result.Item = new Episode
{
IndexNumber = episodeNumber
};
}
this.Log("GuessEpisodeNumber: fileName: {0} episodeNumber: {1}", fileName, episodeNumber);
this.Log("FixSeasionNumber: old: {0} new: {1}", seasonNumber, season.IndexNumber);
seasonNumber = season.IndexNumber;
}
// 没有season级目录时会为null
if (seasonNumber is null or 0)
{
seasonNumber = 1;
}
// 修正anime命名格式导致的episodeNumber错误
var fileName = Path.GetFileName(info.Path) ?? string.Empty;
var newEpisodeNumber = this.GuessEpisodeNumber(fileName);
if (newEpisodeNumber.HasValue && newEpisodeNumber != episodeNumber)
{
episodeNumber = newEpisodeNumber;
result.HasMetadata = true;
result.Item = new Episode
{
ParentIndexNumber = seasonNumber,
IndexNumber = episodeNumber
};
this.Log("GuessEpisodeNumber: fileName: {0} episodeNumber: {1}", fileName, newEpisodeNumber);
}
if (episodeNumber is null or 0 || seasonNumber is null or 0 || string.IsNullOrEmpty(seriesTmdbId))
{
@ -88,6 +105,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
.ConfigureAwait(false);
if (seasonResult == null || seasonResult.Episodes.Count < episodeNumber.Value)
{
this.Log("Cant found episode data from tmdb. Name: {0} seriesTmdbId: {1} seasonNumber: {2} episodeNumber: {3}", info.Name, seriesTmdbId, seasonNumber, episodeNumber);
return result;
}
@ -128,9 +146,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return _httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
}
private int? GuessEpisodeNumber(int? current, string fileName, double max = double.PositiveInfinity)
public int? GuessEpisodeNumber(string fileName, double max = double.PositiveInfinity)
{
var episodeIndex = current;
int? episodeIndex = null;
var result = AnitomySharp.AnitomySharp.Parse(fileName).FirstOrDefault(x => x.Category == AnitomySharp.Element.ElementCategory.ElementEpisodeNumber);
if (result != null)
@ -148,6 +166,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers
break;
}
if (episodeIndex > 1000)
{
// 可能解析了分辨率,忽略返回
episodeIndex = null;
}
return episodeIndex;
}

View File

@ -118,7 +118,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
this.Log("[GetSeasonImages] GetImageResponse url: {0}", url);
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
}

View File

@ -69,6 +69,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
info.SeriesProviderIds.TryGetValue(Plugin.ProviderId, out var metaSource);
info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid);
var seasonNumber = info.IndexNumber;
var seasonSid = info.GetProviderId(DoubanProviderId);
if (metaSource == MetaSource.Douban && !string.IsNullOrEmpty(sid))
{
@ -78,57 +79,63 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
return result;
}
var seiresName = series.Name;
var seriesName = series.Name;
// 存在tmdbid尝试从tmdb获取对应季的年份信息用于从豆瓣搜索对应季数据
int seasonYear = 0;
if (!string.IsNullOrEmpty(seriesTmdbId) && seasonNumber.HasValue)
if (string.IsNullOrEmpty(seasonSid))
{
var season = await this._tmdbApi
.GetSeasonAsync(seriesTmdbId.ToInt(), seasonNumber.Value, info.MetadataLanguage, info.MetadataLanguage, cancellationToken)
.ConfigureAwait(false);
seasonYear = season?.AirDate?.Year ?? 0;
}
if (!string.IsNullOrEmpty(seiresName) && seasonYear > 0)
{
var seasonSid = await this.GuestSeasonByDoubanAsync(seiresName, seasonYear, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(seasonSid))
var seasonYear = 0;
if (!string.IsNullOrEmpty(seriesTmdbId) && seasonNumber.HasValue)
{
var subject = await this._doubanApi.GetMovieAsync(seasonSid, cancellationToken).ConfigureAwait(false);
if (subject != null)
{
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(seasonSid, cancellationToken).ConfigureAwait(false);
var season = await this._tmdbApi
.GetSeasonAsync(seriesTmdbId.ToInt(), seasonNumber.Value, info.MetadataLanguage, info.MetadataLanguage, cancellationToken)
.ConfigureAwait(false);
seasonYear = season?.AirDate?.Year ?? 0;
}
var movie = new Season
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid } },
Name = subject.Name,
OriginalTitle = subject.OriginalName,
CommunityRating = subject.Rating,
Overview = subject.Intro,
ProductionYear = subject.Year,
Genres = subject.Genres,
PremiereDate = subject.ScreenTime,
IndexNumber = info.IndexNumber,
};
result.Item = movie;
result.HasMetadata = true;
subject.Celebrities.ForEach(c => result.AddPerson(new PersonInfo
{
Name = c.Name,
Type = c.RoleType,
Role = c.Role,
ImageUrl = c.Img,
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
}));
return result;
}
if (!string.IsNullOrEmpty(seriesName) && seasonYear > 0)
{
seasonSid = await this.GuestSeasonByDoubanAsync(seriesName, seasonYear, cancellationToken).ConfigureAwait(false);
}
}
// 获取季豆瓣数据
if (!string.IsNullOrEmpty(seasonSid))
{
var subject = await this._doubanApi.GetMovieAsync(seasonSid, cancellationToken).ConfigureAwait(false);
if (subject != null)
{
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(seasonSid, cancellationToken).ConfigureAwait(false);
var movie = new Season
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid } },
Name = subject.Name,
OriginalTitle = subject.OriginalName,
CommunityRating = subject.Rating,
Overview = subject.Intro,
ProductionYear = subject.Year,
Genres = subject.Genres,
PremiereDate = subject.ScreenTime,
IndexNumber = info.IndexNumber,
};
result.Item = movie;
result.HasMetadata = true;
subject.Celebrities.ForEach(c => result.AddPerson(new PersonInfo
{
Name = c.Name,
Type = c.RoleType,
Role = c.Role,
ImageUrl = c.Img,
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
}));
return result;
}
}
// 从豆瓣获取不到季信息直接使用series信息
result.Item = new Season
{

View File

@ -78,7 +78,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{
this.Log($"GetSeriesMetadata of [name]: {info.Name} [providerIds]: {info.ProviderIds.ToJson()}");
info.Name = this.RemoveMetaSourcePrefix(info.Name);
var result = new MetadataResult<Series>();
var sid = info.GetProviderId(DoubanProviderId);
@ -120,16 +119,18 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (!string.IsNullOrEmpty(subject.Imdb))
{
item.SetProviderId(MetadataProvider.Imdb, subject.Imdb);
// 通过imdb获取TMDB id (豆瓣的imdb id可能是旧的需要先从omdb接口获取最新的imdb id
var omdbItem = await this._omdbApi.GetByImdbID(subject.Imdb, cancellationToken).ConfigureAwait(false);
if (omdbItem != null)
if (string.IsNullOrEmpty(tmdbId))
{
var findResult = await this._tmdbApi.FindByExternalIdAsync(omdbItem.ImdbID, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (findResult?.TvResults != null && findResult.TvResults.Count > 0)
// 通过imdb获取TMDB id (豆瓣的imdb id可能是旧的需要先从omdb接口获取最新的imdb id
var omdbItem = await this._omdbApi.GetByImdbID(subject.Imdb, cancellationToken).ConfigureAwait(false);
if (omdbItem != null)
{
this.Log($"GetSeriesMetadata found tmdb [id]: {findResult.TvResults[0].Id} by imdb id: {subject.Imdb}");
item.SetProviderId(MetadataProvider.Tmdb, $"{findResult.TvResults[0].Id}");
var findResult = await this._tmdbApi.FindByExternalIdAsync(omdbItem.ImdbID, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (findResult?.TvResults != null && findResult.TvResults.Count > 0)
{
this.Log($"GetSeriesMetadata found tmdb [id]: {findResult.TvResults[0].Id} by imdb id: {subject.Imdb}");
item.SetProviderId(MetadataProvider.Tmdb, $"{findResult.TvResults[0].Id}");
}
}
}
}

View File

@ -6,6 +6,10 @@
jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMovieDb补充缺失的季数据和剧集数据。
功能:
* 支持从豆瓣和TMDB获取元数据
* 兼容anime动画名称格式
![preview](doc/logo.png)
## 安装插件