import { S } from './state.js'; import { esc, toast } from './utils.js'; import { itunesSearch } from './api.js'; // Circular imports — safe (only used inside function bodies) import { makeGlobalTrackItem } from './components.js'; import { openDlModal } from './ui.js'; // ─── UNIFIED IMPORTER INPUT ─── export function onImporterInput(val) { // Always clear results when URL changes document.getElementById('spotify-preview').style.display = 'none'; document.getElementById('sc-preview').style.display = 'none'; document.getElementById('yt-preview').style.display = 'none'; document.getElementById('importer-hint').style.display = val ? 'none' : ''; const badge = document.getElementById('importer-badge'); const btn = document.getElementById('importer-btn'); if (!badge || !btn) return; if (val.includes('spotify.com')) { badge.textContent = 'Spotify'; badge.style.background = 'rgba(29,185,84,.15)'; badge.style.color = '#1DB954'; btn.style.background = '#1DB954'; btn.style.color = '#000'; btn.style.borderColor = 'transparent'; } else if (val.includes('soundcloud.com')) { badge.textContent = 'SoundCloud'; badge.style.background = 'rgba(255,85,0,.15)'; badge.style.color = '#ff5500'; btn.style.background = '#ff5500'; btn.style.color = '#fff'; btn.style.borderColor = 'transparent'; } else if (val.includes('youtube.com') || val.includes('youtu.be')) { badge.textContent = 'YouTube'; badge.style.background = 'rgba(255,0,0,.15)'; badge.style.color = '#FF0000'; btn.style.background = '#FF0000'; btn.style.color = '#fff'; btn.style.borderColor = 'transparent'; } else { badge.textContent = 'URL'; badge.style.background = ''; badge.style.color = ''; btn.style.background = ''; btn.style.color = ''; btn.style.borderColor = ''; } } export async function resolveImporterLink() { const url = document.getElementById('importer-link-input')?.value.trim(); if (!url) return; // Mirror to the hidden inputs so existing resolve functions pick it up document.getElementById('spotify-link-input').value = url; document.getElementById('sc-link-input').value = url; document.getElementById('importer-hint').style.display = 'none'; if (url.includes('soundcloud.com')) { window.resolveSoundCloudLink(); } else if (url.includes('youtube.com') || url.includes('youtu.be')) { window.resolveYouTubeLink(); } else { resolveSpotifyLink(); } } // ─── SPOTIFY IMPORTER PAGE ─── export function loadSpotifyImporter() { const input = document.getElementById('importer-link-input'); if (!input?.value) { document.getElementById('importer-hint').style.display = ''; document.getElementById('spotify-preview').style.display = 'none'; } } export function clearSpotifyPreview() { // no-op — handled by onImporterInput } export async function resolveSpotifyLink() { const url = document.getElementById('spotify-link-input')?.value.trim() || document.getElementById('importer-link-input')?.value.trim(); if (!url) return; if (!S.cfg.backendUrl) { toast('⚠ Backend URL in Einstellungen konfigurieren'); return; } const hint = document.getElementById('spotify-importer-hint'); const preview = document.getElementById('spotify-preview'); const inner = document.getElementById('spotify-preview-inner'); const section = document.getElementById('spotify-tracks-section'); if (hint) hint.style.display = 'none'; preview.style.display = ''; section.style.display = 'none'; inner.innerHTML = '
'; try { const res = await fetch(`${S.cfg.backendUrl}/api/spotify/resolve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), signal: AbortSignal.timeout(30000), }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (err.error === 'EDITORIAL_PLAYLIST') throw new Error('EDITORIAL_PLAYLIST'); if (err.error === 'RATE_LIMITED') throw new Error(`RATE_LIMITED:${err.retryAfter || 30}`); throw new Error(err.error || err.message || `HTTP ${res.status}`); } const data = await res.json(); window._spotifyResolved = data; if (data.skippedLocal > 0) toast(`${data.skippedLocal} lokale Datei(en) übersprungen (kein Stream verfügbar)`); _renderSpotifyPreview(data); } catch(e) { if (e.name === 'TimeoutError' || e.name === 'AbortError') { inner.innerHTML = `
Zeitüberschreitung — der Server hat nicht rechtzeitig geantwortet. Läuft das Backend?
`; return; } if (e.message.startsWith('RATE_LIMITED:')) { const secs = parseInt(e.message.split(':')[1], 10) || 30; _startRateLimitCountdown(inner, secs); return; } if (e.message === 'EDITORIAL_PLAYLIST') { inner.innerHTML = `
💡 Editorial Playlist erkannt
Spotify hat den API-Zugriff auf eigene kuratierte Playlists (wie "Today's Top Hits", "Discover Weekly" etc.) für Drittanbieter-Apps gesperrt.

Lösung:
  1. In Spotify die Playlist öffnen
  2. Alle Tracks markieren (Strg+A)
  3. Rechtsklick → "Zu Playlist hinzufügen" → "Neue Playlist"
  4. Diese eigene Kopie hier importieren
`; return; } inner.innerHTML = `
${esc(e.message)}
`; } } export function spotifyDlAll(cbId, nameId) { const data = window._spotifyResolved; if (!data) return; window.executeSpotifyDownload({ type: 'playlist', title: document.getElementById(nameId)?.value || data.name, artist: data.artist, artworkUrl: data.artworkUrl, tracks: data.tracks, createPlaylist: document.getElementById(cbId)?.checked || false, playlistName: document.getElementById(nameId)?.value || data.name, }); } function _renderSpotifyPreview(data) { const inner = document.getElementById('spotify-preview-inner'); const section = document.getElementById('spotify-tracks-section'); const list = document.getElementById('spotify-tracks-list'); const titleEl = document.getElementById('spotify-tracks-title'); const typeLabel = { track: 'Spotify Track', album: 'Spotify Album', playlist: 'Spotify Playlist' }[data.type] || 'Spotify'; const art = (data.artworkUrl || '').replace(/\/\d+x\d+bb/, '/300x300bb').replace('100x100', '300x300'); const count = data.tracks?.length || 0; const uid = Date.now(); const plCbId = `sp-cb-${uid}`, plNmId = `sp-pln-${uid}`; let actionsHtml = ''; if (data.type === 'track' && count > 0) { actionsHtml = `
${data.tracks[0]?.collectionName ? `` : ''}
`; } else if (data.type === 'album') { actionsHtml = ` `; } else if (data.type === 'playlist') { actionsHtml = `
`; } const artSize = data.type === 'playlist' ? '80px' : '110px'; const artRadius = data.type === 'playlist' ? '8px' : '10px'; const artHtml = art ? `` : ''; inner.innerHTML = `
${artHtml}
🎵
${typeLabel}
${esc(data.name)}
${esc(data.artist)}${count > 1 ? ` · ${count} Tracks` : ''}
${actionsHtml}
`; titleEl.textContent = `${count} Tracks`; list.innerHTML = ''; (data.tracks || []).forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, data.tracks))); section.style.display = ''; // Lazy-load correct per-track artwork and canonical names via iTunes in the background if (count > 0 && S.cfg.backendUrl) { _loadSpotifyTrackArtwork(data.tracks, list); } } function _setEnrichStatus(done, current, total) { const el = document.getElementById('spotify-enrich-status'); if (!el) return; if (done) { el.className = 'done'; el.innerHTML = ' iTunes-Daten geladen'; el.style.display = 'flex'; el.style.opacity = '1'; setTimeout(() => { el.style.opacity = '0'; setTimeout(() => { el.style.display = 'none'; el.className = ''; }, 400); }, 2000); } else { el.className = ''; el.innerHTML = `
Lade iTunes-Daten… (${current}/${total})`; el.style.display = 'flex'; el.style.opacity = '1'; } } async function _loadSpotifyTrackArtwork(tracks, listEl) { const artEls = listEl.querySelectorAll('.gtrack-art'); // Show any Spotify artwork that's already present immediately tracks.forEach((t, i) => { const url = t.artworkUrl100 || t.artworkUrl60 || ''; if (!url) return; const el = artEls[i]; if (!el) return; _setArt(el, url); }); // Enrich each track with iTunes data — same call as global search const CONC = 4; let enriched = 0; _setEnrichStatus(false, 0, tracks.length); for (let i = 0; i < tracks.length; i += CONC) { const batch = tracks.slice(i, i + CONC); await Promise.all(batch.map(async (t, bi) => { const idx = i + bi; try { const firstArtist = (t.artistName || '').split(/\s*[,/•]\s*/)[0].trim(); const results = await itunesSearch(`${t.trackName} ${firstArtist}`, 'musicTrack', 1); const match = results[0]; if (!match) return; // Always safe to enrich: artwork, preview, explicit, iTunes artist name if (match.artworkUrl100) t.artworkUrl100 = match.artworkUrl100; if (match.trackExplicitness) t.trackExplicitness = match.trackExplicitness; if (match.previewUrl) t.previewUrl = match.previewUrl; // Store iTunes canonical name separately — keeps original Spotify/Japanese in artistName // so the UI shows the original and YTM search uses the original channel name. if (match.artistName) t.itunesArtistName = match.artistName; // Only enrich album/duration if we're confident the iTunes match is the correct song. // For Japanese titles we can't word-match, so we skip — the backend's lookupItunesTrackMeta // handles it with better matching. For ASCII titles, require at least one shared word. const JP_RE = /[ぁ-んァ-ン一-龯]/; const isJapanese = JP_RE.test(t.trackName); const _norm = s => new Set((s||'').toLowerCase().replace(/[^a-z0-9]/g,' ').split(/\s+/).filter(Boolean)); const titleMatch = !isJapanese && [..._norm(t.trackName)].some(w => _norm(match.trackName).has(w)); if (titleMatch) { if (match.collectionName) t.collectionName = match.collectionName; if (match.releaseDate) t.releaseDate = match.releaseDate; if (match.trackNumber) t.trackNumber = match.trackNumber; // Only use iTunes duration if it looks like a full song (> 30s) — jingles/previews // returning a small trackTimeMillis would corrupt the yt-dlp duration filter. if (match.trackTimeMillis > 30000) t.durationMs = match.trackTimeMillis; } if (window._spotifyResolved?.tracks?.[idx]) Object.assign(window._spotifyResolved.tracks[idx], t); const url = match.artworkUrl100 || match.artworkUrl60 || ''; if (url) _setArt(artEls[idx], url); if (match.previewUrl) { const item = artEls[idx]?.closest('.gtrack-item'); if (item && !item.querySelector('.gtrack-actions .icon-btn.blue')) { const actions = item.querySelector('.gtrack-actions'); if (actions) { const btn = document.createElement('button'); btn.className = 'icon-btn blue'; btn.title = '30s Preview'; btn.innerHTML = ''; btn.onclick = (e) => { e.stopPropagation(); window.playPreview(t); }; actions.insertBefore(btn, actions.firstChild); item.onclick = () => window.playPreview(t); } } } } catch { /* non-critical */ } enriched++; _setEnrichStatus(false, enriched, tracks.length); })); } _setEnrichStatus(true); } function _startRateLimitCountdown(inner, totalSecs) { const endMs = Date.now() + totalSecs * 1000; const render = (remaining) => { const s = Math.max(0, Math.ceil(remaining / 1000)); const label = s >= 60 ? `${Math.ceil(s / 60)} Min.` : `${s}s`; inner.innerHTML = `
⏳ Spotify Rate Limit
Bitte warte noch ${label} — nicht erneut versuchen, das setzt den Timer zurück.
`; }; render(totalSecs * 1000); const iv = setInterval(() => { const remaining = endMs - Date.now(); if (remaining <= 0) { clearInterval(iv); render(0); return; } render(remaining); }, 1000); } function _setArt(el, url) { if (!el) return; const img = document.createElement('img'); img.loading = 'lazy'; img.onerror = () => { el.innerHTML = '
🎵
'; }; img.src = url; el.innerHTML = ''; el.appendChild(img); }