import subprocess from PySide6.QtWidgets import ( QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QSizePolicy, ) from PySide6.QtCore import Qt, QSize, QTimer from PySide6.QtGui import QFont class MediaScreen(QWidget): def __init__(self): super().__init__() root = QVBoxLayout(self) root.setContentsMargins(18, 16, 18, 16) root.setSpacing(14) header = QHBoxLayout() header.setContentsMargins(0, 0, 0, 0) header.setSpacing(12) info_col = QVBoxLayout() info_col.setContentsMargins(0, 0, 0, 0) info_col.setSpacing(6) self.source = QLabel("Источник: Bluetooth") self.source.setObjectName("MediaSource") self.source.setFont(QFont("", 14, 600)) self.title = QLabel("Название трека") self.title.setObjectName("MediaTitle") self.title.setFont(QFont("", 22, 700)) self.artist = QLabel("Исполнитель") self.artist.setObjectName("MediaArtist") self.artist.setFont(QFont("", 16, 600)) info_col.addWidget(self.source) info_col.addWidget(self.title) info_col.addWidget(self.artist) info_col.addStretch(1) cover = QLabel("COVER") cover.setObjectName("MediaCover") cover.setAlignment(Qt.AlignCenter) cover.setFixedSize(QSize(240, 240)) header.addLayout(info_col, 1) header.addWidget(cover, 0, Qt.AlignRight | Qt.AlignTop) controls = QVBoxLayout() controls.setContentsMargins(0, 0, 0, 0) controls.setSpacing(12) time_row = QHBoxLayout() time_row.setContentsMargins(0, 0, 0, 0) time_row.setSpacing(10) self.time_pos = QLabel("0:00") self.time_pos.setObjectName("MediaTimePos") self.time_pos.setFont(QFont("", 12, 600)) self.time_total = QLabel("0:00") self.time_total.setObjectName("MediaTimeTotal") self.time_total.setFont(QFont("", 12, 600)) self.time_total.setAlignment(Qt.AlignRight | Qt.AlignVCenter) time_row.addWidget(self.time_pos) time_row.addStretch(1) time_row.addWidget(self.time_total) progress = QSlider(Qt.Horizontal) progress.setObjectName("MediaProgress") progress.setRange(0, 100) progress.setValue(35) self.progress = progress transport = QHBoxLayout() transport.setContentsMargins(0, 0, 0, 0) transport.setSpacing(16) btn_prev = QPushButton("⏮") btn_prev.setObjectName("MediaTransportBtn") btn_prev.setFixedSize(QSize(72, 72)) btn_prev.clicked.connect(self._prev) btn_play = QPushButton("▶") btn_play.setObjectName("MediaTransportBtnPrimary") btn_play.setFixedSize(QSize(96, 72)) btn_play.clicked.connect(self._toggle_play) btn_next = QPushButton("⏭") btn_next.setObjectName("MediaTransportBtn") btn_next.setFixedSize(QSize(72, 72)) btn_next.clicked.connect(self._next) transport.addStretch(1) transport.addWidget(btn_prev) transport.addWidget(btn_play) transport.addWidget(btn_next) transport.addStretch(1) volume_row = QHBoxLayout() volume_row.setContentsMargins(0, 0, 0, 0) volume_row.setSpacing(10) volume_lbl = QLabel("Громкость") volume_lbl.setObjectName("MediaVolumeLabel") volume_lbl.setFont(QFont("", 14, 600)) volume = QSlider(Qt.Horizontal) volume.setObjectName("MediaVolume") volume.setRange(0, 100) volume.setValue(55) volume.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) volume_row.addWidget(volume_lbl) volume_row.addWidget(volume, 1) controls.addLayout(time_row) controls.addWidget(progress) controls.addLayout(transport) controls.addLayout(volume_row) soft_keys = QHBoxLayout() soft_keys.setContentsMargins(0, 0, 0, 0) soft_keys.setSpacing(10) for label in ["SOURCE", "EQ", "FOLDER", "RANDOM", "REPEAT"]: btn = QPushButton(label) btn.setObjectName("MediaSoftBtn") btn.setMinimumHeight(52) soft_keys.addWidget(btn, 1) root.addLayout(header) root.addLayout(controls) root.addLayout(soft_keys) self._poll_timer = QTimer(self) self._poll_timer.timeout.connect(self._refresh_metadata) self._poll_timer.start(2000) self._refresh_metadata() def _toggle_play(self): status = self._player_status() if status == "playing": self._run_btctl(["menu player", "pause"]) else: self._run_btctl(["menu player", "play"]) QTimer.singleShot(300, self._refresh_metadata) def _prev(self): self._run_btctl(["menu player", "previous"]) QTimer.singleShot(300, self._refresh_metadata) def _next(self): self._run_btctl(["menu player", "next"]) QTimer.singleShot(300, self._refresh_metadata) def _player_status(self) -> str | None: out = self._run_btctl(["menu player", "show"]) for line in out.splitlines(): if "Status:" in line: return line.split("Status:", 1)[1].strip() return None def _refresh_metadata(self): out = self._run_btctl(["menu player", "show"]) title = None artist = None source = None position = None duration = None for line in out.splitlines(): if "Name:" in line: source = line.split("Name:", 1)[1].strip() if "Track.Title:" in line: title = line.split("Track.Title:", 1)[1].strip() if "Track.Artist:" in line: artist = line.split("Track.Artist:", 1)[1].strip() if "Position:" in line: position = self._parse_hex_value(line) if "Track.Duration:" in line: duration = self._parse_hex_value(line) if title: self.title.setText(title) if artist: self.artist.setText(artist) if source: self.source.setText(f"Источник: {source}") if duration is not None and duration > 0: self.progress.setRange(0, duration) self.time_total.setText(self._format_time(duration)) if position is not None: self.progress.setValue(position) self.time_pos.setText(self._format_time(position)) def _run_btctl(self, commands: list[str]) -> str: script = "\n".join(commands + ["back", "quit"]) + "\n" try: result = subprocess.run( ["bluetoothctl"], input=script, capture_output=True, text=True, timeout=3, check=False, ) except (subprocess.SubprocessError, OSError): return "" return result.stdout.strip() def _parse_hex_value(self, line: str) -> int | None: start = line.find("0x") if start == -1: return None hex_part = line[start:].split()[0] try: return int(hex_part, 16) except ValueError: return None def _format_time(self, ms: int) -> str: total_seconds = max(ms, 0) // 1000 minutes = total_seconds // 60 seconds = total_seconds % 60 return f"{minutes}:{seconds:02d}"