!volume +10 increases by 10, !volume -5 decreases by 5.
Out-of-range results (below 0 or above 100) are rejected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tracks playback position via frame counting in stream_audio().
On stop/skip, saves URL + elapsed time to bot.state (SQLite).
!resume reloads the track and seeks to the saved position via
ffmpeg -ss. State persists across bot restarts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Non-URL input (e.g. !play classical music) searches YouTube for 10
results and picks one randomly. Also fixes --flat-playlist returning
"NA" as the URL for single videos by falling back to the original
input URL.
_resolve_title replaced with _resolve_tracks using --flat-playlist to
enumerate playlist entries. cmd_play enqueues each track individually,
with truncation when the queue is nearly full. Single-video behavior
unchanged.
stream_audio now accepts a callable for volume, re-read on each PCM
frame instead of capturing a static float at track start.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
asyncio's SSL memory-BIO transport silently drops voice packets even
though text works fine. pymumble uses blocking ssl.SSLSocket.send()
which reliably delivers voice data.
- Rewrite MumbleBot to use pymumble for connection, SSL, ping, and
voice encoding/sending
- Bridge pymumble thread callbacks to asyncio via
run_coroutine_threadsafe for text dispatch
- Voice via sound_output.add_sound(pcm) -- pymumble handles Opus
encoding, packetization, and timing
- Remove custom protobuf codec, voice varint, and opus ctypes wrapper
- Add container patches for pymumble ssl.wrap_socket (Python 3.13) and
opuslib find_library (musl/Alpine)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>