mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
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:
parent
ca3d0eb9e1
commit
5a552cd565
12 changed files with 141 additions and 16 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
81
frontend/src/components/forms/settings/EpgSettingsForm.jsx
Normal file
81
frontend/src/components/forms/settings/EpgSettingsForm.jsx
Normal 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;
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export const getEpgSettingsFormInitialValues = () => ({
|
||||
xmltv_prev_days_override: 0,
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue