Initial MVP: full-stack music discovery app
Backend (FastAPI): - User auth with email/password and Spotify OAuth - Spotify playlist import with audio feature extraction - AI recommendation engine using Claude API with taste profiling - Save/bookmark recommendations - Rate limiting for free tier (10 recs/day, 1 playlist) - PostgreSQL models with Alembic migrations - Redis-ready configuration Frontend (React 19 + TypeScript + Vite + Tailwind): - Landing page, auth flows (email + Spotify OAuth) - Dashboard with stats and quick discover - Playlist management and import from Spotify - Discover page with custom query support - Recommendation cards with explanations and save toggle - Taste profile visualization - Responsive layout with mobile navigation - PWA-ready configuration Infrastructure: - Docker Compose with PostgreSQL, Redis, backend, frontend - Environment-based configuration
This commit is contained in:
77
backend/app/api/endpoints/recommendations.py
Normal file
77
backend/app/api/endpoints/recommendations.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.recommendation import Recommendation
|
||||
from app.schemas.recommendation import RecommendationRequest, RecommendationResponse, RecommendationItem
|
||||
from app.services.recommender import generate_recommendations
|
||||
|
||||
router = APIRouter(prefix="/recommendations", tags=["recommendations"])
|
||||
|
||||
|
||||
@router.post("/generate", response_model=RecommendationResponse)
|
||||
async def generate(
|
||||
data: RecommendationRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not data.playlist_id and not data.query:
|
||||
raise HTTPException(status_code=400, detail="Provide a playlist_id or query")
|
||||
|
||||
recs, remaining = await generate_recommendations(
|
||||
db, user, playlist_id=data.playlist_id, query=data.query
|
||||
)
|
||||
|
||||
if not recs and remaining == 0:
|
||||
raise HTTPException(status_code=429, detail="Daily recommendation limit reached. Upgrade to Pro for unlimited.")
|
||||
|
||||
return RecommendationResponse(
|
||||
recommendations=[RecommendationItem.model_validate(r) for r in recs],
|
||||
remaining_today=remaining,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/history", response_model=list[RecommendationItem])
|
||||
async def history(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Recommendation)
|
||||
.where(Recommendation.user_id == user.id)
|
||||
.order_by(Recommendation.created_at.desc())
|
||||
.limit(50)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/saved", response_model=list[RecommendationItem])
|
||||
async def saved(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Recommendation)
|
||||
.where(Recommendation.user_id == user.id, Recommendation.saved == True)
|
||||
.order_by(Recommendation.created_at.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/{rec_id}/save")
|
||||
async def save_recommendation(
|
||||
rec_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Recommendation).where(Recommendation.id == rec_id, Recommendation.user_id == user.id)
|
||||
)
|
||||
rec = result.scalar_one_or_none()
|
||||
if not rec:
|
||||
raise HTTPException(status_code=404, detail="Recommendation not found")
|
||||
rec.saved = not rec.saved
|
||||
return {"saved": rec.saved}
|
||||
Reference in New Issue
Block a user