This commit is contained in:
limil 2026-01-02 23:35:05 +08:00
parent 3d58c7d497
commit 3525df1e95
20 changed files with 425 additions and 172 deletions

View File

@ -38,7 +38,7 @@ public partial class MainTreePanel : Tree
SetColumnTitlesVisible(true); SetColumnTitlesVisible(true);
SetColumnTitle(0, "文件名"); SetColumnTitle(0, "文件名");
SetColumnCustomMinimumWidth(0, 500); SetColumnCustomMinimumWidth(0, 800);
SetColumnExpand(0, true); SetColumnExpand(0, true);
MultiSelected += OnMultiSelected; MultiSelected += OnMultiSelected;

View File

@ -5,5 +5,5 @@ namespace Learn.Config;
[ConfigItem("APP")] [ConfigItem("APP")]
public class AppConfig : IConfigItem public class AppConfig : IConfigItem
{ {
public string ScanPath = ""; public string ScanPath { get; set; } = "";
} }

1
Config/AppConfig.cs.uid Normal file
View File

@ -0,0 +1 @@
uid://bakkudpxylhe8

View File

@ -1,9 +0,0 @@
using Learn.Utils;
namespace Learn.Config;
[ConfigItem("Proxy")]
public class ProxyConfig : IConfigItem
{
public string HttpProxy = "";
}

View File

@ -1 +0,0 @@
uid://k75xslv0u3pm

58
Config/RawParserConfig.cs Normal file
View File

@ -0,0 +1,58 @@
using System.Collections.Generic;
using Learn.Utils;
namespace Learn.Config;
public class MatchGroupsRules
{
public List<string> Full { get; set; }
public List<string> Partial { get; set; }
}
public class MatchTypeExtra
{
public List<string> IfDirNameIs { get; set; }
public List<string> IfFileExtensionIs { get; set; }
}
public class MatchTypeSubtitle
{
public List<string> IfFileExtensionIs { get; set; }
}
public class MatchTypeEpisode
{
public List<string> IfFileExtensionIs { get; set; }
}
public class MatchTypeRules
{
public MatchTypeExtra Extra { get; set; }
public MatchTypeSubtitle Subtitle { get; set; }
public MatchTypeEpisode Episode { get; set; }
}
public class MatchSeasonRules
{
public List<string> Regexes { get; set; }
}
public class FilterTokenRules
{
public List<string> Regexes { get; set; }
}
[ConfigItem("RawParser")]
public class RawParserConfig : IConfigItem
{
public string SplitRegex { get; set; } = "";
public MatchTypeRules TypeMatchRules { get; set; }
public MatchGroupsRules GroupsMatchRules { get; set; }
public MatchSeasonRules SeasonMatchRules { get; set; }
public FilterTokenRules TokenFilterRules { get; set; }
}

View File

@ -0,0 +1 @@
uid://ccldbt2fp1mxn

View File

@ -1,9 +0,0 @@
using Learn.Utils;
namespace Learn.Config;
[ConfigItem("TMDB")]
public class TMDBConfig : IConfigItem
{
public string ApiKey = "";
}

View File

@ -1 +0,0 @@
uid://v07s3j12o0lw

View File

@ -0,0 +1,10 @@
using Learn.Utils;
namespace Learn.Config;
[ConfigItem("TMDBParser")]
public class TMDBParserConfig : IConfigItem
{
public string HttpProxy { get; set; } = "";
public string ApiKey { get; set; } = "";
}

View File

@ -0,0 +1 @@
uid://dp5xqvvuiclbg

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F94e5a9ba04d74002870fbeeec71ff78cca738_003F85_003Fce11ac0c_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARawInfoPanel_005FScriptMethods_002Egenerated_002Ecs_002Fl_003AC_0021_003FUsers_003Flianzefeng_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9fdf5fe7bab299c9c5ee4ca6784782ae9851b9b_003FRawInfoPanel_005FScriptMethods_002Egenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARawInfoPanel_005FScriptMethods_002Egenerated_002Ecs_002Fl_003AC_0021_003FUsers_003Flianzefeng_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9fdf5fe7bab299c9c5ee4ca6784782ae9851b9b_003FRawInfoPanel_005FScriptMethods_002Egenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003AC_0021_003FUsers_003Flianzefeng_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa03380083db34a2faee436e29e06a72ae8e910_003Fa8_003Fec982e59_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003AC_0021_003FUsers_003Flianzefeng_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa03380083db34a2faee436e29e06a72ae8e910_003Fa8_003Fec982e59_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATreeItem_002Ecs_002Fl_003AC_0021_003FUsers_003Flianzefeng_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1c378f459c054fecaf4484a0fa6d44c055a800_003F1d_003Fbc7bd422_003FTreeItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATreeItem_002Ecs_002Fl_003AC_0021_003FUsers_003Flianzefeng_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1c378f459c054fecaf4484a0fa6d44c055a800_003F1d_003Fbc7bd422_003FTreeItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>

View File

@ -21,6 +21,7 @@ public partial class Main : Node
[Export] private Button _saveButton; [Export] private Button _saveButton;
[Export] private Button _resetButton; [Export] private Button _resetButton;
[Export] private Button _loadButton; [Export] private Button _loadButton;
[Export] private Button _reloadConfigButton;
[ExportGroup("编辑面板")] [ExportGroup("编辑面板")]
[Export] private NodeInfoEditPanel _nodeInfoEditPanel; [Export] private NodeInfoEditPanel _nodeInfoEditPanel;
@ -70,6 +71,7 @@ public partial class Main : Node
_saveButton.Pressed += DoSave; _saveButton.Pressed += DoSave;
_resetButton.Pressed += DoReset; _resetButton.Pressed += DoReset;
_loadButton.Pressed += LoadData; _loadButton.Pressed += LoadData;
_reloadConfigButton.Pressed += ReloadConfig;
_doParseButton.Text = "开始解析"; _doParseButton.Text = "开始解析";
_configs = new Configs(); _configs = new Configs();
@ -82,6 +84,11 @@ public partial class Main : Node
_refreshPanels = true; _refreshPanels = true;
} }
private void ReloadConfig()
{
_configs.Load(true);
}
private void LoadData() private void LoadData()
{ {
if (File.Exists(DataPath)) if (File.Exists(DataPath))
@ -146,7 +153,7 @@ public partial class Main : Node
var originName = _doParseButton.Text; var originName = _doParseButton.Text;
_doParseButton.Disabled = true; _doParseButton.Disabled = true;
ItemParser parser = new RawParser(); ItemParser parser = new RawParser(_configs);
if (_mainTreePanel.Query(root, out var node)) if (_mainTreePanel.Query(root, out var node))
{ {

View File

@ -17,7 +17,7 @@ grow_vertical = 2
[node name="FileDirDialog" type="FileDialog" parent="."] [node name="FileDirDialog" type="FileDialog" parent="."]
script = ExtResource("1_d2g23") script = ExtResource("1_d2g23")
[node name="Main" type="Node" parent="." node_paths=PackedStringArray("_dirSelector", "_openDirButton", "_doParseButton", "_saveButton", "_resetButton", "_loadButton", "_nodeInfoEditPanel", "_addKeyButton", "_removeKeyButton", "_inspectorPanel", "_mainTreePanel", "_columnIndexText", "_columnWidthText", "_columnText", "_addColumnButton", "_removeColumnButton", "_clearColumnButton", "_expandAllButton", "_foldAllButton")] [node name="Main" type="Node" parent="." node_paths=PackedStringArray("_dirSelector", "_openDirButton", "_doParseButton", "_saveButton", "_resetButton", "_loadButton", "_reloadConfigButton", "_nodeInfoEditPanel", "_addKeyButton", "_removeKeyButton", "_inspectorPanel", "_mainTreePanel", "_columnIndexText", "_columnWidthText", "_columnText", "_addColumnButton", "_removeColumnButton", "_clearColumnButton", "_expandAllButton", "_foldAllButton")]
script = ExtResource("2_0727o") script = ExtResource("2_0727o")
_dirSelector = NodePath("../FileDirDialog") _dirSelector = NodePath("../FileDirDialog")
_openDirButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/文件夹操作/ScanDir") _openDirButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/文件夹操作/ScanDir")
@ -25,6 +25,7 @@ _doParseButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VB
_saveButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/保存操作/Save") _saveButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/保存操作/Save")
_resetButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/保存操作/Reset") _resetButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/保存操作/Reset")
_loadButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/保存操作/Load") _loadButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/保存操作/Load")
_reloadConfigButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/保存操作/ReloadConfig")
_nodeInfoEditPanel = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer/VBoxContainer/NodeEditPanel") _nodeInfoEditPanel = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer/VBoxContainer/NodeEditPanel")
_addKeyButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer/VBoxContainer/HBoxContainer/AddKey") _addKeyButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer/VBoxContainer/HBoxContainer/AddKey")
_removeKeyButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer/VBoxContainer/HBoxContainer/RemoveKey") _removeKeyButton = NodePath("../MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer/VBoxContainer/HBoxContainer/RemoveKey")
@ -85,9 +86,10 @@ layout_mode = 2
[node name="TabContainer" type="TabContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2"] [node name="TabContainer" type="TabContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2"]
custom_minimum_size = Vector2(0, 200) custom_minimum_size = Vector2(0, 200)
layout_mode = 2 layout_mode = 2
current_tab = 0 current_tab = 1
[node name="文件夹操作" type="VBoxContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer"] [node name="文件夹操作" type="VBoxContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer"]
visible = false
layout_mode = 2 layout_mode = 2
metadata/_tab_index = 0 metadata/_tab_index = 0
@ -102,7 +104,6 @@ size_flags_vertical = 4
text = "开始解析" text = "开始解析"
[node name="保存操作" type="VBoxContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer"] [node name="保存操作" type="VBoxContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer"]
visible = false
layout_mode = 2 layout_mode = 2
metadata/_tab_index = 1 metadata/_tab_index = 1
@ -118,6 +119,10 @@ text = "加载树表"
layout_mode = 2 layout_mode = 2
text = "重置树表" text = "重置树表"
[node name="ReloadConfig" type="Button" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer/保存操作"]
layout_mode = 2
text = "重载配置"
[node name="树表操作" type="VBoxContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer"] [node name="树表操作" type="VBoxContainer" parent="MarginContainer/HSplitContainer/ScrollContainer/VBoxContainer2/FoldableContainer4/VBoxContainer2/TabContainer"]
visible = false visible = false
layout_mode = 2 layout_mode = 2

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Learn.Models; namespace Learn.Models;
@ -28,47 +29,45 @@ public class TreeNode(Item item)
public bool TryGetValue(string key, out string value, out bool isInherited) public bool TryGetValue(string key, out string value, out bool isInherited)
{ {
value = null;
isInherited = false;
var curr = this; var curr = this;
bool valueExists = false;
while (curr.Info != null) while (curr.Info != null)
{ {
if (curr.Info.Info.TryGetValue(key, out value)) if (curr.Info.Info.TryGetValue(key, out var currValue))
{ {
isInherited = curr != this; value = currValue;
return true; valueExists = true;
isInherited = (curr != this);
} }
curr = curr.Parent; curr = curr.Parent;
} }
value = null; return valueExists;
isInherited = false;
return false;
} }
public IEnumerable<KeyValueInfo> GetKeyValueInfos() public IEnumerable<KeyValueInfo> GetKeyValueInfos()
{ {
var result = new List<KeyValueInfo>(); var dict = new Dictionary<string, KeyValueInfo>();
var keys = new HashSet<string>();
var curr = this; var curr = this;
while (curr.Info != null) while (curr.Info != null)
{ {
foreach (var kv in curr.Info.Info) foreach (var kv in curr.Info.Info)
{ {
if(!keys.Add(kv.Key)) continue; var keyValueInfo = new KeyValueInfo
var keyValueInfo = new KeyValueInfo();
if (curr != this)
{ {
keyValueInfo.IsInherited = true; IsInherited = curr != this,
} Key = kv.Key,
Value = kv.Value
};
keyValueInfo.Key = kv.Key; dict[kv.Key] = keyValueInfo;
keyValueInfo.Value = kv.Value;
result.Add(keyValueInfo);
} }
curr = curr.Parent; curr = curr.Parent;
} }
return result; return dict.Values.ToList();
} }
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Learn.Models; using Learn.Models;
@ -6,6 +7,14 @@ namespace Learn.Parsers;
public static class ItemFields public static class ItemFields
{ {
public enum ItemType
{
Unknown,
Extra,
Subtitle,
Episode
}
#region #region
public static string MainKey_Path => "Path"; public static string MainKey_Path => "Path";
@ -25,36 +34,27 @@ public static class ItemFields
#endregion #endregion
#region
public static string Key_Group => "Group"; public static string Key_Group => "Group";
public static string Key_Title => "Title"; public static string Key_Title => "Title";
public static string Key_RawTitle => "RawTitle"; public static string Key_RawTitle => "RawTitle";
public static string Key_Season => "Season"; public static string Key_Season => "Season";
public static string Key_Year => "Year"; public static string Key_Type => "Type";
public static string Key_SubtitleLanguage => "SubLang";
public static void SetGroupIfNotExist(this Item item, string group) public static ItemType Type(this Item item)
{ {
item.Info.TryAdd(Key_Group, group); if (item.Info.TryGetValue(Key_Type, out var typeName))
{
if(Enum.TryParse(typeName, out ItemType type))
{
return type;
}
}
return ItemType.Unknown;
} }
public static void SetTitleIfNotExist(this Item item, string title) #endregion
{
item.Info.TryAdd(Key_Title, title);
}
public static void SetRawTitleIfNotExist(this Item item, string rawTitle)
{
item.Info.TryAdd(Key_RawTitle, rawTitle);
}
public static void SetSeasonIfNotExist(this Item item, string season)
{
item.Info.TryAdd(Key_Season, season);
}
public static void SetYear(this Item item, int year)
{
item.Info[Key_Year] = year.ToString();
}
} }

View File

@ -12,7 +12,6 @@ namespace Learn.Parsers;
/// "集数" /// "集数"
/// "类型""额外内容", "字幕", "剧集" /// "类型""额外内容", "字幕", "剧集"
/// "字幕语言" /// "字幕语言"
/// "年份"
/// </summary> /// </summary>
public interface ItemParser public interface ItemParser
{ {

View File

@ -1,135 +1,321 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Learn.Config;
using Learn.Models; using Learn.Models;
using Learn.Utils;
namespace Learn.Parsers; namespace Learn.Parsers;
public class RawParser : ItemParser public class RawParser(Configs configs) : ItemParser
{ {
private string ParseSeasonFromCN(string season) private RawParserConfig config => configs.Get<RawParserConfig>();
{
if (int.TryParse(season, out _)) return season;
switch (season) private List<string> FilterParts(List<string> parts)
{
var result = parts.ToList();
foreach (var regex in config.TokenFilterRules.Regexes)
{ {
case "零": return "0"; result.RemoveAll(part => Regex.Match(part.Trim(), regex).Success);
case "一": return "1";
case "二": return "2";
case "三": return "3";
case "四": return "4";
case "五": return "5";
case "六": return "6";
case "七": return "7";
case "八": return "8";
case "九": return "9";
} }
return season; return result;
} }
private List<string> GetParts(Item item)
private (string, string) SplitTitleAndSeason(string rawTitle)
{ {
var match1 = Regex.Match(rawTitle, @"第(.+)季"); var matches = Regex.Matches(item.Name(), config.SplitRegex).Select(match => match.Value)
if (match1.Success) .Select(match => match.Trim())
{ .Where(match => !string.IsNullOrEmpty(match))
var seasonStr = match1.Groups[0].Value.Trim(); .ToList();
var season = ParseSeasonFromCN(match1.Groups[1].Value.Trim()); return FilterParts(matches);
var title = rawTitle.Replace(seasonStr, "").Trim();
return (title, season);
}
var match2 = Regex.Match(rawTitle, @"[Ss]eason *(\d+)");
if (match2.Success)
{
var seasonStr = match1.Groups[0].Value.Trim();
var season = int.Parse(match1.Groups[1].Value.Trim()).ToString();
var title = rawTitle.Replace(seasonStr, "").Trim();
return (title, season);
}
return (rawTitle, null);
} }
private void SimplifyMatches(List<string> matches) private bool TryNormalizeSeason(string seasonPart, out string season)
{
matches.RemoveAll(match => string.IsNullOrEmpty(match.Trim()));
matches.RemoveAll(match => Regex.Match(match.Trim(), @"\d+[Pp]$").Success);
}
private List<string> GetParts(string name)
{
var matches = Regex.Matches(name, @"[^\[\]_【】]+").Select(match => match.Value).ToList();
SimplifyMatches(matches);
return matches;
}
private bool TryParseRawTitle(Item item, out string rawTitle)
{
rawTitle = null;
var name = item.Name();
var matches = GetParts(name);
if (matches.Count == 0) return false;
if (matches.Count == 1)
{
rawTitle = matches[0];
}
else
{
rawTitle = matches[1];
}
(rawTitle, _) = SplitTitleAndSeason(rawTitle);
return true;
}
private bool TryParseSeason(Item item, out string season)
{ {
season = null; season = null;
var name = item.Name(); if (int.TryParse(seasonPart, out var seasonInt))
var matches = GetParts(name);
if (matches.Count == 0) return false;
if (matches.Count == 1)
{ {
(_, season) = SplitTitleAndSeason(matches[0]); season = seasonInt.ToString();
if (!string.IsNullOrEmpty(season)) return true; return true;
} }
else switch (seasonPart)
{ {
(_, season) = SplitTitleAndSeason(matches[1]); case "零":
if (!string.IsNullOrEmpty(season)) return true; season = "0";
return true;
case "一":
season = "1";
return true;
case "二":
season = "2";
return true;
case "三":
season = "3";
return true;
case "四":
season = "4";
return true;
case "五":
season = "5";
return true;
case "六":
season = "6";
return true;
case "七":
season = "7";
return true;
case "八":
season = "8";
return true;
case "九":
season = "9";
return true;
}
return false;
}
private bool TryMatchSeason(string token, out string seasonPart, out string matchPart)
{
seasonPart = null;
matchPart = null;
var regexes = config.SeasonMatchRules?.Regexes;
if (regexes == null) return false;
token = token.Trim();
foreach (var regex in regexes)
{
var match = Regex.Match(token, regex);
if (!match.Success) continue;
matchPart = match.Value;
return TryNormalizeSeason(match.Groups[1].Value, out seasonPart);
}
return false;
}
private bool TryParseSeason(TreeNode node, out string season, out MatchInfo matchInfo)
{
season = null;
matchInfo = null;
var parts = GetParts(node.Info);
for (int i = 0; i < parts.Count; i++)
{
var part = parts[i];
var tokens = part.Split("-");
foreach (var token in tokens)
{
if (!TryMatchSeason(token, out season, out var content)) continue;
matchInfo = new MatchInfo
{
content = content,
partIndex = i
};
return true;
}
} }
return false; return false;
} }
private bool TryParseGroup(Item item, out string group) private bool IsFullMatch(string item, List<string> sequence)
{
if(sequence == null) return false;
for (int i = 0; i < sequence.Count; i++)
{
if (item.Equals(sequence[i], StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private bool IsPartialMatch(string item, List<string> sequence)
{
if(sequence == null) return false;
for (int i = 0; i < sequence.Count; i++)
{
if (item.Contains(sequence[i], StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private bool TryParseGroup(TreeNode node, out string group, out MatchInfo matchInfo)
{ {
group = null; group = null;
var name = item.Name(); matchInfo = null;
var matches = GetParts(name);
if (matches.Count <= 1) return false; var parts = GetParts(node.Info);
group = matches[0]; for (int i = 0; i < parts.Count; i++)
{
if (IsFullMatch(parts[i], config.GroupsMatchRules?.Full))
{
group = parts[i];
matchInfo = new MatchInfo
{
content = parts[i],
partIndex = i
};
return true;
}
}
for (int i = 0; i < parts.Count; i++)
{
if (IsPartialMatch(parts[i], config.GroupsMatchRules?.Partial))
{
group = parts[i];
matchInfo = new MatchInfo
{
content = parts[i],
partIndex = i
};
return true;
}
}
return false;
}
private ItemFields.ItemType ParseItemType(TreeNode node)
{
// 1. 判断是否属于Extras
foreach (var extraMatchName in config.TypeMatchRules.Extra.IfDirNameIs)
{
if (node.Info.Name().Equals(extraMatchName, StringComparison.OrdinalIgnoreCase))
{
return ItemFields.ItemType.Extra;
}
}
if (node.Info.IsFolder())
{
return ItemFields.ItemType.Unknown;
}
var infoExt = Path.GetExtension(node.Info.Name());
if (string.IsNullOrEmpty(infoExt)) return ItemFields.ItemType.Extra;
foreach (var ext in config.TypeMatchRules.Extra.IfFileExtensionIs)
{
if (infoExt.Equals(ext, StringComparison.OrdinalIgnoreCase))
{
return ItemFields.ItemType.Extra;
}
}
// 2. 判断是不是字幕
foreach (var ext in config.TypeMatchRules.Subtitle.IfFileExtensionIs)
{
if (infoExt.Equals(ext, StringComparison.OrdinalIgnoreCase))
{
return ItemFields.ItemType.Subtitle;
}
}
// 3. 判断是不是剧集
foreach (var ext in config.TypeMatchRules.Episode.IfFileExtensionIs)
{
if (infoExt.Equals(ext, StringComparison.OrdinalIgnoreCase))
{
return ItemFields.ItemType.Episode;
}
}
// 4. 啥都不是,不知道
return ItemFields.ItemType.Unknown;
}
private bool TryParseType(TreeNode node, out string type, out MatchInfo matchInfo)
{
matchInfo = null;
type = null;
var typeEnum = ParseItemType(node);
if (typeEnum == ItemFields.ItemType.Unknown) return false;
type = typeEnum.ToString();
return true; return true;
} }
private delegate bool FieldParser(Item item, out string result); private bool TryParseSubtitleLanguage(TreeNode node, out string language, out MatchInfo matchInfo)
{
language = null;
matchInfo = null;
if (node.Info.Type() != ItemFields.ItemType.Subtitle)
{
return false;
}
var name = node.Info.Name();
var parts = name.Split(".");
if (parts.Length < 3) return false;
language = parts[^2];
return true;
}
private bool TryParseEpisode(TreeNode node, out string episode, out MatchInfo matchInfo)
{
episode = null;
matchInfo = null;
if (node.Info.Type() != ItemFields.ItemType.Episode)
{
return false;
}
var parts = GetParts(node.Info);
for (int i = 0; i < parts.Count; i++)
{
var part = parts[i];
var tokens = part.Split("-");
foreach (var token in tokens)
{
var match = Regex.Match(token.Trim(), @"^\d{1,2}$");
if (match.Success)
{
matchInfo = new MatchInfo
{
content = content,
partIndex = i
};
}
return true;
}
}
return false;
}
//
// private bool TryParseRawTitle(TreeNode node, out string rawTitle, out MatchInfo matchInfo)
// {
//
// }
class MatchInfo
{
public string content;
public int partIndex;
}
private delegate bool FieldParser(TreeNode node, out string result, out MatchInfo matchInfo);
private bool TryParseField(TreeNode node, FieldParser fieldParser, out string result) private bool TryParseField(TreeNode node, FieldParser fieldParser, out string result)
{ {
result = null; result = null;
if (node.Info == null) return false; if (node.Info == null) return false;
if (!fieldParser(node.Info, out var fieldValue)) return false;
if (!fieldParser(node, out var fieldValue, out _)) return false;
var parsed = new List<string>(); var parsed = new List<string>();
foreach (var child in node.Children) foreach (var child in node.Children)
{ {
if (fieldParser(child.Info, out var childFieldValue)) if (fieldParser(child, out var childFieldValue, out _))
{ {
parsed.Add(childFieldValue); parsed.Add(childFieldValue);
} }
@ -162,6 +348,7 @@ public class RawParser : ItemParser
while (queue.Count > 0) while (queue.Count > 0)
{ {
var current = queue.Dequeue(); var current = queue.Dequeue();
if(current.Info.Info.ContainsKey(fieldName)) continue;
if (TryParseField(current, fieldParser, out var fieldValue)) if (TryParseField(current, fieldParser, out var fieldValue))
{ {
current.Info.Info.TryAdd(fieldName, fieldValue); current.Info.Info.TryAdd(fieldName, fieldValue);
@ -178,8 +365,10 @@ public class RawParser : ItemParser
public async Task Parse(TreeNode node) public async Task Parse(TreeNode node)
{ {
DoParse(node, TryParseRawTitle, ItemFields.Key_RawTitle);
DoParse(node, TryParseSeason, ItemFields.Key_Season); DoParse(node, TryParseSeason, ItemFields.Key_Season);
DoParse(node, TryParseGroup, ItemFields.Key_Group); DoParse(node, TryParseGroup, ItemFields.Key_Group);
DoParse(node, TryParseType, ItemFields.Key_Type);
DoParse(node, TryParseSubtitleLanguage, ItemFields.Key_SubtitleLanguage);
} }
} }

View File

@ -14,6 +14,8 @@ namespace Learn.Parsers;
public class TMDBParser(Configs configs) : ItemParser public class TMDBParser(Configs configs) : ItemParser
{ {
private TMDBParserConfig config => configs.Get<TMDBParserConfig>();
private readonly Dictionary<string, SearchTv> _cache = new(); private readonly Dictionary<string, SearchTv> _cache = new();
private TMDbClient _client; private TMDbClient _client;
@ -37,8 +39,8 @@ public class TMDBParser(Configs configs) : ItemParser
{ {
if (_client != null) return _client; if (_client != null) return _client;
var apiKey = configs.Get<TMDBConfig>().ApiKey; var apiKey = config.ApiKey;
var proxy = configs.Get<ProxyConfig>().HttpProxy; var proxy = config.HttpProxy;
if (string.IsNullOrEmpty(proxy)) if (string.IsNullOrEmpty(proxy))
{ {

View File

@ -17,8 +17,8 @@ config/icon="res://icon.svg"
[display] [display]
window/size/viewport_width=1920 window/size/viewport_width=1600
window/size/viewport_height=1080 window/size/viewport_height=900
[dotnet] [dotnet]