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
+80 -12
View File
@@ -13,7 +13,7 @@ public interface IVideoService
Task<YouTubeVideoDetails?> GetVideoDetailsAsync(string url);
bool IsValidYouTubeUrl(string url);
string ExtractVideoId(string url);
Task<string?> ProcessVideoWithProgressAsync(string url, string format, Action<int, string> progressCallback);
Task<string?> ProcessVideoWithProgressAsync(string url, string format, string jobId, Action<int, string> progressCallback);
}
public class VideoService : IVideoService
@@ -90,7 +90,7 @@ public class VideoService : IVideoService
}
}
public async Task<string?> ProcessVideoWithProgressAsync(string url, string format, Action<int, string> progressCallback)
public async Task<string?> ProcessVideoWithProgressAsync(string url, string format, string jobId, Action<int, string> progressCallback)
{
try
{
@@ -105,10 +105,15 @@ public class VideoService : IVideoService
var downloadDir = Path.Combine(_environment.WebRootPath, "downloads");
Directory.CreateDirectory(downloadDir);
// Generate safe filename
var videoId = ExtractVideoId(url);
var fileName = $"{videoId}_{DateTime.Now:yyyyMMdd_HHmmss}.%(ext)s";
var outputTemplate = Path.Combine(downloadDir, fileName);
// Get video details first for better filename
var videoDetails = await GetVideoDetailsAsync(url);
// Generate filename with format: jobId_title.mp3
var safeTitle = SanitizeFileName(videoDetails?.Title ?? "video");
var customFileName = $"{jobId}_{safeTitle}";
var outputTemplate = Path.Combine(downloadDir, $"{customFileName}.%(ext)s");
_logger.LogInformation("Processing video with filename pattern: {FileName}", customFileName);
// Build yt-dlp arguments
var arguments = new StringBuilder();
@@ -183,21 +188,21 @@ public class VideoService : IVideoService
throw new Exception($"yt-dlp failed with exit code {process.ExitCode}. Error: {error}");
}
// Find the downloaded file
var downloadedFiles = Directory.GetFiles(downloadDir, $"{videoId}_*.*")
// Find the downloaded file with our job ID pattern
var downloadedFiles = Directory.GetFiles(downloadDir, $"{jobId}_*.*")
.Where(f => !f.EndsWith(".part"))
.OrderByDescending(File.GetCreationTime)
.OrderByDescending(System.IO.File.GetCreationTime)
.ToArray();
if (downloadedFiles.Length == 0)
{
throw new Exception("No downloaded file found after processing");
throw new Exception($"No downloaded file found for job {jobId} after processing");
}
var downloadedFile = downloadedFiles[0];
var finalFileName = Path.GetFileName(downloadedFile);
_logger.LogInformation("Successfully processed video. File: {FileName}", finalFileName);
_logger.LogInformation("Successfully processed video. Job: {JobId}, File: {FileName}", jobId, finalFileName);
progressCallback(100, "Processing completed successfully");
@@ -205,12 +210,75 @@ public class VideoService : IVideoService
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing video with yt-dlp: {Url}", url);
_logger.LogError(ex, "Error processing video with yt-dlp for job {JobId}: {Url}", jobId, url);
progressCallback(0, $"Processing failed: {ex.Message}");
throw;
}
}
private string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
{
return "video";
}
// Remove or replace invalid filename characters
var invalidChars = Path.GetInvalidFileNameChars();
var sanitized = new StringBuilder();
foreach (char c in fileName.Take(50)) // Limit length to 50 characters
{
if (invalidChars.Contains(c))
{
// Replace invalid characters with underscores or safe alternatives
switch (c)
{
case '/':
case '\\':
case '?':
case '*':
sanitized.Append('_');
break;
case ':':
case '|':
sanitized.Append('-');
break;
case '<':
sanitized.Append('(');
break;
case '>':
sanitized.Append(')');
break;
case '"':
sanitized.Append('\'');
break;
default:
sanitized.Append('_');
break;
}
}
else if (char.IsWhiteSpace(c))
{
sanitized.Append('_');
}
else
{
sanitized.Append(c);
}
}
var result = sanitized.ToString().Trim('_');
// Ensure we have a valid filename
if (string.IsNullOrWhiteSpace(result))
{
result = "video";
}
return result;
}
private void ParseProgressFromOutput(string output, Action<int, string> progressCallback)
{
try