@tomorrowos/sdk 0.9.2 → 0.9.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/REPLIT_SETUP.md
CHANGED
|
@@ -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;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;
|
|
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;AAyDD,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;IA8B/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;IAiEvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAO7B,mFAAmF;IAC7E,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAKjF;;;OAGG;IACG,+BAA+B,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC;IA4BrD,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
|
@@ -40,6 +40,15 @@ function cloneSnapshot(playlist, mediaBaseUrl) {
|
|
|
40
40
|
items: absolutizePlaylistItems(playlist.items, mediaBaseUrl)
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
/** True when the playlist has no start/end date or time (always-on / default content). */
|
|
44
|
+
function isUnscheduledPlaylist(schedule) {
|
|
45
|
+
if (!schedule)
|
|
46
|
+
return true;
|
|
47
|
+
return !(schedule.startDate ||
|
|
48
|
+
schedule.endDate ||
|
|
49
|
+
schedule.start ||
|
|
50
|
+
schedule.end);
|
|
51
|
+
}
|
|
43
52
|
export class PlaylistCatalog {
|
|
44
53
|
store;
|
|
45
54
|
constructor(store) {
|
|
@@ -106,25 +115,40 @@ export class PlaylistCatalog {
|
|
|
106
115
|
}
|
|
107
116
|
const existing = await this.store.getDeviceAssignments(deviceId);
|
|
108
117
|
const existingById = new Map(existing.map((a) => [a.playlistId, a]));
|
|
118
|
+
// Resolve incoming playlists first so we know whether this publish includes
|
|
119
|
+
// an always-on (no start/end) playlist that should override prior always-on ones.
|
|
120
|
+
const incomingSnapshots = new Map();
|
|
121
|
+
for (const playlistId of incoming) {
|
|
122
|
+
const playlist = await this.store.getPlaylist(playlistId);
|
|
123
|
+
if (!playlist || playlist.retired) {
|
|
124
|
+
throw Object.assign(new Error(`Playlist not available: ${playlistId}`), {
|
|
125
|
+
code: "PLAYLIST_NOT_FOUND"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
incomingSnapshots.set(playlistId, cloneSnapshot(playlist, options.mediaBaseUrl));
|
|
129
|
+
}
|
|
130
|
+
const incomingHasUnscheduled = [...incomingSnapshots.values()].some((snap) => isUnscheduledPlaylist(snap.schedule));
|
|
109
131
|
const allIds = [...new Set([...existing.map((a) => a.playlistId), ...incoming])];
|
|
132
|
+
const publishedAt = new Date().toISOString();
|
|
110
133
|
const assignments = [];
|
|
111
134
|
for (const playlistId of allIds) {
|
|
112
135
|
if (!incoming.includes(playlistId)) {
|
|
113
136
|
const kept = existingById.get(playlistId);
|
|
114
|
-
if (kept)
|
|
115
|
-
|
|
137
|
+
if (!kept)
|
|
138
|
+
continue;
|
|
139
|
+
// Later publish of an unscheduled playlist replaces earlier unscheduled ones.
|
|
140
|
+
// Scheduled (dated) assignments are preserved.
|
|
141
|
+
if (incomingHasUnscheduled &&
|
|
142
|
+
isUnscheduledPlaylist(kept.snapshot.schedule)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
assignments.push(kept);
|
|
116
146
|
continue;
|
|
117
147
|
}
|
|
118
|
-
const playlist = await this.store.getPlaylist(playlistId);
|
|
119
|
-
if (!playlist || playlist.retired) {
|
|
120
|
-
throw Object.assign(new Error(`Playlist not available: ${playlistId}`), {
|
|
121
|
-
code: "PLAYLIST_NOT_FOUND"
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
148
|
assignments.push({
|
|
125
149
|
playlistId,
|
|
126
|
-
publishedAt
|
|
127
|
-
snapshot:
|
|
150
|
+
publishedAt,
|
|
151
|
+
snapshot: incomingSnapshots.get(playlistId)
|
|
128
152
|
});
|
|
129
153
|
}
|
|
130
154
|
await this.store.setDeviceAssignments(deviceId, assignments);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.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.9.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build-player": "tomorrowos build --platform tizen"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@tomorrowos/sdk": "^0.9.
|
|
16
|
+
"@tomorrowos/sdk": "^0.9.4",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|
|
@@ -566,21 +566,32 @@ function parseScheduleDateTimeMs(dateStr, timeStr, defaultTime) {
|
|
|
566
566
|
return Number.isNaN(ms) ? null : ms;
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
/** True when the playlist has no start/end date or time (always-on / default content). */
|
|
570
|
+
function isUnscheduledPublishedPlaylist(playlist) {
|
|
571
|
+
const schedule = playlist?.schedule;
|
|
572
|
+
if (!schedule) return true;
|
|
573
|
+
return !(
|
|
574
|
+
schedule.startDate ||
|
|
575
|
+
schedule.endDate ||
|
|
576
|
+
schedule.start ||
|
|
577
|
+
schedule.end
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
569
581
|
function getPublishedPlaylistStartMs(playlist) {
|
|
570
582
|
const schedule = playlist?.schedule;
|
|
571
|
-
if (!schedule) return null;
|
|
583
|
+
if (isUnscheduledPublishedPlaylist(playlist) || !schedule) return null;
|
|
572
584
|
return parseScheduleDateTimeMs(schedule.startDate, schedule.start, "00:00");
|
|
573
585
|
}
|
|
574
586
|
|
|
575
587
|
function getPublishedPlaylistEndMs(playlist) {
|
|
576
588
|
const schedule = playlist?.schedule;
|
|
577
|
-
if (!schedule) return null;
|
|
589
|
+
if (isUnscheduledPublishedPlaylist(playlist) || !schedule) return null;
|
|
578
590
|
return parseScheduleDateTimeMs(schedule.endDate, schedule.end, "23:59");
|
|
579
591
|
}
|
|
580
592
|
|
|
581
593
|
function isPublishedPlaylistActiveNow(playlist, now = new Date()) {
|
|
582
|
-
|
|
583
|
-
if (!schedule) return true;
|
|
594
|
+
if (isUnscheduledPublishedPlaylist(playlist)) return true;
|
|
584
595
|
const nowMs = now.getTime();
|
|
585
596
|
const startMs = getPublishedPlaylistStartMs(playlist);
|
|
586
597
|
const endMs = getPublishedPlaylistEndMs(playlist);
|
|
@@ -589,24 +600,39 @@ function isPublishedPlaylistActiveNow(playlist, now = new Date()) {
|
|
|
589
600
|
return true;
|
|
590
601
|
}
|
|
591
602
|
|
|
603
|
+
/**
|
|
604
|
+
* Green-light target for the device card.
|
|
605
|
+
* - Prefer an in-window scheduled playlist (latest start wins).
|
|
606
|
+
* - Else among always-on (no schedule) playlists, prefer the latest publishedAt
|
|
607
|
+
* so a later publish overrides earlier ones in the activity indicator.
|
|
608
|
+
*/
|
|
592
609
|
function pickScheduledPlaylistForIndicator(playlists, now = new Date()) {
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
610
|
+
const list = Array.isArray(playlists) ? playlists : [];
|
|
611
|
+
const active = list
|
|
612
|
+
.map((playlist, index) => ({ playlist, index }))
|
|
613
|
+
.filter(({ playlist }) => isPublishedPlaylistActiveNow(playlist, now));
|
|
596
614
|
if (!active.length) return null;
|
|
597
615
|
|
|
598
|
-
active.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
616
|
+
const scheduled = active.filter(
|
|
617
|
+
({ playlist }) => !isUnscheduledPublishedPlaylist(playlist)
|
|
618
|
+
);
|
|
619
|
+
// Match player: scheduled takeovers beat always-on; always-on uses latest publish.
|
|
620
|
+
const pool = scheduled.length ? scheduled : active;
|
|
621
|
+
|
|
622
|
+
pool.sort((a, b) => {
|
|
623
|
+
if (scheduled.length) {
|
|
624
|
+
const aStart = getPublishedPlaylistStartMs(a.playlist) ?? 0;
|
|
625
|
+
const bStart = getPublishedPlaylistStartMs(b.playlist) ?? 0;
|
|
626
|
+
if (aStart !== bStart) return bStart - aStart;
|
|
627
|
+
}
|
|
628
|
+
const aPublished = new Date(a.playlist?.publishedAt || 0).getTime() || 0;
|
|
629
|
+
const bPublished = new Date(b.playlist?.publishedAt || 0).getTime() || 0;
|
|
630
|
+
if (aPublished !== bPublished) return bPublished - aPublished;
|
|
631
|
+
// Same publish time → later entry in the assignments list (last published) wins.
|
|
632
|
+
return b.index - a.index;
|
|
607
633
|
});
|
|
608
634
|
|
|
609
|
-
return
|
|
635
|
+
return pool[0]?.playlist || null;
|
|
610
636
|
}
|
|
611
637
|
|
|
612
638
|
function getSelectedPlaylist() {
|
|
@@ -1151,7 +1177,7 @@ function renderDeviceCards() {
|
|
|
1151
1177
|
if (isPlaying) {
|
|
1152
1178
|
const playingLight = document.createElement("span");
|
|
1153
1179
|
playingLight.className = "playlist-playing-light";
|
|
1154
|
-
playingLight.title = "Active now
|
|
1180
|
+
playingLight.title = "Active now";
|
|
1155
1181
|
label.appendChild(playingLight);
|
|
1156
1182
|
}
|
|
1157
1183
|
const rm = document.createElement("button");
|
|
@@ -1292,7 +1318,9 @@ function openPublishModal(deviceId) {
|
|
|
1292
1318
|
return;
|
|
1293
1319
|
}
|
|
1294
1320
|
|
|
1295
|
-
hint.textContent =
|
|
1321
|
+
hint.textContent =
|
|
1322
|
+
`Device ${deviceId} — add playlists not yet on this device (snapshot at publish time). ` +
|
|
1323
|
+
`Publishing a playlist with no start/end date/time replaces any previously published unscheduled playlist on this device.`;
|
|
1296
1324
|
checklist.innerHTML = "";
|
|
1297
1325
|
|
|
1298
1326
|
for (const pl of unpublished) {
|