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>