@xiboplayer/pwa 0.7.18 → 0.7.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/assets/{main-BG2cZsg-.js → main-BmtbYkOU.js} +4 -4
  2. package/dist/assets/{main-BG2cZsg-.js.map → main-BmtbYkOU.js.map} +1 -1
  3. package/dist/assets/main-ByurY8zd.js +3 -0
  4. package/dist/assets/{main-CkREX2X6.js.map → main-ByurY8zd.js.map} +1 -1
  5. package/dist/assets/{setup-COzyFqMn.js → setup-TzYPvQkI.js} +2 -2
  6. package/dist/assets/{setup-COzyFqMn.js.map → setup-TzYPvQkI.js.map} +1 -1
  7. package/dist/assets/{src-CfCGHb7u.js → src-BbJ9IAbR.js} +2 -2
  8. package/dist/assets/{src-CfCGHb7u.js.map → src-BbJ9IAbR.js.map} +1 -1
  9. package/dist/assets/{src-IJdgG16K.js → src-BhNSI3YP.js} +2 -2
  10. package/dist/assets/{src-IJdgG16K.js.map → src-BhNSI3YP.js.map} +1 -1
  11. package/dist/assets/{src-BdgQ2CiL.js → src-CR4vHRyW.js} +2 -2
  12. package/dist/assets/{src-BdgQ2CiL.js.map → src-CR4vHRyW.js.map} +1 -1
  13. package/dist/assets/{src-C3Sg89t9.js → src-CRWze-JF.js} +2 -2
  14. package/dist/assets/{src-C3Sg89t9.js.map → src-CRWze-JF.js.map} +1 -1
  15. package/dist/assets/{src-BF_sMbmn.js → src-CSIdq4vk.js} +2 -2
  16. package/dist/assets/{src-BF_sMbmn.js.map → src-CSIdq4vk.js.map} +1 -1
  17. package/dist/assets/{src-BV-4JdnK.js → src-DAFtXxFV.js} +2 -2
  18. package/dist/assets/{src-BV-4JdnK.js.map → src-DAFtXxFV.js.map} +1 -1
  19. package/dist/assets/{src-M1enQEwh.js → src-DLAmD0IH.js} +2 -2
  20. package/dist/assets/{src-M1enQEwh.js.map → src-DLAmD0IH.js.map} +1 -1
  21. package/dist/assets/{src-DnPIj2iO.js → src-Jz8Hu7ah.js} +2 -2
  22. package/dist/assets/{src-DnPIj2iO.js.map → src-Jz8Hu7ah.js.map} +1 -1
  23. package/dist/assets/{src-BYVnjdc0.js → src-S093VuQs.js} +2 -2
  24. package/dist/assets/{src-BYVnjdc0.js.map → src-S093VuQs.js.map} +1 -1
  25. package/dist/assets/{src-BA9Y85MN.js → src-cynIeb-W.js} +2 -2
  26. package/dist/assets/{src-BA9Y85MN.js.map → src-cynIeb-W.js.map} +1 -1
  27. package/dist/assets/{sync-manager-C68deMxf.js → sync-manager-CJQQp9_5.js} +2 -2
  28. package/dist/assets/{sync-manager-C68deMxf.js.map → sync-manager-CJQQp9_5.js.map} +1 -1
  29. package/dist/index.html +1 -1
  30. package/dist/setup.html +3 -3
  31. package/dist/sw-pwa.js +2 -2
  32. package/dist/sw-pwa.js.map +1 -1
  33. package/package.json +13 -13
  34. package/dist/assets/main-CkREX2X6.js +0 -3
@@ -1,2 +1,2 @@
1
- import{c as e,u as t}from"./src-C3Sg89t9.js";var n={name:`@xiboplayer/settings`,version:`0.7.18`,description:`CMS settings management and application for xiboplayer`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./manager":`./src/settings.js`},scripts:{test:`vitest run`,"test:watch":`vitest`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},devDependencies:{jsdom:`^29.0.2`,vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`settings`,`configuration`,`display-settings`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/settings`},homepage:`https://xiboplayer.org`},r=t(`DisplaySettings`),i=class extends e{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 r.warn(`No settings provided`),{changed:[],settings:this.settings};let t=[],n=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),n!==this.settings.collectInterval&&(t.push(`collectInterval`),this.emit(`interval-changed`,this.settings.collectInterval)),this.emit(`settings-applied`,this.settings,t),r.info(`Applied settings:`,{collectInterval:this.settings.collectInterval,displayName:this.settings.displayName,statsEnabled:this.settings.statsEnabled,changes:t}),{changed:t,settings:this.settings}}parseCollectInterval(e){let 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?t:this.settings[e]}isInDownloadWindow(){if(!this.settings.downloadStartWindow||!this.settings.downloadEndWindow||this.settings.downloadStartWindow===`:`||this.settings.downloadEndWindow===`:`)return!0;try{let e=new Date,t=e.getHours()*60+e.getMinutes(),n=this.parseTimeWindow(this.settings.downloadStartWindow),r=this.parseTimeWindow(this.settings.downloadEndWindow);return n>r?t>=n||t<r:t>=n&&t<r}catch(e){return r.warn(`Failed to parse download window:`,e),!0}}parseTimeWindow(e){if(!e||typeof e!=`string`)throw Error(`Invalid time window format`);let t=e.split(`:`);if(t.length!==2)throw Error(`Invalid time window format (expected HH:MM)`);let n=parseInt(t[0],10),r=parseInt(t[1],10);if(isNaN(n)||isNaN(r)||n<0||n>23||r<0||r>59)throw Error(`Invalid time window values`);return n*60+r}getNextDownloadWindow(){if(!this.settings.downloadStartWindow||!this.settings.downloadEndWindow||this.settings.downloadStartWindow===`:`||this.settings.downloadEndWindow===`:`)return null;try{let e=new Date,t=e.getHours()*60+e.getMinutes(),n=this.parseTimeWindow(this.settings.downloadStartWindow),r=new Date(e);return t<n||r.setDate(r.getDate()+1),r.setHours(Math.floor(n/60),n%60,0,0),r}catch(e){return r.warn(`Failed to calculate next download window:`,e),null}}shouldTakeScreenshot(e){return e?(Date.now()-e.getTime())/1e3>=this.settings.screenshotInterval:!0}},a=n.version;export{i as DisplaySettings,a as VERSION};
2
- //# sourceMappingURL=src-DnPIj2iO.js.map
1
+ import{c as e,u as t}from"./src-CRWze-JF.js";var n={name:`@xiboplayer/settings`,version:`0.7.19`,description:`CMS settings management and application for xiboplayer`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./manager":`./src/settings.js`},scripts:{test:`vitest run`,"test:watch":`vitest`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},devDependencies:{jsdom:`^29.0.2`,vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`settings`,`configuration`,`display-settings`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/settings`},homepage:`https://xiboplayer.org`},r=t(`DisplaySettings`),i=class extends e{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 r.warn(`No settings provided`),{changed:[],settings:this.settings};let t=[],n=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),n!==this.settings.collectInterval&&(t.push(`collectInterval`),this.emit(`interval-changed`,this.settings.collectInterval)),this.emit(`settings-applied`,this.settings,t),r.info(`Applied settings:`,{collectInterval:this.settings.collectInterval,displayName:this.settings.displayName,statsEnabled:this.settings.statsEnabled,changes:t}),{changed:t,settings:this.settings}}parseCollectInterval(e){let 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?t:this.settings[e]}isInDownloadWindow(){if(!this.settings.downloadStartWindow||!this.settings.downloadEndWindow||this.settings.downloadStartWindow===`:`||this.settings.downloadEndWindow===`:`)return!0;try{let e=new Date,t=e.getHours()*60+e.getMinutes(),n=this.parseTimeWindow(this.settings.downloadStartWindow),r=this.parseTimeWindow(this.settings.downloadEndWindow);return n>r?t>=n||t<r:t>=n&&t<r}catch(e){return r.warn(`Failed to parse download window:`,e),!0}}parseTimeWindow(e){if(!e||typeof e!=`string`)throw Error(`Invalid time window format`);let t=e.split(`:`);if(t.length!==2)throw Error(`Invalid time window format (expected HH:MM)`);let n=parseInt(t[0],10),r=parseInt(t[1],10);if(isNaN(n)||isNaN(r)||n<0||n>23||r<0||r>59)throw Error(`Invalid time window values`);return n*60+r}getNextDownloadWindow(){if(!this.settings.downloadStartWindow||!this.settings.downloadEndWindow||this.settings.downloadStartWindow===`:`||this.settings.downloadEndWindow===`:`)return null;try{let e=new Date,t=e.getHours()*60+e.getMinutes(),n=this.parseTimeWindow(this.settings.downloadStartWindow),r=new Date(e);return t<n||r.setDate(r.getDate()+1),r.setHours(Math.floor(n/60),n%60,0,0),r}catch(e){return r.warn(`Failed to calculate next download window:`,e),null}}shouldTakeScreenshot(e){return e?(Date.now()-e.getTime())/1e3>=this.settings.screenshotInterval:!0}},a=n.version;export{i as DisplaySettings,a as VERSION};
2
+ //# sourceMappingURL=src-Jz8Hu7ah.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"src-DnPIj2iO.js","names":["pkg"],"sources":["../../../settings/package.json","../../../settings/src/settings.js","../../../settings/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/settings\",\n \"version\": \"0.7.18\",\n \"description\": \"CMS settings management and application for xiboplayer\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.js\",\n \"./manager\": \"./src/settings.js\"\n },\n \"scripts\": {\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\"\n },\n \"dependencies\": {\n \"@xiboplayer/utils\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"jsdom\": \"^29.0.2\",\n \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"settings\",\n \"configuration\",\n \"display-settings\"\n ],\n \"author\": \"Pau Aliagas <linuxnow@gmail.com>\",\n \"license\": \"AGPL-3.0-or-later\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/xibo-players/xiboplayer.git\",\n \"directory\": \"packages/settings\"\n },\n \"homepage\": \"https://xiboplayer.org\"\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * 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, createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('DisplaySettings');\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 log.warn('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 log.info('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 // CMS sends \":\" when unconfigured, treat as empty\n if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow ||\n 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 log.warn('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 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 log.warn('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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/settings - CMS settings management\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\n\n/**\n * Settings manager for xiboplayer\n * @module @xiboplayer/settings\n */\nexport { DisplaySettings } from './settings.js';\n"],"mappings":"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"}
1
+ {"version":3,"file":"src-Jz8Hu7ah.js","names":["pkg"],"sources":["../../../settings/package.json","../../../settings/src/settings.js","../../../settings/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/settings\",\n \"version\": \"0.7.19\",\n \"description\": \"CMS settings management and application for xiboplayer\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.js\",\n \"./manager\": \"./src/settings.js\"\n },\n \"scripts\": {\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\"\n },\n \"dependencies\": {\n \"@xiboplayer/utils\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"jsdom\": \"^29.0.2\",\n \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"settings\",\n \"configuration\",\n \"display-settings\"\n ],\n \"author\": \"Pau Aliagas <linuxnow@gmail.com>\",\n \"license\": \"AGPL-3.0-or-later\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/xibo-players/xiboplayer.git\",\n \"directory\": \"packages/settings\"\n },\n \"homepage\": \"https://xiboplayer.org\"\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * 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, createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('DisplaySettings');\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 log.warn('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 log.info('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 // CMS sends \":\" when unconfigured, treat as empty\n if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow ||\n 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 log.warn('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 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 log.warn('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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/settings - CMS settings management\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\n\n/**\n * Settings manager for xiboplayer\n * @module @xiboplayer/settings\n */\nexport { DisplaySettings } from './settings.js';\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{n as e}from"./chunk-7ZXdHUL4.js";import{t,u as n}from"./src-C3Sg89t9.js";var r={name:`@xiboplayer/cache`,version:`0.7.18`,description:`Offline caching and download management with parallel chunk downloads`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./cache":`./src/cache.js`,"./store-client":`./src/store-client.js`,"./download-manager":`./src/download-manager.js`,"./cache-analyzer":`./src/cache-analyzer.js`,"./widget-html":`./src/widget-html.js`},scripts:{test:`vitest run`,"test:watch":`vitest`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`,"spark-md5":`^3.0.2`},devDependencies:{jsdom:`^29.0.2`,vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`cache`,`offline`,`download`,`indexeddb`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/cache`},homepage:`https://xiboplayer.org`},i=n(`Cache`),a=class{constructor(){this.dependants=new Map}addDependant(e,t){let n=String(e);this.dependants.has(n)||this.dependants.set(n,new Set),this.dependants.get(n).add(String(t))}removeLayoutDependants(e){let t=String(e),n=[];for(let[e,r]of this.dependants)r.delete(t),r.size===0&&(this.dependants.delete(e),n.push(e));return n.length>0&&i.info(`${n.length} media files orphaned after layout ${e} removed:`,n),n}isMediaReferenced(e){let t=this.dependants.get(String(e));return t?t.size>0:!1}async clearAll(){this.dependants.clear()}},o=new a,s=n(`StoreClient`),c=class{async has(e,t){try{let n=await fetch(`/store/${e}/${t}`,{method:`HEAD`});if(n.status===204)return!1;if(n.status===200)return n.headers.get(`X-Store-Complete`)!==`false`;if(n.status===404)return!1;let r=Error(`Store error: ${n.status}`);throw r.status=n.status,s.warn(`has(${e}/${t}): unexpected status ${n.status}`),r}catch(n){throw n.status||n.name===`AbortError`||n.name===`TimeoutError`||s.warn(`has(${e}/${t}): network error — ${n.message}`),n}}async get(e,t){try{let n=await fetch(`/store/${e}/${t}`);if(n.status===204||n.status===404)return n.body?.cancel(),null;if(!n.ok)throw n.body?.cancel(),Error(`Failed to get file: ${n.status}`);return await n.blob()}catch(e){return s.error(`get error:`,e.message),null}}async put(e,t,n,r=`application/octet-stream`){try{let i=await fetch(`/store/${e}/${t}`,{method:`PUT`,headers:{"Content-Type":r},body:n});return i.body?.cancel(),i.ok}catch(e){return s.error(`put error:`,e.message),!1}}async remove(e){try{let t=await(await fetch(`/store/delete`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({files:e})})).json();return{deleted:t.deleted||0,total:t.total||e.length}}catch(t){return s.error(`remove error:`,t.message),{deleted:0,total:e.length}}}async list(){try{return(await(await fetch(`/store/list`)).json()).files||[]}catch(e){return s.error(`list error:`,e.message),[]}}},l={media:{maxRetries:3,retryDelayMs:500,retryDelays:null,maxReenqueues:0,reenqueueDelayMs:0,skipHead:!1,cacheTtl:1/0},layout:{maxRetries:3,retryDelayMs:500,retryDelays:null,maxReenqueues:0,reenqueueDelayMs:0,skipHead:!1,cacheTtl:1/0},dataset:{maxRetries:4,retryDelayMs:0,retryDelays:[15e3,3e4,6e4,12e4],maxReenqueues:5,reenqueueDelayMs:6e4,skipHead:!0,cacheTtl:300},static:{maxRetries:3,retryDelayMs:500,retryDelays:null,maxReenqueues:0,reenqueueDelayMs:0,skipHead:!1,cacheTtl:1/0}};function u(e){return l[e]||l.media}var d=n(`Download`),f=6,p=50*1024*1024,m=3,h=100*1024*1024,g=2,_=6e5,v=15e3;function y(e){return{mp4:`video/mp4`,webm:`video/webm`,mp3:`audio/mpeg`,png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,svg:`image/svg+xml`,webp:`image/webp`,css:`text/css`,js:`application/javascript`,ttf:`font/ttf`,otf:`font/otf`,woff:`font/woff`,woff2:`font/woff2`,xml:`application/xml`,xlf:`application/xml`}[(e.path||e.code||``).split(`.`).pop()?.split(`?`)[0]?.toLowerCase()]||`application/octet-stream`}var b={normal:0,layout:1,high:2,urgent:3},x=Symbol(`BARRIER`);function S(e){try{let t=new URL(e,`http://localhost`),n=t.searchParams.get(`X-Amz-Date`),r=t.searchParams.get(`X-Amz-Expires`);if(n&&r){let e=n.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/,`$1-$2-$3T$4:$5:$6Z`),t=new Date(e).getTime()/1e3;return isNaN(t)?1/0:t+parseInt(r,10)}return 1/0}catch{return 1/0}}function C(e,t=30){let n=S(e);return n===1/0?!1:Date.now()/1e3>=n-t}var w=class{constructor(e,t={}){this.fileInfo=e,this.chunkIndex=t.chunkIndex??null,this.rangeStart=t.rangeStart??null,this.rangeEnd=t.rangeEnd??null,this.state=`pending`,this.blob=null,this._parentFile=null,this._priority=b.normal,this._typeConfig=u(e.type)}getUrl(){let e=this.fileInfo.path;if(C(e))throw Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);return e}async start(){this.state=`downloading`;let e={};this.rangeStart!=null&&(e.Range=`bytes=${this.rangeStart}-${this.rangeEnd}`),this.chunkIndex!=null&&(e[`X-Store-Chunk-Index`]=String(this.chunkIndex),this._parentFile&&(e[`X-Store-Num-Chunks`]=String(this._parentFile.totalChunks),e[`X-Store-Chunk-Size`]=String(this._parentFile.options.chunkSize||104857600))),this.fileInfo.md5&&(e[`X-Store-MD5`]=this.fileInfo.md5),this.fileInfo.updateInterval&&(e[`X-Cache-TTL`]=String(this.fileInfo.updateInterval)),this.fileInfo.cmsDownloadUrl&&(e[`X-Cms-Download-Url`]=this.fileInfo.cmsDownloadUrl);let t=this._typeConfig.maxRetries;for(let n=1;n<=t;n++){let r=new AbortController,i=setTimeout(()=>r.abort(),_);try{let t=this.getUrl(),n={signal:r.signal};Object.keys(e).length>0&&(n.headers=e);let i=await fetch(t,n);if(!i.ok&&i.status!==206)throw Error(`Fetch failed: ${i.status}`);return this.blob=await i.blob(),this.state=`complete`,this.blob}catch(e){let i=r.signal.aborted?`Timeout after ${_/1e3}s`:e.message;if(n<t){let e=this._typeConfig.retryDelays?.[n-1]??this._typeConfig.retryDelayMs*n,r=this.chunkIndex==null?``:` chunk ${this.chunkIndex}`;d.warn(`[DownloadTask] ${this.fileInfo.type}/${this.fileInfo.id}${r} attempt ${n}/${t} failed: ${i}. Retrying in ${e/1e3}s...`),await new Promise(t=>setTimeout(t,e))}else throw this.state=`failed`,r.signal.aborted?Error(i):e}finally{clearTimeout(i)}}}},T=class{constructor(e,t={}){this.fileInfo=e,this.options=t,this.state=`pending`,this.tasks=[],this.completedChunks=0,this.totalChunks=0,this.totalBytes=0,this.downloadedBytes=0,this.onChunkDownloaded=null,this.skipChunks=e.skipChunks||new Set,this._contentType=`application/octet-stream`,this._chunkBlobs=new Map,this._runningCount=0,this._resolve=null,this._reject=null,this._promise=new Promise((e,t)=>{this._resolve=e,this._reject=t}),this._promise.catch(()=>{})}getUrl(){let e=this.fileInfo.path;if(C(e))throw Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);return e}wait(){return this._promise}async prepare(e){try{this.state=`preparing`;let{id:t,type:n,size:r}=this.fileInfo;d.info(`[FileDownload] Starting:`,`${n}/${t}`),this.totalBytes=r&&r>0?parseInt(r):0,this._contentType=y(this.fileInfo);let i=u(this.fileInfo.type).skipHead;if(this.totalBytes===0&&!i){let e=this.getUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),v);try{let n=await fetch(e,{method:`HEAD`,signal:t.signal});n.ok&&(this.totalBytes=parseInt(n.headers.get(`Content-Length`)||`0`),this._contentType=n.headers.get(`Content-Type`)||this._contentType)}finally{clearTimeout(n)}}d.info(`[FileDownload] File size:`,(this.totalBytes/1024/1024).toFixed(1),`MB`);let a=this.options.chunkSize||p;if(this.totalBytes>h){let e=[];for(let t=0;t<this.totalBytes;t+=a)e.push({start:t,end:Math.min(t+a-1,this.totalBytes-1),index:e.length});this.totalChunks=e.length;let r=e.filter(e=>!this.skipChunks.has(e.index)),i=e.length-r.length;for(let t of e)this.skipChunks.has(t.index)&&(this.downloadedBytes+=t.end-t.start+1);if(r.length===0){d.info(`[FileDownload] All chunks already cached, nothing to download`),this.state=`complete`,this._resolve(new Blob([],{type:this._contentType}));return}i>0&&d.info(`[FileDownload] Resuming: ${i} chunks cached, ${r.length} to download`);let o=i>0;if(o){let e=r.sort((e,t)=>e.index-t.index);for(let t of e){let e=new w(this.fileInfo,{chunkIndex:t.index,rangeStart:t.start,rangeEnd:t.end});e._parentFile=this,e._priority=b.normal,this.tasks.push(e)}}else for(let t of r){let n=new w(this.fileInfo,{chunkIndex:t.index,rangeStart:t.start,rangeEnd:t.end});n._parentFile=this,n._priority=t.index===0||t.index===e.length-1?b.high:b.normal,this.tasks.push(n)}let s=this.tasks.filter(e=>e._priority>=b.high).length;d.info(`[FileDownload] ${n}/${t}: ${this.tasks.length} chunks`+(s>0?` (${s} priority)`:``)+(o?` (resume)`:``))}else{this.totalChunks=1;let e=new w(this.fileInfo,{});e._parentFile=this,this.tasks.push(e)}e.enqueueChunkTasks(this.tasks),this.state=`downloading`}catch(e){d.error(`[FileDownload] Prepare failed:`,`${this.fileInfo.type}/${this.fileInfo.id}`,e),this.state=`failed`,this._reject(e)}}async onTaskComplete(e){if(this.completedChunks++,this.downloadedBytes+=e.blob.size,e.chunkIndex!=null&&this._chunkBlobs.set(e.chunkIndex,e.blob),this.options.onProgress&&this.options.onProgress(this.downloadedBytes,this.totalBytes),this.onChunkDownloaded&&e.chunkIndex!=null)try{await this.onChunkDownloaded(e.chunkIndex,e.blob,this.totalChunks)}catch(e){d.warn(`[FileDownload] onChunkDownloaded callback error:`,e)}if(this.completedChunks===this.tasks.length&&this.state!==`complete`){this.state=`complete`;let{type:t,id:n}=this.fileInfo;if(e.chunkIndex==null)d.info(`[FileDownload] Complete:`,`${t}/${n}`,`(${e.blob.size} bytes)`),this._resolve(e.blob);else if(this.onChunkDownloaded)d.info(`[FileDownload] Complete:`,`${t}/${n}`,`(progressive, ${this.totalChunks} chunks)`),this._resolve(new Blob([],{type:this._contentType}));else{let e=[];for(let t=0;t<this.totalChunks;t++){let n=this._chunkBlobs.get(t);n&&e.push(n)}let r=new Blob(e,{type:this._contentType});d.info(`[FileDownload] Complete:`,`${t}/${n}`,`(${r.size} bytes, reassembled)`),this._resolve(r)}this._chunkBlobs.clear()}}onTaskFailed(e,t){if(!(this.state===`complete`||this.state===`failed`)){if(t.message?.includes(`URL expired`)){let t=e.chunkIndex==null?``:` chunk ${e.chunkIndex}`;d.warn(`[FileDownload] URL expired, dropping${t}:`,`${this.fileInfo.type}/${this.fileInfo.id}`),this.tasks=this.tasks.filter(t=>t!==e),(this.tasks.length===0||this.completedChunks>=this.tasks.length)&&(this.state=`complete`,this._urlExpired=!0,this._resolve(new Blob([],{type:this._contentType})));return}d.error(`[FileDownload] Failed:`,`${this.fileInfo.type}/${this.fileInfo.id}`,t),this.state=`failed`,this._reject(t)}}},E=class{constructor(e){this.queue=e,this._filesToPrepare=[],this._tasks=[],this._maxPreparing=2}addFile(e){let t=D.stableKey(e);if(this.queue.active.has(t)){let n=this.queue.active.get(t);if(e.path&&e.path!==n.fileInfo.path){let t=S(n.fileInfo.path);S(e.path)>t&&(n.fileInfo.path=e.path)}return n}let n=new T(e,{chunkSize:this.queue.chunkSize,calculateMD5:this.queue.calculateMD5,onProgress:this.queue.onProgress});return this.queue.active.set(t,n),this._filesToPrepare.push(n),n}enqueueChunkTasks(e){this._tasks.push(...e)}async build(){return await this._prepareAll(),this._sortWithBarriers()}async _prepareAll(){await new Promise(e=>{let t=0,n=0,r=()=>{for(;t<this._maxPreparing&&n<this._filesToPrepare.length;){let i=this._filesToPrepare[n++];t++,i.prepare(this).finally(()=>{t--,n>=this._filesToPrepare.length&&t===0?e():r()})}};this._filesToPrepare.length===0?e():r()})}_sortWithBarriers(){let e=[],t=[],n=[],r=[];for(let i of this._tasks)if(i.chunkIndex==null)e.push(i);else if(i.chunkIndex===0)t.push(i);else{let e=i._parentFile?.totalChunks||0;e>1&&i.chunkIndex===e-1?n.push(i):r.push(i)}e.sort((e,t)=>(e._parentFile?.totalBytes||0)-(t._parentFile?.totalBytes||0)),r.sort((e,t)=>e.chunkIndex-t.chunkIndex);let i=[...e,...t,...n];return r.length>0&&i.push(x,...r),i}},D=class e{constructor(e={}){this.concurrency=e.concurrency||f,this.chunkSize=e.chunkSize||p,this.maxChunksPerFile=e.chunksPerFile||m,this.calculateMD5=e.calculateMD5,this.onProgress=e.onProgress,this.queue=[],this.active=new Map,this._activeTasks=[],this.running=0,this._prepareQueue=[],this._preparingCount=0,this._maxPreparing=2,this.paused=!1,this._reenqueueTimers=new Set}static stableKey(e){return`${e.type}/${e.id}`}enqueue(t){let n=e.stableKey(t);if(this.active.has(n)){let e=this.active.get(n);if(t.path&&t.path!==e.fileInfo.path){let r=S(e.fileInfo.path);S(t.path)>r&&(d.info(`[DownloadQueue] Refreshing URL for`,n),e.fileInfo.path=t.path)}return e}let r=new T(t,{chunkSize:this.chunkSize,calculateMD5:this.calculateMD5,onProgress:this.onProgress});return this.active.set(n,r),d.info(`[DownloadQueue] Enqueued:`,n),this._schedulePrepare(r),r}_schedulePrepare(e){this._prepareQueue.push(e),this._processPrepareQueue()}_processPrepareQueue(){for(;this._preparingCount<this._maxPreparing&&this._prepareQueue.length>0;){let e=this._prepareQueue.shift();this._preparingCount++,e.prepare(this).finally(()=>{this._preparingCount--,this._processPrepareQueue()})}}enqueueChunkTasks(e){for(let t of e)this.queue.push(t);this._sortQueue(),d.info(`[DownloadQueue] ${e.length} tasks added (${this.queue.length} pending, ${this.running} active)`),this.processQueue()}enqueueOrderedTasks(e){let t=0,n=0;for(let r of e)r===x?(this.queue.push(x),n++):(this.queue.push(r),t++);d.info(`[DownloadQueue] Ordered queue: ${t} tasks, ${n} barriers (${this.queue.length} pending, ${this.running} active)`),this.processQueue()}_sortQueue(){this.queue.sort((e,t)=>t._priority-e._priority)}prioritizeLayoutFiles(e,t=b.high){let n=new Set(e.map(String)),r=0;for(let e of this.queue){let i=e._parentFile?.fileInfo.saveAs||String(e._parentFile?.fileInfo.id);n.has(i)&&e._priority<t&&(e._priority=t,r++)}for(let e of this._activeTasks){let r=e._parentFile?.fileInfo.saveAs||String(e._parentFile?.fileInfo.id);n.has(r)&&e._priority<t&&(e._priority=t)}this._sortQueue(),d.info(`[DownloadQueue] Layout files prioritized:`,n.size,`files,`,r,`tasks boosted to`,t)}urgentChunk(e,t,n){let r=`${e}/${t}`,i=this.active.get(r);if(!i)return d.info(`[DownloadQueue] urgentChunk: file not active:`,r,`chunk`,n),!1;if(this._activeTasks.some(e=>e._parentFile===i&&e.chunkIndex===n&&e.state===`downloading`)){let e=this._activeTasks.find(e=>e._parentFile===i&&e.chunkIndex===n);return e&&e._priority<b.urgent?(e._priority=b.urgent,d.info(`[DownloadQueue] URGENT: ${r} chunk ${n} (already in-flight, limiting slots)`),!0):(d.info(`[DownloadQueue] urgentChunk: already urgent:`,r,`chunk`,n),!1)}let a=this.queue.findIndex(e=>e!==x&&e._parentFile===i&&e.chunkIndex===n);if(a===-1)return d.info(`[DownloadQueue] urgentChunk: chunk not in queue:`,r,`chunk`,n),!1;let o=this.queue.splice(a,1)[0];return o._priority=b.urgent,this.queue.unshift(o),d.info(`[DownloadQueue] URGENT: ${r} chunk ${n} (moved to front)`),this.processQueue(),!0}processQueue(){if(this.paused)return;let e=this.queue.some(e=>e!==x&&e._priority>=b.urgent)||this._activeTasks?.some(e=>e._priority>=b.urgent&&e.state===`downloading`),t=e?g:this.concurrency,n=e?b.urgent:0;for(;this.running<t&&this.queue.length>0;){let e=this.queue[0];if(e===x){if(this.running>0)break;this.queue.shift();continue}if(e._priority<n||!this._canStartTask(e)){let e=!1;for(let t=1;t<this.queue.length&&this.queue[t]!==x;t++){let r=this.queue[t];if(r._priority>=n&&this._canStartTask(r)){this.queue.splice(t,1),this._startTask(r),e=!0;break}}if(!e)break;continue}this.queue.shift(),this._startTask(e)}this.queue.length===0&&this.running===0&&d.info(`[DownloadQueue] All downloads complete`)}_canStartTask(e){return e._parentFile._runningCount<this.maxChunksPerFile}_startTask(e){this.running++,e._parentFile._runningCount++,this._activeTasks.push(e);let t=`${e.fileInfo.type}/${e.fileInfo.id}`,n=e.chunkIndex==null?``:` chunk ${e.chunkIndex}`;d.info(`[DownloadQueue] Starting: ${t}${n} (${this.running}/${this.concurrency} active)`),e.start().then(()=>(this.running--,e._parentFile._runningCount--,this._activeTasks=this._activeTasks.filter(t=>t!==e),d.info(`[DownloadQueue] Fetched: ${t}${n} (${this.running} active, ${this.queue.length} pending)`),this.processQueue(),e._parentFile.onTaskComplete(e))).catch(n=>{this.running--,e._parentFile._runningCount--,this._activeTasks=this._activeTasks.filter(t=>t!==e);let{maxReenqueues:r,reenqueueDelayMs:i}=e._typeConfig;if(r>0){if(e._reenqueueCount=(e._reenqueueCount||0)+1,e._reenqueueCount>r){d.error(`[DownloadQueue] ${t} exceeded ${r} re-enqueues, failing permanently`),this.processQueue(),e._parentFile.onTaskFailed(e,n);return}d.warn(`[DownloadQueue] ${t} failed all retries (attempt ${e._reenqueueCount}/${r}), scheduling re-enqueue in ${i/1e3}s`);let a=setTimeout(()=>{this._reenqueueTimers.delete(a),e.state=`pending`,e._parentFile.state=`downloading`,this.queue.push(e),d.info(`[DownloadQueue] ${t} re-enqueued for retry`),this.processQueue()},i);this._reenqueueTimers.add(a),this.processQueue();return}this.processQueue(),e._parentFile.onTaskFailed(e,n)})}removeCompleted(e){let t=this.active.get(e);t&&(t.state===`complete`||t.state===`failed`)&&(this.queue=this.queue.filter(e=>e===x||e._parentFile!==t),this.active.delete(e))}getTask(e){return this.active.get(e)||null}getProgress(){let e={};for(let[t,n]of this.active.entries())n.state===`complete`||n.state===`failed`||(e[t]={downloaded:n.downloadedBytes,total:n.totalBytes,percent:n.totalBytes>0?(n.downloadedBytes/n.totalBytes*100).toFixed(1):0,state:n.state});return e}clear(){this.queue=[],this.active.clear(),this.running=0,this._prepareQueue=[],this._preparingCount=0;for(let e of this._reenqueueTimers)clearTimeout(e);this._reenqueueTimers.clear()}},O=class{constructor(e={}){this.queue=new D(e)}enqueue(e){return this.queue.enqueue(e)}getTask(e){return this.queue.getTask(e)}getProgress(){return this.queue.getProgress()}prioritizeLayoutFiles(e,t){this.queue.prioritizeLayoutFiles(e,t),this.queue.processQueue()}urgentChunk(e,t,n){return this.queue.urgentChunk(e,t,n)}createTaskBuilder(){return new E(this.queue)}enqueueOrderedTasks(e){this.queue.enqueueOrderedTasks(e)}removeCompleted(e){this.queue.removeCompleted(e)}get running(){return this.queue.running}get queued(){return this.queue.queue.length}clear(){this.queue.clear()}},k=n(`CacheAnalyzer`);function A(e){if(e===0)return`0 B`;if(!Number.isFinite(e))return`∞`;let t=[`B`,`KB`,`MB`,`GB`,`TB`],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/1024**n).toFixed(+(n>0))} ${t[n]}`}var j=class{constructor(e,{threshold:t=80}={}){this.cache=e,this.threshold=t}async analyze(e){let t=await this.cache.list(),n=await this._getStorageEstimate(),r=new Set(e.map(e=>String(e.id))),i=[],a=[];for(let e of t)if(r.has(String(e.id)))i.push(e);else if(e.type===`widget`){let t=String(e.id).split(`/`)[0];r.has(t)?i.push(e):a.push(e)}else e.type===`static`?i.push(e):a.push(e);a.sort((e,t)=>(e.cachedAt||0)-(t.cachedAt||0));let o=a.reduce((e,t)=>e+(t.size||0),0),s={timestamp:Date.now(),storage:{usage:n.usage,quota:n.quota,percent:n.quota>0?Math.round(n.usage/n.quota*100):0},files:{required:i.length,orphaned:a.length,total:t.length},orphaned:a.map(e=>({id:e.id,type:e.type,size:e.size||0,cachedAt:e.cachedAt||0})),orphanedSize:o,evicted:[],threshold:this.threshold};if(k.info(`Storage: ${A(n.usage)} / ${A(n.quota)} (${s.storage.percent}%)`),k.info(`Cache: ${i.length} required, ${a.length} orphaned (${A(o)} reclaimable)`),a.length>0)for(let e of a){let t=Date.now()-(e.cachedAt||0),n=Math.floor(t/864e5),r=Math.floor(t%864e5/36e5),i=n>0?`${n}d ago`:`${r}h ago`;k.info(` Orphaned: ${e.type}/${e.id} (${A(e.size||0)}, cached ${i})`)}if(s.storage.percent>this.threshold&&a.length>0){k.warn(`Storage exceeds ${this.threshold}% threshold — evicting orphaned files`);let e=n.usage-n.quota*this.threshold/100;s.evicted=await this._evict(a,e)}else k.info(`No eviction needed (threshold: ${this.threshold}%)`);return s}async _getStorageEstimate(){try{if(typeof navigator<`u`&&navigator.storage?.estimate){let{usage:e,quota:t}=await navigator.storage.estimate();return{usage:e||0,quota:t||1/0}}}catch(e){k.warn(`Storage estimate unavailable:`,e.message)}return{usage:0,quota:1/0}}async _evict(e,t){let n=[],r=0;for(let i of e){if(r>=t)break;n.push(i),r+=i.size||0}if(n.length===0)return[];try{let e=n.map(e=>({type:e.type,id:e.id}));await this.cache.remove(e);for(let e of n)k.info(` Evicted: ${e.type}/${e.id} (${A(e.size||0)})`);k.info(`Evicted ${n.length} files, freed ${A(r)}`)}catch(e){return k.warn(`Eviction failed:`,e.message),[]}return n.map(e=>({id:e.id,type:e.type,size:e.size||0,cachedAt:e.cachedAt||0}))}},M=n(`Cache`),N=typeof window<`u`&&window.location.pathname.replace(/\/[^/]*$/,``).replace(/\/$/,``)||`/player/pwa`;async function P(e,n,r,i){let a=`${t}/widgets/${e}/${n}/${r}`,o=`<base href="${t}/media/file/">`,s=i;i.includes(`<base `)||(s=i.includes(`<head>`)?i.replace(`<head>`,`<head>`+o):i.includes(`<HEAD>`)?i.replace(`<HEAD>`,`<HEAD>`+o):o+i);let c=`<style>img,video{object-position:center center}</style>`;s.includes(`object-position:center center`)||(s.includes(`</head>`)?s=s.replace(`</head>`,c+`</head>`):s.includes(`</HEAD>`)&&(s=s.replace(`</HEAD>`,c+`</HEAD>`))),s=s.replace(/\[\[ViewPortWidth]]/g,`device-width`),s=s.replace(/https?:\/\/[^\s"')<]+\.m3u8\b/gi,e=>`/stream-proxy?url=`+encodeURIComponent(e));let l=RegExp(`https?://[^"'\\s)]+?(${t.replace(/\//g,`\\/`)}/dependencies/[^"'\\s?)]+)(\\?[^"'\\s)]*)?`,`g`);if(s=s.replace(l,(e,t)=>t),s=s.replace(/(<(?:script|link)\b[^>]*(?:src|href)=")(?!\/|https?:\/\/)(bundle\.min\.js|fonts\.css)(")/g,`$1${t}/dependencies/$2$3`),!s.includes(`xiboICTargetId`)){let e=`<script>var xiboICTargetId = '${r}';<\/script>`;s=s.includes(o)?s.replace(o,o+e):s.includes(`<head>`)?s.replace(`<head>`,`<head>`+e):e+s}return s=s.replace(/hostAddress\s*:\s*["']https?:\/\/[^"']+["']/g,`hostAddress: "${N}/ic"`),M.info(`Injected base tag in widget HTML`),(await fetch(`/store${t}/widgets/${e}/${n}/${r}`,{method:`PUT`,headers:{"Content-Type":`text/html; charset=utf-8`},body:s})).body?.cancel(),M.info(`Stored widget HTML at ${a} (${s.length} bytes)`),{cacheKey:a,html:s}}var F=e({BARRIER:()=>x,CacheAnalyzer:()=>j,CacheManager:()=>a,DownloadManager:()=>O,FILE_TYPES:()=>l,FileDownload:()=>T,LayoutTaskBuilder:()=>E,StoreClient:()=>c,VERSION:()=>I,cacheManager:()=>o,cacheWidgetHtml:()=>P,getFileTypeConfig:()=>u,isUrlExpired:()=>C}),I=r.version;export{O as a,x as i,P as n,c as o,j as r,F as t};
2
- //# sourceMappingURL=src-BYVnjdc0.js.map
1
+ import{n as e}from"./chunk-7ZXdHUL4.js";import{t,u as n}from"./src-CRWze-JF.js";var r={name:`@xiboplayer/cache`,version:`0.7.19`,description:`Offline caching and download management with parallel chunk downloads`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./cache":`./src/cache.js`,"./store-client":`./src/store-client.js`,"./download-manager":`./src/download-manager.js`,"./cache-analyzer":`./src/cache-analyzer.js`,"./widget-html":`./src/widget-html.js`},scripts:{test:`vitest run`,"test:watch":`vitest`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`,"spark-md5":`^3.0.2`},devDependencies:{jsdom:`^29.0.2`,vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`cache`,`offline`,`download`,`indexeddb`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/cache`},homepage:`https://xiboplayer.org`},i=n(`Cache`),a=class{constructor(){this.dependants=new Map}addDependant(e,t){let n=String(e);this.dependants.has(n)||this.dependants.set(n,new Set),this.dependants.get(n).add(String(t))}removeLayoutDependants(e){let t=String(e),n=[];for(let[e,r]of this.dependants)r.delete(t),r.size===0&&(this.dependants.delete(e),n.push(e));return n.length>0&&i.info(`${n.length} media files orphaned after layout ${e} removed:`,n),n}isMediaReferenced(e){let t=this.dependants.get(String(e));return t?t.size>0:!1}async clearAll(){this.dependants.clear()}},o=new a,s=n(`StoreClient`),c=class{async has(e,t){try{let n=await fetch(`/store/${e}/${t}`,{method:`HEAD`});if(n.status===204)return!1;if(n.status===200)return n.headers.get(`X-Store-Complete`)!==`false`;if(n.status===404)return!1;let r=Error(`Store error: ${n.status}`);throw r.status=n.status,s.warn(`has(${e}/${t}): unexpected status ${n.status}`),r}catch(n){throw n.status||n.name===`AbortError`||n.name===`TimeoutError`||s.warn(`has(${e}/${t}): network error — ${n.message}`),n}}async get(e,t){try{let n=await fetch(`/store/${e}/${t}`);if(n.status===204||n.status===404)return n.body?.cancel(),null;if(!n.ok)throw n.body?.cancel(),Error(`Failed to get file: ${n.status}`);return await n.blob()}catch(e){return s.error(`get error:`,e.message),null}}async put(e,t,n,r=`application/octet-stream`){try{let i=await fetch(`/store/${e}/${t}`,{method:`PUT`,headers:{"Content-Type":r},body:n});return i.body?.cancel(),i.ok}catch(e){return s.error(`put error:`,e.message),!1}}async remove(e){try{let t=await(await fetch(`/store/delete`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({files:e})})).json();return{deleted:t.deleted||0,total:t.total||e.length}}catch(t){return s.error(`remove error:`,t.message),{deleted:0,total:e.length}}}async list(){try{return(await(await fetch(`/store/list`)).json()).files||[]}catch(e){return s.error(`list error:`,e.message),[]}}},l={media:{maxRetries:3,retryDelayMs:500,retryDelays:null,maxReenqueues:0,reenqueueDelayMs:0,skipHead:!1,cacheTtl:1/0},layout:{maxRetries:3,retryDelayMs:500,retryDelays:null,maxReenqueues:0,reenqueueDelayMs:0,skipHead:!1,cacheTtl:1/0},dataset:{maxRetries:4,retryDelayMs:0,retryDelays:[15e3,3e4,6e4,12e4],maxReenqueues:5,reenqueueDelayMs:6e4,skipHead:!0,cacheTtl:300},static:{maxRetries:3,retryDelayMs:500,retryDelays:null,maxReenqueues:0,reenqueueDelayMs:0,skipHead:!1,cacheTtl:1/0}};function u(e){return l[e]||l.media}var d=n(`Download`),f=6,p=50*1024*1024,m=3,h=100*1024*1024,g=2,_=6e5,v=15e3;function y(e){return{mp4:`video/mp4`,webm:`video/webm`,mp3:`audio/mpeg`,png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,svg:`image/svg+xml`,webp:`image/webp`,css:`text/css`,js:`application/javascript`,ttf:`font/ttf`,otf:`font/otf`,woff:`font/woff`,woff2:`font/woff2`,xml:`application/xml`,xlf:`application/xml`}[(e.path||e.code||``).split(`.`).pop()?.split(`?`)[0]?.toLowerCase()]||`application/octet-stream`}var b={normal:0,layout:1,high:2,urgent:3},x=Symbol(`BARRIER`);function S(e){try{let t=new URL(e,`http://localhost`),n=t.searchParams.get(`X-Amz-Date`),r=t.searchParams.get(`X-Amz-Expires`);if(n&&r){let e=n.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/,`$1-$2-$3T$4:$5:$6Z`),t=new Date(e).getTime()/1e3;return isNaN(t)?1/0:t+parseInt(r,10)}return 1/0}catch{return 1/0}}function C(e,t=30){let n=S(e);return n===1/0?!1:Date.now()/1e3>=n-t}var w=class{constructor(e,t={}){this.fileInfo=e,this.chunkIndex=t.chunkIndex??null,this.rangeStart=t.rangeStart??null,this.rangeEnd=t.rangeEnd??null,this.state=`pending`,this.blob=null,this._parentFile=null,this._priority=b.normal,this._typeConfig=u(e.type)}getUrl(){let e=this.fileInfo.path;if(C(e))throw Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);return e}async start(){this.state=`downloading`;let e={};this.rangeStart!=null&&(e.Range=`bytes=${this.rangeStart}-${this.rangeEnd}`),this.chunkIndex!=null&&(e[`X-Store-Chunk-Index`]=String(this.chunkIndex),this._parentFile&&(e[`X-Store-Num-Chunks`]=String(this._parentFile.totalChunks),e[`X-Store-Chunk-Size`]=String(this._parentFile.options.chunkSize||104857600))),this.fileInfo.md5&&(e[`X-Store-MD5`]=this.fileInfo.md5),this.fileInfo.updateInterval&&(e[`X-Cache-TTL`]=String(this.fileInfo.updateInterval)),this.fileInfo.cmsDownloadUrl&&(e[`X-Cms-Download-Url`]=this.fileInfo.cmsDownloadUrl);let t=this._typeConfig.maxRetries;for(let n=1;n<=t;n++){let r=new AbortController,i=setTimeout(()=>r.abort(),_);try{let t=this.getUrl(),n={signal:r.signal};Object.keys(e).length>0&&(n.headers=e);let i=await fetch(t,n);if(!i.ok&&i.status!==206)throw Error(`Fetch failed: ${i.status}`);return this.blob=await i.blob(),this.state=`complete`,this.blob}catch(e){let i=r.signal.aborted?`Timeout after ${_/1e3}s`:e.message;if(n<t){let e=this._typeConfig.retryDelays?.[n-1]??this._typeConfig.retryDelayMs*n,r=this.chunkIndex==null?``:` chunk ${this.chunkIndex}`;d.warn(`[DownloadTask] ${this.fileInfo.type}/${this.fileInfo.id}${r} attempt ${n}/${t} failed: ${i}. Retrying in ${e/1e3}s...`),await new Promise(t=>setTimeout(t,e))}else throw this.state=`failed`,r.signal.aborted?Error(i):e}finally{clearTimeout(i)}}}},T=class{constructor(e,t={}){this.fileInfo=e,this.options=t,this.state=`pending`,this.tasks=[],this.completedChunks=0,this.totalChunks=0,this.totalBytes=0,this.downloadedBytes=0,this.onChunkDownloaded=null,this.skipChunks=e.skipChunks||new Set,this._contentType=`application/octet-stream`,this._chunkBlobs=new Map,this._runningCount=0,this._resolve=null,this._reject=null,this._promise=new Promise((e,t)=>{this._resolve=e,this._reject=t}),this._promise.catch(()=>{})}getUrl(){let e=this.fileInfo.path;if(C(e))throw Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);return e}wait(){return this._promise}async prepare(e){try{this.state=`preparing`;let{id:t,type:n,size:r}=this.fileInfo;d.info(`[FileDownload] Starting:`,`${n}/${t}`),this.totalBytes=r&&r>0?parseInt(r):0,this._contentType=y(this.fileInfo);let i=u(this.fileInfo.type).skipHead;if(this.totalBytes===0&&!i){let e=this.getUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),v);try{let n=await fetch(e,{method:`HEAD`,signal:t.signal});n.ok&&(this.totalBytes=parseInt(n.headers.get(`Content-Length`)||`0`),this._contentType=n.headers.get(`Content-Type`)||this._contentType)}finally{clearTimeout(n)}}d.info(`[FileDownload] File size:`,(this.totalBytes/1024/1024).toFixed(1),`MB`);let a=this.options.chunkSize||p;if(this.totalBytes>h){let e=[];for(let t=0;t<this.totalBytes;t+=a)e.push({start:t,end:Math.min(t+a-1,this.totalBytes-1),index:e.length});this.totalChunks=e.length;let r=e.filter(e=>!this.skipChunks.has(e.index)),i=e.length-r.length;for(let t of e)this.skipChunks.has(t.index)&&(this.downloadedBytes+=t.end-t.start+1);if(r.length===0){d.info(`[FileDownload] All chunks already cached, nothing to download`),this.state=`complete`,this._resolve(new Blob([],{type:this._contentType}));return}i>0&&d.info(`[FileDownload] Resuming: ${i} chunks cached, ${r.length} to download`);let o=i>0;if(o){let e=r.sort((e,t)=>e.index-t.index);for(let t of e){let e=new w(this.fileInfo,{chunkIndex:t.index,rangeStart:t.start,rangeEnd:t.end});e._parentFile=this,e._priority=b.normal,this.tasks.push(e)}}else for(let t of r){let n=new w(this.fileInfo,{chunkIndex:t.index,rangeStart:t.start,rangeEnd:t.end});n._parentFile=this,n._priority=t.index===0||t.index===e.length-1?b.high:b.normal,this.tasks.push(n)}let s=this.tasks.filter(e=>e._priority>=b.high).length;d.info(`[FileDownload] ${n}/${t}: ${this.tasks.length} chunks`+(s>0?` (${s} priority)`:``)+(o?` (resume)`:``))}else{this.totalChunks=1;let e=new w(this.fileInfo,{});e._parentFile=this,this.tasks.push(e)}e.enqueueChunkTasks(this.tasks),this.state=`downloading`}catch(e){d.error(`[FileDownload] Prepare failed:`,`${this.fileInfo.type}/${this.fileInfo.id}`,e),this.state=`failed`,this._reject(e)}}async onTaskComplete(e){if(this.completedChunks++,this.downloadedBytes+=e.blob.size,e.chunkIndex!=null&&this._chunkBlobs.set(e.chunkIndex,e.blob),this.options.onProgress&&this.options.onProgress(this.downloadedBytes,this.totalBytes),this.onChunkDownloaded&&e.chunkIndex!=null)try{await this.onChunkDownloaded(e.chunkIndex,e.blob,this.totalChunks)}catch(e){d.warn(`[FileDownload] onChunkDownloaded callback error:`,e)}if(this.completedChunks===this.tasks.length&&this.state!==`complete`){this.state=`complete`;let{type:t,id:n}=this.fileInfo;if(e.chunkIndex==null)d.info(`[FileDownload] Complete:`,`${t}/${n}`,`(${e.blob.size} bytes)`),this._resolve(e.blob);else if(this.onChunkDownloaded)d.info(`[FileDownload] Complete:`,`${t}/${n}`,`(progressive, ${this.totalChunks} chunks)`),this._resolve(new Blob([],{type:this._contentType}));else{let e=[];for(let t=0;t<this.totalChunks;t++){let n=this._chunkBlobs.get(t);n&&e.push(n)}let r=new Blob(e,{type:this._contentType});d.info(`[FileDownload] Complete:`,`${t}/${n}`,`(${r.size} bytes, reassembled)`),this._resolve(r)}this._chunkBlobs.clear()}}onTaskFailed(e,t){if(!(this.state===`complete`||this.state===`failed`)){if(t.message?.includes(`URL expired`)){let t=e.chunkIndex==null?``:` chunk ${e.chunkIndex}`;d.warn(`[FileDownload] URL expired, dropping${t}:`,`${this.fileInfo.type}/${this.fileInfo.id}`),this.tasks=this.tasks.filter(t=>t!==e),(this.tasks.length===0||this.completedChunks>=this.tasks.length)&&(this.state=`complete`,this._urlExpired=!0,this._resolve(new Blob([],{type:this._contentType})));return}d.error(`[FileDownload] Failed:`,`${this.fileInfo.type}/${this.fileInfo.id}`,t),this.state=`failed`,this._reject(t)}}},E=class{constructor(e){this.queue=e,this._filesToPrepare=[],this._tasks=[],this._maxPreparing=2}addFile(e){let t=D.stableKey(e);if(this.queue.active.has(t)){let n=this.queue.active.get(t);if(e.path&&e.path!==n.fileInfo.path){let t=S(n.fileInfo.path);S(e.path)>t&&(n.fileInfo.path=e.path)}return n}let n=new T(e,{chunkSize:this.queue.chunkSize,calculateMD5:this.queue.calculateMD5,onProgress:this.queue.onProgress});return this.queue.active.set(t,n),this._filesToPrepare.push(n),n}enqueueChunkTasks(e){this._tasks.push(...e)}async build(){return await this._prepareAll(),this._sortWithBarriers()}async _prepareAll(){await new Promise(e=>{let t=0,n=0,r=()=>{for(;t<this._maxPreparing&&n<this._filesToPrepare.length;){let i=this._filesToPrepare[n++];t++,i.prepare(this).finally(()=>{t--,n>=this._filesToPrepare.length&&t===0?e():r()})}};this._filesToPrepare.length===0?e():r()})}_sortWithBarriers(){let e=[],t=[],n=[],r=[];for(let i of this._tasks)if(i.chunkIndex==null)e.push(i);else if(i.chunkIndex===0)t.push(i);else{let e=i._parentFile?.totalChunks||0;e>1&&i.chunkIndex===e-1?n.push(i):r.push(i)}e.sort((e,t)=>(e._parentFile?.totalBytes||0)-(t._parentFile?.totalBytes||0)),r.sort((e,t)=>e.chunkIndex-t.chunkIndex);let i=[...e,...t,...n];return r.length>0&&i.push(x,...r),i}},D=class e{constructor(e={}){this.concurrency=e.concurrency||f,this.chunkSize=e.chunkSize||p,this.maxChunksPerFile=e.chunksPerFile||m,this.calculateMD5=e.calculateMD5,this.onProgress=e.onProgress,this.queue=[],this.active=new Map,this._activeTasks=[],this.running=0,this._prepareQueue=[],this._preparingCount=0,this._maxPreparing=2,this.paused=!1,this._reenqueueTimers=new Set}static stableKey(e){return`${e.type}/${e.id}`}enqueue(t){let n=e.stableKey(t);if(this.active.has(n)){let e=this.active.get(n);if(t.path&&t.path!==e.fileInfo.path){let r=S(e.fileInfo.path);S(t.path)>r&&(d.info(`[DownloadQueue] Refreshing URL for`,n),e.fileInfo.path=t.path)}return e}let r=new T(t,{chunkSize:this.chunkSize,calculateMD5:this.calculateMD5,onProgress:this.onProgress});return this.active.set(n,r),d.info(`[DownloadQueue] Enqueued:`,n),this._schedulePrepare(r),r}_schedulePrepare(e){this._prepareQueue.push(e),this._processPrepareQueue()}_processPrepareQueue(){for(;this._preparingCount<this._maxPreparing&&this._prepareQueue.length>0;){let e=this._prepareQueue.shift();this._preparingCount++,e.prepare(this).finally(()=>{this._preparingCount--,this._processPrepareQueue()})}}enqueueChunkTasks(e){for(let t of e)this.queue.push(t);this._sortQueue(),d.info(`[DownloadQueue] ${e.length} tasks added (${this.queue.length} pending, ${this.running} active)`),this.processQueue()}enqueueOrderedTasks(e){let t=0,n=0;for(let r of e)r===x?(this.queue.push(x),n++):(this.queue.push(r),t++);d.info(`[DownloadQueue] Ordered queue: ${t} tasks, ${n} barriers (${this.queue.length} pending, ${this.running} active)`),this.processQueue()}_sortQueue(){this.queue.sort((e,t)=>t._priority-e._priority)}prioritizeLayoutFiles(e,t=b.high){let n=new Set(e.map(String)),r=0;for(let e of this.queue){let i=e._parentFile?.fileInfo.saveAs||String(e._parentFile?.fileInfo.id);n.has(i)&&e._priority<t&&(e._priority=t,r++)}for(let e of this._activeTasks){let r=e._parentFile?.fileInfo.saveAs||String(e._parentFile?.fileInfo.id);n.has(r)&&e._priority<t&&(e._priority=t)}this._sortQueue(),d.info(`[DownloadQueue] Layout files prioritized:`,n.size,`files,`,r,`tasks boosted to`,t)}urgentChunk(e,t,n){let r=`${e}/${t}`,i=this.active.get(r);if(!i)return d.info(`[DownloadQueue] urgentChunk: file not active:`,r,`chunk`,n),!1;if(this._activeTasks.some(e=>e._parentFile===i&&e.chunkIndex===n&&e.state===`downloading`)){let e=this._activeTasks.find(e=>e._parentFile===i&&e.chunkIndex===n);return e&&e._priority<b.urgent?(e._priority=b.urgent,d.info(`[DownloadQueue] URGENT: ${r} chunk ${n} (already in-flight, limiting slots)`),!0):(d.info(`[DownloadQueue] urgentChunk: already urgent:`,r,`chunk`,n),!1)}let a=this.queue.findIndex(e=>e!==x&&e._parentFile===i&&e.chunkIndex===n);if(a===-1)return d.info(`[DownloadQueue] urgentChunk: chunk not in queue:`,r,`chunk`,n),!1;let o=this.queue.splice(a,1)[0];return o._priority=b.urgent,this.queue.unshift(o),d.info(`[DownloadQueue] URGENT: ${r} chunk ${n} (moved to front)`),this.processQueue(),!0}processQueue(){if(this.paused)return;let e=this.queue.some(e=>e!==x&&e._priority>=b.urgent)||this._activeTasks?.some(e=>e._priority>=b.urgent&&e.state===`downloading`),t=e?g:this.concurrency,n=e?b.urgent:0;for(;this.running<t&&this.queue.length>0;){let e=this.queue[0];if(e===x){if(this.running>0)break;this.queue.shift();continue}if(e._priority<n||!this._canStartTask(e)){let e=!1;for(let t=1;t<this.queue.length&&this.queue[t]!==x;t++){let r=this.queue[t];if(r._priority>=n&&this._canStartTask(r)){this.queue.splice(t,1),this._startTask(r),e=!0;break}}if(!e)break;continue}this.queue.shift(),this._startTask(e)}this.queue.length===0&&this.running===0&&d.info(`[DownloadQueue] All downloads complete`)}_canStartTask(e){return e._parentFile._runningCount<this.maxChunksPerFile}_startTask(e){this.running++,e._parentFile._runningCount++,this._activeTasks.push(e);let t=`${e.fileInfo.type}/${e.fileInfo.id}`,n=e.chunkIndex==null?``:` chunk ${e.chunkIndex}`;d.info(`[DownloadQueue] Starting: ${t}${n} (${this.running}/${this.concurrency} active)`),e.start().then(()=>(this.running--,e._parentFile._runningCount--,this._activeTasks=this._activeTasks.filter(t=>t!==e),d.info(`[DownloadQueue] Fetched: ${t}${n} (${this.running} active, ${this.queue.length} pending)`),this.processQueue(),e._parentFile.onTaskComplete(e))).catch(n=>{this.running--,e._parentFile._runningCount--,this._activeTasks=this._activeTasks.filter(t=>t!==e);let{maxReenqueues:r,reenqueueDelayMs:i}=e._typeConfig;if(r>0){if(e._reenqueueCount=(e._reenqueueCount||0)+1,e._reenqueueCount>r){d.error(`[DownloadQueue] ${t} exceeded ${r} re-enqueues, failing permanently`),this.processQueue(),e._parentFile.onTaskFailed(e,n);return}d.warn(`[DownloadQueue] ${t} failed all retries (attempt ${e._reenqueueCount}/${r}), scheduling re-enqueue in ${i/1e3}s`);let a=setTimeout(()=>{this._reenqueueTimers.delete(a),e.state=`pending`,e._parentFile.state=`downloading`,this.queue.push(e),d.info(`[DownloadQueue] ${t} re-enqueued for retry`),this.processQueue()},i);this._reenqueueTimers.add(a),this.processQueue();return}this.processQueue(),e._parentFile.onTaskFailed(e,n)})}removeCompleted(e){let t=this.active.get(e);t&&(t.state===`complete`||t.state===`failed`)&&(this.queue=this.queue.filter(e=>e===x||e._parentFile!==t),this.active.delete(e))}getTask(e){return this.active.get(e)||null}getProgress(){let e={};for(let[t,n]of this.active.entries())n.state===`complete`||n.state===`failed`||(e[t]={downloaded:n.downloadedBytes,total:n.totalBytes,percent:n.totalBytes>0?(n.downloadedBytes/n.totalBytes*100).toFixed(1):0,state:n.state});return e}clear(){this.queue=[],this.active.clear(),this.running=0,this._prepareQueue=[],this._preparingCount=0;for(let e of this._reenqueueTimers)clearTimeout(e);this._reenqueueTimers.clear()}},O=class{constructor(e={}){this.queue=new D(e)}enqueue(e){return this.queue.enqueue(e)}getTask(e){return this.queue.getTask(e)}getProgress(){return this.queue.getProgress()}prioritizeLayoutFiles(e,t){this.queue.prioritizeLayoutFiles(e,t),this.queue.processQueue()}urgentChunk(e,t,n){return this.queue.urgentChunk(e,t,n)}createTaskBuilder(){return new E(this.queue)}enqueueOrderedTasks(e){this.queue.enqueueOrderedTasks(e)}removeCompleted(e){this.queue.removeCompleted(e)}get running(){return this.queue.running}get queued(){return this.queue.queue.length}clear(){this.queue.clear()}},k=n(`CacheAnalyzer`);function A(e){if(e===0)return`0 B`;if(!Number.isFinite(e))return`∞`;let t=[`B`,`KB`,`MB`,`GB`,`TB`],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/1024**n).toFixed(+(n>0))} ${t[n]}`}var j=class{constructor(e,{threshold:t=80}={}){this.cache=e,this.threshold=t}async analyze(e){let t=await this.cache.list(),n=await this._getStorageEstimate(),r=new Set(e.map(e=>String(e.id))),i=[],a=[];for(let e of t)if(r.has(String(e.id)))i.push(e);else if(e.type===`widget`){let t=String(e.id).split(`/`)[0];r.has(t)?i.push(e):a.push(e)}else e.type===`static`?i.push(e):a.push(e);a.sort((e,t)=>(e.cachedAt||0)-(t.cachedAt||0));let o=a.reduce((e,t)=>e+(t.size||0),0),s={timestamp:Date.now(),storage:{usage:n.usage,quota:n.quota,percent:n.quota>0?Math.round(n.usage/n.quota*100):0},files:{required:i.length,orphaned:a.length,total:t.length},orphaned:a.map(e=>({id:e.id,type:e.type,size:e.size||0,cachedAt:e.cachedAt||0})),orphanedSize:o,evicted:[],threshold:this.threshold};if(k.info(`Storage: ${A(n.usage)} / ${A(n.quota)} (${s.storage.percent}%)`),k.info(`Cache: ${i.length} required, ${a.length} orphaned (${A(o)} reclaimable)`),a.length>0)for(let e of a){let t=Date.now()-(e.cachedAt||0),n=Math.floor(t/864e5),r=Math.floor(t%864e5/36e5),i=n>0?`${n}d ago`:`${r}h ago`;k.info(` Orphaned: ${e.type}/${e.id} (${A(e.size||0)}, cached ${i})`)}if(s.storage.percent>this.threshold&&a.length>0){k.warn(`Storage exceeds ${this.threshold}% threshold — evicting orphaned files`);let e=n.usage-n.quota*this.threshold/100;s.evicted=await this._evict(a,e)}else k.info(`No eviction needed (threshold: ${this.threshold}%)`);return s}async _getStorageEstimate(){try{if(typeof navigator<`u`&&navigator.storage?.estimate){let{usage:e,quota:t}=await navigator.storage.estimate();return{usage:e||0,quota:t||1/0}}}catch(e){k.warn(`Storage estimate unavailable:`,e.message)}return{usage:0,quota:1/0}}async _evict(e,t){let n=[],r=0;for(let i of e){if(r>=t)break;n.push(i),r+=i.size||0}if(n.length===0)return[];try{let e=n.map(e=>({type:e.type,id:e.id}));await this.cache.remove(e);for(let e of n)k.info(` Evicted: ${e.type}/${e.id} (${A(e.size||0)})`);k.info(`Evicted ${n.length} files, freed ${A(r)}`)}catch(e){return k.warn(`Eviction failed:`,e.message),[]}return n.map(e=>({id:e.id,type:e.type,size:e.size||0,cachedAt:e.cachedAt||0}))}},M=n(`Cache`),N=typeof window<`u`&&window.location.pathname.replace(/\/[^/]*$/,``).replace(/\/$/,``)||`/player/pwa`;async function P(e,n,r,i){let a=`${t}/widgets/${e}/${n}/${r}`,o=`<base href="${t}/media/file/">`,s=i;i.includes(`<base `)||(s=i.includes(`<head>`)?i.replace(`<head>`,`<head>`+o):i.includes(`<HEAD>`)?i.replace(`<HEAD>`,`<HEAD>`+o):o+i);let c=`<style>img,video{object-position:center center}</style>`;s.includes(`object-position:center center`)||(s.includes(`</head>`)?s=s.replace(`</head>`,c+`</head>`):s.includes(`</HEAD>`)&&(s=s.replace(`</HEAD>`,c+`</HEAD>`))),s=s.replace(/\[\[ViewPortWidth]]/g,`device-width`),s=s.replace(/https?:\/\/[^\s"')<]+\.m3u8\b/gi,e=>`/stream-proxy?url=`+encodeURIComponent(e));let l=RegExp(`https?://[^"'\\s)]+?(${t.replace(/\//g,`\\/`)}/dependencies/[^"'\\s?)]+)(\\?[^"'\\s)]*)?`,`g`);if(s=s.replace(l,(e,t)=>t),s=s.replace(/(<(?:script|link)\b[^>]*(?:src|href)=")(?!\/|https?:\/\/)(bundle\.min\.js|fonts\.css)(")/g,`$1${t}/dependencies/$2$3`),!s.includes(`xiboICTargetId`)){let e=`<script>var xiboICTargetId = '${r}';<\/script>`;s=s.includes(o)?s.replace(o,o+e):s.includes(`<head>`)?s.replace(`<head>`,`<head>`+e):e+s}return s=s.replace(/hostAddress\s*:\s*["']https?:\/\/[^"']+["']/g,`hostAddress: "${N}/ic"`),M.info(`Injected base tag in widget HTML`),(await fetch(`/store${t}/widgets/${e}/${n}/${r}`,{method:`PUT`,headers:{"Content-Type":`text/html; charset=utf-8`},body:s})).body?.cancel(),M.info(`Stored widget HTML at ${a} (${s.length} bytes)`),{cacheKey:a,html:s}}var F=e({BARRIER:()=>x,CacheAnalyzer:()=>j,CacheManager:()=>a,DownloadManager:()=>O,FILE_TYPES:()=>l,FileDownload:()=>T,LayoutTaskBuilder:()=>E,StoreClient:()=>c,VERSION:()=>I,cacheManager:()=>o,cacheWidgetHtml:()=>P,getFileTypeConfig:()=>u,isUrlExpired:()=>C}),I=r.version;export{O as a,x as i,P as n,c as o,j as r,F as t};
2
+ //# sourceMappingURL=src-S093VuQs.js.map