diff --git a/backend/app/services/recommender.py b/backend/app/services/recommender.py index 441bce1..01d6d8e 100644 --- a/backend/app/services/recommender.py +++ b/backend/app/services/recommender.py @@ -237,17 +237,44 @@ Return ONLY the JSON array, no other text.""" except json.JSONDecodeError: return [], remaining + # Look up YouTube video IDs for embeddable players + import asyncio + from app.services.youtube_music import search_track as yt_search + + def get_youtube_links(tracks: list[dict]) -> list[dict]: + """Get YouTube video IDs for a batch of tracks (sync, runs in thread).""" + results = [] + for t in tracks: + artist = t.get("artist", "") + title = t.get("title", "") + try: + yt_results = yt_search(f"{artist} {title}") + if yt_results and yt_results[0].get("youtube_id"): + yt = yt_results[0] + results.append({ + "youtube_url": f"https://music.youtube.com/watch?v={yt['youtube_id']}", + "image_url": yt.get("image_url"), + }) + continue + except Exception: + pass + results.append({ + "youtube_url": f"https://music.youtube.com/search?q={quote_plus(f'{artist} {title}')}", + "image_url": None, + }) + return results + + # Get YouTube data BEFORE any DB operations (avoids greenlet conflict) + tracks_to_lookup = [{"artist": r.get("artist", ""), "title": r.get("title", "")} for r in recs_data[:count]] + yt_data = await asyncio.to_thread(get_youtube_links, tracks_to_lookup) + # Save to DB with YouTube Music links recommendations = [] - for rec in recs_data: - if len(recommendations) >= count: - break - + for i, rec in enumerate(recs_data[:count]): artist = rec.get("artist", "Unknown") title = rec.get("title", "Unknown") reason = rec.get("reason", "") - - youtube_url = f"https://music.youtube.com/search?q={quote_plus(f'{artist} {title}')}" + yt = yt_data[i] if i < len(yt_data) else {} r = Recommendation( user_id=user.id, @@ -255,10 +282,11 @@ Return ONLY the JSON array, no other text.""" title=title, artist=artist, album=rec.get("album"), + image_url=yt.get("image_url"), reason=reason, score=rec.get("score"), query=query, - youtube_url=youtube_url, + youtube_url=yt.get("youtube_url", f"https://music.youtube.com/search?q={quote_plus(f'{artist} {title}')}"), ) db.add(r) recommendations.append(r) diff --git a/frontend/src/components/RecommendationCard.tsx b/frontend/src/components/RecommendationCard.tsx index a5e10c8..1c00279 100644 --- a/frontend/src/components/RecommendationCard.tsx +++ b/frontend/src/components/RecommendationCard.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { Heart, ExternalLink, Music, ThumbsDown, Repeat, Share2, Check, Calendar, MapPin, Ticket, ChevronDown, ChevronUp } from 'lucide-react' +import { Heart, ExternalLink, Music, ThumbsDown, Repeat, Share2, Check, Calendar, MapPin, Ticket, ChevronUp, Play, X } from 'lucide-react' import type { RecommendationItem, ConcertEvent } from '../lib/api' import { shareRecommendation, findConcerts } from '../lib/api' @@ -19,6 +19,16 @@ export default function RecommendationCard({ recommendation, onToggleSave, onDis const [concertsOpen, setConcertsOpen] = useState(false) const [concerts, setConcerts] = useState(null) const [concertsLoading, setConcertsLoading] = useState(false) + const [playerOpen, setPlayerOpen] = useState(false) + + // Extract YouTube video ID from URL if it's a direct link + const getYouTubeVideoId = (url: string | null): string | null => { + if (!url) return null + const match = url.match(/[?&]v=([a-zA-Z0-9_-]{11})/) + return match ? match[1] : null + } + + const videoId = getYouTubeVideoId(recommendation.youtube_url) const handleMoreLikeThis = () => { const q = `find songs similar to ${recommendation.artist} - ${recommendation.title}` @@ -191,19 +201,49 @@ export default function RecommendationCard({ recommendation, onToggleSave, onDis {recommendation.youtube_url && ( - - - + videoId ? ( + + ) : ( + + + + ) )} + {/* YouTube Player */} + {playerOpen && videoId && ( +
+