""" library_panel.py — Musikbibliotek med søgning og drag-and-drop til danseliste. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QListWidget, QListWidgetItem, QLineEdit, QLabel, QHBoxLayout, QPushButton, QProgressBar, QAbstractItemView, ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QMimeData, QByteArray from PyQt6.QtGui import QColor, QDrag class DraggableLibraryList(QListWidget): """QListWidget der understøtter drag-start med sang-data som mime.""" def __init__(self, parent=None): super().__init__(parent) self.setDragEnabled(True) self.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly) self.setDefaultDropAction(Qt.DropAction.CopyAction) def startDrag(self, supported_actions): item = self.currentItem() if not item: return song = item.data(Qt.ItemDataRole.UserRole) if not song: return import json data = json.dumps(song).encode("utf-8") mime = QMimeData() mime.setData("application/x-linedance-song", QByteArray(data)) mime.setText(song.get("title", "")) drag = QDrag(self) drag.setMimeData(mime) drag.exec(Qt.DropAction.CopyAction) class LibraryPanel(QWidget): song_selected = pyqtSignal(dict) add_to_playlist = pyqtSignal(dict) scan_requested = pyqtSignal() edit_tags_requested = pyqtSignal(dict) send_mail_requested = pyqtSignal(dict) def __init__(self, parent=None): super().__init__(parent) self._all_songs: list[dict] = [] self._filtered: list[dict] = [] self._search_timer = QTimer(self) self._search_timer.setSingleShot(True) self._search_timer.setInterval(150) self._search_timer.timeout.connect(self._do_search) self._build_ui() def _build_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Header header = QHBoxLayout() header.setContentsMargins(10, 6, 10, 6) lbl = QLabel("BIBLIOTEK") lbl.setObjectName("section_title") header.addWidget(lbl) header.addStretch() btn_manage = QPushButton("⚙ Mapper") btn_manage.setFixedHeight(24) btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker") btn_manage.clicked.connect(self._manage_libraries) header.addWidget(btn_manage) layout.addLayout(header) # Scan status self._scan_bar = QProgressBar() self._scan_bar.setObjectName("scan_bar") self._scan_bar.setTextVisible(True) self._scan_bar.setFormat("Scanner...") self._scan_bar.setFixedHeight(16) self._scan_bar.setRange(0, 0) self._scan_bar.hide() layout.addWidget(self._scan_bar) self._scan_label = QLabel("") self._scan_label.setObjectName("result_count") self._scan_label.hide() layout.addWidget(self._scan_label) # Søgefelt self._search = QLineEdit() self._search.setPlaceholderText("Søg i titel, artist, album, dans...") self._search.textChanged.connect(self._on_search_changed) layout.addWidget(self._search) # Resultat-tæller + drag-hint hint_row = QHBoxLayout() hint_row.setContentsMargins(8, 2, 8, 2) self._count_label = QLabel("0 sange") self._count_label.setObjectName("result_count") hint_row.addWidget(self._count_label) hint_row.addStretch() drag_hint = QLabel("træk til danseliste →") drag_hint.setObjectName("result_count") hint_row.addWidget(drag_hint) layout.addLayout(hint_row) # Liste — draggable self._list = DraggableLibraryList() self._list.setObjectName("library_list") self._list.itemDoubleClicked.connect(self._on_double_click) self._list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._list.customContextMenuRequested.connect(self._show_context_menu) layout.addWidget(self._list) # ── Scanning ────────────────────────────────────────────────────────────── def _on_scan_clicked(self): self.scan_requested.emit() def set_scanning(self, scanning: bool, status_text: str = ""): if scanning: self._scan_bar.show() self._scan_label.setText(status_text or "Starter...") self._scan_label.show() else: self._scan_bar.hide() self._scan_label.hide() def update_scan_status(self, text: str): self._scan_label.setText(text) # ── Sange ───────────────────────────────────────────────────────────────── def load_songs(self, songs: list[dict]): self._all_songs = songs self._do_search() # ── Søgning ─────────────────────────────────────────────────────────────── def _on_search_changed(self): self._search_timer.start() def _do_search(self): q = self._search.text().strip().lower() self._filtered = [s for s in self._all_songs if self._matches(s, q)] if q else list(self._all_songs) total = len(self._all_songs) found = len(self._filtered) q_text = self._search.text().strip() self._count_label.setText( f"{found} resultat{'er' if found != 1 else ''} for \"{q_text}\"" if q_text else f"{total} sang{'e' if total != 1 else ''}" ) self._render() def _matches(self, song: dict, q: str) -> bool: return any(q in f.lower() for f in [ song.get("title", ""), song.get("artist", ""), song.get("album", ""), song.get("file_format", ""), ] + song.get("dances", [])) def _render(self): self._list.clear() q = self._search.text().strip().lower() for song in self._filtered: dances = song.get("dances", []) dance_str = " · " + " / ".join(dances) if dances else "" missing = song.get("file_missing", False) line1 = ("⚠ " if missing else "") + song.get("title", "—") line2 = f" {song.get('artist','—')} · {song.get('bpm',0)} BPM · {song.get('file_format','').upper()}{dance_str}" item = QListWidgetItem(f"{line1}\n{line2}") item.setData(Qt.ItemDataRole.UserRole, song) if missing: item.setForeground(QColor("#5a6070")) elif q and any(q in d.lower() for d in dances): item.setForeground(QColor("#e8a020")) self._list.addItem(item) # ── Handlinger ──────────────────────────────────────────────────────────── def _on_double_click(self, item: QListWidgetItem): song = item.data(Qt.ItemDataRole.UserRole) if song: self.song_selected.emit(song) def _show_context_menu(self, pos): from PyQt6.QtWidgets import QMenu item = self._list.itemAt(pos) if not item: return song = item.data(Qt.ItemDataRole.UserRole) if not song: return menu = QMenu(self) act_add = menu.addAction("Tilføj til danseliste") act_play = menu.addAction("Afspil") menu.addSeparator() act_tags = menu.addAction("✎ Rediger dans-tags...") act_bpm = menu.addAction("♩ Analysér BPM") menu.addSeparator() send_menu = menu.addMenu("Send til") act_mail = send_menu.addAction("✉ Send som mail") action = menu.exec(self._list.mapToGlobal(pos)) if action == act_add: self.add_to_playlist.emit(song) elif action == act_play: self.song_selected.emit(song) elif action == act_tags: self.edit_tags_requested.emit(song) elif action == act_bpm: self._analyze_bpm(song) elif action == act_mail: self.send_mail_requested.emit(song) def _analyze_bpm(self, song: dict): """Analysér BPM i baggrundstråd og opdater biblioteket.""" path = song.get("local_path", "") song_id = song.get("id", "") if not path or not song_id: return from PyQt6.QtCore import QThread, pyqtSignal as _sig class BpmWorker(QThread): done = _sig(float) def __init__(self, p, sid): super().__init__() self._p, self._sid = p, sid def run(self): from local.tag_reader import analyze_and_save_bpm bpm = analyze_and_save_bpm(self._p, self._sid) if bpm: self.done.emit(bpm) self._bpm_worker = BpmWorker(path, song_id) self._bpm_worker.done.connect( lambda bpm: ( self._do_search(), print(f"BPM analyseret: {bpm}") ) ) self._bpm_worker.start() def _manage_libraries(self): from ui.library_manager import LibraryManagerDialog dialog = LibraryManagerDialog(parent=self.window()) dialog.library_removed.connect(lambda _: self.scan_requested.emit()) dialog.exec() def _add_folder(self): from PyQt6.QtWidgets import QFileDialog folder = QFileDialog.getExistingDirectory(self, "Vælg musikmappe") if folder: mw = self.window() if hasattr(mw, "add_library_path"): mw.add_library_path(folder)