Removed /api/video/details endpoint; video details now fetched as first step of processing and included in progress/status responses. Updated backend models and controller logic to support integrated workflow and improved error handling. Refactored frontend to show video info and conversion progress in real time, removed legacy API calls, and enhanced UI/UX with responsive video info cards and progress bars. Updated documentation and example code to reflect new workflow.
243 lines
8.3 KiB
C#
243 lines
8.3 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 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 (NEW FIRST STEP)
|
|
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);
|
|
|
|
// Continue with remaining processing steps
|
|
var remainingSteps = new[]
|
|
{
|
|
("Analyzing audio stream", 30),
|
|
("Downloading audio data", 60),
|
|
("Converting to MP3", 85),
|
|
("Finalizing", 100)
|
|
};
|
|
|
|
foreach (var (message, percentage) in remainingSteps)
|
|
{
|
|
job.Progress.Message = message;
|
|
job.Progress.PercentageComplete = percentage;
|
|
|
|
_logger.LogInformation("Job {JobId}: {Message} ({Percentage}%)", jobId, message, percentage);
|
|
|
|
// Simulate processing time
|
|
await Task.Delay(Random.Shared.Next(1000, 3000));
|
|
}
|
|
|
|
// Complete processing
|
|
job.Progress.Status = "Completed";
|
|
job.Progress.Message = "Processing completed successfully";
|
|
job.Progress.PercentageComplete = 100;
|
|
|
|
job.Result = new ProcessVideoResponse
|
|
{
|
|
Success = true,
|
|
Message = "Video processed successfully",
|
|
FileName = $"{videoDetails.Title?.Replace(" ", "_") ?? "video"}.{job.Request.Format ?? "mp3"}",
|
|
DownloadUrl = $"/api/download/{jobId}",
|
|
VideoDetails = videoDetails
|
|
};
|
|
|
|
_logger.LogInformation("Successfully completed processing for job {JobId}", jobId);
|
|
}
|
|
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
|
|
};
|
|
}
|
|
}
|
|
} |