Verify all recommendations against YouTube Music - no more hallucinated songs, direct YT Music links
This commit is contained in:
@@ -186,7 +186,7 @@ Already in their library (do NOT recommend these):
|
|||||||
{disliked_instruction}
|
{disliked_instruction}
|
||||||
{exclude_instruction}
|
{exclude_instruction}
|
||||||
|
|
||||||
Respond with exactly {count} music recommendations as a JSON array. Each item should have:
|
Respond with exactly {count * 3} music recommendations as a JSON array. Give more than requested so we can verify they exist. Each item should have:
|
||||||
- "title": song title
|
- "title": song title
|
||||||
- "artist": artist name
|
- "artist": artist name
|
||||||
- "album": album name (if known)
|
- "album": album name (if known)
|
||||||
@@ -216,7 +216,36 @@ Return ONLY the JSON array, no other text."""
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return [], remaining
|
return [], remaining
|
||||||
|
|
||||||
# Save to DB with YouTube links
|
# Verify each recommendation exists on YouTube Music before saving
|
||||||
|
import asyncio
|
||||||
|
from app.services.youtube_music import search_track
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
|
def _normalize(s: str) -> str:
|
||||||
|
import re
|
||||||
|
return re.sub(r'[^a-z0-9\s]', '', s.lower()).strip()
|
||||||
|
|
||||||
|
def _sim(a: str, b: str) -> float:
|
||||||
|
return SequenceMatcher(None, _normalize(a), _normalize(b)).ratio()
|
||||||
|
|
||||||
|
def verify_track(artist: str, title: str) -> dict | None:
|
||||||
|
"""Search YouTube Music to verify a track exists and get the real data."""
|
||||||
|
try:
|
||||||
|
results = search_track(f"{artist} {title}")
|
||||||
|
for r in results[:3]:
|
||||||
|
r_artist = r.get("artist", "")
|
||||||
|
r_title = r.get("title", "")
|
||||||
|
# Check artist similarity (>0.5) and title similarity (>0.4)
|
||||||
|
if _sim(r_artist, artist) >= 0.5 and _sim(r_title, title) >= 0.4:
|
||||||
|
return r
|
||||||
|
# Or very strong artist match with any title
|
||||||
|
if _sim(r_artist, artist) >= 0.8:
|
||||||
|
return r
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Save to DB — only keep verified tracks
|
||||||
recommendations = []
|
recommendations = []
|
||||||
for rec in recs_data:
|
for rec in recs_data:
|
||||||
if len(recommendations) >= count:
|
if len(recommendations) >= count:
|
||||||
@@ -224,17 +253,34 @@ Return ONLY the JSON array, no other text."""
|
|||||||
|
|
||||||
artist = rec.get("artist", "Unknown")
|
artist = rec.get("artist", "Unknown")
|
||||||
title = rec.get("title", "Unknown")
|
title = rec.get("title", "Unknown")
|
||||||
|
reason = rec.get("reason", "")
|
||||||
|
|
||||||
# YouTube search link for every recommendation
|
# Verify on YouTube Music (run sync in thread)
|
||||||
youtube_url = f"https://www.youtube.com/results?search_query={quote_plus(f'{artist} {title} official music video')}"
|
verified = await asyncio.to_thread(verify_track, artist, title)
|
||||||
|
|
||||||
|
if not verified:
|
||||||
|
continue # Skip hallucinated songs
|
||||||
|
|
||||||
|
# Use verified data (correct artist/title from YouTube Music)
|
||||||
|
real_artist = verified.get("artist", artist)
|
||||||
|
real_title = verified.get("title", title)
|
||||||
|
youtube_id = verified.get("youtube_id")
|
||||||
|
image_url = verified.get("image_url")
|
||||||
|
|
||||||
|
# Direct YouTube link if we have a video ID, otherwise search
|
||||||
|
if youtube_id:
|
||||||
|
youtube_url = f"https://music.youtube.com/watch?v={youtube_id}"
|
||||||
|
else:
|
||||||
|
youtube_url = f"https://www.youtube.com/results?search_query={quote_plus(f'{real_artist} {real_title} official music video')}"
|
||||||
|
|
||||||
r = Recommendation(
|
r = Recommendation(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
title=title,
|
title=real_title,
|
||||||
artist=artist,
|
artist=real_artist,
|
||||||
album=rec.get("album"),
|
album=rec.get("album"),
|
||||||
reason=rec.get("reason", ""),
|
image_url=image_url,
|
||||||
|
reason=reason,
|
||||||
score=rec.get("score"),
|
score=rec.get("score"),
|
||||||
query=query,
|
query=query,
|
||||||
youtube_url=youtube_url,
|
youtube_url=youtube_url,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Disc3, Music, ExternalLink, Loader2 } from 'lucide-react'
|
import { Disc3, Music, ExternalLink, Loader2 } from 'lucide-react'
|
||||||
import { discoverBandcamp, getBandcampTags, BandcampRelease } from '../lib/api'
|
import { discoverBandcamp, getBandcampTags, type BandcampRelease } from '../lib/api'
|
||||||
|
|
||||||
const SORT_OPTIONS = [
|
const SORT_OPTIONS = [
|
||||||
{ value: 'new', label: 'New Releases' },
|
{ value: 'new', label: 'New Releases' },
|
||||||
|
|||||||
Reference in New Issue
Block a user