agent-noti 1.3.0 → 1.4.0
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 +57 -10
- package/bin/cli.mjs +239 -1
- package/bin/play.mjs +64 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,16 +37,18 @@ Each theme includes a separate idle and input sound.
|
|
|
37
37
|
## Commands
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
|
-
agent-noti install # Add hooks + pick theme
|
|
40
|
+
agent-noti install # Add hooks + pick theme (i)
|
|
41
41
|
agent-noti uninstall # Remove hooks
|
|
42
|
-
agent-noti test # Play current sounds
|
|
43
|
-
agent-noti sounds # List available themes
|
|
44
|
-
agent-noti pick # Interactive sound picker
|
|
45
|
-
agent-noti add-custom # Use your own sound files
|
|
46
|
-
agent-noti volume <1-10> # Set volume level
|
|
47
|
-
agent-noti mute # Mute notifications
|
|
48
|
-
agent-noti unmute # Unmute notifications
|
|
49
|
-
agent-noti
|
|
42
|
+
agent-noti test # Play current sounds (t)
|
|
43
|
+
agent-noti sounds # List available themes (s)
|
|
44
|
+
agent-noti pick # Interactive sound picker (p)
|
|
45
|
+
agent-noti add-custom # Use your own sound files (ac)
|
|
46
|
+
agent-noti volume <1-10> # Set volume level (v)
|
|
47
|
+
agent-noti mute # Mute notifications (m)
|
|
48
|
+
agent-noti unmute # Unmute notifications (u)
|
|
49
|
+
agent-noti ntfy # Configure ntfy.sh push alerts (n)
|
|
50
|
+
agent-noti ntfy-test # Send a test push notification (nt)
|
|
51
|
+
agent-noti reset # Reset everything (r)
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
Every command has a short alias shown in parentheses — e.g. `agent-noti v 5` instead of `agent-noti volume 5`.
|
|
@@ -87,6 +89,43 @@ agent-noti unmute # Re-enable notifications
|
|
|
87
89
|
|
|
88
90
|
Setting volume while muted auto-unmutes. Volume works across all platforms.
|
|
89
91
|
|
|
92
|
+
## Push notifications (ntfy.sh)
|
|
93
|
+
|
|
94
|
+
Get push notifications on your phone or desktop via [ntfy.sh](https://ntfy.sh) — even when you're away from the terminal. ntfy works independently from audio: you can mute sounds and still receive push alerts.
|
|
95
|
+
|
|
96
|
+
### Setup
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
agent-noti ntfy
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This opens an interactive TUI where you can:
|
|
103
|
+
- Set your ntfy server and topic
|
|
104
|
+
- Toggle which events send push notifications (task complete / approval needed)
|
|
105
|
+
- Set priority level
|
|
106
|
+
- Send a test notification
|
|
107
|
+
|
|
108
|
+
On first run you'll be prompted for a topic name. Subscribe to the same topic in the [ntfy app](https://ntfy.sh) (Android/iOS/web) to receive notifications.
|
|
109
|
+
|
|
110
|
+
### Quick test
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
agent-noti ntfy-test
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Sends a test notification to your configured topic without opening the interactive TUI.
|
|
117
|
+
|
|
118
|
+
### Config fields
|
|
119
|
+
|
|
120
|
+
| Field | Default | Description |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `ntfy.enabled` | `false` | Master on/off |
|
|
123
|
+
| `ntfy.server` | `https://ntfy.sh` | ntfy server URL |
|
|
124
|
+
| `ntfy.topic` | — | Your topic name (required) |
|
|
125
|
+
| `ntfy.priority` | `default` | `min`, `low`, `default`, `high`, `urgent` |
|
|
126
|
+
| `ntfy.idle` | `true` | Notify on task complete |
|
|
127
|
+
| `ntfy.input` | `true` | Notify on approval needed |
|
|
128
|
+
|
|
90
129
|
## Config
|
|
91
130
|
|
|
92
131
|
All settings are stored in `~/.agent-noti/config.json`:
|
|
@@ -96,7 +135,15 @@ All settings are stored in `~/.agent-noti/config.json`:
|
|
|
96
135
|
"idle": "cow",
|
|
97
136
|
"input": "cow",
|
|
98
137
|
"volume": 10,
|
|
99
|
-
"muted": false
|
|
138
|
+
"muted": false,
|
|
139
|
+
"ntfy": {
|
|
140
|
+
"enabled": true,
|
|
141
|
+
"server": "https://ntfy.sh",
|
|
142
|
+
"topic": "my-agent-alerts",
|
|
143
|
+
"priority": "default",
|
|
144
|
+
"idle": true,
|
|
145
|
+
"input": true
|
|
146
|
+
}
|
|
100
147
|
}
|
|
101
148
|
```
|
|
102
149
|
|
package/bin/cli.mjs
CHANGED
|
@@ -421,6 +421,14 @@ function sounds() {
|
|
|
421
421
|
console.log(" Theme: idle=%s, input=%s", idleLabel, inputLabel);
|
|
422
422
|
const volBar = "#".repeat(vol) + "-".repeat(10 - vol);
|
|
423
423
|
console.log(` Volume: [${volBar}] ${vol}/10${muted ? " (MUTED)" : ""}`);
|
|
424
|
+
|
|
425
|
+
if (config.ntfy && config.ntfy.topic) {
|
|
426
|
+
const n = config.ntfy;
|
|
427
|
+
const server = (n.server || "https://ntfy.sh").replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
428
|
+
const status = n.enabled ? "enabled" : "disabled";
|
|
429
|
+
console.log(` ntfy: ${status} (${server}/${n.topic})`);
|
|
430
|
+
}
|
|
431
|
+
|
|
424
432
|
console.log("");
|
|
425
433
|
}
|
|
426
434
|
|
|
@@ -564,6 +572,232 @@ async function addCustom() {
|
|
|
564
572
|
console.log(" Custom sounds applied.\n");
|
|
565
573
|
}
|
|
566
574
|
|
|
575
|
+
// --- ntfy.sh push notifications ---
|
|
576
|
+
|
|
577
|
+
const NTFY_PRIORITIES = ["min", "low", "default", "high", "urgent"];
|
|
578
|
+
|
|
579
|
+
function ntfyPrompt(label) {
|
|
580
|
+
return new Promise((resolve) => {
|
|
581
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
582
|
+
rl.question(` ${label}`, (answer) => {
|
|
583
|
+
rl.close();
|
|
584
|
+
resolve(answer.trim());
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function ntfyInitialSetup() {
|
|
590
|
+
console.log("\n First-time ntfy.sh setup\n");
|
|
591
|
+
const topic = await ntfyPrompt("Topic (required): ");
|
|
592
|
+
if (!topic) {
|
|
593
|
+
console.log(" Topic is required. Aborting.\n");
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
const server = await ntfyPrompt("Server (enter for https://ntfy.sh): ");
|
|
597
|
+
return { topic, server: server || "https://ntfy.sh" };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function ntfySendTest(ntfyConfig) {
|
|
601
|
+
const server = (ntfyConfig.server || "https://ntfy.sh").replace(/\/+$/, "");
|
|
602
|
+
const url = `${server}/${ntfyConfig.topic}`;
|
|
603
|
+
const res = await fetch(url, {
|
|
604
|
+
method: "POST",
|
|
605
|
+
headers: {
|
|
606
|
+
Title: "Test Notification",
|
|
607
|
+
Priority: ntfyConfig.priority || "default",
|
|
608
|
+
Tags: "bell",
|
|
609
|
+
},
|
|
610
|
+
body: "agent-noti test notification",
|
|
611
|
+
});
|
|
612
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function ntfy() {
|
|
616
|
+
return new Promise(async (resolve) => {
|
|
617
|
+
if (!process.stdin.isTTY) {
|
|
618
|
+
console.log("\n This command requires an interactive terminal.\n");
|
|
619
|
+
resolve();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const config = readConfig();
|
|
624
|
+
|
|
625
|
+
// First-time setup: prompt for topic
|
|
626
|
+
if (!config.ntfy || !config.ntfy.topic) {
|
|
627
|
+
const setup = await ntfyInitialSetup();
|
|
628
|
+
if (!setup) { resolve(); return; }
|
|
629
|
+
config.ntfy = {
|
|
630
|
+
enabled: true,
|
|
631
|
+
server: setup.server,
|
|
632
|
+
topic: setup.topic,
|
|
633
|
+
priority: "default",
|
|
634
|
+
idle: true,
|
|
635
|
+
input: true,
|
|
636
|
+
};
|
|
637
|
+
writeConfig(config);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const ntfyConf = config.ntfy;
|
|
641
|
+
const rows = ["idle", "input"];
|
|
642
|
+
let selected = 0;
|
|
643
|
+
let statusMsg = "";
|
|
644
|
+
const totalLines = 12;
|
|
645
|
+
|
|
646
|
+
function render(firstTime) {
|
|
647
|
+
if (!firstTime) process.stdout.write(`\x1b[${totalLines}A`);
|
|
648
|
+
|
|
649
|
+
process.stdout.write("\x1b[2K\n");
|
|
650
|
+
process.stdout.write(`\x1b[2K \x1b[1mntfy.sh push notifications\x1b[0m\n`);
|
|
651
|
+
process.stdout.write("\x1b[2K\n");
|
|
652
|
+
process.stdout.write(`\x1b[2K Server: \x1b[36m${ntfyConf.server || "https://ntfy.sh"}\x1b[0m\n`);
|
|
653
|
+
process.stdout.write(`\x1b[2K Topic: \x1b[36m${ntfyConf.topic}\x1b[0m\n`);
|
|
654
|
+
process.stdout.write(`\x1b[2K Priority: \x1b[36m${ntfyConf.priority || "default"}\x1b[0m\n`);
|
|
655
|
+
process.stdout.write("\x1b[2K\n");
|
|
656
|
+
|
|
657
|
+
for (let i = 0; i < rows.length; i++) {
|
|
658
|
+
const checked = ntfyConf[rows[i]] ? "x" : " ";
|
|
659
|
+
const label = rows[i] === "idle" ? "Notify on task complete (idle)" : "Notify on approval needed (input)";
|
|
660
|
+
const arrow = i === selected ? "\x1b[36m> " : " ";
|
|
661
|
+
const color = i === selected ? "\x1b[36m" : "\x1b[0m";
|
|
662
|
+
process.stdout.write(`\x1b[2K ${arrow}${color}[${checked}] ${label}\x1b[0m\n`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
process.stdout.write("\x1b[2K\n");
|
|
666
|
+
const status = statusMsg ? ` ${statusMsg}` : "";
|
|
667
|
+
process.stdout.write(`\x1b[2K \x1b[90m[space] Toggle [up/down] Navigate [e] Edit [t] Test [q] Save & quit\x1b[0m${status}\n`);
|
|
668
|
+
process.stdout.write(`\x1b[2K\n`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const stdin = process.stdin;
|
|
672
|
+
stdin.setRawMode(true);
|
|
673
|
+
stdin.resume();
|
|
674
|
+
stdin.setEncoding("utf8");
|
|
675
|
+
|
|
676
|
+
render(true);
|
|
677
|
+
|
|
678
|
+
function cleanup() {
|
|
679
|
+
stdin.removeListener("data", onKey);
|
|
680
|
+
stdin.setRawMode(false);
|
|
681
|
+
stdin.pause();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function save() {
|
|
685
|
+
ntfyConf.enabled = ntfyConf.idle || ntfyConf.input;
|
|
686
|
+
config.ntfy = ntfyConf;
|
|
687
|
+
writeConfig(config);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function editFields() {
|
|
691
|
+
cleanup();
|
|
692
|
+
console.log("");
|
|
693
|
+
|
|
694
|
+
const newServer = await ntfyPrompt(`Server (${ntfyConf.server || "https://ntfy.sh"}): `);
|
|
695
|
+
if (newServer) ntfyConf.server = newServer;
|
|
696
|
+
|
|
697
|
+
const newTopic = await ntfyPrompt(`Topic (${ntfyConf.topic}): `);
|
|
698
|
+
if (newTopic) ntfyConf.topic = newTopic;
|
|
699
|
+
|
|
700
|
+
const priIdx = NTFY_PRIORITIES.indexOf(ntfyConf.priority || "default");
|
|
701
|
+
const newPri = await ntfyPrompt(`Priority [${NTFY_PRIORITIES.join("/")}] (${NTFY_PRIORITIES[priIdx]}): `);
|
|
702
|
+
if (newPri && NTFY_PRIORITIES.includes(newPri)) ntfyConf.priority = newPri;
|
|
703
|
+
|
|
704
|
+
// Re-enter raw mode and re-render
|
|
705
|
+
stdin.setRawMode(true);
|
|
706
|
+
stdin.resume();
|
|
707
|
+
stdin.setEncoding("utf8");
|
|
708
|
+
stdin.on("data", onKey);
|
|
709
|
+
render(true);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function testNotification() {
|
|
713
|
+
cleanup();
|
|
714
|
+
statusMsg = "\x1b[33mSending...\x1b[0m";
|
|
715
|
+
// Re-enter raw mode to render
|
|
716
|
+
stdin.setRawMode(true);
|
|
717
|
+
stdin.resume();
|
|
718
|
+
stdin.setEncoding("utf8");
|
|
719
|
+
render(true);
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
await ntfySendTest(ntfyConf);
|
|
723
|
+
statusMsg = "\x1b[32mSent!\x1b[0m";
|
|
724
|
+
} catch (e) {
|
|
725
|
+
statusMsg = `\x1b[31mFailed: ${e.message}\x1b[0m`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
stdin.on("data", onKey);
|
|
729
|
+
render();
|
|
730
|
+
setTimeout(() => { statusMsg = ""; render(); }, 3000);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function onKey(key) {
|
|
734
|
+
if (key === "\x03") {
|
|
735
|
+
cleanup();
|
|
736
|
+
save();
|
|
737
|
+
console.log("");
|
|
738
|
+
process.exit(0);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (key === "q" || key === "Q" || key === "\r" || key === "\n") {
|
|
742
|
+
cleanup();
|
|
743
|
+
save();
|
|
744
|
+
console.log(`\n ntfy config saved (${ntfyConf.enabled ? "enabled" : "disabled"}).\n`);
|
|
745
|
+
resolve();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (key === " ") {
|
|
750
|
+
ntfyConf[rows[selected]] = !ntfyConf[rows[selected]];
|
|
751
|
+
render();
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (key === "\x1b[A" || key === "k") {
|
|
756
|
+
selected = (selected - 1 + rows.length) % rows.length;
|
|
757
|
+
render();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (key === "\x1b[B" || key === "j") {
|
|
762
|
+
selected = (selected + 1) % rows.length;
|
|
763
|
+
render();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (key === "e" || key === "E") {
|
|
768
|
+
await editFields();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (key === "t" || key === "T") {
|
|
773
|
+
stdin.removeListener("data", onKey);
|
|
774
|
+
await testNotification();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
stdin.on("data", onKey);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function ntfyTest() {
|
|
784
|
+
const config = readConfig();
|
|
785
|
+
const ntfyConf = config.ntfy;
|
|
786
|
+
|
|
787
|
+
if (!ntfyConf || !ntfyConf.topic) {
|
|
788
|
+
console.log("\n ntfy not configured. Run 'agent-noti ntfy' first.\n");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
process.stdout.write("\n Sending test notification...");
|
|
793
|
+
try {
|
|
794
|
+
await ntfySendTest(ntfyConf);
|
|
795
|
+
console.log(" sent!\n");
|
|
796
|
+
} catch (e) {
|
|
797
|
+
console.log(` failed: ${e.message}\n`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
567
801
|
function mute() {
|
|
568
802
|
const config = readConfig();
|
|
569
803
|
config.muted = true;
|
|
@@ -604,7 +838,7 @@ function volume(args) {
|
|
|
604
838
|
|
|
605
839
|
function reset() {
|
|
606
840
|
writeConfig({ idle: "default", input: "default", volume: 10, muted: false });
|
|
607
|
-
console.log("\n Reset to defaults (theme=default, volume=10, unmuted).\n");
|
|
841
|
+
console.log("\n Reset to defaults (theme=default, volume=10, unmuted, ntfy cleared).\n");
|
|
608
842
|
}
|
|
609
843
|
|
|
610
844
|
function applyPickerChoice(choice) {
|
|
@@ -651,6 +885,8 @@ async function main() {
|
|
|
651
885
|
case "mute": case "m": mute(); break;
|
|
652
886
|
case "unmute": case "u": unmute(); break;
|
|
653
887
|
case "reset": case "r": reset(); break;
|
|
888
|
+
case "ntfy": case "n": await ntfy(); break;
|
|
889
|
+
case "ntfy-test": case "nt": await ntfyTest(); break;
|
|
654
890
|
default:
|
|
655
891
|
console.log("");
|
|
656
892
|
console.log(" agent-noti install (i) Add hooks + pick theme");
|
|
@@ -662,6 +898,8 @@ async function main() {
|
|
|
662
898
|
console.log(" agent-noti volume (v) Set volume <1-10>");
|
|
663
899
|
console.log(" agent-noti mute (m) Mute notifications");
|
|
664
900
|
console.log(" agent-noti unmute (u) Unmute notifications");
|
|
901
|
+
console.log(" agent-noti ntfy (n) Configure ntfy.sh push notifications");
|
|
902
|
+
console.log(" agent-noti ntfy-test (nt) Send a test push notification");
|
|
665
903
|
console.log(" agent-noti reset (r) Reset everything");
|
|
666
904
|
console.log("");
|
|
667
905
|
}
|
package/bin/play.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* node play.mjs --file <path> — plays a file directly
|
|
7
7
|
*
|
|
8
8
|
* Respects ~/.agent-noti/config.json for mute and volume (1-10).
|
|
9
|
+
* Also sends ntfy.sh push notifications when configured.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { execFile, exec } from "child_process";
|
|
@@ -69,36 +70,72 @@ function resolveSound(arg) {
|
|
|
69
70
|
return findFile(`${arg}-idle`) || findFile(arg) || null;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
const
|
|
73
|
-
|
|
73
|
+
const NTFY_MESSAGES = {
|
|
74
|
+
idle: { title: "Task Complete", tags: "white_check_mark", body: "Agent finished task" },
|
|
75
|
+
input: { title: "Approval Needed", tags: "warning", body: "Agent needs your approval" },
|
|
76
|
+
};
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
78
|
+
async function sendNtfy(event, config) {
|
|
79
|
+
try {
|
|
80
|
+
const ntfy = config.ntfy;
|
|
81
|
+
if (!ntfy || !ntfy.enabled || !ntfy.topic) return;
|
|
82
|
+
if (!ntfy[event]) return;
|
|
83
|
+
|
|
84
|
+
const msg = NTFY_MESSAGES[event];
|
|
85
|
+
if (!msg) return;
|
|
86
|
+
|
|
87
|
+
const server = (ntfy.server || "https://ntfy.sh").replace(/\/+$/, "");
|
|
88
|
+
const url = `${server}/${ntfy.topic}`;
|
|
89
|
+
|
|
90
|
+
await fetch(url, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
Title: msg.title,
|
|
94
|
+
Priority: ntfy.priority || "default",
|
|
95
|
+
Tags: msg.tags,
|
|
96
|
+
},
|
|
97
|
+
body: msg.body,
|
|
98
|
+
});
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
(async () => {
|
|
103
|
+
const arg = process.argv[2];
|
|
104
|
+
if (!arg) process.exit(0);
|
|
82
105
|
|
|
83
|
-
|
|
84
|
-
const vol = Math.max(1, Math.min(10, config.volume ?? 10));
|
|
85
|
-
const volFloat = vol / 10; // 0.1 – 1.0 (macOS, Windows)
|
|
86
|
-
const volPct = vol * 10; // 10 – 100 (Linux ffplay, mpv)
|
|
87
|
-
const volPulse = Math.round(volFloat * 65536); // paplay scale
|
|
106
|
+
const config = readConfig();
|
|
88
107
|
|
|
89
|
-
|
|
108
|
+
// Send ntfy push notification for actual events (not --file previews)
|
|
109
|
+
if (EVENTS.includes(arg)) {
|
|
110
|
+
await sendNtfy(arg, config);
|
|
111
|
+
}
|
|
90
112
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
// Mute check (skip for --file, which is used by picker previews)
|
|
114
|
+
if (arg !== "--file" && config.muted) process.exit(0);
|
|
115
|
+
|
|
116
|
+
const file = resolveSound(arg);
|
|
117
|
+
if (!file) process.exit(1);
|
|
118
|
+
|
|
119
|
+
// Volume: 1-10 config → 0.0-1.0 native scale
|
|
120
|
+
const vol = Math.max(1, Math.min(10, config.volume ?? 10));
|
|
121
|
+
const volFloat = vol / 10; // 0.1 – 1.0 (macOS, Windows)
|
|
122
|
+
const volPct = vol * 10; // 10 – 100 (Linux ffplay, mpv)
|
|
123
|
+
const volPulse = Math.round(volFloat * 65536); // paplay scale
|
|
124
|
+
|
|
125
|
+
const os = platform();
|
|
126
|
+
|
|
127
|
+
if (os === "darwin") {
|
|
128
|
+
execFile("afplay", ["-v", String(volFloat), file], () => {});
|
|
129
|
+
} else if (os === "win32") {
|
|
130
|
+
exec(
|
|
131
|
+
`powershell -NoProfile -Command "Add-Type -AssemblyName PresentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([uri]'${file.replace(/'/g, "''")}'); $p.Volume = ${volFloat}; $p.Play(); Start-Sleep -Seconds 3"`,
|
|
132
|
+
() => {}
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
execFile("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", String(volPct), file], (err) => {
|
|
136
|
+
if (err) execFile("paplay", ["--volume", String(volPulse), file], (err2) => {
|
|
137
|
+
if (err2) execFile("mpv", ["--no-video", `--volume=${volPct}`, file], () => {});
|
|
138
|
+
});
|
|
102
139
|
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
140
|
+
}
|
|
141
|
+
})();
|