aboutsummaryrefslogtreecommitdiffstats
path: root/dev-docs/daft.md
blob: 880b95cc7d963d32e9d7a9815c2aa8768c8cafce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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`.