kikoeru 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ name: Node.js CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ node-version: [22.x, 24.x]
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - name: Use Node.js ${{ matrix.node-version }}
18
+ uses: actions/setup-node@v7
19
+ with:
20
+ node-version: ${{ matrix.node-version }}
21
+ - name: Install dependencies
22
+ run: npm install
23
+ - name: Run tests
24
+ run: npm test
25
+ publish:
26
+ if: github.ref == 'refs/heads/main'
27
+ needs: test
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ contents: read
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ - name: Use Node.js 24.x
34
+ uses: actions/setup-node@v7
35
+ with:
36
+ node-version: 24.x
37
+ registry-url: https://registry.npmjs.org
38
+ - name: Install dependencies
39
+ run: npm install
40
+ - name: Publish to npm
41
+ env:
42
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
43
+ run: npm publish
package/README.md CHANGED
@@ -1,3 +1,293 @@
1
- # Temporary Holding Version
1
+ [![npm version](https://badge.fury.io/js/kikoeru.svg)](https://badge.fury.io/js/kikoeru)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # kikoeru
4
+
5
+ A small server that relays YouTube audio. It fetches the sound in
6
+ small pieces and passes the pieces to the listener right away, so
7
+ playback starts fast.
8
+
9
+ ## Why this exists
10
+
11
+ YouTube gives each video a special web address for its sound. Tools
12
+ like `yt-dlp` can find this address. The address works, but it has a
13
+ problem:
14
+
15
+ - If a player asks for the sound in the normal way (from the start,
16
+ in order), YouTube sends the data very slowly — about as fast as
17
+ the sound plays (about 31 KB per second in our tests). A long
18
+ video can take 40 seconds before the first sound comes out.
19
+ - If a player asks for a part of the file (a "range request"), YouTube
20
+ sends the data fast (about 10 MB per second in our tests). But if
21
+ the part is too big, YouTube slows down again.
22
+ - Asking for small parts, one after another, stays fast to the end.
23
+ In our tests, a 16.6 MB file arrived in about 1.7 seconds this way.
24
+
25
+ A phone browser cannot be told to fetch in small parts. So kikoeru sits
26
+ in the middle: the phone asks kikoeru, and kikoeru asks YouTube in small
27
+ parts.
28
+
29
+ ## What kikoeru does
30
+
31
+ - It listens for requests of one form: `GET /<token>/<videoId>`.
32
+ - It looks up the video's sound address in a cache file (see below).
33
+ - It asks YouTube for the file in small parts of at most 4 MiB
34
+ (4,194,304 bytes) and sends each part to the listener as soon as
35
+ it arrives.
36
+ - It saves nothing. No sound file is ever written to disk. kikoeru is a
37
+ relay, not a file host.
38
+
39
+ ## What kikoeru does not do
40
+
41
+ Finding the sound address is **not** kikoeru's job. A shell pipeline
42
+ (`yt-audio` and `yt-audio-batch`, which run `yt-dlp`) finds the
43
+ addresses and writes them to a cache file:
44
+
45
+ ```text
46
+ ~/.yt-audio/cache.json
47
+ ```
48
+
49
+ kikoeru only reads this file. For each video, the file holds the sound
50
+ address and the time it was found:
51
+
52
+ ```json
53
+ {
54
+ "htzMcSM6Ft0": {
55
+ "url": "https://...googlevideo.com/...",
56
+ "ts": 1763200000
57
+ }
58
+ }
59
+ ```
60
+
61
+ The rules are simple:
62
+
63
+ - Entry exists and is less than 5 hours old → kikoeru relays the sound.
64
+ - Entry is missing or older than 5 hours → kikoeru answers 404.
65
+ (The shell pipeline will find a fresh address on its next run.)
66
+
67
+ Sound addresses stop working after some hours. If YouTube answers
68
+ 403 ("forbidden") while kikoeru is working, kikoeru reads the cache file
69
+ one more time — the shell pipeline may have put a fresh address in
70
+ it — and tries once more. If the address still does not work, kikoeru
71
+ answers 404.
72
+
73
+ If YouTube fails while the sound is being sent, kikoeru never leaves
74
+ the listener waiting. When no sound has been sent yet, kikoeru answers
75
+ 502 ("bad gateway") at once. When some sound has already been sent,
76
+ kikoeru ends the connection at once, so the player can show an error
77
+ instead of waiting for ever.
78
+
79
+ Built for the [reader](https://github.com/freddiefujiwara/reader)
80
+ web RSS reader, which plays YouTube videos like podcasts.
81
+
82
+ ## Requirements
83
+
84
+ - Node.js 18 or later
85
+ - No extra packages. kikoeru uses only the Node.js standard library.
86
+
87
+ ## Installation
88
+
89
+ From npm:
90
+
91
+ ```bash
92
+ npm i -g kikoeru
93
+ ```
94
+
95
+ Or from a tarball built from this repository:
96
+
97
+ ```bash
98
+ npm pack # makes kikoeru-0.1.2.tgz
99
+ npm i -g ./kikoeru-0.1.2.tgz
100
+ ```
101
+
102
+ With mise's Node on our home server, the global install goes to the
103
+ Node directory itself, so the module lands in
104
+ `~/.local/share/mise/installs/node/<version>/lib/node_modules/kikoeru`
105
+ and the `kikoeru` command in the `bin/` folder next to it.
106
+
107
+ ## Usage
108
+
109
+ ```bash
110
+ kikoeru
111
+ ```
112
+
113
+ This starts the server on `127.0.0.1:8791`. It only listens on the
114
+ local machine. To let others reach it, put a reverse proxy in front
115
+ of it (see "Reverse proxy" below).
116
+
117
+ ### The request form
118
+
119
+ ```text
120
+ GET /<token>/<videoId>
121
+ ```
122
+
123
+ - `token` — a shared secret word. It is part of the address, like a
124
+ password in the link. A wrong token gets a 404 answer.
125
+ - `videoId` — the 11-character YouTube video id (letters, numbers,
126
+ `-` and `_`). Anything else gets a 404 answer.
127
+
128
+ Every other address also gets a 404 answer: the top page `/`,
129
+ addresses with extra parts, and so on. Only GET and HEAD requests
130
+ are answered.
131
+
132
+ ### Ranges
133
+
134
+ Players may ask for one part of the file with a `Range` header:
135
+
136
+ ```text
137
+ Range: bytes=1000000-1999999
138
+ Range: bytes=1000000-
139
+ Range: bytes=-1000
140
+ ```
141
+
142
+ kikoeru answers `206` with exactly those bytes and a `Content-Range`
143
+ header. The parts are still fetched from YouTube in pieces of at
144
+ most 4 MiB. If the part cannot be given (for example, it starts
145
+ after the end of the file), kikoeru answers `416`. A `Range` header
146
+ that kikoeru cannot read is ignored, and the whole file is sent.
147
+ `If-Range` is ignored.
148
+
149
+ ## The token
150
+
151
+ The token lives in a file:
152
+
153
+ ```text
154
+ ~/.config/kikoeru/token
155
+ ```
156
+
157
+ - If the file exists, kikoeru reads the token from it.
158
+ - If the file does not exist, kikoeru makes a random token (32 hex
159
+ digits) on the first start, saves it to the file with permission
160
+ `600` (only the owner can read it), and uses it from then on.
161
+ - You can also set the token with the `KIKOERU_TOKEN` environment
162
+ variable (see below). Then the file is not used.
163
+
164
+ Anyone who knows the token and a video id can listen through your
165
+ server, so keep the token secret.
166
+
167
+ ## Configuration
168
+
169
+ Every setting has a default. Each one can be changed with an
170
+ environment variable.
171
+
172
+ | Variable | Default | Meaning |
173
+ | --- | --- | --- |
174
+ | `KIKOERU_HOST` | `127.0.0.1` | Address to listen on |
175
+ | `KIKOERU_PORT` | `8791` | Port to listen on |
176
+ | `KIKOERU_TOKEN` | (contents of the token file) | The shared secret |
177
+ | `KIKOERU_TOKEN_FILE` | `~/.config/kikoeru/token` | Where the token is kept |
178
+ | `YT_AUDIO_CACHE` | `~/.yt-audio/cache.json` | The shell pipeline's cache file |
179
+
180
+ ## Example
181
+
182
+ Start the server, read the token, and ask for a video:
183
+
184
+ ```bash
185
+ kikoeru &
186
+ TOKEN=$(cat ~/.config/kikoeru/token)
187
+ curl -o sound.m4a "http://127.0.0.1:8791/$TOKEN/htzMcSM6Ft0"
188
+ ```
189
+
190
+ Ask for only the first 100 bytes:
191
+
192
+ ```bash
193
+ curl -H 'Range: bytes=0-99' "http://127.0.0.1:8791/$TOKEN/htzMcSM6Ft0"
194
+ # -> 206, Content-Range: bytes 0-99/<full size>, 100 bytes of sound
195
+ ```
196
+
197
+ A video that is not in the cache, or a wrong token:
198
+
199
+ ```bash
200
+ curl -i "http://127.0.0.1:8791/wrong-token/htzMcSM6Ft0"
201
+ # -> 404 Not Found
202
+ ```
203
+
204
+ ## Run it as a service (systemd)
205
+
206
+ On our home server, kikoeru runs as a systemd user service. The unit
207
+ file is `~/.config/systemd/user/kikoeru.service`:
208
+
209
+ ```ini
210
+ [Unit]
211
+ Description=kikoeru (YouTube audio streaming relay)
212
+ After=network-online.target
213
+ Wants=network-online.target
214
+
215
+ [Service]
216
+ ExecStart=/home/fumikazu/.local/share/mise/installs/node/24.14.1/bin/node /home/fumikazu/.local/share/mise/installs/node/24.14.1/lib/node_modules/kikoeru/bin/kikoeru.js
217
+ Restart=on-failure
218
+ RestartSec=3
219
+
220
+ [Install]
221
+ WantedBy=default.target
222
+ ```
223
+
224
+ Change the Node version in the paths when Node is upgraded. Then:
225
+
226
+ ```bash
227
+ systemctl --user daemon-reload
228
+ systemctl --user enable --now kikoeru
229
+ ```
230
+
231
+ If user lingering is on (`loginctl enable-linger`), the service
232
+ starts at boot and keeps running after logout.
233
+
234
+ ## Reverse proxy
235
+
236
+ kikoeru only listens on the local machine, so we publish it with nginx
237
+ on its own host name. In this example the host is `y.ze.gs` and the
238
+ whole site is passed to kikoeru:
239
+
240
+ ```nginx
241
+ server {
242
+ listen 443 ssl;
243
+ server_name y.ze.gs;
244
+
245
+ ssl_certificate /etc/letsencrypt/live/ze.gs/fullchain.pem;
246
+ ssl_certificate_key /etc/letsencrypt/live/ze.gs/privkey.pem;
247
+
248
+ location / {
249
+ proxy_pass http://127.0.0.1:8791;
250
+ proxy_http_version 1.1;
251
+ proxy_set_header Host $host;
252
+ proxy_set_header Range $http_range;
253
+ proxy_set_header If-Range $http_if_range;
254
+ proxy_buffering off;
255
+ proxy_cache off;
256
+ proxy_read_timeout 600s;
257
+ }
258
+ }
259
+ ```
260
+
261
+ Then a listener uses `https://y.ze.gs/<token>/<videoId>`. Notes:
262
+
263
+ - `proxy_buffering off` matters: the sound must flow to the listener
264
+ as it arrives, not after nginx has collected a big part of it.
265
+ - The token is the only lock. There is no login page.
266
+
267
+ ## Development
268
+
269
+ Run the tests:
270
+
271
+ ```bash
272
+ npm test
273
+ ```
274
+
275
+ The tests use the Node.js test runner. They start a fake YouTube
276
+ server on the local machine, so no network is needed. They cover:
277
+
278
+ - the request form: right and wrong tokens, video id rules, wrong
279
+ paths, request methods;
280
+ - the cache rules: fresh, old, missing and broken cache files;
281
+ - ranges: all three forms, ends past the file size, parts that
282
+ cannot be given (416);
283
+ - the small-parts fetching: every request to the fake YouTube is
284
+ recorded and checked to be at most 4 MiB, in order, and complete;
285
+ - errors from YouTube: 403 and the one retry with a fresh cache,
286
+ 500 answers, and refused connections;
287
+ - the token file: how it is made, its permission, and reuse after
288
+ a restart;
289
+ - the environment variables for all settings.
290
+
291
+ ## Contributing
292
+
293
+ Bug reports and pull requests are welcome on GitHub at https://github.com/freddiefujiwara/kikoeru
package/bin/kikoeru.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { createServer, loadConfig } = require('../src/server');
5
+
6
+ const cfg = loadConfig();
7
+ const server = createServer(cfg);
8
+ server.listen(cfg.port, cfg.host, () => {
9
+ console.log(
10
+ `[kikoeru] listening on http://${cfg.host}:${cfg.port}/<token>/<videoId>`,
11
+ );
12
+ });
package/package.json CHANGED
@@ -1,6 +1 @@
1
- {
2
- "name": "kikoeru",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {"bin":{"kikoeru":"bin/kikoeru.js"},"description":"Streaming relay for YouTube audio: fetches googlevideo streams with small ranged requests and forwards them to the client without storing any files.","engines":{"node":">=18"},"license":"MIT","main":"src/server.js","name":"kikoeru","scripts":{"start":"node bin/kikoeru.js","test":"node --test \"test/**/*.test.js\""},"version":"0.1.2"}