Real YouTube download & MP3 conversion with yt-dlp
- Integrate yt-dlp.exe for real YouTube audio download/conversion - Serve actual MP3 files from wwwroot/downloads via API and static files - Report real-time progress from yt-dlp to frontend - Enhance DownloadController: file serving, metadata, deletion endpoints - Update VideoController to track and return real file info/URLs - Enable static file serving in Program.cs; update API info/logging - Update Blazor frontend to use real download URLs and remove old JS logic - Add README_UPDATED.md with new workflow and troubleshooting - Improve error handling, logging, and file cleanup throughout backend - Update .gitignore for downloads and yt-dlp.exe
This commit is contained in:
@@ -6,6 +6,15 @@ namespace TuneRipper.Api.Controllers;
|
||||
[Route("api/[controller]")]
|
||||
public class DownloadController : ControllerBase
|
||||
{
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly ILogger<DownloadController> _logger;
|
||||
|
||||
public DownloadController(IWebHostEnvironment environment, ILogger<DownloadController> logger)
|
||||
{
|
||||
_environment = environment;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("{jobId}")]
|
||||
public IActionResult DownloadFile(string jobId)
|
||||
{
|
||||
@@ -16,19 +25,81 @@ public class DownloadController : ControllerBase
|
||||
|
||||
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";
|
||||
var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads");
|
||||
|
||||
return File(sampleContent, "audio/mpeg", fileName);
|
||||
if (!Directory.Exists(downloadsDir))
|
||||
{
|
||||
_logger.LogWarning("Downloads directory not found: {Path}", downloadsDir);
|
||||
return NotFound("Downloads directory not found");
|
||||
}
|
||||
|
||||
// Find files related to this job ID
|
||||
var jobFiles = Directory.GetFiles(downloadsDir, $"*{jobId}*.*")
|
||||
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
|
||||
.OrderByDescending(System.IO.File.GetCreationTime)
|
||||
.ToArray();
|
||||
|
||||
if (jobFiles.Length == 0)
|
||||
{
|
||||
// Fallback: look for any audio files that might match the pattern
|
||||
var audioFiles = Directory.GetFiles(downloadsDir, "*.*")
|
||||
.Where(f => f.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase) ||
|
||||
f.EndsWith(".m4a", StringComparison.OrdinalIgnoreCase) ||
|
||||
f.EndsWith(".webm", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
|
||||
.OrderByDescending(System.IO.File.GetCreationTime)
|
||||
.ToArray();
|
||||
|
||||
if (audioFiles.Length == 0)
|
||||
{
|
||||
_logger.LogWarning("No files found for job ID: {JobId}", jobId);
|
||||
return NotFound("File not found");
|
||||
}
|
||||
|
||||
jobFiles = audioFiles.Take(1).ToArray();
|
||||
}
|
||||
|
||||
var filePath = jobFiles[0];
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
{
|
||||
_logger.LogWarning("File not found: {FilePath}", filePath);
|
||||
return NotFound("File not found");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Serving file: {FileName} for job: {JobId}", fileName, jobId);
|
||||
|
||||
// Determine content type based on file extension
|
||||
var contentType = Path.GetExtension(fileName).ToLowerInvariant() switch
|
||||
{
|
||||
".mp3" => "audio/mpeg",
|
||||
".m4a" => "audio/mp4",
|
||||
".webm" => "audio/webm",
|
||||
".wav" => "audio/wav",
|
||||
".ogg" => "audio/ogg",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
|
||||
// Create a safe download filename
|
||||
var safeFileName = fileName;
|
||||
if (fileName.Contains("_"))
|
||||
{
|
||||
// Try to create a more user-friendly filename
|
||||
var parts = fileName.Split('_');
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName);
|
||||
safeFileName = $"TuneRipper_{parts[0]}{extension}";
|
||||
}
|
||||
}
|
||||
|
||||
var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
return File(fileStream, contentType, safeFileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error downloading file for job ID: {JobId}", jobId);
|
||||
return StatusCode(500, $"Error downloading file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
@@ -41,18 +112,111 @@ public class DownloadController : ControllerBase
|
||||
return BadRequest("Job ID is required");
|
||||
}
|
||||
|
||||
// Return download metadata
|
||||
var info = new
|
||||
try
|
||||
{
|
||||
jobId,
|
||||
fileName = $"video_{jobId}.mp3",
|
||||
fileSize = "3.2 MB",
|
||||
format = "MP3",
|
||||
quality = "128 kbps",
|
||||
createdAt = DateTime.UtcNow,
|
||||
expiresAt = DateTime.UtcNow.AddHours(24)
|
||||
};
|
||||
var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads");
|
||||
|
||||
if (!Directory.Exists(downloadsDir))
|
||||
{
|
||||
return NotFound("Downloads directory not found");
|
||||
}
|
||||
|
||||
return Ok(info);
|
||||
// Find files related to this job ID
|
||||
var jobFiles = Directory.GetFiles(downloadsDir, $"*{jobId}*.*")
|
||||
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
|
||||
.OrderByDescending(System.IO.File.GetCreationTime)
|
||||
.ToArray();
|
||||
|
||||
if (jobFiles.Length == 0)
|
||||
{
|
||||
return NotFound("File not found");
|
||||
}
|
||||
|
||||
var filePath = jobFiles[0];
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
// Return download metadata
|
||||
var info = new
|
||||
{
|
||||
jobId,
|
||||
fileName,
|
||||
fileSize = FormatFileSize(fileInfo.Length),
|
||||
fileSizeBytes = fileInfo.Length,
|
||||
format = Path.GetExtension(fileName).TrimStart('.').ToUpperInvariant(),
|
||||
createdAt = fileInfo.CreationTime,
|
||||
lastModified = fileInfo.LastWriteTime,
|
||||
downloadUrl = $"/api/download/{jobId}"
|
||||
};
|
||||
|
||||
return Ok(info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting download info for job ID: {JobId}", jobId);
|
||||
return StatusCode(500, $"Error getting download info: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{jobId}")]
|
||||
public IActionResult DeleteFile(string jobId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jobId))
|
||||
{
|
||||
return BadRequest("Job ID is required");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads");
|
||||
|
||||
if (!Directory.Exists(downloadsDir))
|
||||
{
|
||||
return NotFound("Downloads directory not found");
|
||||
}
|
||||
|
||||
// Find and delete files related to this job ID
|
||||
var jobFiles = Directory.GetFiles(downloadsDir, $"*{jobId}*.*");
|
||||
var deletedCount = 0;
|
||||
|
||||
foreach (var filePath in jobFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.File.Delete(filePath);
|
||||
deletedCount++;
|
||||
_logger.LogInformation("Deleted file: {FilePath}", filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to delete file: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount == 0)
|
||||
{
|
||||
return NotFound("No files found to delete");
|
||||
}
|
||||
|
||||
return Ok(new { message = $"Deleted {deletedCount} file(s)", deletedCount });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting files for job ID: {JobId}", jobId);
|
||||
return StatusCode(500, $"Error deleting files: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatFileSize(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB" };
|
||||
double len = bytes;
|
||||
int order = 0;
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len = len / 1024;
|
||||
}
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user