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.

This commit is contained in:
SergeantPanda 2026-06-26 17:48:06 -05:00
parent ca3d0eb9e1
commit 5a552cd565
12 changed files with 141 additions and 16 deletions

View file

@ -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.

View file

@ -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()

View file

@ -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:

View file

@ -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

View file

@ -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 (
<form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap="md">
{saved && (
<Alert variant="light" color="green" title="Saved Successfully" />
)}
<NumberInput
label={prevDaysConfig.label}
description={prevDaysConfig.description}
min={0}
max={30}
{...form.getInputProps('xmltv_prev_days_override')}
/>
<Text size="xs" c="dimmed">
Per-user defaults and URL parameters still override this global value.
EPG channel matching options are configured from the Channels page.
</Text>
<Flex justify="flex-end">
<Button type="submit">Save</Button>
</Flex>
</Stack>
</form>
);
});
export default EpgSettingsForm;

View file

@ -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 (
<>

View file

@ -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:

View file

@ -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 = () => {
</AccordionPanel>
</AccordionItem>
<AccordionItem value="epg-settings">
<AccordionControl>EPG</AccordionControl>
<AccordionPanel>
<ErrorBoundary>
<Suspense fallback={<Loader />}>
<EpgSettingsForm
active={accordianValue === 'epg-settings'}
/>
</Suspense>
</ErrorBoundary>
</AccordionPanel>
</AccordionItem>
<AccordionItem value="system-settings">
<AccordionControl>System Settings</AccordionControl>
<AccordionPanel>

View file

@ -0,0 +1,3 @@
export const getEpgSettingsFormInitialValues = () => ({
xmltv_prev_days_override: 0,
});

View file

@ -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,
};
};

View file

@ -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');
});
});
});

View file

@ -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;
}