diff --git a/BangumiRenamer.csproj b/BangumiRenamer.csproj index 6c442a9..9604884 100644 --- a/BangumiRenamer.csproj +++ b/BangumiRenamer.csproj @@ -21,6 +21,7 @@ + diff --git a/Src/Tools/EmbededResourceViewer.cs b/Src/Tools/EmbededResourceViewer.cs index 3314497..7704d51 100644 --- a/Src/Tools/EmbededResourceViewer.cs +++ b/Src/Tools/EmbededResourceViewer.cs @@ -4,6 +4,14 @@ namespace BangumiRenamer.Tools; public class EmbededResourceViewer { + private static readonly Lazy _lazy = new (() => + { + var log = new Log(new Log.LogConfig()); + if (!log.Init()) return null; + return log; + }); + static ILog Log => _lazy.Value; + public static void PrintResourceNames() { var names = ResourceLoader.GetAllResNames(); diff --git a/Src/Tools/FolderCloner.cs b/Src/Tools/FolderCloner.cs index da3b8d4..aa6d183 100644 --- a/Src/Tools/FolderCloner.cs +++ b/Src/Tools/FolderCloner.cs @@ -6,6 +6,14 @@ namespace BangumiRenamer.Tools; public static class FolderCloner { + private static readonly Lazy _lazy = new (() => + { + var log = new Log(new Log.LogConfig()); + if (!log.Init()) return null; + return log; + }); + static ILog Log => _lazy.Value; + public static void Run() { var result = Dialog.FolderPicker(); diff --git a/Src/Tools/ShowCompletionChecker.cs b/Src/Tools/ShowCompletionChecker.cs index f33d296..65451c5 100644 --- a/Src/Tools/ShowCompletionChecker.cs +++ b/Src/Tools/ShowCompletionChecker.cs @@ -13,6 +13,22 @@ using System.Text.RegularExpressions; public static class ShowCompletionChecker { + private static readonly Lazy _lazyLog = new (() => + { + var log = new Log(new Log.LogConfig()); + if (!log.Init()) return null; + return log; + }); + static ILog Log => _lazyLog.Value; + + private static readonly Lazy _lazyConfig = new (() => + { + var config = new Config("config.json"); + if (!config.Load()) return null; + return config; + }); + static IConfig Config => _lazyConfig.Value; + static List FindShows(string checkPath) { var shows = new List(); @@ -59,8 +75,8 @@ public static class ShowCompletionChecker Log.Info($"Total Shows: {shows.Count}"); var client = new TMDbClient( - apiKey: Config.Default.Get().ApiKey , - proxy: new WebProxy(Config.Default.Get().HttpProxy)); + apiKey: Config.Get().ApiKey , + proxy: new WebProxy(Config.Get().HttpProxy)); var output = new StringBuilder(); foreach (var t in shows) diff --git a/Src/Utils/Config.cs b/Src/Utils/Config.cs index d785809..35979ba 100644 --- a/Src/Utils/Config.cs +++ b/Src/Utils/Config.cs @@ -1,35 +1,44 @@ +using System.Collections.Concurrent; +using System.Diagnostics; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace BangumiRenamer.Utils; -public class ConfigItemAttribute : System.Attribute +public class ConfigItemAttribute(string name) : Attribute { - public string Name; + public readonly string Name = name; +} - public ConfigItemAttribute(string name) +public interface IConfigItem +{ + public void BeforeSave() + { + } + + public void AfterLoad() { - Name = name; } } -public interface IConfigItem; - -public class Config(string configPath) +public interface IConfig { - private static readonly Lazy _lazy = new (() => - { - var config = new Config("config.json"); - config.ReLoad(); - return config; - }); - public static Config Default => _lazy.Value; + public T Get() where T : class, IConfigItem, new(); + public void Reset() where T : class, IConfigItem, new(); + + public bool Save(); +} + +public sealed class Config(string configPath) : IConfig +{ + public IConfig G => _G; + public Config _G { private get; set; } - private static readonly Dictionary Cache = new(); - private readonly Dictionary _configObjects = new(); - private readonly Dictionary _configs = new(); + private static readonly ConcurrentDictionary Cache = new(); + private readonly ConcurrentDictionary _configObjects = new(); + private readonly ConcurrentDictionary _configs = new(); private static string GetConfigItemName(Type type) { @@ -54,6 +63,7 @@ public class Config(string configPath) if (_configs.TryGetValue(name, out var jObject)) { result = jObject.ToObject(); + result.AfterLoad(); } else { @@ -66,36 +76,49 @@ public class Config(string configPath) public void Reset() where T : class, IConfigItem, new() { var name = GetConfigItemName(typeof(T)); - _configs.Remove(name); - _configObjects.Remove(name); + _configs.TryRemove(name, out _); + _configObjects.Remove(name, out _); } - public void Clear() + public bool Load() { _configs.Clear(); _configObjects.Clear(); + if (!File.Exists(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 void ReLoad() + public bool Save() { - _configs.Clear(); - _configObjects.Clear(); - if (!File.Exists(configPath)) return; - var configJson = File.ReadAllText(configPath); - var config = JObject.Parse(configJson); - foreach (var kv in config) + if (string.IsNullOrEmpty(configPath)) return false; + try { - _configs[kv.Key] = kv.Value.ToObject(); - } - } + foreach (var config in _configObjects) + { + config.Value.BeforeSave(); + _configs[config.Key] = JObject.FromObject(config.Value); + } - public void Save() - { - foreach (var config in _configObjects) - { - _configs[config.Key] = JObject.FromObject(config.Value); + File.WriteAllText(configPath, JsonConvert.SerializeObject(_configs, Formatting.Indented)); } - File.WriteAllText(configPath, JsonConvert.SerializeObject(_configs, Formatting.Indented)); + catch (Exception) + { + return false; + } + return true; } public static void CreateEmptyConfig() @@ -116,13 +139,23 @@ public class Config(string configPath) !type.IsAbstract && typeof(IConfigItem).IsAssignableFrom(type)) .ToList(); - - var configs = new Dictionary(); + + var filePath = "config.json"; + var config = new Config(filePath); foreach (var type in configItemTypes) { var name = GetConfigItemName(type); - configs[name] = JObject.FromObject(Activator.CreateInstance(type)); + config._configObjects[name] = (IConfigItem) Activator.CreateInstance(type); + } + config.Save(); + + if (File.Exists(filePath)) + { + // 使用 explorer.exe 的 /select 命令行参数 + string argument = $"/select,\"{filePath}\""; + + // 启动 explorer.exe 进程 + Process.Start("explorer.exe", argument); } - File.WriteAllText("config_default.json", JsonConvert.SerializeObject(configs, Formatting.Indented)); } } \ No newline at end of file diff --git a/Src/Utils/Log.cs b/Src/Utils/Log.cs index 274760c..94475bd 100644 --- a/Src/Utils/Log.cs +++ b/Src/Utils/Log.cs @@ -1,162 +1,169 @@ +using System.Runtime.CompilerServices; +using Newtonsoft.Json; using Serilog; +using Serilog.Context; using Serilog.Core; using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; namespace BangumiRenamer.Utils; -public sealed class Log +public interface ILog { - private static readonly Lazy _instance = new Lazy(() => new Log()); + public void Debug(string message, [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = ""); + + public void Info(string message, [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = ""); + + public void Warn(string message, [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = ""); + + public void Error(string message, [CallerFilePath] string callerFilePath = "", + [CallerMemberName] string callerMemberName = ""); + + public void Error(Exception ex, string message); +} + +public sealed class Log : ILog +{ + public enum LogLevel + { + Error, + Warn, + Info, + Debug, + } + + [ConfigItem("Log")] + public class LogConfig : IConfigItem + { + [JsonIgnore] public LogLevel Level = LogLevel.Debug; + [JsonProperty("Level")] private string _level; + + public bool ToConsole = true; + + public bool ToFile = false; + public string LogFilePath; + + public void AfterLoad() + { + if (!string.IsNullOrEmpty(_level)) LogLevel.TryParse(_level, out Level); + } + + public void BeforeSave() + { + _level = Level.ToString(); + } + } + + public static ILog G => _G; + public static Log _G { private get; set; } + + private LogConfig _config; + private Logger _logger; - private bool _isInitialized = false; - private readonly object _lockObject = new object(); - - public static Log Instance => _instance.Value; - public static ILogger Logger => Instance._logger; - private Log() { } - - /// - /// 初始化日志配置 - /// - public static void Initialize(Action configure = null) + public Log(LogConfig config) { - Instance.InitializeInternal(configure); + _config = config; } - - private void InitializeInternal(Action configure = null) + + public bool Init() { - lock (_lockObject) + if (_config == null) return false; + var config = new LoggerConfiguration(); + switch (_config.Level) { - if (_isInitialized) + case LogLevel.Error: + config.MinimumLevel.Error(); + break; + case LogLevel.Warn: + config.MinimumLevel.Warning(); + break; + case LogLevel.Debug: + config.MinimumLevel.Debug(); + break; + case LogLevel.Info: + default: + config.MinimumLevel.Information(); + break; + } + + var outputTemplate = "[{Timestamp:HH:mm:ss}][{CallerPlace}][{Level:u3}] {Message:lj}{NewLine}{Exception}"; + + if (_config.ToConsole) + { + config.WriteTo.Console( + outputTemplate: outputTemplate, + theme: AnsiConsoleTheme.Sixteen + ); + } + + if (_config.ToFile) + { + if (string.IsNullOrEmpty(_config.LogFilePath)) { - Warn("日志系统已经初始化过"); - return; + return false; } - - var config = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.Console( - outputTemplate: "[{Timestamp:HH:mm:ss}][{Level:u3}] {Message:lj}{NewLine}{Exception}", - theme: AnsiConsoleTheme.Sixteen - ) - .Enrich.FromLogContext() - .Enrich.WithThreadId(); - - configure?.Invoke(config); - + config.WriteTo.File( + outputTemplate: outputTemplate, + path: _config.LogFilePath + ); + } + + config.Enrich.FromLogContext() + .Enrich.WithThreadId(); + try + { _logger = config.CreateLogger(); - _isInitialized = true; - - Info("日志系统初始化完成"); } + catch (Exception) + { + return false; + } + return true; } - + + // 静态快捷方法 - 使用更简短的名称 - public static void Debug(string message, params object[] properties) - => Instance.EnsureAndLog(LogEventLevel.Debug, message, properties); - - public static void Info(string message, params object[] properties) - => Instance.EnsureAndLog(LogEventLevel.Information, message, properties); - - public static void Warn(string message, params object[] properties) - => Instance.EnsureAndLog(LogEventLevel.Warning, message, properties); - - public static void Error(string message, params object[] properties) - => Instance.EnsureAndLog(LogEventLevel.Error, message, properties); - - public static void Error(Exception ex, string message, params object[] properties) - => Instance.EnsureAndLog(ex, message, properties); - - public static void Fatal(string message, params object[] properties) - => Instance.EnsureAndLog(LogEventLevel.Fatal, message, properties); - - // 实例方法 - private void EnsureAndLog(LogEventLevel level, string message, object[] properties) + public void Debug(string message, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMemberName = "") { - EnsureInitialized(); - _logger.Write(level, message, properties); - } - - private void EnsureAndLog(Exception ex, string message, object[] properties) - { - EnsureInitialized(); - _logger.Error(ex, message, properties); - } - - /// - /// 为特定类型创建Logger - /// - public static ILogger ForContext() - { - Instance.EnsureInitialized(); - return Instance._logger.ForContext(); - } - - /// - /// 为特定源创建Logger - /// - public static ILogger ForContext(string source) - { - Instance.EnsureInitialized(); - return Instance._logger.ForContext("SourceContext", source); - } - - /// - /// 开始一个带属性的日志上下文 - /// - public static IDisposable BeginScope(string propertyName, object value) - { - Instance.EnsureInitialized(); - return Serilog.Context.LogContext.PushProperty(propertyName, value); - } - - /// - /// 开始多个属性的日志上下文 - /// - public static IDisposable BeginScope(params (string Name, object Value)[] properties) - { - Instance.EnsureInitialized(); - var disposables = properties.Select(p => - Serilog.Context.LogContext.PushProperty(p.Name, p.Value)).ToArray(); - return new DisposableGroup(disposables); - } - - /// - /// 关闭并刷新日志 - /// - public static void Close() - { - Instance._logger?.Dispose(); - Serilog.Log.CloseAndFlush(); - Instance._isInitialized = false; - } - - private void EnsureInitialized() - { - if (!_isInitialized) + using (LogContext.PushProperty("CallerPlace", Path.GetFileNameWithoutExtension(callerFilePath) + "::" + callerMemberName)) { - InitializeInternal(); + _logger.Write(LogEventLevel.Debug, message); } } - // 辅助类:用于同时释放多个IDisposable - private class DisposableGroup : IDisposable + public void Info(string message, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMemberName = "") { - private readonly IDisposable[] _disposables; - - public DisposableGroup(IDisposable[] disposables) + using (LogContext.PushProperty("CallerPlace", Path.GetFileNameWithoutExtension(callerFilePath) + "::" + callerMemberName)) { - _disposables = disposables; + _logger.Write(LogEventLevel.Information, message); } - - public void Dispose() + } + + public void Warn(string message, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMemberName = "") + { + using (LogContext.PushProperty("CallerPlace", Path.GetFileNameWithoutExtension(callerFilePath) + "::" + callerMemberName)) { - foreach (var disposable in _disposables) - { - disposable?.Dispose(); - } + _logger.Write(LogEventLevel.Warning, message); } } + + public void Error(string message, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMemberName = "") + { + using (LogContext.PushProperty("CallerPlace", Path.GetFileNameWithoutExtension(callerFilePath) + "::" + callerMemberName)) + { + _logger.Write(LogEventLevel.Error, message); + } + } + + public void Error(Exception ex, string message) + => WriteLog(ex, message); + + private void WriteLog(Exception ex, string message) + { + _logger.Error(ex, message); + } } \ No newline at end of file