Files
gti_radiostudio/fingerprint_widget.py
T

296 lines
11 KiB
Python

# 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;")