Files
LinedanceAfspiller/linedance-app/ui/playlist_manager.py
2026-04-19 13:43:11 +02:00

333 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, int) # (navn, sange, pl_id)
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 d.name FROM song_dances sd
JOIN dances d ON d.id = sd.dance_id
WHERE sd.song_id=? ORDER BY sd.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["name"] for d in dances],
})
self.playlist_loaded.emit(pl["name"], songs, pl["id"])
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(
"INSERT INTO deleted_playlists (name) "
"SELECT name FROM playlists WHERE id=?", (pl["id"],)
)
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 d.name FROM song_dances sd
JOIN dances d ON d.id = sd.dance_id
WHERE sd.song_id=? ORDER BY sd.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["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, 0)
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}")