Compare commits
2 Commits
9727680c57
...
bcc119d4fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
bcc119d4fe
|
|||
|
96eb8c5f51
|
@@ -48,6 +48,7 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="KeyListener.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
339
KeyListener.cs
Normal file
339
KeyListener.cs
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
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);
|
||||||
|
#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
|
||||||
|
/// <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;
|
||||||
|
#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();
|
||||||
|
|
||||||
|
// Добавляем комбинацию по умолчанию
|
||||||
|
RegisterCombination("ControlAltR",
|
||||||
|
new[] { VirtualKeyCodes.VK_CONTROL, VirtualKeyCodes.VK_ALT, VirtualKeyCodes.VK_R });
|
||||||
|
}
|
||||||
|
#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
|
||||||
|
{
|
||||||
|
// Переводим консоль на передний план для лучшего захвата клавиш
|
||||||
|
BringConsoleToFront();
|
||||||
|
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
73
Program.cs
73
Program.cs
@@ -1,14 +1,18 @@
|
|||||||
using System;
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Newtonsoft.Json;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EpisodeRenamer
|
||||||
|
{
|
||||||
class EpisodeRenamer
|
class EpisodeRenamer
|
||||||
{
|
{
|
||||||
private class PatternConfig
|
private class PatternConfig
|
||||||
{
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
public string Regex { get; set; }
|
public string Regex { get; set; }
|
||||||
public int? Start { get; set; }
|
public int? Start { get; set; }
|
||||||
public int? End { get; set; }
|
public int? End { get; set; }
|
||||||
@@ -38,7 +42,42 @@ class EpisodeRenamer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Main()
|
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<AssemblyTitleAttribute>().Title;
|
Console.Title = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyTitleAttribute>().Title;
|
||||||
|
|
||||||
@@ -54,13 +93,14 @@ class EpisodeRenamer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.CancelKeyPress += (sender, e) =>
|
//Console.CancelKeyPress += (sender, e) =>
|
||||||
{
|
//{
|
||||||
Console.WriteLine("\nВыход из программы.");
|
// Console.WriteLine("\nВыход из программы.");
|
||||||
Environment.Exit(0);
|
// Environment.Exit(0);
|
||||||
};
|
//};
|
||||||
|
|
||||||
Console.WriteLine("Чтобы оставить текущую директорию нажмите 'Enter'");
|
Console.WriteLine("Чтобы оставить текущую директорию нажмите 'Enter'");
|
||||||
|
Console.WriteLine("Чтобы перезагрузить конфигурацию нажмите 'Ctrl + Alt + R'");
|
||||||
Console.WriteLine("Чтобы выйти нажмите 'Ctrl + C'");
|
Console.WriteLine("Чтобы выйти нажмите 'Ctrl + C'");
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
@@ -68,7 +108,8 @@ class EpisodeRenamer
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Console.Write("\nВведите путь до папки с эпизодами: ");
|
Console.Write("\nВведите путь до папки с эпизодами: ");
|
||||||
string input = Console.ReadLine().Trim() ?? string.Empty;
|
string input = Console.ReadLine() ?? string.Empty;
|
||||||
|
input = input.Trim();
|
||||||
|
|
||||||
string folder = string.IsNullOrEmpty(input)
|
string folder = string.IsNullOrEmpty(input)
|
||||||
? Directory.GetCurrentDirectory()
|
? Directory.GetCurrentDirectory()
|
||||||
@@ -90,8 +131,15 @@ class EpisodeRenamer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LoadConfiguration()
|
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");
|
string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
|
||||||
|
|
||||||
if (!File.Exists(configPath))
|
if (!File.Exists(configPath))
|
||||||
@@ -109,13 +157,15 @@ class EpisodeRenamer
|
|||||||
patterns = new List<Pattern>();
|
patterns = new List<Pattern>();
|
||||||
foreach (PatternConfig patternConfig in config.Patterns)
|
foreach (PatternConfig patternConfig in config.Patterns)
|
||||||
{
|
{
|
||||||
|
if (!patternConfig.Enabled)
|
||||||
|
continue;
|
||||||
RegexOptions options = patternConfig.IgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None;
|
RegexOptions options = patternConfig.IgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None;
|
||||||
Regex regex = new Regex(patternConfig.Regex, options);
|
Regex regex = new Regex(patternConfig.Regex, options);
|
||||||
patterns.Add(new Pattern(regex, patternConfig.Start, patternConfig.End));
|
patterns.Add(new Pattern(regex, patternConfig.Start, patternConfig.End));
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("Конфигурация успешно загружена.");
|
Console.WriteLine("Конфигурация успешно загружена.");
|
||||||
Console.WriteLine($"Загружено {patterns.Count} паттернов и {extensions.Count} расширений.");
|
Console.WriteLine($"Загружено паттернов: {patterns.Count}; расширений: {extensions.Count}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ProcessFolder(string folder)
|
private static void ProcessFolder(string folder)
|
||||||
@@ -180,3 +230,4 @@ class EpisodeRenamer
|
|||||||
return index.Value;
|
return index.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
10
config.json
10
config.json
@@ -1,23 +1,33 @@
|
|||||||
{
|
{
|
||||||
"Patterns": [
|
"Patterns": [
|
||||||
{
|
{
|
||||||
|
"Enabled": true,
|
||||||
"Regex": "\\[\\d+\\]",
|
"Regex": "\\[\\d+\\]",
|
||||||
"Start": 1,
|
"Start": 1,
|
||||||
"End": -1
|
"End": -1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"Enabled": true,
|
||||||
"Regex": "[s]\\d+[e]\\d+",
|
"Regex": "[s]\\d+[e]\\d+",
|
||||||
"Start": 4,
|
"Start": 4,
|
||||||
"End": null,
|
"End": null,
|
||||||
"IgnoreCase": true
|
"IgnoreCase": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"Enabled": true,
|
||||||
"Regex": "[s]\\d+[.][e]\\d+",
|
"Regex": "[s]\\d+[.][e]\\d+",
|
||||||
"Start": 5,
|
"Start": 5,
|
||||||
"End": null,
|
"End": null,
|
||||||
"IgnoreCase": true
|
"IgnoreCase": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"Enabled": false,
|
||||||
|
"Regex": "[ ]\\d[2][ ]",
|
||||||
|
"Start": 1,
|
||||||
|
"End": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Enabled": true,
|
||||||
"Regex": "\\d+$",
|
"Regex": "\\d+$",
|
||||||
"Start": 0,
|
"Start": 0,
|
||||||
"End": null
|
"End": null
|
||||||
|
|||||||
Reference in New Issue
Block a user