From bcc119d4fe3dd28a48555ba7e9ecabc63a989e28 Mon Sep 17 00:00:00 2001
From: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
Date: Tue, 23 Sep 2025 09:54:08 +0700
Subject: [PATCH] Update files
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
---
EpisodeRenamer.csproj | 1 +
KeyListener.cs | 339 ++++++++++++++++++++++++++++++++++++++++
Program.cs | 356 ++++++++++++++++++++++++------------------
3 files changed, 542 insertions(+), 154 deletions(-)
create mode 100644 KeyListener.cs
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