kikoeru 0.1.1 → 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- [![npm version](https://badge.fury.io/js/koed.svg)](https://badge.fury.io/js/koed)
1
+ [![npm version](https://badge.fury.io/js/kikoeru.svg)](https://badge.fury.io/js/kikoeru)
2
2
 
3
- # koed
3
+ # kikoeru
4
4
 
5
5
  A small server that relays YouTube audio. It fetches the sound in
6
6
  small pieces and passes the pieces to the listener right away, so
@@ -22,23 +22,23 @@ problem:
22
22
  - Asking for small parts, one after another, stays fast to the end.
23
23
  In our tests, a 16.6 MB file arrived in about 1.7 seconds this way.
24
24
 
25
- A phone browser cannot be told to fetch in small parts. So koed sits
26
- in the middle: the phone asks koed, and koed asks YouTube in small
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
27
  parts.
28
28
 
29
- ## What koed does
29
+ ## What kikoeru does
30
30
 
31
31
  - It listens for requests of one form: `GET /<token>/<videoId>`.
32
32
  - It looks up the video's sound address in a cache file (see below).
33
33
  - It asks YouTube for the file in small parts of at most 4 MiB
34
34
  (4,194,304 bytes) and sends each part to the listener as soon as
35
35
  it arrives.
36
- - It saves nothing. No sound file is ever written to disk. koed is a
36
+ - It saves nothing. No sound file is ever written to disk. kikoeru is a
37
37
  relay, not a file host.
38
38
 
39
- ## What koed does not do
39
+ ## What kikoeru does not do
40
40
 
41
- Finding the sound address is **not** koed's job. A shell pipeline
41
+ Finding the sound address is **not** kikoeru's job. A shell pipeline
42
42
  (`yt-audio` and `yt-audio-batch`, which run `yt-dlp`) finds the
43
43
  addresses and writes them to a cache file:
44
44
 
@@ -46,7 +46,7 @@ addresses and writes them to a cache file:
46
46
  ~/.yt-audio/cache.json
47
47
  ```
48
48
 
49
- koed only reads this file. For each video, the file holds the sound
49
+ kikoeru only reads this file. For each video, the file holds the sound
50
50
  address and the time it was found:
51
51
 
52
52
  ```json
@@ -60,20 +60,20 @@ address and the time it was found:
60
60
 
61
61
  The rules are simple:
62
62
 
63
- - Entry exists and is less than 5 hours old → koed relays the sound.
64
- - Entry is missing or older than 5 hours → koed answers 404.
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
65
  (The shell pipeline will find a fresh address on its next run.)
66
66
 
67
67
  Sound addresses stop working after some hours. If YouTube answers
68
- 403 ("forbidden") while koed is working, koed reads the cache file
68
+ 403 ("forbidden") while kikoeru is working, kikoeru reads the cache file
69
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, koed
70
+ it — and tries once more. If the address still does not work, kikoeru
71
71
  answers 404.
72
72
 
73
- If YouTube fails while the sound is being sent, koed never leaves
74
- the listener waiting. When no sound has been sent yet, koed answers
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
75
  502 ("bad gateway") at once. When some sound has already been sent,
76
- koed ends the connection at once, so the player can show an error
76
+ kikoeru ends the connection at once, so the player can show an error
77
77
  instead of waiting for ever.
78
78
 
79
79
  Built for the [reader](https://github.com/freddiefujiwara/reader)
@@ -82,32 +82,32 @@ web RSS reader, which plays YouTube videos like podcasts.
82
82
  ## Requirements
83
83
 
84
84
  - Node.js 18 or later
85
- - No extra packages. koed uses only the Node.js standard library.
85
+ - No extra packages. kikoeru uses only the Node.js standard library.
86
86
 
87
87
  ## Installation
88
88
 
89
89
  From npm:
90
90
 
91
91
  ```bash
92
- npm i -g koed
92
+ npm i -g kikoeru
93
93
  ```
94
94
 
95
95
  Or from a tarball built from this repository:
96
96
 
97
97
  ```bash
98
- npm pack # makes koed-0.1.0.tgz
99
- npm i -g ./koed-0.1.0.tgz
98
+ npm pack # makes kikoeru-0.1.2.tgz
99
+ npm i -g ./kikoeru-0.1.2.tgz
100
100
  ```
101
101
 
102
102
  With mise's Node on our home server, the global install goes to the
103
103
  Node directory itself, so the module lands in
104
- `~/.local/share/mise/installs/node/<version>/lib/node_modules/koed`
105
- and the `koed` command in the `bin/` folder next to it.
104
+ `~/.local/share/mise/installs/node/<version>/lib/node_modules/kikoeru`
105
+ and the `kikoeru` command in the `bin/` folder next to it.
106
106
 
107
107
  ## Usage
108
108
 
109
109
  ```bash
110
- koed
110
+ kikoeru
111
111
  ```
112
112
 
113
113
  This starts the server on `127.0.0.1:8791`. It only listens on the
@@ -139,11 +139,11 @@ Range: bytes=1000000-
139
139
  Range: bytes=-1000
140
140
  ```
141
141
 
142
- koed answers `206` with exactly those bytes and a `Content-Range`
142
+ kikoeru answers `206` with exactly those bytes and a `Content-Range`
143
143
  header. The parts are still fetched from YouTube in pieces of at
144
144
  most 4 MiB. If the part cannot be given (for example, it starts
145
- after the end of the file), koed answers `416`. A `Range` header
146
- that koed cannot read is ignored, and the whole file is sent.
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
147
  `If-Range` is ignored.
148
148
 
149
149
  ## The token
@@ -151,14 +151,14 @@ that koed cannot read is ignored, and the whole file is sent.
151
151
  The token lives in a file:
152
152
 
153
153
  ```text
154
- ~/.config/koed/token
154
+ ~/.config/kikoeru/token
155
155
  ```
156
156
 
157
- - If the file exists, koed reads the token from it.
158
- - If the file does not exist, koed makes a random token (32 hex
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
159
  digits) on the first start, saves it to the file with permission
160
160
  `600` (only the owner can read it), and uses it from then on.
161
- - You can also set the token with the `KOED_TOKEN` environment
161
+ - You can also set the token with the `KIKOERU_TOKEN` environment
162
162
  variable (see below). Then the file is not used.
163
163
 
164
164
  Anyone who knows the token and a video id can listen through your
@@ -171,10 +171,10 @@ environment variable.
171
171
 
172
172
  | Variable | Default | Meaning |
173
173
  | --- | --- | --- |
174
- | `KOED_HOST` | `127.0.0.1` | Address to listen on |
175
- | `KOED_PORT` | `8791` | Port to listen on |
176
- | `KOED_TOKEN` | (contents of the token file) | The shared secret |
177
- | `KOED_TOKEN_FILE` | `~/.config/koed/token` | Where the token is kept |
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
178
  | `YT_AUDIO_CACHE` | `~/.yt-audio/cache.json` | The shell pipeline's cache file |
179
179
 
180
180
  ## Example
@@ -182,8 +182,8 @@ environment variable.
182
182
  Start the server, read the token, and ask for a video:
183
183
 
184
184
  ```bash
185
- koed &
186
- TOKEN=$(cat ~/.config/koed/token)
185
+ kikoeru &
186
+ TOKEN=$(cat ~/.config/kikoeru/token)
187
187
  curl -o sound.m4a "http://127.0.0.1:8791/$TOKEN/htzMcSM6Ft0"
188
188
  ```
189
189
 
@@ -203,17 +203,17 @@ curl -i "http://127.0.0.1:8791/wrong-token/htzMcSM6Ft0"
203
203
 
204
204
  ## Run it as a service (systemd)
205
205
 
206
- On our home server, koed runs as a systemd user service. The unit
207
- file is `~/.config/systemd/user/koed.service`:
206
+ On our home server, kikoeru runs as a systemd user service. The unit
207
+ file is `~/.config/systemd/user/kikoeru.service`:
208
208
 
209
209
  ```ini
210
210
  [Unit]
211
- Description=koed (YouTube audio streaming relay)
211
+ Description=kikoeru (YouTube audio streaming relay)
212
212
  After=network-online.target
213
213
  Wants=network-online.target
214
214
 
215
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/koed/bin/koed.js
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
217
  Restart=on-failure
218
218
  RestartSec=3
219
219
 
@@ -225,7 +225,7 @@ Change the Node version in the paths when Node is upgraded. Then:
225
225
 
226
226
  ```bash
227
227
  systemctl --user daemon-reload
228
- systemctl --user enable --now koed
228
+ systemctl --user enable --now kikoeru
229
229
  ```
230
230
 
231
231
  If user lingering is on (`loginctl enable-linger`), the service
@@ -233,9 +233,9 @@ starts at boot and keeps running after logout.
233
233
 
234
234
  ## Reverse proxy
235
235
 
236
- koed only listens on the local machine, so we publish it with nginx
236
+ kikoeru only listens on the local machine, so we publish it with nginx
237
237
  on its own host name. In this example the host is `y.ze.gs` and the
238
- whole site is passed to koed:
238
+ whole site is passed to kikoeru:
239
239
 
240
240
  ```nginx
241
241
  server {
@@ -290,4 +290,4 @@ server on the local machine, so no network is needed. They cover:
290
290
 
291
291
  ## Contributing
292
292
 
293
- Bug reports and pull requests are welcome on GitHub at https://github.com/freddiefujiwara/koed
293
+ Bug reports and pull requests are welcome on GitHub at https://github.com/freddiefujiwara/kikoeru
@@ -7,6 +7,6 @@ const cfg = loadConfig();
7
7
  const server = createServer(cfg);
8
8
  server.listen(cfg.port, cfg.host, () => {
9
9
  console.log(
10
- `[koed] listening on http://${cfg.host}:${cfg.port}/<token>/<videoId>`,
10
+ `[kikoeru] listening on http://${cfg.host}:${cfg.port}/<token>/<videoId>`,
11
11
  );
12
12
  });
package/package.json CHANGED
@@ -1 +1 @@
1
- {"bin":{"koed":"bin/koed.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/koed.js","test":"node --test \"test/**/*.test.js\""},"version":"0.1.1"}
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"}
package/src/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * koed: a streaming relay for YouTube audio.
4
+ * kikoeru: a streaming relay for YouTube audio.
5
5
  *
6
6
  * Clients (e.g. a phone browser) that play a googlevideo audio URL directly
7
7
  * are throttled to roughly the playback bitrate when they fetch the file
@@ -44,11 +44,11 @@ const ROUTE_RE = /^\/([^/]+)\/([^/]+)$/;
44
44
  function loadConfig(env = process.env) {
45
45
  const home = os.homedir();
46
46
  return {
47
- host: env.KOED_HOST || '127.0.0.1',
48
- port: Number(env.KOED_PORT || 8791),
49
- token: env.KOED_TOKEN || null,
47
+ host: env.KIKOERU_HOST || '127.0.0.1',
48
+ port: Number(env.KIKOERU_PORT || 8791),
49
+ token: env.KIKOERU_TOKEN || null,
50
50
  tokenFile:
51
- env.KOED_TOKEN_FILE || path.join(home, '.config/koed/token'),
51
+ env.KIKOERU_TOKEN_FILE || path.join(home, '.config/kikoeru/token'),
52
52
  batchCacheFile:
53
53
  env.YT_AUDIO_CACHE || path.join(home, '.yt-audio/cache.json'),
54
54
  };
@@ -212,7 +212,7 @@ async function handle(cfg, state, token, req, res) {
212
212
  try {
213
213
  meta = await getMeta(cfg, state, videoId, retried);
214
214
  } catch (e) {
215
- console.error(`[koed] meta failed for ${videoId}: ${e.message}`);
215
+ console.error(`[kikoeru] meta failed for ${videoId}: ${e.message}`);
216
216
  meta = null;
217
217
  }
218
218
  if (!meta) {
@@ -286,7 +286,7 @@ async function handle(cfg, state, token, req, res) {
286
286
  continue;
287
287
  }
288
288
  if (e.code !== 'UPSTREAM_EXPIRED') {
289
- console.error(`[koed] stream failed for ${videoId}: ${e.message}`);
289
+ console.error(`[kikoeru] stream failed for ${videoId}: ${e.message}`);
290
290
  }
291
291
  if (!tracker.committed && !res.headersSent && !res.writableEnded) {
292
292
  // Nothing has been sent to the client yet, so the failure can
@@ -341,7 +341,7 @@ function createServer(cfg) {
341
341
  const state = { meta: new Map() };
342
342
  const server = http.createServer((req, res) => {
343
343
  handle(cfg, state, token, req, res).catch((e) => {
344
- console.error(`[koed] handler error: ${e.message}`);
344
+ console.error(`[kikoeru] handler error: ${e.message}`);
345
345
  if (!res.headersSent) {
346
346
  res.writeHead(500, { 'Content-Type': 'text/plain' });
347
347
  }
@@ -43,7 +43,7 @@ after(async () => {
43
43
  fs.rmSync(tmp, { recursive: true, force: true });
44
44
  });
45
45
 
46
- // Start a throwaway koed whose batch cache is `cacheFile`, run `fn`
46
+ // Start a throwaway kikoeru whose batch cache is `cacheFile`, run `fn`
47
47
  // against its port, and always close it.
48
48
  async function withServer(cacheFile, fn) {
49
49
  const p = createServer({
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // The whole point of koed: the upstream is only ever asked for small
3
+ // The whole point of kikoeru: the upstream is only ever asked for small
4
4
  // ranged pieces (<= 4 MiB), never for a plain GET and never for one
5
5
  // big range. These tests record every Range header the fake upstream
6
6
  // receives for a > 10 MiB payload and check the pieces exactly: size,
@@ -51,7 +51,7 @@ test('loadConfig uses the documented defaults', () => {
51
51
  assert.equal(cfg.token, null);
52
52
  assert.equal(
53
53
  cfg.tokenFile,
54
- path.join(os.homedir(), '.config/koed/token'),
54
+ path.join(os.homedir(), '.config/kikoeru/token'),
55
55
  );
56
56
  assert.equal(
57
57
  cfg.batchCacheFile,
@@ -61,10 +61,10 @@ test('loadConfig uses the documented defaults', () => {
61
61
 
62
62
  test('loadConfig honours every environment override', () => {
63
63
  const cfg = loadConfig({
64
- KOED_HOST: '0.0.0.0',
65
- KOED_PORT: '9999',
66
- KOED_TOKEN: 'from-env',
67
- KOED_TOKEN_FILE: '/tmp/somewhere/token',
64
+ KIKOERU_HOST: '0.0.0.0',
65
+ KIKOERU_PORT: '9999',
66
+ KIKOERU_TOKEN: 'from-env',
67
+ KIKOERU_TOKEN_FILE: '/tmp/somewhere/token',
68
68
  YT_AUDIO_CACHE: '/tmp/somewhere/cache.json',
69
69
  });
70
70
  assert.equal(cfg.host, '0.0.0.0');
package/test/helpers.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Shared helpers for the koed test suites. This file is not a test
3
+ // Shared helpers for the kikoeru test suites. This file is not a test
4
4
  // itself (it does not match the *.test.js glob); it only builds fake
5
5
  // upstreams, temp dirs and small HTTP clients so every suite can stay
6
6
  // off the real network.
@@ -81,7 +81,7 @@ function makeData(size) {
81
81
  }
82
82
 
83
83
  function tmpDir() {
84
- return fs.mkdtempSync(path.join(os.tmpdir(), 'koed-test-'));
84
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'kikoeru-test-'));
85
85
  }
86
86
 
87
87
  function writeCache(file, obj) {
@@ -34,7 +34,7 @@ let upstream;
34
34
  let upPort;
35
35
  let tmp;
36
36
  let hits; // per-path request counts
37
- let cacheFile; // the batch cache the running koed reads
37
+ let cacheFile; // the batch cache the running kikoeru reads
38
38
  let onFlakyChunk; // hook fired when /flaky gets a data (non-probe) request
39
39
 
40
40
  function serveRange(req, res) {
@@ -121,7 +121,7 @@ after(async () => {
121
121
  fs.rmSync(tmp, { recursive: true, force: true });
122
122
  });
123
123
 
124
- // Start an koed whose cache points VIDEO_ID at `upstreamPath`.
124
+ // Start an kikoeru whose cache points VIDEO_ID at `upstreamPath`.
125
125
  async function withServerAt(upstreamPath, fn) {
126
126
  writeCache(cacheFile, {
127
127
  [VIDEO_ID]: {
@@ -188,14 +188,14 @@ test('403 on the first data chunk retries with the refreshed cache URL', async (
188
188
 
189
189
  test('403 on a data chunk with no cache refresh gives a prompt 502', async () => {
190
190
  // The retry re-reads the same dead URL and fails again. Nothing
191
- // has been sent to the client at that point, so koed must answer
191
+ // has been sent to the client at that point, so kikoeru must answer
192
192
  // promptly with 502 instead of leaving the request hanging.
193
193
  hits = {};
194
194
  onFlakyChunk = () => {};
195
195
  try {
196
196
  await withServerAt('/flaky', async (port) => {
197
197
  const r = await getExpectTimeout(port, `/${TOKEN}/${VIDEO_ID}`);
198
- assert.equal(r.timedOut, false, 'koed must answer, not hang');
198
+ assert.equal(r.timedOut, false, 'kikoeru must answer, not hang');
199
199
  assert.equal(r.status, 502);
200
200
  });
201
201
  } finally {
@@ -215,7 +215,7 @@ test('500 on a data chunk gives a prompt 502', async () => {
215
215
  // is sent to the client, so it is answered with a prompt 502.
216
216
  await withServerAt('/chunk500', async (port) => {
217
217
  const r = await getExpectTimeout(port, `/${TOKEN}/${VIDEO_ID}`);
218
- assert.equal(r.timedOut, false, 'koed must answer, not hang');
218
+ assert.equal(r.timedOut, false, 'kikoeru must answer, not hang');
219
219
  assert.equal(r.status, 502);
220
220
  });
221
221
  });
@@ -259,7 +259,7 @@ test('500 on a later chunk cuts the response short instead of hanging', async ()
259
259
  // The payload is one chunk plus a little more, so the first chunk
260
260
  // is streamed to the client and the failure only happens in
261
261
  // streamRest, after the 200 status and the first bytes are out.
262
- // No error status can be sent then; koed must cut the response so
262
+ // No error status can be sent then; kikoeru must cut the response so
263
263
  // the client sees it end (incomplete) right away, not hang.
264
264
  const bigSize = CHUNK_SIZE + 1024;
265
265
  const big = makeData(bigSize);
@@ -298,7 +298,7 @@ test('500 on a later chunk cuts the response short instead of hanging', async ()
298
298
  const port = await listen(p);
299
299
  try {
300
300
  const r = await getObserveEnd(port, `/${TOKEN}/${VIDEO_ID}`);
301
- assert.equal(r.timedOut, false, 'koed must end the response, not hang');
301
+ assert.equal(r.timedOut, false, 'kikoeru must end the response, not hang');
302
302
  assert.equal(r.status, 200);
303
303
  assert.equal(r.bytes, CHUNK_SIZE, 'only the first chunk was delivered');
304
304
  assert.equal(r.complete, false, 'the body ends incomplete');