fediversion/backend/importers/phish.py
fullsizemalt b4cddf41ea feat: Initialize Fediversion multi-band platform
- Fork elmeg-demo codebase for multi-band support
- Add data importer infrastructure with base class
- Create band-specific importers:
  - phish.py: Phish.net API v5
  - grateful_dead.py: Grateful Stats API
  - setlistfm.py: Dead & Company, Billy Strings (Setlist.fm)
- Add spec-kit configuration for Gemini
- Update README with supported bands and architecture
2025-12-28 12:39:28 -08:00

246 lines
8 KiB
Python

"""
Phish.net API v5 Importer
Imports Phish setlist data from the official Phish.net API.
Requires API key from https://phish.net/api
API Documentation: https://phish.net/api/docs
"""
import os
from datetime import datetime
from typing import Dict, Optional, List
from sqlmodel import Session
from .base import ImporterBase
class PhishImporter(ImporterBase):
"""Import Phish data from Phish.net API v5"""
VERTICAL_NAME = "Phish"
VERTICAL_SLUG = "phish"
VERTICAL_DESCRIPTION = "Phish is an American rock band from Burlington, Vermont, formed in 1983."
# Phish.net API settings
BASE_URL = "https://api.phish.net/v5"
def __init__(self, session: Session, api_key: Optional[str] = None):
super().__init__(session)
self.api_key = api_key or os.getenv("PHISHNET_API_KEY")
if not self.api_key:
raise ValueError(
"Phish.net API key required. Set PHISHNET_API_KEY environment variable "
"or pass api_key parameter. Get key at https://phish.net/api"
)
def _api_get(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
"""Make API request with key"""
url = f"{self.BASE_URL}/{endpoint}.json"
params = params or {}
params["apikey"] = self.api_key
data = self.fetch_json(url, params)
if data and data.get("error") == 0:
return data.get("data", [])
elif data:
error_msg = data.get("error_message", "Unknown error")
print(f"❌ API Error: {error_msg}")
return None
def import_all(self):
"""Run full import of all Phish data"""
print("=" * 60)
print("PHISH.NET DATA IMPORTER")
print("=" * 60)
# 1. Create/get vertical
self.get_or_create_vertical()
# 2. Import venues
self.import_venues()
# 3. Import songs
self.import_songs()
# 4. Import shows
self.import_shows()
# 5. Import setlists
self.import_setlists()
print("\n" + "=" * 60)
print("✓ PHISH IMPORT COMPLETE!")
print("=" * 60)
print(f"{len(self.venue_map)} venues")
print(f"{len(self.song_map)} songs")
print(f"{len(self.show_map)} shows")
def import_venues(self) -> Dict[str, int]:
"""Import all Phish venues"""
print("\n🏛️ Importing venues...")
# Phish.net doesn't have a dedicated venues endpoint
# Venues are included with show data, so we'll extract them during show import
print(" (Venues will be extracted from show data)")
return self.venue_map
def import_songs(self) -> Dict[str, int]:
"""Import all Phish songs"""
print("\n🎵 Importing songs...")
data = self._api_get("songs")
if not data:
print("❌ Failed to fetch songs")
return self.song_map
for song_data in data:
song_id = str(song_data.get("songid"))
title = song_data.get("song", "Unknown")
original_artist = song_data.get("original_artist")
# Skip if not a real song title
if not title or title == "Unknown":
continue
self.upsert_song(
title=title,
original_artist=original_artist if original_artist != "Phish" else None,
external_id=song_id
)
print(f"✓ Imported {len(self.song_map)} songs")
return self.song_map
def import_shows(self) -> Dict[str, int]:
"""Import all Phish shows"""
print("\n🎤 Importing shows...")
data = self._api_get("shows")
if not data:
print("❌ Failed to fetch shows")
return self.show_map
for show_data in data:
show_id = str(show_data.get("showid"))
show_date_str = show_data.get("showdate")
if not show_date_str:
continue
try:
show_date = datetime.strptime(show_date_str, "%Y-%m-%d")
except ValueError:
continue
# Extract venue info
venue_name = show_data.get("venue", "Unknown Venue")
city = show_data.get("city", "Unknown")
state = show_data.get("state")
country = show_data.get("country", "USA")
venue_id = self.upsert_venue(
name=venue_name,
city=city,
state=state,
country=country,
external_id=f"phish_venue_{venue_name}_{city}"
)
# Handle tour if present
tour_id = None
tour_name = show_data.get("tour_name")
if tour_name:
tour_id = self.upsert_tour(
name=tour_name,
external_id=f"phish_tour_{tour_name}"
)
self.upsert_show(
date=show_date,
venue_id=venue_id,
tour_id=tour_id,
notes=show_data.get("setlistnotes"),
external_id=show_id
)
if len(self.show_map) % 100 == 0:
print(f" Progress: {len(self.show_map)} shows...")
print(f"✓ Imported {len(self.show_map)} shows")
return self.show_map
def import_setlists(self):
"""Import setlists for all shows"""
print("\n📋 Importing setlists...")
performance_count = 0
# Process each show
for external_show_id, internal_show_id in self.show_map.items():
# Fetch setlist for this show by showid
data = self._api_get(f"setlists/showid/{external_show_id}")
if not data:
continue
for perf_data in data:
song_id_ext = str(perf_data.get("songid"))
song_id = self.song_map.get(song_id_ext)
if not song_id:
# Song not in our map, try to add it
song_title = perf_data.get("song")
if song_title:
song_id = self.upsert_song(title=song_title, external_id=song_id_ext)
if not song_id:
continue
# Parse set information
set_name = self._parse_set_name(perf_data.get("set", "1"))
position = perf_data.get("position", 0)
# Check for segue (> symbol in transition)
trans = perf_data.get("trans", "")
segue = ">" in trans if trans else False
self.upsert_performance(
show_id=internal_show_id,
song_id=song_id,
position=position,
set_name=set_name,
segue=segue,
notes=perf_data.get("footnote")
)
performance_count += 1
if len(self.show_map) > 0 and performance_count % 500 == 0:
print(f" Progress: {performance_count} performances...")
print(f"✓ Imported {performance_count} performances")
def _parse_set_name(self, set_value: str) -> str:
"""Convert Phish.net set notation to display name"""
set_map = {
"1": "Set 1",
"2": "Set 2",
"3": "Set 3",
"e": "Encore",
"e2": "Encore 2",
"s": "Soundcheck",
}
return set_map.get(str(set_value).lower(), f"Set {set_value}")
def main():
"""Run Phish import"""
from database import engine
with Session(engine) as session:
importer = PhishImporter(session)
importer.import_all()
if __name__ == "__main__":
main()