Compare commits

...

2 Commits

9 changed files with 477 additions and 236 deletions

View File

@@ -380,10 +380,11 @@ def pull(
continue continue
level = db.query(DanceLevel).filter_by(id=dance.level_id).first() if dance.level_id else None level = db.query(DanceLevel).filter_by(id=dance.level_id).first() if dance.level_id else None
song_tags.append({ song_tags.append({
"song_id": sd.song_id, "song_id": sd.song_id,
"dance_name": dance.name, "dance_name": dance.name,
"level_name": level.name if level else "", "choreographer": dance.choreographer or "",
"dance_order": sd.dance_order, "level_name": level.name if level else "",
"dance_order": sd.dance_order,
}) })
return { return {

View File

@@ -88,6 +88,7 @@ CREATE TABLE IF NOT EXISTS dance_levels (
); );
-- Danse -- Danse
-- Dans + niveau + koreograf er unik kombination
CREATE TABLE IF NOT EXISTS dances ( CREATE TABLE IF NOT EXISTS dances (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
@@ -98,7 +99,7 @@ CREATE TABLE IF NOT EXISTS dances (
notes TEXT NOT NULL DEFAULT '', notes TEXT NOT NULL DEFAULT '',
use_count INTEGER NOT NULL DEFAULT 1, use_count INTEGER NOT NULL DEFAULT 1,
source TEXT NOT NULL DEFAULT 'local', source TEXT NOT NULL DEFAULT 'local',
UNIQUE(name, level_id) UNIQUE(name, level_id, choreographer)
); );
-- Sang-dans tags -- Sang-dans tags
@@ -582,4 +583,111 @@ def upsert_dance_levels(levels: list[dict]):
ON CONFLICT(name) DO UPDATE SET ON CONFLICT(name) DO UPDATE SET
sort_order=excluded.sort_order, sort_order=excluded.sort_order,
description=excluded.description description=excluded.description
""", lvl) """, 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

View File

@@ -158,30 +158,28 @@ def scan_library(library_id: int, library_path: str, db_path: str,
# Opret eller opdater fil-post # Opret eller opdater fil-post
_upsert_file_conn(conn, song_id, path_str, file_format, mtime, extra_tags) _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", []) file_dances = tags.get("dances", [])
if file_dances: if file_dances:
existing_count = conn.execute( import uuid
"SELECT COUNT(*) FROM song_dances WHERE song_id=?", (song_id,) # Slet eksisterende og genindsæt fra filen
).fetchone()[0] conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
if existing_count == 0: for order, dance_name in enumerate(file_dances, start=1):
import uuid dance_row = conn.execute(
for order, dance_name in enumerate(file_dances, start=1): "SELECT id FROM dances WHERE name=? COLLATE NOCASE LIMIT 1",
dance_row = conn.execute( (dance_name,)
"SELECT id FROM dances WHERE name=? COLLATE NOCASE LIMIT 1", ).fetchone()
(dance_name,) if not dance_row:
).fetchone() cur = conn.execute(
if not dance_row: "INSERT INTO dances (name) VALUES (?)", (dance_name,)
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)
) )
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() conn.commit()
except Exception as e: except Exception as e:

View File

@@ -323,22 +323,30 @@ class SyncManager:
conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA journal_mode=WAL")
try: try:
# Opdater dans-info # Synkroniser danse fra server — opret nye, opdater eksisterende
for d in data.get("dances", []): for d in data.get("dances", []):
if not d.get("name"): if not d.get("name"):
continue continue
choreo = d.get("choreographer", "") or ""
existing = conn.execute( 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() ).fetchone()
if existing and (d.get("choreographer") or d.get("video_url")): if existing:
conn.execute(""" conn.execute("""
UPDATE dances SET UPDATE dances SET
choreographer = CASE WHEN choreographer='' THEN ? ELSE choreographer END,
video_url = CASE WHEN video_url='' THEN ? ELSE video_url END, video_url = CASE WHEN video_url='' THEN ? ELSE video_url END,
stepsheet_url = CASE WHEN stepsheet_url='' THEN ? ELSE stepsheet_url END stepsheet_url = CASE WHEN stepsheet_url='' THEN ? ELSE stepsheet_url END
WHERE id=? WHERE id=?
""", (d.get("choreographer",""), d.get("video_url",""), """, (d.get("video_url",""), d.get("stepsheet_url",""), existing["id"]))
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 # Hent soft-slettede server-IDs så vi springer dem over
deleted_server_ids = { deleted_server_ids = {
@@ -468,6 +476,49 @@ class SyncManager:
song_data.get("dance_override","") or "")) song_data.get("dance_override","") or ""))
position += 1 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() conn.commit()
except Exception: except Exception:

View File

@@ -410,6 +410,11 @@ def read_dances_from_file(path: str | Path) -> list[str]:
return tags.get("dances", []) 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 ─────────────────────────────────────────────────────────────── # ── BPM-analyse ───────────────────────────────────────────────────────────────
def analyze_bpm(path: str | Path) -> float | None: def analyze_bpm(path: str | Path) -> float | None:
@@ -513,4 +518,4 @@ def _write_mbid_m4a(path: Path, mbid: str) -> bool:
return True return True
except Exception as e: except Exception as e:
logger.warning(f"MBID M4A skrivefejl {path}: {e}") logger.warning(f"MBID M4A skrivefejl {path}: {e}")
return False return False

View File

@@ -31,54 +31,51 @@ class DanceInfoDialog(QDialog):
def _load_dances(self): def _load_dances(self):
try: try:
from local.local_db import get_dances_for_song, get_alt_dances_for_song, new_conn from local.local_db import get_db
conn = new_conn() 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(""" for row in rows:
SELECT d.id, d.name, d.level_id, d.choreographer, self._dances.append({
d.video_url, d.stepsheet_url, d.notes, "dance_id": row["id"],
dl.name as level_name "name": row["name"],
FROM song_dances sd "level_name": row["level_name"] or "",
JOIN dances d ON d.id = sd.dance_id "choreographer": row["choreographer"] or "",
LEFT JOIN dance_levels dl ON dl.id = d.level_id "video_url": row["video_url"] or "",
WHERE sd.song_id=? ORDER BY sd.dance_order "stepsheet_url": row["stepsheet_url"] or "",
""", (self._song.get("id"),)).fetchall() "notes": row["notes"] or "",
"is_alt": False,
})
for row in rows: alt_rows = conn.execute("""
self._dances.append({ SELECT d.id, d.name, d.choreographer,
"dance_id": row["id"], d.video_url, d.stepsheet_url, d.notes,
"name": row["name"], dl.name as level_name
"level_name": row["level_name"] or "", FROM song_alt_dances sad
"choreographer": row["choreographer"] or "", JOIN dances d ON d.id = sad.dance_id
"video_url": row["video_url"] or "", LEFT JOIN dance_levels dl ON dl.id = d.level_id
"stepsheet_url": row["stepsheet_url"] or "", WHERE sad.song_id=? ORDER BY d.name
"notes": row["notes"] or "", """, (self._song.get("id"),)).fetchall()
"is_alt": False,
})
# Alternativ-danse for row in alt_rows:
alt_rows = conn.execute(""" self._dances.append({
SELECT d.id, d.name, d.level_id, d.choreographer, "dance_id": row["id"],
d.video_url, d.stepsheet_url, d.notes, "name": row["name"],
dl.name as level_name "level_name": row["level_name"] or "",
FROM song_alt_dances sad "choreographer": row["choreographer"] or "",
JOIN dances d ON d.id = sad.dance_id "video_url": row["video_url"] or "",
LEFT JOIN dance_levels dl ON dl.id = d.level_id "stepsheet_url": row["stepsheet_url"] or "",
WHERE sad.song_id=? ORDER BY d.name "notes": row["notes"] or "",
""", (self._song.get("id"),)).fetchall() "is_alt": True,
})
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()
except Exception as e: except Exception as e:
print(f"DanceInfoDialog load fejl: {e}") print(f"DanceInfoDialog load fejl: {e}")
@@ -204,15 +201,14 @@ class DanceInfoDialog(QDialog):
def _save(self): def _save(self):
self._save_to_cache(self._current_idx) self._save_to_cache(self._current_idx)
try: try:
from local.local_db import update_dance_info from local.local_db import get_db
for d in self._dances: with get_db() as conn:
update_dance_info( for d in self._dances:
d["dance_id"], conn.execute("""
choreographer = d["choreographer"], UPDATE dances SET choreographer=?, video_url=?,
video_url = d["video_url"], stepsheet_url=?, notes=? WHERE id=?
stepsheet_url = d["stepsheet_url"], """, (d["choreographer"], d["video_url"],
notes = d["notes"], d["stepsheet_url"], d["notes"], d["dance_id"]))
)
self.accept() self.accept()
except Exception as e: except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}") QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}")
@@ -223,4 +219,4 @@ class DanceInfoDialog(QDialog):
return return
if not url.startswith("http"): if not url.startswith("http"):
url = "https://" + url url = "https://" + url
QDesktopServices.openUrl(QUrl(url)) QDesktopServices.openUrl(QUrl(url))

View File

@@ -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 ( from PyQt6.QtWidgets import (
@@ -10,18 +12,18 @@ from PyQt6.QtCore import Qt, QTimer
class DancePickerDialog(QDialog): class DancePickerDialog(QDialog):
def __init__(self, current_dance: str = "", current_choreo: str = "", def __init__(self, current_dance: str = "", song_title: str = "",
song_title: str = "", parent=None): existing_dances: list[str] = None, parent=None):
super().__init__(parent) super().__init__(parent)
self._chosen_dance = current_dance self._chosen_dance = current_dance
self._chosen_choreo = current_choreo self._existing_dances = existing_dances or []
self.setWindowTitle("Vælg dans") self.setWindowTitle("Vælg dans")
self.setMinimumWidth(400) self.setMinimumWidth(420)
self.setFixedWidth(440) self.setFixedWidth(460)
self._build_ui(current_dance, current_choreo, song_title) self._build_ui(current_dance, song_title)
self._load_dance_suggestions("") 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 = QVBoxLayout(self)
layout.setContentsMargins(12, 12, 12, 12) layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8) layout.setSpacing(8)
@@ -32,65 +34,38 @@ class DancePickerDialog(QDialog):
lbl.setWordWrap(True) lbl.setWordWrap(True)
layout.addWidget(lbl) layout.addWidget(lbl)
# ── Dans ────────────────────────────────────────────────────────────── layout.addWidget(QLabel("Dans:"))
lbl2 = QLabel("Dans:")
lbl2.setObjectName("track_meta")
layout.addWidget(lbl2)
self._edit_dance = QLineEdit() self._edit = QLineEdit()
self._edit_dance.setText(current_dance) self._edit.setText(current_dance)
self._edit_dance.setPlaceholderText("Skriv dans-navn...") self._edit.setPlaceholderText("Skriv dans-navn...")
self._edit_dance.selectAll() self._edit.selectAll()
self._edit_dance.textChanged.connect(self._on_dance_text_changed) self._edit.textChanged.connect(self._on_text_changed)
self._edit_dance.returnPressed.connect(lambda: self._edit_choreo.setFocus()) self._edit.returnPressed.connect(self._on_accept)
layout.addWidget(self._edit_dance) layout.addWidget(self._edit)
self._dance_list = QListWidget() # Forslagsliste
self._dance_list.setMaximumHeight(160) self._list = QListWidget()
self._dance_list.itemDoubleClicked.connect(self._on_dance_selected) self._list.setMinimumHeight(200)
self._dance_list.itemClicked.connect( self._list.itemDoubleClicked.connect(self._on_selected)
lambda item: self._edit_dance.setText( self._list.itemClicked.connect(self._on_item_clicked)
item.data(Qt.ItemDataRole.UserRole) or item.text().split(" / ")[0] layout.addWidget(self._list)
)
)
layout.addWidget(self._dance_list)
# ── Koreograf ───────────────────────────────────────────────────────── # Info-label — viser niveau/koreograf for valgt dans
lbl3 = QLabel("Koreograf (valgfri):") self._info_lbl = QLabel("")
lbl3.setObjectName("track_meta") self._info_lbl.setObjectName("result_count")
layout.addWidget(lbl3) self._info_lbl.setWordWrap(True)
layout.addWidget(self._info_lbl)
self._edit_choreo = QLineEdit() # Debounce timer
self._edit_choreo.setText(current_choreo) self._timer = QTimer(self)
self._edit_choreo.setPlaceholderText("Koreografens navn...") self._timer.setSingleShot(True)
self._edit_choreo.textChanged.connect(self._on_choreo_text_changed) self._timer.setInterval(150)
self._edit_choreo.returnPressed.connect(self._on_accept) self._timer.timeout.connect(
layout.addWidget(self._edit_choreo) lambda: self._load_suggestions(self._edit.text().strip())
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())
) )
self._choreo_timer = QTimer(self) # Knapper
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 ───────────────────────────────────────────────────────────
btn_row = QHBoxLayout() btn_row = QHBoxLayout()
btn_row.addStretch() btn_row.addStretch()
btn_cancel = QPushButton("Annuller") btn_cancel = QPushButton("Annuller")
@@ -102,62 +77,73 @@ class DancePickerDialog(QDialog):
btn_row.addWidget(btn_ok) btn_row.addWidget(btn_ok)
layout.addLayout(btn_row) layout.addLayout(btn_row)
self._edit_dance.setFocus() self._edit.setFocus()
def _on_dance_text_changed(self): def _on_text_changed(self):
self._dance_timer.start() self._timer.start()
def _on_choreo_text_changed(self): def _load_suggestions(self, prefix: str):
self._choreo_timer.start()
def _load_dance_suggestions(self, prefix: str):
try: try:
from local.local_db import get_dance_suggestions from local.local_db import get_dance_suggestions
suggestions = get_dance_suggestions(prefix or "", limit=20) suggestions = get_dance_suggestions(prefix or "", limit=25)
self._dance_list.clear() 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: for s in suggestions:
label = f"{s['name']} / {s['level_name']}" if s.get("level_name") else s["name"] s = dict(s)
if s.get("choreographer"): name = s["name"]
label += f" ({s['choreographer']})" 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 = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, s["name"]) item.setData(Qt.ItemDataRole.UserRole, {
item.setData(Qt.ItemDataRole.UserRole + 1, s.get("choreographer", "")) "name": name,
self._dance_list.addItem(item) "level": level,
except Exception: "choreo": choreo,
pass })
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): def _on_item_clicked(self, item: QListWidgetItem):
try: data = item.data(Qt.ItemDataRole.UserRole) or {}
from local.local_db import get_choreographer_suggestions name = data.get("name", "")
suggestions = get_choreographer_suggestions(prefix or "", limit=15) level = data.get("level", "")
self._choreo_list.clear() choreo = data.get("choreo", "")
for name in suggestions: self._edit.setText(name)
self._choreo_list.addItem(QListWidgetItem(name)) # Vis info
except Exception: parts = []
pass 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): def _on_selected(self, item: QListWidgetItem):
name = item.data(Qt.ItemDataRole.UserRole) or item.text().split(" / ")[0] self._on_item_clicked(item)
choreo = item.data(Qt.ItemDataRole.UserRole + 1) or "" self._on_accept()
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_accept(self): def _on_accept(self):
self._chosen_dance = self._edit_dance.text().strip() self._chosen_dance = self._edit.text().strip()
self._chosen_choreo = self._edit_choreo.text().strip()
if self._chosen_dance: if self._chosen_dance:
self.accept() self.accept()
def get_dance(self) -> str: def get_dance(self) -> str:
return self._chosen_dance return self._chosen_dance
# Behold get_choreo for bagudkompatibilitet — returnerer altid ""
def get_choreo(self) -> str: def get_choreo(self) -> str:
return self._chosen_choreo return ""

View File

@@ -756,28 +756,40 @@ class PlaylistPanel(QWidget):
def _change_dance(self, idx: int, song: dict): def _change_dance(self, idx: int, song: dict):
"""Lad brugeren vælge/skrive hvilken dans der vises for dette nummer.""" """Lad brugeren vælge/skrive hvilken dans der vises for dette nummer."""
from ui.dance_picker_dialog import DancePickerDialog from ui.dance_picker_dialog import DancePickerDialog
dances = song.get("dances", [])
current = song.get("active_dance", "") current = song.get("active_dance", "")
if not current: if not current:
dances = song.get("dances", [])
current = dances[0] if dances else "" current = dances[0] if dances else ""
current_choreo = song.get("active_choreo", "") 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( dlg = DancePickerDialog(
current_dance=current, current_dance=current,
current_choreo=current_choreo,
song_title=song.get("title", ""), song_title=song.get("title", ""),
existing_dances=dances,
parent=self.window() parent=self.window()
) )
if dlg.exec(): if dlg.exec():
chosen = dlg.get_dance() chosen = dlg.get_dance()
choreo = dlg.get_choreo() choreo = "" # Koreograf redigeres i tag-editoren, ikke her
if chosen: if chosen:
song["active_dance"] = chosen song["active_dance"] = chosen
song["active_choreo"] = choreo song["active_choreo"] = choreo
self._refresh() 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): 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: if not self._named_playlist_id:
return return
try: try:
@@ -791,6 +803,82 @@ class PlaylistPanel(QWidget):
except Exception: except Exception:
pass 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): def _sync_ws_to_db(self, idx: int, song: dict):
"""Gem is_workshop til playlist_songs — både navngiven og aktiv liste.""" """Gem is_workshop til playlist_songs — både navngiven og aktiv liste."""
pl_ids = [] pl_ids = []
@@ -1267,11 +1355,11 @@ class PlaylistPanel(QWidget):
status = self._statuses[i] status = self._statuses[i]
icon = self.STATUS_ICON.get(status, " ") 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", "") active = song.get("active_dance", "")
if not active: if not active:
dances = song.get("dances", []) 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 "" ws_tag = " 🎓" if song.get("is_workshop") else ""
# Tilgængeligheds-dot til højre — kun hvis tjekket (ikke yellow) # 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_color = {"green": "#27ae60", "red": "#e74c3c"}.get(avail, None)
avail_tip = {"green": "Tilgængelig lokalt", "red": "Ikke fundet lokalt"}.get(avail, "") avail_tip = {"green": "Tilgængelig lokalt", "red": "Ikke fundet lokalt"}.get(avail, "")
text = (f"{i+1:>2}. {song.get('title','')}{ws_tag}\n" text = (f"{i+1:>2}. {active}{ws_tag}\n"
f" {song.get('artist','')} · {active}") f" {song.get('title','')} · {song.get('artist','')}")
item = QListWidgetItem(f"{icon} {text}") item = QListWidgetItem(f"{icon} {text}")
item.setData(Qt.ItemDataRole.UserRole, i) item.setData(Qt.ItemDataRole.UserRole, i)
item.setData(Qt.ItemDataRole.UserRole + 1, avail_color) item.setData(Qt.ItemDataRole.UserRole + 1, avail_color)

View File

@@ -164,7 +164,7 @@ class TagEditorDialog(QDialog):
# Forslags-liste # Forslags-liste
self._dance_suggestions = QListWidget() self._dance_suggestions = QListWidget()
self._dance_suggestions.setMaximumHeight(120) self._dance_suggestions.setFixedHeight(150)
self._dance_suggestions.setFocusPolicy(Qt.FocusPolicy.NoFocus) self._dance_suggestions.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self._dance_suggestions.itemClicked.connect( self._dance_suggestions.itemClicked.connect(
lambda item: self._add_from_suggestion(item, "dance") lambda item: self._add_from_suggestion(item, "dance")
@@ -328,7 +328,13 @@ class TagEditorDialog(QDialog):
suggestions = get_dance_suggestions(prefix, limit=20) suggestions = get_dance_suggestions(prefix, limit=20)
list_widget.clear() list_widget.clear()
for s in suggestions: 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 = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, s.get("level_id")) item.setData(Qt.ItemDataRole.UserRole, s.get("level_id"))
item.setData(Qt.ItemDataRole.UserRole + 1, s["name"]) item.setData(Qt.ItemDataRole.UserRole + 1, s["name"])
@@ -373,16 +379,21 @@ class TagEditorDialog(QDialog):
suggestions = get_dance_suggestions(prefix, limit=15) suggestions = get_dance_suggestions(prefix, limit=15)
list_widget.clear() list_widget.clear()
for s in suggestions: 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"): if s.get("choreographer"):
label += f" · {s['choreographer']}" parts.append(s["choreographer"])
label = " / ".join(parts)
item = QListWidgetItem(label) item = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, s.get("level_id")) item.setData(Qt.ItemDataRole.UserRole, s.get("level_id"))
item.setData(Qt.ItemDataRole.UserRole + 1, s["name"]) item.setData(Qt.ItemDataRole.UserRole + 1, s["name"])
item.setData(Qt.ItemDataRole.UserRole + 2, s.get("choreographer", "")) item.setData(Qt.ItemDataRole.UserRole + 2, s.get("choreographer", ""))
list_widget.addItem(item) list_widget.addItem(item)
except Exception: except Exception as e:
pass import logging
logging.getLogger(__name__).warning(f"Dans-forslag fejl: {e}", exc_info=True)
def _add_from_suggestion(self, item, panel: str): def _add_from_suggestion(self, item, panel: str):
"""Tilføj dans fra forslags-listen ved klik.""" """Tilføj dans fra forslags-listen ved klik."""
@@ -451,7 +462,7 @@ class TagEditorDialog(QDialog):
layout.addWidget(self._new_alt) layout.addWidget(self._new_alt)
self._alt_suggestions = QListWidget() self._alt_suggestions = QListWidget()
self._alt_suggestions.setMaximumHeight(120) self._alt_suggestions.setFixedHeight(150)
self._alt_suggestions.setFocusPolicy(Qt.FocusPolicy.NoFocus) self._alt_suggestions.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self._alt_suggestions.itemClicked.connect( self._alt_suggestions.itemClicked.connect(
lambda item: self._add_from_suggestion(item, "alt") lambda item: self._add_from_suggestion(item, "alt")
@@ -530,7 +541,8 @@ class TagEditorDialog(QDialog):
local_path = self._song.get("local_path", "") local_path = self._song.get("local_path", "")
try: 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 from local.tag_reader import write_dances, can_write_dances
# Saml data fra UI # Saml data fra UI
@@ -554,33 +566,29 @@ class TagEditorDialog(QDialog):
"note": "", "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 # Indsæt hoveddanse
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,)) for i, d in enumerate(dances, 1):
conn.execute("DELETE FROM song_alt_dances WHERE song_id=?", (song_id,)) 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 # Indsæt alternativ-danse
for i, d in enumerate(dances, 1): for a in alts:
dance_id = get_or_create_dance(d["name"], d["level_id"], conn, dance_id = get_or_create_dance(a["name"], a["level_id"], conn)
choreographer=d.get("choreographer", "")) conn.execute(
conn.execute( "INSERT OR IGNORE INTO song_alt_dances (id, song_id, dance_id, note) "
"INSERT OR IGNORE INTO song_dances (song_id, dance_id, dance_order) " "VALUES (?,?,?,?)",
"VALUES (?,?,?)", (str(uuid.uuid4()), song_id, dance_id, a.get("note", ""))
(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()
# Skriv danse-navne til filen # Skriv danse-navne til filen
if local_path and can_write_dances(local_path): if local_path and can_write_dances(local_path):
@@ -599,4 +607,4 @@ class TagEditorDialog(QDialog):
except Exception as e: except Exception as e:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}") QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}")