diff --git a/EpisodeRenamer.csproj b/EpisodeRenamer.csproj index f92d1b6..9c4d12a 100644 --- a/EpisodeRenamer.csproj +++ b/EpisodeRenamer.csproj @@ -48,6 +48,7 @@ + diff --git a/KeyListener.cs b/KeyListener.cs new file mode 100644 index 0000000..fee87a5 --- /dev/null +++ b/KeyListener.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace EpisodeRenamer +{ + /// + /// Класс для обработки горячих клавиш с использованием Task + /// + public class HotkeyListener : IDisposable + { + #region Windows API Imports + [DllImport("user32.dll")] + private static extern short GetAsyncKeyState(int vKey); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + #endregion + + #region Key Codes + private static class VirtualKeyCodes + { + public const int VK_CONTROL = 0x11; + public const int VK_ALT = 0x12; + public const int VK_SHIFT = 0x10; + public const int VK_R = 0x52; + public const int VK_ESCAPE = 0x1B; + // Добавьте другие коды клавиш по необходимости + } + #endregion + + #region Events and Delegates + /// + /// Делегат для события нажатия комбинации клавиш + /// + public delegate void HotkeyEventHandler(string combinationName); + + /// + /// Событие, возникающее при нажатии зарегистрированной комбинации клавиш + /// + public event HotkeyEventHandler OnHotkeyPressed; + + /// + /// Событие, возникающее при ошибке в работе слушателя + /// + public event Action OnError; + #endregion + + #region Private Fields + private readonly List _keyCombinations; + private CancellationTokenSource _cancellationTokenSource; + private Task _listenerTask; + private bool _isDisposed; + private readonly object _lockObject = new object(); + private bool _isRunning; + #endregion + + #region Key Combination Class + /// + /// Класс для описания комбинации клавиш + /// + private class KeyCombination + { + public string Name { get; set; } + public int[] KeyCodes { get; set; } + public int CooldownMs { get; set; } = 300; + public DateTime LastTriggered { get; set; } = DateTime.MinValue; + } + #endregion + + #region Constructor + /// + /// Создает новый экземпляр слушателя горячих клавиш + /// + public HotkeyListener() + { + _keyCombinations = new List(); + _cancellationTokenSource = new CancellationTokenSource(); + + // Добавляем комбинацию по умолчанию + RegisterCombination("ControlAltR", + new[] { VirtualKeyCodes.VK_CONTROL, VirtualKeyCodes.VK_ALT, VirtualKeyCodes.VK_R }); + } + #endregion + + #region Public Methods + /// + /// Регистрирует новую комбинацию клавиш для отслеживания + /// + /// Имя комбинации + /// Коды клавиш (все должны быть нажаты одновременно) + /// Время задержки между срабатываниями (мс) + public void RegisterCombination(string name, int[] keyCodes, int cooldownMs = 300) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Имя комбинации не может быть пустым", nameof(name)); + + if (keyCodes == null || keyCodes.Length == 0) + throw new ArgumentException("Массив кодов клавиш не может быть пустым", nameof(keyCodes)); + + lock (_lockObject) + { + var existing = _keyCombinations.Find(k => k.Name == name); + if (existing != null) + { + _keyCombinations.Remove(existing); + } + + _keyCombinations.Add(new KeyCombination + { + Name = name, + KeyCodes = keyCodes, + CooldownMs = cooldownMs + }); + } + } + + /// + /// Удаляет комбинацию клавиш из отслеживания + /// + /// Имя комбинации + public void UnregisterCombination(string name) + { + lock (_lockObject) + { + var combination = _keyCombinations.Find(k => k.Name == name); + if (combination != null) + { + _keyCombinations.Remove(combination); + } + } + } + + /// + /// Запускает прослушивание горячих клавиш + /// + public async Task StartListeningAsync() + { + if (_isRunning) + return; + + lock (_lockObject) + { + if (_isRunning) + return; + + _cancellationTokenSource = new CancellationTokenSource(); + _isRunning = true; + } + + try + { + // Переводим консоль на передний план для лучшего захвата клавиш + BringConsoleToFront(); + + _listenerTask = Task.Run(() => ListenForHotkeys(_cancellationTokenSource.Token), + _cancellationTokenSource.Token); + + await Task.CompletedTask; + } + catch (Exception ex) + { + OnError?.Invoke(ex); + throw; + } + } + + /// + /// Останавливает прослушивание горячих клавиш + /// + public async Task StopListeningAsync() + { + if (!_isRunning) + return; + + lock (_lockObject) + { + if (!_isRunning) + return; + + _isRunning = false; + } + + try + { + _cancellationTokenSource?.Cancel(); + + if (_listenerTask != null && !_listenerTask.IsCompleted) + { + await _listenerTask.ContinueWith(t => + { + // Игнорируем исключения отмены задачи + if (t.IsFaulted) OnError?.Invoke(t.Exception); + }); + } + } + catch (OperationCanceledException) + { + // Ожидаемое исключение при отмене + } + catch (Exception ex) + { + OnError?.Invoke(ex); + throw; + } + } + + /// + /// Получает статус работы слушателя + /// + public bool IsListening => _isRunning; + + /// + /// Получает список зарегистрированных комбинаций + /// + public IReadOnlyList RegisteredCombinations + { + get + { + lock (_lockObject) + { + return _keyCombinations.ConvertAll(k => k.Name).AsReadOnly(); + } + } + } + #endregion + + #region Private Methods + private async Task ListenForHotkeys(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(10, cancellationToken); // Небольшая задержка для снижения нагрузки + + lock (_lockObject) + { + foreach (var combination in _keyCombinations) + { + if (IsCombinationPressed(combination.KeyCodes)) + { + // Проверяем cooldown + if ((DateTime.Now - combination.LastTriggered).TotalMilliseconds >= combination.CooldownMs) + { + combination.LastTriggered = DateTime.Now; + TriggerHotkeyEvent(combination.Name); + } + } + } + } + } + } + catch (OperationCanceledException) + { + // Ожидаемое исключение при отмене + } + catch (Exception ex) + { + OnError?.Invoke(ex); + } + } + + private bool IsCombinationPressed(int[] keyCodes) + { + foreach (var keyCode in keyCodes) + { + if (!IsKeyPressed(keyCode)) + return false; + } + return true; + } + + private bool IsKeyPressed(int keyCode) + { + return (GetAsyncKeyState(keyCode) & 0x8000) != 0; + } + + private void TriggerHotkeyEvent(string combinationName) + { + try + { + OnHotkeyPressed?.Invoke(combinationName); + } + catch (Exception ex) + { + OnError?.Invoke(new InvalidOperationException( + $"Ошибка при обработке события для комбинации {combinationName}", ex)); + } + } + + private void BringConsoleToFront() + { + try + { + var consoleHandle = GetConsoleWindow(); + SetForegroundWindow(consoleHandle); + } + catch (Exception ex) + { + // Не критичная ошибка, просто логируем + OnError?.Invoke(new InvalidOperationException("Не удалось перевести консоль на передний план", ex)); + } + } + #endregion + + #region IDisposable Implementation + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + StopListeningAsync().GetAwaiter().GetResult(); + _cancellationTokenSource?.Dispose(); + } + + _isDisposed = true; + } + } + + ~HotkeyListener() + { + Dispose(false); + } + #endregion + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index d1e8809..d099e25 100644 --- a/Program.cs +++ b/Program.cs @@ -1,185 +1,233 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text.RegularExpressions; -using Newtonsoft.Json; +using System.Threading.Tasks; -class EpisodeRenamer +namespace EpisodeRenamer { - private class PatternConfig + class EpisodeRenamer { - public bool Enabled { get; set; } - public string Regex { get; set; } - public int? Start { get; set; } - public int? End { get; set; } - public bool IgnoreCase { get; set; } - } - - private class Config - { - public List Patterns { get; set; } - public List Extensions { get; set; } - } - - private static List patterns; - private static HashSet extensions; - - private class Pattern - { - public Regex Regex { get; } - public int? Start { get; } - public int? End { get; } - - public Pattern(Regex regex, int? start, int? end) + private class PatternConfig { - Regex = regex; - Start = start; - End = end; - } - } - - static void Main() - { - Console.Title = Assembly.GetExecutingAssembly().GetCustomAttribute().Title; - - try - { - LoadConfiguration(); - } - catch (Exception ex) - { - Console.WriteLine($"Ошибка загрузки конфигурации: {ex.Message}"); - Console.WriteLine("Программа будет завершена."); - Console.ReadLine(); - return; + public bool Enabled { get; set; } + public string Regex { get; set; } + public int? Start { get; set; } + public int? End { get; set; } + public bool IgnoreCase { get; set; } } - Console.CancelKeyPress += (sender, e) => + private class Config { - Console.WriteLine("\nВыход из программы."); - Environment.Exit(0); - }; + public List Patterns { get; set; } + public List Extensions { get; set; } + } - Console.WriteLine("Чтобы оставить текущую директорию нажмите 'Enter'"); - Console.WriteLine("Чтобы выйти нажмите 'Ctrl + C'"); + private static List patterns; + private static HashSet extensions; - while (true) + private class Pattern { + public Regex Regex { get; } + public int? Start { get; } + public int? End { get; } + + public Pattern(Regex regex, int? start, int? end) + { + Regex = regex; + Start = start; + End = end; + } + } + + static async Task Main(string[] args) + { + // Создаем экземпляр слушателя + using (var hotkeyListener = new HotkeyListener()) + { + // Подписываемся на события + hotkeyListener.OnHotkeyPressed += (combination) => + { + switch (combination) + { + case "ControlAltR": + LoadConfiguration(true); + break; + case "ControlC": + Console.WriteLine("\nВыход из программы."); + Environment.Exit(0); + break; + default: + break; + } + }; + hotkeyListener.OnError += (ex) => Console.WriteLine($"Ошибка: {ex.Message}"); + hotkeyListener.RegisterCombination("ControlC", new[] { 0x11, 0x43 }); + + // Запускаем прослушивание + await hotkeyListener.StartListeningAsync(); + + // Основной цикл программы + await RunMainProgramLoop(); + + // Останавливаем слушатель (автоматически вызывается в Dispose) + await hotkeyListener.StopListeningAsync(); + } + } + + static async Task RunMainProgramLoop() + { + Console.Title = Assembly.GetExecutingAssembly().GetCustomAttribute().Title; + try { - Console.Write("\nВведите путь до папки с эпизодами: "); - string input = Console.ReadLine().Trim() ?? string.Empty; - - string folder = string.IsNullOrEmpty(input) - ? Directory.GetCurrentDirectory() - : Path.GetFullPath(input); - - if (Directory.Exists(folder)) - { - ProcessFolder(folder); - } - else - { - Console.WriteLine("Указанная папка не существует."); - } + LoadConfiguration(); } catch (Exception ex) { - Console.WriteLine($"Ошибка: {ex.Message}"); + Console.WriteLine($"Ошибка загрузки конфигурации: {ex.Message}"); + Console.WriteLine("Программа будет завершена."); + Console.ReadLine(); + return; } - } - } - private static void LoadConfiguration() - { - string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); + //Console.CancelKeyPress += (sender, e) => + //{ + // Console.WriteLine("\nВыход из программы."); + // Environment.Exit(0); + //}; - if (!File.Exists(configPath)) - { - throw new FileNotFoundException("Конфигурационный файл config.json не найден"); - } + Console.WriteLine("Чтобы оставить текущую директорию нажмите 'Enter'"); + Console.WriteLine("Чтобы перезагрузить конфигурацию нажмите 'Ctrl + Alt + R'"); + Console.WriteLine("Чтобы выйти нажмите 'Ctrl + C'"); - string json = File.ReadAllText(configPath); - Config config = JsonConvert.DeserializeObject(json); - - // Загружаем расширения файлов - extensions = new HashSet(config.Extensions, StringComparer.OrdinalIgnoreCase); - - // Загружаем паттерны - patterns = new List(); - foreach (PatternConfig patternConfig in config.Patterns) - { - if (!patternConfig.Enabled) - continue; - RegexOptions options = patternConfig.IgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None; - Regex regex = new Regex(patternConfig.Regex, options); - patterns.Add(new Pattern(regex, patternConfig.Start, patternConfig.End)); - } - - Console.WriteLine("Конфигурация успешно загружена."); - Console.WriteLine($"Загружено паттернов: {patterns.Count}; расширений: {extensions.Count}."); - } - - private static void ProcessFolder(string folder) - { - foreach (string filePath in Directory.GetFiles(folder)) - { - string fileName = Path.GetFileName(filePath); - string extension = Path.GetExtension(fileName); - - if (extensions.Contains(extension)) + while (true) { - RenameFile(filePath, fileName, folder); - } - } - } - - private static void RenameFile(string filePath, string fileName, string folder) - { - string nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); - - foreach (var pattern in patterns) - { - Match match = pattern.Regex.Match(nameWithoutExt); - if (!match.Success) - continue; - - string found = match.Value; - int startIndex = AdjustIndex(pattern.Start, found.Length); - int endIndex = AdjustIndex(pattern.End, found.Length); - - if (startIndex < 0 || endIndex < 0 || startIndex >= endIndex) - continue; - - string numberStr = found.Substring( - startIndex, - endIndex - startIndex - ); - - if (int.TryParse(numberStr, out int episode)) - { - string newName = $"{episode:D2}{Path.GetExtension(fileName)}"; - string newPath = Path.Combine(folder, newName); - - if (!File.Exists(newPath)) + try { - File.Move(filePath, newPath); - Console.WriteLine($"\"{fileName}\" успешно переименован в \"{newName}\"."); - return; - } + Console.Write("\nВведите путь до папки с эпизодами: "); + string input = Console.ReadLine() ?? string.Empty; + input = input.Trim(); - Console.WriteLine($"Ошибка: файл \"{newName}\" уже существует."); + string folder = string.IsNullOrEmpty(input) + ? Directory.GetCurrentDirectory() + : Path.GetFullPath(input); + + if (Directory.Exists(folder)) + { + ProcessFolder(folder); + } + else + { + Console.WriteLine("Указанная папка не существует."); + } + } + catch (Exception ex) + { + Console.WriteLine($"Ошибка: {ex.Message}"); + } } } - } - private static int AdjustIndex(int? index, int length) - { - if (!index.HasValue) - return length; - if (index.Value < 0) - return length + index.Value; - return index.Value; + private static void LoadConfiguration(bool reloadConfiguration = false) + { + if (reloadConfiguration) + { + extensions.Clear(); + patterns.Clear(); + Console.WriteLine("\nЗапрошена перезагрузка конфигурации."); + } + + string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); + + if (!File.Exists(configPath)) + { + throw new FileNotFoundException("Конфигурационный файл config.json не найден"); + } + + string json = File.ReadAllText(configPath); + Config config = JsonConvert.DeserializeObject(json); + + // Загружаем расширения файлов + extensions = new HashSet(config.Extensions, StringComparer.OrdinalIgnoreCase); + + // Загружаем паттерны + patterns = new List(); + foreach (PatternConfig patternConfig in config.Patterns) + { + if (!patternConfig.Enabled) + continue; + RegexOptions options = patternConfig.IgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None; + Regex regex = new Regex(patternConfig.Regex, options); + patterns.Add(new Pattern(regex, patternConfig.Start, patternConfig.End)); + } + + Console.WriteLine("Конфигурация успешно загружена."); + Console.WriteLine($"Загружено паттернов: {patterns.Count}; расширений: {extensions.Count}."); + } + + private static void ProcessFolder(string folder) + { + foreach (string filePath in Directory.GetFiles(folder)) + { + string fileName = Path.GetFileName(filePath); + string extension = Path.GetExtension(fileName); + + if (extensions.Contains(extension)) + { + RenameFile(filePath, fileName, folder); + } + } + } + + private static void RenameFile(string filePath, string fileName, string folder) + { + string nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); + + foreach (var pattern in patterns) + { + Match match = pattern.Regex.Match(nameWithoutExt); + if (!match.Success) + continue; + + string found = match.Value; + int startIndex = AdjustIndex(pattern.Start, found.Length); + int endIndex = AdjustIndex(pattern.End, found.Length); + + if (startIndex < 0 || endIndex < 0 || startIndex >= endIndex) + continue; + + string numberStr = found.Substring( + startIndex, + endIndex - startIndex + ); + + if (int.TryParse(numberStr, out int episode)) + { + string newName = $"{episode:D2}{Path.GetExtension(fileName)}"; + string newPath = Path.Combine(folder, newName); + + if (!File.Exists(newPath)) + { + File.Move(filePath, newPath); + Console.WriteLine($"\"{fileName}\" успешно переименован в \"{newName}\"."); + return; + } + + Console.WriteLine($"Ошибка: файл \"{newName}\" уже существует."); + } + } + } + + private static int AdjustIndex(int? index, int length) + { + if (!index.HasValue) + return length; + if (index.Value < 0) + return length + index.Value; + return index.Value; + } } } \ No newline at end of file