# history_widget.py import threading import musicbrainzngs from mutagen import File as MutagenFile from PySide6.QtWidgets import ( QFrame, QVBoxLayout, QLabel, QScrollArea, QWidget ) from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QFont musicbrainzngs.set_useragent("RadioPanel", "1.0", "radio@panel.local") def _read_file_tags(filepath: str) -> dict: try: tags = MutagenFile(filepath, easy=True) if tags is None: return {} def get(key): val = tags.get(key) return val[0] if val else None return { 'artist': get('artist'), 'title': get('title'), 'album': get('album'), 'year': get('date'), } except Exception: return {} def _lookup_musicbrainz(artist: str, title: str) -> dict: try: result = musicbrainzngs.search_recordings( recording=title, artist=artist, limit=1 ) recordings = result.get('recording-list', []) if not recordings: return {} rec = recordings[0] releases = rec.get('release-list', []) if not releases: return {} release = releases[0] album = release.get('title', None) year = None date = release.get('date', '') if date: year = date[:4] return {'album': album, 'year': year} except Exception: return {} class HistoryEntry(QFrame): def __init__(self, index: int, filepath: str, metadata: dict, parent=None): super().__init__(parent) self.setFrameStyle(QFrame.Box) self.setStyleSheet(""" QFrame { background-color: #1a1a1a; border: none; border-bottom: 1px solid #2a2a2a; } """) layout = QVBoxLayout(self) layout.setContentsMargins(8, 6, 8, 6) layout.setSpacing(2) artist = metadata.get('artist') or 'Unknown Artist' title = metadata.get('title') or self._filename(filepath) top_row = QLabel(f"{index}. {artist} — {title}") top_row.setFont(QFont("Arial", 11, QFont.Bold)) top_row.setStyleSheet("color: #00ff41; background: transparent; border: none;") top_row.setWordWrap(True) layout.addWidget(top_row) album = metadata.get('album') or '' year = metadata.get('year') or '' sub_parts = [] if album: sub_parts.append(album) if year: sub_parts.append(year[:4]) if sub_parts: sub_row = QLabel(" " + " · ".join(sub_parts)) sub_row.setFont(QFont("Arial", 9)) sub_row.setStyleSheet( "color: #888888; background: transparent; border: none;" ) sub_row.setWordWrap(True) layout.addWidget(sub_row) def _filename(self, filepath: str) -> str: import os name = os.path.basename(filepath) return os.path.splitext(name)[0] class HistoryWidget(QFrame): _metadata_ready = Signal(str, dict) def __init__(self, parent=None): super().__init__(parent) self._history = [] self._pending = {} self.setFrameStyle(QFrame.Box | QFrame.Raised) self.setStyleSheet(""" QFrame { background-color: #111111; border: 2px solid #336633; border-radius: 6px; } """) self._build_ui() self._metadata_ready.connect(self._on_metadata_ready) def _build_ui(self): root = QVBoxLayout(self) root.setContentsMargins(8, 8, 8, 8) root.setSpacing(6) header = QLabel("PLAY HISTORY") header.setFont(QFont("Arial", 11, QFont.Bold)) header.setStyleSheet(""" color: #336633; background: transparent; border: none; padding: 2px; """) header.setAlignment(Qt.AlignCenter) root.addWidget(header) self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll.setStyleSheet(""" QScrollArea { border: none; background: transparent; } QScrollBar:vertical { background: #1a1a1a; width: 8px; border-radius: 4px; } QScrollBar::handle:vertical { background: #336633; border-radius: 4px; } """) self.list_widget = QWidget() self.list_widget.setStyleSheet("background: transparent;") self.list_layout = QVBoxLayout(self.list_widget) self.list_layout.setContentsMargins(0, 0, 0, 0) self.list_layout.setSpacing(0) self.list_layout.addStretch() self.scroll.setWidget(self.list_widget) root.addWidget(self.scroll) def add_track(self, filepath: str): index = len(self._history) + 1 metadata = _read_file_tags(filepath) self._history.append((filepath, metadata)) self._insert_entry(index, filepath, metadata) if not metadata.get('artist') or not metadata.get('album'): self._pending[filepath] = index threading.Thread( target=self._enrich_metadata, args=(filepath, metadata), daemon=True ).start() def _insert_entry(self, index: int, filepath: str, metadata: dict): entry = HistoryEntry(index, filepath, metadata) self.list_layout.insertWidget(0, entry) self.scroll.verticalScrollBar().setValue(0) def _enrich_metadata(self, filepath: str, existing: dict): artist = existing.get('artist') title = existing.get('title') if not artist or not title: return mb_data = _lookup_musicbrainz(artist, title) if not mb_data: return merged = dict(existing) if not merged.get('album') and mb_data.get('album'): merged['album'] = mb_data['album'] if not merged.get('year') and mb_data.get('year'): merged['year'] = mb_data['year'] self._metadata_ready.emit(filepath, merged) def _on_metadata_ready(self, filepath: str, metadata: dict): for i, (fp, _) in enumerate(self._history): if fp == filepath: self._history[i] = (fp, metadata) break self._rebuild_list() def _rebuild_list(self): while self.list_layout.count() > 1: item = self.list_layout.takeAt(0) if item.widget(): item.widget().deleteLater() for i, (filepath, metadata) in enumerate(reversed(self._history)): index = len(self._history) - i entry = HistoryEntry(index, filepath, metadata) self.list_layout.insertWidget( self.list_layout.count() - 1, entry )