Compare commits

...

2 Commits

9 changed files with 477 additions and 236 deletions

View File

@@ -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 {

View File

@@ -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)
""", 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
_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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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))

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 (
@@ -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 ""

View File

@@ -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)

View File

@@ -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}")