Compare commits

...

2 Commits
v2.0.2 ... main

Author SHA1 Message Date
cwhzy 2f44ddf6ec
update README.md.
Signed-off-by: cwhzy <cwhzy@foxmail.com>
2024-07-01 04:51:16 +00:00
cxfksword f337406ac9 tweak: optimize identify 2024-06-07 21:45:01 +08:00
7 changed files with 126 additions and 67 deletions

View File

@ -64,7 +64,11 @@ namespace Jellyfin.Plugin.MetaShark.Test
var imdbApi = new ImdbApi(loggerFactory); var imdbApi = new ImdbApi(loggerFactory);
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi); 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); Assert.AreEqual(result, 2);
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第2季"); result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第2季");

View File

@ -246,15 +246,21 @@ namespace Jellyfin.Plugin.MetaShark.Core
return null; 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; 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; var folder = Path.GetFileName(Path.GetDirectoryName(path))?.ToUpper() ?? string.Empty;
if (isDirectory) {
folder = Path.GetFileName(path)?.ToUpper() ?? string.Empty;
}
return folder == "EXTRA" return folder == "EXTRA"
|| folder == "MENU" || folder == "MENU"
|| folder == "MENUS" || folder == "MENUS"

View File

@ -19,6 +19,7 @@ using TMDbLib.Objects.General;
using Jellyfin.Plugin.MetaShark.Configuration; using Jellyfin.Plugin.MetaShark.Configuration;
using Jellyfin.Plugin.MetaShark.Core; using Jellyfin.Plugin.MetaShark.Core;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using MediaBrowser.Controller.Entities.TV;
namespace Jellyfin.Plugin.MetaShark.Providers namespace Jellyfin.Plugin.MetaShark.Providers
{ {
@ -401,8 +402,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public int? GuessSeasonNumberByDirectoryName(string path) public int? GuessSeasonNumberByDirectoryName(string path)
{ {
// TODO: 有时series name中会带有季信息 // TODO: 有时 series name 中会带有季信息
// 当没有season级目录时path为空直接返回 // 当没有 season 级目录时,或 season 文件夹特殊不规范命名时,会解析不到 seasonNumber这时 path 为空,直接返回
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
this.Log($"Season path is empty!"); this.Log($"Season path is empty!");
@ -416,6 +417,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return null; return null;
} }
// 中文季名
var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled); var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled);
var match = regSeason.Match(fileName); var match = regSeason.Match(fileName);
if (match.Success && match.Groups.Count > 1) if (match.Success && match.Groups.Count > 1)
@ -432,12 +434,26 @@ namespace Jellyfin.Plugin.MetaShark.Providers
} }
} }
// SXX 季名
regSeason = new Regex(@"(?<![a-z])S(\d\d?)(?![0-9a-z])", RegexOptions.Compiled | RegexOptions.IgnoreCase);
match = regSeason.Match(fileName);
if (match.Success && match.Groups.Count > 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<string, int>() { var seasonNameMap = new Dictionary<string, int>() {
{@"[ ._](I|1st|S01|S1)[ ._]", 1}, {@"[ ._](I|1st)[ ._]", 1},
{@"[ ._](II|2nd|S02|S2)[ ._]", 2}, {@"[ ._](II|2nd)[ ._]", 2},
{@"[ ._](III|3rd|S03|S3)[ ._]", 3}, {@"[ ._](III|3rd)[ ._]", 3},
{@"[ ._](IIII|4th|S04|S4)[ ._]", 3}, {@"[ ._](IIII|4th)[ ._]", 3},
}; };
foreach (var entry in seasonNameMap) 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, ""); return regSeasonNameSuffix.Replace(name, "");
} }

View File

@ -46,7 +46,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
// 识别info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的provinceIds有指定选择项的ProvinceId // 识别info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的provinceIds有指定选择项的ProvinceId
// 覆盖所有元数据info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的provinceIds保留所有旧值 // 覆盖所有元数据info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的provinceIds保留所有旧值
// 搜索缺少的元数据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<Episode>(); var result = new MetadataResult<Episode>();
// 动画特典和extras处理 // 动画特典和extras处理
@ -129,69 +130,59 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <summary> /// <summary>
/// 重新解析文件名 /// 重新解析文件名
/// 注意:这里修改替换ParentIndexNumber值后会重新触发SeasonProvier的GetMetadata方法并带上最新的季数IndexNumber /// 注意:这里修改替换 ParentIndexNumber 值后,会重新触发 SeasonProvier GetMetadata 方法,并带上最新的季数 IndexNumber
/// </summary> /// </summary>
public EpisodeInfo FixParseInfo(EpisodeInfo info) public EpisodeInfo FixParseInfo(EpisodeInfo info)
{ {
// 使用AnitomySharp进行重新解析解决anime识别错误 // 使用 AnitomySharp 进行重新解析,解决 anime 识别错误
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name; var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
var parseResult = NameParser.ParseEpisode(fileName); var parseResult = NameParser.ParseEpisode(fileName);
info.Year = parseResult.Year; info.Year = parseResult.Year;
info.Name = parseResult.ChineseName ?? parseResult.Name; 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: 会导致覆盖用户手动修改元数据的季数 // 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); this.Log("FixSeasonNumber by anitomy. old: {0} new: {1}", info.ParentIndexNumber, parseResult.ParentIndexNumber);
info.ParentIndexNumber = parseResult.ParentIndexNumber; info.ParentIndexNumber = parseResult.ParentIndexNumber;
} }
// 没有season级目录(即虚拟季)ParentIndexNumber默认是1季文件夹命名不规范时ParentIndexNumber默认是null // // 修正anime命名格式导致的seasonNumber错误从season元数据读取)
if (info.ParentIndexNumber is null) // if (info.ParentIndexNumber is null)
{ // {
info.ParentIndexNumber = parseResult.ParentIndexNumber; // 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元数据读取) // 从季文件夹名称猜出 season number
if (info.ParentIndexNumber is null) // 没有 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 guestSeasonNumber = this.GuessSeasonNumberByDirectoryName(seasonFolderPath);
var season = episodeItem != null ? ((Episode)episodeItem).Season : null; if (guestSeasonNumber.HasValue && guestSeasonNumber != info.ParentIndexNumber)
if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber)
{ {
this.Log("FixSeasonNumber by season. old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber); this.Log("FixSeasonNumber by season path. old: {0} new: {1}", info.ParentIndexNumber, guestSeasonNumber);
info.ParentIndexNumber = season.IndexNumber; 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))) 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; 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 等默认的) // 特典优先使用文件名(特典除了前面特别设置,还有 SXX/Season XX 等默认的)
if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber == 0) if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber == 0)
{ {
@ -205,7 +196,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers
info.IndexNumber = parseResult.IndexNumber; info.IndexNumber = parseResult.IndexNumber;
} }
this.Log("FixParseInfo: fileName: {0} seasonNumber: {1} episodeNumber: {2} name: {3}", fileName, info.ParentIndexNumber, info.IndexNumber, info.Name);
return info; return info;
} }
@ -241,7 +231,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return result; return result;
} }
//// 特典也有剧集信息,不在这里处理 //// 特典也有 tmdb 剧集信息,不在这里处理
// if (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path)) // if (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path))
// { // {
// this.Log($"Found anime sp of [name]: {fileName}"); // this.Log($"Found anime sp of [name]: {fileName}");

View File

@ -15,6 +15,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using System.IO;
namespace Jellyfin.Plugin.MetaShark.Providers namespace Jellyfin.Plugin.MetaShark.Providers
{ {
@ -41,25 +42,27 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
var result = new MetadataResult<Season>(); var result = new MetadataResult<Season>();
// 使用刷新元数据时,之前识别的seasonNumber会保留不会被覆盖 // 使用刷新元数据时,之前识别的 seasonNumber 会保留,不会被覆盖
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId); info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
info.SeriesProviderIds.TryGetMetaSource(Plugin.ProviderId, out var metaSource); info.SeriesProviderIds.TryGetMetaSource(Plugin.ProviderId, out var metaSource);
info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid); info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid);
var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0 var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0
var seasonSid = info.GetProviderId(DoubanProviderId); 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}"); 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)) if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{ {
// 季文件夹名称不规范没法拿到seasonNumber尝试从文件夹名猜出 // seasonNumber 为 null 有三种情况:
// 注意:本办法没法处理没有季文件夹的/虚拟季因为path会为空 // 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) if (seasonNumber is null)
{ {
seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path); seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path);
} }
// 搜索豆瓣季id // 搜索豆瓣季 id
if (string.IsNullOrEmpty(seasonSid)) if (string.IsNullOrEmpty(seasonSid))
{ {
seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false); seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false);
@ -96,13 +99,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } }, ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
})); }));
this.Log($"GetSeasonMetaData of douban [sid]: {seasonSid}"); this.Log($"Season [{info.Name}] found douban [sid]: {seasonSid}");
return result; return result;
} }
} }
else else
{ {
this.Log($"GetSeasonMetaData of [name]: {info.Name} not found douban season id!"); this.Log($"Season [{info.Name}] not found douban season id!");
} }
@ -140,13 +143,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return null; return null;
} }
// 没有季文件夹或季文件夹名不规范即虚拟季info.Path会为空直接用series的sid // 没有季文件夹或季文件夹名不规范即虚拟季info.Path 会为空seasonNumber 为 null
if (string.IsNullOrEmpty(info.Path)) if (string.IsNullOrEmpty(info.Path) && !seasonNumber.HasValue)
{ {
return sid; return null;
} }
// 从文件夹名属性格式获取,如[douban-12345]或[doubanid-12345] // 从文件夹名属性格式获取,如 [douban-12345] [doubanid-12345]
var fileName = this.GetOriginalFileName(info); var fileName = this.GetOriginalFileName(info);
var doubanId = this.regDoubanIdAttribute.FirstMatchGroup(fileName); var doubanId = this.regDoubanIdAttribute.FirstMatchGroup(fileName);
if (!string.IsNullOrWhiteSpace(doubanId)) if (!string.IsNullOrWhiteSpace(doubanId))
@ -161,7 +164,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
return null; return null;
} }
var seriesName = RemoveSeasonSubfix(series.Name); var seriesName = this.RemoveSeasonSuffix(series.Name);
// 没有季id但存在tmdbid尝试从tmdb获取对应季的年份信息用于从豆瓣搜索对应季数据 // 没有季id但存在tmdbid尝试从tmdb获取对应季的年份信息用于从豆瓣搜索对应季数据
var seasonYear = 0; var seasonYear = 0;

View File

@ -105,12 +105,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers
} }
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false); subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false);
var seriesName = RemoveSeasonSubfix(subject.Name); var seriesName = RemoveSeasonSuffix(subject.Name);
var item = new Series var item = new Series
{ {
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } }, ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } },
Name = seriesName, Name = seriesName,
OriginalTitle = RemoveSeasonSubfix(subject.OriginalName), OriginalTitle = RemoveSeasonSuffix(subject.OriginalName),
CommunityRating = subject.Rating, CommunityRating = subject.Rating,
Overview = subject.Intro, Overview = subject.Intro,
ProductionYear = subject.Year, ProductionYear = subject.Year,

View File

@ -16,7 +16,7 @@ jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMov
添加插件存储库: 添加插件存储库:
国内加速https://mirror.ghproxy.com/https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest_cn.json 国内加速https://gitee.com/cwhzy/jellyfin-plugin-metashark/releases/download/manifest/manifest_cn.json
国外访问https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json 国外访问https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json