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:
Michal Vrba
2025-12-11 14:27:29 +01:00
parent 5230bddc03
commit e2d9ee3c59
8 changed files with 637 additions and 6 deletions
@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc;
namespace TuneRipper.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class DownloadController : ControllerBase
{
[HttpGet("{jobId}")]
public IActionResult DownloadFile(string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
}
try
{
// In production, you would:
// 1. Verify the job exists and is completed
// 2. Get the actual file path from storage
// 3. Return the actual file
// For demo purposes, return a sample response
var sampleContent = System.Text.Encoding.UTF8.GetBytes("Sample MP3 file content - this would be actual audio data in production");
var fileName = $"video_{jobId}.mp3";
return File(sampleContent, "audio/mpeg", fileName);
}
catch (Exception ex)
{
return StatusCode(500, $"Error downloading file: {ex.Message}");
}
}
[HttpGet("info/{jobId}")]
public IActionResult GetDownloadInfo(string jobId)
{
if (string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
}
// Return download metadata
var info = new
{
jobId,
fileName = $"video_{jobId}.mp3",
fileSize = "3.2 MB",
format = "MP3",
quality = "128 kbps",
createdAt = DateTime.UtcNow,
expiresAt = DateTime.UtcNow.AddHours(24)
};
return Ok(info);
}
}
@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Mvc;
namespace TuneRipper.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class HealthController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
var response = new
{
status = "healthy",
timestamp = DateTime.UtcNow,
version = "1.0.0",
service = "TuneRipper.Api",
environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"
};
return Ok(response);
}
[HttpGet("ping")]
public IActionResult Ping()
{
return Ok(new { message = "pong", timestamp = DateTime.UtcNow });
}
}
@@ -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
};
}
}
}