Enhancement: Improved the HDHR, M3U, and EPG URL builder popovers in the Channels table

This commit is contained in:
SergeantPanda 2026-04-12 17:24:17 -05:00
parent 95fba1cb84
commit 62848c0ea0
2 changed files with 78 additions and 55 deletions

View file

@ -65,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the HDHR, M3U, and EPG URL builder popovers in the Channels table: each popover now opens with a brief intro sentence describing its purpose. Toggle switches were refactored to use Mantine's native `label` and `description` props (replacing the previous manual `Group`/`Stack`/`Text` layout), giving each switch a properly styled description line beneath its label. Switch alignment was also corrected. Toggles now appear on the left with the label and description stacked to the right, consistent with standard Mantine form layout.
- Redesigned the User settings modal with a tabbed layout: **Account** (username, email, name, password), **Permissions** (user level, stream limit, channel profiles, mature content filter - admin only), **EPG Defaults** (days forward/back), and **API & XC** (XC password, API key management). Fields are now logically grouped rather than split across two ad-hoc columns.
- EPG channel scanning now automatically removes stale `EPGData` entries. tvg-ids that were present in a previous scan but are no longer found in the upstream source, provided they are not mapped to any channel. This prevents unbounded database bloat over time. Entries mapped to at least one channel are always preserved.
- Rewrote the M3U line parser as an `iter_m3u_entries` generator that owns the full per-entry state machine. Intermediate directive lines between `#EXTINF` and the stream URL are now handled correctly rather than corrupting the pending entry or being silently misassigned. A `#EXTINF` with no following URL is discarded with a warning instead of carrying over a `url`-less entry into batch processing. Attribute keys are normalised to lowercase during parsing (provider attribute names remain case-insensitive end-to-end). The `#EXTINF` attribute regex is pre-compiled at module load, and attribute lookups use O(1) `dict.get()` instead of linear scans — approximately 10% faster parsing on large M3U files.

View file

@ -1251,7 +1251,7 @@ const ChannelsTable = ({ onReady }) => {
</Button>
</Popover.Target>
<Popover.Dropdown>
<Group
<Stack
gap="sm"
style={{
minWidth: 250,
@ -1259,16 +1259,27 @@ const ChannelsTable = ({ onReady }) => {
width: 'max-content',
}}
>
<TextInput value={hdhrUrl} size="small" readOnly />
<ActionIcon
onClick={copyHDHRUrl}
<Text size="sm" c="dimmed">
Use this URL in HDHomeRun-compatible apps and IPTV
clients.
</Text>
<TextInput
value={hdhrUrl}
size="sm"
variant="transparent"
color="gray.5"
>
<Copy size="18" fontSize="small" />
</ActionIcon>
</Group>
readOnly
style={{ width: '100%' }}
rightSection={
<ActionIcon
onClick={copyHDHRUrl}
size="sm"
variant="transparent"
color="gray.5"
>
<Copy size="16" />
</ActionIcon>
}
/>
</Stack>
</Popover.Dropdown>
</Popover>
<Popover
@ -1303,9 +1314,13 @@ const ChannelsTable = ({ onReady }) => {
onClick={stopPropagation}
onMouseDown={stopPropagation}
>
<Text size="sm" c="dimmed">
Use this URL in your media player or IPTV app to load your
channel list.
</Text>
<TextInput
value={buildM3UUrl()}
size="xs"
size="sm"
readOnly
label="Generated URL"
rightSection={
@ -1319,35 +1334,34 @@ const ChannelsTable = ({ onReady }) => {
</ActionIcon>
}
/>
<Group justify="space-between">
<Text size="sm">Use cached logos</Text>
<Switch
size="sm"
checked={m3uParams.cachedlogos}
onChange={(event) =>
setM3uParams((prev) => ({
...prev,
cachedlogos: event.target.checked,
}))
}
/>
</Group>
<Group justify="space-between">
<Text size="sm">Direct stream URLs</Text>
<Switch
size="sm"
checked={m3uParams.direct}
onChange={(event) =>
setM3uParams((prev) => ({
...prev,
direct: event.target.checked,
}))
}
/>
</Group>{' '}
<Switch
label="Use cached logos"
description="Proxy channel logos through Dispatcharr"
size="sm"
checked={m3uParams.cachedlogos}
onChange={(event) =>
setM3uParams((prev) => ({
...prev,
cachedlogos: event.target.checked,
}))
}
/>
<Switch
label="Direct stream URLs"
description="Bypass the Dispatcharr proxy; client connects directly to the source"
size="sm"
checked={m3uParams.direct}
onChange={(event) =>
setM3uParams((prev) => ({
...prev,
direct: event.target.checked,
}))
}
/>
<Select
label="TVG-ID Source"
size="xs"
description="Value used as the tvg-id attribute in the M3U"
size="sm"
value={m3uParams.tvg_id_source}
onChange={(value) =>
setM3uParams((prev) => ({
@ -1398,9 +1412,15 @@ const ChannelsTable = ({ onReady }) => {
onClick={stopPropagation}
onMouseDown={stopPropagation}
>
<Text size="sm" c="dimmed">
Use this URL in your IPTV app for program guide data.
Per-user defaults for days forward/back can be set in
account settings, which apply automatically for XC
clients.
</Text>
<TextInput
value={buildEPGUrl()}
size="xs"
size="sm"
readOnly
label="Generated URL"
rightSection={
@ -1414,22 +1434,22 @@ const ChannelsTable = ({ onReady }) => {
</ActionIcon>
}
/>
<Group justify="space-between">
<Text size="sm">Use cached logos</Text>
<Switch
size="sm"
checked={epgParams.cachedlogos}
onChange={(event) =>
setEpgParams((prev) => ({
...prev,
cachedlogos: event.target.checked,
}))
}
/>
</Group>
<Switch
label="Use cached logos"
description="Proxy channel logos through Dispatcharr"
size="sm"
checked={epgParams.cachedlogos}
onChange={(event) =>
setEpgParams((prev) => ({
...prev,
cachedlogos: event.target.checked,
}))
}
/>
<Select
label="TVG-ID Source"
size="xs"
description="Value used to match EPG channels to M3U streams"
size="sm"
value={epgParams.tvg_id_source}
onChange={(value) =>
setEpgParams((prev) => ({
@ -1446,7 +1466,8 @@ const ChannelsTable = ({ onReady }) => {
/>
<NumberInput
label="Days forward (0 = all)"
size="xs"
description="Limit EPG to this many future days; 0 returns all available data"
size="sm"
min={0}
max={365}
value={epgParams.days}
@ -1459,7 +1480,8 @@ const ChannelsTable = ({ onReady }) => {
/>
<NumberInput
label="Days back (0 = none)"
size="xs"
description="Include this many past days of EPG data (max 30)"
size="sm"
min={0}
max={30}
value={epgParams.prev_days}