prismcast 1.9.0 → 1.10.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/dist/app.d.ts +1 -1
- package/dist/app.js +27 -9
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.js +7 -7
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +41 -11
- package/dist/browser/channelSelection.js +75 -24
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/display.d.ts +1 -1
- package/dist/browser/index.d.ts +8 -8
- package/dist/browser/index.js +42 -43
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/login.d.ts +2 -2
- package/dist/browser/login.js +1 -1
- package/dist/browser/manifestInterceptor.d.ts +77 -0
- package/dist/browser/manifestInterceptor.js +323 -0
- package/dist/browser/manifestInterceptor.js.map +1 -0
- package/dist/browser/precaching.js +4 -4
- package/dist/browser/tuning/comcastPolymer.d.ts +1 -1
- package/dist/browser/tuning/cox.d.ts +1 -1
- package/dist/browser/tuning/directv.d.ts +1 -1
- package/dist/browser/tuning/directv.js +61 -62
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +1 -1
- package/dist/browser/tuning/fox.js +171 -14
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.d.ts +1 -1
- package/dist/browser/tuning/hbo.js +6 -6
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +1 -1
- package/dist/browser/tuning/hulu.js +117 -110
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +3 -3
- package/dist/browser/tuning/shared.js +5 -5
- package/dist/browser/tuning/sling.d.ts +1 -1
- package/dist/browser/tuning/sling.js +28 -28
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.d.ts +1 -1
- package/dist/browser/tuning/spectrum.js +21 -22
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/thumbnailRow.d.ts +1 -1
- package/dist/browser/tuning/thumbnailRow.js +1 -1
- package/dist/browser/tuning/tileClick.d.ts +1 -1
- package/dist/browser/tuning/tileClick.js +5 -5
- package/dist/browser/tuning/xfinity.d.ts +1 -1
- package/dist/browser/tuning/youtubeTv.d.ts +1 -1
- package/dist/browser/tuning/youtubeTv.js +17 -18
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +20 -6
- package/dist/browser/video.js +30 -38
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +1 -2
- package/dist/channels/index.js +73 -40
- package/dist/channels/index.js.map +1 -1
- package/dist/config/channelForm.d.ts +58 -0
- package/dist/config/channelForm.js +129 -0
- package/dist/config/channelForm.js.map +1 -0
- package/dist/config/consistencyProbe.d.ts +7 -0
- package/dist/config/consistencyProbe.js +143 -0
- package/dist/config/consistencyProbe.js.map +1 -0
- package/dist/config/health.d.ts +4 -3
- package/dist/config/health.js +75 -62
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +19 -12
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +9 -3
- package/dist/config/paths.js +12 -4
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.context.d.ts +8 -0
- package/dist/config/persistence.context.js +30 -0
- package/dist/config/persistence.context.js.map +1 -0
- package/dist/config/persistence.d.ts +110 -12
- package/dist/config/persistence.js +351 -32
- package/dist/config/persistence.js.map +1 -1
- package/dist/config/presets.d.ts +14 -4
- package/dist/config/presets.js +84 -75
- package/dist/config/presets.js.map +1 -1
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.js +7 -9
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/servicePacks.d.ts +2 -2
- package/dist/config/servicePacks.js +28 -28
- package/dist/config/servicePacks.js.map +1 -1
- package/dist/config/services.d.ts +62 -36
- package/dist/config/services.js +150 -136
- package/dist/config/services.js.map +1 -1
- package/dist/config/sites.d.ts +3 -3
- package/dist/config/sites.js +16 -16
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +500 -41
- package/dist/config/userChannels.js +1201 -312
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +103 -10
- package/dist/config/userConfig.js +277 -117
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +4 -2
- package/dist/config/userProfiles.js +72 -26
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.js +8 -11
- package/dist/hdhr/channelMap.js.map +1 -1
- package/dist/hdhr/deviceId.js +10 -9
- package/dist/hdhr/deviceId.js.map +1 -1
- package/dist/hdhr/discover.js +4 -5
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.js +6 -10
- package/dist/hdhr/index.js.map +1 -1
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/native/codecInference.d.ts +44 -0
- package/dist/native/codecInference.js +327 -0
- package/dist/native/codecInference.js.map +1 -0
- package/dist/native/decrypt.d.ts +1 -1
- package/dist/native/index.d.ts +5 -5
- package/dist/native/index.js +42 -64
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +27 -9
- package/dist/native/probe.js +158 -67
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +4 -2
- package/dist/native/proxy.js +52 -49
- package/dist/native/proxy.js.map +1 -1
- package/dist/native/tokenExpiry.d.ts +22 -0
- package/dist/native/tokenExpiry.js +153 -0
- package/dist/native/tokenExpiry.js.map +1 -0
- package/dist/routes/auth.js +6 -10
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/components.js +2 -2
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/browse.js +195 -0
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -0
- package/dist/routes/config/channels/endpoints/bulk.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/bulk.js +128 -0
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -0
- package/dist/routes/config/channels/endpoints/crud.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/crud.js +504 -0
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -0
- package/dist/routes/config/channels/endpoints/importExport.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/importExport.js +131 -0
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -0
- package/dist/routes/config/channels/endpoints/predefined.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/predefined.js +78 -0
- package/dist/routes/config/channels/endpoints/predefined.js.map +1 -0
- package/dist/routes/config/channels/endpoints/prefs.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/prefs.js +58 -0
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -0
- package/dist/routes/config/channels/endpoints/service.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/service.js +149 -0
- package/dist/routes/config/channels/endpoints/service.js.map +1 -0
- package/dist/routes/config/channels/endpoints/tags.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/tags.js +185 -0
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -0
- package/dist/routes/config/channels/http/handler.d.ts +10 -0
- package/dist/routes/config/channels/http/handler.js +20 -0
- package/dist/routes/config/channels/http/handler.js.map +1 -0
- package/dist/routes/config/channels/http/playlistHint.d.ts +25 -0
- package/dist/routes/config/channels/http/playlistHint.js +40 -0
- package/dist/routes/config/channels/http/playlistHint.js.map +1 -0
- package/dist/routes/config/channels/http/serviceWarning.d.ts +10 -0
- package/dist/routes/config/channels/http/serviceWarning.js +28 -0
- package/dist/routes/config/channels/http/serviceWarning.js.map +1 -0
- package/dist/routes/config/channels/index.d.ts +3 -3
- package/dist/routes/config/channels/index.js +1 -1
- package/dist/routes/config/channels/index.js.map +1 -1
- package/dist/routes/config/channels/{routes.d.ts → setup.d.ts} +1 -1
- package/dist/routes/config/channels/setup.js +23 -0
- package/dist/routes/config/channels/setup.js.map +1 -0
- package/dist/routes/config/channels/table.d.ts +9 -9
- package/dist/routes/config/channels/table.js +124 -76
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +111 -0
- package/dist/routes/config/http/envelope.js +103 -0
- package/dist/routes/config/http/envelope.js.map +1 -0
- package/dist/routes/config/index.d.ts +9 -9
- package/dist/routes/config/index.js +2 -2
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/services.js +69 -68
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/config/settings.d.ts +1 -1
- package/dist/routes/config/settings.js +45 -38
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +12 -11
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/icons.js +2 -2
- package/dist/routes/icons.js.map +1 -1
- package/dist/routes/logs.js +19 -30
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.js +1 -1
- package/dist/routes/playlist.d.ts +1 -1
- package/dist/routes/playlist.js +37 -26
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.js +27 -6
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.js +2 -5
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +16 -16
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +59 -59
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +4 -4
- package/dist/routes/root/scripts/shared.js +24 -10
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.d.ts +4 -0
- package/dist/routes/root/scripts/status.handlers.d.ts +182 -0
- package/dist/routes/root/scripts/status.handlers.js +713 -0
- package/dist/routes/root/scripts/status.handlers.js.map +1 -0
- package/dist/routes/root/scripts/status.js +86 -488
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +5 -5
- package/dist/routes/services.js +18 -15
- package/dist/routes/services.js.map +1 -1
- package/dist/routes/sse.d.ts +16 -0
- package/dist/routes/sse.js +29 -0
- package/dist/routes/sse.js.map +1 -0
- package/dist/routes/streams.js +10 -23
- package/dist/routes/streams.js.map +1 -1
- package/dist/routes/ui.d.ts +2 -2
- package/dist/routes/upgrade.js +6 -7
- package/dist/routes/upgrade.js.map +1 -1
- package/dist/service/commands.context.d.ts +6 -0
- package/dist/service/commands.context.js +47 -0
- package/dist/service/commands.context.js.map +1 -0
- package/dist/service/commands.d.ts +53 -8
- package/dist/service/commands.js +171 -188
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.context.d.ts +6 -0
- package/dist/service/generators.context.js +45 -0
- package/dist/service/generators.context.js.map +1 -0
- package/dist/service/generators.d.ts +56 -16
- package/dist/service/generators.js +499 -383
- package/dist/service/generators.js.map +1 -1
- package/dist/service/index.d.ts +2 -2
- package/dist/streaming/clients.d.ts +2 -2
- package/dist/streaming/clients.js +2 -2
- package/dist/streaming/codec.d.ts +2 -2
- package/dist/streaming/codec.js +0 -1
- package/dist/streaming/codec.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +2 -2
- package/dist/streaming/fmp4Segmenter.js +12 -12
- package/dist/streaming/hls.d.ts +3 -3
- package/dist/streaming/hls.js +81 -70
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +3 -3
- package/dist/streaming/hlsResume.js +5 -5
- package/dist/streaming/hlsSegments.d.ts +5 -2
- package/dist/streaming/hlsSegments.js +14 -9
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/monitor.d.ts +12 -12
- package/dist/streaming/monitor.js +80 -54
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +34 -4
- package/dist/streaming/mp4Parser.js +111 -29
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +1 -1
- package/dist/streaming/mpegts.js +13 -10
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -1
- package/dist/streaming/playlistBuilder.js +2 -2
- package/dist/streaming/preroll.d.ts +9 -9
- package/dist/streaming/preroll.js +39 -41
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.js +4 -4
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/recovery.d.ts +3 -3
- package/dist/streaming/recovery.js +4 -4
- package/dist/streaming/registry.d.ts +6 -6
- package/dist/streaming/setup.d.ts +9 -8
- package/dist/streaming/setup.js +97 -49
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +9 -3
- package/dist/streaming/showInfo.js +26 -18
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +4 -4
- package/dist/streaming/statusEmitter.js +2 -2
- package/dist/types/channels.d.ts +133 -34
- package/dist/types/channels.js +39 -1
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +20 -1
- package/dist/types/index.d.ts +9 -8
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +5 -4
- package/dist/types/selection.d.ts +48 -4
- package/dist/types/streaming.d.ts +1 -1
- package/dist/upgrade/commands.context.d.ts +6 -0
- package/dist/upgrade/commands.context.js +29 -0
- package/dist/upgrade/commands.context.js.map +1 -0
- package/dist/upgrade/commands.d.ts +29 -2
- package/dist/upgrade/commands.js +87 -134
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.d.ts +6 -0
- package/dist/upgrade/detection.context.js +24 -0
- package/dist/upgrade/detection.context.js.map +1 -0
- package/dist/upgrade/detection.d.ts +140 -10
- package/dist/upgrade/detection.js +153 -66
- package/dist/upgrade/detection.js.map +1 -1
- package/dist/upgrade/index.d.ts +2 -2
- package/dist/utils/chromeFetch.d.ts +1 -1
- package/dist/utils/chromeFetch.js +1 -1
- package/dist/utils/clock.d.ts +14 -0
- package/dist/utils/clock.js +28 -0
- package/dist/utils/clock.js.map +1 -0
- package/dist/utils/debugFilter.d.ts +1 -1
- package/dist/utils/debugFilter.js +6 -4
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +2 -3
- package/dist/utils/delay.js +10 -16
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/evaluate.js +12 -14
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.context.d.ts +6 -0
- package/dist/utils/ffmpeg.context.js +31 -0
- package/dist/utils/ffmpeg.context.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +85 -29
- package/dist/utils/ffmpeg.js +126 -129
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +18 -2
- package/dist/utils/fileLogger.js +58 -30
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +1 -1
- package/dist/utils/format.js +2 -2
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/html.js +1 -3
- package/dist/utils/html.js.map +1 -1
- package/dist/utils/index.d.ts +21 -20
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.js +16 -20
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/m3u.d.ts +18 -3
- package/dist/utils/m3u.js +28 -14
- package/dist/utils/m3u.js.map +1 -1
- package/dist/utils/memo.d.ts +19 -0
- package/dist/utils/memo.js +53 -0
- package/dist/utils/memo.js.map +1 -0
- package/dist/utils/morganStream.js +2 -2
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/pid.d.ts +2 -2
- package/dist/utils/pid.js +2 -2
- package/dist/utils/platform.d.ts +1 -1
- package/dist/utils/platform.js +3 -3
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/retry.d.ts +2 -0
- package/dist/utils/retry.js +9 -7
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/sanitize.d.ts +2 -2
- package/dist/utils/sanitize.js +4 -4
- package/dist/utils/timing.d.ts +6 -3
- package/dist/utils/timing.js +10 -6
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.d.ts +3 -2
- package/dist/utils/version.js +14 -27
- package/dist/utils/version.js.map +1 -1
- package/package.json +13 -7
- package/dist/native/intercept.d.ts +0 -35
- package/dist/native/intercept.js +0 -181
- package/dist/native/intercept.js.map +0 -1
- package/dist/routes/config/channels/routes.js +0 -1505
- package/dist/routes/config/channels/routes.js.map +0 -1
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* crud.ts: RESTful CRUD endpoints for individual channel records.
|
|
4
|
+
*
|
|
5
|
+
* Each handler is a thin adapter: parse the typed body, delegate decisions to the channelForm domain helpers, write through mutateChannels, respond with an
|
|
6
|
+
* envelope patch.
|
|
7
|
+
*
|
|
8
|
+
* Endpoints:
|
|
9
|
+
* POST /config/channels - Create a new user channel.
|
|
10
|
+
* PUT /config/channels/:key - Full replace/edit. Decides between revert, variant revert, or delta override based on submitted values.
|
|
11
|
+
* DELETE /config/channels/:key - Delete a user channel.
|
|
12
|
+
* POST /config/channels/:key/revert - Revert a predefined channel override to defaults.
|
|
13
|
+
* PATCH /config/channels/:key - Partial update (inline cell edits: channelNumber, stationId, hdhrEnabled, tags).
|
|
14
|
+
*/
|
|
15
|
+
import { CHANNEL_BINDING_KEYS, CHANNEL_IDENTITY_KEYS } from "../../../../types/index.js";
|
|
16
|
+
import { LOG, sanitizeString } from "../../../../utils/index.js";
|
|
17
|
+
import { clearChannelOverrides, getPredefinedChannel, isPredefinedChannel, isUserChannel, mutateChannels, parseTagInput, sortTags, validateChannelKey, validateChannelName, validateChannelNumber, validateChannelProfile, validateChannelUrl } from "../../../../config/userChannels.js";
|
|
18
|
+
import { channelMatches, computePredefinedDelta, findMatchingVariant } from "../../../../config/channelForm.js";
|
|
19
|
+
import { getResolvedChannel, resolveServiceKey } from "../../../../config/services.js";
|
|
20
|
+
import { playlistHintForChange, playlistHintForDelta, playlistHintForStored } from "../http/playlistHint.js";
|
|
21
|
+
import { sendFormErrors, sendSuccess, sendValidationError } from "../../http/envelope.js";
|
|
22
|
+
import { PREDEFINED_CHANNELS } from "../../../../channels/index.js";
|
|
23
|
+
import { buildServiceFilterWarning } from "../http/serviceWarning.js";
|
|
24
|
+
import { getProfiles } from "../../../../config/profiles.js";
|
|
25
|
+
import { route } from "../http/handler.js";
|
|
26
|
+
import { updateChannelLogo } from "../../../../streaming/showInfo.js";
|
|
27
|
+
/* Single source of truth for fields that PATCH /config/channels/:key accepts. The tuple is the one place an inline-editable field is declared; the InlineEditBody
|
|
28
|
+
* shape and INLINE_EDIT_LABELS record both derive from it. Adding a new inline-editable field is a single-line change to the tuple plus a label entry. The
|
|
29
|
+
* `satisfies` constraint validates every entry is a real ChannelDelta key (PATCH writes through the delta model), so renaming or removing a field surfaces here
|
|
30
|
+
* at compile time rather than silently orphaning the allowlist.
|
|
31
|
+
*/
|
|
32
|
+
const INLINE_EDIT_FIELDS = ["channelNumber", "hdhrEnabled", "stationId", "tags"];
|
|
33
|
+
// Human-readable labels for inline-edit fields, used in the success message and log line. Record<InlineEditField, ...> enforces completeness at compile time.
|
|
34
|
+
const INLINE_EDIT_LABELS = {
|
|
35
|
+
channelNumber: "Channel number",
|
|
36
|
+
hdhrEnabled: "HDHR lineup",
|
|
37
|
+
stationId: "Station ID",
|
|
38
|
+
tags: "Tags"
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Parses and sanitizes submitted form fields into the normalized ChannelFormValues shape plus the tag array.
|
|
42
|
+
* @param body - The raw request body as a string-keyed record.
|
|
43
|
+
* @returns The normalized form values and tag array ready for validation and comparison.
|
|
44
|
+
*/
|
|
45
|
+
function parseFormBody(body) {
|
|
46
|
+
const channelNumberStr = sanitizeString(body["channelNumber"] ?? "");
|
|
47
|
+
const formValues = {
|
|
48
|
+
channelNumber: channelNumberStr ? parseInt(channelNumberStr, 10) : undefined,
|
|
49
|
+
channelSelector: sanitizeString(body["channelSelector"] ?? ""),
|
|
50
|
+
guideTitle: sanitizeString(body["guideTitle"] ?? ""),
|
|
51
|
+
hdhrEnabled: body["hdhrEnabled"] !== "false",
|
|
52
|
+
logoUrl: sanitizeString(body["logoUrl"] ?? ""),
|
|
53
|
+
name: sanitizeString(body["name"] ?? ""),
|
|
54
|
+
profile: sanitizeString(body["profile"] ?? ""),
|
|
55
|
+
stationId: sanitizeString(body["stationId"] ?? ""),
|
|
56
|
+
url: sanitizeString(body["url"] ?? "")
|
|
57
|
+
};
|
|
58
|
+
const tags = parseTagInput(sanitizeString(body["tags"] ?? ""));
|
|
59
|
+
return { formValues, tags };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validates submitted form values and returns a map of field errors. An empty map means validation passed.
|
|
63
|
+
* @param formValues - The normalized form values.
|
|
64
|
+
* @param key - The channel key being created or edited.
|
|
65
|
+
* @param isCreate - True for create (validate key), false for edit (key already exists).
|
|
66
|
+
* @param validProfiles - List of valid profile names for the profile field check.
|
|
67
|
+
* @returns Map of field name to error message. Empty when valid.
|
|
68
|
+
*/
|
|
69
|
+
function validateFormValues(formValues, key, isCreate, validProfiles) {
|
|
70
|
+
const errors = {};
|
|
71
|
+
// Channel number: re-derive the string representation since validateChannelNumber expects a string (it parses and range-checks internally).
|
|
72
|
+
const channelNumberStr = (formValues.channelNumber !== undefined) ? String(formValues.channelNumber) : "";
|
|
73
|
+
const channelNumberError = validateChannelNumber(channelNumberStr, key);
|
|
74
|
+
if (channelNumberError) {
|
|
75
|
+
errors["channelNumber"] = channelNumberError;
|
|
76
|
+
}
|
|
77
|
+
if (isCreate) {
|
|
78
|
+
const keyError = validateChannelKey(key, true);
|
|
79
|
+
if (keyError) {
|
|
80
|
+
errors["key"] = keyError;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const nameError = validateChannelName(formValues.name);
|
|
84
|
+
if (nameError) {
|
|
85
|
+
errors["name"] = nameError;
|
|
86
|
+
}
|
|
87
|
+
const urlError = validateChannelUrl(formValues.url);
|
|
88
|
+
if (urlError) {
|
|
89
|
+
errors["url"] = urlError;
|
|
90
|
+
}
|
|
91
|
+
const profileError = validateChannelProfile(formValues.profile, validProfiles);
|
|
92
|
+
if (profileError) {
|
|
93
|
+
errors["profile"] = profileError;
|
|
94
|
+
}
|
|
95
|
+
return errors;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Builds a UserChannel record from validated form values and a tag array. Omits optional fields that are empty/absent so the stored record is minimal.
|
|
99
|
+
* @param formValues - The normalized form values.
|
|
100
|
+
* @param tags - The sorted tag array.
|
|
101
|
+
* @returns The assembled UserChannel ready to store.
|
|
102
|
+
*/
|
|
103
|
+
function buildUserChannelFromForm(formValues, tags) {
|
|
104
|
+
const channel = {
|
|
105
|
+
name: formValues.name,
|
|
106
|
+
url: formValues.url
|
|
107
|
+
};
|
|
108
|
+
if (formValues.guideTitle) {
|
|
109
|
+
channel.guideTitle = formValues.guideTitle;
|
|
110
|
+
}
|
|
111
|
+
if (formValues.logoUrl) {
|
|
112
|
+
channel.logoUrl = formValues.logoUrl;
|
|
113
|
+
}
|
|
114
|
+
if (formValues.profile) {
|
|
115
|
+
channel.profile = formValues.profile;
|
|
116
|
+
}
|
|
117
|
+
if (formValues.stationId) {
|
|
118
|
+
channel.stationId = formValues.stationId;
|
|
119
|
+
}
|
|
120
|
+
if (formValues.channelSelector) {
|
|
121
|
+
channel.channelSelector = formValues.channelSelector;
|
|
122
|
+
}
|
|
123
|
+
if (formValues.channelNumber !== undefined) {
|
|
124
|
+
channel.channelNumber = formValues.channelNumber;
|
|
125
|
+
}
|
|
126
|
+
if (tags.length > 0) {
|
|
127
|
+
channel.tags = [...tags];
|
|
128
|
+
}
|
|
129
|
+
// Absent hdhrEnabled means included by default. Only persist when explicitly disabled.
|
|
130
|
+
if (!formValues.hdhrEnabled) {
|
|
131
|
+
channel.hdhrEnabled = false;
|
|
132
|
+
}
|
|
133
|
+
return channel;
|
|
134
|
+
}
|
|
135
|
+
/* Field category sets used by handlePredefinedEdit to route per-field saves to the correct stored entry. Identity fields live on the canonical entry; binding
|
|
136
|
+
* fields live on the active variant entry (when a non-canonical service is resolved) or the canonical entry (when canonical is active). Built once from the
|
|
137
|
+
* type-system source of truth so renaming or adding a field surfaces here at compile time via the as-const tuples in types/channels.ts.
|
|
138
|
+
*/
|
|
139
|
+
const IDENTITY_FIELD_SET = new Set(CHANNEL_IDENTITY_KEYS);
|
|
140
|
+
const BINDING_FIELD_SET = new Set(CHANNEL_BINDING_KEYS);
|
|
141
|
+
/**
|
|
142
|
+
* Filters a delta to only the fields in the allowlist. Used to split a full delta into identity-only and binding-only halves so the routed save can write
|
|
143
|
+
* each half to the correct stored entry.
|
|
144
|
+
*/
|
|
145
|
+
function filterDeltaFields(delta, allowlist) {
|
|
146
|
+
const filtered = {};
|
|
147
|
+
for (const [field, value] of Object.entries(delta)) {
|
|
148
|
+
if (allowlist.has(field)) {
|
|
149
|
+
filtered[field] = value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return filtered;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Applies the new variant-binding delta to an existing variant stored entry while preserving any non-binding fields the entry already carries (notably
|
|
156
|
+
* canonicalKey). Removes any prior binding-field overrides that the new delta does not include - the new delta is the complete declaration of binding-field
|
|
157
|
+
* customization for this save. Returns null when the resulting entry would be empty (no binding overrides, no preserved non-binding fields), signalling the
|
|
158
|
+
* caller to delete the entry entirely.
|
|
159
|
+
*/
|
|
160
|
+
function applyVariantDelta(existing, delta) {
|
|
161
|
+
const next = { ...existing };
|
|
162
|
+
// Strip prior binding-field overrides; the new delta is authoritative for this category.
|
|
163
|
+
for (const field of CHANNEL_BINDING_KEYS) {
|
|
164
|
+
Reflect.deleteProperty(next, field);
|
|
165
|
+
}
|
|
166
|
+
// Apply the new binding-field overrides.
|
|
167
|
+
Object.assign(next, delta);
|
|
168
|
+
return (Object.keys(next).length > 0) ? next : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Handles PUT /config/channels/:key for a channel that has a predefined base. Routes the submitted form values to the correct stored entry per field category:
|
|
172
|
+
* identity fields go to the canonical entry; binding fields go to the active variant entry (when a non-canonical service is resolved via explicit selection or
|
|
173
|
+
* service-filter fallback) or the canonical entry (when canonical service is active). All writes happen in a single atomic mutate so per-field routing is
|
|
174
|
+
* indistinguishable from a single transaction.
|
|
175
|
+
*
|
|
176
|
+
* The form's "no changes" detection compares submitted values against the resolved display channel (which may be a variant), so saving without modifications is
|
|
177
|
+
* a true no-op even when a variant is active. The "values match predefined" path is an implicit revert: with a variant active, both stored entries (canonical
|
|
178
|
+
* override and variant override) are deleted; without a variant, only the canonical entry is deleted. The "values match a sibling variant" path is unchanged -
|
|
179
|
+
* it switches the service selection to that variant and clears the canonical override.
|
|
180
|
+
* @param key - The canonical channel key being edited.
|
|
181
|
+
* @param predefinedBase - The canonical predefined channel for this key.
|
|
182
|
+
* @param formValues - The normalized form values.
|
|
183
|
+
* @param tags - The sorted tag array.
|
|
184
|
+
* @param res - The Express response.
|
|
185
|
+
*/
|
|
186
|
+
async function handlePredefinedEdit(key, predefinedBase, formValues, tags, res) {
|
|
187
|
+
// First check: does the submission match what the form showed? The edit form is pre-populated with the resolved display channel (which may be a variant), so
|
|
188
|
+
// saving without modification should be a no-op - preserve all existing overrides on both entries.
|
|
189
|
+
const resolvedKey = resolveServiceKey(key);
|
|
190
|
+
const displayChannel = getResolvedChannel(resolvedKey) ?? predefinedBase;
|
|
191
|
+
if (channelMatches(displayChannel, formValues, tags)) {
|
|
192
|
+
sendSuccess(res, { data: { key }, message: "No changes to save." });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Determine the active variant. Undefined when the canonical service is active (no override needed); otherwise the variant key the canonical resolves to.
|
|
196
|
+
const activeVariantKey = (resolvedKey === key) ? undefined : resolvedKey;
|
|
197
|
+
const predefinedVariant = activeVariantKey ? PREDEFINED_CHANNELS[activeVariantKey] : undefined;
|
|
198
|
+
// Compute the canonical-relative delta. This determines the no-op-vs-canonical case (revert path) and supplies identity-field values for the canonical
|
|
199
|
+
// entry. With a variant active, binding-field values in this delta are computed against canonical's binding (e.g., Cox URL) - those values are not what we
|
|
200
|
+
// want to store; the variant-relative delta below has the correct binding-field comparisons.
|
|
201
|
+
const { delta: canonicalRelativeDelta, hasChanges: canonicalHasChanges } = computePredefinedDelta(predefinedBase, formValues, tags);
|
|
202
|
+
if (!canonicalHasChanges) {
|
|
203
|
+
// Submitted values match the canonical predefined exactly. With a variant active, this means the user has cleared all customizations from both entries
|
|
204
|
+
// (identity and binding both equal canonical) - delete both entries. Without a variant, only the canonical entry exists to delete. Both cases route through
|
|
205
|
+
// the shared clearChannelOverrides helper.
|
|
206
|
+
const hasCanonicalEntry = isUserChannel(key);
|
|
207
|
+
const hasVariantEntry = activeVariantKey ? isUserChannel(activeVariantKey) : false;
|
|
208
|
+
if (hasCanonicalEntry || hasVariantEntry) {
|
|
209
|
+
let clearedEntry;
|
|
210
|
+
await mutateChannels((data) => {
|
|
211
|
+
clearedEntry = clearChannelOverrides(data.channels, key);
|
|
212
|
+
});
|
|
213
|
+
LOG.info("Channel '%s' reverted to predefined defaults (edit matched predefined values).", key);
|
|
214
|
+
sendSuccess(res, { affectedKeys: [key], data: { key }, message: "Channel '" + key + "' reverted to defaults." + playlistHintForStored(clearedEntry) });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
sendSuccess(res, { data: { key }, message: "No changes to save." });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Sibling variant match: the submitted values match a different variant's predefined definition exactly. That's an implicit revert-to-that-variant -
|
|
221
|
+
// delete the canonical override and switch the service selection. Variant-stored overrides on the active variant are also cleared since the user is
|
|
222
|
+
// explicitly reverting to a sibling.
|
|
223
|
+
const matchedVariantKey = isUserChannel(key) ? findMatchingVariant(key, formValues, tags) : undefined;
|
|
224
|
+
if (matchedVariantKey) {
|
|
225
|
+
let variantRevertHint = "";
|
|
226
|
+
await mutateChannels((data) => {
|
|
227
|
+
variantRevertHint = playlistHintForStored(data.channels[key]);
|
|
228
|
+
Reflect.deleteProperty(data.channels, key);
|
|
229
|
+
// Clear any stored override on the previously-active variant since the user is switching away from it.
|
|
230
|
+
if (activeVariantKey && (activeVariantKey !== matchedVariantKey)) {
|
|
231
|
+
Reflect.deleteProperty(data.channels, activeVariantKey);
|
|
232
|
+
}
|
|
233
|
+
data.serviceSelections[key] = matchedVariantKey;
|
|
234
|
+
});
|
|
235
|
+
LOG.info("Channel '%s' reverted to variant '%s' (edit matched variant values).", key, matchedVariantKey);
|
|
236
|
+
sendSuccess(res, { affectedKeys: [key], data: { key }, message: "Channel '" + key + "' reverted to defaults." + variantRevertHint });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Real edit. Route per-field to the correct stored entry. With a variant active: identity fields go to canonical, binding fields go to variant (computed
|
|
240
|
+
// against the predefined variant's binding values, since that's the right baseline for the variant entry). Without a variant: everything goes to canonical
|
|
241
|
+
// as before.
|
|
242
|
+
if (activeVariantKey && predefinedVariant) {
|
|
243
|
+
const { delta: variantRelativeDelta } = computePredefinedDelta(predefinedVariant, formValues, tags);
|
|
244
|
+
const canonicalEntryDelta = filterDeltaFields(canonicalRelativeDelta, IDENTITY_FIELD_SET);
|
|
245
|
+
const variantEntryDelta = filterDeltaFields(variantRelativeDelta, BINDING_FIELD_SET);
|
|
246
|
+
await mutateChannels((data) => {
|
|
247
|
+
// Canonical entry: replace with identity-only delta. Empty delta -> remove the entry.
|
|
248
|
+
if (Object.keys(canonicalEntryDelta).length > 0) {
|
|
249
|
+
data.channels[key] = canonicalEntryDelta;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
Reflect.deleteProperty(data.channels, key);
|
|
253
|
+
}
|
|
254
|
+
// Variant entry: merge binding-only delta with the entry's preserved non-binding fields (notably canonicalKey). Empty result -> remove the entry.
|
|
255
|
+
const next = applyVariantDelta(data.channels[activeVariantKey], variantEntryDelta);
|
|
256
|
+
if (next) {
|
|
257
|
+
data.channels[activeVariantKey] = next;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
Reflect.deleteProperty(data.channels, activeVariantKey);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// No variant active - everything routes to canonical. The service selection is cleared so the dropdown reflects the "Custom" state if the user has now
|
|
266
|
+
// diverged from any sibling variant.
|
|
267
|
+
await mutateChannels((data) => {
|
|
268
|
+
data.channels[key] = canonicalRelativeDelta;
|
|
269
|
+
Reflect.deleteProperty(data.serviceSelections, key);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
LOG.info("User channel '%s' updated.", key);
|
|
273
|
+
if (formValues.stationId) {
|
|
274
|
+
updateChannelLogo(formValues.name || key, formValues.stationId);
|
|
275
|
+
}
|
|
276
|
+
sendSuccess(res, {
|
|
277
|
+
affectedKeys: [key],
|
|
278
|
+
data: { key },
|
|
279
|
+
message: "Channel '" + key + "' updated successfully." + playlistHintForDelta(canonicalRelativeDelta)
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Registers the RESTful CRUD endpoints on the Express application.
|
|
284
|
+
* @param app - The Express application.
|
|
285
|
+
*/
|
|
286
|
+
export function registerCrudRoutes(app) {
|
|
287
|
+
// POST /config/channels - Create a new user channel.
|
|
288
|
+
app.post("/config/channels", route("save channel", async (req, res) => {
|
|
289
|
+
const body = req.body;
|
|
290
|
+
const key = body["key"]?.trim();
|
|
291
|
+
if (!key) {
|
|
292
|
+
sendValidationError(res, "Channel key is required.");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const { formValues, tags } = parseFormBody(body);
|
|
296
|
+
const profiles = getProfiles();
|
|
297
|
+
const errors = validateFormValues(formValues, key, true, profiles.map((p) => p.name));
|
|
298
|
+
if (Object.keys(errors).length > 0) {
|
|
299
|
+
sendFormErrors(res, errors);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const channel = buildUserChannelFromForm(formValues, tags);
|
|
303
|
+
await mutateChannels((data) => {
|
|
304
|
+
data.channels[key] = channel;
|
|
305
|
+
});
|
|
306
|
+
if (formValues.stationId) {
|
|
307
|
+
updateChannelLogo(formValues.name || key, formValues.stationId);
|
|
308
|
+
}
|
|
309
|
+
LOG.info("User channel '%s' added.", key);
|
|
310
|
+
sendSuccess(res, {
|
|
311
|
+
affectedKeys: [key],
|
|
312
|
+
data: { isNew: true, key },
|
|
313
|
+
message: "Channel '" + key + "' added successfully.",
|
|
314
|
+
playlistHint: true,
|
|
315
|
+
serviceWarning: buildServiceFilterWarning(formValues.url)
|
|
316
|
+
});
|
|
317
|
+
}));
|
|
318
|
+
// PUT /config/channels/:key - Edit an existing channel. Dispatches to the predefined-edit path (with delta/variant revert decisions) or the user-channel path
|
|
319
|
+
// (direct replacement) based on whether a predefined base exists.
|
|
320
|
+
app.put("/config/channels/:key", route("save channel", async (req, res) => {
|
|
321
|
+
const key = req.params.key?.trim();
|
|
322
|
+
if (!key) {
|
|
323
|
+
sendValidationError(res, "Channel key is required.");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const body = req.body;
|
|
327
|
+
const { formValues, tags } = parseFormBody(body);
|
|
328
|
+
const profiles = getProfiles();
|
|
329
|
+
const errors = validateFormValues(formValues, key, false, profiles.map((p) => p.name));
|
|
330
|
+
if (Object.keys(errors).length > 0) {
|
|
331
|
+
sendFormErrors(res, errors);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const predefinedBase = getPredefinedChannel(key);
|
|
335
|
+
if (predefinedBase) {
|
|
336
|
+
await handlePredefinedEdit(key, predefinedBase, formValues, tags, res);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// User-only channel: build the full record and replace. Snapshot the old record inside the mutation so we can compute the playlist hint from the actual diff.
|
|
340
|
+
const channel = buildUserChannelFromForm(formValues, tags);
|
|
341
|
+
let playlistHint = "";
|
|
342
|
+
await mutateChannels((data) => {
|
|
343
|
+
// User-only path: the entry is structurally a CanonicalChannel (no predefined exists for this key), which assigns to ResolvedChannel for the
|
|
344
|
+
// playlistHintForChange comparison below.
|
|
345
|
+
const oldChannel = (key in data.channels) ? data.channels[key] : undefined;
|
|
346
|
+
data.channels[key] = channel;
|
|
347
|
+
playlistHint = playlistHintForChange(oldChannel, channel);
|
|
348
|
+
});
|
|
349
|
+
if (formValues.stationId) {
|
|
350
|
+
updateChannelLogo(formValues.name || key, formValues.stationId);
|
|
351
|
+
}
|
|
352
|
+
LOG.info("User channel '%s' updated.", key);
|
|
353
|
+
sendSuccess(res, {
|
|
354
|
+
affectedKeys: [key],
|
|
355
|
+
data: { key },
|
|
356
|
+
message: "Channel '" + key + "' updated successfully." + playlistHint
|
|
357
|
+
});
|
|
358
|
+
}));
|
|
359
|
+
// DELETE /config/channels/:key - Delete a user channel. If a predefined version exists with the same key, the patch replaces the row with the predefined original.
|
|
360
|
+
app.delete("/config/channels/:key", route("delete channel", async (req, res) => {
|
|
361
|
+
const key = req.params.key?.trim();
|
|
362
|
+
if (!key) {
|
|
363
|
+
sendValidationError(res, "Channel key is required.");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!isUserChannel(key)) {
|
|
367
|
+
sendValidationError(res, "Cannot delete '" + key + "': it is not a user-defined channel.");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
await mutateChannels((data) => {
|
|
371
|
+
Reflect.deleteProperty(data.channels, key);
|
|
372
|
+
});
|
|
373
|
+
LOG.info("User channel '%s' deleted.", key);
|
|
374
|
+
sendSuccess(res, {
|
|
375
|
+
affectedKeys: [key],
|
|
376
|
+
data: { key },
|
|
377
|
+
message: "Channel '" + key + "' deleted successfully.",
|
|
378
|
+
playlistHint: true
|
|
379
|
+
});
|
|
380
|
+
}));
|
|
381
|
+
// POST /config/channels/:key/revert - Remove the override of a predefined channel, restoring it to defaults. Mirrors the PUT handler's per-field routing model
|
|
382
|
+
// in reverse: revert clears both the canonical-stored override (identity fields) and the active-variant-stored override (binding fields), since either one
|
|
383
|
+
// alone would leave the channel in a partially-customized state. The active variant is determined by resolveServiceKey, so service-filter fallback and explicit
|
|
384
|
+
// service selection both surface the variant entry that needs clearing.
|
|
385
|
+
app.post("/config/channels/:key/revert", route("revert channel", async (req, res) => {
|
|
386
|
+
const key = req.params.key?.trim();
|
|
387
|
+
if (!key) {
|
|
388
|
+
sendValidationError(res, "Channel key is required for revert.");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (!isPredefinedChannel(key)) {
|
|
392
|
+
sendValidationError(res, "Cannot revert '" + key + "': it is not a predefined channel.");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// Determine the active variant. Undefined when the canonical service is active (no variant override possible); otherwise the variant key the canonical
|
|
396
|
+
// resolves to via explicit service selection or service-filter fallback.
|
|
397
|
+
const resolvedKey = resolveServiceKey(key);
|
|
398
|
+
const activeVariantKey = (resolvedKey === key) ? undefined : resolvedKey;
|
|
399
|
+
const hasCanonicalEntry = isUserChannel(key);
|
|
400
|
+
const hasVariantEntry = activeVariantKey ? isUserChannel(activeVariantKey) : false;
|
|
401
|
+
if (!hasCanonicalEntry && !hasVariantEntry) {
|
|
402
|
+
sendValidationError(res, "Cannot revert '" + key + "': no override exists.");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
let clearedEntry;
|
|
406
|
+
await mutateChannels((data) => {
|
|
407
|
+
clearedEntry = clearChannelOverrides(data.channels, key);
|
|
408
|
+
});
|
|
409
|
+
LOG.info("Channel '%s' reverted to predefined defaults.", key);
|
|
410
|
+
sendSuccess(res, {
|
|
411
|
+
affectedKeys: [key],
|
|
412
|
+
data: { key },
|
|
413
|
+
message: "Channel '" + key + "' reverted to defaults." + playlistHintForStored(clearedEntry)
|
|
414
|
+
});
|
|
415
|
+
}));
|
|
416
|
+
// PATCH /config/channels/:key - Partial update from inline cell edits. Accepts any subset of { channelNumber, hdhrEnabled, stationId, tags }. Each field is typed:
|
|
417
|
+
// a typed value to set, null to clear.
|
|
418
|
+
app.patch("/config/channels/:key", route("save channel", async (req, res) => {
|
|
419
|
+
const key = req.params.key?.trim();
|
|
420
|
+
if (!key) {
|
|
421
|
+
sendValidationError(res, "Channel key is required.");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const body = req.body;
|
|
425
|
+
// Filter against the explicit allowlist so unknown keys in the request body are ignored rather than mistakenly admitted. Exactly one field must be present -
|
|
426
|
+
// the client's inline editors update one cell at a time.
|
|
427
|
+
const presentFields = INLINE_EDIT_FIELDS.filter((k) => body[k] !== undefined);
|
|
428
|
+
if (presentFields.length === 0) {
|
|
429
|
+
sendValidationError(res, "No field provided.");
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (presentFields.length > 1) {
|
|
433
|
+
sendValidationError(res, "Only one field may be updated per request.");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
// presentFields.length was validated above to be exactly 1. Narrow with an explicit check - TypeScript does not propagate the length check to the index
|
|
437
|
+
// access, but the guard is free at runtime (an impossible branch) and keeps the rest of the handler type-safe without a non-null assertion.
|
|
438
|
+
const [field] = presentFields;
|
|
439
|
+
if (field === undefined) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const value = body[field];
|
|
443
|
+
// channelNumber validation uses the shared validator, which takes a string and re-parses. Feed it the numeric value rendered as a string.
|
|
444
|
+
if (field === "channelNumber") {
|
|
445
|
+
const rendered = (value === null) ? "" : String(value);
|
|
446
|
+
const numberError = validateChannelNumber(rendered, key);
|
|
447
|
+
if (numberError) {
|
|
448
|
+
sendValidationError(res, numberError);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
await mutateChannels((data) => {
|
|
453
|
+
// The inline-edit endpoint writes scalar override fields that all align with ChannelDelta's nullable shape. Reuse the stored entry when present so
|
|
454
|
+
// existing fields survive, otherwise start from an empty delta. mutateChannels() persists the resulting record either way.
|
|
455
|
+
const stored = data.channels[key] ?? {};
|
|
456
|
+
const delta = stored;
|
|
457
|
+
switch (field) {
|
|
458
|
+
case "channelNumber": {
|
|
459
|
+
delta.channelNumber = (value === null) ? null : value;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case "hdhrEnabled": {
|
|
463
|
+
// The existing behavior: store false when disabling, null to revert to the default (included). true is equivalent to "clear the override".
|
|
464
|
+
delta.hdhrEnabled = (value === false) ? false : null;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "stationId": {
|
|
468
|
+
delta.stationId = ((value === null) || (value === "")) ? null : value;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "tags": {
|
|
472
|
+
const nextTags = (value === null) ? [] : value;
|
|
473
|
+
// Sort before storing so the normalizer's JSON.stringify equality check (used to collapse redundant deltas) sees a canonical order regardless of the
|
|
474
|
+
// order the client submitted the array in.
|
|
475
|
+
delta.tags = (nextTags.length > 0) ? sortTags(nextTags) : null;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
data.channels[key] = stored;
|
|
480
|
+
});
|
|
481
|
+
const fieldLabel = INLINE_EDIT_LABELS[field];
|
|
482
|
+
// Display value for the log line. hdhrEnabled is rendered as included/excluded. Other fields show the value or "(cleared)".
|
|
483
|
+
let displayValue;
|
|
484
|
+
if (field === "hdhrEnabled") {
|
|
485
|
+
displayValue = (value === false) ? "excluded" : "included";
|
|
486
|
+
}
|
|
487
|
+
else if ((value === null) || (value === "") || (Array.isArray(value) && (value.length === 0))) {
|
|
488
|
+
displayValue = "(cleared)";
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
displayValue = String(value);
|
|
492
|
+
}
|
|
493
|
+
LOG.info("Inline edit: %s for '%s' set to '%s'.", fieldLabel, key, displayValue);
|
|
494
|
+
// hdhrEnabled changes don't affect the M3U playlist (HDHomeRun is a separate discovery path); every other inline edit may affect it.
|
|
495
|
+
const applyPlaylistHint = (field !== "hdhrEnabled");
|
|
496
|
+
sendSuccess(res, {
|
|
497
|
+
affectedKeys: [key],
|
|
498
|
+
data: { key },
|
|
499
|
+
message: fieldLabel + " updated.",
|
|
500
|
+
playlistHint: applyPlaylistHint
|
|
501
|
+
});
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
//# sourceMappingURL=crud.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crud.js","sourceRoot":"","sources":["../../../../../src/routes/config/channels/endpoints/crud.ts"],"names":[],"mappings":"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"}
|