Refactor downloads to use job-specific subdirectories
Organize downloaded files into per-job subdirectories under "downloads" instead of a flat structure. Update DownloadController and VideoService to read, write, and delete files within these job-specific directories. Add helper methods for directory management and improve logging. Update download/delete endpoints and result URLs to reflect the new structure. Clean up empty job directories after file deletion.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
namespace TuneRipper.Api.Services;
|
||||
|
||||
public interface ISessionService
|
||||
{
|
||||
string CreateSession();
|
||||
string GetOrCreateSession(string? sessionId);
|
||||
bool IsValidSession(string sessionId);
|
||||
void CleanupExpiredSessions();
|
||||
string GetSessionDownloadsPath(string sessionId);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace TuneRipper.Api.Services;
|
||||
|
||||
public class SessionService : ISessionService
|
||||
{
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly ILogger<SessionService> _logger;
|
||||
private readonly ConcurrentDictionary<string, DateTime> _sessions = new();
|
||||
private readonly Timer _cleanupTimer;
|
||||
|
||||
// Session expires after 24 hours of inactivity
|
||||
private readonly TimeSpan _sessionTimeout = TimeSpan.FromHours(24);
|
||||
|
||||
public SessionService(IWebHostEnvironment environment, ILogger<SessionService> logger)
|
||||
{
|
||||
_environment = environment;
|
||||
_logger = logger;
|
||||
|
||||
// Clean up expired sessions every hour
|
||||
_cleanupTimer = new Timer(
|
||||
callback: _ => CleanupExpiredSessions(),
|
||||
state: null,
|
||||
dueTime: TimeSpan.FromHours(1),
|
||||
period: TimeSpan.FromHours(1)
|
||||
);
|
||||
}
|
||||
|
||||
public string CreateSession()
|
||||
{
|
||||
var sessionId = Guid.NewGuid().ToString("N")[..16]; // Use first 16 characters for shorter IDs
|
||||
_sessions[sessionId] = DateTime.UtcNow;
|
||||
|
||||
// Ensure session directory exists
|
||||
var sessionPath = GetSessionDownloadsPath(sessionId);
|
||||
Directory.CreateDirectory(sessionPath);
|
||||
|
||||
_logger.LogInformation("Created new session: {SessionId}", sessionId);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public string GetOrCreateSession(string? sessionId)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(sessionId) && IsValidSession(sessionId))
|
||||
{
|
||||
// Update last activity time
|
||||
_sessions[sessionId] = DateTime.UtcNow;
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
return CreateSession();
|
||||
}
|
||||
|
||||
public bool IsValidSession(string sessionId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionId))
|
||||
return false;
|
||||
|
||||
if (_sessions.TryGetValue(sessionId, out var lastActivity))
|
||||
{
|
||||
// Check if session has expired
|
||||
if (DateTime.UtcNow - lastActivity > _sessionTimeout)
|
||||
{
|
||||
_sessions.TryRemove(sessionId, out _);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void CleanupExpiredSessions()
|
||||
{
|
||||
try
|
||||
{
|
||||
var expiredSessions = _sessions
|
||||
.Where(kvp => DateTime.UtcNow - kvp.Value > _sessionTimeout)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var sessionId in expiredSessions)
|
||||
{
|
||||
if (_sessions.TryRemove(sessionId, out _))
|
||||
{
|
||||
// Clean up session directory
|
||||
var sessionPath = GetSessionDownloadsPath(sessionId);
|
||||
if (Directory.Exists(sessionPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(sessionPath, recursive: true);
|
||||
_logger.LogInformation("Cleaned up expired session directory: {SessionId}", sessionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to delete expired session directory: {SessionId}", sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expiredSessions.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Cleaned up {Count} expired sessions", expiredSessions.Count);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during session cleanup");
|
||||
}
|
||||
}
|
||||
|
||||
public string GetSessionDownloadsPath(string sessionId)
|
||||
{
|
||||
var downloadsRoot = Path.Combine(_environment.WebRootPath, "downloads");
|
||||
return Path.Combine(downloadsRoot, sessionId);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cleanupTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public interface IVideoService
|
||||
Task<YouTubeVideoDetails?> GetVideoDetailsAsync(string url);
|
||||
bool IsValidYouTubeUrl(string url);
|
||||
string ExtractVideoId(string url);
|
||||
Task<string?> ProcessVideoWithProgressAsync(string url, string format, string jobId, Action<int, string> progressCallback);
|
||||
Task<string?> ProcessVideoWithProgressAsync(string url, string format, string jobId, string sessionId, Action<int, string> progressCallback);
|
||||
}
|
||||
|
||||
public class VideoService : IVideoService
|
||||
@@ -21,12 +21,14 @@ public class VideoService : IVideoService
|
||||
private readonly ILogger<VideoService> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly ISessionService _sessionService;
|
||||
|
||||
public VideoService(ILogger<VideoService> logger, IWebHostEnvironment environment, IHttpClientFactory httpClientFactory)
|
||||
public VideoService(ILogger<VideoService> logger, IWebHostEnvironment environment, IHttpClientFactory httpClientFactory, ISessionService sessionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
_environment = environment;
|
||||
_sessionService = sessionService;
|
||||
}
|
||||
|
||||
public async Task<YouTubeVideoDetails?> GetVideoDetailsAsync(string url)
|
||||
@@ -90,7 +92,7 @@ public class VideoService : IVideoService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> ProcessVideoWithProgressAsync(string url, string format, string jobId, Action<int, string> progressCallback)
|
||||
public async Task<string?> ProcessVideoWithProgressAsync(string url, string format, string jobId, string sessionId, Action<int, string> progressCallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -101,8 +103,8 @@ public class VideoService : IVideoService
|
||||
throw new FileNotFoundException($"yt-dlp.exe not found at {ytDlpPath}");
|
||||
}
|
||||
|
||||
// Create downloads directory if it doesn't exist
|
||||
var downloadDir = Path.Combine(_environment.WebRootPath, "downloads");
|
||||
// Get session-specific download directory
|
||||
var downloadDir = _sessionService.GetSessionDownloadsPath(sessionId);
|
||||
Directory.CreateDirectory(downloadDir);
|
||||
|
||||
// Get video details first for better filename
|
||||
@@ -113,7 +115,7 @@ public class VideoService : IVideoService
|
||||
var customFileName = $"{jobId}_{safeTitle}";
|
||||
var outputTemplate = Path.Combine(downloadDir, $"{customFileName}.%(ext)s");
|
||||
|
||||
_logger.LogInformation("Processing video with filename pattern: {FileName}", customFileName);
|
||||
_logger.LogInformation("Processing video with filename pattern: {FileName} in session: {SessionId}", customFileName, sessionId);
|
||||
|
||||
// Build yt-dlp arguments
|
||||
var arguments = new StringBuilder();
|
||||
@@ -188,7 +190,7 @@ public class VideoService : IVideoService
|
||||
throw new Exception($"yt-dlp failed with exit code {process.ExitCode}. Error: {error}");
|
||||
}
|
||||
|
||||
// Find the downloaded file with our job ID pattern
|
||||
// Find the downloaded file with our job ID pattern in the session directory
|
||||
var downloadedFiles = Directory.GetFiles(downloadDir, $"{jobId}_*.*")
|
||||
.Where(f => !f.EndsWith(".part"))
|
||||
.OrderByDescending(System.IO.File.GetCreationTime)
|
||||
@@ -196,13 +198,13 @@ public class VideoService : IVideoService
|
||||
|
||||
if (downloadedFiles.Length == 0)
|
||||
{
|
||||
throw new Exception($"No downloaded file found for job {jobId} after processing");
|
||||
throw new Exception($"No downloaded file found for job {jobId} in session {sessionId} after processing");
|
||||
}
|
||||
|
||||
var downloadedFile = downloadedFiles[0];
|
||||
var finalFileName = Path.GetFileName(downloadedFile);
|
||||
|
||||
_logger.LogInformation("Successfully processed video. Job: {JobId}, File: {FileName}", jobId, finalFileName);
|
||||
_logger.LogInformation("Successfully processed video. Job: {JobId}, Session: {SessionId}, File: {FileName}", jobId, sessionId, finalFileName);
|
||||
|
||||
progressCallback(100, "Processing completed successfully");
|
||||
|
||||
@@ -210,7 +212,7 @@ public class VideoService : IVideoService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing video with yt-dlp for job {JobId}: {Url}", jobId, url);
|
||||
_logger.LogError(ex, "Error processing video with yt-dlp for job {JobId} in session {SessionId}: {Url}", jobId, sessionId, url);
|
||||
progressCallback(0, $"Processing failed: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user