519 lines
20 KiB
Python
519 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(int, 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"], 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"], 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."
|
|
)
|