""" player.py — VLC-baseret afspiller med PyQt6 signals. Sender signals til GUI: position_changed(float) — 0.0–1.0 progress time_changed(int, int) — (current_sec, total_sec) levels_changed(float, float) — VU-meter L/R 0.0–1.0 song_ended() — sang færdig state_changed(str) — 'playing'|'paused'|'stopped' """ from PyQt6.QtCore import QObject, pyqtSignal, QTimer import random import math try: import vlc VLC_AVAILABLE = True except ImportError: VLC_AVAILABLE = False print("Advarsel: python-vlc ikke installeret — afspilning deaktiveret") class Player(QObject): position_changed = pyqtSignal(float) time_changed = pyqtSignal(int, int) levels_changed = pyqtSignal(float, float) song_ended = pyqtSignal() state_changed = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self._path: str | None = None self._duration: int = 0 self._demo_mode = False self._demo_stop_sec = 10 self._demo_fade_sec = 5 self._demo_fading = False self._volume = 78 if VLC_AVAILABLE: self._instance = vlc.Instance("--no-video", "--quiet") self._media_player = self._instance.media_player_new() self._events = self._media_player.event_manager() self._events.event_attach( vlc.EventType.MediaPlayerEndReached, self._on_end_reached, ) else: self._media_player = None # Timer til polling af position + VU-simulation self._poll_timer = QTimer(self) self._poll_timer.setInterval(80) self._poll_timer.timeout.connect(self._poll) # ── Indlæsning ──────────────────────────────────────────────────────────── def load(self, path: str, duration_sec: int = 0): """Indlæs en lydfil uden at starte afspilning.""" self._path = path self._duration = duration_sec self._demo_mode = False if VLC_AVAILABLE and self._media_player: # Konverter GVFS SMB-sti til SMB URL som VLC kan tilgå direkte vlc_path = self._resolve_path(path) media = self._instance.media_new(vlc_path) self._media_player.set_media(media) self._media_player.audio_set_volume(self._volume) self.position_changed.emit(0.0) self.time_changed.emit(0, self._duration) self.state_changed.emit("stopped") def _resolve_path(self, path: str) -> str: """Konverter platform-specifikke netværksstier til URL'er VLC kan bruge.""" import re, sys # Linux GVFS SMB: /run/user/1000/gvfs/smb-share:server=X,share=Y/sti/fil.mp3 m = re.match(r".*/gvfs/smb-share:server=([^,]+),share=([^/]+)(/.+)$", path) if m: server, share, rest = m.group(1), m.group(2), m.group(3) return f"smb://{server}/{share}{rest}" # Windows UNC: \\server\share\sti\fil.mp3 if path.startswith("\\\\"): # \\server\share\rest → smb://server/share/rest parts = path.replace("\\", "/").lstrip("/").split("/", 2) if len(parts) >= 2: server = parts[0] share = parts[1] rest = "/" + parts[2] if len(parts) > 2 else "" return f"smb://{server}/{share}{rest}" # Lokale stier og drevbogstaver (C:\...) — VLC håndterer dem fint return path self.position_changed.emit(0.0) self.time_changed.emit(0, self._duration) self.state_changed.emit("stopped") # ── Transport ───────────────────────────────────────────────────────────── def play(self): self._demo_mode = False if VLC_AVAILABLE and self._media_player: self._media_player.play() self._poll_timer.start() self.state_changed.emit("playing") def play_demo(self, stop_at_sec: int = 10, fade_sec: int = 5): """ Afspil fra start, fade ud over fade_sec sekunder og stop. Total afspilningstid = stop_at_sec + fade_sec. fade_sec=0 giver ingen fade. """ self._demo_mode = True self._demo_stop_sec = stop_at_sec + fade_sec # total tid inkl. fade self._demo_fade_sec = fade_sec self._demo_fading = False if VLC_AVAILABLE and self._media_player: self._media_player.set_time(0) self._media_player.audio_set_volume(self._volume) self._media_player.play() self._poll_timer.start() self.state_changed.emit("playing") def pause(self): if VLC_AVAILABLE and self._media_player: self._media_player.pause() self.state_changed.emit("paused") def stop(self): self._demo_mode = False self._demo_fading = False if VLC_AVAILABLE and self._media_player: self._media_player.audio_set_volume(self._volume) self._media_player.stop() self._poll_timer.stop() self.position_changed.emit(0.0) self.time_changed.emit(0, self._duration) self.state_changed.emit("stopped") def is_playing(self) -> bool: if VLC_AVAILABLE and self._media_player: return self._media_player.is_playing() return False def set_volume(self, volume: int): """0–100""" self._volume = volume if VLC_AVAILABLE and self._media_player: self._media_player.audio_set_volume(volume) def set_position(self, fraction: float): """Søg til position 0.0–1.0""" if VLC_AVAILABLE and self._media_player: self._media_player.set_position(fraction) # ── Intern polling ──────────────────────────────────────────────────────── def _poll(self): """Køres ~12 gange per sekund — opdaterer position og VU-meter.""" if VLC_AVAILABLE and self._media_player: pos = self._media_player.get_position() ms = self._media_player.get_time() cur = max(0, ms // 1000) else: # Simuleret tilstand (til UI-test uden VLC) pos = getattr(self, "_sim_pos", 0.0) self._sim_pos = min(1.0, pos + 0.001) cur = int(self._sim_pos * self._duration) pos = self._sim_pos if self._sim_pos >= 1.0: self._on_end_reached(None) return self.position_changed.emit(pos) self.time_changed.emit(cur, self._duration) # Demo fade-out og stop if self._demo_mode and cur >= self._demo_stop_sec: # Færdig — gendan volumen og stop if VLC_AVAILABLE and self._media_player: self._media_player.audio_set_volume(self._volume) self.stop() self._demo_mode = False self._demo_fading = False self.position_changed.emit(0.0) self.time_changed.emit(0, self._duration) self.state_changed.emit("demo_ended") return # Demo fade-out — de sidste _demo_fade_sec sekunder (0 = ingen fade) if self._demo_mode and VLC_AVAILABLE and self._media_player and self._demo_fade_sec > 0: secs_left = self._demo_stop_sec - cur if secs_left <= self._demo_fade_sec and secs_left > 0: fade_fraction = secs_left / self._demo_fade_sec # 1.0 → 0.0 log_fraction = math.log10(1 + fade_fraction * 9) / math.log10(10) faded_vol = int(self._volume * log_fraction) self._media_player.audio_set_volume(max(0, faded_vol)) self._demo_fading = True elif not self._demo_fading: self._media_player.audio_set_volume(self._volume) # VU-meter: brug VLC's audio-amplitude hvis tilgængelig, ellers simulér if VLC_AVAILABLE and self._media_player and self._media_player.is_playing(): # VLC eksponerer ikke amplitude direkte — vi bruger en blød simulation # der er baseret på position så det ser organisk ud base = 0.55 + 0.3 * abs(pos - 0.5) l = min(1.0, base + random.gauss(0, 0.12)) r = min(1.0, base + random.gauss(0, 0.12)) else: l = r = 0.0 self.levels_changed.emit(max(0.0, l), max(0.0, r)) def set_audio_device(self, device_id: str): """Sæt lydoutput-enhed. device_id fra get_audio_devices().""" if VLC_AVAILABLE and self._media_player: self._media_player.audio_output_device_set(None, device_id) @staticmethod def get_audio_devices() -> list[dict]: """Returner liste af tilgængelige lydenheder.""" if not VLC_AVAILABLE: return [] try: instance = vlc.Instance("--no-video", "--quiet") mp = instance.media_player_new() devices = [] d = mp.audio_output_device_enum() if d: node = d while node: devices.append({ "id": node.contents.device.decode("utf-8", errors="replace"), "name": node.contents.description.decode("utf-8", errors="replace"), }) node = node.contents.next vlc.libvlc_audio_output_device_list_release(d) mp.release() instance.release() return devices except Exception: return [] def _on_end_reached(self, event): """Kaldes fra VLC's event-tråd — må IKKE røre Qt-objekter direkte.""" from PyQt6.QtCore import QTimer as _QTimer _QTimer.singleShot(0, self._handle_end_in_main_thread) def _handle_end_in_main_thread(self): """Kaldes i main thread — her er det sikkert at røre Qt.""" self._poll_timer.stop() self.song_ended.emit() self.state_changed.emit("stopped") class PreviewPlayer(QObject): """Simpel preview-afspiller til bibliotek — ingen signals, bare play/stop.""" def __init__(self, parent=None): super().__init__(parent) self._volume = 78 self._device_id = "" if VLC_AVAILABLE: self._instance = vlc.Instance("--no-video", "--quiet") self._mp = self._instance.media_player_new() else: self._mp = None def play(self, path: str): if not VLC_AVAILABLE or not self._mp: return from player.player import Player vlc_path = Player._resolve_path(self, path) media = self._instance.media_new(vlc_path) self._mp.set_media(media) self._mp.audio_set_volume(self._volume) self._mp.play() # Sæt lydenhed efter play — VLC nulstiller den ved ny media if self._device_id: self._mp.audio_output_device_set(None, self._device_id) def pause(self): if VLC_AVAILABLE and self._mp: self._mp.pause() def resume(self): if VLC_AVAILABLE and self._mp: self._mp.play() def stop(self): if VLC_AVAILABLE and self._mp: self._mp.stop() def seek(self, fraction: float): if VLC_AVAILABLE and self._mp: self._mp.set_position(fraction) def is_playing(self) -> bool: if VLC_AVAILABLE and self._mp: return bool(self._mp.is_playing()) return False def get_position(self) -> float: if VLC_AVAILABLE and self._mp: return max(0.0, self._mp.get_position()) return 0.0 def get_time(self) -> int: if VLC_AVAILABLE and self._mp: return max(0, self._mp.get_time() // 1000) return 0 def get_duration(self) -> int: if VLC_AVAILABLE and self._mp: ms = self._mp.get_length() return max(0, ms // 1000) return 0 def set_volume(self, volume: int): self._volume = volume if VLC_AVAILABLE and self._mp: self._mp.audio_set_volume(volume) def set_audio_device(self, device_id: str): self._device_id = device_id if VLC_AVAILABLE and self._mp: self._mp.audio_output_device_set(None, device_id)