Synk virker
This commit is contained in:
@@ -419,13 +419,13 @@ def get_all_songs_with_files(limit: int = 5000) -> list:
|
||||
|
||||
# ── Playlister ────────────────────────────────────────────────────────────────
|
||||
|
||||
def create_playlist(name: str, description: str = "") -> str:
|
||||
def create_playlist(name: str, description: str = "", tags: str = "") -> str:
|
||||
import uuid as _uuid
|
||||
pl_id = str(_uuid.uuid4())
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO playlists (id, name, description) VALUES (?,?,?)",
|
||||
(pl_id, name, description)
|
||||
"INSERT INTO playlists (id, name, description, tags) VALUES (?,?,?,?)",
|
||||
(pl_id, name, description, tags)
|
||||
)
|
||||
return pl_id
|
||||
|
||||
@@ -434,7 +434,9 @@ def get_playlists(tag_filter: str | None = None) -> list:
|
||||
with get_db() as conn:
|
||||
if tag_filter:
|
||||
return conn.execute("""
|
||||
SELECT p.*, COUNT(ps.id) as song_count
|
||||
SELECT p.id, p.name, p.description, p.tags, p.api_project_id,
|
||||
p.is_linked, p.server_permission, p.is_deleted, p.created_at,
|
||||
COUNT(ps.position) as song_count
|
||||
FROM playlists p
|
||||
LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id
|
||||
WHERE p.name != '__aktiv__' AND p.is_deleted = 0
|
||||
@@ -444,7 +446,9 @@ def get_playlists(tag_filter: str | None = None) -> list:
|
||||
f"%, {tag_filter}", tag_filter)).fetchall()
|
||||
else:
|
||||
return conn.execute("""
|
||||
SELECT p.*, COUNT(ps.id) as song_count
|
||||
SELECT p.id, p.name, p.description, p.tags, p.api_project_id,
|
||||
p.is_linked, p.server_permission, p.is_deleted, p.created_at,
|
||||
COUNT(ps.position) as song_count
|
||||
FROM playlists p
|
||||
LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id
|
||||
WHERE p.name != '__aktiv__' AND p.is_deleted = 0
|
||||
@@ -506,6 +510,10 @@ def add_song_to_playlist(playlist_id: str, song_id: str,
|
||||
|
||||
# ── Dans-tags ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_all_playlist_tags() -> list[str]:
|
||||
return get_playlist_tags()
|
||||
|
||||
|
||||
def get_playlist_tags() -> list[str]:
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
|
||||
@@ -14,10 +14,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class SyncManager:
|
||||
|
||||
def __init__(self, api_url: str, db_path: str):
|
||||
self._api_url = api_url.rstrip("/")
|
||||
def __init__(self, api_url: str = "", db_path: str = "",
|
||||
server_url: str = "", token: str | None = None):
|
||||
# Støt både api_url og server_url som parameter-navn
|
||||
self._api_url = (api_url or server_url).rstrip("/")
|
||||
self._db_path = db_path
|
||||
self._token: str | None = None
|
||||
self._token: str | None = token
|
||||
|
||||
def set_token(self, token: str):
|
||||
self._token = token
|
||||
|
||||
@@ -19,7 +19,7 @@ from PyQt6.QtGui import QColor
|
||||
class PlaylistBrowserDialog(QDialog):
|
||||
"""Kombineret gem/hent dialog til danselister."""
|
||||
|
||||
playlist_selected = pyqtSignal(int, str) # playlist_id, name
|
||||
playlist_selected = pyqtSignal(str, str) # playlist_id, name
|
||||
sync_requested = pyqtSignal() # bed main_window om at køre sync
|
||||
|
||||
def __init__(self, mode: str = "load", current_songs: list = None,
|
||||
@@ -315,7 +315,9 @@ class PlaylistBrowserDialog(QDialog):
|
||||
)
|
||||
for i, song in enumerate(self._current_songs, start=1):
|
||||
if song.get("id"):
|
||||
add_song_to_playlist(pl_id, song["id"], position=i)
|
||||
add_song_to_playlist(pl_id, song["id"],
|
||||
file_id=song.get("file_id"),
|
||||
position=i)
|
||||
self.playlist_selected.emit(pl_id, name)
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
@@ -327,7 +329,9 @@ class PlaylistBrowserDialog(QDialog):
|
||||
pl_id = create_playlist(name, tags=tags)
|
||||
for i, song in enumerate(self._current_songs, start=1):
|
||||
if song.get("id"):
|
||||
add_song_to_playlist(pl_id, song["id"], position=i)
|
||||
add_song_to_playlist(pl_id, song["id"],
|
||||
file_id=song.get("file_id"),
|
||||
position=i)
|
||||
self.playlist_selected.emit(pl_id, name)
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
|
||||
@@ -50,8 +50,8 @@ class PlaylistPanel(QWidget):
|
||||
self._statuses: list[str] = []
|
||||
self._current_idx = -1
|
||||
self._song_ended = False
|
||||
self._active_playlist_id: int | None = None
|
||||
self._named_playlist_id: int | None = None # den indlæste/gemte navngivne liste
|
||||
self._active_playlist_id: str | None = None
|
||||
self._named_playlist_id: str | None = None # den indlæste/gemte navngivne liste
|
||||
self._build_ui()
|
||||
self.setAcceptDrops(True)
|
||||
# Autogem-timer — venter 800ms efter sidst ændring
|
||||
@@ -317,7 +317,7 @@ class PlaylistPanel(QWidget):
|
||||
pass
|
||||
return False
|
||||
|
||||
def get_named_playlist_id(self) -> int | None:
|
||||
def get_named_playlist_id(self) -> str | None:
|
||||
return self._named_playlist_id
|
||||
|
||||
def next_playable_idx(self) -> int | None:
|
||||
@@ -360,11 +360,14 @@ class PlaylistPanel(QWidget):
|
||||
for i, song in enumerate(self._songs, start=1):
|
||||
if song.get("id"):
|
||||
status = self._statuses[i-1] if i-1 < len(self._statuses) else "pending"
|
||||
import uuid as _uuid
|
||||
conn.execute(
|
||||
"INSERT INTO playlist_songs "
|
||||
"(playlist_id, song_id, position, status, is_workshop, dance_override) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(self._named_playlist_id, song["id"], i, status,
|
||||
"(id, playlist_id, song_id, file_id, position, status, is_workshop, dance_override) "
|
||||
"VALUES (?,?,?,?,?,?,?,?)",
|
||||
(str(_uuid.uuid4()), self._named_playlist_id,
|
||||
song["id"], song.get("file_id"),
|
||||
i, status,
|
||||
1 if song.get("is_workshop") else 0,
|
||||
song.get("active_dance") or "")
|
||||
)
|
||||
@@ -374,7 +377,7 @@ class PlaylistPanel(QWidget):
|
||||
except Exception as e:
|
||||
self._lbl_autosave.setText("⚠ gemfejl")
|
||||
|
||||
def _save_named_playlist_id(self, pl_id: int | None):
|
||||
def _save_named_playlist_id(self, pl_id: str | None):
|
||||
"""Gem hvilken navngiven liste der er aktiv — til brug ved næste opstart."""
|
||||
from PyQt6.QtCore import QSettings
|
||||
s = QSettings("LineDance", "Player")
|
||||
@@ -388,7 +391,7 @@ class PlaylistPanel(QWidget):
|
||||
try:
|
||||
from PyQt6.QtCore import QSettings
|
||||
s = QSettings("LineDance", "Player")
|
||||
pl_id = s.value("session/named_playlist_id", None, type=int)
|
||||
pl_id = s.value("session/named_playlist_id", None, type=str)
|
||||
if not pl_id:
|
||||
return False
|
||||
|
||||
@@ -410,11 +413,14 @@ class PlaylistPanel(QWidget):
|
||||
# file_missing betyder bare at filen ikke er på denne maskine
|
||||
songs_raw = conn.execute("""
|
||||
SELECT s.id, s.title, s.artist, s.album,
|
||||
s.bpm, s.duration_sec, s.file_format,
|
||||
s.local_path, s.file_missing,
|
||||
s.bpm, s.duration_sec,
|
||||
ps.file_id,
|
||||
f.local_path, f.file_format,
|
||||
COALESCE(f.file_missing, 1) as file_missing,
|
||||
ps.position, ps.status, ps.is_workshop, ps.dance_override
|
||||
FROM playlist_songs ps
|
||||
JOIN songs s ON s.id = ps.song_id
|
||||
LEFT JOIN files f ON f.id = ps.file_id
|
||||
WHERE ps.playlist_id=? ORDER BY ps.position
|
||||
""", (pl_id,)).fetchall()
|
||||
|
||||
@@ -433,13 +439,13 @@ class PlaylistPanel(QWidget):
|
||||
local_path = row["local_path"] or ""
|
||||
file_missing = bool(row["file_missing"])
|
||||
|
||||
# Forsøg at finde filen lokalt hvis den mangler på denne maskine
|
||||
# Forsøg at finde en anden fil lokalt hvis den specifikke mangler
|
||||
if file_missing or not local_path:
|
||||
match = conn.execute("""
|
||||
SELECT local_path FROM songs
|
||||
WHERE title=? AND artist=? AND file_missing=0
|
||||
LIMIT 1
|
||||
""", (row["title"], row["artist"])).fetchone()
|
||||
match = conn.execute(
|
||||
"SELECT f.local_path FROM files f "
|
||||
"WHERE f.song_id=? AND f.file_missing=0 LIMIT 1",
|
||||
(row["id"],)
|
||||
).fetchone()
|
||||
if match:
|
||||
local_path = match["local_path"]
|
||||
file_missing = False
|
||||
@@ -451,6 +457,7 @@ class PlaylistPanel(QWidget):
|
||||
"album": row["album"] or "",
|
||||
"bpm": row["bpm"] or 0,
|
||||
"duration_sec": row["duration_sec"] or 0,
|
||||
"file_id": row["file_id"] if "file_id" in row.keys() else None,
|
||||
"local_path": local_path,
|
||||
"file_format": row["file_format"] or "",
|
||||
"file_missing": file_missing,
|
||||
@@ -551,11 +558,14 @@ class PlaylistPanel(QWidget):
|
||||
for i, song in enumerate(self._songs, start=1):
|
||||
if song.get("id"):
|
||||
status = self._statuses[i-1] if i-1 < len(self._statuses) else "pending"
|
||||
import uuid as _uuid
|
||||
conn.execute(
|
||||
"INSERT INTO playlist_songs "
|
||||
"(playlist_id, song_id, position, status, is_workshop, dance_override) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(self._named_playlist_id, song["id"], i, status,
|
||||
"(id, playlist_id, song_id, file_id, position, status, is_workshop, dance_override) "
|
||||
"VALUES (?,?,?,?,?,?,?,?)",
|
||||
(str(_uuid.uuid4()), self._named_playlist_id,
|
||||
song["id"], song.get("file_id"),
|
||||
i, status,
|
||||
1 if song.get("is_workshop") else 0,
|
||||
song.get("active_dance") or "")
|
||||
)
|
||||
@@ -589,7 +599,7 @@ class PlaylistPanel(QWidget):
|
||||
dialog.sync_requested.connect(self._request_sync)
|
||||
dialog.exec()
|
||||
|
||||
def _load_playlist_by_id(self, pl_id: int, pl_name: str):
|
||||
def _load_playlist_by_id(self, pl_id: str, pl_name: str):
|
||||
try:
|
||||
from local.local_db import get_db
|
||||
|
||||
@@ -612,11 +622,14 @@ class PlaylistPanel(QWidget):
|
||||
# JOIN songs — sangen er altid i songs tabellen (oprettet ved pull med file_missing=1)
|
||||
songs_raw = conn.execute("""
|
||||
SELECT s.id, s.title, s.artist, s.album,
|
||||
s.bpm, s.duration_sec, s.file_format,
|
||||
s.local_path, s.file_missing,
|
||||
s.bpm, s.duration_sec,
|
||||
ps.file_id,
|
||||
f.local_path, f.file_format,
|
||||
COALESCE(f.file_missing, 1) as file_missing,
|
||||
ps.position, ps.status, ps.is_workshop, ps.dance_override
|
||||
FROM playlist_songs ps
|
||||
JOIN songs s ON s.id = ps.song_id
|
||||
LEFT JOIN files f ON f.id = ps.file_id
|
||||
WHERE ps.playlist_id=? ORDER BY ps.position
|
||||
""", (pl_id,)).fetchall()
|
||||
songs = []
|
||||
@@ -625,7 +638,7 @@ class PlaylistPanel(QWidget):
|
||||
for row in songs_raw:
|
||||
dances = conn.execute("""
|
||||
SELECT d.name FROM song_dances sd
|
||||
JOIN dances d ON d.id = sad.dance_id
|
||||
JOIN dances d ON d.id = sd.dance_id
|
||||
WHERE sd.song_id=? ORDER BY sd.dance_order
|
||||
""", (row["id"],)).fetchall()
|
||||
dance_names = [d["name"] for d in dances]
|
||||
@@ -635,13 +648,13 @@ class PlaylistPanel(QWidget):
|
||||
local_path = row["local_path"] or ""
|
||||
file_missing = bool(row["file_missing"])
|
||||
|
||||
# Forsøg at finde filen lokalt hvis den mangler på denne maskine
|
||||
# Forsøg at finde en anden fil lokalt hvis den specifikke mangler
|
||||
if file_missing or not local_path:
|
||||
match = conn.execute("""
|
||||
SELECT local_path FROM songs
|
||||
WHERE title=? AND artist=? AND file_missing=0
|
||||
LIMIT 1
|
||||
""", (row["title"], row["artist"])).fetchone()
|
||||
match = conn.execute(
|
||||
"SELECT f.local_path FROM files f "
|
||||
"WHERE f.song_id=? AND f.file_missing=0 LIMIT 1",
|
||||
(row["id"],)
|
||||
).fetchone()
|
||||
if match:
|
||||
local_path = match["local_path"]
|
||||
file_missing = False
|
||||
@@ -654,6 +667,7 @@ class PlaylistPanel(QWidget):
|
||||
"album": row["album"] or "",
|
||||
"bpm": row["bpm"] or 0,
|
||||
"duration_sec": row["duration_sec"] or 0,
|
||||
"file_id": row["file_id"] if "file_id" in row.keys() else None,
|
||||
"local_path": local_path,
|
||||
"file_format": row["file_format"] or "",
|
||||
"file_missing": file_missing,
|
||||
@@ -798,20 +812,24 @@ class PlaylistPanel(QWidget):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _pull_linked_playlist(self, pl_id: int, server_id: str):
|
||||
"""Hent seneste version af en linket liste fra serveren."""
|
||||
def _pull_linked_playlist(self, pl_id: str, server_id: str):
|
||||
"""Hent seneste version af en linket liste fra serveren — i baggrundstråd."""
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
def _do_pull():
|
||||
try:
|
||||
from ui.settings_dialog import load_settings
|
||||
from local.local_db import get_db, DB_PATH
|
||||
from local.local_db import DB_PATH
|
||||
import sqlite3, urllib.request, json
|
||||
|
||||
s = load_settings()
|
||||
server_url = s.get("server_url", "").rstrip("/")
|
||||
# Hent token fra main_window
|
||||
mw = self.window()
|
||||
token = getattr(mw, "_api_token", None)
|
||||
if not token or not server_url:
|
||||
return
|
||||
|
||||
import urllib.request, json
|
||||
req = urllib.request.Request(
|
||||
f"{server_url}/sharing/playlists/{server_id}",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
@@ -819,32 +837,66 @@ class PlaylistPanel(QWidget):
|
||||
with urllib.request.urlopen(req, timeout=8) as resp:
|
||||
pl_data = json.loads(resp.read())
|
||||
|
||||
# Opdater lokal liste med server-data
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
conn = sqlite3.connect(str(DB_PATH), timeout=10)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,))
|
||||
|
||||
position = 1
|
||||
for song_data in pl_data.get("songs", []):
|
||||
title = song_data.get("title", "")
|
||||
artist = song_data.get("artist", "")
|
||||
if not title:
|
||||
continue
|
||||
# Find sang via titel+artist
|
||||
local = conn.execute(
|
||||
"SELECT id FROM songs WHERE title=? AND artist=? AND file_missing=0",
|
||||
(song_data["title"], song_data["artist"])
|
||||
"SELECT s.id FROM songs s "
|
||||
"JOIN files f ON f.song_id = s.id AND f.file_missing=0 "
|
||||
"WHERE s.title=? AND s.artist=? LIMIT 1",
|
||||
(title, artist)
|
||||
).fetchone()
|
||||
if local:
|
||||
if not local:
|
||||
# Sang mangler lokalt — opret som missing
|
||||
local = conn.execute(
|
||||
"SELECT id FROM songs WHERE title=? AND artist=? LIMIT 1",
|
||||
(title, artist)
|
||||
).fetchone()
|
||||
if not local:
|
||||
new_id = str(uuid.uuid4())
|
||||
conn.execute(
|
||||
"INSERT INTO songs (id, title, artist) VALUES (?,?,?)",
|
||||
(new_id, title, artist)
|
||||
)
|
||||
song_id = new_id
|
||||
else:
|
||||
song_id = local["id"]
|
||||
|
||||
# Find fil
|
||||
file_row = conn.execute(
|
||||
"SELECT id FROM files WHERE song_id=? AND file_missing=0 LIMIT 1",
|
||||
(song_id,)
|
||||
).fetchone()
|
||||
file_id = file_row["id"] if file_row else None
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO playlist_songs "
|
||||
"(playlist_id, song_id, position, status, is_workshop, dance_override) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(pl_id, local["id"], song_data["position"],
|
||||
song_data.get("status", "pending"),
|
||||
"(id, playlist_id, song_id, file_id, position, status, is_workshop, dance_override) "
|
||||
"VALUES (?,?,?,?,?,?,?,?)",
|
||||
(str(uuid.uuid4()), pl_id, song_id, file_id,
|
||||
position, song_data.get("status", "pending"),
|
||||
1 if song_data.get("is_workshop") else 0,
|
||||
song_data.get("dance_override") or "")
|
||||
)
|
||||
position += 1
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass # Offline — brug lokalt cachet version
|
||||
|
||||
def _push_linked_playlist(self, pl_id: int, server_id: str):
|
||||
threading.Thread(target=_do_pull, daemon=True).start()
|
||||
|
||||
def _push_linked_playlist(self, pl_id: str, server_id: str):
|
||||
"""Push ændringer til server for en linket liste."""
|
||||
try:
|
||||
from ui.settings_dialog import load_settings
|
||||
@@ -969,42 +1021,41 @@ class PlaylistPanel(QWidget):
|
||||
with get_db() as conn:
|
||||
for song in self._songs:
|
||||
path = song.get("local_path", "")
|
||||
# Grøn — eksisterer og tilgængeligt
|
||||
# Grøn — filen eksisterer lokalt
|
||||
if path and Path(path).exists():
|
||||
song["availability"] = "green"
|
||||
song["file_missing"] = False
|
||||
# Opdater songs tabellen
|
||||
# Opdater files tabellen
|
||||
conn.execute(
|
||||
"UPDATE songs SET file_missing=0, local_path=? WHERE id=?",
|
||||
(path, song["id"])
|
||||
"UPDATE files SET file_missing=0 WHERE local_path=?",
|
||||
(path,)
|
||||
)
|
||||
continue
|
||||
|
||||
# Forsøg auto-match via titel+artist
|
||||
# Forsøg auto-match via titel+artist i files tabellen
|
||||
title = song.get("title", "")
|
||||
artist = song.get("artist", "")
|
||||
match = conn.execute("""
|
||||
SELECT id, local_path FROM songs
|
||||
WHERE title=? AND artist=? AND file_missing=0
|
||||
AND local_path IS NOT NULL AND local_path != ''
|
||||
SELECT f.id as file_id, f.local_path, s.id as song_id
|
||||
FROM files f
|
||||
JOIN songs s ON s.id = f.song_id
|
||||
WHERE s.title=? AND s.artist=? AND f.file_missing=0
|
||||
AND f.local_path IS NOT NULL AND f.local_path != ''
|
||||
LIMIT 1
|
||||
""", (title, artist)).fetchone()
|
||||
|
||||
if match and Path(match["local_path"]).exists():
|
||||
song["local_path"] = match["local_path"]
|
||||
song["id"] = match["id"]
|
||||
song["file_id"] = match["file_id"]
|
||||
song["availability"] = "green"
|
||||
song["file_missing"] = False
|
||||
# Opdater playlist_songs til at pege på den fundne sang
|
||||
# Opdater playlist_songs til at pege på den fundne fil
|
||||
if self._named_playlist_id:
|
||||
conn.execute("""
|
||||
UPDATE playlist_songs SET song_id=?
|
||||
WHERE playlist_id=? AND song_id=(
|
||||
SELECT id FROM songs
|
||||
WHERE title=? AND artist=?
|
||||
LIMIT 1
|
||||
conn.execute(
|
||||
"UPDATE playlist_songs SET file_id=? "
|
||||
"WHERE playlist_id=? AND song_id=?",
|
||||
(match["file_id"], self._named_playlist_id, song["id"])
|
||||
)
|
||||
""", (match["id"], self._named_playlist_id, title, artist))
|
||||
else:
|
||||
song["availability"] = "red"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user