diff --git a/core/spotify_client.py b/core/spotify_client.py index d3a63e5..6e3d496 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -61,9 +61,18 @@ class Track: popularity: int preview_url: Optional[str] = None external_urls: Optional[Dict[str, str]] = None - + image_url: Optional[str] = None + @classmethod def from_spotify_track(cls, track_data: Dict[str, Any]) -> 'Track': + # Extract album image (medium size preferred) + album_image_url = None + if 'album' in track_data and 'images' in track_data['album']: + images = track_data['album']['images'] + if images: + # Get medium size image (usually index 1), or largest if not available + album_image_url = images[1]['url'] if len(images) > 1 else images[0]['url'] + return cls( id=track_data['id'], name=track_data['name'], @@ -72,7 +81,8 @@ class Track: duration_ms=track_data['duration_ms'], popularity=track_data['popularity'], preview_url=track_data.get('preview_url'), - external_urls=track_data.get('external_urls') + external_urls=track_data.get('external_urls'), + image_url=album_image_url ) @dataclass diff --git a/web_server.py b/web_server.py index d1e354f..9f1c4e0 100644 --- a/web_server.py +++ b/web_server.py @@ -3224,6 +3224,102 @@ def search_music(): print(f"Search error: {e}") return jsonify({"error": str(e)}), 500 +@app.route('/api/enhanced-search', methods=['POST']) +def enhanced_search(): + """ + Unified search across Spotify and local database for enhanced search mode. + Returns categorized results: DB artists, Spotify artists, albums, and tracks. + """ + data = request.get_json() + query = data.get('query', '').strip() + + if not query: + return jsonify({ + "db_artists": [], + "spotify_artists": [], + "spotify_albums": [], + "spotify_tracks": [] + }) + + logger.info(f"Enhanced search initiated for: '{query}'") + + try: + # Search local database for artists + database = get_database() + db_artists_objs = database.search_artists(query, limit=5) + + # Convert database artists to dictionaries + db_artists = [] + for artist in db_artists_objs: + image_url = None + if hasattr(artist, 'thumb_url') and artist.thumb_url: + image_url = fix_artist_image_url(artist.thumb_url) + + db_artists.append({ + "id": artist.id, + "name": artist.name, + "image_url": image_url + }) + logger.debug(f"DB Artist: {artist.name}, thumb_url: {artist.thumb_url if hasattr(artist, 'thumb_url') else None}, fixed_url: {image_url}") + + # Search Spotify for artists, albums, tracks + spotify_artists = [] + spotify_albums = [] + spotify_tracks = [] + + if spotify_client and spotify_client.is_authenticated(): + # Search for artists + artist_objs = spotify_client.search_artists(query, limit=5) + for artist in artist_objs: + spotify_artists.append({ + "id": artist.id, + "name": artist.name, + "image_url": artist.image_url + }) + + # Search for albums + album_objs = spotify_client.search_albums(query, limit=10) + for album in album_objs: + # Album has 'artists' (list), convert to string + artist_name = ', '.join(album.artists) if album.artists else 'Unknown Artist' + + spotify_albums.append({ + "id": album.id, + "name": album.name, + "artist": artist_name, + "image_url": album.image_url, + "release_date": album.release_date, + "total_tracks": album.total_tracks + }) + + # Search for tracks + track_objs = spotify_client.search_tracks(query, limit=10) + for track in track_objs: + # Track has 'artists' (list), convert to string + artist_name = ', '.join(track.artists) if track.artists else 'Unknown Artist' + + spotify_tracks.append({ + "id": track.id, + "name": track.name, + "artist": artist_name, + "album": track.album, + "duration_ms": track.duration_ms, + "image_url": track.image_url + }) + + logger.info(f"Enhanced search results: {len(db_artists)} DB artists, {len(spotify_artists)} Spotify artists, {len(spotify_albums)} albums, {len(spotify_tracks)} tracks") + + return jsonify({ + "db_artists": db_artists, + "spotify_artists": spotify_artists, + "spotify_albums": spotify_albums, + "spotify_tracks": spotify_tracks + }) + + except Exception as e: + logger.error(f"Enhanced search error: {e}") + return jsonify({"error": str(e)}), 500 + @app.route('/api/download', methods=['POST']) def start_download(): """Simple download route""" diff --git a/webui/index.html b/webui/index.html index f77b35c..9c599f3 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1273,36 +1273,94 @@
- -
-
-
- - -
- -
+ +
+
+
+
+ + +
+ +
+ + + - -
-
-

Enhanced Results

-
0 results
+ +
+
+

Search Results

-
-
-
-

Enhanced Search Ready

-

Intelligent filtering • Smart ranking • Better results

+
+
+

Search results will appear here when you select an album or track.

diff --git a/webui/static/script.js b/webui/static/script.js index 1f9bf1c..03d0271 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -2373,31 +2373,419 @@ function initializeSearchModeToggle() { }); }); - // Initialize enhanced search input handlers + // Initialize enhanced search const enhancedInput = document.getElementById('enhanced-search-input'); const enhancedSearchBtn = document.getElementById('enhanced-search-btn'); const enhancedCancelBtn = document.getElementById('enhanced-cancel-btn'); + const enhancedDropdown = document.getElementById('enhanced-dropdown'); + const loadingState = document.getElementById('enhanced-loading'); + const emptyState = document.getElementById('enhanced-empty'); + const resultsContainer = document.getElementById('enhanced-results-container'); - if (enhancedSearchBtn && enhancedInput) { - enhancedSearchBtn.addEventListener('click', () => { - console.log('Enhanced search clicked - functionality to be implemented'); - showToast('Enhanced search coming soon!', 'info'); + let debounceTimer = null; + let abortController = null; + + // Live search with debouncing + if (enhancedInput) { + enhancedInput.addEventListener('input', (e) => { + const query = e.target.value.trim(); + + // Show/hide cancel button + if (enhancedCancelBtn) { + enhancedCancelBtn.classList.toggle('hidden', query.length === 0); + } + + // Clear debounce timer + clearTimeout(debounceTimer); + + // Hide dropdown if query too short + if (query.length < 2) { + hideDropdown(); + return; + } + + // Debounce search + debounceTimer = setTimeout(() => { + performEnhancedSearch(query); + }, 300); }); enhancedInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { - console.log('Enhanced search Enter pressed - functionality to be implemented'); - showToast('Enhanced search coming soon!', 'info'); + const query = e.target.value.trim(); + if (query.length >= 2) { + clearTimeout(debounceTimer); + performEnhancedSearch(query); + } + } + }); + } + + if (enhancedSearchBtn) { + enhancedSearchBtn.addEventListener('click', () => { + const query = enhancedInput.value.trim(); + if (query.length >= 2) { + performEnhancedSearch(query); + } else { + showToast('Please enter at least 2 characters', 'error'); } }); } if (enhancedCancelBtn) { enhancedCancelBtn.addEventListener('click', () => { - console.log('Enhanced search cancelled'); - // Cancel logic will be added when functionality is implemented + enhancedInput.value = ''; + enhancedCancelBtn.classList.add('hidden'); + hideDropdown(); + }); + } + + // Close dropdown when clicking outside + document.addEventListener('click', (e) => { + if (enhancedDropdown && !enhancedDropdown.classList.contains('hidden')) { + const isClickInside = e.target.closest('.enhanced-search-input-wrapper'); + if (!isClickInside) { + hideDropdown(); + } + } + }); + + async function performEnhancedSearch(query) { + console.log('Enhanced search:', query); + + // Show loading state + showDropdown(); + loadingState.classList.remove('hidden'); + emptyState.classList.add('hidden'); + resultsContainer.classList.add('hidden'); + + // Abort previous request + if (abortController) { + abortController.abort(); + } + abortController = new AbortController(); + + try { + const response = await fetch('/api/enhanced-search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + signal: abortController.signal + }); + + if (!response.ok) throw new Error('Search failed'); + + const data = await response.json(); + console.log('Enhanced results:', data); + + // Calculate total + const total = (data.db_artists?.length || 0) + + (data.spotify_artists?.length || 0) + + (data.spotify_albums?.length || 0) + + (data.spotify_tracks?.length || 0); + + // Hide loading + loadingState.classList.add('hidden'); + + if (total === 0) { + emptyState.classList.remove('hidden'); + } else { + renderDropdownResults(data); + resultsContainer.classList.remove('hidden'); + } + + } catch (error) { + if (error.name !== 'AbortError') { + console.error('Enhanced search error:', error); + loadingState.classList.add('hidden'); + emptyState.classList.remove('hidden'); + } + } + } + + function renderDropdownResults(data) { + // Render DB Artists + renderCompactSection( + 'enh-db-artists-section', + 'enh-db-artists-list', + 'enh-db-artists-count', + data.db_artists || [], + (artist) => ({ + image: artist.image_url, + placeholder: '📚', + name: artist.name, + meta: 'In Your Library', + badge: { text: 'Library', class: 'enh-badge-library' }, + onClick: () => { + hideDropdown(); + navigateToPage('library'); + } + }) + ); + + // Render Spotify Artists + renderCompactSection( + 'enh-spotify-artists-section', + 'enh-spotify-artists-list', + 'enh-spotify-artists-count', + data.spotify_artists || [], + (artist) => ({ + image: artist.image_url, + placeholder: '🎤', + name: artist.name, + meta: 'Artist', + badge: { text: 'Spotify', class: 'enh-badge-spotify' }, + onClick: () => { + hideDropdown(); + navigateToPage('artists'); + setTimeout(() => { + const input = document.getElementById('artist-search-input'); + if (input) { + input.value = artist.name; + input.dispatchEvent(new Event('input', { bubbles: true })); + } + }, 100); + } + }) + ); + + // Render Albums + renderCompactSection( + 'enh-albums-section', + 'enh-albums-list', + 'enh-albums-count', + data.spotify_albums || [], + (album) => ({ + image: album.image_url, + placeholder: '💿', + name: album.name, + meta: `${album.artist} • ${album.release_date ? album.release_date.substring(0, 4) : 'N/A'}`, + onClick: () => searchSlskdFor('album', album) + }) + ); + + // Render Tracks + renderCompactSection( + 'enh-tracks-section', + 'enh-tracks-list', + 'enh-tracks-count', + data.spotify_tracks || [], + (track) => ({ + image: track.image_url, + placeholder: '🎵', + name: track.name, + meta: `${track.artist} • ${track.album}`, + onClick: () => searchSlskdFor('track', track) + }) + ); + } + + function renderCompactSection(sectionId, listId, countId, items, mapItem) { + const section = document.getElementById(sectionId); + const list = document.getElementById(listId); + const count = document.getElementById(countId); + + if (!list) return; + + list.innerHTML = ''; + + if (!items || items.length === 0) { + section.classList.add('hidden'); + return; + } + + section.classList.remove('hidden'); + count.textContent = items.length; + + // Determine type based on section ID + const isArtist = sectionId.includes('artists'); + const isAlbum = sectionId.includes('albums'); + const isTrack = sectionId.includes('tracks'); + + // Add appropriate grid class to list + if (isArtist) { + list.classList.add('artists-grid'); + } else if (isAlbum) { + list.classList.add('albums-grid'); + } else if (isTrack) { + list.classList.add('tracks-list'); + } + + items.forEach(item => { + const config = mapItem(item); + const elem = document.createElement('div'); + + // Add appropriate card class + if (isArtist) { + elem.className = 'enh-compact-item artist-card'; + } else if (isAlbum) { + elem.className = 'enh-compact-item album-card'; + } else if (isTrack) { + elem.className = 'enh-compact-item track-item'; + } + + // Build image HTML with type-specific classes + let imageClass = 'enh-item-image'; + let placeholderClass = 'enh-item-image-placeholder'; + + if (isArtist) { + imageClass += ' artist-image'; + placeholderClass += ' artist-placeholder'; + } else if (isAlbum) { + imageClass += ' album-cover'; + placeholderClass += ' album-placeholder'; + } else if (isTrack) { + imageClass += ' track-cover'; + placeholderClass += ' track-placeholder'; + } + + const imageHtml = config.image + ? `${escapeHtml(config.name)}` + : `
${config.placeholder}
`; + + const badgeHtml = config.badge + ? `
${config.badge.text}
` + : ''; + + elem.innerHTML = ` + ${imageHtml} +
+
${escapeHtml(config.name)}
+
${escapeHtml(config.meta)}
+
+ ${badgeHtml} + `; + + elem.addEventListener('click', config.onClick); + list.appendChild(elem); }); } + + async function searchSlskdFor(type, item) { + const mainResultsArea = document.getElementById('enhanced-main-results-area'); + if (!mainResultsArea) return; + + // Show loading in main results area + mainResultsArea.innerHTML = ` +
+
+

Searching for ${type === 'album' ? 'album' : 'track'}...

+
+ `; + + const query = `${item.artist} ${item.name}`; + + try { + const response = await fetch('/api/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }) + }); + + const data = await response.json(); + + if (data.error) { + showToast(`Search error: ${data.error}`, 'error'); + return; + } + + // Filter results + const filtered = data.results.filter(r => r.result_type === type); + + // Render slskd results in main area + renderSlskdInMainArea(filtered, type, item); + + } catch (error) { + console.error('Slskd search error:', error); + showToast('Search failed', 'error'); + mainResultsArea.innerHTML = '

Search failed. Please try again.

'; + } + } + + function renderSlskdInMainArea(results, type, originalItem) { + const mainResultsArea = document.getElementById('enhanced-main-results-area'); + if (!mainResultsArea) return; + + if (!results || results.length === 0) { + mainResultsArea.innerHTML = '

No matches found for this ' + type + '.

'; + return; + } + + // Render results using same style as basic search + mainResultsArea.innerHTML = results.map(result => { + const title = type === 'album' + ? `${result.album_title} (${result.tracks ? result.tracks.length : 0} tracks)` + : result.title; + + return ` +
+
+

${escapeHtml(title)}

+ +
+
+ ${result.bitrate ? `${result.bitrate} kbps` : ''} + ${result.format ? `${result.format.toUpperCase()}` : ''} + ${result.size ? `${(result.size / 1024 / 1024).toFixed(1)} MB` : ''} + ${result.username ? `👤 ${escapeHtml(result.username)}` : ''} +
+
+ `; + }).join(''); + + // Attach download handlers + mainResultsArea.querySelectorAll('.download-result-btn').forEach(btn => { + btn.addEventListener('click', async function() { + const result = JSON.parse(this.dataset.result); + const type = this.dataset.type; + + this.disabled = true; + this.textContent = 'Downloading...'; + + try { + const downloadData = type === 'album' + ? { result_type: 'album', tracks: result.tracks || [] } + : { result_type: 'track', username: result.username, filename: result.filename, size: result.size }; + + const response = await fetch('/api/download', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(downloadData) + }); + + const data = await response.json(); + + if (data.error) { + showToast(`Download error: ${data.error}`, 'error'); + this.disabled = false; + this.innerHTML = '💾 Download'; + } else { + showToast('Download started!', 'success'); + this.innerHTML = '✅ Added'; + } + } catch (error) { + console.error('Download error:', error); + showToast('Download failed', 'error'); + this.disabled = false; + this.innerHTML = '💾 Download'; + } + }); + }); + } + + function showDropdown() { + if (enhancedDropdown) { + enhancedDropdown.classList.remove('hidden'); + } + } + + function hideDropdown() { + if (enhancedDropdown) { + enhancedDropdown.classList.add('hidden'); + } + } } async function performSearch() { diff --git a/webui/static/style.css b/webui/static/style.css index fde88b5..249db82 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -18885,6 +18885,12 @@ body { /* ENHANCED SEARCH STYLING */ /* ========================================= */ +/* Enhanced Search Input Wrapper (relative for dropdown positioning) */ +.enhanced-search-input-wrapper { + position: relative; + margin-bottom: 20px; +} + /* Enhanced Search Bar */ .enhanced-search-bar-container { background: linear-gradient(135deg, rgba(138, 43, 226, 0.15), rgba(75, 0, 130, 0.15)); @@ -19110,3 +19116,784 @@ body { margin: 0; } +/* ========================================= */ +/* ENHANCED SEARCH DROPDOWN OVERLAY */ +/* ========================================= */ + +.enhanced-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 8px; + background: rgba(24, 24, 24, 0.98); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6); + max-height: 600px; + overflow: hidden; + z-index: 1000; + backdrop-filter: blur(40px); + animation: slideDown 0.15s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.enhanced-dropdown-content { + max-height: 600px; + overflow-y: auto; + padding: 16px 20px; +} + +.enhanced-dropdown-content::-webkit-scrollbar { + width: 6px; +} + +.enhanced-dropdown-content::-webkit-scrollbar-track { + background: transparent; +} + +.enhanced-dropdown-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.enhanced-dropdown-content::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +/* Loading and Empty States */ +.enhanced-loading, +.enhanced-empty { + text-align: center; + padding: 40px 20px; + color: rgba(255, 255, 255, 0.7); +} + +.enhanced-loading .spinner { + width: 40px; + height: 40px; + margin: 0 auto 16px; + border: 3px solid rgba(138, 43, 226, 0.2); + border-top-color: rgba(138, 43, 226, 0.8); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.empty-icon { + font-size: 48px; + margin-bottom: 12px; + opacity: 0.5; +} + +/* Dropdown Sections */ +.enh-dropdown-section { + margin-bottom: 24px; +} + +.enh-dropdown-section:last-child { + margin-bottom: 0; +} + +.enh-section-header { + display: flex; + align-items: center; + gap: 0; + padding: 0; + margin-bottom: 12px; +} + +.enh-section-icon { + display: none; +} + +.enh-section-title { + flex-grow: 1; + font-size: 16px; + font-weight: 700; + color: #ffffff; + margin: 0; + text-transform: none; + letter-spacing: -0.2px; +} + +.enh-section-count { + display: none; +} + +/* ========================================= */ +/* ARTIST CARDS - Clean Spotify Style */ +/* ========================================= */ + +.enh-artists-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 24px; +} + +.enh-artist-section { + margin-bottom: 0 !important; +} + +.enh-compact-list.artists-grid { + display: flex; + gap: 16px; + flex-wrap: nowrap; + overflow-x: auto; + padding-bottom: 4px; +} + +.enh-compact-list.artists-grid::-webkit-scrollbar { + height: 4px; +} + +.enh-compact-list.artists-grid::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; +} + +.enh-compact-item.artist-card { + display: flex; + flex-direction: column; + position: relative; + width: 170px; + height: 170px; + background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid rgba(255, 255, 255, 0.1); + flex-shrink: 0; + overflow: hidden; +} + +.enh-compact-item.artist-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4); + border-color: rgba(255, 255, 255, 0.2); +} + +.enh-item-image.artist-image { + width: 100%; + height: 110px; + object-fit: cover; + border-radius: 8px 8px 0 0; + border: none; +} + +.enh-item-image-placeholder.artist-placeholder { + width: 100%; + height: 110px; + background: rgba(40, 40, 40, 1); + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; + border: none; + border-radius: 8px 8px 0 0; +} + +.enh-compact-item.artist-card .enh-item-info { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 10px 12px; + background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.6) 70%, transparent 100%); +} + +.enh-compact-item.artist-card .enh-item-name { + font-size: 14px; + font-weight: 600; + color: #ffffff; + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +.enh-compact-item.artist-card .enh-item-meta { + display: block; + font-size: 12px; + font-weight: 500; + color: rgba(255, 255, 255, 0.7); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-compact-item.artist-card .enh-item-badge { + display: none; +} + + +/* ========================================= */ +/* ALBUM CARDS - Clean Grid */ +/* ========================================= */ + +.enh-compact-list.albums-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); + gap: 16px; +} + +.enh-compact-item.album-card { + display: flex; + flex-direction: column; + padding: 12px; + background: transparent; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s ease; + border: none; +} + +.enh-compact-item.album-card:hover { + background: rgba(255, 255, 255, 0.1); +} + +.enh-item-image.album-cover { + width: 100%; + aspect-ratio: 1; + border-radius: 4px; + object-fit: cover; + margin-bottom: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.enh-item-image-placeholder.album-placeholder { + width: 100%; + aspect-ratio: 1; + border-radius: 4px; + background: rgba(40, 40, 40, 1); + display: flex; + align-items: center; + justify-content: center; + font-size: 56px; + margin-bottom: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.enh-compact-item.album-card .enh-item-name { + font-size: 14px; + font-weight: 600; + color: #ffffff; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 1.3; + max-height: 2.6em; +} + +.enh-compact-item.album-card .enh-item-meta { + font-size: 13px; + color: rgba(255, 255, 255, 0.6); + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 1.3; +} + +/* ========================================= */ +/* TRACK ITEMS - Two Column Grid */ +/* ========================================= */ + +.enh-compact-list.tracks-list { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 12px; +} + +.enh-compact-item.track-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background: transparent; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s ease; + border: none; +} + +.enh-compact-item.track-item:hover { + background: rgba(255, 255, 255, 0.1); +} + +.enh-item-image.track-cover { + width: 48px; + height: 48px; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.enh-item-image-placeholder.track-placeholder { + width: 48px; + height: 48px; + border-radius: 4px; + flex-shrink: 0; + background: rgba(40, 40, 40, 1); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.enh-item-info { + flex-grow: 1; + min-width: 0; +} + +.enh-compact-item.track-item .enh-item-name { + font-size: 14px; + font-weight: 500; + color: #ffffff; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-compact-item.track-item .enh-item-meta { + font-size: 13px; + color: rgba(255, 255, 255, 0.6); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ========================================= */ +/* ENHANCED SEARCH CATEGORIZED RESULTS */ +/* (OLD - REMOVE IF NOT NEEDED) */ +/* ========================================= */ + +/* Categorized Container */ +.enh-sr-categorized-container { + display: flex; + flex-direction: column; + gap: 24px; +} + +/* Category Section */ +.enh-sr-category-section { + background: rgba(30, 30, 30, 0.5); + border-radius: 12px; + padding: 20px; + border: 1px solid rgba(138, 43, 226, 0.2); +} + +.enh-sr-category-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(138, 43, 226, 0.2); +} + +.enh-sr-category-icon { + font-size: 20px; +} + +.enh-sr-category-title { + flex-grow: 1; + font-size: 16px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin: 0; +} + +.enh-sr-category-count { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.4), rgba(123, 31, 162, 0.4)); + border: 1px solid rgba(138, 43, 226, 0.5); + border-radius: 12px; + padding: 4px 10px; + font-size: 12px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); +} + +/* Artist Grid (for both DB and Spotify artists) */ +.enh-sr-artist-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 16px; +} + +.enh-sr-artist-card { + background: linear-gradient(135deg, rgba(40, 40, 40, 0.8), rgba(30, 30, 30, 0.8)); + border-radius: 12px; + padding: 16px; + cursor: pointer; + transition: all 0.3s ease; + border: 2px solid transparent; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.enh-sr-artist-card:hover { + transform: translateY(-4px); + border-color: rgba(138, 43, 226, 0.6); + box-shadow: 0 8px 24px rgba(138, 43, 226, 0.3); +} + +.enh-sr-artist-image-container { + width: 80px; + height: 80px; + border-radius: 50%; + overflow: hidden; + margin-bottom: 12px; + border: 3px solid rgba(138, 43, 226, 0.3); +} + +.enh-sr-artist-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.enh-sr-artist-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(138, 43, 226, 0.4), rgba(123, 31, 162, 0.4)); + font-size: 36px; +} + +.enh-sr-artist-name { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin-bottom: 6px; + word-wrap: break-word; +} + +.enh-sr-artist-badge { + font-size: 11px; + padding: 4px 8px; + border-radius: 8px; + font-weight: 600; +} + +.enh-sr-artist-badge-library { + background: linear-gradient(135deg, rgba(29, 185, 84, 0.3), rgba(24, 156, 71, 0.3)); + border: 1px solid rgba(29, 185, 84, 0.5); + color: rgba(29, 185, 84, 1); +} + +.enh-sr-artist-badge-spotify { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.3), rgba(123, 31, 162, 0.3)); + border: 1px solid rgba(138, 43, 226, 0.5); + color: rgba(138, 43, 226, 1); +} + +/* Album Grid */ +.enh-sr-album-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 16px; +} + +.enh-sr-album-card { + background: linear-gradient(135deg, rgba(40, 40, 40, 0.8), rgba(30, 30, 30, 0.8)); + border-radius: 12px; + padding: 12px; + cursor: pointer; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.enh-sr-album-card:hover { + transform: translateY(-4px); + border-color: rgba(138, 43, 226, 0.6); + box-shadow: 0 8px 24px rgba(138, 43, 226, 0.3); +} + +.enh-sr-album-cover { + width: 100%; + aspect-ratio: 1; + border-radius: 8px; + overflow: hidden; + margin-bottom: 12px; + background: linear-gradient(135deg, rgba(138, 43, 226, 0.3), rgba(123, 31, 162, 0.3)); +} + +.enh-sr-album-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.enh-sr-album-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 48px; +} + +.enh-sr-album-name { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-sr-album-artist { + font-size: 12px; + color: rgba(255, 255, 255, 0.6); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 6px; +} + +.enh-sr-album-meta { + font-size: 11px; + color: rgba(255, 255, 255, 0.5); + display: flex; + justify-content: space-between; +} + +/* Track List */ +.enh-sr-track-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.enh-sr-track-item { + background: linear-gradient(135deg, rgba(40, 40, 40, 0.6), rgba(30, 30, 30, 0.6)); + border-radius: 10px; + padding: 12px 16px; + cursor: pointer; + transition: all 0.2s ease; + border: 2px solid transparent; + display: flex; + align-items: center; + gap: 12px; +} + +.enh-sr-track-item:hover { + border-color: rgba(138, 43, 226, 0.6); + background: linear-gradient(135deg, rgba(50, 50, 50, 0.8), rgba(40, 40, 40, 0.8)); + transform: translateX(4px); +} + +.enh-sr-track-cover { + width: 48px; + height: 48px; + border-radius: 6px; + overflow: hidden; + flex-shrink: 0; + background: linear-gradient(135deg, rgba(138, 43, 226, 0.3), rgba(123, 31, 162, 0.3)); +} + +.enh-sr-track-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.enh-sr-track-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; +} + +.enh-sr-track-info { + flex-grow: 1; + min-width: 0; +} + +.enh-sr-track-name { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-sr-track-meta { + font-size: 12px; + color: rgba(255, 255, 255, 0.6); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-sr-track-duration { + font-size: 12px; + color: rgba(255, 255, 255, 0.5); + flex-shrink: 0; + margin-left: 12px; +} + +/* ========================================= */ +/* SLSKD SEARCH RESULTS VIEW */ +/* ========================================= */ + +.enh-sr-slskd-view { + display: flex; + flex-direction: column; + gap: 16px; +} + +.enh-sr-back-btn { + background: rgba(40, 40, 40, 0.8); + border: 1px solid rgba(138, 43, 226, 0.4); + border-radius: 10px; + padding: 10px 20px; + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; + align-self: flex-start; +} + +.enh-sr-back-btn:hover { + background: rgba(50, 50, 50, 0.9); + border-color: rgba(138, 43, 226, 0.6); + transform: translateX(-4px); +} + +.enh-sr-back-icon { + font-size: 16px; +} + +.enh-sr-slskd-header { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.15), rgba(75, 0, 130, 0.15)); + border-radius: 12px; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid rgba(138, 43, 226, 0.3); +} + +.enh-sr-slskd-title { + font-size: 16px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin: 0; +} + +.enh-sr-slskd-badge { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.4), rgba(123, 31, 162, 0.4)); + border: 1px solid rgba(138, 43, 226, 0.5); + border-radius: 12px; + padding: 6px 12px; + font-size: 12px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); +} + +.enh-sr-slskd-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Slskd Result Item (reuses download manager card styles with enh-sr prefix) */ +.enh-sr-download-card { + background: linear-gradient(135deg, rgba(40, 40, 40, 0.8), rgba(30, 30, 30, 0.8)); + border-radius: 12px; + padding: 16px; + border: 2px solid rgba(138, 43, 226, 0.2); + transition: all 0.3s ease; +} + +.enh-sr-download-card:hover { + border-color: rgba(138, 43, 226, 0.5); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(138, 43, 226, 0.2); +} + +.enh-sr-download-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.enh-sr-download-title { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin: 0; + flex-grow: 1; +} + +.enh-sr-download-btn { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.8), rgba(123, 31, 162, 0.8)); + border: none; + border-radius: 8px; + padding: 8px 16px; + color: #ffffff; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + margin-left: 12px; +} + +.enh-sr-download-btn:hover { + background: linear-gradient(135deg, rgba(138, 43, 226, 1), rgba(123, 31, 162, 1)); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(138, 43, 226, 0.4); +} + +.enh-sr-download-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + font-size: 11px; + color: rgba(255, 255, 255, 0.6); +} + +.enh-sr-meta-badge { + background: rgba(50, 50, 50, 0.6); + border-radius: 6px; + padding: 4px 8px; +} +