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 ISessionService _sessionService; private readonly ILogger _logger; // In-memory storage for processing status (in production, use database or Redis) private static readonly ConcurrentDictionary _processingJobs = new(); 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; } 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, ISessionService sessionService, ILogger logger) { _videoService = videoService; _sessionService = sessionService; _logger = logger; } [HttpPost("process")] public async Task> 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); // Get or create session var sessionId = _sessionService.GetOrCreateSession(request.SessionId); // Generate a job ID var jobId = Guid.NewGuid().ToString(); // Create processing job var job = new ProcessingJob { Id = jobId, SessionId = sessionId, 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 SessionId = sessionId // Return session ID to client }; 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 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 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/{sessionId?}")] public ActionResult> GetJobsBySession(string? sessionId = null) { 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, 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); } // Keep the old endpoint for backward compatibility [HttpGet("jobs")] public ActionResult> GetAllJobs() { return GetJobsBySession(null); } private async Task ProcessVideoAsync(string jobId) { if (!_processingJobs.TryGetValue(jobId, out var job)) return; try { _logger.LogInformation("Processing video for job {JobId} in session {SessionId}", jobId, job.SessionId); // 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 job.SessionId, // Pass the session ID for directory organization (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 within session var downloadUrl = $"/api/download/{job.SessionId}/{jobId}"; job.Result = new ProcessVideoResponse { Success = true, Message = "Video processed successfully", FileName = downloadedFileName, DownloadUrl = downloadUrl, VideoDetails = videoDetails, SessionId = job.SessionId }; _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} in session {SessionId}", jobId, job.SessionId); 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 SessionId = job.SessionId }; } } }