Files
MTS-music-another-client/player.py
jads e71f5623a7 style: format all code to PEP8 (flake8 clean)
Reformat every module with autopep8 and hand-tune the remaining long
lines so that `flake8` passes with default settings (max line length
79). No behavior changes: long f-strings, messages, QSS/Textual CSS
rules and argument lists are only wrapped or split, an unused import
and an unused local are dropped, and dbus-next D-Bus signature
annotations carry targeted `# noqa` comments.
2026-10-23 23:52:14 +03:00

168 lines
5.2 KiB
Python

"""Playback engine for the MTS Music clients.
ffplay supports pause via SIGSTOP/SIGCONT. Seeking restarts the stream with
`-ss` at the requested offset, which keeps the implementation tiny and avoids
requiring mpv/libmpv.
"""
from __future__ import annotations
import asyncio
import os
import shutil
import signal
import time
from pathlib import Path
class MusicPlayer:
def __init__(self, explicit: str | None = None, volume: int = 80) -> None:
self.path = self._pick(explicit)
self.proc: asyncio.subprocess.Process | None = None
self.url: str | None = None
self.offset = 0.0
self.paused = False
self.started_at = 0.0
self.paused_total = 0.0
self.paused_at = 0.0
self.switching = False
self.volume = max(0, min(100, int(volume)))
@staticmethod
def _pick(explicit: str | None) -> str | None:
if explicit:
return explicit
env = os.environ.get("MTSMUSIC_PLAYER")
if env:
return env
for candidate in ("mpv", "ffplay"):
found = shutil.which(candidate)
if found:
return found
return None
def _command(self, url: str, start: float = 0.0) -> list[str]:
name = Path(self.path or "").name
if name == "ffplay":
command = [
self.path or "ffplay",
"-nodisp",
"-autoexit",
"-volume",
str(self.volume),
"-loglevel",
"error",
]
if start > 0:
command += ["-ss", f"{start:.3f}"]
return command + [url]
command = [
self.path or "mpv",
"--no-video",
"--really-quiet",
f"--volume={self.volume}",
]
if start > 0:
command.append(f"--start={start:.3f}")
return command + [url]
async def set_volume(self, volume: int) -> None:
"""Меняет громкость; ffplay/mpv не умеют на лету, поэтому
перезапускаем."""
self.volume = max(0, min(100, int(volume)))
if self.url and self.is_playing():
position = self.elapsed()
was_paused = self.paused
await self.play(self.url, position)
if was_paused:
self.pause()
async def play(self, url: str, start: float = 0.0) -> None:
await self.stop()
if not self.path:
raise RuntimeError("не найден mpv/ffplay")
self.switching = True
self.url = url
self.offset = max(0.0, start)
self.proc = await asyncio.create_subprocess_exec(
*self._command(url, self.offset),
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
self.paused = False
self.started_at = time.monotonic()
self.paused_total = 0.0
self.switching = False
async def stop(self) -> None:
self.switching = True
proc = self.proc
if proc and proc.returncode is None:
if self.paused:
try:
proc.send_signal(signal.SIGCONT)
except ProcessLookupError:
pass
proc.terminate()
try:
await asyncio.wait_for(proc.wait(), timeout=2)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
self.proc = None
self.paused = False
self.switching = False
def pause(self) -> None:
if self.proc and self.proc.returncode is None and not self.paused:
try:
self.proc.send_signal(signal.SIGSTOP)
except ProcessLookupError:
return
self.paused = True
self.paused_at = time.monotonic()
def resume(self) -> None:
if self.proc and self.proc.returncode is None and self.paused:
try:
self.proc.send_signal(signal.SIGCONT)
except ProcessLookupError:
return
self.paused = False
self.paused_total += time.monotonic() - self.paused_at
def toggle_pause(self) -> bool:
if self.paused:
self.resume()
else:
self.pause()
return self.paused
def is_playing(self) -> bool:
return bool(self.proc and self.proc.returncode is None)
def elapsed(self) -> float:
if not self.proc:
return 0.0
now = self.paused_at if self.paused else time.monotonic()
return max(
0.0,
self.offset +
now -
self.started_at -
self.paused_total)
async def seek(self, delta: float, duration: float) -> None:
"""Seek by delta seconds. Restarts ffplay at the new position."""
if not self.url or not self.is_playing():
return
limit = max(1.0, duration - 1.0) if duration else None
target = max(0.0, self.elapsed() + delta)
if limit:
target = min(target, limit)
was_paused = self.paused
await self.play(self.url, target)
if was_paused:
self.pause()