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