@ciphore/radiocli 0.1.9 → 0.2.1
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/CHANGELOG.md +65 -0
- package/README.md +107 -411
- package/SECURITY.md +6 -3
- package/dist/activity/stats.js +14 -2
- package/dist/cli.js +53 -3
- package/dist/player/airplay-discovery.js +16 -4
- package/dist/player/airplay-worker-protocol.js +1 -0
- package/dist/player/airplay-worker.js +4 -1
- package/dist/player/command.js +37 -14
- package/dist/player/mpv-ipc-client.js +194 -0
- package/dist/player/player-controller.js +179 -98
- package/dist/providers/cache.js +77 -13
- package/dist/providers/provider-manager.js +28 -8
- package/dist/providers/radio-browser.js +141 -57
- package/dist/safety.js +44 -0
- package/dist/storage/store.js +264 -149
- package/dist/types.js +2 -53
- package/dist/ui/AdaptiveContent.js +332 -0
- package/dist/ui/App.js +489 -54
- package/dist/ui/AppContent.js +11 -12
- package/dist/ui/app-state.js +26 -16
- package/dist/ui/ascii.js +42 -1
- package/dist/ui/components/Logo.js +6 -1
- package/dist/ui/components/ScreenHeader.js +2 -2
- package/dist/ui/components/StationList.js +8 -6
- package/dist/ui/components/TopTabs.js +8 -7
- package/dist/ui/cosmo-land-data.js +1 -1
- package/dist/ui/exit-confirmation.js +7 -0
- package/dist/ui/explore-map-layout.js +3 -1
- package/dist/ui/format.js +56 -2
- package/dist/ui/help-content.js +10 -2
- package/dist/ui/layout.js +26 -9
- package/dist/ui/page-footer.js +36 -13
- package/dist/ui/playback-footer.js +15 -2
- package/dist/ui/screen-items.js +60 -21
- package/dist/ui/screen-meta.js +35 -0
- package/dist/ui/screens/AirPlaySettingsScreen.js +4 -2
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/ExploreScreen.js +1 -1
- package/dist/ui/screens/HelpScreen.js +17 -3
- package/dist/ui/screens/HomeScreen.js +2 -3
- package/dist/ui/screens/MapScreen.js +2 -2
- package/dist/ui/screens/NowPlayingScreen.js +31 -51
- package/dist/ui/screens/SearchScreen.js +1 -1
- package/dist/ui/screens/SettingsScreen.js +109 -10
- package/dist/ui/screens/StationScreen.js +14 -1
- package/dist/ui/screens/StatsScreen.js +35 -44
- package/dist/ui/system-actions.js +10 -3
- package/dist/ui/terminal-mouse.js +44 -0
- package/dist/ui/theme.js +0 -1
- package/dist/ui/use-app-input.js +37 -6
- package/dist/ui/use-command-executor.js +34 -1
- package/dist/ui/visualizers/receiver-style-registry.js +101 -0
- package/dist/ui/visualizers/receiver-visualizers.js +2856 -745
- package/dist/update-check.js +122 -0
- package/docs/THIRD_PARTY_NOTICES.md +7 -0
- package/package.json +2 -2
- package/dist/ui/screens/screen-render.test.js +0 -214
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,10 @@ import { PlayerController } from './player/player-controller.js';
|
|
|
8
8
|
import { JsonLibraryStore } from './storage/store.js';
|
|
9
9
|
import { parsePlaylistFile, stationFromUrl, writeM3u } from './playlists/playlist.js';
|
|
10
10
|
import { detectPlaybackBackends, playbackBackendStatusLines } from './player/backend-install.js';
|
|
11
|
+
import { resolveCommand } from './player/command.js';
|
|
12
|
+
import { airPlaySenderHealth } from './player/airplay-sender-health.js';
|
|
11
13
|
import { appVersion } from './version.js';
|
|
14
|
+
import { checkForUpdate, updateCommandForInstall } from './update-check.js';
|
|
12
15
|
if (isDirectRun(process.argv[1], import.meta.url)) {
|
|
13
16
|
const args = process.argv.slice(2);
|
|
14
17
|
if (args.length > 0) {
|
|
@@ -20,7 +23,8 @@ if (isDirectRun(process.argv[1], import.meta.url)) {
|
|
|
20
23
|
else {
|
|
21
24
|
const [{ render }, { App }] = await Promise.all([import('ink'), import('./ui/App.js')]);
|
|
22
25
|
render(_jsx(App, {}), {
|
|
23
|
-
|
|
26
|
+
// App owns Ctrl+C so it can confirm before performing a clean shutdown.
|
|
27
|
+
exitOnCtrlC: false,
|
|
24
28
|
kittyKeyboard: {
|
|
25
29
|
mode: 'auto',
|
|
26
30
|
flags: ['disambiguateEscapeCodes', 'reportEventTypes', 'reportAllKeysAsEscapeCodes']
|
|
@@ -43,10 +47,28 @@ export async function runCommand(args) {
|
|
|
43
47
|
}
|
|
44
48
|
if (command === 'doctor') {
|
|
45
49
|
const backends = detectPlaybackBackends();
|
|
50
|
+
if (rest.includes('--json')) {
|
|
51
|
+
console.log(JSON.stringify(doctorReport(backends), null, 2));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
46
54
|
console.log(`backends=${backends.join(',') || 'none'}`);
|
|
47
55
|
printPlaybackBackendStatus(backends);
|
|
48
56
|
return;
|
|
49
57
|
}
|
|
58
|
+
if (command === 'update') {
|
|
59
|
+
const updateCheck = await checkForUpdate();
|
|
60
|
+
const updateCommand = updateCommandForInstall();
|
|
61
|
+
if (updateCheck.error) {
|
|
62
|
+
console.log(`update_check=failed ${updateCheck.error}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(`installed=${appVersion()}`);
|
|
66
|
+
console.log(`latest=${updateCheck.latestVersion ?? 'unknown'}`);
|
|
67
|
+
console.log(`available=${updateCheck.updateAvailable ? 'yes' : 'no'}`);
|
|
68
|
+
}
|
|
69
|
+
console.log(`command=${updateCommand.command}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
50
72
|
if (command === 'check') {
|
|
51
73
|
const store = new JsonLibraryStore();
|
|
52
74
|
const providers = new ProviderManager();
|
|
@@ -120,7 +142,8 @@ Usage:
|
|
|
120
142
|
radiocli Start the TUI
|
|
121
143
|
radiocli version Print the installed version
|
|
122
144
|
radiocli check Show provider/backend health
|
|
123
|
-
radiocli doctor
|
|
145
|
+
radiocli doctor [--json] Show local playback setup guidance
|
|
146
|
+
radiocli update Show update availability and install command
|
|
124
147
|
radiocli countries Print top countries
|
|
125
148
|
radiocli search <query> Search public stations
|
|
126
149
|
radiocli import <file> Import .m3u, .pls, or .xspf streams
|
|
@@ -140,10 +163,37 @@ export function isDirectRun(entryPath, moduleUrl) {
|
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
function isKnownCommand(command) {
|
|
143
|
-
return ['check', 'doctor', 'countries', 'search', 'import', 'export', 'add-url'].includes(command);
|
|
166
|
+
return ['check', 'doctor', 'update', 'countries', 'search', 'import', 'export', 'add-url'].includes(command);
|
|
144
167
|
}
|
|
145
168
|
function printPlaybackBackendStatus(backends) {
|
|
146
169
|
for (const line of playbackBackendStatusLines(backends)) {
|
|
147
170
|
console.log(line);
|
|
148
171
|
}
|
|
149
172
|
}
|
|
173
|
+
function doctorReport(backends) {
|
|
174
|
+
const commands = Object.fromEntries(['mpv', 'ffplay', 'vlc', 'cvlc', 'ffmpeg', 'dns-sd'].map(command => [command, redactHome(resolveCommand(command))]));
|
|
175
|
+
const airPlay = airPlaySenderHealth();
|
|
176
|
+
return {
|
|
177
|
+
radioCliVersion: appVersion(),
|
|
178
|
+
nodeVersion: process.version,
|
|
179
|
+
platform: process.platform,
|
|
180
|
+
architecture: process.arch,
|
|
181
|
+
backends,
|
|
182
|
+
commands,
|
|
183
|
+
airPlay: {
|
|
184
|
+
available: airPlay.available,
|
|
185
|
+
safe: airPlay.safe,
|
|
186
|
+
package: airPlay.packageName,
|
|
187
|
+
version: airPlay.version ?? null,
|
|
188
|
+
vulnerablePackages: airPlay.vulnerablePackages,
|
|
189
|
+
warningPackages: airPlay.warningPackages
|
|
190
|
+
},
|
|
191
|
+
guidance: playbackBackendStatusLines(backends)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function redactHome(path) {
|
|
195
|
+
if (!path)
|
|
196
|
+
return null;
|
|
197
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
198
|
+
return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
199
|
+
}
|
|
@@ -129,9 +129,23 @@ function runDnsSd(args, timeoutMs, maxOutputBytes) {
|
|
|
129
129
|
const child = spawn('dns-sd', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
130
130
|
let output = '';
|
|
131
131
|
let killedForLimit = false;
|
|
132
|
+
let settled = false;
|
|
133
|
+
const finish = () => {
|
|
134
|
+
if (settled)
|
|
135
|
+
return;
|
|
136
|
+
settled = true;
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
clearTimeout(forceTimer);
|
|
139
|
+
resolve(output);
|
|
140
|
+
};
|
|
132
141
|
const timer = setTimeout(() => {
|
|
133
142
|
child.kill('SIGTERM');
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
if (child.exitCode === null)
|
|
145
|
+
child.kill('SIGKILL');
|
|
146
|
+
}, 250).unref();
|
|
134
147
|
}, timeoutMs);
|
|
148
|
+
const forceTimer = setTimeout(finish, timeoutMs + 750);
|
|
135
149
|
const appendOutput = (chunk) => {
|
|
136
150
|
if (Buffer.byteLength(output, 'utf8') >= maxOutputBytes) {
|
|
137
151
|
if (!killedForLimit) {
|
|
@@ -150,12 +164,10 @@ function runDnsSd(args, timeoutMs, maxOutputBytes) {
|
|
|
150
164
|
appendOutput(chunk);
|
|
151
165
|
});
|
|
152
166
|
child.once('error', () => {
|
|
153
|
-
|
|
154
|
-
resolve(output);
|
|
167
|
+
finish();
|
|
155
168
|
});
|
|
156
169
|
child.once('exit', () => {
|
|
157
|
-
|
|
158
|
-
resolve(output);
|
|
170
|
+
finish();
|
|
159
171
|
});
|
|
160
172
|
});
|
|
161
173
|
}
|
|
@@ -40,6 +40,7 @@ function validateWorkerStart(value) {
|
|
|
40
40
|
: [];
|
|
41
41
|
return {
|
|
42
42
|
streamUrl: boundedHttpUrl(value.streamUrl, 'streamUrl'),
|
|
43
|
+
ffmpegPath: boundedString(value.ffmpegPath, 'ffmpegPath', 4096),
|
|
43
44
|
stationName: boundedText(value.stationName, 'stationName', maxWorkerTextBytes),
|
|
44
45
|
volume: clampVolume(Number(value.volume)),
|
|
45
46
|
muted: typeof value.muted === 'boolean' ? value.muted : false,
|
|
@@ -108,7 +108,7 @@ function handleCommand(command) {
|
|
|
108
108
|
}
|
|
109
109
|
function startFfmpeg(streamUrl) {
|
|
110
110
|
const previous = ffmpeg;
|
|
111
|
-
const child = spawn(
|
|
111
|
+
const child = spawn(start.ffmpegPath, [
|
|
112
112
|
'-hide_banner',
|
|
113
113
|
'-loglevel',
|
|
114
114
|
'error',
|
|
@@ -208,7 +208,10 @@ function stop(code) {
|
|
|
208
208
|
ffmpeg.kill('SIGTERM');
|
|
209
209
|
}
|
|
210
210
|
ffmpeg = null;
|
|
211
|
+
const watchdog = setTimeout(() => process.exit(code), 1500);
|
|
212
|
+
watchdog.unref();
|
|
211
213
|
airtunes.stopAll(() => {
|
|
214
|
+
clearTimeout(watchdog);
|
|
212
215
|
emit({ type: 'stopped' });
|
|
213
216
|
process.exit(code);
|
|
214
217
|
});
|
package/dist/player/command.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { accessSync, constants, existsSync } from 'node:fs';
|
|
3
2
|
import { homedir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
export function commandExists(command) {
|
|
@@ -11,23 +10,31 @@ export function commandExists(command) {
|
|
|
11
10
|
// package-manager shim looks "missing" to a bare PATH lookup. We therefore fall
|
|
12
11
|
// back to probing well-known install locations before giving up.
|
|
13
12
|
export function resolveCommand(command) {
|
|
14
|
-
|
|
13
|
+
const cached = commandCache.get(command);
|
|
14
|
+
if (cached && Date.now() - cached.checkedAt < commandCacheTtlMs) {
|
|
15
|
+
return cached.path;
|
|
16
|
+
}
|
|
17
|
+
const path = lookupOnPath(command) ?? probeKnownLocations(command);
|
|
18
|
+
commandCache.set(command, { path, checkedAt: Date.now() });
|
|
19
|
+
return path;
|
|
15
20
|
}
|
|
16
21
|
function lookupOnPath(command) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
if (command.includes('/') || command.includes('\\')) {
|
|
23
|
+
return isRunnable(command) ? command : null;
|
|
24
|
+
}
|
|
25
|
+
const pathEntries = (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
|
|
26
|
+
for (const directory of pathEntries) {
|
|
27
|
+
for (const name of candidateNames(command)) {
|
|
28
|
+
const candidate = join(directory.replace(/^"|"$/g, ''), name);
|
|
29
|
+
if (isRunnable(candidate))
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
21
32
|
}
|
|
22
|
-
|
|
23
|
-
.split(/\r?\n/)
|
|
24
|
-
.map(line => line.trim())
|
|
25
|
-
.find(Boolean);
|
|
26
|
-
return first ?? null;
|
|
33
|
+
return null;
|
|
27
34
|
}
|
|
28
35
|
function probeKnownLocations(command) {
|
|
29
36
|
for (const candidate of candidatePaths(command)) {
|
|
30
|
-
if (
|
|
37
|
+
if (isRunnable(candidate)) {
|
|
31
38
|
return candidate;
|
|
32
39
|
}
|
|
33
40
|
}
|
|
@@ -46,10 +53,26 @@ function candidatePaths(command) {
|
|
|
46
53
|
}
|
|
47
54
|
function candidateNames(command) {
|
|
48
55
|
if (process.platform === 'win32' && !/\.[a-z0-9]+$/i.test(command)) {
|
|
49
|
-
|
|
56
|
+
const extensions = (process.env.PATHEXT ?? '.EXE;.COM;.BAT;.CMD').split(';').filter(Boolean);
|
|
57
|
+
return [...extensions.map(extension => `${command}${extension.toLowerCase()}`), command];
|
|
50
58
|
}
|
|
51
59
|
return [command];
|
|
52
60
|
}
|
|
61
|
+
const commandCacheTtlMs = 5000;
|
|
62
|
+
const commandCache = new Map();
|
|
63
|
+
function isRunnable(path) {
|
|
64
|
+
if (!existsSync(path))
|
|
65
|
+
return false;
|
|
66
|
+
if (process.platform === 'win32')
|
|
67
|
+
return true;
|
|
68
|
+
try {
|
|
69
|
+
accessSync(path, constants.X_OK);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
53
76
|
function knownBinaryDirs() {
|
|
54
77
|
const home = homedir();
|
|
55
78
|
if (process.platform === 'darwin') {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Socket } from 'node:net';
|
|
2
|
+
/**
|
|
3
|
+
* Multiplexes mpv JSON IPC requests over one reusable socket.
|
|
4
|
+
*
|
|
5
|
+
* mpv may also emit unsolicited event objects on this connection; responses
|
|
6
|
+
* are therefore matched strictly by request_id. A dropped connection rejects
|
|
7
|
+
* every in-flight request, while the next query reconnects lazily.
|
|
8
|
+
*/
|
|
9
|
+
export class MpvIpcClient {
|
|
10
|
+
path;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
socket = null;
|
|
13
|
+
connectingSocket = null;
|
|
14
|
+
connecting = null;
|
|
15
|
+
pending = new Map();
|
|
16
|
+
buffer = '';
|
|
17
|
+
nextRequestId = 1;
|
|
18
|
+
closed = false;
|
|
19
|
+
constructor(path, timeoutMs = 1000) {
|
|
20
|
+
this.path = path;
|
|
21
|
+
this.timeoutMs = timeoutMs;
|
|
22
|
+
}
|
|
23
|
+
async query(payload) {
|
|
24
|
+
if (this.closed) {
|
|
25
|
+
throw new Error('mpv IPC client is closed.');
|
|
26
|
+
}
|
|
27
|
+
const socket = await this.connect();
|
|
28
|
+
const requestId = this.allocateRequestId();
|
|
29
|
+
const requestPayload = attachMpvRequestId(payload, requestId);
|
|
30
|
+
const message = `${JSON.stringify(requestPayload)}\n`;
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const timeout = setTimeout(() => {
|
|
33
|
+
this.settleRequest(requestId, pending => pending.reject(new Error('mpv IPC timed out.')));
|
|
34
|
+
}, this.timeoutMs);
|
|
35
|
+
this.pending.set(requestId, {
|
|
36
|
+
resolve: value => resolve((value ?? null)),
|
|
37
|
+
reject,
|
|
38
|
+
timeout
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
socket.write(message, error => {
|
|
42
|
+
if (error) {
|
|
43
|
+
this.settleRequest(requestId, pending => pending.reject(error));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
this.settleRequest(requestId, pending => pending.reject(error instanceof Error ? error : new Error('mpv IPC write failed.')));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
close(reason = new Error('mpv IPC connection closed.')) {
|
|
53
|
+
if (this.closed) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.closed = true;
|
|
57
|
+
this.rejectPending(reason);
|
|
58
|
+
this.socket?.destroy();
|
|
59
|
+
this.connectingSocket?.destroy();
|
|
60
|
+
this.socket = null;
|
|
61
|
+
this.connectingSocket = null;
|
|
62
|
+
this.connecting = null;
|
|
63
|
+
this.buffer = '';
|
|
64
|
+
}
|
|
65
|
+
connect() {
|
|
66
|
+
if (this.socket && !this.socket.destroyed) {
|
|
67
|
+
return Promise.resolve(this.socket);
|
|
68
|
+
}
|
|
69
|
+
if (this.connecting) {
|
|
70
|
+
return this.connecting;
|
|
71
|
+
}
|
|
72
|
+
const socket = new Socket();
|
|
73
|
+
this.connectingSocket = socket;
|
|
74
|
+
const connecting = new Promise((resolve, reject) => {
|
|
75
|
+
let settled = false;
|
|
76
|
+
const connectionTimeout = setTimeout(() => failConnection(new Error('mpv IPC connection timed out.')), this.timeoutMs);
|
|
77
|
+
const closeBeforeConnection = () => failConnection(new Error('mpv IPC connection closed before it was ready.'));
|
|
78
|
+
const failConnection = (error) => {
|
|
79
|
+
if (settled) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
settled = true;
|
|
83
|
+
clearTimeout(connectionTimeout);
|
|
84
|
+
socket.off('connect', finishConnection);
|
|
85
|
+
socket.off('close', closeBeforeConnection);
|
|
86
|
+
socket.destroy();
|
|
87
|
+
reject(error);
|
|
88
|
+
};
|
|
89
|
+
const finishConnection = () => {
|
|
90
|
+
if (settled) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
settled = true;
|
|
94
|
+
clearTimeout(connectionTimeout);
|
|
95
|
+
socket.off('error', failConnection);
|
|
96
|
+
socket.off('close', closeBeforeConnection);
|
|
97
|
+
if (this.closed) {
|
|
98
|
+
socket.destroy();
|
|
99
|
+
reject(new Error('mpv IPC client is closed.'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.socket = socket;
|
|
103
|
+
this.connectingSocket = null;
|
|
104
|
+
this.buffer = '';
|
|
105
|
+
socket.on('data', chunk => this.consume(chunk.toString('utf8')));
|
|
106
|
+
socket.on('error', error => this.dropSocket(socket, error));
|
|
107
|
+
socket.on('close', () => this.dropSocket(socket, new Error('mpv IPC connection closed.')));
|
|
108
|
+
resolve(socket);
|
|
109
|
+
};
|
|
110
|
+
socket.once('error', failConnection);
|
|
111
|
+
socket.once('close', closeBeforeConnection);
|
|
112
|
+
socket.once('connect', finishConnection);
|
|
113
|
+
socket.connect(this.path);
|
|
114
|
+
});
|
|
115
|
+
let trackedConnection;
|
|
116
|
+
trackedConnection = connecting.finally(() => {
|
|
117
|
+
if (this.connecting === trackedConnection) {
|
|
118
|
+
this.connecting = null;
|
|
119
|
+
}
|
|
120
|
+
if (this.connectingSocket === socket && !this.socket) {
|
|
121
|
+
this.connectingSocket = null;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
this.connecting = trackedConnection;
|
|
125
|
+
return trackedConnection;
|
|
126
|
+
}
|
|
127
|
+
consume(chunk) {
|
|
128
|
+
this.buffer += chunk;
|
|
129
|
+
let newlineIndex = this.buffer.indexOf('\n');
|
|
130
|
+
while (newlineIndex !== -1) {
|
|
131
|
+
const line = this.buffer.slice(0, newlineIndex);
|
|
132
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
133
|
+
newlineIndex = this.buffer.indexOf('\n');
|
|
134
|
+
if (!line.trim()) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
let response;
|
|
138
|
+
try {
|
|
139
|
+
response = JSON.parse(line);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (typeof response.request_id !== 'number' || !this.pending.has(response.request_id)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (response.error && response.error !== 'success') {
|
|
148
|
+
this.settleRequest(response.request_id, pending => pending.reject(new Error(`mpv IPC failed: ${response.error}`)));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.settleRequest(response.request_id, pending => pending.resolve(response.data));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
dropSocket(socket, error) {
|
|
156
|
+
if (this.socket !== socket) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.socket = null;
|
|
160
|
+
this.buffer = '';
|
|
161
|
+
this.rejectPending(error);
|
|
162
|
+
}
|
|
163
|
+
rejectPending(error) {
|
|
164
|
+
for (const requestId of [...this.pending.keys()]) {
|
|
165
|
+
this.settleRequest(requestId, pending => pending.reject(error));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
settleRequest(requestId, settle) {
|
|
169
|
+
const pending = this.pending.get(requestId);
|
|
170
|
+
if (!pending) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.pending.delete(requestId);
|
|
174
|
+
clearTimeout(pending.timeout);
|
|
175
|
+
settle(pending);
|
|
176
|
+
}
|
|
177
|
+
allocateRequestId() {
|
|
178
|
+
while (this.pending.has(this.nextRequestId)) {
|
|
179
|
+
this.nextRequestId = incrementRequestId(this.nextRequestId);
|
|
180
|
+
}
|
|
181
|
+
const requestId = this.nextRequestId;
|
|
182
|
+
this.nextRequestId = incrementRequestId(this.nextRequestId);
|
|
183
|
+
return requestId;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function attachMpvRequestId(payload, requestId) {
|
|
187
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
188
|
+
return { ...payload, request_id: requestId };
|
|
189
|
+
}
|
|
190
|
+
return { command: payload, request_id: requestId };
|
|
191
|
+
}
|
|
192
|
+
function incrementRequestId(requestId) {
|
|
193
|
+
return requestId >= Number.MAX_SAFE_INTEGER ? 1 : requestId + 1;
|
|
194
|
+
}
|