@tomorrowos/sdk 0.3.2 → 0.3.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.
@@ -13,8 +13,15 @@ export interface BuiltDevicePolicy {
13
13
  };
14
14
  revision?: number;
15
15
  syncMode?: "latest" | "snapshot";
16
+ /** One-shot: player may start outside schedule right after publish. */
17
+ playNow?: boolean;
16
18
  };
17
19
  }
20
+ export interface PolicyBuildOptions {
21
+ useLatest?: boolean;
22
+ playNow?: boolean;
23
+ mediaBaseUrl?: string;
24
+ }
18
25
  export declare class PlaylistCatalog {
19
26
  private readonly store;
20
27
  constructor(store: TomorrowOSStore);
@@ -24,11 +31,9 @@ export declare class PlaylistCatalog {
24
31
  savePlaylist(input: SavePlaylistInput): Promise<StoredPlaylist>;
25
32
  retirePlaylist(id: string): Promise<StoredPlaylist>;
26
33
  getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
27
- publishPlaylistsToDevice(deviceId: string, playlistIds: string[]): Promise<BuiltDevicePolicy>;
34
+ publishPlaylistsToDevice(deviceId: string, playlistIds: string[], options?: PolicyBuildOptions): Promise<BuiltDevicePolicy>;
28
35
  removePlaylistFromDevice(deviceId: string, playlistId: string): Promise<BuiltDevicePolicy>;
29
- buildPolicyForDevice(deviceId: string, options?: {
30
- useLatest?: boolean;
31
- }): Promise<BuiltDevicePolicy>;
36
+ buildPolicyForDevice(deviceId: string, options?: PolicyBuildOptions): Promise<BuiltDevicePolicy>;
32
37
  private buildPolicyFromAssignments;
33
38
  canPublishPlaylistToNewDevice(playlistId: string): Promise<boolean>;
34
39
  }
@@ -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;AAYD,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,GACpB,OAAO,CAAC,iBAAiB,CAAC;IA8BvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAOvB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GACpC,OAAO,CAAC,iBAAiB,CAAC;YAKf,0BAA0B;IA+BxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
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;QACjC,uEAAuE;QACvE,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,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;IAkCvB,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;IAmCxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
@@ -1,11 +1,44 @@
1
1
  import { randomUUID } from "crypto";
2
- function cloneSnapshot(playlist) {
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.map((item) => ({ ...item }))
41
+ items: absolutizePlaylistItems(playlist.items, mediaBaseUrl)
9
42
  };
10
43
  }
11
44
  export class PlaylistCatalog {
@@ -67,7 +100,7 @@ export class PlaylistCatalog {
67
100
  async getDeviceAssignments(deviceId) {
68
101
  return this.store.getDeviceAssignments(deviceId);
69
102
  }
70
- async publishPlaylistsToDevice(deviceId, playlistIds) {
103
+ async publishPlaylistsToDevice(deviceId, playlistIds, options = {}) {
71
104
  const ids = [...new Set(playlistIds.map((x) => String(x).trim()).filter(Boolean))];
72
105
  if (ids.length === 0) {
73
106
  throw Object.assign(new Error("Select at least one playlist"), {
@@ -86,11 +119,15 @@ export class PlaylistCatalog {
86
119
  playlistId,
87
120
  publishedVersion: playlist.version,
88
121
  publishedAt: new Date().toISOString(),
89
- snapshot: cloneSnapshot(playlist)
122
+ snapshot: cloneSnapshot(playlist, options.mediaBaseUrl)
90
123
  });
91
124
  }
92
125
  await this.store.setDeviceAssignments(deviceId, assignments);
93
- return this.buildPolicyFromAssignments(assignments, { useLatest: false });
126
+ return this.buildPolicyFromAssignments(assignments, {
127
+ useLatest: false,
128
+ playNow: true,
129
+ mediaBaseUrl: options.mediaBaseUrl
130
+ });
94
131
  }
95
132
  async removePlaylistFromDevice(deviceId, playlistId) {
96
133
  const assignments = await this.store.getDeviceAssignments(deviceId);
@@ -104,28 +141,31 @@ export class PlaylistCatalog {
104
141
  }
105
142
  async buildPolicyFromAssignments(assignments, options) {
106
143
  const useLatest = options.useLatest === true;
144
+ const mediaBaseUrl = options.mediaBaseUrl;
107
145
  const playlists = [];
108
146
  for (const assignment of assignments) {
109
147
  if (useLatest) {
110
148
  const current = await this.store.getPlaylist(assignment.playlistId);
111
149
  if (current && !current.retired) {
112
- playlists.push(cloneSnapshot(current));
150
+ playlists.push(cloneSnapshot(current, mediaBaseUrl));
113
151
  continue;
114
152
  }
115
153
  }
116
154
  playlists.push({
117
155
  ...assignment.snapshot,
118
- items: assignment.snapshot.items.map((item) => ({ ...item }))
156
+ items: absolutizePlaylistItems(assignment.snapshot.items, mediaBaseUrl)
119
157
  });
120
158
  }
121
- return {
122
- policy: {
123
- playlists,
124
- fallback: { type: "brand" },
125
- revision: Date.now(),
126
- syncMode: useLatest ? "latest" : "snapshot"
127
- }
159
+ const policy = {
160
+ playlists,
161
+ fallback: { type: "brand" },
162
+ revision: Date.now(),
163
+ syncMode: useLatest ? "latest" : "snapshot"
128
164
  };
165
+ if (options.playNow === true) {
166
+ policy.playNow = true;
167
+ }
168
+ return { policy };
129
169
  }
130
170
  canPublishPlaylistToNewDevice(playlistId) {
131
171
  return this.store.getPlaylist(playlistId).then((p) => !!p && !p?.retired);
@@ -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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"}
@@ -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.2",
3
+ "version": "0.3.3",
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.2",
3
+ "version": "0.3.3",
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.2"
13
+ "@tomorrowos/sdk": "^0.3.3"
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 URL for TVs (not localhost).
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
@@ -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 fromInput = normalizeMediaBaseUrl(
84
- document.getElementById("cmsDeviceBaseUrl")?.value ||
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
- return "";
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.2 or newer."
220
+ "CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.3 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);
@@ -624,12 +661,34 @@ async function confirmPublishModal() {
624
661
  return;
625
662
  }
626
663
 
664
+ const selectedPlaylists = ids
665
+ .map((id) => playlistsCatalog.find((p) => p.id === id))
666
+ .filter(Boolean);
667
+
668
+ for (const pl of selectedPlaylists) {
669
+ if (!(pl.items || []).length) {
670
+ alert(`Playlist "${pl.name || pl.id}" has no assets. Save the playlist first.`);
671
+ return;
672
+ }
673
+ }
674
+
675
+ const mediaBaseUrl = getPublishMediaBaseUrl(selectedPlaylists);
676
+ if (mediaBaseUrl === "") {
677
+ alert(
678
+ "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."
679
+ );
680
+ return;
681
+ }
682
+
683
+ const publishBody = { playlistIds: ids };
684
+ if (mediaBaseUrl) publishBody.mediaBaseUrl = mediaBaseUrl;
685
+
627
686
  const res = await fetch(
628
687
  `/device/${encodeURIComponent(publishModalDeviceId)}/assignments`,
629
688
  {
630
689
  method: "POST",
631
690
  headers: { "Content-Type": "application/json" },
632
- body: JSON.stringify({ playlistIds: ids })
691
+ body: JSON.stringify(publishBody)
633
692
  }
634
693
  );
635
694
  const data = await res.json();
@@ -743,12 +802,14 @@ function startDevicePolling() {
743
802
  }
744
803
 
745
804
  document.addEventListener("DOMContentLoaded", () => {
805
+ const cmsUrlSection = document.getElementById("cmsUrlSection");
806
+ if (cmsUrlSection && !isLocalPanelHost(window.location.hostname)) {
807
+ cmsUrlSection.classList.add("hidden");
808
+ }
809
+
746
810
  const savedMediaBase = localStorage.getItem(PANEL_MEDIA_BASE_KEY);
747
811
  const cmsBaseInput = document.getElementById("cmsDeviceBaseUrl");
748
812
  if (savedMediaBase && cmsBaseInput) cmsBaseInput.value = savedMediaBase;
749
- else if (cmsBaseInput && !isLocalPanelHost(window.location.hostname)) {
750
- cmsBaseInput.value = window.location.origin;
751
- }
752
813
 
753
814
  void fetchPlaylists();
754
815
  startDevicePolling();