325 lines
13 KiB
Python
325 lines
13 KiB
Python
"""
|
|
playlist_manager.py — Dialog til danseliste-administration.
|
|
Ny liste, gem, load og importer M3U/M3U8/tekst.
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
|
QPushButton, QListWidget, QListWidgetItem, QFileDialog,
|
|
QMessageBox, QTabWidget, QWidget, QTextEdit,
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal
|
|
|
|
|
|
class PlaylistManagerDialog(QDialog):
|
|
"""
|
|
Fanebaseret dialog med tre faner:
|
|
1. Gem aktuel liste
|
|
2. Indlæs gemt liste
|
|
3. Importer fra fil (M3U / M3U8 / tekst)
|
|
"""
|
|
playlist_loaded = pyqtSignal(str, list) # (navn, liste af dict)
|
|
|
|
def __init__(self, current_songs: list[dict], parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Danseliste-administration")
|
|
self.setMinimumWidth(500)
|
|
self.setMinimumHeight(460)
|
|
self._current_songs = current_songs
|
|
self._build_ui()
|
|
self._load_saved_playlists()
|
|
|
|
def _build_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
|
|
tabs = QTabWidget()
|
|
tabs.addTab(self._build_save_tab(), "💾 Gem liste")
|
|
tabs.addTab(self._build_load_tab(), "📂 Indlæs liste")
|
|
tabs.addTab(self._build_import_tab(), "📥 Importer")
|
|
layout.addWidget(tabs)
|
|
|
|
btn_close = QPushButton("Luk")
|
|
btn_close.clicked.connect(self.accept)
|
|
row = QHBoxLayout()
|
|
row.addStretch()
|
|
row.addWidget(btn_close)
|
|
layout.addLayout(row)
|
|
|
|
# ── Fane 1: Gem ───────────────────────────────────────────────────────────
|
|
|
|
def _build_save_tab(self) -> QWidget:
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
layout.setSpacing(10)
|
|
|
|
layout.addWidget(QLabel(f"Aktuel liste har {len(self._current_songs)} sange."))
|
|
|
|
layout.addWidget(QLabel("Navn på danselisten:"))
|
|
self._save_name = QLineEdit()
|
|
self._save_name.setPlaceholderText("f.eks. Sommer Event 2025")
|
|
layout.addWidget(self._save_name)
|
|
|
|
btn_save = QPushButton("💾 Gem")
|
|
btn_save.clicked.connect(self._save_playlist)
|
|
layout.addWidget(btn_save)
|
|
|
|
self._save_status = QLabel("")
|
|
self._save_status.setObjectName("result_count")
|
|
layout.addWidget(self._save_status)
|
|
layout.addStretch()
|
|
return tab
|
|
|
|
def _save_playlist(self):
|
|
name = self._save_name.text().strip()
|
|
if not name:
|
|
self._save_status.setText("Angiv et navn")
|
|
return
|
|
if not self._current_songs:
|
|
self._save_status.setText("Danselisten er tom")
|
|
return
|
|
try:
|
|
from local.local_db import create_playlist, add_song_to_playlist, get_db
|
|
pl_id = create_playlist(name)
|
|
for i, song in enumerate(self._current_songs, start=1):
|
|
add_song_to_playlist(pl_id, song["id"], position=i)
|
|
self._save_status.setText(f"✓ Gemt som \"{name}\"")
|
|
self._load_saved_playlists()
|
|
except Exception as e:
|
|
self._save_status.setText(f"Fejl: {e}")
|
|
|
|
# ── Fane 2: Indlæs ────────────────────────────────────────────────────────
|
|
|
|
def _build_load_tab(self) -> QWidget:
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
layout.addWidget(QLabel("Gemte danselister:"))
|
|
self._pl_list = QListWidget()
|
|
self._pl_list.itemDoubleClicked.connect(self._load_selected)
|
|
layout.addWidget(self._pl_list)
|
|
|
|
btn_row = QHBoxLayout()
|
|
btn_load = QPushButton("📂 Indlæs valgte")
|
|
btn_load.clicked.connect(self._load_selected_btn)
|
|
btn_delete = QPushButton("🗑 Slet valgte")
|
|
btn_delete.clicked.connect(self._delete_selected)
|
|
btn_row.addWidget(btn_load)
|
|
btn_row.addWidget(btn_delete)
|
|
layout.addLayout(btn_row)
|
|
|
|
self._load_status = QLabel("")
|
|
self._load_status.setObjectName("result_count")
|
|
layout.addWidget(self._load_status)
|
|
return tab
|
|
|
|
def _load_saved_playlists(self):
|
|
if not hasattr(self, "_pl_list"):
|
|
return
|
|
self._pl_list.clear()
|
|
try:
|
|
from local.local_db import get_playlists
|
|
for pl in get_playlists():
|
|
item = QListWidgetItem(pl["name"])
|
|
item.setData(Qt.ItemDataRole.UserRole, dict(pl))
|
|
self._pl_list.addItem(item)
|
|
except Exception:
|
|
pass
|
|
|
|
def _load_selected_btn(self):
|
|
item = self._pl_list.currentItem()
|
|
if item:
|
|
self._load_selected(item)
|
|
|
|
def _load_selected(self, item: QListWidgetItem):
|
|
pl = item.data(Qt.ItemDataRole.UserRole)
|
|
if not pl:
|
|
return
|
|
try:
|
|
from local.local_db import get_playlist_with_songs, get_db
|
|
data = get_playlist_with_songs(pl["id"])
|
|
songs = []
|
|
for row in data.get("songs", []):
|
|
with get_db() as conn:
|
|
dances = conn.execute(
|
|
"SELECT dance_name FROM song_dances WHERE song_id=? ORDER BY dance_order",
|
|
(row["id"],)
|
|
).fetchall()
|
|
songs.append({
|
|
"id": row["id"],
|
|
"title": row.get("title", ""),
|
|
"artist": row.get("artist", ""),
|
|
"album": row.get("album", ""),
|
|
"bpm": row.get("bpm", 0),
|
|
"duration_sec": row.get("duration_sec", 0),
|
|
"local_path": row.get("local_path", ""),
|
|
"file_format": row.get("file_format", ""),
|
|
"file_missing": bool(row.get("file_missing", False)),
|
|
"dances": [d["dance_name"] for d in dances],
|
|
})
|
|
self.playlist_loaded.emit(pl["name"], songs)
|
|
self._load_status.setText(f"✓ Indlæst: {pl['name']} ({len(songs)} sange)")
|
|
except Exception as e:
|
|
self._load_status.setText(f"Fejl: {e}")
|
|
|
|
def _delete_selected(self):
|
|
item = self._pl_list.currentItem()
|
|
if not item:
|
|
return
|
|
pl = item.data(Qt.ItemDataRole.UserRole)
|
|
reply = QMessageBox.question(
|
|
self, "Slet liste",
|
|
f"Slet danselisten \"{pl['name']}\"?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
)
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
try:
|
|
from local.local_db import get_db
|
|
with get_db() as conn:
|
|
conn.execute("DELETE FROM playlists WHERE id=?", (pl["id"],))
|
|
self._load_saved_playlists()
|
|
except Exception as e:
|
|
self._load_status.setText(f"Fejl: {e}")
|
|
|
|
# ── Fane 3: Importer ──────────────────────────────────────────────────────
|
|
|
|
def _build_import_tab(self) -> QWidget:
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
layout.setSpacing(8)
|
|
|
|
lbl = QLabel(
|
|
"Importer fra M3U, M3U8 eller en tekstfil med én filsti per linje.\n"
|
|
"Sange der ikke er i biblioteket forsøges tilføjet automatisk."
|
|
)
|
|
lbl.setWordWrap(True)
|
|
lbl.setObjectName("result_count")
|
|
layout.addWidget(lbl)
|
|
|
|
btn_browse = QPushButton("📂 Vælg fil...")
|
|
btn_browse.clicked.connect(self._browse_import)
|
|
layout.addWidget(btn_browse)
|
|
|
|
layout.addWidget(QLabel("Eller indsæt filstier direkte (én per linje):"))
|
|
self._import_text = QTextEdit()
|
|
self._import_text.setPlaceholderText(
|
|
"/sti/til/sang1.mp3\n/sti/til/sang2.flac\n..."
|
|
)
|
|
self._import_text.setMaximumHeight(120)
|
|
layout.addWidget(self._import_text)
|
|
|
|
layout.addWidget(QLabel("Navn på den importerede liste:"))
|
|
self._import_name = QLineEdit()
|
|
self._import_name.setPlaceholderText("Importeret liste")
|
|
layout.addWidget(self._import_name)
|
|
|
|
btn_import = QPushButton("📥 Importer")
|
|
btn_import.clicked.connect(self._do_import)
|
|
layout.addWidget(btn_import)
|
|
|
|
self._import_status = QLabel("")
|
|
self._import_status.setObjectName("result_count")
|
|
self._import_status.setWordWrap(True)
|
|
layout.addWidget(self._import_status)
|
|
layout.addStretch()
|
|
return tab
|
|
|
|
def _browse_import(self):
|
|
path, _ = QFileDialog.getOpenFileName(
|
|
self, "Vælg afspilningsliste",
|
|
filter="Afspilningslister (*.m3u *.m3u8 *.txt);;Alle filer (*)"
|
|
)
|
|
if path:
|
|
self._import_name.setText(Path(path).stem)
|
|
paths = self._parse_playlist_file(path)
|
|
self._import_text.setPlainText("\n".join(paths))
|
|
|
|
def _parse_playlist_file(self, path: str) -> list[str]:
|
|
"""Parser M3U, M3U8 og tekst — returnerer liste af filstier."""
|
|
paths = []
|
|
base_dir = str(Path(path).parent)
|
|
try:
|
|
enc = "utf-8-sig" if path.lower().endswith(".m3u8") else "latin-1"
|
|
with open(path, encoding=enc, errors="replace") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
# Gør relativ sti absolut
|
|
if not os.path.isabs(line):
|
|
line = os.path.join(base_dir, line)
|
|
paths.append(line)
|
|
except Exception as e:
|
|
self._import_status.setText(f"Læsefejl: {e}")
|
|
return paths
|
|
|
|
def _do_import(self):
|
|
raw = self._import_text.toPlainText().strip()
|
|
if not raw:
|
|
self._import_status.setText("Ingen filstier angivet")
|
|
return
|
|
|
|
name = self._import_name.text().strip() or "Importeret liste"
|
|
paths = [line.strip() for line in raw.splitlines() if line.strip()]
|
|
|
|
found = []
|
|
missing = []
|
|
|
|
try:
|
|
from local.local_db import get_song_by_path, upsert_song, get_db
|
|
from local.tag_reader import read_tags, is_supported
|
|
|
|
for p in paths:
|
|
row = get_song_by_path(p)
|
|
if row:
|
|
# Hent danse
|
|
with get_db() as conn:
|
|
dances = conn.execute(
|
|
"SELECT dance_name FROM song_dances WHERE song_id=? ORDER BY dance_order",
|
|
(row["id"],)
|
|
).fetchall()
|
|
found.append({
|
|
"id": row["id"],
|
|
"title": row["title"],
|
|
"artist": row["artist"],
|
|
"album": row["album"],
|
|
"bpm": row["bpm"],
|
|
"duration_sec": row["duration_sec"],
|
|
"local_path": row["local_path"],
|
|
"file_format": row["file_format"],
|
|
"file_missing": bool(row["file_missing"]),
|
|
"dances": [d["dance_name"] for d in dances],
|
|
})
|
|
elif os.path.exists(p) and is_supported(p):
|
|
# Filen er ikke scannet endnu — høst tags og tilføj
|
|
tags = read_tags(p)
|
|
song_id = upsert_song(tags)
|
|
found.append({
|
|
"id": song_id,
|
|
"title": tags.get("title", Path(p).stem),
|
|
"artist": tags.get("artist", ""),
|
|
"album": tags.get("album", ""),
|
|
"bpm": tags.get("bpm", 0),
|
|
"duration_sec": tags.get("duration_sec", 0),
|
|
"local_path": p,
|
|
"file_format": tags.get("file_format", ""),
|
|
"file_missing": False,
|
|
"dances": tags.get("dances", []),
|
|
})
|
|
else:
|
|
missing.append(p)
|
|
|
|
if found:
|
|
self.playlist_loaded.emit(name, found)
|
|
status = f"✓ Importeret {len(found)} sange som \"{name}\""
|
|
if missing:
|
|
status += f"\n⚠ {len(missing)} filer ikke fundet"
|
|
self._import_status.setText(status)
|
|
else:
|
|
self._import_status.setText("Ingen filer fundet — tjek stierne")
|
|
|
|
except Exception as e:
|
|
self._import_status.setText(f"Importfejl: {e}")
|