diff --git a/linedance-api/app/routers/sync.py b/linedance-api/app/routers/sync.py index d4c9e780..42429d55 100644 --- a/linedance-api/app/routers/sync.py +++ b/linedance-api/app/routers/sync.py @@ -185,11 +185,17 @@ def push( db.flush() dance_id_map[key] = dance.id - # ── Sang-dans tags ──────────────────────────────────────────────────────── + # ── Sang-dans tags — synkroniser fuldt per sang ────────────────────────── + # Slet eksisterende tags for sange der er med i push, genindsæt fra klient + synced_song_ids = set() for sd in payload.song_dances: song_id = song_id_map.get(sd.song_local_id) if not song_id: continue + if song_id not in synced_song_ids: + db.execute(_sa.text("DELETE FROM song_dances WHERE song_id=:sid"), + {"sid": song_id}) + synced_song_ids.add(song_id) level_id = level_map.get(sd.level_name.lower()) if sd.level_name else None key = f"{sd.dance_name.lower()}|{level_id}" dance_id = dance_id_map.get(key) @@ -201,6 +207,13 @@ def push( ), {"id": str(uuid.uuid4()), "song_id": song_id, "dance_id": dance_id, "dance_order": sd.dance_order}) + # Sange pushet uden dans-tags — slet også på server + sent_local_ids = {sd.song_local_id for sd in payload.song_dances} + for local_id, song_id in song_id_map.items(): + if local_id in sent_local_ids and song_id not in synced_song_ids: + db.execute(_sa.text("DELETE FROM song_dances WHERE song_id=:sid"), + {"sid": song_id}) + for sa in payload.song_alts: song_id = song_id_map.get(sa.song_local_id) if not song_id: diff --git a/linedance-app/local/acoustid_worker.py b/linedance-app/local/acoustid_worker.py index b9f95d6b..f987a4ba 100644 --- a/linedance-app/local/acoustid_worker.py +++ b/linedance-app/local/acoustid_worker.py @@ -213,10 +213,16 @@ def run_acoustid_scan(db_path: str, api_key: str = "", on_progress=None, stop_ev if result: mbid = result.get("mbid", "") acoustid = result.get("acoustid", "") + # Opdater acoustid altid, men kun mbid hvis det ikke allerede bruges conn.execute( - "UPDATE songs SET mbid=?, acoustid=? WHERE id=?", - (mbid or None, acoustid or None, row["id"]) + "UPDATE songs SET acoustid=? WHERE id=?", + (acoustid or None, row["id"]) ) + if mbid: + conn.execute( + "UPDATE songs SET mbid=? WHERE id=? AND (mbid IS NULL OR mbid='')", + (mbid, row["id"]) + ) conn.commit() found += 1 total_found += 1 diff --git a/linedance-app/local/local_db.py b/linedance-app/local/local_db.py index 7e7b147c..cc335a11 100644 --- a/linedance-app/local/local_db.py +++ b/linedance-app/local/local_db.py @@ -647,8 +647,8 @@ def get_choreographer_suggestions(prefix: str = "", limit: int = 15) -> list[str def get_dances_for_song(song_id: str) -> list: """Hent alle danse tagget på en sang med niveau og koreograf.""" with get_db() as conn: - return conn.execute(""" - SELECT d.id, d.name, dl.name as level_name, d.choreographer, + rows = conn.execute(""" + SELECT d.id, d.name, d.level_id, dl.name as level_name, d.choreographer, d.video_url, d.stepsheet_url, d.notes, sd.dance_order FROM song_dances sd JOIN dances d ON d.id = sd.dance_id @@ -656,13 +656,14 @@ def get_dances_for_song(song_id: str) -> list: WHERE sd.song_id = ? ORDER BY sd.dance_order """, (song_id,)).fetchall() + return [dict(r) for r in rows] def get_alt_dances_for_song(song_id: str) -> list: """Hent alle alternativ-danse tagget på en sang.""" with get_db() as conn: - return conn.execute(""" - SELECT d.id, d.name, dl.name as level_name, d.choreographer, + rows = conn.execute(""" + SELECT d.id, d.name, d.level_id, dl.name as level_name, d.choreographer, d.video_url, d.stepsheet_url, sad.note FROM song_alt_dances sad JOIN dances d ON d.id = sad.dance_id @@ -670,6 +671,7 @@ def get_alt_dances_for_song(song_id: str) -> list: WHERE sad.song_id = ? ORDER BY d.name """, (song_id,)).fetchall() + return [dict(r) for r in rows] def get_or_create_dance(name: str, level_id: int | None, conn, choreographer: str = "") -> int: diff --git a/linedance-app/ui/dance_picker_dialog.py b/linedance-app/ui/dance_picker_dialog.py index c405468a..45d4fa12 100644 --- a/linedance-app/ui/dance_picker_dialog.py +++ b/linedance-app/ui/dance_picker_dialog.py @@ -85,16 +85,41 @@ class DancePickerDialog(QDialog): def _load_suggestions(self, prefix: str): try: from local.local_db import get_dance_suggestions + from PyQt6.QtGui import QColor suggestions = get_dance_suggestions(prefix or "", limit=25) self._list.clear() - # Vis eksisterende danse øverst hvis ingen prefix - if not prefix and self._existing_dances: - for name in self._existing_dances: - item = QListWidgetItem(f"★ {name}") - item.setData(Qt.ItemDataRole.UserRole, {"name": name}) - item.setForeground(__import__('PyQt6.QtGui', fromlist=['QColor']).QColor("#e8a020")) - self._list.addItem(item) + # Allerøverst: mulighed for at fjerne dans + no_dance = QListWidgetItem("✕ Ingen dans") + no_dance.setForeground(QColor("#5a6070")) + no_dance.setData(Qt.ItemDataRole.UserRole, {"name": ""}) + self._list.addItem(no_dance) + + # Øverst: danse registreret på denne sang + if self._existing_dances: + # Filtrer på prefix hvis der skrives + matching = [d for d in self._existing_dances + if not prefix or prefix.lower() in d.lower()] + if matching: + # Separator-header + sep = QListWidgetItem("── Registreret på denne sang ──") + sep.setForeground(QColor("#5a6070")) + sep.setFlags(Qt.ItemFlag.ItemIsEnabled) # synlig men ikke valgbar + sep.setData(Qt.ItemDataRole.UserRole, None) + self._list.addItem(sep) + for name in matching: + item = QListWidgetItem(f"★ {name}") + item.setData(Qt.ItemDataRole.UserRole, {"name": name}) + item.setForeground(QColor("#e8a020")) + self._list.addItem(item) + + # Separator for alle danse + if suggestions: + sep2 = QListWidgetItem("── Alle danse ──") + sep2.setForeground(QColor("#5a6070")) + sep2.setFlags(Qt.ItemFlag.ItemIsEnabled) # synlig men ikke valgbar + sep2.setData(Qt.ItemDataRole.UserRole, None) + self._list.addItem(sep2) for s in suggestions: s = dict(s) @@ -119,7 +144,9 @@ class DancePickerDialog(QDialog): logging.getLogger(__name__).warning(f'Dans-forslag fejl: {e}', exc_info=True) def _on_item_clicked(self, item: QListWidgetItem): - data = item.data(Qt.ItemDataRole.UserRole) or {} + data = item.data(Qt.ItemDataRole.UserRole) + if not data: # separator — ignorer + return name = data.get("name", "") level = data.get("level", "") choreo = data.get("choreo", "") @@ -133,13 +160,15 @@ class DancePickerDialog(QDialog): self._info_lbl.setText(" · ".join(parts) if parts else "") def _on_selected(self, item: QListWidgetItem): + data = item.data(Qt.ItemDataRole.UserRole) + if not data: # separator + return self._on_item_clicked(item) self._on_accept() def _on_accept(self): self._chosen_dance = self._edit.text().strip() - if self._chosen_dance: - self.accept() + self.accept() # tillad tom streng = ingen dans def get_dance(self) -> str: return self._chosen_dance diff --git a/linedance-app/ui/main_window.py b/linedance-app/ui/main_window.py index 44dadf67..e1020a65 100644 --- a/linedance-app/ui/main_window.py +++ b/linedance-app/ui/main_window.py @@ -992,6 +992,8 @@ class MainWindow(QMainWindow): if dialog.exec(): # Genindlæs biblioteket så ændringer vises QTimer.singleShot(200, self._reload_library) + # Push ændringer til server med det samme + QTimer.singleShot(500, self._manual_sync) def _send_mail(self, song: dict): import subprocess, sys, shutil, urllib.parse diff --git a/linedance-app/ui/playlist_panel.py b/linedance-app/ui/playlist_panel.py index ae2373ab..4950c7f7 100644 --- a/linedance-app/ui/playlist_panel.py +++ b/linedance-app/ui/playlist_panel.py @@ -774,34 +774,31 @@ class PlaylistPanel(QWidget): ) if dlg.exec(): chosen = dlg.get_dance() - choreo = "" # Koreograf redigeres i tag-editoren, ikke her - if chosen: - song["active_dance"] = chosen - song["active_choreo"] = choreo - self._refresh() - - # Gem permanent hvis sangen ikke allerede har denne dans tagget - already_tagged = chosen in dances - if not already_tagged: - self._save_dance_permanently(idx, song, chosen, choreo) - else: - # Midlertidigt — kun dance_override på listen - self._sync_dance_to_db(idx, song) + # Dans-valg i playlisten er altid midlertidigt — kun dance_override + song["active_dance"] = chosen # tom streng = ingen dans + self._refresh() + self._sync_dance_to_db(idx, song) def _sync_dance_to_db(self, idx: int, song: dict): """Gem dance_override til playlist_songs (midlertidigt valg).""" + import logging + _log = logging.getLogger(__name__) if not self._named_playlist_id: + _log.warning("_sync_dance_to_db: ingen named_playlist_id") return try: from local.local_db import get_db + dance_val = song.get("active_dance") or "" with get_db() as conn: - conn.execute( + rows_affected = conn.execute( "UPDATE playlist_songs SET dance_override=? " "WHERE playlist_id=? AND position=?", - (song.get("active_dance", ""), self._named_playlist_id, idx + 1) - ) - except Exception: - pass + (dance_val, self._named_playlist_id, idx + 1) + ).rowcount + _log.info(f"dance_override='{dance_val}' gemt på position {idx+1}, {rows_affected} rækker") + except Exception as e: + import logging + logging.getLogger(__name__).warning(f"_sync_dance_to_db fejl: {e}") def _save_dance_permanently(self, idx: int, song: dict, dance_name: str, choreo: str = ""): """ diff --git a/linedance-app/ui/tag_editor.py b/linedance-app/ui/tag_editor.py index 0a6da8d0..cdb163b6 100644 --- a/linedance-app/ui/tag_editor.py +++ b/linedance-app/ui/tag_editor.py @@ -591,16 +591,24 @@ class TagEditorDialog(QDialog): ) # Skriv danse-navne til filen + import logging as _logging + _log = _logging.getLogger(__name__) + dance_names = [d["name"] for d in dances] + _log.info(f"Gemmer {len(dances)} danse: {dance_names}, local_path={local_path!r}") if local_path and can_write_dances(local_path): - dance_names = [d["name"] for d in dances] try: - if not write_dances(local_path, dance_names): + result = write_dances(local_path, dance_names) + _log.info(f"write_dances resultat: {result}") + if not result: QMessageBox.warning(self, "Advarsel", "Gemt i database, men kunne ikke skrive til mp3-filen.\n" "(Filen understøtter ikke dans-tags)") except Exception as write_err: + _log.warning(f"write_dances fejl: {write_err}") QMessageBox.warning(self, "Advarsel", f"Gemt i database, men fejl ved skrivning til fil:\n{write_err}") + else: + _log.info(f"Springer fil-skrivning over: local_path={local_path!r}") self.accept()