Initial implementation of TuneRipper API backend
- Add Health, Video, and Download controllers with endpoints for health checks, video details, processing, status, results, and simulated downloads - Implement IVideoService for YouTube URL validation and mock video info - Add API models for responses and job status tracking - Configure DI, CORS (BlazorPolicy), OpenAPI, and root metadata endpoint in Program.cs - Add README with usage, troubleshooting, and development notes - Minor project file cleanup and encoding fix
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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 DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public VideoController(IVideoService videoService, ILogger<VideoController> logger)
|
||||
{
|
||||
_videoService = videoService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("details")]
|
||||
public async Task<ActionResult<YouTubeVideoDetails>> GetVideoDetails([FromQuery] string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return BadRequest("URL is required");
|
||||
}
|
||||
|
||||
if (!_videoService.IsValidYouTubeUrl(url))
|
||||
{
|
||||
return BadRequest("Invalid YouTube URL");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Fetching video details for URL: {Url}", url);
|
||||
|
||||
var videoDetails = await _videoService.GetVideoDetailsAsync(url);
|
||||
|
||||
if (videoDetails == null)
|
||||
{
|
||||
return NotFound("Video not found or unavailable");
|
||||
}
|
||||
|
||||
return Ok(videoDetails);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching video details for URL: {Url}", url);
|
||||
return StatusCode(500, $"Error fetching video details: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[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 Ok(job.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);
|
||||
|
||||
// Simulate video processing steps
|
||||
var steps = new[]
|
||||
{
|
||||
("Fetching video information", 10),
|
||||
("Analyzing audio stream", 25),
|
||||
("Downloading audio data", 60),
|
||||
("Converting to MP3", 85),
|
||||
("Finalizing", 100)
|
||||
};
|
||||
|
||||
foreach (var (message, percentage) in steps)
|
||||
{
|
||||
job.Progress.Status = "Processing";
|
||||
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));
|
||||
}
|
||||
|
||||
// Get video details using the service
|
||||
var videoDetails = await _videoService.GetVideoDetailsAsync(job.Request.Url!);
|
||||
|
||||
// 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 = null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user