feat: switch Spotify auth to official Client Credentials Flow
Replaces the broken open.spotify.com/get_access_token scrape with accounts.spotify.com/api/token (Basic auth, client_credentials grant). Adds SPOTIFY_CLIENT_ID/SECRET to config, docker-compose, and .env.example. Health endpoint now reports spotify.configured; frontend disables the importer and shows a setup guide when credentials are missing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c1c444bf2
commit
468827571a
5 files changed
+77
-13
No files matched your search
@@ -22,3 +22,10 @@ DOWNLOAD_BACKEND=auto
|
|||||||
# flac - FLAC 16-bit / 44.1 kHz
|
# flac - FLAC 16-bit / 44.1 kHz
|
||||||
# hi-res - FLAC 24-bit / 96 kHz
|
# hi-res - FLAC 24-bit / 96 kHz
|
||||||
SQUID_QUALITY=mp3
|
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=
|
||||||
@@ -9,6 +9,9 @@ const CONFIG = {
|
|||||||
downloadBackend: (process.env.DOWNLOAD_BACKEND || 'auto').toLowerCase(),
|
downloadBackend: (process.env.DOWNLOAD_BACKEND || 'auto').toLowerCase(),
|
||||||
// squid.wtf quality: 'mp3' | 'flac' | 'hi-res'
|
// squid.wtf quality: 'mp3' | 'flac' | 'hi-res'
|
||||||
squidQuality: (process.env.SQUID_QUALITY || 'mp3').toLowerCase(),
|
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;
|
module.exports = CONFIG;
|
||||||
+27
-12
@@ -72,23 +72,35 @@ let _spTokenExpiry = 0;
|
|||||||
|
|
||||||
async function fetchSpotifyToken() {
|
async function fetchSpotifyToken() {
|
||||||
if (_spTokenCache && Date.now() < _spTokenExpiry) return _spTokenCache;
|
if (_spTokenCache && Date.now() < _spTokenExpiry) return _spTokenCache;
|
||||||
// spotdl trick: send a random sp_t cookie — Spotify requires some value but doesn't validate it
|
if (!CONFIG.spotifyClientId || !CONFIG.spotifyClientSecret) {
|
||||||
const sp_t = require('crypto').randomUUID();
|
throw new Error(
|
||||||
const r = await fetch('https://open.spotify.com/get_access_token?reason=transport&productType=web_player', {
|
'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: {
|
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',
|
'Authorization': `Basic ${basic}`,
|
||||||
'Accept': 'application/json',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
|
||||||
'Referer': 'https://open.spotify.com/',
|
|
||||||
'Cookie': `sp_t=${sp_t}`,
|
|
||||||
},
|
},
|
||||||
|
body: 'grant_type=client_credentials',
|
||||||
signal: AbortSignal.timeout(10000),
|
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();
|
const d = await r.json();
|
||||||
if (!d.accessToken) throw new Error('Kein Spotify-Token erhalten');
|
if (!d.access_token) throw new Error('Kein access_token von Spotify erhalten');
|
||||||
_spTokenCache = d.accessToken;
|
|
||||||
_spTokenExpiry = (d.accessTokenExpirationTimestampMs || (Date.now() + 55 * 60 * 1000)) - 30000;
|
_spTokenCache = d.access_token;
|
||||||
|
_spTokenExpiry = Date.now() + (d.expires_in * 1000) - 30000;
|
||||||
return _spTokenCache;
|
return _spTokenCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +217,9 @@ app.get('/api/health', (req, res) => {
|
|||||||
squidQuality: CONFIG.squidQuality,
|
squidQuality: CONFIG.squidQuality,
|
||||||
activeJobs: activeDownloads,
|
activeJobs: activeDownloads,
|
||||||
queueLength: downloadQueue.length,
|
queueLength: downloadQueue.length,
|
||||||
|
spotify: {
|
||||||
|
configured: !!(CONFIG.spotifyClientId && CONFIG.spotifyClientSecret),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ services:
|
|||||||
- MAX_PARALLEL=${MAX_PARALLEL:-2}
|
- MAX_PARALLEL=${MAX_PARALLEL:-2}
|
||||||
- DOWNLOAD_BACKEND=${DOWNLOAD_BACKEND:-auto}
|
- DOWNLOAD_BACKEND=${DOWNLOAD_BACKEND:-auto}
|
||||||
- SQUID_QUALITY=${SQUID_QUALITY:-mp3}
|
- SQUID_QUALITY=${SQUID_QUALITY:-mp3}
|
||||||
|
- SPOTIFY_CLIENT_ID=${SPOTIFY_CLIENT_ID:-}
|
||||||
|
- SPOTIFY_CLIENT_SECRET=${SPOTIFY_CLIENT_SECRET:-}
|
||||||
command: >
|
command: >
|
||||||
sh -c "
|
sh -c "
|
||||||
apk add --no-cache yt-dlp ffmpeg python3 &&
|
apk add --no-cache yt-dlp ffmpeg python3 &&
|
||||||
|
|||||||
+38
-1
@@ -6,13 +6,32 @@ import { makeGlobalTrackItem } from './components.js';
|
|||||||
import { openDlModal } from './ui.js';
|
import { openDlModal } from './ui.js';
|
||||||
|
|
||||||
// ─── SPOTIFY IMPORTER PAGE ───
|
// ─── SPOTIFY IMPORTER PAGE ───
|
||||||
export function loadSpotifyImporter() {
|
export async function loadSpotifyImporter() {
|
||||||
if (!document.getElementById('spotify-link-input')?.value) {
|
if (!document.getElementById('spotify-link-input')?.value) {
|
||||||
const hint = document.getElementById('spotify-importer-hint');
|
const hint = document.getElementById('spotify-importer-hint');
|
||||||
const preview = document.getElementById('spotify-preview');
|
const preview = document.getElementById('spotify-preview');
|
||||||
if (hint) hint.style.display = '';
|
if (hint) hint.style.display = '';
|
||||||
if (preview) preview.style.display = 'none';
|
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 += `
|
||||||
|
<div style="margin-top:16px;padding:12px;background:var(--surface);border-radius:10px;border-left:3px solid #1DB954;font-size:13px;color:var(--text2)">
|
||||||
|
⚠ Server-Admin muss <code>SPOTIFY_CLIENT_ID</code> und <code>SPOTIFY_CLIENT_SECRET</code> setzen.
|
||||||
|
Kostenlos unter <a href="https://developer.spotify.com/dashboard" target="_blank" style="color:#1DB954">developer.spotify.com/dashboard</a>.
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
} catch { /* health check failed — leave UI as-is */ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearSpotifyPreview() {
|
export function clearSpotifyPreview() {
|
||||||
@@ -54,6 +73,24 @@ export async function resolveSpotifyLink() {
|
|||||||
if (data.skippedLocal > 0) toast(`${data.skippedLocal} lokale Datei(en) übersprungen (kein Stream verfügbar)`);
|
if (data.skippedLocal > 0) toast(`${data.skippedLocal} lokale Datei(en) übersprungen (kein Stream verfügbar)`);
|
||||||
_renderSpotifyPreview(data);
|
_renderSpotifyPreview(data);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
if (e.message.includes('SPOTIFY_CLIENT_ID') ||
|
||||||
|
e.message.includes('Credentials fehlen') ||
|
||||||
|
e.message.includes('Spotify-Token-Fehler')) {
|
||||||
|
inner.innerHTML = `<div style="color:var(--text);padding:16px;background:var(--surface);border-radius:10px;border-left:3px solid #1DB954">
|
||||||
|
<div style="font-weight:600;margin-bottom:8px">Spotify-Importer nicht konfiguriert</div>
|
||||||
|
<div style="font-size:13px;line-height:1.5">
|
||||||
|
Der Server-Admin muss einmalig Spotify-Credentials hinterlegen.<br><br>
|
||||||
|
Schritte:<br>
|
||||||
|
1. <a href="https://developer.spotify.com/dashboard" target="_blank" style="color:#1DB954">developer.spotify.com/dashboard</a> öffnen<br>
|
||||||
|
2. "Create app" — Name beliebig, Redirect URI: <code>http://localhost</code><br>
|
||||||
|
3. Client ID + Secret als Environment-Variablen setzen:<br>
|
||||||
|
<code>SPOTIFY_CLIENT_ID=...</code><br>
|
||||||
|
<code>SPOTIFY_CLIENT_SECRET=...</code><br>
|
||||||
|
4. Backend neu starten
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
inner.innerHTML = `<div style="color:var(--danger);padding:16px;background:var(--surface);border-radius:10px">${esc(e.message)}</div>`;
|
inner.innerHTML = `<div style="color:var(--danger);padding:16px;background:var(--surface);border-radius:10px">${esc(e.message)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user