From 5a552cd5658cc5f53d7e58869f86bb91ace5f28e Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 26 Jun 2026 17:48:06 -0500 Subject: [PATCH] refactor(epg): Update xmltv_prev_days_override setting to EPG settings and adjust related logic. Refactor CoreSettings to include a dedicated method for retrieving the override value. Update tests and frontend components to reflect the new structure and ensure consistent behavior across settings management. --- CHANGELOG.md | 2 +- apps/channels/tests/test_catchup_utils.py | 4 +- apps/channels/utils.py | 5 +- core/models.py | 12 ++- .../forms/settings/EpgSettingsForm.jsx | 81 +++++++++++++++++++ .../forms/settings/ProxySettingsForm.jsx | 5 +- frontend/src/constants.js | 3 + frontend/src/pages/Settings.jsx | 16 ++++ .../forms/settings/EpgSettingsFormUtils.js | 3 + .../forms/settings/ProxySettingsFormUtils.js | 1 - .../__tests__/ProxySettingsFormUtils.test.js | 2 - frontend/src/utils/pages/SettingsUtils.js | 23 +++++- 12 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/forms/settings/EpgSettingsForm.jsx create mode 100644 frontend/src/utils/forms/settings/EpgSettingsFormUtils.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4890e407..232b06df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Multi-provider failover.** Catch-up walks the channel's catch-up streams in order (like live playback): if one provider cannot serve the archive the next is tried, each attempt with its own account context (credentials, reported timezone, user-agent). Accounts that fail with an auth/ban-class status are not retried via their other streams. - **Provider pool accounting.** Catch-up reserves a provider profile slot (`connection_pool`) before connecting upstream — same contract as live and VOD — and releases it exactly once when the session ends (one-shot Redis token, covering disconnects before the first chunk). When the default profile is at capacity it walks the account's alternate profiles with their own resolved credentials, and answers `503` when every profile is full. - **One catch-up session per user and channel.** A seek or programme jump displaces the user's previous session on the same channel: its provider slot is released synchronously and the old stream is stopped through the standard stop-key mechanism, so rapid seeking can't stack upstream provider connections. - - **Single setting**, `xmltv_prev_days_override`, under `Settings → Proxy Settings` (default `0` = auto-detect). Verbose timeshift logging follows the standard logger DEBUG level. + - **Single setting**, `xmltv_prev_days_override`, under `Settings → EPG` (default `0` = auto-detect). Verbose timeshift logging follows the standard logger DEBUG level. - **EPG XMLTV `prev_days` auto-detection** from the provider's largest `tv_archive_duration` (capped at 30 days), overridable via setting or `?prev_days=` URL parameter. - **Byte path streams directly via `iter_content` + generator yield** (same pattern as the VOD proxy), with an upfront MPEG-TS sync peek that strips any PHP-warning preamble before bytes reach strict demuxers. Throughput stays comfortably above the typical 5 Mbps FHD bitrate so clients don't buffer. diff --git a/apps/channels/tests/test_catchup_utils.py b/apps/channels/tests/test_catchup_utils.py index 68ac20c3..d79eec11 100644 --- a/apps/channels/tests/test_catchup_utils.py +++ b/apps/channels/tests/test_catchup_utils.py @@ -45,8 +45,8 @@ class ResolveXcEpgPrevDaysTests(SimpleTestCase): mock_compute.assert_not_called() @patch("apps.channels.utils.compute_provider_archive_days_capped", return_value=14) - @patch("core.models.CoreSettings.get_proxy_settings", return_value={"xmltv_prev_days_override": 7}) - def test_proxy_override_prevents_auto_detect(self, _settings, mock_compute): + @patch("core.models.CoreSettings.get_xmltv_prev_days_override", return_value=7) + def test_epg_settings_override_prevents_auto_detect(self, _override, mock_compute): request = self.factory.get("/xmltv.php") self.assertEqual(resolve_xc_epg_prev_days(request, self.user), 7) mock_compute.assert_not_called() diff --git a/apps/channels/utils.py b/apps/channels/utils.py index b2cceab6..70b9eecd 100644 --- a/apps/channels/utils.py +++ b/apps/channels/utils.py @@ -67,7 +67,7 @@ def resolve_xc_epg_prev_days(request, user, *, auto_detect_fallback=True): Resolution order: 1. URL ``?prev_days=`` (explicit; 0 means no past programmes) 2. ``user.custom_properties.epg_prev_days`` - 3. ``CoreSettings.proxy_settings.xmltv_prev_days_override`` when > 0 + 3. ``CoreSettings.epg_settings.xmltv_prev_days_override`` when > 0 4. Auto-detect (only when *auto_detect_fallback* is True) """ user_custom = (user.custom_properties or {}) if user else {} @@ -87,9 +87,8 @@ def resolve_xc_epg_prev_days(request, user, *, auto_detect_fallback=True): from core.models import CoreSettings - proxy_settings = CoreSettings.get_proxy_settings() try: - override = int(proxy_settings.get("xmltv_prev_days_override", 0) or 0) + override = int(CoreSettings.get_xmltv_prev_days_override() or 0) except (TypeError, ValueError): override = 0 if override > 0: diff --git a/core/models.py b/core/models.py index 9c5a5795..58fd37da 100644 --- a/core/models.py +++ b/core/models.py @@ -280,8 +280,18 @@ class CoreSettings(models.Model): "epg_match_ignore_prefixes": [], "epg_match_ignore_suffixes": [], "epg_match_ignore_custom": [], + # XC catch-up: forced XMLTV lookback (0 = auto-detect, capped at 30). + "xmltv_prev_days_override": 0, }) + @classmethod + def get_xmltv_prev_days_override(cls): + """Global XC XMLTV prev_days default (0 = auto-detect from provider archives).""" + try: + return int(cls.get_epg_settings().get("xmltv_prev_days_override", 0) or 0) + except (TypeError, ValueError): + return 0 + @classmethod def _safe_string_list(cls, value): """Return a list of strings, filtering out non-list or non-string values.""" @@ -396,8 +406,6 @@ class CoreSettings(models.Model): "channel_shutdown_delay": 0, "channel_init_grace_period": 5, "new_client_behind_seconds": 5, - # XC catch-up: forced XMLTV lookback (0 = auto-detect, capped at 30). - "xmltv_prev_days_override": 0, }) # System Settings diff --git a/frontend/src/components/forms/settings/EpgSettingsForm.jsx b/frontend/src/components/forms/settings/EpgSettingsForm.jsx new file mode 100644 index 00000000..5668dbdd --- /dev/null +++ b/frontend/src/components/forms/settings/EpgSettingsForm.jsx @@ -0,0 +1,81 @@ +import useSettingsStore from '../../../store/settings.jsx'; +import React, { useEffect, useState } from 'react'; +import { useForm } from '@mantine/form'; +import { + Alert, + Button, + Flex, + NumberInput, + Stack, + Text, +} from '@mantine/core'; +import { EPG_SETTINGS_OPTIONS } from '../../../constants.js'; +import { + getChangedSettings, + parseSettings, + saveChangedSettings, +} from '../../../utils/pages/SettingsUtils.js'; +import { getEpgSettingsFormInitialValues } from '../../../utils/forms/settings/EpgSettingsFormUtils.js'; + +const EpgSettingsForm = React.memo(({ active }) => { + const settings = useSettingsStore((s) => s.settings); + const [saved, setSaved] = useState(false); + + const form = useForm({ + mode: 'controlled', + initialValues: getEpgSettingsFormInitialValues(), + }); + + useEffect(() => { + if (!active) setSaved(false); + }, [active]); + + useEffect(() => { + if (settings) { + const parsed = parseSettings(settings); + form.setFieldValue( + 'xmltv_prev_days_override', + parsed.xmltv_prev_days_override ?? 0, + ); + } + }, [settings]); + + const onSubmit = async () => { + setSaved(false); + const changedSettings = getChangedSettings(form.getValues(), settings); + try { + await saveChangedSettings(settings, changedSettings); + setSaved(true); + } catch (error) { + console.error('Error saving EPG settings:', error); + } + }; + + const prevDaysConfig = EPG_SETTINGS_OPTIONS.xmltv_prev_days_override; + + return ( +
+ + {saved && ( + + )} + + + Per-user defaults and URL parameters still override this global value. + EPG channel matching options are configured from the Channels page. + + + + + +
+ ); +}); + +export default EpgSettingsForm; diff --git a/frontend/src/components/forms/settings/ProxySettingsForm.jsx b/frontend/src/components/forms/settings/ProxySettingsForm.jsx index 9b2c2c62..52440769 100644 --- a/frontend/src/components/forms/settings/ProxySettingsForm.jsx +++ b/frontend/src/components/forms/settings/ProxySettingsForm.jsx @@ -25,7 +25,6 @@ const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => { 'channel_shutdown_delay', 'channel_init_grace_period', 'new_client_behind_seconds', - 'xmltv_prev_days_override', ].includes(key); }; const isFloatField = (key) => { @@ -40,9 +39,7 @@ const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => { ? 300 : key === 'new_client_behind_seconds' ? 120 - : key === 'xmltv_prev_days_override' - ? 30 - : 60; + : 60; }; return ( <> diff --git a/frontend/src/constants.js b/frontend/src/constants.js index 00af156e..b4503dc6 100644 --- a/frontend/src/constants.js +++ b/frontend/src/constants.js @@ -61,6 +61,9 @@ export const PROXY_SETTINGS_OPTIONS = { description: 'Seconds of received buffer to start behind live when a new client connects (0 = start at live). Note: this is chunk receive time, not video duration.', }, +}; + +export const EPG_SETTINGS_OPTIONS = { xmltv_prev_days_override: { label: 'XMLTV prev_days Override (catch-up)', description: diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 29d46dd8..1c7964cc 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -45,6 +45,9 @@ const DvrSettingsForm = React.lazy( const SystemSettingsForm = React.lazy( () => import('../components/forms/settings/SystemSettingsForm.jsx') ); +const EpgSettingsForm = React.lazy( + () => import('../components/forms/settings/EpgSettingsForm.jsx') +); const NavOrderForm = React.lazy( () => import('../components/forms/settings/NavOrderForm.jsx') ); @@ -122,6 +125,19 @@ const SettingsPage = () => { + + EPG + + + }> + + + + + + System Settings diff --git a/frontend/src/utils/forms/settings/EpgSettingsFormUtils.js b/frontend/src/utils/forms/settings/EpgSettingsFormUtils.js new file mode 100644 index 00000000..d2138b6a --- /dev/null +++ b/frontend/src/utils/forms/settings/EpgSettingsFormUtils.js @@ -0,0 +1,3 @@ +export const getEpgSettingsFormInitialValues = () => ({ + xmltv_prev_days_override: 0, +}); diff --git a/frontend/src/utils/forms/settings/ProxySettingsFormUtils.js b/frontend/src/utils/forms/settings/ProxySettingsFormUtils.js index 93cccc9b..eddbba91 100644 --- a/frontend/src/utils/forms/settings/ProxySettingsFormUtils.js +++ b/frontend/src/utils/forms/settings/ProxySettingsFormUtils.js @@ -15,6 +15,5 @@ export const getProxySettingDefaults = () => { channel_shutdown_delay: 0, channel_init_grace_period: 5, new_client_behind_seconds: 5, - xmltv_prev_days_override: 0, }; }; diff --git a/frontend/src/utils/forms/settings/__tests__/ProxySettingsFormUtils.test.js b/frontend/src/utils/forms/settings/__tests__/ProxySettingsFormUtils.test.js index c1fd0908..21c48269 100644 --- a/frontend/src/utils/forms/settings/__tests__/ProxySettingsFormUtils.test.js +++ b/frontend/src/utils/forms/settings/__tests__/ProxySettingsFormUtils.test.js @@ -62,7 +62,6 @@ describe('ProxySettingsFormUtils', () => { channel_shutdown_delay: 0, channel_init_grace_period: 5, new_client_behind_seconds: 5, - xmltv_prev_days_override: 0, }); }); @@ -83,7 +82,6 @@ describe('ProxySettingsFormUtils', () => { expect(typeof result.channel_shutdown_delay).toBe('number'); expect(typeof result.channel_init_grace_period).toBe('number'); expect(typeof result.new_client_behind_seconds).toBe('number'); - expect(typeof result.xmltv_prev_days_override).toBe('number'); }); }); }); diff --git a/frontend/src/utils/pages/SettingsUtils.js b/frontend/src/utils/pages/SettingsUtils.js index acdc6cae..bd46ac52 100644 --- a/frontend/src/utils/pages/SettingsUtils.js +++ b/frontend/src/utils/pages/SettingsUtils.js @@ -39,6 +39,7 @@ export const saveChangedSettings = async (settings, changedSettings) => { 'epg_match_ignore_prefixes', 'epg_match_ignore_suffixes', 'epg_match_ignore_custom', + 'xmltv_prev_days_override', ]; const dvrFields = [ 'tv_template', @@ -125,6 +126,7 @@ export const saveChangedSettings = async (settings, changedSettings) => { 'retention_count', 'schedule_day_of_week', 'max_system_events', + 'xmltv_prev_days_override', ]; if (numericFields.includes(formKey) && value != null) { value = typeof value === 'number' ? value : parseInt(value, 10); @@ -222,6 +224,20 @@ export const getChangedSettings = (values, settings) => { continue; } + if (settingKey === 'xmltv_prev_days_override') { + const baseline = Number( + settings['epg_settings']?.value?.xmltv_prev_days_override ?? 0, + ); + const nextVal = + typeof actualValue === 'number' + ? actualValue + : parseInt(actualValue, 10) || 0; + if (nextVal !== baseline) { + changedSettings[settingKey] = nextVal; + } + continue; + } + // Convert array values (like m3u_hash_key) to comma-separated strings for comparison if (Array.isArray(actualValue)) { actualValue = actualValue.join(','); @@ -296,6 +312,12 @@ export const parseSettings = (settings) => { epgSettings && Array.isArray(epgSettings.epg_match_ignore_custom) ? epgSettings.epg_match_ignore_custom : []; + parsed.xmltv_prev_days_override = + epgSettings && epgSettings.xmltv_prev_days_override != null + ? typeof epgSettings.xmltv_prev_days_override === 'number' + ? epgSettings.xmltv_prev_days_override + : parseInt(epgSettings.xmltv_prev_days_override, 10) || 0 + : 0; // DVR settings - direct mapping with underscore keys const dvrSettings = settings['dvr_settings']?.value; @@ -367,7 +389,6 @@ export const parseSettings = (settings) => { } // Proxy and network access are already grouped objects - // (xmltv_prev_days_override lives inside proxy_settings) if (settings['proxy_settings']?.value) { parsed.proxy_settings = settings['proxy_settings'].value; }