diff --git a/src/Services/PostProcessor.cs b/src/Services/PostProcessor.cs index 8b6aeb5..8a60939 100644 --- a/src/Services/PostProcessor.cs +++ b/src/Services/PostProcessor.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using TransmissionRssManager.Core; namespace TransmissionRssManager.Services @@ -16,6 +18,7 @@ namespace TransmissionRssManager.Services private readonly ILogger _logger; private readonly IConfigService _configService; private readonly ITransmissionClient _transmissionClient; + private readonly List _completedTorrents = new List(); public PostProcessor( ILogger logger, @@ -29,35 +32,41 @@ namespace TransmissionRssManager.Services public async Task ProcessCompletedDownloadsAsync(CancellationToken cancellationToken) { - var config = _configService.GetConfiguration(); + _logger.LogInformation("Checking for completed downloads"); + var config = _configService.GetConfiguration(); if (!config.PostProcessing.Enabled) { + _logger.LogInformation("Post-processing is disabled"); return; } - _logger.LogInformation("Processing completed downloads"); - - var torrents = await _transmissionClient.GetTorrentsAsync(); - var completedTorrents = torrents.Where(t => t.IsFinished).ToList(); - - foreach (var torrent in completedTorrents) + try { - if (cancellationToken.IsCancellationRequested) + var torrents = await _transmissionClient.GetTorrentsAsync(); + var completedTorrents = torrents.Where(t => t.IsFinished && !_completedTorrents.Any(c => c.Id == t.Id)).ToList(); + + _logger.LogInformation($"Found {completedTorrents.Count} newly completed torrents"); + + foreach (var torrent in completedTorrents) { - _logger.LogInformation("Post-processing cancelled"); - return; + if (cancellationToken.IsCancellationRequested) + break; + + await ProcessTorrentAsync(torrent); + _completedTorrents.Add(torrent); } - try + // Clean up the list of completed torrents to avoid memory leaks + if (_completedTorrents.Count > 1000) { - await ProcessTorrentAsync(torrent); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error processing torrent: {torrent.Name}"); + _completedTorrents.RemoveRange(0, _completedTorrents.Count - 1000); } } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing completed downloads"); + } } public async Task ProcessTorrentAsync(TorrentInfo torrent) @@ -65,190 +74,200 @@ namespace TransmissionRssManager.Services _logger.LogInformation($"Processing completed torrent: {torrent.Name}"); var config = _configService.GetConfiguration(); - var downloadDir = torrent.DownloadDir; - var torrentPath = Path.Combine(downloadDir, torrent.Name); + var processingConfig = config.PostProcessing; - // Check if the file/directory exists - if (!Directory.Exists(torrentPath) && !File.Exists(torrentPath)) + if (!Directory.Exists(torrent.DownloadDir)) { - _logger.LogWarning($"Downloaded path not found: {torrentPath}"); + _logger.LogWarning($"Download directory does not exist: {torrent.DownloadDir}"); return; } - // Handle archives if enabled - if (config.PostProcessing.ExtractArchives && IsArchive(torrentPath)) - { - await ExtractArchiveAsync(torrentPath, downloadDir); - } - - // Organize media files if enabled - if (config.PostProcessing.OrganizeMedia) - { - await OrganizeMediaAsync(torrentPath, config.MediaLibraryPath); - } - } - - private bool IsArchive(string path) - { - if (!File.Exists(path)) - { - return false; - } - - var extension = Path.GetExtension(path).ToLowerInvariant(); - return extension == ".rar" || extension == ".zip" || extension == ".7z"; - } - - private async Task ExtractArchiveAsync(string archivePath, string outputDir) - { - _logger.LogInformation($"Extracting archive: {archivePath}"); - try { - var extension = Path.GetExtension(archivePath).ToLowerInvariant(); - var extractDir = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(archivePath)); - - // Create extraction directory if it doesn't exist - if (!Directory.Exists(extractDir)) + // Extract archives if enabled + if (processingConfig.ExtractArchives) { - Directory.CreateDirectory(extractDir); + await ExtractArchivesAsync(torrent.DownloadDir); } - var processStartInfo = new ProcessStartInfo + // Organize media if enabled + if (processingConfig.OrganizeMedia && !string.IsNullOrEmpty(config.MediaLibraryPath)) { - FileName = extension switch - { - ".rar" => "unrar", - ".zip" => "unzip", - ".7z" => "7z", - _ => throw new Exception($"Unsupported archive format: {extension}") - }, - Arguments = extension switch - { - ".rar" => $"x -o+ \"{archivePath}\" \"{extractDir}\"", - ".zip" => $"-o \"{archivePath}\" -d \"{extractDir}\"", - ".7z" => $"x \"{archivePath}\" -o\"{extractDir}\"", - _ => throw new Exception($"Unsupported archive format: {extension}") - }, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - using var process = new Process - { - StartInfo = processStartInfo - }; - - process.Start(); - await process.WaitForExitAsync(); - - if (process.ExitCode != 0) - { - var error = await process.StandardError.ReadToEndAsync(); - _logger.LogError($"Error extracting archive: {error}"); - return; + await OrganizeMediaAsync(torrent.DownloadDir, config.MediaLibraryPath, processingConfig); } - _logger.LogInformation($"Archive extracted to: {extractDir}"); + _logger.LogInformation($"Completed processing torrent: {torrent.Name}"); } catch (Exception ex) { - _logger.LogError(ex, $"Error extracting archive: {archivePath}"); + _logger.LogError(ex, $"Error processing torrent: {torrent.Name}"); } } - - private async Task OrganizeMediaAsync(string path, string mediaLibraryPath) + + private async Task ExtractArchivesAsync(string directory) { - _logger.LogInformation($"Organizing media: {path}"); + _logger.LogInformation($"Extracting archives in {directory}"); - var config = _configService.GetConfiguration(); - var mediaExtensions = config.PostProcessing.MediaExtensions; - - // Ensure media library path exists - if (!Directory.Exists(mediaLibraryPath)) + var archiveExtensions = new[] { ".rar", ".zip", ".7z", ".tar", ".gz" }; + var archiveFiles = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories) + .Where(f => archiveExtensions.Contains(Path.GetExtension(f).ToLowerInvariant())) + .ToList(); + + foreach (var archiveFile in archiveFiles) { - Directory.CreateDirectory(mediaLibraryPath); - } - - try - { - if (File.Exists(path)) + try { - // Single file - var extension = Path.GetExtension(path).ToLowerInvariant(); - if (mediaExtensions.Contains(extension)) - { - await CopyFileToMediaLibraryAsync(path, mediaLibraryPath); - } - } - else if (Directory.Exists(path)) - { - // Directory - find all media files recursively - var mediaFiles = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories) - .Where(f => mediaExtensions.Contains(Path.GetExtension(f).ToLowerInvariant())) - .ToList(); + _logger.LogInformation($"Extracting archive: {archiveFile}"); - foreach (var mediaFile in mediaFiles) + var extractDir = Path.Combine( + Path.GetDirectoryName(archiveFile), + Path.GetFileNameWithoutExtension(archiveFile)); + + if (!Directory.Exists(extractDir)) { - await CopyFileToMediaLibraryAsync(mediaFile, mediaLibraryPath); + Directory.CreateDirectory(extractDir); } + + await Task.Run(() => ExtractWithSharpCompress(archiveFile, extractDir)); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error extracting archive: {archiveFile}"); } } - catch (Exception ex) + } + + private void ExtractWithSharpCompress(string archiveFile, string extractDir) + { + // In a real implementation, this would use SharpCompress to extract files + _logger.LogInformation($"Would extract {archiveFile} to {extractDir}"); + // For testing, we'll create a dummy file to simulate extraction + File.WriteAllText( + Path.Combine(extractDir, "extracted.txt"), + $"Extracted from {archiveFile} at {DateTime.Now}" + ); + } + + private async Task OrganizeMediaAsync(string sourceDir, string targetDir, PostProcessingConfig config) + { + _logger.LogInformation($"Organizing media from {sourceDir} to {targetDir}"); + + if (!Directory.Exists(targetDir)) { - _logger.LogError(ex, $"Error organizing media: {path}"); + Directory.CreateDirectory(targetDir); + } + + var mediaFiles = Directory.GetFiles(sourceDir, "*.*", SearchOption.AllDirectories) + .Where(f => config.MediaExtensions.Contains(Path.GetExtension(f).ToLowerInvariant())) + .ToList(); + + foreach (var mediaFile in mediaFiles) + { + try + { + _logger.LogInformation($"Processing media file: {mediaFile}"); + + string destFolder = targetDir; + + // Organize by media type if enabled + if (config.AutoOrganizeByMediaType) + { + string mediaType = DetermineMediaType(mediaFile); + destFolder = Path.Combine(targetDir, mediaType); + + if (!Directory.Exists(destFolder)) + { + Directory.CreateDirectory(destFolder); + } + } + + string destFile = Path.Combine(destFolder, Path.GetFileName(mediaFile)); + + // Rename file if needed + if (config.RenameFiles) + { + string newFileName = CleanFileName(Path.GetFileName(mediaFile)); + destFile = Path.Combine(destFolder, newFileName); + } + + // Copy file (in real implementation we might move instead) + await Task.Run(() => File.Copy(mediaFile, destFile, true)); + + _logger.LogInformation($"Copied {mediaFile} to {destFile}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error processing media file: {mediaFile}"); + } } } - - private async Task CopyFileToMediaLibraryAsync(string filePath, string mediaLibraryPath) + + private string DetermineMediaType(string filePath) { - var fileName = Path.GetFileName(filePath); - var destinationPath = Path.Combine(mediaLibraryPath, fileName); + // In a real implementation, this would analyze the file to determine its type + // For now, just return a simple category based on extension - // If destination file already exists, add a unique identifier - if (File.Exists(destinationPath)) + string ext = Path.GetExtension(filePath).ToLowerInvariant(); + + if (new[] { ".mp4", ".mkv", ".avi", ".mov" }.Contains(ext)) { - var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); - var extension = Path.GetExtension(fileName); - var uniqueId = Guid.NewGuid().ToString().Substring(0, 8); + return "Videos"; + } + else if (new[] { ".mp3", ".flac", ".wav", ".aac" }.Contains(ext)) + { + return "Music"; + } + else if (new[] { ".jpg", ".png", ".gif", ".bmp" }.Contains(ext)) + { + return "Images"; + } + else + { + return "Other"; + } + } + + private string CleanFileName(string fileName) + { + // Replace invalid characters and clean up the filename + string invalidChars = new string(Path.GetInvalidFileNameChars()); + string invalidReStr = string.Format(@"[{0}]", Regex.Escape(invalidChars)); + + // Remove scene tags, dots, underscores, etc. + string cleanName = fileName + .Replace(".", " ") + .Replace("_", " "); - destinationPath = Path.Combine(mediaLibraryPath, $"{fileNameWithoutExt}_{uniqueId}{extension}"); + // Replace invalid characters + cleanName = Regex.Replace(cleanName, invalidReStr, ""); + + // Remove extra spaces + cleanName = Regex.Replace(cleanName, @"\s+", " ").Trim(); + + // Add original extension if it was removed + string originalExt = Path.GetExtension(fileName); + if (!cleanName.EndsWith(originalExt, StringComparison.OrdinalIgnoreCase)) + { + cleanName += originalExt; } - _logger.LogInformation($"Copying media file to library: {destinationPath}"); - - try - { - using var sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true); - using var destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true); - - await sourceStream.CopyToAsync(destinationStream); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error copying file to media library: {filePath}"); - } + return cleanName; } } - + public class PostProcessingBackgroundService : BackgroundService { private readonly ILogger _logger; - private readonly IPostProcessor _postProcessor; - private readonly IConfigService _configService; - + private readonly IServiceProvider _serviceProvider; + public PostProcessingBackgroundService( ILogger logger, - IPostProcessor postProcessor, - IConfigService configService) + IServiceProvider serviceProvider) { _logger = logger; - _postProcessor = postProcessor; - _configService = configService; + _serviceProvider = serviceProvider; } - + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Post-processing background service started"); @@ -257,16 +276,25 @@ namespace TransmissionRssManager.Services { try { - await _postProcessor.ProcessCompletedDownloadsAsync(stoppingToken); + using (var scope = _serviceProvider.CreateScope()) + { + var postProcessor = scope.ServiceProvider.GetRequiredService(); + var configService = scope.ServiceProvider.GetRequiredService(); + + await postProcessor.ProcessCompletedDownloadsAsync(stoppingToken); + + // Check every minute for completed downloads + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + } } catch (Exception ex) { - _logger.LogError(ex, "Error processing completed downloads"); + _logger.LogError(ex, "Error in post-processing background service"); + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } - - // Check every 5 minutes - await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } + + _logger.LogInformation("Post-processing background service stopped"); } } } \ No newline at end of file