""" EU-Utility - Spotify Controller Plugin Control Spotify playback and display current track info. """ import subprocess import platform from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSlider, QProgressBar ) from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal from plugins.base_plugin import BasePlugin class SpotifyInfoThread(QThread): """Background thread to fetch Spotify info.""" info_ready = pyqtSignal(dict) error = pyqtSignal(str) def __init__(self, system): super().__init__() self.system = system def run(self): """Fetch Spotify info.""" try: if self.system == "Linux": result = subprocess.run( ['playerctl', '--player=spotify', 'metadata', '--format', '{{title}}|{{artist}}|{{album}}|{{position}}|{{mpris:length}}|{{status}}'], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: parts = result.stdout.strip().split('|') if len(parts) >= 6: self.info_ready.emit({ 'title': parts[0] or 'Unknown', 'artist': parts[1] or 'Unknown Artist', 'album': parts[2] or '', 'position': self._parse_time(parts[3]), 'duration': self._parse_time(parts[4]), 'is_playing': parts[5] == 'Playing' }) return elif self.system == "Darwin": script = ''' tell application "Spotify" if player state is playing then return (name of current track) & "|" & (artist of current track) & "|" & (album of current track) & "|" & (player position) & "|" & (duration of current track / 1000) & "|Playing" else return (name of current track) & "|" & (artist of current track) & "|" & (album of current track) & "|" & (player position) & "|" & (duration of current track / 1000) & "|Paused" end if end tell ''' result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True, timeout=5) if result.returncode == 0: parts = result.stdout.strip().split('|') if len(parts) >= 6: self.info_ready.emit({ 'title': parts[0] or 'Unknown', 'artist': parts[1] or 'Unknown Artist', 'album': parts[2] or '', 'position': float(parts[3]) if parts[3] else 0, 'duration': float(parts[4]) if parts[4] else 0, 'is_playing': parts[5] == 'Playing' }) return # Default/empty response self.info_ready.emit({ 'title': 'Not playing', 'artist': '', 'album': '', 'position': 0, 'duration': 0, 'is_playing': False }) except Exception as e: self.error.emit(str(e)) self.info_ready.emit({ 'title': 'Not playing', 'artist': '', 'album': '', 'position': 0, 'duration': 0, 'is_playing': False }) def _parse_time(self, time_str): """Parse time string to seconds.""" try: return int(time_str) / 1000000 except: return 0 class SpotifyControllerPlugin(BasePlugin): """Control Spotify playback and display current track.""" name = "Spotify" version = "1.1.0" author = "ImpulsiveFPS" description = "Control Spotify and view current track info" hotkey = "ctrl+shift+m" def initialize(self): """Setup Spotify controller.""" self.system = platform.system() self.update_timer = None self.info_thread = None self.current_info = { 'title': 'Not playing', 'artist': '', 'album': '', 'position': 0, 'duration': 0, 'is_playing': False } def get_ui(self): """Create Spotify controller UI.""" widget = QWidget() layout = QVBoxLayout(widget) layout.setSpacing(12) # Title title = QLabel("Spotify") title.setStyleSheet("color: #1DB954; font-size: 18px; font-weight: bold;") layout.addWidget(title) # Album Art Placeholder self.album_art = QLabel("💿") self.album_art.setAlignment(Qt.AlignmentFlag.AlignCenter) self.album_art.setStyleSheet(""" QLabel { background-color: #282828; border-radius: 8px; font-size: 64px; padding: 20px; min-height: 120px; } """) layout.addWidget(self.album_art) # Track info container info_container = QWidget() info_container.setStyleSheet(""" QWidget { background-color: #1a1a1a; border-radius: 8px; padding: 12px; } """) info_layout = QVBoxLayout(info_container) info_layout.setSpacing(4) # Track title self.track_label = QLabel("Not playing") self.track_label.setStyleSheet(""" color: white; font-size: 16px; font-weight: bold; """) self.track_label.setWordWrap(True) info_layout.addWidget(self.track_label) # Artist self.artist_label = QLabel("") self.artist_label.setStyleSheet(""" color: #b3b3b3; font-size: 13px; """) info_layout.addWidget(self.artist_label) # Album self.album_label = QLabel("") self.album_label.setStyleSheet(""" color: #666; font-size: 11px; """) info_layout.addWidget(self.album_label) layout.addWidget(info_container) # Time info time_layout = QHBoxLayout() self.position_label = QLabel("0:00") self.position_label.setStyleSheet("color: #888; font-size: 11px;") time_layout.addWidget(self.position_label) time_layout.addStretch() self.duration_label = QLabel("0:00") self.duration_label.setStyleSheet("color: #888; font-size: 11px;") time_layout.addWidget(self.duration_label) layout.addLayout(time_layout) # Progress bar self.progress = QProgressBar() self.progress.setRange(0, 100) self.progress.setValue(0) self.progress.setTextVisible(False) self.progress.setStyleSheet(""" QProgressBar { background-color: #404040; border: none; height: 4px; border-radius: 2px; } QProgressBar::chunk { background-color: #1DB954; border-radius: 2px; } """) layout.addWidget(self.progress) # Control buttons btn_layout = QHBoxLayout() btn_layout.setSpacing(15) btn_layout.addStretch() # Previous prev_btn = QPushButton("⏮") prev_btn.setFixedSize(50, 50) prev_btn.setStyleSheet(self._get_button_style("#404040")) prev_btn.clicked.connect(self._previous_track) btn_layout.addWidget(prev_btn) # Play/Pause self.play_btn = QPushButton("▶") self.play_btn.setFixedSize(60, 60) self.play_btn.setStyleSheet(self._get_play_button_style()) self.play_btn.clicked.connect(self._toggle_playback) btn_layout.addWidget(self.play_btn) # Next next_btn = QPushButton("⏭") next_btn.setFixedSize(50, 50) next_btn.setStyleSheet(self._get_button_style("#404040")) next_btn.clicked.connect(self._next_track) btn_layout.addWidget(next_btn) btn_layout.addStretch() layout.addLayout(btn_layout) # Volume volume_layout = QHBoxLayout() volume_layout.addWidget(QLabel("🔈")) self.volume_slider = QSlider(Qt.Orientation.Horizontal) self.volume_slider.setRange(0, 100) self.volume_slider.setValue(50) self.volume_slider.setStyleSheet(""" QSlider::groove:horizontal { background: #404040; height: 4px; border-radius: 2px; } QSlider::handle:horizontal { background: #fff; width: 12px; margin: -4px 0; border-radius: 6px; } QSlider::sub-page:horizontal { background: #1DB954; border-radius: 2px; } """) self.volume_slider.valueChanged.connect(self._set_volume) volume_layout.addWidget(self.volume_slider) volume_layout.addWidget(QLabel("🔊")) layout.addLayout(volume_layout) # Status self.status_label = QLabel("Click play to control Spotify") self.status_label.setStyleSheet("color: #666; font-size: 10px;") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.status_label) layout.addStretch() # Start update timer self._start_timer() return widget def _get_button_style(self, color): """Get button stylesheet.""" return f""" QPushButton {{ background-color: {color}; color: white; font-size: 18px; border: none; border-radius: 25px; }} QPushButton:hover {{ background-color: #505050; }} QPushButton:pressed {{ background-color: #303030; }} """ def _get_play_button_style(self): """Get play button style (green).""" return """ QPushButton { background-color: #1DB954; color: white; font-size: 22px; border: none; border-radius: 30px; } QPushButton:hover { background-color: #1ed760; } QPushButton:pressed { background-color: #1aa34a; } """ def _start_timer(self): """Start status update timer.""" self.update_timer = QTimer() self.update_timer.timeout.connect(self._fetch_spotify_info) self.update_timer.start(1000) def _fetch_spotify_info(self): """Fetch Spotify info in background.""" if self.info_thread and self.info_thread.isRunning(): return self.info_thread = SpotifyInfoThread(self.system) self.info_thread.info_ready.connect(self._update_ui) self.info_thread.start() def _update_ui(self, info): """Update UI with Spotify info.""" self.current_info = info # Update track info self.track_label.setText(info.get('title', 'Unknown')) self.artist_label.setText(info.get('artist', '')) self.album_label.setText(info.get('album', '')) # Update play button is_playing = info.get('is_playing', False) self.play_btn.setText("⏸" if is_playing else "▶") # Update time position = info.get('position', 0) duration = info.get('duration', 0) self.position_label.setText(self._format_time(position)) self.duration_label.setText(self._format_time(duration)) # Update progress bar if duration > 0: progress = int((position / duration) * 100) self.progress.setValue(progress) else: self.progress.setValue(0) def _format_time(self, seconds): """Format seconds to mm:ss.""" try: minutes = int(seconds) // 60 secs = int(seconds) % 60 return f"{minutes}:{secs:02d}" except: return "0:00" def _send_media_key(self, key): """Send media key press to system.""" try: if self.system == "Windows": import ctypes key_codes = { 'play': 0xB3, 'next': 0xB0, 'prev': 0xB1, } if key in key_codes: ctypes.windll.user32.keybd_event(key_codes[key], 0, 0, 0) ctypes.windll.user32.keybd_event(key_codes[key], 0, 2, 0) return True elif self.system == "Linux": cmd_map = { 'play': ['playerctl', '--player=spotify', 'play-pause'], 'next': ['playerctl', '--player=spotify', 'next'], 'prev': ['playerctl', '--player=spotify', 'previous'], } if key in cmd_map: subprocess.run(cmd_map[key], capture_output=True) return True elif self.system == "Darwin": cmd_map = { 'play': ['osascript', '-e', 'tell application "Spotify" to playpause'], 'next': ['osascript', '-e', 'tell application "Spotify" to next track'], 'prev': ['osascript', '-e', 'tell application "Spotify" to previous track'], } if key in cmd_map: subprocess.run(cmd_map[key], capture_output=True) return True except Exception as e: print(f"Error sending media key: {e}") return False def _toggle_playback(self): """Toggle play/pause.""" if self._send_media_key('play'): self.current_info['is_playing'] = not self.current_info.get('is_playing', False) self.play_btn.setText("⏸" if self.current_info['is_playing'] else "▶") self.status_label.setText("Command sent to Spotify") else: self.status_label.setText("❌ Could not control Spotify") def _next_track(self): """Next track.""" if self._send_media_key('next'): self.status_label.setText("⏭ Next track") else: self.status_label.setText("❌ Could not skip") def _previous_track(self): """Previous track.""" if self._send_media_key('prev'): self.status_label.setText("⏮ Previous track") else: self.status_label.setText("❌ Could not go back") def _set_volume(self, value): """Set volume (0-100).""" try: if self.system == "Linux": subprocess.run(['playerctl', '--player=spotify', 'volume', str(value / 100)], capture_output=True) except: pass def on_hotkey(self): """Toggle play/pause with hotkey.""" self._toggle_playback() def on_hide(self): """Stop timer when overlay hidden.""" if self.update_timer: self.update_timer.stop() def on_show(self): """Restart timer when overlay shown.""" if self.update_timer: self.update_timer.start() self._fetch_spotify_info() def shutdown(self): """Cleanup.""" if self.update_timer: self.update_timer.stop() if self.info_thread and self.info_thread.isRunning(): self.info_thread.wait()