listenbrainz update
This commit is contained in:
parent
bc54f2c1a3
commit
333d984527
5 changed files with 514 additions and 200 deletions
|
|
@ -2,6 +2,7 @@ import requests
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
from config.settings import config_manager
|
from config.settings import config_manager
|
||||||
|
import time
|
||||||
|
|
||||||
logger = get_logger("listenbrainz_client")
|
logger = get_logger("listenbrainz_client")
|
||||||
|
|
||||||
|
|
@ -13,18 +14,49 @@ class ListenBrainzClient:
|
||||||
self.token = config_manager.get("listenbrainz.token", "")
|
self.token = config_manager.get("listenbrainz.token", "")
|
||||||
self.username = None
|
self.username = None
|
||||||
|
|
||||||
|
# Create a session for connection pooling
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({
|
||||||
|
'User-Agent': 'SoulSync/1.0'
|
||||||
|
})
|
||||||
|
|
||||||
if self.token:
|
if self.token:
|
||||||
# Validate token and get username
|
# Validate token and get username
|
||||||
self._validate_and_get_username()
|
self._validate_and_get_username()
|
||||||
|
|
||||||
|
def _make_request_with_retry(self, method: str, url: str, max_retries: int = 3, **kwargs):
|
||||||
|
"""Make HTTP request with retry logic"""
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
if method.lower() == 'get':
|
||||||
|
response = self.session.get(url, **kwargs)
|
||||||
|
elif method.lower() == 'post':
|
||||||
|
response = self.session.post(url, **kwargs)
|
||||||
|
else:
|
||||||
|
response = self.session.request(method, url, **kwargs)
|
||||||
|
|
||||||
|
return response
|
||||||
|
except (requests.exceptions.ConnectionError,
|
||||||
|
requests.exceptions.Timeout,
|
||||||
|
ConnectionResetError) as e:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
wait_time = (attempt + 1) * 2 # Exponential backoff
|
||||||
|
logger.warning(f"Connection error (attempt {attempt + 1}/{max_retries}), retrying in {wait_time}s: {e}")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed after {max_retries} attempts: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def _validate_and_get_username(self):
|
def _validate_and_get_username(self):
|
||||||
"""Validate token and retrieve username"""
|
"""Validate token and retrieve username"""
|
||||||
try:
|
try:
|
||||||
url = f"{self.base_url}/validate-token"
|
url = f"{self.base_url}/validate-token"
|
||||||
headers = {'Authorization': f'Token {self.token}'}
|
headers = {'Authorization': f'Token {self.token}'}
|
||||||
response = requests.get(url, headers=headers, timeout=5)
|
response = self._make_request_with_retry('get', url, headers=headers, timeout=10)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response and response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if data.get('valid'):
|
if data.get('valid'):
|
||||||
self.username = data.get('user_name')
|
self.username = data.get('user_name')
|
||||||
|
|
@ -57,18 +89,19 @@ class ListenBrainzClient:
|
||||||
'offset': offset
|
'offset': offset
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.get(url, params=params, timeout=10)
|
response = self._make_request_with_retry('get', url, params=params, timeout=15)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response and response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
playlists = data.get('playlists', [])
|
playlists = data.get('playlists', [])
|
||||||
logger.info(f"📋 Fetched {len(playlists)} playlists created for {self.username}")
|
logger.info(f"📋 Fetched {len(playlists)} playlists created for {self.username}")
|
||||||
return playlists
|
return playlists
|
||||||
elif response.status_code == 404:
|
elif response and response.status_code == 404:
|
||||||
logger.warning(f"User {self.username} not found")
|
logger.warning(f"User {self.username} not found")
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to fetch created-for playlists: {response.status_code}")
|
status = response.status_code if response else 'No response'
|
||||||
|
logger.error(f"Failed to fetch created-for playlists: {status}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -92,18 +125,19 @@ class ListenBrainzClient:
|
||||||
'offset': offset
|
'offset': offset
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.get(url, headers=headers, params=params, timeout=10)
|
response = self._make_request_with_retry('get', url, headers=headers, params=params, timeout=15)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response and response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
playlists = data.get('playlists', [])
|
playlists = data.get('playlists', [])
|
||||||
logger.info(f"📋 Fetched {len(playlists)} user playlists for {self.username}")
|
logger.info(f"📋 Fetched {len(playlists)} user playlists for {self.username}")
|
||||||
return playlists
|
return playlists
|
||||||
elif response.status_code == 404:
|
elif response and response.status_code == 404:
|
||||||
logger.warning(f"User {self.username} not found")
|
logger.warning(f"User {self.username} not found")
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to fetch user playlists: {response.status_code}")
|
status = response.status_code if response else 'No response'
|
||||||
|
logger.error(f"Failed to fetch user playlists: {status}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -127,18 +161,19 @@ class ListenBrainzClient:
|
||||||
'offset': offset
|
'offset': offset
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.get(url, headers=headers, params=params, timeout=10)
|
response = self._make_request_with_retry('get', url, headers=headers, params=params, timeout=15)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response and response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
playlists = data.get('playlists', [])
|
playlists = data.get('playlists', [])
|
||||||
logger.info(f"📋 Fetched {len(playlists)} collaborative playlists for {self.username}")
|
logger.info(f"📋 Fetched {len(playlists)} collaborative playlists for {self.username}")
|
||||||
return playlists
|
return playlists
|
||||||
elif response.status_code == 404:
|
elif response and response.status_code == 404:
|
||||||
logger.warning(f"User {self.username} not found")
|
logger.warning(f"User {self.username} not found")
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to fetch collaborative playlists: {response.status_code}")
|
status = response.status_code if response else 'No response'
|
||||||
|
logger.error(f"Failed to fetch collaborative playlists: {status}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -165,22 +200,23 @@ class ListenBrainzClient:
|
||||||
if self.token:
|
if self.token:
|
||||||
headers['Authorization'] = f'Token {self.token}'
|
headers['Authorization'] = f'Token {self.token}'
|
||||||
|
|
||||||
response = requests.get(url, headers=headers, params=params, timeout=10)
|
response = self._make_request_with_retry('get', url, headers=headers, params=params, timeout=20)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response and response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
playlist = data.get('playlist', {})
|
playlist = data.get('playlist', {})
|
||||||
track_count = len(playlist.get('track', []))
|
track_count = len(playlist.get('track', []))
|
||||||
logger.info(f"📋 Fetched playlist '{playlist.get('title')}' with {track_count} tracks")
|
logger.info(f"📋 Fetched playlist '{playlist.get('title')}' with {track_count} tracks")
|
||||||
return playlist
|
return playlist
|
||||||
elif response.status_code == 404:
|
elif response and response.status_code == 404:
|
||||||
logger.warning(f"Playlist {playlist_mbid} not found")
|
logger.warning(f"Playlist {playlist_mbid} not found")
|
||||||
return None
|
return None
|
||||||
elif response.status_code == 401:
|
elif response and response.status_code == 401:
|
||||||
logger.warning(f"Unauthorized to access playlist {playlist_mbid}")
|
logger.warning(f"Unauthorized to access playlist {playlist_mbid}")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to fetch playlist: {response.status_code}")
|
status = response.status_code if response else 'No response'
|
||||||
|
logger.error(f"Failed to fetch playlist: {status}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -15300,12 +15300,18 @@ def get_listenbrainz_playlist_tracks(playlist_mbid):
|
||||||
extension = track.get('extension', {})
|
extension = track.get('extension', {})
|
||||||
mb_data = extension.get('https://musicbrainz.org/doc/jspf#track', {})
|
mb_data = extension.get('https://musicbrainz.org/doc/jspf#track', {})
|
||||||
|
|
||||||
|
# Extract album cover from extension if available
|
||||||
|
album_cover_url = None
|
||||||
|
if mb_data and 'album_cover_url' in mb_data:
|
||||||
|
album_cover_url = mb_data['album_cover_url']
|
||||||
|
|
||||||
track_data = {
|
track_data = {
|
||||||
'title': track.get('title', 'Unknown Track'),
|
'track_name': track.get('title', 'Unknown Track'),
|
||||||
'creator': track.get('creator', 'Unknown Artist'),
|
'artist_name': track.get('creator', 'Unknown Artist'),
|
||||||
'album': track.get('album', 'Unknown Album'),
|
'album_name': track.get('album', 'Unknown Album'),
|
||||||
'duration_ms': track.get('duration', 0),
|
'duration_ms': track.get('duration', 0),
|
||||||
'recording_mbid': recording_mbid,
|
'mbid': recording_mbid,
|
||||||
|
'album_cover_url': album_cover_url,
|
||||||
'additional_metadata': mb_data
|
'additional_metadata': mb_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15313,13 +15319,8 @@ def get_listenbrainz_playlist_tracks(playlist_mbid):
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"playlist": {
|
"tracks": tracks,
|
||||||
"identifier": playlist.get('identifier'),
|
"track_count": len(tracks)
|
||||||
"title": playlist.get('title'),
|
|
||||||
"creator": playlist.get('creator'),
|
|
||||||
"tracks": tracks,
|
|
||||||
"track_count": len(tracks)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -2134,36 +2134,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ListenBrainz Recommendations (Created For You) -->
|
<!-- ListenBrainz Playlists (Tabbed) -->
|
||||||
<div class="discover-section">
|
<div class="discover-section">
|
||||||
<div class="discover-section-header">
|
<div class="discover-section-header">
|
||||||
<h2 class="discover-section-title">🧠 ListenBrainz Recommendations</h2>
|
<h2 class="discover-section-title">🧠 ListenBrainz Playlists</h2>
|
||||||
<p class="discover-section-subtitle">Playlists curated for you by ListenBrainz</p>
|
<p class="discover-section-subtitle">Playlists from ListenBrainz</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="discover-carousel" id="listenbrainz-created-for-carousel">
|
|
||||||
<div class="discover-loading"><div class="loading-spinner"></div><p>Loading ListenBrainz recommendations...</p></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Your ListenBrainz Playlists -->
|
<!-- ListenBrainz Tabs -->
|
||||||
<div class="discover-section">
|
<div class="listenbrainz-tabs" id="listenbrainz-tabs">
|
||||||
<div class="discover-section-header">
|
<div class="discover-loading"><div class="loading-spinner"></div><p>Loading playlists...</p></div>
|
||||||
<h2 class="discover-section-title">📚 Your ListenBrainz Playlists</h2>
|
|
||||||
<p class="discover-section-subtitle">Your personal playlists from ListenBrainz</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="discover-carousel" id="listenbrainz-user-playlists-carousel">
|
|
||||||
<div class="discover-loading"><div class="loading-spinner"></div><p>Loading your playlists...</p></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Collaborative ListenBrainz Playlists -->
|
<!-- ListenBrainz Tab Content -->
|
||||||
<div class="discover-section">
|
<div class="listenbrainz-tab-content" id="listenbrainz-tab-content">
|
||||||
<div class="discover-section-header">
|
<!-- Content will be populated dynamically -->
|
||||||
<h2 class="discover-section-title">🤝 Collaborative Playlists</h2>
|
|
||||||
<p class="discover-section-subtitle">Playlists you collaborate on</p>
|
|
||||||
</div>
|
|
||||||
<div class="discover-carousel" id="listenbrainz-collaborative-carousel">
|
|
||||||
<div class="discover-loading"><div class="loading-spinner"></div><p>Loading collaborative playlists...</p></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24744,9 +24744,7 @@ async function loadDiscoverPage() {
|
||||||
loadPersonalizedForgottenFavorites(), // NEW: Forgotten favorites
|
loadPersonalizedForgottenFavorites(), // NEW: Forgotten favorites
|
||||||
loadDiscoveryShuffle(), // NEW: Discovery Shuffle
|
loadDiscoveryShuffle(), // NEW: Discovery Shuffle
|
||||||
loadFamiliarFavorites(), // NEW: Familiar Favorites
|
loadFamiliarFavorites(), // NEW: Familiar Favorites
|
||||||
loadListenBrainzCreatedFor(), // ListenBrainz recommendations
|
initializeListenBrainzTabs(), // ListenBrainz playlists (tabbed)
|
||||||
loadListenBrainzUserPlaylists(), // ListenBrainz user playlists
|
|
||||||
loadListenBrainzCollaborative(), // ListenBrainz collaborative playlists
|
|
||||||
loadDecadeBrowserTabs(), // Time Machine (tabbed by decade)
|
loadDecadeBrowserTabs(), // Time Machine (tabbed by decade)
|
||||||
loadGenreBrowserTabs() // Browse by Genre (tabbed by genre)
|
loadGenreBrowserTabs() // Browse by Genre (tabbed by genre)
|
||||||
]);
|
]);
|
||||||
|
|
@ -26267,193 +26265,474 @@ async function openDownloadModalForGenre(genreName) {
|
||||||
// LISTENBRAINZ PLAYLISTS
|
// LISTENBRAINZ PLAYLISTS
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
||||||
async function loadListenBrainzCreatedFor() {
|
let listenbrainzPlaylistsCache = {}; // Store playlists by type
|
||||||
try {
|
let listenbrainzTracksCache = {}; // Store tracks for each playlist
|
||||||
const carousel = document.getElementById('listenbrainz-created-for-carousel');
|
let activeListenBrainzTab = 'recommendations'; // Track active tab
|
||||||
if (!carousel) return;
|
|
||||||
|
|
||||||
const response = await fetch('/api/discover/listenbrainz/created-for');
|
async function initializeListenBrainzTabs() {
|
||||||
if (!response.ok) {
|
try {
|
||||||
if (response.status === 401) {
|
console.log('🧠 Initializing ListenBrainz tabs...');
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>Please configure ListenBrainz token in Settings to view recommendations</p></div>';
|
|
||||||
return;
|
// Fetch all playlists types
|
||||||
|
const [createdForRes, userPlaylistsRes, collaborativeRes] = await Promise.all([
|
||||||
|
fetch('/api/discover/listenbrainz/created-for'),
|
||||||
|
fetch('/api/discover/listenbrainz/user-playlists'),
|
||||||
|
fetch('/api/discover/listenbrainz/collaborative')
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('📡 API Responses:', {
|
||||||
|
createdFor: createdForRes.status,
|
||||||
|
userPlaylists: userPlaylistsRes.status,
|
||||||
|
collaborative: collaborativeRes.status
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'recommendations', label: '🎁 Recommendations', hasData: false },
|
||||||
|
{ id: 'user', label: '📚 Your Playlists', hasData: false },
|
||||||
|
{ id: 'collaborative', label: '🤝 Collaborative', hasData: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check which tabs have data
|
||||||
|
if (createdForRes.ok) {
|
||||||
|
const data = await createdForRes.json();
|
||||||
|
console.log('📋 Created For data:', data);
|
||||||
|
if (data.success && data.playlists && data.playlists.length > 0) {
|
||||||
|
listenbrainzPlaylistsCache['recommendations'] = data.playlists;
|
||||||
|
tabs[0].hasData = true;
|
||||||
|
console.log(`✅ Found ${data.playlists.length} recommendation playlists`);
|
||||||
}
|
}
|
||||||
throw new Error('Failed to fetch ListenBrainz recommendations');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
if (userPlaylistsRes.ok) {
|
||||||
if (!data.success || !data.playlists || data.playlists.length === 0) {
|
const data = await userPlaylistsRes.json();
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>No ListenBrainz recommendations available yet</p></div>';
|
console.log('📚 User Playlists data:', data);
|
||||||
|
if (data.success && data.playlists && data.playlists.length > 0) {
|
||||||
|
listenbrainzPlaylistsCache['user'] = data.playlists;
|
||||||
|
tabs[1].hasData = true;
|
||||||
|
console.log(`✅ Found ${data.playlists.length} user playlists`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collaborativeRes.ok) {
|
||||||
|
const data = await collaborativeRes.json();
|
||||||
|
console.log('🤝 Collaborative data:', data);
|
||||||
|
if (data.success && data.playlists && data.playlists.length > 0) {
|
||||||
|
listenbrainzPlaylistsCache['collaborative'] = data.playlists;
|
||||||
|
tabs[2].hasData = true;
|
||||||
|
console.log(`✅ Found ${data.playlists.length} collaborative playlists`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tabs HTML
|
||||||
|
const tabsContainer = document.getElementById('listenbrainz-tabs');
|
||||||
|
console.log('🔧 Building tabs. Available tabs:', tabs.filter(t => t.hasData).map(t => t.label));
|
||||||
|
|
||||||
|
let tabsHtml = '<div class="decade-tabs-inner">'; // Reuse decade tabs styling
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
if (tab.hasData) {
|
||||||
|
const isActive = tab.id === activeListenBrainzTab;
|
||||||
|
tabsHtml += `
|
||||||
|
<button class="decade-tab${isActive ? ' active' : ''}"
|
||||||
|
onclick="switchListenBrainzTab('${tab.id}')"
|
||||||
|
data-tab="${tab.id}">
|
||||||
|
${tab.label}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tabsHtml += '</div>';
|
||||||
|
|
||||||
|
if (tabs.every(t => !t.hasData)) {
|
||||||
|
console.log('⚠️ No tabs have data');
|
||||||
|
tabsContainer.innerHTML = '<div class="discover-empty"><p>No ListenBrainz playlists available. Configure your token in Settings.</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build playlist cards
|
tabsContainer.innerHTML = tabsHtml;
|
||||||
let html = '';
|
|
||||||
data.playlists.forEach(playlist => {
|
|
||||||
// JSPF structure: playlist.playlist contains the actual data
|
|
||||||
const playlistData = playlist.playlist || playlist;
|
|
||||||
const identifier = playlistData.identifier?.split('/').pop() || '';
|
|
||||||
const title = playlistData.title || 'Untitled Playlist';
|
|
||||||
const creator = playlistData.creator || 'ListenBrainz';
|
|
||||||
|
|
||||||
// Track count - default to 50 if not available or 0
|
// Load first available tab
|
||||||
let trackCount = 50; // Default
|
const firstTab = tabs.find(t => t.hasData);
|
||||||
if (playlistData.annotation?.track_count && playlistData.annotation.track_count > 0) {
|
if (firstTab) {
|
||||||
trackCount = playlistData.annotation.track_count;
|
console.log(`🎯 Loading first tab: ${firstTab.label} (${firstTab.id})`);
|
||||||
} else if (playlistData.track && Array.isArray(playlistData.track) && playlistData.track.length > 0) {
|
activeListenBrainzTab = firstTab.id;
|
||||||
trackCount = playlistData.track.length;
|
loadListenBrainzTabContent(firstTab.id);
|
||||||
}
|
} else {
|
||||||
|
console.log('❌ No first tab found');
|
||||||
html += `
|
}
|
||||||
<div class="discover-card" onclick="openListenBrainzPlaylist('${identifier}', '${escapeHtml(title)}')">
|
|
||||||
<div class="discover-card-image listenbrainz-playlist-card">
|
|
||||||
<div class="listenbrainz-icon">🧠</div>
|
|
||||||
</div>
|
|
||||||
<div class="discover-card-info">
|
|
||||||
<h4 class="discover-card-title">${title}</h4>
|
|
||||||
<p class="discover-card-subtitle">by ${creator}</p>
|
|
||||||
<p class="discover-card-meta">${trackCount} tracks</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
carousel.innerHTML = html;
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading ListenBrainz created-for playlists:', error);
|
console.error('Error initializing ListenBrainz tabs:', error);
|
||||||
const carousel = document.getElementById('listenbrainz-created-for-carousel');
|
const tabsContainer = document.getElementById('listenbrainz-tabs');
|
||||||
if (carousel) {
|
if (tabsContainer) {
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>Failed to load recommendations</p></div>';
|
tabsContainer.innerHTML = '<div class="discover-empty"><p>Failed to load playlists</p></div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadListenBrainzUserPlaylists() {
|
function switchListenBrainzTab(tabId) {
|
||||||
try {
|
// Update active tab
|
||||||
const carousel = document.getElementById('listenbrainz-user-playlists-carousel');
|
activeListenBrainzTab = tabId;
|
||||||
if (!carousel) return;
|
|
||||||
|
|
||||||
const response = await fetch('/api/discover/listenbrainz/user-playlists');
|
// Update tab buttons
|
||||||
if (!response.ok) {
|
const tabs = document.querySelectorAll('#listenbrainz-tabs .decade-tab');
|
||||||
if (response.status === 401) {
|
tabs.forEach(tab => {
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>Please configure ListenBrainz token in Settings to view your playlists</p></div>';
|
if (tab.dataset.tab === tabId) {
|
||||||
return;
|
tab.classList.add('active');
|
||||||
}
|
} else {
|
||||||
throw new Error('Failed to fetch ListenBrainz user playlists');
|
tab.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load content
|
||||||
|
loadListenBrainzTabContent(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadListenBrainzTabContent(tabId) {
|
||||||
|
const container = document.getElementById('listenbrainz-tab-content');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const playlists = listenbrainzPlaylistsCache[tabId] || [];
|
||||||
|
if (playlists.length === 0) {
|
||||||
|
container.innerHTML = '<div class="discover-empty"><p>No playlists in this category</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build HTML for all playlists in this tab
|
||||||
|
let html = '';
|
||||||
|
playlists.forEach((playlist, index) => {
|
||||||
|
const playlistData = playlist.playlist || playlist;
|
||||||
|
const identifier = playlistData.identifier?.split('/').pop() || '';
|
||||||
|
console.log(`📋 Playlist ${index}:`, {
|
||||||
|
title: playlistData.title,
|
||||||
|
fullIdentifier: playlistData.identifier,
|
||||||
|
extractedIdentifier: identifier
|
||||||
|
});
|
||||||
|
const title = playlistData.title || 'Untitled Playlist';
|
||||||
|
const creator = playlistData.creator || 'ListenBrainz';
|
||||||
|
|
||||||
|
let trackCount = 50;
|
||||||
|
if (playlistData.annotation?.track_count && playlistData.annotation.track_count > 0) {
|
||||||
|
trackCount = playlistData.annotation.track_count;
|
||||||
|
} else if (playlistData.track && Array.isArray(playlistData.track) && playlistData.track.length > 0) {
|
||||||
|
trackCount = playlistData.track.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const playlistId = `lb-${tabId}-${index}`;
|
||||||
|
const virtualPlaylistId = `discover_lb_${tabId}_${identifier}`;
|
||||||
|
|
||||||
if (!data.success || !data.playlists || data.playlists.length === 0) {
|
html += `
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>No playlists found. <a href="https://listenbrainz.org/playlists/" target="_blank" style="color: #eb743b; text-decoration: underline;">Create your first playlist on ListenBrainz!</a></p></div>';
|
<div class="discover-section-subsection">
|
||||||
|
<div class="discover-section-header">
|
||||||
|
<div>
|
||||||
|
<h3 class="discover-section-subtitle-large">${title}</h3>
|
||||||
|
<p class="discover-section-meta" id="${playlistId}-meta">by ${creator} • Loading tracks...</p>
|
||||||
|
</div>
|
||||||
|
<div class="discover-section-actions">
|
||||||
|
<button class="action-button secondary"
|
||||||
|
onclick="openDownloadModalForListenBrainzPlaylist('${identifier}', '${escapeHtml(title)}')"
|
||||||
|
title="Download missing tracks">
|
||||||
|
<span class="button-icon">↓</span>
|
||||||
|
<span class="button-text">Download</span>
|
||||||
|
</button>
|
||||||
|
<button class="action-button primary"
|
||||||
|
id="${playlistId}-sync-btn"
|
||||||
|
onclick="startListenBrainzPlaylistSync('${identifier}', '${escapeHtml(title)}', '${playlistId}')"
|
||||||
|
title="Sync to media server">
|
||||||
|
<span class="button-icon">⟳</span>
|
||||||
|
<span class="button-text">Sync</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Sync Status Display -->
|
||||||
|
<div class="discover-sync-status" id="${playlistId}-sync-status" style="display: none;">
|
||||||
|
<div class="sync-status-content">
|
||||||
|
<div class="sync-status-label">
|
||||||
|
<span class="sync-icon">⟳</span>
|
||||||
|
<span>Syncing to media server...</span>
|
||||||
|
</div>
|
||||||
|
<div class="sync-status-stats">
|
||||||
|
<span class="sync-stat">♪ <span id="${playlistId}-sync-total">0</span></span>
|
||||||
|
<span class="sync-separator">/</span>
|
||||||
|
<span class="sync-stat">✓ <span id="${playlistId}-sync-matched">0</span></span>
|
||||||
|
<span class="sync-separator">/</span>
|
||||||
|
<span class="sync-stat">✗ <span id="${playlistId}-sync-failed">0</span></span>
|
||||||
|
<span class="sync-stat">(<span id="${playlistId}-sync-percentage">0</span>%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="discover-playlist-container compact" id="${playlistId}-playlist">
|
||||||
|
<div class="discover-loading"><div class="loading-spinner"></div><p>Loading tracks...</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Load tracks for all playlists in this tab
|
||||||
|
playlists.forEach((playlist, index) => {
|
||||||
|
const playlistData = playlist.playlist || playlist;
|
||||||
|
const identifier = playlistData.identifier?.split('/').pop() || '';
|
||||||
|
const playlistId = `lb-${tabId}-${index}`;
|
||||||
|
loadListenBrainzPlaylistTracks(identifier, playlistId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadListenBrainzPlaylistTracks(identifier, playlistId) {
|
||||||
|
try {
|
||||||
|
const playlistContainer = document.getElementById(`${playlistId}-playlist`);
|
||||||
|
if (!playlistContainer) return;
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (listenbrainzTracksCache[identifier]) {
|
||||||
|
displayListenBrainzTracks(listenbrainzTracksCache[identifier], playlistId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build playlist cards
|
console.log(`🔄 Fetching tracks for playlist: ${identifier}`);
|
||||||
let html = '';
|
const response = await fetch(`/api/discover/listenbrainz/playlist/${identifier}`);
|
||||||
data.playlists.forEach(playlist => {
|
console.log(`📡 Response status: ${response.status}`);
|
||||||
// JSPF structure: playlist.playlist contains the actual data
|
|
||||||
const playlistData = playlist.playlist || playlist;
|
|
||||||
const identifier = playlistData.identifier?.split('/').pop() || '';
|
|
||||||
const title = playlistData.title || 'Untitled Playlist';
|
|
||||||
const creator = playlistData.creator || 'You';
|
|
||||||
|
|
||||||
// Track count - default to 50 if not available or 0
|
if (!response.ok) {
|
||||||
let trackCount = 50; // Default
|
const errorText = await response.text();
|
||||||
if (playlistData.annotation?.track_count && playlistData.annotation.track_count > 0) {
|
console.error(`❌ Failed to fetch playlist: ${response.status} - ${errorText}`);
|
||||||
trackCount = playlistData.annotation.track_count;
|
throw new Error('Failed to fetch playlist tracks');
|
||||||
} else if (playlistData.track && Array.isArray(playlistData.track) && playlistData.track.length > 0) {
|
}
|
||||||
trackCount = playlistData.track.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPublic = playlistData.annotation?.public !== false;
|
const data = await response.json();
|
||||||
|
console.log(`📋 Received data:`, data);
|
||||||
|
console.log(`📊 Tracks count: ${data.tracks?.length || 0}`);
|
||||||
|
|
||||||
html += `
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||||
<div class="discover-card" onclick="openListenBrainzPlaylist('${identifier}', '${escapeHtml(title)}')">
|
playlistContainer.innerHTML = '<div class="discover-empty"><p>No tracks available</p></div>';
|
||||||
<div class="discover-card-image listenbrainz-playlist-card">
|
return;
|
||||||
<div class="listenbrainz-icon">${isPublic ? '📚' : '🔒'}</div>
|
}
|
||||||
</div>
|
|
||||||
<div class="discover-card-info">
|
|
||||||
<h4 class="discover-card-title">${title}</h4>
|
|
||||||
<p class="discover-card-subtitle">by ${creator}</p>
|
|
||||||
<p class="discover-card-meta">${trackCount} tracks • ${isPublic ? 'Public' : 'Private'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
carousel.innerHTML = html;
|
// Cache the tracks
|
||||||
|
listenbrainzTracksCache[identifier] = data.tracks;
|
||||||
|
|
||||||
|
// Display tracks
|
||||||
|
displayListenBrainzTracks(data.tracks, playlistId);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading ListenBrainz user playlists:', error);
|
console.error('Error loading ListenBrainz playlist tracks:', error);
|
||||||
const carousel = document.getElementById('listenbrainz-user-playlists-carousel');
|
const playlistContainer = document.getElementById(`${playlistId}-playlist`);
|
||||||
if (carousel) {
|
if (playlistContainer) {
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>Failed to load your playlists</p></div>';
|
playlistContainer.innerHTML = '<div class="discover-empty"><p>Failed to load tracks</p></div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadListenBrainzCollaborative() {
|
function displayListenBrainzTracks(tracks, playlistId) {
|
||||||
|
const playlistContainer = document.getElementById(`${playlistId}-playlist`);
|
||||||
|
if (!playlistContainer) return;
|
||||||
|
|
||||||
|
console.log(`🎨 Displaying ${tracks.length} tracks for ${playlistId}`);
|
||||||
|
if (tracks.length > 0) {
|
||||||
|
console.log('Sample track data:', tracks[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update track count in the metadata section
|
||||||
|
const metaElement = document.getElementById(`${playlistId}-meta`);
|
||||||
|
if (metaElement) {
|
||||||
|
// Extract creator from existing text (before the bullet)
|
||||||
|
const currentText = metaElement.textContent;
|
||||||
|
const creatorMatch = currentText.match(/by (.+?) •/);
|
||||||
|
const creator = creatorMatch ? creatorMatch[1] : 'ListenBrainz';
|
||||||
|
metaElement.textContent = `by ${creator} • ${tracks.length} track${tracks.length !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple SVG placeholder for missing album art
|
||||||
|
const placeholderImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9IiMzMzMiLz48cGF0aCBkPSJNMjAgMTBDMTguMzQzMSAxMCAxNyAxMS4zNDMxIDE3IDEzQzE3IDE0LjY1NjkgMTguMzQzMSAxNiAyMCAxNkMyMS42NTY5IDE2IDIzIDE0LjY1NjkgMjMgMTNDMjMgMTEuMzQzMSAyMS42NTY5IDEwIDIwIDEwWk0yNSAyMEgyNUMyNSAxOC44OTU0IDI0LjEwNDYgMTggMjMgMThIMTdDMTUuODk1NCAxOCAxNSAxOC44OTU0IDE1IDIwVjI4QzE1IDI5LjEwNDYgMTUuODk1NCAzMCAxNyAzMEgyM0MyNC4xMDQ2IDMwIDI1IDI5LjEwNDYgMjUgMjhWMjBaIiBmaWxsPSIjNjY2Ii8+PC9zdmc+';
|
||||||
|
|
||||||
|
let html = '<div class="discover-playlist-tracks-compact">';
|
||||||
|
tracks.forEach((track, index) => {
|
||||||
|
const coverUrl = track.album_cover_url || placeholderImage;
|
||||||
|
const durationMin = Math.floor(track.duration_ms / 60000);
|
||||||
|
const durationSec = Math.floor((track.duration_ms % 60000) / 1000);
|
||||||
|
const duration = `${durationMin}:${durationSec.toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
|
const albumName = escapeHtml(track.album_name || 'Unknown Album');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="discover-playlist-track-compact" data-track-index="${index}">
|
||||||
|
<div class="track-compact-number">${index + 1}</div>
|
||||||
|
<div class="track-compact-image">
|
||||||
|
<img src="${coverUrl}" alt="${albumName}" onerror="this.src='${placeholderImage}'">
|
||||||
|
</div>
|
||||||
|
<div class="track-compact-info">
|
||||||
|
<div class="track-compact-name">${escapeHtml(track.track_name || 'Unknown Track')}</div>
|
||||||
|
<div class="track-compact-artist">${escapeHtml(track.artist_name || 'Unknown Artist')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="track-compact-album">${albumName}</div>
|
||||||
|
<div class="track-compact-duration">${duration}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
playlistContainer.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startListenBrainzPlaylistSync(identifier, title, playlistId) {
|
||||||
try {
|
try {
|
||||||
const carousel = document.getElementById('listenbrainz-collaborative-carousel');
|
console.log(`🔄 Starting sync for ListenBrainz playlist:`, { identifier, title, playlistId });
|
||||||
if (!carousel) return;
|
|
||||||
|
|
||||||
const response = await fetch('/api/discover/listenbrainz/collaborative');
|
const tracks = listenbrainzTracksCache[identifier];
|
||||||
if (!response.ok) {
|
console.log(`📊 Cached tracks for ${identifier}:`, tracks?.length || 0);
|
||||||
if (response.status === 401) {
|
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>Please configure ListenBrainz token in Settings to view collaborative playlists</p></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('Failed to fetch ListenBrainz collaborative playlists');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
if (!tracks || tracks.length === 0) {
|
||||||
|
console.error('❌ No tracks found in cache');
|
||||||
if (!data.success || !data.playlists || data.playlists.length === 0) {
|
showToast('No tracks to sync', 'error');
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>You\'re not collaborating on any playlists yet. Join or create collaborative playlists on <a href="https://listenbrainz.org/playlists/" target="_blank" style="color: #eb743b; text-decoration: underline;">ListenBrainz</a>!</p></div>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build playlist cards
|
console.log(`✅ Found ${tracks.length} tracks to sync`);
|
||||||
let html = '';
|
|
||||||
data.playlists.forEach(playlist => {
|
|
||||||
// JSPF structure: playlist.playlist contains the actual data
|
|
||||||
const playlistData = playlist.playlist || playlist;
|
|
||||||
const identifier = playlistData.identifier?.split('/').pop() || '';
|
|
||||||
const title = playlistData.title || 'Untitled Playlist';
|
|
||||||
const creator = playlistData.creator || 'Unknown';
|
|
||||||
|
|
||||||
// Track count - default to 50 if not available or 0
|
// Show sync status
|
||||||
let trackCount = 50; // Default
|
const statusDisplay = document.getElementById(`${playlistId}-sync-status`);
|
||||||
if (playlistData.annotation?.track_count && playlistData.annotation.track_count > 0) {
|
const syncButton = document.getElementById(`${playlistId}-sync-btn`);
|
||||||
trackCount = playlistData.annotation.track_count;
|
|
||||||
} else if (playlistData.track && Array.isArray(playlistData.track) && playlistData.track.length > 0) {
|
|
||||||
trackCount = playlistData.track.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
console.log('UI Elements:', {
|
||||||
<div class="discover-card" onclick="openListenBrainzPlaylist('${identifier}', '${escapeHtml(title)}')">
|
statusDisplay: !!statusDisplay,
|
||||||
<div class="discover-card-image listenbrainz-playlist-card">
|
syncButton: !!syncButton
|
||||||
<div class="listenbrainz-icon">🤝</div>
|
|
||||||
</div>
|
|
||||||
<div class="discover-card-info">
|
|
||||||
<h4 class="discover-card-title">${title}</h4>
|
|
||||||
<p class="discover-card-subtitle">by ${creator}</p>
|
|
||||||
<p class="discover-card-meta">${trackCount} tracks • Collaborative</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
carousel.innerHTML = html;
|
if (statusDisplay) statusDisplay.style.display = 'block';
|
||||||
|
if (syncButton) syncButton.disabled = true;
|
||||||
|
|
||||||
|
// Prepare tracks for sync
|
||||||
|
const tracksForSync = tracks.map(track => ({
|
||||||
|
name: track.track_name,
|
||||||
|
artist: track.artist_name,
|
||||||
|
album: track.album_name,
|
||||||
|
mbid: track.mbid || null,
|
||||||
|
duration_ms: track.duration_ms || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
const virtualPlaylistId = `discover_lb_${identifier}`;
|
||||||
|
console.log(`🆔 Virtual playlist ID: ${virtualPlaylistId}`);
|
||||||
|
|
||||||
|
// Start sync
|
||||||
|
console.log('📤 Sending sync request...');
|
||||||
|
const response = await fetch('/api/sync/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
virtual_playlist_id: virtualPlaylistId,
|
||||||
|
playlist_name: title,
|
||||||
|
tracks: tracksForSync
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📡 Response status: ${response.status}`);
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('📋 Response data:', result);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to start sync');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Sync started successfully for ${title}`);
|
||||||
|
showToast(`Syncing "${title}" to media server`, 'success');
|
||||||
|
|
||||||
|
// Start polling for status
|
||||||
|
startListenBrainzSyncPolling(playlistId, virtualPlaylistId);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading ListenBrainz collaborative playlists:', error);
|
console.error('❌ Error starting ListenBrainz playlist sync:', error);
|
||||||
const carousel = document.getElementById('listenbrainz-collaborative-carousel');
|
showToast('Failed to start sync', 'error');
|
||||||
if (carousel) {
|
|
||||||
carousel.innerHTML = '<div class="discover-empty"><p>Failed to load collaborative playlists</p></div>';
|
const statusDisplay = document.getElementById(`${playlistId}-sync-status`);
|
||||||
|
const syncButton = document.getElementById(`${playlistId}-sync-btn`);
|
||||||
|
if (statusDisplay) statusDisplay.style.display = 'none';
|
||||||
|
if (syncButton) syncButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startListenBrainzSyncPolling(playlistId, virtualPlaylistId) {
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/sync/status/${virtualPlaylistId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
const totalEl = document.getElementById(`${playlistId}-sync-total`);
|
||||||
|
const matchedEl = document.getElementById(`${playlistId}-sync-matched`);
|
||||||
|
const failedEl = document.getElementById(`${playlistId}-sync-failed`);
|
||||||
|
const percentageEl = document.getElementById(`${playlistId}-sync-percentage`);
|
||||||
|
|
||||||
|
if (totalEl) totalEl.textContent = status.total_tracks || 0;
|
||||||
|
if (matchedEl) matchedEl.textContent = status.matched_tracks || 0;
|
||||||
|
if (failedEl) failedEl.textContent = status.failed_tracks || 0;
|
||||||
|
|
||||||
|
const percentage = status.total_tracks > 0
|
||||||
|
? Math.round(((status.matched_tracks || 0) / status.total_tracks) * 100)
|
||||||
|
: 0;
|
||||||
|
if (percentageEl) percentageEl.textContent = percentage;
|
||||||
|
|
||||||
|
// Check if complete
|
||||||
|
if (status.is_complete) {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
|
||||||
|
const statusDisplay = document.getElementById(`${playlistId}-sync-status`);
|
||||||
|
const syncButton = document.getElementById(`${playlistId}-sync-btn`);
|
||||||
|
|
||||||
|
if (statusDisplay) {
|
||||||
|
setTimeout(() => {
|
||||||
|
statusDisplay.style.display = 'none';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncButton) syncButton.disabled = false;
|
||||||
|
|
||||||
|
showToast(`Sync complete: ${status.matched_tracks}/${status.total_tracks} tracks matched`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error polling sync status:', error);
|
||||||
|
clearInterval(pollInterval);
|
||||||
}
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDownloadModalForListenBrainzPlaylist(identifier, title) {
|
||||||
|
try {
|
||||||
|
const tracks = listenbrainzTracksCache[identifier];
|
||||||
|
if (!tracks || tracks.length === 0) {
|
||||||
|
showToast('No tracks to download', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📥 Opening download modal for ListenBrainz playlist: ${title}`);
|
||||||
|
|
||||||
|
// Convert ListenBrainz tracks to Spotify-compatible format
|
||||||
|
const spotifyTracks = tracks.map(track => ({
|
||||||
|
id: null, // No Spotify ID for ListenBrainz tracks
|
||||||
|
name: track.track_name,
|
||||||
|
artists: [track.artist_name],
|
||||||
|
album: {
|
||||||
|
name: track.album_name,
|
||||||
|
images: track.album_cover_url ? [{ url: track.album_cover_url }] : []
|
||||||
|
},
|
||||||
|
duration_ms: track.duration_ms || 0,
|
||||||
|
mbid: track.mbid // Include MusicBrainz ID for matching
|
||||||
|
}));
|
||||||
|
|
||||||
|
const virtualPlaylistId = `discover_lb_${identifier}`;
|
||||||
|
|
||||||
|
// Open the download modal
|
||||||
|
await openDownloadMissingModalForYouTube(virtualPlaylistId, title, spotifyTracks);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error opening download modal for ListenBrainz playlist:', error);
|
||||||
|
showToast('Failed to open download modal', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17463,6 +17463,19 @@ body {
|
||||||
LISTENBRAINZ PLAYLIST CARDS
|
LISTENBRAINZ PLAYLIST CARDS
|
||||||
=============================== */
|
=============================== */
|
||||||
|
|
||||||
|
.listenbrainz-tabs {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listenbrainz-tab-content .discover-section-subsection {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listenbrainz-tab-content .discover-section-subsection:not(:first-child) {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
.listenbrainz-playlist-card {
|
.listenbrainz-playlist-card {
|
||||||
background: linear-gradient(135deg, #eb743b 0%, #d26230 100%);
|
background: linear-gradient(135deg, #eb743b 0%, #d26230 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue