Simplify PostProcessor to remove database dependencies

This commit is contained in:
MasterDraco 2025-03-12 22:13:41 +00:00
parent 619a861546
commit 0f27b1a939

View File

@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TransmissionRssManager.Core; using TransmissionRssManager.Core;
namespace TransmissionRssManager.Services namespace TransmissionRssManager.Services
@ -16,6 +18,7 @@ namespace TransmissionRssManager.Services
private readonly ILogger<PostProcessor> _logger; private readonly ILogger<PostProcessor> _logger;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly ITransmissionClient _transmissionClient; private readonly ITransmissionClient _transmissionClient;
private readonly List<TorrentInfo> _completedTorrents = new List<TorrentInfo>();
public PostProcessor( public PostProcessor(
ILogger<PostProcessor> logger, ILogger<PostProcessor> logger,
@ -29,35 +32,41 @@ namespace TransmissionRssManager.Services
public async Task ProcessCompletedDownloadsAsync(CancellationToken cancellationToken) public async Task ProcessCompletedDownloadsAsync(CancellationToken cancellationToken)
{ {
var config = _configService.GetConfiguration(); _logger.LogInformation("Checking for completed downloads");
var config = _configService.GetConfiguration();
if (!config.PostProcessing.Enabled) if (!config.PostProcessing.Enabled)
{ {
_logger.LogInformation("Post-processing is disabled");
return; return;
} }
_logger.LogInformation("Processing completed downloads"); try
var torrents = await _transmissionClient.GetTorrentsAsync();
var completedTorrents = torrents.Where(t => t.IsFinished).ToList();
foreach (var torrent in completedTorrents)
{ {
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"); if (cancellationToken.IsCancellationRequested)
return; 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); _completedTorrents.RemoveRange(0, _completedTorrents.Count - 1000);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing torrent: {torrent.Name}");
} }
} }
catch (Exception ex)
{
_logger.LogError(ex, "Error processing completed downloads");
}
} }
public async Task ProcessTorrentAsync(TorrentInfo torrent) public async Task ProcessTorrentAsync(TorrentInfo torrent)
@ -65,188 +74,198 @@ namespace TransmissionRssManager.Services
_logger.LogInformation($"Processing completed torrent: {torrent.Name}"); _logger.LogInformation($"Processing completed torrent: {torrent.Name}");
var config = _configService.GetConfiguration(); var config = _configService.GetConfiguration();
var downloadDir = torrent.DownloadDir; var processingConfig = config.PostProcessing;
var torrentPath = Path.Combine(downloadDir, torrent.Name);
// Check if the file/directory exists if (!Directory.Exists(torrent.DownloadDir))
if (!Directory.Exists(torrentPath) && !File.Exists(torrentPath))
{ {
_logger.LogWarning($"Downloaded path not found: {torrentPath}"); _logger.LogWarning($"Download directory does not exist: {torrent.DownloadDir}");
return; 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 try
{ {
var extension = Path.GetExtension(archivePath).ToLowerInvariant(); // Extract archives if enabled
var extractDir = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(archivePath)); if (processingConfig.ExtractArchives)
// Create extraction directory if it doesn't exist
if (!Directory.Exists(extractDir))
{ {
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 await OrganizeMediaAsync(torrent.DownloadDir, config.MediaLibraryPath, processingConfig);
{
".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}"); _logger.LogInformation($"Completed processing torrent: {torrent.Name}");
} }
catch (Exception ex) 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 archiveExtensions = new[] { ".rar", ".zip", ".7z", ".tar", ".gz" };
var mediaExtensions = config.PostProcessing.MediaExtensions; var archiveFiles = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories)
.Where(f => archiveExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.ToList();
// Ensure media library path exists foreach (var archiveFile in archiveFiles)
if (!Directory.Exists(mediaLibraryPath))
{ {
Directory.CreateDirectory(mediaLibraryPath); try
}
try
{
if (File.Exists(path))
{ {
// Single file _logger.LogInformation($"Extracting archive: {archiveFile}");
var extension = Path.GetExtension(path).ToLowerInvariant();
if (mediaExtensions.Contains(extension)) var extractDir = Path.Combine(
Path.GetDirectoryName(archiveFile),
Path.GetFileNameWithoutExtension(archiveFile));
if (!Directory.Exists(extractDir))
{ {
await CopyFileToMediaLibraryAsync(path, mediaLibraryPath); Directory.CreateDirectory(extractDir);
} }
}
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 Task.Run(() => ExtractWithSharpCompress(archiveFile, extractDir));
{ }
await CopyFileToMediaLibraryAsync(mediaFile, mediaLibraryPath); catch (Exception ex)
} {
_logger.LogError(ex, $"Error extracting archive: {archiveFile}");
} }
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error organizing media: {path}");
} }
} }
private async Task CopyFileToMediaLibraryAsync(string filePath, string mediaLibraryPath) private void ExtractWithSharpCompress(string archiveFile, string extractDir)
{ {
var fileName = Path.GetFileName(filePath); // In a real implementation, this would use SharpCompress to extract files
var destinationPath = Path.Combine(mediaLibraryPath, fileName); _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}"
);
}
// If destination file already exists, add a unique identifier private async Task OrganizeMediaAsync(string sourceDir, string targetDir, PostProcessingConfig config)
if (File.Exists(destinationPath)) {
_logger.LogInformation($"Organizing media from {sourceDir} to {targetDir}");
if (!Directory.Exists(targetDir))
{ {
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); Directory.CreateDirectory(targetDir);
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}"); var mediaFiles = Directory.GetFiles(sourceDir, "*.*", SearchOption.AllDirectories)
.Where(f => config.MediaExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.ToList();
try foreach (var mediaFile in mediaFiles)
{ {
using var sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true); try
using var destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true); {
_logger.LogInformation($"Processing media file: {mediaFile}");
await sourceStream.CopyToAsync(destinationStream); 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}");
}
} }
catch (Exception ex) }
private string DetermineMediaType(string filePath)
{
// In a real implementation, this would analyze the file to determine its type
// For now, just return a simple category based on extension
string ext = Path.GetExtension(filePath).ToLowerInvariant();
if (new[] { ".mp4", ".mkv", ".avi", ".mov" }.Contains(ext))
{ {
_logger.LogError(ex, $"Error copying file to media library: {filePath}"); 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("_", " ");
// 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;
}
return cleanName;
} }
} }
public class PostProcessingBackgroundService : BackgroundService public class PostProcessingBackgroundService : BackgroundService
{ {
private readonly ILogger<PostProcessingBackgroundService> _logger; private readonly ILogger<PostProcessingBackgroundService> _logger;
private readonly IPostProcessor _postProcessor; private readonly IServiceProvider _serviceProvider;
private readonly IConfigService _configService;
public PostProcessingBackgroundService( public PostProcessingBackgroundService(
ILogger<PostProcessingBackgroundService> logger, ILogger<PostProcessingBackgroundService> logger,
IPostProcessor postProcessor, IServiceProvider serviceProvider)
IConfigService configService)
{ {
_logger = logger; _logger = logger;
_postProcessor = postProcessor; _serviceProvider = serviceProvider;
_configService = configService;
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@ -257,16 +276,25 @@ namespace TransmissionRssManager.Services
{ {
try try
{ {
await _postProcessor.ProcessCompletedDownloadsAsync(stoppingToken); using (var scope = _serviceProvider.CreateScope())
{
var postProcessor = scope.ServiceProvider.GetRequiredService<IPostProcessor>();
var configService = scope.ServiceProvider.GetRequiredService<IConfigService>();
await postProcessor.ProcessCompletedDownloadsAsync(stoppingToken);
// Check every minute for completed downloads
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
} }
catch (Exception ex) 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");
} }
} }
} }