diff --git a/linedance-api/app/routers/sync.py b/linedance-api/app/routers/sync.py index e3ebe6bf..d4c9e780 100644 --- a/linedance-api/app/routers/sync.py +++ b/linedance-api/app/routers/sync.py @@ -380,10 +380,11 @@ def pull( continue level = db.query(DanceLevel).filter_by(id=dance.level_id).first() if dance.level_id else None song_tags.append({ - "song_id": sd.song_id, - "dance_name": dance.name, - "level_name": level.name if level else "", - "dance_order": sd.dance_order, + "song_id": sd.song_id, + "dance_name": dance.name, + "choreographer": dance.choreographer or "", + "level_name": level.name if level else "", + "dance_order": sd.dance_order, }) return { diff --git a/linedance-app/local/local_db.py b/linedance-app/local/local_db.py index 647975e8..7e7b147c 100644 --- a/linedance-app/local/local_db.py +++ b/linedance-app/local/local_db.py @@ -88,6 +88,7 @@ CREATE TABLE IF NOT EXISTS dance_levels ( ); -- Danse +-- Dans + niveau + koreograf er unik kombination CREATE TABLE IF NOT EXISTS dances ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, @@ -98,7 +99,7 @@ CREATE TABLE IF NOT EXISTS dances ( notes TEXT NOT NULL DEFAULT '', use_count INTEGER NOT NULL DEFAULT 1, source TEXT NOT NULL DEFAULT 'local', - UNIQUE(name, level_id) + UNIQUE(name, level_id, choreographer) ); -- Sang-dans tags @@ -582,4 +583,111 @@ def upsert_dance_levels(levels: list[dict]): ON CONFLICT(name) DO UPDATE SET sort_order=excluded.sort_order, description=excluded.description - """, lvl) \ No newline at end of file + """, lvl) + +# ── Dans-søgning (til DancePickerDialog) ───────────────────────────────────── + +def get_dance_suggestions(prefix: str = "", limit: int = 20) -> list: + """Hent dans-forslag med niveau og koreograf til autoudfyld.""" + with get_db() as conn: + pattern = f"{prefix}%" + return conn.execute(""" + SELECT d.id, d.name, d.level_id, dl.name as level_name, + d.choreographer, d.use_count + FROM dances d + LEFT JOIN dance_levels dl ON dl.id = d.level_id + WHERE d.name LIKE ? COLLATE NOCASE + ORDER BY d.use_count DESC, d.name + LIMIT ? + """, (pattern, limit)).fetchall() + + +def get_choreographer_suggestions(prefix: str = "", limit: int = 15) -> list[str]: + """Hent koreograf-navne til autoudfyld.""" + with get_db() as conn: + pattern = f"{prefix}%" + rows = conn.execute(""" + SELECT DISTINCT choreographer FROM dances + WHERE choreographer != '' AND choreographer LIKE ? COLLATE NOCASE + ORDER BY choreographer + LIMIT ? + """, (pattern, limit)).fetchall() + return [r["choreographer"] for r in rows] + +# ── Dans-søgning (til DancePickerDialog og DanceInfoDialog) ────────────────── + +def get_dance_suggestions(prefix: str = "", limit: int = 20) -> list: + """Hent dans-forslag med niveau og koreograf til autoudfyld.""" + with get_db() as conn: + pattern = f"{prefix}%" + return conn.execute(""" + SELECT d.id, d.name, d.level_id, dl.name as level_name, + d.choreographer, d.use_count + FROM dances d + LEFT JOIN dance_levels dl ON dl.id = d.level_id + WHERE d.name LIKE ? COLLATE NOCASE + ORDER BY d.use_count DESC, d.name + LIMIT ? + """, (pattern, limit)).fetchall() + + +def get_choreographer_suggestions(prefix: str = "", limit: int = 15) -> list[str]: + """Hent koreograf-navne til autoudfyld.""" + with get_db() as conn: + pattern = f"{prefix}%" + rows = conn.execute(""" + SELECT DISTINCT choreographer FROM dances + WHERE choreographer != '' AND choreographer LIKE ? COLLATE NOCASE + ORDER BY choreographer + LIMIT ? + """, (pattern, limit)).fetchall() + return [r["choreographer"] for r in rows] + + +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, + d.video_url, d.stepsheet_url, d.notes, sd.dance_order + FROM song_dances sd + JOIN dances d ON d.id = sd.dance_id + LEFT JOIN dance_levels dl ON dl.id = d.level_id + WHERE sd.song_id = ? + ORDER BY sd.dance_order + """, (song_id,)).fetchall() + + +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, + d.video_url, d.stepsheet_url, sad.note + FROM song_alt_dances sad + JOIN dances d ON d.id = sad.dance_id + LEFT JOIN dance_levels dl ON dl.id = d.level_id + WHERE sad.song_id = ? + ORDER BY d.name + """, (song_id,)).fetchall() + +def get_or_create_dance(name: str, level_id: int | None, conn, + choreographer: str = "") -> int: + """ + Find eller opret dans. Returnerer dance_id. + Dans + niveau + koreograf er unik kombination. + """ + choreo = choreographer or "" + existing = conn.execute( + "SELECT id FROM dances WHERE name=? " + "AND (level_id=? OR (level_id IS NULL AND ? IS NULL)) " + "AND choreographer=?", + (name, level_id, level_id, choreo) + ).fetchone() + if existing: + return existing["id"] + cur = conn.execute( + "INSERT INTO dances (name, level_id, choreographer) VALUES (?,?,?)", + (name, level_id, choreo) + ) + return cur.lastrowid \ No newline at end of file diff --git a/linedance-app/local/scanner.py b/linedance-app/local/scanner.py index b85a5817..7d5423f7 100644 --- a/linedance-app/local/scanner.py +++ b/linedance-app/local/scanner.py @@ -158,30 +158,28 @@ def scan_library(library_id: int, library_path: str, db_path: str, # Opret eller opdater fil-post _upsert_file_conn(conn, song_id, path_str, file_format, mtime, extra_tags) - # Dans-tags fra fil + # Dans-tags fra fil — synkroniser altid fra filen file_dances = tags.get("dances", []) if file_dances: - existing_count = conn.execute( - "SELECT COUNT(*) FROM song_dances WHERE song_id=?", (song_id,) - ).fetchone()[0] - if existing_count == 0: - import uuid - for order, dance_name in enumerate(file_dances, start=1): - dance_row = conn.execute( - "SELECT id FROM dances WHERE name=? COLLATE NOCASE LIMIT 1", - (dance_name,) - ).fetchone() - if not dance_row: - cur = conn.execute( - "INSERT INTO dances (name) VALUES (?)", (dance_name,) - ) - dance_id = cur.lastrowid - else: - dance_id = dance_row["id"] - conn.execute( - "INSERT OR IGNORE INTO song_dances (id, song_id, dance_id, dance_order) VALUES (?,?,?,?)", - (str(uuid.uuid4()), song_id, dance_id, order) + import uuid + # Slet eksisterende og genindsæt fra filen + conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,)) + for order, dance_name in enumerate(file_dances, start=1): + dance_row = conn.execute( + "SELECT id FROM dances WHERE name=? COLLATE NOCASE LIMIT 1", + (dance_name,) + ).fetchone() + if not dance_row: + cur = conn.execute( + "INSERT INTO dances (name) VALUES (?)", (dance_name,) ) + dance_id = cur.lastrowid + else: + dance_id = dance_row["id"] + conn.execute( + "INSERT OR IGNORE INTO song_dances (id, song_id, dance_id, dance_order) VALUES (?,?,?,?)", + (str(uuid.uuid4()), song_id, dance_id, order) + ) conn.commit() except Exception as e: diff --git a/linedance-app/local/sync_manager.py b/linedance-app/local/sync_manager.py index a5d2e35d..49621bb6 100644 --- a/linedance-app/local/sync_manager.py +++ b/linedance-app/local/sync_manager.py @@ -323,22 +323,30 @@ class SyncManager: conn.execute("PRAGMA journal_mode=WAL") try: - # Opdater dans-info + # Synkroniser danse fra server — opret nye, opdater eksisterende for d in data.get("dances", []): if not d.get("name"): continue + choreo = d.get("choreographer", "") or "" existing = conn.execute( - "SELECT id FROM dances WHERE name=? COLLATE NOCASE", (d["name"],) + "SELECT id FROM dances WHERE name=? COLLATE NOCASE " + "AND choreographer=? LIMIT 1", + (d["name"], choreo) ).fetchone() - if existing and (d.get("choreographer") or d.get("video_url")): + if existing: conn.execute(""" UPDATE dances SET - choreographer = CASE WHEN choreographer='' THEN ? ELSE choreographer END, video_url = CASE WHEN video_url='' THEN ? ELSE video_url END, stepsheet_url = CASE WHEN stepsheet_url='' THEN ? ELSE stepsheet_url END WHERE id=? - """, (d.get("choreographer",""), d.get("video_url",""), - d.get("stepsheet_url",""), existing["id"])) + """, (d.get("video_url",""), d.get("stepsheet_url",""), existing["id"])) + else: + conn.execute( + "INSERT OR IGNORE INTO dances (name, choreographer, video_url, stepsheet_url, notes) " + "VALUES (?,?,?,?,?)", + (d["name"], choreo, + d.get("video_url",""), d.get("stepsheet_url",""), d.get("notes","")) + ) # Hent soft-slettede server-IDs så vi springer dem over deleted_server_ids = { @@ -468,6 +476,49 @@ class SyncManager: song_data.get("dance_override","") or "")) position += 1 + # Importer sang-dans tags fra server + for st in data.get("song_tags", []): + server_song_id = st.get("song_id", "") + dance_name = st.get("dance_name", "") + dance_order = st.get("dance_order", 1) + choreo = st.get("choreographer", "") or "" + if not server_song_id or not dance_name: + continue + + # Find lokal sang + local_song = conn.execute( + "SELECT id FROM songs WHERE id=?", (server_song_id,) + ).fetchone() + if not local_song: + continue + + # Find dans + dance_row = conn.execute( + "SELECT id FROM dances WHERE name=? COLLATE NOCASE " + "AND choreographer=? LIMIT 1", + (dance_name, choreo) + ).fetchone() + if not dance_row: + cur = conn.execute( + "INSERT OR IGNORE INTO dances (name, choreographer) VALUES (?,?)", + (dance_name, choreo) + ) + dance_id = cur.lastrowid + else: + dance_id = dance_row["id"] + + # Tilføj sang-dans tag hvis ikke allerede der + existing_sd = conn.execute( + "SELECT id FROM song_dances WHERE song_id=? AND dance_id=?", + (server_song_id, dance_id) + ).fetchone() + if not existing_sd: + conn.execute( + "INSERT OR IGNORE INTO song_dances (id, song_id, dance_id, dance_order) " + "VALUES (?,?,?,?)", + (str(uuid.uuid4()), server_song_id, dance_id, dance_order) + ) + conn.commit() except Exception: diff --git a/linedance-app/local/tag_reader.py b/linedance-app/local/tag_reader.py index bc6d09d5..1c0bce61 100644 --- a/linedance-app/local/tag_reader.py +++ b/linedance-app/local/tag_reader.py @@ -410,6 +410,11 @@ def read_dances_from_file(path: str | Path) -> list[str]: return tags.get("dances", []) +def write_dance_to_file(path: str | Path, dances: list[str]) -> bool: + """Alias for write_dances — skriv danse-liste til fil.""" + return write_dances(path, dances) + + # ── BPM-analyse ─────────────────────────────────────────────────────────────── def analyze_bpm(path: str | Path) -> float | None: @@ -513,4 +518,4 @@ def _write_mbid_m4a(path: Path, mbid: str) -> bool: return True except Exception as e: logger.warning(f"MBID M4A skrivefejl {path}: {e}") - return False + return False \ No newline at end of file diff --git a/linedance-app/ui/dance_info_dialog.py b/linedance-app/ui/dance_info_dialog.py index 3cb2c355..f1570a25 100644 --- a/linedance-app/ui/dance_info_dialog.py +++ b/linedance-app/ui/dance_info_dialog.py @@ -31,54 +31,51 @@ class DanceInfoDialog(QDialog): def _load_dances(self): try: - from local.local_db import get_dances_for_song, get_alt_dances_for_song, new_conn - conn = new_conn() + from local.local_db import get_db + with get_db() as conn: + rows = conn.execute(""" + SELECT d.id, d.name, d.choreographer, + d.video_url, d.stepsheet_url, d.notes, + dl.name as level_name + FROM song_dances sd + JOIN dances d ON d.id = sd.dance_id + LEFT JOIN dance_levels dl ON dl.id = d.level_id + WHERE sd.song_id=? ORDER BY sd.dance_order + """, (self._song.get("id"),)).fetchall() - rows = conn.execute(""" - SELECT d.id, d.name, d.level_id, d.choreographer, - d.video_url, d.stepsheet_url, d.notes, - dl.name as level_name - FROM song_dances sd - JOIN dances d ON d.id = sd.dance_id - LEFT JOIN dance_levels dl ON dl.id = d.level_id - WHERE sd.song_id=? ORDER BY sd.dance_order - """, (self._song.get("id"),)).fetchall() + for row in rows: + self._dances.append({ + "dance_id": row["id"], + "name": row["name"], + "level_name": row["level_name"] or "", + "choreographer": row["choreographer"] or "", + "video_url": row["video_url"] or "", + "stepsheet_url": row["stepsheet_url"] or "", + "notes": row["notes"] or "", + "is_alt": False, + }) - for row in rows: - self._dances.append({ - "dance_id": row["id"], - "name": row["name"], - "level_name": row["level_name"] or "", - "choreographer": row["choreographer"] or "", - "video_url": row["video_url"] or "", - "stepsheet_url": row["stepsheet_url"] or "", - "notes": row["notes"] or "", - "is_alt": False, - }) + alt_rows = conn.execute(""" + SELECT d.id, d.name, d.choreographer, + d.video_url, d.stepsheet_url, d.notes, + dl.name as level_name + FROM song_alt_dances sad + JOIN dances d ON d.id = sad.dance_id + LEFT JOIN dance_levels dl ON dl.id = d.level_id + WHERE sad.song_id=? ORDER BY d.name + """, (self._song.get("id"),)).fetchall() - # Alternativ-danse - alt_rows = conn.execute(""" - SELECT d.id, d.name, d.level_id, d.choreographer, - d.video_url, d.stepsheet_url, d.notes, - dl.name as level_name - FROM song_alt_dances sad - JOIN dances d ON d.id = sad.dance_id - LEFT JOIN dance_levels dl ON dl.id = d.level_id - WHERE sad.song_id=? ORDER BY d.name - """, (self._song.get("id"),)).fetchall() - - for row in alt_rows: - self._dances.append({ - "dance_id": row["id"], - "name": row["name"], - "level_name": row["level_name"] or "", - "choreographer": row["choreographer"] or "", - "video_url": row["video_url"] or "", - "stepsheet_url": row["stepsheet_url"] or "", - "notes": row["notes"] or "", - "is_alt": True, - }) - conn.close() + for row in alt_rows: + self._dances.append({ + "dance_id": row["id"], + "name": row["name"], + "level_name": row["level_name"] or "", + "choreographer": row["choreographer"] or "", + "video_url": row["video_url"] or "", + "stepsheet_url": row["stepsheet_url"] or "", + "notes": row["notes"] or "", + "is_alt": True, + }) except Exception as e: print(f"DanceInfoDialog load fejl: {e}") @@ -204,15 +201,14 @@ class DanceInfoDialog(QDialog): def _save(self): self._save_to_cache(self._current_idx) try: - from local.local_db import update_dance_info - for d in self._dances: - update_dance_info( - d["dance_id"], - choreographer = d["choreographer"], - video_url = d["video_url"], - stepsheet_url = d["stepsheet_url"], - notes = d["notes"], - ) + from local.local_db import get_db + with get_db() as conn: + for d in self._dances: + conn.execute(""" + UPDATE dances SET choreographer=?, video_url=?, + stepsheet_url=?, notes=? WHERE id=? + """, (d["choreographer"], d["video_url"], + d["stepsheet_url"], d["notes"], d["dance_id"])) self.accept() except Exception as e: QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}") @@ -223,4 +219,4 @@ class DanceInfoDialog(QDialog): return if not url.startswith("http"): url = "https://" + url - QDesktopServices.openUrl(QUrl(url)) + QDesktopServices.openUrl(QUrl(url)) \ No newline at end of file diff --git a/linedance-app/ui/dance_picker_dialog.py b/linedance-app/ui/dance_picker_dialog.py index 75d1efff..c405468a 100644 --- a/linedance-app/ui/dance_picker_dialog.py +++ b/linedance-app/ui/dance_picker_dialog.py @@ -1,5 +1,7 @@ """ -dance_picker_dialog.py — Dialog til at vælge dans og koreograf med autoudfyld. +dance_picker_dialog.py — Simpel dans-vælger til danselisten. +Viser dansenavn primært. Niveau og koreograf vises som info hvis tilgængeligt. +Ingen redigering af metadata — det hører til i tag-editoren i biblioteket. """ from PyQt6.QtWidgets import ( @@ -10,18 +12,18 @@ from PyQt6.QtCore import Qt, QTimer class DancePickerDialog(QDialog): - def __init__(self, current_dance: str = "", current_choreo: str = "", - song_title: str = "", parent=None): + def __init__(self, current_dance: str = "", song_title: str = "", + existing_dances: list[str] = None, parent=None): super().__init__(parent) self._chosen_dance = current_dance - self._chosen_choreo = current_choreo + self._existing_dances = existing_dances or [] self.setWindowTitle("Vælg dans") - self.setMinimumWidth(400) - self.setFixedWidth(440) - self._build_ui(current_dance, current_choreo, song_title) - self._load_dance_suggestions("") + self.setMinimumWidth(420) + self.setFixedWidth(460) + self._build_ui(current_dance, song_title) + self._load_suggestions("") - def _build_ui(self, current_dance: str, current_choreo: str, song_title: str): + def _build_ui(self, current_dance: str, song_title: str): layout = QVBoxLayout(self) layout.setContentsMargins(12, 12, 12, 12) layout.setSpacing(8) @@ -32,65 +34,38 @@ class DancePickerDialog(QDialog): lbl.setWordWrap(True) layout.addWidget(lbl) - # ── Dans ────────────────────────────────────────────────────────────── - lbl2 = QLabel("Dans:") - lbl2.setObjectName("track_meta") - layout.addWidget(lbl2) + layout.addWidget(QLabel("Dans:")) - self._edit_dance = QLineEdit() - self._edit_dance.setText(current_dance) - self._edit_dance.setPlaceholderText("Skriv dans-navn...") - self._edit_dance.selectAll() - self._edit_dance.textChanged.connect(self._on_dance_text_changed) - self._edit_dance.returnPressed.connect(lambda: self._edit_choreo.setFocus()) - layout.addWidget(self._edit_dance) + self._edit = QLineEdit() + self._edit.setText(current_dance) + self._edit.setPlaceholderText("Skriv dans-navn...") + self._edit.selectAll() + self._edit.textChanged.connect(self._on_text_changed) + self._edit.returnPressed.connect(self._on_accept) + layout.addWidget(self._edit) - self._dance_list = QListWidget() - self._dance_list.setMaximumHeight(160) - self._dance_list.itemDoubleClicked.connect(self._on_dance_selected) - self._dance_list.itemClicked.connect( - lambda item: self._edit_dance.setText( - item.data(Qt.ItemDataRole.UserRole) or item.text().split(" / ")[0] - ) - ) - layout.addWidget(self._dance_list) + # Forslagsliste + self._list = QListWidget() + self._list.setMinimumHeight(200) + self._list.itemDoubleClicked.connect(self._on_selected) + self._list.itemClicked.connect(self._on_item_clicked) + layout.addWidget(self._list) - # ── Koreograf ───────────────────────────────────────────────────────── - lbl3 = QLabel("Koreograf (valgfri):") - lbl3.setObjectName("track_meta") - layout.addWidget(lbl3) + # Info-label — viser niveau/koreograf for valgt dans + self._info_lbl = QLabel("") + self._info_lbl.setObjectName("result_count") + self._info_lbl.setWordWrap(True) + layout.addWidget(self._info_lbl) - self._edit_choreo = QLineEdit() - self._edit_choreo.setText(current_choreo) - self._edit_choreo.setPlaceholderText("Koreografens navn...") - self._edit_choreo.textChanged.connect(self._on_choreo_text_changed) - self._edit_choreo.returnPressed.connect(self._on_accept) - layout.addWidget(self._edit_choreo) - - self._choreo_list = QListWidget() - self._choreo_list.setMaximumHeight(100) - self._choreo_list.itemDoubleClicked.connect(self._on_choreo_selected) - self._choreo_list.itemClicked.connect( - lambda item: self._edit_choreo.setText(item.text()) - ) - layout.addWidget(self._choreo_list) - - # ── Debounce timere ─────────────────────────────────────────────────── - self._dance_timer = QTimer(self) - self._dance_timer.setSingleShot(True) - self._dance_timer.setInterval(200) - self._dance_timer.timeout.connect( - lambda: self._load_dance_suggestions(self._edit_dance.text().strip()) + # Debounce timer + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.setInterval(150) + self._timer.timeout.connect( + lambda: self._load_suggestions(self._edit.text().strip()) ) - self._choreo_timer = QTimer(self) - self._choreo_timer.setSingleShot(True) - self._choreo_timer.setInterval(200) - self._choreo_timer.timeout.connect( - lambda: self._load_choreo_suggestions(self._edit_choreo.text().strip()) - ) - - # ── Knapper ─────────────────────────────────────────────────────────── + # Knapper btn_row = QHBoxLayout() btn_row.addStretch() btn_cancel = QPushButton("Annuller") @@ -102,62 +77,73 @@ class DancePickerDialog(QDialog): btn_row.addWidget(btn_ok) layout.addLayout(btn_row) - self._edit_dance.setFocus() + self._edit.setFocus() - def _on_dance_text_changed(self): - self._dance_timer.start() + def _on_text_changed(self): + self._timer.start() - def _on_choreo_text_changed(self): - self._choreo_timer.start() - - def _load_dance_suggestions(self, prefix: str): + def _load_suggestions(self, prefix: str): try: from local.local_db import get_dance_suggestions - suggestions = get_dance_suggestions(prefix or "", limit=20) - self._dance_list.clear() + 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) + for s in suggestions: - label = f"{s['name']} / {s['level_name']}" if s.get("level_name") else s["name"] - if s.get("choreographer"): - label += f" ({s['choreographer']})" + s = dict(s) + name = s["name"] + level = s.get("level_name") or "" + choreo = s.get("choreographer") or "" + parts = [name] + if level: + parts.append(level) + if choreo: + parts.append(choreo) + label = " / ".join(parts) item = QListWidgetItem(label) - item.setData(Qt.ItemDataRole.UserRole, s["name"]) - item.setData(Qt.ItemDataRole.UserRole + 1, s.get("choreographer", "")) - self._dance_list.addItem(item) - except Exception: - pass + item.setData(Qt.ItemDataRole.UserRole, { + "name": name, + "level": level, + "choreo": choreo, + }) + self._list.addItem(item) + except Exception as e: + import logging + logging.getLogger(__name__).warning(f'Dans-forslag fejl: {e}', exc_info=True) - def _load_choreo_suggestions(self, prefix: str): - try: - from local.local_db import get_choreographer_suggestions - suggestions = get_choreographer_suggestions(prefix or "", limit=15) - self._choreo_list.clear() - for name in suggestions: - self._choreo_list.addItem(QListWidgetItem(name)) - except Exception: - pass + def _on_item_clicked(self, item: QListWidgetItem): + data = item.data(Qt.ItemDataRole.UserRole) or {} + name = data.get("name", "") + level = data.get("level", "") + choreo = data.get("choreo", "") + self._edit.setText(name) + # Vis info + parts = [] + if level: + parts.append(level) + if choreo: + parts.append(choreo) + self._info_lbl.setText(" · ".join(parts) if parts else "") - def _on_dance_selected(self, item: QListWidgetItem): - name = item.data(Qt.ItemDataRole.UserRole) or item.text().split(" / ")[0] - choreo = item.data(Qt.ItemDataRole.UserRole + 1) or "" - self._edit_dance.setText(name) - if choreo and not self._edit_choreo.text().strip(): - self._edit_choreo.setText(choreo) - self._chosen_dance = name - self._chosen_choreo = self._edit_choreo.text().strip() - self.accept() - - def _on_choreo_selected(self, item: QListWidgetItem): - self._edit_choreo.setText(item.text()) - self._choreo_list.clear() + def _on_selected(self, item: QListWidgetItem): + self._on_item_clicked(item) + self._on_accept() def _on_accept(self): - self._chosen_dance = self._edit_dance.text().strip() - self._chosen_choreo = self._edit_choreo.text().strip() + self._chosen_dance = self._edit.text().strip() if self._chosen_dance: self.accept() def get_dance(self) -> str: return self._chosen_dance + # Behold get_choreo for bagudkompatibilitet — returnerer altid "" def get_choreo(self) -> str: - return self._chosen_choreo + return "" \ No newline at end of file diff --git a/linedance-app/ui/playlist_panel.py b/linedance-app/ui/playlist_panel.py index 41ed4892..ae2373ab 100644 --- a/linedance-app/ui/playlist_panel.py +++ b/linedance-app/ui/playlist_panel.py @@ -756,28 +756,40 @@ class PlaylistPanel(QWidget): def _change_dance(self, idx: int, song: dict): """Lad brugeren vælge/skrive hvilken dans der vises for dette nummer.""" from ui.dance_picker_dialog import DancePickerDialog + dances = song.get("dances", []) current = song.get("active_dance", "") if not current: - dances = song.get("dances", []) current = dances[0] if dances else "" current_choreo = song.get("active_choreo", "") + + # Afgør om valget er permanent eller midlertidigt + # Permanent: ingen dans tagget, eller valgt dans er ikke i de taggede + # Midlertidig: sangen har flere danse og brugeren vælger en af dem + dlg = DancePickerDialog( current_dance=current, - current_choreo=current_choreo, song_title=song.get("title", ""), + existing_dances=dances, parent=self.window() ) if dlg.exec(): chosen = dlg.get_dance() - choreo = dlg.get_choreo() + choreo = "" # Koreograf redigeres i tag-editoren, ikke her if chosen: song["active_dance"] = chosen song["active_choreo"] = choreo self._refresh() - self._sync_dance_to_db(idx, song) + + # 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) def _sync_dance_to_db(self, idx: int, song: dict): - """Gem dance_override til playlist_songs.""" + """Gem dance_override til playlist_songs (midlertidigt valg).""" if not self._named_playlist_id: return try: @@ -791,6 +803,82 @@ class PlaylistPanel(QWidget): except Exception: pass + def _save_dance_permanently(self, idx: int, song: dict, dance_name: str, choreo: str = ""): + """ + Gem dans permanent på sangen: + 1. song_dances tabellen + 2. ID3-tag i filen (hvis tilgængelig) + 3. Opdater sang-dict så listen vises korrekt + """ + import uuid + song_id = song.get("id", "") + local_path = song.get("local_path", "") + + try: + from local.local_db import get_db + with get_db() as conn: + # Find eller opret dans + dance_row = conn.execute( + "SELECT id FROM dances WHERE name=? COLLATE NOCASE LIMIT 1", + (dance_name,) + ).fetchone() + if dance_row: + dance_id = dance_row["id"] + if choreo: + conn.execute( + "UPDATE dances SET choreographer=? WHERE id=? AND choreographer=''", + (choreo, dance_id) + ) + else: + cur = conn.execute( + "INSERT INTO dances (name, choreographer) VALUES (?,?)", + (dance_name, choreo or "") + ) + dance_id = cur.lastrowid + + # Tilføj til song_dances + existing = conn.execute( + "SELECT id FROM song_dances WHERE song_id=? AND dance_id=?", + (song_id, dance_id) + ).fetchone() + if not existing: + # Find næste dance_order + max_order = conn.execute( + "SELECT MAX(dance_order) FROM song_dances WHERE song_id=?", + (song_id,) + ).fetchone()[0] or 0 + conn.execute( + "INSERT INTO song_dances (id, song_id, dance_id, dance_order) VALUES (?,?,?,?)", + (str(uuid.uuid4()), song_id, dance_id, max_order + 1) + ) + + # Opdater sang-dict + dances = song.get("dances", []) + if dance_name not in dances: + dances.append(dance_name) + song["dances"] = dances + song["active_dance"] = dance_name + + # Gem i ID3-tag hvis filen er tilgængelig + if local_path: + try: + from local.tag_reader import write_dance_to_file + write_dance_to_file(local_path, dances) + except Exception: + pass + + # Opdater også dance_override på listen + self._sync_dance_to_db(idx, song) + + import logging + logging.getLogger(__name__).info( + f"Dans gemt permanent: '{dance_name}' → '{song.get('title','?')}'" + ) + + except Exception as e: + import logging + logging.getLogger(__name__).warning(f"Kunne ikke gemme dans permanent: {e}") + def _sync_ws_to_db(self, idx: int, song: dict): """Gem is_workshop til playlist_songs — både navngiven og aktiv liste.""" pl_ids = [] @@ -1267,11 +1355,11 @@ class PlaylistPanel(QWidget): status = self._statuses[i] icon = self.STATUS_ICON.get(status, " ") - # Vis active_dance (override eller første dans) eller alle danse + # Dans er primær tekst, sang er sekundær active = song.get("active_dance", "") if not active: dances = song.get("dances", []) - active = dances[0] if dances else "ingen dans tagget" + active = dances[0] if dances else "— ingen dans —" ws_tag = " 🎓" if song.get("is_workshop") else "" # Tilgængeligheds-dot til højre — kun hvis tjekket (ikke yellow) @@ -1279,8 +1367,8 @@ class PlaylistPanel(QWidget): avail_color = {"green": "#27ae60", "red": "#e74c3c"}.get(avail, None) avail_tip = {"green": "Tilgængelig lokalt", "red": "Ikke fundet lokalt"}.get(avail, "") - text = (f"{i+1:>2}. {song.get('title','—')}{ws_tag}\n" - f" {song.get('artist','')} · {active}") + text = (f"{i+1:>2}. {active}{ws_tag}\n" + f" {song.get('title','—')} · {song.get('artist','')}") item = QListWidgetItem(f"{icon} {text}") item.setData(Qt.ItemDataRole.UserRole, i) item.setData(Qt.ItemDataRole.UserRole + 1, avail_color) diff --git a/linedance-app/ui/tag_editor.py b/linedance-app/ui/tag_editor.py index cc78388a..0a6da8d0 100644 --- a/linedance-app/ui/tag_editor.py +++ b/linedance-app/ui/tag_editor.py @@ -164,7 +164,7 @@ class TagEditorDialog(QDialog): # Forslags-liste self._dance_suggestions = QListWidget() - self._dance_suggestions.setMaximumHeight(120) + self._dance_suggestions.setFixedHeight(150) self._dance_suggestions.setFocusPolicy(Qt.FocusPolicy.NoFocus) self._dance_suggestions.itemClicked.connect( lambda item: self._add_from_suggestion(item, "dance") @@ -328,7 +328,13 @@ class TagEditorDialog(QDialog): suggestions = get_dance_suggestions(prefix, limit=20) list_widget.clear() for s in suggestions: - label = f"{s['name']} / {s['level_name']}" if s.get("level_name") else s["name"] + s = dict(s) + parts = [s["name"]] + if s.get("level_name"): + parts.append(s["level_name"]) + if s.get("choreographer"): + parts.append(s["choreographer"]) + label = " / ".join(parts) item = QListWidgetItem(label) item.setData(Qt.ItemDataRole.UserRole, s.get("level_id")) item.setData(Qt.ItemDataRole.UserRole + 1, s["name"]) @@ -373,16 +379,21 @@ class TagEditorDialog(QDialog): suggestions = get_dance_suggestions(prefix, limit=15) list_widget.clear() for s in suggestions: - label = f"{s['name']} / {s['level_name']}" if s.get("level_name") else s["name"] + s = dict(s) + parts = [s["name"]] + if s.get("level_name"): + parts.append(s["level_name"]) if s.get("choreographer"): - label += f" · {s['choreographer']}" + parts.append(s["choreographer"]) + label = " / ".join(parts) item = QListWidgetItem(label) item.setData(Qt.ItemDataRole.UserRole, s.get("level_id")) item.setData(Qt.ItemDataRole.UserRole + 1, s["name"]) item.setData(Qt.ItemDataRole.UserRole + 2, s.get("choreographer", "")) list_widget.addItem(item) - except Exception: - pass + except Exception as e: + import logging + logging.getLogger(__name__).warning(f"Dans-forslag fejl: {e}", exc_info=True) def _add_from_suggestion(self, item, panel: str): """Tilføj dans fra forslags-listen ved klik.""" @@ -451,7 +462,7 @@ class TagEditorDialog(QDialog): layout.addWidget(self._new_alt) self._alt_suggestions = QListWidget() - self._alt_suggestions.setMaximumHeight(120) + self._alt_suggestions.setFixedHeight(150) self._alt_suggestions.setFocusPolicy(Qt.FocusPolicy.NoFocus) self._alt_suggestions.itemClicked.connect( lambda item: self._add_from_suggestion(item, "alt") @@ -530,7 +541,8 @@ class TagEditorDialog(QDialog): local_path = self._song.get("local_path", "") try: - from local.local_db import new_conn, get_or_create_dance + import uuid + from local.local_db import get_db, get_or_create_dance from local.tag_reader import write_dances, can_write_dances # Saml data fra UI @@ -554,33 +566,29 @@ class TagEditorDialog(QDialog): "note": "", }) - conn = new_conn() + with get_db() as conn: + # Slet eksisterende + conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,)) + conn.execute("DELETE FROM song_alt_dances WHERE song_id=?", (song_id,)) - # Slet eksisterende - conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,)) - conn.execute("DELETE FROM song_alt_dances WHERE song_id=?", (song_id,)) + # Indsæt hoveddanse + for i, d in enumerate(dances, 1): + dance_id = get_or_create_dance(d["name"], d["level_id"], conn, + choreographer=d.get("choreographer", "")) + conn.execute( + "INSERT OR IGNORE INTO song_dances (id, song_id, dance_id, dance_order) " + "VALUES (?,?,?,?)", + (str(uuid.uuid4()), song_id, dance_id, i) + ) - # Indsæt hoveddanse - for i, d in enumerate(dances, 1): - dance_id = get_or_create_dance(d["name"], d["level_id"], conn, - choreographer=d.get("choreographer", "")) - conn.execute( - "INSERT OR IGNORE INTO song_dances (song_id, dance_id, dance_order) " - "VALUES (?,?,?)", - (song_id, dance_id, i) - ) - - # Indsæt alternativ-danse - for a in alts: - dance_id = get_or_create_dance(a["name"], a["level_id"], conn) - conn.execute( - "INSERT OR IGNORE INTO song_alt_dances (song_id, dance_id, note) " - "VALUES (?,?,?)", - (song_id, dance_id, a.get("note", "")) - ) - - conn.commit() - conn.close() + # Indsæt alternativ-danse + for a in alts: + dance_id = get_or_create_dance(a["name"], a["level_id"], conn) + conn.execute( + "INSERT OR IGNORE INTO song_alt_dances (id, song_id, dance_id, note) " + "VALUES (?,?,?,?)", + (str(uuid.uuid4()), song_id, dance_id, a.get("note", "")) + ) # Skriv danse-navne til filen if local_path and can_write_dances(local_path): @@ -599,4 +607,4 @@ class TagEditorDialog(QDialog): except Exception as e: import traceback traceback.print_exc() - QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}") + QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}") \ No newline at end of file