From 20f753a8266641aba4da8e3320116bc5ab9f0d00 Mon Sep 17 00:00:00 2001 From: cxfksword Date: Tue, 1 Nov 2022 13:19:47 +0800 Subject: [PATCH] Optimize season & episode metadata fetch --- Jellyfin.Plugin.MetaShark/Api/DoubanApi.cs | 3 + .../Model/DoubanSubject.cs | 8 ++ .../Providers/BaseProvider.cs | 95 +++++++++++++++---- .../Providers/EpisodeProvider.cs | 16 +++- .../Providers/MovieProvider.cs | 6 +- .../Providers/SeasonProvider.cs | 8 +- .../Providers/SeriesProvider.cs | 6 +- 7 files changed, 111 insertions(+), 31 deletions(-) diff --git a/Jellyfin.Plugin.MetaShark/Api/DoubanApi.cs b/Jellyfin.Plugin.MetaShark/Api/DoubanApi.cs index 7e6c7b2..69d163d 100644 --- a/Jellyfin.Plugin.MetaShark/Api/DoubanApi.cs +++ b/Jellyfin.Plugin.MetaShark/Api/DoubanApi.cs @@ -175,6 +175,7 @@ namespace Jellyfin.Plugin.MetaShark.Api movie.Sid = sid; movie.Name = name; movie.Genre = cat; + movie.Category = cat; movie.Img = img; movie.Rating = rating.ToFloat(); movie.Year = year.ToInt(); @@ -230,6 +231,7 @@ namespace Jellyfin.Plugin.MetaShark.Api var img = contentNode.GetAttr("a.nbgnbg>img", "src") ?? string.Empty; var intro = contentNode.GetText("div.indent>span") ?? string.Empty; intro = intro.Replace("©豆瓣", string.Empty); + var category = contentNode.QuerySelector("div.episode_list") == null ? "电影" : "电视剧"; var info = contentNode.GetText("#info") ?? string.Empty; var director = info.GetMatchGroup(this.regDirector); @@ -254,6 +256,7 @@ namespace Jellyfin.Plugin.MetaShark.Api movie.Subname = subname; movie.Director = director; movie.Genre = genre; + movie.Category = category; movie.Country = country; movie.Language = language; movie.Duration = duration; diff --git a/Jellyfin.Plugin.MetaShark/Model/DoubanSubject.cs b/Jellyfin.Plugin.MetaShark/Model/DoubanSubject.cs index 4366523..316b92b 100644 --- a/Jellyfin.Plugin.MetaShark/Model/DoubanSubject.cs +++ b/Jellyfin.Plugin.MetaShark/Model/DoubanSubject.cs @@ -31,6 +31,8 @@ namespace Jellyfin.Plugin.MetaShark.Model public string Actor { get; set; } // "genre": "奇幻 / 冒险", public string Genre { get; set; } + // 电影/电视剧 + public string Category { get; set; } // "site": "www.harrypotter.co.uk", public string Site { get; set; } // "country": "美国 / 英国", @@ -66,12 +68,18 @@ namespace Jellyfin.Plugin.MetaShark.Model public List Celebrities { get; set; } + [JsonIgnore] public List LimitDirectorCelebrities { get { // 限制导演最多返回5个 var limitCelebrities = new List(); + if (Celebrities == null || Celebrities.Count == 0) + { + return limitCelebrities; + } + limitCelebrities.AddRange(Celebrities.Where(x => x.RoleType == MediaBrowser.Model.Entities.PersonType.Director).Take(5)); limitCelebrities.AddRange(Celebrities.Where(x => x.RoleType != MediaBrowser.Model.Entities.PersonType.Director)); diff --git a/Jellyfin.Plugin.MetaShark/Providers/BaseProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/BaseProvider.cs index adea498..77bcb98 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/BaseProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/BaseProvider.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.IO; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -19,6 +20,7 @@ using System.Threading.Tasks; using System.Web; using TMDbLib.Objects.General; using Jellyfin.Plugin.MetaShark.Configuration; +using Jellyfin.Plugin.MetaShark.Core; namespace Jellyfin.Plugin.MetaShark.Providers { @@ -74,17 +76,26 @@ namespace Jellyfin.Plugin.MetaShark.Providers this._httpClientFactory = httpClientFactory; } - - protected async Task GuestByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken) + protected async Task GuessByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken) { // ParseName is required here. // Caller provides the filename with extension stripped and NOT the parsed filename var parsedName = this._libraryManager.ParseName(info.Name); - this.Log($"GuestByDouban of [name]: {info.Name} year: {info.Year} search name: {parsedName.Name}"); + this.Log($"GuessByDouban of [name]: {info.Name} year: {info.Year} search name: {parsedName.Name}"); var result = await this._doubanApi.SearchAsync(parsedName.Name, cancellationToken).ConfigureAwait(false); var jw = new JaroWinkler(); foreach (var item in result) { + if (info is MovieInfo && item.Category != "电影") + { + continue; + } + + if (info is SeriesInfo && item.Category != "电视剧") + { + continue; + } + if (jw.Similarity(parsedName.Name, item.Name) < 0.8) { continue; @@ -92,13 +103,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers if (parsedName.Year == null || parsedName.Year == 0) { - this.Log($"GuestByDouban of [name] found Sid: \"{item.Sid}\""); + this.Log($"GuessByDouban of [name] found Sid: {item.Sid}"); return item.Sid; } if (parsedName.Year == item.Year) { - this.Log($"GuestByDouban of [name] found Sid: \"{item.Sid}\""); + this.Log($"GuessByDouban of [name] found Sid: {item.Sid}"); return item.Sid; } } @@ -106,20 +117,24 @@ namespace Jellyfin.Plugin.MetaShark.Providers return null; } - protected async Task GuestSeasonByDoubanAsync(string name, int? year, CancellationToken cancellationToken) + protected async Task GuestDoubanSeasonByYearAsync(string name, int? year, CancellationToken cancellationToken) { if (year == null || year == 0) { return null; } - this.Log($"GuestSeasonByDouban of [name]: {name} year: {year}"); + this.Log($"GuestDoubanSeasonByYear of [name]: {name} year: {year}"); var result = await this._doubanApi.SearchAsync(name, cancellationToken).ConfigureAwait(false); var jw = new JaroWinkler(); foreach (var item in result) { + if (item.Category != "电视剧") + { + continue; + } 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} "); + // this.Log($"GuestDoubanSeasonByYear name: {name} douban_name: {item.Name} douban_sid: {item.Sid} douban_year: {item.Year} score: {score} "); if (score < 0.8) { continue; @@ -127,7 +142,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers if (year == item.Year) { - this.Log($"GuestSeasonByDouban of [name] found Sid: \"{item.Sid}\""); + this.Log($"GuestDoubanSeasonByYear of [name] found Sid: {item.Sid}"); return item.Sid; } } @@ -135,6 +150,45 @@ namespace Jellyfin.Plugin.MetaShark.Providers return null; } + protected async Task GuestDoubanSeasonByNumberAsync(string name, int? seasonNumber, CancellationToken cancellationToken) + { + if (seasonNumber == null || seasonNumber == 0) + { + return null; + } + + this.Log($"GuestDoubanSeasonByNumber of [name]: {name} seasonNumber: {seasonNumber}"); + var result = await this._doubanApi.SearchAsync(name, cancellationToken).ConfigureAwait(false); + var jw = new JaroWinkler(); + var matchList = new List(); + foreach (var item in result) + { + if (item.Category != "电视剧") + { + continue; + } + var score = jw.Similarity(name, item.Name); + if (score < 0.8) + { + continue; + } + + // this.Log($"GuestDoubanSeasonByNumber name: {name} douban_name: {item.Name} douban_sid: {item.Sid} douban_year: {item.Year} score: {score} "); + matchList.Add(item); + } + + matchList.Sort((x, y) => x.Year.CompareTo(y.Year)); + if (matchList.Count >= seasonNumber) + { + var matchItem = matchList[seasonNumber.Value - 1]; + var sid = matchItem.Sid; + this.Log($"GuestDoubanSeasonByNumber of [name] found Sid: {sid}"); + return sid; + } + + return null; + } + protected async Task GuestByTmdbAsync(ItemLookupInfo info, CancellationToken cancellationToken) { // ParseName is required here. @@ -173,22 +227,25 @@ namespace Jellyfin.Plugin.MetaShark.Providers } - protected string AppendMetaSourcePrefix(string name, string source) + protected int GetVideoFileCount(string? dir) { - if (string.IsNullOrEmpty(name)) + if (dir == null) { - return name; + return 0; } - return $"[{source}]{name}"; - } - protected string RemoveMetaSourcePrefix(string name) - { - if (string.IsNullOrEmpty(name)) + var dirInfo = new DirectoryInfo(dir); + var files = dirInfo.GetFiles(); + var nameOptions = new Emby.Naming.Common.NamingOptions(); + var videoFilesCount = 0; + foreach (var fileInfo in files.Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden))) { - return name; + if (Emby.Naming.Video.VideoResolver.IsVideoFile(fileInfo.FullName, nameOptions)) + { + videoFilesCount++; + } } - return regMetaSourcePrefix.Replace(name, string.Empty); + return videoFilesCount; } diff --git a/Jellyfin.Plugin.MetaShark/Providers/EpisodeProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/EpisodeProvider.cs index 26e005c..5867877 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/EpisodeProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/EpisodeProvider.cs @@ -18,6 +18,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; namespace Jellyfin.Plugin.MetaShark.Providers { @@ -65,7 +66,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers var indexNumberEnd = info.IndexNumberEnd; // 修正anime命名格式导致的seasonNumber错误(从season元数据读取) var parent = _libraryManager.FindByPath(Path.GetDirectoryName(info.Path), true); - if (parent is Season season) + if (parent is Season season && seasonNumber != season.IndexNumber) { this.Log("FixSeasionNumber: old: {0} new: {1}", seasonNumber, season.IndexNumber); seasonNumber = season.IndexNumber; @@ -73,6 +74,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers // 没有season级目录或目录不命名不规范时,会为null if (seasonNumber is null or 0) { + this.Log("FixSeasionNumber: season number is null, set to default 1"); seasonNumber = 1; } // 修正anime命名格式导致的episodeNumber错误 @@ -83,7 +85,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers { seasonNumber = guessInfo.seasonNumber.Value; } - if (guessInfo.episodeNumber.HasValue && guessInfo.episodeNumber != episodeNumber) + if (guessInfo.episodeNumber.HasValue) { episodeNumber = guessInfo.episodeNumber; @@ -95,8 +97,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers }; } - - if (episodeNumber is null or 0 || seasonNumber is null or 0 || string.IsNullOrEmpty(seriesTmdbId)) { this.Log("Lack meta message. episodeNumber: {0} seasonNumber: {1} seriesTmdbId:{2}", episodeNumber, seasonNumber, seriesTmdbId); @@ -112,6 +112,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers this.Log("Can‘t found episode data from tmdb. Name: {0} seriesTmdbId: {1} seasonNumber: {2} episodeNumber: {3}", info.Name, seriesTmdbId, seasonNumber, episodeNumber); return result; } + var videoFilesCount = this.GetVideoFileCount(Path.GetDirectoryName(info.Path)); + if (videoFilesCount > 0 && seasonResult.Episodes.Count != videoFilesCount) + { + this.Log("Tmdb episode number not match. Name: {0} tmdb episode count: {1} video files count: {2}", info.Name, seasonResult.Episodes.Count, videoFilesCount); + return result; + } var episodeResult = seasonResult.Episodes[episodeNumber.Value - 1]; @@ -136,7 +142,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers item.ProductionYear = episodeResult.AirDate?.Year; item.Name = episodeResult.Name; item.Overview = episodeResult.Overview; - item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage); + item.CommunityRating = (float)System.Math.Round(episodeResult.VoteAverage, 1); result.Item = item; diff --git a/Jellyfin.Plugin.MetaShark/Providers/MovieProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/MovieProvider.cs index 22103f3..5dfbb63 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/MovieProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/MovieProvider.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers // 从tmdb搜索 - if (Plugin.Instance?.Configuration.EnableTmdbSearch ?? false) + if (this.config.EnableTmdbSearch) { var tmdbList = await _tmdbApi.SearchMovieAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); result.AddRange(tmdbList.Take(this.config.MaxSearchResult).Select(x => @@ -97,7 +97,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers if (string.IsNullOrEmpty(sid) && string.IsNullOrEmpty(tmdbId)) { // 刷新元数据匹配搜索 - sid = await this.GuestByDoubanAsync(info, cancellationToken).ConfigureAwait(false); + sid = await this.GuessByDoubanAsync(info, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(sid)) { tmdbId = await this.GuestByTmdbAsync(info, cancellationToken).ConfigureAwait(false); @@ -187,7 +187,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId); movie.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb); - movie.CommunityRating = Convert.ToSingle(movieResult.VoteAverage); + movie.CommunityRating = (float)System.Math.Round(movieResult.VoteAverage, 2); movie.PremiereDate = movieResult.ReleaseDate; movie.ProductionYear = movieResult.ReleaseDate?.Year; diff --git a/Jellyfin.Plugin.MetaShark/Providers/SeasonProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/SeasonProvider.cs index 0e8bb5b..50e721f 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/SeasonProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/SeasonProvider.cs @@ -78,10 +78,16 @@ namespace Jellyfin.Plugin.MetaShark.Providers if (!string.IsNullOrEmpty(seriesName) && seasonYear > 0) { - seasonSid = await this.GuestSeasonByDoubanAsync(seriesName, seasonYear, cancellationToken).ConfigureAwait(false); + seasonSid = await this.GuestDoubanSeasonByYearAsync(seriesName, seasonYear, cancellationToken).ConfigureAwait(false); } } + // 尝试通过豆瓣按年份排序后,按季数索引取对应一个 + if (string.IsNullOrEmpty(seasonSid) && !string.IsNullOrEmpty(seriesName) && seasonNumber.HasValue) + { + seasonSid = await this.GuestDoubanSeasonByNumberAsync(seriesName, seasonNumber, cancellationToken).ConfigureAwait(false); + } + // 获取季豆瓣数据 if (!string.IsNullOrEmpty(seasonSid)) { diff --git a/Jellyfin.Plugin.MetaShark/Providers/SeriesProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/SeriesProvider.cs index c4fb435..c474cb6 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/SeriesProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/SeriesProvider.cs @@ -57,7 +57,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers })); // 尝试从tmdb搜索 - if (Plugin.Instance?.Configuration.EnableTmdbSearch ?? false) + if (this.config.EnableTmdbSearch) { var tmdbList = await this._tmdbApi.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); result.AddRange(tmdbList.Take(this.config.MaxSearchResult).Select(x => @@ -89,7 +89,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers if (string.IsNullOrEmpty(sid) && string.IsNullOrEmpty(tmdbId)) { // 刷新元数据自动匹配搜索 - sid = await this.GuestByDoubanAsync(info, cancellationToken).ConfigureAwait(false); + sid = await this.GuessByDoubanAsync(info, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(sid)) { tmdbId = await this.GuestByTmdbAsync(info, cancellationToken).ConfigureAwait(false); @@ -209,7 +209,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture)); - series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage); + series.CommunityRating = (float)System.Math.Round(seriesResult.VoteAverage, 2); series.Overview = seriesResult.Overview;