diff --git a/TuneRipper.Api/Controllers/VideoController.cs b/TuneRipper.Api/Controllers/VideoController.cs index 24a6d4f..d29a7a9 100644 --- a/TuneRipper.Api/Controllers/VideoController.cs +++ b/TuneRipper.Api/Controllers/VideoController.cs @@ -21,6 +21,7 @@ public class VideoController : ControllerBase public ProcessVideoRequest Request { get; set; } = new(); public DownloadProgress Progress { get; set; } = new(); public ProcessVideoResponse? Result { get; set; } + public YouTubeVideoDetails? VideoDetails { get; set; } // Add video details to job public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } @@ -30,39 +31,6 @@ public class VideoController : ControllerBase _logger = logger; } - [HttpGet("details")] - public async Task> GetVideoDetails([FromQuery] string url) - { - if (string.IsNullOrWhiteSpace(url)) - { - return BadRequest("URL is required"); - } - - if (!_videoService.IsValidYouTubeUrl(url)) - { - return BadRequest("Invalid YouTube URL"); - } - - try - { - _logger.LogInformation("Fetching video details for URL: {Url}", url); - - var videoDetails = await _videoService.GetVideoDetailsAsync(url); - - if (videoDetails == null) - { - return NotFound("Video not found or unavailable"); - } - - return Ok(videoDetails); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching video details for URL: {Url}", url); - return StatusCode(500, $"Error fetching video details: {ex.Message}"); - } - } - [HttpPost("process")] public async Task> StartProcessing([FromBody] ProcessVideoRequest request) { @@ -132,7 +100,22 @@ public class VideoController : ControllerBase return NotFound("Job not found"); } - return Ok(job.Progress); + // Return progress with video details if available + var progress = job.Progress; + + // If we have video details and we're past the first step, include them in a custom response + if (job.VideoDetails != null && job.Progress.PercentageComplete >= 15) + { + return Ok(new + { + PercentageComplete = progress.PercentageComplete, + Status = progress.Status, + Message = progress.Message, + VideoDetails = job.VideoDetails + }); + } + + return Ok(progress); } [HttpGet("result/{jobId}")] @@ -182,19 +165,38 @@ public class VideoController : ControllerBase { _logger.LogInformation("Processing video for job {JobId}", jobId); - // Simulate video processing steps - var steps = new[] + // Step 1: Fetch video details (NEW FIRST STEP) + job.Progress.Status = "Processing"; + job.Progress.Message = "Fetching video details..."; + job.Progress.PercentageComplete = 5; + + _logger.LogInformation("Job {JobId}: Fetching video details", jobId); + + var videoDetails = await _videoService.GetVideoDetailsAsync(job.Request.Url!); + + if (videoDetails == null) { - ("Fetching video information", 10), - ("Analyzing audio stream", 25), + throw new Exception("Unable to fetch video details. Invalid URL or video unavailable."); + } + + // Store video details in the job + job.VideoDetails = videoDetails; + job.Progress.Message = "Video details retrieved successfully"; + job.Progress.PercentageComplete = 15; + + _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) }; - foreach (var (message, percentage) in steps) + foreach (var (message, percentage) in remainingSteps) { - job.Progress.Status = "Processing"; job.Progress.Message = message; job.Progress.PercentageComplete = percentage; @@ -204,9 +206,6 @@ public class VideoController : ControllerBase await Task.Delay(Random.Shared.Next(1000, 3000)); } - // Get video details using the service - var videoDetails = await _videoService.GetVideoDetailsAsync(job.Request.Url!); - // Complete processing job.Progress.Status = "Completed"; job.Progress.Message = "Processing completed successfully"; @@ -216,7 +215,7 @@ public class VideoController : ControllerBase { Success = true, Message = "Video processed successfully", - FileName = $"{videoDetails?.Title?.Replace(" ", "_") ?? "video"}.{job.Request.Format ?? "mp3"}", + FileName = $"{videoDetails.Title?.Replace(" ", "_") ?? "video"}.{job.Request.Format ?? "mp3"}", DownloadUrl = $"/api/download/{jobId}", VideoDetails = videoDetails }; @@ -237,7 +236,7 @@ public class VideoController : ControllerBase Message = ex.Message, FileName = null, DownloadUrl = null, - VideoDetails = null + VideoDetails = job.VideoDetails // Include video details even on failure if we got them }; } } diff --git a/TuneRipper.Shared/Models/ProcessVideoModels.cs b/TuneRipper.Shared/Models/ProcessVideoModels.cs index 034e9c8..2b2ea2d 100644 --- a/TuneRipper.Shared/Models/ProcessVideoModels.cs +++ b/TuneRipper.Shared/Models/ProcessVideoModels.cs @@ -21,4 +21,5 @@ public class DownloadProgress public int PercentageComplete { get; set; } public string? Status { get; set; } public string? Message { get; set; } + public YouTubeVideoDetails? VideoDetails { get; set; } // Add optional video details } \ No newline at end of file diff --git a/TuneRipper/API_USAGE_EXAMPLES.md b/TuneRipper/API_USAGE_EXAMPLES.md index 0118a04..864f7b1 100644 --- a/TuneRipper/API_USAGE_EXAMPLES.md +++ b/TuneRipper/API_USAGE_EXAMPLES.md @@ -8,26 +8,18 @@ In any Razor component, inject the API service: @inject ITuneRipperApiService ApiService ``` +## Updated API Workflow + +The API has been streamlined - video details are now fetched as the first step of processing, eliminating the need for a separate details endpoint. + +### New Workflow: +1. **Start Processing** - Includes video details fetching as first step +2. **Monitor Status** - Video details appear when progress reaches ~15% +3. **Get Final Result** - Complete processing result with download URL + ## Usage Examples -### 1. Get Video Details - -```csharp -private async Task GetVideoDetailsExample() -{ - var url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; - var videoDetails = await ApiService.GetVideoDetailsAsync(url); - - if (videoDetails != null) - { - Console.WriteLine($"Title: {videoDetails.Title}"); - Console.WriteLine($"Duration: {videoDetails.Duration}"); - Console.WriteLine($"Author: {videoDetails.Author}"); - } -} -``` - -### 2. Start Video Processing +### 1. Start Video Processing (includes video details fetching) ```csharp private async Task StartProcessingExample() @@ -44,37 +36,138 @@ private async Task StartProcessingExample() if (response?.Success == true) { var jobId = response.FileName; // This contains the job ID - await MonitorProcessing(jobId); + await MonitorProcessingWithVideoDetails(jobId); } } ``` -### 3. Monitor Processing Status +### 2. Monitor Processing Status (includes video details when available) ```csharp -private async Task MonitorProcessing(string jobId) +private async Task MonitorProcessingWithVideoDetails(string jobId) { + YouTubeVideoDetails? videoDetails = null; + bool detailsReceived = false; + while (true) { var status = await ApiService.GetProcessingStatusAsync(jobId); if (status == null) break; - Console.WriteLine($"Status: {status.Status} - {status.PercentageComplete}%"); + Console.WriteLine($"Progress: {status.PercentageComplete}% - {status.Message}"); + + // Video details become available at ~15% progress + if (!detailsReceived && status.VideoDetails != null) + { + videoDetails = status.VideoDetails; + detailsReceived = true; + + Console.WriteLine($"Video Details Received:"); + Console.WriteLine($" Title: {videoDetails.Title}"); + Console.WriteLine($" Author: {videoDetails.Author}"); + Console.WriteLine($" Duration: {videoDetails.Duration}"); + } if (status.Status == "Completed" || status.Status == "Failed") { break; } - await Task.Delay(2000); // Wait 2 seconds before checking again + await Task.Delay(2000); // Poll every 2 seconds } // Get final result var result = await ApiService.GetProcessingResultAsync(jobId); if (result?.Success == true) { - Console.WriteLine($"Download URL: {result.DownloadUrl}"); + Console.WriteLine($"Processing completed! Download: {result.DownloadUrl}"); + } +} +``` + +### 3. Complete Processing Workflow + +```csharp +private async Task CompleteWorkflowExample() +{ + try + { + // Step 1: Start processing + var request = new ProcessVideoRequest + { + Url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + Format = "mp3", + Quality = "128" + }; + + var startResponse = await ApiService.StartProcessingAsync(request); + if (startResponse?.Success != true) + { + Console.WriteLine($"Failed to start processing: {startResponse?.Message}"); + return; + } + + var jobId = startResponse.FileName; + Console.WriteLine($"Processing started with job ID: {jobId}"); + + // Step 2: Monitor progress and get video details + YouTubeVideoDetails? videoDetails = null; + bool processingComplete = false; + + while (!processingComplete) + { + var status = await ApiService.GetProcessingStatusAsync(jobId); + + if (status == null) + { + Console.WriteLine("Failed to get status"); + break; + } + + Console.WriteLine($"{status.PercentageComplete}%: {status.Message}"); + + // Capture video details when they become available + if (videoDetails == null && status.VideoDetails != null) + { + videoDetails = status.VideoDetails; + Console.WriteLine($"?? Video: {videoDetails.Title} by {videoDetails.Author}"); + } + + if (status.Status == "Completed") + { + processingComplete = true; + } + else if (status.Status == "Failed") + { + Console.WriteLine($"Processing failed: {status.Message}"); + return; + } + + if (!processingComplete) + { + await Task.Delay(2000); + } + } + + // Step 3: Get final result + var result = await ApiService.GetProcessingResultAsync(jobId); + if (result?.Success == true) + { + Console.WriteLine($"? Success! File: {result.FileName}"); + Console.WriteLine($"?? Download: {result.DownloadUrl}"); + + // Step 4: Download the file + var fileBytes = await ApiService.DownloadFileAsync(jobId); + if (fileBytes != null) + { + Console.WriteLine($"?? Downloaded {fileBytes.Length} bytes"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); } } ``` @@ -120,17 +213,22 @@ private async Task ErrorHandlingExample() { try { - var videoDetails = await ApiService.GetVideoDetailsAsync("invalid-url"); - - if (videoDetails == null) + var request = new ProcessVideoRequest { - // Handle case where API returns null (error occurred) - Console.WriteLine("Failed to get video details"); + Url = "invalid-url", + Format = "mp3", + Quality = "128" + }; + + var response = await ApiService.StartProcessingAsync(request); + + if (response?.Success != true) + { + Console.WriteLine($"Processing failed: {response?.Message}"); } } catch (Exception ex) { - // Handle unexpected exceptions Console.WriteLine($"Error: {ex.Message}"); } } @@ -153,40 +251,106 @@ The API service automatically uses the configuration from `appsettings.json`: } ``` +## API Endpoints + +### Current Endpoints: +- **POST** `/api/video/process` - Start processing (includes video details fetching) +- **GET** `/api/video/status/{jobId}` - Get processing status (includes video details when available) +- **GET** `/api/video/result/{jobId}` - Get final processing result +- **GET** `/api/video/jobs` - List all processing jobs +- **GET** `/api/download/{jobId}` - Download converted file +- **GET** `/api/health` - Health check + +### Removed Endpoints: +- ~~**GET** `/api/video/details?url={url}` - Removed (now integrated into processing)~~ + ## Complete Component Example ```razor @page "/example" @inject ITuneRipperApiService ApiService -

API Service Example

+

Integrated Video Processing

- + + -@if (!string.IsNullOrEmpty(message)) +@if (currentVideo != null) { -

@message

+
+

@currentVideo.Title

+

By: @currentVideo.Author

+

Duration: @currentVideo.Duration

+
+} + +@if (isProcessing) +{ +
+
+ @progressMessage (@progressPercent%) +
} @code { - private string message = ""; + private string youtubeUrl = ""; + private bool isProcessing = false; + private int progressPercent = 0; + private string progressMessage = ""; + private YouTubeVideoDetails? currentVideo = null; - private async Task TestApi() + private async Task StartProcessing() { - message = "Testing API..."; + if (string.IsNullOrWhiteSpace(youtubeUrl)) return; - var isHealthy = await ApiService.IsApiHealthyAsync(); + isProcessing = true; + currentVideo = null; + progressPercent = 0; - if (isHealthy) + try { - message = "API is healthy!"; + // Start processing + var request = new ProcessVideoRequest { Url = youtubeUrl, Format = "mp3", Quality = "128" }; + var response = await ApiService.StartProcessingAsync(request); + + if (response?.Success == true) + { + await MonitorProgress(response.FileName); + } } - else + finally { - message = "API is not responding"; + isProcessing = false; } - - StateHasChanged(); } -} -``` \ No newline at end of file + + private async Task MonitorProgress(string jobId) + { + while (isProcessing) + { + var status = await ApiService.GetProcessingStatusAsync(jobId); + + if (status != null) + { + progressPercent = status.PercentageComplete; + progressMessage = status.Message ?? "Processing..."; + + // Video details appear at ~15% + if (currentVideo == null && status.VideoDetails != null) + { + currentVideo = status.VideoDetails; + } + + if (status.Status == "Completed" || status.Status == "Failed") + { + break; + } + } + + StateHasChanged(); + await Task.Delay(2000); + } + } +} \ No newline at end of file diff --git a/TuneRipper/Pages/Home.razor b/TuneRipper/Pages/Home.razor index b64b53b..fa2763c 100644 --- a/TuneRipper/Pages/Home.razor +++ b/TuneRipper/Pages/Home.razor @@ -1,6 +1,8 @@ @page "/" @using Microsoft.AspNetCore.Components.Web @inject IJSRuntime JSRuntime +@inject ITuneRipperApiService ApiService +@inject ILogger Logger TuneRipper - YouTube to MP3 Downloader @@ -33,14 +35,83 @@ } + + @* Video Information Section - Show as soon as we have details *@ + @if (currentVideoDetails != null) + { +
+

📹 Video Information

+
+
+ @if (!string.IsNullOrEmpty(currentVideoDetails.ThumbnailUrl)) + { + Video thumbnail + @if (isProcessing) + { +
+
🎵
+
+ } + } +
+
+

@currentVideoDetails.Title

+
+ + 👤 + @currentVideoDetails.Author + + + ⏱️ + @FormatDuration(currentVideoDetails.Duration) + + + 👁️ + @FormatViewCount(currentVideoDetails.ViewCount) views + + + 📅 + @currentVideoDetails.UploadDate.ToString("MMM dd, yyyy") + +
+ @if (currentVideoDetails.Tags?.Any() == true) + { +
+ @foreach (var tag in currentVideoDetails.Tags.Take(5)) + { + @tag + } +
+ } + @if (isProcessing) + { +
+ ⚡ + Converting to MP3... +
+ } +
+
+
+ } @if (isProcessing) { -
+
-
@progressMessage (@progressPercentage%)
+
+ @if (currentVideoDetails != null && progressPercentage > 15) + { + @progressMessage + (@progressPercentage%) + } + else + { + @progressMessage (@progressPercentage%) + } +
} @@ -50,6 +121,14 @@

@outputMessage

} + + @* API Error Message *@ + @if (!string.IsNullOrWhiteSpace(apiErrorMessage) && !isProcessing) + { +
+

❌ @apiErrorMessage

+
+ } @if (downloadLinks.Any()) @@ -96,6 +175,14 @@ width: 100%; } + .progress-container.with-video-details { + margin-top: 15px; + background: #f8f9ff; + padding: 15px; + border-radius: 8px; + border: 1px solid #e3e7ff; + } + .progress-bar { width: 100%; height: 8px; @@ -105,6 +192,11 @@ margin-bottom: 10px; } + .progress-container.with-video-details .progress-bar { + height: 6px; + background-color: #d1d9ff; + } + .progress-fill { height: 100%; background: linear-gradient(90deg, #4CAF50, #45a049); @@ -119,6 +211,22 @@ font-weight: 500; } + .progress-container.with-video-details .progress-text { + font-size: 13px; + color: #4a5568; + } + + .progress-highlight { + color: #2d3748; + font-weight: 600; + } + + .progress-percentage { + color: #718096; + font-weight: 400; + margin-left: 4px; + } + .download-links-section { margin-top: 30px; width: 100%; @@ -197,15 +305,224 @@ .download-icon { font-size: 16px; } + + /* Video Info Styles */ + .video-info-section { + margin-top: 20px; + width: 100%; + animation: slideIn 0.5s ease-out; + } + + .video-info-title { + color: #333; + margin-bottom: 15px; + text-align: center; + font-size: 18px; + font-weight: 600; + } + + .video-info-card { + background: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 12px; + padding: 20px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + display: flex; + gap: 20px; + align-items: flex-start; + } + + .video-info-card.processing { + border-color: #4CAF50; + box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15); + } + + .video-thumbnail { + flex-shrink: 0; + position: relative; + } + + .thumbnail-img { + width: 160px; + height: 90px; + object-fit: cover; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: opacity 0.3s ease; + } + + .processing-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease-out; + } + + .processing-spinner { + font-size: 24px; + animation: spin 2s linear infinite; + } + + .video-details { + flex: 1; + min-width: 0; + } + + .video-title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + line-height: 1.4; + word-wrap: break-word; + } + + .video-meta { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 12px; + } + + .meta-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: #666; + } + + .meta-icon { + font-size: 14px; + } + + .video-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + .tag { + background: #f0f0f0; + color: #666; + font-size: 11px; + padding: 4px 8px; + border-radius: 12px; + font-weight: 500; + } + + .error-message { + margin-top: 15px; + padding: 12px; + background: #ffebee; + border: 1px solid #ffcdd2; + border-radius: 8px; + color: #c62828; + text-align: center; + } + + .conversion-status { + margin-top: 8px; + padding: 8px 12px; + background: linear-gradient(90deg, #4CAF50, #45a049); + color: white; + border-radius: 6px; + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 500; + animation: pulse 2s ease-in-out infinite; + } + + .status-icon { + font-size: 16px; + } + + @@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + @@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + @@keyframes pulse { + 0%, 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); + } + 50% { + transform: scale(1.02); + box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); + } + } + + @@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* Responsive Design */ + @@media (max-width: 768px) { + .video-info-card { + flex-direction: column; + align-items: center; + text-align: center; + } + + .thumbnail-img { + width: 200px; + height: 112px; + } + + .video-meta { + justify-content: center; + } + } + + @@media (max-width: 480px) { + .video-info-card { + padding: 15px; + } + + .thumbnail-img { + width: 160px; + height: 90px; + } + + .video-meta { + flex-direction: column; + align-items: center; + gap: 8px; + } + } @code { private string youtubeUrl = ""; private string outputMessage = ""; + private string apiErrorMessage = ""; private bool isProcessing = false; private int progressPercentage = 0; private string progressMessage = ""; private List downloadLinks = new(); + private YouTubeVideoDetails? currentVideoDetails = null; public class DownloadItem { @@ -221,59 +538,190 @@ isProcessing = true; progressPercentage = 0; outputMessage = ""; + apiErrorMessage = ""; + currentVideoDetails = null; - await SimulateProgress(); + try + { + // Step 1: Start processing (which includes video details fetching) + progressMessage = "Starting video processing..."; + progressPercentage = 5; + StateHasChanged(); + + var request = new ProcessVideoRequest + { + Url = youtubeUrl, + Format = "mp3", + Quality = "128" + }; + + var processingResponse = await ApiService.StartProcessingAsync(request); + + if (processingResponse?.Success == true) + { + var jobId = processingResponse.FileName; // This contains the job ID + + // Step 2: Monitor processing status and get video details + await MonitorProcessingAndShowDetails(jobId); + } + else + { + // Processing start failed + isProcessing = false; + apiErrorMessage = processingResponse?.Message ?? "Failed to start processing. Please check the URL and try again."; + StateHasChanged(); + return; + } + } + catch (Exception ex) + { + isProcessing = false; + apiErrorMessage = $"An error occurred: {ex.Message}"; + StateHasChanged(); + } + } + } + + private async Task MonitorProcessingAndShowDetails(string jobId) + { + try + { + bool videoDetailsReceived = false; + int maxAttempts = 60; // Maximum polling attempts (about 2 minutes) + int attemptCount = 0; - string title = ExtractTitleFromUrl(youtubeUrl); + while (attemptCount < maxAttempts && isProcessing) + { + var status = await ApiService.GetProcessingStatusAsync(jobId); + + if (status != null) + { + // Update progress + progressPercentage = status.PercentageComplete; + progressMessage = status.Message ?? "Processing..."; + + // Check if we received video details + if (!videoDetailsReceived && status.VideoDetails != null) + { + currentVideoDetails = status.VideoDetails; + videoDetailsReceived = true; + Logger.LogInformation("Video details received: {Title}", status.VideoDetails.Title); + } + + StateHasChanged(); + + // Check if processing is complete + if (status.Status == "Completed") + { + // Get final result + var result = await ApiService.GetProcessingResultAsync(jobId); + if (result?.Success == true) + { + await CompleteProcessing(result); + } + else + { + apiErrorMessage = result?.Message ?? "Processing completed but failed to get result."; + isProcessing = false; + StateHasChanged(); + } + return; + } + else if (status.Status == "Failed") + { + apiErrorMessage = status.Message ?? "Processing failed."; + isProcessing = false; + StateHasChanged(); + return; + } + } + + attemptCount++; + await Task.Delay(2000); // Poll every 2 seconds + } + + // Timeout handling + if (attemptCount >= maxAttempts) + { + apiErrorMessage = "Processing timeout. Please try again."; + isProcessing = false; + StateHasChanged(); + } + } + catch (Exception ex) + { + isProcessing = false; + apiErrorMessage = $"Error monitoring processing: {ex.Message}"; + StateHasChanged(); + } + } + + private async Task CompleteProcessing(ProcessVideoResponse result) + { + try + { + // Use the video details from the result if we don't have them yet + if (currentVideoDetails == null && result.VideoDetails != null) + { + currentVideoDetails = result.VideoDetails; + } + + // Use the actual video title from API + string title = currentVideoDetails?.Title ?? result.VideoDetails?.Title ?? ExtractTitleFromUrl(youtubeUrl); downloadLinks.Add(new DownloadItem { Title = title, - FileName = "sample.mp3", + FileName = result.FileName ?? "sample.mp3", CreatedAt = DateTime.Now }); outputMessage = $"✅ Successfully converted: {title}"; + progressPercentage = 100; + progressMessage = "Complete!"; + + // Small delay to show completion + await Task.Delay(500); + isProcessing = false; youtubeUrl = ""; StateHasChanged(); } + catch (Exception ex) + { + isProcessing = false; + apiErrorMessage = $"Error completing processing: {ex.Message}"; + StateHasChanged(); + } } - private async Task SimulateProgress() + private string FormatDuration(TimeSpan duration) { - string[] progressSteps = { - "Fetching video information...", - "Analyzing audio stream...", - "Downloading audio data...", - "Converting to MP3...", - "Optimizing quality...", - "Finalizing download..." - }; - - int stepDuration = 800; - int stepsPerProgress = 100 / progressSteps.Length; - - for (int i = 0; i < progressSteps.Length; i++) + if (duration.TotalHours >= 1) { - progressMessage = progressSteps[i]; - - int startProgress = i * stepsPerProgress; - int endProgress = (i + 1) * stepsPerProgress; - - for (int progress = startProgress; progress <= endProgress; progress++) - { - progressPercentage = Math.Min(progress, 100); - StateHasChanged(); - await Task.Delay(stepDuration / stepsPerProgress); - } + return duration.ToString(@"h\:mm\:ss"); + } + else + { + return duration.ToString(@"m\:ss"); + } + } + + private string FormatViewCount(int viewCount) + { + if (viewCount >= 1_000_000) + { + return $"{viewCount / 1_000_000.0:F1}M"; + } + else if (viewCount >= 1_000) + { + return $"{viewCount / 1_000.0:F1}K"; + } + else + { + return viewCount.ToString("N0"); } - - progressPercentage = 100; - progressMessage = "Complete!"; - StateHasChanged(); - await Task.Delay(500); } private string ExtractTitleFromUrl(string url) diff --git a/TuneRipper/Services/TuneRipperApiService.cs b/TuneRipper/Services/TuneRipperApiService.cs index e4606be..8fd7d3c 100644 --- a/TuneRipper/Services/TuneRipperApiService.cs +++ b/TuneRipper/Services/TuneRipperApiService.cs @@ -9,7 +9,6 @@ namespace TuneRipper.Services; public interface ITuneRipperApiService { - Task GetVideoDetailsAsync(string url); Task StartProcessingAsync(ProcessVideoRequest request); Task GetProcessingStatusAsync(string jobId); Task GetProcessingResultAsync(string jobId); @@ -51,51 +50,6 @@ public class TuneRipperApiService : ITuneRipperApiService } } - public async Task GetVideoDetailsAsync(string url) - { - try - { - if (string.IsNullOrWhiteSpace(url)) - { - _logger.LogWarning("GetVideoDetailsAsync called with empty URL"); - return null; - } - - _logger.LogInformation("Fetching video details for URL: {Url}", url); - - var encodedUrl = Uri.EscapeDataString(url); - var response = await _httpClient.GetAsync($"api/video/details?url={encodedUrl}"); - - if (response.IsSuccessStatusCode) - { - var videoDetails = await response.Content.ReadFromJsonAsync(_jsonOptions); - _logger.LogInformation("Successfully retrieved video details for URL: {Url}", url); - return videoDetails; - } - - var errorContent = await response.Content.ReadAsStringAsync(); - _logger.LogWarning("Failed to get video details. Status: {StatusCode}, Error: {Error}", - response.StatusCode, errorContent); - - return null; - } - catch (HttpRequestException ex) - { - _logger.LogError(ex, "HTTP error while fetching video details for URL: {Url}", url); - return null; - } - catch (TaskCanceledException ex) - { - _logger.LogError(ex, "Timeout while fetching video details for URL: {Url}", url); - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error while fetching video details for URL: {Url}", url); - return null; - } - } - public async Task StartProcessingAsync(ProcessVideoRequest request) { try @@ -159,8 +113,20 @@ public class TuneRipperApiService : ITuneRipperApiService if (response.IsSuccessStatusCode) { - var status = await response.Content.ReadFromJsonAsync(_jsonOptions); - return status; + var content = await response.Content.ReadAsStringAsync(); + + // Try to deserialize as extended status first (with video details) + try + { + var extendedStatus = JsonSerializer.Deserialize(content, _jsonOptions); + return extendedStatus; + } + catch + { + // Fallback to basic status + var basicStatus = JsonSerializer.Deserialize(content, _jsonOptions); + return basicStatus; + } } if (response.StatusCode == System.Net.HttpStatusCode.NotFound)