From cd3ed811f65b1f923ffca7cf7c61b3906bf3e991 Mon Sep 17 00:00:00 2001 From: Carsten Kvist Date: Tue, 14 Apr 2026 20:02:15 +0200 Subject: [PATCH] Playliste - online --- linedance-api/app/main.py | 3 +- linedance-api/app/routers/live.py | 108 +++++++ linedance-api/web/public/live.html | 403 +++++++++++++++++++++++++ linedance-app/local/acoustid_worker.py | 6 +- linedance-app/ui/main_window.py | 50 ++- 5 files changed, 564 insertions(+), 6 deletions(-) create mode 100644 linedance-api/app/routers/live.py create mode 100644 linedance-api/web/public/live.html diff --git a/linedance-api/app/main.py b/linedance-api/app/main.py index c85f2f19..76677cf1 100644 --- a/linedance-api/app/main.py +++ b/linedance-api/app/main.py @@ -8,7 +8,7 @@ from app.models import ( PlaylistShare, CommunityDance, CommunityDanceAlt, DanceAltRating, SongDance, SongAltDance, ) -from app.routers import auth, projects, songs, alternatives, dances, sync, sharing +from app.routers import auth, projects, songs, alternatives, dances, sync, sharing, live from app.websocket.manager import router as ws_router # Opret tabeller hvis de ikke findes @@ -35,6 +35,7 @@ app.include_router(alternatives.router) app.include_router(dances.router) app.include_router(sync.router) app.include_router(sharing.router) +app.include_router(live.router) @app.on_event("startup") diff --git a/linedance-api/app/routers/live.py b/linedance-api/app/routers/live.py new file mode 100644 index 00000000..bccd6a59 --- /dev/null +++ b/linedance-api/app/routers/live.py @@ -0,0 +1,108 @@ +""" +live.py — Live playliste-status til storskærm/mobil. +Appen pusher status hertil, storskærmen poller hvert 5 sek. +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from pydantic import BaseModel +from typing import Optional +import json +from datetime import datetime, timezone + +from app.core.database import get_db +from app.core.security import get_current_user +from app.models import User, Project + +router = APIRouter(prefix="/live", tags=["live"]) + +# In-memory cache: server_id → {songs, updated_at} +_live_cache: dict = {} + + +class SongStatus(BaseModel): + title: str + artist: str = "" + status: str = "pending" + position: int + dance: str = "" + duration: int = 0 + + +class LiveStatus(BaseModel): + songs: list[SongStatus] + + +# ── Push fra app ────────────────────────────────────────────────────────────── + +@router.post("/{project_id}/status") +def push_status( + project_id: str, + data: LiveStatus, + db: Session = Depends(get_db), + me: User = Depends(get_current_user), +): + """App pusher aktuel playliste-status.""" + p = db.query(Project).filter_by(id=project_id, owner_id=me.id).first() + if not p: + raise HTTPException(404, "Playliste ikke fundet") + + _live_cache[project_id] = { + "name": p.name, + "songs": [s.model_dump() for s in data.songs], + "updated_at": datetime.now(timezone.utc).isoformat(), + } + return {"status": "ok"} + + +# ── Pull til storskærm ──────────────────────────────────────────────────────── + +@router.get("/{project_id}") +def get_live_status(project_id: str, db: Session = Depends(get_db)): + """Storskærm poller dette endpoint — ingen login krævet.""" + # Tjek at playlisten eksisterer og er tilgængelig + p = db.query(Project).filter_by(id=project_id).first() + if not p: + raise HTTPException(404, "Playliste ikke fundet") + + cached = _live_cache.get(project_id) + if cached: + return cached + + # Ingen live data endnu — returner statisk data fra DB + from app.models import ProjectSong, Song + songs = [] + for ps in sorted(p.project_songs, key=lambda x: x.position): + song = db.query(Song).filter_by(id=ps.song_id).first() + if not song: + continue + songs.append({ + "title": song.title, + "artist": song.artist, + "status": ps.status or "pending", + "position": ps.position, + "dance": ps.dance_override or "", + "duration": song.duration_sec or 0, + }) + + return { + "name": p.name, + "songs": songs, + "updated_at": None, + } + + +# ── Liste over aktive live-playlister ───────────────────────────────────────── + +@router.get("/") +def list_live(db: Session = Depends(get_db)): + """Hvilke playlister har aktiv live-data?""" + result = [] + for pid, data in _live_cache.items(): + playing = next((s for s in data["songs"] if s["status"] == "playing"), None) + result.append({ + "id": pid, + "name": data["name"], + "updated_at": data["updated_at"], + "now_playing": playing["title"] if playing else None, + }) + return result diff --git a/linedance-api/web/public/live.html b/linedance-api/web/public/live.html new file mode 100644 index 00000000..dcf52889 --- /dev/null +++ b/linedance-api/web/public/live.html @@ -0,0 +1,403 @@ + + + + + +LineDance — Live + + + + +
+ +
+
+ Forbinder... +
+
+ + +
+

Vælg playliste

+

Ingen aktiv playliste fundet. Vælg en nedenfor eller brug URL-parametret ?id=PLAYLIST_ID

+
+
+ + +
+
🎵
+
Ingen aktiv playliste
+
Åbn LineDance Player og start et event
+
+ + + + + + + + + +
+ + + + + + diff --git a/linedance-app/local/acoustid_worker.py b/linedance-app/local/acoustid_worker.py index a913b8f8..d71393ba 100644 --- a/linedance-app/local/acoustid_worker.py +++ b/linedance-app/local/acoustid_worker.py @@ -18,9 +18,9 @@ from pathlib import Path logger = logging.getLogger(__name__) -# AcoustID API nøgle — gratis til open-source apps -# https://acoustid.org/api-key -ACOUSTID_API_KEY = "9JYq1saI1H" +# AcoustID API nøgle — kan overskrives i Indstillinger → Afspilning +# Registrér din egen på https://acoustid.org/new-application +ACOUSTID_API_KEY = "71W9SJdajAI" ACOUSTID_API_URL = "https://api.acoustid.org/v2/lookup" # Pause mellem API-kald — rolig baggrundskørsel diff --git a/linedance-app/ui/main_window.py b/linedance-app/ui/main_window.py index 2c33525c..3e69865b 100644 --- a/linedance-app/ui/main_window.py +++ b/linedance-app/ui/main_window.py @@ -1241,11 +1241,12 @@ class MainWindow(QMainWindow): self._song_ended = False def _sync_event_status_to_playlist(self): - """Gem event-fremgang (afspillet/sprunget over) til den navngivne liste.""" + """Gem event-fremgang lokalt og mini-sync til server.""" try: pl_id = self._playlist_panel.get_named_playlist_id() if not pl_id: return + songs = self._playlist_panel.get_songs() statuses = self._playlist_panel.get_statuses() from local.local_db import get_db with get_db() as conn: @@ -1255,9 +1256,54 @@ class MainWindow(QMainWindow): "WHERE playlist_id=? AND position=?", (status, pl_id, position) ) - except Exception as e: + # Hent server_id for denne playliste + row = conn.execute( + "SELECT api_project_id FROM playlists WHERE id=?", (pl_id,) + ).fetchone() + server_id = row["api_project_id"] if row else None + + # Mini-sync til server hvis online + if server_id and self._api_token: + self._mini_sync_to_server(server_id, songs, statuses) + except Exception: pass + def _mini_sync_to_server(self, server_id: str, songs: list, statuses: list): + """Send kun playliste-status til server — kører i baggrundstråd.""" + import threading, urllib.request, json + url = f"{self._api_url}/live/{server_id}/status" + token = self._api_token + + payload = json.dumps({ + "songs": [ + { + "title": s.get("title", ""), + "artist": s.get("artist", ""), + "status": statuses[i] if i < len(statuses) else "pending", + "position": i + 1, + "dance": s.get("active_dance", "") or + (s.get("dances", [""])[0] if s.get("dances") else ""), + "duration": s.get("duration_sec", 0), + } + for i, s in enumerate(songs) + ] + }).encode() + + def _push(): + try: + req = urllib.request.Request( + url, data=payload, method="POST", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + } + ) + urllib.request.urlopen(req, timeout=4) + except Exception: + pass + + threading.Thread(target=_push, daemon=True).start() + def _on_state_changed(self, state: str): if state == "playing": self._btn_play.setText("⏸")