diff --git a/Jellyfin.Plugin.MetaShark.Test/SeasonProviderTest.cs b/Jellyfin.Plugin.MetaShark.Test/SeasonProviderTest.cs index c405314..03f5895 100644 --- a/Jellyfin.Plugin.MetaShark.Test/SeasonProviderTest.cs +++ b/Jellyfin.Plugin.MetaShark.Test/SeasonProviderTest.cs @@ -64,7 +64,11 @@ namespace Jellyfin.Plugin.MetaShark.Test var imdbApi = new ImdbApi(loggerFactory); var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi); - var result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季"); + + var result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/冰与火之歌S01-S08.Game.of.Thrones.1080p.Blu-ray.x265.10bit.AC3/冰与火之歌S2.列王的纷争.2012.1080p.Blu-ray.x265.10bit.AC3"); + Assert.AreEqual(result, 2); + + result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季"); Assert.AreEqual(result, 2); result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第2季"); diff --git a/Jellyfin.Plugin.MetaShark/Core/NameParser.cs b/Jellyfin.Plugin.MetaShark/Core/NameParser.cs index 394bf4b..afd3b3f 100644 --- a/Jellyfin.Plugin.MetaShark/Core/NameParser.cs +++ b/Jellyfin.Plugin.MetaShark/Core/NameParser.cs @@ -246,15 +246,21 @@ namespace Jellyfin.Plugin.MetaShark.Core return null; } - public static bool IsSpecialDirectory(string path) + public static bool IsSpecialDirectory(string path, bool isDirectory = false) { var folder = Path.GetFileName(Path.GetDirectoryName(path))?.ToUpper() ?? string.Empty; - return folder == "SPS" || folder == "SPECIALS" || folder.Contains("特典"); + if (isDirectory) { + folder = Path.GetFileName(path)?.ToUpper() ?? string.Empty; + } + return folder == "SP" || folder == "SPS" || folder == "SPECIALS" || folder.Contains("特典"); } - public static bool IsExtraDirectory(string path) + public static bool IsExtraDirectory(string path, bool isDirectory = false) { var folder = Path.GetFileName(Path.GetDirectoryName(path))?.ToUpper() ?? string.Empty; + if (isDirectory) { + folder = Path.GetFileName(path)?.ToUpper() ?? string.Empty; + } return folder == "EXTRA" || folder == "MENU" || folder == "MENUS" diff --git a/Jellyfin.Plugin.MetaShark/Providers/BaseProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/BaseProvider.cs index 753ec48..ba9aeef 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/BaseProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/BaseProvider.cs @@ -19,6 +19,7 @@ using TMDbLib.Objects.General; using Jellyfin.Plugin.MetaShark.Configuration; using Jellyfin.Plugin.MetaShark.Core; using Microsoft.AspNetCore.Http; +using MediaBrowser.Controller.Entities.TV; namespace Jellyfin.Plugin.MetaShark.Providers { @@ -401,8 +402,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers public int? GuessSeasonNumberByDirectoryName(string path) { - // TODO: 有时series name中会带有季信息 - // 当没有season级目录时,path为空,直接返回 + // TODO: 有时 series name 中会带有季信息 + // 当没有 season 级目录时,或 season 文件夹特殊不规范命名时,会解析不到 seasonNumber,这时 path 为空,直接返回 if (string.IsNullOrEmpty(path)) { this.Log($"Season path is empty!"); @@ -416,6 +417,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers return null; } + // 中文季名 var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled); var match = regSeason.Match(fileName); if (match.Success && match.Groups.Count > 1) @@ -432,12 +434,26 @@ namespace Jellyfin.Plugin.MetaShark.Providers } } + // SXX 季名 + regSeason = new Regex(@"(? 1) + { + var seasonNumber = match.Groups[1].Value.ToInt(); + if (seasonNumber > 0) + { + this.Log($"Found season number of filename: {fileName} seasonNumber: {seasonNumber}"); + return seasonNumber; + } + } + + // 动漫季特殊命名 var seasonNameMap = new Dictionary() { - {@"[ ._](I|1st|S01|S1)[ ._]", 1}, - {@"[ ._](II|2nd|S02|S2)[ ._]", 2}, - {@"[ ._](III|3rd|S03|S3)[ ._]", 3}, - {@"[ ._](IIII|4th|S04|S4)[ ._]", 3}, + {@"[ ._](I|1st)[ ._]", 1}, + {@"[ ._](II|2nd)[ ._]", 2}, + {@"[ ._](III|3rd)[ ._]", 3}, + {@"[ ._](IIII|4th)[ ._]", 3}, }; foreach (var entry in seasonNameMap) @@ -607,7 +623,47 @@ namespace Jellyfin.Plugin.MetaShark.Providers } } - protected string RemoveSeasonSubfix(string name) + protected string? GetOriginalSeasonPath(EpisodeInfo info) + { + if (info.Path == null) { + return null; + } + + var seasonPath = Path.GetDirectoryName(info.Path); + var item = this._libraryManager.FindByPath(seasonPath, true); + // 没有季文件夹 + if (item is Series) { + return null; + } + + return seasonPath; + } + + protected bool IsVirtualSeason(EpisodeInfo info) + { + if (info.Path == null) + { + return false; + } + + var seasonPath = Path.GetDirectoryName(info.Path); + var parent = this._libraryManager.FindByPath(seasonPath, true); + // 没有季文件夹 + if (parent is Series) { + return true; + } + + var seriesPath = Path.GetDirectoryName(seasonPath); + var series = this._libraryManager.FindByPath(seriesPath, true); + // 季文件夹不规范,没法识别 + if (series is Series && parent is not Season) { + return true; + } + + return false; + } + + protected string RemoveSeasonSuffix(string name) { return regSeasonNameSuffix.Replace(name, ""); } diff --git a/Jellyfin.Plugin.MetaShark/Providers/EpisodeProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/EpisodeProvider.cs index b22ad44..6008ba0 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/EpisodeProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/EpisodeProvider.cs @@ -46,7 +46,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers // 识别:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,provinceIds有指定选择项的ProvinceId // 覆盖所有元数据:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,provinceIds保留所有旧值 // 搜索缺少的元数据:info的Name、IndexNumber和ParentIndexNumber是从当前的元数据获取,provinceIds保留所有旧值 - this.Log($"GetEpisodeMetadata of [name]: {info.Name} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber} EnableTmdb: {config.EnableTmdb}"); + var fileName = Path.GetFileName(info.Path); + this.Log($"GetEpisodeMetadata of [name]: {info.Name} [fileName]: {fileName} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber} EnableTmdb: {config.EnableTmdb}"); var result = new MetadataResult(); // 动画特典和extras处理 @@ -129,69 +130,59 @@ namespace Jellyfin.Plugin.MetaShark.Providers /// /// 重新解析文件名 - /// 注意:这里修改替换ParentIndexNumber值后,会重新触发SeasonProvier的GetMetadata方法,并带上最新的季数IndexNumber + /// 注意:这里修改替换 ParentIndexNumber 值后,会重新触发 SeasonProvier 的 GetMetadata 方法,并带上最新的季数 IndexNumber /// public EpisodeInfo FixParseInfo(EpisodeInfo info) { - // 使用AnitomySharp进行重新解析,解决anime识别错误 + // 使用 AnitomySharp 进行重新解析,解决 anime 识别错误 var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name; var parseResult = NameParser.ParseEpisode(fileName); info.Year = parseResult.Year; info.Name = parseResult.ChineseName ?? parseResult.Name; - // 修正文件名有特殊命名SXXEPXX时,默认解析到错误季数的问题,如神探狄仁杰 Detective.Dee.S01EP01.2006.2160p.WEB-DL.x264.AAC-HQC + // 文件名带有季数数据时,从文件名解析出季数进行修正 + // 修正文件名有特殊命名 SXXEPXX 时,默认解析到错误季数的问题,如神探狄仁杰 Detective.Dee.S01EP01.2006.2160p.WEB-DL.x264.AAC-HQC // TODO: 会导致覆盖用户手动修改元数据的季数 - if (info.ParentIndexNumber.HasValue && parseResult.ParentIndexNumber.HasValue && parseResult.ParentIndexNumber > 0 && info.ParentIndexNumber != parseResult.ParentIndexNumber) + if (parseResult.ParentIndexNumber.HasValue && parseResult.ParentIndexNumber > 0 && info.ParentIndexNumber != parseResult.ParentIndexNumber) { this.Log("FixSeasonNumber by anitomy. old: {0} new: {1}", info.ParentIndexNumber, parseResult.ParentIndexNumber); info.ParentIndexNumber = parseResult.ParentIndexNumber; } - // 没有season级目录(即虚拟季)ParentIndexNumber默认是1,季文件夹命名不规范时,ParentIndexNumber默认是null - if (info.ParentIndexNumber is null) - { - info.ParentIndexNumber = parseResult.ParentIndexNumber; - } + // // 修正anime命名格式导致的seasonNumber错误(从season元数据读取) + // if (info.ParentIndexNumber is null) + // { + // var episodeItem = this._libraryManager.FindByPath(info.Path, false); + // var season = episodeItem != null ? ((Episode)episodeItem).Season : null; + // if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber) + // { + // info.ParentIndexNumber = season.IndexNumber; + // this.Log("FixSeasonNumber by season. old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber); + // } + // } - // 修正anime命名格式导致的seasonNumber错误(从season元数据读取) - if (info.ParentIndexNumber is null) + // 从季文件夹名称猜出 season number + // 没有 season 级目录或部分特殊不规范命名,会变成虚拟季,ParentIndexNumber 默认设为 1 + // https://github.com/jellyfin/jellyfin/blob/926470829d91d93b4c0b22c5b8b89a791abbb434/Emby.Server.Implementations/Library/LibraryManager.cs#L2626 + var isVirtualSeason = this.IsVirtualSeason(info); + var seasonFolderPath = this.GetOriginalSeasonPath(info); + if (info.ParentIndexNumber is null or 1 && isVirtualSeason && seasonFolderPath != null) { - var episodeItem = _libraryManager.FindByPath(info.Path, false); - var season = episodeItem != null ? ((Episode)episodeItem).Season : null; - if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber) + var guestSeasonNumber = this.GuessSeasonNumberByDirectoryName(seasonFolderPath); + if (guestSeasonNumber.HasValue && guestSeasonNumber != info.ParentIndexNumber) { - this.Log("FixSeasonNumber by season. old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber); - info.ParentIndexNumber = season.IndexNumber; + this.Log("FixSeasonNumber by season path. old: {0} new: {1}", info.ParentIndexNumber, guestSeasonNumber); + info.ParentIndexNumber = guestSeasonNumber; } - - // // 当没有season级目录时,默认为1,即当成只有一季(不需要处理,虚拟季jellyfin默认传的ParentIndexNumber=1) - // if (info.ParentIndexNumber is null && season != null && season.LocationType == LocationType.Virtual) - // { - // this.Log("FixSeasonNumber: season is virtual, set to default 1"); - // info.ParentIndexNumber = 1; - // } - } - - // 从季文件夹名称猜出season number - var seasonFolderPath = Path.GetDirectoryName(info.Path); - if (info.ParentIndexNumber is null && seasonFolderPath != null) - { - info.ParentIndexNumber = this.GuessSeasonNumberByDirectoryName(seasonFolderPath); } // 识别特典 if (info.ParentIndexNumber is null && NameParser.IsAnime(fileName) && (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path))) { + this.Log("FixSeasonNumber to special. old: {0} new: 0", info.ParentIndexNumber); info.ParentIndexNumber = 0; } - // // 设为默认季数为1(问题:当同时存在S01和剧场版季文件夹时,剧场版的影片会因为默认第一季而在S01也显示出来) - // if (info.ParentIndexNumber is null) - // { - // this.Log("FixSeasonNumber: season number is null, set to default 1"); - // info.ParentIndexNumber = 1; - // } - // 特典优先使用文件名(特典除了前面特别设置,还有 SXX/Season XX 等默认的) if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber == 0) { @@ -205,7 +196,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers info.IndexNumber = parseResult.IndexNumber; } - this.Log("FixParseInfo: fileName: {0} seasonNumber: {1} episodeNumber: {2} name: {3}", fileName, info.ParentIndexNumber, info.IndexNumber, info.Name); return info; } @@ -241,7 +231,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers return result; } - //// 特典也有剧集信息,不在这里处理 + //// 特典也有 tmdb 剧集信息,不在这里处理 // if (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path)) // { // this.Log($"Found anime sp of [name]: {fileName}"); diff --git a/Jellyfin.Plugin.MetaShark/Providers/SeasonProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/SeasonProvider.cs index b896e69..ffabef9 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/SeasonProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/SeasonProvider.cs @@ -14,6 +14,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using System.IO; namespace Jellyfin.Plugin.MetaShark.Providers { @@ -40,25 +41,27 @@ namespace Jellyfin.Plugin.MetaShark.Providers { var result = new MetadataResult(); - // 使用刷新元数据时,之前识别的seasonNumber会保留,不会被覆盖 + // 使用刷新元数据时,之前识别的 seasonNumber 会保留,不会被覆盖 info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId); info.SeriesProviderIds.TryGetMetaSource(Plugin.ProviderId, out var metaSource); info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid); var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0 var seasonSid = info.GetProviderId(DoubanProviderId); - var fileName = this.GetOriginalFileName(info); + var fileName = Path.GetFileName(info.Path); this.Log($"GetSeasonMetaData of [name]: {info.Name} [fileName]: {fileName} number: {info.IndexNumber} seriesTmdbId: {seriesTmdbId} sid: {sid} metaSource: {metaSource} EnableTmdb: {config.EnableTmdb}"); - if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid)) { - // 季文件夹名称不规范,没法拿到seasonNumber,尝试从文件夹名猜出 - // 注意:本办法没法处理没有季文件夹的/虚拟季,因为path会为空 + // seasonNumber 为 null 有三种情况: + // 1. 没有季文件夹时,即虚拟季,info.Path 为空 + // 2. 一般不规范文件夹命名,没法被 EpisodeResolver 解析的,info.Path 不为空,如:摇曳露营△ + // 3. 特殊不规范文件夹命名,能被 EpisodeResolver 错误解析,这时被当成了视频文件,相当于没有季文件夹,info.Path 为空,如:冰与火之歌 S02.列王的纷争.2012.1080p.Blu-ray.x265.10bit.AC3 + // 相关代码:https://github.com/jellyfin/jellyfin/blob/dc2eca9f2ca259b46c7b53f59251794903c730a4/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs#L70 if (seasonNumber is null) { seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path); } - // 搜索豆瓣季id + // 搜索豆瓣季 id if (string.IsNullOrEmpty(seasonSid)) { seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false); @@ -95,13 +98,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers ProviderIds = new Dictionary { { DoubanProviderId, c.Id } }, })); - this.Log($"GetSeasonMetaData of douban [sid]: {seasonSid}"); + this.Log($"Season [{info.Name}] found douban [sid]: {seasonSid}"); return result; } } else { - this.Log($"GetSeasonMetaData of [name]: {info.Name} not found douban season id!"); + this.Log($"Season [{info.Name}] not found douban season id!"); } @@ -139,10 +142,19 @@ namespace Jellyfin.Plugin.MetaShark.Providers return null; } - // 没有季文件夹(即虚拟季),info.Path会为空,直接用series的sid - if (string.IsNullOrEmpty(info.Path)) + // 没有季文件夹或季文件夹名不规范时(即虚拟季),info.Path 会为空,seasonNumber 为 null + if (string.IsNullOrEmpty(info.Path) && !seasonNumber.HasValue) { - return sid; + return null; + } + + // 从季文件夹名属性格式获取,如 [douban-12345] 或 [doubanid-12345] + var fileName = this.GetOriginalFileName(info); + var doubanId = this.regDoubanIdAttribute.FirstMatchGroup(fileName); + if (!string.IsNullOrWhiteSpace(doubanId)) + { + this.Log($"Found season douban [id] by attr: {doubanId}"); + return doubanId; } // 从sereis获取正确名称,info.Name当是标准格式如S01等时,会变成第x季,非标准名称默认文件名 @@ -151,7 +163,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers { return null; } - var seriesName = RemoveSeasonSubfix(series.Name); + var seriesName = this.RemoveSeasonSuffix(series.Name); // 没有季id,但存在tmdbid,尝试从tmdb获取对应季的年份信息,用于从豆瓣搜索对应季数据 var seasonYear = 0; diff --git a/Jellyfin.Plugin.MetaShark/Providers/SeriesProvider.cs b/Jellyfin.Plugin.MetaShark/Providers/SeriesProvider.cs index 6dcd7b6..3b70820 100644 --- a/Jellyfin.Plugin.MetaShark/Providers/SeriesProvider.cs +++ b/Jellyfin.Plugin.MetaShark/Providers/SeriesProvider.cs @@ -104,12 +104,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers } subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false); - var seriesName = RemoveSeasonSubfix(subject.Name); + var seriesName = RemoveSeasonSuffix(subject.Name); var item = new Series { ProviderIds = new Dictionary { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } }, Name = seriesName, - OriginalTitle = RemoveSeasonSubfix(subject.OriginalName), + OriginalTitle = RemoveSeasonSuffix(subject.OriginalName), CommunityRating = subject.Rating, Overview = subject.Intro, ProductionYear = subject.Year,