Organize downloaded files into per-job subdirectories under "downloads" instead of a flat structure. Update DownloadController and VideoService to read, write, and delete files within these job-specific directories. Add helper methods for directory management and improve logging. Update download/delete endpoints and result URLs to reflect the new structure. Clean up empty job directories after file deletion.
278 lines
10 KiB
C#
278 lines
10 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 ISessionService _sessionService;
|
|
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 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<VideoController> logger)
|
|
{
|
|
_videoService = videoService;
|
|
_sessionService = sessionService;
|
|
_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);
|
|
|
|
// 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<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/{sessionId?}")]
|
|
public ActionResult<IEnumerable<JobStatusResponse>> 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<IEnumerable<JobStatusResponse>> 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
|
|
};
|
|
}
|
|
}
|
|
} |