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.
This commit is contained in:
Michal Vrba
2025-12-12 10:28:48 +01:00
parent 406cf31b49
commit 3ecff7ca73
4 changed files with 247 additions and 102 deletions
@@ -33,33 +33,19 @@ public class DownloadController : ControllerBase
return NotFound("Downloads directory not found");
}
// Find files related to this job ID
var jobFiles = Directory.GetFiles(downloadsDir, $"*{jobId}*.*")
// 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)
{
// 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();
_logger.LogWarning("No files found for job ID: {JobId}", jobId);
return NotFound("File not found for the specified job ID");
}
var filePath = jobFiles[0];
var filePath = jobFiles[0]; // Take the first (most recent) file
var fileName = Path.GetFileName(filePath);
if (!System.IO.File.Exists(filePath))
@@ -81,21 +67,11 @@ public class DownloadController : ControllerBase
_ => "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}";
}
}
// 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, safeFileName);
return File(fileStream, contentType, downloadFileName);
}
catch (Exception ex)
{
@@ -104,6 +80,44 @@ public class DownloadController : ControllerBase
}
}
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)
{
@@ -121,26 +135,28 @@ public class DownloadController : ControllerBase
return NotFound("Downloads directory not found");
}
// Find files related to this job ID
var jobFiles = Directory.GetFiles(downloadsDir, $"*{jobId}*.*")
// 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");
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(),
@@ -175,8 +191,10 @@ public class DownloadController : ControllerBase
return NotFound("Downloads directory not found");
}
// Find and delete files related to this job ID
var jobFiles = Directory.GetFiles(downloadsDir, $"*{jobId}*.*");
// 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)
@@ -185,7 +203,7 @@ public class DownloadController : ControllerBase
{
System.IO.File.Delete(filePath);
deletedCount++;
_logger.LogInformation("Deleted file: {FilePath}", filePath);
_logger.LogInformation("Deleted file: {FilePath} for job: {JobId}", filePath, jobId);
}
catch (Exception ex)
{
@@ -195,10 +213,10 @@ public class DownloadController : ControllerBase
if (deletedCount == 0)
{
return NotFound("No files found to delete");
return NotFound("No files found to delete for the specified job ID");
}
return Ok(new { message = $"Deleted {deletedCount} file(s)", deletedCount });
return Ok(new { message = $"Deleted {deletedCount} file(s) for job {jobId}", deletedCount, jobId });
}
catch (Exception ex)
{