using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Hosting; using TransmissionRssManager.Core; namespace TransmissionRssManager.Services { public class PostProcessor : IPostProcessor { private readonly ILogger _logger; private readonly IConfigService _configService; private readonly ITransmissionClient _transmissionClient; public PostProcessor( ILogger logger, IConfigService configService, ITransmissionClient transmissionClient) { _logger = logger; _configService = configService; _transmissionClient = transmissionClient; } public async Task ProcessCompletedDownloadsAsync(CancellationToken cancellationToken) { var config = _configService.GetConfiguration(); if (!config.PostProcessing.Enabled) { return; } _logger.LogInformation("Processing completed downloads"); var torrents = await _transmissionClient.GetTorrentsAsync(); var completedTorrents = torrents.Where(t => t.IsFinished).ToList(); foreach (var torrent in completedTorrents) { if (cancellationToken.IsCancellationRequested) { _logger.LogInformation("Post-processing cancelled"); return; } try { await ProcessTorrentAsync(torrent); } catch (Exception ex) { _logger.LogError(ex, $"Error processing torrent: {torrent.Name}"); } } } public async Task ProcessTorrentAsync(TorrentInfo torrent) { _logger.LogInformation($"Processing completed torrent: {torrent.Name}"); var config = _configService.GetConfiguration(); var downloadDir = torrent.DownloadDir; var torrentPath = Path.Combine(downloadDir, torrent.Name); // Check if the file/directory exists if (!Directory.Exists(torrentPath) && !File.Exists(torrentPath)) { _logger.LogWarning($"Downloaded path not found: {torrentPath}"); 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)) { Directory.CreateDirectory(extractDir); } var processStartInfo = new ProcessStartInfo { 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; } _logger.LogInformation($"Archive extracted to: {extractDir}"); } catch (Exception ex) { _logger.LogError(ex, $"Error extracting archive: {archivePath}"); } } private async Task OrganizeMediaAsync(string path, string mediaLibraryPath) { _logger.LogInformation($"Organizing media: {path}"); var config = _configService.GetConfiguration(); var mediaExtensions = config.PostProcessing.MediaExtensions; // Ensure media library path exists if (!Directory.Exists(mediaLibraryPath)) { Directory.CreateDirectory(mediaLibraryPath); } try { if (File.Exists(path)) { // 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(); foreach (var mediaFile in mediaFiles) { await CopyFileToMediaLibraryAsync(mediaFile, mediaLibraryPath); } } } catch (Exception ex) { _logger.LogError(ex, $"Error organizing media: {path}"); } } private async Task CopyFileToMediaLibraryAsync(string filePath, string mediaLibraryPath) { var fileName = Path.GetFileName(filePath); var destinationPath = Path.Combine(mediaLibraryPath, fileName); // If destination file already exists, add a unique identifier if (File.Exists(destinationPath)) { var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); var extension = Path.GetExtension(fileName); var uniqueId = Guid.NewGuid().ToString().Substring(0, 8); destinationPath = Path.Combine(mediaLibraryPath, $"{fileNameWithoutExt}_{uniqueId}{extension}"); } _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}"); } } } public class PostProcessingBackgroundService : BackgroundService { private readonly ILogger _logger; private readonly IPostProcessor _postProcessor; private readonly IConfigService _configService; public PostProcessingBackgroundService( ILogger logger, IPostProcessor postProcessor, IConfigService configService) { _logger = logger; _postProcessor = postProcessor; _configService = configService; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Post-processing background service started"); while (!stoppingToken.IsCancellationRequested) { try { await _postProcessor.ProcessCompletedDownloadsAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "Error processing completed downloads"); } // Check every 5 minutes await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } } } }