feat: Add video integration - display videos on performance pages and indicators
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
- Add YouTubeEmbed to performance detail page when youtube_link exists - Add YouTube icon indicator on setlist items that have videos - Add YouTube badge on show cards in archive when full show video exists - Add youtube_link to ShowRead and PerformanceRead schemas - Add VIDEO_INTEGRATION_SPEC.md documentation
This commit is contained in:
parent
171b8a38ca
commit
4a103511da
5 changed files with 215 additions and 3 deletions
|
|
@ -91,6 +91,7 @@ class PerformanceRead(PerformanceBase):
|
||||||
slug: Optional[str] = None
|
slug: Optional[str] = None
|
||||||
song: Optional["SongRead"] = None
|
song: Optional["SongRead"] = None
|
||||||
nicknames: List["PerformanceNicknameRead"] = []
|
nicknames: List["PerformanceNicknameRead"] = []
|
||||||
|
youtube_link: Optional[str] = None
|
||||||
|
|
||||||
class PerformanceReadWithShow(PerformanceRead):
|
class PerformanceReadWithShow(PerformanceRead):
|
||||||
show_date: datetime
|
show_date: datetime
|
||||||
|
|
@ -151,6 +152,7 @@ class ShowRead(ShowBase):
|
||||||
tour: Optional["TourRead"] = None
|
tour: Optional["TourRead"] = None
|
||||||
tags: List["TagRead"] = []
|
tags: List["TagRead"] = []
|
||||||
performances: List["PerformanceRead"] = []
|
performances: List["PerformanceRead"] = []
|
||||||
|
youtube_link: Optional[str] = None
|
||||||
|
|
||||||
class ShowUpdate(SQLModel):
|
class ShowUpdate(SQLModel):
|
||||||
date: Optional[datetime] = None
|
date: Optional[datetime] = None
|
||||||
|
|
|
||||||
177
docs/VIDEO_INTEGRATION_SPEC.md
Normal file
177
docs/VIDEO_INTEGRATION_SPEC.md
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
# Video Integration Specification
|
||||||
|
|
||||||
|
**Date:** 2025-12-22
|
||||||
|
**Status:** In Progress
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This spec outlines the complete video integration for elmeg.xyz, ensuring YouTube videos are properly displayed and discoverable across the application.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
### Database Schema ✅
|
||||||
|
|
||||||
|
- `Performance.youtube_link` - Individual performance video URL
|
||||||
|
- `Show.youtube_link` - Full show video URL
|
||||||
|
- `Song.youtube_link` - Studio/canonical video URL
|
||||||
|
|
||||||
|
### Import Pipeline ✅
|
||||||
|
|
||||||
|
- `import_youtube.py` processes `youtube_videos.json`
|
||||||
|
- Handles: single songs, sequences (→), and full shows
|
||||||
|
- Sequences link the SAME video to ALL performances in the sequence
|
||||||
|
|
||||||
|
### Frontend Display (Current)
|
||||||
|
|
||||||
|
| Page | Video Display | Status |
|
||||||
|
|------|--------------|--------|
|
||||||
|
| Show Page | Full show video (`show.youtube_link`) | ✅ Working |
|
||||||
|
| Song Page | Top performance video or song video | ✅ Working |
|
||||||
|
| Performance Page | Should show `performance.youtube_link` | ❌ MISSING |
|
||||||
|
| Videos Page | Lists all videos | ✅ Working |
|
||||||
|
|
||||||
|
### Visual Indicators (Current)
|
||||||
|
|
||||||
|
| Location | Indicator | Status |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Setlist items | Video icon for performances with video | ❌ MISSING |
|
||||||
|
| Archive/Show list | Video badge for shows with video | ❌ MISSING |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Performance Page Video Display ⚡ HIGH PRIORITY
|
||||||
|
|
||||||
|
**File:** `frontend/app/performances/[id]/page.tsx`
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
|
||||||
|
1. Import `YouTubeEmbed` component
|
||||||
|
2. Add video section ABOVE the "Version Timeline" card when `performance.youtube_link` exists
|
||||||
|
3. Style consistently with show page video section
|
||||||
|
|
||||||
|
**UI Placement:**
|
||||||
|
|
||||||
|
```
|
||||||
|
[Hero Banner]
|
||||||
|
[VIDEO EMBED] <-- NEW: Only when youtube_link exists
|
||||||
|
[Version Timeline]
|
||||||
|
[About This Performance]
|
||||||
|
[Comments]
|
||||||
|
[Reviews]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Setlist Video Indicators
|
||||||
|
|
||||||
|
**File:** `frontend/app/shows/[id]/page.tsx`
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
|
||||||
|
1. Add small YouTube icon (📹 or `<Youtube>`) next to song title when `perf.youtube_link` exists
|
||||||
|
2. Make icon clickable - links to performance page (where video is embedded)
|
||||||
|
3. Use red color for YouTube brand recognition
|
||||||
|
|
||||||
|
**Visual Design:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Dramophone 📹 >
|
||||||
|
2. The Empress of Organos 📹
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Archive Video Badge
|
||||||
|
|
||||||
|
**File:** `frontend/app/archive/page.tsx` (or show list component)
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
|
||||||
|
1. Add video badge to show cards that have:
|
||||||
|
- `show.youtube_link` (full show video), OR
|
||||||
|
- Any `performance.youtube_link` in their setlist
|
||||||
|
2. API enhancement: Add `has_videos` or `video_count` to show list endpoint
|
||||||
|
|
||||||
|
**Backend Enhancement:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In routers/shows.py - list_shows endpoint
|
||||||
|
# Add computed field: has_videos = show.youtube_link is not None or any performance has youtube_link
|
||||||
|
```
|
||||||
|
|
||||||
|
**Visual Design:**
|
||||||
|
|
||||||
|
- Small YouTube icon in corner of show card
|
||||||
|
- Tooltip: "Full show video available" or "X song videos available"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
YouTube Video → import_youtube.py → Database
|
||||||
|
↓
|
||||||
|
┌──────────────┼──────────────┐
|
||||||
|
↓ ↓ ↓
|
||||||
|
Show.youtube_link Performance.youtube_link Song.youtube_link
|
||||||
|
↓ ↓ ↓
|
||||||
|
Show Page Performance Page Song Page
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Changes Required
|
||||||
|
|
||||||
|
### 1. Shows List Enhancement (Phase 3)
|
||||||
|
|
||||||
|
**Endpoint:** `GET /shows/`
|
||||||
|
|
||||||
|
**New Response Fields:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"date": "2025-12-13T00:00:00",
|
||||||
|
"has_video": true, // NEW: true if show.youtube_link OR any perf.youtube_link
|
||||||
|
"video_count": 3 // NEW: count of performances with videos
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Performance Detail (Already Exists)
|
||||||
|
|
||||||
|
**Endpoint:** `GET /performances/{id}`
|
||||||
|
|
||||||
|
**Verify Field Included:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"youtube_link": "https://www.youtube.com/watch?v=zQI6-LloYwI"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Dramophone 2025-12-13 shows video on performance page
|
||||||
|
- [ ] Empress of Organos 2025-12-13 shows SAME video on performance page
|
||||||
|
- [ ] Setlist on 2025-12-13 show shows video icons for both songs
|
||||||
|
- [ ] Archive view shows video indicator for 2025-12-13 show
|
||||||
|
- [ ] Video page accurately reflects all linked videos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
### Phase 1
|
||||||
|
|
||||||
|
- `frontend/app/performances/[id]/page.tsx` - Add video embed
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
|
||||||
|
- `frontend/app/shows/[id]/page.tsx` - Add video icons to setlist
|
||||||
|
|
||||||
|
### Phase 3
|
||||||
|
|
||||||
|
- `backend/routers/shows.py` - Add has_videos to list response
|
||||||
|
- `frontend/app/archive/page.tsx` - Add video badge to cards
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { ArrowLeft, Calendar, MapPin, ChevronRight, ChevronLeft, Music, Clock, Hash, Play, ExternalLink, Sparkles } from "lucide-react"
|
import { ArrowLeft, Calendar, MapPin, ChevronRight, ChevronLeft, Music, Clock, Hash, Play, ExternalLink, Sparkles, Youtube } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { getApiUrl } from "@/lib/api-config"
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
|
|
@ -9,6 +9,7 @@ import { EntityReviews } from "@/components/reviews/entity-reviews"
|
||||||
import { SocialWrapper } from "@/components/social/social-wrapper"
|
import { SocialWrapper } from "@/components/social/social-wrapper"
|
||||||
import { EntityRating } from "@/components/social/entity-rating"
|
import { EntityRating } from "@/components/social/entity-rating"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { YouTubeEmbed } from "@/components/ui/youtube-embed"
|
||||||
|
|
||||||
async function getPerformance(id: string) {
|
async function getPerformance(id: string) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -141,6 +142,24 @@ export default async function PerformanceDetailPage({ params }: { params: Promis
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Video Section - Show when performance has a video */}
|
||||||
|
{performance.youtube_link && (
|
||||||
|
<Card className="border-2 border-red-500/20 bg-gradient-to-br from-red-500/5 to-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Youtube className="h-5 w-5 text-red-500" />
|
||||||
|
Video
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<YouTubeEmbed
|
||||||
|
url={performance.youtube_link}
|
||||||
|
title={`${performance.song.title} - ${formattedDate}`}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-[1fr_300px]">
|
<div className="grid gap-6 md:grid-cols-[1fr_300px]">
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Version Navigation - Prominent */}
|
{/* Version Navigation - Prominent */}
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,14 @@ export default async function ShowDetailPage({ params }: { params: Promise<{ id:
|
||||||
<PlayCircle className="h-3.5 w-3.5" />
|
<PlayCircle className="h-3.5 w-3.5" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
{perf.youtube_link && (
|
||||||
|
<span
|
||||||
|
className="text-red-500"
|
||||||
|
title="Video available"
|
||||||
|
>
|
||||||
|
<Youtube className="h-3.5 w-3.5" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{perf.segue && <span className="ml-1 text-muted-foreground">></span>}
|
{perf.segue && <span className="ml-1 text-muted-foreground">></span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useEffect, useState, Suspense } from "react"
|
||||||
import { getApiUrl } from "@/lib/api-config"
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Calendar, MapPin, Loader2 } from "lucide-react"
|
import { Calendar, MapPin, Loader2, Youtube } from "lucide-react"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface Show {
|
||||||
id: number
|
id: number
|
||||||
slug?: string
|
slug?: string
|
||||||
date: string
|
date: string
|
||||||
|
youtube_link?: string
|
||||||
venue: {
|
venue: {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
|
@ -84,7 +85,12 @@ function ShowsContent() {
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{shows.map((show) => (
|
{shows.map((show) => (
|
||||||
<Link key={show.id} href={`/shows/${show.slug || show.id}`} className="block group">
|
<Link key={show.id} href={`/shows/${show.slug || show.id}`} className="block group">
|
||||||
<Card className="h-full transition-all duration-300 hover:scale-[1.02] hover:shadow-lg group-hover:border-primary/50">
|
<Card className="h-full transition-all duration-300 hover:scale-[1.02] hover:shadow-lg group-hover:border-primary/50 relative">
|
||||||
|
{show.youtube_link && (
|
||||||
|
<div className="absolute top-2 right-2 bg-red-500/10 text-red-500 p-1.5 rounded-full" title="Full show video available">
|
||||||
|
<Youtube className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 group-hover:text-primary transition-colors">
|
<CardTitle className="flex items-center gap-2 group-hover:text-primary transition-colors">
|
||||||
<Calendar className="h-5 w-5 text-muted-foreground group-hover:text-primary/70 transition-colors" />
|
<Calendar className="h-5 w-5 text-muted-foreground group-hover:text-primary/70 transition-colors" />
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue