Files
LinedanceAfspiller/linedance-app/ui/playlist_browser.py
2026-04-20 01:41:24 +02:00

522 lines
20 KiB
Python

"""
playlist_browser.py — Dialog til at hente og gemme danselister med tag-organisering.
Viser en liste over alle gemte danselister med:
- Navn, dato, antal sange
- Tag-filtrering i venstre side
- Gem ny liste med tags
"""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QListWidget, QListWidgetItem, QWidget,
QSplitter, QFrame, QMessageBox, QInputDialog,
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QColor
class PlaylistBrowserDialog(QDialog):
"""Kombineret gem/hent dialog til danselister."""
playlist_selected = pyqtSignal(str, str) # playlist_id, name
sync_requested = pyqtSignal() # bed main_window om at køre sync
def __init__(self, mode: str = "load", current_songs: list = None,
current_name: str = "", parent=None):
super().__init__(parent)
self._mode = mode # "load" eller "save"
self._current_songs = current_songs or []
self._current_name = current_name
self._all_playlists = []
self._active_tag = None
title = "Gem danseliste" if mode == "save" else "Hent danseliste"
self.setWindowTitle(title)
self.setMinimumSize(700, 480)
self.resize(780, 520)
self._build_ui()
self._load_data()
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
# Gem-felter (kun i save-mode)
if self._mode == "save":
save_frame = QFrame()
save_frame.setObjectName("track_display")
save_layout = QVBoxLayout(save_frame)
save_layout.setContentsMargins(10, 8, 10, 8)
row1 = QHBoxLayout()
row1.addWidget(QLabel("Navn:"))
self._name_input = QLineEdit()
self._name_input.setText(self._current_name)
self._name_input.setPlaceholderText("Navn på danselisten...")
row1.addWidget(self._name_input)
save_layout.addLayout(row1)
row2 = QHBoxLayout()
row2.addWidget(QLabel("Tags:"))
self._tags_input = QLineEdit()
self._tags_input.setPlaceholderText("stævne, øvning, workshop (komma-separeret)")
self._tags_input.textChanged.connect(self._suggest_tags)
row2.addWidget(self._tags_input)
save_layout.addLayout(row2)
# Tag-forslag
self._tag_suggestions = QListWidget()
self._tag_suggestions.setMaximumHeight(80)
self._tag_suggestions.hide()
self._tag_suggestions.itemClicked.connect(self._add_tag_suggestion)
save_layout.addWidget(self._tag_suggestions)
lbl = QLabel(f"{len(self._current_songs)} sange vil blive gemt")
lbl.setObjectName("result_count")
save_layout.addWidget(lbl)
layout.addWidget(save_frame)
# Splitter: tags til venstre, lister til højre
splitter = QSplitter(Qt.Orientation.Horizontal)
# ── Venstre: tag-filtrering ──
tag_panel = QWidget()
tag_layout = QVBoxLayout(tag_panel)
tag_layout.setContentsMargins(0, 0, 0, 0)
tag_layout.setSpacing(4)
lbl_tags = QLabel("FILTRÉR PÅ TAG")
lbl_tags.setObjectName("section_title")
tag_layout.addWidget(lbl_tags)
self._tag_list = QListWidget()
self._tag_list.currentItemChanged.connect(self._on_tag_selected)
tag_layout.addWidget(self._tag_list)
tag_panel.setMaximumWidth(180)
splitter.addWidget(tag_panel)
# ── Højre: danseliste-oversigt ──
list_panel = QWidget()
list_layout = QVBoxLayout(list_panel)
list_layout.setContentsMargins(0, 0, 0, 0)
list_layout.setSpacing(4)
# Søgefelt
self._search = QLineEdit()
self._search.setPlaceholderText("Søg i navn...")
self._search.textChanged.connect(self._filter)
list_layout.addWidget(self._search)
self._count_label = QLabel("")
self._count_label.setObjectName("result_count")
list_layout.addWidget(self._count_label)
self._list = QListWidget()
self._list.itemDoubleClicked.connect(self._on_double_click)
list_layout.addWidget(self._list)
splitter.addWidget(list_panel)
splitter.setSizes([160, 580])
layout.addWidget(splitter, stretch=1)
# Knapper
btn_row = QHBoxLayout()
if self._mode == "load":
btn_delete = QPushButton("🗑 Slet valgte")
btn_delete.clicked.connect(self._delete_selected)
btn_row.addWidget(btn_delete)
btn_tags = QPushButton("🏷 Rediger tags")
btn_tags.clicked.connect(self._edit_tags)
btn_row.addWidget(btn_tags)
btn_share = QPushButton("↗ Del...")
btn_share.clicked.connect(self._share_selected)
btn_row.addWidget(btn_share)
btn_shared = QPushButton("🌐 Hent delte")
btn_shared.clicked.connect(self._fetch_shared)
btn_row.addWidget(btn_shared)
btn_row.addStretch()
btn_cancel = QPushButton("Annuller")
btn_cancel.clicked.connect(self.reject)
btn_row.addWidget(btn_cancel)
if self._mode == "save":
btn_ok = QPushButton("💾 Gem")
btn_ok.setObjectName("btn_play")
btn_ok.clicked.connect(self._save)
else:
btn_ok = QPushButton("📂 Hent valgte")
btn_ok.setObjectName("btn_play")
btn_ok.clicked.connect(self._load_selected)
btn_row.addWidget(btn_ok)
layout.addLayout(btn_row)
def _load_data(self):
try:
from local.local_db import get_playlists, get_all_playlist_tags
self._all_playlists = [dict(r) for r in get_playlists()]
# Udfyld tag-liste
self._tag_list.clear()
all_item = QListWidgetItem("Alle lister")
all_item.setData(Qt.ItemDataRole.UserRole, None)
self._tag_list.addItem(all_item)
for tag in get_all_playlist_tags():
item = QListWidgetItem(f"# {tag}")
item.setData(Qt.ItemDataRole.UserRole, tag)
self._tag_list.addItem(item)
self._tag_list.setCurrentRow(0)
self._render(self._all_playlists)
except Exception as e:
print(f"Playlist browser load fejl: {e}")
def _on_tag_selected(self, current, previous):
if not current:
return
self._active_tag = current.data(Qt.ItemDataRole.UserRole)
self._filter()
def _suggest_tags(self, text: str):
"""Vis forslag til det sidst indtastede tag."""
if not hasattr(self, '_tag_suggestions'):
return
parts = text.split(",")
prefix = parts[-1].strip().lower()
if not prefix:
self._tag_suggestions.hide()
return
try:
from local.local_db import get_all_playlist_tags
all_tags = get_all_playlist_tags()
matches = [t for t in all_tags
if t.startswith(prefix) and t not in
[p.strip().lower() for p in parts[:-1]]]
if matches:
self._tag_suggestions.clear()
for t in matches[:5]:
self._tag_suggestions.addItem(t)
self._tag_suggestions.show()
else:
self._tag_suggestions.hide()
except Exception:
self._tag_suggestions.hide()
def _add_tag_suggestion(self, item):
"""Tilføj et foreslået tag til tekstfeltet."""
parts = self._tags_input.text().split(",")
parts[-1] = " " + item.text()
self._tags_input.setText(",".join(parts) + ", ")
self._tag_suggestions.hide()
self._tags_input.setFocus()
def _edit_tags(self):
"""Rediger tags på den valgte liste."""
item = self._list.currentItem()
if not item:
return
pl = item.data(Qt.ItemDataRole.UserRole)
if not pl or not isinstance(pl, dict):
return
from PyQt6.QtWidgets import QInputDialog
current = pl.get("tags", "")
new_tags, ok = QInputDialog.getText(
self, "Rediger tags", "Tags (komma-separeret):", text=current
)
if ok:
try:
from local.local_db import update_playlist_tags
update_playlist_tags(pl["id"], new_tags.strip())
self._load_data()
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke opdatere tags: {e}")
def _filter(self):
query = self._search.text().strip().lower()
tag = self._active_tag
filtered = self._all_playlists
if tag:
filtered = [
p for p in filtered
if tag in [t.strip().lower() for t in p.get("tags", "").split(",")]
]
if query:
filtered = [p for p in filtered if query in p["name"].lower()]
self._render(filtered)
def _render(self, playlists: list):
self._list.clear()
self._count_label.setText(f"{len(playlists)} liste{'r' if len(playlists) != 1 else ''}")
for pl in playlists:
date = pl.get("created_at", "")[:10]
count = pl.get("song_count", 0)
tags = pl.get("tags", "")
tag_str = f" [{tags}]" if tags else ""
item = QListWidgetItem(
f"{pl['name']}\n"
f" {date} · {count} sange{tag_str}"
)
item.setData(Qt.ItemDataRole.UserRole, pl)
self._list.addItem(item)
def _on_double_click(self, item: QListWidgetItem):
if self._mode == "load":
self._load_selected()
else:
self._save()
def _load_selected(self):
item = self._list.currentItem()
if not item:
QMessageBox.information(self, "Vælg", "Vælg en liste først.")
return
pl = item.data(Qt.ItemDataRole.UserRole)
self.playlist_selected.emit(pl["id"], pl["name"])
self.accept()
def _save(self):
name = self._name_input.text().strip()
if not name:
QMessageBox.warning(self, "Navn mangler", "Angiv et navn til danselisten.")
self._name_input.setFocus()
return
tags = self._tags_input.text().strip()
# Tjek om navn allerede eksisterer
existing = [p for p in self._all_playlists
if p["name"].lower() == name.lower()]
if existing:
reply = QMessageBox.question(
self, "Navn eksisterer allerede",
f"Der findes allerede en liste med navnet '{name}'.\n"
f"Vil du overskrive den?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
try:
from local.local_db import get_db, add_song_to_playlist
pl_id = existing[0]["id"]
with get_db() as conn:
conn.execute(
"DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,)
)
if tags:
conn.execute(
"UPDATE playlists SET tags=? WHERE id=?", (tags, pl_id)
)
for i, song in enumerate(self._current_songs, start=1):
if song.get("id"):
add_song_to_playlist(pl_id, song["id"],
file_id=song.get("file_id"),
position=i)
self.playlist_selected.emit(pl_id, name)
self.accept()
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke overskrive: {e}")
return
try:
from local.local_db import create_playlist, add_song_to_playlist
pl_id = create_playlist(name, tags=tags)
for i, song in enumerate(self._current_songs, start=1):
if song.get("id"):
add_song_to_playlist(pl_id, song["id"],
file_id=song.get("file_id"),
position=i)
self.playlist_selected.emit(pl_id, name)
self.accept()
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke gemme: {e}")
def _delete_selected(self):
item = self._list.currentItem()
if not item:
return
pl = item.data(Qt.ItemDataRole.UserRole)
reply = QMessageBox.question(
self, "Slet danseliste",
f"Slet '{pl['name']}'?\n\nDette kan ikke fortrydes.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
try:
from local.local_db import delete_playlist
delete_playlist(pl["id"])
self._load_data()
# Signal til main_window om at køre sync
self.sync_requested.emit()
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke slette: {e}")
def _share_selected(self):
"""Åbn del-dialog for den valgte playliste."""
item = self._list.currentItem()
if not item:
QMessageBox.information(self, "Del", "Vælg en playliste først.")
return
pl = item.data(Qt.ItemDataRole.UserRole)
if not isinstance(pl, dict):
return
# Hent server-info fra settings
try:
from ui.settings_dialog import load_settings
s = load_settings()
server_url = s.get("server_url", "")
token = self._get_token()
if not token:
QMessageBox.warning(self, "Ikke logget ind",
"Du skal være logget ind for at dele.")
return
# Find server-ID for playlisten
server_id = pl.get("api_project_id")
if not server_id:
QMessageBox.warning(self, "Ikke synkroniseret",
"Synkroniser playlisten til serveren først\n"
"(Filer → Synkroniser nu).")
return
from ui.share_dialog import ShareDialog
dlg = ShareDialog(server_id, pl["name"], server_url, token,
parent=self)
dlg.exec()
except Exception as e:
QMessageBox.warning(self, "Fejl", str(e))
def _get_token(self) -> str | None:
"""Hent JWT token fra main_window."""
mw = self.parent()
while mw and not hasattr(mw, "_api_token"):
mw = mw.parent()
return getattr(mw, "_api_token", None) if mw else None
def _fetch_shared(self):
"""Hent playlister der er delt med mig fra serveren."""
try:
from ui.settings_dialog import load_settings
s = load_settings()
server_url = s.get("server_url", "").rstrip("/")
token = self._get_token()
if not token:
QMessageBox.warning(self, "Ikke logget ind",
"Du skal være logget ind for at hente delte lister.")
return
import urllib.request, json
req = urllib.request.Request(
f"{server_url}/sharing/playlists/shared-with-me",
headers={"Authorization": f"Bearer {token}"}
)
with urllib.request.urlopen(req, timeout=10) as resp:
shared = json.loads(resp.read())
if not shared:
QMessageBox.information(self, "Ingen delte lister",
"Ingen playlister er delt med dig.")
return
# Vis valgdialog
from PyQt6.QtWidgets import QInputDialog
options = [
f"{p['name']} (af {p['owner']}, {p['song_count']} sange, {p['permission']})"
for p in shared
]
choice, ok = QInputDialog.getItem(
self, "Hent delt playliste",
"Vælg en playliste at hente:",
options, 0, False
)
if not ok:
return
idx = options.index(choice)
chosen = shared[idx]
# Hent indholdet
req2 = urllib.request.Request(
f"{server_url}/sharing/playlists/{chosen['project_id']}",
headers={"Authorization": f"Bearer {token}"}
)
with urllib.request.urlopen(req2, timeout=10) as resp:
pl_data = json.loads(resp.read())
self._import_shared_playlist(pl_data, server_url, token,
permission=chosen.get("permission", "view"))
except Exception as e:
QMessageBox.warning(self, "Fejl", f"Kunne ikke hente: {e}")
def _import_shared_playlist(self, pl_data: dict, server_url: str, token: str,
permission: str = "view"):
"""Importer en delt playliste som en linket liste."""
import sqlite3
from local.local_db import DB_PATH, get_db, add_song_to_playlist
name = pl_data["name"]
server_id = pl_data["id"]
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
# Tjek om listen allerede er linket
existing = conn.execute(
"SELECT id FROM playlists WHERE api_project_id=?", (server_id,)
).fetchone()
conn.close()
if existing:
# Opdater eksisterende
pl_id = existing["id"]
with get_db() as c:
c.execute("DELETE FROM playlist_songs WHERE playlist_id=?", (pl_id,))
else:
# Opret ny linket playliste
with get_db() as c:
c.execute(
"INSERT INTO playlists (name, api_project_id, is_linked, server_permission) "
"VALUES (?, ?, 1, ?)",
(name, server_id, permission)
)
pl_id = c.execute("SELECT last_insert_rowid()").fetchone()[0]
# Indsæt sange med sang-matching
matched = 0
with get_db() as c:
for song_data in pl_data.get("songs", []):
local = c.execute(
"SELECT id FROM songs WHERE title=? AND artist=? AND file_missing=0",
(song_data["title"], song_data["artist"])
).fetchone()
if local:
c.execute(
"INSERT INTO playlist_songs "
"(playlist_id, song_id, position, status, is_workshop, dance_override) "
"VALUES (?,?,?,?,?,?)",
(pl_id, local["id"], song_data["position"],
song_data.get("status", "pending"),
1 if song_data.get("is_workshop") else 0,
song_data.get("dance_override") or "")
)
matched += 1
self._load_data()
self.playlist_selected.emit(pl_id, name)
perm_text = {"view": "se", "copy": "kopiere", "edit": "redigere"}.get(
permission, permission
)
QMessageBox.information(
self, "Linket",
f"'{name}' er nu linket til server-listen.\n"
f"Du har rettighed til at {perm_text} listen.\n\n"
f"{matched} af {len(pl_data.get('songs', []))} sange fundet lokalt."
)