// main.js import { downloadQueue } from './queue.js'; document.addEventListener('DOMContentLoaded', function() { // DOM elements const searchInput = document.getElementById('searchInput'); const searchButton = document.getElementById('searchButton'); const searchType = document.getElementById('searchType'); const resultsContainer = document.getElementById('resultsContainer'); const queueIcon = document.getElementById('queueIcon'); const emptyState = document.getElementById('emptyState'); const loadingResults = document.getElementById('loadingResults'); // Initialize the queue if (queueIcon) { queueIcon.addEventListener('click', () => { downloadQueue.toggleVisibility(); }); } // Add event listeners if (searchButton) { searchButton.addEventListener('click', performSearch); } if (searchInput) { searchInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { performSearch(); } }); // Auto-detect and handle pasted Spotify URLs searchInput.addEventListener('input', function(e) { const inputVal = e.target.value.trim(); if (isSpotifyUrl(inputVal)) { const details = getSpotifyResourceDetails(inputVal); if (details) { searchType.value = details.type; } } }); } // Restore last search type if no URL override const savedType = localStorage.getItem('lastSearchType'); if (savedType && ['track','album','playlist','artist'].includes(savedType)) { searchType.value = savedType; } // Save last selection on change if (searchType) { searchType.addEventListener('change', () => { localStorage.setItem('lastSearchType', searchType.value); }); } // Check for URL parameters const urlParams = new URLSearchParams(window.location.search); const query = urlParams.get('q'); const type = urlParams.get('type'); if (query) { searchInput.value = query; if (type && ['track', 'album', 'playlist', 'artist'].includes(type)) { searchType.value = type; } performSearch(); } else { // Show empty state if no query showEmptyState(true); } /** * Performs the search based on input values */ async function performSearch() { const query = searchInput.value.trim(); if (!query) return; // Handle direct Spotify URLs if (isSpotifyUrl(query)) { const details = getSpotifyResourceDetails(query); if (details && details.id) { // Redirect to the appropriate page window.location.href = `/${details.type}/${details.id}`; return; } } // Update URL without reloading page const newUrl = `${window.location.pathname}?q=${encodeURIComponent(query)}&type=${searchType.value}`; window.history.pushState({ path: newUrl }, '', newUrl); // Show loading state showEmptyState(false); showLoading(true); resultsContainer.innerHTML = ''; try { const url = `/api/search?q=${encodeURIComponent(query)}&search_type=${searchType.value}&limit=40`; const response = await fetch(url); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); // Hide loading indicator showLoading(false); // Render results if (data && data.items && data.items.length > 0) { resultsContainer.innerHTML = ''; // Filter out items with null/undefined essential display parameters const validItems = filterValidItems(data.items, searchType.value); if (validItems.length === 0) { // No valid items found after filtering resultsContainer.innerHTML = `

No valid results found for "${query}"

`; return; } validItems.forEach((item, index) => { const cardElement = createResultCard(item, searchType.value, index); // Store the item data directly on the button element const downloadBtn = cardElement.querySelector('.download-btn'); if (downloadBtn) { downloadBtn.dataset.itemIndex = index; } resultsContainer.appendChild(cardElement); }); // Attach download handlers to the newly created cards attachDownloadListeners(validItems); } else { // No results found resultsContainer.innerHTML = `

No results found for "${query}"

`; } } catch (error) { console.error('Error:', error); showLoading(false); resultsContainer.innerHTML = `

Error searching: ${error.message}

`; } } /** * Filters out items with null/undefined essential display parameters based on search type */ function filterValidItems(items, type) { if (!items) return []; return items.filter(item => { // Skip null/undefined items if (!item) return false; // Skip explicit content if filter is enabled if (downloadQueue.isExplicitFilterEnabled() && item.explicit === true) { return false; } // Check essential parameters based on search type switch (type) { case 'track': // For tracks, we need name, artists, and album return ( item.name && item.artists && item.artists.length > 0 && item.artists[0] && item.artists[0].name && item.album && item.album.name && item.external_urls && item.external_urls.spotify ); case 'album': // For albums, we need name, artists, and cover image return ( item.name && item.artists && item.artists.length > 0 && item.artists[0] && item.artists[0].name && item.external_urls && item.external_urls.spotify ); case 'playlist': // For playlists, we need name, owner, and tracks return ( item.name && item.owner && item.owner.display_name && item.tracks && item.external_urls && item.external_urls.spotify ); case 'artist': // For artists, we need name return ( item.name && item.external_urls && item.external_urls.spotify ); default: // Default case - just check if the item exists return true; } }); } /** * Attaches download handlers to result cards */ function attachDownloadListeners(items) { document.querySelectorAll('.download-btn').forEach((btn) => { btn.addEventListener('click', (e) => { e.stopPropagation(); // Get the item index from the button's dataset const itemIndex = parseInt(btn.dataset.itemIndex, 10); // Get the corresponding item const item = items[itemIndex]; if (!item) return; const type = searchType.value; let url; // Determine the URL based on item type if (item.external_urls && item.external_urls.spotify) { url = item.external_urls.spotify; } else if (item.href) { url = item.href; } else { showError('Could not determine download URL'); return; } // Prepare metadata for the download const metadata = { name: item.name || 'Unknown', artist: item.artists ? item.artists[0]?.name : undefined }; // Disable the button and update text btn.disabled = true; // For artist downloads, show a different message since it will queue multiple albums if (type === 'artist') { btn.innerHTML = 'Queueing albums...'; } else { btn.innerHTML = 'Queueing...'; } // Start the download startDownload(url, type, metadata, item.album ? item.album.album_type : null) .then(() => { // For artists, show how many albums were queued if (type === 'artist') { btn.innerHTML = 'Albums queued!'; // Open the queue automatically for artist downloads downloadQueue.toggleVisibility(true); } else { btn.innerHTML = 'Queued!'; } }) .catch((error) => { btn.disabled = false; btn.innerHTML = 'Download'; showError('Failed to queue download: ' + error.message); }); }); }); } /** * Starts the download process via API */ async function startDownload(url, type, item, albumType) { if (!url || !type) { showError('Missing URL or type for download'); return; } try { // Use the centralized downloadQueue.download method await downloadQueue.download(url, type, item, albumType); // Make the queue visible after queueing downloadQueue.toggleVisibility(true); } catch (error) { showError('Download failed: ' + (error.message || 'Unknown error')); throw error; } } /** * Shows an error message */ function showError(message) { const errorDiv = document.createElement('div'); errorDiv.className = 'error'; errorDiv.textContent = message; document.body.appendChild(errorDiv); // Auto-remove after 5 seconds setTimeout(() => errorDiv.remove(), 5000); } /** * Shows a success message */ function showSuccess(message) { const successDiv = document.createElement('div'); successDiv.className = 'success'; successDiv.textContent = message; document.body.appendChild(successDiv); // Auto-remove after 5 seconds setTimeout(() => successDiv.remove(), 5000); } /** * Checks if a string is a valid Spotify URL */ function isSpotifyUrl(url) { return url.includes('open.spotify.com') || url.includes('spotify:') || url.includes('link.tospotify.com'); } /** * Extracts details from a Spotify URL */ function getSpotifyResourceDetails(url) { // Allow optional path segments (e.g. intl-fr) before resource type const regex = /spotify\.com\/(?:[^\/]+\/)??(track|album|playlist|artist)\/([a-zA-Z0-9]+)/i; const match = url.match(regex); if (match) { return { type: match[1], id: match[2] }; } return null; } /** * Formats milliseconds to MM:SS */ function msToMinutesSeconds(ms) { if (!ms) return '0:00'; const minutes = Math.floor(ms / 60000); const seconds = ((ms % 60000) / 1000).toFixed(0); return `${minutes}:${seconds.padStart(2, '0')}`; } /** * Creates a result card element */ function createResultCard(item, type, index) { const cardElement = document.createElement('div'); cardElement.className = 'result-card'; // Set cursor to pointer for clickable cards cardElement.style.cursor = 'pointer'; // Get the appropriate image URL let imageUrl = '/static/images/placeholder.jpg'; if (item.album && item.album.images && item.album.images.length > 0) { imageUrl = item.album.images[0].url; } else if (item.images && item.images.length > 0) { imageUrl = item.images[0].url; } // Get the appropriate details based on type let subtitle = ''; let details = ''; switch (type) { case 'track': subtitle = item.artists ? item.artists.map(a => a.name).join(', ') : 'Unknown Artist'; details = item.album ? `${item.album.name}${msToMinutesSeconds(item.duration_ms)}` : ''; break; case 'album': subtitle = item.artists ? item.artists.map(a => a.name).join(', ') : 'Unknown Artist'; details = `${item.total_tracks || 0} tracks${item.release_date ? new Date(item.release_date).getFullYear() : ''}`; break; case 'playlist': subtitle = `By ${item.owner ? item.owner.display_name : 'Unknown'}`; details = `${item.tracks && item.tracks.total ? item.tracks.total : 0} tracks`; break; case 'artist': subtitle = 'Artist'; details = item.genres ? `${item.genres.slice(0, 2).join(', ')}` : ''; break; } // Build the HTML cardElement.innerHTML = `
${item.name || 'Item'}
${item.name || 'Unknown'}
${subtitle}
${details}
`; // Add click event to navigate to the item's detail page cardElement.addEventListener('click', (e) => { // Don't trigger if the download button was clicked if (e.target.classList.contains('download-btn') || e.target.parentElement.classList.contains('download-btn')) { return; } if (item.id) { window.location.href = `/${type}/${item.id}`; } }); return cardElement; } /** * Show/hide the empty state */ function showEmptyState(show) { if (emptyState) { emptyState.style.display = show ? 'flex' : 'none'; } } /** * Show/hide the loading indicator */ function showLoading(show) { if (loadingResults) { loadingResults.classList.toggle('hidden', !show); } } });