style youtube cards

This commit is contained in:
Broque Thomas 2026-01-04 14:56:05 -08:00
parent 61e822bf6f
commit 58f7fdb005
2 changed files with 1500 additions and 1473 deletions

View file

@ -513,7 +513,7 @@ class YouTubeClient:
video_id = entry.get('id', '') video_id = entry.get('id', '')
filename = f"{video_id}||{title}" # Store video_id and title for later download filename = f"{video_id}||{title}" # Store video_id and title for later download
return TrackResult( track_result = TrackResult(
username="youtube", # YouTube doesn't have users - use constant username="youtube", # YouTube doesn't have users - use constant
filename=filename, filename=filename,
size=file_size, size=file_size,
@ -529,6 +529,11 @@ class YouTubeClient:
track_number=None track_number=None
) )
# Add thumbnail for frontend (surgical addition)
track_result.thumbnail = entry.get('thumbnail')
return track_result
async def search(self, query: str, timeout: int = None, progress_callback=None) -> tuple[List[TrackResult], List[AlbumResult]]: async def search(self, query: str, timeout: int = None, progress_callback=None) -> tuple[List[TrackResult], List[AlbumResult]]:
""" """
Search YouTube for tracks matching the query (async, Soulseek-compatible interface). Search YouTube for tracks matching the query (async, Soulseek-compatible interface).
@ -564,8 +569,8 @@ class YouTubeClient:
} }
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(ydl_opts) as ydl:
# Search YouTube (max 10 results) # Search YouTube (max 50 results)
search_results = ydl.extract_info(f"ytsearch10:{query}", download=False) search_results = ydl.extract_info(f"ytsearch50:{query}", download=False)
if not search_results or 'entries' not in search_results: if not search_results or 'entries' not in search_results:
return [] return []

View file

@ -93,39 +93,39 @@ let similarArtistsController = null; // Track ongoing similar artists stream to
// --- Wishlist Modal Persistence State Management --- // --- Wishlist Modal Persistence State Management ---
const WishlistModalState = { const WishlistModalState = {
// Track if wishlist modal was visible before page refresh // Track if wishlist modal was visible before page refresh
setVisible: function() { setVisible: function () {
localStorage.setItem('wishlist_modal_visible', 'true'); localStorage.setItem('wishlist_modal_visible', 'true');
console.log('📱 [Modal State] Wishlist modal marked as visible in localStorage'); console.log('📱 [Modal State] Wishlist modal marked as visible in localStorage');
}, },
setHidden: function() { setHidden: function () {
localStorage.setItem('wishlist_modal_visible', 'false'); localStorage.setItem('wishlist_modal_visible', 'false');
console.log('📱 [Modal State] Wishlist modal marked as hidden in localStorage'); console.log('📱 [Modal State] Wishlist modal marked as hidden in localStorage');
}, },
wasVisible: function() { wasVisible: function () {
const visible = localStorage.getItem('wishlist_modal_visible') === 'true'; const visible = localStorage.getItem('wishlist_modal_visible') === 'true';
console.log(`📱 [Modal State] Checking if wishlist modal was visible: ${visible}`); console.log(`📱 [Modal State] Checking if wishlist modal was visible: ${visible}`);
return visible; return visible;
}, },
clear: function() { clear: function () {
localStorage.removeItem('wishlist_modal_visible'); localStorage.removeItem('wishlist_modal_visible');
console.log('📱 [Modal State] Cleared wishlist modal visibility state'); console.log('📱 [Modal State] Cleared wishlist modal visibility state');
}, },
// Track if user manually closed the modal during auto-processing // Track if user manually closed the modal during auto-processing
setUserClosed: function() { setUserClosed: function () {
localStorage.setItem('wishlist_modal_user_closed', 'true'); localStorage.setItem('wishlist_modal_user_closed', 'true');
console.log('📱 [Modal State] User manually closed wishlist modal during auto-processing'); console.log('📱 [Modal State] User manually closed wishlist modal during auto-processing');
}, },
clearUserClosed: function() { clearUserClosed: function () {
localStorage.removeItem('wishlist_modal_user_closed'); localStorage.removeItem('wishlist_modal_user_closed');
console.log('📱 [Modal State] Cleared user closed state'); console.log('📱 [Modal State] Cleared user closed state');
}, },
wasUserClosed: function() { wasUserClosed: function () {
const closed = localStorage.getItem('wishlist_modal_user_closed') === 'true'; const closed = localStorage.getItem('wishlist_modal_user_closed') === 'true';
console.log(`📱 [Modal State] Checking if user closed modal: ${closed}`); console.log(`📱 [Modal State] Checking if user closed modal: ${closed}`);
return closed; return closed;
@ -294,7 +294,7 @@ const API = {
// INITIALIZATION // INITIALIZATION
// =============================== // ===============================
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
console.log('SoulSync WebUI initializing...'); console.log('SoulSync WebUI initializing...');
// Initialize components // Initialize components
@ -330,7 +330,7 @@ document.addEventListener('DOMContentLoaded', function() {
loadInitialData(); loadInitialData();
// Handle window resize to re-check track title scrolling // Handle window resize to re-check track title scrolling
window.addEventListener('resize', function() { window.addEventListener('resize', function () {
if (currentTrack) { if (currentTrack) {
const trackTitleElement = document.getElementById('track-title'); const trackTitleElement = document.getElementById('track-title');
const trackTitle = currentTrack.title || 'Unknown Track'; const trackTitle = currentTrack.title || 'Unknown Track';
@ -1631,11 +1631,11 @@ async function loadSettingsData() {
document.getElementById('tidal-callback-display').textContent = settings.tidal?.redirect_uri || 'http://127.0.0.1:8889/tidal/callback'; document.getElementById('tidal-callback-display').textContent = settings.tidal?.redirect_uri || 'http://127.0.0.1:8889/tidal/callback';
// Add event listeners to update display URLs when input changes // Add event listeners to update display URLs when input changes
document.getElementById('spotify-redirect-uri').addEventListener('input', function() { document.getElementById('spotify-redirect-uri').addEventListener('input', function () {
document.getElementById('spotify-callback-display').textContent = this.value || 'http://127.0.0.1:8888/callback'; document.getElementById('spotify-callback-display').textContent = this.value || 'http://127.0.0.1:8888/callback';
}); });
document.getElementById('tidal-redirect-uri').addEventListener('input', function() { document.getElementById('tidal-redirect-uri').addEventListener('input', function () {
document.getElementById('tidal-callback-display').textContent = this.value || 'http://127.0.0.1:8889/tidal/callback'; document.getElementById('tidal-callback-display').textContent = this.value || 'http://127.0.0.1:8889/tidal/callback';
}); });
@ -3141,7 +3141,7 @@ function initializeSearchModeToggle() {
// Attach download handlers // Attach download handlers
mainResultsArea.querySelectorAll('.download-result-btn').forEach(btn => { mainResultsArea.querySelectorAll('.download-result-btn').forEach(btn => {
btn.addEventListener('click', async function() { btn.addEventListener('click', async function () {
const result = JSON.parse(this.dataset.result); const result = JSON.parse(this.dataset.result);
const type = this.dataset.type; const type = this.dataset.type;
@ -7674,7 +7674,7 @@ function processModalStatusUpdate(playlistId, data) {
if (document.getElementById(`analysis-progress-fill-${playlistId}`).style.width !== '100%') { if (document.getElementById(`analysis-progress-fill-${playlistId}`).style.width !== '100%') {
document.getElementById(`analysis-progress-fill-${playlistId}`).style.width = '100%'; document.getElementById(`analysis-progress-fill-${playlistId}`).style.width = '100%';
document.getElementById(`analysis-progress-text-${playlistId}`).textContent = 'Analysis complete!'; document.getElementById(`analysis-progress-text-${playlistId}`).textContent = 'Analysis complete!';
if(data.analysis_results) { if (data.analysis_results) {
updateTrackAnalysisResults(playlistId, data.analysis_results); updateTrackAnalysisResults(playlistId, data.analysis_results);
const foundCount = data.analysis_results.filter(r => r.found).length; const foundCount = data.analysis_results.filter(r => r.found).length;
const missingCount = data.analysis_results.filter(r => !r.found).length; const missingCount = data.analysis_results.filter(r => !r.found).length;
@ -7751,7 +7751,7 @@ function processModalStatusUpdate(playlistId, data) {
} }
} }
if(statusEl) { if (statusEl) {
statusEl.textContent = statusText; statusEl.textContent = statusText;
console.debug(`✅ [Status Update] Updated track ${task.track_index} to: ${statusText}${isV2Task ? ' (V2)' : ''}`); console.debug(`✅ [Status Update] Updated track ${task.track_index} to: ${statusText}${isV2Task ? ' (V2)' : ''}`);
} else { } else {
@ -8059,7 +8059,7 @@ async function updateModalWithLiveDownloadProgress() {
if (downloadData.error) return; if (downloadData.error) return;
// Get all active and finished downloads // Get all active and finished downloads
const allDownloads = {...(downloadData.active || {}), ...(downloadData.finished || {})}; const allDownloads = { ...(downloadData.active || {}), ...(downloadData.finished || {}) };
// Update modal tracks that have active downloads // Update modal tracks that have active downloads
const modalRows = document.querySelectorAll('.download-missing-modal tr[data-track-index]'); const modalRows = document.querySelectorAll('.download-missing-modal tr[data-track-index]');
@ -9073,8 +9073,30 @@ function displayDownloadsResults(results) {
let html = ''; let html = '';
results.forEach((result, index) => { results.forEach((result, index) => {
const isAlbum = result.result_type === 'album'; const isAlbum = result.result_type === 'album';
if (result.username === 'youtube') {
const thumbnail = result.thumbnail || '';
const durationText = result.duration ? formatDuration(result.duration) : '';
if (isAlbum) { html += `
<div class="youtube-result-card" style="display: flex; gap: 1rem; padding: 1rem; background: rgba(255, 0, 0, 0.05); border: 1px solid rgba(255, 0, 0, 0.1); border-radius: 8px; margin-bottom: 0.5rem; align-items: center;">
<div class="yt-thumb" style="width: 120px; height: 68px; flex-shrink: 0; border-radius: 4px; overflow: hidden; background: #000;">
<img src="${thumbnail}" style="width: 100%; height: 100%; object-fit: cover;" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iNjgiPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiMzMzMiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZmlsbD0iIzY2NiIgZm9udC1zaXplPSIxMiIgZGVtaW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gSW1hZ2U8L3RleHQ+PC9zdmc+'">
</div>
<div class="yt-info" style="flex: 1;">
<div class="yt-title" style="font-weight: 600; margin-bottom: 4px;">${escapeHtml(result.title)}</div>
<div class="yt-channel" style="font-size: 0.9em; opacity: 0.8;">${escapeHtml(result.artist || result.username)} ${durationText}</div>
</div>
<div class="yt-actions">
<button onclick="streamAlbumTrack(${index}, -1)" class="track-stream-btn">Stream </button>
<button onclick="downloadAlbumTrack(${index}, -1)" class="track-download-btn">Download </button>
</div>
</div>
`;
return; // Skip standard rendering for YouTube result
}
if (isAlbum && result.username !== 'youtube') {
const trackCount = result.tracks ? result.tracks.length : 0; const trackCount = result.tracks ? result.tracks.length : 0;
const totalSize = result.total_size ? `${(result.total_size / 1024 / 1024).toFixed(1)} MB` : 'Unknown size'; const totalSize = result.total_size ? `${(result.total_size / 1024 / 1024).toFixed(1)} MB` : 'Unknown size';
@ -9892,7 +9914,7 @@ function openDiscoveryFixModal(platform, identifier, trackIndex) {
} }
// Add new enter key handler // Add new enter key handler
discoveryFixEnterHandler = function(e) { discoveryFixEnterHandler = function (e) {
if (e.key === 'Enter') searchDiscoveryFix(); if (e.key === 'Enter') searchDiscoveryFix();
}; };
trackInput.addEventListener('keypress', discoveryFixEnterHandler); trackInput.addEventListener('keypress', discoveryFixEnterHandler);
@ -13223,7 +13245,7 @@ async function handleTidalCardClick(playlistId) {
state.discovery_results = fullState.discovery_results; state.discovery_results = fullState.discovery_results;
state.spotify_matches = fullState.spotify_matches || state.spotify_matches; state.spotify_matches = fullState.spotify_matches || state.spotify_matches;
state.discovery_progress = fullState.discovery_progress || state.discovery_progress; state.discovery_progress = fullState.discovery_progress || state.discovery_progress;
tidalPlaylistStates[playlistId] = {...tidalPlaylistStates[playlistId], ...state}; tidalPlaylistStates[playlistId] = { ...tidalPlaylistStates[playlistId], ...state };
console.log(`✅ [Card Click] Restored ${fullState.discovery_results.length} discovery results from backend`); console.log(`✅ [Card Click] Restored ${fullState.discovery_results.length} discovery results from backend`);
} }
} }
@ -14991,7 +15013,7 @@ function handleBeatportCategoryClick(category) {
console.log(`🎵 Beatport category clicked: ${category}`); console.log(`🎵 Beatport category clicked: ${category}`);
// Only handle genres category now - homepage has direct chart buttons // Only handle genres category now - homepage has direct chart buttons
switch(category) { switch (category) {
case 'genres': case 'genres':
showBeatportSubView('genres'); showBeatportSubView('genres');
loadBeatportGenres(); // Load genres dynamically loadBeatportGenres(); // Load genres dynamically
@ -15645,7 +15667,7 @@ async function getRebuildPageTrackData(trackDataKey) {
// Hook into the loadBeatportTop10Lists function to cache track data // Hook into the loadBeatportTop10Lists function to cache track data
const originalLoadBeatportTop10Lists = window.loadBeatportTop10Lists; const originalLoadBeatportTop10Lists = window.loadBeatportTop10Lists;
if (originalLoadBeatportTop10Lists) { if (originalLoadBeatportTop10Lists) {
window.loadBeatportTop10Lists = async function() { window.loadBeatportTop10Lists = async function () {
const result = await originalLoadBeatportTop10Lists.apply(this, arguments); const result = await originalLoadBeatportTop10Lists.apply(this, arguments);
// If the load was successful, we can potentially cache the track data // If the load was successful, we can potentially cache the track data
@ -19172,7 +19194,7 @@ async function startListenBrainzDiscovery(playlistMbid) {
// Call backend to start discovery worker // Call backend to start discovery worker
const response = await fetch(`/api/listenbrainz/discovery/start/${playlistMbid}`, { const response = await fetch(`/api/listenbrainz/discovery/start/${playlistMbid}`, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
playlist: state.playlist playlist: state.playlist
}) })
@ -22678,7 +22700,7 @@ async function extractImageColors(imageUrl, callback) {
img.crossOrigin = 'anonymous'; img.crossOrigin = 'anonymous';
img.onload = function() { img.onload = function () {
// Resize to small dimensions for faster processing // Resize to small dimensions for faster processing
const size = 50; const size = 50;
canvas.width = size; canvas.width = size;
@ -22730,7 +22752,7 @@ async function extractImageColors(imageUrl, callback) {
} }
}; };
img.onerror = function() { img.onerror = function () {
callback(['#1db954', '#1ed760']); // Fallback on error callback(['#1db954', '#1ed760']); // Fallback on error
}; };
@ -31615,10 +31637,10 @@ async function openListenBrainzPlaylist(playlistMbid, playlistName) {
const spotifyTracks = tracks.map(track => ({ const spotifyTracks = tracks.map(track => ({
id: track.recording_mbid || `listenbrainz_${track.title}_${track.creator}`.replace(/[^a-z0-9]/gi, '_'), // Generate ID if missing id: track.recording_mbid || `listenbrainz_${track.title}_${track.creator}`.replace(/[^a-z0-9]/gi, '_'), // Generate ID if missing
name: track.title || 'Unknown', name: track.title || 'Unknown',
artists: [{name: cleanArtistName(track.creator || 'Unknown')}], // Proper Spotify format artists: [{ name: cleanArtistName(track.creator || 'Unknown') }], // Proper Spotify format
album: { album: {
name: track.album || 'Unknown Album', name: track.album || 'Unknown Album',
images: track.album_cover_url ? [{url: track.album_cover_url}] : [] images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
}, },
duration_ms: track.duration_ms || 0, duration_ms: track.duration_ms || 0,
listenbrainz_metadata: track.additional_metadata listenbrainz_metadata: track.additional_metadata
@ -33224,7 +33246,7 @@ async function rehydrateDiscoverDownloadModal(playlistId) {
const spotifyTracks = tracks.map(track => ({ const spotifyTracks = tracks.map(track => ({
id: track.mbid || `listenbrainz_${track.track_name}_${track.artist_name}`.replace(/[^a-z0-9]/gi, '_'), // Generate ID if missing id: track.mbid || `listenbrainz_${track.track_name}_${track.artist_name}`.replace(/[^a-z0-9]/gi, '_'), // Generate ID if missing
name: track.track_name, name: track.track_name,
artists: [{name: cleanArtistName(track.artist_name)}], // Proper Spotify format artists: [{ name: cleanArtistName(track.artist_name) }], // Proper Spotify format
album: { album: {
name: track.album_name, name: track.album_name,
images: track.album_cover_url ? [{ url: track.album_cover_url }] : [] images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
@ -33279,23 +33301,23 @@ async function rehydrateDiscoverDownloadModal(playlistId) {
if (track.artists && Array.isArray(track.artists)) { if (track.artists && Array.isArray(track.artists)) {
artistsArray = track.artists.map(artist => { artistsArray = track.artists.map(artist => {
if (typeof artist === 'string') { if (typeof artist === 'string') {
return {name: artist}; return { name: artist };
} else if (artist && artist.name) { } else if (artist && artist.name) {
return {name: artist.name}; return { name: artist.name };
} else { } else {
return {name: String(artist || 'Unknown Artist')}; return { name: String(artist || 'Unknown Artist') };
} }
}); });
} else if (track.artists && typeof track.artists === 'string') { } else if (track.artists && typeof track.artists === 'string') {
artistsArray = [{name: track.artists}]; artistsArray = [{ name: track.artists }];
} else { } else {
artistsArray = [{name: 'Unknown Artist'}]; artistsArray = [{ name: 'Unknown Artist' }];
} }
return { return {
id: track.id, id: track.id,
name: track.name, name: track.name,
artists: artistsArray, artists: artistsArray,
album: track.album || {name: 'Unknown Album', images: []}, album: track.album || { name: 'Unknown Album', images: [] },
duration_ms: track.duration_ms || 0, duration_ms: track.duration_ms || 0,
external_urls: track.external_urls || {} external_urls: track.external_urls || {}
}; };