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:
Michal Vrba
2025-12-14 11:04:46 +01:00
parent 7c99721174
commit 2d2e855202
14 changed files with 1073 additions and 66 deletions
+38 -12
View File
@@ -10,6 +10,7 @@ namespace TuneRipper.Api.Controllers;
public class VideoController : ControllerBase
{
private readonly IVideoService _videoService;
private readonly ISessionService _sessionService;
private readonly ILogger<VideoController> _logger;
// In-memory storage for processing status (in production, use database or Redis)
@@ -18,6 +19,7 @@ public class VideoController : ControllerBase
private class ProcessingJob
{
public string Id { get; set; } = string.Empty;
public string SessionId { get; set; } = string.Empty; // Add session tracking
public ProcessVideoRequest Request { get; set; } = new();
public DownloadProgress Progress { get; set; } = new();
public ProcessVideoResponse? Result { get; set; }
@@ -26,9 +28,10 @@ public class VideoController : ControllerBase
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public VideoController(IVideoService videoService, ILogger<VideoController> logger)
public VideoController(IVideoService videoService, ISessionService sessionService, ILogger<VideoController> logger)
{
_videoService = videoService;
_sessionService = sessionService;
_logger = logger;
}
@@ -49,6 +52,9 @@ public class VideoController : ControllerBase
{
_logger.LogInformation("Starting video processing for URL: {Url}", request.Url);
// Get or create session
var sessionId = _sessionService.GetOrCreateSession(request.SessionId);
// Generate a job ID
var jobId = Guid.NewGuid().ToString();
@@ -56,6 +62,7 @@ public class VideoController : ControllerBase
var job = new ProcessingJob
{
Id = jobId,
SessionId = sessionId,
Request = request,
Progress = new DownloadProgress
{
@@ -76,7 +83,8 @@ public class VideoController : ControllerBase
Message = "Processing started",
FileName = jobId, // Use job ID as temporary filename
DownloadUrl = null, // Will be available when processing completes
VideoDetails = null // Will be populated during processing
VideoDetails = null, // Will be populated during processing
SessionId = sessionId // Return session ID to client
};
return Accepted(response);
@@ -140,10 +148,18 @@ public class VideoController : ControllerBase
return Ok(job.Result);
}
[HttpGet("jobs")]
public ActionResult<IEnumerable<JobStatusResponse>> GetAllJobs()
[HttpGet("jobs/{sessionId?}")]
public ActionResult<IEnumerable<JobStatusResponse>> GetJobsBySession(string? sessionId = null)
{
var jobs = _processingJobs.Values.Select(job => new JobStatusResponse
var jobsQuery = _processingJobs.Values.AsEnumerable();
// Filter by session if provided
if (!string.IsNullOrWhiteSpace(sessionId))
{
jobsQuery = jobsQuery.Where(job => job.SessionId == sessionId);
}
var jobs = jobsQuery.Select(job => new JobStatusResponse
{
JobId = job.Id,
Status = job.Progress.Status,
@@ -157,6 +173,13 @@ public class VideoController : ControllerBase
return Ok(jobs);
}
// Keep the old endpoint for backward compatibility
[HttpGet("jobs")]
public ActionResult<IEnumerable<JobStatusResponse>> GetAllJobs()
{
return GetJobsBySession(null);
}
private async Task ProcessVideoAsync(string jobId)
{
if (!_processingJobs.TryGetValue(jobId, out var job))
@@ -164,7 +187,7 @@ public class VideoController : ControllerBase
try
{
_logger.LogInformation("Processing video for job {JobId}", jobId);
_logger.LogInformation("Processing video for job {JobId} in session {SessionId}", jobId, job.SessionId);
// Step 1: Fetch video details
job.Progress.Status = "Processing";
@@ -195,6 +218,7 @@ public class VideoController : ControllerBase
job.Request.Url!,
job.Request.Format ?? "mp3",
jobId, // Pass the job ID for filename generation
job.SessionId, // Pass the session ID for directory organization
(percentage, message) =>
{
// Update progress from yt-dlp
@@ -217,8 +241,8 @@ public class VideoController : ControllerBase
job.Progress.Message = "Processing completed successfully";
job.Progress.PercentageComplete = 100;
// Create download URL for the actual file
var downloadUrl = $"/downloads/{Uri.EscapeDataString(downloadedFileName)}";
// Create download URL for the actual file within session
var downloadUrl = $"/api/download/{job.SessionId}/{jobId}";
job.Result = new ProcessVideoResponse
{
@@ -226,14 +250,15 @@ public class VideoController : ControllerBase
Message = "Video processed successfully",
FileName = downloadedFileName,
DownloadUrl = downloadUrl,
VideoDetails = videoDetails
VideoDetails = videoDetails,
SessionId = job.SessionId
};
_logger.LogInformation("Successfully completed processing for job {JobId}. File: {FileName}", jobId, downloadedFileName);
_logger.LogInformation("Successfully completed processing for job {JobId} in session {SessionId}. File: {FileName}", jobId, job.SessionId, downloadedFileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing video for job {JobId}", jobId);
_logger.LogError(ex, "Error processing video for job {JobId} in session {SessionId}", jobId, job.SessionId);
job.Progress.Status = "Failed";
job.Progress.Message = $"Processing failed: {ex.Message}";
@@ -245,7 +270,8 @@ public class VideoController : ControllerBase
Message = ex.Message,
FileName = null,
DownloadUrl = null,
VideoDetails = job.VideoDetails // Include video details even on failure if we got them
VideoDetails = job.VideoDetails, // Include video details even on failure if we got them
SessionId = job.SessionId
};
}
}