unreleased-cli 0.1.2 → 0.1.4

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
@@ -26,6 +26,8 @@ sudo add-apt-repository ppa:saint-duckworth/ppa
26
26
  sudo apt install unreleased-cli
27
27
  ```
28
28
 
29
+ To update later, run `unreleased update` (or `update` inside the shell). It checks for a newer version and installs it if you installed from npm; `update -c` only checks.
30
+
29
31
  From source:
30
32
 
31
33
  ```
@@ -50,7 +52,7 @@ Commands (type `help <command>` in the shell for the details of each one):
50
52
 
51
53
  - **Files:** `cd`, `ls`, `pwd`, `lcd`, `get`, `cat`, `head`, `tail`, `wc`, `grep`, `locate`, `tree`, `du`
52
54
  - **Library:** `find`, `song`, `like`, `unlike`, `liked`, `playlists`, `playlist`, `stats`
53
- - **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`
54
56
  - **People:** `user`, `lookup`
55
57
  - **Admin** (administrators only): `pending`, `proposals`, `comps`, `applications`, `inspect`, `approve`, `reject`, `reverse`, `users`, `sitebans`, `siteunban`
56
58
  - **Fun:** `neofetch`, `fortune`, `juicesay`, `matrix`, `visualizer`, `karaoke`, `wordle`, `heardle`
@@ -128,6 +130,7 @@ These cover the Admin page's review queues and site moderation, using the same e
128
130
  - `matrix` is digital rain. Any key leaves.
129
131
  - `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.
130
132
  - `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).
133
+ - `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.
131
134
  - `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.
132
135
  - `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.
133
136
 
@@ -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.2";
85
+ var VERSION = "0.1.4";
86
86
  function apiBase() {
87
87
  return (process.env.UNRELEASED_API || loadConfig().api || DEFAULT_API).replace(/\/+$/, "");
88
88
  }
@@ -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);
@@ -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 num = /^#?(\d+)$/.exec(arg.trim());
3814
- if (num) return list[Number(num[1]) - 1] ?? fail(`pick a playlist from 1 to ${list.length}`);
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) {
@@ -4416,6 +4484,172 @@ var SETTINGS_COMMANDS = [
4416
4484
  }
4417
4485
  ];
4418
4486
 
4487
+ // src/update.ts
4488
+ import { spawn as spawn2 } from "node:child_process";
4489
+ import { realpathSync } from "node:fs";
4490
+ var PACKAGE = "unreleased-cli";
4491
+ async function latestVersion() {
4492
+ let res;
4493
+ try {
4494
+ res = await fetch(`https://registry.npmjs.org/${PACKAGE}/latest`, { signal: AbortSignal.timeout(1e4) });
4495
+ } catch {
4496
+ return fail("couldn't reach npm to check for updates (are you online?)");
4497
+ }
4498
+ if (!res.ok) fail(`npm answered ${res.status} when checking for updates`);
4499
+ const { version } = await res.json();
4500
+ return version ?? fail("npm sent no version number");
4501
+ }
4502
+ function compare(a, b) {
4503
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
4504
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
4505
+ for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
4506
+ return 0;
4507
+ }
4508
+ function installKind() {
4509
+ let file = process.argv[1] ?? "";
4510
+ try {
4511
+ file = realpathSync(file);
4512
+ } catch {
4513
+ }
4514
+ const p = file.replace(/\\/g, "/");
4515
+ if (p.startsWith("/usr/lib/unreleased-cli/")) return "apt";
4516
+ if (p.includes("/node_modules/")) return "npm";
4517
+ return "source";
4518
+ }
4519
+ function runNpmInstall() {
4520
+ return new Promise((resolve3) => {
4521
+ const child = spawn2("npm", ["install", "-g", `${PACKAGE}@latest`], { stdio: "inherit", shell: process.platform === "win32" });
4522
+ child.on("error", () => resolve3(1));
4523
+ child.on("close", (code2) => resolve3(code2 ?? 1));
4524
+ });
4525
+ }
4526
+ var UPDATE_COMMANDS = [
4527
+ {
4528
+ name: "update",
4529
+ aliases: ["upgrade"],
4530
+ group: "Shell",
4531
+ usage: "update [-c]",
4532
+ description: "Check for a newer version and install it. -c only checks",
4533
+ run: async (arg, sh) => {
4534
+ const checkOnly = /^-c$|^--check$/.test(arg.trim());
4535
+ if (arg.trim() && !checkOnly) fail("usage: update [-c]");
4536
+ const latest = await latestVersion();
4537
+ if (compare(latest, VERSION) <= 0) {
4538
+ sh.print(`you're up to date (v${VERSION})`, "ok");
4539
+ return;
4540
+ }
4541
+ sh.print(`new version: v${latest} (you have v${VERSION})`);
4542
+ if (checkOnly) {
4543
+ sh.print("run update to install it", "dim");
4544
+ return;
4545
+ }
4546
+ const kind = installKind();
4547
+ if (kind === "apt") {
4548
+ sh.print("this copy came from the Ubuntu PPA, so update it with:\n sudo apt update && sudo apt install --only-upgrade unreleased-cli");
4549
+ return;
4550
+ }
4551
+ if (kind === "source") {
4552
+ sh.print("this copy runs from a source checkout, so update it with:\n git pull && npm install && npm run build");
4553
+ return;
4554
+ }
4555
+ sh.print(`installing v${latest}\u2026`, "dim");
4556
+ const code2 = await runNpmInstall();
4557
+ if (code2 !== 0) fail(`the install failed (npm exit code ${code2}). If it says permission denied, run it with sudo: sudo npm install -g ${PACKAGE}@latest`);
4558
+ sh.print(`updated to v${latest} - leave and start unreleased again to use it`, "ok");
4559
+ }
4560
+ }
4561
+ ];
4562
+
4563
+ // src/eq.ts
4564
+ var WORDS = ["on", "off", "list", "reset", "reverb", "boost", "balance", "speed", "mono", "pitch", ...EQ_PRESETS.map((p) => p.id)];
4565
+ function describe(speed, pitch) {
4566
+ const e = effects();
4567
+ const preset = EQ_PRESETS.find((p) => p.id === e.preset)?.id ?? e.preset;
4568
+ return `equalizer ${e.eq ? "on" : "off"} \xB7 preset ${preset} \xB7 boost ${Math.round(e.boost * 100)}% \xB7 balance ${Math.round(e.balance * 100)}
4569
+ 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"}`;
4570
+ }
4571
+ var onOff = (v, cur) => v === "on" ? true : v === "off" ? false : v === void 0 ? !cur : fail("expected on or off");
4572
+ function numIn(v, lo, hi, what) {
4573
+ const n = Number(v);
4574
+ if (v === void 0 || v === "" || !Number.isFinite(n) || n < lo || n > hi) fail(`${what} must be ${lo} to ${hi}`);
4575
+ return n;
4576
+ }
4577
+ var EQ_COMMANDS = [
4578
+ {
4579
+ name: "eq",
4580
+ group: "Player",
4581
+ 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]]",
4582
+ description: "Show or change the equalizer, reverb and sound effects; a preset name turns it on with that preset",
4583
+ complete: async (arg) => /\s/.test(arg.trim()) ? [] : WORDS.filter((w) => w.startsWith(arg.trim().toLowerCase())),
4584
+ run: async (args, sh) => {
4585
+ const player = playerOf(sh);
4586
+ const arg = args.trim().toLowerCase();
4587
+ if (!arg) {
4588
+ sh.print(describe(player.speed, player.pitchShift));
4589
+ return;
4590
+ }
4591
+ const [sub, ...rest] = arg.split(/\s+/);
4592
+ const apply = async (message) => {
4593
+ await player.setFilter(filterChain());
4594
+ sh.print(message, "ok");
4595
+ };
4596
+ if (arg === "list") {
4597
+ sh.print(EQ_PRESETS.map((p) => `${p.id.padEnd(16)}${p.name}`).join("\n"));
4598
+ return;
4599
+ }
4600
+ if (sub === "reverb") {
4601
+ const [a, b, c] = rest;
4602
+ const mix = a === void 0 ? void 0 : a === "on" || a === "off" ? void 0 : numIn(a, 0, 100, "reverb amount") / 100;
4603
+ const decayArg = b === "decay" ? c : b;
4604
+ const decay = decayArg === void 0 ? void 0 : numIn(decayArg, 1, 8, "reverb decay");
4605
+ const on = a === void 0 ? !effects().reverb : a === "off" ? false : true;
4606
+ if (mix !== void 0) saveEffect("reverbMix", mix);
4607
+ if (decay !== void 0) saveEffect("reverbDecay", decay);
4608
+ saveEffect("reverb", on);
4609
+ const e = effects();
4610
+ await apply(`reverb ${e.reverb ? "on" : "off"} \xB7 mix ${Math.round(e.reverbMix * 100)}% \xB7 decay ${e.reverbDecay}s`);
4611
+ return;
4612
+ }
4613
+ if (sub === "boost") {
4614
+ saveEffect("boost", numIn(rest[0], 100, 200, "boost") / 100);
4615
+ await apply(describe(player.speed, player.pitchShift));
4616
+ return;
4617
+ }
4618
+ if (sub === "balance") {
4619
+ saveEffect("balance", numIn(rest[0], -100, 100, "balance") / 100);
4620
+ await apply(describe(player.speed, player.pitchShift));
4621
+ return;
4622
+ }
4623
+ if (sub === "speed") {
4624
+ const v = rest[0] === "reset" ? 1 : numIn(rest[0]?.replace(/x$/, ""), 0.5, 2, "speed");
4625
+ await player.setSpeed(Math.round(v * 100) / 100);
4626
+ sh.print(`speed ${player.speed}x`, "ok");
4627
+ return;
4628
+ }
4629
+ if (sub === "pitch") {
4630
+ await player.setPitchShift(onOff(rest[0], player.pitchShift));
4631
+ sh.print(`pitch follows speed: ${player.pitchShift ? "on" : "off"}`, "ok");
4632
+ return;
4633
+ }
4634
+ if (sub === "mono") {
4635
+ saveEffect("mono", onOff(rest[0], effects().mono));
4636
+ await apply(`mono ${effects().mono ? "on" : "off"}`);
4637
+ return;
4638
+ }
4639
+ if (arg === "on" || arg === "off") {
4640
+ saveEffect("eq", arg === "on");
4641
+ await apply(describe(player.speed, player.pitchShift));
4642
+ return;
4643
+ }
4644
+ const preset = arg === "reset" ? EQ_PRESETS[0] : pickByName(EQ_PRESETS, (p) => p.id, arg) ?? pickByName(EQ_PRESETS, (p) => p.name, arg);
4645
+ if (!preset) fail(`no preset "${args.trim()}" (try: eq list)`);
4646
+ saveEffect("preset", preset.id);
4647
+ saveEffect("eq", true);
4648
+ await apply(describe(player.speed, player.pitchShift));
4649
+ }
4650
+ }
4651
+ ];
4652
+
4419
4653
  // src/karaoke.ts
4420
4654
  var TIME = /\[(\d{1,2}):(\d{2})[.:](\d{2,3})\]/g;
4421
4655
  function parseLrc(lrc) {
@@ -4547,7 +4781,7 @@ var KARAOKE_COMMANDS = [
4547
4781
  ];
4548
4782
 
4549
4783
  // src/visualizer.ts
4550
- import { spawn as spawn2 } from "node:child_process";
4784
+ import { spawn as spawn3 } from "node:child_process";
4551
4785
  import { closeSync, fstatSync, openSync, readSync, rmSync as rmSync2 } from "node:fs";
4552
4786
  import { tmpdir as tmpdir2 } from "node:os";
4553
4787
  import { join as join5 } from "node:path";
@@ -4564,7 +4798,7 @@ var Decoder = class {
4564
4798
  finished = false;
4565
4799
  failed = false;
4566
4800
  constructor(binary, url) {
4567
- this.proc = spawn2(binary, [
4801
+ this.proc = spawn3(binary, [
4568
4802
  "--no-config",
4569
4803
  "--no-video",
4570
4804
  "--no-terminal",
@@ -4979,6 +5213,7 @@ ${color.dim(`local ${sh.localDir}`)}`)
4979
5213
  { name: "du", group: "Files", usage: "du [folder]", path: "dir", description: "How much is in each subfolder, and in total", run: fileTool(diskUsage) },
4980
5214
  ...LIBRARY_COMMANDS,
4981
5215
  ...PLAYER_COMMANDS,
5216
+ ...EQ_COMMANDS,
4982
5217
  ...PEOPLE_COMMANDS,
4983
5218
  ...ADMIN_COMMANDS,
4984
5219
  ...FUN_COMMANDS,
@@ -4986,6 +5221,7 @@ ${color.dim(`local ${sh.localDir}`)}`)
4986
5221
  ...KARAOKE_COMMANDS,
4987
5222
  ...GAME_COMMANDS,
4988
5223
  ...SETTINGS_COMMANDS,
5224
+ ...UPDATE_COMMANDS,
4989
5225
  {
4990
5226
  name: "source",
4991
5227
  aliases: ["."],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unreleased-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The Unreleased site terminal as a command-line shell",
5
5
  "keywords": [
6
6
  "juice-wrld",