Update video details to fetch real title & thumbnail only
Refactored backend to extract actual YouTube title and thumbnail from HTML, removing simulated metadata fields (description, author, duration, views, upload date, tags). Simplified UI to display only title and thumbnail, and enhanced visuals with animated musical notes background. Removed related formatting and display logic for unused metadata.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
using TuneRipper.Shared.Models;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TuneRipper.Api.Services;
|
||||
|
||||
@@ -13,10 +16,12 @@ public interface IVideoService
|
||||
public class VideoService : IVideoService
|
||||
{
|
||||
private readonly ILogger<VideoService> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public VideoService(ILogger<VideoService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<YouTubeVideoDetails?> GetVideoDetailsAsync(string url)
|
||||
@@ -29,26 +34,48 @@ public class VideoService : IVideoService
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simulate API call delay
|
||||
await Task.Delay(Random.Shared.Next(500, 2000));
|
||||
|
||||
var videoId = ExtractVideoId(url);
|
||||
|
||||
// In production, use YouTube API or yt-dlp to get actual video details
|
||||
var thumbnailUrl = $"https://img.youtube.com/vi/{videoId}/maxresdefault.jpg";
|
||||
string? title = null;
|
||||
|
||||
// Fetch HTML and parse title
|
||||
try
|
||||
{
|
||||
var html = await _httpClient.GetStringAsync(url);
|
||||
// Try OpenGraph title first
|
||||
var ogTitleMatch = Regex.Match(html, "<meta property='og:title' content='(.*?)'", RegexOptions.IgnoreCase);
|
||||
if (!ogTitleMatch.Success)
|
||||
{
|
||||
ogTitleMatch = Regex.Match(html, "<meta property=\"og:title\" content=\"(.*?)\"", RegexOptions.IgnoreCase);
|
||||
}
|
||||
if (ogTitleMatch.Success)
|
||||
{
|
||||
title = ogTitleMatch.Groups[1].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to <title>
|
||||
var titleMatch = Regex.Match(html, "<title>(.*?)</title>", RegexOptions.IgnoreCase);
|
||||
if (titleMatch.Success)
|
||||
{
|
||||
title = titleMatch.Groups[1].Value.Replace(" - YouTube", "").Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch or parse video title for {Url}", url);
|
||||
title = $"YouTube Video {videoId}";
|
||||
}
|
||||
|
||||
var videoDetails = new YouTubeVideoDetails
|
||||
{
|
||||
Id = videoId,
|
||||
Title = GenerateSampleTitle(videoId),
|
||||
Description = $"Video description for {videoId}. In production, this would be fetched from YouTube API.",
|
||||
ThumbnailUrl = $"https://img.youtube.com/vi/{videoId}/maxresdefault.jpg",
|
||||
Duration = TimeSpan.FromMinutes(Random.Shared.Next(2, 10)).Add(TimeSpan.FromSeconds(Random.Shared.Next(0, 59))),
|
||||
Author = $"Channel_{Random.Shared.Next(1, 1000)}",
|
||||
ViewCount = Random.Shared.Next(100, 5000000),
|
||||
UploadDate = DateTime.UtcNow.AddDays(-Random.Shared.Next(1, 1000)),
|
||||
Tags = GenerateSampleTags()
|
||||
Title = title,
|
||||
ThumbnailUrl = thumbnailUrl
|
||||
};
|
||||
|
||||
_logger.LogInformation("Successfully fetched details for video {VideoId}", videoId);
|
||||
_logger.LogInformation("Fetched image and title for video {VideoId}", videoId);
|
||||
return videoDetails;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -81,7 +108,6 @@ public class VideoService : IVideoService
|
||||
|
||||
try
|
||||
{
|
||||
// Different YouTube URL formats
|
||||
var patterns = new Dictionary<string, string>
|
||||
{
|
||||
{ @"youtu\.be/([^?&]+)", "$1" },
|
||||
@@ -106,29 +132,4 @@ public class VideoService : IVideoService
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GenerateSampleTitle(string videoId)
|
||||
{
|
||||
var sampleTitles = new[]
|
||||
{
|
||||
"Amazing Music Video",
|
||||
"Best Song Ever",
|
||||
"Epic Guitar Solo",
|
||||
"Relaxing Piano Music",
|
||||
"Dance Hit 2024",
|
||||
"Acoustic Session",
|
||||
"Live Performance",
|
||||
"Official Music Video"
|
||||
};
|
||||
|
||||
var randomTitle = sampleTitles[Random.Shared.Next(sampleTitles.Length)];
|
||||
return $"{randomTitle} - {videoId}";
|
||||
}
|
||||
|
||||
private static List<string> GenerateSampleTags()
|
||||
{
|
||||
var allTags = new[] { "music", "video", "song", "artist", "album", "live", "official", "acoustic", "cover", "remix" };
|
||||
var tagCount = Random.Shared.Next(3, 7);
|
||||
return allTags.OrderBy(x => Random.Shared.Next()).Take(tagCount).ToList();
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,5 @@ public class YouTubeVideoDetails
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? ThumbnailUrl { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
public string? Author { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public DateTime UploadDate { get; set; }
|
||||
public List<string> Tags { get; set; } = new();
|
||||
}
|
||||
+125
-206
@@ -6,166 +6,147 @@
|
||||
|
||||
<PageTitle>TuneRipper - YouTube to MP3 Downloader</PageTitle>
|
||||
|
||||
<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>
|
||||
<!-- 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="download-form">
|
||||
<div class="input-container">
|
||||
<input type="text"
|
||||
@bind="youtubeUrl"
|
||||
@onkeypress="HandleKeyPress"
|
||||
placeholder="Paste your YouTube link here..."
|
||||
class="url-input"
|
||||
disabled="@isProcessing" />
|
||||
<button @onclick="DownloadMp3"
|
||||
class="download-btn"
|
||||
disabled="@(string.IsNullOrWhiteSpace(youtubeUrl) || isProcessing)">
|
||||
<span class="btn-icon">🎵</span>
|
||||
@if (isProcessing)
|
||||
{
|
||||
<span>Processing...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Download MP3</span>
|
||||
}
|
||||
</button>
|
||||
</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>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="video-details">
|
||||
<h4 class="video-title">@currentVideoDetails.Title</h4>
|
||||
<div class="video-meta">
|
||||
<span class="meta-item">
|
||||
<span class="meta-icon">👤</span>
|
||||
<span>@currentVideoDetails.Author</span>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-icon">⏱️</span>
|
||||
<span>@FormatDuration(currentVideoDetails.Duration)</span>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-icon">👁️</span>
|
||||
<span>@FormatViewCount(currentVideoDetails.ViewCount) views</span>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-icon">📅</span>
|
||||
<span>@currentVideoDetails.UploadDate.ToString("MMM dd, yyyy")</span>
|
||||
</span>
|
||||
</div>
|
||||
@if (currentVideoDetails.Tags?.Any() == true)
|
||||
{
|
||||
<div class="video-tags">
|
||||
@foreach (var tag in currentVideoDetails.Tags.Take(5))
|
||||
{
|
||||
<span class="tag">@tag</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (isProcessing)
|
||||
{
|
||||
<div class="conversion-status">
|
||||
<span class="status-icon">⚡</span>
|
||||
<span class="status-text">Converting to MP3...</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@if (isProcessing)
|
||||
{
|
||||
<div class="progress-container @(currentVideoDetails != null ? "with-video-details" : "")">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: @(progressPercentage)%"></div>
|
||||
</div>
|
||||
<div class="progress-text">
|
||||
@if (currentVideoDetails != null && progressPercentage > 15)
|
||||
<div class="download-form">
|
||||
<div class="input-container">
|
||||
<input type="text"
|
||||
@bind="youtubeUrl"
|
||||
@onkeypress="HandleKeyPress"
|
||||
placeholder="Paste your YouTube link here..."
|
||||
class="url-input"
|
||||
disabled="@isProcessing" />
|
||||
<button @onclick="DownloadMp3"
|
||||
class="download-btn"
|
||||
disabled="@(string.IsNullOrWhiteSpace(youtubeUrl) || isProcessing)">
|
||||
<span class="btn-icon">🎵</span>
|
||||
@if (isProcessing)
|
||||
{
|
||||
<span class="progress-highlight">@progressMessage</span>
|
||||
<span class="progress-percentage">(@progressPercentage%)</span>
|
||||
<span>Processing...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@progressMessage (@progressPercentage%)</span>
|
||||
<span>Download MP3</span>
|
||||
}
|
||||
</button>
|
||||
</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>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="video-details">
|
||||
<h4 class="video-title">@currentVideoDetails.Title</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isProcessing)
|
||||
{
|
||||
<div class="progress-container @(currentVideoDetails != null ? "with-video-details" : "")">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: @(progressPercentage)%"></div>
|
||||
</div>
|
||||
<div class="progress-text">
|
||||
@if (currentVideoDetails != null && progressPercentage > 15)
|
||||
{
|
||||
<span class="progress-highlight">@progressMessage</span>
|
||||
<span class="progress-percentage">(@progressPercentage%)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@progressMessage (@progressPercentage%)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(outputMessage) && !isProcessing)
|
||||
{
|
||||
<div class="output-message">
|
||||
<p>@outputMessage</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* API Error Message *@
|
||||
@if (!string.IsNullOrWhiteSpace(apiErrorMessage) && !isProcessing)
|
||||
{
|
||||
<div class="error-message">
|
||||
<p>❌ @apiErrorMessage</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (downloadLinks.Any())
|
||||
{
|
||||
<div class="download-links-section">
|
||||
<h3 class="downloads-title">📥 Your Downloads</h3>
|
||||
<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.FileName)">
|
||||
<span class="download-icon">⬇️</span>
|
||||
Download MP3
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(outputMessage) && !isProcessing)
|
||||
{
|
||||
<div class="output-message">
|
||||
<p>@outputMessage</p>
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<span class="feature-icon">⚡</span>
|
||||
<span>Fast Conversion</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* API Error Message *@
|
||||
@if (!string.IsNullOrWhiteSpace(apiErrorMessage) && !isProcessing)
|
||||
{
|
||||
<div class="error-message">
|
||||
<p>❌ @apiErrorMessage</p>
|
||||
<div class="feature">
|
||||
<span class="feature-icon">🎧</span>
|
||||
<span>High Quality</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (downloadLinks.Any())
|
||||
{
|
||||
<div class="download-links-section">
|
||||
<h3 class="downloads-title">📥 Your Downloads</h3>
|
||||
<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.FileName)">
|
||||
<span class="download-icon">⬇️</span>
|
||||
Download MP3
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<div class="feature">
|
||||
<span class="feature-icon">🆓</span>
|
||||
<span>Completely Free</span>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -384,40 +365,6 @@
|
||||
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;
|
||||
@@ -696,34 +643,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatDuration(TimeSpan duration)
|
||||
{
|
||||
if (duration.TotalHours >= 1)
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
private string ExtractTitleFromUrl(string url)
|
||||
{
|
||||
if (url.Contains("youtube.com/watch?v=") || url.Contains("youtu.be/"))
|
||||
|
||||
Reference in New Issue
Block a user