@xiboplayer/schedule 0.6.13 → 0.7.1

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.6.13",
3
+ "version": "0.7.1",
4
4
  "description": "Complete scheduling solution: campaigns, dayparting, interrupts, and overlays",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -12,7 +12,7 @@
12
12
  "./overlays": "./src/overlays.js"
13
13
  },
14
14
  "dependencies": {
15
- "@xiboplayer/utils": "0.6.13"
15
+ "@xiboplayer/utils": "0.7.1"
16
16
  },
17
17
  "devDependencies": {
18
18
  "vitest": "^2.0.0"
package/src/index.d.ts CHANGED
@@ -17,5 +17,6 @@ export class ScheduleManager {
17
17
  export const scheduleManager: ScheduleManager;
18
18
 
19
19
  export function calculateTimeline(queue: Array<{layoutId: string, duration: number}>, queuePosition: number, options?: any): any[];
20
+ export function parseLayoutFile(f: string | number): number;
20
21
  export function parseLayoutDuration(xlf: string, videoDurations?: Map<string, number> | null): { duration: number; isDynamic: boolean };
21
22
  export function buildScheduleQueue(schedule: any, durations: Map<string, number>): any[];
package/src/index.js CHANGED
@@ -27,4 +27,4 @@ export { OverlayScheduler } from './overlays.js';
27
27
  * Offline timeline calculator — duration parser + timeline simulator
28
28
  * @module @xiboplayer/schedule/timeline
29
29
  */
30
- export { calculateTimeline, parseLayoutDuration, buildScheduleQueue } from './timeline.js';
30
+ export { calculateTimeline, parseLayoutDuration, parseLayoutFile, buildScheduleQueue } from './timeline.js';
@@ -0,0 +1,241 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>
3
+ import { describe, it, expect, beforeEach } from 'vitest';
4
+ import { OverlayScheduler } from './overlays.js';
5
+
6
+ function makeOverlay(overrides = {}) {
7
+ return {
8
+ file: 100,
9
+ fromdt: '2026-01-01T00:00:00',
10
+ todt: '2026-12-31T23:59:59',
11
+ priority: 0,
12
+ ...overrides,
13
+ };
14
+ }
15
+
16
+ describe('OverlayScheduler', () => {
17
+ let scheduler;
18
+
19
+ beforeEach(() => {
20
+ scheduler = new OverlayScheduler();
21
+ });
22
+
23
+ // ── Constructor ────────────────────────────────────────────────
24
+
25
+ describe('constructor', () => {
26
+ it('initializes with empty overlays', () => {
27
+ expect(scheduler.overlays).toEqual([]);
28
+ expect(scheduler.displayProperties).toEqual({});
29
+ });
30
+ });
31
+
32
+ // ── setOverlays ───────────────────────────────────────────────
33
+
34
+ describe('setOverlays', () => {
35
+ it('stores overlay list', () => {
36
+ scheduler.setOverlays([makeOverlay()]);
37
+ expect(scheduler.overlays).toHaveLength(1);
38
+ });
39
+
40
+ it('handles null/undefined', () => {
41
+ scheduler.setOverlays(null);
42
+ expect(scheduler.overlays).toEqual([]);
43
+ });
44
+
45
+ it('replaces previous overlays', () => {
46
+ scheduler.setOverlays([makeOverlay({ file: 1 })]);
47
+ scheduler.setOverlays([makeOverlay({ file: 2 }), makeOverlay({ file: 3 })]);
48
+ expect(scheduler.overlays).toHaveLength(2);
49
+ expect(scheduler.overlays[0].file).toBe(2);
50
+ });
51
+ });
52
+
53
+ // ── isTimeActive ──────────────────────────────────────────────
54
+
55
+ describe('isTimeActive', () => {
56
+ it('returns true when now is within time window', () => {
57
+ const overlay = makeOverlay({
58
+ fromdt: '2026-03-01T00:00:00',
59
+ todt: '2026-03-31T23:59:59',
60
+ });
61
+ const now = new Date('2026-03-15T12:00:00');
62
+ expect(scheduler.isTimeActive(overlay, now)).toBe(true);
63
+ });
64
+
65
+ it('returns false when now is before fromdt', () => {
66
+ const overlay = makeOverlay({ fromdt: '2026-06-01T00:00:00' });
67
+ const now = new Date('2026-05-01T12:00:00');
68
+ expect(scheduler.isTimeActive(overlay, now)).toBe(false);
69
+ });
70
+
71
+ it('returns false when now is after todt', () => {
72
+ const overlay = makeOverlay({ todt: '2026-01-01T00:00:00' });
73
+ const now = new Date('2026-06-01T12:00:00');
74
+ expect(scheduler.isTimeActive(overlay, now)).toBe(false);
75
+ });
76
+
77
+ it('returns true when no time bounds set', () => {
78
+ const overlay = { file: 1 }; // no fromdt/todt
79
+ expect(scheduler.isTimeActive(overlay, new Date())).toBe(true);
80
+ });
81
+
82
+ it('supports toDt (camelCase) alias', () => {
83
+ const overlay = { file: 1, fromDt: '2026-01-01', toDt: '2026-12-31' };
84
+ expect(scheduler.isTimeActive(overlay, new Date('2026-06-15'))).toBe(true);
85
+ });
86
+ });
87
+
88
+ // ── getCurrentOverlays ────────────────────────────────────────
89
+
90
+ describe('getCurrentOverlays', () => {
91
+ it('returns empty array when no overlays set', () => {
92
+ expect(scheduler.getCurrentOverlays()).toEqual([]);
93
+ });
94
+
95
+ it('returns only overlays within time window', () => {
96
+ scheduler.setOverlays([
97
+ makeOverlay({ file: 1, fromdt: '2026-01-01', todt: '2026-06-30' }),
98
+ makeOverlay({ file: 2, fromdt: '2026-07-01', todt: '2026-12-31' }),
99
+ ]);
100
+
101
+ // Mock Date to be in Q1
102
+ const origDate = global.Date;
103
+ global.Date = class extends origDate {
104
+ constructor(...args) {
105
+ if (args.length === 0) return new origDate('2026-03-15T12:00:00');
106
+ return new origDate(...args);
107
+ }
108
+ };
109
+
110
+ const active = scheduler.getCurrentOverlays();
111
+ expect(active).toHaveLength(1);
112
+ expect(active[0].file).toBe(1);
113
+
114
+ global.Date = origDate;
115
+ });
116
+
117
+ it('sorts by priority descending', () => {
118
+ scheduler.setOverlays([
119
+ makeOverlay({ file: 1, priority: 5 }),
120
+ makeOverlay({ file: 2, priority: 10 }),
121
+ makeOverlay({ file: 3, priority: 1 }),
122
+ ]);
123
+
124
+ // All overlays are within 2026 time window
125
+ const origDate = global.Date;
126
+ global.Date = class extends origDate {
127
+ constructor(...args) {
128
+ if (args.length === 0) return new origDate('2026-06-15T12:00:00');
129
+ return new origDate(...args);
130
+ }
131
+ };
132
+
133
+ const active = scheduler.getCurrentOverlays();
134
+ expect(active[0].priority).toBe(10);
135
+ expect(active[1].priority).toBe(5);
136
+ expect(active[2].priority).toBe(1);
137
+
138
+ global.Date = origDate;
139
+ });
140
+
141
+ it('defaults priority to 0 when not set', () => {
142
+ scheduler.setOverlays([
143
+ makeOverlay({ file: 1 }), // no priority
144
+ makeOverlay({ file: 2, priority: 5 }),
145
+ ]);
146
+
147
+ const origDate = global.Date;
148
+ global.Date = class extends origDate {
149
+ constructor(...args) {
150
+ if (args.length === 0) return new origDate('2026-06-15T12:00:00');
151
+ return new origDate(...args);
152
+ }
153
+ };
154
+
155
+ const active = scheduler.getCurrentOverlays();
156
+ expect(active[0].file).toBe(2); // priority 5 first
157
+ expect(active[1].file).toBe(1); // priority 0
158
+
159
+ global.Date = origDate;
160
+ });
161
+
162
+ it('filters by geo-fence when isGeoAware', () => {
163
+ const mockScheduleManager = {
164
+ isWithinGeoFence: () => false,
165
+ };
166
+ scheduler.setScheduleManager(mockScheduleManager);
167
+
168
+ scheduler.setOverlays([
169
+ makeOverlay({ file: 1, isGeoAware: true, geoLocation: { lat: 41, lng: 2 } }),
170
+ makeOverlay({ file: 2 }), // not geo-aware
171
+ ]);
172
+
173
+ const origDate = global.Date;
174
+ global.Date = class extends origDate {
175
+ constructor(...args) {
176
+ if (args.length === 0) return new origDate('2026-06-15T12:00:00');
177
+ return new origDate(...args);
178
+ }
179
+ };
180
+
181
+ const active = scheduler.getCurrentOverlays();
182
+ expect(active).toHaveLength(1);
183
+ expect(active[0].file).toBe(2);
184
+
185
+ global.Date = origDate;
186
+ });
187
+ });
188
+
189
+ // ── getOverlayByFile ──────────────────────────────────────────
190
+
191
+ describe('getOverlayByFile', () => {
192
+ it('finds overlay by file ID', () => {
193
+ scheduler.setOverlays([makeOverlay({ file: 42 })]);
194
+ expect(scheduler.getOverlayByFile(42)).not.toBeNull();
195
+ expect(scheduler.getOverlayByFile(42).file).toBe(42);
196
+ });
197
+
198
+ it('returns null for unknown file', () => {
199
+ expect(scheduler.getOverlayByFile(999)).toBeNull();
200
+ });
201
+ });
202
+
203
+ // ── shouldCheckOverlays ───────────────────────────────────────
204
+
205
+ describe('shouldCheckOverlays', () => {
206
+ it('returns true when no last check', () => {
207
+ expect(scheduler.shouldCheckOverlays(null)).toBe(true);
208
+ expect(scheduler.shouldCheckOverlays(undefined)).toBe(true);
209
+ });
210
+
211
+ it('returns true after 60+ seconds', () => {
212
+ const lastCheck = Date.now() - 61000;
213
+ expect(scheduler.shouldCheckOverlays(lastCheck)).toBe(true);
214
+ });
215
+
216
+ it('returns false within 60 seconds', () => {
217
+ const lastCheck = Date.now() - 30000;
218
+ expect(scheduler.shouldCheckOverlays(lastCheck)).toBe(false);
219
+ });
220
+ });
221
+
222
+ // ── clear / processOverlays ───────────────────────────────────
223
+
224
+ describe('clear', () => {
225
+ it('removes all overlays', () => {
226
+ scheduler.setOverlays([makeOverlay()]);
227
+ scheduler.clear();
228
+ expect(scheduler.overlays).toHaveLength(0);
229
+ });
230
+ });
231
+
232
+ describe('processOverlays', () => {
233
+ it('sets overlays and returns layouts unchanged', () => {
234
+ const layouts = [{ id: 1 }, { id: 2 }];
235
+ const overlays = [makeOverlay({ file: 10 })];
236
+ const result = scheduler.processOverlays(layouts, overlays);
237
+ expect(result).toBe(layouts);
238
+ expect(scheduler.overlays).toHaveLength(1);
239
+ });
240
+ });
241
+ });
package/src/schedule.js CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { createLogger } from '@xiboplayer/utils';
8
8
  import { evaluateCriteria } from './criteria.js';
9
- import { buildScheduleQueue } from './timeline.js';
9
+ import { buildScheduleQueue, parseLayoutFile } from './timeline.js';
10
10
 
11
11
  const log = createLogger('Schedule');
12
12
 
@@ -63,7 +63,7 @@ export class ScheduleManager {
63
63
  const globalDeps = this.schedule.dependants || [];
64
64
 
65
65
  const addLayout = (layout) => {
66
- const id = parseInt(String(layout.file || layout.id).replace('.xlf', ''), 10);
66
+ const id = parseLayoutFile(layout.file || layout.id);
67
67
  const deps = [...globalDeps, ...(layout.dependants || [])];
68
68
  if (deps.length > 0) map.set(id, deps);
69
69
  };
@@ -691,6 +691,31 @@ export class ScheduleManager {
691
691
  * @param {Object} [options]
692
692
  * @returns {{ layoutId: string, duration: number } | null}
693
693
  */
694
+ /**
695
+ * Get current queue position.
696
+ * @returns {number}
697
+ */
698
+ getQueuePosition() {
699
+ return this._queuePosition;
700
+ }
701
+
702
+ /**
703
+ * Rewind the queue by N positions (wraps around).
704
+ * Used by advanceToPreviousLayout to go back in the schedule.
705
+ * @param {number} steps - Number of positions to rewind
706
+ * @param {Map<string, number>} durations
707
+ * @param {Object} [options]
708
+ * @returns {{ layoutId: string, duration: number } | null}
709
+ */
710
+ rewindQueue(steps, durations, options = {}) {
711
+ const { queue } = this.getScheduleQueue(durations, options);
712
+ if (queue.length === 0) return null;
713
+ this._queuePosition = (this._queuePosition - steps + queue.length * steps) % queue.length;
714
+ const entry = queue[this._queuePosition];
715
+ this._queuePosition = (this._queuePosition + 1) % queue.length;
716
+ return entry;
717
+ }
718
+
694
719
  peekNextInQueue(durations, options = {}) {
695
720
  const { queue } = this.getScheduleQueue(durations, options);
696
721
  if (queue.length === 0) return null;
package/src/timeline.js CHANGED
@@ -27,6 +27,15 @@
27
27
  * @param {Map<string, number>|null} [videoDurations=null] - Optional map of fileId → probed duration in seconds
28
28
  * @returns {{ duration: number, isDynamic: boolean }} Duration in seconds and whether any widget has useDuration=0
29
29
  */
30
+ /**
31
+ * Extract numeric layout ID from a schedule filename like "123.xlf" or "123"
32
+ * @param {string|number} f - Layout file reference
33
+ * @returns {number}
34
+ */
35
+ export function parseLayoutFile(f) {
36
+ return parseInt(String(f).replace('.xlf', ''), 10);
37
+ }
38
+
30
39
  export function parseLayoutDuration(xlfXml, videoDurations = null) {
31
40
  const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');
32
41
  const layoutEl = doc.querySelector('layout');