diff --git a/.gitignore b/.gitignore index 9491a2f..ceaf7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,5 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +/TuneRipper.Api/wwwroot/downloads diff --git a/TuneRipper.Api/Controllers/DownloadController.cs b/TuneRipper.Api/Controllers/DownloadController.cs index b932542..8f69c0d 100644 --- a/TuneRipper.Api/Controllers/DownloadController.cs +++ b/TuneRipper.Api/Controllers/DownloadController.cs @@ -6,6 +6,15 @@ namespace TuneRipper.Api.Controllers; [Route("api/[controller]")] public class DownloadController : ControllerBase { + private readonly IWebHostEnvironment _environment; + private readonly ILogger _logger; + + public DownloadController(IWebHostEnvironment environment, ILogger logger) + { + _environment = environment; + _logger = logger; + } + [HttpGet("{jobId}")] public IActionResult DownloadFile(string jobId) { @@ -16,19 +25,81 @@ public class DownloadController : ControllerBase try { - // In production, you would: - // 1. Verify the job exists and is completed - // 2. Get the actual file path from storage - // 3. Return the actual file - - // For demo purposes, return a sample response - var sampleContent = System.Text.Encoding.UTF8.GetBytes("Sample MP3 file content - this would be actual audio data in production"); - var fileName = $"video_{jobId}.mp3"; + var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads"); - return File(sampleContent, "audio/mpeg", fileName); + if (!Directory.Exists(downloadsDir)) + { + _logger.LogWarning("Downloads directory not found: {Path}", downloadsDir); + return NotFound("Downloads directory not found"); + } + + // Find files related to this job ID + 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(); + } + + var filePath = jobFiles[0]; + 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 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}"; + } + } + + var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + return File(fileStream, contentType, safeFileName); } catch (Exception ex) { + _logger.LogError(ex, "Error downloading file for job ID: {JobId}", jobId); return StatusCode(500, $"Error downloading file: {ex.Message}"); } } @@ -41,18 +112,111 @@ public class DownloadController : ControllerBase return BadRequest("Job ID is required"); } - // Return download metadata - var info = new + try { - jobId, - fileName = $"video_{jobId}.mp3", - fileSize = "3.2 MB", - format = "MP3", - quality = "128 kbps", - createdAt = DateTime.UtcNow, - expiresAt = DateTime.UtcNow.AddHours(24) - }; + var downloadsDir = Path.Combine(_environment.WebRootPath, "downloads"); + + if (!Directory.Exists(downloadsDir)) + { + return NotFound("Downloads directory not found"); + } - return Ok(info); + // Find files related to this job ID + 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"); + } + + var filePath = jobFiles[0]; + var fileName = Path.GetFileName(filePath); + var fileInfo = new FileInfo(filePath); + + // Return download metadata + var info = new + { + jobId, + fileName, + 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 related to this job ID + var jobFiles = Directory.GetFiles(downloadsDir, $"*{jobId}*.*"); + var deletedCount = 0; + + foreach (var filePath in jobFiles) + { + try + { + System.IO.File.Delete(filePath); + deletedCount++; + _logger.LogInformation("Deleted file: {FilePath}", filePath); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete file: {FilePath}", filePath); + } + } + + if (deletedCount == 0) + { + return NotFound("No files found to delete"); + } + + return Ok(new { message = $"Deleted {deletedCount} file(s)", deletedCount }); + } + 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]}"; } } \ No newline at end of file diff --git a/TuneRipper.Api/Controllers/VideoController.cs b/TuneRipper.Api/Controllers/VideoController.cs index d29a7a9..849d304 100644 --- a/TuneRipper.Api/Controllers/VideoController.cs +++ b/TuneRipper.Api/Controllers/VideoController.cs @@ -22,6 +22,7 @@ public class VideoController : ControllerBase public DownloadProgress Progress { get; set; } = new(); public ProcessVideoResponse? Result { get; set; } public YouTubeVideoDetails? VideoDetails { get; set; } // Add video details to job + public string? DownloadedFileName { get; set; } // Store actual downloaded file name public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } @@ -165,7 +166,7 @@ public class VideoController : ControllerBase { _logger.LogInformation("Processing video for job {JobId}", jobId); - // Step 1: Fetch video details (NEW FIRST STEP) + // Step 1: Fetch video details job.Progress.Status = "Processing"; job.Progress.Message = "Fetching video details..."; job.Progress.PercentageComplete = 5; @@ -186,41 +187,48 @@ public class VideoController : ControllerBase _logger.LogInformation("Job {JobId}: Video details retrieved - Title: {Title}", jobId, videoDetails.Title); - // Continue with remaining processing steps - var remainingSteps = new[] - { - ("Analyzing audio stream", 30), - ("Downloading audio data", 60), - ("Converting to MP3", 85), - ("Finalizing", 100) - }; + // Step 2: Process video with yt-dlp + job.Progress.Message = "Starting download and conversion..."; + job.Progress.PercentageComplete = 20; + + var downloadedFileName = await _videoService.ProcessVideoWithProgressAsync( + job.Request.Url!, + job.Request.Format ?? "mp3", + (percentage, message) => + { + // Update progress from yt-dlp + job.Progress.PercentageComplete = Math.Max(20, percentage); // Ensure minimum 20% + job.Progress.Message = message; + _logger.LogDebug("Job {JobId}: Progress {Percentage}% - {Message}", jobId, percentage, message); + } + ); - foreach (var (message, percentage) in remainingSteps) + if (string.IsNullOrEmpty(downloadedFileName)) { - job.Progress.Message = message; - job.Progress.PercentageComplete = percentage; - - _logger.LogInformation("Job {JobId}: {Message} ({Percentage}%)", jobId, message, percentage); - - // Simulate processing time - await Task.Delay(Random.Shared.Next(1000, 3000)); + throw new Exception("Processing completed but no file was generated"); } + // Store the downloaded file name + job.DownloadedFileName = downloadedFileName; + // Complete processing job.Progress.Status = "Completed"; job.Progress.Message = "Processing completed successfully"; job.Progress.PercentageComplete = 100; + // Create download URL for the actual file + var downloadUrl = $"/downloads/{Uri.EscapeDataString(downloadedFileName)}"; + job.Result = new ProcessVideoResponse { Success = true, Message = "Video processed successfully", - FileName = $"{videoDetails.Title?.Replace(" ", "_") ?? "video"}.{job.Request.Format ?? "mp3"}", - DownloadUrl = $"/api/download/{jobId}", + FileName = downloadedFileName, + DownloadUrl = downloadUrl, VideoDetails = videoDetails }; - _logger.LogInformation("Successfully completed processing for job {JobId}", jobId); + _logger.LogInformation("Successfully completed processing for job {JobId}. File: {FileName}", jobId, downloadedFileName); } catch (Exception ex) { diff --git a/TuneRipper.Api/Program.cs b/TuneRipper.Api/Program.cs index 36cfa1e..1ff6139 100644 --- a/TuneRipper.Api/Program.cs +++ b/TuneRipper.Api/Program.cs @@ -5,6 +5,9 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); +// Add HttpClient services +builder.Services.AddHttpClient(); + // Register services builder.Services.AddScoped(); @@ -32,6 +35,9 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); +// Enable static files serving (for downloading MP3 files) +app.UseStaticFiles(); + // Enable CORS app.UseCors("BlazorPolicy"); @@ -58,6 +64,7 @@ app.MapGet("/", (IWebHostEnvironment env) => new check_status = "/api/video/status/{jobId}", get_result = "/api/video/result/{jobId}", download = "/api/download/{jobId}", + static_files = "/downloads/{fileName}", openapi = "/openapi/v1.json" }, testing = new @@ -77,5 +84,6 @@ app.Logger.LogInformation(" app.Logger.LogInformation(" • HTTP: http://localhost:5112"); app.Logger.LogInformation("?? Health check: /api/health"); app.Logger.LogInformation("?? API info: /"); +app.Logger.LogInformation("?? Static files served from: /downloads/*"); app.Run(); diff --git a/TuneRipper.Api/README_UPDATED.md b/TuneRipper.Api/README_UPDATED.md new file mode 100644 index 0000000..4d3f7c2 --- /dev/null +++ b/TuneRipper.Api/README_UPDATED.md @@ -0,0 +1,238 @@ +# TuneRipper API - yt-dlp Integration + +## Overview + +The TuneRipper API has been updated to use `yt-dlp.exe` for real YouTube video processing with progress monitoring. Videos are downloaded and converted to MP3 files, stored in the `wwwroot/downloads` directory, and served directly through the API. + +## Setup + +### Prerequisites +1. **.NET 10** installed +2. **yt-dlp.exe** placed in `TuneRipper.Api/wwwroot/yt-dlp.exe` +3. **ffmpeg** available in PATH (required by yt-dlp for audio conversion) + +### File Structure +``` +TuneRipper.Api/ +??? wwwroot/ +? ??? yt-dlp.exe # YouTube downloader executable +? ??? downloads/ # Downloaded MP3 files (auto-created) +??? Controllers/ +??? Services/ +??? Program.cs +``` + +## Key Changes + +### 1. Program.cs Updates +- Added `UseStaticFiles()` middleware for serving downloaded files +- Added `AddHttpClient()` for VideoService HTTP requests +- Updated logging and endpoint information + +### 2. VideoService Updates +- Integrated real `yt-dlp.exe` processing with progress monitoring +- Real-time progress parsing from yt-dlp output +- File storage in `wwwroot/downloads/` directory +- Progress callbacks for UI updates + +### 3. VideoController Updates +- Uses actual yt-dlp processing instead of simulation +- Stores downloaded file information in job tracking +- Returns real download URLs for completed files + +### 4. DownloadController Updates +- Serves actual MP3 files from `wwwroot/downloads/` +- Proper content type detection +- File cleanup capabilities +- Download metadata endpoints + +## API Workflow + +### 1. Start Processing +```http +POST /api/video/process +Content-Type: application/json + +{ + "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "format": "mp3", + "quality": "128" +} +``` + +**Response:** +```json +{ + "success": true, + "message": "Processing started", + "fileName": "job-id-guid", + "downloadUrl": null, + "videoDetails": null +} +``` + +### 2. Monitor Progress +```http +GET /api/video/status/{jobId} +``` + +**Response (Early Stage):** +```json +{ + "percentageComplete": 15, + "status": "Processing", + "message": "Video details retrieved successfully", + "videoDetails": { + "id": "dQw4w9WgXcQ", + "title": "Rick Astley - Never Gonna Give You Up", + "thumbnailUrl": "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" + } +} +``` + +**Response (Processing):** +```json +{ + "percentageComplete": 45, + "status": "Processing", + "message": "Downloading audio...", + "videoDetails": { ... } +} +``` + +### 3. Get Final Result +```http +GET /api/video/result/{jobId} +``` + +**Response:** +```json +{ + "success": true, + "message": "Video processed successfully", + "fileName": "dQw4w9WgXcQ_20241212_093022.mp3", + "downloadUrl": "/downloads/dQw4w9WgXcQ_20241212_093022.mp3", + "videoDetails": { ... } +} +``` + +### 4. Download File +```http +GET /api/download/{jobId} +``` +**Response:** Binary MP3 file download + +**Alternative Static File Access:** +```http +GET /downloads/{fileName} +``` + +## Progress Monitoring + +The VideoService monitors yt-dlp output and translates it to progress updates: + +- **5-15%**: Fetching video details +- **15-70%**: Downloading audio (yt-dlp download progress × 0.7) +- **70-85%**: Converting to MP3 (ffmpeg processing) +- **85-95%**: Finalizing (cleanup) +- **100%**: Complete + +## Error Handling + +### Common Errors: +1. **yt-dlp.exe not found**: Ensure yt-dlp.exe is in wwwroot directory +2. **ffmpeg not available**: Install ffmpeg and add to PATH +3. **Invalid YouTube URL**: URL format validation +4. **Download timeout**: 5-minute timeout for processing +5. **File not found**: Cleanup or missing download directory + +### Error Response Format: +```json +{ + "success": false, + "message": "Error description", + "fileName": null, + "downloadUrl": null, + "videoDetails": null +} +``` + +## File Management + +### Download Directory +- Location: `wwwroot/downloads/` +- Naming: `{videoId}_{timestamp}.{ext}` +- Auto-cleanup: Implement retention policies as needed + +### File Serving +- Static files: Direct access via `/downloads/{fileName}` +- API download: Controlled access via `/api/download/{jobId}` +- Content-Type: Automatic detection based on file extension + +## Configuration + +### appsettings.json +```json +{ + "Logging": { + "LogLevel": { + "TuneRipper.Api.Services.VideoService": "Debug" + } + } +} +``` + +### yt-dlp Arguments Used: +- `--extract-audio`: Extract audio only +- `--audio-format mp3`: Convert to MP3 +- `--audio-quality 0`: Best quality +- `--no-playlist`: Single video only +- `--no-warnings`: Clean output +- `--newline`: Line-by-line progress + +## Testing + +### Manual Testing: +1. Start the API: `dotnet run --project TuneRipper.Api` +2. Use API testing tools or the Blazor client +3. Check logs for yt-dlp progress +4. Verify files in `wwwroot/downloads/` + +### Health Check: +```http +GET /api/health +``` + +### Job Listing: +```http +GET /api/video/jobs +``` + +## Production Considerations + +1. **File Storage**: Consider cloud storage for production +2. **Cleanup**: Implement automatic file cleanup/retention +3. **Security**: Validate URLs and sanitize filenames +4. **Performance**: Consider queue-based processing for high load +5. **Monitoring**: Add detailed logging and metrics +6. **Updates**: Keep yt-dlp.exe updated for compatibility + +## Troubleshooting + +### Common Issues: +1. **Permissions**: Ensure API can write to wwwroot/downloads +2. **Antivirus**: yt-dlp.exe may be flagged by antivirus +3. **Network**: YouTube may block requests from some IPs +4. **Format Support**: Some videos may not support audio extraction + +### Debug Logging: +Enable debug logging for VideoService to see detailed yt-dlp output: +```json +{ + "Logging": { + "LogLevel": { + "TuneRipper.Api.Services.VideoService": "Debug" + } + } +} +``` \ No newline at end of file diff --git a/TuneRipper.Api/Services/VideoService.cs b/TuneRipper.Api/Services/VideoService.cs index 80c5998..65dcb70 100644 --- a/TuneRipper.Api/Services/VideoService.cs +++ b/TuneRipper.Api/Services/VideoService.cs @@ -3,6 +3,8 @@ using System.Text.RegularExpressions; using System.Net.Http; using System.Threading.Tasks; using System.Text.RegularExpressions; +using System.Diagnostics; +using System.Text; namespace TuneRipper.Api.Services; @@ -11,17 +13,20 @@ public interface IVideoService Task GetVideoDetailsAsync(string url); bool IsValidYouTubeUrl(string url); string ExtractVideoId(string url); + Task ProcessVideoWithProgressAsync(string url, string format, Action progressCallback); } public class VideoService : IVideoService { private readonly ILogger _logger; private readonly HttpClient _httpClient; + private readonly IWebHostEnvironment _environment; - public VideoService(ILogger logger) + public VideoService(ILogger logger, IWebHostEnvironment environment, IHttpClientFactory httpClientFactory) { _logger = logger; - _httpClient = new HttpClient(); + _httpClient = httpClientFactory.CreateClient(); + _environment = environment; } public async Task GetVideoDetailsAsync(string url) @@ -85,6 +90,169 @@ public class VideoService : IVideoService } } + public async Task ProcessVideoWithProgressAsync(string url, string format, Action progressCallback) + { + try + { + var ytDlpPath = Path.Combine(_environment.WebRootPath, "yt-dlp.exe"); + + if (!File.Exists(ytDlpPath)) + { + throw new FileNotFoundException($"yt-dlp.exe not found at {ytDlpPath}"); + } + + // Create downloads directory if it doesn't exist + 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); + + // Build yt-dlp arguments + var arguments = new StringBuilder(); + arguments.Append($"--extract-audio "); + arguments.Append($"--audio-format {format} "); + arguments.Append($"--audio-quality 0 "); + arguments.Append("--no-playlist "); + arguments.Append("--no-warnings "); + arguments.Append("--newline "); + arguments.Append($"--output \"{outputTemplate}\" "); + arguments.Append($"\"{url}\""); + + _logger.LogInformation("Starting yt-dlp process with arguments: {Args}", arguments.ToString()); + + var processInfo = new ProcessStartInfo + { + FileName = ytDlpPath, + Arguments = arguments.ToString(), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = downloadDir + }; + + using var process = Process.Start(processInfo); + if (process == null) + { + throw new Exception("Failed to start yt-dlp process"); + } + + var outputBuilder = new StringBuilder(); + var errorBuilder = new StringBuilder(); + + // Monitor progress + process.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + outputBuilder.AppendLine(e.Data); + _logger.LogDebug("yt-dlp output: {Output}", e.Data); + + // Parse progress from yt-dlp output + ParseProgressFromOutput(e.Data, progressCallback); + } + }; + + process.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + errorBuilder.AppendLine(e.Data); + _logger.LogWarning("yt-dlp error: {Error}", e.Data); + } + }; + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // Wait for process completion with timeout + var completed = await Task.Run(() => process.WaitForExit(300000)); // 5 minute timeout + + if (!completed) + { + process.Kill(); + throw new TimeoutException("yt-dlp process timed out after 5 minutes"); + } + + if (process.ExitCode != 0) + { + var error = errorBuilder.ToString(); + throw new Exception($"yt-dlp failed with exit code {process.ExitCode}. Error: {error}"); + } + + // Find the downloaded file + var downloadedFiles = Directory.GetFiles(downloadDir, $"{videoId}_*.*") + .Where(f => !f.EndsWith(".part")) + .OrderByDescending(File.GetCreationTime) + .ToArray(); + + if (downloadedFiles.Length == 0) + { + throw new Exception("No downloaded file found after processing"); + } + + var downloadedFile = downloadedFiles[0]; + var finalFileName = Path.GetFileName(downloadedFile); + + _logger.LogInformation("Successfully processed video. File: {FileName}", finalFileName); + + progressCallback(100, "Processing completed successfully"); + + return finalFileName; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing video with yt-dlp: {Url}", url); + progressCallback(0, $"Processing failed: {ex.Message}"); + throw; + } + } + + private void ParseProgressFromOutput(string output, Action progressCallback) + { + try + { + // Parse different yt-dlp progress patterns + if (output.Contains("[download]")) + { + // Example: "[download] 45.2% of 3.45MiB at 1.23MiB/s ETA 00:02" + var percentMatch = Regex.Match(output, @"\[download\]\s+(\d+(?:\.\d+)?)%"); + if (percentMatch.Success) + { + if (double.TryParse(percentMatch.Groups[1].Value, out var percent)) + { + var progressPercent = (int)Math.Min(percent * 0.7, 70); // Download is 70% of total progress + progressCallback(progressPercent, "Downloading audio..."); + return; + } + } + + if (output.Contains("100%") || output.Contains("has already been downloaded")) + { + progressCallback(70, "Download complete, converting..."); + return; + } + } + else if (output.Contains("[ffmpeg]")) + { + progressCallback(85, "Converting to MP3..."); + return; + } + else if (output.Contains("Deleting original file")) + { + progressCallback(95, "Finalizing..."); + return; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error parsing progress from output: {Output}", output); + } + } + public bool IsValidYouTubeUrl(string url) { if (string.IsNullOrWhiteSpace(url)) diff --git a/TuneRipper.Api/wwwroot/yt-dlp.exe b/TuneRipper.Api/wwwroot/yt-dlp.exe new file mode 100644 index 0000000..e3f3018 Binary files /dev/null and b/TuneRipper.Api/wwwroot/yt-dlp.exe differ diff --git a/TuneRipper/Pages/Home.razor b/TuneRipper/Pages/Home.razor index c5d354e..af0dafc 100644 --- a/TuneRipper/Pages/Home.razor +++ b/TuneRipper/Pages/Home.razor @@ -123,7 +123,7 @@ @download.Title @download.CreatedAt.ToString("MMM dd, yyyy HH:mm") - @@ -475,6 +475,7 @@ { public string Title { get; set; } = ""; public string FileName { get; set; } = ""; + public string DownloadUrl { get; set; } = ""; public DateTime CreatedAt { get; set; } } @@ -564,7 +565,7 @@ var result = await ApiService.GetProcessingResultAsync(jobId); if (result?.Success == true) { - await CompleteProcessing(result); + await CompleteProcessing(result, jobId); } else { @@ -603,7 +604,7 @@ } } - private async Task CompleteProcessing(ProcessVideoResponse result) + private async Task CompleteProcessing(ProcessVideoResponse result, string jobId) { try { @@ -616,10 +617,24 @@ // Use the actual video title from API string title = currentVideoDetails?.Title ?? result.VideoDetails?.Title ?? ExtractTitleFromUrl(youtubeUrl); + // Create the download URL - either from the result or construct it from the job ID + string downloadUrl; + if (!string.IsNullOrEmpty(result.DownloadUrl)) + { + // If the result contains a direct download URL, use it + downloadUrl = result.DownloadUrl.StartsWith("http") ? result.DownloadUrl : $"https://localhost:7160{result.DownloadUrl}"; + } + else + { + // Fallback: construct download URL from job ID + downloadUrl = $"https://localhost:7160/api/download/{jobId}"; + } + downloadLinks.Add(new DownloadItem { Title = title, FileName = result.FileName ?? "sample.mp3", + DownloadUrl = downloadUrl, CreatedAt = DateTime.Now }); @@ -655,9 +670,19 @@ return $"Audio File {DateTime.Now:HHmmss}"; } - private async Task DownloadFile(string fileName) + private async Task DownloadFile(string downloadUrl) { - await JSRuntime.InvokeVoidAsync("downloadFile", fileName); + try + { + // Use the browser to navigate directly to the download URL + await JSRuntime.InvokeVoidAsync("open", downloadUrl, "_blank"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error downloading file from URL: {DownloadUrl}", downloadUrl); + apiErrorMessage = $"Error downloading file: {ex.Message}"; + StateHasChanged(); + } } private async Task HandleKeyPress(KeyboardEventArgs e) @@ -667,21 +692,4 @@ await DownloadMp3(); } } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - await JSRuntime.InvokeVoidAsync("eval", @" - window.downloadFile = function(fileName) { - const link = document.createElement('a'); - link.href = fileName; - link.download = fileName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - "); - } - } }