Files
vynl/backend/app/api/endpoints/youtube_music.py

120 lines
3.6 KiB
Python

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