import asyncio from functools import partial from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.database import get_db from app.core.security import get_current_user from app.models.user import User from app.models.playlist import Playlist from app.models.track import Track from app.services.youtube_music import get_playlist_tracks, search_track from app.services.recommender import build_taste_profile from app.schemas.playlist import PlaylistDetailResponse router = APIRouter(prefix="/youtube-music", tags=["youtube-music"]) class ImportYouTubeRequest(BaseModel): url: str class SearchYouTubeRequest(BaseModel): query: str class YouTubeTrackResult(BaseModel): title: str artist: str album: str | None = None youtube_id: str | None = None image_url: str | None = None @router.post("/import", response_model=PlaylistDetailResponse) async def import_youtube_playlist( data: ImportYouTubeRequest, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): # Free tier limit if not user.is_pro: result = await db.execute( select(Playlist).where(Playlist.user_id == user.id) ) existing = list(result.scalars().all()) if len(existing) >= settings.FREE_MAX_PLAYLISTS: raise HTTPException( status_code=403, detail="Free tier limited to 1 playlist. Upgrade to Pro for unlimited.", ) # Fetch tracks from YouTube Music (run sync ytmusicapi in thread) loop = asyncio.get_event_loop() try: playlist_name, playlist_image, raw_tracks = await loop.run_in_executor( None, partial(get_playlist_tracks, data.url) ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception: raise HTTPException(status_code=400, detail="Failed to fetch playlist from YouTube Music. Make sure the URL is valid and the playlist is public.") if not raw_tracks: raise HTTPException(status_code=400, detail="Playlist is empty or could not be read.") # Create playlist playlist = Playlist( user_id=user.id, name=playlist_name, platform_source="youtube_music", external_id=data.url, track_count=len(raw_tracks), ) db.add(playlist) await db.flush() # Create tracks (no audio features available from YouTube Music) tracks = [] for rt in raw_tracks: track = Track( playlist_id=playlist.id, title=rt["title"], artist=rt["artist"], album=rt.get("album"), image_url=rt.get("image_url"), ) db.add(track) tracks.append(track) await db.flush() # Build taste profile (without audio features, will be limited) playlist.taste_profile = build_taste_profile(tracks) playlist.tracks = tracks return playlist @router.post("/search", response_model=list[YouTubeTrackResult]) async def search_youtube_music( data: SearchYouTubeRequest, user: User = Depends(get_current_user), ): if not data.query.strip(): raise HTTPException(status_code=400, detail="Query cannot be empty") loop = asyncio.get_event_loop() try: results = await loop.run_in_executor( None, partial(search_track, data.query.strip()) ) except Exception: raise HTTPException(status_code=500, detail="Failed to search YouTube Music") return results