car_ui/screens/media.py
2026-01-08 23:13:01 +03:00

252 lines
8.1 KiB
Python

import subprocess
from PySide6.QtWidgets import (
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QSlider,
QSizePolicy,
)
from PySide6.QtCore import Qt, QSize, QTimer, Signal
from PySide6.QtGui import QFont
class MediaScreen(QWidget):
source_changed = Signal(str)
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))
self.album = QLabel("Альбом")
self.album.setObjectName("MediaAlbum")
self.album.setFont(QFont("", 14, 600))
info_col.addWidget(self.source)
info_col.addWidget(self.title)
info_col.addWidget(self.artist)
info_col.addWidget(self.album)
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)
self.btn_play = QPushButton("")
self.btn_play.setObjectName("MediaTransportBtnPrimary")
self.btn_play.setFixedSize(QSize(96, 72))
self.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(self.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
album = None
source = None
position = None
duration = None
status = 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 "Track.Album:" in line:
album = line.split("Track.Album:", 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 "Status:" in line:
status = line.split("Status:", 1)[1].strip()
if title:
self.title.setText(title)
if artist:
self.artist.setText(artist)
if album:
self.album.setText(album)
if source:
text = f"Источник: {source}"
self.source.setText(text)
self.source_changed.emit(text)
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))
if status:
self.btn_play.setText("" if status == "playing" else "")
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}"