""" sync.py — Push/pull synkronisering mellem lokal app og server. POST /sync/push — send lokal data op til server GET /sync/pull — hent server-data ned til app """ from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from pydantic import BaseModel from typing import Optional from app.core.database import get_db from app.core.security import get_current_user from app.models import ( User, Song, Dance, DanceLevel, Project, ProjectSong, PlaylistShare, CommunityDance, CommunityDanceAlt, ) router = APIRouter(prefix="/sync", tags=["sync"]) # ── Schemas ─────────────────────────────────────────────────────────────────── class SongData(BaseModel): local_id: str title: str artist: str = "" album: str = "" bpm: int = 0 duration_sec: int = 0 file_format: str = "" class DanceData(BaseModel): name: str level_name: str = "" choreographer: str = "" video_url: str = "" stepsheet_url: str = "" notes: str = "" class SongDanceData(BaseModel): song_local_id: str dance_name: str level_name: str = "" dance_order: int = 1 class SongAltDanceData(BaseModel): song_local_id: str dance_name: str level_name: str = "" note: str = "" class PlaylistSongData(BaseModel): song_local_id: str position: int status: str = "pending" is_workshop: bool = False dance_override: str = "" class PlaylistData(BaseModel): local_id: str name: str description: str = "" tags: str = "" visibility: str = "private" songs: list[PlaylistSongData] = [] class PushPayload(BaseModel): songs: list[SongData] = [] dances: list[DanceData] = [] song_dances: list[SongDanceData] = [] song_alts: list[SongAltDanceData] = [] playlists: list[PlaylistData] = [] # ── Push ────────────────────────────────────────────────────────────────────── @router.post("/push") def push( payload: PushPayload, db: Session = Depends(get_db), me: User = Depends(get_current_user), ): """Upload lokal data til server. Returnerer server-IDs.""" song_id_map = {} # local_id → server Song.id dance_id_map = {} # "name|level" → server Dance.id level_map = {} # level_name → DanceLevel.id # ── Dans-niveauer ───────────────────────────────────────────────────────── for lvl in db.query(DanceLevel).all(): level_map[lvl.name.lower()] = lvl.id # ── Sange ───────────────────────────────────────────────────────────────── for s in payload.songs: if not s.title: continue # Match globalt på titel+artist — samme sang deles på tværs af brugere existing = db.query(Song).filter( Song.title == s.title, Song.artist == s.artist, ).first() if existing: song_id_map[s.local_id] = existing.id # Opdater BPM hvis det mangler if s.bpm and not existing.bpm: existing.bpm = s.bpm else: song = Song( owner_id=me.id, title=s.title, artist=s.artist, album=s.album, bpm=s.bpm, duration_sec=s.duration_sec, file_format=s.file_format, ) db.add(song) db.flush() song_id_map[s.local_id] = song.id # ── Danse ────────────────────────────────────────────────────────────────── for d in payload.dances: level_id = level_map.get(d.level_name.lower()) if d.level_name else None key = f"{d.name.lower()}|{level_id}" existing = db.query(Dance).filter_by(name=d.name, level_id=level_id).first() if existing: # Opdater info hvis den har ny data if d.choreographer: existing.choreographer = d.choreographer if d.video_url: existing.video_url = d.video_url if d.stepsheet_url: existing.stepsheet_url = d.stepsheet_url if d.notes: existing.notes = d.notes dance_id_map[key] = existing.id else: dance = Dance( name=d.name, level_id=level_id, choreographer=d.choreographer, video_url=d.video_url, stepsheet_url=d.stepsheet_url, notes=d.notes, ) db.add(dance) db.flush() dance_id_map[key] = dance.id # ── Community dans-tags ──────────────────────────────────────────────────── for sd in payload.song_dances: song_id = song_id_map.get(sd.song_local_id) if not song_id: continue song = db.query(Song).filter_by(id=song_id).first() level_id = level_map.get(sd.level_name.lower()) if sd.level_name else None key = f"{sd.dance_name.lower()}|{level_id}" dance_id = dance_id_map.get(key) if not dance_id: continue # Indsend som community dans-tag existing = db.query(CommunityDance).filter_by( song_title=song.title, song_artist=song.artist, dance_id=dance_id ).first() if not existing: cd = CommunityDance( song_title=song.title, song_artist=song.artist, dance_id=dance_id, submitted_by=me.id, ) db.add(cd) # ── Playlister ──────────────────────────────────────────────────────────── playlist_id_map = {} for pl in payload.playlists: existing = db.query(Project).filter_by( owner_id=me.id, name=pl.name ).first() if existing: existing.description = pl.description existing.visibility = pl.visibility # Slet og geninsert sange db.query(ProjectSong).filter_by(project_id=existing.id).delete() project = existing else: project = Project( owner_id=me.id, name=pl.name, description=pl.description, visibility=pl.visibility, ) db.add(project) db.flush() playlist_id_map[pl.local_id] = project.id for ps in pl.songs: song_id = song_id_map.get(ps.song_local_id) if not song_id: continue proj_song = ProjectSong( project_id=project.id, song_id=song_id, position=ps.position, status=ps.status, is_workshop=ps.is_workshop, dance_override=ps.dance_override, ) db.add(proj_song) db.commit() return { "status": "ok", "songs_synced": len(song_id_map), "playlists_synced": len(playlist_id_map), "song_id_map": song_id_map, "playlist_id_map": playlist_id_map, } # ── Pull ────────────────────────────────────────────────────────────────────── @router.get("/pull") def pull( db: Session = Depends(get_db), me: User = Depends(get_current_user), ): """Hent server-data til lokal app.""" # Dans-niveauer levels = [ {"id": l.id, "name": l.name, "sort_order": l.sort_order} for l in db.query(DanceLevel).order_by(DanceLevel.sort_order).all() ] # Danse med info dances = [ { "name": d.name, "level_id": d.level_id, "choreographer": d.choreographer, "video_url": d.video_url, "stepsheet_url": d.stepsheet_url, "notes": d.notes, "use_count": d.use_count, } for d in db.query(Dance).order_by(Dance.use_count.desc()).limit(500).all() ] # Community dans-tags (populære) community = [] for cd in db.query(CommunityDance).limit(1000).all(): community.append({ "song_title": cd.song_title, "song_artist": cd.song_artist, "dance_id": cd.dance_id, }) # Delte playlister shared_ids = [ s.project_id for s in db.query(PlaylistShare).filter_by(shared_with_id=me.id).all() ] shared = [] for p in db.query(Project).filter(Project.id.in_(shared_ids)).all(): shared.append({ "id": p.id, "name": p.name, "owner_id": p.owner_id, "visibility": p.visibility, "songs": [ { "song_id": ps.song_id, "position": ps.position, "status": ps.status, "is_workshop": ps.is_workshop, "dance_override": ps.dance_override, } for ps in p.project_songs ] }) return { "levels": levels, "dances": dances, "community": community, "shared": shared, }