Rettelsaer
This commit is contained in:
274
linedance-api/app/routers/sync.py
Normal file
274
linedance-api/app/routers/sync.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user