Files
LinedanceAfspiller/linedance-api/app/routers/sync.py
2026-04-13 07:23:37 +02:00

275 lines
9.8 KiB
Python

"""
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,
}