@tomorrowos/sdk 0.3.2 → 0.3.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/dist/playlist-catalog.d.ts +6 -4
- package/dist/playlist-catalog.d.ts.map +1 -1
- package/dist/playlist-catalog.js +56 -10
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +6 -2
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/index.html +7 -3
- package/templates/cms-starter/public/methods.js +88 -16
|
@@ -15,6 +15,10 @@ export interface BuiltDevicePolicy {
|
|
|
15
15
|
syncMode?: "latest" | "snapshot";
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
export interface PolicyBuildOptions {
|
|
19
|
+
useLatest?: boolean;
|
|
20
|
+
mediaBaseUrl?: string;
|
|
21
|
+
}
|
|
18
22
|
export declare class PlaylistCatalog {
|
|
19
23
|
private readonly store;
|
|
20
24
|
constructor(store: TomorrowOSStore);
|
|
@@ -24,11 +28,9 @@ export declare class PlaylistCatalog {
|
|
|
24
28
|
savePlaylist(input: SavePlaylistInput): Promise<StoredPlaylist>;
|
|
25
29
|
retirePlaylist(id: string): Promise<StoredPlaylist>;
|
|
26
30
|
getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
|
|
27
|
-
publishPlaylistsToDevice(deviceId: string, playlistIds: string[]): Promise<BuiltDevicePolicy>;
|
|
31
|
+
publishPlaylistsToDevice(deviceId: string, playlistIds: string[], options?: PolicyBuildOptions): Promise<BuiltDevicePolicy>;
|
|
28
32
|
removePlaylistFromDevice(deviceId: string, playlistId: string): Promise<BuiltDevicePolicy>;
|
|
29
|
-
buildPolicyForDevice(deviceId: string, options?:
|
|
30
|
-
useLatest?: boolean;
|
|
31
|
-
}): Promise<BuiltDevicePolicy>;
|
|
33
|
+
buildPolicyForDevice(deviceId: string, options?: PolicyBuildOptions): Promise<BuiltDevicePolicy>;
|
|
32
34
|
private buildPolicyFromAssignments;
|
|
33
35
|
canPublishPlaylistToNewDevice(playlistId: string): Promise<boolean>;
|
|
34
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playlist-catalog.d.ts","sourceRoot":"","sources":["../src/playlist-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,yBAAyB,EAAE,CAAC;QACvC,QAAQ,EAAE;YAAE,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;KAClC,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"playlist-catalog.d.ts","sourceRoot":"","sources":["../src/playlist-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,yBAAyB,EAAE,CAAC;QACvC,QAAQ,EAAE;YAAE,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;KAClC,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+CD,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,eAAe;IAE7C,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAK1C,6BAA6B,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAgC/D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAcnD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;IA2CvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAOvB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;YAKf,0BAA0B;IAgCxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
|
package/dist/playlist-catalog.js
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
function
|
|
2
|
+
function normalizeMediaBaseUrl(raw) {
|
|
3
|
+
let s = String(raw || "").trim();
|
|
4
|
+
if (!s)
|
|
5
|
+
return "";
|
|
6
|
+
if (!/^https?:\/\//i.test(s))
|
|
7
|
+
s = `http://${s}`;
|
|
8
|
+
try {
|
|
9
|
+
return new URL(s).origin;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function absolutizeMediaUrl(url, mediaBaseUrl) {
|
|
16
|
+
const trimmed = String(url || "").trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
return trimmed;
|
|
19
|
+
if (/^https?:\/\//i.test(trimmed))
|
|
20
|
+
return trimmed;
|
|
21
|
+
const base = normalizeMediaBaseUrl(mediaBaseUrl);
|
|
22
|
+
if (!base)
|
|
23
|
+
return trimmed;
|
|
24
|
+
return `${base}${trimmed.startsWith("/") ? trimmed : `/${trimmed}`}`;
|
|
25
|
+
}
|
|
26
|
+
function absolutizePlaylistItems(items, mediaBaseUrl) {
|
|
27
|
+
const base = normalizeMediaBaseUrl(mediaBaseUrl);
|
|
28
|
+
if (!base)
|
|
29
|
+
return items.map((item) => ({ ...item }));
|
|
30
|
+
return items.map((item) => ({
|
|
31
|
+
...item,
|
|
32
|
+
url: absolutizeMediaUrl(item.url, base)
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
function cloneSnapshot(playlist, mediaBaseUrl) {
|
|
3
36
|
return {
|
|
4
37
|
id: playlist.id,
|
|
5
38
|
name: playlist.name,
|
|
6
39
|
version: playlist.version,
|
|
7
40
|
schedule: playlist.schedule ? { ...playlist.schedule } : undefined,
|
|
8
|
-
items: playlist.items
|
|
41
|
+
items: absolutizePlaylistItems(playlist.items, mediaBaseUrl)
|
|
9
42
|
};
|
|
10
43
|
}
|
|
11
44
|
export class PlaylistCatalog {
|
|
@@ -67,15 +100,24 @@ export class PlaylistCatalog {
|
|
|
67
100
|
async getDeviceAssignments(deviceId) {
|
|
68
101
|
return this.store.getDeviceAssignments(deviceId);
|
|
69
102
|
}
|
|
70
|
-
async publishPlaylistsToDevice(deviceId, playlistIds) {
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
103
|
+
async publishPlaylistsToDevice(deviceId, playlistIds, options = {}) {
|
|
104
|
+
const incoming = [...new Set(playlistIds.map((x) => String(x).trim()).filter(Boolean))];
|
|
105
|
+
if (incoming.length === 0) {
|
|
73
106
|
throw Object.assign(new Error("Select at least one playlist"), {
|
|
74
107
|
code: "PLAYLIST_INVALID"
|
|
75
108
|
});
|
|
76
109
|
}
|
|
110
|
+
const existing = await this.store.getDeviceAssignments(deviceId);
|
|
111
|
+
const existingById = new Map(existing.map((a) => [a.playlistId, a]));
|
|
112
|
+
const allIds = [...new Set([...existing.map((a) => a.playlistId), ...incoming])];
|
|
77
113
|
const assignments = [];
|
|
78
|
-
for (const playlistId of
|
|
114
|
+
for (const playlistId of allIds) {
|
|
115
|
+
if (!incoming.includes(playlistId)) {
|
|
116
|
+
const kept = existingById.get(playlistId);
|
|
117
|
+
if (kept)
|
|
118
|
+
assignments.push(kept);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
79
121
|
const playlist = await this.store.getPlaylist(playlistId);
|
|
80
122
|
if (!playlist || playlist.retired) {
|
|
81
123
|
throw Object.assign(new Error(`Playlist not available: ${playlistId}`), {
|
|
@@ -86,11 +128,14 @@ export class PlaylistCatalog {
|
|
|
86
128
|
playlistId,
|
|
87
129
|
publishedVersion: playlist.version,
|
|
88
130
|
publishedAt: new Date().toISOString(),
|
|
89
|
-
snapshot: cloneSnapshot(playlist)
|
|
131
|
+
snapshot: cloneSnapshot(playlist, options.mediaBaseUrl)
|
|
90
132
|
});
|
|
91
133
|
}
|
|
92
134
|
await this.store.setDeviceAssignments(deviceId, assignments);
|
|
93
|
-
return this.buildPolicyFromAssignments(assignments, {
|
|
135
|
+
return this.buildPolicyFromAssignments(assignments, {
|
|
136
|
+
useLatest: false,
|
|
137
|
+
mediaBaseUrl: options.mediaBaseUrl
|
|
138
|
+
});
|
|
94
139
|
}
|
|
95
140
|
async removePlaylistFromDevice(deviceId, playlistId) {
|
|
96
141
|
const assignments = await this.store.getDeviceAssignments(deviceId);
|
|
@@ -104,18 +149,19 @@ export class PlaylistCatalog {
|
|
|
104
149
|
}
|
|
105
150
|
async buildPolicyFromAssignments(assignments, options) {
|
|
106
151
|
const useLatest = options.useLatest === true;
|
|
152
|
+
const mediaBaseUrl = options.mediaBaseUrl;
|
|
107
153
|
const playlists = [];
|
|
108
154
|
for (const assignment of assignments) {
|
|
109
155
|
if (useLatest) {
|
|
110
156
|
const current = await this.store.getPlaylist(assignment.playlistId);
|
|
111
157
|
if (current && !current.retired) {
|
|
112
|
-
playlists.push(cloneSnapshot(current));
|
|
158
|
+
playlists.push(cloneSnapshot(current, mediaBaseUrl));
|
|
113
159
|
continue;
|
|
114
160
|
}
|
|
115
161
|
}
|
|
116
162
|
playlists.push({
|
|
117
163
|
...assignment.snapshot,
|
|
118
|
-
items: assignment.snapshot.items
|
|
164
|
+
items: absolutizePlaylistItems(assignment.snapshot.items, mediaBaseUrl)
|
|
119
165
|
});
|
|
120
166
|
}
|
|
121
167
|
return {
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -639,14 +639,18 @@ export class TomorrowOS extends EventEmitter {
|
|
|
639
639
|
}
|
|
640
640
|
try {
|
|
641
641
|
let built;
|
|
642
|
+
const mediaBaseUrl = typeof body.mediaBaseUrl === "string" ? body.mediaBaseUrl : undefined;
|
|
642
643
|
if (body.useLatest === true) {
|
|
643
644
|
built = await this.playlists.buildPolicyForDevice(deviceId, {
|
|
644
|
-
useLatest: true
|
|
645
|
+
useLatest: true,
|
|
646
|
+
mediaBaseUrl
|
|
645
647
|
});
|
|
646
648
|
}
|
|
647
649
|
else {
|
|
648
650
|
const ids = Array.isArray(body.playlistIds) ? body.playlistIds : [];
|
|
649
|
-
built = await this.playlists.publishPlaylistsToDevice(deviceId, ids
|
|
651
|
+
built = await this.playlists.publishPlaylistsToDevice(deviceId, ids, {
|
|
652
|
+
mediaBaseUrl
|
|
653
|
+
});
|
|
650
654
|
}
|
|
651
655
|
const result = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", {
|
|
652
656
|
policy: built.policy
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "TomorrowOS CMS server SDK — WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build-player": "tomorrowos build --platform tizen"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@tomorrowos/sdk": "^0.3.
|
|
13
|
+
"@tomorrowos/sdk": "^0.3.4"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^20.0.0",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
<section class="card" id="cmsUrlSection">
|
|
42
42
|
<h2>CMS URL for screens</h2>
|
|
43
43
|
<p class="hint">
|
|
44
|
-
<strong>Local development only.</strong> LAN
|
|
44
|
+
<strong>Local development only.</strong> Optional LAN override so TVs reach your PC (not localhost).
|
|
45
|
+
Hosted CMS (e.g. Replit) uses its public URL automatically — leave this empty there.
|
|
45
46
|
</p>
|
|
46
47
|
<div class="row">
|
|
47
48
|
<input
|
|
@@ -63,7 +64,10 @@
|
|
|
63
64
|
</label>
|
|
64
65
|
|
|
65
66
|
<h3 class="subheading">When this playlist plays</h3>
|
|
66
|
-
<p class="hint">
|
|
67
|
+
<p class="hint">
|
|
68
|
+
Leave blank for always on. Uses the <strong>TV/device local clock</strong> (not CMS server time).
|
|
69
|
+
“Until” includes that minute (e.g. Until 18:00 plays through 18:00).
|
|
70
|
+
</p>
|
|
67
71
|
<div class="schedule-grid">
|
|
68
72
|
<label>
|
|
69
73
|
Start date
|
|
@@ -123,7 +127,7 @@
|
|
|
123
127
|
<div class="modal-backdrop" data-close-modal="1"></div>
|
|
124
128
|
<div class="modal-card">
|
|
125
129
|
<h2>Publish to device</h2>
|
|
126
|
-
<p class="hint" id="publishModalHint">Select playlists to
|
|
130
|
+
<p class="hint" id="publishModalHint">Select playlists to add to this device. Already-published playlists are not listed here.</p>
|
|
127
131
|
<div id="publishChecklist" class="publish-checklist"></div>
|
|
128
132
|
<div class="modal-actions">
|
|
129
133
|
<button type="button" id="publishConfirmBtn" class="primary">Publish selected</button>
|
|
@@ -79,15 +79,44 @@ function normalizeMediaBaseUrl(raw) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** User-saved LAN override (local dev only). Not used on hosted CMS unless explicitly set. */
|
|
83
|
+
function getExplicitLanMediaBase() {
|
|
84
|
+
return normalizeMediaBaseUrl(localStorage.getItem(PANEL_MEDIA_BASE_KEY) || "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function playlistHasRelativeMediaUrls(playlist) {
|
|
88
|
+
return (playlist?.items || []).some((item) => {
|
|
89
|
+
const url = String(item?.url || "").trim();
|
|
90
|
+
return url && !/^https?:\/\//i.test(url);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Base URL sent on publish only when needed.
|
|
96
|
+
* - Hosted (e.g. Replit): use public origin only if items are still relative (/uploads/...).
|
|
97
|
+
* - Local: use saved LAN override only when set; never default to rewriting otherwise.
|
|
98
|
+
* - Returns null to omit mediaBaseUrl (playlist already has absolute https URLs).
|
|
99
|
+
*/
|
|
100
|
+
function getPublishMediaBaseUrl(selectedPlaylists) {
|
|
101
|
+
const explicitLan = getExplicitLanMediaBase();
|
|
102
|
+
if (explicitLan) return explicitLan;
|
|
103
|
+
|
|
104
|
+
const needsRewrite = selectedPlaylists.some(playlistHasRelativeMediaUrls);
|
|
105
|
+
if (!needsRewrite) return null;
|
|
106
|
+
|
|
107
|
+
if (!isLocalPanelHost(window.location.hostname)) {
|
|
108
|
+
return window.location.origin;
|
|
109
|
+
}
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Resolve media URLs in the editor (save / thumbnails). */
|
|
82
114
|
function getMediaBaseOrigin() {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
localStorage.getItem(PANEL_MEDIA_BASE_KEY) ||
|
|
86
|
-
""
|
|
87
|
-
);
|
|
88
|
-
if (fromInput) return fromInput;
|
|
115
|
+
const explicitLan = getExplicitLanMediaBase();
|
|
116
|
+
if (explicitLan) return explicitLan;
|
|
89
117
|
if (!isLocalPanelHost(window.location.hostname)) return window.location.origin;
|
|
90
|
-
|
|
118
|
+
const draft = normalizeMediaBaseUrl(document.getElementById("cmsDeviceBaseUrl")?.value);
|
|
119
|
+
return draft;
|
|
91
120
|
}
|
|
92
121
|
|
|
93
122
|
function saveCmsDeviceBaseUrl() {
|
|
@@ -188,7 +217,7 @@ async function fetchPlaylists() {
|
|
|
188
217
|
showResult({
|
|
189
218
|
status: "failed",
|
|
190
219
|
error:
|
|
191
|
-
"CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.
|
|
220
|
+
"CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.4 or newer."
|
|
192
221
|
});
|
|
193
222
|
}
|
|
194
223
|
return;
|
|
@@ -547,6 +576,13 @@ function renderDeviceCards() {
|
|
|
547
576
|
infoBtn.textContent = "Info";
|
|
548
577
|
infoBtn.addEventListener("click", () => deviceAction(device.deviceId, "get-info"));
|
|
549
578
|
|
|
579
|
+
const capBtn = document.createElement("button");
|
|
580
|
+
capBtn.type = "button";
|
|
581
|
+
capBtn.textContent = "Get capabilities";
|
|
582
|
+
capBtn.addEventListener("click", () =>
|
|
583
|
+
deviceAction(device.deviceId, "get-capabilities")
|
|
584
|
+
);
|
|
585
|
+
|
|
550
586
|
const rebootBtn = document.createElement("button");
|
|
551
587
|
rebootBtn.type = "button";
|
|
552
588
|
rebootBtn.textContent = "Reboot";
|
|
@@ -565,6 +601,7 @@ function renderDeviceCards() {
|
|
|
565
601
|
|
|
566
602
|
actions.appendChild(publishBtn);
|
|
567
603
|
actions.appendChild(infoBtn);
|
|
604
|
+
actions.appendChild(capBtn);
|
|
568
605
|
actions.appendChild(rebootBtn);
|
|
569
606
|
actions.appendChild(clearBtn);
|
|
570
607
|
actions.appendChild(unpairBtn);
|
|
@@ -589,17 +626,24 @@ function openPublishModal(deviceId) {
|
|
|
589
626
|
return;
|
|
590
627
|
}
|
|
591
628
|
|
|
592
|
-
|
|
629
|
+
const pubs = devicesCache.find((d) => d.deviceId === deviceId)?.publishedPlaylists || [];
|
|
630
|
+
const publishedIds = new Set(pubs.map((p) => p.playlistId));
|
|
631
|
+
const unpublished = playlistsCatalog.filter((pl) => !publishedIds.has(pl.id));
|
|
632
|
+
|
|
633
|
+
if (unpublished.length === 0) {
|
|
634
|
+
alert("All playlists are already published to this device. Use Remove on the card to unpublish one first.");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
hint.textContent = `Device ${deviceId} — add playlists not yet on this device (snapshot at publish time).`;
|
|
593
639
|
checklist.innerHTML = "";
|
|
594
640
|
|
|
595
|
-
for (const pl of
|
|
641
|
+
for (const pl of unpublished) {
|
|
596
642
|
const label = document.createElement("label");
|
|
597
643
|
const cb = document.createElement("input");
|
|
598
644
|
cb.type = "checkbox";
|
|
599
645
|
cb.value = pl.id;
|
|
600
646
|
cb.dataset.name = pl.name;
|
|
601
|
-
const pubs = devicesCache.find((d) => d.deviceId === deviceId)?.publishedPlaylists || [];
|
|
602
|
-
if (pubs.some((p) => p.playlistId === pl.id)) cb.checked = true;
|
|
603
647
|
label.appendChild(cb);
|
|
604
648
|
label.appendChild(document.createTextNode(` ${pl.name} (v${pl.version})`));
|
|
605
649
|
checklist.appendChild(label);
|
|
@@ -624,12 +668,38 @@ async function confirmPublishModal() {
|
|
|
624
668
|
return;
|
|
625
669
|
}
|
|
626
670
|
|
|
671
|
+
const selectedPlaylists = ids
|
|
672
|
+
.map((id) => playlistsCatalog.find((p) => p.id === id))
|
|
673
|
+
.filter(Boolean);
|
|
674
|
+
|
|
675
|
+
for (const pl of selectedPlaylists) {
|
|
676
|
+
if (!(pl.items || []).length) {
|
|
677
|
+
alert(`Playlist "${pl.name || pl.id}" has no assets. Save the playlist first.`);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const mediaBaseUrl = getPublishMediaBaseUrl(selectedPlaylists);
|
|
683
|
+
if (mediaBaseUrl === "") {
|
|
684
|
+
alert(
|
|
685
|
+
"Local CMS: save a LAN URL under “CMS URL for screens” (e.g. http://192.168.1.105:3000) so TVs can load /uploads paths. On Replit/hosted CMS this field is not required."
|
|
686
|
+
);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const device = devicesCache.find((d) => d.deviceId === publishModalDeviceId);
|
|
691
|
+
const alreadyOnDevice = (device?.publishedPlaylists || []).map((p) => p.playlistId);
|
|
692
|
+
const playlistIds = [...new Set([...alreadyOnDevice, ...ids])];
|
|
693
|
+
|
|
694
|
+
const publishBody = { playlistIds };
|
|
695
|
+
if (mediaBaseUrl) publishBody.mediaBaseUrl = mediaBaseUrl;
|
|
696
|
+
|
|
627
697
|
const res = await fetch(
|
|
628
698
|
`/device/${encodeURIComponent(publishModalDeviceId)}/assignments`,
|
|
629
699
|
{
|
|
630
700
|
method: "POST",
|
|
631
701
|
headers: { "Content-Type": "application/json" },
|
|
632
|
-
body: JSON.stringify(
|
|
702
|
+
body: JSON.stringify(publishBody)
|
|
633
703
|
}
|
|
634
704
|
);
|
|
635
705
|
const data = await res.json();
|
|
@@ -743,12 +813,14 @@ function startDevicePolling() {
|
|
|
743
813
|
}
|
|
744
814
|
|
|
745
815
|
document.addEventListener("DOMContentLoaded", () => {
|
|
816
|
+
const cmsUrlSection = document.getElementById("cmsUrlSection");
|
|
817
|
+
if (cmsUrlSection && !isLocalPanelHost(window.location.hostname)) {
|
|
818
|
+
cmsUrlSection.classList.add("hidden");
|
|
819
|
+
}
|
|
820
|
+
|
|
746
821
|
const savedMediaBase = localStorage.getItem(PANEL_MEDIA_BASE_KEY);
|
|
747
822
|
const cmsBaseInput = document.getElementById("cmsDeviceBaseUrl");
|
|
748
823
|
if (savedMediaBase && cmsBaseInput) cmsBaseInput.value = savedMediaBase;
|
|
749
|
-
else if (cmsBaseInput && !isLocalPanelHost(window.location.hostname)) {
|
|
750
|
-
cmsBaseInput.value = window.location.origin;
|
|
751
|
-
}
|
|
752
824
|
|
|
753
825
|
void fetchPlaylists();
|
|
754
826
|
startDevicePolling();
|