]> git.ipfire.org Git - thirdparty/tvheadend.git/log
thirdparty/tvheadend.git
48 min agointl: Translate intl/tvheadend.pot in pl master
transifex-integration[bot] [Mon, 3 Aug 2026 10:03:44 +0000 (10:03 +0000)] 
intl: Translate intl/tvheadend.pot in pl

100% translated source file: 'intl/tvheadend.pot'
on 'pl'.

48 min agointl: Translate intl/tvheadend.pot in en_US
transifex-integration[bot] [Mon, 3 Aug 2026 06:36:44 +0000 (06:36 +0000)] 
intl: Translate intl/tvheadend.pot in en_US

98% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'en_US'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

48 min agointl: Translate intl/tvheadend.pot in zh-Hans
transifex-integration[bot] [Mon, 3 Aug 2026 06:36:35 +0000 (06:36 +0000)] 
intl: Translate intl/tvheadend.pot in zh-Hans

99% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'zh-Hans'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

48 min agointl: Translate intl/tvheadend.pot in en_GB
transifex-integration[bot] [Mon, 3 Aug 2026 06:36:26 +0000 (06:36 +0000)] 
intl: Translate intl/tvheadend.pot in en_GB

98% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'en_GB'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

48 min agointl: Translate intl/tvheadend.pot in pl
transifex-integration[bot] [Mon, 3 Aug 2026 06:36:14 +0000 (06:36 +0000)] 
intl: Translate intl/tvheadend.pot in pl

99% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'pl'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

48 min agointl: Translate intl/tvheadend.pot in zh
transifex-integration[bot] [Mon, 3 Aug 2026 06:36:03 +0000 (06:36 +0000)] 
intl: Translate intl/tvheadend.pot in zh

99% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'zh'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

48 min agointl: Translate intl/tvheadend.pot in fr
transifex-integration[bot] [Mon, 3 Aug 2026 06:35:51 +0000 (06:35 +0000)] 
intl: Translate intl/tvheadend.pot in fr

94% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'fr'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 hours agoUpdate Internationalisation Templates
Flole998 [Mon, 3 Aug 2026 03:13:28 +0000 (03:13 +0000)] 
Update Internationalisation Templates

14 hours agompegts: per-network default for automatic EPG source mapping on new channels
Pim Zandbergen [Tue, 14 Jul 2026 21:08:08 +0000 (23:08 +0200)] 
mpegts: per-network default for automatic EPG source mapping on new channels

Channels created by the service mapper or bouquet mapping always start
with "Automatically map EPG source" enabled, hardcoded in
channel_create0(). Whether that is desirable depends on the network the
channel comes from: on DVB networks the OTA EIT grabber follows the
service mapping regardless, while the flag lets any enabled grabber
channel with a matching name attach itself. On IPTV networks name
matching is usually exactly what is wanted.

Add a per-network option "Auto-map EPG source on new channels"
(default enabled, so behaviour is unchanged) that supplies the initial
value of the per-channel option at channel creation time. The
per-channel option remains authoritative for the grabbers and changing
the network option does not modify existing channels. Channels merged
into an existing channel keep that channel's setting; the network of
the service that first created the channel determines the initial
value.

No UI changes are required or included; both the ExtJS and Vue
interfaces render the new option through the generic idnode forms.

Implements #2177.

14 hours agosupport: getmuxlist: fix configure failure during git branch reset
Christian Hewitt [Fri, 31 Jul 2026 14:08:40 +0000 (14:08 +0000)] 
support: getmuxlist: fix configure failure during git branch reset

Fixes "ERROR: Failed to fetch dvb-scan data (use --disable-dvbscan)"
in ./configure due to the hardcoded reset against the master branch
of the dtv-scan-tables repo which no longer exists.

Suggested-by: Pim Zandbergen <pim@zandbergen.org>
Signed-off-by: Christian Hewitt <christianshewitt@gmail.com>
14 hours agoci: build aarch64 debs natively on arm64 runners
Pim Zandbergen [Sat, 1 Aug 2026 10:50:44 +0000 (12:50 +0200)] 
ci: build aarch64 debs natively on arm64 runners

uraimo/run-on-arch-action cannot run on arm64 hosts: it pulls an
amd64-only QEMU helper image, so the job fails on a platform mismatch
before building anything (uraimo/run-on-arch-action#161). Instead of
emulating aarch64 on x86 runners, move those legs into the existing
build-deb-native job, which already builds in manually started distro
containers.

The job gains a runner matrix axis (the same pattern
build-el-rpm-native already uses); the aarch64 containers are include
entries carrying runner: ubuntu-24.04-arm, so they are added as their
own combinations and every build step stays shared with the x86 legs.
The armv6/armv7 QEMU legs are untouched.

The aarch64 legs keep the job names of the QEMU builds they replace
(a display-name element in the container matrix entries), so the
required status checks on master keep matching by name and branch
protection needs no changes.

3 days agowebui: honour dedup-skipped recordings in the new UI
Oliver Sluke [Thu, 30 Jul 2026 19:29:44 +0000 (21:29 +0200)] 
webui: honour dedup-skipped recordings in the new UI

The server's grid_upcoming endpoint includes dedup-skipped rerun
entries unless the client passes duplicates=0; the classic UI has
always passed it, the new UI did not. Skipped reruns therefore
appeared in the new Upcoming view as ordinary "Scheduled for
recording" rows although they will not record, and the two UIs
disagreed on the row set and count.

Pass duplicates=0 in the new Upcoming view, matching the classic
grid. And since a hidden rerun needs an explanation somewhere, the
EPG event drawer now shows the classic details dialog's "Will be
skipped because it is a rerun of: <date>" note: for a scheduled
entry it loads the DVR entry's read-only "duplicate" property (the
schedule status cannot carry this — a skipped rerun still reports
plain "scheduled") and renders the note when set. The note reuses
the classic message ids, so existing translations apply.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
5 days agoUpdate Internationalisation Templates
Flole998 [Mon, 27 Jul 2026 03:06:08 +0000 (03:06 +0000)] 
Update Internationalisation Templates

6 days agowizard: flag the setup wizard password fields as passwords
Oliver Sluke [Fri, 24 Jul 2026 11:14:15 +0000 (13:14 +0200)] 
wizard: flag the setup wizard password fields as passwords

The login step's admin_password and password props are plain PT_STR
without PO_PASSWORD, so the serialized metadata lacks the
`password: true` marker web UIs key off for input masking — both UIs
rendered the wizard's password fields as clear text. Other password
consumers (access.c, cclient.c) already carry the flag.

Add PO_PASSWORD to both prop declarations, and drop the Vue UI's
scoped client-side overlay that patched the marker onto exactly these
two fields — the server metadata now says it itself.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
7 days agowebui: filter the EPG Table by content type from its column header
Oliver Sluke [Mon, 20 Jul 2026 20:31:41 +0000 (22:31 +0200)] 
webui: filter the EPG Table by content type from its column header

The Vue EPG Table view could already filter by content type, but only
from the view-options popover. The "Content Type" column itself had no
filter affordance — it's not sortable and had no column filter, so its
header showed no funnel or kebab menu. Users coming from the classic UI
(which exposes a dedicated content-type filter) clicked the column,
nothing happened, and concluded the feature was missing (issue #2194).

Give the column a header funnel + kebab "Filter…" that opens the same
content-type multiselect the popover uses. Selections accumulate in the
open popover and commit to the shared viewOptions.genre state on Apply
(the column's dropdown panel is kept inside the filter overlay so
option clicks don't dismiss it). The option list is factored into a
useEpgGenreOptions composable so the column filter and the popover
share one source.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
7 days agowebui: show recording status in the EPG Table view
Oliver Sluke [Wed, 22 Jul 2026 07:53:13 +0000 (09:53 +0200)] 
webui: show recording status in the EPG Table view

The Timeline and Magazine views mark scheduled and in-progress
recordings with the DVR overlay bars, but the Table view gave no
visual cue — the only way to see whether an event records was to open
its drawer. The classic UI's grid shows a per-row dvrState icon.

Add a leading icon column to the Table view driven by the row's
dvrState (already joined into every epg/events/grid row): a red dot
while recording, a warning triangle for a recording with errors, a
clock for a scheduled recording, with tooltips naming the state. The
column is icon-only, toggleable, and empty for events without a DVR
entry.

Also fix the staleness that made the column (and the drawer's
Record/Stop buttons in the Table view) lag until a page reload: the
DVR-entries cache was primed only on day navigation, which the Table
view never performs — so the cache stayed unloaded, its dvrentry
Comet handler discarded every notification, and the grid refetch that
keeps dvrState fresh never fired. Prime the cache reactively on DVR
access instead.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
7 days agowebui: draw signal strength and SNR bars in the Status Stream view
Oliver Sluke [Thu, 23 Jul 2026 20:12:48 +0000 (22:12 +0200)] 
webui: draw signal strength and SNR bars in the Status Stream view

The Vue Status → Stream grid printed Signal Strength and SNR as bare
numbers. For tuners reporting the DVBv5 relative scale that's a raw
0..65535 integer with no context, where the classic UI draws a
coloured progress bar.

Render both columns through a SignalBarCell: relative-scale readings
get a coloured bar filled against the 65535 ceiling (classic
ProgressColumn thresholds — green above 2/3, amber above 1/3, red
below) with the raw value beside it and the percentage as the hover
tooltip; decibel-scale readings stay text-only ("-52.3 dBm" /
"12.3 dB") since no honest fill ceiling exists for dB values. The
phone card's Signal/SNR row picks up the same rendering.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
7 days agoprop: fix octal handling of permission values (PT_PERM)
Oliver Sluke [Fri, 24 Jul 2026 13:49:11 +0000 (15:49 +0200)] 
prop: fix octal handling of permission values (PT_PERM)

Permission fields (the DVR profile's file/directory permissions) are
written and read through two halves that disagree about octal
notation. Writing uses "%04o", which only keeps a leading zero while
the value fits in three octal digits; reading uses strtol with base
auto-detection, which treats a string without a leading zero as
DECIMAL. A value with a special bit set (setuid/setgid/sticky, e.g.
setgid 02775) is therefore written as "2775" and read back as decimal
2775 = octal 5327 -- the stored value silently changes on every config
load and on every save that echoes the serialized form, drifting
further each cycle (02775 -> 05327 -> 012317 -> ...). The corrupted
mode is applied to real recordings: files are created setuid and
world-writable, directories become unlistable for their owner and
untraversable for the group. Plain values like 0775 keep their
padding zero and round-trip fine, which is why the asymmetry -- as
old as PT_PERM itself (220833201) -- stayed unnoticed: the web UI's
field validation has only ever accepted the 0xxx form, so a trigger
value could only enter via the raw API or a hand-edited config.

Read permission strings as octal always (the fields are documented
"octal, e.g. 0775"; this also repairs values already stored without
the prefix) and write them with an explicit leading zero so base
auto-detecting parsers keep reading them correctly. Widen the web
UI's permission-field validation to accept the special-bit form
0xxxx -- required now that stored special-bit values render with
five characters, which the old pattern would reject as invalid.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
7 days agoapi: add access/whoami returning the session's access info, API v20 (#2201)
Oliver Sluke [Sun, 26 Jul 2026 17:49:14 +0000 (19:49 +0200)] 
api: add access/whoami returning the session's access info, API v20 (#2201)

The session's resolved permissions and UI preferences were only
available via the comet "accessUpdate" notification, which the server
emits at WebSocket-connect time — 100-500 ms after page load. The web
UI therefore stalled its route guards on the first message, painted
the default theme until it arrived, and had to force a full page
reload after saving any UI preference (theme, UI level, quicktips, …)
because a running session never receives a fresh copy.

Add api/access/whoami (API v20): the same payload as accessUpdate,
built by the shared comet_access_info_build() helper and available as
a plain synchronous request. The comet notification is unchanged (the
"address" field stays comet-only — the API layer has no connection
handle).

The new UI hydrates the access store from the endpoint before
mounting — correct theme, UI level and permissions on first paint —
and re-pulls it after saving an access-backed preference so theme and
level changes now apply live; only a UI-language change still reloads
(the locale catalog is loaded once at bootstrap). Comet remains the
live-update channel and pre-v20 responses are ignored gracefully.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
Co-authored-by: Flole <Flole998@users.noreply.github.com>
7 days agoidnode: strict gt/lt and new ge/le/ne filter comparators, API v20
Oliver Sluke [Wed, 20 May 2026 18:42:18 +0000 (20:42 +0200)] 
idnode: strict gt/lt and new ge/le/ne filter comparators, API v20

The idnode numeric filter offered only gt/lt/eq, and gt/lt matched
inclusively — gt behaved as >=, lt as <= — contradicting their names.
Tighten gt/lt to strict inequality, add the missing ge/le/ne
operators (integer and double filters), and bump TVH_API_VERSION
19 -> 20 so external clients can detect the corrected and expanded
operator set via api/serverinfo.

The Vue web UI adopts the new operators in the same change so server
and UI stay consistent:

- The numeric column filter offers the full truthful set — = != <
  <= > >= plus Between, mapping 1:1 onto the server comparators.
  Between now translates to ge:min + le:max, keeping the displayed
  range inclusive at both bounds. Filter summary chips and the
  funnel-icon hover render matching glyphs (the hover previously
  stringified the filter model object).
- Persisted per-grid filters are versioned; legacy blobs written
  while gt/lt were labelled >=/<= migrate to ge/le on load so a
  saved filter keeps the meaning it was created with.
- The EPG grid endpoint keeps its own eq/gt/lt vocabulary
  (api_epg.c filtcmptab, matched inclusively) and is deliberately
  untouched; the client-side senders that talk to it stay on gt/lt
  and the two comparator vocabularies are documented at the type
  and call sites.

The legacy ExtJS UI needs no change: its numeric filter menu has
always displayed strict > / < icons while the server matched
inclusively — the strict semantics make the displayed operators
truthful.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
2 weeks agoXMLTV EPG Grabber - Channel Matching Remediation
DeltaMikeCharlie [Sat, 18 Jul 2026 05:59:57 +0000 (15:59 +1000)] 
XMLTV EPG Grabber - Channel Matching Remediation

2 weeks agowebui: use the shared GRID_LIMIT_ALL in the video player channel fetch
Oliver Sluke [Sat, 18 Jul 2026 11:14:20 +0000 (13:14 +0200)] 
webui: use the shared GRID_LIMIT_ALL in the video player channel fetch

The channel-picker fetch in VideoPlayerDialog still carried an inline
999_999_999 "fetch all" literal: it wasn't on master when GRID_LIMIT_ALL
was introduced. Point it at the shared constant so every grid/list fetch
uses the single source.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
2 weeks agowebui: show ATSC fractional channel numbers
Oliver Sluke [Sun, 19 Jul 2026 06:27:54 +0000 (08:27 +0200)] 
webui: show ATSC fractional channel numbers

The server returns a channel's number as an integer for most channels
but as a string for ATSC fractional LCNs (e.g. "10.1"). The renderers
only handled the integer case, so fractional numbers were dropped from
the EPG Timeline and Magazine channel columns (issue #2190); the Live
TV channel-picker label had the same gap. Widen the channel-number type
and render the string form wherever a channel number is shown.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
2 weeks agowebui: centralise the grid "fetch all" limit in a shared constant
Oliver Sluke [Thu, 16 Jul 2026 18:45:57 +0000 (20:45 +0200)] 
webui: centralise the grid "fetch all" limit in a shared constant

The 999_999_999 "return every row" limit — the server's page_size_ui
"All" sentinel (access.c:1518) — was repeated inline across the EPG,
DVR and idnode grid fetches. Introduce GRID_LIMIT_ALL in a shared
module and use it at every call site, so the sentinel is defined and
documented once.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
2 weeks agowebui: play any channel from the EPG, including ones with no programme data
Oliver Sluke [Thu, 16 Jul 2026 18:12:32 +0000 (20:12 +0200)] 
webui: play any channel from the EPG, including ones with no programme data

The Vue UI could only start in-browser playback from an EPG event, so
channels carrying no EPG had no way to be watched — a gap versus the
classic UI's "Watch TV" toolbar button (issue #2183).

Add a "Live TV" button to the Timeline, Magazine and Table EPG
toolbars. It opens the in-browser player with no channel preselected;
the player dialog now owns channel selection, with a Channel dropdown
(enabled channels, ordered by number, searchable) beside the
stream-profile selector. Choosing or switching a channel re-points the
stream the same way a profile switch does, so every channel — including
one with no programme data — is reachable. Until a channel is picked
the dialog shows a "Select a channel to watch." prompt.

The Timeline and Magazine channel cells also gain a click-to-play
affordance that funnels through the same player: in-browser when a
stream profile is available, the external player otherwise.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
2 weeks agoAdd Content-Disposition for playlist downloads
Ömer Görür [Sun, 21 Jun 2026 17:54:27 +0000 (20:54 +0300)] 
Add Content-Disposition for playlist downloads

Add Content-Disposition for playlist downloads

Add support for sending a Content-Disposition header so playlists are
downloaded instead of rendered inline by the browser.

Change http_send_reply signature to accept a disposition string and update
callers to pass NULL where not used. Add http_output_content_disposition
wrapper and its prototype.

Implement http_playlist_disposition to build an attachment filename for
playlist downloads, providing an RFC 6266 ASCII fallback (filename=) together
with an RFC 8187 percent-encoded UTF-8 name (filename*=) so non-ASCII channel
and recording names are transmitted safely. Update playlist handlers
(page_http_playlist, page_xspf, page_m3u) to use the new disposition behavior

2 weeks agoUpdate Internationalisation Templates
Flole998 [Mon, 13 Jul 2026 03:05:57 +0000 (03:05 +0000)] 
Update Internationalisation Templates

2 weeks agowebui: per-route cache-control for the Vue UI's hashed assets
Pim Zandbergen [Thu, 9 Jul 2026 15:44:58 +0000 (17:44 +0200)] 
webui: per-route cache-control for the Vue UI's hashed assets

The /gui/static route was registered on page_static_file, whose
10-second default Cache-Control max-age exists for the unhashed
ExtJS assets that handler also serves. The Vue dist is different:
Vite fingerprints everything under dist/assets/ with a content
hash (index-B3kR9xQz.js), so an asset's URL changes whenever its
content does and the response is safe to cache long — the same
reasoning that already grants jpg/png an hour inside
page_static_file.

Split the body of page_static_file into page_static_file_maxage(),
which takes the default max-age as an argument; page_static_file
remains a thin wrapper passing the old default of 10, so no
existing caller or http_path_add registration changes behavior.
The jpg/png rule now raises the max-age instead of overwriting it,
so it can no longer shorten a lifetime the caller asked for (a
content-hashed .png keeps its year).

/gui/static now registers a Vue-specific callback, page_vue_asset,
which serves files whose basename matches the Vite fingerprint
pattern (a dash followed by 8+ hash characters before the final
extension) with max-age=31536000, and everything unhashed —
notably dist/index.html, reachable at /gui/static/index.html —
with the short default. The /gui route itself (the SPA shell)
stays on the short-cache path so deployments propagate on reload.
The pattern check lives in vue.c rather than in page_static_file_maxage
because the fingerprint convention belongs to the Vue build
pipeline, not to static file serving in general.

2 weeks agowebui: static: update logo{mid|big}.png images
Christian Hewitt [Thu, 9 Jul 2026 11:08:11 +0000 (11:08 +0000)] 
webui: static: update logo{mid|big}.png images

Signed-off-by: Christian Hewitt <christianshewitt@gmail.com>
2 weeks agowebui: NavRail: simplify to Expand/Collapse
Christian Hewitt [Thu, 9 Jul 2026 08:35:51 +0000 (08:35 +0000)] 
webui: NavRail: simplify to Expand/Collapse

Signed-off-by: Christian Hewitt <christianshewitt@gmail.com>
2 weeks agowebui: AboutView: tweaks and cleanups
Christian Hewitt [Thu, 9 Jul 2026 08:05:26 +0000 (08:05 +0000)] 
webui: AboutView: tweaks and cleanups

Signed-off-by: Christian Hewitt <christianshewitt@gmail.com>
2 weeks agowebui: sort channel numbers numerically in the EPG grids
Oliver Sluke [Tue, 14 Jul 2026 11:18:36 +0000 (13:18 +0200)] 
webui: sort channel numbers numerically in the EPG grids

Channel numbers arrive from the API as a 'major.minor' string
(api_epg_add_channel in src/api/api_epg.c), so the Vue grids' generic
client-side comparators ordered them as text - 1, 10, 101, 11, 2, 20 -
instead of 1, 2, 3. Reported for the EPG Table view's '#' column
(#2172); the same lexical fallback also affects DataGrid's grouped
within-cluster secondary sort, so both are fixed here.

Add a shared channelNumberCompare (utils/channelNumberSort) that
splits the value into major and minor integer parts and compares
major first, then minor. The minor is a sub-index up to
CHANNEL_SPLIT-1 (channels.h), not a fraction, so a plain decimal
parse would be wrong too: as decimals 10.9 > 10.10, but as channel
numbers 10.9 must precede 10.10.

Expose it generically through a new optional ColumnDef.sortComparator
(direction-agnostic) so no grid needs to know about channel numbers:
  - the EPG Table view's '#' column sets it, and applyInMemorySort
    (extracted to epgTableSort.ts) uses the active column's
    comparator;
  - DataGrid's grouped secondary sort uses the sorted column's
    comparator, falling back to the generic value comparison.

Unit tests cover the comparator (lexical and 10.9/10.10 decimal
traps), applyInMemorySort deferring to the column comparator, and
DataGrid's within-cluster secondary sort honouring it.

Fixes #2172

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
2 weeks agowebui: label the home health line 'active subscriptions'
Oliver Sluke [Tue, 14 Jul 2026 19:31:19 +0000 (21:31 +0200)] 
webui: label the home health line 'active subscriptions'

The home health line summed each tuner's subscription count and
linked to Status > Subscriptions, but labelled it 'N active
streams'. The number and the link target are both subscriptions,
so the label read as a mismatch. Rename it to 'N active
subscription(s)' (and the backing identifiers) so the label matches
what it counts and where it lands.

Refs #2175

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
2 weeks agoDisable Pcloud downloads in GH actions to rely on native cache
Flole [Tue, 14 Jul 2026 12:59:11 +0000 (14:59 +0200)] 
Disable Pcloud downloads in GH actions to rely on native cache

2 weeks agoFix staticlib cache keys referencing wrong matrix variables in build-cloudsmith.yml
Flole [Tue, 14 Jul 2026 12:54:28 +0000 (14:54 +0200)] 
Fix staticlib cache keys referencing wrong matrix variables in build-cloudsmith.yml

3 weeks agowebui: honour webroot (--http_root) in the Vue UI
Oliver Sluke [Thu, 9 Jul 2026 19:22:20 +0000 (21:22 +0200)] 
webui: honour webroot (--http_root) in the Vue UI

With a webroot configured the Vue UI blanked and looped, growing one
<webroot>/gui segment per redirect cycle: the bundle baked absolute
asset URLs and a /gui/ router base, and every server URL (api, comet,
play, images, login, ...) was hardcoded root-absolute — so behind a
webroot each of them missed, and the server's page_no_webroot catch-all
turned the misses into 302 cycles instead of clean failures. The server
side has always been ready: http_path_add registers every route under
tvheadend_webroot and http_redirect prefixes absolute targets.

Make the mount point a runtime discovery with one anchor:

- index.html carries <base href="/gui/static/">; vue.c rewrites the
  href to {webroot}/gui/static/ at serve time (the no-webroot path
  still streams the file untouched).
- The production Vite base becomes relative ('./'): index.html assets
  resolve through the base tag, lazy chunks via import.meta.url, so
  the bundle works at any mount point.
- New utils/base.ts derives the Vue Router base and the server root
  from document.baseURI, falling back to the root-mounted defaults
  when no matching <base> is present (dev server, tests).
- Every server URL construction goes through serverUrl(): the api
  client and comet poll choke points plus play/stream/dvrfile links,
  channel icons, static images, locale + marked loaders, markdown
  help fetches, login/logout, the Classic UI link, and the wizard
  finish redirect. The skip-link jumps via JS since a bare #fragment
  href would resolve against the base tag.

Also complete the webroot normalisation in main.c: "/" and "" left
tvheadend_webroot pointing at the raw value instead of NULL, which
prefixed every registered route with "/" and broke the whole HTTP
surface — both now normalise to no webroot.

A source-scan test fences regressions (new root-absolute server URLs
outside utils/base.ts fail the suite), unit tests cover the base
derivation, and a contract test pins the Vite base + index.html tag
the vue.c rewrite depends on.

Fixes #2167

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
3 weeks agowebui: keep the EPG viewport following "now" over long sessions
Oliver Sluke [Thu, 9 Jul 2026 08:40:05 +0000 (10:40 +0200)] 
webui: keep the EPG viewport following "now" over long sessions

The EPG scrolled to "now" only once, at mount. Left open for a few
hours the red now-cursor drifted across a frozen viewport while the
server expired already-aired programmes, leaving a growing empty gap
on the left with current/upcoming shows bunched near the drifted
cursor. Both Timeline and Magazine were affected (shared view state).

Add an opt-in "follow now": while the user is parked at the live edge,
a wall-clock tick re-pins the viewport so "now" stays at the leading
edge (left for Timeline, top for Magazine). Following starts on the
initial scroll-to-now, stops the moment the user scrolls away to
browse ahead, and re-engages via the existing Now button.

It lives in useEpgScrollDaySync, which already owns the scroll
listener, the Now button, and the intent-scroll suppression latch:
the re-pin routes through that latch so its own scroll emit isn't read
as a user scroll, and any unlatched scroll disables following. The
re-pin is driven by the visibility-aware nowEpoch tick and drift-gated
against the half-hour-snapped target, so it is a no-op between :30
boundaries and fires only when the boundary advances or the tab
regains focus after being hidden. trackStart stays frozen — only the
scroll offset moves.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
3 weeks agowebui: activate PrimeVue dark palette under the Access theme
Oliver Sluke [Thu, 9 Jul 2026 06:32:21 +0000 (08:32 +0200)] 
webui: activate PrimeVue dark palette under the Access theme

PrimeVue's darkModeSelector was set to [data-theme="dark"], a theme
that was never shipped — the app's only dark theme is Access
([data-theme="access"]). So Aura's dark palette never activated, and
teleported overlays (Select dropdown, MultiSelect, Datepicker, menus,
the paginator rows-per-page popup) kept Aura's light surface — a white
panel on the dark Access page. Our own components themed correctly
because they use the --tvh-* tokens; the overlays use Aura's own
component tokens (e.g. select.overlay.background) that primevue.css
doesn't map, so they depended entirely on Aura's light/dark mode.

Point darkModeSelector at [data-theme="access"] so Aura switches to
its dark palette exactly when the Access theme is active; Blue and Gray
stay light. Refresh the now-stale comments in main.ts and the EPG event
drawer's surface override, which still described the old dark selector.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
3 weeks agobuild: run the Vue UI pcloud steps only when PCLOUD_TOKEN is available
Pim Zandbergen [Wed, 8 Jul 2026 22:12:55 +0000 (00:12 +0200)] 
build: run the Vue UI pcloud steps only when PCLOUD_TOKEN is available

The 'Check pcloud' and 'Upload to pcloud' steps were gated only on
github.ref == 'refs/heads/master', which is also true on forks, where
the PCLOUD_TOKEN secret is absent and pcloud.py exits with an error,
failing the whole job.

Additionally, the check step never received the token at all, so
listfolder always failed silently (stderr discarded) and reported
'Missing from pcloud', causing a redundant re-upload on every
upstream master build.

Gate both steps on the secret being present and inject the token
into the check step as well.

3 weeks agobuild: cache staticlib locally in GH actions and intelligently upload to pcloud
Flole [Mon, 6 Jul 2026 22:47:53 +0000 (00:47 +0200)] 
build: cache staticlib locally in GH actions and intelligently upload to pcloud

3 weeks agobuild: improve GitHub Actions Vue UI caching and RPM builds
Flole [Tue, 7 Jul 2026 08:04:32 +0000 (10:04 +0200)] 
build: improve GitHub Actions Vue UI caching and RPM builds

3 weeks agobuild: integrate Vue UI build + pcloud-cache delivery
Oliver Sluke [Wed, 24 Jun 2026 18:05:07 +0000 (20:05 +0200)] 
build: integrate Vue UI build + pcloud-cache delivery

Build-system glue for the Vue admin UI shipped in the preceding
commit. Two delivery paths sit side-by-side:

- `--enable-vue_build` (default with node + npm on PATH) runs the
  local Vite/npm build during `make`. Configure auto-detects.

- `--enable-vue_cache` downloads a pre-built dist tarball from
  pcloud instead. Off by default; downstream packagers without a
  Node toolchain set `--disable-vue_build --enable-vue_cache` to
  ship the UI without building it. Requires `--enable-pcloud_cache`
  (default yes).

Either flag (or both) causes the dist to be bundled into the
binary.

Files
- `configure` adds the `vue_cache` option entry and a sanity
  check that errors if `vue_cache` is enabled without
  `pcloud_cache`.
- `Makefile` gains a `vue_cache_get` target that delegates to
  Makefile.webui-vue's `libcacheget`, plus a `BUNDLES-$(CONFIG_VUE_
  CACHE) += ...` clause so the dist is bundled whether it came
  from a local build or a cache fetch.
- `Makefile.webui-vue` mirrors the shape of Makefile.ffmpeg with
  `libcacheget` / `libcacheput` / `build` / `source-hash` targets.
  The tarball is named `webui-vue-<source-hash>.tgz` where the
  hash is sha256 over the inputs that actually affect the dist
  output (Vue src/ tree, package.json, package-lock.json,
  vite.config.ts, tsconfig.json, index.html). Tests and lint /
  format configs don't contribute.
- `TVHEADEND_FILE_CACHE` is honoured by `libcacheget` for
  reproducer / air-gapped builds, matching the same env-var
  support/lib.sh:DOWNLOAD already implements.
- New `.github/workflows/webui-vue-cache.yml` builds the UI on a
  Node-equipped runner and uploads the dist to pcloud. Triggers
  on push to master / webui/v2-vue (paths-filtered to the source
  tree) and on workflow_dispatch. A skip-if-exists check via
  `pcloud.py listfolder` avoids rebuilding when the source hash
  already has an artifact. The upload step is internally guarded
  by PCLOUD_USER/PASS being non-empty, so the workflow is safe
  to land before the org-level secrets are populated.
- `.github/workflows/build-ci.yml` and
  `.github/workflows/build-cloudsmith.yml` are untouched in this
  commit. Flipping them to `--enable-vue_cache` to consume the
  cached dist is a follow-up once the secrets are confirmed
  working — premature flipping would break their build before
  the artifacts exist.

`libcacheget` is fatal on miss (unlike Makefile.static which
falls through to local build). When the user explicitly picks
`--enable-vue_cache`, silently producing a binary without the UI
bundled would be worse than failing the build outright.

Verified locally with three configurations:
- `--enable-vue_build` (default): unchanged, npm-builds locally.
- `--disable-vue_build --enable-vue_cache` plus
  `TVHEADEND_FILE_CACHE` pointing at a pre-packed tarball: dist
  populates from the tarball, binary builds + bundles cleanly.
- `--enable-vue_cache` without `--enable-pcloud_cache`: configure
  errors with an actionable message.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
3 weeks agowebui: new Vue 3 admin UI at /gui
Oliver Sluke [Wed, 24 Jun 2026 18:05:07 +0000 (20:05 +0200)] 
webui: new Vue 3 admin UI at /gui

A second admin UI served at /gui alongside the existing ExtJS UI
at /extjs.html. Built in Vue 3 + Pinia + PrimeVue 4 with the same
access contract (HTTP digest, same idnode permissions, same comet
notifications). The desktop root (/) now redirects to /gui by
default; the ExtJS UI keeps working unchanged and stays reachable
from the new UI's Classic UI menu entry until it is eventually
decommissioned. The simple.html redirect for legacy/WAP clients is
unchanged.

Scope of this commit covers the application layer only:

- The full Vue source tree under src/webui/static-vue/. Pinia
  stores, composables, view components, the EPG (Timeline /
  Magazine / Table) renderer, DVR views (Upcoming / Finished /
  AutoRec / TimeRec), Configuration tabs, the Setup Wizard port,
  Status views, the Help dialog, the i18n bridge over the
  existing /locale.js endpoint, error / toast plumbing, and the
  unit-test suite (Vitest, 2500+ tests).

- C-side plumbing in src/webui/vue.c (route registration for the
  /gui tree, mount via webui_init() in src/webui/webui.c). A
  minimal SPA-style fallback page is served when the dist tree
  isn't present, so the route stays functional even without the
  built UI.

- The build artifacts in src/webui/static-vue/package.json,
  package-lock.json, vite.config.ts, tsconfig.json, index.html,
  ESLint / Stylelint / Prettier configs, and the SPDX header
  enforcement script — everything the npm-side toolchain needs
  to produce the dist.

The companion build-system glue (configure flag, Makefile
recipes, Makefile.webui-vue, GitHub Actions workflow) lives in a
follow-on build: commit so the application and the infrastructure
review and bisect cleanly as separate units.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
5 weeks agointl: Translate intl/tvheadend.pot in zh-Hans
transifex-integration[bot] [Tue, 23 Jun 2026 03:23:50 +0000 (03:23 +0000)] 
intl: Translate intl/tvheadend.pot in zh-Hans

100% translated source file: 'intl/tvheadend.pot'
on 'zh-Hans'.

5 weeks agointl: Translate tvheadend.js.pot in zh-Hans
transifex-integration[bot] [Tue, 23 Jun 2026 03:23:26 +0000 (03:23 +0000)] 
intl: Translate tvheadend.js.pot in zh-Hans

100% translated source file: 'tvheadend.js.pot'
on 'zh-Hans'.

5 weeks agointl: Translate tvheadend.doc.pot in zh-Hans
transifex-integration[bot] [Tue, 23 Jun 2026 03:23:13 +0000 (03:23 +0000)] 
intl: Translate tvheadend.doc.pot in zh-Hans

100% translated source file: 'tvheadend.doc.pot'
on 'zh-Hans'.

5 weeks agoUpdate Internationalisation Templates
Flole998 [Mon, 22 Jun 2026 03:11:03 +0000 (03:11 +0000)] 
Update Internationalisation Templates

5 weeks agointl: Translate intl/js/tvheadend.js.pot in zh
transifex-integration[bot] [Mon, 22 Jun 2026 17:07:31 +0000 (17:07 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in zh

100% translated source file: 'intl/js/tvheadend.js.pot'
on 'zh'.

5 weeks agointl: Translate intl/tvheadend.pot in zh
transifex-integration[bot] [Mon, 22 Jun 2026 17:06:39 +0000 (17:06 +0000)] 
intl: Translate intl/tvheadend.pot in zh

100% translated source file: 'intl/tvheadend.pot'
on 'zh'.

5 weeks agointl: Translate tvheadend.doc.pot in zh-Hans
transifex-integration[bot] [Mon, 22 Jun 2026 17:06:10 +0000 (17:06 +0000)] 
intl: Translate tvheadend.doc.pot in zh-Hans

100% translated source file: 'tvheadend.doc.pot'
on 'zh-Hans'.

5 weeks agointl: Translate tvheadend.js.pot in zh-Hans
transifex-integration[bot] [Mon, 22 Jun 2026 17:04:36 +0000 (17:04 +0000)] 
intl: Translate tvheadend.js.pot in zh-Hans

100% translated source file: 'tvheadend.js.pot'
on 'zh-Hans'.

5 weeks agointl: Translate intl/tvheadend.pot in zh-Hans
transifex-integration[bot] [Mon, 22 Jun 2026 17:02:58 +0000 (17:02 +0000)] 
intl: Translate intl/tvheadend.pot in zh-Hans

100% translated source file: 'intl/tvheadend.pot'
on 'zh-Hans'.

6 weeks agointl: Translate intl/js/tvheadend.js.pot in pl
transifex-integration[bot] [Thu, 18 Jun 2026 20:32:51 +0000 (20:32 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in pl

100% translated source file: 'intl/js/tvheadend.js.pot'
on 'pl'.

6 weeks agointl: Translate intl/js/tvheadend.js.pot in pl
transifex-integration[bot] [Thu, 18 Jun 2026 18:31:27 +0000 (18:31 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in pl

99% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'pl'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/js/tvheadend.js.pot in pt
transifex-integration[bot] [Thu, 18 Jun 2026 18:31:15 +0000 (18:31 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in pt

82% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'pt'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/js/tvheadend.js.pot in fr
transifex-integration[bot] [Thu, 18 Jun 2026 18:31:01 +0000 (18:31 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in fr

96% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'fr'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/js/tvheadend.js.pot in ko
transifex-integration[bot] [Thu, 18 Jun 2026 18:30:47 +0000 (18:30 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in ko

80% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'ko'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/js/tvheadend.js.pot in et
transifex-integration[bot] [Thu, 18 Jun 2026 18:30:34 +0000 (18:30 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in et

83% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'et'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/js/tvheadend.js.pot in es
transifex-integration[bot] [Thu, 18 Jun 2026 18:30:20 +0000 (18:30 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in es

82% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'es'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/js/tvheadend.js.pot in en_US
transifex-integration[bot] [Thu, 18 Jun 2026 18:30:07 +0000 (18:30 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in en_US

99% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'en_US'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/js/tvheadend.js.pot in en_GB
transifex-integration[bot] [Thu, 18 Jun 2026 18:29:54 +0000 (18:29 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in en_GB

99% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'en_GB'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/js/tvheadend.js.pot in de
transifex-integration[bot] [Thu, 18 Jun 2026 18:29:43 +0000 (18:29 +0000)] 
intl: Translate intl/js/tvheadend.js.pot in de

82% of minimum 80% translated source file: 'intl/js/tvheadend.js.pot'
on 'de'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agobuild: clean up doozer build system references
Flole [Fri, 19 Jun 2026 19:50:35 +0000 (21:50 +0200)] 
build: clean up doozer build system references

6 weeks agorpm: generate local tarball using git archive to fix build errors
Flole [Thu, 18 Jun 2026 13:24:32 +0000 (15:24 +0200)] 
rpm: generate local tarball using git archive to fix build errors

6 weeks agoRename 'Parent disabled' to 'Inactive services' (fixes #1694)
Flole [Thu, 18 Jun 2026 13:23:14 +0000 (15:23 +0200)] 
Rename 'Parent disabled' to 'Inactive services' (fixes #1694)

6 weeks agointl: Translate intl/tvheadend.pot in pl
transifex-integration[bot] [Mon, 15 Jun 2026 15:21:49 +0000 (15:21 +0000)] 
intl: Translate intl/tvheadend.pot in pl

100% translated source file: 'intl/tvheadend.pot'
on 'pl'.

6 weeks agoratinglabels: document why enabled_notify is intentionally empty
Pim Zandbergen [Sun, 14 Jun 2026 13:42:55 +0000 (15:42 +0200)] 
ratinglabels: document why enabled_notify is intentionally empty

The notify callback for the enabled property is a registered no-op.
Add a comment explaining the empty body rather than removing the
function, addressing review feedback.

6 weeks agoFix GCC 16 -Werror build failures (satip, ratinglabels)
Pim Zandbergen [Sat, 13 Jun 2026 22:50:50 +0000 (00:50 +0200)] 
Fix GCC 16 -Werror build failures (satip, ratinglabels)

GCC 16's stricter checks turn two issues into -Werror build failures:

* satip_rtsp.c: the fixed 16-byte _w buffer cannot hold "&tvhweight=%d"
  for the full range of int weight (worst case 23 bytes incl. NUL),
  flagged as -Werror=format-truncation. Enlarge _w to 24 bytes. While
  here, size the buf VLA from the floor-clamped max_pids_len (so a small
  or negative caller value cannot under-size or invalidate it) and enlarge
  buf (max_pids_len + 32 -> + 64) so the combined "delpids=%s&addpids=%s%s"
  formatting (del+add up to max_pids_len, plus the larger _w) cannot
  silently truncate.

* ratinglabels.c: remove the dead body of ratinglabel_class_enabled_notify
  (a set-but-unused local plus an unused obj), which GCC 16 rejects.

Note: the buf resize and floor-clamped sizing reach slightly beyond the
original -Werror fix. They were added while iterating on this PR's Gemini
Code Assist review, which flagged those adjacent latent truncation /
VLA-sizing issues in the lines being touched.

6 weeks agobuild: never reuse a stale .config.mk from a failed configure
Pim Zandbergen [Sun, 14 Jun 2026 14:28:19 +0000 (16:28 +0200)] 
build: never reuse a stale .config.mk from a failed configure

write_config() is the sole writer of .config.mk and only runs after every
dependency check has passed. If configure instead dies on a missing
dependency (any of the ~40 `check_* || die` gates) - or is interrupted by a
signal - it returned without touching .config.mk, leaving the previous run's
file in place. The Makefile unconditionally `include`s that file, so make
would silently build against a stale configuration. In practice this could
link the wrong ffmpeg: e.g. a re-configure that died because libtheora-dev
was missing left behind a .config.mk from an earlier system-libav run,
pulling in the distro's ffmpeg instead of the bundled static one.

Establish the invariant "the .config.mk exists only if a configure run
completed":

- Remove any .config.mk up-front, right after argument parsing and before
  the checks, so that whatever ends the run early (die or signal) leaves no
  stale file and make fails loudly instead of using the wrong config. Placed
  after parsing so --help, which exits earlier, never deletes a valid config.
- Write .config.mk to a temp file in write_config() and rename it into place
  atomically at the very end, so it is never observed half-written.

6 weeks agointl: Translate intl/tvheadend.pot in fr
transifex-integration[bot] [Mon, 15 Jun 2026 12:08:20 +0000 (12:08 +0000)] 
intl: Translate intl/tvheadend.pot in fr

94% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'fr'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/tvheadend.pot in en_US
transifex-integration[bot] [Mon, 15 Jun 2026 12:08:07 +0000 (12:08 +0000)] 
intl: Translate intl/tvheadend.pot in en_US

98% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'en_US'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/tvheadend.pot in pl
transifex-integration[bot] [Mon, 15 Jun 2026 12:07:55 +0000 (12:07 +0000)] 
intl: Translate intl/tvheadend.pot in pl

99% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'pl'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

6 weeks agointl: Translate intl/tvheadend.pot in en_GB
transifex-integration[bot] [Mon, 15 Jun 2026 12:07:41 +0000 (12:07 +0000)] 
intl: Translate intl/tvheadend.pot in en_GB

98% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'en_GB'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agoUpdate Internationalisation Templates
Flole998 [Mon, 15 Jun 2026 03:11:24 +0000 (03:11 +0000)] 
Update Internationalisation Templates

7 weeks agoaccess: store the superuser password obfuscated like normal accounts
Sam Nazarko [Sat, 13 Jun 2026 07:11:50 +0000 (08:11 +0100)] 
access: store the superuser password obfuscated like normal accounts

The debian postinst wrote the superuser password to the superuser file
in cleartext, whereas normal user accounts are stored using the reversible
"TVHeadend-Hide-" base64 obfuscation. Write the superuser password the
same way (a new "password2" field) and teach access_init() to decode it,
falling back to the plaintext "password" field so existing superuser
files keep working.

If base64/tr are not available on the host (e.g. a stripped-down busybox
system), the postinst falls back to writing the cleartext "password"
field, so the superuser file is always valid regardless of userland.

This is obfuscation for consistency, not encryption: HTTP Digest auth
requires a recoverable password, so a one-way hash is not possible here.
The superuser file is still created mode 0600.

7 weeks agodebian: fix superuser file location and config directory ownership
Sam Nazarko [Sat, 13 Jun 2026 07:07:43 +0000 (08:07 +0100)] 
debian: fix superuser file location and config directory ownership

The postinst wrote the superuser account into $HOME (/var/lib/tvheadend),
but Tvheadend resolves its configuration directory via config_get_dir(),
which only uses /var/lib/tvheadend when that directory is owned by the
service user. On a stock package install /var/lib/tvheadend is owned by
root (adduser does not chown a pre-existing home directory), so the daemon
falls back to ~hts/.config/hts and never reads a superuser file placed in
the parent - the initial login then fails with "403 Forbidden".

- Take ownership of /var/lib/tvheadend so config_get_dir() uses it as the
  canonical configuration directory. This is skipped when an earlier
  install already has configuration under a fallback location, so existing
  settings are never stranded.
- Resolve the configuration directory in the postinst the same way
  config_get_dir() does (using "stat -L" to match the daemon's symlink
  dereferencing) and write the superuser file there.
- Document the ownership requirement of the /var/lib/tvheadend shortcut in
  config_get_dir().

7 weeks agoci: remove PCLOUD_TOKEN from build-ci.yml
Flole [Sat, 13 Jun 2026 21:08:30 +0000 (23:08 +0200)] 
ci: remove PCLOUD_TOKEN from build-ci.yml

7 weeks agoFix pcloud caching
Flole [Fri, 12 Jun 2026 13:39:39 +0000 (13:39 +0000)] 
Fix pcloud caching

7 weeks agointl: Translate intl/docs/tvheadend.doc.pot in fr
transifex-integration[bot] [Mon, 8 Jun 2026 16:06:09 +0000 (16:06 +0000)] 
intl: Translate intl/docs/tvheadend.doc.pot in fr

94% of minimum 80% translated source file: 'intl/docs/tvheadend.doc.pot'
on 'fr'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agointl: Translate intl/docs/tvheadend.doc.pot in pl
transifex-integration[bot] [Mon, 8 Jun 2026 15:53:34 +0000 (15:53 +0000)] 
intl: Translate intl/docs/tvheadend.doc.pot in pl

100% translated source file: 'intl/docs/tvheadend.doc.pot'
on 'pl'.

7 weeks agointl: Translate intl/tvheadend.pot in pl
transifex-integration[bot] [Mon, 8 Jun 2026 15:53:18 +0000 (15:53 +0000)] 
intl: Translate intl/tvheadend.pot in pl

100% translated source file: 'intl/tvheadend.pot'
on 'pl'.

7 weeks agointl: Translate intl/tvheadend.pot in en_GB
transifex-integration[bot] [Mon, 8 Jun 2026 13:44:37 +0000 (13:44 +0000)] 
intl: Translate intl/tvheadend.pot in en_GB

98% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'en_GB'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agointl: Translate intl/tvheadend.pot in en_US
transifex-integration[bot] [Mon, 8 Jun 2026 13:44:23 +0000 (13:44 +0000)] 
intl: Translate intl/tvheadend.pot in en_US

98% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'en_US'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agointl: Translate intl/tvheadend.pot in pl
transifex-integration[bot] [Mon, 8 Jun 2026 13:44:11 +0000 (13:44 +0000)] 
intl: Translate intl/tvheadend.pot in pl

99% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'pl'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agointl: Translate tvheadend.doc.pot in en_US
transifex-integration[bot] [Mon, 8 Jun 2026 13:43:54 +0000 (13:43 +0000)] 
intl: Translate tvheadend.doc.pot in en_US

98% of minimum 80% translated source file: 'tvheadend.doc.pot'
on 'en_US'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agointl: Translate intl/docs/tvheadend.doc.pot in pl
transifex-integration[bot] [Mon, 8 Jun 2026 13:43:43 +0000 (13:43 +0000)] 
intl: Translate intl/docs/tvheadend.doc.pot in pl

98% of minimum 80% translated source file: 'intl/docs/tvheadend.doc.pot'
on 'pl'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agointl: Translate intl/docs/tvheadend.doc.pot in fr
transifex-integration[bot] [Mon, 8 Jun 2026 13:43:29 +0000 (13:43 +0000)] 
intl: Translate intl/docs/tvheadend.doc.pot in fr

94% of minimum 80% translated source file: 'intl/docs/tvheadend.doc.pot'
on 'fr'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agointl: Translate tvheadend.doc.pot in en_GB
transifex-integration[bot] [Mon, 8 Jun 2026 13:43:15 +0000 (13:43 +0000)] 
intl: Translate tvheadend.doc.pot in en_GB

98% of minimum 80% translated source file: 'tvheadend.doc.pot'
on 'en_GB'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agointl: Translate intl/tvheadend.pot in fr
transifex-integration[bot] [Mon, 8 Jun 2026 13:43:02 +0000 (13:43 +0000)] 
intl: Translate intl/tvheadend.pot in fr

94% of minimum 80% translated source file: 'intl/tvheadend.pot'
on 'fr'.

Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format

7 weeks agosatip: expose DVB-S frequency override and fix matching priority
maedula [Fri, 5 Jun 2026 19:19:24 +0000 (21:19 +0200)] 
satip: expose DVB-S frequency override and fix matching priority

Expose the existing but hidden mm_dvb_satip_dvbs_freq field in the
DVB-S mux editor UI as an advanced property. This allows SAT>IP clients
to address DVB-S2X MIS muxes that share the same physical frequency but
have different ISI values, by assigning each mux a unique virtual
frequency within the Ku-band range.

Also move the SAT>IP frequency override matching (for DVB-S/C/T) to run
before normal dvb_network_find_mux() matching. This ensures virtual
frequency overrides always take priority, preventing accidental matches
against real transponder frequencies.

Guard the override match against unconfigured (zero) values using a
switch statement, so deltaU32(freq, 0) cannot produce false matches
when no override is configured.

Background: SAT>IP RTSP does not pass ISI (dmc_fe_stream_id), so
dvb_network_find_mux() can never match MIS muxes from SAT>IP clients.
The frequency override is the only path to reach MIS streams via SAT>IP.

7 weeks agoUpdate Internationalisation Templates
Flole998 [Mon, 8 Jun 2026 03:10:35 +0000 (03:10 +0000)] 
Update Internationalisation Templates

8 weeks agoepggrab: per-service EIT processing policy with global default
Oliver Sluke [Thu, 21 May 2026 22:04:08 +0000 (00:04 +0200)] 
epggrab: per-service EIT processing policy with global default

DVB EIT comes in two forms. Actual transport stream tables (table_id
0x4e present/following, 0x50-0x5f schedule) describe the multiplex
that carries them. Other transport stream tables (0x4f, 0x60-0x6f)
describe services on a different multiplex. The OTA grabber tunes
every multiplex, so when one multiplex carries other-TS EIT for a
service whose own multiplex is also grabbed, both descriptions reach
the EPG store and the last writer wins. A coarse other-TS placeholder
from a neighbouring multiplex can then overwrite a service's own
detailed schedule, and vice versa, so the EPG flips between the two
on successive grabs.

Replace the per-service "Ignore EPG (EIT)" boolean with an "EIT
processing" enum, and add a matching "EIT processing (default)"
global setting. Per-service values:

  Default       Defer to the global default
  None          Ignore EIT for this service
  Actual only   Accept only actual-TS sub-tables (0x4e, 0x50-0x5f)
  Other only    Accept only other-TS sub-tables (0x4f, 0x60-0x6f)
  Either        Accept both (today's behaviour, no precedence)
  Adaptive      Accept both, but once the service's own actual-TS
                schedule has been seen, drop further other-TS for
                it so a neighbour's coarse description cannot
                overwrite the detailed schedule

The global default exposes the same enum minus Default. New installs
get Either, which matches today's behaviour with no surprise
migrations.

The legacy "dvb_ignore_eit" key is read on first load: true migrates
to None (same observable behaviour), false to Default (defers to the
global, also same observable behaviour while global stays at Either).
The old key disappears from the config on next save.

The EIT handler resolves the effective policy per call (per-service
unless Default, in which case the global applies) and filters tableid
acceptance accordingly. Adaptive tracks per-service "actual-TS
schedule seen" at runtime; the flag is not persisted, so it is
relearned each session. P/F (0x4e / 0x4f) is only two events per
service and is not used as the seen trigger.

Full per-value help text lives in docs/property/eit_processing.md and
docs/property/eit_processing_default.md, wired in via the PROP_DOC
macro; the in-form ".desc" tooltips are short pointers to the Help
icon, matching the existing pattern (e.g. PROP_DOC(cron),
PROP_DOC(ota_genre_translation)).

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
8 weeks agodvr: make data error threshold configurable
Bernhard Berger [Sun, 31 May 2026 21:28:26 +0000 (23:28 +0200)] 
dvr: make data error threshold configurable

8 weeks agodvr: size subscription-title buffer to the title length
Oliver Sluke [Mon, 25 May 2026 11:46:16 +0000 (13:46 +0200)] 
dvr: size subscription-title buffer to the title length

`dvr_rec_subscribe` formatted the subscription title into a fixed
100-byte buffer via snprintf. snprintf truncates byte-wise, so a
long multibyte title (CJK at 3 bytes/char, or 4-byte emoji) could
leave a dangling partial UTF-8 sequence at the end. That title
flowed into the comet/ws JSON feed; the invalid byte made the
browser drop the frame, and the web UI stayed silent until the
recording ended.

Size the buffer to the actual title via alloca() so the title
isn't truncated at all. On the common case of short ASCII titles
the buffer is smaller than the previous fixed 100 bytes; on long
multibyte titles it grows just enough. Matches the existing
alloca pattern used elsewhere in the tree (cron.c, http.c,
string_list.c, rtsp.c, htsmsg_xml.c).

Fixes: #2121
Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
8 weeks agoepg: fix channel-name DESC sort (was calling description sort)
Oliver Sluke [Sat, 16 May 2026 21:17:29 +0000 (23:17 +0200)] 
epg: fix channel-name DESC sort (was calling description sort)

src/epg.c:2607-2610 — `_epg_sort_channel_descending` delegates to
`_epg_sort_description_ascending` and negates, so a request for
channel-name DESC returns events ordered by reverse description
text instead of reverse channel name. Rows cluster by adjacent
description prose, not by channel.

The sibling `_epg_sort_channel_ascending` (lines 2598-2605) uses
`channel_get_name` correctly, and every other `_descending`
variant in the file delegates to its own `_ascending` (title,
subtitle, summary, description, extratext, genre). Channel is
the only outlier — a copy-paste typo.

Fix: delegate to `_epg_sort_channel_ascending`.

Verifiable via:
  curl 'http://<host>/api/epg/events/grid?sort=channelName&dir=DESC&limit=20'

Pre-fix: rows interleave by description text. Post-fix: rows
cluster by channel in reverse alphabetical order.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
8 weeks agoAdd Ubuntu 26.04 (resolute) CI and Cloudsmith builds
copilot-swe-agent[bot] [Mon, 4 May 2026 13:24:40 +0000 (13:24 +0000)] 
Add Ubuntu 26.04 (resolute) CI and Cloudsmith builds

Agent-Logs-Url: https://github.com/tvheadend/tvheadend/sessions/6f069c7c-8b1f-4063-8606-70d3fe938d77

Co-authored-by: Flole998 <9951871+Flole998@users.noreply.github.com>
2 months agoFix const qualifier assignments
copilot-swe-agent[bot] [Sun, 17 May 2026 16:29:42 +0000 (16:29 +0000)] 
Fix const qualifier assignments

Agent-Logs-Url: https://github.com/tvheadend/tvheadend/sessions/72422d26-8fa7-4f11-a236-b5e8eef20c2d

Co-authored-by: Flole998 <9951871+Flole998@users.noreply.github.com>
2 months agoFix discarded-qualifiers warnings in channels.c, httpc.c, webui.c
copilot-swe-agent[bot] [Wed, 6 May 2026 09:30:02 +0000 (09:30 +0000)] 
Fix discarded-qualifiers warnings in channels.c, httpc.c, webui.c

Agent-Logs-Url: https://github.com/tvheadend/tvheadend/sessions/bc91d39b-dbd2-4eac-8b9d-3caf92a47726

Co-authored-by: Flole998 <9951871+Flole998@users.noreply.github.com>
2 months agowizard: fix SIGSEGV when unknown network type is submitted; add PO_LISTONLY for stric...
Copilot [Fri, 15 May 2026 21:18:52 +0000 (23:18 +0200)] 
wizard: fix SIGSEGV when unknown network type is submitted; add PO_LISTONLY for strict dropdowns (#2102)

* Fix crash when invalid network type is entered in wizard; add PO_LISTONLY for dropdown-only combo

Agent-Logs-Url: https://github.com/tvheadend/tvheadend/sessions/3e2bd0ab-3c09-47ce-af52-3bf7788e242f

Co-authored-by: Flole998 <9951871+Flole998@users.noreply.github.com>
2 months agompegts: align DVB delivery-system and modulation defaults with the enum
Oliver Sluke [Sat, 9 May 2026 06:01:58 +0000 (08:01 +0200)] 
mpegts: align DVB delivery-system and modulation defaults with the enum

Several mux classes in mpegts_mux_dvb.c declare a default value
that the corresponding *_enum callback never emits. Each enum
function iterates a small list of DVB_* constants and emits the
canonical string returned by the matching dvb_*2str helper —
which scans the relevant strtab in dvb_support.c and returns
the first matching row.

Two distinct mismatches:

1. Delivery-system defaults

   Three classes used the unhyphenated spelling and one carried
   the wrong constant entirely (copy-pasted from the dvbt class
   above):

     - dvb_mux_dvbt_class.delsys     "DVBT"  -> "DVB-T"
     - dvb_mux_dvbs_class.delsys     "DVBS"  -> "DVB-S"
     - dvb_mux_isdb_s_class.delsys   "ISDBS" -> "ISDB-S"
     - dvb_mux_dtmb_class.delsys     "DVBT"  -> "DTMB"

   delsystab carries both spellings on the same DVB_SYS_*
   constant, so configs persisted with the legacy form continue
   to load unchanged.

2. QAM constellation / modulation defaults

   qamtab carries TWO entries for "auto" — { "AUTO", DVB_MOD_AUTO }
   and { "QAM/AUTO", DVB_MOD_QAM_AUTO }. The enum arrays for the
   QAM-based modulation classes pass DVB_MOD_QAM_AUTO, so the
   dropdown publishes "QAM/AUTO" — but each class declared the
   default as "AUTO", which parses to a different constant and
   isn't a member of the published enum.

     - dvb_mux_dvbt_class.constellation        "AUTO" -> "QAM/AUTO"
     - dvb_mux_dvbc_class.constellation        "AUTO" -> "QAM/AUTO"
     - dvb_mux_atsc_t_class.modulation         "AUTO" -> "QAM/AUTO"
     - dvb_mux_atsc_c_class.constellation      "AUTO" -> "QAM/AUTO"
     - dvb_mux_isdb_t_class.layera_mod         "AUTO" -> "QAM/AUTO"
     - dvb_mux_isdb_t_class.layerb_mod         "AUTO" -> "QAM/AUTO"
     - dvb_mux_isdb_t_class.layerc_mod         "AUTO" -> "QAM/AUTO"
     - dvb_mux_isdb_c_class.constellation      "AUTO" -> "QAM/AUTO"
     - dvb_mux_dtmb_class.constellation        "AUTO" -> "QAM/AUTO"

   The other DVB tables (modetab, guardtab, hiertab) only carry
   a single AUTO entry, and bw / fec fall through to
   dvb_common2str which canonicalises value 1 to "AUTO" — so the
   remaining 22 MUX_PROP_STR(..., N_("AUTO")) declarations are
   already in alignment.

Effect: clients that validate enum membership on submit refuse
to send the default — the user has to manually re-pick the
canonical value before the form will save. The ExtJS combobox
doesn't enforce membership on submit, so the bug went unnoticed
there.

Migration: the delsys side has no on-disk impact (legacy and
canonical strings map to the same constant). For the QAM cases
"AUTO" parses to DVB_MOD_AUTO and "QAM/AUTO" parses to
DVB_MOD_QAM_AUTO — different constants. Existing configs
persisted with the legacy "AUTO" continue to load and tune
correctly (both signal "let the driver decide"); the dropdown
synthesises a current-value option so the form remains editable
until the user re-saves with the canonical choice.

Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>