@xiboplayer/schedule 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/schedule",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "Complete scheduling solution: campaigns, dayparting, interrupts, and overlays",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -11,7 +11,7 @@
11
11
  "./overlays": "./src/overlays.js"
12
12
  },
13
13
  "dependencies": {
14
- "@xiboplayer/utils": "0.4.0"
14
+ "@xiboplayer/utils": "0.4.3"
15
15
  },
16
16
  "devDependencies": {
17
17
  "vitest": "^2.0.0"
package/src/schedule.js CHANGED
@@ -41,6 +41,36 @@ export class ScheduleManager {
41
41
  return this.schedule?.dataConnectors || [];
42
42
  }
43
43
 
44
+ /**
45
+ * Get dependants map: layoutId → filenames that must be cached before that layout plays.
46
+ * Includes both per-layout and global dependants.
47
+ * Used by download manager to prioritize sub-playlist media alongside its parent layout.
48
+ * @returns {Map<number, string[]>} layoutId → dependant filenames
49
+ */
50
+ getDependantsMap() {
51
+ const map = new Map();
52
+ if (!this.schedule) return map;
53
+
54
+ const globalDeps = this.schedule.dependants || [];
55
+
56
+ const addLayout = (layout) => {
57
+ const id = parseInt(String(layout.file || layout.id).replace('.xlf', ''), 10);
58
+ const deps = [...globalDeps, ...(layout.dependants || [])];
59
+ if (deps.length > 0) map.set(id, deps);
60
+ };
61
+
62
+ if (this.schedule.layouts) {
63
+ for (const layout of this.schedule.layouts) addLayout(layout);
64
+ }
65
+ if (this.schedule.campaigns) {
66
+ for (const campaign of this.schedule.campaigns) {
67
+ for (const layout of campaign.layouts) addLayout(layout);
68
+ }
69
+ }
70
+
71
+ return map;
72
+ }
73
+
44
74
  /**
45
75
  * Check if a schedule item is active based on recurrence rules.
46
76
  * Supports Week, Day, and Month recurrence types.
@@ -707,4 +707,74 @@ describe('ScheduleManager - Actions and Commands', () => {
707
707
  expect(windowMs).toBeGreaterThanOrEqual(60 * 60 * 1000); // at least 1 hour
708
708
  });
709
709
  });
710
+
711
+ describe('getDependantsMap', () => {
712
+ it('should return empty map when no schedule', () => {
713
+ const map = manager.getDependantsMap();
714
+ expect(map.size).toBe(0);
715
+ });
716
+
717
+ it('should collect per-layout dependants from standalone layouts', () => {
718
+ manager.setSchedule({
719
+ layouts: [
720
+ { file: '472.xlf', dependants: ['11.pdf', 'video.mp4'] },
721
+ { file: '500.xlf', dependants: ['logo.png'] },
722
+ ],
723
+ campaigns: [],
724
+ });
725
+ const map = manager.getDependantsMap();
726
+ expect(map.get(472)).toEqual(['11.pdf', 'video.mp4']);
727
+ expect(map.get(500)).toEqual(['logo.png']);
728
+ });
729
+
730
+ it('should collect per-layout dependants from campaign layouts', () => {
731
+ manager.setSchedule({
732
+ layouts: [],
733
+ campaigns: [
734
+ {
735
+ id: 'c1',
736
+ layouts: [
737
+ { file: '300.xlf', dependants: ['font.woff2'] },
738
+ ],
739
+ },
740
+ ],
741
+ });
742
+ const map = manager.getDependantsMap();
743
+ expect(map.get(300)).toEqual(['font.woff2']);
744
+ });
745
+
746
+ it('should merge global dependants with per-layout dependants', () => {
747
+ manager.setSchedule({
748
+ dependants: ['global-font.woff2'],
749
+ layouts: [
750
+ { file: '472.xlf', dependants: ['11.pdf'] },
751
+ ],
752
+ campaigns: [],
753
+ });
754
+ const map = manager.getDependantsMap();
755
+ expect(map.get(472)).toEqual(['global-font.woff2', '11.pdf']);
756
+ });
757
+
758
+ it('should skip layouts with no dependants', () => {
759
+ manager.setSchedule({
760
+ layouts: [
761
+ { file: '100.xlf', dependants: [] },
762
+ { file: '200.xlf', dependants: ['bg.jpg'] },
763
+ ],
764
+ campaigns: [],
765
+ });
766
+ const map = manager.getDependantsMap();
767
+ expect(map.has(100)).toBe(false);
768
+ expect(map.get(200)).toEqual(['bg.jpg']);
769
+ });
770
+
771
+ it('should handle file IDs without .xlf extension', () => {
772
+ manager.setSchedule({
773
+ layouts: [{ file: '472', dependants: ['11.pdf'] }],
774
+ campaigns: [],
775
+ });
776
+ const map = manager.getDependantsMap();
777
+ expect(map.get(472)).toEqual(['11.pdf']);
778
+ });
779
+ });
710
780
  });
package/src/timeline.js CHANGED
@@ -153,6 +153,7 @@ function getPlayableLayouts(allLayouts, simPlays, timeMs) {
153
153
  * @param {Date} [options.from] - Start time (default: now)
154
154
  * @param {number} [options.hours] - Hours to simulate (default: 2)
155
155
  * @param {number} [options.defaultDuration] - Fallback duration in seconds (default: 60)
156
+ * @param {Date} [options.currentLayoutStartedAt] - When current layout started (adjusts first entry to remaining time)
156
157
  * @returns {Array<{layoutFile: string, startTime: Date, endTime: Date, duration: number, isDefault: boolean}>}
157
158
  */
158
159
  export function calculateTimeline(schedule, durations, options = {}) {
@@ -160,8 +161,10 @@ export function calculateTimeline(schedule, durations, options = {}) {
160
161
  const hours = options.hours || 2;
161
162
  const to = new Date(from.getTime() + hours * 3600000);
162
163
  const defaultDuration = options.defaultDuration || 60;
164
+ const currentLayoutStartedAt = options.currentLayoutStartedAt || null;
163
165
  const timeline = [];
164
166
  let currentTime = new Date(from);
167
+ let isFirstEntry = true;
165
168
 
166
169
  // Use getAllLayoutsAtTime if available (new API), fall back to getLayoutsAtTime (old API)
167
170
  const hasFullApi = typeof schedule.getAllLayoutsAtTime === 'function';
@@ -216,7 +219,16 @@ export function calculateTimeline(schedule, durations, options = {}) {
216
219
  // Round-robin through playable layouts
217
220
  for (let i = 0; i < playable.length && currentTime < to && timeline.length < maxEntries; i++) {
218
221
  const file = playable[i];
219
- const dur = durations.get(file) || defaultDuration;
222
+ let dur = durations.get(file) || defaultDuration;
223
+
224
+ // First entry: use remaining duration if we know when the current layout started
225
+ if (isFirstEntry && currentLayoutStartedAt) {
226
+ const elapsedSec = (from.getTime() - currentLayoutStartedAt.getTime()) / 1000;
227
+ const remaining = Math.max(1, Math.round(dur - elapsedSec));
228
+ dur = remaining;
229
+ isFirstEntry = false;
230
+ }
231
+
220
232
  const endMs = currentTime.getTime() + dur * 1000;
221
233
 
222
234
  const entry = {