Compare commits

...

7 Commits

Author SHA1 Message Date
a318d2e625 1.2
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
2025-10-10 22:07:04 +07:00
2cc96ff67c Add drag'n'drop to executable
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
2025-10-10 22:03:02 +07:00
901f35870f 1.1
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
2025-09-28 18:05:19 +07:00
020412e3a8 Update files
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
2025-09-28 18:04:10 +07:00
ab3eef14f3 Update files
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
2025-09-26 13:14:42 +07:00
bcc119d4fe Update files
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
2025-09-23 09:54:08 +07:00
96eb8c5f51 Update files
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
2025-09-23 08:14:05 +07:00
5 changed files with 557 additions and 154 deletions

View File

@@ -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
View 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
}
}

View File

@@ -1,182 +1,248 @@
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 string Regex { get; set; }
public int? Start { get; set; }
public int? End { get; set; }
public bool IgnoreCase { get; set; }
}
private class Config
{
public List<PatternConfig> Patterns { get; set; }
public List<string> Extensions { get; set; }
}
private static List<Pattern> patterns;
private static HashSet<string> 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<AssemblyTitleAttribute>().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<PatternConfig> Patterns { get; set; }
public List<string> Extensions { get; set; }
}
Console.WriteLine("Чтобы оставить текущую директорию нажмите 'Enter'");
Console.WriteLine("Чтобы выйти нажмите 'Ctrl + C'");
private static List<Pattern> patterns;
private static HashSet<string> 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)
{
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;
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.WriteLine("Чтобы оставить текущую директорию нажмите 'Enter'");
Console.WriteLine("Чтобы перезагрузить конфигурацию нажмите 'Ctrl + Alt + R'");
Console.WriteLine("Чтобы выйти нажмите 'Ctrl + C'");
if (!File.Exists(configPath))
{
throw new FileNotFoundException("Конфигурационный файл config.json не найден");
}
string json = File.ReadAllText(configPath);
Config config = JsonConvert.DeserializeObject<Config>(json);
// Загружаем расширения файлов
extensions = new HashSet<string>(config.Extensions, StringComparer.OrdinalIgnoreCase);
// Загружаем паттерны
patterns = new List<Pattern>();
foreach (PatternConfig patternConfig in config.Patterns)
{
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();
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);
// Загружаем расширения файлов
extensions = new HashSet<string>(config.Extensions, StringComparer.OrdinalIgnoreCase);
// Загружаем паттерны
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}.");
}
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;
}
}
}

View File

@@ -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")]

View File

@@ -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