diff --git a/.gitignore b/.gitignore
index 58164f3..e658aaa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
.godot/
/android/
.idea/
-data.json
\ No newline at end of file
+data.json
+config.json
\ No newline at end of file
diff --git a/Component/MainTreePanel.cs b/Component/MainTreePanel.cs
index 70bd5be..c147f8c 100644
--- a/Component/MainTreePanel.cs
+++ b/Component/MainTreePanel.cs
@@ -1,8 +1,11 @@
+using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Godot;
using Learn.Models;
+using Learn.Parsers;
using Newtonsoft.Json;
namespace Learn.Component;
@@ -95,15 +98,22 @@ public partial class MainTreePanel : Tree
private void OnMultiSelected(TreeItem item, long column, bool selected)
{
- if(selected) _selectingNodes.Add(_mapper[item]);
- else _selectingNodes.Remove(_mapper[item]);
+ for (int i = 0; i < Columns; i++)
+ {
+ if (i == column ? selected : item.IsSelected(i))
+ {
+ _selectingNodes.Add(_mapper[item]);
+ return;
+ }
+ }
+ _selectingNodes.Remove(_mapper[item]);
}
private TreeItem CreateNode(TreeItem father, Item item)
{
var node = CreateItem(father);
- var name = Path.GetFileName(item.MainInfo["Path"]);
+ var name = Path.GetFileName(item.MainInfo[ItemFields.MainKey_Path]);
node.SetText(0, name);
return node;
}
@@ -118,8 +128,8 @@ public partial class MainTreePanel : Tree
{
MainInfo =
{
- ["IsDir"] = isDir ? "True" : "False",
- ["Path"] = path
+ [ItemFields.MainKey_IsDir] = isDir ? "True" : "False",
+ [ItemFields.MainKey_Path] = path
}
};
_items.Add(path, item);
diff --git a/Component/NodeInfoEditPanel.cs b/Component/NodeInfoEditPanel.cs
index 1c3b1be..b0222c5 100644
--- a/Component/NodeInfoEditPanel.cs
+++ b/Component/NodeInfoEditPanel.cs
@@ -39,14 +39,15 @@ public partial class NodeInfoEditPanel : Tree
private void OnMultiSelected(TreeItem item, long column, bool selected)
{
- if (selected)
+ for (int i = 0; i < Columns; i++)
{
- _selectingNodes.Add(item);
- }
- else
- {
- _selectingNodes.Remove(item);
+ if (i == column ? selected : item.IsSelected(i))
+ {
+ _selectingNodes.Add(item);
+ return;
+ }
}
+ _selectingNodes.Remove(item);
}
private void OnItemEdited()
diff --git a/Config/ProxyConfig.cs b/Config/ProxyConfig.cs
new file mode 100644
index 0000000..9c1d178
--- /dev/null
+++ b/Config/ProxyConfig.cs
@@ -0,0 +1,9 @@
+using Learn.Utils;
+
+namespace Learn.Config;
+
+[ConfigItem("Proxy")]
+public class ProxyConfig : IConfigItem
+{
+ public string HttpProxy = "";
+}
\ No newline at end of file
diff --git a/Config/ProxyConfig.cs.uid b/Config/ProxyConfig.cs.uid
new file mode 100644
index 0000000..db66392
--- /dev/null
+++ b/Config/ProxyConfig.cs.uid
@@ -0,0 +1 @@
+uid://k75xslv0u3pm
diff --git a/Config/TMDBConfig.cs b/Config/TMDBConfig.cs
new file mode 100644
index 0000000..bde84e2
--- /dev/null
+++ b/Config/TMDBConfig.cs
@@ -0,0 +1,9 @@
+using Learn.Utils;
+
+namespace Learn.Config;
+
+[ConfigItem("TMDB")]
+public class TMDBConfig : IConfigItem
+{
+ public string ApiKey = "";
+}
\ No newline at end of file
diff --git a/Config/TMDBConfig.cs.uid b/Config/TMDBConfig.cs.uid
new file mode 100644
index 0000000..905f30b
--- /dev/null
+++ b/Config/TMDBConfig.cs.uid
@@ -0,0 +1 @@
+uid://v07s3j12o0lw
diff --git a/Learn.csproj b/Learn.csproj
index 05f4301..5cb36ba 100644
--- a/Learn.csproj
+++ b/Learn.csproj
@@ -6,5 +6,6 @@
+
\ No newline at end of file
diff --git a/Main.cs b/Main.cs
index 25ca3c0..49c77e6 100644
--- a/Main.cs
+++ b/Main.cs
@@ -1,9 +1,13 @@
+using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using Godot;
using System.Linq;
+using System.Threading.Tasks;
using Learn.Component;
using Learn.Parsers;
+using Learn.Utils;
public partial class Main : Node
{
@@ -36,6 +40,8 @@ public partial class Main : Node
[Export] private Button _foldAllButton;
private bool _refreshPanels;
+
+ private Configs _configs;
public override void _Ready()
{
@@ -57,6 +63,7 @@ public partial class Main : Node
_saveButton.Pressed += DoSave;
_resetButton.Pressed += DoReset;
_loadButton.Pressed += LoadData;
+ _doParseButton.Text = "开始解析";
LoadData();
@@ -64,6 +71,10 @@ public partial class Main : Node
FoldAll();
_refreshPanels = true;
+
+ _configs = new Configs();
+ _configs.ConfigPath = "config.json";
+ _configs.Load(true);
}
private void LoadData()
@@ -129,29 +140,54 @@ public partial class Main : Node
_refreshPanels = true;
}
- private void DoParse()
+ private async void DoParse()
{
var root = _mainTreePanel.GetRoot();
if (root == null) return;
- ItemParser parser = new NormalParser();
+ var originName = _doParseButton.Text;
+ _doParseButton.Disabled = true;
- var queue = new Queue();
- queue.Enqueue(root);
- while (queue.Count > 0)
+ ItemParser parser = new TMDBParser(_configs);
+
+ var stopwatch = Stopwatch.StartNew();
+ var stack = new Stack();
+ stack.Push(root);
+
+ while (stack.Count > 0)
{
- var treeItem = queue.Dequeue();
+ _doParseButton.Text = "解析中";
+ if (stopwatch.ElapsedMilliseconds > 2000)
+ {
+ _mainTreePanel.UpdateColumns();
+ stopwatch.Restart();
+ }
+ var treeItem = stack.Pop();
foreach (var child in treeItem.GetChildren())
{
if(child == null) continue;
if (_mainTreePanel.Query(child, out var node))
{
- parser.DoParse(node);
+ try
+ {
+ if (!await parser.DoParse(node))
+ {
+ GD.PrintErr($"解析{child.GetText(0)}失败");
+ }
+ }
+ catch (Exception ex)
+ {
+ GD.PrintErr($"解析{child.GetText(0)}出现异常:" + ex);
+ }
}
- queue.Enqueue(child);
+ stack.Push(child);
}
}
+
+ _doParseButton.Text = originName;
+ _doParseButton.Disabled = false;
_mainTreePanel.UpdateColumns();
+ GD.Print("Done");
}
private const string DataPath = "data.json";
diff --git a/Parsers/ItemFieldExtension.cs b/Parsers/ItemFieldExtension.cs
deleted file mode 100644
index da42ee3..0000000
--- a/Parsers/ItemFieldExtension.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using Learn.Models;
-
-namespace Learn.Parsers;
-
-public static class ItemFieldExtension
-{
- #region 基础信息
-
- public static string Name(this Item item)
- {
- var path = item.MainInfo["Path"];
- return Path.GetFileName(path);
- }
-
- public static bool IsFolder(this Item item)
- {
- return item.MainInfo["IsDir"] == "True";
- }
-
- #endregion
-
-
- public static void SetGroupIfNotExist(this Item item, string group)
- {
- item.Info.TryAdd("Group", group);
- }
-
- public static void SetTitleIfNotExist(this Item item, string title)
- {
- item.Info.TryAdd("Title", title);
- }
-
- public static void SetRawTitle(this Item item, string rawTitle)
- {
- item.Info.TryAdd("RawTitle", rawTitle);
- }
-
- public static string RawTitle(this Item item)
- {
- return item.Info["RawTitle"];
- }
-
- public static void SetSeasonIfNotExist(this Item item, string season)
- {
- item.Info.TryAdd("Season", season);
- }
-}
\ No newline at end of file
diff --git a/Parsers/ItemFieldExtension.cs.uid b/Parsers/ItemFieldExtension.cs.uid
deleted file mode 100644
index 0737d00..0000000
--- a/Parsers/ItemFieldExtension.cs.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://fo70m114nwe7
diff --git a/Parsers/ItemFields.cs b/Parsers/ItemFields.cs
new file mode 100644
index 0000000..68d5312
--- /dev/null
+++ b/Parsers/ItemFields.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.IO;
+using Learn.Models;
+
+namespace Learn.Parsers;
+
+public static class ItemFields
+{
+ #region 基础信息
+
+ public static string MainKey_Path => "Path";
+
+ public static string MainKey_IsDir => "IsDir";
+
+ public static string Name(this Item item)
+ {
+ var path = item.MainInfo[MainKey_Path];
+ return Path.GetFileName(path);
+ }
+
+ public static bool IsFolder(this Item item)
+ {
+ return item.MainInfo[MainKey_IsDir] == "True";
+ }
+
+ #endregion
+
+
+ public static string Key_Group => "Group";
+ public static string Key_Title => "Title";
+ public static string Key_RawTitle => "RawTitle";
+ public static string Key_Season => "Season";
+ public static string Key_Year => "Year";
+
+
+ public static void SetGroupIfNotExist(this Item item, string group)
+ {
+ item.Info.TryAdd(Key_Group, group);
+ }
+
+ public static void SetTitleIfNotExist(this Item item, string title)
+ {
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/Parsers/ItemFields.cs.uid b/Parsers/ItemFields.cs.uid
new file mode 100644
index 0000000..18b944f
--- /dev/null
+++ b/Parsers/ItemFields.cs.uid
@@ -0,0 +1 @@
+uid://cavjlh7k1lb6m
diff --git a/Parsers/ItemParser.cs b/Parsers/ItemParser.cs
index 3df1cc6..3dbd0cc 100644
--- a/Parsers/ItemParser.cs
+++ b/Parsers/ItemParser.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Threading.Tasks;
using Learn.Models;
namespace Learn.Parsers;
@@ -15,5 +16,5 @@ namespace Learn.Parsers;
///
public interface ItemParser
{
- public bool DoParse(TreeNode node);
+ public Task DoParse(TreeNode node);
}
\ No newline at end of file
diff --git a/Parsers/NormalParser.cs b/Parsers/NormalParser.cs
deleted file mode 100644
index 12316c2..0000000
--- a/Parsers/NormalParser.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Linq;
-using System.Text.RegularExpressions;
-using Learn.Models;
-
-namespace Learn.Parsers;
-
-public class NormalParser : ItemParser
-{
- private string ParseSeason(string season)
- {
- if (int.TryParse(season, out _)) return season;
-
- switch (season)
- {
- case "零": return "0";
- 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;
- }
-
- public bool DoParse(TreeNode node)
- {
- var item = node.Info;
- var name = item.Name();
- var matches = Regex.Matches(name, @"[^\[\]_]+").Select(match => match.Value).ToArray();
-
- if (matches.Length == 1)
- {
- node.Info.SetRawTitle(matches[0]);
- return true;
- }
-
- if (matches.Length >= 2)
- {
- node.Info.SetGroupIfNotExist(matches[0]);
-
- var title = matches[1].Trim();
- var match = Regex.Match(title, "(.+)第(.+)季");
- if (match.Success)
- {
- node.Info.SetSeasonIfNotExist(ParseSeason(match.Groups[2].Value.Trim()));
- node.Info.SetRawTitle(match.Groups[1].Value.Trim());
- }
- else
- {
- node.Info.SetRawTitle(title);
- }
- }
-
-
- return true;
- }
-}
\ No newline at end of file
diff --git a/Parsers/NormalParser.cs.uid b/Parsers/NormalParser.cs.uid
deleted file mode 100644
index c482b65..0000000
--- a/Parsers/NormalParser.cs.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bfixayy5gpwd6
diff --git a/Parsers/TMDBParser.cs b/Parsers/TMDBParser.cs
new file mode 100644
index 0000000..6fd0b17
--- /dev/null
+++ b/Parsers/TMDBParser.cs
@@ -0,0 +1,141 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Godot;
+using Learn.Config;
+using Learn.Models;
+using Learn.Utils;
+using TMDbLib.Client;
+using TMDbLib.Objects.Search;
+
+namespace Learn.Parsers;
+
+public class TMDBParser(Configs configs) : ItemParser
+{
+ private readonly Dictionary _cache = new();
+
+ private TMDbClient _client;
+
+ private async Task QueryTMDB(string title)
+ {
+ if (string.IsNullOrEmpty(title)) return null;
+
+ if (_cache.TryGetValue(title, out var result))
+ {
+ return result;
+ }
+
+ var results = (await GetTMDbClient().SearchTvShowAsync(title, language: "zh-CN")).Results;
+ result = results.FirstOrDefault();
+ _cache[title] = result;
+ return result;
+ }
+
+ private TMDbClient GetTMDbClient()
+ {
+ if (_client != null) return _client;
+
+ var apiKey = configs.Get().ApiKey;
+ var proxy = configs.Get().HttpProxy;
+
+ if (string.IsNullOrEmpty(proxy))
+ {
+ _client = new TMDbClient(apiKey);
+ }
+ else
+ {
+ _client = new TMDbClient(apiKey , proxy: new WebProxy(proxy));
+ }
+
+ return _client;
+ }
+
+ private string ParseSeason(string season)
+ {
+ if (int.TryParse(season, out _)) return season;
+
+ switch (season)
+ {
+ case "零": return "0";
+ 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;
+ }
+
+ private bool DoRawParse(TreeNode node)
+ {
+ var item = node.Info;
+ var name = item.Name();
+ var matches = Regex.Matches(name, @"[^\[\]_]+").Select(match => match.Value).ToArray();
+
+ if (matches.Length == 0) return false;
+
+ if (matches.Length == 1)
+ {
+ node.Info.SetRawTitleIfNotExist(matches[0]);
+ return true;
+ }
+
+ if (matches.Length >= 2)
+ {
+ node.Info.SetGroupIfNotExist(matches[0]);
+
+ var title = matches[1].Trim();
+ var match = Regex.Match(title, "(.+)第(.+)季");
+ if (match.Success)
+ {
+ node.Info.SetSeasonIfNotExist(ParseSeason(match.Groups[2].Value.Trim()));
+ node.Info.SetRawTitleIfNotExist(match.Groups[1].Value.Trim());
+ }
+ else
+ {
+ node.Info.SetRawTitleIfNotExist(title);
+ }
+ }
+
+ return true;
+ }
+
+ public async Task DoParse(TreeNode node)
+ {
+ if (!node.TryGetValue(ItemFields.Key_RawTitle, out var rawTitle, out _))
+ {
+ if (!DoRawParse(node))
+ {
+ return false;
+ }
+ node.TryGetValue(ItemFields.Key_RawTitle, out rawTitle, out _);
+ }
+
+ if (string.IsNullOrEmpty(rawTitle))
+ {
+ return false;
+ }
+
+ var result = await QueryTMDB(rawTitle);
+ if (result == null)
+ {
+ GD.PrintErr($"找不到对应的TV:{rawTitle}");
+ return false;
+ }
+
+ if (result.FirstAirDate != null)
+ {
+ node.Info.SetYear(result.FirstAirDate.Value.Year);
+ }
+
+ node.Info.SetTitleIfNotExist(result.Name);
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Parsers/TMDBParser.cs.uid b/Parsers/TMDBParser.cs.uid
new file mode 100644
index 0000000..550d763
--- /dev/null
+++ b/Parsers/TMDBParser.cs.uid
@@ -0,0 +1 @@
+uid://bw1sbx8ay4qsw
diff --git a/Utils/Configs.cs b/Utils/Configs.cs
new file mode 100644
index 0000000..13a1cb5
--- /dev/null
+++ b/Utils/Configs.cs
@@ -0,0 +1,184 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Learn.Utils;
+
+public class ConfigItemAttribute(string name) : Attribute
+{
+ public readonly string Name = name;
+}
+
+public interface IConfigItem
+{
+ public void OnBeforeSave()
+ {
+ }
+
+ public void OnAfterLoad()
+ {
+ }
+}
+
+public sealed class Configs
+{
+ private static readonly ConcurrentDictionary Cache = new();
+ private readonly ConcurrentDictionary _configObjects = new();
+ private readonly ConcurrentDictionary _configs = new();
+
+ public string ConfigPath { get; set; } = string.Empty;
+
+ private static string GetConfigItemName(Type type)
+ {
+ if (Cache.TryGetValue(type, out var name)) return name;
+ name = type.Name;
+ if(type.GetCustomAttribute(typeof(ConfigItemAttribute)) is ConfigItemAttribute info)
+ {
+ name = string.IsNullOrEmpty(info.Name) ? type.Name : info.Name;
+ }
+ Cache[type] = name;
+ return name;
+ }
+
+ ///
+ /// 获取配置项对象
+ ///
+ /// 配置项类
+ /// 获取的配置项对象是单例的
+ /// 配置项对象
+ public T Get() where T : class, IConfigItem, new()
+ {
+ var name = GetConfigItemName(typeof(T));
+ if (_configObjects.TryGetValue(name, out var value))
+ {
+ return (T) value;
+ }
+ T result;
+ if (_configs.TryGetValue(name, out var jObject))
+ {
+ result = jObject.ToObject();
+ result.OnAfterLoad();
+ }
+ else
+ {
+ result = new T();
+ }
+ _configObjects[name] = result;
+ return result;
+ }
+
+ ///
+ /// 重置某个配置项。重置完后再取就是一个新的配置项
+ ///
+ /// 配置项类
+ public void Reset() where T : class, IConfigItem, new()
+ {
+ var name = GetConfigItemName(typeof(T));
+ _configs.TryRemove(name, out _);
+ _configObjects.Remove(name, out _);
+ }
+
+ ///
+ /// 清空当前配置并重新从文件中加载配置
+ ///
+ /// 当不存在配置文件路径创建默认配置
+ /// 加载是否成功。无论是否加载成功,当前配置都会被清空
+ public bool Load(bool createDefaultIfNotExist)
+ {
+ _configs.Clear();
+ _configObjects.Clear();
+ if (string.IsNullOrEmpty(ConfigPath))
+ {
+ return false;
+ }
+
+ if (!File.Exists(ConfigPath))
+ {
+ if (!createDefaultIfNotExist) return false;
+ if (!CreateEmptyConfig(ConfigPath)) return false;
+ }
+
+ try
+ {
+ var configJson = File.ReadAllText(ConfigPath);
+ var config = JObject.Parse(configJson);
+ foreach (var kv in config)
+ {
+ _configs[kv.Key] = kv.Value.ToObject();
+ }
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ ///
+ /// 将当前配置保存到文件中
+ ///
+ ///
+ public bool Save()
+ {
+ if (string.IsNullOrEmpty(ConfigPath)) return false;
+ try
+ {
+ foreach (var config in _configObjects)
+ {
+ config.Value.OnBeforeSave();
+ _configs[config.Key] = JObject.FromObject(config.Value);
+ }
+
+ File.WriteAllText(ConfigPath, JsonConvert.SerializeObject(_configs, Formatting.Indented));
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ private static bool CreateEmptyConfig(string filePath)
+ {
+ var configItemTypes = AppDomain.CurrentDomain.GetAssemblies()
+ .SelectMany(assembly =>
+ {
+ try
+ {
+ return assembly.GetTypes();
+ }
+ catch (Exception)
+ {
+ return Array.Empty();
+ }
+ })
+ .Where(type => type.IsClass &&
+ !type.IsAbstract &&
+ typeof(IConfigItem).IsAssignableFrom(type))
+ .ToList();
+
+ var configs = new Dictionary();
+ foreach (var type in configItemTypes)
+ {
+ var name = GetConfigItemName(type);
+ var item = (IConfigItem) Activator.CreateInstance(type);
+ item.OnBeforeSave();
+ configs[name] = item;
+
+ }
+ try
+ {
+ File.WriteAllText(filePath, JsonConvert.SerializeObject(configs, Formatting.Indented));
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Utils/Configs.cs.uid b/Utils/Configs.cs.uid
new file mode 100644
index 0000000..c754160
--- /dev/null
+++ b/Utils/Configs.cs.uid
@@ -0,0 +1 @@
+uid://b8ta3kj0ksopd