diff --git a/app/routers/__pycache__/alternatives.cpython-312.pyc b/app/routers/__pycache__/alternatives.cpython-312.pyc deleted file mode 100644 index fbf4b070..00000000 Binary files a/app/routers/__pycache__/alternatives.cpython-312.pyc and /dev/null differ diff --git a/app/routers/__pycache__/auth.cpython-312.pyc b/app/routers/__pycache__/auth.cpython-312.pyc deleted file mode 100644 index 67355e42..00000000 Binary files a/app/routers/__pycache__/auth.cpython-312.pyc and /dev/null differ diff --git a/app/routers/__pycache__/projects.cpython-312.pyc b/app/routers/__pycache__/projects.cpython-312.pyc deleted file mode 100644 index 404e032a..00000000 Binary files a/app/routers/__pycache__/projects.cpython-312.pyc and /dev/null differ diff --git a/app/routers/__pycache__/songs.cpython-312.pyc b/app/routers/__pycache__/songs.cpython-312.pyc deleted file mode 100644 index 53965bdc..00000000 Binary files a/app/routers/__pycache__/songs.cpython-312.pyc and /dev/null differ diff --git a/app/routers/alternatives.py b/app/routers/alternatives.py deleted file mode 100644 index 12fedf9a..00000000 --- a/app/routers/alternatives.py +++ /dev/null @@ -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() diff --git a/app/routers/auth.py b/app/routers/auth.py deleted file mode 100644 index 0e16aaac..00000000 --- a/app/routers/auth.py +++ /dev/null @@ -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} diff --git a/app/routers/projects.py b/app/routers/projects.py deleted file mode 100644 index 1d341b0e..00000000 --- a/app/routers/projects.py +++ /dev/null @@ -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() diff --git a/app/routers/songs.py b/app/routers/songs.py deleted file mode 100644 index 3e10b145..00000000 --- a/app/routers/songs.py +++ /dev/null @@ -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() diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py deleted file mode 100644 index 21227c3c..00000000 --- a/app/schemas/__init__.py +++ /dev/null @@ -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() diff --git a/app/schemas/__pycache__/__init__.cpython-312.pyc b/app/schemas/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 1a72197c..00000000 Binary files a/app/schemas/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/app/websocket/__pycache__/manager.cpython-312.pyc b/app/websocket/__pycache__/manager.cpython-312.pyc deleted file mode 100644 index 19852e9e..00000000 Binary files a/app/websocket/__pycache__/manager.cpython-312.pyc and /dev/null differ diff --git a/app/websocket/manager.py b/app/websocket/manager.py deleted file mode 100644 index 42f05c3b..00000000 --- a/app/websocket/manager.py +++ /dev/null @@ -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, - })