# 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`.