238 lines
7.0 KiB
Python
238 lines
7.0 KiB
Python
# 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
|
|
) |