Files
TuneRipper/TuneRipper.Api/Controllers/VideoController.cs
T
Michal Vrba 3ecff7ca73 Refactor: Use jobId_title.ext for downloads & improve UX
Adopt GUID-based {jobId}_{title}.ext filename convention for all downloads, ensuring unique, human-readable, and traceable files. Update DownloadController to use precise jobId-based file matching and generate user-friendly download names. Pass jobId to VideoService for consistent naming; add robust title sanitization. Remove fallback file search logic for predictability. Update README and API docs to reflect new format and workflow. Improve logging and error messages for clarity.
2025-12-12 10:28:48 +01:00

252 lines
8.8 KiB
C#

using Microsoft.AspNetCore.Mvc;
using TuneRipper.Shared.Models;
using TuneRipper.Api.Services;
using System.Collections.Concurrent;
namespace TuneRipper.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class VideoController : ControllerBase
{
private readonly IVideoService _videoService;
private readonly ILogger<VideoController> _logger;
// In-memory storage for processing status (in production, use database or Redis)
private static readonly ConcurrentDictionary<string, ProcessingJob> _processingJobs = new();
private class ProcessingJob
{
public string Id { get; set; } = string.Empty;
public ProcessVideoRequest Request { get; set; } = new();
public DownloadProgress Progress { get; set; } = new();
public ProcessVideoResponse? Result { get; set; }
public YouTubeVideoDetails? VideoDetails { get; set; } // Add video details to job
public string? DownloadedFileName { get; set; } // Store actual downloaded file name
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public VideoController(IVideoService videoService, ILogger<VideoController> logger)
{
_videoService = videoService;
_logger = logger;
}
[HttpPost("process")]
public async Task<ActionResult<ProcessVideoResponse>> StartProcessing([FromBody] ProcessVideoRequest request)
{
if (request == null || string.IsNullOrWhiteSpace(request.Url))
{
return BadRequest("Invalid request");
}
if (!_videoService.IsValidYouTubeUrl(request.Url))
{
return BadRequest("Invalid YouTube URL");
}
try
{
_logger.LogInformation("Starting video processing for URL: {Url}", request.Url);
// Generate a job ID
var jobId = Guid.NewGuid().ToString();
// Create processing job
var job = new ProcessingJob
{
Id = jobId,
Request = request,
Progress = new DownloadProgress
{
PercentageComplete = 0,
Status = "Queued",
Message = "Processing request queued"
}
};
_processingJobs[jobId] = job;
// Start processing in background (fire and forget)
_ = Task.Run(() => ProcessVideoAsync(jobId));
var response = new ProcessVideoResponse
{
Success = true,
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
};
return Accepted(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting video processing for URL: {Url}", request.Url);
return StatusCode(500, $"Error starting video processing: {ex.Message}");
}
}
[HttpGet("status/{jobId}")]
public ActionResult<DownloadProgress> GetProcessingStatus(string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
}
if (!_processingJobs.TryGetValue(jobId, out var job))
{
return NotFound("Job not found");
}
// Return progress with video details if available
var progress = job.Progress;
// If we have video details and we're past the first step, include them in a custom response
if (job.VideoDetails != null && job.Progress.PercentageComplete >= 15)
{
return Ok(new
{
PercentageComplete = progress.PercentageComplete,
Status = progress.Status,
Message = progress.Message,
VideoDetails = job.VideoDetails
});
}
return Ok(progress);
}
[HttpGet("result/{jobId}")]
public ActionResult<ProcessVideoResponse> GetProcessingResult(string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
}
if (!_processingJobs.TryGetValue(jobId, out var job))
{
return NotFound("Job not found");
}
if (job.Result == null)
{
return Accepted("Processing not completed yet");
}
return Ok(job.Result);
}
[HttpGet("jobs")]
public ActionResult<IEnumerable<JobStatusResponse>> GetAllJobs()
{
var jobs = _processingJobs.Values.Select(job => new JobStatusResponse
{
JobId = job.Id,
Status = job.Progress.Status,
PercentageComplete = job.Progress.PercentageComplete,
Message = job.Progress.Message,
CreatedAt = job.CreatedAt,
CompletedAt = job.Result != null ? job.CreatedAt.AddMinutes(5) : null, // Simulated completion time
Result = job.Result
}).OrderByDescending(j => j.CreatedAt);
return Ok(jobs);
}
private async Task ProcessVideoAsync(string jobId)
{
if (!_processingJobs.TryGetValue(jobId, out var job))
return;
try
{
_logger.LogInformation("Processing video for job {JobId}", jobId);
// Step 1: Fetch video details
job.Progress.Status = "Processing";
job.Progress.Message = "Fetching video details...";
job.Progress.PercentageComplete = 5;
_logger.LogInformation("Job {JobId}: Fetching video details", jobId);
var videoDetails = await _videoService.GetVideoDetailsAsync(job.Request.Url!);
if (videoDetails == null)
{
throw new Exception("Unable to fetch video details. Invalid URL or video unavailable.");
}
// Store video details in the job
job.VideoDetails = videoDetails;
job.Progress.Message = "Video details retrieved successfully";
job.Progress.PercentageComplete = 15;
_logger.LogInformation("Job {JobId}: Video details retrieved - Title: {Title}", jobId, videoDetails.Title);
// Step 2: Process video with yt-dlp
job.Progress.Message = "Starting download and conversion...";
job.Progress.PercentageComplete = 20;
var downloadedFileName = await _videoService.ProcessVideoWithProgressAsync(
job.Request.Url!,
job.Request.Format ?? "mp3",
jobId, // Pass the job ID for filename generation
(percentage, message) =>
{
// Update progress from yt-dlp
job.Progress.PercentageComplete = Math.Max(20, percentage); // Ensure minimum 20%
job.Progress.Message = message;
_logger.LogDebug("Job {JobId}: Progress {Percentage}% - {Message}", jobId, percentage, message);
}
);
if (string.IsNullOrEmpty(downloadedFileName))
{
throw new Exception("Processing completed but no file was generated");
}
// Store the downloaded file name
job.DownloadedFileName = downloadedFileName;
// Complete processing
job.Progress.Status = "Completed";
job.Progress.Message = "Processing completed successfully";
job.Progress.PercentageComplete = 100;
// Create download URL for the actual file
var downloadUrl = $"/downloads/{Uri.EscapeDataString(downloadedFileName)}";
job.Result = new ProcessVideoResponse
{
Success = true,
Message = "Video processed successfully",
FileName = downloadedFileName,
DownloadUrl = downloadUrl,
VideoDetails = videoDetails
};
_logger.LogInformation("Successfully completed processing for job {JobId}. File: {FileName}", jobId, downloadedFileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing video for job {JobId}", jobId);
job.Progress.Status = "Failed";
job.Progress.Message = $"Processing failed: {ex.Message}";
job.Progress.PercentageComplete = 0;
job.Result = new ProcessVideoResponse
{
Success = false,
Message = ex.Message,
FileName = null,
DownloadUrl = null,
VideoDetails = job.VideoDetails // Include video details even on failure if we got them
};
}
}
}