diff --git a/BangumiRenamer.csproj b/BangumiRenamer.csproj
index 56073f6..eb83632 100644
--- a/BangumiRenamer.csproj
+++ b/BangumiRenamer.csproj
@@ -18,6 +18,7 @@
+
diff --git a/Src/Config/Attribute/ConfigItemAttribute.cs b/Src/Config/Attribute/ConfigItemAttribute.cs
new file mode 100644
index 0000000..4d25ff9
--- /dev/null
+++ b/Src/Config/Attribute/ConfigItemAttribute.cs
@@ -0,0 +1,11 @@
+namespace BangumiRenamer.Config;
+
+public class ConfigItemAttribute : System.Attribute
+{
+ public string Name;
+
+ public ConfigItemAttribute(string name)
+ {
+ Name = name;
+ }
+}
\ No newline at end of file
diff --git a/Src/ConfigManager/ConfigManager.cs b/Src/Config/Config.cs
similarity index 51%
rename from Src/ConfigManager/ConfigManager.cs
rename to Src/Config/Config.cs
index 886cbde..498063e 100644
--- a/Src/ConfigManager/ConfigManager.cs
+++ b/Src/Config/Config.cs
@@ -1,79 +1,115 @@
-using System.Reflection;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace BangumiRenamer.ConfigManager;
-
-public class ConfigManager(string configPath)
-{
- private static readonly Dictionary Cache = new();
- private readonly Dictionary _configObjects = new();
- private readonly Dictionary _configs = new();
-
- private static string GetConfigName(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, new()
- {
- var name = GetConfigName(typeof(T));
- if (_configObjects.TryGetValue(name, out var value))
- {
- return (T) value;
- }
- T result;
- if (_configs.TryGetValue(name, out var jObject))
- {
- result = jObject.ToObject();
- }
- else
- {
- result = new T();
- }
- _configObjects[name] = result;
- return result;
- }
-
- public void Reset()
- {
- var name = GetConfigName(typeof(T));
- _configs.Remove(name);
- _configObjects.Remove(name);
- }
-
- public void Clear()
- {
- _configs.Clear();
- _configObjects.Clear();
- }
-
- public void Load()
- {
- _configs.Clear();
- _configObjects.Clear();
- if (!File.Exists(configPath)) return;
- var configJson = File.ReadAllText(configPath);
- var config = JObject.Parse(configJson);
- foreach (var kv in config)
- {
- _configs[kv.Key] = kv.Value.ToObject();
- }
- }
-
- public void Save()
- {
- foreach (var config in _configObjects)
- {
- _configs[config.Key] = JObject.FromObject(config.Value);
- }
- File.WriteAllText(configPath, JsonConvert.SerializeObject(_configs, Formatting.Indented));
- }
+namespace BangumiRenamer.Config;
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+public class Config(string configPath)
+{
+ private static readonly Lazy _lazy = new (() =>
+ {
+ var config = new Config("config.json");
+ config.ReLoad();
+ return config;
+ });
+ public static Config Default => _lazy.Value;
+
+
+ private static readonly Dictionary Cache = new();
+ private readonly Dictionary _configObjects = new();
+ private readonly Dictionary _configs = new();
+
+ 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();
+ }
+ else
+ {
+ result = new T();
+ }
+ _configObjects[name] = result;
+ return result;
+ }
+
+ public void Reset() where T : class, IConfigItem, new()
+ {
+ var name = GetConfigItemName(typeof(T));
+ _configs.Remove(name);
+ _configObjects.Remove(name);
+ }
+
+ public void Clear()
+ {
+ _configs.Clear();
+ _configObjects.Clear();
+ }
+
+ public void ReLoad()
+ {
+ _configs.Clear();
+ _configObjects.Clear();
+ if (!File.Exists(configPath)) return;
+ var configJson = File.ReadAllText(configPath);
+ var config = JObject.Parse(configJson);
+ foreach (var kv in config)
+ {
+ _configs[kv.Key] = kv.Value.ToObject();
+ }
+ }
+
+ public void Save()
+ {
+ foreach (var config in _configObjects)
+ {
+ _configs[config.Key] = JObject.FromObject(config.Value);
+ }
+ File.WriteAllText(configPath, JsonConvert.SerializeObject(_configs, Formatting.Indented));
+ }
+
+ public static void CreateEmptyConfig()
+ {
+ var configItemTypes = AppDomain.CurrentDomain.GetAssemblies()
+ .SelectMany(assembly =>
+ {
+ try
+ {
+ return assembly.GetTypes();
+ }
+ catch (ReflectionTypeLoadException)
+ {
+ 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);
+ configs[name] = JObject.FromObject(Activator.CreateInstance(type));
+ }
+ File.WriteAllText("config_default.json", JsonConvert.SerializeObject(configs, Formatting.Indented));
+ }
}
\ No newline at end of file
diff --git a/Src/Config/ConfigSchema/ProxyConfig.cs b/Src/Config/ConfigSchema/ProxyConfig.cs
new file mode 100644
index 0000000..0e22646
--- /dev/null
+++ b/Src/Config/ConfigSchema/ProxyConfig.cs
@@ -0,0 +1,7 @@
+namespace BangumiRenamer.Config;
+
+[ConfigItem("Proxy")]
+public class ProxyConfig : IConfigItem
+{
+ public string HttpProxy = "";
+}
\ No newline at end of file
diff --git a/Src/Config/ConfigSchema/TMDBConfig.cs b/Src/Config/ConfigSchema/TMDBConfig.cs
new file mode 100644
index 0000000..1943e7d
--- /dev/null
+++ b/Src/Config/ConfigSchema/TMDBConfig.cs
@@ -0,0 +1,7 @@
+namespace BangumiRenamer.Config;
+
+[ConfigItem("TMDB")]
+public class TMDBConfig : IConfigItem
+{
+ public string ApiKey = "";
+}
\ No newline at end of file
diff --git a/Src/Config/IConfigItem.cs b/Src/Config/IConfigItem.cs
new file mode 100644
index 0000000..b0c4aa5
--- /dev/null
+++ b/Src/Config/IConfigItem.cs
@@ -0,0 +1,3 @@
+namespace BangumiRenamer.Config;
+
+public interface IConfigItem;
\ No newline at end of file
diff --git a/Src/ConfigManager/Attribute/ConfigItemAttribute.cs b/Src/ConfigManager/Attribute/ConfigItemAttribute.cs
deleted file mode 100644
index 082e142..0000000
--- a/Src/ConfigManager/Attribute/ConfigItemAttribute.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace BangumiRenamer.ConfigManager;
-
-public class ConfigItemAttribute : Attribute
-{
- public string Name;
-
- public ConfigItemAttribute()
- {
- }
-
- public ConfigItemAttribute(string name)
- {
- Name = name;
- }
-}
\ No newline at end of file
diff --git a/Src/Entity/Show.cs b/Src/Entity/Show.cs
new file mode 100644
index 0000000..efdbbee
--- /dev/null
+++ b/Src/Entity/Show.cs
@@ -0,0 +1,13 @@
+namespace BangumiRenamer.Data;
+
+public class Show
+{
+ public string title;
+ public List sessions;
+}
+
+public class Session
+{
+ public int id;
+ public List episodes;
+}
diff --git a/Src/Tools/ShowCompletionChecker.cs b/Src/Tools/ShowCompletionChecker.cs
new file mode 100644
index 0000000..e42ab41
--- /dev/null
+++ b/Src/Tools/ShowCompletionChecker.cs
@@ -0,0 +1,95 @@
+namespace BangumiRenamer.Tools;
+
+using Config;
+using TMDbLib.Client;
+using Data;
+using NativeFileDialogSharp;
+using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
+
+public static class ShowCompletionChecker
+{
+ static List FindShows(string checkPath)
+ {
+ var shows = new List();
+ var showPaths = Directory.GetDirectories(checkPath);
+ foreach (var showPath in showPaths)
+ {
+ var sessions = new List();
+ var sessionPaths = Directory.GetDirectories(showPath);
+ foreach (var sessionPath in sessionPaths)
+ {
+ var episodePaths = Directory.GetFiles(sessionPath);
+ HashSet episodes = new HashSet();
+ foreach (var episodePath in episodePaths)
+ {
+ var matches = Regex.Matches(episodePath, @".*S\d+E(\d+).*");
+ if (matches.Count == 0) continue;
+ var episode = int.Parse(matches[0].Groups[1].Value);
+ episodes.Add(episode);
+ }
+ var session = new Session
+ {
+ id = int.Parse(Path.GetFileName(sessionPath).Replace("Season ", "")),
+ episodes = episodes.ToList()
+ };
+ sessions.Add(session);
+ }
+ shows.Add(new Show
+ {
+ title = Path.GetFileName(showPath),
+ sessions = sessions
+ });
+ }
+ return shows;
+ }
+
+
+ public static void Run()
+ {
+ var dir = Dialog.FolderPicker();
+ if (!dir.IsOk) return;
+ var checkPath = dir.Path;
+
+ var shows = FindShows(checkPath);
+ Console.WriteLine($"Total Shows: {shows.Count}");
+
+ var client = new TMDbClient(
+ apiKey: Config.Default.Get().ApiKey ,
+ proxy: new WebProxy(Config.Default.Get().HttpProxy));
+
+ var output = new StringBuilder();
+ foreach (var t in shows)
+ {
+ var match = Regex.Match(t.title, @"(.*) \((\d{4})\)");
+ var title = match.Groups[1].Value;
+ var year = int.Parse(match.Groups[2].Value);
+ var result = client.SearchTvShowAsync(title, firstAirDateYear: year).Result;
+ var info = result.Results.FirstOrDefault();
+ if (info == null)
+ {
+ Console.WriteLine($"找不到对应的TV:{t.title}");
+ continue;
+ }
+ foreach (var session in t.sessions)
+ {
+ var sessionInfo = client.GetTvSeasonAsync(info.Id, session.id).Result;
+ if (sessionInfo == null)
+ {
+ Console.WriteLine($"季度对不上,可能找错了:{t.title} -> {info.OriginalName}");
+ break;
+ }
+ foreach (var episode in sessionInfo.Episodes)
+ {
+ if (DateTime.Now.AddDays(-7) < episode.AirDate) continue;
+ if (!session.episodes.Contains(episode.EpisodeNumber))
+ {
+ output.AppendLine($"{title} 的第 {session.id} 季少了第 {episode.EpisodeNumber} 集");
+ }
+ }
+ }
+ }
+ Console.Write(output);
+ }
+}
\ No newline at end of file