feat(videos): add video icons to setlists and song versions
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
- Backend: Added video_links relationship to Performance model - Backend: Updated shows and songs routers to eager-load videos and populate youtube_link - Frontend: Added YouTube icon to performance list items if video exists
This commit is contained in:
parent
8e7be96991
commit
c090a395dc
4 changed files with 51 additions and 14 deletions
|
|
@ -22,6 +22,7 @@ class Performance(SQLModel, table=True):
|
||||||
nicknames: List["PerformanceNickname"] = Relationship(back_populates="performance")
|
nicknames: List["PerformanceNickname"] = Relationship(back_populates="performance")
|
||||||
show: "Show" = Relationship(back_populates="performances")
|
show: "Show" = Relationship(back_populates="performances")
|
||||||
song: "Song" = Relationship()
|
song: "Song" = Relationship()
|
||||||
|
video_links: List["VideoPerformance"] = Relationship(back_populates="performance")
|
||||||
|
|
||||||
class ShowArtist(SQLModel, table=True):
|
class ShowArtist(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
|
@ -696,7 +697,7 @@ class VideoPerformance(SQLModel, table=True):
|
||||||
notes: Optional[str] = Field(default=None)
|
notes: Optional[str] = Field(default=None)
|
||||||
|
|
||||||
video: Video = Relationship(back_populates="performances")
|
video: Video = Relationship(back_populates="performances")
|
||||||
performance: "Performance" = Relationship()
|
performance: "Performance" = Relationship(back_populates="video_links")
|
||||||
|
|
||||||
|
|
||||||
class VideoSong(SQLModel, table=True):
|
class VideoSong(SQLModel, table=True):
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,17 @@ def read_upcoming_shows(
|
||||||
|
|
||||||
@router.get("/{slug}", response_model=ShowRead)
|
@router.get("/{slug}", response_model=ShowRead)
|
||||||
def read_show(slug: str, session: Session = Depends(get_session)):
|
def read_show(slug: str, session: Session = Depends(get_session)):
|
||||||
show = session.exec(select(Show).where(Show.slug == slug)).first()
|
from sqlalchemy.orm import selectinload, joinedload
|
||||||
|
from models import Performance, VideoPerformance, Video, VideoPlatform
|
||||||
|
|
||||||
|
# Eager load relationships clearly
|
||||||
|
show = session.exec(
|
||||||
|
select(Show)
|
||||||
|
.options(
|
||||||
|
selectinload(Show.performances).selectinload(Performance.video_links).joinedload(VideoPerformance.video)
|
||||||
|
)
|
||||||
|
.where(Show.slug == slug)
|
||||||
|
).first()
|
||||||
|
|
||||||
if not show:
|
if not show:
|
||||||
raise HTTPException(status_code=404, detail="Show not found")
|
raise HTTPException(status_code=404, detail="Show not found")
|
||||||
|
|
@ -176,15 +186,6 @@ def read_show(slug: str, session: Session = Depends(get_session)):
|
||||||
.where(EntityTag.entity_id == show.id)
|
.where(EntityTag.entity_id == show.id)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Manually populate performances to ensure nicknames are filtered if needed
|
|
||||||
# (Though for now we just return all, or filter approved in schema if we had a custom getter)
|
|
||||||
# The relationship `show.performances` is already loaded if we access it, but we might want to sort.
|
|
||||||
|
|
||||||
# Re-fetch show with relationships if needed, or just rely on lazy loading + validation
|
|
||||||
# But for nicknames, we only want "approved" ones usually.
|
|
||||||
# Let's let the frontend filter or do it here.
|
|
||||||
# Doing it here is safer.
|
|
||||||
|
|
||||||
show_data = ShowRead.model_validate(show)
|
show_data = ShowRead.model_validate(show)
|
||||||
show_data.tags = tags
|
show_data.tags = tags
|
||||||
|
|
||||||
|
|
@ -195,10 +196,17 @@ def read_show(slug: str, session: Session = Depends(get_session)):
|
||||||
# Sort performances by position
|
# Sort performances by position
|
||||||
sorted_perfs = sorted(show.performances, key=lambda p: p.position)
|
sorted_perfs = sorted(show.performances, key=lambda p: p.position)
|
||||||
|
|
||||||
# Filter nicknames for each performance
|
# Process performances: Filter nicknames and populate video links
|
||||||
for perf in sorted_perfs:
|
for perf in sorted_perfs:
|
||||||
perf.nicknames = [n for n in perf.nicknames if n.status == "approved"]
|
perf.nicknames = [n for n in perf.nicknames if n.status == "approved"]
|
||||||
|
|
||||||
|
# Backfill youtube_link from Video entity if not present
|
||||||
|
if not perf.youtube_link and perf.video_links:
|
||||||
|
for link in perf.video_links:
|
||||||
|
if link.video and link.video.platform == VideoPlatform.YOUTUBE:
|
||||||
|
perf.youtube_link = link.video.url
|
||||||
|
break
|
||||||
|
|
||||||
show_data.performances = sorted_perfs
|
show_data.performances = sorted_perfs
|
||||||
|
|
||||||
return show_data
|
return show_data
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,17 @@ def read_song(slug: str, session: Session = Depends(get_session)):
|
||||||
.where(EntityTag.entity_id == song_id)
|
.where(EntityTag.entity_id == song_id)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Fetch performances
|
# Fetch performances with video links
|
||||||
|
from sqlalchemy.orm import selectinload, joinedload
|
||||||
|
from models import VideoPerformance
|
||||||
|
from models import Video, VideoPlatform
|
||||||
|
|
||||||
perfs = session.exec(
|
perfs = session.exec(
|
||||||
select(Performance)
|
select(Performance)
|
||||||
.join(Show)
|
.join(Show)
|
||||||
|
.options(
|
||||||
|
selectinload(Performance.video_links).joinedload(VideoPerformance.video)
|
||||||
|
)
|
||||||
.where(Performance.song_id == song_id)
|
.where(Performance.song_id == song_id)
|
||||||
.order_by(Show.date.desc())
|
.order_by(Show.date.desc())
|
||||||
).all()
|
).all()
|
||||||
|
|
@ -97,9 +104,18 @@ def read_song(slug: str, session: Session = Depends(get_session)):
|
||||||
venue_state = p.show.venue.state
|
venue_state = p.show.venue.state
|
||||||
|
|
||||||
r_stats = rating_stats.get(p.id, {"avg": 0.0, "count": 0})
|
r_stats = rating_stats.get(p.id, {"avg": 0.0, "count": 0})
|
||||||
|
|
||||||
|
# Backfill youtube_link
|
||||||
|
youtube_link = p.youtube_link
|
||||||
|
if not youtube_link and p.video_links:
|
||||||
|
for link in p.video_links:
|
||||||
|
if link.video and link.video.platform == VideoPlatform.YOUTUBE:
|
||||||
|
youtube_link = link.video.url
|
||||||
|
break
|
||||||
|
|
||||||
perf_dtos.append(PerformanceReadWithShow(
|
perf_dtos.append(PerformanceReadWithShow(
|
||||||
**p.model_dump(),
|
**p.model_dump(),
|
||||||
|
youtube_link=youtube_link,
|
||||||
show_date=show_date,
|
show_date=show_date,
|
||||||
show_slug=show_slug,
|
show_slug=show_slug,
|
||||||
venue_name=venue_name,
|
venue_name=venue_name,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import Link from "next/link"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { RatePerformanceDialog } from "@/components/songs/rate-performance-dialog"
|
import { RatePerformanceDialog } from "@/components/songs/rate-performance-dialog"
|
||||||
import { Star, Music } from "lucide-react"
|
import { Star, Music, Youtube } from "lucide-react"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
|
||||||
export interface Performance {
|
export interface Performance {
|
||||||
|
|
@ -23,6 +23,7 @@ export interface Performance {
|
||||||
venue_state: string | null
|
venue_state: string | null
|
||||||
avg_rating: number
|
avg_rating: number
|
||||||
total_reviews: number
|
total_reviews: number
|
||||||
|
youtube_link?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PerformanceListProps {
|
interface PerformanceListProps {
|
||||||
|
|
@ -98,6 +99,17 @@ export function PerformanceList({ performances }: PerformanceListProps) {
|
||||||
<span className="text-xs text-muted-foreground uppercase tracking-wider font-mono">
|
<span className="text-xs text-muted-foreground uppercase tracking-wider font-mono">
|
||||||
{perf.set_name || "Set ?"}
|
{perf.set_name || "Set ?"}
|
||||||
</span>
|
</span>
|
||||||
|
{perf.youtube_link && (
|
||||||
|
<a
|
||||||
|
href={perf.youtube_link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-red-500 hover:text-red-600 transition-colors"
|
||||||
|
title="Watch Video"
|
||||||
|
>
|
||||||
|
<Youtube className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground truncate">
|
<div className="text-sm text-muted-foreground truncate">
|
||||||
{perf.venue_name} • {perf.venue_city}{perf.venue_state ? `, ${perf.venue_state}` : ""}
|
{perf.venue_name} • {perf.venue_city}{perf.venue_state ? `, ${perf.venue_state}` : ""}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue