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.
240 lines
8.3 KiB
C#
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]}";
|
|
}
|
|
} |