@ciphore/radiocli 0.2.1 → 0.2.3
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 +133 -1
- package/README.md +76 -6
- package/dist/agent/alarm-service.js +210 -0
- package/dist/agent/cli.js +193 -0
- package/dist/agent/headless-host.js +143 -0
- package/dist/agent/launcher.js +71 -0
- package/dist/agent/mcp-install.js +467 -0
- package/dist/agent/mcp-server.js +139 -0
- package/dist/agent/service.js +347 -0
- package/dist/agent/session.js +248 -0
- package/dist/alarms/active-session.js +183 -0
- package/dist/alarms/cli.js +312 -0
- package/dist/alarms/guard.js +343 -0
- package/dist/alarms/inhibitor.js +48 -0
- package/dist/alarms/power-guard-store.js +169 -0
- package/dist/alarms/runner.js +342 -0
- package/dist/alarms/runtime-health.js +79 -0
- package/dist/alarms/schedule.js +149 -0
- package/dist/alarms/scheduler.js +250 -0
- package/dist/alarms/setup-verification.js +187 -0
- package/dist/alarms/system-volume.js +43 -0
- package/dist/alarms/terminal-launcher.js +181 -0
- package/dist/alarms/tui-presence.js +38 -0
- package/dist/cli.js +113 -5
- package/dist/player/backend-install.js +2 -1
- package/dist/player/command-diagnostics.js +27 -0
- package/dist/player/command.js +123 -62
- package/dist/player/player-controller.js +32 -2
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +36 -6
- package/dist/setup.js +462 -0
- package/dist/storage/store.js +262 -2
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +111 -26
- package/dist/ui/App.js +401 -67
- package/dist/ui/AppContent.js +24 -7
- package/dist/ui/adaptive-explore-layout.js +47 -0
- package/dist/ui/alarm-editor.js +174 -0
- package/dist/ui/alarm-tui-service.js +84 -0
- package/dist/ui/app-state.js +3 -0
- package/dist/ui/ascii.js +8 -0
- package/dist/ui/components/AdaptiveMarquee.js +28 -0
- package/dist/ui/components/StationList.js +10 -5
- package/dist/ui/components/VersionIndicator.js +19 -0
- package/dist/ui/cosmo-world-map.js +5 -2
- package/dist/ui/explore-map-layout.js +18 -6
- package/dist/ui/format.js +21 -0
- package/dist/ui/help-content.js +14 -2
- package/dist/ui/layout.js +1 -1
- package/dist/ui/page-footer.js +120 -2
- package/dist/ui/receiver-animation.js +68 -0
- package/dist/ui/screen-items.js +41 -9
- package/dist/ui/screen-meta.js +4 -0
- package/dist/ui/screens/AlarmsScreen.js +202 -0
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/ExploreScreen.js +8 -3
- package/dist/ui/screens/HomeScreen.js +3 -1
- package/dist/ui/screens/NowPlayingScreen.js +6 -2
- package/dist/ui/screens/SettingsScreen.js +70 -53
- package/dist/ui/screens/StationScreen.js +3 -2
- package/dist/ui/selection-state.js +10 -0
- package/dist/ui/terminal-mouse.js +18 -3
- package/dist/ui/use-alarm-tui.js +727 -0
- package/dist/ui/use-app-input.js +107 -46
- package/dist/ui/visualizers/gallop.js +118 -0
- package/dist/ui/visualizers/horse-stride.js +20 -0
- package/dist/ui/visualizers/receiver-style-registry.js +14 -7
- package/dist/ui/visualizers/receiver-visualizers.js +233 -128
- package/dist/ui/visualizers/retro-receivers.js +4 -0
- package/dist/ui/visualizers/terminal-receivers.js +57 -0
- package/dist/update-check.js +26 -7
- package/package.json +6 -1
package/dist/storage/store.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
import { defaultReceiverStyle, receiverStyleNames, themeNames } from '../types.js';
|
|
7
|
+
import { canonicalizeAlarmTime, canonicalizeIsoWeekdays, canonicalizeTimeZone } from '../alarms/schedule.js';
|
|
6
8
|
import { backupBadFile } from '../providers/cache.js';
|
|
7
9
|
import { migrateReceiverStyle } from '../ui/visualizers/receiver-style-registry.js';
|
|
8
10
|
const stationSchema = z
|
|
@@ -35,6 +37,7 @@ const defaultMediaKeys = {
|
|
|
35
37
|
playPause: [],
|
|
36
38
|
next: []
|
|
37
39
|
};
|
|
40
|
+
const MAX_ALARMS = 500;
|
|
38
41
|
const settingsSchema = z.object({
|
|
39
42
|
theme: z.enum(themeNames).default('green'),
|
|
40
43
|
receiverStyle: z.preprocess(migrateReceiverStyle, z.enum(receiverStyleNames).default(defaultReceiverStyle)),
|
|
@@ -58,7 +61,102 @@ const settingsSchema = z.object({
|
|
|
58
61
|
transparentBackground: z.boolean().default(false),
|
|
59
62
|
asciiMode: z.boolean().default(false),
|
|
60
63
|
reduceMotion: z.boolean().default(false),
|
|
61
|
-
mouseSupport: z.boolean().default(true)
|
|
64
|
+
mouseSupport: z.boolean().default(true),
|
|
65
|
+
automaticUpdateChecks: z.boolean().default(true),
|
|
66
|
+
agentControl: z.object({
|
|
67
|
+
enabled: z.boolean().default(false),
|
|
68
|
+
openUiOnPlay: z.boolean().default(true),
|
|
69
|
+
focusNowPlaying: z.boolean().default(true),
|
|
70
|
+
completionPreset: z.object({
|
|
71
|
+
action: z.enum(['play', 'pause', 'resume', 'stop']).default('play'),
|
|
72
|
+
source: z.enum(['recent', 'favorite', 'popular', 'country']).default('recent'),
|
|
73
|
+
countryCode: z.string().length(2).optional(),
|
|
74
|
+
ifPlaying: z.enum(['keep', 'replace']).default('keep'),
|
|
75
|
+
openUi: z.boolean().optional()
|
|
76
|
+
}).default({ action: 'play', source: 'recent', ifPlaying: 'keep' })
|
|
77
|
+
}).default({
|
|
78
|
+
enabled: false,
|
|
79
|
+
openUiOnPlay: true,
|
|
80
|
+
focusNowPlaying: true,
|
|
81
|
+
completionPreset: { action: 'play', source: 'recent', ifPlaying: 'keep' }
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
const absoluteInstantSchema = z.string().refine(value => /(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value)), { message: 'Expected an absolute ISO-8601 instant with a timezone offset.' });
|
|
85
|
+
const alarmMinuteInstantSchema = absoluteInstantSchema.refine(value => new Date(value).getUTCSeconds() === 0 && new Date(value).getUTCMilliseconds() === 0, { message: 'Alarm occurrences use minute precision; seconds must be zero.' });
|
|
86
|
+
const alarmScheduleSchema = z.discriminatedUnion('type', [
|
|
87
|
+
z.object({ type: z.literal('once'), at: alarmMinuteInstantSchema }),
|
|
88
|
+
z.object({
|
|
89
|
+
type: z.literal('recurring'),
|
|
90
|
+
time: z.string().refine(value => {
|
|
91
|
+
try {
|
|
92
|
+
canonicalizeAlarmTime(value);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}, { message: 'Invalid alarm time; expected HH:mm.' }),
|
|
99
|
+
weekdays: z.array(z.number().int().min(1).max(7).transform(value => value)).min(1, 'Choose at least one ISO weekday.'),
|
|
100
|
+
timezone: z.string().refine(value => {
|
|
101
|
+
try {
|
|
102
|
+
canonicalizeTimeZone(value);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}, { message: 'Invalid IANA timezone.' })
|
|
109
|
+
})
|
|
110
|
+
]);
|
|
111
|
+
const alarmPlaybackSchema = z.object({
|
|
112
|
+
volume: z.number().int().min(0).max(100),
|
|
113
|
+
fadeSeconds: z.number().int().min(0).max(3600),
|
|
114
|
+
stopAfterMinutes: z.number().int().min(1).max(7 * 24 * 60),
|
|
115
|
+
fallbackStation: stationSchema.optional()
|
|
116
|
+
});
|
|
117
|
+
const alarmReliabilitySchema = z.object({
|
|
118
|
+
missedRunGraceMinutes: z.number().int().min(0).max(7 * 24 * 60),
|
|
119
|
+
wakeIfSupported: z.boolean(),
|
|
120
|
+
keepAwakeUntilAlarm: z.boolean().default(false)
|
|
121
|
+
});
|
|
122
|
+
const alarmRunRecordSchema = z.object({
|
|
123
|
+
status: z.enum(['played', 'failed', 'missed', 'dismissed']),
|
|
124
|
+
scheduledAt: absoluteInstantSchema,
|
|
125
|
+
firedAt: absoluteInstantSchema.optional(),
|
|
126
|
+
finishedAt: absoluteInstantSchema.optional(),
|
|
127
|
+
message: z.string().max(1000).optional()
|
|
128
|
+
});
|
|
129
|
+
const alarmSchema = z.object({
|
|
130
|
+
id: z.string().min(1),
|
|
131
|
+
label: z.string().trim().min(1).max(120),
|
|
132
|
+
enabled: z.boolean(),
|
|
133
|
+
station: stationSchema,
|
|
134
|
+
schedule: alarmScheduleSchema,
|
|
135
|
+
playback: alarmPlaybackSchema,
|
|
136
|
+
reliability: alarmReliabilitySchema,
|
|
137
|
+
createdAt: absoluteInstantSchema,
|
|
138
|
+
updatedAt: absoluteInstantSchema,
|
|
139
|
+
lastRun: alarmRunRecordSchema.optional(),
|
|
140
|
+
nextOverride: z.object({
|
|
141
|
+
at: alarmMinuteInstantSchema,
|
|
142
|
+
createdAt: absoluteInstantSchema,
|
|
143
|
+
reason: z.literal('snooze')
|
|
144
|
+
}).optional()
|
|
145
|
+
});
|
|
146
|
+
const alarmArraySchema = z.array(alarmSchema)
|
|
147
|
+
.max(MAX_ALARMS, `RadioCLI supports at most ${MAX_ALARMS} alarms.`)
|
|
148
|
+
.superRefine((alarms, context) => {
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
for (const [index, alarm] of alarms.entries()) {
|
|
151
|
+
if (seen.has(alarm.id)) {
|
|
152
|
+
context.addIssue({
|
|
153
|
+
code: 'custom',
|
|
154
|
+
message: `Duplicate alarm ID: ${alarm.id}`,
|
|
155
|
+
path: [index, 'id']
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
seen.add(alarm.id);
|
|
159
|
+
}
|
|
62
160
|
});
|
|
63
161
|
const librarySchema = z.object({
|
|
64
162
|
recent: z
|
|
@@ -78,6 +176,7 @@ const librarySchema = z.object({
|
|
|
78
176
|
}))
|
|
79
177
|
.default([]),
|
|
80
178
|
searchHistory: z.array(z.string()).default([]),
|
|
179
|
+
alarms: alarmArraySchema.default([]),
|
|
81
180
|
updateCheck: z
|
|
82
181
|
.object({
|
|
83
182
|
checkedAt: z.string(),
|
|
@@ -113,6 +212,13 @@ const librarySchema = z.object({
|
|
|
113
212
|
tuneTimeoutSeconds: 12,
|
|
114
213
|
skipBrokenStreams: true,
|
|
115
214
|
mouseSupport: true,
|
|
215
|
+
automaticUpdateChecks: true,
|
|
216
|
+
agentControl: {
|
|
217
|
+
enabled: false,
|
|
218
|
+
openUiOnPlay: true,
|
|
219
|
+
focusNowPlaying: true,
|
|
220
|
+
completionPreset: { action: 'play', source: 'recent', ifPlaying: 'keep' }
|
|
221
|
+
},
|
|
116
222
|
mediaKeys: defaultMediaKeys
|
|
117
223
|
})
|
|
118
224
|
});
|
|
@@ -126,8 +232,12 @@ const legacyLibraryBackupSchema = z.object({ settings: z.unknown() }).passthroug
|
|
|
126
232
|
export class JsonLibraryStore {
|
|
127
233
|
filePath;
|
|
128
234
|
state;
|
|
129
|
-
|
|
235
|
+
idGenerator;
|
|
236
|
+
now;
|
|
237
|
+
constructor(filePath = defaultStorePath(), options = {}) {
|
|
130
238
|
this.filePath = filePath;
|
|
239
|
+
this.idGenerator = options.idGenerator ?? randomUUID;
|
|
240
|
+
this.now = options.now ?? (() => new Date());
|
|
131
241
|
this.state = this.read();
|
|
132
242
|
}
|
|
133
243
|
snapshot() {
|
|
@@ -162,6 +272,8 @@ export class JsonLibraryStore {
|
|
|
162
272
|
}
|
|
163
273
|
const parsed = JSON.parse(readFileSync(sourcePath, 'utf8'));
|
|
164
274
|
const backupResult = libraryBackupSchema.safeParse(parsed);
|
|
275
|
+
if (!backupResult.success && isRadioCliBackup(parsed))
|
|
276
|
+
throw backupResult.error;
|
|
165
277
|
const importedState = migrateLibraryState(backupResult.success
|
|
166
278
|
? backupResult.data.library
|
|
167
279
|
: librarySchema.parse(legacyLibraryBackupSchema.parse(parsed)));
|
|
@@ -194,6 +306,107 @@ export class JsonLibraryStore {
|
|
|
194
306
|
updateCheckState(updateCheck) {
|
|
195
307
|
return this.commit({ ...this.state, updateCheck });
|
|
196
308
|
}
|
|
309
|
+
addAlarm(input) {
|
|
310
|
+
const release = acquireStoreLock(this.filePath);
|
|
311
|
+
try {
|
|
312
|
+
const latestState = this.read();
|
|
313
|
+
if (latestState.alarms.length >= MAX_ALARMS) {
|
|
314
|
+
throw new Error(`RadioCLI supports at most ${MAX_ALARMS} alarms.`);
|
|
315
|
+
}
|
|
316
|
+
const existingIds = new Set([...this.state.alarms, ...latestState.alarms].map(alarm => alarm.id));
|
|
317
|
+
let id;
|
|
318
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
319
|
+
const candidate = this.idGenerator();
|
|
320
|
+
if (candidate && !existingIds.has(candidate)) {
|
|
321
|
+
id = candidate;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!id)
|
|
326
|
+
throw new Error('Unable to allocate a unique alarm ID after 100 attempts.');
|
|
327
|
+
const timestamp = this.now().toISOString();
|
|
328
|
+
const alarm = normalizeAlarm(alarmSchema.parse({
|
|
329
|
+
...input,
|
|
330
|
+
id,
|
|
331
|
+
createdAt: timestamp,
|
|
332
|
+
updatedAt: timestamp
|
|
333
|
+
}));
|
|
334
|
+
const nextState = { ...this.state, alarms: [alarm, ...this.state.alarms] };
|
|
335
|
+
const mergedState = mergeLibraryChanges(this.state, latestState, nextState);
|
|
336
|
+
this.write(mergedState);
|
|
337
|
+
this.state = mergedState;
|
|
338
|
+
return structuredClone(alarm);
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
release();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
updateAlarm(id, updates) {
|
|
345
|
+
return this.mutateAlarm(id, existing => normalizeAlarm(alarmSchema.parse({
|
|
346
|
+
...existing,
|
|
347
|
+
...updates,
|
|
348
|
+
updatedAt: this.now().toISOString()
|
|
349
|
+
})));
|
|
350
|
+
}
|
|
351
|
+
removeAlarm(id) {
|
|
352
|
+
const release = acquireStoreLock(this.filePath);
|
|
353
|
+
try {
|
|
354
|
+
const latestState = this.read();
|
|
355
|
+
if (!latestState.alarms.some(alarm => alarm.id === id)) {
|
|
356
|
+
this.state = latestState;
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const nextState = { ...latestState, alarms: latestState.alarms.filter(alarm => alarm.id !== id) };
|
|
360
|
+
this.write(nextState);
|
|
361
|
+
this.state = nextState;
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
release();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
toggleAlarm(id, enabled) {
|
|
369
|
+
return this.mutateAlarm(id, existing => alarmSchema.parse({
|
|
370
|
+
...existing,
|
|
371
|
+
enabled: enabled ?? !existing.enabled,
|
|
372
|
+
updatedAt: this.now().toISOString()
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
getAlarm(id) {
|
|
376
|
+
const alarm = this.state.alarms.find(item => item.id === id);
|
|
377
|
+
return alarm ? structuredClone(alarm) : undefined;
|
|
378
|
+
}
|
|
379
|
+
listAlarms() {
|
|
380
|
+
return structuredClone(this.state.alarms);
|
|
381
|
+
}
|
|
382
|
+
recordAlarmOutcome(id, outcome, options = {}) {
|
|
383
|
+
return this.mutateAlarm(id, existing => alarmSchema.parse({
|
|
384
|
+
...existing,
|
|
385
|
+
lastRun: outcome,
|
|
386
|
+
nextOverride: options.clearNextOverride === false ? existing.nextOverride : undefined,
|
|
387
|
+
updatedAt: this.now().toISOString()
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
snoozeAlarm(id, until) {
|
|
391
|
+
if (!Number.isFinite(until.getTime()))
|
|
392
|
+
throw new Error('Invalid alarm snooze instant.');
|
|
393
|
+
const now = this.now();
|
|
394
|
+
const canonicalUntil = new Date(Math.ceil(until.getTime() / 60_000) * 60_000);
|
|
395
|
+
if (canonicalUntil.getTime() <= now.getTime())
|
|
396
|
+
throw new Error('Alarm snooze must be set to a future time.');
|
|
397
|
+
return this.mutateAlarm(id, existing => alarmSchema.parse({
|
|
398
|
+
...existing,
|
|
399
|
+
nextOverride: { at: canonicalUntil.toISOString(), createdAt: now.toISOString(), reason: 'snooze' },
|
|
400
|
+
updatedAt: now.toISOString()
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
clearAlarmSnooze(id) {
|
|
404
|
+
return this.mutateAlarm(id, existing => alarmSchema.parse({
|
|
405
|
+
...existing,
|
|
406
|
+
nextOverride: undefined,
|
|
407
|
+
updatedAt: this.now().toISOString()
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
197
410
|
addRecent(station) {
|
|
198
411
|
const key = stationKey(station);
|
|
199
412
|
const recent = [
|
|
@@ -299,6 +512,28 @@ export class JsonLibraryStore {
|
|
|
299
512
|
const key = stationKey(station);
|
|
300
513
|
return this.state.favorites.some(item => stationKey(item) === key);
|
|
301
514
|
}
|
|
515
|
+
mutateAlarm(id, change) {
|
|
516
|
+
const release = acquireStoreLock(this.filePath);
|
|
517
|
+
try {
|
|
518
|
+
const latestState = this.read();
|
|
519
|
+
const existing = latestState.alarms.find(alarm => alarm.id === id);
|
|
520
|
+
if (!existing) {
|
|
521
|
+
this.state = latestState;
|
|
522
|
+
throw new Error(`Alarm not found: ${id}`);
|
|
523
|
+
}
|
|
524
|
+
const alarm = change(existing);
|
|
525
|
+
const nextState = {
|
|
526
|
+
...latestState,
|
|
527
|
+
alarms: latestState.alarms.map(item => item.id === id ? alarm : item)
|
|
528
|
+
};
|
|
529
|
+
this.write(nextState);
|
|
530
|
+
this.state = nextState;
|
|
531
|
+
return structuredClone(alarm);
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
release();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
302
537
|
read() {
|
|
303
538
|
if (!existsSync(this.filePath)) {
|
|
304
539
|
return defaultState();
|
|
@@ -412,6 +647,7 @@ function defaultState() {
|
|
|
412
647
|
imported: [],
|
|
413
648
|
trackHistory: [],
|
|
414
649
|
searchHistory: [],
|
|
650
|
+
alarms: [],
|
|
415
651
|
activity: { sessions: [] },
|
|
416
652
|
settings: {
|
|
417
653
|
theme: 'green',
|
|
@@ -425,6 +661,13 @@ function defaultState() {
|
|
|
425
661
|
tuneTimeoutSeconds: 12,
|
|
426
662
|
skipBrokenStreams: true,
|
|
427
663
|
mouseSupport: true,
|
|
664
|
+
automaticUpdateChecks: true,
|
|
665
|
+
agentControl: {
|
|
666
|
+
enabled: false,
|
|
667
|
+
openUiOnPlay: true,
|
|
668
|
+
focusNowPlaying: true,
|
|
669
|
+
completionPreset: { action: 'play', source: 'recent', ifPlaying: 'keep' }
|
|
670
|
+
},
|
|
428
671
|
mediaKeys: defaultMediaKeys
|
|
429
672
|
}
|
|
430
673
|
};
|
|
@@ -432,6 +675,7 @@ function defaultState() {
|
|
|
432
675
|
function migrateLibraryState(state) {
|
|
433
676
|
return {
|
|
434
677
|
...state,
|
|
678
|
+
alarms: state.alarms.map(normalizeAlarm),
|
|
435
679
|
settings: {
|
|
436
680
|
...state.settings,
|
|
437
681
|
preferredBackend: state.settings.preferredBackend === 'airplay' ? 'auto' : state.settings.preferredBackend,
|
|
@@ -440,6 +684,10 @@ function migrateLibraryState(state) {
|
|
|
440
684
|
}
|
|
441
685
|
};
|
|
442
686
|
}
|
|
687
|
+
function isRadioCliBackup(value) {
|
|
688
|
+
return typeof value === 'object' && value !== null && 'format' in value
|
|
689
|
+
&& value.format === 'radiocli-backup';
|
|
690
|
+
}
|
|
443
691
|
function libraryStateForDisk(state) {
|
|
444
692
|
return state.settings.preferredBackend === 'airplay'
|
|
445
693
|
? {
|
|
@@ -501,6 +749,7 @@ function mergeLibraryChanges(base, disk, next) {
|
|
|
501
749
|
imported: mergeKeyedChanges(base.imported, disk.imported, next.imported, stationKey, 1000, true),
|
|
502
750
|
trackHistory: mergeKeyedChanges(base.trackHistory, disk.trackHistory, next.trackHistory, item => `${item.at}:${item.stationKey}:${item.title}`, 100),
|
|
503
751
|
searchHistory: mergeSearchHistory(base.searchHistory, disk.searchHistory, next.searchHistory),
|
|
752
|
+
alarms: mergeKeyedChanges(base.alarms, disk.alarms, next.alarms, alarm => alarm.id, MAX_ALARMS, true),
|
|
504
753
|
updateCheck: sameValue(base.updateCheck, next.updateCheck) ? disk.updateCheck : next.updateCheck,
|
|
505
754
|
activity: {
|
|
506
755
|
sessions: mergeKeyedChanges(base.activity.sessions, disk.activity.sessions, next.activity.sessions, item => item.id, 2000)
|
|
@@ -508,6 +757,17 @@ function mergeLibraryChanges(base, disk, next) {
|
|
|
508
757
|
settings: mergeSettings(base.settings, disk.settings, next.settings)
|
|
509
758
|
};
|
|
510
759
|
}
|
|
760
|
+
function normalizeAlarm(alarm) {
|
|
761
|
+
const schedule = alarm.schedule.type === 'once'
|
|
762
|
+
? { type: 'once', at: new Date(alarm.schedule.at).toISOString() }
|
|
763
|
+
: {
|
|
764
|
+
type: 'recurring',
|
|
765
|
+
time: canonicalizeAlarmTime(alarm.schedule.time),
|
|
766
|
+
weekdays: canonicalizeIsoWeekdays(alarm.schedule.weekdays),
|
|
767
|
+
timezone: canonicalizeTimeZone(alarm.schedule.timezone)
|
|
768
|
+
};
|
|
769
|
+
return { ...alarm, schedule };
|
|
770
|
+
}
|
|
511
771
|
function mergeKeyedChanges(base, disk, next, keyFor, limit, applyRemovals = false) {
|
|
512
772
|
const baseByKey = new Map(base.map(item => [keyFor(item), item]));
|
|
513
773
|
const nextKeys = new Set(next.map(keyFor));
|
package/dist/types.js
CHANGED
|
@@ -2,3 +2,9 @@ import { defaultReceiverStyle, receiverStyleNames } from './ui/visualizers/recei
|
|
|
2
2
|
export { defaultReceiverStyle, receiverStyleNames };
|
|
3
3
|
const providerIds = ['radio-browser', 'radio-garden', 'playlist'];
|
|
4
4
|
export const themeNames = ['green', 'amber', 'blue', 'ruby', 'ice', 'teal', 'violet', 'copper', 'cyan', 'lime', 'coral', 'rose', 'slate', 'mono'];
|
|
5
|
+
export const defaultAgentControlSettings = {
|
|
6
|
+
enabled: false,
|
|
7
|
+
openUiOnPlay: true,
|
|
8
|
+
focusNowPlaying: true,
|
|
9
|
+
completionPreset: { action: 'play', source: 'recent', ifPlaying: 'keep' }
|
|
10
|
+
};
|