fix: Robust Deezer URL generation with direct track links and sanitized search paths to prevent HTTP_NOT_FOUND

This commit is contained in:
Jannik committed 2026-09-13 12:29:50 +02:00
1 parent fc1adda97d
commit 867cc191cd
5 files changed
+59 -9

No files matched your search

+1 -1
View File
@@ -243,7 +243,7 @@
<script src="wheel.js?v=2.1.0"></script>
<script src="cover.js?v=2.1.0"></script>
<script src="search.js?v=2.1.0"></script>
<script src="search.js?v=2.1.1"></script>
<script src="app.js?v=2.1.0"></script>
</body>
</html>
+38 -7
View File
@@ -112,6 +112,28 @@ function formatDuration(sec) {
return `${m}:${s < 10 ? '0' : ''}${s}`;
}
function getCleanSearchTerm(artist, title) {
const raw = `${artist || ''} ${title || ''}`;
return raw.replace(/[/\\?#%&"'\(\)\[\]]/g, ' ').replace(/\s+/g, ' ').trim();
}
function getDeezerUrl(track) {
if (track && track.deezer_link) return track.deezer_link;
if (track && track.deezer_id) return `https://www.deezer.com/track/${track.deezer_id}`;
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
return `https://www.deezer.com/search/${encodeURIComponent(term)}/track`;
}
function getSpotifyUrl(track) {
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
return `https://open.spotify.com/search/${encodeURIComponent(term)}`;
}
function getYoutubeUrl(track) {
const term = getCleanSearchTerm(track ? track.artist : '', track ? track.title : '');
return `https://www.youtube.com/results?search_query=${encodeURIComponent(term)}`;
}
async function selectTrack(track) {
// Update Hero UI
document.getElementById('track-title').textContent = track.title;
@@ -137,13 +159,12 @@ async function selectTrack(track) {
const trackActions = document.getElementById('track-actions');
if (trackActions) {
trackActions.style.display = 'flex';
const query = encodeURIComponent(`${track.artist} ${track.title}`);
const spotEl = document.getElementById('action-spotify');
const deezEl = document.getElementById('action-deezer');
const ytEl = document.getElementById('action-youtube');
if (spotEl) spotEl.href = `https://open.spotify.com/search/${query}`;
if (deezEl) deezEl.href = `https://www.deezer.com/search/${query}`;
if (ytEl) ytEl.href = `https://www.youtube.com/results?search_query=${query}`;
if (spotEl) spotEl.href = getSpotifyUrl(track);
if (deezEl) deezEl.href = getDeezerUrl(track);
if (ytEl) ytEl.href = getYoutubeUrl(track);
}
// Fetch or apply metadata
@@ -155,6 +176,8 @@ async function selectTrack(track) {
let energy = track.energy || null;
let danceability = track.danceability || null;
let happiness = track.happiness || null;
let deezerLink = track.deezer_link || (track.deezer_id ? `https://www.deezer.com/track/${track.deezer_id}` : null);
let deezerId = track.deezer_id || null;
if (!key || !bpm || !energy) {
// Query metadata route to get key, bpm, and persist into PostgreSQL
@@ -171,6 +194,14 @@ async function selectTrack(track) {
energy = d.energy;
danceability = d.danceability;
happiness = d.happiness;
if (d.deezer_link || d.deezer_id) {
deezerLink = d.deezer_link || deezerLink;
deezerId = d.deezer_id || deezerId;
track.deezer_link = deezerLink;
track.deezer_id = deezerId;
const deezEl = document.getElementById('action-deezer');
if (deezEl) deezEl.href = getDeezerUrl(track);
}
document.getElementById('stat-match').textContent = d.is_corrected ? '100%' : (d.source === 'postgresql' ? '100%' : 'Online');
document.getElementById('stat-source-sub').textContent = d.source === 'postgresql' ? (d.is_corrected ? 'PostgreSQL (Fix)' : 'PostgreSQL') : 'PostgreSQL (Neu)';
}
@@ -350,13 +381,13 @@ function applySuggestionsFilter() {
<div class="sug-bottom-row">
<div class="sug-compat">${s.compatLabel || 'KOMPATIBEL'}</div>
<div class="sug-links">
<a href="https://open.spotify.com/search/${encodeURIComponent(s.artist + ' ' + s.title)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-spotify" onclick="event.stopPropagation()" title="Auf Spotify öffnen">
<a href="${getSpotifyUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-spotify" onclick="event.stopPropagation()" title="Auf Spotify öffnen">
<i class="fa-brands fa-spotify"></i>
</a>
<a href="https://www.deezer.com/search/${encodeURIComponent(s.artist + ' ' + s.title)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-deezer" onclick="event.stopPropagation()" title="Auf Deezer öffnen">
<a href="${getDeezerUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-deezer" onclick="event.stopPropagation()" title="Auf Deezer öffnen">
<i class="fa-brands fa-deezer"></i>
</a>
<a href="https://www.youtube.com/results?search_query=${encodeURIComponent(s.artist + ' ' + s.title)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-youtube" onclick="event.stopPropagation()" title="Auf YouTube suchen">
<a href="${getYoutubeUrl(s)}" target="_blank" rel="noopener" class="sug-link-icon sug-link-youtube" onclick="event.stopPropagation()" title="Auf YouTube suchen">
<i class="fa-brands fa-youtube"></i>
</a>
</div>
+13
View File
@@ -122,6 +122,8 @@ router.get('/', async (req, res) => {
is_corrected: !!local.is_corrected,
cover_url: local.cover_url || null,
preview_url: local.preview_url || null,
deezer_id: local.deezer_id || null,
deezer_link: local.deezer_link || (local.deezer_id ? `https://www.deezer.com/track/${local.deezer_id}` : null),
duration_sec: local.duration_sec || 210,
energy: local.energy || est.energy,
danceability: local.danceability || est.danceability,
@@ -193,6 +195,8 @@ router.get('/', async (req, res) => {
// 4. Query Deezer for cover, duration, preview, and genre fallback
let durationSec = 210;
let previewUrl = null;
let deezerLink = null;
let deezerId = null;
try {
const deezerRes = await axios.get(`https://api.deezer.com/search?q=${encodeURIComponent(artist + ' ' + track)}&limit=1`, {
timeout: 3000
@@ -208,6 +212,13 @@ router.get('/', async (req, res) => {
if (dTrack.preview) {
previewUrl = dTrack.preview;
}
if (dTrack.link) {
deezerLink = dTrack.link;
}
if (dTrack.id) {
deezerId = dTrack.id;
if (!deezerLink) deezerLink = `https://www.deezer.com/track/${dTrack.id}`;
}
}
} catch (e) {
// ignore
@@ -250,6 +261,8 @@ router.get('/', async (req, res) => {
cover_url: coverUrl,
preview_url: previewUrl,
duration_sec: durationSec,
deezer_id: deezerId,
deezer_link: deezerLink,
energy: audioAttrs.energy,
danceability: audioAttrs.danceability,
happiness: audioAttrs.happiness,
+6 -1
View File
@@ -35,7 +35,8 @@ router.get('/', async (req, res) => {
cover_url: item.album && item.album.cover_medium ? item.album.cover_medium : (item.album ? item.album.cover : null),
preview_url: item.preview || null,
duration_sec: item.duration || 210,
deezer_id: item.id
deezer_id: item.id,
deezer_link: item.link || (item.id ? `https://www.deezer.com/track/${item.id}` : null)
}));
}
} catch (e) {
@@ -63,6 +64,8 @@ router.get('/', async (req, res) => {
energy: lt.energy,
danceability: lt.danceability,
happiness: lt.happiness,
deezer_id: lt.deezer_id || null,
deezer_link: lt.deezer_link || (lt.deezer_id ? `https://www.deezer.com/track/${lt.deezer_id}` : null),
is_local: true,
is_corrected: lt.is_corrected
});
@@ -81,6 +84,8 @@ router.get('/', async (req, res) => {
cover_url: ot.cover_url,
preview_url: ot.preview_url,
duration_sec: ot.duration_sec,
deezer_id: ot.deezer_id,
deezer_link: ot.deezer_link,
camelot_key: existingLocal ? existingLocal.camelot_key : null,
bpm: existingLocal ? existingLocal.bpm : null,
music_key: existingLocal ? existingLocal.music_key : null,
+1
View File
@@ -80,6 +80,7 @@ router.get('/', async (req, res) => {
compatLabel: compatLabel,
cover_url: t.cover_url || null,
preview_url: t.preview_url || null,
deezer_link: t.deezer_link || (t.deezer_id ? `https://www.deezer.com/track/${t.deezer_id}` : null),
duration_sec: t.duration_sec || 215,
energy: t.energy || defaultEnergy,
danceability: t.danceability || defaultDance,