From e1f8fa67231da21829161aae8cc43c722dffc548 Mon Sep 17 00:00:00 2001 From: Bastien Wirtz Date: Thu, 7 Aug 2025 20:11:22 +0200 Subject: [PATCH] feat(auto-refresh): centralized auto refresh System --- src/App.vue | 5 + src/components/services/DockerSocketProxy.vue | 8 +- src/components/services/FreshRSS.vue | 6 +- src/components/services/Glances.vue | 8 +- src/components/services/Immich.vue | 8 +- src/components/services/Lidarr.vue | 9 +- src/components/services/PiAlert.vue | 8 +- src/components/services/PiHole.vue | 33 +---- src/components/services/Ping.vue | 7 +- src/components/services/Plex.vue | 8 +- src/components/services/Prowlarr.vue | 9 +- src/components/services/Radarr.vue | 9 +- src/components/services/Rtorrent.vue | 22 ++- src/components/services/SABnzbd.vue | 7 +- src/components/services/Scrutiny.vue | 8 +- src/components/services/Sonarr.vue | 9 +- src/components/services/Tautulli.vue | 7 +- src/components/services/Tdarr.vue | 7 +- src/components/services/qBittorrent.vue | 19 ++- src/mixins/service.js | 96 ++++++++++++- src/utils/updateScheduler.js | 131 ++++++++++++++++++ 21 files changed, 310 insertions(+), 114 deletions(-) create mode 100644 src/utils/updateScheduler.js diff --git a/src/App.vue b/src/App.vue index 7a12097..2e0e5d0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -130,6 +130,11 @@ export default { DarkMode, DynamicTheme, }, + provide() { + return { + config: () => this.config, + }; + }, data: function () { return { loaded: false, diff --git a/src/components/services/DockerSocketProxy.vue b/src/components/services/DockerSocketProxy.vue index 444f11a..345c7ed 100644 --- a/src/components/services/DockerSocketProxy.vue +++ b/src/components/services/DockerSocketProxy.vue @@ -48,10 +48,10 @@ export default { }; }, created: function () { - const checkInterval = parseInt(this.item.checkInterval, 10) || 0; - if (checkInterval > 0) { - setInterval(() => this.fetchData(), checkInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchData; + + // Initial data fetch this.fetchData(); }, methods: { diff --git a/src/components/services/FreshRSS.vue b/src/components/services/FreshRSS.vue index fa49fe6..5947bef 100644 --- a/src/components/services/FreshRSS.vue +++ b/src/components/services/FreshRSS.vue @@ -40,10 +40,10 @@ export default { }; }, created: function () { - const updateInterval = parseInt(this.item.updateInterval, 10) || 0; - if (updateInterval > 0) - setInterval(() => this.fetchConfig(), updateInterval); + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchConfig; + // Initial data fetch this.fetchConfig(); }, methods: { diff --git a/src/components/services/Glances.vue b/src/components/services/Glances.vue index 0630d88..327ae6c 100644 --- a/src/components/services/Glances.vue +++ b/src/components/services/Glances.vue @@ -29,10 +29,10 @@ export default { error: null, }), created() { - const updateInterval = parseInt(this.item.updateInterval, 10) || 0; - if (updateInterval > 0) { - setInterval(() => this.fetchStat(), updateInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchStat; + + // Initial data fetch this.fetchStat(); }, methods: { diff --git a/src/components/services/Immich.vue b/src/components/services/Immich.vue index 1967588..390dd81 100644 --- a/src/components/services/Immich.vue +++ b/src/components/services/Immich.vue @@ -62,10 +62,10 @@ export default { }, }, created: function () { - const updateInterval = parseInt(this.item.updateInterval, 10) || 0; - if (updateInterval > 0) { - setInterval(() => this.fetchConfig(), updateInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchConfig; + + // Initial data fetch this.fetchConfig(); }, methods: { diff --git a/src/components/services/Lidarr.vue b/src/components/services/Lidarr.vue index 970d238..1846338 100644 --- a/src/components/services/Lidarr.vue +++ b/src/components/services/Lidarr.vue @@ -43,12 +43,11 @@ export default { serverError: false, }; }, - created: function () { - const checkInterval = parseInt(this.item.checkInterval, 10) || 0; - if (checkInterval > 0) { - setInterval(() => this.fetchConfig(), checkInterval); - } + created() { + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchConfig; + // Initial data fetch this.fetchConfig(); }, methods: { diff --git a/src/components/services/PiAlert.vue b/src/components/services/PiAlert.vue index 63bfebc..ba231ee 100644 --- a/src/components/services/PiAlert.vue +++ b/src/components/services/PiAlert.vue @@ -44,10 +44,10 @@ export default { }; }, created() { - const updateInterval = parseInt(this.item.updateInterval, 10) || 0; - if (updateInterval > 0) { - setInterval(() => this.fetchStatus(), updateInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchStatus; + + // Initial data fetch this.fetchStatus(); }, methods: { diff --git a/src/components/services/PiHole.vue b/src/components/services/PiHole.vue index 0d9dd78..7f9a9c7 100644 --- a/src/components/services/PiHole.vue +++ b/src/components/services/PiHole.vue @@ -39,8 +39,6 @@ export default { retryCount: 0, maxRetries: 3, retryDelay: 5000, - localCheckInterval: 1000, // Default value or a fallback - pollInterval: null, }), computed: { percentage: function () { @@ -57,18 +55,16 @@ export default { }, created() { if (parseInt(this.item.apiVersion, 10) === 6) { - // Set the interval to the checkInterval or default to 5 minutes - this.localCheckInterval = parseInt(this.item.checkInterval, 10) || 300000; this.loadCachedSession(); - this.startStatusPolling(); + + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchStatus; } else { - this.fetchStatus_v5(); - } - }, - beforeUnmount() { - if (parseInt(this.item.apiVersion, 10) === 6) { - this.stopStatusPolling(); + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchStatus_v5(); } + // Initial data fetch + this.autoUpdateMethod(); }, methods: { handleError: function (error, status) { @@ -76,21 +72,6 @@ export default { this.subtitle = error; this.status = status; }, - startStatusPolling: function () { - this.fetchStatus(); - if (this.localCheckInterval < 1000) { - this.localCheckInterval = 1000; - } - this.pollInterval = setInterval( - this.fetchStatus, - this.localCheckInterval, - ); - }, - stopStatusPolling: function () { - if (this.pollInterval) { - clearInterval(this.pollInterval); - } - }, loadCachedSession: function () { try { const cachedSession = localStorage.getItem( diff --git a/src/components/services/Ping.vue b/src/components/services/Ping.vue index 88b03c7..46b5362 100644 --- a/src/components/services/Ping.vue +++ b/src/components/services/Ping.vue @@ -41,11 +41,10 @@ export default { }, }, created() { - const updateInterval = parseInt(this.item.updateInterval, 10) || 0; - if (updateInterval > 0) { - setInterval(this.fetchStatus, updateInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchStatus; + // Initial data fetch this.fetchStatus(); }, methods: { diff --git a/src/components/services/Plex.vue b/src/components/services/Plex.vue index f65ff18..135b0e0 100644 --- a/src/components/services/Plex.vue +++ b/src/components/services/Plex.vue @@ -52,10 +52,10 @@ export default { }; }, created: function () { - const checkInterval = parseInt(this.item.checkInterval, 10) || 0; - if (checkInterval > 0) { - setInterval(() => this.fetchData(), checkInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchData; + + // Initial data fetch this.fetchData(); }, methods: { diff --git a/src/components/services/Prowlarr.vue b/src/components/services/Prowlarr.vue index 95890da..3ea9ef5 100644 --- a/src/components/services/Prowlarr.vue +++ b/src/components/services/Prowlarr.vue @@ -36,12 +36,11 @@ export default { serverError: false, }; }, - created: function () { - const checkInterval = parseInt(this.item.checkInterval, 10) || 0; - if (checkInterval > 0) { - setInterval(() => this.fetchConfig(), checkInterval); - } + created() { + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchConfig; + // Initial data fetch this.fetchConfig(); }, methods: { diff --git a/src/components/services/Radarr.vue b/src/components/services/Radarr.vue index 77a3af9..014f731 100644 --- a/src/components/services/Radarr.vue +++ b/src/components/services/Radarr.vue @@ -51,12 +51,11 @@ export default { return this.item.legacyApi ? LEGACY_API : V3_API; }, }, - created: function () { - const checkInterval = parseInt(this.item.checkInterval, 10) || 0; - if (checkInterval > 0) { - setInterval(() => this.fetchConfig(), checkInterval); - } + created() { + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchConfig; + // Initial data fetch this.fetchConfig(); }, methods: { diff --git a/src/components/services/Rtorrent.vue b/src/components/services/Rtorrent.vue index 7830a68..5c0be86 100644 --- a/src/components/services/Rtorrent.vue +++ b/src/components/services/Rtorrent.vue @@ -59,24 +59,18 @@ export default { }, }, created() { - // Set intervals if configured so the rates and/or torrent count - // will be updated. - const rateInterval = parseInt(this.item.rateInterval, 10) || 0; - const torrentInterval = parseInt(this.item.torrentInterval, 10) || 0; - - if (rateInterval > 0) { - setInterval(() => this.fetchRates(), rateInterval); - } - - if (torrentInterval > 0) { - setInterval(() => this.fetchCount(), torrentInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchAllData; // Fetch the initial values. - this.fetchRates(); - this.fetchCount(); + this.fetchAllData(); }, methods: { + // Combined method for scheduler - fetches both rates and count + fetchAllData: async function () { + this.fetchRates(); + this.fetchCount(); + }, // Perform two calls to the XML-RPC service and fetch download // and upload rates. Values are saved to the `ul` and `dl` // properties. diff --git a/src/components/services/SABnzbd.vue b/src/components/services/SABnzbd.vue index 417968a..1223fa1 100644 --- a/src/components/services/SABnzbd.vue +++ b/src/components/services/SABnzbd.vue @@ -80,11 +80,10 @@ export default { }, }, created() { - const downloadInterval = parseInt(this.item.downloadInterval, 10) || 0; - if (downloadInterval > 0) { - setInterval(() => this.fetchStatus(), downloadInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchStatus; + // Initial data fetch this.fetchStatus(); }, methods: { diff --git a/src/components/services/Scrutiny.vue b/src/components/services/Scrutiny.vue index 17721ca..0b2be1b 100644 --- a/src/components/services/Scrutiny.vue +++ b/src/components/services/Scrutiny.vue @@ -40,10 +40,10 @@ export default { }; }, created: function () { - const updateInterval = parseInt(this.item.updateInterval, 10) || 0; - if (updateInterval > 0) { - setInterval(() => this.fetchSummary(), updateInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchSummary; + + // Initial data fetch this.fetchSummary(); }, methods: { diff --git a/src/components/services/Sonarr.vue b/src/components/services/Sonarr.vue index 3a610d5..69a6671 100644 --- a/src/components/services/Sonarr.vue +++ b/src/components/services/Sonarr.vue @@ -52,12 +52,11 @@ export default { return this.item.legacyApi ? LEGACY_API : V3_API; }, }, - created: function () { - const checkInterval = parseInt(this.item.checkInterval, 10) || 0; - if (checkInterval > 0) { - setInterval(() => this.fetchConfig(), checkInterval); - } + created() { + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchConfig; + // Initial data fetch this.fetchConfig(); }, methods: { diff --git a/src/components/services/Tautulli.vue b/src/components/services/Tautulli.vue index 77e781d..e7bf947 100644 --- a/src/components/services/Tautulli.vue +++ b/src/components/services/Tautulli.vue @@ -41,11 +41,10 @@ export default { }, }, created() { - const checkInterval = parseInt(this.item.checkInterval, 10) || 0; - if (checkInterval > 0) { - setInterval(() => this.fetchStatus(), checkInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchStatus; + // Initial data fetch this.fetchStatus(); }, methods: { diff --git a/src/components/services/Tdarr.vue b/src/components/services/Tdarr.vue index bc5c892..dd821ca 100644 --- a/src/components/services/Tdarr.vue +++ b/src/components/services/Tdarr.vue @@ -54,11 +54,10 @@ export default { }, }, created() { - const checkInterval = parseInt(this.item.checkInterval, 10) || 0; - if (checkInterval > 0) { - setInterval(() => this.fetchStatus(), checkInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchStatus; + // Initial data fetch this.fetchStatus(); }, methods: { diff --git a/src/components/services/qBittorrent.vue b/src/components/services/qBittorrent.vue index 829e850..1829588 100644 --- a/src/components/services/qBittorrent.vue +++ b/src/components/services/qBittorrent.vue @@ -61,19 +61,18 @@ export default { }, }, created() { - const rateInterval = parseInt(this.item.rateInterval, 10) || 0; - const torrentInterval = parseInt(this.item.torrentInterval, 10) || 0; - if (rateInterval > 0) { - setInterval(() => this.getRate(), rateInterval); - } - if (torrentInterval > 0) { - setInterval(() => this.fetchCount(), torrentInterval); - } + // Set up auto-update method for the scheduler + this.autoUpdateMethod = this.fetchAllData; - this.getRate(); - this.fetchCount(); + // Fetch initial values + this.fetchAllData(); }, methods: { + // Combined method for scheduler - fetches both rates and count + fetchAllData: async function () { + this.getRate(); + this.fetchCount(); + }, fetchCount: async function () { try { const body = await this.fetch("/api/v2/torrents/info"); diff --git a/src/mixins/service.js b/src/mixins/service.js index 6d5c253..aae6985 100644 --- a/src/mixins/service.js +++ b/src/mixins/service.js @@ -1,15 +1,35 @@ +import updateScheduler from "@/utils/updateScheduler.js"; + export default { props: { proxy: Object, }, + inject: { + // Inject global config from parent components + config: { + default: () => ({}), + }, + }, + computed: { + globalConfig() { + return this.config() || {}; + }, + }, created: function () { - // custom service often consume info from an API using the item link (url) as a base url, + // Custom service often consume info from an API using the item link (url) as a base url, // but sometimes the base url is different. An optional alternative URL can be provided with the "endpoint" key. this.endpoint = this.item.endpoint || this.item.url; if (this.endpoint && this.endpoint.endsWith("/")) { this.endpoint = this.endpoint.slice(0, -1); } + + // Initialize auto-update if configured + this.initAutoUpdate(); + }, + beforeUnmount() { + // Clean up auto-update registration + updateScheduler.unregister(this); }, methods: { fetch: function (path, init, json = true) { @@ -62,5 +82,79 @@ export default { return json ? response.json() : response.text(); }); }, + initAutoUpdate: function () { + // Check if component has defined an auto-update method and interval + const interval = this.getUpdateInterval(); + if ( + interval > 0 && + this.autoUpdateMethod && + typeof this.autoUpdateMethod === "function" + ) { + updateScheduler.register(this, interval, this.autoUpdateMethod); + } + }, + getUpdateInterval: function () { + // Check if auto-update is explicitly disabled for this service + if (this.item.autoUpdateInterval === false) { + return 0; + } + + // Use service-specific interval if defined + if (this.item.autoUpdateInterval) { + return parseInt(this.item.autoUpdateInterval, 10) || 0; + } + + // Check for deprecated keys and warn users + const deprecatedKeys = [ + "updateInterval", + "checkInterval", + "localCheckInterval", + "downloadInterval", + "rateInterval", + "torrentInterval", + ]; + + for (const key of deprecatedKeys) { + if (this.item[key]) { + console.warn( + `[DEPRECATED] Service "${this.item.name || "unknown"}" uses deprecated config key "${key}". ` + + `Please use "autoUpdateInterval" instead. Support for "${key}" will be removed in a future version.`, + ); + return parseInt(this.item[key], 10) || 0; + } + } + + // Use global auto-update configuration + return this.getGlobalAutoUpdateInterval(); + }, + + getGlobalAutoUpdateInterval: function () { + const globalAutoUpdate = this.globalConfig.autoUpdate; + + // If auto-update is not configured globally, disable + if (!globalAutoUpdate) { + return 0; + } + + // If global auto-update is explicitly disabled + if (globalAutoUpdate.enabled === false) { + return 0; + } + + // If autoUpdate is just a number (simplified config) + if (typeof globalAutoUpdate === "number") { + return globalAutoUpdate; + } + + // If autoUpdate is an object, use defaultInterval + if ( + typeof globalAutoUpdate === "object" && + globalAutoUpdate.defaultInterval + ) { + return parseInt(globalAutoUpdate.defaultInterval, 10) || 0; + } + + return 0; + }, }, }; diff --git a/src/utils/updateScheduler.js b/src/utils/updateScheduler.js new file mode 100644 index 0000000..64e126b --- /dev/null +++ b/src/utils/updateScheduler.js @@ -0,0 +1,131 @@ +/** + * This module provides a single-timer solution for managing automatic data updates + * across all service components in Homer. Instead of each service component creating + * its own setInterval timer, all components register with this centralized scheduler. + * + */ +class UpdateScheduler { + constructor() { + this.registeredComponents = new Map(); + this.globalTimer = null; + this.tickCount = 0; + this.isRunning = false; + } + + register(component, intervalMs, updateMethod) { + if (!component || !updateMethod || intervalMs <= 0) { + console.warn("UpdateScheduler: Invalid registration parameters"); + return; + } + + const intervalSeconds = Math.floor(intervalMs / 1000); + const componentId = this.generateComponentId(component); + + this.registeredComponents.set(componentId, { + component, + interval: intervalSeconds, + method: updateMethod, + lastUpdate: 0, + }); + + this.startGlobalTimer(); + console.log( + `UpdateScheduler: Registered component with ${intervalSeconds}s interval`, + ); + } + + unregister(component) { + const componentId = this.generateComponentId(component); + const removed = this.registeredComponents.delete(componentId); + + if (removed) { + console.log("UpdateScheduler: Unregistered component"); + } + + if (this.registeredComponents.size === 0) { + this.stopGlobalTimer(); + } + } + + generateComponentId(component) { + // Use component's unique identifier or Vue instance uid + return component._uid || component.$.uid || Symbol("component"); + } + + startGlobalTimer() { + if (!this.globalTimer && !this.isRunning) { + this.isRunning = true; + this.tickCount = 0; + + this.globalTimer = setInterval(() => { + this.tickCount++; + this.processUpdates(); + }, 1000); + + console.log("UpdateScheduler: Global timer started"); + } + } + + stopGlobalTimer() { + if (this.globalTimer) { + clearInterval(this.globalTimer); + this.globalTimer = null; + this.isRunning = false; + this.tickCount = 0; + console.log("UpdateScheduler: Global timer stopped"); + } + } + + processUpdates() { + for (const [, config] of this.registeredComponents) { + try { + if (this.tickCount - config.lastUpdate >= config.interval) { + config.method.call(config.component); + config.lastUpdate = this.tickCount; + } + } catch (error) { + console.error("UpdateScheduler: Error during component update:", error); + } + } + } + + pause() { + if (this.globalTimer) { + clearInterval(this.globalTimer); + this.globalTimer = null; + this.isRunning = false; + console.log("UpdateScheduler: Paused"); + } + } + + resume() { + if (!this.globalTimer && this.registeredComponents.size > 0) { + this.startGlobalTimer(); + console.log("UpdateScheduler: Resumed"); + } + } + + getStatus() { + return { + isRunning: this.isRunning, + registeredCount: this.registeredComponents.size, + tickCount: this.tickCount, + }; + } +} + +// Create and export global singleton instance +const updateScheduler = new UpdateScheduler(); + +// Pause updates when tab is hidden (power saving) +if (typeof document !== "undefined") { + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + updateScheduler.pause(); + } else { + updateScheduler.resume(); + } + }); +} + +export default updateScheduler;