Compare commits

..

8 Commits
v2.0.0 ... 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
cxfksword cb00027b77 fix: episode number not correctly identified. #82 2024-05-25 16:00:19 +08:00
cxfksword 9e9074bb5a build: update github action 2024-05-22 20:38:52 +08:00
cxfksword 747e69f3bf tweak: read attribute from season folder name 2024-05-18 16:12:52 +08:00
cxfksword 104d200e7b fix: actor lack of overview. close #80 2024-05-18 16:04:05 +08:00
cxfksword 8ded89422f
Merge pull request #78 from cxfksword/10.9/preview
tweak: update generate manifest version
2024-05-12 18:04:12 +08:00
cxfksword f1f35a6d32
Merge pull request #77 from cxfksword/10.9/preview
Compatible with Jellyfin 10.9
2024-05-12 18:00:02 +08:00
16 changed files with 318 additions and 147 deletions

View File

@ -54,6 +54,7 @@ jobs:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./artifacts/${{ env.artifact }}_*.zip
tag: ${{ github.ref }}
release_name: '${{ github.ref_name }}: Jellyfin v10.9'
file_glob: true
overwrite: true
- name: Publish manifest

View File

@ -67,7 +67,7 @@ namespace AnitomySharp
"番外編", "總集編","DRAMA",
"映像特典","特典","特典アニメ",
// 特典 Special 剩下的各种类型可以全部命名成 SP对于较特殊意义的特典也可以自定义命名
"SPECIAL", "SPECIALS", "SP", "SPs",
"SPECIAL", "SPECIALS", "SP", "SPs", "特報",
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
"IV",
// 音乐视频 Music Video
@ -85,7 +85,7 @@ namespace AnitomySharp
// 无字 OP/ED Non-Credit Opening/Ending
"ED", "ENDING", "NCED", "NCOP", "OP", "OPENING",
// 预告 Preview 预告下一话内容 注意编号表示其预告的是第几话的内容而不是跟在哪一话后面
"PREVIEW", "YOKOKU",
"PREVIEW", "YOKOKU", "予告",
// 菜单 Menu BD/DVD 播放选择菜单
"MENU",
// 广告 Commercial Message 电视放送广告,时长一般在 7s/15s/30s/45s/... 左右

View File

@ -119,6 +119,29 @@ namespace AnitomySharp
if (string.IsNullOrEmpty(str)) return "";
return Ordinals.TryGetValue(str, out var foundString) ? foundString : "";
}
/// <summary>
/// 转换原始值中的全角数字
///
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string GetNumberFromFullWidth(string str)
{
string output = str;
for (int i = 0; i < str.Length; i++)
{
if (char.IsDigit(str[i]))
{
int fullwidthDigit = (int)str[i];
if (fullwidthDigit >= 65296 && fullwidthDigit <= 65305)
{
int halfwidthDigit = fullwidthDigit - 65248;
output = output.Replace(str[i], (char)halfwidthDigit);
}
}
}
return output;
}
/// <summary>
/// Returns the index of the first digit in the <c>str</c>; -1 otherwise.
@ -273,7 +296,7 @@ namespace AnitomySharp
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if (!IsTokenCategory(prevToken, Token.TokenCategory.Bracket)) return false;
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if (nextToken < 0) return false;
if (!Token.InListRange(prevToken, _parser.Tokens) || !Token.InListRange(nextToken, _parser.Tokens)) return false;
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[nextToken].Content);
}
/// <summary>
@ -285,8 +308,8 @@ namespace AnitomySharp
{
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if(!Token.InListRange(prevToken, _parser.Tokens)||!Token.InListRange(nextToken, _parser.Tokens)) return false;
if (!IsTokenCategory(nextToken, Token.TokenCategory.Bracket)) return false;
if (prevToken < 0) return false;
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[prevToken].Content);
}
/// <summary>
@ -298,8 +321,8 @@ namespace AnitomySharp
{
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if (!Token.InListRange(prevToken, _parser.Tokens) || !Token.InListRange(nextToken, _parser.Tokens)) return false;
if (!IsTokenCategory(nextToken, Token.TokenCategory.Bracket)) return false;
if (prevToken < 0) return false;
return KeywordManager.ContainsInPeekEntries(Element.ElementCategory.ElementAnimeType, _parser.Tokens[prevToken].Content);
}
@ -435,4 +458,4 @@ namespace AnitomySharp
}
}
}
}
}

View File

@ -513,7 +513,8 @@ namespace AnitomySharp
return true;
}
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参]+)([期章話话巻卷幕夜期発縛])";
// 全角数字:\uFF10-\uFF19
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([回集話话幕夜発縛])";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success)
{
@ -522,11 +523,33 @@ namespace AnitomySharp
{
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
}
episodeNumber = ParserHelper.GetNumberFromFullWidth(episodeNumber);
SetEpisodeNumber(episodeNumber, token, false);
return true;
}
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([期章巻卷])";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success)
{
var episodeNumber = match.Groups[2].Value;
if (!StringHelper.IsNumericString(episodeNumber))
{
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
}
episodeNumber = ParserHelper.GetNumberFromFullWidth(episodeNumber);
SetEpisodeNumber(episodeNumber, token, false);
return true;
}
regexPattern = @"(vol|EPISODE|ACT|scene|ep|volume|screen|voice|case|menu|rail|round|game|page|collection|cage|office|doll|Princess)([ \.\-_])([0-9]+)";
regexPattern = @"(EPISODE|ACT|scene|ep|screen|voice|case|menu|rail|round|game|page|collection|cage|office|doll|Princess)([ \.\-_])([0-9]+)";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success)
{
var episodeNumber = match.Groups[3].Value;
SetEpisodeNumber(episodeNumber, token, false);
return true;
}
regexPattern = @"(vol|volume)([ \.\-_])([0-9]+)";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success)
{
@ -874,4 +897,4 @@ namespace AnitomySharp
return false;
}
}
}
}

View File

@ -167,23 +167,36 @@ namespace Jellyfin.Plugin.MetaShark.Test
[TestMethod]
public void TestEposideParse()
{
// 普通数字
var fileName = "03.mp4";
var parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "03");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 3);
fileName = "03 4K.mp4";
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "03");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 3);
// 混合中英文
var fileName = "新世界.New.World.2013.BluRay.1080p.x265.10bit.MNHD-FRDS";
var parseResult = NameParser.Parse(fileName);
fileName = "新世界.New.World.2013.BluRay.1080p.x265.10bit.MNHD-FRDS";
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.ChineseName, "新世界");
Assert.AreEqual(parseResult.Name, "New World");
Assert.AreEqual(parseResult.Year, 2013);
// 只英文 S01E01
fileName = "She-Hulk.Attorney.At.Law.S01E01.1080p.WEBRip.x265-RARBG";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "She-Hulk Attorney At Law");
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
Assert.AreEqual(parseResult.IndexNumber, 1);
// 测试 SXXEPXX 格式
fileName = "神探狄仁杰2 Detective.Dee.Ⅱ.S02EP02.2006.2160p.WEB-DL.x264.AAC-HQC";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.ChineseName, "神探狄仁杰2");
Assert.AreEqual(parseResult.Name, "Detective Dee Ⅱ");
Assert.AreEqual(parseResult.ParentIndexNumber, 2);
@ -192,26 +205,26 @@ namespace Jellyfin.Plugin.MetaShark.Test
// 日文
fileName = "プロポーズ大作戦Ep05_x264.mp4";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "プロポーズ大作戦Ep05");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 5);
fileName = "[01] [ANK-Raws] あっちこっち 01 (BDrip 1920x1080 HEVC-YUV420P10 FLAC)";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "あっちこっち 01");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 1);
// 只中文
fileName = "齊天大聖 第02集";
parseResult = NameParser.Parse(fileName);
Assert.AreEqual(parseResult.Name, "齊天大聖 第02集");
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "齊天大聖");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 2);
fileName = "齊天大聖 第 02 期";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "齊天大聖");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 2);
@ -219,38 +232,40 @@ namespace Jellyfin.Plugin.MetaShark.Test
// anime
fileName = "[YYDM-11FANS][THERMAE_ROMAE][02][BDRIP][720P][X264-10bit_AAC][7FF2269F]";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "THERMAE ROMAE");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 2);
// anime带季数
fileName = "[WMSUB][Detective Conan - Zeros Tea Time ][S01][E06][BIG5][1080P].mp4";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "Detective Conan - Zeros Tea Time");
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
Assert.AreEqual(parseResult.IndexNumber, 6);
fileName = "[KTXP][Machikado_Mazoku_S2][01][BIG5][1080p]";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "Machikado Mazoku");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 1);
fileName = "[異域字幕組][她和她的貓 - Everything Flows -][She and Her Cat - Everything Flows -][01][720p][繁體]";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "她和她的貓 - Everything Flows");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 1);
// anime特典
fileName = "[KissSub][Steins;Gate][SP][GB_BIG5_JP][BDrip][1080P][HEVC] 边界曲面的缺失之环";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.IsTrue(parseResult.IsSpecial);
Assert.AreEqual(parseResult.Name, "边界曲面的缺失之环");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, null);
}

View File

@ -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季");

View File

@ -52,18 +52,10 @@ namespace Jellyfin.Plugin.MetaShark.Api
Regex regSubname = new Regex(@"又名: (.+?)\n", RegexOptions.Compiled);
Regex regImdb = new Regex(@"IMDb: (tt\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
Regex regSite = new Regex(@"官方网站: (.+?)\n", RegexOptions.Compiled);
Regex regNameMath = new Regex(@"(.+第\w季|[\w\uff1a\uff01\uff0c\u00b7]+)\s*(.*)", RegexOptions.Compiled);
Regex regRole = new Regex(@"\([饰|配]?\s*?(.+?)\)", RegexOptions.Compiled);
Regex regBackgroundImage = new Regex(@"url\(([^)]+?)\)$", RegexOptions.Compiled);
Regex regGender = new Regex(@"性别: \n(.+?)\n", RegexOptions.Compiled);
Regex regConstellation = new Regex(@"星座: \n(.+?)\n", RegexOptions.Compiled);
Regex regBirthdate = new Regex(@"出生日期: \n(.+?)\n", RegexOptions.Compiled);
Regex regLifedate = new Regex(@"生卒日期: \n(.+?) 至 (.+)", RegexOptions.Compiled);
Regex regBirthplace = new Regex(@"出生地: \n(.+?)\n", RegexOptions.Compiled);
Regex regCelebrityRole = new Regex(@"职业: \n(.+?)\n", RegexOptions.Compiled);
Regex regNickname = new Regex(@"更多外文名: \n(.+?)\n", RegexOptions.Compiled);
Regex regFamily = new Regex(@"家庭成员: \n(.+?)\n", RegexOptions.Compiled);
Regex regCelebrityImdb = new Regex(@"imdb编号:\s+?(nm\d+)", RegexOptions.Compiled);
Regex regLifedate = new Regex(@"(.+?) 至 (.+)", RegexOptions.Compiled);
Regex regHtmlTag = new Regex(@"<.?>", RegexOptions.Compiled);
Regex regImgHost = new Regex(@"\/\/(img\d+?)\.", RegexOptions.Compiled);
// 匹配除了换行符之外所有空白
Regex regOverviewSpace = new Regex(@"\n[^\S\n]+", RegexOptions.Compiled);
@ -90,7 +82,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
var handler = new HttpClientHandlerEx();
this._cookieContainer = handler.CookieContainer;
httpClient = new HttpClient(handler);
httpClient.Timeout = TimeSpan.FromSeconds(10);
httpClient.Timeout = TimeSpan.FromSeconds(20);
httpClient.DefaultRequestHeaders.Add("User-Agent", HTTP_USER_AGENT);
httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com");
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
@ -519,48 +511,64 @@ namespace Jellyfin.Plugin.MetaShark.Api
var contentNode = doc.QuerySelector("#content");
if (contentNode != null)
{
var img = contentNode.GetAttr("#headline .nbg img", "src") ?? string.Empty;
var nameStr = contentNode.GetText("h1") ?? string.Empty;
var name = this.ParseCelebrityName(nameStr);
var englishName = nameStr.Replace(name, "").Trim();
celebrity.Img = contentNode.GetAttr("img.avatar", "src") ?? string.Empty;
var nameStr = contentNode.GetText("h1.subject-name") ?? string.Empty;
celebrity.Name = this.ParseCelebrityName(nameStr);
celebrity.EnglishName = nameStr.Replace(celebrity.Name, "").Trim();
var intro = contentNode.GetText("#intro span.all") ?? string.Empty;
if (string.IsNullOrEmpty(intro))
var family = string.Empty;
var propertyNodes = contentNode.QuerySelectorAll("ul.subject-property>li");
foreach (var li in propertyNodes)
{
intro = contentNode.GetText("#intro div.bd") ?? string.Empty;
}
var info = contentNode.GetText("div.info") ?? string.Empty;
var gender = info.GetMatchGroup(this.regGender);
var constellation = info.GetMatchGroup(this.regConstellation);
var birthdate = info.GetMatchGroup(this.regBirthdate);
// 生卒日期
var enddate = string.Empty;
var match = this.regLifedate.Match(info);
if (match.Success && match.Groups.Count > 2)
{
birthdate = match.Groups[1].Value.Trim();
enddate = match.Groups[2].Value.Trim();
var label = li.GetText("span.label") ?? string.Empty;
var value = li.GetText("span.value") ?? string.Empty;
switch (label)
{
case "性别:":
celebrity.Gender = value;
break;
case "星座:":
celebrity.Constellation = value;
break;
case "出生日期:":
celebrity.Birthdate = value;
break;
case "去世日期:":
celebrity.Enddate = value;
break;
case "生卒日期:":
var match = this.regLifedate.Match(value);
if (match.Success && match.Groups.Count > 2)
{
celebrity.Birthdate = match.Groups[1].Value.Trim();
celebrity.Enddate = match.Groups[2].Value.Trim();
}
break;
case "出生地:":
celebrity.Birthplace = value;
break;
case "职业:":
celebrity.Role = value;
break;
case "更多外文名:":
celebrity.NickName = value;
break;
case "家庭成员:":
family = value;
break;
case "IMDb编号:":
celebrity.Imdb = value;
break;
default:
break;
}
}
var birthplace = info.GetMatchGroup(this.regBirthplace);
var role = info.GetMatchGroup(this.regCelebrityRole);
var nickname = info.GetMatchGroup(this.regNickname);
var family = info.GetMatchGroup(this.regFamily);
var imdb = info.GetMatchGroup(this.regCelebrityImdb);
celebrity.Img = img;
celebrity.Gender = gender;
celebrity.Birthdate = birthdate;
celebrity.Enddate = enddate;
celebrity.NickName = nickname;
celebrity.EnglishName = englishName;
celebrity.Imdb = imdb;
celebrity.Birthplace = birthplace;
celebrity.Name = name;
// 保留段落关系,把段落替换为换行符
var intro = contentNode.GetHtml("section.subject-intro div.content") ?? string.Empty;
intro = regHtmlTag.Replace(intro.Replace("</p>", "\n"), "");
celebrity.Intro = formatOverview(intro);
celebrity.Constellation = constellation;
celebrity.Role = role;
_memoryCache.Set<DoubanCelebrity?>(cacheKey, celebrity, expiredOption);
return celebrity;
}

View File

@ -20,6 +20,17 @@ namespace Jellyfin.Plugin.MetaShark.Core
return null;
}
public static string? GetHtml(this IElement el, string css)
{
var node = el.QuerySelector(css);
if (node != null)
{
return node.Html().Trim();
}
return null;
}
public static string GetTextOrDefault(this IElement el, string css, string defaultVal = "")
{
var node = el.QuerySelector(css);

View File

@ -108,7 +108,7 @@ namespace Jellyfin.Plugin.MetaShark.Core
}
}
// 假如Anitomy解析不到year尝试使用jellyfin默认parser看能不能解析成功
// 假如 Anitomy 解析不到 year尝试使用 jellyfin 默认 parser看能不能解析成功
if (parseResult.Year == null && !isAnime)
{
var nativeParseResult = ParseMovieByDefault(fileName);
@ -118,13 +118,22 @@ namespace Jellyfin.Plugin.MetaShark.Core
}
}
// 假如 Anitomy 解析不到集数,判断 name 是否是数字集号
if (parseResult.IndexNumber is null && isEpisode)
{
if (!string.IsNullOrEmpty(parseResult.Name) && parseResult.Name.IsNumericString())
{
parseResult.IndexNumber = parseResult.Name.ToInt();
}
}
// 修复纯中文集数/特殊标识集数
if (parseResult.IndexNumber is null)
{
parseResult.IndexNumber = ParseChineseOrSpecialIndexNumber(fileName);
}
// 解析不到title时或解析出多个title时使用默认名
// 解析不到 title 时,或解析出多个 title 时,使用默认名
if (string.IsNullOrEmpty(parseResult.Name))
{
parseResult.Name = fileName;
@ -133,6 +142,11 @@ namespace Jellyfin.Plugin.MetaShark.Core
return parseResult;
}
public static ParseNameResult ParseEpisode(string fileName)
{
return Parse(fileName, true);
}
private static string CleanName(string name)
{
// 电视剧名称后紧跟季信息时,会附加到名称中,需要去掉
@ -173,9 +187,11 @@ namespace Jellyfin.Plugin.MetaShark.Core
/// </summary>
public static EpisodePathParserResult ParseEpisodeByDefault(string fileName)
{
// EpisodePathParser需要路径信息 这里添加一个分隔符模拟路径
var path = Path.DirectorySeparatorChar + fileName;
var nameOptions = new Emby.Naming.Common.NamingOptions();
return new EpisodePathParser(nameOptions)
.Parse(fileName, false);
.Parse(path, false);
}
@ -230,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"

View File

@ -78,5 +78,10 @@ namespace Jellyfin.Plugin.MetaShark.Core
return string.Empty;
}
public static bool IsNumericString(this string str)
{
return str.All(char.IsDigit);
}
}
}

View File

@ -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(@"(?<![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>() {
{@"[ ._](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, "");
}

View File

@ -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<Episode>();
// 动画特典和extras处理
@ -129,83 +130,72 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <summary>
/// 重新解析文件名
/// 注意:这里修改替换ParentIndexNumber值后会重新触发SeasonProvier的GetMetadata方法并带上最新的季数IndexNumber
/// 注意:这里修改替换 ParentIndexNumber 值后,会重新触发 SeasonProvier GetMetadata 方法,并带上最新的季数 IndexNumber
/// </summary>
public EpisodeInfo FixParseInfo(EpisodeInfo info)
{
// 使用AnitomySharp进行重新解析解决anime识别错误
// 使用 AnitomySharp 进行重新解析,解决 anime 识别错误
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
var parseResult = NameParser.Parse(fileName);
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等默认的
// 特典优先使用文件名(特典除了前面特别设置,还有 SXX/Season XX 等默认的)
if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber == 0)
{
info.Name = parseResult.SpecialName == info.Name ? fileName : parseResult.SpecialName;
}
// 大于1000可能错误解析了分辨率
if (parseResult.IndexNumber.HasValue && parseResult.IndexNumber < 1000 && info.IndexNumber != parseResult.IndexNumber)
// 修正 episode number
if (parseResult.IndexNumber.HasValue && info.IndexNumber != parseResult.IndexNumber)
{
this.Log("FixEpisodeNumber by anitomy. old: {0} new: {1}", 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;
}
@ -214,7 +204,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
// 特典或extra视频可能和正片剧集放在同一目录
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
var parseResult = NameParser.Parse(fileName);
var parseResult = NameParser.ParseEpisode(fileName);
if (parseResult.IsExtra)
{
this.Log($"Found anime extra of [name]: {fileName}");
@ -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}");

View File

@ -120,8 +120,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var findResult = await this._tmdbApi.FindByExternalIdAsync(c.Imdb, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (findResult?.PersonResults != null && findResult.PersonResults.Count > 0)
{
this.Log($"GetPersonMetadata of found tmdb [id]: {findResult.PersonResults[0].Id}");
item.SetProviderId(MetadataProvider.Tmdb, $"{findResult.PersonResults[0].Id}");
var foundTmdbId = findResult.PersonResults.First().Id.ToString();
this.Log($"GetPersonMetadata of found tmdb [id]: {foundTmdbId}");
item.SetProviderId(MetadataProvider.Tmdb, $"{foundTmdbId}");
}
}

View File

@ -15,6 +15,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Jellyfin.Data.Enums;
using System.IO;
namespace Jellyfin.Plugin.MetaShark.Providers
{
@ -41,25 +42,27 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
var result = new MetadataResult<Season>();
// 使用刷新元数据时,之前识别的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);
@ -96,13 +99,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
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;
}
}
else
{
this.Log($"GetSeasonMetaData of [name]: {info.Name} not found douban season id!");
this.Log($"Season [{info.Name}] not found douban season id!");
}
@ -140,10 +143,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季非标准名称默认文件名
@ -152,7 +164,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;

View File

@ -105,12 +105,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<string, string> { { 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,

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