This commit is contained in:
2026-04-11 00:32:36 +02:00
parent 4771d99186
commit c90159d9da
12 changed files with 0 additions and 766 deletions

View File

@@ -1,235 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func
from pydantic import BaseModel
from app.core.database import get_db
from app.core.security import get_current_user
from app.models import User, SongDance, DanceAlternative, DanceAlternativeRating
router = APIRouter(prefix="/alternatives", tags=["alternatives"])
# Bayesiansk minimum — alternativer med færre ratings trækkes mod gennemsnittet
BAYESIAN_MIN_VOTES = 5
# ── Schemas ───────────────────────────────────────────────────────────────────
class AlternativeCreate(BaseModel):
song_dance_id: str # dans der foreslås alternativ TIL
alt_song_dance_id: str # den alternative dans
note: str = ""
class AlternativeOut(BaseModel):
id: str
song_dance_id: str
alt_song_dance_id: str
alt_dance_name: str
alt_song_title: str
created_by_username: str
note: str
my_score: int | None # den indloggede brugers egen rating
avg_score: float | None # simpelt gennemsnit (til visning)
bayesian_score: float # bruges til sortering
rating_count: int
model_config = {"from_attributes": True}
class RatingUpsert(BaseModel):
score: int # 1-5
# ── Hjælpefunktion: genberegn bayesian score ─────────────────────────────────
def _recalculate_bayesian(alternative: DanceAlternative, db: Session):
"""
Bayesiansk score: vægter gennemsnittet mod et globalt gennemsnit
når der er få ratings, så nye alternativer ikke dominerer listen.
Formel: (n × avg + m × global_avg) / (n + m)
n = antal ratings på dette alternativ
avg = gennemsnit for dette alternativ
m = BAYESIAN_MIN_VOTES (tillid-konstant)
global_avg = gennemsnit på tværs af ALLE ratings
"""
# Beregn stats for dette alternativ
result = db.query(
func.count(DanceAlternativeRating.id),
func.avg(DanceAlternativeRating.score),
).filter(DanceAlternativeRating.alternative_id == alternative.id).one()
n = result[0] or 0
avg = float(result[1]) if result[1] else 0.0
# Globalt gennemsnit på tværs af alle ratings
global_avg_result = db.query(func.avg(DanceAlternativeRating.score)).scalar()
global_avg = float(global_avg_result) if global_avg_result else 3.0 # 3.0 som neutral fallback
m = BAYESIAN_MIN_VOTES
bayesian = (n * avg + m * global_avg) / (n + m) if (n + m) > 0 else global_avg
alternative.bayesian_score = round(bayesian, 4)
db.flush()
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.post("/", status_code=201)
def create_alternative(
data: AlternativeCreate,
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
"""Opret et nyt alternativ-dans forslag. Alle registrerede brugere kan bidrage."""
dance = db.query(SongDance).filter(SongDance.id == data.song_dance_id).first()
if not dance:
raise HTTPException(404, "Dans ikke fundet")
alt_dance = db.query(SongDance).filter(SongDance.id == data.alt_song_dance_id).first()
if not alt_dance:
raise HTTPException(404, "Alternativ-dans ikke fundet")
if data.song_dance_id == data.alt_song_dance_id:
raise HTTPException(400, "En dans kan ikke være sit eget alternativ")
# Undgå dubletter fra samme bruger
existing = db.query(DanceAlternative).filter_by(
song_dance_id=data.song_dance_id,
alt_song_dance_id=data.alt_song_dance_id,
created_by=me.id,
).first()
if existing:
raise HTTPException(400, "Du har allerede foreslået dette alternativ")
alt = DanceAlternative(
song_dance_id=data.song_dance_id,
alt_song_dance_id=data.alt_song_dance_id,
created_by=me.id,
note=data.note,
bayesian_score=3.0, # starter på globalt neutral
)
db.add(alt)
db.commit()
db.refresh(alt)
return {"id": alt.id, "detail": "Alternativ oprettet"}
@router.get("/for-dance/{song_dance_id}", response_model=list[AlternativeOut])
def list_alternatives_for_dance(
song_dance_id: str,
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
"""
Hent alle alternativer til en given dans, sorteret efter bayesiansk score.
Viser din egen rating og gennemsnittet.
"""
alternatives = (
db.query(DanceAlternative)
.filter(DanceAlternative.song_dance_id == song_dance_id)
.order_by(DanceAlternative.bayesian_score.desc())
.all()
)
result = []
for alt in alternatives:
# Din egen rating
my_rating = db.query(DanceAlternativeRating).filter_by(
alternative_id=alt.id, user_id=me.id
).first()
# Aggregeret stats
stats = db.query(
func.count(DanceAlternativeRating.id),
func.avg(DanceAlternativeRating.score),
).filter(DanceAlternativeRating.alternative_id == alt.id).one()
result.append(AlternativeOut(
id=alt.id,
song_dance_id=alt.song_dance_id,
alt_song_dance_id=alt.alt_song_dance_id,
alt_dance_name=alt.alt_song_dance.dance_name,
alt_song_title=alt.alt_song_dance.song.title,
created_by_username=alt.creator.username,
note=alt.note,
my_score=my_rating.score if my_rating else None,
avg_score=round(float(stats[1]), 1) if stats[1] else None,
bayesian_score=alt.bayesian_score,
rating_count=stats[0] or 0,
))
return result
@router.put("/{alternative_id}/rate")
def rate_alternative(
alternative_id: str,
data: RatingUpsert,
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
"""Sæt eller opdater din rating (1-5) på et alternativ."""
if not 1 <= data.score <= 5:
raise HTTPException(400, "Score skal være mellem 1 og 5")
alt = db.query(DanceAlternative).filter(DanceAlternative.id == alternative_id).first()
if not alt:
raise HTTPException(404, "Alternativ ikke fundet")
# Upsert — opdater eksisterende rating eller opret ny
existing = db.query(DanceAlternativeRating).filter_by(
alternative_id=alternative_id, user_id=me.id
).first()
if existing:
existing.score = data.score
else:
db.add(DanceAlternativeRating(
alternative_id=alternative_id,
user_id=me.id,
score=data.score,
))
db.flush()
_recalculate_bayesian(alt, db)
db.commit()
return {
"detail": "Rating gemt",
"my_score": data.score,
"bayesian_score": alt.bayesian_score,
}
@router.delete("/{alternative_id}/rate", status_code=204)
def remove_rating(
alternative_id: str,
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
"""Fjern din rating fra et alternativ."""
rating = db.query(DanceAlternativeRating).filter_by(
alternative_id=alternative_id, user_id=me.id
).first()
if not rating:
raise HTTPException(404, "Du har ikke rated dette alternativ")
alt = db.query(DanceAlternative).filter(DanceAlternative.id == alternative_id).first()
db.delete(rating)
db.flush()
_recalculate_bayesian(alt, db)
db.commit()
@router.delete("/{alternative_id}", status_code=204)
def delete_alternative(
alternative_id: str,
db: Session = Depends(get_db),
me: User = Depends(get_current_user),
):
"""Slet et alternativ — kun den der oprettede det."""
alt = db.query(DanceAlternative).filter(DanceAlternative.id == alternative_id).first()
if not alt:
raise HTTPException(404, "Alternativ ikke fundet")
if alt.created_by != me.id:
raise HTTPException(403, "Du kan kun slette dine egne forslag")
db.delete(alt)
db.commit()

View File

@@ -1,39 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import hash_password, verify_password, create_access_token
from app.models import User
from app.schemas import UserCreate, UserOut, Token
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", response_model=UserOut, status_code=201)
def register(data: UserCreate, db: Session = Depends(get_db)):
if db.query(User).filter(User.username == data.username).first():
raise HTTPException(400, "Brugernavnet er allerede i brug")
if db.query(User).filter(User.email == data.email).first():
raise HTTPException(400, "E-mailen er allerede i brug")
user = User(
username=data.username,
email=data.email,
password_hash=hash_password(data.password),
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.post("/login", response_model=Token)
def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == form.username).first()
if not user or not verify_password(form.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Forkert brugernavn eller kodeord",
)
token = create_access_token({"sub": user.id})
return {"access_token": token}

View File

@@ -1,190 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import get_current_user
from app.models import User, Project, ProjectMember, ProjectSong, Song
from app.schemas import (
ProjectCreate, ProjectUpdate, ProjectOut,
InviteMember, ProjectSongAdd, ProjectSongStatusUpdate, ProjectSongOut,
)
router = APIRouter(prefix="/projects", tags=["projects"])
def _get_project_or_404(project_id: str, db: Session) -> Project:
p = db.query(Project).filter(Project.id == project_id).first()
if not p:
raise HTTPException(404, "Projekt ikke fundet")
return p
def _assert_role(project: Project, user: User, db: Session, min_role: str = "viewer"):
roles = ["viewer", "editor", "owner"]
if project.owner_id == user.id:
return # ejer har altid adgang
member = db.query(ProjectMember).filter_by(project_id=project.id, user_id=user.id, status="accepted").first()
if not member:
if project.is_public and min_role == "viewer":
return
raise HTTPException(403, "Du har ikke adgang til dette projekt")
if roles.index(member.role) < roles.index(min_role):
raise HTTPException(403, "Din rolle giver ikke rettighed til dette")
# ── CRUD ──────────────────────────────────────────────────────────────────────
@router.get("/", response_model=list[ProjectOut])
def list_projects(db: Session = Depends(get_db), me: User = Depends(get_current_user)):
owned = db.query(Project).filter(Project.owner_id == me.id).all()
member_ids = [m.project_id for m in db.query(ProjectMember).filter_by(user_id=me.id, status="accepted").all()]
shared = db.query(Project).filter(Project.id.in_(member_ids)).all()
return list({p.id: p for p in owned + shared}.values())
@router.post("/", response_model=ProjectOut, status_code=201)
def create_project(data: ProjectCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
project = Project(owner_id=me.id, **data.model_dump())
db.add(project)
db.commit()
db.refresh(project)
return project
@router.get("/{project_id}", response_model=ProjectOut)
def get_project(project_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
p = _get_project_or_404(project_id, db)
_assert_role(p, me, db, "viewer")
return p
@router.patch("/{project_id}", response_model=ProjectOut)
def update_project(project_id: str, data: ProjectUpdate, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
p = _get_project_or_404(project_id, db)
_assert_role(p, me, db, "editor")
for field, val in data.model_dump(exclude_none=True).items():
setattr(p, field, val)
db.commit()
db.refresh(p)
return p
@router.delete("/{project_id}", status_code=204)
def delete_project(project_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
p = _get_project_or_404(project_id, db)
if p.owner_id != me.id:
raise HTTPException(403, "Kun ejeren kan slette projektet")
db.delete(p)
db.commit()
# ── Invitationer ──────────────────────────────────────────────────────────────
@router.post("/{project_id}/invite", status_code=201)
def invite_member(project_id: str, data: InviteMember, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
p = _get_project_or_404(project_id, db)
if p.owner_id != me.id:
raise HTTPException(403, "Kun ejeren kan invitere")
target = db.query(User).filter(User.username == data.username).first()
if not target:
raise HTTPException(404, f"Brugeren '{data.username}' findes ikke")
if target.id == me.id:
raise HTTPException(400, "Du kan ikke invitere dig selv")
existing = db.query(ProjectMember).filter_by(project_id=project_id, user_id=target.id).first()
if existing:
raise HTTPException(400, "Brugeren er allerede inviteret eller medlem")
member = ProjectMember(project_id=project_id, user_id=target.id, role=data.role, status="pending")
db.add(member)
db.commit()
return {"detail": f"{data.username} er inviteret som {data.role}"}
@router.get("/invitations/pending")
def get_pending_invitations(db: Session = Depends(get_db), me: User = Depends(get_current_user)):
invitations = db.query(ProjectMember).filter_by(user_id=me.id, status="pending").all()
return [
{"invitation_id": inv.id, "project_id": inv.project_id, "role": inv.role, "invited_at": inv.invited_at}
for inv in invitations
]
@router.post("/invitations/{invitation_id}/accept")
def accept_invitation(invitation_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
inv = db.query(ProjectMember).filter_by(id=invitation_id, user_id=me.id).first()
if not inv:
raise HTTPException(404, "Invitation ikke fundet")
inv.status = "accepted"
db.commit()
return {"detail": "Invitation accepteret"}
@router.delete("/invitations/{invitation_id}")
def decline_invitation(invitation_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
inv = db.query(ProjectMember).filter_by(id=invitation_id, user_id=me.id).first()
if not inv:
raise HTTPException(404, "Invitation ikke fundet")
db.delete(inv)
db.commit()
return {"detail": "Invitation afvist"}
# ── Danseliste (ProjectSongs) ─────────────────────────────────────────────────
@router.get("/{project_id}/songs", response_model=list[ProjectSongOut])
def list_project_songs(project_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
p = _get_project_or_404(project_id, db)
_assert_role(p, me, db, "viewer")
return p.project_songs
@router.post("/{project_id}/songs", response_model=ProjectSongOut, status_code=201)
def add_song_to_project(project_id: str, data: ProjectSongAdd, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
p = _get_project_or_404(project_id, db)
_assert_role(p, me, db, "editor")
song = db.query(Song).filter(Song.id == data.song_id).first()
if not song:
raise HTTPException(404, "Sang ikke fundet")
position = data.position
if position is None:
last = db.query(ProjectSong).filter_by(project_id=project_id).order_by(ProjectSong.position.desc()).first()
position = (last.position + 1) if last else 1
ps = ProjectSong(project_id=project_id, song_id=data.song_id, position=position)
db.add(ps)
db.commit()
db.refresh(ps)
return ps
@router.patch("/{project_id}/songs/{ps_id}/status", response_model=ProjectSongOut)
def update_song_status(project_id: str, ps_id: str, data: ProjectSongStatusUpdate, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
p = _get_project_or_404(project_id, db)
_assert_role(p, me, db, "editor")
ps = db.query(ProjectSong).filter_by(id=ps_id, project_id=project_id).first()
if not ps:
raise HTTPException(404, "Sang ikke fundet i projektet")
valid = {"pending", "playing", "played", "skipped"}
if data.status not in valid:
raise HTTPException(400, f"Ugyldig status. Vælg én af: {valid}")
ps.status = data.status
db.commit()
db.refresh(ps)
return ps
@router.delete("/{project_id}/songs/{ps_id}", status_code=204)
def remove_song_from_project(project_id: str, ps_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
p = _get_project_or_404(project_id, db)
_assert_role(p, me, db, "editor")
ps = db.query(ProjectSong).filter_by(id=ps_id, project_id=project_id).first()
if not ps:
raise HTTPException(404, "Sang ikke fundet i projektet")
db.delete(ps)
db.commit()

View File

@@ -1,109 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import get_current_user
from app.models import User, Song, SongDance, DanceAlternative
from app.schemas import (
SongCreate, SongOut,
SongDanceCreate, SongDanceOut,
DanceAlternativeCreate, DanceAlternativeOut,
)
router = APIRouter(prefix="/songs", tags=["songs"])
# ── Sange ─────────────────────────────────────────────────────────────────────
@router.get("/", response_model=list[SongOut])
def list_songs(db: Session = Depends(get_db), me: User = Depends(get_current_user)):
return db.query(Song).filter(Song.owner_id == me.id).all()
@router.post("/", response_model=SongOut, status_code=201)
def create_song(data: SongCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
song = Song(owner_id=me.id, **data.model_dump())
db.add(song)
db.commit()
db.refresh(song)
return song
@router.get("/{song_id}", response_model=SongOut)
def get_song(song_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first()
if not song:
raise HTTPException(404, "Sang ikke fundet")
return song
@router.delete("/{song_id}", status_code=204)
def delete_song(song_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first()
if not song:
raise HTTPException(404, "Sang ikke fundet")
db.delete(song)
db.commit()
# ── Danse på en sang ──────────────────────────────────────────────────────────
@router.post("/{song_id}/dances", response_model=SongDanceOut, status_code=201)
def add_dance(song_id: str, data: SongDanceCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first()
if not song:
raise HTTPException(404, "Sang ikke fundet")
dance = SongDance(song_id=song_id, **data.model_dump())
db.add(dance)
db.commit()
db.refresh(dance)
return dance
@router.delete("/{song_id}/dances/{dance_id}", status_code=204)
def remove_dance(song_id: str, dance_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first()
if not song:
raise HTTPException(404, "Sang ikke fundet")
dance = db.query(SongDance).filter(SongDance.id == dance_id, SongDance.song_id == song_id).first()
if not dance:
raise HTTPException(404, "Dans ikke fundet")
db.delete(dance)
db.commit()
# ── Alternativ-danse ──────────────────────────────────────────────────────────
@router.post("/{song_id}/dances/{dance_id}/alternatives", response_model=DanceAlternativeOut, status_code=201)
def add_alternative(song_id: str, dance_id: str, data: DanceAlternativeCreate, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
song = db.query(Song).filter(Song.id == song_id, Song.owner_id == me.id).first()
if not song:
raise HTTPException(404, "Sang ikke fundet")
dance = db.query(SongDance).filter(SongDance.id == dance_id, SongDance.song_id == song_id).first()
if not dance:
raise HTTPException(404, "Dans ikke fundet")
alt_dance = db.query(SongDance).filter(SongDance.id == data.alt_song_dance_id).first()
if not alt_dance:
raise HTTPException(404, "Alternativ-dans ikke fundet")
alt = DanceAlternative(song_dance_id=dance_id, **data.model_dump())
db.add(alt)
db.commit()
db.refresh(alt)
return alt
@router.get("/{song_id}/dances/{dance_id}/alternatives", response_model=list[DanceAlternativeOut])
def list_alternatives(song_id: str, dance_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
dance = db.query(SongDance).filter(SongDance.id == dance_id, SongDance.song_id == song_id).first()
if not dance:
raise HTTPException(404, "Dans ikke fundet")
return dance.alternatives
@router.delete("/{song_id}/dances/{dance_id}/alternatives/{alt_id}", status_code=204)
def remove_alternative(song_id: str, dance_id: str, alt_id: str, db: Session = Depends(get_db), me: User = Depends(get_current_user)):
alt = db.query(DanceAlternative).filter(DanceAlternative.id == alt_id, DanceAlternative.song_dance_id == dance_id).first()
if not alt:
raise HTTPException(404, "Alternativ ikke fundet")
db.delete(alt)
db.commit()

View File

@@ -1,115 +0,0 @@
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, EmailStr
# ── Auth ──────────────────────────────────────────────────────────────────────
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserOut(BaseModel):
id: str
username: str
email: str
created_at: datetime
model_config = {"from_attributes": True}
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
# ── Project ───────────────────────────────────────────────────────────────────
class ProjectCreate(BaseModel):
name: str
description: str = ""
is_public: bool = False
class ProjectUpdate(BaseModel):
name: str | None = None
description: str | None = None
is_public: bool | None = None
class ProjectOut(BaseModel):
id: str
owner_id: str
name: str
description: str
is_public: bool
updated_at: datetime
model_config = {"from_attributes": True}
class InviteMember(BaseModel):
username: str
role: str = "viewer" # editor | viewer
# ── Song ──────────────────────────────────────────────────────────────────────
class SongCreate(BaseModel):
title: str
artist: str = ""
local_path: str = ""
bpm: int = 0
duration_sec: int = 0
class SongOut(BaseModel):
id: str
owner_id: str
title: str
artist: str
local_path: str
bpm: int
duration_sec: int
synced_at: datetime
dances: list[SongDanceOut] = []
model_config = {"from_attributes": True}
# ── Dance ─────────────────────────────────────────────────────────────────────
class SongDanceCreate(BaseModel):
dance_name: str
dance_order: int = 1
class SongDanceOut(BaseModel):
id: str
dance_name: str
dance_order: int
model_config = {"from_attributes": True}
class DanceAlternativeCreate(BaseModel):
alt_song_dance_id: str
note: str = ""
class DanceAlternativeOut(BaseModel):
id: str
song_dance_id: str
alt_song_dance_id: str
note: str
model_config = {"from_attributes": True}
# ── ProjectSong ───────────────────────────────────────────────────────────────
class ProjectSongAdd(BaseModel):
song_id: str
position: int | None = None # None = tilføj sidst
class ProjectSongStatusUpdate(BaseModel):
status: str # pending | playing | played | skipped
class ProjectSongOut(BaseModel):
id: str
song_id: str
position: int
status: str
song: SongOut
model_config = {"from_attributes": True}
SongOut.model_rebuild()

View File

@@ -1,78 +0,0 @@
import json
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models import Project, ProjectSong
router = APIRouter(prefix="/ws", tags=["websocket"])
class ConnectionManager:
def __init__(self):
# project_id -> liste af aktive forbindelser
self.rooms: dict[str, list[WebSocket]] = {}
async def connect(self, project_id: str, ws: WebSocket):
await ws.accept()
self.rooms.setdefault(project_id, []).append(ws)
def disconnect(self, project_id: str, ws: WebSocket):
if project_id in self.rooms:
self.rooms[project_id].discard(ws) if hasattr(self.rooms[project_id], 'discard') else None
try:
self.rooms[project_id].remove(ws)
except ValueError:
pass
async def broadcast(self, project_id: str, message: dict):
dead = []
for ws in self.rooms.get(project_id, []):
try:
await ws.send_text(json.dumps(message))
except Exception:
dead.append(ws)
for ws in dead:
self.disconnect(project_id, ws)
manager = ConnectionManager()
@router.websocket("/{project_id}")
async def project_live(
project_id: str,
websocket: WebSocket,
db: Session = Depends(get_db),
):
project = db.query(Project).filter(Project.id == project_id).first()
if not project:
await websocket.close(code=4004)
return
await manager.connect(project_id, websocket)
# Send nuværende tilstand med det samme ved opkobling
songs = db.query(ProjectSong).filter_by(project_id=project_id).order_by(ProjectSong.position).all()
await websocket.send_text(json.dumps({
"event": "state",
"project_id": project_id,
"songs": [
{"id": ps.id, "position": ps.position, "status": ps.status, "song_id": ps.song_id}
for ps in songs
],
}))
try:
while True:
await websocket.receive_text() # hold forbindelsen åben
except WebSocketDisconnect:
manager.disconnect(project_id, websocket)
async def notify_status_change(project_id: str, project_song_id: str, new_status: str):
"""Kaldes fra projects-router når en sangs status ændres."""
await manager.broadcast(project_id, {
"event": "status_update",
"project_song_id": project_song_id,
"status": new_status,
})