NY db struktur

This commit is contained in:
2026-04-19 23:45:59 +02:00
parent a9aa451d63
commit efc30cdbb2
6 changed files with 1056 additions and 1390 deletions

View File

@@ -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")

View File

@@ -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,