diff --git a/.env.example b/.env.example index ea891d1..992b6b6 100644 --- a/.env.example +++ b/.env.example @@ -22,3 +22,10 @@ DOWNLOAD_BACKEND=auto # flac - FLAC 16-bit / 44.1 kHz # hi-res - FLAC 24-bit / 96 kHz SQUID_QUALITY=mp3 + +# Spotify Importer (optional — free, no Premium needed) +# 1. Go to https://developer.spotify.com/dashboard and create an app +# 2. Set any name, Redirect URI: http://localhost +# 3. Copy the Client ID and Client Secret below +SPOTIFY_CLIENT_ID= +SPOTIFY_CLIENT_SECRET= diff --git a/backend/lib/config.js b/backend/lib/config.js index c6c951c..30ce49c 100644 --- a/backend/lib/config.js +++ b/backend/lib/config.js @@ -9,6 +9,9 @@ const CONFIG = { downloadBackend: (process.env.DOWNLOAD_BACKEND || 'auto').toLowerCase(), // squid.wtf quality: 'mp3' | 'flac' | 'hi-res' squidQuality: (process.env.SQUID_QUALITY || 'mp3').toLowerCase(), + // Spotify Developer credentials (Client Credentials Flow — free, no Premium needed) + spotifyClientId: process.env.SPOTIFY_CLIENT_ID || '', + spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET || '', }; module.exports = CONFIG; diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index a5279b8..337ff17 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -72,23 +72,35 @@ let _spTokenExpiry = 0; async function fetchSpotifyToken() { if (_spTokenCache && Date.now() < _spTokenExpiry) return _spTokenCache; - // spotdl trick: send a random sp_t cookie — Spotify requires some value but doesn't validate it - const sp_t = require('crypto').randomUUID(); - const r = await fetch('https://open.spotify.com/get_access_token?reason=transport&productType=web_player', { + if (!CONFIG.spotifyClientId || !CONFIG.spotifyClientSecret) { + throw new Error( + 'Spotify-Credentials fehlen. Bitte SPOTIFY_CLIENT_ID und ' + + 'SPOTIFY_CLIENT_SECRET in der Umgebung setzen. ' + + 'Setup-Anleitung: https://developer.spotify.com/dashboard' + ); + } + + const basic = Buffer.from(`${CONFIG.spotifyClientId}:${CONFIG.spotifyClientSecret}`).toString('base64'); + const r = await fetch('https://accounts.spotify.com/api/token', { + method: 'POST', headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - 'Accept': 'application/json', - 'Accept-Language': 'en-US,en;q=0.9', - 'Referer': 'https://open.spotify.com/', - 'Cookie': `sp_t=${sp_t}`, + 'Authorization': `Basic ${basic}`, + 'Content-Type': 'application/x-www-form-urlencoded', }, + body: 'grant_type=client_credentials', signal: AbortSignal.timeout(10000), }); - if (!r.ok) throw new Error(`Spotify-Token nicht abrufbar (${r.status})`); + + if (!r.ok) { + const errBody = await r.text().catch(() => ''); + throw new Error(`Spotify-Token-Fehler (${r.status}): ${errBody.slice(0, 200)}`); + } + const d = await r.json(); - if (!d.accessToken) throw new Error('Kein Spotify-Token erhalten'); - _spTokenCache = d.accessToken; - _spTokenExpiry = (d.accessTokenExpirationTimestampMs || (Date.now() + 55 * 60 * 1000)) - 30000; + if (!d.access_token) throw new Error('Kein access_token von Spotify erhalten'); + + _spTokenCache = d.access_token; + _spTokenExpiry = Date.now() + (d.expires_in * 1000) - 30000; return _spTokenCache; } @@ -205,6 +217,9 @@ app.get('/api/health', (req, res) => { squidQuality: CONFIG.squidQuality, activeJobs: activeDownloads, queueLength: downloadQueue.length, + spotify: { + configured: !!(CONFIG.spotifyClientId && CONFIG.spotifyClientSecret), + }, }); }); diff --git a/docker-compose.yml b/docker-compose.yml index e37e113..1009cfa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,8 @@ services: - MAX_PARALLEL=${MAX_PARALLEL:-2} - DOWNLOAD_BACKEND=${DOWNLOAD_BACKEND:-auto} - SQUID_QUALITY=${SQUID_QUALITY:-mp3} + - SPOTIFY_CLIENT_ID=${SPOTIFY_CLIENT_ID:-} + - SPOTIFY_CLIENT_SECRET=${SPOTIFY_CLIENT_SECRET:-} command: > sh -c " apk add --no-cache yt-dlp ffmpeg python3 && diff --git a/src/spotify.js b/src/spotify.js index f28ca2a..287e2d0 100644 --- a/src/spotify.js +++ b/src/spotify.js @@ -6,13 +6,32 @@ import { makeGlobalTrackItem } from './components.js'; import { openDlModal } from './ui.js'; // ─── SPOTIFY IMPORTER PAGE ─── -export function loadSpotifyImporter() { +export async function loadSpotifyImporter() { if (!document.getElementById('spotify-link-input')?.value) { const hint = document.getElementById('spotify-importer-hint'); const preview = document.getElementById('spotify-preview'); if (hint) hint.style.display = ''; if (preview) preview.style.display = 'none'; } + + if (S.cfg.backendUrl) { + try { + const h = await fetch(`${S.cfg.backendUrl}/api/health`).then(r => r.json()); + if (!h.spotify?.configured) { + const input = document.getElementById('spotify-link-input'); + if (input) { + input.disabled = true; + input.placeholder = 'Spotify-Importer nicht konfiguriert (Server-Setup erforderlich)'; + } + const hint = document.getElementById('spotify-importer-hint'); + if (hint) hint.innerHTML += ` +
SPOTIFY_CLIENT_ID und SPOTIFY_CLIENT_SECRET setzen.
+ Kostenlos unter developer.spotify.com/dashboard.
+ http://localhostSPOTIFY_CLIENT_ID=...SPOTIFY_CLIENT_SECRET=...