using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Jellyfin.Plugin.MetaShark.Model; namespace Jellyfin.Plugin.MetaShark.Core { public static class NameParser { private static readonly Regex yearReg = new Regex(@"[12][890][0-9][0-9]", RegexOptions.Compiled); private static readonly Regex seasonSuffixReg = new Regex(@"[ .]S\d{1,2}$", RegexOptions.Compiled); private static readonly Regex unusedReg = new Regex(@"\[.+?\]|\(.+?\)|【.+?】", RegexOptions.Compiled); private static readonly Regex fixSeasonNumberReg = new Regex(@"(\[|\.)S(\d{1,2})(\]|\.)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex startWithHyphenCharReg = new Regex(@"^[-~~]", RegexOptions.Compiled); private static readonly Regex chineseIndexNumberReg = new Regex(@"第([0-9零一二三四五六七八九]+?)(集|章|话|話)", RegexOptions.Compiled); public static ParseNameResult Parse(string fileName, bool isTvSeries = false) { var parseResult = new ParseNameResult(); var anitomyResult = AnitomySharp.AnitomySharp.Parse(fileName); var isAnime = IsAnime(fileName); foreach (var item in anitomyResult) { switch (item.Category) { case AnitomySharp.Element.ElementCategory.ElementAnimeTitle: // 处理混合中英文的标题,中文一般在最前面,如V字仇杀队.V.for.Vendetta char[] delimiters = { ' ', '.' }; var firstSpaceIndex = item.Value.IndexOfAny(delimiters); if (firstSpaceIndex > 0) { var firstString = item.Value.Substring(0, firstSpaceIndex); var lastString = item.Value.Substring(firstSpaceIndex + 1); if (firstString.HasChinese() && !lastString.HasChinese() && !startWithHyphenCharReg.IsMatch(lastString)) { parseResult.ChineseName = CleanName(firstString); parseResult.Name = CleanName(lastString); } else { parseResult.Name = CleanName(item.Value); } } else { parseResult.Name = CleanName(item.Value); } break; case AnitomySharp.Element.ElementCategory.ElementEpisodeTitle: parseResult.EpisodeName = item.Value; break; case AnitomySharp.Element.ElementCategory.ElementAnimeSeason: var seasonNumber = item.Value.ToInt(); if (seasonNumber > 0) { parseResult.ParentIndexNumber = seasonNumber; } break; case AnitomySharp.Element.ElementCategory.ElementEpisodeNumber: var year = ParseYear(item.Value); if (year > 0) { parseResult.Year = year; } else { var episodeNumber = item.Value.ToInt(); if (episodeNumber > 0) { parseResult.IndexNumber = episodeNumber; } } break; case AnitomySharp.Element.ElementCategory.ElementAnimeType: parseResult.AnimeType = item.Value; break; case AnitomySharp.Element.ElementCategory.ElementAnimeYear: parseResult.Year = item.Value.ToInt(); break; default: break; } } // 修正动画季信息特殊情况,格式:[SXX] if (!parseResult.ParentIndexNumber.HasValue && isAnime) { var match = fixSeasonNumberReg.Match(fileName); if (match.Success && match.Groups.Count > 2) { parseResult.ParentIndexNumber = match.Groups[2].Value.ToInt(); } } // 假如Anitomy解析不到year,尝试使用jellyfin默认parser,看能不能解析成功 if (parseResult.Year == null && !isAnime) { var nativeParseResult = ParseMovie(fileName); if (nativeParseResult.Year != null) { parseResult = nativeParseResult; } } // 修复纯中文集数 if (parseResult.IndexNumber is null) { parseResult.IndexNumber = ParseChineseIndexNumber(fileName); } // 解析不到title时,使用默认名 if (string.IsNullOrEmpty(parseResult.Name)) { parseResult.Name = fileName; } return parseResult; } private static string CleanName(string name) { // 电视剧名称后紧跟季信息时,会附加到名称中,需要去掉 name = seasonSuffixReg.Replace(name, string.Empty); // 删除多余的[]/()附加信息 name = unusedReg.Replace(name, string.Empty); return name.Replace(".", " ").Trim(); } // emby原始电影解析 public static ParseNameResult ParseMovie(string fileName) { var parseResult = new ParseNameResult(); var nameOptions = new Emby.Naming.Common.NamingOptions(); var result = Emby.Naming.Video.VideoResolver.CleanDateTime(fileName, nameOptions); if (Emby.Naming.Video.VideoResolver.TryCleanString(result.Name, nameOptions, out var cleanName)) { parseResult.Name = CleanName(cleanName); parseResult.Year = result.Year; } else { parseResult.Name = CleanName(result.Name); parseResult.Year = result.Year; } return parseResult; } private static int ParseYear(string val) { var match = yearReg.Match(val); if (match.Success && match.Groups.Count > 0) { return match.Groups[0].Value.ToInt(); } return 0; } private static int? ParseChineseIndexNumber(string fileName) { var match = chineseIndexNumberReg.Match(fileName); if (match.Success && match.Groups.Count > 1) { if (int.TryParse(match.Groups[1].Value, out var seasonNumber)) { return seasonNumber; } var number = Utils.ChineseNumberToInt(match.Groups[1].Value); if (number.HasValue) { return number; } } return null; } public static bool IsSpecialDirectory(string path) { var fileName = Path.GetFileNameWithoutExtension(path) ?? string.Empty; if (IsAnime(fileName) && fileName.Contains("[SP]")) { return true; } var folder = Path.GetFileName(Path.GetDirectoryName(path)) ?? string.Empty; return folder == "SPs" || folder.Contains("特典"); } // 判断是否为动漫 // https://github.com/jxxghp/nas-tools/blob/f549c924558fd49e183333285bc6a804af1a2cb7/app/media/meta/metainfo.py#L51 public static bool IsAnime(string name) { if (string.IsNullOrWhiteSpace(name)) { return false; } if (Regex.Match(name, @"【[+0-9XVPI-]+】\s*【", RegexOptions.IgnoreCase).Success) { return true; } if (Regex.Match(name, @"\s+-\s+[\dv]{1,4}\s+", RegexOptions.IgnoreCase).Success) { return true; } if (Regex.Match(name, @"S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}", RegexOptions.IgnoreCase).Success) { return true; } if (Regex.Match(name, @"\[[+0-9XVPI-]+]\s*\[", RegexOptions.IgnoreCase).Success) { return true; } if (Regex.Match(name, @"\[.+\].*?\[.+?\]", RegexOptions.IgnoreCase).Success) { return true; } return false; } } }