Refactored API and Blazor client to dynamically construct download URLs using the configured API base URL instead of hardcoded localhost addresses. Enhanced root endpoint and startup logs to reflect environment-specific URLs. Added GetBaseUrl() to ITuneRipperApiService for consistent URL generation.
732 lines
28 KiB
Plaintext
732 lines
28 KiB
Plaintext
@page "/"
|
|
@using Microsoft.AspNetCore.Components.Web
|
|
@using TuneRipper.Shared.Models
|
|
@using TuneRipper.Services
|
|
@inject IJSRuntime JSRuntime
|
|
@inject ITuneRipperApiService ApiService
|
|
@inject IClientSessionService SessionService
|
|
@inject ILogger<Home> Logger
|
|
|
|
<PageTitle>TuneRipper - YouTube to MP3 Downloader</PageTitle>
|
|
|
|
<!-- Floating Musical Notes Background -->
|
|
<div class="page-background">
|
|
<div class="music-notes">
|
|
<div class="note">♪</div>
|
|
<div class="note">♫</div>
|
|
<div class="note">♪</div>
|
|
<div class="note">♬</div>
|
|
<div class="note">♫</div>
|
|
<div class="note">♪</div>
|
|
<div class="note">♬</div>
|
|
<div class="note">♫</div>
|
|
<div class="note">♪</div>
|
|
</div>
|
|
|
|
<div class="hero-section">
|
|
<div class="logo-container">
|
|
<img src="images/logo.png" alt="TuneRipper Logo" class="logo" />
|
|
<h1 class="app-title">TuneRipper</h1>
|
|
<p class="app-subtitle">Convert YouTube videos to MP3 instantly</p>
|
|
</div>
|
|
|
|
<div class="download-form">
|
|
<div class="input-container">
|
|
<input type="text"
|
|
@bind="youtubeUrl"
|
|
@onkeypress="HandleKeyPress"
|
|
@onblur="OnInputBlur"
|
|
placeholder="Paste your YouTube link here..."
|
|
class="url-input"
|
|
disabled="@isProcessing" />
|
|
<button @onclick="DownloadMp3"
|
|
@onmousedown="PreventDefault"
|
|
class="download-btn @(isProcessing ? "processing" : "")"
|
|
disabled="@(isProcessing)">
|
|
<span class="btn-icon">🎵</span>
|
|
@if (isProcessing)
|
|
{
|
|
<span>Processing... (@progressPercentage%)</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Download MP3</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
|
|
@* Enhanced Progress Section - Show immediately when processing starts *@
|
|
@if (isProcessing)
|
|
{
|
|
<div class="enhanced-progress-section">
|
|
<div class="progress-header">
|
|
<h3 class="progress-title">
|
|
<span class="progress-icon @GetProgressIconClass()">@GetProgressIcon()</span>
|
|
@GetProgressStageTitle()
|
|
</h3>
|
|
<div class="progress-stats">
|
|
<span class="progress-percentage">@progressPercentage%</span>
|
|
<span class="progress-elapsed">⏱️ @GetElapsedTime()</span>
|
|
@if (estimatedTimeRemaining.HasValue && estimatedTimeRemaining.Value > TimeSpan.Zero)
|
|
{
|
|
<span class="progress-eta">ETA: @GetEstimatedTimeRemaining()</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar-background">
|
|
<div class="progress-bar-fill" style="width: @(progressPercentage)%">
|
|
<div class="progress-bar-shimmer"></div>
|
|
</div>
|
|
</div>
|
|
<div class="progress-stages">
|
|
@foreach (var stage in progressStages)
|
|
{
|
|
<div class="progress-stage @GetStageClass(stage)">
|
|
<div class="stage-icon">@stage.Icon</div>
|
|
<div class="stage-label">@stage.Label</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="progress-details">
|
|
<div class="progress-message">
|
|
<span class="message-text">@progressMessage</span>
|
|
@if (progressPercentage > 0 && progressPercentage < 100)
|
|
{
|
|
<div class="progress-dots">
|
|
<span class="dot">.</span>
|
|
<span class="dot">.</span>
|
|
<span class="dot">.</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (currentStageDetails != null)
|
|
{
|
|
<div class="stage-details">
|
|
<small>@currentStageDetails</small>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@* Video Information Section - Show as soon as we have details *@
|
|
@if (currentVideoDetails != null)
|
|
{
|
|
<div class="video-info-section">
|
|
<h3 class="video-info-title">📹 Video Information</h3>
|
|
<div class="video-info-card @(isProcessing ? "processing" : "")">
|
|
<div class="video-thumbnail">
|
|
@if (!string.IsNullOrEmpty(currentVideoDetails.ThumbnailUrl))
|
|
{
|
|
<img src="@currentVideoDetails.ThumbnailUrl" alt="Video thumbnail" class="thumbnail-img" />
|
|
@if (isProcessing)
|
|
{
|
|
<div class="processing-overlay">
|
|
<div class="processing-spinner">🎵</div>
|
|
<div class="processing-text">@progressPercentage%</div>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
<div class="video-details">
|
|
<h4 class="video-title">@currentVideoDetails.Title</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrWhiteSpace(outputMessage) && !isProcessing)
|
|
{
|
|
<div class="output-message success">
|
|
<div class="success-icon">✅</div>
|
|
<p>@outputMessage</p>
|
|
</div>
|
|
}
|
|
|
|
@* API Error Message *@
|
|
@if (!string.IsNullOrWhiteSpace(apiErrorMessage) && !isProcessing)
|
|
{
|
|
<div class="error-message">
|
|
<div class="error-icon">❌</div>
|
|
<p>@apiErrorMessage</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (downloadLinks.Any())
|
|
{
|
|
<div class="download-links-section">
|
|
<h3 class="downloads-title">📥 Your Downloads</h3>
|
|
|
|
@* Download All in ZIP Button *@
|
|
@if (!string.IsNullOrWhiteSpace(currentSessionId) && downloadLinks.Count > 1)
|
|
{
|
|
<div class="download-all-container">
|
|
<button class="download-all-btn @(isLoadingZip ? "loading" : "")"
|
|
@onclick="DownloadAllInZip"
|
|
disabled="@isLoadingZip">
|
|
<span class="download-all-icon">📦</span>
|
|
@if (isLoadingZip)
|
|
{
|
|
<span>Preparing ZIP...</span>
|
|
<div class="loading-spinner">⏳</div>
|
|
}
|
|
else
|
|
{
|
|
<span>Download All in ZIP</span>
|
|
}
|
|
@if (sessionZipInfo != null)
|
|
{
|
|
<span class="zip-info">(@sessionZipInfo.Mp3FilesCount files, @sessionZipInfo.FileSize)</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<div class="download-links">
|
|
@foreach (var download in downloadLinks.AsEnumerable().Reverse())
|
|
{
|
|
<div class="download-item">
|
|
<div class="download-info">
|
|
<span class="download-title">@download.Title</span>
|
|
<span class="download-date">@download.CreatedAt.ToString("MMM dd, yyyy HH:mm")</span>
|
|
</div>
|
|
<button class="download-link-btn" @onclick="() => DownloadFile(download.DownloadUrl)">
|
|
<span class="download-icon">⬇️</span>
|
|
Download MP3
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="features">
|
|
<div class="feature">
|
|
<span class="feature-icon">⚡</span>
|
|
<span>Fast Conversion</span>
|
|
</div>
|
|
<div class="feature">
|
|
<span class="feature-icon">🎧</span>
|
|
<span>High Quality</span>
|
|
</div>
|
|
<div class="feature">
|
|
<span class="feature-icon">🆓</span>
|
|
<span>Completely Free</span>
|
|
</div>
|
|
</div>
|
|
|
|
<DonateButton />
|
|
|
|
@* Session info display - moved to bottom *@
|
|
@if (!string.IsNullOrWhiteSpace(currentSessionId))
|
|
{
|
|
<div class="session-info">
|
|
<small>Session: @currentSessionId.Substring(0, 8)...</small>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private string youtubeUrl = "";
|
|
private string outputMessage = "";
|
|
private string apiErrorMessage = "";
|
|
private bool isProcessing = false;
|
|
private bool isLoadingZip = false;
|
|
private int progressPercentage = 0;
|
|
private string progressMessage = "";
|
|
private string? currentStageDetails = null;
|
|
private List<DownloadItem> downloadLinks = new();
|
|
private YouTubeVideoDetails? currentVideoDetails = null;
|
|
private string currentSessionId = "";
|
|
private SessionZipInfo? sessionZipInfo = null;
|
|
|
|
// Enhanced progress tracking
|
|
private DateTime processingStartTime;
|
|
private TimeSpan? estimatedTimeRemaining;
|
|
private List<ProgressStage> progressStages = new();
|
|
private string currentProgressStage = "initializing";
|
|
|
|
public class DownloadItem
|
|
{
|
|
public string Title { get; set; } = "";
|
|
public string FileName { get; set; } = "";
|
|
public string DownloadUrl { get; set; } = "";
|
|
public DateTime CreatedAt { get; set; }
|
|
}
|
|
|
|
public class ProgressStage
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Label { get; set; } = "";
|
|
public string Icon { get; set; } = "";
|
|
public bool IsCompleted { get; set; }
|
|
public bool IsActive { get; set; }
|
|
public int MinProgress { get; set; }
|
|
public int MaxProgress { get; set; }
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
InitializeProgressStages();
|
|
|
|
try
|
|
{
|
|
// Initialize session on component load
|
|
currentSessionId = await SessionService.GetCurrentSessionIdAsync();
|
|
Logger.LogInformation("Initialized with session: {SessionId}", currentSessionId);
|
|
|
|
// Load session ZIP info if available
|
|
await LoadSessionZipInfoAsync();
|
|
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Error initializing session");
|
|
// Don't block the UI if session initialization fails
|
|
}
|
|
}
|
|
|
|
private void InitializeProgressStages()
|
|
{
|
|
progressStages = new List<ProgressStage>
|
|
{
|
|
new() { Id = "initializing", Label = "Initializing", Icon = "🚀", MinProgress = 0, MaxProgress = 15 },
|
|
new() { Id = "fetching", Label = "Fetching Video", Icon = "📡", MinProgress = 15, MaxProgress = 35 },
|
|
new() { Id = "extracting", Label = "Extracting Audio", Icon = "🔄", MinProgress = 35, MaxProgress = 70 },
|
|
new() { Id = "converting", Label = "Converting to MP3", Icon = "🎵", MinProgress = 70, MaxProgress = 90 },
|
|
new() { Id = "finalizing", Label = "Finalizing", Icon = "✨", MinProgress = 90, MaxProgress = 100 }
|
|
};
|
|
}
|
|
|
|
private void UpdateProgressStage(int percentage)
|
|
{
|
|
foreach (var stage in progressStages)
|
|
{
|
|
stage.IsCompleted = percentage > stage.MaxProgress;
|
|
stage.IsActive = percentage >= stage.MinProgress && percentage <= stage.MaxProgress;
|
|
}
|
|
|
|
var activeStage = progressStages.FirstOrDefault(s => s.IsActive);
|
|
if (activeStage != null && currentProgressStage != activeStage.Id)
|
|
{
|
|
currentProgressStage = activeStage.Id;
|
|
UpdateStageDetails(activeStage.Id, percentage);
|
|
}
|
|
}
|
|
|
|
private void UpdateStageDetails(string stageId, int percentage)
|
|
{
|
|
currentStageDetails = stageId switch
|
|
{
|
|
"initializing" => "Preparing download request and validating URL...",
|
|
"fetching" => $"Analyzing video content and metadata... ({percentage}%)",
|
|
"extracting" => $"Extracting high-quality audio stream... ({percentage}%)",
|
|
"converting" => $"Converting to MP3 format with optimal settings... ({percentage}%)",
|
|
"finalizing" => $"Completing conversion and preparing download... ({percentage}%)",
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private string GetProgressStageTitle()
|
|
{
|
|
var activeStage = progressStages.FirstOrDefault(s => s.IsActive);
|
|
return activeStage?.Label ?? "Processing";
|
|
}
|
|
|
|
private string GetProgressIcon()
|
|
{
|
|
var activeStage = progressStages.FirstOrDefault(s => s.IsActive);
|
|
return activeStage?.Icon ?? "🎵";
|
|
}
|
|
|
|
private string GetProgressIconClass()
|
|
{
|
|
if (progressPercentage < 25) return "spinning";
|
|
if (progressPercentage < 50) return "pulse";
|
|
if (progressPercentage < 75) return "bounce";
|
|
if (progressPercentage < 100) return "glow";
|
|
return "success";
|
|
}
|
|
|
|
private string GetStageClass(ProgressStage stage)
|
|
{
|
|
if (stage.IsCompleted) return "completed";
|
|
if (stage.IsActive) return "active";
|
|
return "pending";
|
|
}
|
|
|
|
private string GetElapsedTime()
|
|
{
|
|
var elapsed = DateTime.Now - processingStartTime;
|
|
return elapsed.TotalMinutes >= 1
|
|
? $"{elapsed.Minutes:D2}:{elapsed.Seconds:D2}"
|
|
: $"{elapsed.Seconds}s";
|
|
}
|
|
|
|
private string GetEstimatedTimeRemaining()
|
|
{
|
|
if (!estimatedTimeRemaining.HasValue) return "";
|
|
|
|
var eta = estimatedTimeRemaining.Value;
|
|
return eta.TotalMinutes >= 1
|
|
? $"{eta.Minutes:D2}:{eta.Seconds:D2}"
|
|
: $"{eta.Seconds}s";
|
|
}
|
|
|
|
private void CalculateEstimatedTime(int currentProgress)
|
|
{
|
|
if (currentProgress > 5) // Only estimate after some progress
|
|
{
|
|
var elapsed = DateTime.Now - processingStartTime;
|
|
var progressRate = currentProgress / elapsed.TotalSeconds;
|
|
var remainingProgress = 100 - currentProgress;
|
|
var estimatedSeconds = remainingProgress / progressRate;
|
|
|
|
if (estimatedSeconds > 0 && estimatedSeconds < 300) // Cap at 5 minutes
|
|
{
|
|
estimatedTimeRemaining = TimeSpan.FromSeconds(estimatedSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task LoadSessionZipInfoAsync()
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(currentSessionId))
|
|
{
|
|
sessionZipInfo = await ApiService.GetSessionZipInfoAsync(currentSessionId);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Error loading session ZIP info");
|
|
// Don't block the UI if ZIP info loading fails
|
|
}
|
|
}
|
|
|
|
private async Task DownloadAllInZip()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(currentSessionId))
|
|
{
|
|
apiErrorMessage = "Session not available for ZIP download";
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
isLoadingZip = true;
|
|
StateHasChanged();
|
|
|
|
Logger.LogInformation("Downloading session ZIP for session: {SessionId}", currentSessionId);
|
|
|
|
// Create the download URL for the ZIP file using the configured API base URL
|
|
var baseUrl = ApiService.GetBaseUrl();
|
|
var zipDownloadUrl = $"{baseUrl}/api/download/zip/{currentSessionId}";
|
|
|
|
// Use browser to download the ZIP file
|
|
await JSRuntime.InvokeVoidAsync("open", zipDownloadUrl, "_blank");
|
|
|
|
Logger.LogInformation("Successfully initiated ZIP download for session: {SessionId}", currentSessionId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Error downloading session ZIP for session: {SessionId}", currentSessionId);
|
|
apiErrorMessage = $"Error downloading ZIP file: {ex.Message}";
|
|
StateHasChanged();
|
|
}
|
|
finally
|
|
{
|
|
isLoadingZip = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task DownloadMp3()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(youtubeUrl))
|
|
{
|
|
isProcessing = true;
|
|
progressPercentage = 0;
|
|
outputMessage = "";
|
|
apiErrorMessage = "";
|
|
currentVideoDetails = null;
|
|
processingStartTime = DateTime.Now;
|
|
estimatedTimeRemaining = null;
|
|
currentStageDetails = null;
|
|
|
|
// Reset progress stages
|
|
foreach (var stage in progressStages)
|
|
{
|
|
stage.IsCompleted = false;
|
|
stage.IsActive = false;
|
|
}
|
|
progressStages.First().IsActive = true;
|
|
|
|
try
|
|
{
|
|
// Ensure we have a session
|
|
if (string.IsNullOrWhiteSpace(currentSessionId))
|
|
{
|
|
currentSessionId = await SessionService.GetCurrentSessionIdAsync();
|
|
}
|
|
|
|
// Step 1: Start processing (which includes video details fetching)
|
|
progressMessage = "Starting video processing...";
|
|
progressPercentage = 5;
|
|
UpdateProgressStage(progressPercentage);
|
|
StateHasChanged();
|
|
|
|
var request = new ProcessVideoRequest
|
|
{
|
|
Url = youtubeUrl,
|
|
Format = "mp3",
|
|
Quality = "128",
|
|
SessionId = currentSessionId // Include session ID in request
|
|
};
|
|
|
|
var processingResponse = await ApiService.StartProcessingAsync(request);
|
|
|
|
if (processingResponse?.Success == true)
|
|
{
|
|
var jobId = processingResponse.FileName; // This contains the job ID
|
|
var sessionId = processingResponse.SessionId ?? currentSessionId;
|
|
|
|
// Update current session ID if server returned a different one
|
|
if (!string.IsNullOrWhiteSpace(processingResponse.SessionId) &&
|
|
processingResponse.SessionId != currentSessionId)
|
|
{
|
|
currentSessionId = processingResponse.SessionId;
|
|
await SessionService.SetSessionIdAsync(currentSessionId);
|
|
}
|
|
|
|
// Step 2: Monitor processing status and get video details
|
|
await MonitorProcessingAndShowDetails(jobId, sessionId);
|
|
}
|
|
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, string sessionId)
|
|
{
|
|
try
|
|
{
|
|
bool videoDetailsReceived = false;
|
|
int maxAttempts = 60; // Maximum polling attempts (about 2 minutes)
|
|
int attemptCount = 0;
|
|
int lastProgress = 0;
|
|
|
|
while (attemptCount < maxAttempts && isProcessing)
|
|
{
|
|
var status = await ApiService.GetProcessingStatusAsync(jobId);
|
|
|
|
if (status != null)
|
|
{
|
|
// Update progress only if it has increased (prevent UI flicker)
|
|
if (status.PercentageComplete > lastProgress)
|
|
{
|
|
progressPercentage = status.PercentageComplete;
|
|
lastProgress = progressPercentage;
|
|
UpdateProgressStage(progressPercentage);
|
|
CalculateEstimatedTime(progressPercentage);
|
|
}
|
|
|
|
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, jobId, sessionId);
|
|
}
|
|
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, string jobId, string sessionId)
|
|
{
|
|
try
|
|
{
|
|
// Ensure we reach 100% completion
|
|
progressPercentage = 100;
|
|
progressMessage = "Complete!";
|
|
UpdateProgressStage(progressPercentage);
|
|
|
|
// 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);
|
|
|
|
// Create the session-aware download URL using the configured API base URL
|
|
string downloadUrl;
|
|
if (!string.IsNullOrEmpty(result.DownloadUrl) && result.DownloadUrl.StartsWith("http"))
|
|
{
|
|
// Server provided a full download URL
|
|
downloadUrl = result.DownloadUrl;
|
|
}
|
|
else if (!string.IsNullOrEmpty(result.DownloadUrl) && result.DownloadUrl.StartsWith("/"))
|
|
{
|
|
// Server provided a relative URL, make it absolute using the API base URL
|
|
var baseUrl = ApiService.GetBaseUrl();
|
|
downloadUrl = $"{baseUrl}{result.DownloadUrl}";
|
|
}
|
|
else
|
|
{
|
|
// Construct session-aware download URL using the API base URL
|
|
var baseUrl = ApiService.GetBaseUrl();
|
|
downloadUrl = $"{baseUrl}/api/download/{sessionId}/{jobId}";
|
|
}
|
|
|
|
downloadLinks.Add(new DownloadItem
|
|
{
|
|
Title = title,
|
|
FileName = result.FileName ?? "sample.mp3",
|
|
DownloadUrl = downloadUrl,
|
|
CreatedAt = DateTime.Now
|
|
});
|
|
|
|
var processingTime = DateTime.Now - processingStartTime;
|
|
outputMessage = $"Successfully converted: {title} (took {processingTime.TotalSeconds:F1}s)";
|
|
|
|
// Reload session ZIP info after successful download
|
|
await LoadSessionZipInfoAsync();
|
|
|
|
// Show completion for a moment
|
|
StateHasChanged();
|
|
await Task.Delay(1000);
|
|
|
|
isProcessing = false;
|
|
youtubeUrl = "";
|
|
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
isProcessing = false;
|
|
apiErrorMessage = $"Error completing processing: {ex.Message}";
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private string ExtractTitleFromUrl(string url)
|
|
{
|
|
if (url.Contains("youtube.com/watch?v=") || url.Contains("youtu.be/"))
|
|
{
|
|
var videoId = url.Contains("youtu.be/")
|
|
? url.Substring(url.LastIndexOf('/') + 1).Split('?')[0]
|
|
: url.Split("v=")[1].Split('&')[0];
|
|
return $"YouTube Video {videoId.Substring(0, Math.Min(8, videoId.Length))}";
|
|
}
|
|
return $"Audio File {DateTime.Now:HHmmss}";
|
|
}
|
|
|
|
private async Task DownloadFile(string downloadUrl)
|
|
{
|
|
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)
|
|
{
|
|
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(youtubeUrl) && !isProcessing)
|
|
{
|
|
await DownloadMp3();
|
|
}
|
|
}
|
|
|
|
// Prevent default blur behavior to allow immediate button clicking
|
|
private void OnInputBlur()
|
|
{
|
|
// This method intentionally left empty to capture blur event without action
|
|
// This helps maintain focus state for better UX
|
|
}
|
|
|
|
// Prevent mousedown default to allow immediate button click
|
|
private async Task PreventDefault(MouseEventArgs e)
|
|
{
|
|
// Allow the button click to proceed immediately without waiting for blur
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|