fix: circular import causing navigate undefined; unified importer input

- Remove top-level ui.js import from soundcloud.js (broke views.js→soundcloud.js→ui.js→views.js cycle)
- Use window.executeSpotifyDownload() instead to avoid the circular dep
- Merge Spotify + SoundCloud into one unified URL input that auto-detects
  the service and colors the badge/button accordingly
- resolveImporterLink() routes to the correct handler based on URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-23 20:39:21 +02:00
1 parent ee65183555
commit 0e8eb311af
4 files changed
+84 -57

No files matched your search

+18 -28
View File
@@ -381,23 +381,29 @@
<div class="page-title">Song <em>Importer</em></div> <div class="page-title">Song <em>Importer</em></div>
</div> </div>
<!-- SPOTIFY SECTION --> <!-- UNIFIED INPUT — auto-detects Spotify vs SoundCloud -->
<div class="importer-section-label" style="color:#1DB954"> <div class="search-wrap" style="padding:0 0 8px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="#1DB954"><circle cx="12" cy="12" r="10"/><path d="M8 11.973c2.5-1.473 5.5-.973 7.5.527M9.5 14.473c1.5-.973 3.5-.973 5 .027M7 9c3-1.5 6.5-1 9 1" stroke="#000" stroke-width="2" fill="none"/></svg>
Spotify
</div>
<div class="search-wrap" style="padding:0 0 12px">
<div style="display:flex;gap:8px"> <div style="display:flex;gap:8px">
<div class="search-box" style="background:var(--surface);border-color:var(--border2);flex:1"> <div class="search-box" style="background:var(--surface);border-color:var(--border2);flex:1">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 11.973c2.5-1.473 5.5-.973 7.5.527M9.5 14.473c1.5-.973 3.5-.973 5 .027M7 9c3-1.5 6.5-1 9 1"/></svg> <div id="importer-badge" style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:10px;background:var(--surface2);color:var(--text3);white-space:nowrap;flex-shrink:0">URL</div>
<input id="spotify-link-input" type="url" placeholder="Spotify Link (Track, Album oder Playlist)…" style="font-size:14px" oninput="clearSpotifyPreview()"> <input id="importer-link-input" type="url" placeholder="Spotify oder SoundCloud Link einfügen…" style="font-size:14px" oninput="onImporterInput(this.value)">
</div> </div>
<button class="btn-outline" style="padding:0 16px;font-size:13px;font-weight:700;white-space:nowrap;flex-shrink:0;background:#1DB954;color:#000;border-color:transparent" onclick="resolveSpotifyLink()">Importieren</button> <button id="importer-btn" class="btn-outline" style="padding:0 16px;font-size:13px;font-weight:700;white-space:nowrap;flex-shrink:0" onclick="resolveImporterLink()">Importieren</button>
</div> </div>
</div> </div>
<div id="spotify-importer-hint" style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;font-size:12px;color:var(--text2);line-height:1.8;margin-bottom:4px"> <div id="importer-hint" style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;font-size:12px;color:var(--text2);line-height:1.8;margin-bottom:4px">
Track, Album oder Playlist-Link einfügen — Metadaten werden direkt von Spotify geladen und Songs via yt-dlp in die Library heruntergeladen. <div style="display:flex;gap:16px;flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:6px"><span style="color:#1DB954;font-weight:700">Spotify</span> — Track, Album oder Playlist</div>
<div style="display:flex;align-items:center;gap:6px"><span style="color:#ff5500;font-weight:700">SoundCloud</span> — Track oder Playlist</div>
</div>
<div style="margin-top:6px;color:var(--text3);font-size:11px">Link einfügen → Metadaten werden geladen → via yt-dlp in die Navidrome Library herunterladen.</div>
</div> </div>
<!-- hidden inputs kept for compat with spotify.js / soundcloud.js -->
<input id="spotify-link-input" type="hidden">
<input id="sc-link-input" type="hidden">
<!-- SPOTIFY RESULTS -->
<div id="spotify-preview" style="display:none;margin-bottom:8px"> <div id="spotify-preview" style="display:none;margin-bottom:8px">
<div id="spotify-preview-inner"></div> <div id="spotify-preview-inner"></div>
<div id="spotify-enrich-status" style="display:none"></div> <div id="spotify-enrich-status" style="display:none"></div>
@@ -409,23 +415,7 @@
</div> </div>
</div> </div>
<!-- SOUNDCLOUD SECTION --> <!-- SOUNDCLOUD RESULTS -->
<div class="importer-section-label" style="color:#ff5500;margin-top:20px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ff5500" stroke-width="2"><path d="M2 16.5c0 1.1.9 2 2 2h14c2.2 0 4-1.8 4-4s-1.8-4-4-4c-.3 0-.6 0-.9.1C16.8 8.4 14.6 7 12 7c-3.3 0-6 2.7-6 6H4c-1.1 0-2 .9-2 1.5z"/></svg>
SoundCloud
</div>
<div class="search-wrap" style="padding:0 0 12px">
<div style="display:flex;gap:8px">
<div class="search-box" style="background:var(--surface);border-color:var(--border2);flex:1">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 16.5c0 1.1.9 2 2 2h14c2.2 0 4-1.8 4-4s-1.8-4-4-4c-.3 0-.6 0-.9.1C16.8 8.4 14.6 7 12 7c-3.3 0-6 2.7-6 6H4c-1.1 0-2 .9-2 1.5z"/></svg>
<input id="sc-link-input" type="url" placeholder="SoundCloud Link (Track oder Playlist)…" style="font-size:14px" oninput="clearSoundCloudPreview()">
</div>
<button class="btn-outline" style="padding:0 16px;font-size:13px;font-weight:700;white-space:nowrap;flex-shrink:0;background:#ff5500;color:#fff;border-color:transparent" onclick="resolveSoundCloudLink()">Importieren</button>
</div>
</div>
<div id="sc-importer-hint" style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;font-size:12px;color:var(--text2);line-height:1.8;margin-bottom:4px">
Track oder Playlist-Link einfügen — Metadaten (Titel, Artist, Genre, Artwork) werden direkt von SoundCloud geladen und der Audio-Stream heruntergeladen.
</div>
<div id="sc-preview" style="display:none;margin-bottom:8px"> <div id="sc-preview" style="display:none;margin-bottom:8px">
<div id="sc-preview-inner"></div> <div id="sc-preview-inner"></div>
<div id="sc-tracks-section" style="display:none"> <div id="sc-tracks-section" style="display:none">
+3 -1
View File
@@ -18,7 +18,8 @@ import {
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist, loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres, plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
} from './views.js'; } from './views.js';
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js'; import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
onImporterInput, resolveImporterLink } from './spotify.js';
import { loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll } from './soundcloud.js'; import { loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll } from './soundcloud.js';
import { import {
absInitFromStorage, absOpenSettings, absConnectSettings, absSetLibrary, absDisconnect, absVerifyApiKey, absInitFromStorage, absOpenSettings, absConnectSettings, absSetLibrary, absDisconnect, absVerifyApiKey,
@@ -308,6 +309,7 @@ Object.assign(window, {
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist, loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres, plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll, loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
onImporterInput, resolveImporterLink,
loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack, loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack,
executeSpotifyDownload, executeSpotifyDownload,
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt, openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
+7 -16
View File
@@ -1,34 +1,25 @@
import { S } from './state.js'; import { S } from './state.js';
import { esc, toast } from './utils.js'; import { esc, toast } from './utils.js';
// Circular imports — safe (only used inside function bodies)
import { executeSpotifyDownload } from './ui.js';
export function loadSoundCloudImporter() { export function loadSoundCloudImporter() {
const input = document.getElementById('sc-link-input'); // Hint is managed by the unified importer input in spotify.js
if (!input?.value) _showHint();
} }
export function clearSoundCloudPreview() { export function clearSoundCloudPreview() {
if (!document.getElementById('sc-link-input')?.value) _showHint(); // no-op — handled by onImporterInput
}
function _showHint() {
document.getElementById('sc-importer-hint').style.display = '';
document.getElementById('sc-preview').style.display = 'none';
} }
export async function resolveSoundCloudLink() { export async function resolveSoundCloudLink() {
const url = document.getElementById('sc-link-input')?.value.trim(); const url = document.getElementById('sc-link-input')?.value.trim()
|| document.getElementById('importer-link-input')?.value.trim();
if (!url) return; if (!url) return;
if (!url.includes('soundcloud.com')) { toast('⚠ Kein SoundCloud-Link'); return; } if (!url.includes('soundcloud.com')) { toast('⚠ Kein SoundCloud-Link'); return; }
if (!S.cfg.backendUrl) { toast('⚠ Backend URL in Einstellungen konfigurieren'); return; } if (!S.cfg.backendUrl) { toast('⚠ Backend URL in Einstellungen konfigurieren'); return; }
const hint = document.getElementById('sc-importer-hint');
const preview = document.getElementById('sc-preview'); const preview = document.getElementById('sc-preview');
const inner = document.getElementById('sc-preview-inner'); const inner = document.getElementById('sc-preview-inner');
const section = document.getElementById('sc-tracks-section'); const section = document.getElementById('sc-tracks-section');
hint.style.display = 'none';
preview.style.display = ''; preview.style.display = '';
section.style.display = 'none'; section.style.display = 'none';
inner.innerHTML = '<div class="spinner"></div>'; inner.innerHTML = '<div class="spinner"></div>';
@@ -59,7 +50,7 @@ export async function resolveSoundCloudLink() {
export function scDlAll() { export function scDlAll() {
const data = window._scResolved; const data = window._scResolved;
if (!data) return; if (!data) return;
executeSpotifyDownload({ window.executeSpotifyDownload({
type: 'playlist', type: 'playlist',
title: data.name, title: data.name,
artist: data.artist, artist: data.artist,
@@ -72,7 +63,7 @@ export function scDlSingle() {
const data = window._scResolved; const data = window._scResolved;
if (!data?.tracks?.[0]) return; if (!data?.tracks?.[0]) return;
const t = data.tracks[0]; const t = data.tracks[0];
executeSpotifyDownload({ window.executeSpotifyDownload({
type: 'track', type: 'track',
title: t.trackName, title: t.trackName,
artist: t.artistName, artist: t.artistName,
@@ -85,7 +76,7 @@ export function scDlTrack(idx) {
const data = window._scResolved; const data = window._scResolved;
const t = data?.tracks?.[idx]; const t = data?.tracks?.[idx];
if (!t) return; if (!t) return;
executeSpotifyDownload({ window.executeSpotifyDownload({
type: 'track', type: 'track',
title: t.trackName, title: t.trackName,
artist: t.artistName, artist: t.artistName,
+56 -12
View File
@@ -5,27 +5,71 @@ import { itunesSearch } from './api.js';
import { makeGlobalTrackItem } from './components.js'; import { makeGlobalTrackItem } from './components.js';
import { openDlModal } from './ui.js'; import { openDlModal } from './ui.js';
// ─── UNIFIED IMPORTER INPUT ───
export function onImporterInput(val) {
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 {
badge.textContent = 'URL';
badge.style.background = '';
badge.style.color = '';
btn.style.background = '';
btn.style.color = '';
btn.style.borderColor = '';
}
// Hide results when input is cleared
if (!val) {
document.getElementById('spotify-preview').style.display = 'none';
document.getElementById('sc-preview').style.display = 'none';
document.getElementById('importer-hint').style.display = '';
}
}
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 {
resolveSpotifyLink();
}
}
// ─── SPOTIFY IMPORTER PAGE ─── // ─── SPOTIFY IMPORTER PAGE ───
export function loadSpotifyImporter() { export function loadSpotifyImporter() {
if (!document.getElementById('spotify-link-input')?.value) { const input = document.getElementById('importer-link-input');
const hint = document.getElementById('spotify-importer-hint'); if (!input?.value) {
const preview = document.getElementById('spotify-preview'); document.getElementById('importer-hint').style.display = '';
if (hint) hint.style.display = ''; document.getElementById('spotify-preview').style.display = 'none';
if (preview) preview.style.display = 'none';
} }
} }
export function clearSpotifyPreview() { export function clearSpotifyPreview() {
if (!document.getElementById('spotify-link-input')?.value) { // no-op — handled by onImporterInput
const hint = document.getElementById('spotify-importer-hint');
const preview = document.getElementById('spotify-preview');
if (hint) hint.style.display = '';
if (preview) preview.style.display = 'none';
}
} }
export async function resolveSpotifyLink() { export async function resolveSpotifyLink() {
const url = document.getElementById('spotify-link-input')?.value.trim(); const url = document.getElementById('spotify-link-input')?.value.trim()
|| document.getElementById('importer-link-input')?.value.trim();
if (!url) return; if (!url) return;
if (!S.cfg.backendUrl) { toast('⚠ Backend URL in Einstellungen konfigurieren'); return; } if (!S.cfg.backendUrl) { toast('⚠ Backend URL in Einstellungen konfigurieren'); return; }