Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6416d983cd
|
|||
|
a318d2e625
|
|||
|
2cc96ff67c
|
|||
|
901f35870f
|
|||
|
020412e3a8
|
|||
|
ab3eef14f3
|
|||
|
bcc119d4fe
|
|||
|
96eb8c5f51
|
@@ -48,6 +48,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="KeyListener.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
326
KeyListener.cs
Normal file
326
KeyListener.cs
Normal file
@@ -0,0 +1,326 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EpisodeRenamer
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс для обработки горячих клавиш с использованием Task
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
#endregion
|
||||
|
||||
#region Key Codes
|
||||
public 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
|
||||
/// <summary>
|
||||
/// Делегат для события нажатия комбинации клавиш
|
||||
/// </summary>
|
||||
public delegate void HotkeyEventHandler(string combinationName);
|
||||
|
||||
/// <summary>
|
||||
/// Событие, возникающее при нажатии зарегистрированной комбинации клавиш
|
||||
/// </summary>
|
||||
public event HotkeyEventHandler OnHotkeyPressed;
|
||||
|
||||
/// <summary>
|
||||
/// Событие, возникающее при ошибке в работе слушателя
|
||||
/// </summary>
|
||||
public event Action<Exception> OnError;
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
private readonly List<KeyCombination> _keyCombinations;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private Task _listenerTask;
|
||||
private bool _isDisposed;
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _isRunning;
|
||||
private IntPtr consoleHandle = Process.GetCurrentProcess().MainWindowHandle;
|
||||
#endregion
|
||||
|
||||
#region Key Combination Class
|
||||
/// <summary>
|
||||
/// Класс для описания комбинации клавиш
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Создает новый экземпляр слушателя горячих клавиш
|
||||
/// </summary>
|
||||
public HotkeyListener()
|
||||
{
|
||||
_keyCombinations = new List<KeyCombination>();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Регистрирует новую комбинацию клавиш для отслеживания
|
||||
/// </summary>
|
||||
/// <param name="name">Имя комбинации</param>
|
||||
/// <param name="keyCodes">Коды клавиш (все должны быть нажаты одновременно)</param>
|
||||
/// <param name="cooldownMs">Время задержки между срабатываниями (мс)</param>
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет комбинацию клавиш из отслеживания
|
||||
/// </summary>
|
||||
/// <param name="name">Имя комбинации</param>
|
||||
public void UnregisterCombination(string name)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
var combination = _keyCombinations.Find(k => k.Name == name);
|
||||
if (combination != null)
|
||||
{
|
||||
_keyCombinations.Remove(combination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Запускает прослушивание горячих клавиш
|
||||
/// </summary>
|
||||
public async Task StartListeningAsync()
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_isRunning = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_listenerTask = Task.Run(() => ListenForHotkeys(_cancellationTokenSource.Token),
|
||||
_cancellationTokenSource.Token);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnError?.Invoke(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Останавливает прослушивание горячих клавиш
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает статус работы слушателя
|
||||
/// </summary>
|
||||
public bool IsListening => _isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Получает список зарегистрированных комбинаций
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> 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)
|
||||
{
|
||||
if (GetForegroundWindow() != consoleHandle)
|
||||
return;
|
||||
try
|
||||
{
|
||||
OnHotkeyPressed?.Invoke(combinationName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnError?.Invoke(new InvalidOperationException(
|
||||
$"Ошибка при обработке события для комбинации {combinationName}", 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
|
||||
}
|
||||
}
|
||||
100
Program.cs
100
Program.cs
@@ -1,14 +1,18 @@
|
||||
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
|
||||
{
|
||||
class EpisodeRenamer
|
||||
{
|
||||
private class PatternConfig
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public string Regex { get; set; }
|
||||
public int? Start { get; set; }
|
||||
public int? End { get; set; }
|
||||
@@ -38,7 +42,66 @@ class EpisodeRenamer
|
||||
}
|
||||
}
|
||||
|
||||
static void Main()
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadConfiguration();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Ошибка загрузки конфигурации: {ex.Message}");
|
||||
Console.WriteLine("Программа будет завершена.");
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
foreach (string line in args)
|
||||
if (Directory.Exists(line))
|
||||
{
|
||||
ProcessFolder(line);
|
||||
Console.WriteLine();
|
||||
}
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем экземпляр слушателя
|
||||
using (var hotkeyListener = new HotkeyListener())
|
||||
{
|
||||
// Подписываемся на события
|
||||
hotkeyListener.OnHotkeyPressed += (combination) =>
|
||||
{
|
||||
switch (combination)
|
||||
{
|
||||
case "ControlAltR":
|
||||
LoadConfiguration(true);
|
||||
break;
|
||||
case "ControlC":
|
||||
Environment.Exit(0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
hotkeyListener.OnError += (ex) => Console.WriteLine($"Ошибка: {ex.Message}");
|
||||
hotkeyListener.RegisterCombination("ControlAltR",
|
||||
new[] { HotkeyListener.VirtualKeyCodes.VK_CONTROL, HotkeyListener.VirtualKeyCodes.VK_ALT, HotkeyListener.VirtualKeyCodes.VK_R });
|
||||
hotkeyListener.RegisterCombination("ControlC", new[] { HotkeyListener.VirtualKeyCodes.VK_CONTROL, 0x43 });
|
||||
|
||||
// Запускаем прослушивание
|
||||
await hotkeyListener.StartListeningAsync();
|
||||
|
||||
// Основной цикл программы
|
||||
await RunMainProgramLoop();
|
||||
|
||||
// Останавливаем слушатель (автоматически вызывается в Dispose)
|
||||
await hotkeyListener.StopListeningAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunMainProgramLoop()
|
||||
{
|
||||
Console.Title = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyTitleAttribute>().Title;
|
||||
|
||||
@@ -54,13 +117,8 @@ class EpisodeRenamer
|
||||
return;
|
||||
}
|
||||
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
Console.WriteLine("\nВыход из программы.");
|
||||
Environment.Exit(0);
|
||||
};
|
||||
|
||||
Console.WriteLine("Чтобы оставить текущую директорию нажмите 'Enter'");
|
||||
Console.WriteLine("Чтобы перезагрузить конфигурацию нажмите 'Ctrl + Alt + R'");
|
||||
Console.WriteLine("Чтобы выйти нажмите 'Ctrl + C'");
|
||||
|
||||
while (true)
|
||||
@@ -68,7 +126,8 @@ class EpisodeRenamer
|
||||
try
|
||||
{
|
||||
Console.Write("\nВведите путь до папки с эпизодами: ");
|
||||
string input = Console.ReadLine().Trim() ?? string.Empty;
|
||||
string input = Console.ReadLine() ?? string.Empty;
|
||||
input = input.Trim();
|
||||
|
||||
string folder = string.IsNullOrEmpty(input)
|
||||
? Directory.GetCurrentDirectory()
|
||||
@@ -90,14 +149,20 @@ class EpisodeRenamer
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadConfiguration()
|
||||
private static void LoadConfiguration(bool reloadConfiguration = false)
|
||||
{
|
||||
if (reloadConfiguration)
|
||||
{
|
||||
extensions.Clear();
|
||||
patterns.Clear();
|
||||
GC.Collect();
|
||||
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<Config>(json);
|
||||
@@ -109,13 +174,15 @@ class EpisodeRenamer
|
||||
patterns = new List<Pattern>();
|
||||
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} расширений.");
|
||||
Console.WriteLine($"Загружено паттернов: {patterns.Count}; расширений: {extensions.Count}.");
|
||||
}
|
||||
|
||||
private static void ProcessFolder(string folder)
|
||||
@@ -126,11 +193,9 @@ class EpisodeRenamer
|
||||
string extension = Path.GetExtension(fileName);
|
||||
|
||||
if (extensions.Contains(extension))
|
||||
{
|
||||
RenameFile(filePath, fileName, folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenameFile(string filePath, string fileName, string folder)
|
||||
{
|
||||
@@ -179,4 +244,5 @@ class EpisodeRenamer
|
||||
return length + index.Value;
|
||||
return index.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,5 +29,5 @@ using System.Runtime.InteropServices;
|
||||
// Номер сборки
|
||||
// Номер редакции
|
||||
//
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyVersion("1.2.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.2.0.0")]
|
||||
|
||||
10
config.json
10
config.json
@@ -1,23 +1,33 @@
|
||||
{
|
||||
"Patterns": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Regex": "\\[\\d+\\]",
|
||||
"Start": 1,
|
||||
"End": -1
|
||||
},
|
||||
{
|
||||
"Enabled": true,
|
||||
"Regex": "[s]\\d+[e]\\d+",
|
||||
"Start": 4,
|
||||
"End": null,
|
||||
"IgnoreCase": true
|
||||
},
|
||||
{
|
||||
"Enabled": true,
|
||||
"Regex": "[s]\\d+[.][e]\\d+",
|
||||
"Start": 5,
|
||||
"End": null,
|
||||
"IgnoreCase": true
|
||||
},
|
||||
{
|
||||
"Enabled": false,
|
||||
"Regex": "[ ]\\d{2}[ ]",
|
||||
"Start": 1,
|
||||
"End": null
|
||||
},
|
||||
{
|
||||
"Enabled": true,
|
||||
"Regex": "\\d+$",
|
||||
"Start": 0,
|
||||
"End": null
|
||||
|
||||
Reference in New Issue
Block a user