# fingerprint_widget.py import os import json import tempfile import subprocess import threading import requests import numpy as np import soundfile as sf from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QLabel, QPushButton, QProgressBar, QFrame ) from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtGui import QFont class FingerprintWidget(QFrame): result_ready = Signal(object) error_ready = Signal(str) def __init__(self, engine, api_key: str, parent=None): super().__init__(parent) self.engine = engine self.acoustid_key = api_key.strip() self.recording_seconds = 25 self.last_signal_check = False self.setFrameStyle(QFrame.Box | QFrame.Raised) self.setStyleSheet(""" QFrame { background-color: #0d0d1a; border: 2px solid #4444aa; border-radius: 6px; } """) self._build_ui() self.result_ready.connect(self._show_result) self.error_ready.connect(self._show_error) self.signal_timer = QTimer(self) self.signal_timer.setInterval(500) self.signal_timer.timeout.connect(self._check_signal) self.signal_timer.start() def _build_ui(self): root = QVBoxLayout(self) root.setContentsMargins(10, 8, 10, 8) root.setSpacing(6) header = QLabel("AUDIO FINGERPRINT") header.setFont(QFont("Arial", 11, QFont.Bold)) header.setStyleSheet("color: #6666cc; background: transparent; border: none;") header.setAlignment(Qt.AlignCenter) root.addWidget(header) port_info = QLabel("JACK: RadioPanel:fingerprint_L / R") port_info.setFont(QFont("Courier New", 8)) port_info.setStyleSheet("color: #444488; background: transparent; border: none;") port_info.setAlignment(Qt.AlignCenter) root.addWidget(port_info) self.signal_label = QLabel("● NO SIGNAL") self.signal_label.setFont(QFont("Courier New", 14, QFont.Bold)) self.signal_label.setStyleSheet("color: #330000; background: transparent; border: none;") self.signal_label.setAlignment(Qt.AlignCenter) root.addWidget(self.signal_label) self.progress = QProgressBar() self.progress.setRange(0, self.recording_seconds * 10) self.progress.setValue(0) self.progress.setTextVisible(False) self.progress.setFixedHeight(8) self.progress.setStyleSheet(""" QProgressBar { border: 1px solid #4444aa; border-radius: 3px; background: #0a0a1a; } QProgressBar::chunk { background-color: #6666cc; } """) self.progress.hide() root.addWidget(self.progress) self.status_label = QLabel("READY") self.status_label.setFont(QFont("Courier New", 10, QFont.Bold)) self.status_label.setStyleSheet("color: #444488; background: transparent; border: none;") self.status_label.setAlignment(Qt.AlignCenter) root.addWidget(self.status_label) result_frame = QFrame() result_frame.setStyleSheet(""" QFrame { background-color: #0a0a1a; border: 1px solid #333366; border-radius: 3px; } """) result_layout = QVBoxLayout(result_frame) result_layout.setContentsMargins(6, 6, 6, 6) self.result_artist = QLabel("Artist: -") self.result_artist.setFont(QFont("Courier New", 10, QFont.Bold)) self.result_artist.setStyleSheet("color: #555588;") self.result_title = QLabel("Title: -") self.result_title.setFont(QFont("Courier New", 11, QFont.Bold)) self.result_title.setStyleSheet("color: #555588;") result_layout.addWidget(self.result_artist) result_layout.addWidget(self.result_title) root.addWidget(result_frame) self.btn_fingerprint = QPushButton("FINGERPRINT") self.btn_fingerprint.setFixedHeight(44) self.btn_fingerprint.setFont(QFont("Arial", 12, QFont.Bold)) self.btn_fingerprint.setStyleSheet(""" QPushButton { background-color: #4444aa; color: white; border: 1px solid #6666cc; border-radius: 4px; } QPushButton:hover { background-color: #5555bb; } QPushButton:pressed { background-color: #333399; } QPushButton:disabled { background-color: #222244; color: #666688; } """) self.btn_fingerprint.clicked.connect(self._start_fingerprint) root.addWidget(self.btn_fingerprint) root.addStretch() def _check_signal(self): if self.engine.recording: return levels = self.engine.fingerprint_levels has_signal = (levels[0] > 0.01 or levels[1] > 0.01) if has_signal != self.last_signal_check: self.last_signal_check = has_signal if has_signal: self.signal_label.setText("● SIGNAL PRESENT") self.signal_label.setStyleSheet("color: #00ff41; background: transparent;") else: self.signal_label.setText("● NO SIGNAL") self.signal_label.setStyleSheet("color: #330000; background: transparent;") def _start_fingerprint(self): if not self.acoustid_key: self.error_ready.emit("Set your AcoustID key in main.py first!") return if self.engine.recording: return self.btn_fingerprint.setEnabled(False) self.progress.show() self.progress.setValue(0) self.status_label.setText("LISTENING...") self.status_label.setStyleSheet("color: #ccaa00;") self.signal_label.setText("● RECORDING...") self.signal_label.setStyleSheet("color: #ccaa00;") self.result_artist.setText("Artist: -") self.result_title.setText("Title: -") self.recording_timer = QTimer(self) self.recording_timer.setInterval(100) self.recording_timer.timeout.connect(self._update_progress) self.recording_timer.start() self.engine.start_recording(self.recording_seconds, self._on_recording_finished) def _update_progress(self): if not self.engine.recording: self.recording_timer.stop() self.progress.setValue(self.progress.maximum()) return if self.engine.record_buffer is not None: progress = self.engine.record_position / self.engine.record_target_frames self.progress.setValue(int(progress * self.progress.maximum())) def _on_recording_finished(self, audio_data: np.ndarray, sr: int): self.recording_timer.stop() self.status_label.setText("ANALYZING...") self.status_label.setStyleSheet("color: #4488ff;") self.signal_label.setText("● PROCESSING") self.signal_label.setStyleSheet("color: #4488ff;") threading.Thread( target=self._fingerprint_thread, args=(audio_data, sr), daemon=True ).start() def _fingerprint_thread(self, audio_data: np.ndarray, sr: int): try: result = self._identify_acoustid(audio_data, sr) self.result_ready.emit(result) except Exception as e: err_str = str(e) if "Invalid API key" in err_str: err_str = "Invalid API Key" elif "limit" in err_str.lower(): err_str = "Daily limit reached" self.error_ready.emit(err_str) def _identify_acoustid(self, audio_data: np.ndarray, sr: int) -> dict: with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f: temp_path = f.name try: sf.write(temp_path, audio_data.T, sr, subtype='PCM_16') result = subprocess.run( ['fpcalc', '-json', temp_path], capture_output=True, text=True, timeout=15 ) if result.returncode != 0: raise RuntimeError(f"fpcalc failed: {result.stderr}") fp_data = json.loads(result.stdout) fingerprint = fp_data['fingerprint'] duration = fp_data.get('duration', 0) finally: os.unlink(temp_path) url = 'https://api.acoustid.org/v2/lookup' params = { 'client': self.acoustid_key, 'fingerprint': fingerprint, 'duration': int(duration), 'meta': 'recordings' } response = requests.get(url, params=params, timeout=15) data = response.json() if data.get('status') != 'ok': err = data.get('error', {}).get('message', 'Unknown') raise RuntimeError(f"AcoustID: {err}") results = data.get('results', []) if not results: return None best = results[0] recordings = best.get('recordings', []) if not recordings: return None rec = recordings[0] title = rec.get('title', 'Unknown') artists = rec.get('artists', []) artist = artists[0].get('name', 'Unknown') if artists else 'Unknown' return { 'artist': artist, 'title': title, 'score': best.get('score', 0) } def _show_result(self, result): self.btn_fingerprint.setEnabled(True) self.progress.hide() self.signal_label.setText("● DONE") self.signal_label.setStyleSheet("color: #00aa33;") if result is None: self.status_label.setText("NO MATCH") self.status_label.setStyleSheet("color: #aa4444;") else: self.status_label.setText(f"MATCH {result['score']:.0%}") self.status_label.setStyleSheet("color: #00ff41;") self.result_artist.setText(f"Artist: {result['artist']}") self.result_title.setText(f"Title: {result['title']}") self.result_artist.setStyleSheet("color: #cccccc;") self.result_title.setStyleSheet("color: #00ff41;") def _show_error(self, error: str): self.btn_fingerprint.setEnabled(True) self.progress.hide() self.signal_label.setText("● ERROR") self.signal_label.setStyleSheet("color: #aa4444;") self.status_label.setText("ERROR") self.status_label.setStyleSheet("color: #ff4444;") self.result_artist.setText("Error:") self.result_title.setText(error[:60]) self.result_artist.setStyleSheet("color: #aa4444;") self.result_title.setStyleSheet("color: #aa4444;")