@ciphore/radiocli 0.1.0 → 0.1.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 +7 -4
- package/CONTRIBUTING.md +5 -1
- package/README.md +115 -20
- package/dist/player/backend-install.js +2 -2
- package/dist/player/player-controller.js +12 -3
- package/dist/ui/screens/StatsScreen.js +2 -1
- package/dist/ui/theme.js +15 -14
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,7 @@ All notable changes to RadioCLI are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.1.
|
|
8
|
+
## [0.1.1] - 2026-05-30
|
|
9
9
|
|
|
10
10
|
Initial public release.
|
|
11
11
|
|
|
@@ -19,7 +19,7 @@ Initial public release.
|
|
|
19
19
|
resolution, plus an experimental, opt-in Radio Garden provider.
|
|
20
20
|
- `mpv` playback with `ffplay` fallback, readiness checks, ICY metadata polling,
|
|
21
21
|
volume, pause, mute, sleep timer, tune timeouts, and skip-broken-stream
|
|
22
|
-
behavior.
|
|
22
|
+
behavior, using Unix sockets on macOS/Linux and named pipes on native Windows.
|
|
23
23
|
- Receiver-style Now Playing screen with 50 selectable visualizers and
|
|
24
24
|
zero-signal frames whenever playback is idle, paused, stopped, or not
|
|
25
25
|
backend-ready.
|
|
@@ -34,7 +34,10 @@ Initial public release.
|
|
|
34
34
|
streaks, active days, and total hours listened.
|
|
35
35
|
- `radiocli doctor` to report local playback readiness and print OS-specific
|
|
36
36
|
setup guidance.
|
|
37
|
-
- Homebrew formula
|
|
37
|
+
- Published Homebrew tap formula for `brew install ciphore/tap/radiocli`.
|
|
38
|
+
- Documented native Linux and Windows install paths, with `winget` guidance for
|
|
39
|
+
Node.js, `mpv`, and optional FFmpeg on Windows.
|
|
40
|
+
- Documented pnpm and Bun as optional npm-package install clients.
|
|
38
41
|
|
|
39
42
|
### Changed
|
|
40
43
|
|
|
@@ -44,4 +47,4 @@ Initial public release.
|
|
|
44
47
|
backend/volume that already appear in the header and footer. The reclaimed
|
|
45
48
|
rows go to the visualizer.
|
|
46
49
|
|
|
47
|
-
[0.1.
|
|
50
|
+
[0.1.1]: https://github.com/Ciphore/RadioCLI/releases/tag/v0.1.1
|
package/CONTRIBUTING.md
CHANGED
|
@@ -18,7 +18,11 @@ Install `mpv` for the best playback experience:
|
|
|
18
18
|
brew install mpv
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
On Linux, use your package manager's `mpv` package.
|
|
21
|
+
On Linux, use your package manager's `mpv` package. On Windows, use:
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
winget install --id shinchiro.mpv -e
|
|
25
|
+
```
|
|
22
26
|
|
|
23
27
|
## Verification
|
|
24
28
|
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# RadioCLI
|
|
2
4
|
|
|
3
5
|
[](https://github.com/Ciphore/RadioCLI/actions/workflows/ci.yml)
|
|
@@ -24,6 +26,32 @@ It is built with [Ink](https://github.com/vadimdemedes/ink), [React](https://rea
|
|
|
24
26
|
- Survive ordinary internet-radio failure modes with provider mirror fallback, stale cache fallback, corrupt-file backups, tune timeouts, and skip-broken-stream behavior.
|
|
25
27
|
- Resize with the terminal. The app listens for terminal resize events and recomputes list row counts, map density, receiver width, and compact-mode fallback from the current dimensions.
|
|
26
28
|
|
|
29
|
+
## Visual Tour
|
|
30
|
+
|
|
31
|
+
The GIFs below are recorded from the real built TUI with `npm run demo:assets`.
|
|
32
|
+
The top GIF cycles multiple Now Playing receiver styles and display colors while
|
|
33
|
+
a real stream is playing through `mpv`.
|
|
34
|
+
|
|
35
|
+
### Library: Favorites And Recents
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
### Explore: World Map Discovery
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
### Search: Query Public Station Directories
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
### Nearby: Opt-In Local Stations
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
### Stats: Listening History And Display Colors
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
27
55
|
## Demo
|
|
28
56
|
|
|
29
57
|
The interactive TUI opens directly into the product, not a marketing screen:
|
|
@@ -109,29 +137,34 @@ Set `NEXT_PUBLIC_SITE_URL` for the canonical public docs URL. Preview builds als
|
|
|
109
137
|
|
|
110
138
|
Requirements:
|
|
111
139
|
|
|
112
|
-
- Node.js
|
|
140
|
+
- Homebrew on macOS: installs RadioCLI, Node.js, and `mpv`
|
|
141
|
+
- npm on macOS, Linux, and Windows: Node.js 22 or newer
|
|
113
142
|
- `mpv` for best playback; RadioCLI expects one local playback backend at runtime
|
|
114
143
|
- `ffplay` from FFmpeg as an optional fallback
|
|
115
144
|
|
|
116
|
-
|
|
117
|
-
system playback tools. Use `radiocli doctor` after installation to check local
|
|
118
|
-
playback readiness and get the right setup command for your OS:
|
|
145
|
+
Recommended macOS install:
|
|
119
146
|
|
|
120
147
|
```bash
|
|
121
|
-
|
|
122
|
-
radiocli doctor
|
|
148
|
+
brew install ciphore/tap/radiocli
|
|
123
149
|
radiocli
|
|
124
150
|
```
|
|
125
151
|
|
|
126
|
-
|
|
152
|
+
The Homebrew formula depends on `node` and `mpv`, so the native playback backend
|
|
153
|
+
comes from the native package manager.
|
|
154
|
+
|
|
155
|
+
Universal npm install:
|
|
127
156
|
|
|
128
157
|
```bash
|
|
129
|
-
brew install mpv
|
|
130
158
|
npm install -g @ciphore/radiocli
|
|
159
|
+
radiocli doctor
|
|
131
160
|
radiocli
|
|
132
161
|
```
|
|
133
162
|
|
|
134
|
-
|
|
163
|
+
The npm package is `@ciphore/radiocli`, and the installed executable is
|
|
164
|
+
`radiocli`. npm installs RadioCLI and its JavaScript dependencies only; it does
|
|
165
|
+
not install system playback tools.
|
|
166
|
+
|
|
167
|
+
Debian/Ubuntu:
|
|
135
168
|
|
|
136
169
|
```bash
|
|
137
170
|
sudo apt install mpv
|
|
@@ -139,25 +172,39 @@ npm install -g @ciphore/radiocli
|
|
|
139
172
|
radiocli
|
|
140
173
|
```
|
|
141
174
|
|
|
142
|
-
|
|
175
|
+
Fedora, Arch, Alpine, and openSUSE users can install the distro `mpv` package
|
|
176
|
+
first, then install `@ciphore/radiocli` with npm.
|
|
177
|
+
|
|
178
|
+
Native Windows with Windows Terminal or PowerShell:
|
|
179
|
+
|
|
180
|
+
```powershell
|
|
181
|
+
winget install --id OpenJS.NodeJS.LTS -e
|
|
182
|
+
winget install --id shinchiro.mpv -e
|
|
183
|
+
npm install -g @ciphore/radiocli
|
|
184
|
+
radiocli doctor
|
|
185
|
+
radiocli
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Scoop users can use `scoop bucket add extras` and `scoop install mpv` instead
|
|
189
|
+
of the `winget` mpv command. WSL remains supported through the Linux path.
|
|
190
|
+
|
|
191
|
+
Optional `ffplay` fallback support:
|
|
192
|
+
|
|
193
|
+
macOS/Linux:
|
|
143
194
|
|
|
144
195
|
```bash
|
|
145
196
|
brew install ffmpeg # macOS
|
|
146
197
|
sudo apt install ffmpeg # Debian/Ubuntu
|
|
147
198
|
```
|
|
148
199
|
|
|
149
|
-
|
|
150
|
-
native one-command macOS tap. Once the tap formula is published, the intended
|
|
151
|
-
Homebrew install path is:
|
|
200
|
+
Windows:
|
|
152
201
|
|
|
153
|
-
```
|
|
154
|
-
|
|
202
|
+
```powershell
|
|
203
|
+
winget install --id Gyan.FFmpeg -e
|
|
155
204
|
```
|
|
156
205
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
CI covers command-mode typecheck, tests, builds, and package checks on Ubuntu. Native Windows terminals are not release-tested yet; use WSL with Linux `mpv` / `ffplay` for the supported path.
|
|
206
|
+
CI covers command-mode typecheck, tests, builds, package checks, and fresh
|
|
207
|
+
install smoke checks on Ubuntu, macOS, and Windows.
|
|
161
208
|
|
|
162
209
|
Local checkout:
|
|
163
210
|
|
|
@@ -288,9 +335,57 @@ RadioCLI is split around four seams:
|
|
|
288
335
|
- playback lifecycle and metadata in `src/player`
|
|
289
336
|
- local JSON persistence in `src/storage`
|
|
290
337
|
|
|
338
|
+
### Audio Pipeline
|
|
339
|
+
|
|
340
|
+
Station lists keep provider metadata and a resolvable stream URL. When you tune
|
|
341
|
+
a station, `ProviderManager.resolve()` follows the provider-specific path, then
|
|
342
|
+
`PlayerController` starts `mpv` with `--no-video`, `--force-window=no`, a local
|
|
343
|
+
JSON IPC endpoint, and the configured volume. The endpoint is a Unix socket on
|
|
344
|
+
macOS/Linux and a named pipe on native Windows. `ffplay` is available as a
|
|
345
|
+
simpler fallback, but `mpv` is the intended backend because it handles
|
|
346
|
+
redirects, HLS, codecs, metadata, pause, mute, volume, and readiness checks more
|
|
347
|
+
reliably than a JavaScript stream client.
|
|
348
|
+
|
|
349
|
+
RadioCLI waits for the backend to become ready before marking playback as
|
|
350
|
+
`playing`. With `mpv`, it polls playback state every 500ms and ICY metadata every
|
|
351
|
+
2.5s, then cleans the metadata before showing it in the receiver and live footer.
|
|
352
|
+
|
|
353
|
+
### FFT Processing
|
|
354
|
+
|
|
355
|
+
RadioCLI does not currently tap decoded PCM audio or run a real FFT in the Node
|
|
356
|
+
process. The receiver visuals are procedural signal displays driven by playback
|
|
357
|
+
truth, the selected style, terminal dimensions, theme, and a small `pulse`
|
|
358
|
+
counter. That keeps the TUI lightweight and avoids duplicating audio decoding
|
|
359
|
+
work already handled by `mpv`. The spectrum-like modes are generated from
|
|
360
|
+
deterministic samples, so they should be read as receiver visualizers rather than
|
|
361
|
+
measurement-grade audio analysis.
|
|
362
|
+
|
|
363
|
+
### Terminal Rendering And Refresh Rate
|
|
364
|
+
|
|
365
|
+
Ink renders React components into ANSI terminal frames. The app recomputes a
|
|
366
|
+
terminal layout from the current row/column size, then each screen gets stable
|
|
367
|
+
row budgets for station lists, the map, receiver panels, and the adaptive
|
|
368
|
+
footer. Visualizers return text rows or colored text segments; the Now Playing
|
|
369
|
+
screen frames those rows inside the receiver panel.
|
|
370
|
+
|
|
371
|
+
Live receiver animation advances every 80ms, about 12.5 frames per second, only
|
|
372
|
+
on the Now Playing screen while playback is `playing` and backend-ready.
|
|
373
|
+
Ambient/idle-style animation uses a slower 140ms interval, and the loading
|
|
374
|
+
spinner uses 120ms. Inactive playback states render zero-signal frames instead
|
|
375
|
+
of animating.
|
|
376
|
+
|
|
377
|
+
### CPU Usage
|
|
378
|
+
|
|
379
|
+
CPU cost is intentionally bounded: audio decode stays in the native backend,
|
|
380
|
+
there is no JavaScript FFT worker, metadata polling is infrequent, and the pulse
|
|
381
|
+
timer does not run outside live Now Playing. In practice the terminal renderer
|
|
382
|
+
does string and color-segment generation for the current screen only; `mpv` does
|
|
383
|
+
the stream work, and idle/library/search/map screens do not pay the visualizer
|
|
384
|
+
animation cost.
|
|
385
|
+
|
|
291
386
|
Radio Browser is the primary provider. Its own docs recommend using a speaking user agent, resolving station clicks through `/json/url`, and retrying with other servers when one fails; RadioCLI follows that shape with mirror fallback and durable cache. Explore and Nearby use a cached geotagged Radio Browser atlas, then compute local distance in the app so map movement is not biased toward the most-clicked stations worldwide. Radio Garden support is experimental because the useful endpoints are publicly discoverable but unofficial, and they can be blocked or changed independently of this project.
|
|
292
387
|
|
|
293
|
-
Playback prefers `mpv` because it handles real-world streams, redirects, HLS, codecs, and metadata better than a hand-rolled stream client. RadioCLI controls `mpv` through JSON IPC for readiness, pause, mute, volume, and metadata polling.
|
|
388
|
+
Playback prefers `mpv` because it handles real-world streams, redirects, HLS, codecs, and metadata better than a hand-rolled stream client. RadioCLI controls `mpv` through JSON IPC for readiness, pause, mute, volume, and metadata polling, using Unix sockets on macOS/Linux and named pipes on Windows.
|
|
294
389
|
|
|
295
390
|
The npm package is `@ciphore/radiocli`, and the installed executable is `radiocli`. Current installs store data under RadioCLI paths such as `radiocli.json` and `radiocli-cache.json`. Existing Radio Atlas data is still discovered when a new RadioCLI store does not exist, and legacy `RADIO_ATLAS_HOME` / animation environment variables remain supported as migration fallbacks. New automation should use `RADIOCLI_HOME` and `RADIOCLI_DISABLE_ANIMATION`.
|
|
296
391
|
|
|
@@ -39,7 +39,7 @@ export function mpvInstallCommand(platform = process.platform, osRelease = readL
|
|
|
39
39
|
return 'brew install mpv';
|
|
40
40
|
}
|
|
41
41
|
if (platform === 'win32') {
|
|
42
|
-
return '
|
|
42
|
+
return 'winget install --id shinchiro.mpv -e';
|
|
43
43
|
}
|
|
44
44
|
if (platform !== 'linux') {
|
|
45
45
|
return 'install mpv with your system package manager';
|
|
@@ -67,7 +67,7 @@ function ffplayInstallCommand(platform = process.platform, osRelease = readLinux
|
|
|
67
67
|
return 'brew install ffmpeg';
|
|
68
68
|
}
|
|
69
69
|
if (platform === 'win32') {
|
|
70
|
-
return '
|
|
70
|
+
return 'winget install --id Gyan.FFmpeg -e';
|
|
71
71
|
}
|
|
72
72
|
if (platform !== 'linux') {
|
|
73
73
|
return 'install FFmpeg with your system package manager';
|
|
@@ -169,7 +169,7 @@ export class PlayerController {
|
|
|
169
169
|
return null;
|
|
170
170
|
}
|
|
171
171
|
playWithMpv(url, initialTitle) {
|
|
172
|
-
this.ipcPath =
|
|
172
|
+
this.ipcPath = createMpvIpcPath();
|
|
173
173
|
this.currentMpvMediaTitle = cleanMediaTitle(initialTitle) ?? 'RadioCLI';
|
|
174
174
|
this.process = spawn('mpv', [
|
|
175
175
|
'--no-video',
|
|
@@ -296,7 +296,7 @@ export class PlayerController {
|
|
|
296
296
|
await waitForStartupWindow(() => this.process, Math.min(500, timeoutMs));
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
|
-
if (this.ipcPath
|
|
299
|
+
if (this.ipcPath) {
|
|
300
300
|
try {
|
|
301
301
|
await this.queryMpv({ command: ['get_property', 'path'] });
|
|
302
302
|
return;
|
|
@@ -374,7 +374,7 @@ export class PlayerController {
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
cleanupIpc() {
|
|
377
|
-
if (this.ipcPath && existsSync(this.ipcPath)) {
|
|
377
|
+
if (this.ipcPath && !isWindowsNamedPipePath(this.ipcPath) && existsSync(this.ipcPath)) {
|
|
378
378
|
try {
|
|
379
379
|
unlinkSync(this.ipcPath);
|
|
380
380
|
}
|
|
@@ -392,6 +392,15 @@ export class PlayerController {
|
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
|
+
export function createMpvIpcPath(platform = process.platform, pid = process.pid, timestamp = Date.now()) {
|
|
396
|
+
if (platform === 'win32') {
|
|
397
|
+
return `\\\\.\\pipe\\radiocli-${pid}-${timestamp}`;
|
|
398
|
+
}
|
|
399
|
+
return join(tmpdir(), `radiocli-${pid}-${timestamp}.sock`);
|
|
400
|
+
}
|
|
401
|
+
function isWindowsNamedPipePath(path) {
|
|
402
|
+
return path.startsWith('\\\\.\\pipe\\');
|
|
403
|
+
}
|
|
395
404
|
function clampVolume(volume) {
|
|
396
405
|
return Math.min(100, Math.max(0, Math.round(volume)));
|
|
397
406
|
}
|
|
@@ -35,7 +35,8 @@ function buildContributionGraph(days, width) {
|
|
|
35
35
|
cells: weeks.map(week => {
|
|
36
36
|
const day = week[dayIndex];
|
|
37
37
|
const level = contributionLevel(day?.seconds ?? 0, maxSeconds);
|
|
38
|
-
|
|
38
|
+
const text = cellWidth > 1 ? `${'█'.repeat(cellWidth - 1)} ` : '█';
|
|
39
|
+
return { key: day?.date ?? `${week[0]?.date ?? 'empty'}-${dayIndex}`, level, text };
|
|
39
40
|
})
|
|
40
41
|
}));
|
|
41
42
|
return { months: monthLine(weeks, cellWidth), rows };
|
package/dist/ui/theme.js
CHANGED
|
@@ -54,46 +54,47 @@ export function themeAccent(theme) {
|
|
|
54
54
|
return '#74f28a';
|
|
55
55
|
}
|
|
56
56
|
export function themeContributionColors(theme) {
|
|
57
|
+
const emptyContribution = '#1c1c1c';
|
|
57
58
|
if (theme === 'amber') {
|
|
58
|
-
return [
|
|
59
|
+
return [emptyContribution, '#5f3700', '#9a6200', '#d68a00', '#ffb000'];
|
|
59
60
|
}
|
|
60
61
|
if (theme === 'blue') {
|
|
61
|
-
return [
|
|
62
|
+
return [emptyContribution, '#12385f', '#1f6feb', '#3388dd', '#53a8ff'];
|
|
62
63
|
}
|
|
63
64
|
if (theme === 'ruby') {
|
|
64
|
-
return [
|
|
65
|
+
return [emptyContribution, '#4c1230', '#8f274f', '#c93f68', '#ff5f87'];
|
|
65
66
|
}
|
|
66
67
|
if (theme === 'ice') {
|
|
67
|
-
return [
|
|
68
|
+
return [emptyContribution, '#24474d', '#4a95a0', '#86dce8', '#b9f6ff'];
|
|
68
69
|
}
|
|
69
70
|
if (theme === 'teal') {
|
|
70
|
-
return [
|
|
71
|
+
return [emptyContribution, '#123f3c', '#1f766c', '#2dd4bf', '#5eead4'];
|
|
71
72
|
}
|
|
72
73
|
if (theme === 'violet') {
|
|
73
|
-
return [
|
|
74
|
+
return [emptyContribution, '#302047', '#5b3f8f', '#7c5cff', '#a78bfa'];
|
|
74
75
|
}
|
|
75
76
|
if (theme === 'copper') {
|
|
76
|
-
return [
|
|
77
|
+
return [emptyContribution, '#44281f', '#7f4f37', '#b86f52', '#d08770'];
|
|
77
78
|
}
|
|
78
79
|
if (theme === 'cyan') {
|
|
79
|
-
return [
|
|
80
|
+
return [emptyContribution, '#0e3b42', '#168a9e', '#22b8cf', '#22d3ee'];
|
|
80
81
|
}
|
|
81
82
|
if (theme === 'lime') {
|
|
82
|
-
return [
|
|
83
|
+
return [emptyContribution, '#2f3d12', '#5f7d1f', '#84b32b', '#a3e635'];
|
|
83
84
|
}
|
|
84
85
|
if (theme === 'coral') {
|
|
85
|
-
return [
|
|
86
|
+
return [emptyContribution, '#4c2018', '#9a4032', '#d65c49', '#ff7e6b'];
|
|
86
87
|
}
|
|
87
88
|
if (theme === 'rose') {
|
|
88
|
-
return [
|
|
89
|
+
return [emptyContribution, '#4c2238', '#8f3f6a', '#d65c9a', '#ff8fc7'];
|
|
89
90
|
}
|
|
90
91
|
if (theme === 'slate') {
|
|
91
|
-
return [
|
|
92
|
+
return [emptyContribution, '#2a3340', '#4f6178', '#7c91ab', '#9fb4cf'];
|
|
92
93
|
}
|
|
93
94
|
if (theme === 'mono') {
|
|
94
|
-
return [
|
|
95
|
+
return [emptyContribution, '#3a3a3a', '#767676', '#b0b0b0', '#d0d0d0'];
|
|
95
96
|
}
|
|
96
|
-
return [
|
|
97
|
+
return [emptyContribution, '#0e4429', '#26a641', '#39d353', '#74f28a'];
|
|
97
98
|
}
|
|
98
99
|
export function nextTheme(theme) {
|
|
99
100
|
const index = themeNames.indexOf(theme);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ciphore/radiocli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"smoke:data": "tsx src/smoke/data-smoke.ts",
|
|
62
62
|
"smoke:playback": "tsx src/smoke/playback-smoke.ts",
|
|
63
63
|
"demo:script": "node scripts/demo-script.mjs",
|
|
64
|
+
"demo:assets": "node scripts/capture-demo-assets.mjs",
|
|
64
65
|
"fresh:check": "node scripts/fresh-check.mjs"
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|