@xiboplayer/pwa 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/assets/cache-proxy-Cx4Z8XMC.js +2 -0
- package/dist/assets/cache-proxy-Cx4Z8XMC.js.map +1 -0
- package/dist/assets/cms-api-kzy_Sw-u.js +2 -0
- package/dist/assets/cms-api-kzy_Sw-u.js.map +1 -0
- package/dist/assets/html2canvas.esm-CBrSDip1.js +23 -0
- package/dist/assets/html2canvas.esm-CBrSDip1.js.map +1 -0
- package/dist/assets/index-BEhNaWZ4.js +2 -0
- package/dist/assets/index-BEhNaWZ4.js.map +1 -0
- package/dist/assets/index-BPNsrSEv.js +2 -0
- package/dist/assets/index-BPNsrSEv.js.map +1 -0
- package/dist/assets/index-BY2j60YZ.js +2 -0
- package/dist/assets/index-BY2j60YZ.js.map +1 -0
- package/dist/assets/index-CTmjUTVM.js +8 -0
- package/dist/assets/index-CTmjUTVM.js.map +1 -0
- package/dist/assets/index-_q2HbdAU.js +2 -0
- package/dist/assets/index-_q2HbdAU.js.map +1 -0
- package/dist/assets/index-_uzldOpz.js +16 -0
- package/dist/assets/index-_uzldOpz.js.map +1 -0
- package/dist/assets/main-C4ABDfkq.js +27 -0
- package/dist/assets/main-C4ABDfkq.js.map +1 -0
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js +2 -0
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js.map +1 -0
- package/dist/assets/pdf-BnPRJEQ6.js +13 -0
- package/dist/assets/pdf-BnPRJEQ6.js.map +1 -0
- package/dist/assets/setup-rqZh5qYs.js +2 -0
- package/dist/assets/setup-rqZh5qYs.js.map +1 -0
- package/dist/index.html +130 -0
- package/dist/setup.html +371 -0
- package/dist/sw-pwa.js +2 -0
- package/dist/sw-pwa.js.map +1 -0
- package/dist/sw-utils.js +210 -0
- package/dist/sw.test.js +271 -0
- package/package.json +39 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import"./cms-api-kzy_Sw-u.js";import{E as i}from"./cache-proxy-Cx4Z8XMC.js";class a extends i{constructor(){super(),this.settings={collectInterval:300,displayName:"Unknown Display",sizeX:1920,sizeY:1080,statsEnabled:!1,aggregationLevel:"Individual",logLevel:"error",xmrNetworkAddress:null,xmrWebSocketAddress:null,xmrCmsKey:null,preventSleep:!0,embeddedServerPort:9696,screenshotInterval:120,downloadStartWindow:null,downloadEndWindow:null,licenceCode:null,isSspEnabled:!1}}applySettings(e){if(!e)return console.warn("[DisplaySettings] No settings provided"),{changed:[],settings:this.settings};const t=[],s=this.settings.collectInterval;return this.settings.collectInterval=this.parseCollectInterval(e.collectInterval||e.CollectInterval),this.settings.displayName=e.displayName||e.DisplayName||this.settings.displayName,this.settings.sizeX=parseInt(e.sizeX||e.SizeX||this.settings.sizeX),this.settings.sizeY=parseInt(e.sizeY||e.SizeY||this.settings.sizeY),this.settings.statsEnabled=this.parseBoolean(e.statsEnabled||e.StatsEnabled),this.settings.aggregationLevel=e.aggregationLevel||e.AggregationLevel||this.settings.aggregationLevel,this.settings.logLevel=e.logLevel||e.LogLevel||this.settings.logLevel,this.settings.xmrNetworkAddress=e.xmrNetworkAddress||e.XmrNetworkAddress||this.settings.xmrNetworkAddress,this.settings.xmrWebSocketAddress=e.xmrWebSocketAddress||e.XmrWebSocketAddress||this.settings.xmrWebSocketAddress,this.settings.xmrCmsKey=e.xmrCmsKey||e.XmrCmsKey||this.settings.xmrCmsKey,this.settings.preventSleep=this.parseBoolean(e.preventSleep||e.PreventSleep,!0),this.settings.embeddedServerPort=parseInt(e.embeddedServerPort||e.EmbeddedServerPort||this.settings.embeddedServerPort),this.settings.screenshotInterval=parseInt(e.screenshotInterval||e.ScreenshotInterval||this.settings.screenshotInterval),this.settings.downloadStartWindow=e.downloadStartWindow||e.DownloadStartWindow||this.settings.downloadStartWindow,this.settings.downloadEndWindow=e.downloadEndWindow||e.DownloadEndWindow||this.settings.downloadEndWindow,this.settings.licenceCode=e.licenceCode||e.LicenceCode||this.settings.licenceCode,this.settings.isSspEnabled=this.parseBoolean(e.isAdspaceEnabled||e.IsAdspaceEnabled),s!==this.settings.collectInterval&&(t.push("collectInterval"),this.emit("interval-changed",this.settings.collectInterval)),this.emit("settings-applied",this.settings,t),console.log("[DisplaySettings] Applied settings:",{collectInterval:this.settings.collectInterval,displayName:this.settings.displayName,statsEnabled:this.settings.statsEnabled,changes:t}),{changed:t,settings:this.settings}}parseCollectInterval(e){const t=parseInt(e,10);return isNaN(t)||t<60?300:t>86400?86400:t}parseBoolean(e,t=!1){return e===!0||e===!1?e:e==="1"||e===1?!0:e==="0"||e===0?!1:t}getCollectInterval(){return this.settings.collectInterval}getDisplayName(){return this.settings.displayName}getDisplaySize(){return{width:this.settings.sizeX,height:this.settings.sizeY}}isStatsEnabled(){return this.settings.statsEnabled}getAllSettings(){return{...this.settings}}getSetting(e,t=null){return this.settings[e]!==void 0?this.settings[e]:t}isInDownloadWindow(){if(!this.settings.downloadStartWindow||!this.settings.downloadEndWindow)return!0;try{const e=new Date,t=e.getHours()*60+e.getMinutes(),s=this.parseTimeWindow(this.settings.downloadStartWindow),n=this.parseTimeWindow(this.settings.downloadEndWindow);return s>n?t>=s||t<n:t>=s&&t<n}catch(e){return console.warn("[DisplaySettings] Failed to parse download window:",e),!0}}parseTimeWindow(e){if(!e||typeof e!="string")throw new Error("Invalid time window format");const t=e.split(":");if(t.length!==2)throw new Error("Invalid time window format (expected HH:MM)");const s=parseInt(t[0],10),n=parseInt(t[1],10);if(isNaN(s)||isNaN(n)||s<0||s>23||n<0||n>59)throw new Error("Invalid time window values");return s*60+n}getNextDownloadWindow(){if(!this.settings.downloadStartWindow||!this.settings.downloadEndWindow)return null;try{const e=new Date,t=e.getHours()*60+e.getMinutes(),s=this.parseTimeWindow(this.settings.downloadStartWindow),n=new Date(e);return t<s||n.setDate(n.getDate()+1),n.setHours(Math.floor(s/60),s%60,0,0),n}catch(e){return console.warn("[DisplaySettings] Failed to calculate next download window:",e),null}}shouldTakeScreenshot(e){return e?(Date.now()-e.getTime())/1e3>=this.settings.screenshotInterval:!0}}export{a as DisplaySettings};
|
|
2
|
+
//# sourceMappingURL=index-BEhNaWZ4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-BEhNaWZ4.js","sources":["../../node_modules/.pnpm/@xiboplayer+settings@0.1.0/node_modules/@xiboplayer/settings/src/settings.js"],"sourcesContent":["/**\n * DisplaySettings - CMS display settings management\n *\n * Parses and applies configuration from RegisterDisplay response.\n * Based on upstream electron-player implementation.\n *\n * Architecture:\n * ┌─────────────────────────────────────────────────────┐\n * │ PlayerCore │\n * │ - Receives RegisterDisplay response │\n * │ - Passes to DisplaySettings.applySettings() │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ DisplaySettings (this module) │\n * │ - Parse all CMS settings │\n * │ - Validate and normalize values │\n * │ - Apply collection interval │\n * │ - Check download windows │\n * │ - Handle screenshot requests │\n * │ - Emit events on changes │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ Platform Layer (PWA/Electron/Mobile) │\n * │ - Listen for setting change events │\n * │ - Update UI with display name │\n * │ - Handle screenshot requests │\n * │ - Respect download windows │\n * └─────────────────────────────────────────────────────┘\n *\n * Usage:\n * const settings = new DisplaySettings();\n * settings.applySettings(regResult.settings);\n *\n * // Get settings\n * const collectInterval = settings.getCollectInterval();\n * const canDownload = settings.isInDownloadWindow();\n *\n * // Listen for changes\n * settings.on('interval-changed', (newInterval) => { ... });\n */\n\nimport { EventEmitter } from '@xiboplayer/utils';\n\nexport class DisplaySettings extends EventEmitter {\n constructor() {\n super();\n\n // Current settings (with defaults)\n this.settings = {\n // Collection\n collectInterval: 300, // seconds (5 minutes default)\n\n // Display info\n displayName: 'Unknown Display',\n sizeX: 1920,\n sizeY: 1080,\n\n // Stats\n statsEnabled: false,\n aggregationLevel: 'Individual', // or 'Aggregate'\n\n // Logging\n logLevel: 'error', // 'error', 'audit', 'info', 'debug'\n\n // XMR\n xmrNetworkAddress: null,\n xmrWebSocketAddress: null,\n xmrCmsKey: null,\n\n // Features\n preventSleep: true,\n embeddedServerPort: 9696,\n screenshotInterval: 120, // seconds\n\n // Download windows\n downloadStartWindow: null,\n downloadEndWindow: null,\n\n // License\n licenceCode: null,\n\n // SSP (ad space)\n isSspEnabled: false,\n };\n }\n\n /**\n * Apply settings from RegisterDisplay response\n * @param {Object} settings - Raw settings from CMS\n * @returns {Object} Applied settings with changes\n */\n applySettings(settings) {\n if (!settings) {\n console.warn('[DisplaySettings] No settings provided');\n return { changed: [], settings: this.settings };\n }\n\n const changes = [];\n const oldInterval = this.settings.collectInterval;\n\n // Parse all settings with defaults\n // Handle both lowercase and CamelCase (uppercase first letter)\n this.settings.collectInterval = this.parseCollectInterval(settings.collectInterval || settings.CollectInterval);\n this.settings.displayName = settings.displayName || settings.DisplayName || this.settings.displayName;\n this.settings.sizeX = parseInt(settings.sizeX || settings.SizeX || this.settings.sizeX);\n this.settings.sizeY = parseInt(settings.sizeY || settings.SizeY || this.settings.sizeY);\n\n // Stats\n this.settings.statsEnabled = this.parseBoolean(settings.statsEnabled || settings.StatsEnabled);\n this.settings.aggregationLevel = settings.aggregationLevel || settings.AggregationLevel || this.settings.aggregationLevel;\n\n // Logging\n this.settings.logLevel = settings.logLevel || settings.LogLevel || this.settings.logLevel;\n\n // XMR\n this.settings.xmrNetworkAddress = settings.xmrNetworkAddress || settings.XmrNetworkAddress || this.settings.xmrNetworkAddress;\n this.settings.xmrWebSocketAddress = settings.xmrWebSocketAddress || settings.XmrWebSocketAddress || this.settings.xmrWebSocketAddress;\n this.settings.xmrCmsKey = settings.xmrCmsKey || settings.XmrCmsKey || this.settings.xmrCmsKey;\n\n // Features\n this.settings.preventSleep = this.parseBoolean(settings.preventSleep || settings.PreventSleep, true);\n this.settings.embeddedServerPort = parseInt(settings.embeddedServerPort || settings.EmbeddedServerPort || this.settings.embeddedServerPort);\n this.settings.screenshotInterval = parseInt(settings.screenshotInterval || settings.ScreenshotInterval || this.settings.screenshotInterval);\n\n // Download windows\n this.settings.downloadStartWindow = settings.downloadStartWindow || settings.DownloadStartWindow || this.settings.downloadStartWindow;\n this.settings.downloadEndWindow = settings.downloadEndWindow || settings.DownloadEndWindow || this.settings.downloadEndWindow;\n\n // License\n this.settings.licenceCode = settings.licenceCode || settings.LicenceCode || this.settings.licenceCode;\n\n // SSP\n this.settings.isSspEnabled = this.parseBoolean(settings.isAdspaceEnabled || settings.IsAdspaceEnabled);\n\n // Detect changes\n if (oldInterval !== this.settings.collectInterval) {\n changes.push('collectInterval');\n this.emit('interval-changed', this.settings.collectInterval);\n }\n\n // Emit generic settings-applied event\n this.emit('settings-applied', this.settings, changes);\n\n console.log('[DisplaySettings] Applied settings:', {\n collectInterval: this.settings.collectInterval,\n displayName: this.settings.displayName,\n statsEnabled: this.settings.statsEnabled,\n changes\n });\n\n return { changed: changes, settings: this.settings };\n }\n\n /**\n * Parse collection interval (seconds)\n * @param {*} value - Raw value from CMS\n * @returns {number} Collection interval in seconds\n */\n parseCollectInterval(value) {\n const interval = parseInt(value, 10);\n\n // Validate range (minimum 60s, maximum 86400s = 24h)\n if (isNaN(interval) || interval < 60) {\n return 300; // 5 minutes default\n }\n\n if (interval > 86400) {\n return 86400; // 24 hours max\n }\n\n return interval;\n }\n\n /**\n * Parse boolean setting\n * @param {*} value - Raw value from CMS (string '1' or '0', or boolean)\n * @param {boolean} defaultValue - Default if not set\n * @returns {boolean}\n */\n parseBoolean(value, defaultValue = false) {\n if (value === true || value === false) {\n return value;\n }\n\n if (value === '1' || value === 1) {\n return true;\n }\n\n if (value === '0' || value === 0) {\n return false;\n }\n\n return defaultValue;\n }\n\n /**\n * Get collection interval in seconds\n * @returns {number}\n */\n getCollectInterval() {\n return this.settings.collectInterval;\n }\n\n /**\n * Get display name\n * @returns {string}\n */\n getDisplayName() {\n return this.settings.displayName;\n }\n\n /**\n * Get display size\n * @returns {{ width: number, height: number }}\n */\n getDisplaySize() {\n return {\n width: this.settings.sizeX,\n height: this.settings.sizeY\n };\n }\n\n /**\n * Check if stats are enabled\n * @returns {boolean}\n */\n isStatsEnabled() {\n return this.settings.statsEnabled;\n }\n\n /**\n * Get all settings\n * @returns {Object}\n */\n getAllSettings() {\n return { ...this.settings };\n }\n\n /**\n * Get a specific setting by key\n * @param {string} key - Setting key\n * @param {*} defaultValue - Default value if not set\n * @returns {*}\n */\n getSetting(key, defaultValue = null) {\n return this.settings[key] !== undefined ? this.settings[key] : defaultValue;\n }\n\n /**\n * Check if current time is within download window\n * @returns {boolean}\n */\n isInDownloadWindow() {\n // If no download window configured, always allow\n if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow) {\n return true;\n }\n\n try {\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const start = this.parseTimeWindow(this.settings.downloadStartWindow);\n const end = this.parseTimeWindow(this.settings.downloadEndWindow);\n\n // Handle overnight window (e.g., 22:00 - 06:00)\n if (start > end) {\n // Overnight: allow if AFTER start OR BEFORE end\n return currentTime >= start || currentTime < end;\n } else {\n // Same day: allow if AFTER start AND BEFORE end\n return currentTime >= start && currentTime < end;\n }\n } catch (error) {\n console.warn('[DisplaySettings] Failed to parse download window:', error);\n return true; // Allow downloads if parsing fails\n }\n }\n\n /**\n * Parse time window string to minutes since midnight\n * @param {string} timeStr - Time string (e.g., \"14:30\", \"22:00\")\n * @returns {number} Minutes since midnight\n */\n parseTimeWindow(timeStr) {\n if (!timeStr || typeof timeStr !== 'string') {\n throw new Error('Invalid time window format');\n }\n\n const parts = timeStr.split(':');\n if (parts.length !== 2) {\n throw new Error('Invalid time window format (expected HH:MM)');\n }\n\n const hours = parseInt(parts[0], 10);\n const minutes = parseInt(parts[1], 10);\n\n if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {\n throw new Error('Invalid time window values');\n }\n\n return hours * 60 + minutes;\n }\n\n /**\n * Get next download window start time\n * @returns {Date|null} Next window start, or null if always allowed\n */\n getNextDownloadWindow() {\n if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow) {\n return null;\n }\n\n try {\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n const start = this.parseTimeWindow(this.settings.downloadStartWindow);\n\n const nextWindow = new Date(now);\n\n if (currentTime < start) {\n // Window is later today\n nextWindow.setHours(Math.floor(start / 60), start % 60, 0, 0);\n } else {\n // Window is tomorrow\n nextWindow.setDate(nextWindow.getDate() + 1);\n nextWindow.setHours(Math.floor(start / 60), start % 60, 0, 0);\n }\n\n return nextWindow;\n } catch (error) {\n console.warn('[DisplaySettings] Failed to calculate next download window:', error);\n return null;\n }\n }\n\n /**\n * Check if screenshot interval has elapsed\n * @param {Date} lastScreenshot - Last screenshot timestamp\n * @returns {boolean}\n */\n shouldTakeScreenshot(lastScreenshot) {\n if (!lastScreenshot) {\n return true;\n }\n\n const elapsed = (Date.now() - lastScreenshot.getTime()) / 1000;\n return elapsed >= this.settings.screenshotInterval;\n }\n}\n"],"names":["DisplaySettings","EventEmitter","settings","changes","oldInterval","value","interval","defaultValue","key","now","currentTime","start","end","error","timeStr","parts","hours","minutes","nextWindow","lastScreenshot"],"mappings":"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","x_google_ignoreList":[0]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{c as y}from"./cms-api-kzy_Sw-u.js";const g=y("schedule:criteria"),v=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];function S(c,e,t={}){switch(c){case"dayOfWeek":return v[e.getDay()];case"dayOfMonth":return String(e.getDate());case"month":return String(e.getMonth()+1);case"hour":return String(e.getHours());case"isoDay":return String(e.getDay()===0?7:e.getDay());default:return t[c]!==void 0?String(t[c]):(g.debug(`Unknown metric: ${c}`),null)}}function D(c,e,t,i){if(c===null)return!1;if(i==="number"){const r=parseFloat(c),o=parseFloat(t);if(isNaN(r)||isNaN(o))return!1;switch(e){case"equals":return r===o;case"notEquals":return r!==o;case"greaterThan":return r>o;case"greaterThanOrEquals":return r>=o;case"lessThan":return r<o;case"lessThanOrEquals":return r<=o;default:return!1}}const s=c.toLowerCase(),n=t.toLowerCase();switch(e){case"equals":return s===n;case"notEquals":return s!==n;case"contains":return s.includes(n);case"notContains":return!s.includes(n);case"startsWith":return s.startsWith(n);case"endsWith":return s.endsWith(n);case"in":return n.split(",").map(r=>r.trim().toLowerCase()).includes(s);case"greaterThan":return s>n;case"lessThan":return s<n;default:return g.debug(`Unknown condition: ${e}`),!1}}function p(c,e={}){if(!c||c.length===0)return!0;const t=e.now||new Date,i=e.displayProperties||{};for(const s of c){const n=S(s.metric,t,i);if(!D(n,s.condition,s.value,s.type))return g.debug(`Criteria failed: ${s.metric} ${s.condition} "${s.value}" (actual: "${n}")`),!1}return!0}class M{constructor(e={}){this.schedule=null,this.playHistory=new Map,this.interruptScheduler=e.interruptScheduler||null,this.displayProperties=e.displayProperties||{},this.playerLocation=null,this._layoutMetadata=new Map}setSchedule(e){this.schedule=e}getDataConnectors(){var e;return((e=this.schedule)==null?void 0:e.dataConnectors)||[]}isRecurringScheduleActive(e,t){if(!e.recurrenceType||e.recurrenceType!=="Week")return!0;if(e.recurrenceRepeatsOn){const i=this.getIsoDayOfWeek(t);if(!e.recurrenceRepeatsOn.split(",").map(n=>parseInt(n.trim())).includes(i))return!1}if(e.recurrenceRange){const i=new Date(e.recurrenceRange);if(t>i)return!1}return!0}getIsoDayOfWeek(e){const t=e.getDay();return t===0?7:t}isTimeActive(e,t){const i=e.fromdt?new Date(e.fromdt):null,s=e.todt?new Date(e.todt):null;if(e.recurrenceType==="Week"){if(i&&s){const n=t.getHours()*3600+t.getMinutes()*60+t.getSeconds(),r=i.getHours()*3600+i.getMinutes()*60+i.getSeconds(),o=s.getHours()*3600+s.getMinutes()*60+s.getSeconds();return r<=o?n>=r&&n<=o:n>=r||n<=o}return!0}return!(i&&t<i||s&&t>s)}getCurrentLayouts(){if(!this.schedule)return[];const e=new Date,t=[];if(this._maxActivePriority=0,this.schedule.campaigns)for(const r of this.schedule.campaigns)this.isRecurringScheduleActive(r,e)&&this.isTimeActive(r,e)&&(this._maxActivePriority=Math.max(this._maxActivePriority,r.priority||0),t.push({type:"campaign",priority:r.priority,layouts:r.layouts,campaignId:r.id}));if(this.schedule.layouts){for(const r of this.schedule.layouts)if(this.isRecurringScheduleActive(r,e)&&this.isTimeActive(r,e)){if(r.criteria&&r.criteria.length>0&&!p(r.criteria,{now:e,displayProperties:this.displayProperties})){console.log("[Schedule] Layout",r.id,"filtered by criteria");continue}if(r.isGeoAware&&r.geoLocation&&!this.isWithinGeoFence(r.geoLocation)){console.log("[Schedule] Layout",r.id,"filtered by geofence");continue}if(this._maxActivePriority=Math.max(this._maxActivePriority,r.priority||0),!this.canPlayLayout(r.id,r.maxPlaysPerHour)){console.log("[Schedule] Layout",r.id,"filtered by maxPlaysPerHour (limit:",r.maxPlaysPerHour,")");continue}t.push({type:"layout",priority:r.priority||0,layouts:[r],layoutId:r.id})}}if(t.length===0)return this.schedule.default?[this.schedule.default]:[];let i=Math.max(...t.map(r=>r.priority));console.log("[Schedule] Max priority:",i,"from",t.length,"active items");let s=[];for(const r of t)r.priority===i?(console.log("[Schedule] Including priority",r.priority,"layouts:",r.layouts.map(o=>o.file)),s.push(...r.layouts)):console.log("[Schedule] Skipping priority",r.priority,"< max",i);this._layoutMetadata.clear();for(const r of s)this._layoutMetadata.set(r.file,{syncEvent:r.syncEvent||!1,shareOfVoice:r.shareOfVoice||0,scheduleid:r.scheduleid,priority:r.priority||0});if(this.interruptScheduler){const{normalLayouts:r,interruptLayouts:o}=this.interruptScheduler.separateLayouts(s);if(o.length>0){console.log("[Schedule] Found",o.length,"interrupt layouts with shareOfVoice");const l=this.interruptScheduler.processInterrupts(r,o).map(a=>a.file);return console.log("[Schedule] Final layouts (with interrupts):",l),l}}const n=s.map(r=>r.file);return console.log("[Schedule] Final layouts:",n),n}shouldCheckSchedule(e){return e?Date.now()-e>=6e4:!0}canPlayLayout(e,t){if(!t||t===0)return!0;const i=Date.now(),s=i-60*60*1e3,r=(this.playHistory.get(e)||[]).filter(o=>o>s);if(r.length>=t)return console.log(`[Schedule] Layout ${e} has reached max plays per hour (${r.length}/${t})`),!1;if(r.length>0){const o=36e5/t,u=Math.max(...r),l=i-u;if(l<o){const a=((o-l)/6e4).toFixed(1);return console.log(`[Schedule] Layout ${e} spacing: next play in ${a} min (${r.length}/${t} plays, ${Math.round(o/6e4)} min gap)`),!1}}return!0}recordPlay(e){this.playHistory.has(e)||this.playHistory.set(e,[]);const t=this.playHistory.get(e);t.push(Date.now());const i=Date.now()-60*60*1e3,s=t.filter(n=>n>i);this.playHistory.set(e,s),console.log(`[Schedule] Recorded play for layout ${e} (${s.length} plays in last hour)`)}getMaxActivePriority(){return this._maxActivePriority||0}isSyncEvent(e){const t=this._layoutMetadata.get(e);return(t==null?void 0:t.syncEvent)===!0}getLayoutMetadata(e){return this._layoutMetadata.get(e)||null}hasSyncEvents(){for(const e of this._layoutMetadata.values())if(e.syncEvent)return!0;return!1}getActiveActions(){var t;if(!((t=this.schedule)!=null&&t.actions))return[];const e=new Date;return this.schedule.actions.filter(i=>this.isTimeActive(i,e))}getCommands(){var e;return((e=this.schedule)==null?void 0:e.commands)||[]}findActionByTrigger(e){return this.getActiveActions().find(i=>i.triggerCode===e)||null}clearPlayHistory(){this.playHistory.clear(),console.log("[Schedule] Play history cleared")}setLocation(e,t){this.playerLocation={latitude:e,longitude:t},console.log(`[Schedule] Location set: ${e}, ${t}`)}setDisplayProperties(e){this.displayProperties=e||{}}isWithinGeoFence(e,t=500){if(!this.playerLocation)return console.log("[Schedule] No player location, skipping geofence check"),!0;if(!e)return!0;const i=e.split(",").map(l=>parseFloat(l.trim()));if(i.length<2||isNaN(i[0])||isNaN(i[1]))return console.log("[Schedule] Invalid geoLocation format:",e),!0;const s=i[0],n=i[1],r=i[2]||t,o=this.haversineDistance(this.playerLocation.latitude,this.playerLocation.longitude,s,n),u=o<=r;return console.log(`[Schedule] Geofence: ${o.toFixed(0)}m from (${s},${n}), radius ${r}m → ${u?"WITHIN":"OUTSIDE"}`),u}haversineDistance(e,t,i,s){const r=a=>a*Math.PI/180,o=r(i-e),u=r(s-t),l=Math.sin(o/2)**2+Math.cos(r(e))*Math.cos(r(i))*Math.sin(u/2)**2;return 6371e3*2*Math.atan2(Math.sqrt(l),Math.sqrt(1-l))}}const A=new M,h=y("schedule:interrupts");class O{constructor(){this.interruptCommittedDurations=new Map}isInterrupt(e){return!!(e.shareOfVoice&&e.shareOfVoice>0)}resetCommittedDurations(){this.interruptCommittedDurations.clear(),h.debug("Reset interrupt committed durations")}getCommittedDuration(e){return this.interruptCommittedDurations.get(e)||0}addCommittedDuration(e,t){const i=this.getCommittedDuration(e);this.interruptCommittedDurations.set(e,i+t)}isInterruptDurationSatisfied(e){if(!e.shareOfVoice)return!0;const t=e.id||e.file,i=e.shareOfVoice/100*3600;return this.getCommittedDuration(t)>=i}getRequiredSeconds(e){return e.shareOfVoice?e.shareOfVoice/100*3600:0}processInterrupts(e,t){if(!t||t.length===0)return h.debug("No interrupt layouts, returning normal layouts"),e;if(!e||e.length===0)return h.warn("No normal layouts available, interrupts will fill entire hour"),this.fillHourWithInterrupts(t);h.info(`Processing ${t.length} interrupt layouts with ${e.length} normal layouts`);for(const a of t){const f=a.id||a.file;this.interruptCommittedDurations.set(f,0)}const i=[];let s=0,n=0,r=!1;for(;!r;){if(n>=t.length){n=0;let f=!0;for(const m of t)if(!this.isInterruptDurationSatisfied(m)){f=!1;break}if(f){r=!0;break}}const a=t[n];if(!this.isInterruptDurationSatisfied(a)){const f=a.id||a.file;this.addCommittedDuration(f,a.duration),s+=a.duration,i.push(a)}n++}if(h.debug(`Resolved ${i.length} interrupt plays (${s}s total)`),s>=3600)return h.info("Interrupts fill entire hour (>= 3600s), no room for normal layouts"),i;const o=3600-s,u=this.fillTimeWithLayouts(e,o);h.debug(`Resolved ${u.length} normal plays (${o}s target)`);const l=this.interleaveLayouts(u,i);return h.info(`Final loop: ${l.length} layouts (${u.length} normal + ${i.length} interrupts)`),l}fillTimeWithLayouts(e,t){const i=[];let s=t,n=0;for(;s>0;){n>=e.length&&(n=0);const r=e[n];i.push(r),s-=r.duration,n++}return i}fillHourWithInterrupts(e){return this.fillTimeWithLayouts(e,3600)}interleaveLayouts(e,t){const i=[],s=Math.max(e.length,t.length),n=Math.ceil(1*s/e.length),r=Math.floor(1*s/t.length);h.debug(`Interleaving: pickCount=${s}, normalPick=${n}, interruptPick=${r}`);let o=0,u=0,l=0;for(let a=0;a<s;a++)a%n===0&&(o>=e.length&&(o=0),i.push(e[o]),l+=e[o].duration,o++),a%r===0&&u<t.length&&(i.push(t[u]),l+=t[u].duration,u++);for(;l<3600;)o>=e.length&&(o=0),i.push(e[o]),l+=e[o].duration,o++;return h.debug(`Interleaved ${i.length} layouts, total duration: ${l}s`),i}separateLayouts(e){const t=[],i=[];for(const s of e)this.isInterrupt(s)?i.push(s):t.push(s);return{normalLayouts:t,interruptLayouts:i}}}const d=y("schedule:overlays");class ${constructor(){this.overlays=[],this.displayProperties={},this.scheduleManager=null,d.debug("OverlayScheduler initialized")}setScheduleManager(e){this.scheduleManager=e}setDisplayProperties(e){this.displayProperties=e||{}}setOverlays(e){this.overlays=e||[],d.info(`Loaded ${this.overlays.length} overlay(s)`)}getCurrentOverlays(){if(!this.overlays||this.overlays.length===0)return[];const e=new Date,t=[];for(const i of this.overlays){if(!this.isTimeActive(i,e)){d.debug(`Overlay ${i.file} not in time window`);continue}if(i.isGeoAware&&i.geoLocation&&this.scheduleManager&&!this.scheduleManager.isWithinGeoFence(i.geoLocation)){d.debug(`Overlay ${i.file} filtered by geofence`);continue}if(i.criteria&&i.criteria.length>0&&!p(i.criteria,{now:e,displayProperties:this.displayProperties})){d.debug(`Overlay ${i.file} filtered by criteria`);continue}t.push(i)}return t.sort((i,s)=>{const n=i.priority||0;return(s.priority||0)-n}),t.length>0&&d.info(`Active overlays: ${t.length}`),t}isTimeActive(e,t){const i=e.fromDt?new Date(e.fromDt):null,s=e.toDt?new Date(e.toDt):null;return!(i&&t<i||s&&t>s)}shouldCheckOverlays(e){return e?Date.now()-e>=6e4:!0}getOverlayByFile(e){return this.overlays.find(t=>t.file===e)||null}clear(){this.overlays=[],d.debug("Cleared all overlays")}processOverlays(e,t){return this.setOverlays(t),e}}new $;export{O as InterruptScheduler,$ as OverlayScheduler,M as ScheduleManager,A as scheduleManager};
|
|
2
|
+
//# sourceMappingURL=index-BPNsrSEv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-BPNsrSEv.js","sources":["../../node_modules/.pnpm/@xiboplayer+schedule@0.1.0/node_modules/@xiboplayer/schedule/src/criteria.js","../../node_modules/.pnpm/@xiboplayer+schedule@0.1.0/node_modules/@xiboplayer/schedule/src/schedule.js","../../node_modules/.pnpm/@xiboplayer+schedule@0.1.0/node_modules/@xiboplayer/schedule/src/interrupts.js","../../node_modules/.pnpm/@xiboplayer+schedule@0.1.0/node_modules/@xiboplayer/schedule/src/overlays.js"],"sourcesContent":["/**\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 * 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 * 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 * @returns {string|null} Metric value or null if unknown\n */\nfunction getMetricValue(metric, now, displayProperties = {}) {\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 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 * @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\n for (const criterion of criteria) {\n const actual = getMetricValue(criterion.metric, now, displayProperties);\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","/**\n * Schedule manager - determines which layouts to show\n */\n\nimport { evaluateCriteria } from './criteria.js';\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.playerLocation = null; // { latitude, longitude } from Geolocation API\n this._layoutMetadata = new Map(); // layoutFile → { syncEvent, shareOfVoice, ... }\n }\n\n /**\n * Update schedule from XMDS\n */\n setSchedule(schedule) {\n this.schedule = schedule;\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 * Check if a schedule item is active based on recurrence rules\n * Supports weekly dayparting (recurring schedules on specific days/times)\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 // Currently only support Weekly recurrence (dayparting)\n if (item.recurrenceType !== 'Week') {\n return true; // Unsupported recurrence type, fallback to date/time checks\n }\n\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\n if (!allowedDays.includes(currentDayOfWeek)) {\n return false; // Today is not in the allowed days\n }\n }\n\n // Check recurrence range if specified\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 return true;\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') {\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 if (!this.schedule) {\n return [];\n }\n\n const now = new Date();\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 })) {\n console.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 console.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 - but track that we filtered it\n if (!this.canPlayLayout(layout.id, layout.maxPlaysPerHour)) {\n console.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 let maxPriority = Math.max(...activeItems.map(item => item.priority));\n console.log('[Schedule] Max priority:', maxPriority, 'from', activeItems.length, 'active items');\n\n // Collect all layouts from items with max priority\n let allLayouts = [];\n for (const item of activeItems) {\n if (item.priority === maxPriority) {\n console.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 console.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\n if (this.interruptScheduler) {\n const { normalLayouts, interruptLayouts } = this.interruptScheduler.separateLayouts(allLayouts);\n\n if (interruptLayouts.length > 0) {\n console.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 console.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 console.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 // If maxPlaysPerHour is 0 or undefined, unlimited plays\n if (!maxPlaysPerHour || maxPlaysPerHour === 0) {\n return true;\n }\n\n const now = Date.now();\n const oneHourAgo = now - (60 * 60 * 1000);\n\n // Get play history for this layout\n const history = this.playHistory.get(layoutId) || [];\n\n // Filter to plays within the last hour\n const playsInLastHour = history.filter(timestamp => timestamp > oneHourAgo);\n\n // Check 1: Total plays in last hour must be under limit\n if (playsInLastHour.length >= maxPlaysPerHour) {\n console.log(`[Schedule] Layout ${layoutId} has reached max plays per hour (${playsInLastHour.length}/${maxPlaysPerHour})`);\n return false;\n }\n\n // Check 2: Minimum gap between plays for even distribution\n // e.g., 3/hour → 1 every 20 min, 6/hour → 1 every 10 min\n if (playsInLastHour.length > 0) {\n const minGapMs = (60 * 60 * 1000) / maxPlaysPerHour;\n const lastPlayTime = Math.max(...playsInLastHour);\n const elapsed = now - lastPlayTime;\n\n if (elapsed < minGapMs) {\n const remainingMin = ((minGapMs - elapsed) / 60000).toFixed(1);\n console.log(`[Schedule] Layout ${layoutId} spacing: next play in ${remainingMin} min (${playsInLastHour.length}/${maxPlaysPerHour} plays, ${Math.round(minGapMs/60000)} min gap)`);\n return false;\n }\n }\n\n return true;\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 console.log(`[Schedule] Recorded play for layout ${layoutId} (${cleaned.length} plays in last hour)`);\n }\n\n /**\n * Get the max priority of any time-active layout (ignoring rate-limit filtering).\n * Returns 0 if no layouts are active or if getCurrentLayouts() hasn't been called.\n * @returns {number}\n */\n getMaxActivePriority() {\n return this._maxActivePriority || 0;\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 * 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 console.log('[Schedule] 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 console.log(`[Schedule] 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 console.log('[Schedule] 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 console.log('[Schedule] 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 console.log(`[Schedule] 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","/**\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","/**\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 * @param {Object} overlay - Overlay object\n * @param {Date} now - Current time\n * @returns {boolean}\n */\n isTimeActive(overlay, now) {\n const from = overlay.fromDt ? new Date(overlay.fromDt) : null;\n const to = overlay.toDt ? new Date(overlay.toDt) : null;\n\n // Check time bounds\n if (from && now < from) {\n return false;\n }\n if (to && now > to) {\n return false;\n }\n\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 * Process overlay layouts (compatibility method for interrupt scheduler pattern)\n * @param {Array} layouts - Base layouts\n * @param {Array} overlays - Overlay layouts\n * @returns {Array} Layouts (unchanged, overlays are separate)\n */\n processOverlays(layouts, overlays) {\n // Overlays don't modify the main layout loop\n // They are rendered separately on top\n this.setOverlays(overlays);\n return layouts;\n }\n}\n\nexport const overlayScheduler = new OverlayScheduler();\n"],"names":["log","createLogger","DAY_NAMES","getMetricValue","metric","now","displayProperties","evaluateCondition","actual","condition","expected","type","a","e","s","evaluateCriteria","criteria","options","criterion","ScheduleManager","schedule","_a","item","currentDayOfWeek","d","rangeEnd","date","day","from","to","currentTime","fromTime","toTime","activeItems","campaign","layout","maxPriority","allLayouts","l","normalLayouts","interruptLayouts","result","lastCheck","layoutId","maxPlaysPerHour","oneHourAgo","playsInLastHour","timestamp","minGapMs","lastPlayTime","elapsed","remainingMin","history","cleaned","layoutFile","meta","action","triggerCode","latitude","longitude","properties","geoLocation","defaultRadius","parts","fenceLat","fenceLng","radius","distance","within","lat1","lon1","lat2","lon2","toRad","deg","dLat","dLon","scheduleManager","logger","InterruptScheduler","duration","current","requiredSeconds","resolvedInterruptLayouts","interruptSecondsInHour","index","satisfied","allSatisfied","currentInterrupt","normalSecondsInHour","resolvedNormalLayouts","loop","layouts","targetSeconds","resolved","remainingSeconds","pickCount","normalPick","interruptPick","normalIndex","interruptIndex","totalSecondsAllocated","i","OverlayScheduler","overlays","activeOverlays","overlay","b","priorityA","fileId","o"],"mappings":"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","x_google_ignoreList":[0,1,2,3]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as F,C as b,c as v}from"./cache-proxy-Cx4Z8XMC.js";import"./cms-api-kzy_Sw-u.js";const C=4,f=50*1024*1024,k=4;class m{constructor(e,o={}){this.fileInfo=e,this.options=o,this.downloadedBytes=0,this.totalBytes=0,this.promise=null,this.resolve=null,this.reject=null,this.waiters=[],this.state="pending",this.onChunkDownloaded=null}async wait(){return this.promise?this.promise:this.state==="complete"?this.promise:new Promise((e,o)=>{this.waiters.push({resolve:e,reject:o})})}async start(){const{id:e,type:o,path:t,md5:i}=this.fileInfo;try{this.state="downloading",console.log("[DownloadTask] Starting:",t);const n=await fetch(t,{method:"HEAD"});if(!n.ok)throw new Error(`HEAD request failed: ${n.status}`);this.totalBytes=parseInt(n.headers.get("Content-Length")||"0");const h=n.headers.get("Content-Type")||"application/octet-stream";console.log("[DownloadTask] File size:",(this.totalBytes/1024/1024).toFixed(1),"MB");let a;const c=this.options.chunkSize||f,d=this.options.chunksPerFile||k;if(this.totalBytes>100*1024*1024?a=await this.downloadChunks(t,h,c,d):a=await this.downloadFull(t),i&&this.options.calculateMD5){const r=await this.options.calculateMD5(a);r&&r!==i&&(console.warn("[DownloadTask] MD5 mismatch:",t),console.warn("[DownloadTask] Expected:",i),console.warn("[DownloadTask] Got:",r))}console.log("[DownloadTask] Complete:",t,`(${a.size} bytes)`),this.state="complete",this.blob=a,this.promise=Promise.resolve(a);for(const r of this.waiters)r.resolve(a);return this.waiters=[],a}catch(n){console.error("[DownloadTask] Failed:",t,n),this.state="failed",this.promise=Promise.reject(n),this.promise.catch(()=>{});for(const h of this.waiters)h.reject(n);throw this.waiters=[],n}}async downloadFull(e){const o=await fetch(e);if(!o.ok)throw new Error(`Download failed: ${o.status}`);const t=await o.blob();return this.downloadedBytes=t.size,t}async downloadChunks(e,o,t,i){const n=[];for(let s=0;s<this.totalBytes;s+=t){const w=Math.min(s+t-1,this.totalBytes-1);n.push({start:s,end:w,index:n.length})}if(n.length>2){const s=n.pop();n.splice(1,0,s)}console.log("[DownloadTask] Downloading",n.length,"chunks (chunk 0 + last prioritized)");const h=new Map;let a=0;const c=async s=>{const w=`bytes=${s.start}-${s.end}`;try{const l=await fetch(e,{headers:{Range:w}});if(!l.ok&&l.status!==206)throw new Error(`Chunk ${s.index} failed: ${l.status}`);const u=await l.blob();h.set(s.index,u),this.downloadedBytes+=u.size;const D=(this.downloadedBytes/this.totalBytes*100).toFixed(1);if(console.log("[DownloadTask] Chunk",s.index+1,"/",n.length,`(${D}%)`),this.onChunkDownloaded)try{await this.onChunkDownloaded(s.index,u,n.length)}catch(y){console.warn("[DownloadTask] onChunkDownloaded callback error:",y)}return this.options.onProgress&&this.options.onProgress(this.downloadedBytes,this.totalBytes),u}catch(l){throw console.error("[DownloadTask] Chunk",s.index,"failed:",l),l}},d=async()=>{for(;a<n.length;){const s=n[a++];await c(s)}},r=[];for(let s=0;s<i;s++)r.push(d());if(await Promise.all(r),this.onChunkDownloaded)return new Blob([],{type:o});const p=[];for(let s=0;s<n.length;s++)p.push(h.get(s));return new Blob(p,{type:o})}}class q{constructor(e={}){this.concurrency=e.concurrency||C,this.chunkSize=e.chunkSize||f,this.chunksPerFile=e.chunksPerFile||k,this.calculateMD5=e.calculateMD5,this.onProgress=e.onProgress,this.queue=[],this.active=new Map,this.running=0}enqueue(e){const{path:o}=e;if(this.active.has(o))return console.log("[DownloadQueue] File already downloading:",o),this.active.get(o);const t=new m(e,{chunkSize:this.chunkSize,chunksPerFile:this.chunksPerFile,calculateMD5:this.calculateMD5,onProgress:this.onProgress});return this.active.set(o,t),this.queue.push(t),console.log("[DownloadQueue] Enqueued:",o,`(${this.queue.length} pending, ${this.running} active)`),this.processQueue(),t}async processQueue(){for(console.log("[DownloadQueue] processQueue:",this.running,"running,",this.queue.length,"queued");this.running<this.concurrency&&this.queue.length>0;){const e=this.queue.shift();this.running++,console.log("[DownloadQueue] Starting:",e.fileInfo.path,`(${this.running}/${this.concurrency} active)`),e.start().catch(()=>{}).finally(()=>{this.running--,this.active.delete(e.fileInfo.path),console.log("[DownloadQueue] Complete:",e.fileInfo.path,`(${this.running} active, ${this.queue.length} pending)`),this.processQueue()})}this.running>=this.concurrency&&console.log("[DownloadQueue] Concurrency limit reached:",this.running,"/",this.concurrency),this.queue.length===0&&this.running===0&&console.log("[DownloadQueue] All downloads complete")}prioritize(e,o){const t=this.queue.findIndex(i=>i.fileInfo.type===e&&String(i.fileInfo.id)===String(o));if(t>0){const[i]=this.queue.splice(t,1);return this.queue.unshift(i),console.log("[DownloadQueue] Prioritized:",`${e}/${o}`,"(moved to front of queue)"),!0}if(t===0)return console.log("[DownloadQueue] Already at front:",`${e}/${o}`),!0;for(const[,i]of this.active)if(i.fileInfo.type===e&&String(i.fileInfo.id)===String(o))return console.log("[DownloadQueue] Already downloading:",`${e}/${o}`),!0;return console.log("[DownloadQueue] Not found in queue:",`${e}/${o}`),!1}getTask(e){return this.active.get(e)||null}getProgress(){const e={};for(const[o,t]of this.active.entries())e[o]={downloaded:t.downloadedBytes,total:t.totalBytes,percent:t.totalBytes>0?(t.downloadedBytes/t.totalBytes*100).toFixed(1):0,state:t.state};return e}clear(){this.queue=[],this.active.clear(),this.running=0}}class P{constructor(e={}){this.queue=new q(e)}enqueue(e){return this.queue.enqueue(e)}getTask(e){return this.queue.getTask(e)}getProgress(){return this.queue.getProgress()}clear(){this.queue.clear()}}export{F as CacheManager,b as CacheProxy,P as DownloadManager,v as cacheManager};
|
|
2
|
+
//# sourceMappingURL=index-BY2j60YZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-BY2j60YZ.js","sources":["../../node_modules/.pnpm/@xiboplayer+cache@0.1.0/node_modules/@xiboplayer/cache/src/download-manager.js"],"sourcesContent":["/**\n * DownloadManager - Standalone file download orchestration\n *\n * Works in both browser and Service Worker contexts.\n * Handles download queue, concurrency control, parallel chunks, and MD5 verification.\n *\n * Architecture:\n * - DownloadQueue: Manages download queue with concurrency control\n * - DownloadTask: Handles individual file download with parallel chunks\n * - MD5Calculator: Calculates MD5 hash (optional, uses SparkMD5 if available)\n *\n * Usage:\n * const dm = new DownloadManager({ concurrency: 4, chunkSize: 50MB });\n * const task = dm.enqueue({ id, type, path, md5 });\n * const blob = await task.wait();\n */\n\nconst DEFAULT_CONCURRENCY = 4; // Max concurrent file downloads\nconst DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunks\nconst DEFAULT_CHUNKS_PER_FILE = 4; // Parallel chunks per file\n\n/**\n * DownloadTask - Handles individual file download\n */\nexport class DownloadTask {\n constructor(fileInfo, options = {}) {\n this.fileInfo = fileInfo;\n this.options = options;\n this.downloadedBytes = 0;\n this.totalBytes = 0;\n this.promise = null;\n this.resolve = null;\n this.reject = null;\n this.waiters = []; // Promises waiting for completion\n this.state = 'pending'; // pending, downloading, complete, failed\n // Progressive streaming: callback fired for each chunk as it downloads\n // Set externally before download starts: (chunkIndex, chunkBlob, totalChunks) => Promise\n this.onChunkDownloaded = null;\n }\n\n /**\n * Wait for download to complete\n * Returns blob when ready\n */\n async wait() {\n if (this.promise) {\n return this.promise;\n }\n\n if (this.state === 'complete') {\n return this.promise;\n }\n\n // Create waiter promise\n return new Promise((resolve, reject) => {\n this.waiters.push({ resolve, reject });\n });\n }\n\n /**\n * Start download with parallel chunks\n */\n async start() {\n const { id, type, path, md5 } = this.fileInfo;\n\n try {\n this.state = 'downloading';\n console.log('[DownloadTask] Starting:', path);\n\n // HEAD request to get file size\n const headResponse = await fetch(path, { method: 'HEAD' });\n if (!headResponse.ok) {\n throw new Error(`HEAD request failed: ${headResponse.status}`);\n }\n\n this.totalBytes = parseInt(headResponse.headers.get('Content-Length') || '0');\n const contentType = headResponse.headers.get('Content-Type') || 'application/octet-stream';\n\n console.log('[DownloadTask] File size:', (this.totalBytes / 1024 / 1024).toFixed(1), 'MB');\n\n // Download in chunks if large file\n let blob;\n const chunkSize = this.options.chunkSize || DEFAULT_CHUNK_SIZE;\n const chunksPerFile = this.options.chunksPerFile || DEFAULT_CHUNKS_PER_FILE;\n\n if (this.totalBytes > 100 * 1024 * 1024) { // > 100MB\n blob = await this.downloadChunks(path, contentType, chunkSize, chunksPerFile);\n } else {\n blob = await this.downloadFull(path);\n }\n\n // Verify MD5 if provided and MD5 calculator available\n if (md5 && this.options.calculateMD5) {\n const calculatedMd5 = await this.options.calculateMD5(blob);\n if (calculatedMd5 && calculatedMd5 !== md5) {\n console.warn('[DownloadTask] MD5 mismatch:', path);\n console.warn('[DownloadTask] Expected:', md5);\n console.warn('[DownloadTask] Got:', calculatedMd5);\n // Continue anyway (kiosk mode)\n }\n }\n\n console.log('[DownloadTask] Complete:', path, `(${blob.size} bytes)`);\n\n // Mark complete\n this.state = 'complete';\n this.blob = blob;\n\n // Resolve all waiters\n this.promise = Promise.resolve(blob);\n for (const waiter of this.waiters) {\n waiter.resolve(blob);\n }\n this.waiters = [];\n\n return blob;\n\n } catch (error) {\n console.error('[DownloadTask] Failed:', path, error);\n this.state = 'failed';\n\n // Reject all waiters\n this.promise = Promise.reject(error);\n this.promise.catch(() => {}); // Prevent unhandled rejection if nobody calls wait()\n for (const waiter of this.waiters) {\n waiter.reject(error);\n }\n this.waiters = [];\n\n throw error;\n }\n }\n\n /**\n * Download full file (for small files)\n */\n async downloadFull(url) {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Download failed: ${response.status}`);\n }\n\n const blob = await response.blob();\n this.downloadedBytes = blob.size;\n return blob;\n }\n\n /**\n * Download file in parallel chunks (for large files)\n * If onChunkDownloaded callback is set, fires it for each chunk as it arrives\n * so the caller can cache chunks progressively (enabling streaming before\n * the entire file is downloaded).\n */\n async downloadChunks(url, contentType, chunkSize, concurrentChunks) {\n // Calculate chunk ranges\n const chunkRanges = [];\n for (let start = 0; start < this.totalBytes; start += chunkSize) {\n const end = Math.min(start + chunkSize - 1, this.totalBytes - 1);\n chunkRanges.push({ start, end, index: chunkRanges.length });\n }\n\n // Prioritize chunk 0 (ftyp header) and last chunk (moov atom) for video early playback.\n // Modern browsers seek to end of MP4 for moov, so having both extremes first\n // lets video start playing while middle chunks are still downloading.\n if (chunkRanges.length > 2) {\n const lastChunk = chunkRanges.pop(); // remove last\n chunkRanges.splice(1, 0, lastChunk); // insert after chunk 0\n }\n console.log('[DownloadTask] Downloading', chunkRanges.length, 'chunks (chunk 0 + last prioritized)');\n\n // Download chunks in parallel with concurrency limit\n const chunkMap = new Map();\n let nextChunkIndex = 0;\n\n const downloadChunk = async (range) => {\n const rangeHeader = `bytes=${range.start}-${range.end}`;\n\n try {\n const response = await fetch(url, {\n headers: { 'Range': rangeHeader }\n });\n\n if (!response.ok && response.status !== 206) {\n throw new Error(`Chunk ${range.index} failed: ${response.status}`);\n }\n\n const chunkBlob = await response.blob();\n chunkMap.set(range.index, chunkBlob);\n\n this.downloadedBytes += chunkBlob.size;\n const progress = (this.downloadedBytes / this.totalBytes * 100).toFixed(1);\n console.log('[DownloadTask] Chunk', range.index + 1, '/', chunkRanges.length, `(${progress}%)`);\n\n // Progressive streaming: notify caller to cache this chunk immediately\n if (this.onChunkDownloaded) {\n try {\n await this.onChunkDownloaded(range.index, chunkBlob, chunkRanges.length);\n } catch (e) {\n console.warn('[DownloadTask] onChunkDownloaded callback error:', e);\n }\n }\n\n // Notify progress if callback provided\n if (this.options.onProgress) {\n this.options.onProgress(this.downloadedBytes, this.totalBytes);\n }\n\n return chunkBlob;\n\n } catch (error) {\n console.error('[DownloadTask] Chunk', range.index, 'failed:', error);\n throw error;\n }\n };\n\n // Download with concurrency control\n const downloadNext = async () => {\n while (nextChunkIndex < chunkRanges.length) {\n const range = chunkRanges[nextChunkIndex++];\n await downloadChunk(range);\n }\n };\n\n // Start concurrent downloaders\n const downloaders = [];\n for (let i = 0; i < concurrentChunks; i++) {\n downloaders.push(downloadNext());\n }\n await Promise.all(downloaders);\n\n // If progressive caching was used, skip reassembly (chunks are already cached)\n if (this.onChunkDownloaded) {\n // Return a lightweight marker — the real data is already in cache\n return new Blob([], { type: contentType });\n }\n\n // Reassemble chunks in order (traditional path for small chunked downloads)\n const orderedChunks = [];\n for (let i = 0; i < chunkRanges.length; i++) {\n orderedChunks.push(chunkMap.get(i));\n }\n\n return new Blob(orderedChunks, { type: contentType });\n }\n}\n\n/**\n * DownloadQueue - Manages download queue with concurrency control\n */\nexport class DownloadQueue {\n constructor(options = {}) {\n this.concurrency = options.concurrency || DEFAULT_CONCURRENCY;\n this.chunkSize = options.chunkSize || DEFAULT_CHUNK_SIZE;\n this.chunksPerFile = options.chunksPerFile || DEFAULT_CHUNKS_PER_FILE;\n this.calculateMD5 = options.calculateMD5; // Optional MD5 calculator function\n this.onProgress = options.onProgress; // Optional progress callback\n\n this.queue = [];\n this.active = new Map(); // url -> DownloadTask\n this.running = 0;\n }\n\n /**\n * Add file to download queue\n * Returns existing task if already downloading\n */\n enqueue(fileInfo) {\n const { path } = fileInfo;\n\n // If already downloading, return existing task\n if (this.active.has(path)) {\n console.log('[DownloadQueue] File already downloading:', path);\n return this.active.get(path);\n }\n\n // Create new download task\n const task = new DownloadTask(fileInfo, {\n chunkSize: this.chunkSize,\n chunksPerFile: this.chunksPerFile,\n calculateMD5: this.calculateMD5,\n onProgress: this.onProgress\n });\n\n this.active.set(path, task);\n this.queue.push(task);\n\n console.log('[DownloadQueue] Enqueued:', path, `(${this.queue.length} pending, ${this.running} active)`);\n\n // Start download if capacity available\n this.processQueue();\n\n return task;\n }\n\n /**\n * Process queue - start downloads up to concurrency limit\n */\n async processQueue() {\n console.log('[DownloadQueue] processQueue:', this.running, 'running,', this.queue.length, 'queued');\n\n while (this.running < this.concurrency && this.queue.length > 0) {\n const task = this.queue.shift();\n this.running++;\n\n console.log('[DownloadQueue] Starting:', task.fileInfo.path, `(${this.running}/${this.concurrency} active)`);\n\n // Start download (don't await - let it run in background)\n // .catch is safe here: errors are already propagated to waiters inside start()\n task.start()\n .catch(() => {}) // Suppress — error handled internally via waiters\n .finally(() => {\n this.running--;\n this.active.delete(task.fileInfo.path);\n console.log('[DownloadQueue] Complete:', task.fileInfo.path, `(${this.running} active, ${this.queue.length} pending)`);\n\n // Process next in queue\n this.processQueue();\n });\n }\n\n if (this.running >= this.concurrency) {\n console.log('[DownloadQueue] Concurrency limit reached:', this.running, '/', this.concurrency);\n }\n if (this.queue.length === 0 && this.running === 0) {\n console.log('[DownloadQueue] All downloads complete');\n }\n }\n\n /**\n * Move a file to the front of the queue (if still queued, not yet started)\n * @param {string} fileType - 'media' or 'layout'\n * @param {string} fileId - File ID\n * @returns {boolean} true if file was found (queued or active)\n */\n prioritize(fileType, fileId) {\n const idx = this.queue.findIndex(task =>\n task.fileInfo.type === fileType && String(task.fileInfo.id) === String(fileId)\n );\n\n if (idx > 0) {\n const [task] = this.queue.splice(idx, 1);\n this.queue.unshift(task);\n console.log('[DownloadQueue] Prioritized:', `${fileType}/${fileId}`, '(moved to front of queue)');\n return true;\n }\n\n if (idx === 0) {\n console.log('[DownloadQueue] Already at front:', `${fileType}/${fileId}`);\n return true;\n }\n\n // Check if already downloading\n for (const [, task] of this.active) {\n if (task.fileInfo.type === fileType && String(task.fileInfo.id) === String(fileId)) {\n console.log('[DownloadQueue] Already downloading:', `${fileType}/${fileId}`);\n return true;\n }\n }\n\n console.log('[DownloadQueue] Not found in queue:', `${fileType}/${fileId}`);\n return false;\n }\n\n /**\n * Get task by URL (returns null if not downloading)\n */\n getTask(url) {\n return this.active.get(url) || null;\n }\n\n /**\n * Get progress for all active downloads\n */\n getProgress() {\n const progress = {};\n for (const [url, task] of this.active.entries()) {\n progress[url] = {\n downloaded: task.downloadedBytes,\n total: task.totalBytes,\n percent: task.totalBytes > 0 ? (task.downloadedBytes / task.totalBytes * 100).toFixed(1) : 0,\n state: task.state\n };\n }\n return progress;\n }\n\n /**\n * Cancel all downloads\n */\n clear() {\n this.queue = [];\n this.active.clear();\n this.running = 0;\n }\n}\n\n/**\n * DownloadManager - Main API\n */\nexport class DownloadManager {\n constructor(options = {}) {\n this.queue = new DownloadQueue(options);\n }\n\n /**\n * Enqueue file for download\n * @param {Object} fileInfo - { id, type, path, md5 }\n * @returns {DownloadTask}\n */\n enqueue(fileInfo) {\n return this.queue.enqueue(fileInfo);\n }\n\n /**\n * Get download task by URL\n */\n getTask(url) {\n return this.queue.getTask(url);\n }\n\n /**\n * Get progress for all downloads\n */\n getProgress() {\n return this.queue.getProgress();\n }\n\n /**\n * Clear all downloads\n */\n clear() {\n this.queue.clear();\n }\n}\n"],"names":["DEFAULT_CONCURRENCY","DEFAULT_CHUNK_SIZE","DEFAULT_CHUNKS_PER_FILE","DownloadTask","fileInfo","options","resolve","reject","id","type","path","md5","headResponse","contentType","blob","chunkSize","chunksPerFile","calculatedMd5","waiter","error","url","response","concurrentChunks","chunkRanges","start","end","lastChunk","chunkMap","nextChunkIndex","downloadChunk","range","rangeHeader","chunkBlob","progress","e","downloadNext","downloaders","i","orderedChunks","DownloadQueue","task","fileType","fileId","idx","DownloadManager"],"mappings":"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","x_google_ignoreList":[0]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{c as y}from"./cms-api-kzy_Sw-u.js";const d=y("@xiboplayer/stats"),w="xibo-player-stats",$=1,g="stats";class P{constructor(){this.db=null,this.inProgressStats=new Map}async init(){if(this.db){d.debug("Stats collector already initialized");return}return new Promise((r,e)=>{if(typeof indexedDB>"u"){const t=new Error("IndexedDB not available");d.error("IndexedDB not available - stats will not be persisted"),e(t);return}const n=indexedDB.open(w,$);n.onerror=()=>{const t=new Error(`Failed to open IndexedDB: ${n.error}`);d.error("Failed to open stats database:",n.error),e(t)},n.onsuccess=()=>{this.db=n.result,d.info("Stats database initialized"),r()},n.onupgradeneeded=t=>{const s=t.target.result;s.objectStoreNames.contains(g)||(s.createObjectStore(g,{keyPath:"id",autoIncrement:!0}).createIndex("submitted","submitted",{unique:!1}),d.info("Stats store created"))}})}async startLayout(r,e){if(!this.db){d.warn("Stats database not initialized");return}const n=`layout-${r}`;if(this.inProgressStats.has(n)){const s=this.inProgressStats.get(n);s.end=new Date,s.duration=Math.floor((s.end-s.start)/1e3),await this._saveStat(s),this.inProgressStats.delete(n),d.debug(`Layout ${r} replay - ended previous cycle (${s.duration}s)`)}const t={type:"layout",layoutId:r,scheduleId:e,start:new Date,end:null,duration:0,count:1,submitted:0};this.inProgressStats.set(n,t),d.debug(`Started tracking layout ${r} (schedule ${e})`)}async endLayout(r,e){if(!this.db){d.warn("Stats database not initialized");return}const n=`layout-${r}`,t=this.inProgressStats.get(n);if(!t){d.debug(`Layout ${r} not found in progress (may have been ended by replay)`);return}t.end=new Date,t.duration=Math.floor((t.end-t.start)/1e3);try{await this._saveStat(t),this.inProgressStats.delete(n),d.debug(`Ended tracking layout ${r} (${t.duration}s)`)}catch(s){throw d.error(`Failed to save layout stat ${r}:`,s),s}}async startWidget(r,e,n){if(!this.db){d.warn("Stats database not initialized");return}const t=`media-${r}-${e}`;if(this.inProgressStats.has(t)){const o=this.inProgressStats.get(t);o.end=new Date,o.duration=Math.floor((o.end-o.start)/1e3),await this._saveStat(o),this.inProgressStats.delete(t),d.debug(`Widget ${r} replay - ended previous cycle (${o.duration}s)`)}const s={type:"media",mediaId:r,layoutId:e,scheduleId:n,start:new Date,end:null,duration:0,count:1,submitted:0};this.inProgressStats.set(t,s),d.debug(`Started tracking widget ${r} in layout ${e}`)}async endWidget(r,e,n){if(!this.db){d.warn("Stats database not initialized");return}const t=`media-${r}-${e}`,s=this.inProgressStats.get(t);if(!s){d.debug(`Widget ${r} not found in progress (expected during layout transitions)`);return}s.end=new Date,s.duration=Math.floor((s.end-s.start)/1e3);try{await this._saveStat(s),this.inProgressStats.delete(t),d.debug(`Ended tracking widget ${r} (${s.duration}s)`)}catch(o){throw d.error(`Failed to save widget stat ${r}:`,o),o}}async getStatsForSubmission(r=50){return this.db?new Promise((e,n)=>{const a=this.db.transaction([g],"readonly").objectStore(g).index("submitted").openCursor(IDBKeyRange.only(0)),c=[];a.onsuccess=u=>{const h=u.target.result;h&&c.length<r?(c.push(h.value),h.continue()):(d.debug(`Retrieved ${c.length} unsubmitted stats`),e(c))},a.onerror=()=>{d.error("Failed to retrieve stats:",a.error),n(new Error(`Failed to retrieve stats: ${a.error}`))}}):(d.warn("Stats database not initialized"),[])}async clearSubmittedStats(r){if(!this.db){d.warn("Stats database not initialized");return}if(!(!r||r.length===0))return new Promise((e,n)=>{const t=this.db.transaction([g],"readwrite"),s=t.objectStore(g);let o=0;r.forEach(a=>{if(a.id){const c=s.delete(a.id);c.onsuccess=()=>{o++},c.onerror=()=>{d.error(`Failed to delete stat ${a.id}:`,c.error)}}}),t.oncomplete=()=>{d.debug(`Deleted ${o} submitted stats`),e()},t.onerror=()=>{d.error("Failed to delete submitted stats:",t.error),n(new Error(`Failed to delete stats: ${t.error}`))}})}async getAggregatedStatsForSubmission(r=50){const e=await this.getStatsForSubmission(r);if(e.length===0)return[];const n=new Map;for(const t of e){const s=t.start instanceof Date?t.start.toISOString().slice(0,13):new Date(t.start).toISOString().slice(0,13),o=`${t.type}|${t.layoutId}|${t.mediaId||""}|${t.scheduleId}|${s}`;if(n.has(o)){const a=n.get(o);a.count+=t.count||1,a.duration+=t.duration||0;const c=t.start instanceof Date?t.start:new Date(t.start),u=t.end instanceof Date?t.end:new Date(t.end||t.start);c<a.start&&(a.start=c),u>a.end&&(a.end=u),a._rawIds.push(t.id)}else n.set(o,{...t,start:t.start instanceof Date?t.start:new Date(t.start),end:t.end instanceof Date?t.end:new Date(t.end||t.start),count:t.count||1,_rawIds:[t.id]})}return Array.from(n.values())}async getAllStats(){return this.db?new Promise((r,e)=>{const s=this.db.transaction([g],"readonly").objectStore(g).getAll();s.onsuccess=()=>{r(s.result)},s.onerror=()=>{d.error("Failed to get all stats:",s.error),e(new Error(`Failed to get all stats: ${s.error}`))}}):(d.warn("Stats database not initialized"),[])}async clearAllStats(){if(!this.db){d.warn("Stats database not initialized");return}return new Promise((r,e)=>{const s=this.db.transaction([g],"readwrite").objectStore(g).clear();s.onsuccess=()=>{d.debug("Cleared all stats"),this.inProgressStats.clear(),r()},s.onerror=()=>{d.error("Failed to clear all stats:",s.error),e(new Error(`Failed to clear stats: ${s.error}`))}})}async _saveStat(r){return new Promise((e,n)=>{const s=this.db.transaction([g],"readwrite").objectStore(g),o=s.add(r);o.onsuccess=()=>{e(o.result)},o.onerror=()=>{o.error.name==="QuotaExceededError"?(d.error("IndexedDB quota exceeded - cleaning old stats"),this._cleanOldStats().then(()=>{const a=s.add(r);a.onsuccess=()=>e(a.result),a.onerror=()=>n(a.error)}).catch(n)):n(o.error)}})}async _cleanOldStats(){if(this.db)return new Promise((r,e)=>{const t=this.db.transaction([g],"readwrite").objectStore(g),o=t.index("submitted").openCursor(1),a=[];o.onsuccess=c=>{const u=c.target.result;u&&a.length<100?(a.push(u.value.id),u.continue()):(a.forEach(h=>{t.delete(h)}),d.info(`Cleaned ${a.length} old stats due to quota`),r())},o.onerror=()=>{d.error("Failed to clean old stats:",o.error),e(o.error)}})}}function L(i){return!i||i.length===0?"<stats></stats>":`<stats>
|
|
2
|
+
${i.map(e=>{const n=S(e.start),t=S(e.end||e.start),s=[`type="${f(e.type)}"`,`fromdt="${f(n)}"`,`todt="${f(t)}"`,`scheduleid="${e.scheduleId}"`,`layoutid="${e.layoutId}"`];return e.type==="media"&&e.mediaId&&s.push(`mediaid="${e.mediaId}"`),s.push(`count="${e.count}"`),s.push(`duration="${e.duration}"`),` <stat ${s.join(" ")} />`}).join(`
|
|
3
|
+
`)}
|
|
4
|
+
</stats>`}function S(i){i instanceof Date||(i=new Date(i));const r=i.getFullYear(),e=String(i.getMonth()+1).padStart(2,"0"),n=String(i.getDate()).padStart(2,"0"),t=String(i.getHours()).padStart(2,"0"),s=String(i.getMinutes()).padStart(2,"0"),o=String(i.getSeconds()).padStart(2,"0");return`${r}-${e}-${n} ${t}:${s}:${o}`}function f(i){return typeof i!="string"?i:i.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}const l=y("@xiboplayer/stats"),m="xibo-player-logs",D=1,b="logs";class x{constructor(){this.db=null,this._reportedFaults=new Map}async init(){if(this.db){l.debug("Log reporter already initialized");return}return new Promise((r,e)=>{if(typeof indexedDB>"u"){const t=new Error("IndexedDB not available");l.error("IndexedDB not available - logs will not be persisted"),e(t);return}const n=indexedDB.open(m,D);n.onerror=()=>{const t=new Error(`Failed to open IndexedDB: ${n.error}`);l.error("Failed to open logs database:",n.error),e(t)},n.onsuccess=()=>{this.db=n.result,l.info("Logs database initialized"),r()},n.onupgradeneeded=t=>{const s=t.target.result;s.objectStoreNames.contains(b)||(s.createObjectStore(b,{keyPath:"id",autoIncrement:!0}).createIndex("submitted","submitted",{unique:!1}),l.info("Logs store created"))}})}async log(r,e,n="PLAYER",t=null){if(!this.db){console.warn("[LogReporter] Database not initialized, dropping log entry");return}["error","warning","audit","info","debug"].includes(r)||(r="info");const o={level:r,message:e,category:n,timestamp:new Date,submitted:0};t&&(t.alertType&&(o.alertType=t.alertType),t.eventType&&(o.eventType=t.eventType));try{await this._saveLog(o)}catch(a){throw console.error("[LogReporter] Failed to save log entry:",a),a}}async reportFault(r,e,n=3e5){const t=this._reportedFaults.get(r);t&&Date.now()-t<n||(this._reportedFaults.set(r,Date.now()),await this.log("error",e,"PLAYER",{alertType:"Player Fault",eventType:r}),l.info(`Fault reported: ${r} - ${e}`))}async error(r,e="PLAYER"){return this.log("error",r,e)}async audit(r,e="PLAYER"){return this.log("audit",r,e)}async info(r,e="PLAYER"){return this.log("info",r,e)}async debug(r,e="PLAYER"){return this.log("debug",r,e)}async getLogsForSubmission(r=100){return this.db?new Promise((e,n)=>{const a=this.db.transaction([b],"readonly").objectStore(b).index("submitted").openCursor(IDBKeyRange.only(0)),c=[];a.onsuccess=u=>{const h=u.target.result;h&&c.length<r?(c.push(h.value),h.continue()):(l.debug(`Retrieved ${c.length} unsubmitted logs`),e(c))},a.onerror=()=>{l.error("Failed to retrieve logs:",a.error),n(new Error(`Failed to retrieve logs: ${a.error}`))}}):(l.warn("Logs database not initialized"),[])}async clearSubmittedLogs(r){if(!this.db){l.warn("Logs database not initialized");return}if(!(!r||r.length===0))return new Promise((e,n)=>{const t=this.db.transaction([b],"readwrite"),s=t.objectStore(b);let o=0;r.forEach(a=>{if(a.id){const c=s.delete(a.id);c.onsuccess=()=>{o++},c.onerror=()=>{l.error(`Failed to delete log ${a.id}:`,c.error)}}}),t.oncomplete=()=>{l.debug(`Deleted ${o} submitted logs`),e()},t.onerror=()=>{l.error("Failed to delete submitted logs:",t.error),n(new Error(`Failed to delete logs: ${t.error}`))}})}async getAllLogs(){return this.db?new Promise((r,e)=>{const s=this.db.transaction([b],"readonly").objectStore(b).getAll();s.onsuccess=()=>{r(s.result)},s.onerror=()=>{l.error("Failed to get all logs:",s.error),e(new Error(`Failed to get all logs: ${s.error}`))}}):(l.warn("Logs database not initialized"),[])}async clearAllLogs(){if(!this.db){l.warn("Logs database not initialized");return}return new Promise((r,e)=>{const s=this.db.transaction([b],"readwrite").objectStore(b).clear();s.onsuccess=()=>{l.debug("Cleared all logs"),r()},s.onerror=()=>{l.error("Failed to clear all logs:",s.error),e(new Error(`Failed to clear logs: ${s.error}`))}})}async _saveLog(r){return new Promise((e,n)=>{const s=this.db.transaction([b],"readwrite").objectStore(b),o=s.add(r);o.onsuccess=()=>{e(o.result)},o.onerror=()=>{o.error.name==="QuotaExceededError"?(console.warn("[LogReporter] IndexedDB quota exceeded - cleaning old logs"),this._cleanOldLogs().then(()=>{const a=s.add(r);a.onsuccess=()=>e(a.result),a.onerror=()=>n(a.error)}).catch(n)):n(o.error)}})}async _cleanOldLogs(){if(this.db)return new Promise((r,e)=>{const t=this.db.transaction([b],"readwrite").objectStore(b),o=t.index("submitted").openCursor(1),a=[];o.onsuccess=c=>{const u=c.target.result;u&&a.length<100?(a.push(u.value.id),u.continue()):(a.forEach(h=>{t.delete(h)}),console.log(`[LogReporter] Cleaned ${a.length} old logs due to quota`),r())},o.onerror=()=>{console.error("[LogReporter] Failed to clean old logs:",o.error),e(o.error)}})}}function E(i){return!i||i.length===0?"<logs></logs>":`<logs>
|
|
5
|
+
${i.map(e=>{const n=F(e.timestamp),t=[`date="${p(n)}"`,`category="${p(e.category)}"`,`type="${p(e.level)}"`,`message="${p(e.message)}"`];return e.alertType&&t.push(`alertType="${p(e.alertType)}"`),e.eventType&&t.push(`eventType="${p(e.eventType)}"`),` <log ${t.join(" ")} />`}).join(`
|
|
6
|
+
`)}
|
|
7
|
+
</logs>`}function F(i){i instanceof Date||(i=new Date(i));const r=i.getFullYear(),e=String(i.getMonth()+1).padStart(2,"0"),n=String(i.getDate()).padStart(2,"0"),t=String(i.getHours()).padStart(2,"0"),s=String(i.getMinutes()).padStart(2,"0"),o=String(i.getSeconds()).padStart(2,"0");return`${r}-${e}-${n} ${t}:${s}:${o}`}function p(i){return typeof i!="string"?i:i.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}export{x as LogReporter,P as StatsCollector,E as formatLogs,L as formatStats};
|
|
8
|
+
//# sourceMappingURL=index-CTmjUTVM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-CTmjUTVM.js","sources":["../../node_modules/.pnpm/@xiboplayer+stats@0.1.0/node_modules/@xiboplayer/stats/src/stats-collector.js","../../node_modules/.pnpm/@xiboplayer+stats@0.1.0/node_modules/@xiboplayer/stats/src/log-reporter.js"],"sourcesContent":["/**\n * StatsCollector - Proof of play tracking for Xibo CMS\n *\n * Tracks layout and widget playback for reporting to CMS via XMDS.\n * Uses IndexedDB for persistent storage across sessions.\n *\n * @module @xiboplayer/stats/collector\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('@xiboplayer/stats');\n\n// IndexedDB configuration\nconst DB_NAME = 'xibo-player-stats';\nconst DB_VERSION = 1;\nconst STATS_STORE = 'stats';\n\n/**\n * Stats collector for proof of play tracking\n *\n * Stores layout and widget playback statistics in IndexedDB.\n * Stats are submitted to CMS via XMDS SubmitStats API.\n *\n * @example\n * const collector = new StatsCollector();\n * await collector.init();\n *\n * // Track layout\n * await collector.startLayout(123, 456);\n * // ... layout plays ...\n * await collector.endLayout(123, 456);\n *\n * // Get stats for submission\n * const stats = await collector.getStatsForSubmission(50);\n * const xml = formatStats(stats);\n * // ... submit to CMS ...\n * await collector.clearSubmittedStats(stats);\n */\nexport class StatsCollector {\n constructor() {\n this.db = null;\n this.inProgressStats = new Map(); // Track in-progress stats by key\n }\n\n /**\n * Initialize IndexedDB\n *\n * Creates stats store with index on 'submitted' field for fast queries.\n * Safe to call multiple times (idempotent).\n *\n * @returns {Promise<void>}\n * @throws {Error} If IndexedDB is not available or initialization fails\n */\n async init() {\n if (this.db) {\n log.debug('Stats collector already initialized');\n return;\n }\n\n return new Promise((resolve, reject) => {\n // Check if IndexedDB is available\n if (typeof indexedDB === 'undefined') {\n const error = new Error('IndexedDB not available');\n log.error('IndexedDB not available - stats will not be persisted');\n reject(error);\n return;\n }\n\n const request = indexedDB.open(DB_NAME, DB_VERSION);\n\n request.onerror = () => {\n const error = new Error(`Failed to open IndexedDB: ${request.error}`);\n log.error('Failed to open stats database:', request.error);\n reject(error);\n };\n\n request.onsuccess = () => {\n this.db = request.result;\n log.info('Stats database initialized');\n resolve();\n };\n\n request.onupgradeneeded = (event) => {\n const db = event.target.result;\n\n // Create stats store if it doesn't exist\n if (!db.objectStoreNames.contains(STATS_STORE)) {\n const store = db.createObjectStore(STATS_STORE, {\n keyPath: 'id',\n autoIncrement: true\n });\n\n // Index on 'submitted' for fast queries\n store.createIndex('submitted', 'submitted', { unique: false });\n\n log.info('Stats store created');\n }\n };\n });\n }\n\n /**\n * Start tracking a layout\n *\n * Creates a new layout stat entry and tracks it as in-progress.\n * If a layout with the same ID is already in progress (replay),\n * silently ends the previous cycle and starts a new one.\n *\n * @param {number} layoutId - Layout ID from CMS\n * @param {number} scheduleId - Schedule ID that triggered this layout\n * @returns {Promise<void>}\n */\n async startLayout(layoutId, scheduleId) {\n if (!this.db) {\n log.warn('Stats database not initialized');\n return;\n }\n\n // Key excludes scheduleId: only one layout instance can be in-progress at a time,\n // and scheduleId may change mid-play when a collection cycle completes.\n const key = `layout-${layoutId}`;\n\n // Layout replay: end previous cycle silently before starting new one\n if (this.inProgressStats.has(key)) {\n const prev = this.inProgressStats.get(key);\n prev.end = new Date();\n prev.duration = Math.floor((prev.end - prev.start) / 1000);\n await this._saveStat(prev);\n this.inProgressStats.delete(key);\n log.debug(`Layout ${layoutId} replay - ended previous cycle (${prev.duration}s)`);\n }\n\n const stat = {\n type: 'layout',\n layoutId,\n scheduleId,\n start: new Date(),\n end: null,\n duration: 0,\n count: 1,\n submitted: 0 // Use 0/1 instead of boolean for IndexedDB compatibility\n };\n\n this.inProgressStats.set(key, stat);\n log.debug(`Started tracking layout ${layoutId} (schedule ${scheduleId})`);\n }\n\n /**\n * End tracking a layout\n *\n * Finalizes the layout stat entry and saves it to IndexedDB.\n * Calculates duration in seconds.\n *\n * @param {number} layoutId - Layout ID from CMS\n * @param {number} scheduleId - Schedule ID that triggered this layout\n * @returns {Promise<void>}\n */\n async endLayout(layoutId, scheduleId) {\n if (!this.db) {\n log.warn('Stats database not initialized');\n return;\n }\n\n const key = `layout-${layoutId}`;\n const stat = this.inProgressStats.get(key);\n\n if (!stat) {\n log.debug(`Layout ${layoutId} not found in progress (may have been ended by replay)`);\n return;\n }\n\n // Calculate duration in seconds\n stat.end = new Date();\n stat.duration = Math.floor((stat.end - stat.start) / 1000);\n\n // Save to database\n try {\n await this._saveStat(stat);\n this.inProgressStats.delete(key);\n log.debug(`Ended tracking layout ${layoutId} (${stat.duration}s)`);\n } catch (error) {\n log.error(`Failed to save layout stat ${layoutId}:`, error);\n throw error;\n }\n }\n\n /**\n * Start tracking a widget/media\n *\n * Creates a new media stat entry and tracks it as in-progress.\n * If a widget with the same key is already in progress (replay),\n * silently ends the previous cycle and starts a new one.\n *\n * @param {number} mediaId - Media ID from CMS\n * @param {number} layoutId - Parent layout ID\n * @param {number} scheduleId - Schedule ID\n * @returns {Promise<void>}\n */\n async startWidget(mediaId, layoutId, scheduleId) {\n if (!this.db) {\n log.warn('Stats database not initialized');\n return;\n }\n\n // Key excludes scheduleId: it may change mid-play during collection cycles.\n const key = `media-${mediaId}-${layoutId}`;\n\n // Widget replay: end previous cycle silently before starting new one\n if (this.inProgressStats.has(key)) {\n const prev = this.inProgressStats.get(key);\n prev.end = new Date();\n prev.duration = Math.floor((prev.end - prev.start) / 1000);\n await this._saveStat(prev);\n this.inProgressStats.delete(key);\n log.debug(`Widget ${mediaId} replay - ended previous cycle (${prev.duration}s)`);\n }\n\n const stat = {\n type: 'media',\n mediaId,\n layoutId,\n scheduleId,\n start: new Date(),\n end: null,\n duration: 0,\n count: 1,\n submitted: 0 // Use 0/1 instead of boolean for IndexedDB compatibility\n };\n\n this.inProgressStats.set(key, stat);\n log.debug(`Started tracking widget ${mediaId} in layout ${layoutId}`);\n }\n\n /**\n * End tracking a widget/media\n *\n * Finalizes the media stat entry and saves it to IndexedDB.\n * Calculates duration in seconds.\n *\n * @param {number} mediaId - Media ID from CMS\n * @param {number} layoutId - Parent layout ID\n * @param {number} scheduleId - Schedule ID\n * @returns {Promise<void>}\n */\n async endWidget(mediaId, layoutId, scheduleId) {\n if (!this.db) {\n log.warn('Stats database not initialized');\n return;\n }\n\n const key = `media-${mediaId}-${layoutId}`;\n const stat = this.inProgressStats.get(key);\n\n if (!stat) {\n log.debug(`Widget ${mediaId} not found in progress (expected during layout transitions)`);\n return;\n }\n\n // Calculate duration in seconds\n stat.end = new Date();\n stat.duration = Math.floor((stat.end - stat.start) / 1000);\n\n // Save to database\n try {\n await this._saveStat(stat);\n this.inProgressStats.delete(key);\n log.debug(`Ended tracking widget ${mediaId} (${stat.duration}s)`);\n } catch (error) {\n log.error(`Failed to save widget stat ${mediaId}:`, error);\n throw error;\n }\n }\n\n /**\n * Get stats ready for submission to CMS\n *\n * Returns unsubmitted stats up to the specified limit.\n * Stats are ordered by ID (oldest first).\n *\n * @param {number} limit - Maximum number of stats to return (default: 50)\n * @returns {Promise<Array>} Array of stat objects\n */\n async getStatsForSubmission(limit = 50) {\n if (!this.db) {\n log.warn('Stats database not initialized');\n return [];\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([STATS_STORE], 'readonly');\n const store = transaction.objectStore(STATS_STORE);\n const index = store.index('submitted');\n\n // Query for unsubmitted stats (0 = false)\n const request = index.openCursor(IDBKeyRange.only(0));\n const stats = [];\n\n request.onsuccess = (event) => {\n const cursor = event.target.result;\n\n if (cursor && stats.length < limit) {\n stats.push(cursor.value);\n cursor.continue();\n } else {\n log.debug(`Retrieved ${stats.length} unsubmitted stats`);\n resolve(stats);\n }\n };\n\n request.onerror = () => {\n log.error('Failed to retrieve stats:', request.error);\n reject(new Error(`Failed to retrieve stats: ${request.error}`));\n };\n });\n }\n\n /**\n * Clear submitted stats from database\n *\n * Deletes stats that were successfully submitted to CMS.\n *\n * @param {Array} stats - Array of stat objects to delete\n * @returns {Promise<void>}\n */\n async clearSubmittedStats(stats) {\n if (!this.db) {\n log.warn('Stats database not initialized');\n return;\n }\n\n if (!stats || stats.length === 0) {\n return;\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([STATS_STORE], 'readwrite');\n const store = transaction.objectStore(STATS_STORE);\n\n let deletedCount = 0;\n\n stats.forEach((stat) => {\n if (stat.id) {\n const request = store.delete(stat.id);\n request.onsuccess = () => {\n deletedCount++;\n };\n request.onerror = () => {\n log.error(`Failed to delete stat ${stat.id}:`, request.error);\n };\n }\n });\n\n transaction.oncomplete = () => {\n log.debug(`Deleted ${deletedCount} submitted stats`);\n resolve();\n };\n\n transaction.onerror = () => {\n log.error('Failed to delete submitted stats:', transaction.error);\n reject(new Error(`Failed to delete stats: ${transaction.error}`));\n };\n });\n }\n\n /**\n * Get aggregated stats for submission\n *\n * Groups stats by (type, layoutId, mediaId, scheduleId, hour) and sums\n * durations/counts. Used when CMS aggregationLevel is 'Aggregate'.\n *\n * @param {number} limit - Maximum number of raw stats to read (default: 50)\n * @returns {Promise<Array>} Aggregated stat objects\n */\n async getAggregatedStatsForSubmission(limit = 50) {\n const rawStats = await this.getStatsForSubmission(limit);\n if (rawStats.length === 0) return [];\n\n // Group by (type, layoutId, mediaId, scheduleId, hour)\n const groups = new Map();\n for (const stat of rawStats) {\n const hour = stat.start instanceof Date\n ? stat.start.toISOString().slice(0, 13)\n : new Date(stat.start).toISOString().slice(0, 13);\n const key = `${stat.type}|${stat.layoutId}|${stat.mediaId || ''}|${stat.scheduleId}|${hour}`;\n\n if (groups.has(key)) {\n const group = groups.get(key);\n group.count += stat.count || 1;\n group.duration += stat.duration || 0;\n // Keep earliest start and latest end\n const statStart = stat.start instanceof Date ? stat.start : new Date(stat.start);\n const statEnd = stat.end instanceof Date ? stat.end : new Date(stat.end || stat.start);\n if (statStart < group.start) group.start = statStart;\n if (statEnd > group.end) group.end = statEnd;\n group._rawIds.push(stat.id);\n } else {\n groups.set(key, {\n ...stat,\n start: stat.start instanceof Date ? stat.start : new Date(stat.start),\n end: stat.end instanceof Date ? stat.end : new Date(stat.end || stat.start),\n count: stat.count || 1,\n _rawIds: [stat.id]\n });\n }\n }\n\n return Array.from(groups.values());\n }\n\n /**\n * Get all stats (for debugging)\n *\n * @returns {Promise<Array>} All stats in database\n */\n async getAllStats() {\n if (!this.db) {\n log.warn('Stats database not initialized');\n return [];\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([STATS_STORE], 'readonly');\n const store = transaction.objectStore(STATS_STORE);\n const request = store.getAll();\n\n request.onsuccess = () => {\n resolve(request.result);\n };\n\n request.onerror = () => {\n log.error('Failed to get all stats:', request.error);\n reject(new Error(`Failed to get all stats: ${request.error}`));\n };\n });\n }\n\n /**\n * Clear all stats (for testing)\n *\n * @returns {Promise<void>}\n */\n async clearAllStats() {\n if (!this.db) {\n log.warn('Stats database not initialized');\n return;\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([STATS_STORE], 'readwrite');\n const store = transaction.objectStore(STATS_STORE);\n const request = store.clear();\n\n request.onsuccess = () => {\n log.debug('Cleared all stats');\n this.inProgressStats.clear();\n resolve();\n };\n\n request.onerror = () => {\n log.error('Failed to clear all stats:', request.error);\n reject(new Error(`Failed to clear stats: ${request.error}`));\n };\n });\n }\n\n /**\n * Save a stat to IndexedDB\n * @private\n */\n async _saveStat(stat) {\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([STATS_STORE], 'readwrite');\n const store = transaction.objectStore(STATS_STORE);\n const request = store.add(stat);\n\n request.onsuccess = () => {\n resolve(request.result);\n };\n\n request.onerror = () => {\n // Check for quota exceeded error\n if (request.error.name === 'QuotaExceededError') {\n log.error('IndexedDB quota exceeded - cleaning old stats');\n this._cleanOldStats().then(() => {\n // Retry once after cleanup\n const retryRequest = store.add(stat);\n retryRequest.onsuccess = () => resolve(retryRequest.result);\n retryRequest.onerror = () => reject(retryRequest.error);\n }).catch(reject);\n } else {\n reject(request.error);\n }\n };\n });\n }\n\n /**\n * Clean old stats when quota is exceeded\n * Deletes oldest 100 submitted stats\n * @private\n */\n async _cleanOldStats() {\n if (!this.db) {\n return;\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([STATS_STORE], 'readwrite');\n const store = transaction.objectStore(STATS_STORE);\n const index = store.index('submitted');\n\n // Get oldest 100 submitted stats (use 1 for boolean true in IndexedDB)\n const request = index.openCursor(1);\n const toDelete = [];\n\n request.onsuccess = (event) => {\n const cursor = event.target.result;\n\n if (cursor && toDelete.length < 100) {\n toDelete.push(cursor.value.id);\n cursor.continue();\n } else {\n // Delete collected IDs\n toDelete.forEach((id) => {\n store.delete(id);\n });\n\n log.info(`Cleaned ${toDelete.length} old stats due to quota`);\n resolve();\n }\n };\n\n request.onerror = () => {\n log.error('Failed to clean old stats:', request.error);\n reject(request.error);\n };\n });\n }\n}\n\n/**\n * Format stats as XML for XMDS submission\n *\n * Converts array of stat objects to XML format expected by CMS.\n *\n * XML format:\n * ```xml\n * <stats>\n * <stat type=\"layout\" fromdt=\"2026-02-10 12:00:00\" todt=\"2026-02-10 12:05:00\"\n * scheduleid=\"123\" layoutid=\"456\" count=\"1\" duration=\"300\" />\n * <stat type=\"media\" fromdt=\"2026-02-10 12:00:00\" todt=\"2026-02-10 12:01:00\"\n * scheduleid=\"123\" layoutid=\"456\" mediaid=\"789\" count=\"1\" duration=\"60\" />\n * </stats>\n * ```\n *\n * @param {Array} stats - Array of stat objects from getStatsForSubmission()\n * @returns {string} XML string for XMDS SubmitStats\n *\n * @example\n * const stats = await collector.getStatsForSubmission(50);\n * const xml = formatStats(stats);\n * await xmds.submitStats(xml);\n */\nexport function formatStats(stats) {\n if (!stats || stats.length === 0) {\n return '<stats></stats>';\n }\n\n const statElements = stats.map((stat) => {\n // Format dates as \"YYYY-MM-DD HH:MM:SS\"\n const fromdt = formatDateTime(stat.start);\n const todt = formatDateTime(stat.end || stat.start);\n\n // Build attributes\n const attrs = [\n `type=\"${escapeXml(stat.type)}\"`,\n `fromdt=\"${escapeXml(fromdt)}\"`,\n `todt=\"${escapeXml(todt)}\"`,\n `scheduleid=\"${stat.scheduleId}\"`,\n `layoutid=\"${stat.layoutId}\"`,\n ];\n\n // Add mediaId for media stats\n if (stat.type === 'media' && stat.mediaId) {\n attrs.push(`mediaid=\"${stat.mediaId}\"`);\n }\n\n // Add count and duration\n attrs.push(`count=\"${stat.count}\"`);\n attrs.push(`duration=\"${stat.duration}\"`);\n\n return ` <stat ${attrs.join(' ')} />`;\n });\n\n return `<stats>\\n${statElements.join('\\n')}\\n</stats>`;\n}\n\n/**\n * Format Date object as \"YYYY-MM-DD HH:MM:SS\"\n * @private\n */\nfunction formatDateTime(date) {\n if (!(date instanceof Date)) {\n date = new Date(date);\n }\n\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n const hours = String(date.getHours()).padStart(2, '0');\n const minutes = String(date.getMinutes()).padStart(2, '0');\n const seconds = String(date.getSeconds()).padStart(2, '0');\n\n return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;\n}\n\n/**\n * Escape XML special characters\n * @private\n */\nfunction escapeXml(str) {\n if (typeof str !== 'string') {\n return str;\n }\n\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","/**\n * LogReporter - CMS logging for Xibo Players\n *\n * Collects and submits logs to CMS via XMDS.\n * Uses IndexedDB for persistent storage across sessions.\n *\n * @module @xiboplayer/stats/logger\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('@xiboplayer/stats');\n\n// IndexedDB configuration\nconst DB_NAME = 'xibo-player-logs';\nconst DB_VERSION = 1;\nconst LOGS_STORE = 'logs';\n\n/**\n * Log reporter for CMS logging\n *\n * Stores log entries in IndexedDB and submits to CMS via XMDS.\n * Supports multiple log levels: error, audit, info, debug.\n *\n * @example\n * const reporter = new LogReporter();\n * await reporter.init();\n *\n * // Log messages\n * await reporter.error('Failed to load layout', 'PLAYER');\n * await reporter.info('Layout loaded successfully', 'PLAYER');\n *\n * // Get logs for submission\n * const logs = await reporter.getLogsForSubmission(100);\n * const xml = formatLogs(logs);\n * // ... submit to CMS ...\n * await reporter.clearSubmittedLogs(logs);\n */\nexport class LogReporter {\n constructor() {\n this.db = null;\n this._reportedFaults = new Map(); // code -> timestamp (deduplication)\n }\n\n /**\n * Initialize IndexedDB\n *\n * Creates logs store with index on 'submitted' field for fast queries.\n * Safe to call multiple times (idempotent).\n *\n * @returns {Promise<void>}\n * @throws {Error} If IndexedDB is not available or initialization fails\n */\n async init() {\n if (this.db) {\n log.debug('Log reporter already initialized');\n return;\n }\n\n return new Promise((resolve, reject) => {\n // Check if IndexedDB is available\n if (typeof indexedDB === 'undefined') {\n const error = new Error('IndexedDB not available');\n log.error('IndexedDB not available - logs will not be persisted');\n reject(error);\n return;\n }\n\n const request = indexedDB.open(DB_NAME, DB_VERSION);\n\n request.onerror = () => {\n const error = new Error(`Failed to open IndexedDB: ${request.error}`);\n log.error('Failed to open logs database:', request.error);\n reject(error);\n };\n\n request.onsuccess = () => {\n this.db = request.result;\n log.info('Logs database initialized');\n resolve();\n };\n\n request.onupgradeneeded = (event) => {\n const db = event.target.result;\n\n // Create logs store if it doesn't exist\n if (!db.objectStoreNames.contains(LOGS_STORE)) {\n const store = db.createObjectStore(LOGS_STORE, {\n keyPath: 'id',\n autoIncrement: true\n });\n\n // Index on 'submitted' for fast queries\n store.createIndex('submitted', 'submitted', { unique: false });\n\n log.info('Logs store created');\n }\n };\n });\n }\n\n /**\n * Log a message\n *\n * Stores a log entry in IndexedDB for later submission to CMS.\n *\n * @param {string} level - Log level: 'error', 'audit', 'info', or 'debug'\n * @param {string} message - Log message\n * @param {string} category - Log category (default: 'PLAYER')\n * @param {Object} [extra] - Optional extra fields (alertType, eventType)\n * @returns {Promise<void>}\n */\n async log(level, message, category = 'PLAYER', extra = null) {\n if (!this.db) {\n // Use console directly — NOT the logger — to avoid infinite feedback loop.\n // The logger dispatches to log sinks, and this method IS the sink target.\n console.warn('[LogReporter] Database not initialized, dropping log entry');\n return;\n }\n\n // Validate log level\n const validLevels = ['error', 'warning', 'audit', 'info', 'debug'];\n if (!validLevels.includes(level)) {\n level = 'info';\n }\n\n const logEntry = {\n level,\n message,\n category,\n timestamp: new Date(),\n submitted: 0 // Use 0/1 instead of boolean for IndexedDB compatibility\n };\n\n // Add alert fields for faults (triggers CMS dashboard alerts)\n if (extra) {\n if (extra.alertType) logEntry.alertType = extra.alertType;\n if (extra.eventType) logEntry.eventType = extra.eventType;\n }\n\n try {\n await this._saveLog(logEntry);\n // NOTE: Do NOT call log.debug() here — it dispatches to sinks, which call\n // logReporter.log() again, creating an infinite async loop.\n } catch (error) {\n // Use console directly to avoid feedback loop\n console.error('[LogReporter] Failed to save log entry:', error);\n throw error;\n }\n }\n\n /**\n * Report a fault to CMS (special log entry that triggers alerts)\n *\n * Faults are log entries with alertType/eventType fields that cause the\n * CMS to show alerts on the display dashboard and optionally send emails.\n * Deduplicates by code: same fault code won't be reported again within\n * the cooldown period (default 5 minutes).\n *\n * @param {string} code - Fault code (e.g., 'LAYOUT_LOAD_FAILED')\n * @param {string} reason - Human-readable description\n * @param {number} [cooldownMs=300000] - Dedup cooldown in ms (default 5 min)\n * @returns {Promise<void>}\n */\n async reportFault(code, reason, cooldownMs = 300000) {\n // Deduplication: skip if same code was reported recently\n const lastReported = this._reportedFaults.get(code);\n if (lastReported && (Date.now() - lastReported) < cooldownMs) {\n return;\n }\n\n this._reportedFaults.set(code, Date.now());\n\n await this.log('error', reason, 'PLAYER', {\n alertType: 'Player Fault',\n eventType: code\n });\n\n log.info(`Fault reported: ${code} - ${reason}`);\n }\n\n /**\n * Log an error message\n *\n * Shorthand for log('error', message, category)\n *\n * @param {string} message - Error message\n * @param {string} category - Log category (default: 'PLAYER')\n * @returns {Promise<void>}\n */\n async error(message, category = 'PLAYER') {\n return this.log('error', message, category);\n }\n\n /**\n * Log an audit message\n *\n * Shorthand for log('audit', message, category)\n *\n * @param {string} message - Audit message\n * @param {string} category - Log category (default: 'PLAYER')\n * @returns {Promise<void>}\n */\n async audit(message, category = 'PLAYER') {\n return this.log('audit', message, category);\n }\n\n /**\n * Log an info message\n *\n * Shorthand for log('info', message, category)\n *\n * @param {string} message - Info message\n * @param {string} category - Log category (default: 'PLAYER')\n * @returns {Promise<void>}\n */\n async info(message, category = 'PLAYER') {\n return this.log('info', message, category);\n }\n\n /**\n * Log a debug message\n *\n * Shorthand for log('debug', message, category)\n *\n * @param {string} message - Debug message\n * @param {string} category - Log category (default: 'PLAYER')\n * @returns {Promise<void>}\n */\n async debug(message, category = 'PLAYER') {\n return this.log('debug', message, category);\n }\n\n /**\n * Get logs ready for submission to CMS\n *\n * Returns unsubmitted logs up to the specified limit.\n * Logs are ordered by ID (oldest first).\n *\n * @param {number} limit - Maximum number of logs to return (default: 100)\n * @returns {Promise<Array>} Array of log objects\n */\n async getLogsForSubmission(limit = 100) {\n if (!this.db) {\n log.warn('Logs database not initialized');\n return [];\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([LOGS_STORE], 'readonly');\n const store = transaction.objectStore(LOGS_STORE);\n const index = store.index('submitted');\n\n // Query for unsubmitted logs (0 = false)\n const request = index.openCursor(IDBKeyRange.only(0));\n const logs = [];\n\n request.onsuccess = (event) => {\n const cursor = event.target.result;\n\n if (cursor && logs.length < limit) {\n logs.push(cursor.value);\n cursor.continue();\n } else {\n log.debug(`Retrieved ${logs.length} unsubmitted logs`);\n resolve(logs);\n }\n };\n\n request.onerror = () => {\n log.error('Failed to retrieve logs:', request.error);\n reject(new Error(`Failed to retrieve logs: ${request.error}`));\n };\n });\n }\n\n /**\n * Clear submitted logs from database\n *\n * Deletes logs that were successfully submitted to CMS.\n *\n * @param {Array} logs - Array of log objects to delete\n * @returns {Promise<void>}\n */\n async clearSubmittedLogs(logs) {\n if (!this.db) {\n log.warn('Logs database not initialized');\n return;\n }\n\n if (!logs || logs.length === 0) {\n return;\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([LOGS_STORE], 'readwrite');\n const store = transaction.objectStore(LOGS_STORE);\n\n let deletedCount = 0;\n\n logs.forEach((logEntry) => {\n if (logEntry.id) {\n const request = store.delete(logEntry.id);\n request.onsuccess = () => {\n deletedCount++;\n };\n request.onerror = () => {\n log.error(`Failed to delete log ${logEntry.id}:`, request.error);\n };\n }\n });\n\n transaction.oncomplete = () => {\n log.debug(`Deleted ${deletedCount} submitted logs`);\n resolve();\n };\n\n transaction.onerror = () => {\n log.error('Failed to delete submitted logs:', transaction.error);\n reject(new Error(`Failed to delete logs: ${transaction.error}`));\n };\n });\n }\n\n /**\n * Get all logs (for debugging)\n *\n * @returns {Promise<Array>} All logs in database\n */\n async getAllLogs() {\n if (!this.db) {\n log.warn('Logs database not initialized');\n return [];\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([LOGS_STORE], 'readonly');\n const store = transaction.objectStore(LOGS_STORE);\n const request = store.getAll();\n\n request.onsuccess = () => {\n resolve(request.result);\n };\n\n request.onerror = () => {\n log.error('Failed to get all logs:', request.error);\n reject(new Error(`Failed to get all logs: ${request.error}`));\n };\n });\n }\n\n /**\n * Clear all logs (for testing)\n *\n * @returns {Promise<void>}\n */\n async clearAllLogs() {\n if (!this.db) {\n log.warn('Logs database not initialized');\n return;\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([LOGS_STORE], 'readwrite');\n const store = transaction.objectStore(LOGS_STORE);\n const request = store.clear();\n\n request.onsuccess = () => {\n log.debug('Cleared all logs');\n resolve();\n };\n\n request.onerror = () => {\n log.error('Failed to clear all logs:', request.error);\n reject(new Error(`Failed to clear logs: ${request.error}`));\n };\n });\n }\n\n /**\n * Save a log entry to IndexedDB\n * @private\n */\n async _saveLog(logEntry) {\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([LOGS_STORE], 'readwrite');\n const store = transaction.objectStore(LOGS_STORE);\n const request = store.add(logEntry);\n\n request.onsuccess = () => {\n resolve(request.result);\n };\n\n request.onerror = () => {\n // Check for quota exceeded error\n if (request.error.name === 'QuotaExceededError') {\n console.warn('[LogReporter] IndexedDB quota exceeded - cleaning old logs');\n this._cleanOldLogs().then(() => {\n // Retry once after cleanup\n const retryRequest = store.add(logEntry);\n retryRequest.onsuccess = () => resolve(retryRequest.result);\n retryRequest.onerror = () => reject(retryRequest.error);\n }).catch(reject);\n } else {\n reject(request.error);\n }\n };\n });\n }\n\n /**\n * Clean old logs when quota is exceeded\n * Deletes oldest 100 submitted logs\n * @private\n */\n async _cleanOldLogs() {\n if (!this.db) {\n return;\n }\n\n return new Promise((resolve, reject) => {\n const transaction = this.db.transaction([LOGS_STORE], 'readwrite');\n const store = transaction.objectStore(LOGS_STORE);\n const index = store.index('submitted');\n\n // Get oldest 100 submitted logs (use 1 for boolean true in IndexedDB)\n const request = index.openCursor(1);\n const toDelete = [];\n\n request.onsuccess = (event) => {\n const cursor = event.target.result;\n\n if (cursor && toDelete.length < 100) {\n toDelete.push(cursor.value.id);\n cursor.continue();\n } else {\n // Delete collected IDs\n toDelete.forEach((id) => {\n store.delete(id);\n });\n\n console.log(`[LogReporter] Cleaned ${toDelete.length} old logs due to quota`);\n resolve();\n }\n };\n\n request.onerror = () => {\n console.error('[LogReporter] Failed to clean old logs:', request.error);\n reject(request.error);\n };\n });\n }\n}\n\n/**\n * Format logs as XML for XMDS submission\n *\n * Converts array of log objects to XML format expected by CMS.\n *\n * XML format:\n * ```xml\n * <logs>\n * <log date=\"2026-02-10 12:00:00\" category=\"PLAYER\" type=\"error\"\n * message=\"Failed to load layout 123\" />\n * </logs>\n * ```\n *\n * @param {Array} logs - Array of log objects from getLogsForSubmission()\n * @returns {string} XML string for XMDS SubmitLog\n *\n * @example\n * const logs = await reporter.getLogsForSubmission(100);\n * const xml = formatLogs(logs);\n * await xmds.submitLog(xml);\n */\nexport function formatLogs(logs) {\n if (!logs || logs.length === 0) {\n return '<logs></logs>';\n }\n\n const logElements = logs.map((logEntry) => {\n // Format date as \"YYYY-MM-DD HH:MM:SS\"\n const date = formatDateTime(logEntry.timestamp);\n\n // Build attributes\n const attrs = [\n `date=\"${escapeXml(date)}\"`,\n `category=\"${escapeXml(logEntry.category)}\"`,\n `type=\"${escapeXml(logEntry.level)}\"`,\n `message=\"${escapeXml(logEntry.message)}\"`\n ];\n\n // Fault alert fields (triggers CMS dashboard alerts)\n if (logEntry.alertType) {\n attrs.push(`alertType=\"${escapeXml(logEntry.alertType)}\"`);\n }\n if (logEntry.eventType) {\n attrs.push(`eventType=\"${escapeXml(logEntry.eventType)}\"`);\n }\n\n return ` <log ${attrs.join(' ')} />`;\n });\n\n return `<logs>\\n${logElements.join('\\n')}\\n</logs>`;\n}\n\n/**\n * Format Date object as \"YYYY-MM-DD HH:MM:SS\"\n * @private\n */\nfunction formatDateTime(date) {\n if (!(date instanceof Date)) {\n date = new Date(date);\n }\n\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n const hours = String(date.getHours()).padStart(2, '0');\n const minutes = String(date.getMinutes()).padStart(2, '0');\n const seconds = String(date.getSeconds()).padStart(2, '0');\n\n return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;\n}\n\n/**\n * Escape XML special characters\n * @private\n */\nfunction escapeXml(str) {\n if (typeof str !== 'string') {\n return str;\n }\n\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n"],"names":["log","createLogger","DB_NAME","DB_VERSION","STATS_STORE","StatsCollector","resolve","reject","error","request","event","db","layoutId","scheduleId","key","prev","stat","mediaId","limit","stats","cursor","transaction","store","deletedCount","rawStats","groups","hour","group","statStart","statEnd","retryRequest","toDelete","id","formatStats","fromdt","formatDateTime","todt","attrs","escapeXml","date","year","month","day","hours","minutes","seconds","str","LOGS_STORE","LogReporter","level","message","category","extra","logEntry","code","reason","cooldownMs","lastReported","logs","formatLogs"],"mappings":"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","x_google_ignoreList":[0,1]}
|