ccdeck 3.27.0 → 3.27.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/dist/web/assets/{BrowserWatchModal-Ce0hVjif.js → BrowserWatchModal-dGoIoVqT.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-Cf6oqnB8.js → UsageHistoryModal-zfjtA5Nv.js} +1 -1
- package/dist/web/assets/index-CQSnj_z2.js +297 -0
- package/dist/web/assets/index-D_RU_J-E.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/server/account-projects.mjs +14 -2
- package/src/server/best-of-nostalgia.mjs +63 -0
- package/src/server/cafe-music-bgm.mjs +63 -0
- package/src/server/good-life-radio.mjs +63 -0
- package/src/server/index.mjs +60 -0
- package/src/server/live-radio-mix.mjs +56 -0
- package/src/server/lofi-girl.mjs +76 -0
- package/dist/web/assets/index-B5RadDLY.css +0 -1
- package/dist/web/assets/index-BrjE0Aat.js +0 -297
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const NOSTALGIA_URL = "https://www.youtube.com/@bestofnostalgia";
|
|
2
|
+
const CACHE_MS = 600_000;
|
|
3
|
+
const MISS_CACHE_MS = 60_000;
|
|
4
|
+
const READ_LIMIT = 2_000_000;
|
|
5
|
+
const TIMEOUT_MS = 6_000;
|
|
6
|
+
const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
7
|
+
|
|
8
|
+
let cache = null;
|
|
9
|
+
let cacheAt = 0;
|
|
10
|
+
let cacheTtl = 0;
|
|
11
|
+
let inflight = null;
|
|
12
|
+
|
|
13
|
+
function currentLiveVideo(html) {
|
|
14
|
+
const marker = '"liveBadgeText":"LIVE"';
|
|
15
|
+
const live = html.indexOf(marker);
|
|
16
|
+
if (live < 0) return null;
|
|
17
|
+
const start = Math.max(0, live - 2_000);
|
|
18
|
+
const window = html.slice(start, live + 2_000);
|
|
19
|
+
let best = null;
|
|
20
|
+
for (const match of window.matchAll(/"videoId":"([\w-]{11})"/g)) {
|
|
21
|
+
const distance = Math.abs(start + match.index - live);
|
|
22
|
+
if (!best || distance < best.distance) best = { id: match[1], distance };
|
|
23
|
+
}
|
|
24
|
+
return best?.id ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function fetchBestOfNostalgia({ fetchImpl } = {}) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (cache && now - cacheAt < cacheTtl) return cache;
|
|
30
|
+
if (inflight) return inflight;
|
|
31
|
+
const get = fetchImpl ?? fetch;
|
|
32
|
+
inflight = (async () => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await get(NOSTALGIA_URL, {
|
|
35
|
+
headers: { "User-Agent": BROWSER_UA, "Accept-Language": "en-US,en;q=0.9" },
|
|
36
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
37
|
+
redirect: "follow",
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) return settle(null);
|
|
40
|
+
const video = currentLiveVideo((await response.text()).slice(0, READ_LIMIT));
|
|
41
|
+
return settle(video ? { id: "best-of-nostalgia", label: "Best of Nostalgia — Live", video } : null);
|
|
42
|
+
} catch {
|
|
43
|
+
return settle(null);
|
|
44
|
+
} finally {
|
|
45
|
+
inflight = null;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
return inflight;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function settle(value) {
|
|
52
|
+
cache = value;
|
|
53
|
+
cacheAt = Date.now();
|
|
54
|
+
cacheTtl = value ? CACHE_MS : MISS_CACHE_MS;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function forgetBestOfNostalgia() {
|
|
59
|
+
cache = null;
|
|
60
|
+
cacheAt = 0;
|
|
61
|
+
cacheTtl = 0;
|
|
62
|
+
inflight = null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const CAFE_MUSIC_URL = "https://www.youtube.com/channel/UCJhjE7wbdYAae1G25m0tHAA";
|
|
2
|
+
const CACHE_MS = 600_000;
|
|
3
|
+
const MISS_CACHE_MS = 60_000;
|
|
4
|
+
const READ_LIMIT = 2_000_000;
|
|
5
|
+
const TIMEOUT_MS = 6_000;
|
|
6
|
+
const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
7
|
+
|
|
8
|
+
let cache = null;
|
|
9
|
+
let cacheAt = 0;
|
|
10
|
+
let cacheTtl = 0;
|
|
11
|
+
let inflight = null;
|
|
12
|
+
|
|
13
|
+
function currentLiveVideo(html) {
|
|
14
|
+
const marker = '"liveBadgeText":"LIVE"';
|
|
15
|
+
const live = html.indexOf(marker);
|
|
16
|
+
if (live < 0) return null;
|
|
17
|
+
const start = Math.max(0, live - 2_000);
|
|
18
|
+
const window = html.slice(start, live + 2_000);
|
|
19
|
+
let best = null;
|
|
20
|
+
for (const match of window.matchAll(/"videoId":"([\w-]{11})"/g)) {
|
|
21
|
+
const distance = Math.abs(start + match.index - live);
|
|
22
|
+
if (!best || distance < best.distance) best = { id: match[1], distance };
|
|
23
|
+
}
|
|
24
|
+
return best?.id ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function fetchCafeMusicBgm({ fetchImpl } = {}) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (cache && now - cacheAt < cacheTtl) return cache;
|
|
30
|
+
if (inflight) return inflight;
|
|
31
|
+
const get = fetchImpl ?? fetch;
|
|
32
|
+
inflight = (async () => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await get(CAFE_MUSIC_URL, {
|
|
35
|
+
headers: { "User-Agent": BROWSER_UA, "Accept-Language": "en-US,en;q=0.9" },
|
|
36
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
37
|
+
redirect: "follow",
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) return settle(null);
|
|
40
|
+
const video = currentLiveVideo((await response.text()).slice(0, READ_LIMIT));
|
|
41
|
+
return settle(video ? { id: "cafe-music-bgm", label: "Cafe Music BGM — Live", video } : null);
|
|
42
|
+
} catch {
|
|
43
|
+
return settle(null);
|
|
44
|
+
} finally {
|
|
45
|
+
inflight = null;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
return inflight;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function settle(value) {
|
|
52
|
+
cache = value;
|
|
53
|
+
cacheAt = Date.now();
|
|
54
|
+
cacheTtl = value ? CACHE_MS : MISS_CACHE_MS;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function forgetCafeMusicBgm() {
|
|
59
|
+
cache = null;
|
|
60
|
+
cacheAt = 0;
|
|
61
|
+
cacheTtl = 0;
|
|
62
|
+
inflight = null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const GOOD_LIFE_URL = "https://www.youtube.com/@TheGoodLiferadio";
|
|
2
|
+
const CACHE_MS = 600_000;
|
|
3
|
+
const MISS_CACHE_MS = 60_000;
|
|
4
|
+
const READ_LIMIT = 2_000_000;
|
|
5
|
+
const TIMEOUT_MS = 6_000;
|
|
6
|
+
const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
7
|
+
|
|
8
|
+
let cache = null;
|
|
9
|
+
let cacheAt = 0;
|
|
10
|
+
let cacheTtl = 0;
|
|
11
|
+
let inflight = null;
|
|
12
|
+
|
|
13
|
+
function currentLiveVideo(html) {
|
|
14
|
+
const marker = '"liveBadgeText":"LIVE"';
|
|
15
|
+
const live = html.indexOf(marker);
|
|
16
|
+
if (live < 0) return null;
|
|
17
|
+
const start = Math.max(0, live - 2_000);
|
|
18
|
+
const window = html.slice(start, live + 2_000);
|
|
19
|
+
let best = null;
|
|
20
|
+
for (const match of window.matchAll(/"videoId":"([\w-]{11})"/g)) {
|
|
21
|
+
const distance = Math.abs(start + match.index - live);
|
|
22
|
+
if (!best || distance < best.distance) best = { id: match[1], distance };
|
|
23
|
+
}
|
|
24
|
+
return best?.id ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function fetchGoodLifeRadio({ fetchImpl } = {}) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (cache && now - cacheAt < cacheTtl) return cache;
|
|
30
|
+
if (inflight) return inflight;
|
|
31
|
+
const get = fetchImpl ?? fetch;
|
|
32
|
+
inflight = (async () => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await get(GOOD_LIFE_URL, {
|
|
35
|
+
headers: { "User-Agent": BROWSER_UA, "Accept-Language": "en-US,en;q=0.9" },
|
|
36
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
37
|
+
redirect: "follow",
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) return settle(null);
|
|
40
|
+
const video = currentLiveVideo((await response.text()).slice(0, READ_LIMIT));
|
|
41
|
+
return settle(video ? { id: "good-life-radio", label: "The Good Life Radio — Live", video } : null);
|
|
42
|
+
} catch {
|
|
43
|
+
return settle(null);
|
|
44
|
+
} finally {
|
|
45
|
+
inflight = null;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
return inflight;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function settle(value) {
|
|
52
|
+
cache = value;
|
|
53
|
+
cacheAt = Date.now();
|
|
54
|
+
cacheTtl = value ? CACHE_MS : MISS_CACHE_MS;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function forgetGoodLifeRadio() {
|
|
59
|
+
cache = null;
|
|
60
|
+
cacheAt = 0;
|
|
61
|
+
cacheTtl = 0;
|
|
62
|
+
inflight = null;
|
|
63
|
+
}
|
package/src/server/index.mjs
CHANGED
|
@@ -309,6 +309,39 @@ export function payloadChars(raw) {
|
|
|
309
309
|
return n;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
async function handleLiveRadioMix(req, res) {
|
|
313
|
+
if (process.env.AGENTS_DECK_NO_MUSIC === "1") {
|
|
314
|
+
return send(res, 200, { ok: true, live: false, off: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const { fetchLiveRadioMix } = await import(
|
|
318
|
+
pathToFileURL(join(PKG_ROOT, "src/server/live-radio-mix.mjs")).href
|
|
319
|
+
);
|
|
320
|
+
const answer = await fetchLiveRadioMix();
|
|
321
|
+
send(res, answer ? 200 : 404, answer ?? { ok: false, error: "Radio Mix is not live" });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function handleBestOfNostalgia(req, res) {
|
|
325
|
+
if (process.env.AGENTS_DECK_NO_MUSIC === "1") return send(res, 200, { ok: true, live: false, off: true });
|
|
326
|
+
const { fetchBestOfNostalgia } = await import(pathToFileURL(join(PKG_ROOT, "src/server/best-of-nostalgia.mjs")).href);
|
|
327
|
+
const answer = await fetchBestOfNostalgia();
|
|
328
|
+
send(res, answer ? 200 : 404, answer ?? { ok: false, error: "Best of Nostalgia is not live" });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function handleGoodLifeRadio(req, res) {
|
|
332
|
+
if (process.env.AGENTS_DECK_NO_MUSIC === "1") return send(res, 200, { ok: true, live: false, off: true });
|
|
333
|
+
const { fetchGoodLifeRadio } = await import(pathToFileURL(join(PKG_ROOT, "src/server/good-life-radio.mjs")).href);
|
|
334
|
+
const answer = await fetchGoodLifeRadio();
|
|
335
|
+
send(res, answer ? 200 : 404, answer ?? { ok: false, error: "The Good Life Radio is not live" });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function handleCafeMusicBgm(req, res) {
|
|
339
|
+
if (process.env.AGENTS_DECK_NO_MUSIC === "1") return send(res, 200, { ok: true, live: false, off: true });
|
|
340
|
+
const { fetchCafeMusicBgm } = await import(pathToFileURL(join(PKG_ROOT, "src/server/cafe-music-bgm.mjs")).href);
|
|
341
|
+
const answer = await fetchCafeMusicBgm();
|
|
342
|
+
send(res, answer ? 200 : 404, answer ?? { ok: false, error: "Cafe Music BGM is not live" });
|
|
343
|
+
}
|
|
344
|
+
|
|
312
345
|
// Where the charge rides. A Symbol key rather than an ordinary field, because
|
|
313
346
|
// the envelope is JSON.stringify'd on the hot path into both the SSE frame and
|
|
314
347
|
// the events.jsonl line, and JSON.stringify ignores symbol-keyed properties
|
|
@@ -5828,6 +5861,23 @@ async function handleClaudeFm(req, res) {
|
|
|
5828
5861
|
send(res, 200, answer);
|
|
5829
5862
|
}
|
|
5830
5863
|
|
|
5864
|
+
async function handleLofiGirl(req, res) {
|
|
5865
|
+
if (process.env.AGENTS_DECK_NO_MUSIC === "1") {
|
|
5866
|
+
return send(res, 200, { ok: true, live: false, off: true });
|
|
5867
|
+
}
|
|
5868
|
+
const url = new URL(req.url, "http://localhost");
|
|
5869
|
+
const station = url.searchParams.get("station");
|
|
5870
|
+
if (!["relax", "game", "vibe", "sleep"].includes(station)) {
|
|
5871
|
+
return send(res, 400, { ok: false, error: "unknown Lofi Girl station" });
|
|
5872
|
+
}
|
|
5873
|
+
const { fetchLofiStations } = await import(
|
|
5874
|
+
pathToFileURL(join(PKG_ROOT, "src/server/lofi-girl.mjs")).href
|
|
5875
|
+
);
|
|
5876
|
+
const stations = await fetchLofiStations();
|
|
5877
|
+
const answer = stations[station];
|
|
5878
|
+
send(res, answer ? 200 : 404, answer ?? { ok: false, error: "station is not live" });
|
|
5879
|
+
}
|
|
5880
|
+
|
|
5831
5881
|
async function handleCodexQuota(req, res) {
|
|
5832
5882
|
const { fetchCodexQuota } = await import(
|
|
5833
5883
|
pathToFileURL(join(PKG_ROOT, "src/server/codex-quota.mjs")).href
|
|
@@ -6215,6 +6265,11 @@ const PINNED_MODULES = [
|
|
|
6215
6265
|
"browser-watch.mjs",
|
|
6216
6266
|
"browser-watch-store.mjs",
|
|
6217
6267
|
"claude-fm.mjs",
|
|
6268
|
+
"lofi-girl.mjs",
|
|
6269
|
+
"live-radio-mix.mjs",
|
|
6270
|
+
"best-of-nostalgia.mjs",
|
|
6271
|
+
"good-life-radio.mjs",
|
|
6272
|
+
"cafe-music-bgm.mjs",
|
|
6218
6273
|
// system-metrics.mjs's two, on the platforms that have them, and the one
|
|
6219
6274
|
// installer.mjs reaches for while it rewrites the hooks.
|
|
6220
6275
|
"macmon.mjs",
|
|
@@ -7539,6 +7594,11 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
7539
7594
|
if (req.method === "GET" && url.pathname === "/api/cswap-auto") return guard(handleCswapAuto(req, res), res);
|
|
7540
7595
|
if (req.method === "POST" && url.pathname === "/api/cswap-auto") return guard(handleCswapAutoAction(req, res), res);
|
|
7541
7596
|
if (req.method === "GET" && url.pathname === "/api/claude-fm") return guard(handleClaudeFm(req, res), res);
|
|
7597
|
+
if (req.method === "GET" && url.pathname === "/api/lofi-girl") return guard(handleLofiGirl(req, res), res);
|
|
7598
|
+
if (req.method === "GET" && url.pathname === "/api/live-radio-mix") return guard(handleLiveRadioMix(req, res), res);
|
|
7599
|
+
if (req.method === "GET" && url.pathname === "/api/best-of-nostalgia") return guard(handleBestOfNostalgia(req, res), res);
|
|
7600
|
+
if (req.method === "GET" && url.pathname === "/api/good-life-radio") return guard(handleGoodLifeRadio(req, res), res);
|
|
7601
|
+
if (req.method === "GET" && url.pathname === "/api/cafe-music-bgm") return guard(handleCafeMusicBgm(req, res), res);
|
|
7542
7602
|
|
|
7543
7603
|
// Through writeJsonArray rather than `send`, and through `guard` like every
|
|
7544
7604
|
// route above it: this is the one answer whose size is the ring's size, and
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const RADIO_MIX_URL = "https://www.youtube.com/@liveradiomix";
|
|
2
|
+
const CACHE_MS = 600_000;
|
|
3
|
+
const MISS_CACHE_MS = 60_000;
|
|
4
|
+
const READ_LIMIT = 2_000_000;
|
|
5
|
+
const TIMEOUT_MS = 6_000;
|
|
6
|
+
const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
7
|
+
|
|
8
|
+
let cache = null;
|
|
9
|
+
let cacheAt = 0;
|
|
10
|
+
let cacheTtl = 0;
|
|
11
|
+
let inflight = null;
|
|
12
|
+
|
|
13
|
+
function currentLiveVideo(html) {
|
|
14
|
+
const live = html.indexOf('"liveBadgeText":"LIVE"');
|
|
15
|
+
if (live < 0) return null;
|
|
16
|
+
const window = html.slice(Math.max(0, live - 2_000), live + 2_000);
|
|
17
|
+
return window.match(/"videoId":"([\w-]{11})"/)?.[1] ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function fetchLiveRadioMix({ fetchImpl } = {}) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (cache && now - cacheAt < cacheTtl) return cache;
|
|
23
|
+
if (inflight) return inflight;
|
|
24
|
+
const get = fetchImpl ?? fetch;
|
|
25
|
+
inflight = (async () => {
|
|
26
|
+
try {
|
|
27
|
+
const response = await get(RADIO_MIX_URL, {
|
|
28
|
+
headers: { "User-Agent": BROWSER_UA, "Accept-Language": "en-US,en;q=0.9" },
|
|
29
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
30
|
+
redirect: "follow",
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) return settle(null);
|
|
33
|
+
const video = currentLiveVideo((await response.text()).slice(0, READ_LIMIT));
|
|
34
|
+
return settle(video ? { id: "radio-mix", label: "Radio Mix — Live", video } : null);
|
|
35
|
+
} catch {
|
|
36
|
+
return settle(null);
|
|
37
|
+
} finally {
|
|
38
|
+
inflight = null;
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
return inflight;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function settle(value) {
|
|
45
|
+
cache = value;
|
|
46
|
+
cacheAt = Date.now();
|
|
47
|
+
cacheTtl = value ? CACHE_MS : MISS_CACHE_MS;
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function forgetLiveRadioMix() {
|
|
52
|
+
cache = null;
|
|
53
|
+
cacheAt = 0;
|
|
54
|
+
cacheTtl = 0;
|
|
55
|
+
inflight = null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const LOFI_GIRL_URL = "https://www.youtube.com/@LofiGirl";
|
|
2
|
+
const CACHE_MS = 600_000;
|
|
3
|
+
const MISS_CACHE_MS = 60_000;
|
|
4
|
+
const READ_LIMIT = 2_000_000;
|
|
5
|
+
const TIMEOUT_MS = 6_000;
|
|
6
|
+
const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
7
|
+
|
|
8
|
+
export const LOFI_STATIONS = {
|
|
9
|
+
relax: { label: "Lofi Girl — relax/study" , terms: ["relax/study"] },
|
|
10
|
+
game: { label: "Lofi Girl — chill/game", terms: ["chill/game"] },
|
|
11
|
+
vibe: { label: "Lofi Girl — vibe/chill", terms: ["vibe/chill"] },
|
|
12
|
+
sleep: { label: "Lofi Girl — sleep/chill", terms: ["sleep/chill"] },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let cache = null;
|
|
16
|
+
let cacheAt = 0;
|
|
17
|
+
let cacheTtl = 0;
|
|
18
|
+
let inflight = null;
|
|
19
|
+
|
|
20
|
+
function stationVideo(html, terms) {
|
|
21
|
+
for (const term of terms) {
|
|
22
|
+
const index = html.toLowerCase().indexOf(term);
|
|
23
|
+
if (index < 0) continue;
|
|
24
|
+
const start = Math.max(0, index - 4_000);
|
|
25
|
+
const window = html.slice(start, index + 4_000);
|
|
26
|
+
const ids = [...window.matchAll(/"videoId":"([\w-]{11})"/g)];
|
|
27
|
+
if (ids.length) {
|
|
28
|
+
ids.sort((a, b) => Math.abs(start + a.index - index) - Math.abs(start + b.index - index));
|
|
29
|
+
return ids[0][1];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function fetchLofiStations({ fetchImpl } = {}) {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
if (cache && now - cacheAt < cacheTtl) return cache;
|
|
38
|
+
if (inflight) return inflight;
|
|
39
|
+
const get = fetchImpl ?? fetch;
|
|
40
|
+
inflight = (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const response = await get(LOFI_GIRL_URL, {
|
|
43
|
+
headers: { "User-Agent": BROWSER_UA, "Accept-Language": "en-US,en;q=0.9" },
|
|
44
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
45
|
+
redirect: "follow",
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) return settle({});
|
|
48
|
+
const html = (await response.text()).slice(0, READ_LIMIT);
|
|
49
|
+
const stations = Object.fromEntries(
|
|
50
|
+
Object.entries(LOFI_STATIONS)
|
|
51
|
+
.map(([id, station]) => [id, { id, label: station.label, video: stationVideo(html, station.terms) }])
|
|
52
|
+
.filter(([, station]) => station.video),
|
|
53
|
+
);
|
|
54
|
+
return settle(stations);
|
|
55
|
+
} catch {
|
|
56
|
+
return settle({});
|
|
57
|
+
} finally {
|
|
58
|
+
inflight = null;
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
return inflight;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function settle(value) {
|
|
65
|
+
cache = value;
|
|
66
|
+
cacheAt = Date.now();
|
|
67
|
+
cacheTtl = Object.keys(value).length ? CACHE_MS : MISS_CACHE_MS;
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function forgetLofiStations() {
|
|
72
|
+
cache = null;
|
|
73
|
+
cacheAt = 0;
|
|
74
|
+
cacheTtl = 0;
|
|
75
|
+
inflight = null;
|
|
76
|
+
}
|