NY db struktur
This commit is contained in:
@@ -26,34 +26,30 @@ class User(Base):
|
||||
verify_token: Mapped[str|None] = mapped_column(String(64), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
projects: Mapped[list["Project"]] = relationship("Project", back_populates="owner")
|
||||
memberships: Mapped[list["ProjectMember"]] = relationship("ProjectMember", back_populates="user")
|
||||
songs: Mapped[list["Song"]] = relationship("Song", back_populates="owner")
|
||||
alt_ratings: Mapped[list["DanceAltRating"]] = relationship("DanceAltRating", back_populates="user")
|
||||
playlist_shares: Mapped[list["PlaylistShare"]] = relationship("PlaylistShare", foreign_keys="PlaylistShare.shared_with_id", back_populates="shared_with")
|
||||
projects: Mapped[list["Project"]] = relationship("Project", back_populates="owner")
|
||||
memberships: Mapped[list["ProjectMember"]] = relationship("ProjectMember", back_populates="user")
|
||||
alt_ratings: Mapped[list["DanceAltRating"]] = relationship("DanceAltRating", back_populates="user")
|
||||
playlist_shares: Mapped[list["PlaylistShare"]] = relationship("PlaylistShare", foreign_keys="PlaylistShare.shared_with_id", back_populates="shared_with")
|
||||
|
||||
|
||||
# ── Song ──────────────────────────────────────────────────────────────────────
|
||||
# ── Song (global — ikke knyttet til en bruger) ────────────────────────────────
|
||||
|
||||
class Song(Base):
|
||||
__tablename__ = "songs"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
owner_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
artist: Mapped[str] = mapped_column(String(255), default="")
|
||||
album: Mapped[str] = mapped_column(String(255), default="")
|
||||
bpm: Mapped[int] = mapped_column(Integer, default=0)
|
||||
duration_sec: Mapped[int] = mapped_column(Integer, default=0)
|
||||
file_format: Mapped[str] = mapped_column(String(8), default="")
|
||||
mbid: Mapped[str|None] = mapped_column(String(36), nullable=True)
|
||||
mbid: Mapped[str|None] = mapped_column(String(36), nullable=True, unique=True)
|
||||
acoustid: Mapped[str|None] = mapped_column(String(64), nullable=True)
|
||||
synced_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
owner: Mapped["User"] = relationship("User", back_populates="songs")
|
||||
project_songs: Mapped[list["ProjectSong"]] = relationship("ProjectSong", back_populates="song")
|
||||
song_dances: Mapped[list["SongDance"]] = relationship("SongDance", back_populates="song", cascade="all, delete-orphan")
|
||||
song_alt_dances: Mapped[list["SongAltDance"]] = relationship("SongAltDance", back_populates="song", cascade="all, delete-orphan")
|
||||
project_songs: Mapped[list["ProjectSong"]] = relationship("ProjectSong", back_populates="song")
|
||||
song_dances: Mapped[list["SongDance"]] = relationship("SongDance", back_populates="song", cascade="all, delete-orphan")
|
||||
song_alt_dances: Mapped[list["SongAltDance"]] = relationship("SongAltDance", back_populates="song", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
# ── Dans-entitet ──────────────────────────────────────────────────────────────
|
||||
@@ -68,7 +64,6 @@ class DanceLevel(Base):
|
||||
|
||||
|
||||
class Dance(Base):
|
||||
"""Dans-entitet: navn + niveau er unik kombination."""
|
||||
__tablename__ = "dances"
|
||||
__table_args__ = (UniqueConstraint("name", "level_id", name="uq_dance_name_level"),)
|
||||
|
||||
@@ -80,7 +75,6 @@ class Dance(Base):
|
||||
stepsheet_url: Mapped[str] = mapped_column(String(512), default="")
|
||||
notes: Mapped[str] = mapped_column(Text, default="")
|
||||
use_count: Mapped[int] = mapped_column(Integer, default=1)
|
||||
source: Mapped[str] = mapped_column(String(16), default="local")
|
||||
synced_at: Mapped[datetime|None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
level: Mapped["DanceLevel|None"] = relationship("DanceLevel")
|
||||
@@ -95,7 +89,7 @@ class Project(Base):
|
||||
owner_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, default="")
|
||||
visibility: Mapped[str] = mapped_column(String(16), default="private") # private|shared|public
|
||||
visibility: Mapped[str] = mapped_column(String(16), default="private")
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc, onupdate=now_utc)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
@@ -111,8 +105,8 @@ class ProjectMember(Base):
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False)
|
||||
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(16), default="viewer") # owner|editor|viewer
|
||||
status: Mapped[str] = mapped_column(String(16), default="pending") # pending|accepted
|
||||
role: Mapped[str] = mapped_column(String(16), default="viewer")
|
||||
status: Mapped[str] = mapped_column(String(16), default="pending")
|
||||
invited_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="members")
|
||||
@@ -122,28 +116,27 @@ class ProjectMember(Base):
|
||||
class ProjectSong(Base):
|
||||
__tablename__ = "project_songs"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False)
|
||||
song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(16), default="pending")
|
||||
is_workshop: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
dance_override: Mapped[str] = mapped_column(String(128), default="")
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False)
|
||||
song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(16), default="pending")
|
||||
is_workshop: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
dance_override: Mapped[str] = mapped_column(String(128), default="")
|
||||
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="project_songs")
|
||||
song: Mapped["Song"] = relationship("Song", back_populates="project_songs")
|
||||
|
||||
|
||||
class PlaylistShare(Base):
|
||||
"""Deling af en playlist med specifikke brugere."""
|
||||
__tablename__ = "playlist_shares"
|
||||
__table_args__ = (UniqueConstraint("project_id", "shared_with_id", name="uq_share"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False)
|
||||
shared_with_id: Mapped[str|None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
invited_email: Mapped[str] = mapped_column(String(255), default="") # til ikke-registrerede
|
||||
permission: Mapped[str] = mapped_column(String(16), default="view") # view|copy|edit
|
||||
invited_email: Mapped[str] = mapped_column(String(255), default="")
|
||||
permission: Mapped[str] = mapped_column(String(16), default="view")
|
||||
accepted_at: Mapped[datetime|None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
@@ -154,28 +147,26 @@ class PlaylistShare(Base):
|
||||
# ── Sang-dans tags ────────────────────────────────────────────────────────────
|
||||
|
||||
class SongDance(Base):
|
||||
"""Dans-tags på en sang (brugerens egne tags)."""
|
||||
__tablename__ = "song_dances"
|
||||
__table_args__ = (UniqueConstraint("song_id", "dance_id", name="uq_song_dance"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False)
|
||||
dance_id: Mapped[int] = mapped_column(Integer, ForeignKey("dances.id"), nullable=False)
|
||||
dance_order: Mapped[int] = mapped_column(Integer, default=1)
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False)
|
||||
dance_id: Mapped[int] = mapped_column(Integer, ForeignKey("dances.id"), nullable=False)
|
||||
dance_order: Mapped[int] = mapped_column(Integer, default=1)
|
||||
|
||||
song: Mapped["Song"] = relationship("Song", back_populates="song_dances")
|
||||
dance: Mapped["Dance"] = relationship("Dance")
|
||||
|
||||
|
||||
class SongAltDance(Base):
|
||||
"""Alternativ-dans tags på en sang."""
|
||||
__tablename__ = "song_alt_dances"
|
||||
__table_args__ = (UniqueConstraint("song_id", "dance_id", name="uq_song_alt_dance"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False)
|
||||
dance_id: Mapped[int] = mapped_column(Integer, ForeignKey("dances.id"), nullable=False)
|
||||
note: Mapped[str] = mapped_column(String(255), default="")
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False)
|
||||
dance_id: Mapped[int] = mapped_column(Integer, ForeignKey("dances.id"), nullable=False)
|
||||
note: Mapped[str] = mapped_column(String(255), default="")
|
||||
|
||||
song: Mapped["Song"] = relationship("Song", back_populates="song_alt_dances")
|
||||
dance: Mapped["Dance"] = relationship("Dance")
|
||||
@@ -184,7 +175,6 @@ class SongAltDance(Base):
|
||||
# ── Community dans-tags ───────────────────────────────────────────────────────
|
||||
|
||||
class CommunityDance(Base):
|
||||
"""Fællesskabets dans-tags på sange."""
|
||||
__tablename__ = "community_dances"
|
||||
__table_args__ = (UniqueConstraint("song_mbid", "song_title", "song_artist", "dance_id", name="uq_comm_dance"),)
|
||||
|
||||
@@ -200,7 +190,6 @@ class CommunityDance(Base):
|
||||
|
||||
|
||||
class CommunityDanceAlt(Base):
|
||||
"""Fællesskabets alternativ-danse til en sang med ratings."""
|
||||
__tablename__ = "community_dance_alts"
|
||||
__table_args__ = (UniqueConstraint("song_mbid", "song_title", "song_artist", "alt_dance_id", name="uq_comm_alt"),)
|
||||
|
||||
@@ -215,20 +204,19 @@ class CommunityDanceAlt(Base):
|
||||
rating_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
alt_dance: Mapped["Dance"] = relationship("Dance")
|
||||
alt_dance: Mapped["Dance"] = relationship("Dance")
|
||||
ratings: Mapped[list["DanceAltRating"]] = relationship("DanceAltRating", back_populates="alternative", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class DanceAltRating(Base):
|
||||
"""1-5 stjerne rating af en alternativ-dans."""
|
||||
__tablename__ = "dance_alt_ratings"
|
||||
__table_args__ = (UniqueConstraint("alternative_id", "user_id", name="uq_rating"),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
alternative_id: Mapped[str] = mapped_column(String(36), ForeignKey("community_dance_alts.id"), nullable=False)
|
||||
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
score: Mapped[int] = mapped_column(Integer, nullable=False) # 1-5
|
||||
score: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc)
|
||||
|
||||
alternative: Mapped["CommunityDanceAlt"] = relationship("CommunityDanceAlt", back_populates="ratings")
|
||||
user: Mapped["User"] = relationship("User", back_populates="alt_ratings")
|
||||
user: Mapped["User"] = relationship("User", back_populates="alt_ratings")
|
||||
@@ -4,20 +4,22 @@ 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
|
||||
"""
|
||||
import uuid
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
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,
|
||||
PlaylistShare, CommunityDance, SongDance, SongAltDance,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/sync", tags=["sync"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Schemas ───────────────────────────────────────────────────────────────────
|
||||
@@ -29,7 +31,6 @@ class SongData(BaseModel):
|
||||
album: str = ""
|
||||
bpm: int = 0
|
||||
duration_sec: int = 0
|
||||
file_format: str = ""
|
||||
mbid: str = ""
|
||||
acoustid: str = ""
|
||||
|
||||
@@ -42,16 +43,16 @@ class DanceData(BaseModel):
|
||||
notes: str = ""
|
||||
|
||||
class SongDanceData(BaseModel):
|
||||
song_local_id: str
|
||||
dance_name: str
|
||||
level_name: str = ""
|
||||
dance_order: int = 1
|
||||
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 = ""
|
||||
song_local_id: str
|
||||
dance_name: str
|
||||
level_name: str = ""
|
||||
note: str = ""
|
||||
|
||||
class PlaylistSongData(BaseModel):
|
||||
song_local_id: str
|
||||
@@ -71,12 +72,65 @@ class PlaylistData(BaseModel):
|
||||
songs: list[PlaylistSongData] = []
|
||||
|
||||
class PushPayload(BaseModel):
|
||||
songs: list[SongData] = []
|
||||
dances: list[DanceData] = []
|
||||
song_dances: list[SongDanceData] = []
|
||||
song_alts: list[SongAltDanceData] = []
|
||||
playlists: list[PlaylistData] = []
|
||||
deleted_playlists: list[str] = [] # server-IDs (api_project_id) på slettede playlister
|
||||
songs: list[SongData] = []
|
||||
dances: list[DanceData] = []
|
||||
song_dances: list[SongDanceData] = []
|
||||
song_alts: list[SongAltDanceData] = []
|
||||
playlists: list[PlaylistData] = []
|
||||
deleted_playlists: list[str] = [] # server-IDs (Project.id)
|
||||
|
||||
|
||||
# ── Hjælpefunktion: find eller opret sang globalt ─────────────────────────────
|
||||
|
||||
def _find_or_create_song(db: Session, title: str, artist: str = "",
|
||||
mbid: str = "", acoustid: str = "",
|
||||
album: str = "", bpm: int = 0,
|
||||
duration_sec: int = 0) -> Song:
|
||||
"""
|
||||
Match-hierarki:
|
||||
1. MBID — sikreste
|
||||
2. AcoustID
|
||||
3. Titel + artist
|
||||
4. Opret ny
|
||||
"""
|
||||
if mbid:
|
||||
song = db.query(Song).filter_by(mbid=mbid).first()
|
||||
if song:
|
||||
return song
|
||||
|
||||
if acoustid:
|
||||
song = db.query(Song).filter_by(acoustid=acoustid).first()
|
||||
if song:
|
||||
# Tilføj mbid hvis den mangler
|
||||
if mbid and not song.mbid:
|
||||
song.mbid = mbid
|
||||
return song
|
||||
|
||||
if title:
|
||||
song = db.query(Song).filter(
|
||||
Song.title == title,
|
||||
Song.artist == artist,
|
||||
).first()
|
||||
if song:
|
||||
# Opdater med bedre data hvis tilgængeligt
|
||||
if mbid and not song.mbid:
|
||||
song.mbid = mbid
|
||||
if acoustid and not song.acoustid:
|
||||
song.acoustid = acoustid
|
||||
if bpm and not song.bpm:
|
||||
song.bpm = bpm
|
||||
return song
|
||||
|
||||
# Opret ny global sang
|
||||
song = Song(
|
||||
title=title, artist=artist, album=album,
|
||||
bpm=bpm, duration_sec=duration_sec,
|
||||
mbid=mbid or None,
|
||||
acoustid=acoustid or None,
|
||||
)
|
||||
db.add(song)
|
||||
db.flush()
|
||||
return song
|
||||
|
||||
|
||||
# ── Push ──────────────────────────────────────────────────────────────────────
|
||||
@@ -88,78 +142,50 @@ def push(
|
||||
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
|
||||
import sqlalchemy as _sa
|
||||
|
||||
song_id_map = {} # local_id → server Song.id
|
||||
dance_id_map = {} # "name|level_id" → Dance.id
|
||||
level_map = {} # level_name.lower() → DanceLevel.id
|
||||
|
||||
# ── Dans-niveauer ─────────────────────────────────────────────────────────
|
||||
for lvl in db.query(DanceLevel).all():
|
||||
level_map[lvl.name.lower()] = lvl.id
|
||||
|
||||
# ── Sange ─────────────────────────────────────────────────────────────────
|
||||
# ── Sange (globale) ───────────────────────────────────────────────────────
|
||||
for s in payload.songs:
|
||||
if not s.title:
|
||||
continue
|
||||
# Match 1: MBID — sikrest
|
||||
existing = None
|
||||
if s.mbid:
|
||||
existing = db.query(Song).filter_by(mbid=s.mbid).first()
|
||||
# Match 2: titel+artist globalt
|
||||
if not existing:
|
||||
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 og MBID hvis de mangler
|
||||
if s.bpm and not existing.bpm:
|
||||
existing.bpm = s.bpm
|
||||
if s.mbid and not existing.mbid:
|
||||
existing.mbid = s.mbid
|
||||
if s.acoustid and not existing.acoustid:
|
||||
existing.acoustid = s.acoustid
|
||||
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,
|
||||
mbid=s.mbid or None,
|
||||
acoustid=s.acoustid or None,
|
||||
)
|
||||
db.add(song)
|
||||
db.flush()
|
||||
song_id_map[s.local_id] = song.id
|
||||
song = _find_or_create_song(
|
||||
db, s.title, s.artist,
|
||||
mbid=s.mbid, acoustid=s.acoustid,
|
||||
album=s.album, bpm=s.bpm, duration_sec=s.duration_sec,
|
||||
)
|
||||
song_id_map[s.local_id] = song.id
|
||||
|
||||
# ── Danse ──────────────────────────────────────────────────────────────────
|
||||
# ── 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
|
||||
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
|
||||
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,
|
||||
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
|
||||
|
||||
# ── Sang-dans tags (brugerens egne) ───────────────────────────────────────
|
||||
from app.models import SongDance, SongAltDance
|
||||
# ── Sang-dans tags ────────────────────────────────────────────────────────
|
||||
from app.models import SongDance, SongAltDance
|
||||
import sqlalchemy as _sa
|
||||
|
||||
for sd in payload.song_dances:
|
||||
song_id = song_id_map.get(sd.song_local_id)
|
||||
if not song_id:
|
||||
@@ -172,12 +198,8 @@ def push(
|
||||
db.execute(_sa.text(
|
||||
"INSERT IGNORE INTO song_dances (id, song_id, dance_id, dance_order) "
|
||||
"VALUES (:id, :song_id, :dance_id, :dance_order)"
|
||||
), {
|
||||
"id": str(__import__("uuid").uuid4()),
|
||||
"song_id": song_id,
|
||||
"dance_id": dance_id,
|
||||
"dance_order": sd.dance_order,
|
||||
})
|
||||
), {"id": str(uuid.uuid4()), "song_id": song_id,
|
||||
"dance_id": dance_id, "dance_order": sd.dance_order})
|
||||
|
||||
for sa in payload.song_alts:
|
||||
song_id = song_id_map.get(sa.song_local_id)
|
||||
@@ -191,35 +213,27 @@ def push(
|
||||
db.execute(_sa.text(
|
||||
"INSERT IGNORE INTO song_alt_dances (id, song_id, dance_id, note) "
|
||||
"VALUES (:id, :song_id, :dance_id, :note)"
|
||||
), {
|
||||
"id": str(__import__("uuid").uuid4()),
|
||||
"song_id": song_id,
|
||||
"dance_id": dance_id,
|
||||
"note": sa.note or "",
|
||||
})
|
||||
), {"id": str(uuid.uuid4()), "song_id": song_id,
|
||||
"dance_id": dance_id, "note": sa.note or ""})
|
||||
|
||||
# ── Playlister ────────────────────────────────────────────────────────────
|
||||
# VIGTIGT: Match altid på local_id (= api_project_id på klienten),
|
||||
# aldrig på navn — navn er ikke unikt og giver duplikater.
|
||||
playlist_id_map = {}
|
||||
for pl in payload.playlists:
|
||||
# Prøv først at finde via server-ID (local_id er klientens lokale db-id
|
||||
# som tidligere er returneret som server-ID via playlist_id_map)
|
||||
# Find eksisterende via server-ID (local_id er api_project_id på klienten)
|
||||
existing = None
|
||||
if pl.local_id:
|
||||
existing = db.query(Project).filter_by(
|
||||
id=pl.local_id, owner_id=me.id
|
||||
).first()
|
||||
# Fallback: navn — kun hvis vi aldrig har set denne liste før
|
||||
if not existing:
|
||||
existing = db.query(Project).filter_by(
|
||||
owner_id=me.id, name=pl.name
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
existing.name = pl.name
|
||||
existing.description = pl.description
|
||||
existing.visibility = pl.visibility
|
||||
# Opdater kun sange hvis push faktisk har sange med
|
||||
if pl.songs:
|
||||
db.query(ProjectSong).filter_by(project_id=existing.id).delete()
|
||||
project = existing
|
||||
@@ -233,27 +247,21 @@ def push(
|
||||
playlist_id_map[pl.local_id] = project.id
|
||||
|
||||
for ps in pl.songs:
|
||||
# Prøv først via song_id_map (lokal ID)
|
||||
# Find sang via song_id_map eller titel+artist
|
||||
song_id = song_id_map.get(ps.song_local_id)
|
||||
# Fallback: match på titel+artist
|
||||
if not song_id and ps.song_title:
|
||||
existing_song = db.query(Song).filter_by(
|
||||
title=ps.song_title, artist=ps.song_artist
|
||||
).first()
|
||||
if existing_song:
|
||||
song_id = existing_song.id
|
||||
song = _find_or_create_song(db, ps.song_title, ps.song_artist)
|
||||
song_id = song.id
|
||||
if not song_id:
|
||||
continue
|
||||
proj_song = ProjectSong(
|
||||
db.add(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)
|
||||
))
|
||||
|
||||
# ── Slet playlister der er fjernet lokalt ─────────────────────────────────
|
||||
# Klienten sender api_project_id (= server Project.id) som strings
|
||||
# ── Slet playlister ───────────────────────────────────────────────────────
|
||||
for project_id in payload.deleted_playlists:
|
||||
proj = db.query(Project).filter_by(id=project_id, owner_id=me.id).first()
|
||||
if proj:
|
||||
@@ -266,6 +274,7 @@ def push(
|
||||
"status": "ok",
|
||||
"songs_synced": len(song_id_map),
|
||||
"playlists_synced": len(playlist_id_map),
|
||||
"song_id_map": {k: str(v) for k, v in song_id_map.items()},
|
||||
"playlist_id_map": {k: str(v) for k, v in playlist_id_map.items()},
|
||||
}
|
||||
|
||||
@@ -285,99 +294,93 @@ def pull(
|
||||
for l in db.query(DanceLevel).order_by(DanceLevel.sort_order).all()
|
||||
]
|
||||
|
||||
# Danse med info
|
||||
# Danse
|
||||
dances = [
|
||||
{
|
||||
"name": d.name,
|
||||
"level_id": d.level_id,
|
||||
"name": d.name,
|
||||
"level_id": d.level_id,
|
||||
"choreographer": d.choreographer,
|
||||
"video_url": d.video_url,
|
||||
"video_url": d.video_url,
|
||||
"stepsheet_url": d.stepsheet_url,
|
||||
"notes": d.notes,
|
||||
"use_count": d.use_count,
|
||||
"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 (read-only — kun ejeren kan redigere)
|
||||
shared_ids = set()
|
||||
for s in db.query(PlaylistShare).filter(
|
||||
(PlaylistShare.shared_with_id == me.id) |
|
||||
(PlaylistShare.invited_email == me.email)
|
||||
).all():
|
||||
shared_ids.add(s.project_id)
|
||||
# Delte playlister
|
||||
shared_ids = {
|
||||
s.project_id for s in db.query(PlaylistShare).filter(
|
||||
(PlaylistShare.shared_with_id == me.id) |
|
||||
(PlaylistShare.invited_email == me.email)
|
||||
).all()
|
||||
}
|
||||
|
||||
shared = []
|
||||
for p in db.query(Project).filter(Project.id.in_(shared_ids)).all():
|
||||
if p.owner_id == me.id:
|
||||
continue # Egne lister håndteres separat
|
||||
continue
|
||||
owner = db.query(User).filter_by(id=p.owner_id).first()
|
||||
songs_out = []
|
||||
for ps in p.project_songs:
|
||||
song = db.query(Song).filter_by(id=ps.song_id).first()
|
||||
if not song:
|
||||
continue
|
||||
songs_out.append({
|
||||
"title": song.title,
|
||||
"artist": song.artist,
|
||||
"position": ps.position,
|
||||
"status": ps.status,
|
||||
"is_workshop": ps.is_workshop,
|
||||
"dance_override": ps.dance_override or "",
|
||||
})
|
||||
shared.append({
|
||||
"server_id": p.id,
|
||||
"name": p.name,
|
||||
"owner": owner.username if owner else "?",
|
||||
"songs": sorted(songs_out, key=lambda x: x["position"]),
|
||||
"songs": [
|
||||
{
|
||||
"song_id": str(ps.song_id),
|
||||
"title": ps.song.title,
|
||||
"artist": ps.song.artist,
|
||||
"mbid": ps.song.mbid or "",
|
||||
"acoustid": ps.song.acoustid or "",
|
||||
"bpm": ps.song.bpm,
|
||||
"duration_sec": ps.song.duration_sec,
|
||||
"position": ps.position,
|
||||
"status": ps.status,
|
||||
"is_workshop": ps.is_workshop,
|
||||
"dance_override": ps.dance_override or "",
|
||||
}
|
||||
for ps in sorted(p.project_songs, key=lambda x: x.position)
|
||||
if ps.song
|
||||
],
|
||||
})
|
||||
|
||||
# Egne playlister
|
||||
my_playlists = []
|
||||
all_projects = db.query(Project).filter_by(owner_id=me.id).all()
|
||||
import logging
|
||||
logging.getLogger(__name__).info(f"Pull: fandt {len(all_projects)} projekter for {me.id}")
|
||||
for p in all_projects:
|
||||
songs_out = []
|
||||
for ps in p.project_songs:
|
||||
song = db.query(Song).filter_by(id=ps.song_id).first()
|
||||
if not song:
|
||||
continue
|
||||
songs_out.append({
|
||||
"title": song.title,
|
||||
"artist": song.artist,
|
||||
"position": ps.position,
|
||||
"status": ps.status,
|
||||
"is_workshop": ps.is_workshop,
|
||||
"dance_override": ps.dance_override or "",
|
||||
})
|
||||
for p in db.query(Project).filter_by(owner_id=me.id).all():
|
||||
my_playlists.append({
|
||||
"server_id": p.id,
|
||||
"name": p.name,
|
||||
"description": p.description or "",
|
||||
"songs": sorted(songs_out, key=lambda x: x["position"]),
|
||||
"songs": [
|
||||
{
|
||||
"song_id": str(ps.song_id),
|
||||
"title": ps.song.title,
|
||||
"artist": ps.song.artist,
|
||||
"mbid": ps.song.mbid or "",
|
||||
"acoustid": ps.song.acoustid or "",
|
||||
"bpm": ps.song.bpm,
|
||||
"duration_sec": ps.song.duration_sec,
|
||||
"position": ps.position,
|
||||
"status": ps.status,
|
||||
"is_workshop": ps.is_workshop,
|
||||
"dance_override": ps.dance_override or "",
|
||||
}
|
||||
for ps in sorted(p.project_songs, key=lambda x: x.position)
|
||||
if ps.song
|
||||
],
|
||||
})
|
||||
|
||||
# Brugerens egne dans-tags
|
||||
from app.models import SongDance, SongAltDance
|
||||
logger.info(f"Pull: {len(my_playlists)} playlister for {me.username}")
|
||||
|
||||
# Dans-tags (brugerens egne)
|
||||
song_tags = []
|
||||
for sd in db.query(SongDance).join(Song).filter(Song.owner_id == me.id).all():
|
||||
for sd in db.query(SongDance).all():
|
||||
dance = db.query(Dance).filter_by(id=sd.dance_id).first()
|
||||
if not dance:
|
||||
continue
|
||||
level = db.query(DanceLevel).filter_by(id=dance.level_id).first() if dance.level_id else None
|
||||
song_tags.append({
|
||||
"song_title": sd.song.title,
|
||||
"song_artist": sd.song.artist,
|
||||
"song_id": sd.song_id,
|
||||
"dance_name": dance.name,
|
||||
"level_name": level.name if level else "",
|
||||
"dance_order": sd.dance_order,
|
||||
@@ -386,7 +389,6 @@ def pull(
|
||||
return {
|
||||
"levels": levels,
|
||||
"dances": dances,
|
||||
"community": community,
|
||||
"shared": shared,
|
||||
"my_playlists": my_playlists,
|
||||
"song_tags": song_tags,
|
||||
|
||||
Reference in New Issue
Block a user