From 1f29cdf290ea78f54f4111aed75a6a1d7f592bf4 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:56:21 -0800 Subject: [PATCH] feat: Bandcamp/Nugs links for shows and performances - Add bandcamp_link, nugs_link to Performance model - Admin endpoints: PATCH /admin/performances/{id} - Bulk import: POST /admin/import/external-links - Spec doc: docs/BANDCAMP_NUGS_SPEC.md --- backend/models.py | 2 + backend/routers/admin.py | 111 +++++++++++++++++++ docs/BANDCAMP_NUGS_SPEC.md | 217 +++++++++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 docs/BANDCAMP_NUGS_SPEC.md diff --git a/backend/models.py b/backend/models.py index 18717e8..6c1e13f 100644 --- a/backend/models.py +++ b/backend/models.py @@ -15,6 +15,8 @@ class Performance(SQLModel, table=True): notes: Optional[str] = Field(default=None) track_url: Optional[str] = Field(default=None, description="Deep link to track audio") youtube_link: Optional[str] = Field(default=None, description="YouTube video URL") + bandcamp_link: Optional[str] = Field(default=None, description="Bandcamp track URL") + nugs_link: Optional[str] = Field(default=None, description="Nugs.net track URL") nicknames: List["PerformanceNickname"] = Relationship(back_populates="performance") show: "Show" = Relationship(back_populates="performances") diff --git a/backend/routers/admin.py b/backend/routers/admin.py index dc45d92..b651018 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -432,3 +432,114 @@ def delete_tour( session.delete(tour) session.commit() return {"message": "Tour deleted", "tour_id": tour_id} + + +# ============ PERFORMANCES ============ + +from models import Performance + +class PerformanceUpdate(BaseModel): + notes: Optional[str] = None + youtube_link: Optional[str] = None + bandcamp_link: Optional[str] = None + nugs_link: Optional[str] = None + track_url: Optional[str] = None + + +@router.patch("/performances/{performance_id}") +def update_performance( + performance_id: int, + update: PerformanceUpdate, + session: Session = Depends(get_session), + _: User = Depends(allow_admin) +): + """Update performance links and notes""" + performance = session.get(Performance, performance_id) + if not performance: + raise HTTPException(status_code=404, detail="Performance not found") + + for key, value in update.model_dump(exclude_unset=True).items(): + setattr(performance, key, value) + + session.add(performance) + session.commit() + session.refresh(performance) + return performance + + +@router.get("/performances/{performance_id}") +def get_performance( + performance_id: int, + session: Session = Depends(get_session), + _: User = Depends(allow_admin) +): + """Get performance details for admin""" + performance = session.get(Performance, performance_id) + if not performance: + raise HTTPException(status_code=404, detail="Performance not found") + + return { + "id": performance.id, + "slug": performance.slug, + "show_id": performance.show_id, + "song_id": performance.song_id, + "position": performance.position, + "set_name": performance.set_name, + "notes": performance.notes, + "youtube_link": performance.youtube_link, + "bandcamp_link": performance.bandcamp_link, + "nugs_link": performance.nugs_link, + "track_url": performance.track_url, + } + + +class BulkLinksImport(BaseModel): + links: List[dict] # {"show_id": 1, "platform": "nugs", "url": "..."} or {"performance_id": 1, ...} + + +@router.post("/import/external-links") +def bulk_import_links( + data: BulkLinksImport, + session: Session = Depends(get_session), + _: User = Depends(allow_admin) +): + """Bulk import external links for shows and performances""" + updated_shows = 0 + updated_performances = 0 + errors = [] + + for item in data.links: + platform = item.get("platform", "").lower() + url = item.get("url") + + if not platform or not url: + errors.append({"item": item, "error": "Missing platform or url"}) + continue + + field_name = f"{platform}_link" + + if "show_id" in item: + show = session.get(Show, item["show_id"]) + if show and hasattr(show, field_name): + setattr(show, field_name, url) + session.add(show) + updated_shows += 1 + else: + errors.append({"item": item, "error": "Show not found or invalid platform"}) + + elif "performance_id" in item: + perf = session.get(Performance, item["performance_id"]) + if perf and hasattr(perf, field_name): + setattr(perf, field_name, url) + session.add(perf) + updated_performances += 1 + else: + errors.append({"item": item, "error": "Performance not found or invalid platform"}) + + session.commit() + + return { + "updated_shows": updated_shows, + "updated_performances": updated_performances, + "errors": errors + } diff --git a/docs/BANDCAMP_NUGS_SPEC.md b/docs/BANDCAMP_NUGS_SPEC.md new file mode 100644 index 0000000..5beed0f --- /dev/null +++ b/docs/BANDCAMP_NUGS_SPEC.md @@ -0,0 +1,217 @@ +# Bandcamp & Nugs Integration Spec + +**Date:** 2023-12-23 +**Purpose:** Link shows and performances to official audio sources + +--- + +## Overview + +Add support for linking to official audio releases on: + +- **Bandcamp** - Official studio/live releases, digital purchases +- **Nugs.net** - Live show streams/downloads, SBD recordings + +--- + +## Database Changes + +### Option A: Simple Link Fields (MVP) + +Add to existing models: + +```python +# Show model +class Show(SQLModel, table=True): + # ... existing fields ... + nugs_link: Optional[str] = None # Full Nugs.net URL + bandcamp_link: Optional[str] = None # Full Bandcamp URL + +# Performance model +class Performance(SQLModel, table=True): + # ... existing fields ... + nugs_link: Optional[str] = None # Link to specific track on Nugs + bandcamp_link: Optional[str] = None # Link to specific track on Bandcamp +``` + +### Option B: Structured Link Table (Future) + +For more flexibility: + +```python +class ExternalLink(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + # Polymorphic reference + entity_type: str # "show" | "performance" | "song" + entity_id: int + + # Link info + platform: str # "nugs" | "bandcamp" | "youtube" | "archive" | "spotify" + url: str + label: Optional[str] = None # Custom label like "SBD Recording" + is_official: bool = True + + created_at: datetime + created_by: Optional[int] # User who added it +``` + +--- + +## API Endpoints + +### Update Show (Admin) + +``` +PATCH /admin/shows/{id} +{ + "nugs_link": "https://nugs.net/...", + "bandcamp_link": "https://bandcamp.com/..." +} +``` + +### Update Performance (Admin) + +``` +PATCH /admin/performances/{id} +{ + "nugs_link": "https://nugs.net/...", + "bandcamp_link": "https://bandcamp.com/..." +} +``` + +### Bulk Import Links + +``` +POST /admin/import/external-links +{ + "links": [ + {"show_id": 123, "platform": "nugs", "url": "..."}, + {"performance_id": 456, "platform": "bandcamp", "url": "..."} + ] +} +``` + +--- + +## Frontend Display + +### Show Page + +``` +┌────────────────────────────────────────────┐ +│ 📅 December 13, 2025 @ The Anthem │ +│ │ +│ [▶ Watch on YouTube] [🎧 Nugs] [🎵 Bandcamp]│ +│ │ +│ Set 1: │ +│ 1. Song Title [🎧] [🎵] ← per-track │ +│ 2. Another Song │ +└────────────────────────────────────────────┘ +``` + +### Icon/Button Design + +```tsx +const PLATFORM_ICONS = { + nugs: { icon: Headphones, label: "Nugs.net", color: "#ff6b00" }, + bandcamp: { icon: Music, label: "Bandcamp", color: "#629aa9" }, + youtube: { icon: Youtube, label: "YouTube", color: "#ff0000" }, +} +``` + +--- + +## Data Sources + +### Nugs.net + +- Shows listed at: ... +- Direct track links available +- May have SBD vs AUD quality indicators + +### Bandcamp + +- Live releases often on artist's Bandcamp +- Track-level linking possible +- May include "pay what you want" vs fixed price + +--- + +## Import Workflow + +### Manual Entry (Admin UI) + +1. Admin navigates to show/performance +2. Clicks "Add External Links" +3. Pastes URL, selects platform +4. Saves + +### Bulk CSV Import + +```csv +show_date,platform,url +2024-12-13,nugs,https://nugs.net/live/... +2024-12-13,bandcamp,https://bandcamp.com/album/... +``` + +### API Scraping (Future) + +- Could auto-detect new releases on Nugs +- Match by date/venue +- Requires API access or web scraping + +--- + +## Implementation Phases + +### Phase 1: MVP (Database + Admin) + +- [ ] Add `nugs_link`, `bandcamp_link` to Show model +- [ ] Add `nugs_link`, `bandcamp_link` to Performance model +- [ ] Run migration +- [ ] Add admin endpoints to update links + +### Phase 2: Frontend Display + +- [ ] Show links on Show page (next to YouTube) +- [ ] Show links on Performance rows +- [ ] Add platform icons/colors + +### Phase 3: Import Tools + +- [ ] CSV import endpoint +- [ ] Admin bulk edit UI + +### Phase 4: User Features (Optional) + +- [ ] Users can suggest links (moderated) +- [ ] "I own this" tracking for collectors + +--- + +## Estimated Effort + +| Phase | Time | +|-------|------| +| Phase 1 | 30 min | +| Phase 2 | 1 hour | +| Phase 3 | 1 hour | +| Phase 4 | 2+ hours | + +--- + +## Questions to Resolve + +1. **Should users be able to add links?** Or admin-only? +2. **Verify link quality?** Some Nugs links are AUD, some SBD +3. **Other platforms?** Spotify, Apple Music, Archive.org, Relisten? +4. **Affiliate links?** If monetizing, need affiliate program setup + +--- + +## Next Steps + +1. Run database migration +2. Add admin API endpoints +3. Update Show page UI with link buttons