""" 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 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) 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 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) 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}")