unreleased-cli 0.1.3 → 0.1.5
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 +4 -2
- package/dist/unreleased.mjs +324 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,9 +52,9 @@ Commands (type `help <command>` in the shell for the details of each one):
|
|
|
52
52
|
|
|
53
53
|
- **Files:** `cd`, `ls`, `pwd`, `lcd`, `get`, `cat`, `head`, `tail`, `wc`, `grep`, `locate`, `tree`, `du`
|
|
54
54
|
- **Library:** `find`, `song`, `like`, `unlike`, `liked`, `playlists`, `playlist`, `stats`
|
|
55
|
-
- **Player:** `play`, `pause`, `toggle`, `next`, `prev`, `seek`, `volume`, `speed`, `shuffle`, `repeat`, `queue`, `status`, `sleep`, `stop`
|
|
55
|
+
- **Player:** `play`, `pause`, `toggle`, `next`, `prev`, `seek`, `volume`, `speed`, `eq`, `shuffle`, `repeat`, `queue`, `status`, `sleep`, `stop`
|
|
56
56
|
- **People:** `user`, `lookup`
|
|
57
|
-
- **Admin** (administrators only): `pending`, `proposals`, `comps`, `applications`, `inspect`, `approve`, `reject`, `reverse`, `users`, `sitebans`, `siteunban`
|
|
57
|
+
- **Admin** (administrators only): `pending`, `proposals`, `comps`, `applications`, `cdn`, `inspect`, `approve`, `reject`, `reverse`, `users`, `sitebans`, `siteunban`
|
|
58
58
|
- **Fun:** `neofetch`, `fortune`, `juicesay`, `matrix`, `visualizer`, `karaoke`, `wordle`, `heardle`
|
|
59
59
|
- **Settings:** `set`, `settings`, `termtheme`
|
|
60
60
|
- **Shell:** `source`, `alias`, `unalias`, `history`, `echo`, `watch`, `full`, `date`, `clear`, `exit`, `help`
|
|
@@ -119,6 +119,7 @@ These cover the Admin page's review queues and site moderation, using the same e
|
|
|
119
119
|
- `reverse [song|comp] <id>` undoes an approved proposal.
|
|
120
120
|
- `users [role] [filter]` lists accounts. `user <name>` shows one person.
|
|
121
121
|
- `sitebans` lists active site-wide bans, mutes and timeouts. `siteunban <user | #id>` lifts them.
|
|
122
|
+
- `cdn` shows the CDN stats and every node, pending ones first. `cdn <node>` shows one in full, where a node is its id, the start of the id, or its name. `cdn approve|revoke|enable|disable|reset|restore|delete <node>` does what the Admin page's CDN nodes tab does. `restore` is for a node the server pulled for hash violations: it switches it back on and resets its trust and violations. `delete` asks first unless you pass `-y`.
|
|
122
123
|
|
|
123
124
|
`reverse` and `siteunban` ask before they act. Outside the interactive shell, such as in one-shot use or a script, they need `-y` instead.
|
|
124
125
|
|
|
@@ -130,6 +131,7 @@ These cover the Admin page's review queues and site moderation, using the same e
|
|
|
130
131
|
- `matrix` is digital rain. Any key leaves.
|
|
131
132
|
- `visualizer` (or `viz`) is a live spectrum of the song that's playing. Any key leaves. mpv can't hand its audio over, so a second mpv decodes the same stream to a temp file and the bars come from that, which means the song is downloaded a second time while the screen is open.
|
|
132
133
|
- `karaoke` (or `lyrics`) shows the lyrics of the song that's playing. Synced lyrics highlight the line being sung and scroll along; plain ones scroll with the arrow keys and Page Up/Down. Space pauses, `q` leaves. It needs a song from the library (not a file played from the tree).
|
|
134
|
+
- `eq` shows the equalizer settings. `eq list` names the presets (the site's own), `eq rock` turns the equalizer on with one, and `eq off` / `eq reset` switch it off or flatten it. `eq boost 100-200`, `eq balance -100..100`, `eq speed 0.5-2` and `eq pitch [on|off]` (the same as the `speed` command and the `pitch-shift` setting), `eq mono [on|off]` and `eq reverb [on|off|0-100] [decay 1-8]` do the rest. It runs through mpv's audio filters, so the reverb is an echo approximation of the site's. Settings carry over between sessions, and changes apply to the song that's playing.
|
|
133
135
|
- `wordle [daily | unlimited]` is the song-title Wordle, using the site's own puzzle logic, so the daily puzzle is the same one. Type a title and press Enter. Esc leaves, and your progress is saved.
|
|
134
136
|
- `heardle` gives practice rounds: Tab plays the clip, Enter guesses (an empty Enter skips), and ↑↓ picks a suggestion. The clip plays through its own mpv. Music that was playing gets paused, and `play` resumes it.
|
|
135
137
|
|
package/dist/unreleased.mjs
CHANGED
|
@@ -82,7 +82,7 @@ function saveSettings(settings) {
|
|
|
82
82
|
|
|
83
83
|
// src/api.ts
|
|
84
84
|
var DEFAULT_API = "https://juicewrldapi.com/juicewrld";
|
|
85
|
-
var VERSION = "0.1.
|
|
85
|
+
var VERSION = "0.1.5";
|
|
86
86
|
function apiBase() {
|
|
87
87
|
return (process.env.UNRELEASED_API || loadConfig().api || DEFAULT_API).replace(/\/+$/, "");
|
|
88
88
|
}
|
|
@@ -253,12 +253,12 @@ var color = {
|
|
|
253
253
|
errRed: paint("red", process.stderr),
|
|
254
254
|
errDim: paint("dim", process.stderr)
|
|
255
255
|
};
|
|
256
|
-
function writeTone(text,
|
|
257
|
-
if (
|
|
256
|
+
function writeTone(text, tone2) {
|
|
257
|
+
if (tone2 === "error") {
|
|
258
258
|
process.stderr.write(color.errRed(text) + "\n");
|
|
259
259
|
return;
|
|
260
260
|
}
|
|
261
|
-
const painted =
|
|
261
|
+
const painted = tone2 === "ok" ? color.green(text) : tone2 === "dim" ? color.dim(text) : text;
|
|
262
262
|
process.stdout.write(painted + "\n");
|
|
263
263
|
}
|
|
264
264
|
function progressLine() {
|
|
@@ -488,6 +488,68 @@ function installHistorySearch(rl, history, idle, closed = () => void 0, stdin =
|
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
+
// src/effects.ts
|
|
492
|
+
var EQ_BANDS = [32, 64, 125, 250, 500, 1e3, 2e3, 4e3, 8e3, 16e3];
|
|
493
|
+
var EQ_PRESETS = [
|
|
494
|
+
{ id: "flat", name: "Flat", gains: EQ_BANDS.map(() => 0) },
|
|
495
|
+
{ id: "bass-boost", name: "Bass Boost", gains: [6, 5, 4, 2.5, 1, 0, 0, 0, 0, 0] },
|
|
496
|
+
{ id: "bass-reducer", name: "Bass Reducer", gains: [-6, -5, -4, -2.5, -1, 0, 0, 0, 0, 0] },
|
|
497
|
+
{ id: "treble-boost", name: "Treble Boost", gains: [0, 0, 0, 0, 0, 1, 2.5, 4, 5, 6] },
|
|
498
|
+
{ id: "treble-reducer", name: "Treble Reducer", gains: [0, 0, 0, 0, 0, -1, -2.5, -4, -5, -6] },
|
|
499
|
+
{ id: "vocal", name: "Vocal Boost", gains: [-2, -3, -3, 1, 4, 4, 3, 1.5, 0, -1.5] },
|
|
500
|
+
{ id: "rock", name: "Rock", gains: [5, 4, 3, 1.5, -0.5, -1, 0.5, 2.5, 3.5, 4.5] },
|
|
501
|
+
{ id: "pop", name: "Pop", gains: [-1.5, -1, 0, 2, 4, 4, 2, 0, -1, -1.5] },
|
|
502
|
+
{ id: "hip-hop", name: "Hip-Hop", gains: [5, 4, 1.5, 3, -1, -1, 1.5, -0.5, 2, 3] },
|
|
503
|
+
{ id: "electronic", name: "Electronic", gains: [4.5, 4, 1.5, 0, -2, 2, 1, 1.5, 4, 5] },
|
|
504
|
+
{ id: "jazz", name: "Jazz", gains: [4, 3, 1.5, 2, -1.5, -1.5, 0, 1.5, 3, 4] },
|
|
505
|
+
{ id: "classical", name: "Classical", gains: [4.5, 3.5, 3, 2.5, -1.5, -1.5, 0, 2, 3, 4] },
|
|
506
|
+
{ id: "acoustic", name: "Acoustic", gains: [5, 5, 4, 1, 2, 1.5, 3.5, 4, 3.5, 2] },
|
|
507
|
+
{ id: "small-speakers", name: "Small Speakers", gains: [5.5, 4.5, 4, 2.5, 1.5, 0, -1.5, -3, -4, -4.5] }
|
|
508
|
+
];
|
|
509
|
+
var effects = () => ({
|
|
510
|
+
eq: pref("eq", false),
|
|
511
|
+
preset: pref("eq-preset", "flat"),
|
|
512
|
+
boost: pref("eq-boost", 1),
|
|
513
|
+
balance: pref("eq-balance", 0),
|
|
514
|
+
mono: pref("eq-mono", false),
|
|
515
|
+
reverb: pref("reverb", false),
|
|
516
|
+
reverbMix: pref("reverb-mix", 0.3),
|
|
517
|
+
reverbDecay: pref("reverb-decay", 2)
|
|
518
|
+
});
|
|
519
|
+
var KEYS = {
|
|
520
|
+
eq: "eq",
|
|
521
|
+
preset: "eq-preset",
|
|
522
|
+
boost: "eq-boost",
|
|
523
|
+
balance: "eq-balance",
|
|
524
|
+
mono: "eq-mono",
|
|
525
|
+
reverb: "reverb",
|
|
526
|
+
reverbMix: "reverb-mix",
|
|
527
|
+
reverbDecay: "reverb-decay"
|
|
528
|
+
};
|
|
529
|
+
function saveEffect(key, value) {
|
|
530
|
+
setPref(KEYS[key], value);
|
|
531
|
+
}
|
|
532
|
+
var num = (n) => String(Math.round(n * 1e3) / 1e3);
|
|
533
|
+
function filterChain(e = effects()) {
|
|
534
|
+
const f = [];
|
|
535
|
+
const gains = EQ_PRESETS.find((p) => p.id === e.preset)?.gains;
|
|
536
|
+
if (e.eq && gains) {
|
|
537
|
+
gains.forEach((g, i) => {
|
|
538
|
+
if (g) f.push(`equalizer=f=${EQ_BANDS[i]}:width_type=o:width=1:g=${num(g)}`);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (e.mono) f.push("pan=stereo|c0=0.5*c0+0.5*c1|c1=0.5*c0+0.5*c1");
|
|
542
|
+
if (e.balance) f.push(`stereotools=balance_out=${num(e.balance)}`);
|
|
543
|
+
if (e.reverb && e.reverbMix > 0) {
|
|
544
|
+
const delays = [0.04, 0.09, 0.17, 0.3].map((t) => Math.round(Math.min(1, t * e.reverbDecay / 2) * 1e3));
|
|
545
|
+
const decays = [0.6, 0.5, 0.4, 0.3].map((d) => num(d * e.reverbMix * 2 > 0.9 ? 0.9 : d * e.reverbMix * 2));
|
|
546
|
+
f.push(`aecho=in_gain=${num(1 - e.reverbMix * 0.4)}:out_gain=${num(0.6 + e.reverbMix * 0.3)}:delays=${delays.join("|")}:decays=${decays.join("|")}`);
|
|
547
|
+
}
|
|
548
|
+
if (e.boost > 1) f.push(`volume=${num(e.boost)}`);
|
|
549
|
+
if (f.length) f.push("alimiter=limit=0.95");
|
|
550
|
+
return f.length ? `lavfi=[${f.join(",")}]` : "";
|
|
551
|
+
}
|
|
552
|
+
|
|
491
553
|
// src/mpv.ts
|
|
492
554
|
import { spawn } from "node:child_process";
|
|
493
555
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -737,6 +799,8 @@ var Player = class {
|
|
|
737
799
|
await mpv.set("mute", this.muted);
|
|
738
800
|
await mpv.set("speed", this.speed);
|
|
739
801
|
await mpv.set("audio-pitch-correction", !this.pitchShift);
|
|
802
|
+
const chain = filterChain();
|
|
803
|
+
if (chain) await mpv.set("af", chain);
|
|
740
804
|
await mpv.command("observe_property", 1, "time-pos");
|
|
741
805
|
await mpv.command("observe_property", 2, "duration");
|
|
742
806
|
this.mpv = mpv;
|
|
@@ -958,6 +1022,10 @@ var Player = class {
|
|
|
958
1022
|
setPref("speed", s);
|
|
959
1023
|
if (this.mpv?.alive) await this.mpv.set("speed", s);
|
|
960
1024
|
}
|
|
1025
|
+
/** Swaps the audio-filter chain (the equalizer and effects) while playing. */
|
|
1026
|
+
async setFilter(chain) {
|
|
1027
|
+
if (this.mpv?.alive) await this.mpv.set("af", chain);
|
|
1028
|
+
}
|
|
961
1029
|
async setPitchShift(on) {
|
|
962
1030
|
this.pitchShift = on;
|
|
963
1031
|
setPref("pitch-shift", on);
|
|
@@ -1603,7 +1671,7 @@ var ADMIN_COMMANDS = [
|
|
|
1603
1671
|
listSiteModeration()
|
|
1604
1672
|
]);
|
|
1605
1673
|
for (const r of [song, comp, apps, mods]) if (r.status === "rejected" && r.reason?.name === "AbortError") throw r.reason;
|
|
1606
|
-
const n = (r,
|
|
1674
|
+
const n = (r, pick2) => r.status === "fulfilled" ? String(pick2(r.value)) : "?";
|
|
1607
1675
|
sh.print([
|
|
1608
1676
|
`song proposals ${n(song, (v) => v.pending)} pending`,
|
|
1609
1677
|
`comp proposals ${n(comp, (v) => v.pending)} pending`,
|
|
@@ -2408,8 +2476,8 @@ async function randomLyricLine() {
|
|
|
2408
2476
|
const pool = await loadPools(["released", "unreleased"]);
|
|
2409
2477
|
if (pool.length === 0) fail("fortune: no songs to pick from");
|
|
2410
2478
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2411
|
-
const
|
|
2412
|
-
const song = await getSong(
|
|
2479
|
+
const pick2 = pool[Math.floor(Math.random() * pool.length)];
|
|
2480
|
+
const song = await getSong(pick2.id);
|
|
2413
2481
|
const lines = (song.lyrics ?? "").split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length >= 8 && l.length <= 120 && !/^[[(].*[\])]$/.test(l));
|
|
2414
2482
|
if (lines.length) return `${lines[Math.floor(Math.random() * lines.length)]}
|
|
2415
2483
|
- ${song.name}`;
|
|
@@ -2460,13 +2528,13 @@ function matrixScreen() {
|
|
|
2460
2528
|
let heads = [];
|
|
2461
2529
|
let age = new Uint16Array(0);
|
|
2462
2530
|
let glyph = [];
|
|
2463
|
-
const
|
|
2531
|
+
const pick2 = () => RAIN[Math.floor(Math.random() * RAIN.length)];
|
|
2464
2532
|
const fit = (s) => {
|
|
2465
2533
|
cols = s.cols;
|
|
2466
2534
|
rows = s.rows;
|
|
2467
2535
|
heads = Array.from({ length: cols }, () => -Math.floor(Math.random() * rows));
|
|
2468
2536
|
age = new Uint16Array(cols * rows).fill(999);
|
|
2469
|
-
glyph = Array.from({ length: cols * rows },
|
|
2537
|
+
glyph = Array.from({ length: cols * rows }, pick2);
|
|
2470
2538
|
};
|
|
2471
2539
|
const frame = (s) => {
|
|
2472
2540
|
if (s.cols !== cols || s.rows !== rows) fit(s);
|
|
@@ -2477,12 +2545,12 @@ function matrixScreen() {
|
|
|
2477
2545
|
const r = heads[c];
|
|
2478
2546
|
if (r >= 0 && r < rows) {
|
|
2479
2547
|
age[r * cols + c] = 0;
|
|
2480
|
-
glyph[r * cols + c] =
|
|
2548
|
+
glyph[r * cols + c] = pick2();
|
|
2481
2549
|
}
|
|
2482
2550
|
}
|
|
2483
2551
|
for (let k = 0; k < cols / 4; k++) {
|
|
2484
2552
|
const i = Math.floor(Math.random() * glyph.length);
|
|
2485
|
-
if (age[i] < TRAIL) glyph[i] =
|
|
2553
|
+
if (age[i] < TRAIL) glyph[i] = pick2();
|
|
2486
2554
|
}
|
|
2487
2555
|
const [dark, mid, bright, hot] = accentRamp().map((p) => `\x1B[0;${p}m`);
|
|
2488
2556
|
const lines = [];
|
|
@@ -2704,9 +2772,9 @@ function letterHints(rows) {
|
|
|
2704
2772
|
for (const row of rows) {
|
|
2705
2773
|
for (let i = 0; i < row.key.length; i++) {
|
|
2706
2774
|
const letter = row.key[i];
|
|
2707
|
-
const
|
|
2775
|
+
const state2 = row.states[i];
|
|
2708
2776
|
const known = out.get(letter);
|
|
2709
|
-
if (!known || RANK[
|
|
2777
|
+
if (!known || RANK[state2] > RANK[known]) out.set(letter, state2);
|
|
2710
2778
|
}
|
|
2711
2779
|
}
|
|
2712
2780
|
return out;
|
|
@@ -2771,14 +2839,14 @@ function loadRound(dayKey, answerId) {
|
|
|
2771
2839
|
if (!saved || saved.day !== dayKey || saved.answerId !== answerId) return null;
|
|
2772
2840
|
return saved;
|
|
2773
2841
|
}
|
|
2774
|
-
function saveRound(
|
|
2775
|
-
lsSet2("round",
|
|
2842
|
+
function saveRound(state2) {
|
|
2843
|
+
lsSet2("round", state2);
|
|
2776
2844
|
}
|
|
2777
2845
|
function loadPracticeRound2() {
|
|
2778
2846
|
return lsGet2("round:unlimited");
|
|
2779
2847
|
}
|
|
2780
|
-
function savePracticeRound2(
|
|
2781
|
-
lsSet2("round:unlimited",
|
|
2848
|
+
function savePracticeRound2(state2) {
|
|
2849
|
+
lsSet2("round:unlimited", state2);
|
|
2782
2850
|
}
|
|
2783
2851
|
function loadStats() {
|
|
2784
2852
|
const saved = lsGet2("stats");
|
|
@@ -3102,7 +3170,7 @@ function heardleScreen(sh, pausedMusic) {
|
|
|
3102
3170
|
let status = "playing";
|
|
3103
3171
|
let startAt = 0;
|
|
3104
3172
|
let input = "";
|
|
3105
|
-
let
|
|
3173
|
+
let pick2 = 0;
|
|
3106
3174
|
let failure = "";
|
|
3107
3175
|
const render = () => {
|
|
3108
3176
|
if (screen && !screen.closed) draw(screen);
|
|
@@ -3125,7 +3193,7 @@ function heardleScreen(sh, pausedMusic) {
|
|
|
3125
3193
|
guesses = [];
|
|
3126
3194
|
status = "playing";
|
|
3127
3195
|
input = "";
|
|
3128
|
-
|
|
3196
|
+
pick2 = 0;
|
|
3129
3197
|
startAt = clipStart(next, ladder[ladder.length - 1], settings.startPoint, null);
|
|
3130
3198
|
save();
|
|
3131
3199
|
};
|
|
@@ -3133,7 +3201,7 @@ function heardleScreen(sh, pausedMusic) {
|
|
|
3133
3201
|
guesses = [...guesses, guess];
|
|
3134
3202
|
status = correct ? "won" : guesses.length >= ladder.length ? "lost" : "playing";
|
|
3135
3203
|
input = "";
|
|
3136
|
-
|
|
3204
|
+
pick2 = 0;
|
|
3137
3205
|
if (status !== "playing") clip2.stop();
|
|
3138
3206
|
save();
|
|
3139
3207
|
};
|
|
@@ -3144,7 +3212,7 @@ function heardleScreen(sh, pausedMusic) {
|
|
|
3144
3212
|
return;
|
|
3145
3213
|
}
|
|
3146
3214
|
const list = suggestions();
|
|
3147
|
-
const song = list[Math.min(
|
|
3215
|
+
const song = list[Math.min(pick2, list.length - 1)];
|
|
3148
3216
|
if (!song) return;
|
|
3149
3217
|
const correct = isCorrectGuess(song, answer, versions);
|
|
3150
3218
|
record({
|
|
@@ -3186,7 +3254,7 @@ function heardleScreen(sh, pausedMusic) {
|
|
|
3186
3254
|
suggestions().forEach((song, i) => {
|
|
3187
3255
|
const alias = normalizeTitle(song.name).includes(normalizeTitle(input)) ? "" : " \xB7 alias";
|
|
3188
3256
|
const text = `${song.name}${song.era ? ` (${song.era})` : ""}${alias}`;
|
|
3189
|
-
lines.push(i ===
|
|
3257
|
+
lines.push(i === pick2 ? color.cyan(`\u203A ${text}`) : color.dim(` ${text}`));
|
|
3190
3258
|
});
|
|
3191
3259
|
}
|
|
3192
3260
|
s.draw(lines);
|
|
@@ -3228,23 +3296,23 @@ function heardleScreen(sh, pausedMusic) {
|
|
|
3228
3296
|
} else if (answer) void clip2.play(streamUrl(answer.path), startAt, unlocked());
|
|
3229
3297
|
return;
|
|
3230
3298
|
}
|
|
3231
|
-
if (key.name === "down")
|
|
3232
|
-
else if (key.name === "up")
|
|
3299
|
+
if (key.name === "down") pick2 = Math.min(Math.max(0, suggestions().length - 1), pick2 + 1);
|
|
3300
|
+
else if (key.name === "up") pick2 = Math.max(0, pick2 - 1);
|
|
3233
3301
|
else if (key.name === "return") {
|
|
3234
3302
|
if (finished()) {
|
|
3235
3303
|
if (pool) deal(pool);
|
|
3236
3304
|
} else submit();
|
|
3237
3305
|
} else if (key.name === "backspace") {
|
|
3238
3306
|
input = input.slice(0, -1);
|
|
3239
|
-
|
|
3307
|
+
pick2 = 0;
|
|
3240
3308
|
} else if (key.ctrl && key.name === "u") {
|
|
3241
3309
|
input = "";
|
|
3242
|
-
|
|
3310
|
+
pick2 = 0;
|
|
3243
3311
|
} else {
|
|
3244
3312
|
const ch = typed(str, key);
|
|
3245
3313
|
if (ch && !finished() && input.length < 80) {
|
|
3246
3314
|
input += ch;
|
|
3247
|
-
|
|
3315
|
+
pick2 = 0;
|
|
3248
3316
|
}
|
|
3249
3317
|
}
|
|
3250
3318
|
render();
|
|
@@ -3810,8 +3878,8 @@ async function myPlaylists(fresh = false) {
|
|
|
3810
3878
|
}
|
|
3811
3879
|
async function playlistFromArg(arg) {
|
|
3812
3880
|
const list = await myPlaylists();
|
|
3813
|
-
const
|
|
3814
|
-
if (
|
|
3881
|
+
const num2 = /^#?(\d+)$/.exec(arg.trim());
|
|
3882
|
+
if (num2) return list[Number(num2[1]) - 1] ?? fail(`pick a playlist from 1 to ${list.length}`);
|
|
3815
3883
|
return pickByName(list, (p) => p.name, arg) ?? fail(`no single playlist matches "${arg.trim()}" (try: playlists)`);
|
|
3816
3884
|
}
|
|
3817
3885
|
async function playlistSongs(id) {
|
|
@@ -4492,6 +4560,221 @@ var UPDATE_COMMANDS = [
|
|
|
4492
4560
|
}
|
|
4493
4561
|
];
|
|
4494
4562
|
|
|
4563
|
+
// src/cdn.ts
|
|
4564
|
+
var DEFAULT_TRUST = 100;
|
|
4565
|
+
var VIOLATION_LIMIT = 5;
|
|
4566
|
+
var ACTIONS = ["approve", "revoke", "enable", "disable", "reset", "restore", "delete"];
|
|
4567
|
+
var BASE2 = "/cdn/admin";
|
|
4568
|
+
var listNodes = async () => (await apiFetch(`${BASE2}/nodes/`))?.nodes ?? [];
|
|
4569
|
+
var nodePath = (n) => `${BASE2}/nodes/${encodeURIComponent(n.node_id)}/`;
|
|
4570
|
+
var patch = (n, body) => apiFetch(nodePath(n), {}, { method: "PATCH", body });
|
|
4571
|
+
var autoDisabled = (n) => n.trust_score <= 0 || n.hash_violations >= VIOLATION_LIMIT;
|
|
4572
|
+
var mbps = (v) => v ? `${Math.round(v * 10) / 10} Mbps` : "\u2014";
|
|
4573
|
+
function state(n) {
|
|
4574
|
+
if (!n.is_approved) return autoDisabled(n) ? "auto-disabled" : "pending";
|
|
4575
|
+
if (!n.is_active) return "disabled";
|
|
4576
|
+
return n.online ? "online" : "offline";
|
|
4577
|
+
}
|
|
4578
|
+
var tone = (s) => s === "online" ? color.green(s) : s === "offline" ? color.dim(s) : color.red(s);
|
|
4579
|
+
function pick(nodes, arg) {
|
|
4580
|
+
const q = arg.trim().replace(/^#/, "").toLowerCase();
|
|
4581
|
+
if (!q) fail("say which node (see: cdn)");
|
|
4582
|
+
const exact = nodes.filter((n) => n.node_id.toLowerCase() === q || n.name.toLowerCase() === q);
|
|
4583
|
+
const hits = exact.length ? exact : nodes.filter((n) => n.node_id.toLowerCase().startsWith(q) || n.name.toLowerCase().includes(q));
|
|
4584
|
+
if (hits.length === 0) fail(`no node matches "${arg.trim()}" (see: cdn)`);
|
|
4585
|
+
if (hits.length > 1) fail(`"${arg.trim()}" matches ${hits.length} nodes: ${hits.slice(0, 5).map((n) => n.name || n.node_id.slice(0, 8)).join(", ")}`);
|
|
4586
|
+
return hits[0];
|
|
4587
|
+
}
|
|
4588
|
+
function describeNode(n, latest) {
|
|
4589
|
+
const pct = n.max_storage_bytes > 0 ? ` of ${formatBytes(n.max_storage_bytes)} (${Math.min(100, Math.round(n.current_storage_bytes / n.max_storage_bytes * 100))}%)` : "";
|
|
4590
|
+
const newest = n.manifest_version ?? latest;
|
|
4591
|
+
const synced = n.synced_manifest_version === void 0 ? "" : `
|
|
4592
|
+
synced manifest ${n.synced_manifest_version == null ? "never" : `v${n.synced_manifest_version}`}${newest != null ? ` (latest v${newest})` : ""}`;
|
|
4593
|
+
const notes = [];
|
|
4594
|
+
if (!n.is_approved && autoDisabled(n)) notes.push("pulled for hash violations - restore it to reset trust and violations too");
|
|
4595
|
+
else if (!n.is_approved) notes.push("waiting for approval - it serves nothing until approved");
|
|
4596
|
+
else if (!n.is_active) notes.push("switched off by an admin");
|
|
4597
|
+
return [
|
|
4598
|
+
`${color.bold(n.name || n.node_id)} ${tone(state(n))}`,
|
|
4599
|
+
`node id ${n.node_id}`,
|
|
4600
|
+
`owner ${n.owner_username || "unclaimed"}`,
|
|
4601
|
+
`region ${n.region || "\u2014"} \xB7 ${n.is_public ? "public" : "private"}`,
|
|
4602
|
+
`address ${n.ip_address ? n.port ? `${n.ip_address}:${n.port}` : n.ip_address : "\u2014"}${n.public_base_url ? ` ${n.public_base_url}` : ""}`,
|
|
4603
|
+
`storage ${formatBytes(n.current_storage_bytes)}${pct} \xB7 ${n.file_count.toLocaleString()} files`,
|
|
4604
|
+
`speed up ${mbps(n.upload_speed_mbps)} \xB7 down ${mbps(n.download_speed_mbps)} \xB7 to listeners ${mbps(n.observed_download_speed_mbps)}`,
|
|
4605
|
+
`served ${formatBytes(n.total_bytes_served)} in ${n.total_requests.toLocaleString()} requests`,
|
|
4606
|
+
`trust ${Math.round(n.trust_score)} \xB7 ${plural(n.hash_violations, "violation")}`,
|
|
4607
|
+
`heartbeat ${relativeTime(n.last_heartbeat)} \xB7 registered ${new Date(n.created_at).toLocaleDateString()}${synced}`,
|
|
4608
|
+
...notes.map((t) => color.yellow(`! ${t}`))
|
|
4609
|
+
].join("\n");
|
|
4610
|
+
}
|
|
4611
|
+
async function act(action, n, sh, yes) {
|
|
4612
|
+
const name = n.name || n.node_id;
|
|
4613
|
+
switch (action) {
|
|
4614
|
+
case "approve":
|
|
4615
|
+
if (n.is_approved) fail(`${name} is already approved`);
|
|
4616
|
+
if (autoDisabled(n)) fail(`${name} was pulled for hash violations - use: cdn restore ${name}`);
|
|
4617
|
+
await patch(n, { is_approved: true });
|
|
4618
|
+
break;
|
|
4619
|
+
case "revoke":
|
|
4620
|
+
if (!n.is_approved) fail(`${name} isn't approved`);
|
|
4621
|
+
await patch(n, { is_approved: false });
|
|
4622
|
+
break;
|
|
4623
|
+
case "enable":
|
|
4624
|
+
if (n.is_active) fail(`${name} is already enabled`);
|
|
4625
|
+
if (autoDisabled(n)) fail(`${name} was pulled for hash violations - use: cdn restore ${name}`);
|
|
4626
|
+
await patch(n, { is_active: true });
|
|
4627
|
+
break;
|
|
4628
|
+
case "disable":
|
|
4629
|
+
if (!n.is_active) fail(`${name} is already disabled`);
|
|
4630
|
+
await patch(n, { is_active: false });
|
|
4631
|
+
break;
|
|
4632
|
+
case "reset":
|
|
4633
|
+
if (n.hash_violations === 0 && n.trust_score >= DEFAULT_TRUST) fail(`${name} has nothing to reset`);
|
|
4634
|
+
await patch(n, { trust_score: DEFAULT_TRUST, hash_violations: 0 });
|
|
4635
|
+
break;
|
|
4636
|
+
case "restore":
|
|
4637
|
+
await patch(n, { is_approved: true, is_active: true, trust_score: DEFAULT_TRUST, hash_violations: 0 });
|
|
4638
|
+
break;
|
|
4639
|
+
case "delete":
|
|
4640
|
+
if (!await confirm(sh, `Permanently delete ${name}? Its file list, history and API key go with it.`, yes, "cdn")) return;
|
|
4641
|
+
await apiFetch(nodePath(n), {}, { method: "DELETE" });
|
|
4642
|
+
sh.print(`deleted ${name}`, "ok");
|
|
4643
|
+
return;
|
|
4644
|
+
}
|
|
4645
|
+
sh.print(`${action} ${name}: done`, "ok");
|
|
4646
|
+
}
|
|
4647
|
+
var CDN_COMMANDS = [
|
|
4648
|
+
{
|
|
4649
|
+
name: "cdn",
|
|
4650
|
+
group: "Admin",
|
|
4651
|
+
usage: "cdn [<node>] \xB7 cdn approve|revoke|enable|disable|reset|restore|delete [-y] <node>",
|
|
4652
|
+
description: "The CDN node roster and stats; one node in full; or approve, revoke, enable, disable, reset trust, restore or delete a node (a node is its id, the start of it, or its name)",
|
|
4653
|
+
complete: kindComplete(ACTIONS),
|
|
4654
|
+
run: admin(async (args, sh) => {
|
|
4655
|
+
const words = args.trim().split(/\s+/).filter(Boolean);
|
|
4656
|
+
const first = words[0]?.toLowerCase();
|
|
4657
|
+
if (first && ACTIONS.includes(first)) {
|
|
4658
|
+
words.shift();
|
|
4659
|
+
const yes = words[0] === "-y" ? (words.shift(), true) : false;
|
|
4660
|
+
const nodes2 = await listNodes();
|
|
4661
|
+
await act(first, pick(nodes2, words.join(" ")), sh, yes);
|
|
4662
|
+
return;
|
|
4663
|
+
}
|
|
4664
|
+
const [stats, nodes] = await Promise.all([apiFetch(`${BASE2}/stats/`), listNodes()]);
|
|
4665
|
+
if (words.length) {
|
|
4666
|
+
sh.print(describeNode(pick(nodes, words.join(" ")), stats.manifest_version));
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
const lines = [
|
|
4670
|
+
`${plural(stats.total_nodes, "node")} \xB7 ${stats.online_nodes} online \xB7 ${stats.pending_nodes} awaiting approval \xB7 ${formatBytes(stats.total_bytes_served)} served in ${stats.total_requests.toLocaleString()} requests`,
|
|
4671
|
+
`master copy: ${stats.master_files.toLocaleString()} files, ${formatBytes(stats.master_bytes)} \xB7 manifest v${stats.manifest_version}`
|
|
4672
|
+
];
|
|
4673
|
+
if (nodes.length === 0) {
|
|
4674
|
+
sh.print([...lines, "", color.dim("no nodes registered")].join("\n"));
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
const order = ["pending", "auto-disabled", "online", "offline", "disabled"];
|
|
4678
|
+
const sorted = [...nodes].sort((a, b) => order.indexOf(state(a)) - order.indexOf(state(b)) || (a.name || a.node_id).localeCompare(b.name || b.node_id));
|
|
4679
|
+
const rows = sorted.map((n) => {
|
|
4680
|
+
const s = state(n);
|
|
4681
|
+
return `${(n.name || n.node_id.slice(0, 8)).slice(0, 24).padEnd(26)}${tone(s)}${" ".repeat(Math.max(1, 15 - s.length))}${(n.owner_username || "unclaimed").slice(0, 16).padEnd(18)}${formatBytes(n.current_storage_bytes).padEnd(11)}${formatBytes(n.total_bytes_served).padEnd(11)}trust ${String(Math.round(n.trust_score)).padEnd(5)}${relativeTime(n.last_heartbeat)}`;
|
|
4682
|
+
});
|
|
4683
|
+
sh.print([...lines, "", ...rows].join("\n"));
|
|
4684
|
+
})
|
|
4685
|
+
}
|
|
4686
|
+
];
|
|
4687
|
+
|
|
4688
|
+
// src/eq.ts
|
|
4689
|
+
var WORDS = ["on", "off", "list", "reset", "reverb", "boost", "balance", "speed", "mono", "pitch", ...EQ_PRESETS.map((p) => p.id)];
|
|
4690
|
+
function describe(speed, pitch) {
|
|
4691
|
+
const e = effects();
|
|
4692
|
+
const preset = EQ_PRESETS.find((p) => p.id === e.preset)?.id ?? e.preset;
|
|
4693
|
+
return `equalizer ${e.eq ? "on" : "off"} \xB7 preset ${preset} \xB7 boost ${Math.round(e.boost * 100)}% \xB7 balance ${Math.round(e.balance * 100)}
|
|
4694
|
+
reverb ${e.reverb ? "on" : "off"} \xB7 mix ${Math.round(e.reverbMix * 100)}% \xB7 decay ${e.reverbDecay}s \xB7 speed ${speed}x \xB7 mono ${e.mono ? "on" : "off"} \xB7 pitch follows speed ${pitch ? "on" : "off"}`;
|
|
4695
|
+
}
|
|
4696
|
+
var onOff = (v, cur) => v === "on" ? true : v === "off" ? false : v === void 0 ? !cur : fail("expected on or off");
|
|
4697
|
+
function numIn(v, lo, hi, what) {
|
|
4698
|
+
const n = Number(v);
|
|
4699
|
+
if (v === void 0 || v === "" || !Number.isFinite(n) || n < lo || n > hi) fail(`${what} must be ${lo} to ${hi}`);
|
|
4700
|
+
return n;
|
|
4701
|
+
}
|
|
4702
|
+
var EQ_COMMANDS = [
|
|
4703
|
+
{
|
|
4704
|
+
name: "eq",
|
|
4705
|
+
group: "Player",
|
|
4706
|
+
usage: "eq [on | off | list | reset | <preset> | reverb [on|off|<0-100>] [decay 1-8] | boost <100-200> | balance <-100..100> | speed <0.5-2> | mono [on|off] | pitch [on|off]]",
|
|
4707
|
+
description: "Show or change the equalizer, reverb and sound effects; a preset name turns it on with that preset",
|
|
4708
|
+
complete: async (arg) => /\s/.test(arg.trim()) ? [] : WORDS.filter((w) => w.startsWith(arg.trim().toLowerCase())),
|
|
4709
|
+
run: async (args, sh) => {
|
|
4710
|
+
const player = playerOf(sh);
|
|
4711
|
+
const arg = args.trim().toLowerCase();
|
|
4712
|
+
if (!arg) {
|
|
4713
|
+
sh.print(describe(player.speed, player.pitchShift));
|
|
4714
|
+
return;
|
|
4715
|
+
}
|
|
4716
|
+
const [sub, ...rest] = arg.split(/\s+/);
|
|
4717
|
+
const apply = async (message) => {
|
|
4718
|
+
await player.setFilter(filterChain());
|
|
4719
|
+
sh.print(message, "ok");
|
|
4720
|
+
};
|
|
4721
|
+
if (arg === "list") {
|
|
4722
|
+
sh.print(EQ_PRESETS.map((p) => `${p.id.padEnd(16)}${p.name}`).join("\n"));
|
|
4723
|
+
return;
|
|
4724
|
+
}
|
|
4725
|
+
if (sub === "reverb") {
|
|
4726
|
+
const [a, b, c] = rest;
|
|
4727
|
+
const mix = a === void 0 ? void 0 : a === "on" || a === "off" ? void 0 : numIn(a, 0, 100, "reverb amount") / 100;
|
|
4728
|
+
const decayArg = b === "decay" ? c : b;
|
|
4729
|
+
const decay = decayArg === void 0 ? void 0 : numIn(decayArg, 1, 8, "reverb decay");
|
|
4730
|
+
const on = a === void 0 ? !effects().reverb : a === "off" ? false : true;
|
|
4731
|
+
if (mix !== void 0) saveEffect("reverbMix", mix);
|
|
4732
|
+
if (decay !== void 0) saveEffect("reverbDecay", decay);
|
|
4733
|
+
saveEffect("reverb", on);
|
|
4734
|
+
const e = effects();
|
|
4735
|
+
await apply(`reverb ${e.reverb ? "on" : "off"} \xB7 mix ${Math.round(e.reverbMix * 100)}% \xB7 decay ${e.reverbDecay}s`);
|
|
4736
|
+
return;
|
|
4737
|
+
}
|
|
4738
|
+
if (sub === "boost") {
|
|
4739
|
+
saveEffect("boost", numIn(rest[0], 100, 200, "boost") / 100);
|
|
4740
|
+
await apply(describe(player.speed, player.pitchShift));
|
|
4741
|
+
return;
|
|
4742
|
+
}
|
|
4743
|
+
if (sub === "balance") {
|
|
4744
|
+
saveEffect("balance", numIn(rest[0], -100, 100, "balance") / 100);
|
|
4745
|
+
await apply(describe(player.speed, player.pitchShift));
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
if (sub === "speed") {
|
|
4749
|
+
const v = rest[0] === "reset" ? 1 : numIn(rest[0]?.replace(/x$/, ""), 0.5, 2, "speed");
|
|
4750
|
+
await player.setSpeed(Math.round(v * 100) / 100);
|
|
4751
|
+
sh.print(`speed ${player.speed}x`, "ok");
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4754
|
+
if (sub === "pitch") {
|
|
4755
|
+
await player.setPitchShift(onOff(rest[0], player.pitchShift));
|
|
4756
|
+
sh.print(`pitch follows speed: ${player.pitchShift ? "on" : "off"}`, "ok");
|
|
4757
|
+
return;
|
|
4758
|
+
}
|
|
4759
|
+
if (sub === "mono") {
|
|
4760
|
+
saveEffect("mono", onOff(rest[0], effects().mono));
|
|
4761
|
+
await apply(`mono ${effects().mono ? "on" : "off"}`);
|
|
4762
|
+
return;
|
|
4763
|
+
}
|
|
4764
|
+
if (arg === "on" || arg === "off") {
|
|
4765
|
+
saveEffect("eq", arg === "on");
|
|
4766
|
+
await apply(describe(player.speed, player.pitchShift));
|
|
4767
|
+
return;
|
|
4768
|
+
}
|
|
4769
|
+
const preset = arg === "reset" ? EQ_PRESETS[0] : pickByName(EQ_PRESETS, (p) => p.id, arg) ?? pickByName(EQ_PRESETS, (p) => p.name, arg);
|
|
4770
|
+
if (!preset) fail(`no preset "${args.trim()}" (try: eq list)`);
|
|
4771
|
+
saveEffect("preset", preset.id);
|
|
4772
|
+
saveEffect("eq", true);
|
|
4773
|
+
await apply(describe(player.speed, player.pitchShift));
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
];
|
|
4777
|
+
|
|
4495
4778
|
// src/karaoke.ts
|
|
4496
4779
|
var TIME = /\[(\d{1,2}):(\d{2})[.:](\d{2,3})\]/g;
|
|
4497
4780
|
function parseLrc(lrc) {
|
|
@@ -4809,12 +5092,12 @@ function visualizerScreen(sh, binary) {
|
|
|
4809
5092
|
const time = track ? `${clock(now)}${track.duration ? ` / ${clock(track.duration)}` : ""}` : "";
|
|
4810
5093
|
lines.push(`${color.bold(title)} ${color.dim(time)}${status ? ` ${color.dim(status)}` : ""}`, "");
|
|
4811
5094
|
for (let row = height - 1; row >= 0; row--) {
|
|
4812
|
-
const
|
|
5095
|
+
const tone2 = row >= height * 0.85 ? hot : row >= height * 0.55 ? bright : row >= height * 0.25 ? mid : dark;
|
|
4813
5096
|
let line = " ";
|
|
4814
5097
|
for (let i = 0; i < bars; i++) {
|
|
4815
5098
|
const filled = levels[i] * height;
|
|
4816
5099
|
const cell = Math.max(0, Math.min(8, Math.round((filled - row) * 8)));
|
|
4817
|
-
line += cell === 0 ? " " : `${
|
|
5100
|
+
line += cell === 0 ? " " : `${tone2}${BLOCKS[cell].repeat(2)}\x1B[0m`;
|
|
4818
5101
|
}
|
|
4819
5102
|
lines.push(line);
|
|
4820
5103
|
}
|
|
@@ -5055,8 +5338,10 @@ ${color.dim(`local ${sh.localDir}`)}`)
|
|
|
5055
5338
|
{ name: "du", group: "Files", usage: "du [folder]", path: "dir", description: "How much is in each subfolder, and in total", run: fileTool(diskUsage) },
|
|
5056
5339
|
...LIBRARY_COMMANDS,
|
|
5057
5340
|
...PLAYER_COMMANDS,
|
|
5341
|
+
...EQ_COMMANDS,
|
|
5058
5342
|
...PEOPLE_COMMANDS,
|
|
5059
5343
|
...ADMIN_COMMANDS,
|
|
5344
|
+
...CDN_COMMANDS,
|
|
5060
5345
|
...FUN_COMMANDS,
|
|
5061
5346
|
...VISUALIZER_COMMANDS,
|
|
5062
5347
|
...KARAOKE_COMMANDS,
|
|
@@ -5343,11 +5628,11 @@ var Shell = class {
|
|
|
5343
5628
|
const user2 = getToken() ? loadConfig().user?.name ?? "user" : "guest";
|
|
5344
5629
|
return `${color.user(`${user2}@unreleased`)}:${color.blue(filesPathString(this.cwd))}$ `;
|
|
5345
5630
|
}
|
|
5346
|
-
print(text,
|
|
5347
|
-
if (
|
|
5631
|
+
print(text, tone2 = "plain") {
|
|
5632
|
+
if (tone2 === "error") this.errors++;
|
|
5348
5633
|
const sink = this.sinks[this.sinks.length - 1];
|
|
5349
|
-
if (sink && (
|
|
5350
|
-
else writeTone(text,
|
|
5634
|
+
if (sink && (tone2 !== "error" || sink.keepErrors)) sink.lines.push(...text.split("\n"));
|
|
5635
|
+
else writeTone(text, tone2);
|
|
5351
5636
|
}
|
|
5352
5637
|
/** A progress note ("loading…"): straight to stderr, never into a pipe or
|
|
5353
5638
|
* a one-shot command's stdout - and not at all over a full screen. */
|
|
@@ -5570,17 +5855,17 @@ async function startRepl() {
|
|
|
5570
5855
|
const flushHeld = () => {
|
|
5571
5856
|
for (const n of held.splice(0)) writeTone(n.text, n.tone);
|
|
5572
5857
|
};
|
|
5573
|
-
const notify = (text,
|
|
5858
|
+
const notify = (text, tone2 = "dim") => {
|
|
5574
5859
|
if (screenActive() || historySearchActive()) {
|
|
5575
|
-
held.push({ text, tone });
|
|
5860
|
+
held.push({ text, tone: tone2 });
|
|
5576
5861
|
return;
|
|
5577
5862
|
}
|
|
5578
5863
|
if (running) {
|
|
5579
|
-
writeTone(text,
|
|
5864
|
+
writeTone(text, tone2);
|
|
5580
5865
|
return;
|
|
5581
5866
|
}
|
|
5582
5867
|
process.stdout.write("\r\x1B[2K");
|
|
5583
|
-
writeTone(text,
|
|
5868
|
+
writeTone(text, tone2);
|
|
5584
5869
|
rl.prompt(true);
|
|
5585
5870
|
};
|
|
5586
5871
|
shell.player = new Player(notify);
|