@ciphore/radiocli 0.1.3 → 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/CHANGELOG.md +35 -3
- package/README.md +23 -27
- package/dist/player/airplay-discovery.js +34 -1
- package/dist/player/airplay-sender-health.js +19 -8
- package/dist/player/airplay-sender-patch.js +138 -0
- package/dist/player/airplay-worker-protocol.js +22 -7
- package/dist/player/airplay-worker.js +79 -39
- package/dist/player/backend-install.js +1 -1
- package/dist/player/player-controller.js +253 -36
- package/dist/storage/store.js +14 -5
- package/dist/ui/App.js +198 -25
- package/dist/ui/AppContent.js +10 -1
- package/dist/ui/airplay-settings.js +56 -0
- package/dist/ui/app-state.js +11 -15
- package/dist/ui/audio-output.js +42 -0
- package/dist/ui/page-footer.js +10 -2
- package/dist/ui/screen-items.js +3 -3
- package/dist/ui/screens/AirPlayCodeScreen.js +18 -0
- package/dist/ui/screens/AirPlaySettingsScreen.js +24 -0
- package/dist/ui/screens/SettingsScreen.js +8 -8
- package/dist/ui/use-app-input.js +38 -7
- package/dist/ui/use-command-executor.js +11 -3
- package/package.json +5 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { airPlayAvailability, airPlayDeviceDetail, selectedAirPlayDevice } from '../airplay-settings.js';
|
|
4
|
+
import { audioOutputLabel } from '../audio-output.js';
|
|
5
|
+
import { Menu, Pointer } from '../components/Menu.js';
|
|
6
|
+
import { ScreenHeader } from '../components/ScreenHeader.js';
|
|
7
|
+
import { truncate } from '../format.js';
|
|
8
|
+
import { textDim, themeAccent } from '../theme.js';
|
|
9
|
+
export function AirPlaySettingsScreen({ selected, settings, backends, devices, theme, width }) {
|
|
10
|
+
const accent = themeAccent(theme);
|
|
11
|
+
const availability = airPlayAvailability(backends, devices);
|
|
12
|
+
const selectedDevice = selectedAirPlayDevice(settings, devices);
|
|
13
|
+
const selectedMissing = Boolean(settings.preferredAirPlayDevice && !selectedDevice);
|
|
14
|
+
const canEnterCode = availability.ready && selectedDevice?.requiresPassword && !selectedDevice.local;
|
|
15
|
+
const lineWidth = Math.max(24, width - 4);
|
|
16
|
+
const preferredOutput = audioOutputLabel(settings.preferredBackend);
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "AirPlay", subtitle: "Choose where AirPlay playback should go", right: availability.label, width: width, theme: theme }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: ["Audio output: ", _jsx(Text, { color: accent, children: preferredOutput }), " \u00B7 Streaming:", ' ', _jsx(Text, { color: availability.ready ? accent : 'yellow', children: availability.ready ? 'ready' : 'unavailable' }), " \u00B7 Receivers:", ' ', _jsx(Text, { color: accent, children: devices.length })] }), _jsx(Text, { color: availability.ready ? 'gray' : 'yellow', children: truncate(availability.detail, lineWidth) }), _jsxs(Text, { color: "gray", children: ["Selected: ", _jsx(Text, { color: selectedDevice ? accent : textDim, children: truncate(selectedDevice?.name ?? 'none', Math.max(4, lineWidth - 10)) })] }), availability.ready && selectedDevice?.requiresPassword && !selectedDevice.local ? (_jsxs(Text, { color: "gray", children: ["Code: ", _jsx(Text, { color: accent, children: "if the receiver shows a code while tuning, RadioCLI will ask for it" })] })) : null, selectedMissing ? (_jsxs(Text, { color: "yellow", children: ["Saved receiver is not visible: ", truncate(settings.preferredAirPlayDevice ?? '', Math.max(8, lineWidth - 31))] })) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: "Receivers" }), devices.length > 0 ? (_jsx(Menu, { items: devices, selected: selected, keyFor: device => device.id, render: (device, _index, active) => {
|
|
18
|
+
const isSelected = device.id === settings.preferredAirPlayDevice;
|
|
19
|
+
const name = truncate(device.name, Math.max(8, Math.floor(lineWidth * 0.35)));
|
|
20
|
+
const selectedLabel = isSelected ? ' selected' : '';
|
|
21
|
+
const detail = truncate(airPlayDeviceDetail(device), Math.max(12, lineWidth - name.length - selectedLabel.length - 5));
|
|
22
|
+
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsx(Text, { color: active ? accent : undefined, bold: active, children: name }), isSelected ? _jsx(Text, { color: accent, children: selectedLabel }) : null, _jsxs(Text, { color: "gray", children: [" \u00B7 ", detail] })] }));
|
|
23
|
+
} })) : (_jsx(Text, { color: "gray", children: "No receivers discovered." })), _jsx(Text, { color: textDim, children: canEnterCode ? 'Enter selects · c opens code entry · r refreshes receivers' : 'Enter selects · r refreshes receivers' })] })] }));
|
|
24
|
+
}
|
|
@@ -5,7 +5,9 @@ import { ScreenHeader } from '../components/ScreenHeader.js';
|
|
|
5
5
|
import { truncate } from '../format.js';
|
|
6
6
|
import { settingsItems } from '../screen-items.js';
|
|
7
7
|
import { themeAccent } from '../theme.js';
|
|
8
|
-
import { playbackBackendCapabilities
|
|
8
|
+
import { playbackBackendCapabilities } from '../../player/backend-install.js';
|
|
9
|
+
import { airPlayReceiverSettingValue } from '../airplay-settings.js';
|
|
10
|
+
import { audioOutputLabel, audioOutputSettingValue } from '../audio-output.js';
|
|
9
11
|
export function SettingsScreen({ selected, settings, storePath, playback, backends, airPlayDevices, providerHealth, theme, diagnostics, width }) {
|
|
10
12
|
const accent = themeAccent(theme);
|
|
11
13
|
const lineWidth = Math.max(32, width - 4);
|
|
@@ -13,7 +15,7 @@ export function SettingsScreen({ selected, settings, storePath, playback, backen
|
|
|
13
15
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ScreenHeader, { title: "Settings", subtitle: "Enter changes the highlighted setting \u00B7 shortcuts in the footer", width: width, theme: theme }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Menu, { items: settingsItems, selected: selected, keyFor: item => item, render: (item, _index, active) => {
|
|
14
16
|
const value = settingValue(item, settings, diagnostics, backends, airPlayDevices);
|
|
15
17
|
return (_jsxs(Box, { children: [_jsx(Pointer, { active: active }), _jsx(Text, { color: active ? accent : undefined, bold: active, children: item }), value ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u00B7 " }), _jsx(Text, { color: accent, children: value })] })) : null] }));
|
|
16
|
-
} }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: "Status" }), _jsxs(Text, { color: "gray", children: ["
|
|
18
|
+
} }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: "Status" }), _jsxs(Text, { color: "gray", children: ["Output: ", _jsx(Text, { color: accent, children: audioOutputLabel(playback.backend) }), " / ", playback.state, " \u00B7", ' ', "Selected: ", _jsx(Text, { color: accent, children: audioOutputLabel(settings.preferredBackend) }), " \u00B7", ' ', diagnostics.muted ? 'muted' : `vol ${diagnostics.volume}`, " \u00B7 tune timeout ", settings.tuneTimeoutSeconds, "s"] }), _jsxs(Text, { color: "gray", children: ["Provider health: ", health.length ? health.map(([provider, status]) => `${provider} ${status}`).join(' · ') : 'not checked yet'] }), _jsxs(Text, { color: "gray", children: ["Active stream: ", truncate(diagnostics.streamUrl ?? 'none', lineWidth - 15)] }), _jsxs(Text, { color: "gray", children: ["Library: ", truncate(storePath, lineWidth - 9)] })] })] }));
|
|
17
19
|
}
|
|
18
20
|
function settingValue(item, settings, diagnostics, backends, airPlayDevices) {
|
|
19
21
|
switch (item) {
|
|
@@ -25,12 +27,10 @@ function settingValue(item, settings, diagnostics, backends, airPlayDevices) {
|
|
|
25
27
|
return settings.enableRadioGarden ? 'on' : 'off';
|
|
26
28
|
case 'Toggle nearby location lookup':
|
|
27
29
|
return settings.enableNearbyLocation ? 'on' : 'off';
|
|
28
|
-
case '
|
|
29
|
-
return
|
|
30
|
-
case '
|
|
31
|
-
|
|
32
|
-
return `${device?.name ?? settings.preferredAirPlayDevice ?? 'auto'} · ${airPlayDevices.length || 'no'} found`;
|
|
33
|
-
}
|
|
30
|
+
case 'Audio output':
|
|
31
|
+
return audioOutputSettingValue(settings, diagnostics, backends);
|
|
32
|
+
case 'AirPlay receiver':
|
|
33
|
+
return airPlayReceiverSettingValue(settings, airPlayDevices, backends);
|
|
34
34
|
case 'Mute or unmute':
|
|
35
35
|
if (diagnostics.backend === 'ffplay' && !playbackBackendCapabilities(diagnostics.backend).supportsMute) {
|
|
36
36
|
return 'requires mpv';
|
package/dist/ui/use-app-input.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useInput } from 'ink';
|
|
|
3
3
|
import { homeItems, settingsItems } from './screen-items.js';
|
|
4
4
|
import { applyTextInput, clamp, favoriteTarget, isEditableInput, isPlainPrintableInput, mediaTransportActionForInput, shouldHandleKeyboardEvent } from './app-state.js';
|
|
5
5
|
import { parseSgrMouseEvents, primaryMousePress } from './terminal-mouse.js';
|
|
6
|
-
export function useAppInput({ adjustVolume, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, currentItemCount,
|
|
6
|
+
export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, currentItemCount, cycleDisplayColor, cycleAudioOutput, cycleReceiverStyle, cycleSleepTimer, editingCountryFilter, editingSearch, executeCommand, filteredCountries, go, lastRawTransportAtRef, lastSubmittedSearchRef, loadCountry, openAdjacentTab, openAirPlayCode, openAirPlaySettings, openScreen, playAdjacent, playStation, player, playingStation, moveExploreCursor, moveExploreCursorToCell, refreshAirPlayTargets, refreshProviderHealth, resetLearnedTransportKeys, runSearch, saveLearnedTransportKey, screen, searchQuery, selected, selectedStation, selectAirPlayDeviceAt, setCapturingTransportAction, setAirPlayCode, setCommandMode, setCommandText, setCountryFilter, setEditingCountryFilter, setEditingSearch, setMessage, setSearchQuery, setSelected, setShowDiagnostics, submitAirPlayCode, settingsRef, shutdown, stdin, toggleFavorite, toggleMute, togglePause, toggleNearbyLocation, toggleRadioGarden, toggleSkipBrokenStreams }) {
|
|
7
7
|
useEffect(() => {
|
|
8
8
|
const onData = (data) => {
|
|
9
9
|
const rawInput = String(data);
|
|
@@ -149,6 +149,21 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
|
|
|
149
149
|
}
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
|
+
if (screen === 'airplay-code') {
|
|
153
|
+
if (key.return) {
|
|
154
|
+
submitAirPlayCode(airPlayCode);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (key.escape) {
|
|
158
|
+
setAirPlayCode('');
|
|
159
|
+
go('airplay-settings');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (isEditableInput(input, key)) {
|
|
163
|
+
setAirPlayCode(value => applyTextInput(value, input, key).slice(0, 64));
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
152
167
|
if (screen === 'explore') {
|
|
153
168
|
const exploreMove = exploreMoveForInput(input);
|
|
154
169
|
if (exploreMove) {
|
|
@@ -200,11 +215,15 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
|
|
|
200
215
|
return;
|
|
201
216
|
}
|
|
202
217
|
if (input === 'o') {
|
|
203
|
-
|
|
218
|
+
cycleAudioOutput();
|
|
204
219
|
return;
|
|
205
220
|
}
|
|
206
221
|
if (input === 'a' && screen === 'settings') {
|
|
207
|
-
|
|
222
|
+
openAirPlaySettings();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (input === 'c' && screen === 'airplay-settings' && canEnterAirPlayCode) {
|
|
226
|
+
openAirPlayCode();
|
|
208
227
|
return;
|
|
209
228
|
}
|
|
210
229
|
if (input === 'g') {
|
|
@@ -219,6 +238,10 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
|
|
|
219
238
|
toggleSkipBrokenStreams();
|
|
220
239
|
return;
|
|
221
240
|
}
|
|
241
|
+
if (input === 'r' && screen === 'airplay-settings') {
|
|
242
|
+
refreshAirPlayTargets();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
222
245
|
if (input === 'r') {
|
|
223
246
|
refreshProviderHealth();
|
|
224
247
|
setMessage('Provider health refreshed.');
|
|
@@ -249,6 +272,10 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
|
|
|
249
272
|
}
|
|
250
273
|
return;
|
|
251
274
|
}
|
|
275
|
+
if ((input === 'b' || key.escape) && screen === 'airplay-settings') {
|
|
276
|
+
go('settings');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
252
279
|
if (input === 'b' || key.escape) {
|
|
253
280
|
go('home');
|
|
254
281
|
return;
|
|
@@ -331,11 +358,11 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
|
|
|
331
358
|
else if (item === 'Toggle nearby location lookup') {
|
|
332
359
|
toggleNearbyLocation();
|
|
333
360
|
}
|
|
334
|
-
else if (item === '
|
|
335
|
-
|
|
361
|
+
else if (item === 'Audio output') {
|
|
362
|
+
cycleAudioOutput();
|
|
336
363
|
}
|
|
337
|
-
else if (item === '
|
|
338
|
-
|
|
364
|
+
else if (item === 'AirPlay receiver') {
|
|
365
|
+
openAirPlaySettings();
|
|
339
366
|
}
|
|
340
367
|
else if (item === 'Volume up') {
|
|
341
368
|
adjustVolume(5);
|
|
@@ -367,6 +394,10 @@ export function useAppInput({ adjustVolume, beginLearningTransportKey, capturing
|
|
|
367
394
|
}
|
|
368
395
|
return;
|
|
369
396
|
}
|
|
397
|
+
if (screen === 'airplay-settings') {
|
|
398
|
+
selectAirPlayDeviceAt(selected);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
370
401
|
if (screen === 'map') {
|
|
371
402
|
const country = filteredCountries[selected];
|
|
372
403
|
if (country) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { favoriteTarget, normalizeMediaKeyBindings, parseMediaActionName } from './app-state.js';
|
|
3
|
-
export function useCommandExecutor({ beginLearningTransportKey, countries, go, loadCountry, openLibrary, player, playingStation, providers, resetLearnedTransportKeys, runSearch, screen, selectedStation, setCountries, setFilters, setLibrary, setMessage, setSearchQuery, setSleepUntil, setVolume, settingsRef, store, toggleFavorite, toggleMute, updateSettings }) {
|
|
3
|
+
export function useCommandExecutor({ beginLearningTransportKey, countries, go, loadCountry, openAirPlaySettings, openLibrary, player, playingStation, providers, resetLearnedTransportKeys, runSearch, screen, selectedStation, setCountries, setFilters, setLibrary, setMessage, setSearchQuery, setSleepUntil, setVolume, settingsRef, store, toggleFavorite, toggleMute, updateSettings }) {
|
|
4
4
|
return useCallback(async (rawCommand) => {
|
|
5
5
|
const trimmed = rawCommand.trim();
|
|
6
6
|
if (!trimmed) {
|
|
@@ -98,10 +98,17 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
98
98
|
}
|
|
99
99
|
if (name === 'airplay-code' || name === 'airplay-passcode') {
|
|
100
100
|
if (!value.trim()) {
|
|
101
|
-
|
|
101
|
+
go('airplay-code');
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
-
player.submitAirPlayPasscode(value.trim());
|
|
104
|
+
const result = player.submitAirPlayPasscode(value.trim());
|
|
105
|
+
if (result.message) {
|
|
106
|
+
setMessage(result.message);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (name === 'airplay' || name === 'airplay-settings') {
|
|
111
|
+
openAirPlaySettings();
|
|
105
112
|
return;
|
|
106
113
|
}
|
|
107
114
|
if (name === 'sleep') {
|
|
@@ -140,6 +147,7 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
140
147
|
countries,
|
|
141
148
|
go,
|
|
142
149
|
loadCountry,
|
|
150
|
+
openAirPlaySettings,
|
|
143
151
|
openLibrary,
|
|
144
152
|
player,
|
|
145
153
|
playingStation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ciphore/radiocli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A terminal-first world radio receiver built with Ink, mpv, and resilient public-radio providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,9 +66,13 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"ink": "^7.0.5",
|
|
69
|
+
"node-airtunes2": "2.5.0",
|
|
69
70
|
"react": "^19.2.6",
|
|
70
71
|
"zod": "^4.4.3"
|
|
71
72
|
},
|
|
73
|
+
"overrides": {
|
|
74
|
+
"protobufjs": "^7.5.8"
|
|
75
|
+
},
|
|
72
76
|
"devDependencies": {
|
|
73
77
|
"@types/node": "^25.9.1",
|
|
74
78
|
"@types/react": "^19.2.15",
|