Compare commits
2 Commits
2deb0260f0
...
ec3989e6a4
| Author | SHA1 | Date | |
|---|---|---|---|
| ec3989e6a4 | |||
| 6ed349277c |
@@ -382,6 +382,7 @@ def pull(
|
||||
song_tags.append({
|
||||
"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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -583,3 +584,110 @@ def upsert_dance_levels(levels: list[dict]):
|
||||
sort_order=excluded.sort_order,
|
||||
description=excluded.description
|
||||
""", 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
|
||||
@@ -158,14 +158,12 @@ 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
|
||||
# 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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -31,11 +31,10 @@ 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.level_id, d.choreographer,
|
||||
SELECT d.id, d.name, d.choreographer,
|
||||
d.video_url, d.stepsheet_url, d.notes,
|
||||
dl.name as level_name
|
||||
FROM song_dances sd
|
||||
@@ -56,9 +55,8 @@ class DanceInfoDialog(QDialog):
|
||||
"is_alt": False,
|
||||
})
|
||||
|
||||
# Alternativ-danse
|
||||
alt_rows = conn.execute("""
|
||||
SELECT d.id, d.name, d.level_id, d.choreographer,
|
||||
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
|
||||
@@ -78,7 +76,6 @@ class DanceInfoDialog(QDialog):
|
||||
"notes": row["notes"] or "",
|
||||
"is_alt": True,
|
||||
})
|
||||
conn.close()
|
||||
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
|
||||
from local.local_db import get_db
|
||||
with get_db() as conn:
|
||||
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"],
|
||||
)
|
||||
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}")
|
||||
|
||||
@@ -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 ""
|
||||
@@ -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()
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -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,8 +566,7 @@ 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,))
|
||||
@@ -565,23 +576,20 @@ class TagEditorDialog(QDialog):
|
||||
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)
|
||||
"INSERT OR IGNORE INTO song_dances (id, song_id, dance_id, dance_order) "
|
||||
"VALUES (?,?,?,?)",
|
||||
(str(uuid.uuid4()), 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", ""))
|
||||
"INSERT OR IGNORE INTO song_alt_dances (id, song_id, dance_id, note) "
|
||||
"VALUES (?,?,?,?)",
|
||||
(str(uuid.uuid4()), song_id, dance_id, a.get("note", ""))
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Skriv danse-navne til filen
|
||||
if local_path and can_write_dances(local_path):
|
||||
dance_names = [d["name"] for d in dances]
|
||||
|
||||
Reference in New Issue
Block a user