using Microsoft.AspNetCore.Mvc; using TuneRipper.Api.Services; namespace TuneRipper.Api.Controllers; [ApiController] [Route("api/[controller]")] public class DownloadController : ControllerBase { private readonly IWebHostEnvironment _environment; private readonly ILogger _logger; private readonly ISessionService _sessionService; public DownloadController(IWebHostEnvironment environment, ILogger logger, ISessionService sessionService) { _environment = environment; _logger = logger; _sessionService = sessionService; } // New session-aware download endpoint [HttpGet("{sessionId}/{jobId}")] public IActionResult DownloadFileBySession(string sessionId, string jobId) { if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(jobId)) { return BadRequest("Session ID and Job ID are required"); } if (!_sessionService.IsValidSession(sessionId)) { return NotFound("Invalid or expired session"); } try { var sessionDownloadsDir = _sessionService.GetSessionDownloadsPath(sessionId); if (!Directory.Exists(sessionDownloadsDir)) { _logger.LogWarning("Session downloads directory not found: {Path}", sessionDownloadsDir); return NotFound("Session downloads directory not found"); } // Find files with the specific job ID pattern in the session directory var jobFiles = Directory.GetFiles(sessionDownloadsDir, $"{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} in session: {SessionId}", jobId, sessionId); 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} in session: {SessionId}", fileName, jobId, sessionId); // 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} in session: {SessionId}", jobId, sessionId); return StatusCode(500, $"Error downloading file: {ex.Message}"); } } // Legacy endpoint for backward compatibility [HttpGet("{jobId}")] public IActionResult DownloadFile(string jobId) { if (string.IsNullOrWhiteSpace(jobId)) { return BadRequest("Job ID is required"); } try { // Check in the legacy downloads directory first var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads"); if (Directory.Exists(downloadsDir)) { var legacyFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*") .Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp")) .OrderByDescending(System.IO.File.GetCreationTime) .ToArray(); if (legacyFiles.Length > 0) { return ServeFile(legacyFiles[0], jobId, null); } } // If not found in legacy location, search all session directories var sessionDirs = Directory.GetDirectories(downloadsDir, "*", SearchOption.TopDirectoryOnly); foreach (var sessionDir in sessionDirs) { var sessionId = Path.GetFileName(sessionDir); if (!_sessionService.IsValidSession(sessionId)) continue; var jobFiles = Directory.GetFiles(sessionDir, $"{jobId}_*.*") .Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp")) .OrderByDescending(System.IO.File.GetCreationTime) .ToArray(); if (jobFiles.Length > 0) { return ServeFile(jobFiles[0], jobId, sessionId); } } _logger.LogWarning("No files found for job ID: {JobId}", jobId); return NotFound("File not found for the specified job ID"); } catch (Exception ex) { _logger.LogError(ex, "Error downloading file for job ID: {JobId}", jobId); return StatusCode(500, $"Error downloading file: {ex.Message}"); } } private IActionResult ServeFile(string filePath, string jobId, string? sessionId) { var fileName = Path.GetFileName(filePath); if (!System.IO.File.Exists(filePath)) { _logger.LogWarning("File not found: {FilePath}", filePath); return NotFound("File not found"); } var sessionInfo = sessionId != null ? $" in session: {sessionId}" : ""; _logger.LogInformation("Serving file: {FileName} for job: {JobId}{SessionInfo}", fileName, jobId, sessionInfo); // 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); } 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/{sessionId}/{jobId}")] public IActionResult GetDownloadInfoBySession(string sessionId, string jobId) { if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(jobId)) { return BadRequest("Session ID and Job ID are required"); } if (!_sessionService.IsValidSession(sessionId)) { return NotFound("Invalid or expired session"); } try { var sessionDownloadsDir = _sessionService.GetSessionDownloadsPath(sessionId); if (!Directory.Exists(sessionDownloadsDir)) { return NotFound("Session downloads directory not found"); } // Find files with the specific job ID pattern in the session directory var jobFiles = Directory.GetFiles(sessionDownloadsDir, $"{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, sessionId, fileName, friendlyFileName, fileSize = FormatFileSize(fileInfo.Length), fileSizeBytes = fileInfo.Length, format = Path.GetExtension(fileName).TrimStart('.').ToUpperInvariant(), createdAt = fileInfo.CreationTime, lastModified = fileInfo.LastWriteTime, downloadUrl = $"/api/download/{sessionId}/{jobId}" }; return Ok(info); } catch (Exception ex) { _logger.LogError(ex, "Error getting download info for job ID: {JobId} in session: {SessionId}", jobId, sessionId); return StatusCode(500, $"Error getting download info: {ex.Message}"); } } // Legacy info endpoint for backward compatibility [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"); } // Check legacy location first var legacyFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*") .Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp")) .OrderByDescending(System.IO.File.GetCreationTime) .ToArray(); if (legacyFiles.Length > 0) { return GetFileInfo(legacyFiles[0], jobId, null); } // Search session directories var sessionDirs = Directory.GetDirectories(downloadsDir, "*", SearchOption.TopDirectoryOnly); foreach (var sessionDir in sessionDirs) { var sessionId = Path.GetFileName(sessionDir); if (!_sessionService.IsValidSession(sessionId)) continue; var jobFiles = Directory.GetFiles(sessionDir, $"{jobId}_*.*") .Where(f => !f.EndsWith(".part") && !f.EndsWith(".tmp")) .OrderByDescending(System.IO.File.GetCreationTime) .ToArray(); if (jobFiles.Length > 0) { return GetFileInfo(jobFiles[0], jobId, sessionId); } } return NotFound("File not found for the specified job ID"); } catch (Exception ex) { _logger.LogError(ex, "Error getting download info for job ID: {JobId}", jobId); return StatusCode(500, $"Error getting download info: {ex.Message}"); } } private IActionResult GetFileInfo(string filePath, string jobId, string? sessionId) { var fileName = Path.GetFileName(filePath); var fileInfo = new FileInfo(filePath); var friendlyFileName = CreateUserFriendlyFileName(fileName); var downloadUrl = sessionId != null ? $"/api/download/{sessionId}/{jobId}" : $"/api/download/{jobId}"; var info = new { jobId, sessionId, fileName, friendlyFileName, fileSize = FormatFileSize(fileInfo.Length), fileSizeBytes = fileInfo.Length, format = Path.GetExtension(fileName).TrimStart('.').ToUpperInvariant(), createdAt = fileInfo.CreationTime, lastModified = fileInfo.LastWriteTime, downloadUrl }; return Ok(info); } [HttpDelete("{sessionId}/{jobId}")] public IActionResult DeleteFileBySession(string sessionId, string jobId) { if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(jobId)) { return BadRequest("Session ID and Job ID are required"); } if (!_sessionService.IsValidSession(sessionId)) { return NotFound("Invalid or expired session"); } try { var sessionDownloadsDir = _sessionService.GetSessionDownloadsPath(sessionId); if (!Directory.Exists(sessionDownloadsDir)) { return NotFound("Session downloads directory not found"); } // Find and delete files with the specific job ID pattern in the session directory var jobFiles = Directory.GetFiles(sessionDownloadsDir, $"{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} in session: {SessionId}", filePath, jobId, sessionId); } 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} in session {sessionId}", deletedCount, jobId, sessionId }); } catch (Exception ex) { _logger.LogError(ex, "Error deleting files for job ID: {JobId} in session: {SessionId}", jobId, sessionId); return StatusCode(500, $"Error deleting files: {ex.Message}"); } } // Legacy delete endpoint for backward compatibility [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"); var deletedCount = 0; if (!Directory.Exists(downloadsDir)) { return NotFound("Downloads directory not found"); } // Delete from legacy location var legacyFiles = Directory.GetFiles(downloadsDir, $"{jobId}_*.*"); foreach (var filePath in legacyFiles) { try { System.IO.File.Delete(filePath); deletedCount++; _logger.LogInformation("Deleted legacy file: {FilePath} for job: {JobId}", filePath, jobId); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to delete legacy file: {FilePath}", filePath); } } // Delete from session directories var sessionDirs = Directory.GetDirectories(downloadsDir, "*", SearchOption.TopDirectoryOnly); foreach (var sessionDir in sessionDirs) { var sessionId = Path.GetFileName(sessionDir); var jobFiles = Directory.GetFiles(sessionDir, $"{jobId}_*.*"); foreach (var filePath in jobFiles) { try { System.IO.File.Delete(filePath); deletedCount++; _logger.LogInformation("Deleted file: {FilePath} for job: {JobId} in session: {SessionId}", filePath, jobId, sessionId); } 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]}"; } }