@xiboplayer/pwa 0.7.12 → 0.7.13

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.
Files changed (34) hide show
  1. package/dist/assets/{html2canvas-D9YrxbSe.js → html2canvas-BdjGaL7S.js} +2 -2
  2. package/dist/assets/{html2canvas-D9YrxbSe.js.map → html2canvas-BdjGaL7S.js.map} +1 -1
  3. package/dist/assets/{main-BytxDgA6.js → main-CT7JIMxf.js} +5 -5
  4. package/dist/assets/{main-BytxDgA6.js.map → main-CT7JIMxf.js.map} +1 -1
  5. package/dist/assets/{pdf-Cim3XbjS.js → pdf-ByOGXKA2.js} +2 -2
  6. package/dist/assets/{pdf-Cim3XbjS.js.map → pdf-ByOGXKA2.js.map} +1 -1
  7. package/dist/assets/{setup-BFWT01-R.js → setup-tmBa6Ubb.js} +2 -2
  8. package/dist/assets/{setup-BFWT01-R.js.map → setup-tmBa6Ubb.js.map} +1 -1
  9. package/dist/assets/{src-wmNLeJQ3.js → src-B9Yas4SH.js} +1 -1
  10. package/dist/assets/{src-BEkBRqOh.js → src-BfTQUSd6.js} +2 -2
  11. package/dist/assets/{src-BEkBRqOh.js.map → src-BfTQUSd6.js.map} +1 -1
  12. package/dist/assets/{src-Cplt1eH9.js → src-BvglFNkK.js} +2 -2
  13. package/dist/assets/{src-Cplt1eH9.js.map → src-BvglFNkK.js.map} +1 -1
  14. package/dist/assets/{src-DhbgZf-p.js → src-Ct_7vG0E.js} +1 -1
  15. package/dist/assets/{src-BCSDJZKF.js → src-Cyk0X_mz.js} +2 -2
  16. package/dist/assets/{src-BCSDJZKF.js.map → src-Cyk0X_mz.js.map} +1 -1
  17. package/dist/assets/{src-BgXR6vhE.js → src-DGSLOm9k.js} +2 -2
  18. package/dist/assets/{src-BgXR6vhE.js.map → src-DGSLOm9k.js.map} +1 -1
  19. package/dist/assets/{src-BxHs3cOi.js → src-DNqc--R2.js} +2 -2
  20. package/dist/assets/{src-BxHs3cOi.js.map → src-DNqc--R2.js.map} +1 -1
  21. package/dist/assets/{src-mns1H4Qq.js → src-DgZHc2Af.js} +2 -2
  22. package/dist/assets/{src-mns1H4Qq.js.map → src-DgZHc2Af.js.map} +1 -1
  23. package/dist/assets/{src-CYB7yWaE.js → src-DmdLCQQf.js} +1 -1
  24. package/dist/assets/{src-B1CMJkFl.js → src-Dr4QlWKq.js} +2 -2
  25. package/dist/assets/{src-B1CMJkFl.js.map → src-Dr4QlWKq.js.map} +1 -1
  26. package/dist/assets/{src-CkqT69S8.js → src-DwSNRd1O.js} +2 -2
  27. package/dist/assets/{src-CkqT69S8.js.map → src-DwSNRd1O.js.map} +1 -1
  28. package/dist/assets/{sync-manager-Djwpg3Cy.js → sync-manager-zTULh3xK.js} +2 -2
  29. package/dist/assets/{sync-manager-Djwpg3Cy.js.map → sync-manager-zTULh3xK.js.map} +1 -1
  30. package/dist/index.html +4 -4
  31. package/dist/setup.html +6 -6
  32. package/dist/sw-pwa.js +2 -2
  33. package/dist/sw-pwa.js.map +1 -1
  34. package/package.json +13 -13
@@ -1,2 +1,2 @@
1
- import{v as e}from"./src-BgXR6vhE.js";var t={name:`@xiboplayer/schedule`,version:`0.7.12`,description:`Complete scheduling solution: campaigns, dayparting, interrupts, and overlays`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./schedule":`./src/schedule.js`,"./interrupts":`./src/interrupts.js`,"./overlays":`./src/overlays.js`},scripts:{test:`vitest run`,"test:watch":`vitest`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},devDependencies:{vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`scheduling`,`dayparting`,`campaigns`,`interrupts`,`overlays`,`shareOfVoice`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/schedule`},homepage:`https://xiboplayer.org`},n=e(`schedule:criteria`),r=[`Sunday`,`Monday`,`Tuesday`,`Wednesday`,`Thursday`,`Friday`,`Saturday`],i={weatherTemp:`temperature`,weatherHumidity:`humidity`,weatherWindSpeed:`windSpeed`,weatherCondition:`condition`,weatherCloudCover:`cloudCover`};function a(e,t,a={},o={}){switch(e){case`dayOfWeek`:return r[t.getDay()];case`dayOfMonth`:return String(t.getDate());case`month`:return String(t.getMonth()+1);case`hour`:return String(t.getHours());case`isoDay`:return String(t.getDay()===0?7:t.getDay());default:if(i[e]){let t=i[e];return o[t]===void 0?(n.debug(`Weather metric "${e}" requested but no weather data available`),null):String(o[t])}return a[e]===void 0?(n.debug(`Unknown metric: ${e}`),null):String(a[e])}}function o(e,t,r,i){if(e===null)return!1;if(i===`number`){let n=parseFloat(e),i=parseFloat(r);if(isNaN(n)||isNaN(i))return!1;switch(t){case`equals`:return n===i;case`notEquals`:return n!==i;case`greaterThan`:return n>i;case`greaterThanOrEquals`:return n>=i;case`lessThan`:return n<i;case`lessThanOrEquals`:return n<=i;default:return!1}}let a=e.toLowerCase(),o=r.toLowerCase();switch(t){case`equals`:return a===o;case`notEquals`:return a!==o;case`contains`:return a.includes(o);case`notContains`:return!a.includes(o);case`startsWith`:return a.startsWith(o);case`endsWith`:return a.endsWith(o);case`in`:return o.split(`,`).map(e=>e.trim().toLowerCase()).includes(a);case`greaterThan`:return a>o;case`lessThan`:return a<o;default:return n.debug(`Unknown condition: ${t}`),!1}}function s(e,t={}){if(!e||e.length===0)return!0;let r=t.now||new Date,i=t.displayProperties||{},s=t.weatherData||{};for(let t of e){let e=a(t.metric,r,i,s);if(!o(e,t.condition,t.value,t.type))return n.debug(`Criteria failed: ${t.metric} ${t.condition} "${t.value}" (actual: "${e}")`),!1}return!0}function c(e){return parseInt(String(e).replace(`.xlf`,``),10)}function l(e,t=null){let n=new DOMParser().parseFromString(e,`text/xml`).querySelector(`layout`);if(!n)return{duration:60,isDynamic:!1};let r=parseInt(n.getAttribute(`duration`)||`0`,10);if(r>0)return{duration:r,isDynamic:!1};let i=0,a=!1;for(let e of n.querySelectorAll(`region`)){let n=e.getAttribute(`type`);if(n===`drawer`)continue;let r=n===`canvas`,o=0;for(let n of e.querySelectorAll(`media`)){let e=parseInt(n.getAttribute(`duration`)||`0`,10),i=parseInt(n.getAttribute(`useDuration`)||`1`,10),s=n.getAttribute(`fileId`)||``,c=t?.get(s),l;c===void 0?e>0&&i!==0?l=e:(l=60,a=!0):l=c,r?o=Math.max(o,l):o+=l}i=Math.max(i,o)}return{duration:i>0?i:60,isDynamic:a}}function u(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function d(e,t,n){if(!t||t===0)return!0;let r=n-36e5,i=e.filter(e=>e>r);if(i.length>=t)return!1;if(i.length>0){let e=36e5/t;if(n-Math.max(...i)<e)return!1}return!0}function f(e,t,n){let r=e.filter(e=>!e.maxPlaysPerHour||e.maxPlaysPerHour===0?!0:d(t.get(e.file)||[],e.maxPlaysPerHour,n));if(r.length===0)return[];let i=Math.max(...r.map(e=>e.priority));return r.filter(e=>e.priority===i).map(e=>e.file)}function p(e,t,n={}){let r=n.from||new Date,i=n.hours||2,a=new Date(r.getTime()+i*36e5);n.currentLayoutStartedAt;let o=n.defaultLayout||null,s=n.durations||null;if(!e||e.length===0)return[];let c=[],l=new Date(r),u=t%e.length;for(;l<a&&c.length<500;){let t=e[u],n=s&&s.get(t.layoutId)||t.duration,r=l.getTime()+n*1e3;c.push({layoutFile:t.layoutId,startTime:new Date(l),endTime:new Date(r),duration:n,isDefault:o?t.layoutId===o:!1}),l=new Date(r),u=(u+1)%e.length}return c}function m(e,t){for(e=Math.abs(Math.round(e)),t=Math.abs(Math.round(t));t;)[e,t]=[t,e%t];return e}function h(e,t){return e===0||t===0?0:Math.abs(Math.round(e)*Math.round(t))/m(e,t)}function g(e){return e.reduce((e,t)=>h(e,t),1)}function _(e,t,n={}){let{defaultLayout:r=null,defaultDuration:i=60}=n;if(e.length===0&&!r)return{queue:[],periodSeconds:0};let a=new Map;for(let t of e)t.duration>0&&a.set(t.file,t.duration);let o=e=>t.get(e)||a.get(e)||i,s=e.filter(e=>e.maxPlaysPerHour>0),c;s.length>0?(c=g(s.map(e=>Math.round(3600/e.maxPlaysPerHour))),c>7200&&(c=7200)):c=e.reduce((e,t)=>e+o(t.file),0)+(r&&!e.some(e=>e.file===r)?o(r):0)||i;let l=[],d=new Map,p=0,m=c*1e3;for(;p<m&&l.length<500;){let t=f(e,d,p);if(t.length===0){if(r){let e=o(r);l.push({layoutId:r,duration:e}),p+=e*1e3}else p+=6e4;continue}for(let n=0;n<t.length&&p<m&&l.length<500;n++){let r=t[n],i=o(r);if(l.push({layoutId:r,duration:i}),d.has(r)||d.set(r,[]),d.get(r).push(p),p+=i*1e3,!u(t,f(e,d,p)))break}}if(l.length===0&&r){let e=o(r);l.push({layoutId:r,duration:e})}return{queue:l,periodSeconds:c}}var v=e(`Schedule`),y=class{constructor(e={}){this.schedule=null,this.playHistory=new Map,this.interruptScheduler=e.interruptScheduler||null,this.displayProperties=e.displayProperties||{},this.weatherData={},this.playerLocation=null,this._layoutMetadata=new Map,this._scheduleQueue=null,this._queuePosition=0,this._queueLayoutSet=null}setSchedule(e){this.schedule=e,this._invalidateQueue()}setWeatherData(e){this.weatherData=e||{}}getDataConnectors(){return this.schedule?.dataConnectors||[]}getDependantsMap(){let e=new Map;if(!this.schedule)return e;let t=this.schedule.dependants||[],n=n=>{let r=c(n.file||n.id),i=[...t,...n.dependants||[]];i.length>0&&e.set(r,i)};if(this.schedule.layouts)for(let e of this.schedule.layouts)n(e);if(this.schedule.campaigns)for(let e of this.schedule.campaigns)for(let t of e.layouts)n(t);return e}isRecurringScheduleActive(e,t){if(!e.recurrenceType)return!0;if(e.recurrenceRange&&t>new Date(e.recurrenceRange))return!1;switch(e.recurrenceType){case`Week`:if(e.recurrenceRepeatsOn){let n=this.getIsoDayOfWeek(t);if(!e.recurrenceRepeatsOn.split(`,`).map(e=>parseInt(e.trim())).includes(n))return!1}return!0;case`Day`:{let n=e.recurrenceDetail||1;if(n>1&&e.fromdt){let r=new Date(e.fromdt),i=t.getTime()-r.getTime(),a=Math.floor(i/864e5);if(a<0||a%n!==0)return!1}return!0}case`Month`:{if(e.recurrenceRepeatsOn){let n=e.recurrenceRepeatsOn.split(`,`).map(e=>parseInt(e.trim())),r=t.getDate();if(!n.includes(r))return!1}let n=e.recurrenceDetail||1;if(n>1&&e.fromdt){let r=new Date(e.fromdt),i=(t.getFullYear()-r.getFullYear())*12+t.getMonth()-r.getMonth();if(i<0||i%n!==0)return!1}return!0}default:return v.debug(`Unsupported recurrence type: ${e.recurrenceType}`),!0}}getIsoDayOfWeek(e){let t=e.getDay();return t===0?7:t}isTimeActive(e,t){let n=e.fromdt?new Date(e.fromdt):null,r=e.todt?new Date(e.todt):null;if(e.recurrenceType===`Week`||e.recurrenceType===`Day`||e.recurrenceType===`Month`){if(n&&r){let e=t.getHours()*3600+t.getMinutes()*60+t.getSeconds(),i=n.getHours()*3600+n.getMinutes()*60+n.getSeconds(),a=r.getHours()*3600+r.getMinutes()*60+r.getSeconds();return i<=a?e>=i&&e<=a:e>=i||e<=a}return!0}return!(n&&t<n||r&&t>r)}getCurrentLayouts(){return this._getLayoutsAt(new Date)}getLayoutsAtTime(e){return this._getLayoutsAt(e,{skipRateLimiting:!0,skipInterrupts:!0,quiet:!0})}getAllLayoutsAtTime(e){if(!this.schedule)return[];let t=e,n=[];if(this.schedule.layouts)for(let e of this.schedule.layouts)this.isRecurringScheduleActive(e,t)&&this.isTimeActive(e,t)&&(e.criteria&&e.criteria.length>0&&!s(e.criteria,{now:t,displayProperties:this.displayProperties,weatherData:this.weatherData})||e.isGeoAware&&e.geoLocation&&!this.isWithinGeoFence(e.geoLocation)||n.push({file:e.file,priority:e.priority||0,maxPlaysPerHour:e.maxPlaysPerHour||0,duration:e.duration||0}));if(this.schedule.campaigns){for(let e of this.schedule.campaigns)if(this.isRecurringScheduleActive(e,t)&&this.isTimeActive(e,t))for(let t of e.layouts)n.push({file:t.file,priority:e.priority||0,maxPlaysPerHour:t.maxPlaysPerHour||0,duration:t.duration||0})}return n}detectConflicts(e={}){let t=e.from||new Date,n=e.hours||24,r=new Date(t.getTime()+n*36e5),i=6e4,a=[],o=null;for(let e=t.getTime();e<r.getTime();e+=i){let t=new Date(e),n=this.getAllLayoutsAtTime(t);if(n.length===0){o&&=(a.push(o),null);continue}let r=Math.max(...n.map(e=>e.priority)),s=n.filter(e=>e.priority<r);if(s.length===0){o&&=(a.push(o),null);continue}let c=n.filter(e=>e.priority===r),l=c.map(e=>e.file).sort().join(`,`),u=s.map(e=>`${e.file}:${e.priority}`).sort().join(`,`);o&&o._winnerKey===l&&o._hiddenKey===u?o.endTime=new Date(e+i):(o&&a.push(o),o={startTime:new Date(e),endTime:new Date(e+i),winner:{file:c[0].file,priority:r},hidden:s.map(e=>({file:e.file,priority:e.priority})),_winnerKey:l,_hiddenKey:u})}o&&a.push(o);for(let e of a)delete e._winnerKey,delete e._hiddenKey;return a}_getLayoutsAt(e,t={}){if(!this.schedule)return[];let{skipRateLimiting:n=!1,skipInterrupts:r=!1,quiet:i=!1}=t,a=i?()=>{}:(...e)=>v.info(...e),o=[];if(this._maxActivePriority=0,this.schedule.campaigns)for(let t of this.schedule.campaigns)this.isRecurringScheduleActive(t,e)&&this.isTimeActive(t,e)&&(this._maxActivePriority=Math.max(this._maxActivePriority,t.priority||0),o.push({type:`campaign`,priority:t.priority,layouts:t.layouts,campaignId:t.id}));if(this.schedule.layouts){for(let t of this.schedule.layouts)if(this.isRecurringScheduleActive(t,e)&&this.isTimeActive(t,e)){if(t.criteria&&t.criteria.length>0&&!s(t.criteria,{now:e,displayProperties:this.displayProperties,weatherData:this.weatherData})){a(`[Schedule] Layout`,t.id,`filtered by criteria`);continue}if(t.isGeoAware&&t.geoLocation&&!this.isWithinGeoFence(t.geoLocation)){a(`[Schedule] Layout`,t.id,`filtered by geofence`);continue}if(this._maxActivePriority=Math.max(this._maxActivePriority,t.priority||0),!n&&!this.canPlayLayout(t.id,t.maxPlaysPerHour)){a(`[Schedule] Layout`,t.id,`filtered by maxPlaysPerHour (limit:`,t.maxPlaysPerHour,`)`);continue}o.push({type:`layout`,priority:t.priority||0,layouts:[t],layoutId:t.id})}}if(o.length===0)return this.schedule.default?[this.schedule.default]:[];let c=Math.max(...o.map(e=>e.priority));a(`[Schedule] Max priority:`,c,`from`,o.length,`active items`);let l=[];for(let e of o)e.priority===c?(a(`[Schedule] Including priority`,e.priority,`layouts:`,e.layouts.map(e=>e.file)),l.push(...e.layouts)):a(`[Schedule] Skipping priority`,e.priority,`< max`,c);this._layoutMetadata.clear();for(let e of l)this._layoutMetadata.set(e.file,{syncEvent:e.syncEvent||!1,shareOfVoice:e.shareOfVoice||0,scheduleid:e.scheduleid,priority:e.priority||0});if(!r&&this.interruptScheduler){let{normalLayouts:e,interruptLayouts:t}=this.interruptScheduler.separateLayouts(l);if(t.length>0){a(`[Schedule] Found`,t.length,`interrupt layouts with shareOfVoice`);let n=this.interruptScheduler.processInterrupts(e,t).map(e=>e.file);return a(`[Schedule] Final layouts (with interrupts):`,n),n}}let u=l.map(e=>e.file);return a(`[Schedule] Final layouts:`,u),u}shouldCheckSchedule(e){return e?Date.now()-e>=6e4:!0}canPlayLayout(e,t){return d(this.playHistory.get(e)||[],t,Date.now())}recordPlay(e){this.playHistory.has(e)||this.playHistory.set(e,[]);let t=this.playHistory.get(e);t.push(Date.now());let n=Date.now()-3600*1e3,r=t.filter(e=>e>n);this.playHistory.set(e,r),v.info(`Recorded play for layout ${e} (${r.length} plays in last hour)`)}isSyncEvent(e){return this._layoutMetadata.get(e)?.syncEvent===!0}getLayoutMetadata(e){return this._layoutMetadata.get(e)||null}getScheduleQueue(e,t={}){let n=this.getAllLayoutsAtTime(new Date),r=n.map(e=>`${e.file}:${e.priority}:${e.maxPlaysPerHour}`).sort().join(`|`);if(this._scheduleQueue&&this._queueLayoutSet===r)return this._scheduleQueue;let i=_(n,e,{defaultLayout:this.schedule?.default||null,defaultDuration:60,dynamicLayouts:t.dynamicLayouts||new Set}),a=this._queueLayoutSet;return this._scheduleQueue=i,this._queueLayoutSet=r,a!==r&&(this._queuePosition=0),i.queue.length>0&&(v.info(`[Schedule] Built queue: ${i.queue.length} entries, period ${i.periodSeconds}s (pos ${this._queuePosition})`),v.info(`[Schedule] Queue: ${i.queue.map(e=>`${e.layoutId}(${e.duration}s)`).join(` → `)}`)),i}popNextFromQueue(e,t={}){let{queue:n}=this.getScheduleQueue(e,t);if(n.length===0)return null;let r=n[this._queuePosition%n.length];return this._queuePosition=(this._queuePosition+1)%n.length,r}getQueuePosition(){return this._queuePosition}rewindQueue(e,t,n={}){let{queue:r}=this.getScheduleQueue(t,n);if(r.length===0)return null;this._queuePosition=(this._queuePosition-e+r.length*e)%r.length;let i=r[this._queuePosition];return this._queuePosition=(this._queuePosition+1)%r.length,i}peekNextInQueue(e,t={}){let{queue:n}=this.getScheduleQueue(e,t);return n.length===0?null:n[this._queuePosition%n.length]}peekAfterNext(e,t={}){let{queue:n}=this.getScheduleQueue(e,t);return n.length<=1?null:n[(this._queuePosition+1)%n.length]}invalidateQueue(){this._invalidateQueue()}_invalidateQueue(){this._scheduleQueue=null}hasSyncEvents(){for(let e of this._layoutMetadata.values())if(e.syncEvent)return!0;return!1}getActiveActions(){if(!this.schedule?.actions)return[];let e=new Date;return this.schedule.actions.filter(t=>this.isTimeActive(t,e))}getCommands(){return this.schedule?.commands||[]}findActionByTrigger(e){return this.getActiveActions().find(t=>t.triggerCode===e)||null}clearPlayHistory(){this.playHistory.clear(),v.info(`Play history cleared`)}setLocation(e,t){this.playerLocation={latitude:e,longitude:t},v.info(`Location set: ${e}, ${t}`)}setDisplayProperties(e){this.displayProperties=e||{}}isWithinGeoFence(e,t=500){if(!this.playerLocation)return v.debug(`No player location, skipping geofence check`),!0;if(!e)return!0;let n=e.split(`,`).map(e=>parseFloat(e.trim()));if(n.length<2||isNaN(n[0])||isNaN(n[1]))return v.warn(`Invalid geoLocation format:`,e),!0;let r=n[0],i=n[1],a=n[2]||t,o=this.haversineDistance(this.playerLocation.latitude,this.playerLocation.longitude,r,i),s=o<=a;return v.info(`Geofence: ${o.toFixed(0)}m from (${r},${i}), radius ${a}m → ${s?`WITHIN`:`OUTSIDE`}`),s}haversineDistance(e,t,n,r){let i=e=>e*Math.PI/180,a=i(n-e),o=i(r-t),s=Math.sin(a/2)**2+Math.cos(i(e))*Math.cos(i(n))*Math.sin(o/2)**2;return 6371e3*2*Math.atan2(Math.sqrt(s),Math.sqrt(1-s))}},b=new y,x=e(`schedule:interrupts`),S=class{constructor(){this.interruptCommittedDurations=new Map}isInterrupt(e){return!!(e.shareOfVoice&&e.shareOfVoice>0)}resetCommittedDurations(){this.interruptCommittedDurations.clear(),x.debug(`Reset interrupt committed durations`)}getCommittedDuration(e){return this.interruptCommittedDurations.get(e)||0}addCommittedDuration(e,t){let n=this.getCommittedDuration(e);this.interruptCommittedDurations.set(e,n+t)}isInterruptDurationSatisfied(e){if(!e.shareOfVoice)return!0;let t=e.id||e.file,n=e.shareOfVoice/100*3600;return this.getCommittedDuration(t)>=n}getRequiredSeconds(e){return e.shareOfVoice?e.shareOfVoice/100*3600:0}processInterrupts(e,t){if(!t||t.length===0)return x.debug(`No interrupt layouts, returning normal layouts`),e;if(!e||e.length===0)return x.warn(`No normal layouts available, interrupts will fill entire hour`),this.fillHourWithInterrupts(t);x.info(`Processing ${t.length} interrupt layouts with ${e.length} normal layouts`);for(let e of t){let t=e.id||e.file;this.interruptCommittedDurations.set(t,0)}let n=[],r=0,i=0,a=!1;for(;!a;){if(i>=t.length){i=0;let e=!0;for(let n of t)if(!this.isInterruptDurationSatisfied(n)){e=!1;break}if(e){a=!0;break}}let e=t[i];if(!this.isInterruptDurationSatisfied(e)){let t=e.id||e.file;this.addCommittedDuration(t,e.duration),r+=e.duration,n.push(e)}i++}if(x.debug(`Resolved ${n.length} interrupt plays (${r}s total)`),r>=3600)return x.info(`Interrupts fill entire hour (>= 3600s), no room for normal layouts`),n;let o=3600-r,s=this.fillTimeWithLayouts(e,o);x.debug(`Resolved ${s.length} normal plays (${o}s target)`);let c=this.interleaveLayouts(s,n);return x.info(`Final loop: ${c.length} layouts (${s.length} normal + ${n.length} interrupts)`),c}fillTimeWithLayouts(e,t){let n=[],r=t,i=0;for(;r>0;){i>=e.length&&(i=0);let t=e[i];n.push(t),r-=t.duration,i++}return n}fillHourWithInterrupts(e){return this.fillTimeWithLayouts(e,3600)}interleaveLayouts(e,t){let n=[],r=Math.max(e.length,t.length),i=Math.ceil(1*r/e.length),a=Math.floor(1*r/t.length);x.debug(`Interleaving: pickCount=${r}, normalPick=${i}, interruptPick=${a}`);let o=0,s=0,c=0;for(let l=0;l<r;l++)l%i===0&&(o>=e.length&&(o=0),n.push(e[o]),c+=e[o].duration,o++),l%a===0&&s<t.length&&(n.push(t[s]),c+=t[s].duration,s++);for(;c<3600;)o>=e.length&&(o=0),n.push(e[o]),c+=e[o].duration,o++;return x.debug(`Interleaved ${n.length} layouts, total duration: ${c}s`),n}separateLayouts(e){let t=[],n=[];for(let r of e)this.isInterrupt(r)?n.push(r):t.push(r);return{normalLayouts:t,interruptLayouts:n}}};new S;var C=e(`schedule:overlays`),w=class{constructor(){this.overlays=[],this.displayProperties={},this.scheduleManager=null,C.debug(`OverlayScheduler initialized`)}setScheduleManager(e){this.scheduleManager=e}setDisplayProperties(e){this.displayProperties=e||{}}setOverlays(e){this.overlays=e||[],C.info(`Loaded ${this.overlays.length} overlay(s)`)}getCurrentOverlays(){if(!this.overlays||this.overlays.length===0)return[];let e=new Date,t=[];for(let n of this.overlays){if(!this.isTimeActive(n,e)){C.debug(`Overlay ${n.file} not in time window`);continue}if(n.isGeoAware&&n.geoLocation&&this.scheduleManager&&!this.scheduleManager.isWithinGeoFence(n.geoLocation)){C.debug(`Overlay ${n.file} filtered by geofence`);continue}if(n.criteria&&n.criteria.length>0&&!s(n.criteria,{now:e,displayProperties:this.displayProperties})){C.debug(`Overlay ${n.file} filtered by criteria`);continue}t.push(n)}return t.sort((e,t)=>{let n=e.priority||0;return(t.priority||0)-n}),t.length>0&&C.info(`Active overlays: ${t.length}`),t}isTimeActive(e,t){if(this.scheduleManager){let n={...e};return!n.fromdt&&n.fromDt&&(n.fromdt=n.fromDt),!n.todt&&n.toDt&&(n.todt=n.toDt),this.scheduleManager.isTimeActive(n,t)}let n=e.fromdt||e.fromDt?new Date(e.fromdt||e.fromDt):null,r=e.todt||e.toDt?new Date(e.todt||e.toDt):null;return!(n&&t<n||r&&t>r)}shouldCheckOverlays(e){return e?Date.now()-e>=6e4:!0}getOverlayByFile(e){return this.overlays.find(t=>t.file===e)||null}clear(){this.overlays=[],C.debug(`Cleared all overlays`)}};new w;var T=t.version;export{b as a,l as c,y as i,c as l,w as n,_ as o,S as r,p as s,T as t};
2
- //# sourceMappingURL=src-BxHs3cOi.js.map
1
+ import{v as e}from"./src-DGSLOm9k.js";var t={name:`@xiboplayer/schedule`,version:`0.7.13`,description:`Complete scheduling solution: campaigns, dayparting, interrupts, and overlays`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./schedule":`./src/schedule.js`,"./interrupts":`./src/interrupts.js`,"./overlays":`./src/overlays.js`},scripts:{test:`vitest run`,"test:watch":`vitest`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},devDependencies:{vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`scheduling`,`dayparting`,`campaigns`,`interrupts`,`overlays`,`shareOfVoice`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/schedule`},homepage:`https://xiboplayer.org`},n=e(`schedule:criteria`),r=[`Sunday`,`Monday`,`Tuesday`,`Wednesday`,`Thursday`,`Friday`,`Saturday`],i={weatherTemp:`temperature`,weatherHumidity:`humidity`,weatherWindSpeed:`windSpeed`,weatherCondition:`condition`,weatherCloudCover:`cloudCover`};function a(e,t,a={},o={}){switch(e){case`dayOfWeek`:return r[t.getDay()];case`dayOfMonth`:return String(t.getDate());case`month`:return String(t.getMonth()+1);case`hour`:return String(t.getHours());case`isoDay`:return String(t.getDay()===0?7:t.getDay());default:if(i[e]){let t=i[e];return o[t]===void 0?(n.debug(`Weather metric "${e}" requested but no weather data available`),null):String(o[t])}return a[e]===void 0?(n.debug(`Unknown metric: ${e}`),null):String(a[e])}}function o(e,t,r,i){if(e===null)return!1;if(i===`number`){let n=parseFloat(e),i=parseFloat(r);if(isNaN(n)||isNaN(i))return!1;switch(t){case`equals`:return n===i;case`notEquals`:return n!==i;case`greaterThan`:return n>i;case`greaterThanOrEquals`:return n>=i;case`lessThan`:return n<i;case`lessThanOrEquals`:return n<=i;default:return!1}}let a=e.toLowerCase(),o=r.toLowerCase();switch(t){case`equals`:return a===o;case`notEquals`:return a!==o;case`contains`:return a.includes(o);case`notContains`:return!a.includes(o);case`startsWith`:return a.startsWith(o);case`endsWith`:return a.endsWith(o);case`in`:return o.split(`,`).map(e=>e.trim().toLowerCase()).includes(a);case`greaterThan`:return a>o;case`lessThan`:return a<o;default:return n.debug(`Unknown condition: ${t}`),!1}}function s(e,t={}){if(!e||e.length===0)return!0;let r=t.now||new Date,i=t.displayProperties||{},s=t.weatherData||{};for(let t of e){let e=a(t.metric,r,i,s);if(!o(e,t.condition,t.value,t.type))return n.debug(`Criteria failed: ${t.metric} ${t.condition} "${t.value}" (actual: "${e}")`),!1}return!0}function c(e){return parseInt(String(e).replace(`.xlf`,``),10)}function l(e,t=null){let n=new DOMParser().parseFromString(e,`text/xml`).querySelector(`layout`);if(!n)return{duration:60,isDynamic:!1};let r=parseInt(n.getAttribute(`duration`)||`0`,10);if(r>0)return{duration:r,isDynamic:!1};let i=0,a=!1;for(let e of n.querySelectorAll(`region`)){let n=e.getAttribute(`type`);if(n===`drawer`)continue;let r=n===`canvas`,o=0;for(let n of e.querySelectorAll(`media`)){let e=parseInt(n.getAttribute(`duration`)||`0`,10),i=parseInt(n.getAttribute(`useDuration`)||`1`,10),s=n.getAttribute(`fileId`)||``,c=t?.get(s),l;c===void 0?e>0&&i!==0?l=e:(l=60,a=!0):l=c,r?o=Math.max(o,l):o+=l}i=Math.max(i,o)}return{duration:i>0?i:60,isDynamic:a}}function u(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function d(e,t,n){if(!t||t===0)return!0;let r=n-36e5,i=e.filter(e=>e>r);if(i.length>=t)return!1;if(i.length>0){let e=36e5/t;if(n-Math.max(...i)<e)return!1}return!0}function f(e,t,n){let r=e.filter(e=>!e.maxPlaysPerHour||e.maxPlaysPerHour===0?!0:d(t.get(e.file)||[],e.maxPlaysPerHour,n));if(r.length===0)return[];let i=Math.max(...r.map(e=>e.priority));return r.filter(e=>e.priority===i).map(e=>e.file)}function p(e,t,n={}){let r=n.from||new Date,i=n.hours||2,a=new Date(r.getTime()+i*36e5);n.currentLayoutStartedAt;let o=n.defaultLayout||null,s=n.durations||null;if(!e||e.length===0)return[];let c=[],l=new Date(r),u=t%e.length;for(;l<a&&c.length<500;){let t=e[u],n=s&&s.get(t.layoutId)||t.duration,r=l.getTime()+n*1e3;c.push({layoutFile:t.layoutId,startTime:new Date(l),endTime:new Date(r),duration:n,isDefault:o?t.layoutId===o:!1}),l=new Date(r),u=(u+1)%e.length}return c}function m(e,t){for(e=Math.abs(Math.round(e)),t=Math.abs(Math.round(t));t;)[e,t]=[t,e%t];return e}function h(e,t){return e===0||t===0?0:Math.abs(Math.round(e)*Math.round(t))/m(e,t)}function g(e){return e.reduce((e,t)=>h(e,t),1)}function _(e,t,n={}){let{defaultLayout:r=null,defaultDuration:i=60}=n;if(e.length===0&&!r)return{queue:[],periodSeconds:0};let a=new Map;for(let t of e)t.duration>0&&a.set(t.file,t.duration);let o=e=>t.get(e)||a.get(e)||i,s=e.filter(e=>e.maxPlaysPerHour>0),c;s.length>0?(c=g(s.map(e=>Math.round(3600/e.maxPlaysPerHour))),c>7200&&(c=7200)):c=e.reduce((e,t)=>e+o(t.file),0)+(r&&!e.some(e=>e.file===r)?o(r):0)||i;let l=[],d=new Map,p=0,m=c*1e3;for(;p<m&&l.length<500;){let t=f(e,d,p);if(t.length===0){if(r){let e=o(r);l.push({layoutId:r,duration:e}),p+=e*1e3}else p+=6e4;continue}for(let n=0;n<t.length&&p<m&&l.length<500;n++){let r=t[n],i=o(r);if(l.push({layoutId:r,duration:i}),d.has(r)||d.set(r,[]),d.get(r).push(p),p+=i*1e3,!u(t,f(e,d,p)))break}}if(l.length===0&&r){let e=o(r);l.push({layoutId:r,duration:e})}return{queue:l,periodSeconds:c}}var v=e(`Schedule`),y=class{constructor(e={}){this.schedule=null,this.playHistory=new Map,this.interruptScheduler=e.interruptScheduler||null,this.displayProperties=e.displayProperties||{},this.weatherData={},this.playerLocation=null,this._layoutMetadata=new Map,this._scheduleQueue=null,this._queuePosition=0,this._queueLayoutSet=null}setSchedule(e){this.schedule=e,this._invalidateQueue()}setWeatherData(e){this.weatherData=e||{}}getDataConnectors(){return this.schedule?.dataConnectors||[]}getDependantsMap(){let e=new Map;if(!this.schedule)return e;let t=this.schedule.dependants||[],n=n=>{let r=c(n.file||n.id),i=[...t,...n.dependants||[]];i.length>0&&e.set(r,i)};if(this.schedule.layouts)for(let e of this.schedule.layouts)n(e);if(this.schedule.campaigns)for(let e of this.schedule.campaigns)for(let t of e.layouts)n(t);return e}isRecurringScheduleActive(e,t){if(!e.recurrenceType)return!0;if(e.recurrenceRange&&t>new Date(e.recurrenceRange))return!1;switch(e.recurrenceType){case`Week`:if(e.recurrenceRepeatsOn){let n=this.getIsoDayOfWeek(t);if(!e.recurrenceRepeatsOn.split(`,`).map(e=>parseInt(e.trim())).includes(n))return!1}return!0;case`Day`:{let n=e.recurrenceDetail||1;if(n>1&&e.fromdt){let r=new Date(e.fromdt),i=t.getTime()-r.getTime(),a=Math.floor(i/864e5);if(a<0||a%n!==0)return!1}return!0}case`Month`:{if(e.recurrenceRepeatsOn){let n=e.recurrenceRepeatsOn.split(`,`).map(e=>parseInt(e.trim())),r=t.getDate();if(!n.includes(r))return!1}let n=e.recurrenceDetail||1;if(n>1&&e.fromdt){let r=new Date(e.fromdt),i=(t.getFullYear()-r.getFullYear())*12+t.getMonth()-r.getMonth();if(i<0||i%n!==0)return!1}return!0}default:return v.debug(`Unsupported recurrence type: ${e.recurrenceType}`),!0}}getIsoDayOfWeek(e){let t=e.getDay();return t===0?7:t}isTimeActive(e,t){let n=e.fromdt?new Date(e.fromdt):null,r=e.todt?new Date(e.todt):null;if(e.recurrenceType===`Week`||e.recurrenceType===`Day`||e.recurrenceType===`Month`){if(n&&r){let e=t.getHours()*3600+t.getMinutes()*60+t.getSeconds(),i=n.getHours()*3600+n.getMinutes()*60+n.getSeconds(),a=r.getHours()*3600+r.getMinutes()*60+r.getSeconds();return i<=a?e>=i&&e<=a:e>=i||e<=a}return!0}return!(n&&t<n||r&&t>r)}getCurrentLayouts(){return this._getLayoutsAt(new Date)}getLayoutsAtTime(e){return this._getLayoutsAt(e,{skipRateLimiting:!0,skipInterrupts:!0,quiet:!0})}getAllLayoutsAtTime(e){if(!this.schedule)return[];let t=e,n=[];if(this.schedule.layouts)for(let e of this.schedule.layouts)this.isRecurringScheduleActive(e,t)&&this.isTimeActive(e,t)&&(e.criteria&&e.criteria.length>0&&!s(e.criteria,{now:t,displayProperties:this.displayProperties,weatherData:this.weatherData})||e.isGeoAware&&e.geoLocation&&!this.isWithinGeoFence(e.geoLocation)||n.push({file:e.file,priority:e.priority||0,maxPlaysPerHour:e.maxPlaysPerHour||0,duration:e.duration||0}));if(this.schedule.campaigns){for(let e of this.schedule.campaigns)if(this.isRecurringScheduleActive(e,t)&&this.isTimeActive(e,t))for(let t of e.layouts)n.push({file:t.file,priority:e.priority||0,maxPlaysPerHour:t.maxPlaysPerHour||0,duration:t.duration||0})}return n}detectConflicts(e={}){let t=e.from||new Date,n=e.hours||24,r=new Date(t.getTime()+n*36e5),i=6e4,a=[],o=null;for(let e=t.getTime();e<r.getTime();e+=i){let t=new Date(e),n=this.getAllLayoutsAtTime(t);if(n.length===0){o&&=(a.push(o),null);continue}let r=Math.max(...n.map(e=>e.priority)),s=n.filter(e=>e.priority<r);if(s.length===0){o&&=(a.push(o),null);continue}let c=n.filter(e=>e.priority===r),l=c.map(e=>e.file).sort().join(`,`),u=s.map(e=>`${e.file}:${e.priority}`).sort().join(`,`);o&&o._winnerKey===l&&o._hiddenKey===u?o.endTime=new Date(e+i):(o&&a.push(o),o={startTime:new Date(e),endTime:new Date(e+i),winner:{file:c[0].file,priority:r},hidden:s.map(e=>({file:e.file,priority:e.priority})),_winnerKey:l,_hiddenKey:u})}o&&a.push(o);for(let e of a)delete e._winnerKey,delete e._hiddenKey;return a}_getLayoutsAt(e,t={}){if(!this.schedule)return[];let{skipRateLimiting:n=!1,skipInterrupts:r=!1,quiet:i=!1}=t,a=i?()=>{}:(...e)=>v.info(...e),o=[];if(this._maxActivePriority=0,this.schedule.campaigns)for(let t of this.schedule.campaigns)this.isRecurringScheduleActive(t,e)&&this.isTimeActive(t,e)&&(this._maxActivePriority=Math.max(this._maxActivePriority,t.priority||0),o.push({type:`campaign`,priority:t.priority,layouts:t.layouts,campaignId:t.id}));if(this.schedule.layouts){for(let t of this.schedule.layouts)if(this.isRecurringScheduleActive(t,e)&&this.isTimeActive(t,e)){if(t.criteria&&t.criteria.length>0&&!s(t.criteria,{now:e,displayProperties:this.displayProperties,weatherData:this.weatherData})){a(`[Schedule] Layout`,t.id,`filtered by criteria`);continue}if(t.isGeoAware&&t.geoLocation&&!this.isWithinGeoFence(t.geoLocation)){a(`[Schedule] Layout`,t.id,`filtered by geofence`);continue}if(this._maxActivePriority=Math.max(this._maxActivePriority,t.priority||0),!n&&!this.canPlayLayout(t.id,t.maxPlaysPerHour)){a(`[Schedule] Layout`,t.id,`filtered by maxPlaysPerHour (limit:`,t.maxPlaysPerHour,`)`);continue}o.push({type:`layout`,priority:t.priority||0,layouts:[t],layoutId:t.id})}}if(o.length===0)return this.schedule.default?[this.schedule.default]:[];let c=Math.max(...o.map(e=>e.priority));a(`[Schedule] Max priority:`,c,`from`,o.length,`active items`);let l=[];for(let e of o)e.priority===c?(a(`[Schedule] Including priority`,e.priority,`layouts:`,e.layouts.map(e=>e.file)),l.push(...e.layouts)):a(`[Schedule] Skipping priority`,e.priority,`< max`,c);this._layoutMetadata.clear();for(let e of l)this._layoutMetadata.set(e.file,{syncEvent:e.syncEvent||!1,shareOfVoice:e.shareOfVoice||0,scheduleid:e.scheduleid,priority:e.priority||0});if(!r&&this.interruptScheduler){let{normalLayouts:e,interruptLayouts:t}=this.interruptScheduler.separateLayouts(l);if(t.length>0){a(`[Schedule] Found`,t.length,`interrupt layouts with shareOfVoice`);let n=this.interruptScheduler.processInterrupts(e,t).map(e=>e.file);return a(`[Schedule] Final layouts (with interrupts):`,n),n}}let u=l.map(e=>e.file);return a(`[Schedule] Final layouts:`,u),u}shouldCheckSchedule(e){return e?Date.now()-e>=6e4:!0}canPlayLayout(e,t){return d(this.playHistory.get(e)||[],t,Date.now())}recordPlay(e){this.playHistory.has(e)||this.playHistory.set(e,[]);let t=this.playHistory.get(e);t.push(Date.now());let n=Date.now()-3600*1e3,r=t.filter(e=>e>n);this.playHistory.set(e,r),v.info(`Recorded play for layout ${e} (${r.length} plays in last hour)`)}isSyncEvent(e){return this._layoutMetadata.get(e)?.syncEvent===!0}getLayoutMetadata(e){return this._layoutMetadata.get(e)||null}getScheduleQueue(e,t={}){let n=this.getAllLayoutsAtTime(new Date),r=n.map(e=>`${e.file}:${e.priority}:${e.maxPlaysPerHour}`).sort().join(`|`);if(this._scheduleQueue&&this._queueLayoutSet===r)return this._scheduleQueue;let i=_(n,e,{defaultLayout:this.schedule?.default||null,defaultDuration:60,dynamicLayouts:t.dynamicLayouts||new Set}),a=this._queueLayoutSet;return this._scheduleQueue=i,this._queueLayoutSet=r,a!==r&&(this._queuePosition=0),i.queue.length>0&&(v.info(`[Schedule] Built queue: ${i.queue.length} entries, period ${i.periodSeconds}s (pos ${this._queuePosition})`),v.info(`[Schedule] Queue: ${i.queue.map(e=>`${e.layoutId}(${e.duration}s)`).join(` → `)}`)),i}popNextFromQueue(e,t={}){let{queue:n}=this.getScheduleQueue(e,t);if(n.length===0)return null;let r=n[this._queuePosition%n.length];return this._queuePosition=(this._queuePosition+1)%n.length,r}getQueuePosition(){return this._queuePosition}rewindQueue(e,t,n={}){let{queue:r}=this.getScheduleQueue(t,n);if(r.length===0)return null;this._queuePosition=(this._queuePosition-e+r.length*e)%r.length;let i=r[this._queuePosition];return this._queuePosition=(this._queuePosition+1)%r.length,i}peekNextInQueue(e,t={}){let{queue:n}=this.getScheduleQueue(e,t);return n.length===0?null:n[this._queuePosition%n.length]}peekAfterNext(e,t={}){let{queue:n}=this.getScheduleQueue(e,t);return n.length<=1?null:n[(this._queuePosition+1)%n.length]}invalidateQueue(){this._invalidateQueue()}_invalidateQueue(){this._scheduleQueue=null}hasSyncEvents(){for(let e of this._layoutMetadata.values())if(e.syncEvent)return!0;return!1}getActiveActions(){if(!this.schedule?.actions)return[];let e=new Date;return this.schedule.actions.filter(t=>this.isTimeActive(t,e))}getCommands(){return this.schedule?.commands||[]}findActionByTrigger(e){return this.getActiveActions().find(t=>t.triggerCode===e)||null}clearPlayHistory(){this.playHistory.clear(),v.info(`Play history cleared`)}setLocation(e,t){this.playerLocation={latitude:e,longitude:t},v.info(`Location set: ${e}, ${t}`)}setDisplayProperties(e){this.displayProperties=e||{}}isWithinGeoFence(e,t=500){if(!this.playerLocation)return v.debug(`No player location, skipping geofence check`),!0;if(!e)return!0;let n=e.split(`,`).map(e=>parseFloat(e.trim()));if(n.length<2||isNaN(n[0])||isNaN(n[1]))return v.warn(`Invalid geoLocation format:`,e),!0;let r=n[0],i=n[1],a=n[2]||t,o=this.haversineDistance(this.playerLocation.latitude,this.playerLocation.longitude,r,i),s=o<=a;return v.info(`Geofence: ${o.toFixed(0)}m from (${r},${i}), radius ${a}m → ${s?`WITHIN`:`OUTSIDE`}`),s}haversineDistance(e,t,n,r){let i=e=>e*Math.PI/180,a=i(n-e),o=i(r-t),s=Math.sin(a/2)**2+Math.cos(i(e))*Math.cos(i(n))*Math.sin(o/2)**2;return 6371e3*2*Math.atan2(Math.sqrt(s),Math.sqrt(1-s))}},b=new y,x=e(`schedule:interrupts`),S=class{constructor(){this.interruptCommittedDurations=new Map}isInterrupt(e){return!!(e.shareOfVoice&&e.shareOfVoice>0)}resetCommittedDurations(){this.interruptCommittedDurations.clear(),x.debug(`Reset interrupt committed durations`)}getCommittedDuration(e){return this.interruptCommittedDurations.get(e)||0}addCommittedDuration(e,t){let n=this.getCommittedDuration(e);this.interruptCommittedDurations.set(e,n+t)}isInterruptDurationSatisfied(e){if(!e.shareOfVoice)return!0;let t=e.id||e.file,n=e.shareOfVoice/100*3600;return this.getCommittedDuration(t)>=n}getRequiredSeconds(e){return e.shareOfVoice?e.shareOfVoice/100*3600:0}processInterrupts(e,t){if(!t||t.length===0)return x.debug(`No interrupt layouts, returning normal layouts`),e;if(!e||e.length===0)return x.warn(`No normal layouts available, interrupts will fill entire hour`),this.fillHourWithInterrupts(t);x.info(`Processing ${t.length} interrupt layouts with ${e.length} normal layouts`);for(let e of t){let t=e.id||e.file;this.interruptCommittedDurations.set(t,0)}let n=[],r=0,i=0,a=!1;for(;!a;){if(i>=t.length){i=0;let e=!0;for(let n of t)if(!this.isInterruptDurationSatisfied(n)){e=!1;break}if(e){a=!0;break}}let e=t[i];if(!this.isInterruptDurationSatisfied(e)){let t=e.id||e.file;this.addCommittedDuration(t,e.duration),r+=e.duration,n.push(e)}i++}if(x.debug(`Resolved ${n.length} interrupt plays (${r}s total)`),r>=3600)return x.info(`Interrupts fill entire hour (>= 3600s), no room for normal layouts`),n;let o=3600-r,s=this.fillTimeWithLayouts(e,o);x.debug(`Resolved ${s.length} normal plays (${o}s target)`);let c=this.interleaveLayouts(s,n);return x.info(`Final loop: ${c.length} layouts (${s.length} normal + ${n.length} interrupts)`),c}fillTimeWithLayouts(e,t){let n=[],r=t,i=0;for(;r>0;){i>=e.length&&(i=0);let t=e[i];n.push(t),r-=t.duration,i++}return n}fillHourWithInterrupts(e){return this.fillTimeWithLayouts(e,3600)}interleaveLayouts(e,t){let n=[],r=Math.max(e.length,t.length),i=Math.ceil(1*r/e.length),a=Math.floor(1*r/t.length);x.debug(`Interleaving: pickCount=${r}, normalPick=${i}, interruptPick=${a}`);let o=0,s=0,c=0;for(let l=0;l<r;l++)l%i===0&&(o>=e.length&&(o=0),n.push(e[o]),c+=e[o].duration,o++),l%a===0&&s<t.length&&(n.push(t[s]),c+=t[s].duration,s++);for(;c<3600;)o>=e.length&&(o=0),n.push(e[o]),c+=e[o].duration,o++;return x.debug(`Interleaved ${n.length} layouts, total duration: ${c}s`),n}separateLayouts(e){let t=[],n=[];for(let r of e)this.isInterrupt(r)?n.push(r):t.push(r);return{normalLayouts:t,interruptLayouts:n}}};new S;var C=e(`schedule:overlays`),w=class{constructor(){this.overlays=[],this.displayProperties={},this.scheduleManager=null,C.debug(`OverlayScheduler initialized`)}setScheduleManager(e){this.scheduleManager=e}setDisplayProperties(e){this.displayProperties=e||{}}setOverlays(e){this.overlays=e||[],C.info(`Loaded ${this.overlays.length} overlay(s)`)}getCurrentOverlays(){if(!this.overlays||this.overlays.length===0)return[];let e=new Date,t=[];for(let n of this.overlays){if(!this.isTimeActive(n,e)){C.debug(`Overlay ${n.file} not in time window`);continue}if(n.isGeoAware&&n.geoLocation&&this.scheduleManager&&!this.scheduleManager.isWithinGeoFence(n.geoLocation)){C.debug(`Overlay ${n.file} filtered by geofence`);continue}if(n.criteria&&n.criteria.length>0&&!s(n.criteria,{now:e,displayProperties:this.displayProperties})){C.debug(`Overlay ${n.file} filtered by criteria`);continue}t.push(n)}return t.sort((e,t)=>{let n=e.priority||0;return(t.priority||0)-n}),t.length>0&&C.info(`Active overlays: ${t.length}`),t}isTimeActive(e,t){if(this.scheduleManager){let n={...e};return!n.fromdt&&n.fromDt&&(n.fromdt=n.fromDt),!n.todt&&n.toDt&&(n.todt=n.toDt),this.scheduleManager.isTimeActive(n,t)}let n=e.fromdt||e.fromDt?new Date(e.fromdt||e.fromDt):null,r=e.todt||e.toDt?new Date(e.todt||e.toDt):null;return!(n&&t<n||r&&t>r)}shouldCheckOverlays(e){return e?Date.now()-e>=6e4:!0}getOverlayByFile(e){return this.overlays.find(t=>t.file===e)||null}clear(){this.overlays=[],C.debug(`Cleared all overlays`)}};new w;var T=t.version;export{b as a,l as c,y as i,c as l,w as n,_ as o,S as r,p as s,T as t};
2
+ //# sourceMappingURL=src-DNqc--R2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"src-BxHs3cOi.js","names":["log","logger","pkg"],"sources":["../../../schedule/package.json","../../../schedule/src/criteria.js","../../../schedule/src/timeline.js","../../../schedule/src/schedule.js","../../../schedule/src/interrupts.js","../../../schedule/src/overlays.js","../../../schedule/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/schedule\",\n \"version\": \"0.7.12\",\n \"description\": \"Complete scheduling solution: campaigns, dayparting, interrupts, and overlays\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.js\",\n \"./schedule\": \"./src/schedule.js\",\n \"./interrupts\": \"./src/interrupts.js\",\n \"./overlays\": \"./src/overlays.js\"\n },\n \"scripts\": {\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\"\n },\n \"dependencies\": {\n \"@xiboplayer/utils\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"scheduling\",\n \"dayparting\",\n \"campaigns\",\n \"interrupts\",\n \"overlays\",\n \"shareOfVoice\"\n ],\n \"author\": \"Pau Aliagas <linuxnow@gmail.com>\",\n \"license\": \"AGPL-3.0-or-later\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/xibo-players/xiboplayer.git\",\n \"directory\": \"packages/schedule\"\n },\n \"homepage\": \"https://xiboplayer.org\"\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Criteria Evaluator\n *\n * Evaluates schedule criteria against current player state.\n * Criteria are conditions set in the CMS that determine whether\n * a layout/overlay should display on a given player.\n *\n * Supported metrics:\n * - dayOfWeek: Current day name (Monday-Sunday)\n * - dayOfMonth: Day number (1-31)\n * - month: Month number (1-12)\n * - hour: Hour (0-23)\n * - isoDay: ISO day of week (1=Monday, 7=Sunday)\n *\n * Weather metrics (require weatherData in options):\n * - weatherTemp: Current temperature\n * - weatherHumidity: Current humidity percentage\n * - weatherWindSpeed: Current wind speed\n * - weatherCondition: Current weather condition (e.g. \"Clear\", \"Rain\")\n * - weatherCloudCover: Cloud cover percentage\n *\n * Supported conditions:\n * - equals, notEquals\n * - greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals\n * - contains, notContains, startsWith, endsWith\n * - in (comma-separated list)\n *\n * Display property metrics are resolved via a property map\n * provided at evaluation time.\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('schedule:criteria');\n\nconst DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n\n/**\n * Weather metric name → weatherData property mapping\n */\nconst WEATHER_METRICS = {\n weatherTemp: 'temperature',\n weatherHumidity: 'humidity',\n weatherWindSpeed: 'windSpeed',\n weatherCondition: 'condition',\n weatherCloudCover: 'cloudCover',\n};\n\n/**\n * Get built-in metric value from current date/time\n * @param {string} metric - Metric name\n * @param {Date} now - Current date\n * @param {Object} displayProperties - Display property map from CMS\n * @param {Object} weatherData - Weather data from GetWeather XMDS call\n * @returns {string|null} Metric value or null if unknown\n */\nfunction getMetricValue(metric, now, displayProperties = {}, weatherData = {}) {\n switch (metric) {\n case 'dayOfWeek':\n return DAY_NAMES[now.getDay()];\n case 'dayOfMonth':\n return String(now.getDate());\n case 'month':\n return String(now.getMonth() + 1);\n case 'hour':\n return String(now.getHours());\n case 'isoDay':\n return String(now.getDay() === 0 ? 7 : now.getDay());\n default:\n // Check weather metrics\n if (WEATHER_METRICS[metric]) {\n const weatherKey = WEATHER_METRICS[metric];\n if (weatherData[weatherKey] !== undefined) {\n return String(weatherData[weatherKey]);\n }\n log.debug(`Weather metric \"${metric}\" requested but no weather data available`);\n return null;\n }\n // Check display properties (custom fields set in CMS)\n if (displayProperties[metric] !== undefined) {\n return String(displayProperties[metric]);\n }\n log.debug(`Unknown metric: ${metric}`);\n return null;\n }\n}\n\n/**\n * Evaluate a single condition\n * @param {string} actual - Actual value from player state\n * @param {string} condition - Condition operator\n * @param {string} expected - Expected value from criteria\n * @param {string} type - Value type ('string' or 'number')\n * @returns {boolean}\n */\nfunction evaluateCondition(actual, condition, expected, type) {\n if (actual === null) return false;\n\n // Number comparison\n if (type === 'number') {\n const a = parseFloat(actual);\n const e = parseFloat(expected);\n if (isNaN(a) || isNaN(e)) return false;\n\n switch (condition) {\n case 'equals': return a === e;\n case 'notEquals': return a !== e;\n case 'greaterThan': return a > e;\n case 'greaterThanOrEquals': return a >= e;\n case 'lessThan': return a < e;\n case 'lessThanOrEquals': return a <= e;\n default: return false;\n }\n }\n\n // String comparison (case-insensitive)\n const a = actual.toLowerCase();\n const e = expected.toLowerCase();\n\n switch (condition) {\n case 'equals': return a === e;\n case 'notEquals': return a !== e;\n case 'contains': return a.includes(e);\n case 'notContains': return !a.includes(e);\n case 'startsWith': return a.startsWith(e);\n case 'endsWith': return a.endsWith(e);\n case 'in': return e.split(',').map(s => s.trim().toLowerCase()).includes(a);\n case 'greaterThan': return a > e;\n case 'lessThan': return a < e;\n default:\n log.debug(`Unknown condition: ${condition}`);\n return false;\n }\n}\n\n/**\n * Evaluate all criteria for a schedule item.\n * All criteria must match (AND logic) for the item to display.\n *\n * @param {Array<{metric: string, condition: string, type: string, value: string}>} criteria\n * @param {Object} options\n * @param {Date} [options.now] - Current date (defaults to new Date())\n * @param {Object} [options.displayProperties] - Display property map from CMS\n * @param {Object} [options.weatherData] - Weather data from GetWeather XMDS call\n * @returns {boolean} True if all criteria match (or no criteria)\n */\nexport function evaluateCriteria(criteria, options = {}) {\n if (!criteria || criteria.length === 0) return true;\n\n const now = options.now || new Date();\n const displayProperties = options.displayProperties || {};\n const weatherData = options.weatherData || {};\n\n for (const criterion of criteria) {\n const actual = getMetricValue(criterion.metric, now, displayProperties, weatherData);\n const matches = evaluateCondition(actual, criterion.condition, criterion.value, criterion.type);\n\n if (!matches) {\n log.debug(`Criteria failed: ${criterion.metric} ${criterion.condition} \"${criterion.value}\" (actual: \"${actual}\")`);\n return false;\n }\n }\n\n return true;\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Offline Schedule Timeline Calculator\n *\n * Calculates deterministic playback timelines by parsing layout XLF durations\n * and simulating round-robin scheduling. Enables the player to answer\n * \"what's the playback plan for the next N hours?\" while offline.\n */\n\n/**\n * Parse layout duration from XLF XML string.\n * Lightweight parser — uses DOMParser, no rendering.\n *\n * Single source of truth for XLF-based duration calculation.\n * Supports a 3-phase progressive refinement pipeline:\n * Phase 1 (ESTIMATE): parseLayoutDuration(xlf) — static duration from XLF\n * Phase 2 (PROBE): parseLayoutDuration(xlf, videoDurations) — refined with real video lengths\n * Phase 3 (LIVE UPDATE): renderer's updateLayoutDuration() — corrections from DURATION comments\n *\n * Duration resolution order:\n * 1. Explicit <layout duration=\"60\"> attribute\n * 2. Sum of widget <media duration=\"X\"> per region (max across regions)\n * 3. Fallback: 60s\n *\n * @param {string} xlfXml - Raw XLF XML string\n * @param {Map<string, number>|null} [videoDurations=null] - Optional map of fileId → probed duration in seconds\n * @returns {{ duration: number, isDynamic: boolean }} Duration in seconds and whether any widget has useDuration=0\n */\n/**\n * Extract numeric layout ID from a schedule filename like \"123.xlf\" or \"123\"\n * @param {string|number} f - Layout file reference\n * @returns {number}\n */\nexport function parseLayoutFile(f) {\n return parseInt(String(f).replace('.xlf', ''), 10);\n}\n\nexport function parseLayoutDuration(xlfXml, videoDurations = null) {\n const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) return { duration: 60, isDynamic: false };\n\n // 1. Explicit layout duration attribute\n const explicit = parseInt(layoutEl.getAttribute('duration') || '0', 10);\n if (explicit > 0) return { duration: explicit, isDynamic: false };\n\n // 2. Calculate from widget durations (max region wins — regions play in parallel)\n let maxDuration = 0;\n let isDynamic = false;\n for (const regionEl of layoutEl.querySelectorAll('region')) {\n const regionType = regionEl.getAttribute('type');\n if (regionType === 'drawer') continue; // Drawers are action-triggered, not timed\n const isCanvas = regionType === 'canvas';\n let regionDuration = 0;\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const dur = parseInt(mediaEl.getAttribute('duration') || '0', 10);\n const useDuration = parseInt(mediaEl.getAttribute('useDuration') || '1', 10);\n const fileId = mediaEl.getAttribute('fileId') || '';\n const probed = videoDurations?.get(fileId);\n\n let widgetDuration;\n if (probed !== undefined) {\n widgetDuration = probed; // Phase 2: probed video duration\n } else if (dur > 0 && useDuration !== 0) {\n widgetDuration = dur; // Explicit CMS duration\n } else {\n // Video with useDuration=0 means \"play to end\" — estimate 60s,\n // corrected later via recordLayoutDuration() when video metadata loads\n widgetDuration = 60;\n isDynamic = true;\n }\n\n if (isCanvas) {\n // Canvas regions play all widgets simultaneously — duration is max, not sum\n regionDuration = Math.max(regionDuration, widgetDuration);\n } else {\n regionDuration += widgetDuration;\n }\n }\n maxDuration = Math.max(maxDuration, regionDuration);\n }\n\n const duration = maxDuration > 0 ? maxDuration : 60;\n return { duration, isDynamic };\n}\n\n/**\n * Compare two arrays of layout files for equality.\n * @param {string[]} a\n * @param {string[]} b\n * @returns {boolean}\n */\nfunction arraysEqual(a, b) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Check if a layout can play at a given time based on simulated play history.\n * Replicates ScheduleManager.canPlayLayout() logic for timeline prediction.\n *\n * Even-distribution rules:\n * 1. Total plays in sliding 1-hour window < maxPlaysPerHour\n * 2. Time since last play >= (60 / maxPlaysPerHour) minutes\n *\n * @param {number[]} history - Simulated play timestamps (ms) for this layout\n * @param {number} maxPlaysPerHour - Max plays per hour (0 = unlimited)\n * @param {number} timeMs - Current simulated time in ms\n * @returns {boolean}\n */\nexport function canSimulatedPlay(history, maxPlaysPerHour, timeMs) {\n if (!maxPlaysPerHour || maxPlaysPerHour === 0) return true;\n\n const oneHourAgo = timeMs - 3600000;\n const playsInLastHour = history.filter(t => t > oneHourAgo);\n\n // Check 1: under hourly limit\n if (playsInLastHour.length >= maxPlaysPerHour) return false;\n\n // Check 2: minimum gap for even distribution\n if (playsInLastHour.length > 0) {\n const minGapMs = 3600000 / maxPlaysPerHour;\n const lastPlay = Math.max(...playsInLastHour);\n if (timeMs - lastPlay < minGapMs) return false;\n }\n\n return true;\n}\n\n/**\n * From a list of layout metadata, apply simulated rate limiting and priority\n * filtering to determine which layouts can actually play at the given time.\n * Mirrors the real player logic: filter rate-limited layouts first, then\n * pick highest remaining priority.\n *\n * @param {Array<{file: string, priority: number, maxPlaysPerHour: number}>} allLayouts\n * @param {Map<string, number[]>} simPlays - Simulated play history\n * @param {number} timeMs - Current simulated time in ms\n * @returns {string[]} Layout files that can play, highest priority first\n */\nfunction getPlayableLayouts(allLayouts, simPlays, timeMs) {\n // Step 1: Filter out rate-limited layouts\n const eligible = allLayouts.filter(l => {\n if (!l.maxPlaysPerHour || l.maxPlaysPerHour === 0) return true;\n const history = simPlays.get(l.file) || [];\n return canSimulatedPlay(history, l.maxPlaysPerHour, timeMs);\n });\n\n if (eligible.length === 0) return [];\n\n // Step 2: Pick highest priority from remaining layouts\n const maxPriority = Math.max(...eligible.map(l => l.priority));\n return eligible\n .filter(l => l.priority === maxPriority)\n .map(l => l.file);\n}\n\n/**\n * Calculate a deterministic playback timeline by walking the pre-built schedule queue.\n *\n * The queue already has all constraints baked in (maxPlaysPerHour, priorities,\n * dayparting, default layout fills). This function simply cycles through it from\n * the current position, generating time-stamped entries for the overlay.\n *\n * @param {Array<{layoutId: string, duration: number}>} queue - Pre-built schedule queue from buildScheduleQueue()\n * @param {number} queuePosition - Current position in the queue (from schedule._queuePosition)\n * @param {Object} [options]\n * @param {Date} [options.from] - Start time (default: now)\n * @param {number} [options.hours] - Hours to project (default: 2)\n * @param {string} [options.defaultLayout] - Default layout file (to tag isDefault entries)\n * @param {Map<string, number>} [options.durations] - Live durations map (overrides queue entry durations with corrected values)\n * @param {Date} [options.currentLayoutStartedAt] - When current layout started (adjusts first entry to remaining time)\n * @returns {Array<{layoutFile: string, startTime: Date, endTime: Date, duration: number, isDefault: boolean}>}\n */\nexport function calculateTimeline(queue, queuePosition, options = {}) {\n const from = options.from || new Date();\n const hours = options.hours || 2;\n const to = new Date(from.getTime() + hours * 3600000);\n const currentLayoutStartedAt = options.currentLayoutStartedAt || null;\n const defaultLayout = options.defaultLayout || null;\n const durations = options.durations || null;\n\n if (!queue || queue.length === 0) return [];\n\n const timeline = [];\n let currentTime = new Date(from);\n // queuePosition has already advanced past the currently-playing layout\n // (via popNextFromQueue), so entries here start from the NEXT layout.\n // The current layout's duration is passed directly to the overlay.\n let pos = queuePosition % queue.length;\n const maxEntries = 500;\n\n while (currentTime < to && timeline.length < maxEntries) {\n const entry = queue[pos];\n // Use live-corrected duration (from video metadata, etc.) if available,\n // otherwise fall back to the queue's baked-in duration\n const dur = (durations && durations.get(entry.layoutId)) || entry.duration;\n\n const endMs = currentTime.getTime() + dur * 1000;\n\n timeline.push({\n layoutFile: entry.layoutId,\n startTime: new Date(currentTime),\n endTime: new Date(endMs),\n duration: dur,\n isDefault: defaultLayout ? entry.layoutId === defaultLayout : false,\n });\n\n currentTime = new Date(endMs);\n pos = (pos + 1) % queue.length;\n }\n\n return timeline;\n}\n\n// ── LCM-based deterministic schedule queue ──────────────────────────────\n\n/**\n * Greatest common divisor (Euclidean algorithm).\n * @param {number} a\n * @param {number} b\n * @returns {number}\n */\nfunction gcd(a, b) {\n a = Math.abs(Math.round(a));\n b = Math.abs(Math.round(b));\n while (b) { [a, b] = [b, a % b]; }\n return a;\n}\n\n/**\n * Least common multiple of two integers.\n * @param {number} a\n * @param {number} b\n * @returns {number}\n */\nfunction lcm(a, b) {\n if (a === 0 || b === 0) return 0;\n return Math.abs(Math.round(a) * Math.round(b)) / gcd(a, b);\n}\n\n/**\n * LCM of an array of integers.\n * @param {number[]} values\n * @returns {number}\n */\nfunction lcmArray(values) {\n return values.reduce((acc, v) => lcm(acc, v), 1);\n}\n\n/**\n * Build a deterministic playback queue by simulating one LCM period.\n *\n * Uses getPlayableLayouts() (the same priority-fallback + rate-limit logic\n * that calculateTimeline uses) to simulate playback for one repeating cycle.\n * This ensures the queue matches the timeline overlay exactly: high-priority\n * rate-limited layouts get their slots, then lower-priority layouts fill gaps.\n *\n * @param {Array<{file: string, priority: number, maxPlaysPerHour: number}>} allLayouts\n * All time-active layouts from schedule.getAllLayoutsAtTime()\n * @param {Map<string, number>} durations\n * Map of layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {string} [options.defaultLayout] - Default layout file (CMS fallback)\n * @param {number} [options.defaultDuration] - Fallback duration (default: 60)\n * @param {Set<string>} [options.dynamicLayouts] - Set of layout files that are dynamic (video, useDuration=0)\n * @returns {{ queue: Array<{layoutId: string, duration: number}>, periodSeconds: number }}\n */\nexport function buildScheduleQueue(allLayouts, durations, options = {}) {\n const {\n defaultLayout = null,\n defaultDuration = 60,\n } = options;\n\n if (allLayouts.length === 0 && !defaultLayout) {\n return { queue: [], periodSeconds: 0 };\n }\n\n // Build CMS duration lookup — use CMS-reported duration as fallback\n // when the durations map (from XLF parsing / video metadata) has no entry.\n const cmsDurations = new Map();\n for (const l of allLayouts) {\n if (l.duration > 0) cmsDurations.set(l.file, l.duration);\n }\n const getDuration = (file) => durations.get(file) || cmsDurations.get(file) || defaultDuration;\n\n // Step 1: Identify rate-limited layouts to calculate LCM period\n const rateLimited = allLayouts.filter(l => l.maxPlaysPerHour > 0);\n\n let periodSeconds;\n if (rateLimited.length > 0) {\n const intervals = rateLimited.map(l => Math.round(3600 / l.maxPlaysPerHour));\n periodSeconds = lcmArray(intervals);\n // Cap at 2 hours to prevent absurd periods\n if (periodSeconds > 7200) periodSeconds = 7200;\n } else {\n // No rate-limited layouts — single round-robin cycle\n const totalDuration = allLayouts.reduce((sum, l) => sum + getDuration(l.file), 0)\n + (defaultLayout && !allLayouts.some(l => l.file === defaultLayout)\n ? getDuration(defaultLayout)\n : 0);\n periodSeconds = totalDuration || defaultDuration;\n }\n\n // Step 2: Simulate playback for one period using getPlayableLayouts()\n const queue = [];\n const simPlays = new Map(); // file → [timestampMs] for rate-limit tracking\n let cursorMs = 0;\n const periodMs = periodSeconds * 1000;\n const maxEntries = 500; // safety cap\n\n while (cursorMs < periodMs && queue.length < maxEntries) {\n // Get playable layouts at current simulated time (priority fallback + rate limits)\n const playable = getPlayableLayouts(allLayouts, simPlays, cursorMs);\n\n if (playable.length === 0) {\n // All layouts exhausted — use default\n if (defaultLayout) {\n const dur = getDuration(defaultLayout);\n queue.push({ layoutId: defaultLayout, duration: dur });\n cursorMs += dur * 1000;\n } else {\n // No default — skip ahead 60s to avoid infinite loop\n cursorMs += 60000;\n }\n continue;\n }\n\n // Play all playable layouts in round-robin order (one each), then re-evaluate\n for (let i = 0; i < playable.length && cursorMs < periodMs && queue.length < maxEntries; i++) {\n const file = playable[i];\n const dur = getDuration(file);\n\n queue.push({ layoutId: file, duration: dur });\n\n // Record simulated play for rate-limit tracking\n if (!simPlays.has(file)) simPlays.set(file, []);\n simPlays.get(file).push(cursorMs);\n\n cursorMs += dur * 1000;\n\n // Re-evaluate after each play: if the playable set changed, break to outer loop\n const nextPlayable = getPlayableLayouts(allLayouts, simPlays, cursorMs);\n if (!arraysEqual(playable, nextPlayable)) break;\n }\n }\n\n // Handle edge case: no layouts and only default\n if (queue.length === 0 && defaultLayout) {\n const defDur = getDuration(defaultLayout);\n queue.push({ layoutId: defaultLayout, duration: defDur });\n }\n\n return { queue, periodSeconds };\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Schedule manager - determines which layouts to show\n */\n\nimport { createLogger } from '@xiboplayer/utils';\nimport { evaluateCriteria } from './criteria.js';\nimport { buildScheduleQueue, canSimulatedPlay, parseLayoutFile } from './timeline.js';\n\nconst log = createLogger('Schedule');\n\nexport class ScheduleManager {\n constructor(options = {}) {\n this.schedule = null;\n this.playHistory = new Map(); // Track plays per layout: layoutId -> [timestamps]\n this.interruptScheduler = options.interruptScheduler || null; // Optional interrupt scheduler\n this.displayProperties = options.displayProperties || {}; // CMS display custom properties\n this.weatherData = {}; // Weather data from GetWeather XMDS call\n this.playerLocation = null; // { latitude, longitude } from Geolocation API\n this._layoutMetadata = new Map(); // layoutFile → { syncEvent, shareOfVoice, ... }\n\n // Pre-calculated schedule queue (LCM-based deterministic timeline)\n this._scheduleQueue = null; // { queue: [{layoutId, duration}], periodSeconds }\n this._queuePosition = 0; // Current position in the queue\n this._queueLayoutSet = null; // Stringified active layout set (for invalidation)\n }\n\n /**\n * Update schedule from XMDS\n */\n setSchedule(schedule) {\n this.schedule = schedule;\n this._invalidateQueue();\n }\n\n /**\n * Update weather data for criteria evaluation\n * @param {Object} data - Parsed weather object { temperature, humidity, windSpeed, condition, cloudCover }\n */\n setWeatherData(data) {\n this.weatherData = data || {};\n }\n\n /**\n * Get data connectors from current schedule\n * @returns {Array} Data connector configurations, or empty array\n */\n getDataConnectors() {\n return this.schedule?.dataConnectors || [];\n }\n\n /**\n * Get dependants map: layoutId → filenames that must be cached before that layout plays.\n * Includes both per-layout and global dependants.\n * Used by download manager to prioritize sub-playlist media alongside its parent layout.\n * @returns {Map<number, string[]>} layoutId → dependant filenames\n */\n getDependantsMap() {\n const map = new Map();\n if (!this.schedule) return map;\n\n const globalDeps = this.schedule.dependants || [];\n\n const addLayout = (layout) => {\n const id = parseLayoutFile(layout.file || layout.id);\n const deps = [...globalDeps, ...(layout.dependants || [])];\n if (deps.length > 0) map.set(id, deps);\n };\n\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) addLayout(layout);\n }\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n for (const layout of campaign.layouts) addLayout(layout);\n }\n }\n\n return map;\n }\n\n /**\n * Check if a schedule item is active based on recurrence rules.\n * Supports Week, Day, and Month recurrence types.\n */\n isRecurringScheduleActive(item, now) {\n // If no recurrence, it's not a recurring schedule\n if (!item.recurrenceType) {\n return true; // Not a recurring schedule, use date/time checks instead\n }\n\n // Check recurrence range first (applies to all types)\n if (item.recurrenceRange) {\n const rangeEnd = new Date(item.recurrenceRange);\n if (now > rangeEnd) {\n return false; // Recurrence has ended\n }\n }\n\n switch (item.recurrenceType) {\n case 'Week': {\n // Check if current day of week matches recurrenceRepeatsOn\n // recurrenceRepeatsOn format: \"1,2,3,4,5\" (1=Monday, 7=Sunday, ISO format)\n if (item.recurrenceRepeatsOn) {\n const currentDayOfWeek = this.getIsoDayOfWeek(now);\n const allowedDays = item.recurrenceRepeatsOn.split(',').map(d => parseInt(d.trim()));\n if (!allowedDays.includes(currentDayOfWeek)) {\n return false;\n }\n }\n return true;\n }\n\n case 'Day': {\n // Daily recurrence with optional interval (recurrenceDetail)\n // If recurrenceDetail > 1, only active every N days from fromdt\n const interval = item.recurrenceDetail || 1;\n if (interval > 1 && item.fromdt) {\n const startDate = new Date(item.fromdt);\n const diffMs = now.getTime() - startDate.getTime();\n const diffDays = Math.floor(diffMs / 86400000);\n if (diffDays < 0 || diffDays % interval !== 0) {\n return false;\n }\n }\n return true;\n }\n\n case 'Month': {\n // Monthly recurrence — recurrenceRepeatsOn is day-of-month (1-31)\n if (item.recurrenceRepeatsOn) {\n const allowedDays = item.recurrenceRepeatsOn.split(',').map(d => parseInt(d.trim()));\n const currentDayOfMonth = now.getDate();\n if (!allowedDays.includes(currentDayOfMonth)) {\n return false;\n }\n }\n // If recurrenceDetail > 1, only active every N months from fromdt\n const interval = item.recurrenceDetail || 1;\n if (interval > 1 && item.fromdt) {\n const startDate = new Date(item.fromdt);\n const monthsDiff = (now.getFullYear() - startDate.getFullYear()) * 12\n + now.getMonth() - startDate.getMonth();\n if (monthsDiff < 0 || monthsDiff % interval !== 0) {\n return false;\n }\n }\n return true;\n }\n\n default:\n log.debug(`Unsupported recurrence type: ${item.recurrenceType}`);\n return true; // Unknown type, fallback to date/time checks\n }\n }\n\n /**\n * Get ISO day of week (1=Monday, 7=Sunday)\n */\n getIsoDayOfWeek(date) {\n const day = date.getDay(); // 0=Sunday, 6=Saturday\n return day === 0 ? 7 : day; // Convert to ISO (1=Monday, 7=Sunday)\n }\n\n /**\n * Check if current time is within the schedule's time window\n * Handles both date ranges and time-of-day for dayparting\n */\n isTimeActive(item, now) {\n const from = item.fromdt ? new Date(item.fromdt) : null;\n const to = item.todt ? new Date(item.todt) : null;\n\n // For recurring schedules, check time-of-day instead of full datetime\n if (item.recurrenceType === 'Week' || item.recurrenceType === 'Day' || item.recurrenceType === 'Month') {\n // Extract time from fromdt/todt and compare with current time\n if (from && to) {\n const currentTime = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();\n const fromTime = from.getHours() * 3600 + from.getMinutes() * 60 + from.getSeconds();\n const toTime = to.getHours() * 3600 + to.getMinutes() * 60 + to.getSeconds();\n\n // Handle midnight crossing\n if (fromTime <= toTime) {\n // Normal case: 09:00 - 17:00\n return currentTime >= fromTime && currentTime <= toTime;\n } else {\n // Midnight crossing: 22:00 - 02:00\n return currentTime >= fromTime || currentTime <= toTime;\n }\n }\n return true;\n }\n\n // For non-recurring schedules, use full date/time comparison\n if (from && now < from) return false;\n if (to && now > to) return false;\n return true;\n }\n\n /**\n * Get current layouts to display\n * Returns array of layout files, prioritized\n *\n * Campaign behavior:\n * - Priority applies at campaign level, not individual layout level\n * - All layouts in a campaign share the campaign's priority\n * - Layouts within a campaign are returned in order for cycling\n * - Standalone layouts compete with campaigns at their own priority\n *\n * Dayparting behavior:\n * - Schedules can recur weekly on specific days (recurrenceType='Week')\n * - recurrenceRepeatsOn specifies days: \"1,2,3,4,5\" (Mon-Fri, ISO format)\n * - Time matching uses time-of-day for recurring schedules\n * - Non-recurring schedules use full date/time ranges\n *\n * Interrupt behavior (shareOfVoice):\n * - Layouts with shareOfVoice > 0 are interrupts\n * - They must play for a percentage of each hour\n * - Normal layouts fill remaining time\n * - Interrupts are interleaved with normal layouts\n */\n getCurrentLayouts() {\n return this._getLayoutsAt(new Date());\n }\n\n /**\n * Get layouts active at a specific time.\n * Skips rate limiting and interrupt processing (those depend on real-time state).\n * Used by timeline calculator to predict future playback.\n * @param {Date} time - The time to evaluate\n * @returns {string[]} Layout files active at that time\n */\n getLayoutsAtTime(time) {\n return this._getLayoutsAt(time, { skipRateLimiting: true, skipInterrupts: true, quiet: true });\n }\n\n /**\n * Get ALL time-active layouts with metadata, without priority or rate-limit filtering.\n * Used by calculateTimeline() to simulate real playback with rate limiting and\n * priority fallback (e.g., when high-priority layouts hit maxPlaysPerHour, lower\n * priority layouts fill the gap).\n *\n * @param {Date} time - The time to evaluate\n * @returns {Array<{file: string, priority: number, maxPlaysPerHour: number}>}\n */\n getAllLayoutsAtTime(time) {\n if (!this.schedule) return [];\n\n const now = time;\n const results = [];\n\n // Standalone layouts\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) {\n if (!this.isRecurringScheduleActive(layout, now)) continue;\n if (!this.isTimeActive(layout, now)) continue;\n if (layout.criteria && layout.criteria.length > 0) {\n if (!evaluateCriteria(layout.criteria, { now, displayProperties: this.displayProperties, weatherData: this.weatherData })) continue;\n }\n if (layout.isGeoAware && layout.geoLocation) {\n if (!this.isWithinGeoFence(layout.geoLocation)) continue;\n }\n results.push({\n file: layout.file,\n priority: layout.priority || 0,\n maxPlaysPerHour: layout.maxPlaysPerHour || 0,\n duration: layout.duration || 0,\n });\n }\n }\n\n // Campaign layouts\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n if (!this.isRecurringScheduleActive(campaign, now)) continue;\n if (!this.isTimeActive(campaign, now)) continue;\n for (const layout of campaign.layouts) {\n results.push({\n file: layout.file,\n priority: campaign.priority || 0,\n maxPlaysPerHour: layout.maxPlaysPerHour || 0,\n duration: layout.duration || 0,\n });\n }\n }\n }\n\n return results;\n }\n\n /**\n * Detect schedule conflicts: time windows where multiple layouts compete\n * and lower-priority ones are hidden.\n *\n * Scans the schedule in 1-minute increments over the given window.\n * At each point, collects all time-active layouts (after criteria/geofence\n * filtering but before priority filtering). If multiple priorities exist,\n * the lower-priority entries are reported as hidden.\n *\n * @param {Object} [options]\n * @param {Date} [options.from] - Start time (default: now)\n * @param {number} [options.hours] - Hours to scan (default: 24)\n * @returns {Array<{startTime: Date, endTime: Date, winner: {file: string, priority: number}, hidden: Array<{file: string, priority: number}>}>}\n */\n detectConflicts(options = {}) {\n const from = options.from || new Date();\n const hours = options.hours || 24;\n const to = new Date(from.getTime() + hours * 3600000);\n const stepMs = 60000; // 1-minute granularity\n const conflicts = [];\n let current = null; // Current conflict window being built\n\n for (let t = from.getTime(); t < to.getTime(); t += stepMs) {\n const time = new Date(t);\n const allLayouts = this.getAllLayoutsAtTime(time);\n\n if (allLayouts.length === 0) {\n // No layouts → close any open conflict\n if (current) { conflicts.push(current); current = null; }\n continue;\n }\n\n const maxPriority = Math.max(...allLayouts.map(l => l.priority));\n const hidden = allLayouts.filter(l => l.priority < maxPriority);\n\n if (hidden.length === 0) {\n // No conflict at this time\n if (current) { conflicts.push(current); current = null; }\n continue;\n }\n\n // Conflict exists — build or extend window\n const winners = allLayouts.filter(l => l.priority === maxPriority);\n const winnerKey = winners.map(w => w.file).sort().join(',');\n const hiddenKey = hidden.map(h => `${h.file}:${h.priority}`).sort().join(',');\n\n if (current && current._winnerKey === winnerKey && current._hiddenKey === hiddenKey) {\n // Same conflict continues — extend window\n current.endTime = new Date(t + stepMs);\n } else {\n // New or changed conflict\n if (current) conflicts.push(current);\n current = {\n startTime: new Date(t),\n endTime: new Date(t + stepMs),\n winner: { file: winners[0].file, priority: maxPriority },\n hidden: hidden.map(h => ({ file: h.file, priority: h.priority })),\n _winnerKey: winnerKey,\n _hiddenKey: hiddenKey,\n };\n }\n }\n\n if (current) conflicts.push(current);\n\n // Clean internal keys\n for (const c of conflicts) {\n delete c._winnerKey;\n delete c._hiddenKey;\n }\n\n return conflicts;\n }\n\n /**\n * Internal: evaluate schedule at a given time.\n * @param {Date} now - Time to evaluate\n * @param {Object} [options] - Options\n * @param {boolean} [options.skipRateLimiting] - Skip maxPlaysPerHour checks\n * @param {boolean} [options.skipInterrupts] - Skip interrupt/shareOfVoice processing\n */\n _getLayoutsAt(now, options = {}) {\n if (!this.schedule) {\n return [];\n }\n\n const { skipRateLimiting = false, skipInterrupts = false, quiet = false } = options;\n const _log = quiet ? () => {} : (...args) => log.info(...args);\n const activeItems = []; // Mix of campaign objects and standalone layouts\n\n // Track the highest priority of any time-active layout BEFORE rate-limit\n // filtering. Used by advanceToNextLayout() to detect when only lower-\n // priority layouts remain (all high-priority ones are rate-limited) and\n // replay the current layout instead of downgrading.\n this._maxActivePriority = 0;\n\n // Find all active campaigns\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n // Check recurrence and time window\n if (!this.isRecurringScheduleActive(campaign, now)) {\n continue;\n }\n if (!this.isTimeActive(campaign, now)) {\n continue;\n }\n\n this._maxActivePriority = Math.max(this._maxActivePriority, campaign.priority || 0);\n\n // Campaign is active - add it as a single item with its priority\n activeItems.push({\n type: 'campaign',\n priority: campaign.priority,\n layouts: campaign.layouts, // Keep full layout objects for interrupt processing\n campaignId: campaign.id\n });\n }\n }\n\n // Find all active standalone layouts\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) {\n // Check recurrence and time window\n if (!this.isRecurringScheduleActive(layout, now)) {\n continue;\n }\n if (!this.isTimeActive(layout, now)) {\n continue;\n }\n\n // Check criteria conditions (date/time, display properties)\n if (layout.criteria && layout.criteria.length > 0) {\n if (!evaluateCriteria(layout.criteria, { now, displayProperties: this.displayProperties, weatherData: this.weatherData })) {\n _log('[Schedule] Layout', layout.id, 'filtered by criteria');\n continue;\n }\n }\n\n // Check geo-fencing\n if (layout.isGeoAware && layout.geoLocation) {\n if (!this.isWithinGeoFence(layout.geoLocation)) {\n _log('[Schedule] Layout', layout.id, 'filtered by geofence');\n continue;\n }\n }\n\n // Track priority before rate-limit filtering\n this._maxActivePriority = Math.max(this._maxActivePriority, layout.priority || 0);\n\n // Check max plays per hour (skip for future time queries)\n if (!skipRateLimiting && !this.canPlayLayout(layout.id, layout.maxPlaysPerHour)) {\n _log('[Schedule] Layout', layout.id, 'filtered by maxPlaysPerHour (limit:', layout.maxPlaysPerHour, ')');\n // Continue to check other layouts, but don't add this one\n continue;\n }\n\n activeItems.push({\n type: 'layout',\n priority: layout.priority || 0,\n layouts: [layout], // Keep full layout object for interrupt processing\n layoutId: layout.id\n });\n }\n }\n\n // If no active schedules, return default\n if (activeItems.length === 0) {\n return this.schedule.default ? [this.schedule.default] : [];\n }\n\n // Find maximum priority across all items (campaigns and layouts)\n const maxPriority = Math.max(...activeItems.map(item => item.priority));\n _log('[Schedule] Max priority:', maxPriority, 'from', activeItems.length, 'active items');\n\n // Collect all layouts from items with max priority\n const allLayouts = [];\n for (const item of activeItems) {\n if (item.priority === maxPriority) {\n _log('[Schedule] Including priority', item.priority, 'layouts:', item.layouts.map(l => l.file));\n // Add all layouts from this campaign or standalone layout\n allLayouts.push(...item.layouts);\n } else {\n _log('[Schedule] Skipping priority', item.priority, '< max', maxPriority);\n }\n }\n\n // Build layout metadata map (syncEvent, shareOfVoice, etc.)\n this._layoutMetadata.clear();\n for (const layout of allLayouts) {\n this._layoutMetadata.set(layout.file, {\n syncEvent: layout.syncEvent || false,\n shareOfVoice: layout.shareOfVoice || 0,\n scheduleid: layout.scheduleid,\n priority: layout.priority || 0,\n });\n }\n\n // Process interrupts if interrupt scheduler is available (skip for future time queries)\n if (!skipInterrupts && this.interruptScheduler) {\n const { normalLayouts, interruptLayouts } = this.interruptScheduler.separateLayouts(allLayouts);\n\n if (interruptLayouts.length > 0) {\n _log('[Schedule] Found', interruptLayouts.length, 'interrupt layouts with shareOfVoice');\n const processedLayouts = this.interruptScheduler.processInterrupts(normalLayouts, interruptLayouts);\n // Extract file IDs from processed layouts\n const result = processedLayouts.map(l => l.file);\n _log('[Schedule] Final layouts (with interrupts):', result);\n return result;\n }\n }\n\n // No interrupts, return layout files\n const result = allLayouts.map(l => l.file);\n _log('[Schedule] Final layouts:', result);\n return result;\n }\n\n /**\n * Check if schedule needs update (every minute)\n */\n shouldCheckSchedule(lastCheck) {\n if (!lastCheck) return true;\n const elapsed = Date.now() - lastCheck;\n return elapsed >= 60000; // 1 minute\n }\n\n /**\n * Check if layout can play based on maxPlaysPerHour with even distribution.\n *\n * Instead of allowing bursts (3 plays back-to-back then nothing for 50 min),\n * plays are distributed evenly across the hour:\n * maxPlaysPerHour=3 → minimum 20 min gap between plays\n * maxPlaysPerHour=6 → minimum 10 min gap between plays\n *\n * Two checks:\n * 1. Total plays in sliding 1-hour window < maxPlaysPerHour\n * 2. Time since last play >= (60 / maxPlaysPerHour) minutes\n *\n * @param {string} layoutId - Layout ID to check\n * @param {number} maxPlaysPerHour - Maximum plays allowed per hour (0 = unlimited)\n * @returns {boolean} True if layout can play, false if exceeded limit\n */\n canPlayLayout(layoutId, maxPlaysPerHour) {\n const history = this.playHistory.get(layoutId) || [];\n return canSimulatedPlay(history, maxPlaysPerHour, Date.now());\n }\n\n /**\n * Record that a layout was played\n * @param {string} layoutId - Layout ID that was played\n */\n recordPlay(layoutId) {\n if (!this.playHistory.has(layoutId)) {\n this.playHistory.set(layoutId, []);\n }\n\n const history = this.playHistory.get(layoutId);\n history.push(Date.now());\n\n // Clean up old entries (older than 1 hour)\n const oneHourAgo = Date.now() - (60 * 60 * 1000);\n const cleaned = history.filter(timestamp => timestamp > oneHourAgo);\n this.playHistory.set(layoutId, cleaned);\n\n log.info(`Recorded play for layout ${layoutId} (${cleaned.length} plays in last hour)`);\n }\n\n /**\n * Check if a layout file is a sync event (part of multi-display sync group)\n * @param {string} layoutFile - Layout file identifier (e.g., '123')\n * @returns {boolean}\n */\n isSyncEvent(layoutFile) {\n const meta = this._layoutMetadata.get(layoutFile);\n return meta?.syncEvent === true;\n }\n\n /**\n * Get metadata for a layout file (syncEvent, shareOfVoice, etc.)\n * @param {string} layoutFile - Layout file identifier\n * @returns {Object|null} Metadata or null if not found\n */\n getLayoutMetadata(layoutFile) {\n return this._layoutMetadata.get(layoutFile) || null;\n }\n\n /**\n * Get (or build) the deterministic schedule queue.\n *\n * Uses LCM-based even distribution to pre-calculate a repeating cycle where\n * each rate-limited layout plays at evenly spaced intervals and gaps are\n * filled by unlimited layouts and the CMS default.\n *\n * The queue is cached and only rebuilt when:\n * - The schedule changes (setSchedule)\n * - The active layout set changes (time boundary crossed)\n * - durations are updated\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {Set<string>} [options.dynamicLayouts] - Set of layout files with useDuration=0\n * @returns {{ queue: Array<{layoutId: string, duration: number}>, periodSeconds: number }}\n */\n getScheduleQueue(durations, options = {}) {\n const allLayouts = this.getAllLayoutsAtTime(new Date());\n const layoutSetKey = allLayouts.map(l => `${l.file}:${l.priority}:${l.maxPlaysPerHour}`).sort().join('|');\n\n // Return cached queue if the active layout set hasn't changed\n if (this._scheduleQueue && this._queueLayoutSet === layoutSetKey) {\n return this._scheduleQueue;\n }\n\n const result = buildScheduleQueue(allLayouts, durations, {\n defaultLayout: this.schedule?.default || null,\n defaultDuration: 60,\n dynamicLayouts: options.dynamicLayouts || new Set(),\n });\n\n const prevLayoutSet = this._queueLayoutSet;\n this._scheduleQueue = result;\n this._queueLayoutSet = layoutSetKey;\n\n // Position only resets when we get a genuinely new set of layouts.\n // Duration corrections rebuild the queue but don't change position.\n if (prevLayoutSet !== layoutSetKey) {\n this._queuePosition = 0;\n }\n\n if (result.queue.length > 0) {\n log.info(`[Schedule] Built queue: ${result.queue.length} entries, period ${result.periodSeconds}s (pos ${this._queuePosition})`);\n log.info(`[Schedule] Queue: ${result.queue.map(e => `${e.layoutId}(${e.duration}s)`).join(' → ')}`);\n }\n\n return result;\n }\n\n /**\n * Pop the next entry from the schedule queue.\n * Wraps around at the end (the LCM period guarantees the pattern repeats).\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {Set<string>} [options.dynamicLayouts] - Dynamic layout set\n * @returns {{ layoutId: string, duration: number } | null}\n */\n popNextFromQueue(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length === 0) return null;\n\n const entry = queue[this._queuePosition % queue.length];\n this._queuePosition = (this._queuePosition + 1) % queue.length;\n return entry;\n }\n\n /**\n * Peek at the next entry in the schedule queue without advancing.\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @returns {{ layoutId: string, duration: number } | null}\n */\n /**\n * Get current queue position.\n * @returns {number}\n */\n getQueuePosition() {\n return this._queuePosition;\n }\n\n /**\n * Rewind the queue by N positions (wraps around).\n * Used by advanceToPreviousLayout to go back in the schedule.\n * @param {number} steps - Number of positions to rewind\n * @param {Map<string, number>} durations\n * @param {Object} [options]\n * @returns {{ layoutId: string, duration: number } | null}\n */\n rewindQueue(steps, durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length === 0) return null;\n this._queuePosition = (this._queuePosition - steps + queue.length * steps) % queue.length;\n const entry = queue[this._queuePosition];\n this._queuePosition = (this._queuePosition + 1) % queue.length;\n return entry;\n }\n\n peekNextInQueue(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length === 0) return null;\n return queue[this._queuePosition % queue.length];\n }\n\n /**\n * Peek at the entry after the current one (two positions ahead).\n * Used for preloading.\n *\n * @param {Map<string, number>} durations\n * @param {Object} [options]\n * @returns {{ layoutId: string, duration: number } | null}\n */\n peekAfterNext(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length <= 1) return null;\n return queue[(this._queuePosition + 1) % queue.length];\n }\n\n /**\n * Public API to invalidate the cached schedule queue.\n * Called by PlayerCore when layout durations are corrected at runtime.\n */\n invalidateQueue() {\n this._invalidateQueue();\n }\n\n /**\n * Invalidate the cached queue (called on schedule change, time boundaries, etc.)\n */\n _invalidateQueue() {\n this._scheduleQueue = null;\n // Keep _queueLayoutSet so getScheduleQueue() can detect whether the\n // layout set actually changed. Position only resets on new layouts.\n // Resetting on every setSchedule() caused the player to replay position 0\n // endlessly because collections happen more often than layout cycles.\n }\n\n /**\n * Check if any current layouts are sync events\n * @returns {boolean}\n */\n hasSyncEvents() {\n for (const meta of this._layoutMetadata.values()) {\n if (meta.syncEvent) return true;\n }\n return false;\n }\n\n /**\n * Get currently active actions (within their time window)\n * @returns {Array} Active action objects\n */\n getActiveActions() {\n if (!this.schedule?.actions) return [];\n\n const now = new Date();\n return this.schedule.actions.filter(action => this.isTimeActive(action, now));\n }\n\n /**\n * Get scheduled commands\n * @returns {Array} Command objects\n */\n getCommands() {\n return this.schedule?.commands || [];\n }\n\n /**\n * Find action by trigger code\n * @param {string} triggerCode - The trigger code to match\n * @returns {Object|null} Matching action or null\n */\n findActionByTrigger(triggerCode) {\n const activeActions = this.getActiveActions();\n return activeActions.find(a => a.triggerCode === triggerCode) || null;\n }\n\n /**\n * Clear play history (useful for testing or reset)\n */\n clearPlayHistory() {\n this.playHistory.clear();\n log.info('Play history cleared');\n }\n\n /**\n * Set player's current GPS location (from Geolocation API or XMR command)\n * @param {number} latitude\n * @param {number} longitude\n */\n setLocation(latitude, longitude) {\n this.playerLocation = { latitude, longitude };\n log.info(`Location set: ${latitude}, ${longitude}`);\n }\n\n /**\n * Set display properties from CMS (custom fields for criteria evaluation)\n * @param {Object} properties - Key-value map of display properties\n */\n setDisplayProperties(properties) {\n this.displayProperties = properties || {};\n }\n\n /**\n * Check if player is within a geo-fence.\n * geoLocation format from CMS: \"lat,lng\" (point + default radius)\n * or \"lat1,lng1;lat2,lng2;...\" (polygon — future)\n *\n * Default radius: 500 meters (Xibo default for point geofences)\n *\n * @param {string} geoLocation - Geo-fence specification from CMS\n * @param {number} [defaultRadius=500] - Default radius in meters for point geofences\n * @returns {boolean} True if within geofence or no location available\n */\n isWithinGeoFence(geoLocation, defaultRadius = 500) {\n if (!this.playerLocation) {\n // No location available — be permissive, show the content\n log.debug('No player location, skipping geofence check');\n return true;\n }\n\n if (!geoLocation) return true;\n\n // Parse \"lat,lng\" format\n const parts = geoLocation.split(',').map(s => parseFloat(s.trim()));\n if (parts.length < 2 || isNaN(parts[0]) || isNaN(parts[1])) {\n log.warn('Invalid geoLocation format:', geoLocation);\n return true; // Invalid format, be permissive\n }\n\n const fenceLat = parts[0];\n const fenceLng = parts[1];\n const radius = parts[2] || defaultRadius; // Optional 3rd param: radius in meters\n\n const distance = this.haversineDistance(\n this.playerLocation.latitude, this.playerLocation.longitude,\n fenceLat, fenceLng\n );\n\n const within = distance <= radius;\n log.info(`Geofence: ${distance.toFixed(0)}m from (${fenceLat},${fenceLng}), radius ${radius}m → ${within ? 'WITHIN' : 'OUTSIDE'}`);\n return within;\n }\n\n /**\n * Haversine formula: calculate distance between two GPS coordinates\n * @param {number} lat1 - Latitude 1 (degrees)\n * @param {number} lon1 - Longitude 1 (degrees)\n * @param {number} lat2 - Latitude 2 (degrees)\n * @param {number} lon2 - Longitude 2 (degrees)\n * @returns {number} Distance in meters\n */\n haversineDistance(lat1, lon1, lat2, lon2) {\n const R = 6371000; // Earth radius in meters\n const toRad = deg => deg * Math.PI / 180;\n\n const dLat = toRad(lat2 - lat1);\n const dLon = toRad(lon2 - lon1);\n\n const a = Math.sin(dLat / 2) ** 2 +\n Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *\n Math.sin(dLon / 2) ** 2;\n\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n }\n}\n\nexport const scheduleManager = new ScheduleManager();\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Interrupt Layout Scheduler (Share of Voice)\n *\n * Implements the shareOfVoice algorithm from upstream electron-player.\n * Interrupts are layouts that must play for a percentage of each hour.\n *\n * Algorithm:\n * 1. Separate interrupts from normal layouts\n * 2. Calculate how many times each interrupt must play per hour\n * 3. Fill remaining time with normal layouts\n * 4. Interleave interrupts and normal layouts evenly\n *\n * Based on: electron-player/src/main/common/scheduleManager.ts (lines 181-321)\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst logger = createLogger('schedule:interrupts');\n\n/**\n * Interrupt Scheduler\n * Handles shareOfVoice layouts that must play for a percentage of each hour\n */\nexport class InterruptScheduler {\n constructor() {\n // Track committed duration per interrupt layout\n this.interruptCommittedDurations = new Map(); // layoutId -> seconds\n }\n\n /**\n * Check if a layout is an interrupt (has shareOfVoice > 0)\n * @param {Object} layout - Layout object with shareOfVoice property\n * @returns {boolean} True if layout is an interrupt\n */\n isInterrupt(layout) {\n return !!(layout.shareOfVoice && layout.shareOfVoice > 0);\n }\n\n /**\n * Reset committed duration tracking (call this every hour)\n */\n resetCommittedDurations() {\n this.interruptCommittedDurations.clear();\n logger.debug('Reset interrupt committed durations');\n }\n\n /**\n * Get committed duration for a layout\n * @param {string} layoutId - Layout ID\n * @returns {number} Committed duration in seconds\n */\n getCommittedDuration(layoutId) {\n return this.interruptCommittedDurations.get(layoutId) || 0;\n }\n\n /**\n * Add committed duration for a layout\n * @param {string} layoutId - Layout ID\n * @param {number} duration - Duration to add in seconds\n */\n addCommittedDuration(layoutId, duration) {\n const current = this.getCommittedDuration(layoutId);\n this.interruptCommittedDurations.set(layoutId, current + duration);\n }\n\n /**\n * Check if interrupt layout has satisfied its shareOfVoice requirement\n * @param {Object} layout - Layout with shareOfVoice and duration\n * @returns {boolean} True if satisfied\n */\n isInterruptDurationSatisfied(layout) {\n if (!layout.shareOfVoice) {\n return true; // Not an interrupt\n }\n\n const layoutId = layout.id || layout.file;\n const requiredSeconds = (layout.shareOfVoice / 100) * 3600; // shareOfVoice is percentage\n const committedSeconds = this.getCommittedDuration(layoutId);\n\n return committedSeconds >= requiredSeconds;\n }\n\n /**\n * Calculate how many seconds this interrupt needs to play per hour\n * @param {Object} layout - Layout with shareOfVoice\n * @returns {number} Required seconds per hour\n */\n getRequiredSeconds(layout) {\n if (!layout.shareOfVoice) {\n return 0;\n }\n return (layout.shareOfVoice / 100) * 3600;\n }\n\n /**\n * Process interrupt layouts and combine with normal layouts\n * Implements the shareOfVoice algorithm from upstream\n *\n * @param {Array} normalLayouts - Normal scheduled layouts\n * @param {Array} interruptLayouts - Interrupt layouts with shareOfVoice\n * @returns {Array} Combined layout loop for the hour\n */\n processInterrupts(normalLayouts, interruptLayouts) {\n if (!interruptLayouts || interruptLayouts.length === 0) {\n logger.debug('No interrupt layouts, returning normal layouts');\n return normalLayouts;\n }\n\n if (!normalLayouts || normalLayouts.length === 0) {\n logger.warn('No normal layouts available, interrupts will fill entire hour');\n return this.fillHourWithInterrupts(interruptLayouts);\n }\n\n logger.info(`Processing ${interruptLayouts.length} interrupt layouts with ${normalLayouts.length} normal layouts`);\n\n // Reset committed durations for this calculation\n for (const layout of interruptLayouts) {\n const layoutId = layout.id || layout.file;\n this.interruptCommittedDurations.set(layoutId, 0);\n }\n\n const resolvedInterruptLayouts = [];\n let interruptSecondsInHour = 0;\n let index = 0;\n let satisfied = false;\n\n // Step 1: Build interrupt loop by cycling through interrupts until all are satisfied\n while (!satisfied) {\n // Gone all the way around? Check if all satisfied\n if (index >= interruptLayouts.length) {\n index = 0;\n\n // Check if all interrupts are satisfied\n let allSatisfied = true;\n for (const layout of interruptLayouts) {\n if (!this.isInterruptDurationSatisfied(layout)) {\n allSatisfied = false;\n break;\n }\n }\n\n if (allSatisfied) {\n satisfied = true;\n break;\n }\n }\n\n const currentInterrupt = interruptLayouts[index];\n\n // If this interrupt is not satisfied, add it to the loop\n if (!this.isInterruptDurationSatisfied(currentInterrupt)) {\n const layoutId = currentInterrupt.id || currentInterrupt.file;\n this.addCommittedDuration(layoutId, currentInterrupt.duration);\n interruptSecondsInHour += currentInterrupt.duration;\n resolvedInterruptLayouts.push(currentInterrupt);\n }\n\n index++;\n }\n\n logger.debug(`Resolved ${resolvedInterruptLayouts.length} interrupt plays (${interruptSecondsInHour}s total)`);\n\n // Step 2: If interrupts fill the entire hour, return only interrupts\n if (interruptSecondsInHour >= 3600) {\n logger.info('Interrupts fill entire hour (>= 3600s), no room for normal layouts');\n return resolvedInterruptLayouts;\n }\n\n // Step 3: Fill remaining time with normal layouts\n const normalSecondsInHour = 3600 - interruptSecondsInHour;\n const resolvedNormalLayouts = this.fillTimeWithLayouts(normalLayouts, normalSecondsInHour);\n\n logger.debug(`Resolved ${resolvedNormalLayouts.length} normal plays (${normalSecondsInHour}s target)`);\n\n // Step 4: Interleave interrupts and normal layouts\n const loop = this.interleaveLayouts(resolvedNormalLayouts, resolvedInterruptLayouts);\n\n logger.info(`Final loop: ${loop.length} layouts (${resolvedNormalLayouts.length} normal + ${resolvedInterruptLayouts.length} interrupts)`);\n\n return loop;\n }\n\n /**\n * Fill time with layouts by repeating them until duration is reached\n * @param {Array} layouts - Layouts to use\n * @param {number} targetSeconds - Target duration in seconds\n * @returns {Array} Resolved layout array\n */\n fillTimeWithLayouts(layouts, targetSeconds) {\n const resolved = [];\n let remainingSeconds = targetSeconds;\n let index = 0;\n\n while (remainingSeconds > 0) {\n if (index >= layouts.length) {\n index = 0; // Loop back\n }\n\n const layout = layouts[index];\n resolved.push(layout);\n remainingSeconds -= layout.duration;\n index++;\n }\n\n return resolved;\n }\n\n /**\n * Fill entire hour with interrupt layouts only\n * @param {Array} interruptLayouts - Interrupt layouts\n * @returns {Array} Layout loop\n */\n fillHourWithInterrupts(interruptLayouts) {\n return this.fillTimeWithLayouts(interruptLayouts, 3600);\n }\n\n /**\n * Interleave normal and interrupt layouts evenly\n * Based on upstream algorithm (scheduleManager.ts lines 268-316)\n *\n * @param {Array} normalLayouts - Normal layouts\n * @param {Array} interruptLayouts - Interrupt layouts\n * @returns {Array} Interleaved layout array\n */\n interleaveLayouts(normalLayouts, interruptLayouts) {\n const loop = [];\n const pickCount = Math.max(normalLayouts.length, interruptLayouts.length);\n\n // Calculate pick intervals\n // Normal: ceiling (pick more often from normal)\n // Interrupt: floor (pick less often from interrupts)\n const normalPick = Math.ceil(1.0 * pickCount / normalLayouts.length);\n const interruptPick = Math.floor(1.0 * pickCount / interruptLayouts.length);\n\n logger.debug(`Interleaving: pickCount=${pickCount}, normalPick=${normalPick}, interruptPick=${interruptPick}`);\n\n let normalIndex = 0;\n let interruptIndex = 0;\n let totalSecondsAllocated = 0;\n\n for (let i = 0; i < pickCount; i++) {\n // Pick from normal list\n if (i % normalPick === 0) {\n // Allow wrapping around\n if (normalIndex >= normalLayouts.length) {\n normalIndex = 0;\n }\n loop.push(normalLayouts[normalIndex]);\n totalSecondsAllocated += normalLayouts[normalIndex].duration;\n normalIndex++;\n }\n\n // Pick from interrupt list (only if we haven't picked them all yet)\n if (i % interruptPick === 0 && interruptIndex < interruptLayouts.length) {\n loop.push(interruptLayouts[interruptIndex]);\n totalSecondsAllocated += interruptLayouts[interruptIndex].duration;\n interruptIndex++;\n }\n }\n\n // Fill remaining time with normal layouts (due to ceiling/floor rounding)\n while (totalSecondsAllocated < 3600) {\n if (normalIndex >= normalLayouts.length) {\n normalIndex = 0;\n }\n loop.push(normalLayouts[normalIndex]);\n totalSecondsAllocated += normalLayouts[normalIndex].duration;\n normalIndex++;\n }\n\n logger.debug(`Interleaved ${loop.length} layouts, total duration: ${totalSecondsAllocated}s`);\n\n return loop;\n }\n\n /**\n * Separate layouts into normal and interrupt arrays\n * @param {Array} layouts - All layouts\n * @returns {Object} { normalLayouts, interruptLayouts }\n */\n separateLayouts(layouts) {\n const normalLayouts = [];\n const interruptLayouts = [];\n\n for (const layout of layouts) {\n if (this.isInterrupt(layout)) {\n interruptLayouts.push(layout);\n } else {\n normalLayouts.push(layout);\n }\n }\n\n return { normalLayouts, interruptLayouts };\n }\n}\n\n// Export singleton instance for convenience\nexport const interruptScheduler = new InterruptScheduler();\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Overlay Layout Scheduler\n *\n * Manages overlay layouts that appear on top of main layouts.\n * Based on upstream electron-player implementation.\n *\n * Overlays:\n * - Render on top of main layout (higher z-index)\n * - Have scheduled start/end times\n * - Support priority ordering (multiple overlays)\n * - Support criteria-based display (future)\n * - Support geofencing (future)\n *\n * Reference: upstream_players/electron-player/src/main/xmds/response/schedule/events/overlayLayout.ts\n */\n\nimport { createLogger } from '@xiboplayer/utils';\nimport { evaluateCriteria } from './criteria.js';\n\nconst logger = createLogger('schedule:overlays');\n\n/**\n * Overlay Scheduler\n * Handles overlay layouts that display on top of main layouts\n */\nexport class OverlayScheduler {\n constructor() {\n this.overlays = [];\n this.displayProperties = {};\n this.scheduleManager = null; // Reference to ScheduleManager for geo checks\n logger.debug('OverlayScheduler initialized');\n }\n\n /**\n * Set reference to ScheduleManager for geo-fence checks\n * @param {ScheduleManager} scheduleManager\n */\n setScheduleManager(scheduleManager) {\n this.scheduleManager = scheduleManager;\n }\n\n /**\n * Set display properties for criteria evaluation\n * @param {Object} properties\n */\n setDisplayProperties(properties) {\n this.displayProperties = properties || {};\n }\n\n /**\n * Update overlays from XMDS Schedule response\n * @param {Array} overlays - Overlay objects from XMDS\n */\n setOverlays(overlays) {\n this.overlays = overlays || [];\n logger.info(`Loaded ${this.overlays.length} overlay(s)`);\n }\n\n /**\n * Get currently active overlays\n * @returns {Array} Active overlay objects sorted by priority (highest first)\n */\n getCurrentOverlays() {\n if (!this.overlays || this.overlays.length === 0) {\n return [];\n }\n\n const now = new Date();\n const activeOverlays = [];\n\n for (const overlay of this.overlays) {\n // Check time window\n if (!this.isTimeActive(overlay, now)) {\n logger.debug(`Overlay ${overlay.file} not in time window`);\n continue;\n }\n\n // Check geo-awareness\n if (overlay.isGeoAware && overlay.geoLocation) {\n if (this.scheduleManager && !this.scheduleManager.isWithinGeoFence(overlay.geoLocation)) {\n logger.debug(`Overlay ${overlay.file} filtered by geofence`);\n continue;\n }\n }\n\n // Check criteria conditions\n if (overlay.criteria && overlay.criteria.length > 0) {\n if (!evaluateCriteria(overlay.criteria, { now, displayProperties: this.displayProperties })) {\n logger.debug(`Overlay ${overlay.file} filtered by criteria`);\n continue;\n }\n }\n\n activeOverlays.push(overlay);\n }\n\n // Sort by priority (highest first)\n activeOverlays.sort((a, b) => {\n const priorityA = a.priority || 0;\n const priorityB = b.priority || 0;\n return priorityB - priorityA;\n });\n\n if (activeOverlays.length > 0) {\n logger.info(`Active overlays: ${activeOverlays.length}`);\n }\n\n return activeOverlays;\n }\n\n /**\n * Check if overlay is within its time window.\n * Delegates to ScheduleManager.isTimeActive() which handles both\n * simple date ranges and recurring schedule dayparting.\n * Falls back to basic date-range check if no scheduleManager is set.\n *\n * @param {Object} overlay - Overlay object\n * @param {Date} now - Current time\n * @returns {boolean}\n */\n isTimeActive(overlay, now) {\n if (this.scheduleManager) {\n // Normalize fromDt → fromdt for ScheduleManager compatibility\n const normalized = { ...overlay };\n if (!normalized.fromdt && normalized.fromDt) normalized.fromdt = normalized.fromDt;\n if (!normalized.todt && normalized.toDt) normalized.todt = normalized.toDt;\n return this.scheduleManager.isTimeActive(normalized, now);\n }\n\n // Fallback: basic date-range check (no scheduleManager available)\n const from = (overlay.fromdt || overlay.fromDt) ? new Date(overlay.fromdt || overlay.fromDt) : null;\n const to = (overlay.todt || overlay.toDt) ? new Date(overlay.todt || overlay.toDt) : null;\n if (from && now < from) return false;\n if (to && now > to) return false;\n return true;\n }\n\n /**\n * Check if overlay schedule needs update (every minute)\n * @param {number} lastCheck - Last check timestamp\n * @returns {boolean}\n */\n shouldCheckOverlays(lastCheck) {\n if (!lastCheck) return true;\n const elapsed = Date.now() - lastCheck;\n return elapsed >= 60000; // 1 minute\n }\n\n /**\n * Get overlay by file ID\n * @param {number} fileId - Layout file ID\n * @returns {Object|null}\n */\n getOverlayByFile(fileId) {\n return this.overlays.find(o => o.file === fileId) || null;\n }\n\n /**\n * Clear all overlays\n */\n clear() {\n this.overlays = [];\n logger.debug('Cleared all overlays');\n }\n\n}\n\nexport const overlayScheduler = new OverlayScheduler();\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/schedule - Campaign scheduling and advanced features\n// Basic scheduling, interrupts, overlays, and dayparting\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\n\n/**\n * Core schedule manager for basic scheduling and dayparting\n * @module @xiboplayer/schedule\n */\nexport { ScheduleManager, scheduleManager } from './schedule.js';\n\n/**\n * Interrupt scheduler for shareOfVoice layouts\n * @module @xiboplayer/schedule/interrupts\n */\nexport { InterruptScheduler } from './interrupts.js';\n\n/**\n * Overlay layout scheduler\n * @module @xiboplayer/schedule/overlays\n */\nexport { OverlayScheduler } from './overlays.js';\n\n/**\n * Offline timeline calculator — duration parser + timeline simulator\n * @module @xiboplayer/schedule/timeline\n */\nexport { calculateTimeline, parseLayoutDuration, parseLayoutFile, buildScheduleQueue } from './timeline.js';\n"],"mappings":"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"}
1
+ {"version":3,"file":"src-DNqc--R2.js","names":["log","logger","pkg"],"sources":["../../../schedule/package.json","../../../schedule/src/criteria.js","../../../schedule/src/timeline.js","../../../schedule/src/schedule.js","../../../schedule/src/interrupts.js","../../../schedule/src/overlays.js","../../../schedule/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/schedule\",\n \"version\": \"0.7.13\",\n \"description\": \"Complete scheduling solution: campaigns, dayparting, interrupts, and overlays\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.js\",\n \"./schedule\": \"./src/schedule.js\",\n \"./interrupts\": \"./src/interrupts.js\",\n \"./overlays\": \"./src/overlays.js\"\n },\n \"scripts\": {\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\"\n },\n \"dependencies\": {\n \"@xiboplayer/utils\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"scheduling\",\n \"dayparting\",\n \"campaigns\",\n \"interrupts\",\n \"overlays\",\n \"shareOfVoice\"\n ],\n \"author\": \"Pau Aliagas <linuxnow@gmail.com>\",\n \"license\": \"AGPL-3.0-or-later\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/xibo-players/xiboplayer.git\",\n \"directory\": \"packages/schedule\"\n },\n \"homepage\": \"https://xiboplayer.org\"\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Criteria Evaluator\n *\n * Evaluates schedule criteria against current player state.\n * Criteria are conditions set in the CMS that determine whether\n * a layout/overlay should display on a given player.\n *\n * Supported metrics:\n * - dayOfWeek: Current day name (Monday-Sunday)\n * - dayOfMonth: Day number (1-31)\n * - month: Month number (1-12)\n * - hour: Hour (0-23)\n * - isoDay: ISO day of week (1=Monday, 7=Sunday)\n *\n * Weather metrics (require weatherData in options):\n * - weatherTemp: Current temperature\n * - weatherHumidity: Current humidity percentage\n * - weatherWindSpeed: Current wind speed\n * - weatherCondition: Current weather condition (e.g. \"Clear\", \"Rain\")\n * - weatherCloudCover: Cloud cover percentage\n *\n * Supported conditions:\n * - equals, notEquals\n * - greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals\n * - contains, notContains, startsWith, endsWith\n * - in (comma-separated list)\n *\n * Display property metrics are resolved via a property map\n * provided at evaluation time.\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('schedule:criteria');\n\nconst DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n\n/**\n * Weather metric name → weatherData property mapping\n */\nconst WEATHER_METRICS = {\n weatherTemp: 'temperature',\n weatherHumidity: 'humidity',\n weatherWindSpeed: 'windSpeed',\n weatherCondition: 'condition',\n weatherCloudCover: 'cloudCover',\n};\n\n/**\n * Get built-in metric value from current date/time\n * @param {string} metric - Metric name\n * @param {Date} now - Current date\n * @param {Object} displayProperties - Display property map from CMS\n * @param {Object} weatherData - Weather data from GetWeather XMDS call\n * @returns {string|null} Metric value or null if unknown\n */\nfunction getMetricValue(metric, now, displayProperties = {}, weatherData = {}) {\n switch (metric) {\n case 'dayOfWeek':\n return DAY_NAMES[now.getDay()];\n case 'dayOfMonth':\n return String(now.getDate());\n case 'month':\n return String(now.getMonth() + 1);\n case 'hour':\n return String(now.getHours());\n case 'isoDay':\n return String(now.getDay() === 0 ? 7 : now.getDay());\n default:\n // Check weather metrics\n if (WEATHER_METRICS[metric]) {\n const weatherKey = WEATHER_METRICS[metric];\n if (weatherData[weatherKey] !== undefined) {\n return String(weatherData[weatherKey]);\n }\n log.debug(`Weather metric \"${metric}\" requested but no weather data available`);\n return null;\n }\n // Check display properties (custom fields set in CMS)\n if (displayProperties[metric] !== undefined) {\n return String(displayProperties[metric]);\n }\n log.debug(`Unknown metric: ${metric}`);\n return null;\n }\n}\n\n/**\n * Evaluate a single condition\n * @param {string} actual - Actual value from player state\n * @param {string} condition - Condition operator\n * @param {string} expected - Expected value from criteria\n * @param {string} type - Value type ('string' or 'number')\n * @returns {boolean}\n */\nfunction evaluateCondition(actual, condition, expected, type) {\n if (actual === null) return false;\n\n // Number comparison\n if (type === 'number') {\n const a = parseFloat(actual);\n const e = parseFloat(expected);\n if (isNaN(a) || isNaN(e)) return false;\n\n switch (condition) {\n case 'equals': return a === e;\n case 'notEquals': return a !== e;\n case 'greaterThan': return a > e;\n case 'greaterThanOrEquals': return a >= e;\n case 'lessThan': return a < e;\n case 'lessThanOrEquals': return a <= e;\n default: return false;\n }\n }\n\n // String comparison (case-insensitive)\n const a = actual.toLowerCase();\n const e = expected.toLowerCase();\n\n switch (condition) {\n case 'equals': return a === e;\n case 'notEquals': return a !== e;\n case 'contains': return a.includes(e);\n case 'notContains': return !a.includes(e);\n case 'startsWith': return a.startsWith(e);\n case 'endsWith': return a.endsWith(e);\n case 'in': return e.split(',').map(s => s.trim().toLowerCase()).includes(a);\n case 'greaterThan': return a > e;\n case 'lessThan': return a < e;\n default:\n log.debug(`Unknown condition: ${condition}`);\n return false;\n }\n}\n\n/**\n * Evaluate all criteria for a schedule item.\n * All criteria must match (AND logic) for the item to display.\n *\n * @param {Array<{metric: string, condition: string, type: string, value: string}>} criteria\n * @param {Object} options\n * @param {Date} [options.now] - Current date (defaults to new Date())\n * @param {Object} [options.displayProperties] - Display property map from CMS\n * @param {Object} [options.weatherData] - Weather data from GetWeather XMDS call\n * @returns {boolean} True if all criteria match (or no criteria)\n */\nexport function evaluateCriteria(criteria, options = {}) {\n if (!criteria || criteria.length === 0) return true;\n\n const now = options.now || new Date();\n const displayProperties = options.displayProperties || {};\n const weatherData = options.weatherData || {};\n\n for (const criterion of criteria) {\n const actual = getMetricValue(criterion.metric, now, displayProperties, weatherData);\n const matches = evaluateCondition(actual, criterion.condition, criterion.value, criterion.type);\n\n if (!matches) {\n log.debug(`Criteria failed: ${criterion.metric} ${criterion.condition} \"${criterion.value}\" (actual: \"${actual}\")`);\n return false;\n }\n }\n\n return true;\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Offline Schedule Timeline Calculator\n *\n * Calculates deterministic playback timelines by parsing layout XLF durations\n * and simulating round-robin scheduling. Enables the player to answer\n * \"what's the playback plan for the next N hours?\" while offline.\n */\n\n/**\n * Parse layout duration from XLF XML string.\n * Lightweight parser — uses DOMParser, no rendering.\n *\n * Single source of truth for XLF-based duration calculation.\n * Supports a 3-phase progressive refinement pipeline:\n * Phase 1 (ESTIMATE): parseLayoutDuration(xlf) — static duration from XLF\n * Phase 2 (PROBE): parseLayoutDuration(xlf, videoDurations) — refined with real video lengths\n * Phase 3 (LIVE UPDATE): renderer's updateLayoutDuration() — corrections from DURATION comments\n *\n * Duration resolution order:\n * 1. Explicit <layout duration=\"60\"> attribute\n * 2. Sum of widget <media duration=\"X\"> per region (max across regions)\n * 3. Fallback: 60s\n *\n * @param {string} xlfXml - Raw XLF XML string\n * @param {Map<string, number>|null} [videoDurations=null] - Optional map of fileId → probed duration in seconds\n * @returns {{ duration: number, isDynamic: boolean }} Duration in seconds and whether any widget has useDuration=0\n */\n/**\n * Extract numeric layout ID from a schedule filename like \"123.xlf\" or \"123\"\n * @param {string|number} f - Layout file reference\n * @returns {number}\n */\nexport function parseLayoutFile(f) {\n return parseInt(String(f).replace('.xlf', ''), 10);\n}\n\nexport function parseLayoutDuration(xlfXml, videoDurations = null) {\n const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) return { duration: 60, isDynamic: false };\n\n // 1. Explicit layout duration attribute\n const explicit = parseInt(layoutEl.getAttribute('duration') || '0', 10);\n if (explicit > 0) return { duration: explicit, isDynamic: false };\n\n // 2. Calculate from widget durations (max region wins — regions play in parallel)\n let maxDuration = 0;\n let isDynamic = false;\n for (const regionEl of layoutEl.querySelectorAll('region')) {\n const regionType = regionEl.getAttribute('type');\n if (regionType === 'drawer') continue; // Drawers are action-triggered, not timed\n const isCanvas = regionType === 'canvas';\n let regionDuration = 0;\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const dur = parseInt(mediaEl.getAttribute('duration') || '0', 10);\n const useDuration = parseInt(mediaEl.getAttribute('useDuration') || '1', 10);\n const fileId = mediaEl.getAttribute('fileId') || '';\n const probed = videoDurations?.get(fileId);\n\n let widgetDuration;\n if (probed !== undefined) {\n widgetDuration = probed; // Phase 2: probed video duration\n } else if (dur > 0 && useDuration !== 0) {\n widgetDuration = dur; // Explicit CMS duration\n } else {\n // Video with useDuration=0 means \"play to end\" — estimate 60s,\n // corrected later via recordLayoutDuration() when video metadata loads\n widgetDuration = 60;\n isDynamic = true;\n }\n\n if (isCanvas) {\n // Canvas regions play all widgets simultaneously — duration is max, not sum\n regionDuration = Math.max(regionDuration, widgetDuration);\n } else {\n regionDuration += widgetDuration;\n }\n }\n maxDuration = Math.max(maxDuration, regionDuration);\n }\n\n const duration = maxDuration > 0 ? maxDuration : 60;\n return { duration, isDynamic };\n}\n\n/**\n * Compare two arrays of layout files for equality.\n * @param {string[]} a\n * @param {string[]} b\n * @returns {boolean}\n */\nfunction arraysEqual(a, b) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Check if a layout can play at a given time based on simulated play history.\n * Replicates ScheduleManager.canPlayLayout() logic for timeline prediction.\n *\n * Even-distribution rules:\n * 1. Total plays in sliding 1-hour window < maxPlaysPerHour\n * 2. Time since last play >= (60 / maxPlaysPerHour) minutes\n *\n * @param {number[]} history - Simulated play timestamps (ms) for this layout\n * @param {number} maxPlaysPerHour - Max plays per hour (0 = unlimited)\n * @param {number} timeMs - Current simulated time in ms\n * @returns {boolean}\n */\nexport function canSimulatedPlay(history, maxPlaysPerHour, timeMs) {\n if (!maxPlaysPerHour || maxPlaysPerHour === 0) return true;\n\n const oneHourAgo = timeMs - 3600000;\n const playsInLastHour = history.filter(t => t > oneHourAgo);\n\n // Check 1: under hourly limit\n if (playsInLastHour.length >= maxPlaysPerHour) return false;\n\n // Check 2: minimum gap for even distribution\n if (playsInLastHour.length > 0) {\n const minGapMs = 3600000 / maxPlaysPerHour;\n const lastPlay = Math.max(...playsInLastHour);\n if (timeMs - lastPlay < minGapMs) return false;\n }\n\n return true;\n}\n\n/**\n * From a list of layout metadata, apply simulated rate limiting and priority\n * filtering to determine which layouts can actually play at the given time.\n * Mirrors the real player logic: filter rate-limited layouts first, then\n * pick highest remaining priority.\n *\n * @param {Array<{file: string, priority: number, maxPlaysPerHour: number}>} allLayouts\n * @param {Map<string, number[]>} simPlays - Simulated play history\n * @param {number} timeMs - Current simulated time in ms\n * @returns {string[]} Layout files that can play, highest priority first\n */\nfunction getPlayableLayouts(allLayouts, simPlays, timeMs) {\n // Step 1: Filter out rate-limited layouts\n const eligible = allLayouts.filter(l => {\n if (!l.maxPlaysPerHour || l.maxPlaysPerHour === 0) return true;\n const history = simPlays.get(l.file) || [];\n return canSimulatedPlay(history, l.maxPlaysPerHour, timeMs);\n });\n\n if (eligible.length === 0) return [];\n\n // Step 2: Pick highest priority from remaining layouts\n const maxPriority = Math.max(...eligible.map(l => l.priority));\n return eligible\n .filter(l => l.priority === maxPriority)\n .map(l => l.file);\n}\n\n/**\n * Calculate a deterministic playback timeline by walking the pre-built schedule queue.\n *\n * The queue already has all constraints baked in (maxPlaysPerHour, priorities,\n * dayparting, default layout fills). This function simply cycles through it from\n * the current position, generating time-stamped entries for the overlay.\n *\n * @param {Array<{layoutId: string, duration: number}>} queue - Pre-built schedule queue from buildScheduleQueue()\n * @param {number} queuePosition - Current position in the queue (from schedule._queuePosition)\n * @param {Object} [options]\n * @param {Date} [options.from] - Start time (default: now)\n * @param {number} [options.hours] - Hours to project (default: 2)\n * @param {string} [options.defaultLayout] - Default layout file (to tag isDefault entries)\n * @param {Map<string, number>} [options.durations] - Live durations map (overrides queue entry durations with corrected values)\n * @param {Date} [options.currentLayoutStartedAt] - When current layout started (adjusts first entry to remaining time)\n * @returns {Array<{layoutFile: string, startTime: Date, endTime: Date, duration: number, isDefault: boolean}>}\n */\nexport function calculateTimeline(queue, queuePosition, options = {}) {\n const from = options.from || new Date();\n const hours = options.hours || 2;\n const to = new Date(from.getTime() + hours * 3600000);\n const currentLayoutStartedAt = options.currentLayoutStartedAt || null;\n const defaultLayout = options.defaultLayout || null;\n const durations = options.durations || null;\n\n if (!queue || queue.length === 0) return [];\n\n const timeline = [];\n let currentTime = new Date(from);\n // queuePosition has already advanced past the currently-playing layout\n // (via popNextFromQueue), so entries here start from the NEXT layout.\n // The current layout's duration is passed directly to the overlay.\n let pos = queuePosition % queue.length;\n const maxEntries = 500;\n\n while (currentTime < to && timeline.length < maxEntries) {\n const entry = queue[pos];\n // Use live-corrected duration (from video metadata, etc.) if available,\n // otherwise fall back to the queue's baked-in duration\n const dur = (durations && durations.get(entry.layoutId)) || entry.duration;\n\n const endMs = currentTime.getTime() + dur * 1000;\n\n timeline.push({\n layoutFile: entry.layoutId,\n startTime: new Date(currentTime),\n endTime: new Date(endMs),\n duration: dur,\n isDefault: defaultLayout ? entry.layoutId === defaultLayout : false,\n });\n\n currentTime = new Date(endMs);\n pos = (pos + 1) % queue.length;\n }\n\n return timeline;\n}\n\n// ── LCM-based deterministic schedule queue ──────────────────────────────\n\n/**\n * Greatest common divisor (Euclidean algorithm).\n * @param {number} a\n * @param {number} b\n * @returns {number}\n */\nfunction gcd(a, b) {\n a = Math.abs(Math.round(a));\n b = Math.abs(Math.round(b));\n while (b) { [a, b] = [b, a % b]; }\n return a;\n}\n\n/**\n * Least common multiple of two integers.\n * @param {number} a\n * @param {number} b\n * @returns {number}\n */\nfunction lcm(a, b) {\n if (a === 0 || b === 0) return 0;\n return Math.abs(Math.round(a) * Math.round(b)) / gcd(a, b);\n}\n\n/**\n * LCM of an array of integers.\n * @param {number[]} values\n * @returns {number}\n */\nfunction lcmArray(values) {\n return values.reduce((acc, v) => lcm(acc, v), 1);\n}\n\n/**\n * Build a deterministic playback queue by simulating one LCM period.\n *\n * Uses getPlayableLayouts() (the same priority-fallback + rate-limit logic\n * that calculateTimeline uses) to simulate playback for one repeating cycle.\n * This ensures the queue matches the timeline overlay exactly: high-priority\n * rate-limited layouts get their slots, then lower-priority layouts fill gaps.\n *\n * @param {Array<{file: string, priority: number, maxPlaysPerHour: number}>} allLayouts\n * All time-active layouts from schedule.getAllLayoutsAtTime()\n * @param {Map<string, number>} durations\n * Map of layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {string} [options.defaultLayout] - Default layout file (CMS fallback)\n * @param {number} [options.defaultDuration] - Fallback duration (default: 60)\n * @param {Set<string>} [options.dynamicLayouts] - Set of layout files that are dynamic (video, useDuration=0)\n * @returns {{ queue: Array<{layoutId: string, duration: number}>, periodSeconds: number }}\n */\nexport function buildScheduleQueue(allLayouts, durations, options = {}) {\n const {\n defaultLayout = null,\n defaultDuration = 60,\n } = options;\n\n if (allLayouts.length === 0 && !defaultLayout) {\n return { queue: [], periodSeconds: 0 };\n }\n\n // Build CMS duration lookup — use CMS-reported duration as fallback\n // when the durations map (from XLF parsing / video metadata) has no entry.\n const cmsDurations = new Map();\n for (const l of allLayouts) {\n if (l.duration > 0) cmsDurations.set(l.file, l.duration);\n }\n const getDuration = (file) => durations.get(file) || cmsDurations.get(file) || defaultDuration;\n\n // Step 1: Identify rate-limited layouts to calculate LCM period\n const rateLimited = allLayouts.filter(l => l.maxPlaysPerHour > 0);\n\n let periodSeconds;\n if (rateLimited.length > 0) {\n const intervals = rateLimited.map(l => Math.round(3600 / l.maxPlaysPerHour));\n periodSeconds = lcmArray(intervals);\n // Cap at 2 hours to prevent absurd periods\n if (periodSeconds > 7200) periodSeconds = 7200;\n } else {\n // No rate-limited layouts — single round-robin cycle\n const totalDuration = allLayouts.reduce((sum, l) => sum + getDuration(l.file), 0)\n + (defaultLayout && !allLayouts.some(l => l.file === defaultLayout)\n ? getDuration(defaultLayout)\n : 0);\n periodSeconds = totalDuration || defaultDuration;\n }\n\n // Step 2: Simulate playback for one period using getPlayableLayouts()\n const queue = [];\n const simPlays = new Map(); // file → [timestampMs] for rate-limit tracking\n let cursorMs = 0;\n const periodMs = periodSeconds * 1000;\n const maxEntries = 500; // safety cap\n\n while (cursorMs < periodMs && queue.length < maxEntries) {\n // Get playable layouts at current simulated time (priority fallback + rate limits)\n const playable = getPlayableLayouts(allLayouts, simPlays, cursorMs);\n\n if (playable.length === 0) {\n // All layouts exhausted — use default\n if (defaultLayout) {\n const dur = getDuration(defaultLayout);\n queue.push({ layoutId: defaultLayout, duration: dur });\n cursorMs += dur * 1000;\n } else {\n // No default — skip ahead 60s to avoid infinite loop\n cursorMs += 60000;\n }\n continue;\n }\n\n // Play all playable layouts in round-robin order (one each), then re-evaluate\n for (let i = 0; i < playable.length && cursorMs < periodMs && queue.length < maxEntries; i++) {\n const file = playable[i];\n const dur = getDuration(file);\n\n queue.push({ layoutId: file, duration: dur });\n\n // Record simulated play for rate-limit tracking\n if (!simPlays.has(file)) simPlays.set(file, []);\n simPlays.get(file).push(cursorMs);\n\n cursorMs += dur * 1000;\n\n // Re-evaluate after each play: if the playable set changed, break to outer loop\n const nextPlayable = getPlayableLayouts(allLayouts, simPlays, cursorMs);\n if (!arraysEqual(playable, nextPlayable)) break;\n }\n }\n\n // Handle edge case: no layouts and only default\n if (queue.length === 0 && defaultLayout) {\n const defDur = getDuration(defaultLayout);\n queue.push({ layoutId: defaultLayout, duration: defDur });\n }\n\n return { queue, periodSeconds };\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Schedule manager - determines which layouts to show\n */\n\nimport { createLogger } from '@xiboplayer/utils';\nimport { evaluateCriteria } from './criteria.js';\nimport { buildScheduleQueue, canSimulatedPlay, parseLayoutFile } from './timeline.js';\n\nconst log = createLogger('Schedule');\n\nexport class ScheduleManager {\n constructor(options = {}) {\n this.schedule = null;\n this.playHistory = new Map(); // Track plays per layout: layoutId -> [timestamps]\n this.interruptScheduler = options.interruptScheduler || null; // Optional interrupt scheduler\n this.displayProperties = options.displayProperties || {}; // CMS display custom properties\n this.weatherData = {}; // Weather data from GetWeather XMDS call\n this.playerLocation = null; // { latitude, longitude } from Geolocation API\n this._layoutMetadata = new Map(); // layoutFile → { syncEvent, shareOfVoice, ... }\n\n // Pre-calculated schedule queue (LCM-based deterministic timeline)\n this._scheduleQueue = null; // { queue: [{layoutId, duration}], periodSeconds }\n this._queuePosition = 0; // Current position in the queue\n this._queueLayoutSet = null; // Stringified active layout set (for invalidation)\n }\n\n /**\n * Update schedule from XMDS\n */\n setSchedule(schedule) {\n this.schedule = schedule;\n this._invalidateQueue();\n }\n\n /**\n * Update weather data for criteria evaluation\n * @param {Object} data - Parsed weather object { temperature, humidity, windSpeed, condition, cloudCover }\n */\n setWeatherData(data) {\n this.weatherData = data || {};\n }\n\n /**\n * Get data connectors from current schedule\n * @returns {Array} Data connector configurations, or empty array\n */\n getDataConnectors() {\n return this.schedule?.dataConnectors || [];\n }\n\n /**\n * Get dependants map: layoutId → filenames that must be cached before that layout plays.\n * Includes both per-layout and global dependants.\n * Used by download manager to prioritize sub-playlist media alongside its parent layout.\n * @returns {Map<number, string[]>} layoutId → dependant filenames\n */\n getDependantsMap() {\n const map = new Map();\n if (!this.schedule) return map;\n\n const globalDeps = this.schedule.dependants || [];\n\n const addLayout = (layout) => {\n const id = parseLayoutFile(layout.file || layout.id);\n const deps = [...globalDeps, ...(layout.dependants || [])];\n if (deps.length > 0) map.set(id, deps);\n };\n\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) addLayout(layout);\n }\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n for (const layout of campaign.layouts) addLayout(layout);\n }\n }\n\n return map;\n }\n\n /**\n * Check if a schedule item is active based on recurrence rules.\n * Supports Week, Day, and Month recurrence types.\n */\n isRecurringScheduleActive(item, now) {\n // If no recurrence, it's not a recurring schedule\n if (!item.recurrenceType) {\n return true; // Not a recurring schedule, use date/time checks instead\n }\n\n // Check recurrence range first (applies to all types)\n if (item.recurrenceRange) {\n const rangeEnd = new Date(item.recurrenceRange);\n if (now > rangeEnd) {\n return false; // Recurrence has ended\n }\n }\n\n switch (item.recurrenceType) {\n case 'Week': {\n // Check if current day of week matches recurrenceRepeatsOn\n // recurrenceRepeatsOn format: \"1,2,3,4,5\" (1=Monday, 7=Sunday, ISO format)\n if (item.recurrenceRepeatsOn) {\n const currentDayOfWeek = this.getIsoDayOfWeek(now);\n const allowedDays = item.recurrenceRepeatsOn.split(',').map(d => parseInt(d.trim()));\n if (!allowedDays.includes(currentDayOfWeek)) {\n return false;\n }\n }\n return true;\n }\n\n case 'Day': {\n // Daily recurrence with optional interval (recurrenceDetail)\n // If recurrenceDetail > 1, only active every N days from fromdt\n const interval = item.recurrenceDetail || 1;\n if (interval > 1 && item.fromdt) {\n const startDate = new Date(item.fromdt);\n const diffMs = now.getTime() - startDate.getTime();\n const diffDays = Math.floor(diffMs / 86400000);\n if (diffDays < 0 || diffDays % interval !== 0) {\n return false;\n }\n }\n return true;\n }\n\n case 'Month': {\n // Monthly recurrence — recurrenceRepeatsOn is day-of-month (1-31)\n if (item.recurrenceRepeatsOn) {\n const allowedDays = item.recurrenceRepeatsOn.split(',').map(d => parseInt(d.trim()));\n const currentDayOfMonth = now.getDate();\n if (!allowedDays.includes(currentDayOfMonth)) {\n return false;\n }\n }\n // If recurrenceDetail > 1, only active every N months from fromdt\n const interval = item.recurrenceDetail || 1;\n if (interval > 1 && item.fromdt) {\n const startDate = new Date(item.fromdt);\n const monthsDiff = (now.getFullYear() - startDate.getFullYear()) * 12\n + now.getMonth() - startDate.getMonth();\n if (monthsDiff < 0 || monthsDiff % interval !== 0) {\n return false;\n }\n }\n return true;\n }\n\n default:\n log.debug(`Unsupported recurrence type: ${item.recurrenceType}`);\n return true; // Unknown type, fallback to date/time checks\n }\n }\n\n /**\n * Get ISO day of week (1=Monday, 7=Sunday)\n */\n getIsoDayOfWeek(date) {\n const day = date.getDay(); // 0=Sunday, 6=Saturday\n return day === 0 ? 7 : day; // Convert to ISO (1=Monday, 7=Sunday)\n }\n\n /**\n * Check if current time is within the schedule's time window\n * Handles both date ranges and time-of-day for dayparting\n */\n isTimeActive(item, now) {\n const from = item.fromdt ? new Date(item.fromdt) : null;\n const to = item.todt ? new Date(item.todt) : null;\n\n // For recurring schedules, check time-of-day instead of full datetime\n if (item.recurrenceType === 'Week' || item.recurrenceType === 'Day' || item.recurrenceType === 'Month') {\n // Extract time from fromdt/todt and compare with current time\n if (from && to) {\n const currentTime = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();\n const fromTime = from.getHours() * 3600 + from.getMinutes() * 60 + from.getSeconds();\n const toTime = to.getHours() * 3600 + to.getMinutes() * 60 + to.getSeconds();\n\n // Handle midnight crossing\n if (fromTime <= toTime) {\n // Normal case: 09:00 - 17:00\n return currentTime >= fromTime && currentTime <= toTime;\n } else {\n // Midnight crossing: 22:00 - 02:00\n return currentTime >= fromTime || currentTime <= toTime;\n }\n }\n return true;\n }\n\n // For non-recurring schedules, use full date/time comparison\n if (from && now < from) return false;\n if (to && now > to) return false;\n return true;\n }\n\n /**\n * Get current layouts to display\n * Returns array of layout files, prioritized\n *\n * Campaign behavior:\n * - Priority applies at campaign level, not individual layout level\n * - All layouts in a campaign share the campaign's priority\n * - Layouts within a campaign are returned in order for cycling\n * - Standalone layouts compete with campaigns at their own priority\n *\n * Dayparting behavior:\n * - Schedules can recur weekly on specific days (recurrenceType='Week')\n * - recurrenceRepeatsOn specifies days: \"1,2,3,4,5\" (Mon-Fri, ISO format)\n * - Time matching uses time-of-day for recurring schedules\n * - Non-recurring schedules use full date/time ranges\n *\n * Interrupt behavior (shareOfVoice):\n * - Layouts with shareOfVoice > 0 are interrupts\n * - They must play for a percentage of each hour\n * - Normal layouts fill remaining time\n * - Interrupts are interleaved with normal layouts\n */\n getCurrentLayouts() {\n return this._getLayoutsAt(new Date());\n }\n\n /**\n * Get layouts active at a specific time.\n * Skips rate limiting and interrupt processing (those depend on real-time state).\n * Used by timeline calculator to predict future playback.\n * @param {Date} time - The time to evaluate\n * @returns {string[]} Layout files active at that time\n */\n getLayoutsAtTime(time) {\n return this._getLayoutsAt(time, { skipRateLimiting: true, skipInterrupts: true, quiet: true });\n }\n\n /**\n * Get ALL time-active layouts with metadata, without priority or rate-limit filtering.\n * Used by calculateTimeline() to simulate real playback with rate limiting and\n * priority fallback (e.g., when high-priority layouts hit maxPlaysPerHour, lower\n * priority layouts fill the gap).\n *\n * @param {Date} time - The time to evaluate\n * @returns {Array<{file: string, priority: number, maxPlaysPerHour: number}>}\n */\n getAllLayoutsAtTime(time) {\n if (!this.schedule) return [];\n\n const now = time;\n const results = [];\n\n // Standalone layouts\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) {\n if (!this.isRecurringScheduleActive(layout, now)) continue;\n if (!this.isTimeActive(layout, now)) continue;\n if (layout.criteria && layout.criteria.length > 0) {\n if (!evaluateCriteria(layout.criteria, { now, displayProperties: this.displayProperties, weatherData: this.weatherData })) continue;\n }\n if (layout.isGeoAware && layout.geoLocation) {\n if (!this.isWithinGeoFence(layout.geoLocation)) continue;\n }\n results.push({\n file: layout.file,\n priority: layout.priority || 0,\n maxPlaysPerHour: layout.maxPlaysPerHour || 0,\n duration: layout.duration || 0,\n });\n }\n }\n\n // Campaign layouts\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n if (!this.isRecurringScheduleActive(campaign, now)) continue;\n if (!this.isTimeActive(campaign, now)) continue;\n for (const layout of campaign.layouts) {\n results.push({\n file: layout.file,\n priority: campaign.priority || 0,\n maxPlaysPerHour: layout.maxPlaysPerHour || 0,\n duration: layout.duration || 0,\n });\n }\n }\n }\n\n return results;\n }\n\n /**\n * Detect schedule conflicts: time windows where multiple layouts compete\n * and lower-priority ones are hidden.\n *\n * Scans the schedule in 1-minute increments over the given window.\n * At each point, collects all time-active layouts (after criteria/geofence\n * filtering but before priority filtering). If multiple priorities exist,\n * the lower-priority entries are reported as hidden.\n *\n * @param {Object} [options]\n * @param {Date} [options.from] - Start time (default: now)\n * @param {number} [options.hours] - Hours to scan (default: 24)\n * @returns {Array<{startTime: Date, endTime: Date, winner: {file: string, priority: number}, hidden: Array<{file: string, priority: number}>}>}\n */\n detectConflicts(options = {}) {\n const from = options.from || new Date();\n const hours = options.hours || 24;\n const to = new Date(from.getTime() + hours * 3600000);\n const stepMs = 60000; // 1-minute granularity\n const conflicts = [];\n let current = null; // Current conflict window being built\n\n for (let t = from.getTime(); t < to.getTime(); t += stepMs) {\n const time = new Date(t);\n const allLayouts = this.getAllLayoutsAtTime(time);\n\n if (allLayouts.length === 0) {\n // No layouts → close any open conflict\n if (current) { conflicts.push(current); current = null; }\n continue;\n }\n\n const maxPriority = Math.max(...allLayouts.map(l => l.priority));\n const hidden = allLayouts.filter(l => l.priority < maxPriority);\n\n if (hidden.length === 0) {\n // No conflict at this time\n if (current) { conflicts.push(current); current = null; }\n continue;\n }\n\n // Conflict exists — build or extend window\n const winners = allLayouts.filter(l => l.priority === maxPriority);\n const winnerKey = winners.map(w => w.file).sort().join(',');\n const hiddenKey = hidden.map(h => `${h.file}:${h.priority}`).sort().join(',');\n\n if (current && current._winnerKey === winnerKey && current._hiddenKey === hiddenKey) {\n // Same conflict continues — extend window\n current.endTime = new Date(t + stepMs);\n } else {\n // New or changed conflict\n if (current) conflicts.push(current);\n current = {\n startTime: new Date(t),\n endTime: new Date(t + stepMs),\n winner: { file: winners[0].file, priority: maxPriority },\n hidden: hidden.map(h => ({ file: h.file, priority: h.priority })),\n _winnerKey: winnerKey,\n _hiddenKey: hiddenKey,\n };\n }\n }\n\n if (current) conflicts.push(current);\n\n // Clean internal keys\n for (const c of conflicts) {\n delete c._winnerKey;\n delete c._hiddenKey;\n }\n\n return conflicts;\n }\n\n /**\n * Internal: evaluate schedule at a given time.\n * @param {Date} now - Time to evaluate\n * @param {Object} [options] - Options\n * @param {boolean} [options.skipRateLimiting] - Skip maxPlaysPerHour checks\n * @param {boolean} [options.skipInterrupts] - Skip interrupt/shareOfVoice processing\n */\n _getLayoutsAt(now, options = {}) {\n if (!this.schedule) {\n return [];\n }\n\n const { skipRateLimiting = false, skipInterrupts = false, quiet = false } = options;\n const _log = quiet ? () => {} : (...args) => log.info(...args);\n const activeItems = []; // Mix of campaign objects and standalone layouts\n\n // Track the highest priority of any time-active layout BEFORE rate-limit\n // filtering. Used by advanceToNextLayout() to detect when only lower-\n // priority layouts remain (all high-priority ones are rate-limited) and\n // replay the current layout instead of downgrading.\n this._maxActivePriority = 0;\n\n // Find all active campaigns\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n // Check recurrence and time window\n if (!this.isRecurringScheduleActive(campaign, now)) {\n continue;\n }\n if (!this.isTimeActive(campaign, now)) {\n continue;\n }\n\n this._maxActivePriority = Math.max(this._maxActivePriority, campaign.priority || 0);\n\n // Campaign is active - add it as a single item with its priority\n activeItems.push({\n type: 'campaign',\n priority: campaign.priority,\n layouts: campaign.layouts, // Keep full layout objects for interrupt processing\n campaignId: campaign.id\n });\n }\n }\n\n // Find all active standalone layouts\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) {\n // Check recurrence and time window\n if (!this.isRecurringScheduleActive(layout, now)) {\n continue;\n }\n if (!this.isTimeActive(layout, now)) {\n continue;\n }\n\n // Check criteria conditions (date/time, display properties)\n if (layout.criteria && layout.criteria.length > 0) {\n if (!evaluateCriteria(layout.criteria, { now, displayProperties: this.displayProperties, weatherData: this.weatherData })) {\n _log('[Schedule] Layout', layout.id, 'filtered by criteria');\n continue;\n }\n }\n\n // Check geo-fencing\n if (layout.isGeoAware && layout.geoLocation) {\n if (!this.isWithinGeoFence(layout.geoLocation)) {\n _log('[Schedule] Layout', layout.id, 'filtered by geofence');\n continue;\n }\n }\n\n // Track priority before rate-limit filtering\n this._maxActivePriority = Math.max(this._maxActivePriority, layout.priority || 0);\n\n // Check max plays per hour (skip for future time queries)\n if (!skipRateLimiting && !this.canPlayLayout(layout.id, layout.maxPlaysPerHour)) {\n _log('[Schedule] Layout', layout.id, 'filtered by maxPlaysPerHour (limit:', layout.maxPlaysPerHour, ')');\n // Continue to check other layouts, but don't add this one\n continue;\n }\n\n activeItems.push({\n type: 'layout',\n priority: layout.priority || 0,\n layouts: [layout], // Keep full layout object for interrupt processing\n layoutId: layout.id\n });\n }\n }\n\n // If no active schedules, return default\n if (activeItems.length === 0) {\n return this.schedule.default ? [this.schedule.default] : [];\n }\n\n // Find maximum priority across all items (campaigns and layouts)\n const maxPriority = Math.max(...activeItems.map(item => item.priority));\n _log('[Schedule] Max priority:', maxPriority, 'from', activeItems.length, 'active items');\n\n // Collect all layouts from items with max priority\n const allLayouts = [];\n for (const item of activeItems) {\n if (item.priority === maxPriority) {\n _log('[Schedule] Including priority', item.priority, 'layouts:', item.layouts.map(l => l.file));\n // Add all layouts from this campaign or standalone layout\n allLayouts.push(...item.layouts);\n } else {\n _log('[Schedule] Skipping priority', item.priority, '< max', maxPriority);\n }\n }\n\n // Build layout metadata map (syncEvent, shareOfVoice, etc.)\n this._layoutMetadata.clear();\n for (const layout of allLayouts) {\n this._layoutMetadata.set(layout.file, {\n syncEvent: layout.syncEvent || false,\n shareOfVoice: layout.shareOfVoice || 0,\n scheduleid: layout.scheduleid,\n priority: layout.priority || 0,\n });\n }\n\n // Process interrupts if interrupt scheduler is available (skip for future time queries)\n if (!skipInterrupts && this.interruptScheduler) {\n const { normalLayouts, interruptLayouts } = this.interruptScheduler.separateLayouts(allLayouts);\n\n if (interruptLayouts.length > 0) {\n _log('[Schedule] Found', interruptLayouts.length, 'interrupt layouts with shareOfVoice');\n const processedLayouts = this.interruptScheduler.processInterrupts(normalLayouts, interruptLayouts);\n // Extract file IDs from processed layouts\n const result = processedLayouts.map(l => l.file);\n _log('[Schedule] Final layouts (with interrupts):', result);\n return result;\n }\n }\n\n // No interrupts, return layout files\n const result = allLayouts.map(l => l.file);\n _log('[Schedule] Final layouts:', result);\n return result;\n }\n\n /**\n * Check if schedule needs update (every minute)\n */\n shouldCheckSchedule(lastCheck) {\n if (!lastCheck) return true;\n const elapsed = Date.now() - lastCheck;\n return elapsed >= 60000; // 1 minute\n }\n\n /**\n * Check if layout can play based on maxPlaysPerHour with even distribution.\n *\n * Instead of allowing bursts (3 plays back-to-back then nothing for 50 min),\n * plays are distributed evenly across the hour:\n * maxPlaysPerHour=3 → minimum 20 min gap between plays\n * maxPlaysPerHour=6 → minimum 10 min gap between plays\n *\n * Two checks:\n * 1. Total plays in sliding 1-hour window < maxPlaysPerHour\n * 2. Time since last play >= (60 / maxPlaysPerHour) minutes\n *\n * @param {string} layoutId - Layout ID to check\n * @param {number} maxPlaysPerHour - Maximum plays allowed per hour (0 = unlimited)\n * @returns {boolean} True if layout can play, false if exceeded limit\n */\n canPlayLayout(layoutId, maxPlaysPerHour) {\n const history = this.playHistory.get(layoutId) || [];\n return canSimulatedPlay(history, maxPlaysPerHour, Date.now());\n }\n\n /**\n * Record that a layout was played\n * @param {string} layoutId - Layout ID that was played\n */\n recordPlay(layoutId) {\n if (!this.playHistory.has(layoutId)) {\n this.playHistory.set(layoutId, []);\n }\n\n const history = this.playHistory.get(layoutId);\n history.push(Date.now());\n\n // Clean up old entries (older than 1 hour)\n const oneHourAgo = Date.now() - (60 * 60 * 1000);\n const cleaned = history.filter(timestamp => timestamp > oneHourAgo);\n this.playHistory.set(layoutId, cleaned);\n\n log.info(`Recorded play for layout ${layoutId} (${cleaned.length} plays in last hour)`);\n }\n\n /**\n * Check if a layout file is a sync event (part of multi-display sync group)\n * @param {string} layoutFile - Layout file identifier (e.g., '123')\n * @returns {boolean}\n */\n isSyncEvent(layoutFile) {\n const meta = this._layoutMetadata.get(layoutFile);\n return meta?.syncEvent === true;\n }\n\n /**\n * Get metadata for a layout file (syncEvent, shareOfVoice, etc.)\n * @param {string} layoutFile - Layout file identifier\n * @returns {Object|null} Metadata or null if not found\n */\n getLayoutMetadata(layoutFile) {\n return this._layoutMetadata.get(layoutFile) || null;\n }\n\n /**\n * Get (or build) the deterministic schedule queue.\n *\n * Uses LCM-based even distribution to pre-calculate a repeating cycle where\n * each rate-limited layout plays at evenly spaced intervals and gaps are\n * filled by unlimited layouts and the CMS default.\n *\n * The queue is cached and only rebuilt when:\n * - The schedule changes (setSchedule)\n * - The active layout set changes (time boundary crossed)\n * - durations are updated\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {Set<string>} [options.dynamicLayouts] - Set of layout files with useDuration=0\n * @returns {{ queue: Array<{layoutId: string, duration: number}>, periodSeconds: number }}\n */\n getScheduleQueue(durations, options = {}) {\n const allLayouts = this.getAllLayoutsAtTime(new Date());\n const layoutSetKey = allLayouts.map(l => `${l.file}:${l.priority}:${l.maxPlaysPerHour}`).sort().join('|');\n\n // Return cached queue if the active layout set hasn't changed\n if (this._scheduleQueue && this._queueLayoutSet === layoutSetKey) {\n return this._scheduleQueue;\n }\n\n const result = buildScheduleQueue(allLayouts, durations, {\n defaultLayout: this.schedule?.default || null,\n defaultDuration: 60,\n dynamicLayouts: options.dynamicLayouts || new Set(),\n });\n\n const prevLayoutSet = this._queueLayoutSet;\n this._scheduleQueue = result;\n this._queueLayoutSet = layoutSetKey;\n\n // Position only resets when we get a genuinely new set of layouts.\n // Duration corrections rebuild the queue but don't change position.\n if (prevLayoutSet !== layoutSetKey) {\n this._queuePosition = 0;\n }\n\n if (result.queue.length > 0) {\n log.info(`[Schedule] Built queue: ${result.queue.length} entries, period ${result.periodSeconds}s (pos ${this._queuePosition})`);\n log.info(`[Schedule] Queue: ${result.queue.map(e => `${e.layoutId}(${e.duration}s)`).join(' → ')}`);\n }\n\n return result;\n }\n\n /**\n * Pop the next entry from the schedule queue.\n * Wraps around at the end (the LCM period guarantees the pattern repeats).\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {Set<string>} [options.dynamicLayouts] - Dynamic layout set\n * @returns {{ layoutId: string, duration: number } | null}\n */\n popNextFromQueue(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length === 0) return null;\n\n const entry = queue[this._queuePosition % queue.length];\n this._queuePosition = (this._queuePosition + 1) % queue.length;\n return entry;\n }\n\n /**\n * Peek at the next entry in the schedule queue without advancing.\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @returns {{ layoutId: string, duration: number } | null}\n */\n /**\n * Get current queue position.\n * @returns {number}\n */\n getQueuePosition() {\n return this._queuePosition;\n }\n\n /**\n * Rewind the queue by N positions (wraps around).\n * Used by advanceToPreviousLayout to go back in the schedule.\n * @param {number} steps - Number of positions to rewind\n * @param {Map<string, number>} durations\n * @param {Object} [options]\n * @returns {{ layoutId: string, duration: number } | null}\n */\n rewindQueue(steps, durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length === 0) return null;\n this._queuePosition = (this._queuePosition - steps + queue.length * steps) % queue.length;\n const entry = queue[this._queuePosition];\n this._queuePosition = (this._queuePosition + 1) % queue.length;\n return entry;\n }\n\n peekNextInQueue(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length === 0) return null;\n return queue[this._queuePosition % queue.length];\n }\n\n /**\n * Peek at the entry after the current one (two positions ahead).\n * Used for preloading.\n *\n * @param {Map<string, number>} durations\n * @param {Object} [options]\n * @returns {{ layoutId: string, duration: number } | null}\n */\n peekAfterNext(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length <= 1) return null;\n return queue[(this._queuePosition + 1) % queue.length];\n }\n\n /**\n * Public API to invalidate the cached schedule queue.\n * Called by PlayerCore when layout durations are corrected at runtime.\n */\n invalidateQueue() {\n this._invalidateQueue();\n }\n\n /**\n * Invalidate the cached queue (called on schedule change, time boundaries, etc.)\n */\n _invalidateQueue() {\n this._scheduleQueue = null;\n // Keep _queueLayoutSet so getScheduleQueue() can detect whether the\n // layout set actually changed. Position only resets on new layouts.\n // Resetting on every setSchedule() caused the player to replay position 0\n // endlessly because collections happen more often than layout cycles.\n }\n\n /**\n * Check if any current layouts are sync events\n * @returns {boolean}\n */\n hasSyncEvents() {\n for (const meta of this._layoutMetadata.values()) {\n if (meta.syncEvent) return true;\n }\n return false;\n }\n\n /**\n * Get currently active actions (within their time window)\n * @returns {Array} Active action objects\n */\n getActiveActions() {\n if (!this.schedule?.actions) return [];\n\n const now = new Date();\n return this.schedule.actions.filter(action => this.isTimeActive(action, now));\n }\n\n /**\n * Get scheduled commands\n * @returns {Array} Command objects\n */\n getCommands() {\n return this.schedule?.commands || [];\n }\n\n /**\n * Find action by trigger code\n * @param {string} triggerCode - The trigger code to match\n * @returns {Object|null} Matching action or null\n */\n findActionByTrigger(triggerCode) {\n const activeActions = this.getActiveActions();\n return activeActions.find(a => a.triggerCode === triggerCode) || null;\n }\n\n /**\n * Clear play history (useful for testing or reset)\n */\n clearPlayHistory() {\n this.playHistory.clear();\n log.info('Play history cleared');\n }\n\n /**\n * Set player's current GPS location (from Geolocation API or XMR command)\n * @param {number} latitude\n * @param {number} longitude\n */\n setLocation(latitude, longitude) {\n this.playerLocation = { latitude, longitude };\n log.info(`Location set: ${latitude}, ${longitude}`);\n }\n\n /**\n * Set display properties from CMS (custom fields for criteria evaluation)\n * @param {Object} properties - Key-value map of display properties\n */\n setDisplayProperties(properties) {\n this.displayProperties = properties || {};\n }\n\n /**\n * Check if player is within a geo-fence.\n * geoLocation format from CMS: \"lat,lng\" (point + default radius)\n * or \"lat1,lng1;lat2,lng2;...\" (polygon — future)\n *\n * Default radius: 500 meters (Xibo default for point geofences)\n *\n * @param {string} geoLocation - Geo-fence specification from CMS\n * @param {number} [defaultRadius=500] - Default radius in meters for point geofences\n * @returns {boolean} True if within geofence or no location available\n */\n isWithinGeoFence(geoLocation, defaultRadius = 500) {\n if (!this.playerLocation) {\n // No location available — be permissive, show the content\n log.debug('No player location, skipping geofence check');\n return true;\n }\n\n if (!geoLocation) return true;\n\n // Parse \"lat,lng\" format\n const parts = geoLocation.split(',').map(s => parseFloat(s.trim()));\n if (parts.length < 2 || isNaN(parts[0]) || isNaN(parts[1])) {\n log.warn('Invalid geoLocation format:', geoLocation);\n return true; // Invalid format, be permissive\n }\n\n const fenceLat = parts[0];\n const fenceLng = parts[1];\n const radius = parts[2] || defaultRadius; // Optional 3rd param: radius in meters\n\n const distance = this.haversineDistance(\n this.playerLocation.latitude, this.playerLocation.longitude,\n fenceLat, fenceLng\n );\n\n const within = distance <= radius;\n log.info(`Geofence: ${distance.toFixed(0)}m from (${fenceLat},${fenceLng}), radius ${radius}m → ${within ? 'WITHIN' : 'OUTSIDE'}`);\n return within;\n }\n\n /**\n * Haversine formula: calculate distance between two GPS coordinates\n * @param {number} lat1 - Latitude 1 (degrees)\n * @param {number} lon1 - Longitude 1 (degrees)\n * @param {number} lat2 - Latitude 2 (degrees)\n * @param {number} lon2 - Longitude 2 (degrees)\n * @returns {number} Distance in meters\n */\n haversineDistance(lat1, lon1, lat2, lon2) {\n const R = 6371000; // Earth radius in meters\n const toRad = deg => deg * Math.PI / 180;\n\n const dLat = toRad(lat2 - lat1);\n const dLon = toRad(lon2 - lon1);\n\n const a = Math.sin(dLat / 2) ** 2 +\n Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *\n Math.sin(dLon / 2) ** 2;\n\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n }\n}\n\nexport const scheduleManager = new ScheduleManager();\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Interrupt Layout Scheduler (Share of Voice)\n *\n * Implements the shareOfVoice algorithm from upstream electron-player.\n * Interrupts are layouts that must play for a percentage of each hour.\n *\n * Algorithm:\n * 1. Separate interrupts from normal layouts\n * 2. Calculate how many times each interrupt must play per hour\n * 3. Fill remaining time with normal layouts\n * 4. Interleave interrupts and normal layouts evenly\n *\n * Based on: electron-player/src/main/common/scheduleManager.ts (lines 181-321)\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst logger = createLogger('schedule:interrupts');\n\n/**\n * Interrupt Scheduler\n * Handles shareOfVoice layouts that must play for a percentage of each hour\n */\nexport class InterruptScheduler {\n constructor() {\n // Track committed duration per interrupt layout\n this.interruptCommittedDurations = new Map(); // layoutId -> seconds\n }\n\n /**\n * Check if a layout is an interrupt (has shareOfVoice > 0)\n * @param {Object} layout - Layout object with shareOfVoice property\n * @returns {boolean} True if layout is an interrupt\n */\n isInterrupt(layout) {\n return !!(layout.shareOfVoice && layout.shareOfVoice > 0);\n }\n\n /**\n * Reset committed duration tracking (call this every hour)\n */\n resetCommittedDurations() {\n this.interruptCommittedDurations.clear();\n logger.debug('Reset interrupt committed durations');\n }\n\n /**\n * Get committed duration for a layout\n * @param {string} layoutId - Layout ID\n * @returns {number} Committed duration in seconds\n */\n getCommittedDuration(layoutId) {\n return this.interruptCommittedDurations.get(layoutId) || 0;\n }\n\n /**\n * Add committed duration for a layout\n * @param {string} layoutId - Layout ID\n * @param {number} duration - Duration to add in seconds\n */\n addCommittedDuration(layoutId, duration) {\n const current = this.getCommittedDuration(layoutId);\n this.interruptCommittedDurations.set(layoutId, current + duration);\n }\n\n /**\n * Check if interrupt layout has satisfied its shareOfVoice requirement\n * @param {Object} layout - Layout with shareOfVoice and duration\n * @returns {boolean} True if satisfied\n */\n isInterruptDurationSatisfied(layout) {\n if (!layout.shareOfVoice) {\n return true; // Not an interrupt\n }\n\n const layoutId = layout.id || layout.file;\n const requiredSeconds = (layout.shareOfVoice / 100) * 3600; // shareOfVoice is percentage\n const committedSeconds = this.getCommittedDuration(layoutId);\n\n return committedSeconds >= requiredSeconds;\n }\n\n /**\n * Calculate how many seconds this interrupt needs to play per hour\n * @param {Object} layout - Layout with shareOfVoice\n * @returns {number} Required seconds per hour\n */\n getRequiredSeconds(layout) {\n if (!layout.shareOfVoice) {\n return 0;\n }\n return (layout.shareOfVoice / 100) * 3600;\n }\n\n /**\n * Process interrupt layouts and combine with normal layouts\n * Implements the shareOfVoice algorithm from upstream\n *\n * @param {Array} normalLayouts - Normal scheduled layouts\n * @param {Array} interruptLayouts - Interrupt layouts with shareOfVoice\n * @returns {Array} Combined layout loop for the hour\n */\n processInterrupts(normalLayouts, interruptLayouts) {\n if (!interruptLayouts || interruptLayouts.length === 0) {\n logger.debug('No interrupt layouts, returning normal layouts');\n return normalLayouts;\n }\n\n if (!normalLayouts || normalLayouts.length === 0) {\n logger.warn('No normal layouts available, interrupts will fill entire hour');\n return this.fillHourWithInterrupts(interruptLayouts);\n }\n\n logger.info(`Processing ${interruptLayouts.length} interrupt layouts with ${normalLayouts.length} normal layouts`);\n\n // Reset committed durations for this calculation\n for (const layout of interruptLayouts) {\n const layoutId = layout.id || layout.file;\n this.interruptCommittedDurations.set(layoutId, 0);\n }\n\n const resolvedInterruptLayouts = [];\n let interruptSecondsInHour = 0;\n let index = 0;\n let satisfied = false;\n\n // Step 1: Build interrupt loop by cycling through interrupts until all are satisfied\n while (!satisfied) {\n // Gone all the way around? Check if all satisfied\n if (index >= interruptLayouts.length) {\n index = 0;\n\n // Check if all interrupts are satisfied\n let allSatisfied = true;\n for (const layout of interruptLayouts) {\n if (!this.isInterruptDurationSatisfied(layout)) {\n allSatisfied = false;\n break;\n }\n }\n\n if (allSatisfied) {\n satisfied = true;\n break;\n }\n }\n\n const currentInterrupt = interruptLayouts[index];\n\n // If this interrupt is not satisfied, add it to the loop\n if (!this.isInterruptDurationSatisfied(currentInterrupt)) {\n const layoutId = currentInterrupt.id || currentInterrupt.file;\n this.addCommittedDuration(layoutId, currentInterrupt.duration);\n interruptSecondsInHour += currentInterrupt.duration;\n resolvedInterruptLayouts.push(currentInterrupt);\n }\n\n index++;\n }\n\n logger.debug(`Resolved ${resolvedInterruptLayouts.length} interrupt plays (${interruptSecondsInHour}s total)`);\n\n // Step 2: If interrupts fill the entire hour, return only interrupts\n if (interruptSecondsInHour >= 3600) {\n logger.info('Interrupts fill entire hour (>= 3600s), no room for normal layouts');\n return resolvedInterruptLayouts;\n }\n\n // Step 3: Fill remaining time with normal layouts\n const normalSecondsInHour = 3600 - interruptSecondsInHour;\n const resolvedNormalLayouts = this.fillTimeWithLayouts(normalLayouts, normalSecondsInHour);\n\n logger.debug(`Resolved ${resolvedNormalLayouts.length} normal plays (${normalSecondsInHour}s target)`);\n\n // Step 4: Interleave interrupts and normal layouts\n const loop = this.interleaveLayouts(resolvedNormalLayouts, resolvedInterruptLayouts);\n\n logger.info(`Final loop: ${loop.length} layouts (${resolvedNormalLayouts.length} normal + ${resolvedInterruptLayouts.length} interrupts)`);\n\n return loop;\n }\n\n /**\n * Fill time with layouts by repeating them until duration is reached\n * @param {Array} layouts - Layouts to use\n * @param {number} targetSeconds - Target duration in seconds\n * @returns {Array} Resolved layout array\n */\n fillTimeWithLayouts(layouts, targetSeconds) {\n const resolved = [];\n let remainingSeconds = targetSeconds;\n let index = 0;\n\n while (remainingSeconds > 0) {\n if (index >= layouts.length) {\n index = 0; // Loop back\n }\n\n const layout = layouts[index];\n resolved.push(layout);\n remainingSeconds -= layout.duration;\n index++;\n }\n\n return resolved;\n }\n\n /**\n * Fill entire hour with interrupt layouts only\n * @param {Array} interruptLayouts - Interrupt layouts\n * @returns {Array} Layout loop\n */\n fillHourWithInterrupts(interruptLayouts) {\n return this.fillTimeWithLayouts(interruptLayouts, 3600);\n }\n\n /**\n * Interleave normal and interrupt layouts evenly\n * Based on upstream algorithm (scheduleManager.ts lines 268-316)\n *\n * @param {Array} normalLayouts - Normal layouts\n * @param {Array} interruptLayouts - Interrupt layouts\n * @returns {Array} Interleaved layout array\n */\n interleaveLayouts(normalLayouts, interruptLayouts) {\n const loop = [];\n const pickCount = Math.max(normalLayouts.length, interruptLayouts.length);\n\n // Calculate pick intervals\n // Normal: ceiling (pick more often from normal)\n // Interrupt: floor (pick less often from interrupts)\n const normalPick = Math.ceil(1.0 * pickCount / normalLayouts.length);\n const interruptPick = Math.floor(1.0 * pickCount / interruptLayouts.length);\n\n logger.debug(`Interleaving: pickCount=${pickCount}, normalPick=${normalPick}, interruptPick=${interruptPick}`);\n\n let normalIndex = 0;\n let interruptIndex = 0;\n let totalSecondsAllocated = 0;\n\n for (let i = 0; i < pickCount; i++) {\n // Pick from normal list\n if (i % normalPick === 0) {\n // Allow wrapping around\n if (normalIndex >= normalLayouts.length) {\n normalIndex = 0;\n }\n loop.push(normalLayouts[normalIndex]);\n totalSecondsAllocated += normalLayouts[normalIndex].duration;\n normalIndex++;\n }\n\n // Pick from interrupt list (only if we haven't picked them all yet)\n if (i % interruptPick === 0 && interruptIndex < interruptLayouts.length) {\n loop.push(interruptLayouts[interruptIndex]);\n totalSecondsAllocated += interruptLayouts[interruptIndex].duration;\n interruptIndex++;\n }\n }\n\n // Fill remaining time with normal layouts (due to ceiling/floor rounding)\n while (totalSecondsAllocated < 3600) {\n if (normalIndex >= normalLayouts.length) {\n normalIndex = 0;\n }\n loop.push(normalLayouts[normalIndex]);\n totalSecondsAllocated += normalLayouts[normalIndex].duration;\n normalIndex++;\n }\n\n logger.debug(`Interleaved ${loop.length} layouts, total duration: ${totalSecondsAllocated}s`);\n\n return loop;\n }\n\n /**\n * Separate layouts into normal and interrupt arrays\n * @param {Array} layouts - All layouts\n * @returns {Object} { normalLayouts, interruptLayouts }\n */\n separateLayouts(layouts) {\n const normalLayouts = [];\n const interruptLayouts = [];\n\n for (const layout of layouts) {\n if (this.isInterrupt(layout)) {\n interruptLayouts.push(layout);\n } else {\n normalLayouts.push(layout);\n }\n }\n\n return { normalLayouts, interruptLayouts };\n }\n}\n\n// Export singleton instance for convenience\nexport const interruptScheduler = new InterruptScheduler();\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Overlay Layout Scheduler\n *\n * Manages overlay layouts that appear on top of main layouts.\n * Based on upstream electron-player implementation.\n *\n * Overlays:\n * - Render on top of main layout (higher z-index)\n * - Have scheduled start/end times\n * - Support priority ordering (multiple overlays)\n * - Support criteria-based display (future)\n * - Support geofencing (future)\n *\n * Reference: upstream_players/electron-player/src/main/xmds/response/schedule/events/overlayLayout.ts\n */\n\nimport { createLogger } from '@xiboplayer/utils';\nimport { evaluateCriteria } from './criteria.js';\n\nconst logger = createLogger('schedule:overlays');\n\n/**\n * Overlay Scheduler\n * Handles overlay layouts that display on top of main layouts\n */\nexport class OverlayScheduler {\n constructor() {\n this.overlays = [];\n this.displayProperties = {};\n this.scheduleManager = null; // Reference to ScheduleManager for geo checks\n logger.debug('OverlayScheduler initialized');\n }\n\n /**\n * Set reference to ScheduleManager for geo-fence checks\n * @param {ScheduleManager} scheduleManager\n */\n setScheduleManager(scheduleManager) {\n this.scheduleManager = scheduleManager;\n }\n\n /**\n * Set display properties for criteria evaluation\n * @param {Object} properties\n */\n setDisplayProperties(properties) {\n this.displayProperties = properties || {};\n }\n\n /**\n * Update overlays from XMDS Schedule response\n * @param {Array} overlays - Overlay objects from XMDS\n */\n setOverlays(overlays) {\n this.overlays = overlays || [];\n logger.info(`Loaded ${this.overlays.length} overlay(s)`);\n }\n\n /**\n * Get currently active overlays\n * @returns {Array} Active overlay objects sorted by priority (highest first)\n */\n getCurrentOverlays() {\n if (!this.overlays || this.overlays.length === 0) {\n return [];\n }\n\n const now = new Date();\n const activeOverlays = [];\n\n for (const overlay of this.overlays) {\n // Check time window\n if (!this.isTimeActive(overlay, now)) {\n logger.debug(`Overlay ${overlay.file} not in time window`);\n continue;\n }\n\n // Check geo-awareness\n if (overlay.isGeoAware && overlay.geoLocation) {\n if (this.scheduleManager && !this.scheduleManager.isWithinGeoFence(overlay.geoLocation)) {\n logger.debug(`Overlay ${overlay.file} filtered by geofence`);\n continue;\n }\n }\n\n // Check criteria conditions\n if (overlay.criteria && overlay.criteria.length > 0) {\n if (!evaluateCriteria(overlay.criteria, { now, displayProperties: this.displayProperties })) {\n logger.debug(`Overlay ${overlay.file} filtered by criteria`);\n continue;\n }\n }\n\n activeOverlays.push(overlay);\n }\n\n // Sort by priority (highest first)\n activeOverlays.sort((a, b) => {\n const priorityA = a.priority || 0;\n const priorityB = b.priority || 0;\n return priorityB - priorityA;\n });\n\n if (activeOverlays.length > 0) {\n logger.info(`Active overlays: ${activeOverlays.length}`);\n }\n\n return activeOverlays;\n }\n\n /**\n * Check if overlay is within its time window.\n * Delegates to ScheduleManager.isTimeActive() which handles both\n * simple date ranges and recurring schedule dayparting.\n * Falls back to basic date-range check if no scheduleManager is set.\n *\n * @param {Object} overlay - Overlay object\n * @param {Date} now - Current time\n * @returns {boolean}\n */\n isTimeActive(overlay, now) {\n if (this.scheduleManager) {\n // Normalize fromDt → fromdt for ScheduleManager compatibility\n const normalized = { ...overlay };\n if (!normalized.fromdt && normalized.fromDt) normalized.fromdt = normalized.fromDt;\n if (!normalized.todt && normalized.toDt) normalized.todt = normalized.toDt;\n return this.scheduleManager.isTimeActive(normalized, now);\n }\n\n // Fallback: basic date-range check (no scheduleManager available)\n const from = (overlay.fromdt || overlay.fromDt) ? new Date(overlay.fromdt || overlay.fromDt) : null;\n const to = (overlay.todt || overlay.toDt) ? new Date(overlay.todt || overlay.toDt) : null;\n if (from && now < from) return false;\n if (to && now > to) return false;\n return true;\n }\n\n /**\n * Check if overlay schedule needs update (every minute)\n * @param {number} lastCheck - Last check timestamp\n * @returns {boolean}\n */\n shouldCheckOverlays(lastCheck) {\n if (!lastCheck) return true;\n const elapsed = Date.now() - lastCheck;\n return elapsed >= 60000; // 1 minute\n }\n\n /**\n * Get overlay by file ID\n * @param {number} fileId - Layout file ID\n * @returns {Object|null}\n */\n getOverlayByFile(fileId) {\n return this.overlays.find(o => o.file === fileId) || null;\n }\n\n /**\n * Clear all overlays\n */\n clear() {\n this.overlays = [];\n logger.debug('Cleared all overlays');\n }\n\n}\n\nexport const overlayScheduler = new OverlayScheduler();\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/schedule - Campaign scheduling and advanced features\n// Basic scheduling, interrupts, overlays, and dayparting\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\n\n/**\n * Core schedule manager for basic scheduling and dayparting\n * @module @xiboplayer/schedule\n */\nexport { ScheduleManager, scheduleManager } from './schedule.js';\n\n/**\n * Interrupt scheduler for shareOfVoice layouts\n * @module @xiboplayer/schedule/interrupts\n */\nexport { InterruptScheduler } from './interrupts.js';\n\n/**\n * Overlay layout scheduler\n * @module @xiboplayer/schedule/overlays\n */\nexport { OverlayScheduler } from './overlays.js';\n\n/**\n * Offline timeline calculator — duration parser + timeline simulator\n * @module @xiboplayer/schedule/timeline\n */\nexport { calculateTimeline, parseLayoutDuration, parseLayoutFile, buildScheduleQueue } from './timeline.js';\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{v as e}from"./src-BgXR6vhE.js";typeof self<`u`&&self.registration?.scope&&new URL(self.registration.scope).pathname.replace(/\/$/,``);function t(e,t){let n=new Set,r=e.matchAll(/<media[^>]+fileId="(\d+)"/g);for(let e of r)n.add(e[1]);let i=e.matchAll(/<media\s+([^>]+)>/g);for(let e of i){let t=e[1];if(!t.includes(`fileId=`)){let e=t.match(/\bid="(\d+)"/);e&&n.add(e[1])}}let a=e.matchAll(/<layout[^>]+background="(\d+)"/g);for(let e of a)n.add(e[1]);return t&&t.debug(`extractMediaIdsFromXlf: found ${n.size} IDs: ${[...n].join(`, `)} (XLF ${e.length} bytes)`),n}function n(t){t||=e(`ChunkConfig`);let n=typeof navigator<`u`&&navigator.deviceMemory||null,r=4;if(n)r=n,t.info(`Detected device memory:`,n,`GB`);else if(typeof navigator<`u`){let e=navigator.userAgent.toLowerCase();e.includes(`raspberry pi`)||e.includes(`armv6`)?(r=.5,t.info(`Detected Pi Zero (512 MB RAM estimated)`)):e.includes(`armv7`)?(r=1,t.info(`Detected ARM device (1 GB RAM estimated)`)):t.info(`Using default RAM estimate:`,r,`GB`)}let i,a,o,s;return r<=.5?(i=10*1024*1024,a=25,o=25*1024*1024,s=1,t.info(`Low-memory config: 10 MB chunks, 25 MB cache, 1 concurrent download`)):r<=1?(i=20*1024*1024,a=50,o=50*1024*1024,s=2,t.info(`1GB-RAM config: 20 MB chunks, 50 MB cache, 2 concurrent downloads`)):r<=2?(i=30*1024*1024,a=100,o=75*1024*1024,s=2,t.info(`2GB-RAM config: 30 MB chunks, 100 MB cache, 2 concurrent downloads`)):r<=4?(i=50*1024*1024,a=200,o=100*1024*1024,s=4,t.info(`4GB-RAM config: 50 MB chunks, 200 MB cache, 4 concurrent downloads`)):(i=100*1024*1024,a=500,o=200*1024*1024,s=4,t.info(`High-RAM config: 100 MB chunks, 500 MB cache, 4 concurrent downloads`)),{chunkSize:i,blobCacheSize:a,threshold:o,concurrency:s}}export{n as calculateChunkConfig,t as extractMediaIdsFromXlf};
2
- //# sourceMappingURL=src-mns1H4Qq.js.map
1
+ import{v as e}from"./src-DGSLOm9k.js";typeof self<`u`&&self.registration?.scope&&new URL(self.registration.scope).pathname.replace(/\/$/,``);function t(e,t){let n=new Set,r=e.matchAll(/<media[^>]+fileId="(\d+)"/g);for(let e of r)n.add(e[1]);let i=e.matchAll(/<media\s+([^>]+)>/g);for(let e of i){let t=e[1];if(!t.includes(`fileId=`)){let e=t.match(/\bid="(\d+)"/);e&&n.add(e[1])}}let a=e.matchAll(/<layout[^>]+background="(\d+)"/g);for(let e of a)n.add(e[1]);return t&&t.debug(`extractMediaIdsFromXlf: found ${n.size} IDs: ${[...n].join(`, `)} (XLF ${e.length} bytes)`),n}function n(t){t||=e(`ChunkConfig`);let n=typeof navigator<`u`&&navigator.deviceMemory||null,r=4;if(n)r=n,t.info(`Detected device memory:`,n,`GB`);else if(typeof navigator<`u`){let e=navigator.userAgent.toLowerCase();e.includes(`raspberry pi`)||e.includes(`armv6`)?(r=.5,t.info(`Detected Pi Zero (512 MB RAM estimated)`)):e.includes(`armv7`)?(r=1,t.info(`Detected ARM device (1 GB RAM estimated)`)):t.info(`Using default RAM estimate:`,r,`GB`)}let i,a,o,s;return r<=.5?(i=10*1024*1024,a=25,o=25*1024*1024,s=1,t.info(`Low-memory config: 10 MB chunks, 25 MB cache, 1 concurrent download`)):r<=1?(i=20*1024*1024,a=50,o=50*1024*1024,s=2,t.info(`1GB-RAM config: 20 MB chunks, 50 MB cache, 2 concurrent downloads`)):r<=2?(i=30*1024*1024,a=100,o=75*1024*1024,s=2,t.info(`2GB-RAM config: 30 MB chunks, 100 MB cache, 2 concurrent downloads`)):r<=4?(i=50*1024*1024,a=200,o=100*1024*1024,s=4,t.info(`4GB-RAM config: 50 MB chunks, 200 MB cache, 4 concurrent downloads`)):(i=100*1024*1024,a=500,o=200*1024*1024,s=4,t.info(`High-RAM config: 100 MB chunks, 500 MB cache, 4 concurrent downloads`)),{chunkSize:i,blobCacheSize:a,threshold:o,concurrency:s}}export{n as calculateChunkConfig,t as extractMediaIdsFromXlf};
2
+ //# sourceMappingURL=src-DgZHc2Af.js.map