Files
TuneRipper/TuneRipper.Api/Controllers/DownloadController.cs
T
Michal Vrba 3ecff7ca73 Refactor: Use jobId_title.ext for downloads & improve UX
Adopt GUID-based {jobId}_{title}.ext filename convention for all downloads, ensuring unique, human-readable, and traceable files. Update DownloadController to use precise jobId-based file matching and generate user-friendly download names. Pass jobId to VideoService for consistent naming; add robust title sanitization. Remove fallback file search logic for predictability. Update README and API docs to reflect new format and workflow. Improve logging and error messages for clarity.
2025-12-12 10:28:48 +01:00

240 lines
8.3 KiB
C#

using Microsoft.AspNetCore.Mvc;
namespace TuneRipper.Api.Controllers;
[ApiController]
[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)
{
if (string.IsNullOrWhiteSpace(jobId))
{
return BadRequest("Job ID is required");
}
try
{
var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads");
if (!Directory.Exists(downloadsDir))
{
_logger.LogWarning("Downloads directory not found: {Path}", downloadsDir);
return NotFound("Downloads directory not found");
}
// Find files with the specific job ID pattern: jobId_title.ext
var jobFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*")
.Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp"))
.OrderByDescending(System.IO.File.GetCreationTime)
.ToArray();
if (jobFiles.Length == 0)
{
_logger.LogWarning("No files found for job ID: {JobId}", jobId);
return NotFound("File not found for the specified job ID");
}
var filePath = jobFiles[0]; // Take the first (most recent) file
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 user-friendly download filename from our pattern
var downloadFileName = CreateUserFriendlyFileName(fileName);
var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
return File(fileStream, contentType, downloadFileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading file for job ID: {JobId}", jobId);
return StatusCode(500, $"Error downloading file: {ex.Message}");
}
}
private string CreateUserFriendlyFileName(string originalFileName)
{
try
{
// Parse our pattern: jobId_title.ext
var nameWithoutExt = Path.GetFileNameWithoutExtension(originalFileName);
var extension = Path.GetExtension(originalFileName);
// Split by underscore - first part is job ID, rest is title
var underscoreIndex = nameWithoutExt.IndexOf('_');
if (underscoreIndex > 0 && underscoreIndex < nameWithoutExt.Length - 1)
{
// Extract title part (everything after the first underscore)
var titlePart = nameWithoutExt.Substring(underscoreIndex + 1);
// Convert underscores back to spaces in title
var friendlyTitle = titlePart.Replace("_", " ");
// Create a clean filename: "Title.mp3"
if (friendlyTitle.Length > 0)
{
// Remove any duplicate spaces and clean up
friendlyTitle = System.Text.RegularExpressions.Regex.Replace(friendlyTitle, @"\s+", " ").Trim();
return $"{friendlyTitle}{extension}";
}
}
// Fallback to original filename if pattern doesn't match
return originalFileName;
}
catch (Exception)
{
// If anything goes wrong, return the original filename
return originalFileName;
}
}
[HttpGet("info/{jobId}")]
public IActionResult GetDownloadInfo(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 files with the specific job ID pattern
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 for the specified job ID");
}
var filePath = jobFiles[0];
var fileName = Path.GetFileName(filePath);
var fileInfo = new FileInfo(filePath);
var friendlyFileName = CreateUserFriendlyFileName(fileName);
// Return download metadata
var info = new
{
jobId,
fileName,
friendlyFileName,
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 with the specific job ID pattern
var jobFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*")
.ToArray();
var deletedCount = 0;
foreach (var filePath in jobFiles)
{
try
{
System.IO.File.Delete(filePath);
deletedCount++;
_logger.LogInformation("Deleted file: {FilePath} for job: {JobId}", filePath, jobId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete file: {FilePath}", filePath);
}
}
if (deletedCount == 0)
{
return NotFound("No files found to delete for the specified job ID");
}
return Ok(new { message = $"Deleted {deletedCount} file(s) for job {jobId}", deletedCount, jobId });
}
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]}";
}
}