fediversion/backend/routers/recommendations.py
fullsizemalt 7b8ba4b54c
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
feat: User Personalization, Playlists, Recommendations, and DSO Importer
2025-12-29 16:28:43 -08:00

156 lines
4.9 KiB
Python

"""
Recommendation API - Personalized suggestions for users.
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from sqlmodel import Session, select, desc, func
from pydantic import BaseModel
from datetime import datetime, timedelta
from database import get_session
from models import Show, Vertical, UserVerticalPreference, Attendance, Rating, Performance, Song, Venue, BandMembership
from auth import get_current_user
from models import User
router = APIRouter(prefix="/recommendations", tags=["recommendations"])
class RecommendedShow(BaseModel):
id: int
date: str
venue_name: str | None
vertical_name: str
vertical_slug: str
reason: str # "Recent Show", "Highly Rated", "Trending"
class RecommendedPerformance(BaseModel):
id: int
song_title: str
show_date: str
vertical_name: str
avg_rating: float
notes: str | None
@router.get("/shows/recent", response_model=List[RecommendedShow])
def get_recent_subscriptions(
limit: int = 10,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""
Get recent shows from bands the user follows, excluding attended shows.
"""
# 1. Get user preferences
prefs = session.exec(
select(UserVerticalPreference).where(UserVerticalPreference.user_id == current_user.id)
).all()
if not prefs:
# Fallback: Just return recent shows from featured verticals
# For now, return empty or generic
return []
subscribed_vertical_ids = [p.vertical_id for p in prefs]
# 2. Get Attended Show IDs
attended = session.exec(
select(Attendance.show_id).where(Attendance.user_id == current_user.id)
).all()
attended_ids = set(attended)
# 3. Query Recent Shows
query = (
select(Show)
.where(Show.vertical_id.in_(subscribed_vertical_ids))
.where(Show.date <= datetime.now()) # Past shows only
.where(Show.date >= datetime.now() - timedelta(days=90)) # Last 90 days
.order_by(desc(Show.date))
.limit(limit * 2) # Fetch extra to filter
)
shows = session.exec(query).all()
results = []
for show in shows:
if show.id in attended_ids:
continue
vertical = session.get(Vertical, show.vertical_id)
venue = session.get(Venue, show.venue_id) if show.venue_id else None
results.append(RecommendedShow(
id=show.id,
date=show.date.strftime("%Y-%m-%d"),
venue_name=venue.name if venue else "Unknown Venue",
vertical_name=vertical.name,
vertical_slug=vertical.slug,
reason="Recent from your bands"
))
if len(results) >= limit:
break
return results
@router.get("/performances/top", response_model=List[RecommendedPerformance])
def get_top_rated_tracks(
limit: int = 10,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""
Get top rated performances from bands the user follows.
"""
prefs = session.exec(
select(UserVerticalPreference).where(UserVerticalPreference.user_id == current_user.id)
).all()
if not prefs:
return []
subscribed_vertical_ids = [p.vertical_id for p in prefs]
# Complex query: Join Performance -> Show -> Vertical, Join Rating
# Getting avg rating per performance
# This might be slow on large datasets without materialized view.
# Optimized approach: Query Rating table, group by performance_id, filter by subscribed verticals
results = session.exec(
select(
Rating.performance_id,
func.avg(Rating.score).label("average"),
func.count(Rating.id).label("count")
)
.join(Performance, Rating.performance_id == Performance.id)
.join(Show, Performance.show_id == Show.id)
.where(Show.vertical_id.in_(subscribed_vertical_ids))
.where(Rating.performance_id.isnot(None))
.group_by(Rating.performance_id)
.having(func.count(Rating.id) >= 1) # At least 1 rating
.order_by(desc("average"))
.limit(limit)
).all()
recommendations = []
for row in results:
perf_id, avg, count = row
perf = session.get(Performance, perf_id)
if not perf: continue
show = session.get(Show, perf.show_id)
song = session.get(Song, perf.song_id)
vertical = session.get(Vertical, show.vertical_id)
recommendations.append(RecommendedPerformance(
id=perf.id,
song_title=song.title,
show_date=show.date.strftime("%Y-%m-%d"),
vertical_name=vertical.name,
avg_rating=round(avg, 1),
notes=f"Rated {round(avg, 1)}/10 by {count} fans"
))
return recommendations