From 5a8d568931d9b23ce0df1265d05259a7012081c9 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 14 Jun 2026 01:46:29 +0300 Subject: init: mostly vibed --- dev-docs/daft.md | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 dev-docs/daft.md (limited to 'dev-docs/daft.md') diff --git a/dev-docs/daft.md b/dev-docs/daft.md new file mode 100644 index 0000000..880b95c --- /dev/null +++ b/dev-docs/daft.md @@ -0,0 +1,132 @@ +# daft - developer documentation + +## Overview + +Daft is a YouTube playlist manager for IRC. Anyone can queue tracks. Ops control +playback. The bot announces tracks and advances the queue on a fixed timer. +There is no actual audio playback. The timer simulates track duration for +queue progression purposes. + +--- + +## Module structure + +``` +daft/ + dj.py - all commands and queue logic + __init__.py - empty +``` + +--- + +## Queue model + +The queue is a plain list stored in `bot.memory["dj_queue"]`. There is no +persistence. The queue is lost on restart. This is intentional: a music queue +is a live social activity, not something that needs to survive a restart. + +`dj_pos` is the index of the currently playing track. `-1` means nothing is +playing. When a track ends, `_on_track_end()` advances `dj_pos` by 1 and +calls `_play_at()` for the next position. + +--- + +## Track duration + +`TRACK_DURATION = 120` seconds. This is a fixed constant, not the actual +duration of the YouTube video. The bot has no way to query video duration +without an API key. The timer is just for queue advancement in the IRC +session. If you want a different advancement interval, change the constant. + +--- + +## URL validation + +`_extract_video_id()` validates and normalizes YouTube URLs: + +1. Ensures the scheme is http or https. +2. Checks the domain against a whitelist of known YouTube hosts. +3. Rejects shorts, embeds, and live stream paths. +4. Extracts the 11-character video ID. + +All queued URLs are normalized to `https://youtu.be/`. This means the +same video added with different URL formats (watch?v=, youtu.be/, m.youtube.com) +is detected as a duplicate. + +The regex `^[A-Za-z0-9_-]{11}$` validates the video ID format. This is the +format YouTube has used since the beginning. If YouTube ever changes their +ID format, this regex needs to be updated. + +--- + +## Threading + +`_on_track_end()` is called from a `threading.Timer` callback, which runs +in a separate thread from Sopel's main event loop. It reads and writes shared +state in `bot.memory`. This requires locking. + +A `threading.RLock()` (`dj_lock`) is used instead of a regular `Lock()` because +`cmd_skip` calls `_on_track_end()` directly from the main thread while holding +the lock. An RLock allows the same thread to acquire it multiple times without +deadlocking. + +The lock is acquired in: +- `_on_track_end()` (timer thread) +- `cmd_play`, `cmd_stop`, `cmd_skip`, `cmd_remove`, `cmd_clear` (main thread) +- `cmd_dj`, `cmd_np`, `cmd_queue` (main thread, for read consistency) + +`_play_at()` and `_cancel_timer()` do not acquire the lock themselves because +they are always called by code that already holds it. + +--- + +## Removing a playing track + +`cmd_remove()` removes the current track and advances to the next one. +The logic: + +1. `queue.pop(pos)` removes the current track. The element that was at + `pos+1` is now at `pos`. +2. `dj_pos` is set to `pos - 1`. +3. `_on_track_end()` is called, which computes `next_pos = (pos-1) + 1 = pos`, + which points to the track that was previously next. + +This correctly advances to the next track without skipping it. + +If the removed track was the last one, `next_pos` will be out of bounds and +playback stops normally. + +--- + +## Loop mode + +In loop mode, `_on_track_end()` wraps around to index 0 when it reaches the +end of the queue. The `!queue` display in loop mode shows up to 4 upcoming +tracks wrapping around the current position, excluding the current track +itself. + +--- + +## dj_channel + +`bot.memory["dj_channel"]` stores the channel where playback was started. +This is used when `_on_track_end()` fires from the timer thread and needs to +know where to send "now playing" messages. It is also used as a fallback in +`cmd_remove()` when `trigger.sender` might differ from the original playback +channel. + +--- + +## Known issues and tradeoffs + +**No actual playback.** The bot announces tracks but does not play audio. +It was designed as a queue coordinator for a separate external player. The +TRACK_DURATION timer is a rough approximation. + +**No duration info.** Without the YouTube Data API, there is no way to know +the actual length of a video. Using a fixed 120s timer means long videos get +cut short and short videos have dead air. + +**Queue position display.** `!queue` shows position 1-indexed from the next +track. The currently playing track is not shown in the queue list, only in +`!np`. -- cgit v1.2.3