aboutsummaryrefslogtreecommitdiffstats
path: root/dev-docs/daft.md
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
committerAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
commit5a8d568931d9b23ce0df1265d05259a7012081c9 (patch)
tree4ea19b8bb1763a38246f342f172c285f6579e09b /dev-docs/daft.md
init: mostly vibed
Diffstat (limited to 'dev-docs/daft.md')
-rw-r--r--dev-docs/daft.md132
1 files changed, 132 insertions, 0 deletions
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/<id>`. 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`.