|
|
před 1 dnem | |
|---|---|---|
| asot_player | před 1 dnem | |
| tests | před 1 dnem | |
| .gitignore | před 1 týdnem | |
| LICENSE | před 1 týdnem | |
| README.md | před 1 dnem | |
| pyproject.toml | před 1 týdnem | |
| uv.lock | před 1 týdnem |
A terminal player for the ASOT (A State of Trance — Armin van Buuren) weekly mix stream from miroppb.com/ASOT. It plays live MP3 streams with mpv, shows a small multi-line ANSI UI (episode number, current song, date, progress bar), and resumes the episode you left off on next launch.
ASOT 1298 PLAYING
Armin van Buuren - A State of Trance (2026) 07 Feb 2026
[██████████████████████░░░░░░] 00:12:34 / 05:28:00 80% ▓▓▓▓░░
space play/pause ←/→ ±15s ↑/↓ volume m mute n next p prev q quit
Range: bytes=0-1048576 HTTP request. miroppb embeds per-track TIT2 frames (plus CHAP, TPE2, TALB, TYER, TXXX) inside the first MiB, so the "song if known" line usually updates within seconds.asot_player/id3.py).~/.config/asot-player/state.json (or $XDG_STATE_HOME/asot-player/, overridable via --state-dir / ASOT_STATE_DIR). Re-opening picks up the saved episode.eof), the next episode in the feed starts automatically; at the end of the feed it reports and exits.Requires Python ≥ 3.10 and the system mpv binary (no Python dependencies):
# Arch sudo pacman -S mpv
# Debian/Ubuntu sudo apt-get install -y mpv
# Fedora sudo dnf install mpv
# macOS brew --cask install mpv
uv sync # builds and installs this package (dev venv)
asot # ready to go
If mpv is installed somewhere outside PATH: asot --mpv-binary /path/to/mpv.
asot # latest episode (or resume saved position)
asot --ep 1298 # start a specific episode
asot --fresh # ignore saved state, start the latest episode
asot --width 78 # force terminal width (tests, narrow terminals)
asot --no-color # disable ANSI colour
asot --fake # no mpv / no stream: exercises UI + state only
asot --fake-speed 1 # fake engine: seconds of "playback" per tick
asot --state-dir /tmp/x # custom state directory (also: $ASOT_STATE_DIR)
Keys (single lines, no full-screen mode):
| Key | Action |
|---|---|
space / t |
play / pause |
m |
mute toggle |
↑ / ↓ |
volume up / down (0.1 steps) |
→ / ← |
forward / backward 15 s |
n / p |
next / previous episode |
q / Ctrl-C |
quit |
playback dropped … recovery attempt N above itself and playback restarts at the saved position keeping the paused/muted/volume state. Up to 5 consecutive failed recoveries are retried (1 s apart); a healthy tick resets the budget.PAUSED (with one stream closed while paused — press play to resume note), mpv is not restarted, and no recovery budget is spent. Press space to start playback again from the saved position.MAX_RUNTIME_SECONDS (36 h, see asot_player/main.py) stops with an explicit runtime limit reached — stopping report; it never closes silently. (An old 50 000-tick cap silently ended sessions after ~5 h with exit code 0 — the reason for a player that "just closed" when left paused.)0 normal (quit, end of feed) · 1 playback failed / runtime limit · 2 engine error (mpv missing, stream unplayable) · 130 interrupted.miroppb.com/rss is parsed for <item> titles matching "Episode NNN" and enclosure URLs.fetch_meta(url) sends Range: bytes=0-1048576, parses the ID3 header from the returned bytes (asot_player/id3.py). Empty/failed probes just mean "no track info" — playback never blocks on metadata.playback-state, pause, mute, volume, playback-time, length.uv run python -m unittest tests.test_asot -v
# ~35 tests, sub-second (all network mocked; ID3 tests include a real 1 MiB
# episode head saved at /tmp/ep1298_head.bin when available)
# UI/state smoke test without mpv or a live stream:
timeout 60 uv run asot --fake --fake-speed 1 \
--state-dir /tmp/stF --ep 1298 --width 78
Package layout is flat (no src/): asot_player/{main,engine,feed,id3,state,ui}.py, entry point asot = asot_player.main:main.
MIT.