- add target-wide TMDB Trending discovery for movie/TV and day/week, bounded by an optional `limit` (default 100, range 1–500) of unique remote references before local matching - compose concrete Trakt and TMDB sources under one target curation policy while retaining legacy `output[].trakt` compatibility - match only positive TMDB IDs of the same media kind and preserve existing catalog identities, series closure, and Xtream alias behavior - require every active selector to complete before target IDs, writers, caches, or watches can change - preserve the YAML-owned block through Source Editor operations and add TMDB setup/failure documentation and application Credits attribution - validate complete decoded pages under independent request, selector, and shared target-batch budgets, using a configured TMDB-only HTTP profile with certificate/hostname verification and no redirects or replays
212 KiB
Changelog
Unreleased
⚠️ Breaking Changes
-
Proxied resource URLs are now restricted to public destinations by default. Tuliprox proxies external resource URLs that come from provider, playlist, and EPG content: channel and small logos, EPG channel and programme icons, cover images, posters, and backdrops. These requests now enforce a destination policy on every route that serves them (
/resource/m3u/..., the Xtream resource routes,/resource/epg/..., and/api/v1/playlist/resource/...), where previously three of them fetched any destination reachable by the configured HTTP client.- A resource URL whose DNS host name resolves to a private address (RFC 1918 or IPv6 ULA) is rejected unless the
input that supplied it lists the exact host name in
resource_policy.allowed_hostsand the address inresource_policy.allowed_networks. A private IP literal requires only a matchingresource_policy.allowed_networksentry because IP literals are not validallowed_hostsvalues. Add the policy to the input that provides the logo or icon; for icons thatlogo_overridecopies out of EPG, that is the EPG input. - Loopback, link-local, cloud-metadata, CGNAT, multicast, and reserved addresses stay blocked with or without a policy. Redirects are re-checked on every hop and are bounded.
- Resource ownership is stored generically with each URL, including nested cover, poster, backdrop, and episode image fields. Legacy raw playlist/Xtream item resources use their containing item's input; legacy EPG resources without an authoritative input remain public-only until regenerated.
resource://is an internal reserved scheme. Provider data and mapping configuration must never supply it; such values are rejected rather than interpreted as authorization claims.- The canonical input name is the authorization identity of a resource origin. Configured input and alias names must be non-empty, globally unique strings; a configuration with duplicate input names, duplicate alias names, or an alias name that shadows an input name is now rejected while loading. Internal IDs are managed separately.
- The resource cache is keyed by the policy that authorized the entry, so an entry fetched under one policy is never served to another. The cache starts cold once on upgrade because the key layout changes.
- Resource proxying now always connects directly: a configured proxy and the
HTTP_PROXY/HTTPS_PROXY/ALL_PROXYenvironment variables are ignored for these requests, and Tuliprox logs a warning at startup and on reload when one is set. Provider fetches, playlist and EPG downloads, and streams keep using the proxy. - The Source Editor exposes the policy on every input under the shield-shaped Resource Policy page. Empty host and network lists restore the public-only default and omit the policy from the saved input.
- See Resource Policy for the parameters and the exact host-plus-network rule.
- A resource URL whose DNS host name resolves to a private address (RFC 1918 or IPv6 ULA) is rejected unless the
input that supplied it lists the exact host name in
-
The Web UI WebSocket protocol is now version 4. Playlist update completion messages carry the correlated run ID and execution order instead of a bare status. Reload existing browser tabs after upgrading the server; version-3 clients are rejected during the handshake rather than receiving incompatible update messages.
-
Smart EPG normalization now preserves XMLTV ID separators by default. The default
normalize_regexchanged from[^a-zA-Z0-9\-]to[^a-zA-Z0-9._\-]. Existing configurations that explicitly set the former pattern keep the legacy separator-removal behavior; remove the override or use the new pattern to adopt the new default. -
STRM names now use the processed target title by default: Previously, STRM folders and filenames preferred the media metadata name. Existing STRM targets that must retain that behavior need
use_metadata: true; otherwise the next export can generate different paths, andcleanup: truecan remove the old files. -
Shared Input Skip Option Names:
- Input options now serialize as
skip_live,skip_vod, andskip_seriesinstead of the old type-prefixedxtream_skip_*/stalker_skip_*names. - Existing config files remain read-compatible because the old names are still accepted as aliases.
- This is still a breaking change for generated config, API payloads, docs snippets, and any tooling that depends on the old serialized field names.
- Input options now serialize as
-
Staged inputs reworked into a first-class
stagedinput type. The old nestedstaged:block on provider inputs (withenabled,live_source,vod_source, andseries_source) has been removed. A staged source is now its own input withtype: staged. It points to one non-stagedm3u/xtreamprovider throughstaged.for_input, andstaged.clustersselects which clusters (live,vod,series) are overlaid by the staged playlist. Inside such a cluster of anxtreamprovider each staged group replaces the provider category it belongs to, matched by the staged channels' stream IDs and only then by category ID or group title; provider categories without a staged counterpart stay as they are. For any other provider type the selected clusters are replaced entirely by the staged groups, and the clusters not selected are loaded from the provider input itself. The merged result is stored under the provider input, so playlist delivery and stream/API routing continue to use the provider. See Staged Sources for the matching rules.Before:
inputs: - name: provider_a type: xtream url: http://provider-a.tv/player_api.php username: alice password: secret staged: url: http://lists.example/list1.m3u type: m3u live_source: stagedAfter:
inputs: - name: provider_a type: xtream url: http://provider-a.tv/player_api.php username: alice password: secret - name: provider_a_list type: staged url: http://lists.example/list1.m3u staged: provider: provider_a clusters: [live]A staged input cannot be linked directly to a target. It must reference an existing non-staged
m3u/xtreamprovider input, and each provider can have at most one staged overlay. Staged inputs do not usepriority,max_connections, orcache_duration; the linked provider controls stream limits and refresh cadence. -
Case-insensitive EPG channel-id matching: EPG channel-id matching is now case-insensitive (ASCII). Ids are no longer lowercased when parsed — output preserves the source's original case. Users whose sources provide MixedCase ids will see MixedCase ids in the M3U/XMLTV output instead of the previously-lowercased form; downstream players may re-map affected channels once. Supersedes #688 (M3U tvg-id lowercasing removed).
-
Removed the
plexSTRM export style. Existing STRM outputs configured withstyle: plexmust switch tokodi,emby, orjellyfin; Plex use cases should use the HDHomeRun integration instead. Existing generated TMDB marker paths remain read-compatible, butstyle: plexis no longer accepted in configuration. -
web_ui.auth.token_ttl_mins: 0no longer means "never expire". A configured0used to mint tokens with a ~100-year lifetime — a permanent bearer credential written as if it were a configuration convenience.0now falls back to the 24-hour default, and anything above the 30-day ceiling (43200minutes) is clamped; both log a warning naming the effective lifetime. Deployments that relied on0for long-lived tokens must set an explicit value within the ceiling, and their clients will start re-authenticating. -
A missing, malformed or wrong-scheme
Authorizationheader now answers401, not403. The auth extractors all returned403 Forbiddenfor a request that never authenticated at all, and sent noWWW-Authenticatechallenge — so a401from this server was never a well-formed401. Responses now carry the challenge for the scheme the endpoint wanted. Only an unresolvable peer address stays a400. Clients that branch on403to mean "not signed in" need to handle401. -
Notification event ids replace
MsgKind.messaging.notify_onis now a list of glob patterns over dotteddomain.eventids —*,recording.*,provider.*.expired, and a leading!to exclude, so["*", "!system.info"]reads the way it looks. Every legacyMsgKindname (info,stats,disk_alert, …) is still accepted and resolves to its canonical id, but the config is rewritten in canonical form the next time it is saved. Existing template filenames keep working; template maps are now keyed by event id wire name. -
Secrets are masked in config API responses.
GETof the main config previously returnedconfig.ymlin full to any client holdingConfigRead, including the Telegram bot token, the Pushover token and user key, and anyAuthorizationheader configured on the REST channel. Those — plus the new ntfy token, Gotify token and REST signing secret — are now masked on the way out. A save restores any secret the client echoes back still masked, so a Web UI round-trip cannot overwrite a real token with the mask; a genuinely changed secret still writes through. Tooling that read provider credentials out of the config endpoint can no longer do so. -
A failed local library scan is now
library.scan.failed, notlibrary.scan.completed. The taxonomy mappedLibraryScanProgressto the completion id whatever the summary said, so the failure path reached operators as "A local library scan finished" at info severity. Failure now takes its own id at error severity, discriminated on the status the payload already carries. Anotify_onglob such aslibrary.*orlibrary.scan.*picks the new id up; a subscription naminglibrary.scan.completedexactly will stop being told about failed scans and needs the new id added.
🌟 New Features
-
Target-owned discovery with TMDB Trending.
target.curationcombines optional Trakt and TMDB sources under onefull/curatedpolicy. TMDB supports movie/TV day/week feeds with explicitscope: first_page, its own Bearer token, exact same-kind TMDB-ID matching, and optional Xtream category projection. Every active selector is required; failed fetches retain finalized target artifacts in both modes. M3U/STRM keep normal selected entries, without Xtream aliases. Legacyoutput[].traktremains supported but cannot coexist with the new declaration on one target. Source Editor preserves the YAML-owned block and shows ownership hints; the dashboard version card now includes TMDB Credits. -
Runtime liveness watchdog. An optional heartbeat/watchdog detects a wedged async runtime (process alive, scheduler no longer making progress, logs stop) and logs a diagnostic snapshot with runtime metrics and a per-thread
/proc/self/taskinventory. It is opt-in and off by default (TULIPROX_WATCHDOG=1to observe,=2to also restart the process on a confirmed stall), and exposes its state through the/healthcheckruntimeobject. -
Trakt curation can now select one target-wide VOD/Series catalog and project Xtream categories independently.
output[].trakt.catalog_selectionacceptsfull(the compatibility default) orcurated;include_xtream_base_categoriesdefaults totrue; and each list/chart has a default-truecreate_xtream_category. Existing YAML therefore keeps the full catalog, normal Xtream categories, and current category-scoped alias IDs. Selection-only selectors may omitcategory_name, while category-producing selectors still require it. The Source Editor exposes the same controls in all supported locales.- Curation now evaluates exact surviving target UUIDs after favourites, group merge, and post-merge content deduplication. M3U and STRM receive selected normal entries rather than Xtream aliases, while Xtream watch behavior continues to observe its category view.
- Every enabled list/chart is required for a refresh. Partial selector success, missing/invalid credentials, request failures, malformed responses, and incomplete pagination now fail that target before IDs, persistence, cache, or watch effects instead of publishing a partial/base fallback.
- A complete empty or no-match result under
catalog_selection: curatedintentionally clears managed VOD/Series Xtream, M3U, and STRM state while preserving Live. Ordinary or failed empty refreshes retain previous artifacts.
-
.envfile support for secrets and environment variables: Tuliprox now automatically loads environment variables from a.envfile at startup.- Discovery order: searches
--env-file <PATH>(or-e),TULIPROX_ENV_FILE, then<config_file_dir>/.env(if-cwas supplied),<config_path>/.env,<home_path>/.env, and./.env. - 12-factor compatibility: variables already present in the host/Docker environment take precedence and are never overwritten.
- Safe thread model:
.envvalues load once at application startup and require a service or container restart to change. - An example configuration template is provided at
config/.env.example.
- Discovery order: searches
-
Target-specific bouquet filters are now managed directly from the Source Editor. Each target shows its current bouquet status below the regular filter settings and opens a full-size editor for selecting Live, VOD, and Series groups. Bouquet filters support both whitelist and blacklist mode, are stored by the target's unique name, and take effect on the next playlist update. Leaving every cluster unselected means no bouquet restriction; an individual cluster may intentionally have no selected groups while another cluster remains configured.
-
Per-cluster update quality guards for Xtream, Stalker, and M3U inputs. Optional
update_qualitythresholds compare Live, VOD, and Series item counts with the last accepted cluster and retain its data when a candidate falls outside the configured range. Xtream retains its active cluster database and categories; Stalker retains its active manifest entry and generation. M3U evaluates the clusters within one downloaded document and combines accepted candidates with retained clusters before persistence. Quality rejections are reported as partial updates, separately from technical failures. Omitted thresholds default to0, disabling the guard;100requires an unchanged item count. The Source Editor exposes these settings through the shared range-slider component. -
Input-focused playlist updates in the Web UI. The Update tab now provides compact input cards with consistent status badges, last-update times, and selection of affected targets by stable ID. Available actions follow each input's capabilities: Xtream, Stalker, and M3U offer Update, Refresh, and Force Update; Plex offers Update and Refresh; Library offers Rescan. Update may reuse a valid cache, Refresh bypasses cache reads while enforcing quality, and Force Update bypasses both for the selected input without changing saved configuration. Selected targets rebuild with all their required inputs; the bulk action remains separate. Existing target-name API requests remain supported.
-
Collapsible pipeline transparency instead of a permanent update log. Update Details separates input/cluster outcomes from target processing and output status. It shows available quality thresholds and evaluations, actual cache/provider origin, effective update mode, and correlated run details. Target summaries use the configured processing order and label Filter/Rename/Mapping counts as configured rules, not runtime hits. Technical failures, quality rejections, and unavailable facts remain distinct. A running target rebuild keeps its run's status and details until completion, even when a follow-up update has already been queued.
-
Reload-safe input and cluster status. Completed input results and one latest snapshot per cluster use the existing
status.json, without a separate history or browser persistence. Available snapshots retain the run's policy, actual data source, historical quality-guard threshold, quality evaluation, active count, and technical outcome. Current correlated runs take priority over persisted snapshots; older status files remain readable and missing facts stay unknown. Browser reloads can also restore in-progress state from the running server without persisting queued or updating states as completed results. -
Library rescans rebuild selected targets and expose catalog and scan facts separately. Rescan completes the filesystem scan before rebuilding targets; scan errors prevent the rebuild. A complete, error-free empty scan is authoritative, including deletion of the last item, while other required inputs remain available to the target's configured filters. Library details show Movies, Series, Episodes, and Total items from the stored catalog; Episodes counts stored episode records while Total items remains the movie/series-entry count. Last rescan shows the actual scanned-file, media-group, added, updated, removed, and error counts for the correlated run, including reported scan failures. Catalog counts survive reloads; unavailable rescan metrics remain unknown rather than being inferred from the catalog. Library cards no longer present their catalog as a generic provider/cache acquisition.
-
Target filters can run during processing or immediately before persistence. The existing scalar
filtersyntax remains theprocessingstage. The staged map accepts optionalprocessingandpersistfilters;persistsees the fully finalized state after EPG processing, mappings, merge, deduplication, sorting, numbering, and counters. Omittingprocessingno longer requires a match-all filter, and targets may omitfilterentirely. Presence checks useIS EMPTY/IS NOT EMPTY;= EMPTY/!= EMPTYremain accepted as compact aliases. -
Targets can clear invalid EPG IDs without removing playlist entries. Setting
options.clear_invalid_epg_ids: trueclears IDs that do not resolve to processed EPG data, including IDs changed by mappings. Without the option, unmatched IDs are preserved. The oldrequired_epgname remains a read-only alias. -
Ten events for the failures that used to be silent: the registry described states nothing emitted, and several subsystems reported their start and their success but never their own failure. The taxonomy is now 42 events (up from 32) and gains two domains,
scheduled_task.*andnotification.*.system.started/system.shutdownhad been registered — and documented — since the registry was written, with nothing in the tree emitting either, so an operator who subscribed got silence. One payload carries both kinds, so a subscriber can ask for restarts alone: the running version and the bound address on start, the signal name on stop. Placement is the whole design: the start event is published after the notification bridge subscribes, because anything published before it reaches nobody, and the stop event before the service tokens are cancelled, because that stops the outbox that would carry it. Neither reaches the WebSocket — there is no panel that renders them, and the Web UI has necessarily disconnected by the time the second one fires.provider.fetch.failedreports what kind of fetch failure it was.ProviderErrorKindalready classified every provider failure across all three families and already exposedis_retryable()andneeds_operator()— the two questions an operator actually asks — and nothing consumed either, so every fetch failure was counted, logged and treated identically. The event carries the classification, the worst error's text, how many there were, and whether any of the playlist came through anyway. Severity follows the classification rather than the registry: aConfigfailure will not fix itself and is an error, everything else may and is a warning. Input name and error text go throughsanitize_sensitive_info, since both can carry a provider URL with credentials in it.provider.pool.exhaustedandprovider.priority.fallback— two moments the lineup manager knew about and told nobody.ActiveProviderreported that connection counts moved; nothing reported that a stream was refused because every provider behind the input was full. The per-provider current/max-plus-expiry snapshot thatlog_exhausted_pool_snapshotbuilt and then discarded unless debug logging happened to be on is now built unconditionally on that (already slow) path, and the debug line and the event render from the same structured data. Priority fallback —acquirewalking priority groups high to low and silently falling through when the preferred ones are at capacity — is reported on transition rather than per allocation, because the fall-through happens on every request while the primary is full and one event per stream start would bury the thing worth hearing; a move back towards group zero is a recovery and says so. Input and provider names are sanitized.user.connection.denied:ActiveUserreports connects and disconnects, and a refusal is neither, so the one outcome a user actually complains about was the one nothing published — the admission ladder modelled it fully and handed it to the caller and nobody else. It carries the user, the address the request was attributed to, and the limit that was reached, and it takesUserReadrather than the system-wide read: "who was turned away" is the same question as "who signed in". Only the strategy path emits; an explicitTerminatealso resolves to exhausted, but that is a requested teardown, not a denial.playlist.watch.disabled/playlist.watch.unmatched:watchhad one event for everything it knows and three ways to stop working without saying so — every pattern failing to compile, which disabled the feature on a typo behind a singlewarn!; the target carrying the reserved default name; and a watch state file that could not be read or written, which either re-baselined the group (losing the change it should have reported) or dropped it entirely. All three now report the reason and, where there is one, the underlying error. The first needed the config layer to stop discarding the distinction: an emptySomeis now load-bearing and means "configured and unusable", which is not the same as "not configured".playlist.watch.unmatchedcovers the fourth silence — a pattern matching no group looks exactly like a group that has not changed, so a typo inwatchwas invisible.playlist.groups.changed:watchtracked channels inside named groups and was blind to the group set itself, in both directions. A group appearing was silent — no baseline file, so one was written, nothing was emitted, and the group's entire channel list read as "not new" from then on — and a group vanishing was worse, since it is absent from the refreshed playlist and no code path observed the disappearance at all. The target's group titles are now diffed against a persisted index before the per-group fan-out, so it sees every group rather than only the ones the watch patterns name; the question is which groups exist, not what is inside the watched ones. The index sits beside the per-group directory (<target>.groups.bin, not<target>/__groups.bin) so it cannot collide with a group whose sanitized title matches, and first sight writes the baseline silently — announcing every existing group as new on the first refresh after an upgrade would be noise. Gated ontarget.watchbeing configured, so it costs nothing for targets that never asked to be watched.metadata.update.failedfor an input whose tasks burn through their retries: the completion event only fires when a cycle drains with changes, and an exhausted task only reached adebug!, so an input whose resolves fail every time emitted a start and then nothing for as long as it stayed broken — on the bus, indistinguishable from one still working through a long queue. The worker now counts the tasks that exhaust their retries during a cycle and reports the input, the count, whether anything resolved anyway, and the last error. Reported alongside the completion rather than instead of it: a cycle can both produce changes and exhaust tasks, and the completion is what triggers the downstream playlist update. Per cycle rather than per task, since a provider that has stopped answering fails every item behind it.scheduled_task.failed: the playlist update and the library scan report their own outcomes, but the GeoIP refresh had no terminal event of its own — it logged one line and moved on — so an operator running on a stale database never found out. It carries the task type and the cron expression that triggered it. The task is typed asScheduleTaskTyperather than a free string, so a task added to that enum cannot be reported under a name nothing recognises. A disabled GeoIP update stays silent: the task ran and correctly found nothing to do, which is not a failure.notification.dead_letteredwas registered and documented; the outbox detected the condition, bumpedhealth().dead_letteredand logged tonotification::audit, and nothing subscribing to the bus could learn that a notification had been permanently lost. It is now emitted at the point the outbox gives up, carrying the event id, the attempt count, the channels that never accepted it, and when it was first enqueued. It is deliberately not notifiable and deliberately absent fromNOTIFIABLE_KINDS, so the bridge is not even woken for it: this event exists because delivery failed, and enqueueing a notice about it into the same outbox against the same channels that just failed is the loop its registry entry warns about. Operators still get the audit line and the counter, and plugins see it on the bus. Emitted at the attempts-exhausted site only — a notification every channel rejects as permanent is also dropped, but that path cannot yet be told apart from a clean delivery.library.scan.failed— see Breaking Changes.EventKindgains it alongside the progress kind rather than reusing it: progress is high-frequency and a scan failure is not, so a subscriber that only wants failures should not have to take the tick firehose to get them.
-
Open-world notification system: adding a notification channel or a notification event is no longer a change across ten sites in three crates.
- Events are ids, not an enum. An event is a dotted
domain.eventstring with a registered severity and description. 42 events are registered today, spanningsystem.*,playlist.*,recording.*,provider.*,config.*,library.*,metadata.*,user.*,auth.*,stream.*,scheduled_task.*andnotification.*. The Web UI event picker is driven by the registry, so an event added in the backend appears in the UI without a frontend change, and template discovery iterates the registry instead of a hardcoded eight-variant list that silently made new kinds undiscoverable. - Four new channels:
ntfy(self-hosted push, no account or bot token),gotify,slack(real Block Kit header/section/context blocks rather than a re-used Discord embed), andcommand, which runs a local program with the event JSON on stdin. The command channel executes the binary directly rather than through a shell, so there are no quoting rules and no shell-injection surface from event content; a missing binary is a permanent failure, while a non-zero exit or timeout is retried. - Webhook HMAC signing: the REST channel takes an optional
signing_secretand sends an HMAC-SHA256 of{timestamp}.{body}asX-Tuliprox-Signature. The timestamp is inside the signed payload, so a captured request cannot be replayed with a fresh header. Verified against the RFC 4231 test vector. - Per-channel routing: each channel accepts an optional
routingblock (notify_on,min_severity,quiet_hours,max_per_hour,dedup_window_secs), so "critical to Pushover, everything to Discord" is now expressible. An absent block inherits the global subscription, so existing configs are unaffected. Quiet hours defer rather than drop — an overnight outage nobody hears about afterwards is worse than one that arrives late — and the hourly ceiling emits one "further notifications suppressed" line when it trips so the silence is distinguishable from a dead notifier. - Durable delivery for every notification. The outbox moved out of the recording supervisor, is no longer gated on
the recording config, and starts unconditionally once the listener is bound. Playlist stats, watch changes, disk
alerts and provider warnings previously fanned out and discarded every outcome, so a transient
502lost them permanently. Entries key pending channels by stable string id, so an outbox written by a build that knows a newer channel no longer fails to deserialize and take every pending notification down with it. Entries left inrecording_notification_outbox.jsonare adopted intonotification_outbox.jsonexactly once. - Failures are classified.
408/429/5xxare transient; other4xxare permanent and dead-letter immediately instead of burning every attempt on a request that will fail identically forever. A provider'sRetry-After— both legal header forms, with a past HTTP-date clamped to "retry now" — wins over our own backoff rather than retrying straight back into the rate limit. - Every event renders on every channel. One notification envelope carries id, severity, timestamp, instance, dedup
key, title, body and the typed payload, with
titleandbodyalways populated. Pushover gains template support, sendstitleseparately and maps severity onto its own priority scale — it previously pushed rawserde_jsondumps of watch changes and playlist stats to phones. Every channel's severity maps onto the target's own priority scale rather than being dropped. - A test endpoint:
POST /api/v1/config/messaging/testrenders and optionally sends a chosen event to a chosen channel and returns the per-channel outcome and the exact rendered body.preview: truerenders without sending, so a template can be iterated without spamming a channel. It deliberately bypassesnotify_onand the suppression window — the operator asked for this one explicitly. - Typed provider account events:
provider.account.status_changed,.expiringand.expiredreplace account status and expiry warnings that previously landed in the generic info/error buckets, so subscribing to "my account is about to expire" no longer means also receiving every processing error. All three carry a dedup key, since they are re-evaluated on every playlist refresh. - Section 5 of the operator documentation is rewritten for this model: the glob grammar, a table of all registered
events with their default severities (checked against the registry by a test in both directions), per-channel
routing, delivery semantics, the new channels, and the uniform
event.*template context alongside every legacy key. The table sits between generated-block markers and is checked against the registry by a test in both directions, so a registered event missing from the table — or a table row for an event that no longer exists — is a test failure rather than stale documentation.
- Events are ids, not an enum. An event is a dotted
-
Event bus as the single event backbone: the WebSocket bus and the notification layer used to be two disconnected worlds with their own emitters. They are now one taxonomy that plugins, notifications and the Web UI all read.
- The notification pipeline subscribes to the bus, so every bus event — playlist updates, config changes, library
scans, user connections, metadata updates, recording changes — can be notified on, and every future event comes
along with it. Everything defaults to unsubscribed, so an upgrade does not start messaging anyone until
notify_onasks for it. High-frequency variants (progress ticks, download deltas, periodic system info) are deliberately not notifiable; their terminal counterparts are what get through. - Nine notification-only lifecycle events moved onto the bus (disk alerts, config reload failures, playlist watch changes, the three recording lifecycle events, and the three provider account events), so they now reach plugins and subscribers rather than only operators on mail.
- New events:
user.created/.updated/.deletedfor API-proxy user CRUD,stream.probe.failedfor ffprobe failures,config.reload_failed, and the auth audit events below. User events carry username, target and state — never the password or token; probe failures carry a sanitized URL and are deduplicated per input, so a provider outage notifies once instead of once per channel behind it. GET /api/v1/events/stats(behindsystem.read, like/status) reports the bus counters — emissions per kind, emissions with no subscriber, and the size of every gap a lagging subscriber was told about — plus a 256-entry ring of recent events with their kind, uptime and outcome, including the ones that were coalesced. "Why did my notification not fire?" was otherwise unanswerable without a debug build.- State snapshots on connect: events that describe current state rather than an occurrence (the system-info and downloads samples) are retained, so a Web UI session that connects between samples gets them immediately instead of showing empty panels for up to three seconds. Occurrences are never replayed.
- Graceful shutdown: the stream-meter registry is flushed at shutdown, so a stream still running when the server stops no longer loses its last window's transferred bytes.
- Plugin subscription seam: a plugin manifest's
events.*list resolves to a subscription mask, with unknown names reported rather than silently narrowing what the plugin asked for, and every event defines its own JSON payload.
- The notification pipeline subscribes to the bus, so every bus event — playlist updates, config changes, library
scans, user connections, metadata updates, recording changes — can be notified on, and every future event comes
along with it. Everything defaults to unsubscribed, so an upgrade does not start messaging anyone until
-
Authentication hardening: sign-in throttling, token revocation, and an audit trail.
- Login throttling:
/auth/tokenused to verify an argon2 hash, answer401and forget, so a password list could be worked against it as fast as the hash function allows. Failures are now counted on two dimensions — client address, which stops one host grinding a list, and username, which stops a distributed attack converging on one account. Three free attempts, then 2s doubling to a 15-minute ceiling, answered as429withRetry-After. The check runs before the argon2 verify, and a correct password clears the block immediately. Usernames are canonicalised the way the rest of the auth path compares them, soAliceandaliceshare one budget. - Token revocation: the tokens this server mints are stateless JWTs, so a leaked one previously stayed valid until
it expired — there was no way to end a session or respond to a compromise short of rotating the signing secret,
which kills every session at once.
POST /auth/revoke/{username}ends one principal's sessions across both identity namespaces andPOST /auth/revokeends everyone's; both requireUserWrite. Revocation is a per-subject watermark ("everything issued at or before this instant is dead") rather than a deny-list, so it is bounded in size and can express "sign out everywhere" and "revoke everything issued before the breach". It is persisted — a revocation that stopped applying at the next restart would be a security control in name only — and a revocation file that will not parse is a startup error rather than an empty store. The refresh endpoint checks revocation too. - Auth audit events: sign-ins, rejected sign-ins, throttled sign-ins and permission denials reach the bus as
auth.sign_in.succeeded/.failed/.throttledandauth.permission.denied— previously they went to a log line and nowhere else, so the events that matter most for spotting an intrusion were the ones nothing could subscribe to. Each is a separate event id, so a subscriber can ask for the failures without being woken by every successful sign-in. The record holds a username, an address and an outcome; the password and the token are not in the type at all rather than being redacted at each render site, because these records reach Telegram, webhooks and shell commands. Notifications dedupe per principal, address and outcome, so a password-guessing run is one piece of news rather than one per attempt. They requireUserRead, notSystemRead, and are not pushed to the Web UI socket.
- Login throttling:
-
Providers remember what they already told us: Stalker capability knowledge — whether a portal implements
get_all_channels, which handshake recipe worked, which of several endpoint candidates answered — was discovered and then thrown away, so every refresh re-probed endpoints already known to404and replayed a chain whose answer was known. Replaying a full handshake chain against a portal with stale credentials looks, from the provider's side, a lot like credential stuffing. The snapshot is a hint rather than a contract: every claim carries the instant it was observed and expires after a day, a remembered endpoint is moved to the front of the candidate list rather than replacing it, and a clock that has run backwards leaves the snapshot alone. A JSON-file store (one file per input, written through the workspace atomic-write helper, ignoring a corrupt file rather than failing) is included for persisting it across restarts; the composition root does not load or write it yet, so today the memory lasts for the life of a client. -
Streaming provider catalogs:
get_live_streamsand friends buffered an entire provider catalog into memory before the caller saw a single row. They now have a streaming variant that hands over batches as they arrive, matching the shape the bulk-EPG path already had. The trade is made explicit rather than hidden: the accumulating sink can still restart pagination on the next endpoint candidate after a mid-catalog failure (which is why a truncated catalog is never returned as success), while the streaming sink reports that it can no longer restart once a page has been released, and an error there means the delivered batches are an incomplete prefix. -
DVR Feature: a full digital video recorder built around a queue-mutation boundary with a typed
QueueMutationError, atomic edit/quota rollback, O(1) edit writes via a rememberedRecordingLocation, server-side conflict preview (POST /api/v1/recording/conflicts/preview), and aConflictSeverityofNoKnownConflict/PossibleCapacityWait/LikelyMissedWindow. Three background supervisors start once the HTTP listener is bound, honour thedownloadscancellation token, and re-read their config each tick so a reload applies without a restart:- Startup reconciliation finishes or undoes deletions interrupted by a crash (tasks whose
recording.deleting_previous_statewas set), and repairs queue/rule-store drift. - Retention performs the age, count, and disk-watermark sweeps described in the operator guide
(
tuliprox/docs/src/operator/dvr.md). - Notification outbox delivers lifecycle notifications durably, retrying per channel with capped exponential
backoff and dead-lettering after
max_attempts. A notification that reached Telegram but not Discord is retried only against Discord, so retries stay compatible with the at-most-once contract. GET /api/v1/recording/health(administrator only) reports each supervisor's last-tick timestamp, the outbox depth, and the dead-letter count.
Two WebSocket notifications carry the recording subsystem:
RecordingChanged(any queue mutation) andRecordingRulesChanged(rule-store mutation). The cancel-recording-task endpoint emits both because cancelling future rule recordings mutates the queue as well as the rule store.Authorization is gated by
Claims::is_system_principal, which now requires bothusername == "recording-supervisor"andsubject_id.is_builtin_admin()so a web user registered with the sentinel name cannot forge the system bypass; the supervisor is the only path that mints both.Media opens for catalog, range, full-body, thumbnail and subtitle flows go through
no_follow_path_in_root, which walks every component fromrecording_rootto the leaf withsymlink_metadata. A symlink at any intermediate path such as<root>/users/aliceis rejected beforeFile::openfollows it, closing the<recording_root>/users/alice -> /etccontainment bypass.bin/dvr_doctor.shexposes supervisor health, the effective recording config block, the quota ledger and on-disk state as one read-only dump suitable for a support ticket. - Startup reconciliation finishes or undoes deletions interrupted by a crash (tasks whose
-
Automatic Xtream Account Expiration Refresh:
- Server mode now refreshes missing or soon-expiring Xtream
exp_datevalues directly through each account'splayer_api.phpcredentials, independently of playlist updates and reseller Panel API provisioning. - Per-account daily checks, five-minute panel-wide spacing across aliases, and a six-hour panel cooldown after transport, HTTP 403/429, or server failures reduce the risk of provider bans.
- Updates are persisted in 15-minute batches to source YAML and Xtream alias CSV files, with timestamped backups, atomic writes, durable throttle state, and a single in-memory config refresh per batch.
- Accounts reported as expired are persisted and disabled immediately; Tuliprox does not re-enable them automatically.
- Server mode now refreshes missing or soon-expiring Xtream
-
QoS snapshot compaction:
qos_aggregation.compaction_interval_secsnow periodically rebuildsqos_snapshot.dbto reclaim storage from expired snapshots. It defaults to daily; set it to0to disable automatic compaction without changing QoS summary windows. -
STRM metadata naming option: STRM outputs now accept
use_metadata: trueto prefer media metadata names for generated folders and filenames. The default remains the target's processed title, so title rename and mapping rules apply to STRM paths without additional configuration. -
Mapping block
stage(processing/after_epg):- Each
mapping.ymlblock now accepts an optionalstage. The defaultprocessingkeeps the block at the target'sMslot;after_epgruns once EPG channel IDs and logos are enriched so mappers can react to them. Counters still run after the final merge and sort. The sharedMappingStageenum rejects unknown values, and the directory merge fails configuration loading when one mapping id uses conflicting stages. - Mapping-directory merges now preserve declaration order and the first block's
match_as_asciivalue.
- Each
-
Mapper Read-Only Metadata Fields:
- Mapper scripts can read
@Inputand@Typedirectly and use them as regex sources or map keys. - Assignments to these immutable fields are rejected while all existing read-write fields remain assignable.
- Mapper scripts can read
-
Dependency-aware parallel playlist updates:
process_parallel: truenow downloads independent inputs concurrently and starts each source's targets as soon as its required inputs are ready.- Added optional non-zero
inputs[].sequential_groupIDs to serialize complete refreshes that share provider credentials or another upstream ban constraint. - Target preparation remains configuration-ordered, while final persistence overlaps only for disjoint normalized storage, M3U, and STRM paths.
- Stalker Live/VOD/Series/EPG selections publish atomically and resume a durable completion checkpoint after a crash.
- Playlist update progress messages identify the affected input.
-
New Stalker Portal Integration:
- Added first-class Stalker input support to Tuliprox.
- Added Stalker catalog preview support in the protected Web UI playlist endpoints.
- Added Stalker playback URL materialization with runtime
create_linkrefresh for stale or expired temp links. - Added typed handling for portal-internal auth/session body codes such as
44and440..449so Stalker playback refresh can react to them. - Added Stalker bulk-EPG ingestion with streaming parse and batched persistence to avoid buffering the full payload in memory first.
- Added explicit unresolved-item semantics for Stalker playlist entries: Tuliprox keeps Stalker playback metadata without
exposing raw portal
cmdvalues as playlist URLs. - Added follow-up hardening for Stalker temp-link playback modes, runtime stale-URL invalidation, endpoint-preference ordering, and soft session-TTL refresh behavior.
- Added explicit Stalker transport-policy handling: Tuliprox only proxies
http/httpsplayback URLs and rejects unsupportedrtmp/rtspcommands up front. - Added the remaining Stalker config fields to the Web UI, including device identity overrides and per-action response-size caps.
- The remaining open edge case is portal-specific header/cookie forwarding for temp-link media requests; fresh temp-link resolution itself is already implemented.
-
Shared HLS stale-origin recovery and finite terminal tails:
- Detects reachable-but-stale HTTP
200origins from host/epoch-local progress evidence and retains the complete configured acceptance burst, including all derivedbeastlanes and slots. - Uses lease-specific READY reserve, measured playback position, recovery ETA, and transition margin for admission, recovery, and cutover; publication lateness and request counters no longer terminalize playback.
- Validates critical MPEG-TS handoffs with bounded read-only
mpeg2ts-readerinspection without modifying origin, candidate, or cached media. - Prepares twelve finite terminal TS blocks ahead of cutover with target-duration timestamp stride and measured asset
duration, then serves compatible warm terminal manifests as HTTP
200with discontinuity, optional key reset, and#EXT-X-ENDLISTon lease- and generation-bound routes. - Adds bounded autonomous terminal-commit retries, sticky terminal leases, GC protection for referenced live tails, and operational alert/rollout guidance for probe, bundle, recovery-deadline, and commit-retry failures.
- Detects reachable-but-stale HTTP
-
ICS Calendar EPG Sources: Import iCalendar (
.ics) events as XMLTV EPG data with M3U and Xtream channel assignment, Smart Match support, configurable four-hour dummy gap filling, bounded atomic cache downloads, and aggregated warnings for recurring events that are detected but not expanded yet. -
Extended EPG Programme Metadata:
- XMLTV imports now preserve all programme
<category>elements, including optionallangattributes, as well as the presence of<live/>and<new/>tags. - The metadata is retained while merging EPG sources, persisted for M3U and Xtream targets, and included in served XMLTV output and Web UI EPG previews.
- ICS
CATEGORIESproperties are converted into individual XMLTV categories, including repeated properties and escaped commas; ICS events do not inferliveornewwithout an unambiguous source value. - Existing persisted EPG data remains readable and defaults the new metadata to empty categories and unset flags.
- XMLTV imports now preserve all programme
-
Trakt Charts: Xtream Trakt integration can now build virtual categories from public Trakt charts via
trakt.charts[].- MVP supports
movies/showswithtrendingandpopular. - User-owned Trakt lists remain configured separately under
trakt.lists[].
- MVP supports
-
Update Log In Playlist Update View:
- The Playlists → Update view now shows a terminal-style log that accumulates
PlaylistUpdateProgressandLibraryScanProgressevents in real time, prefixed with[playlist]/[library]and a localHH:MM:SStimestamp, with auto-scroll to the latest line and a FIFO cap of 500 entries. - The log is cleared synchronously when the user clicks either the playlist Update or the library Update button, so each run starts with a fresh view.
- Styled to match the dark monospace look of a console (uses existing theme CSS variables for background, border, and text color) and honors the same touch / overflow behavior as the rest of the view.
- The Playlists → Update view now shows a terminal-style log that accumulates
-
Session Expiry Handling:
- The Web UI now schedules a client-side logout when the JWT expires, showing a notification and returning the user to the login screen instead of silently failing with 401 errors.
-
Guided Empty States:
- Empty lists now show a short hint explaining what to do next instead of just "No content", applied to HDHomeRun devices, schedules, the API proxy server list, the playlist explorer, and the EPG viewer.
-
Status Health Banner:
- A single green/amber/red health indicator in the header aggregates the realtime connection, backend status, and provider connection capacity, with a hover breakdown and click-through to the Stats view.
- The banner now switches to amber ("degraded") only when every input group is exhausted. A busy provider
with available fallback capacity (another enabled alias or input in the same group) stays green, matching
the
/readyendpoint. The per-provider 80% warning threshold still colors the individual provider rows in the hover breakdown.
-
Readiness Probe Endpoint (
/ready):- New
GET /readyendpoint for load balancers and container orchestration: answers200({"status":"ready"}) while at least one input group has spare connection capacity, and503({"status":"exhausted"}) once every input group is fully used. Before provider connections are registered it answers503({"status":"initializing"}). - Capacity is evaluated per input group: an input and its enabled aliases form one group, so a saturated primary account still counts as ready while any of its aliases (or any other group) has a free slot. Disabled inputs and aliases contribute no capacity.
- The container-template
docker-compose.ymldocuments the probe split: Docker keeps the liveness check via/healthcheck(restarts on crash, not on transient capacity exhaustion), while orchestrators (k8s, swarm) should point their readiness probe at/ready./api/v1/statusremains the detailed status payload.
- New
-
Live Metric Sparklines:
- The Stats cards now show interactive time-series sparklines for CPU, memory, network throughput, active users, and active user connections, keeping a rolling history so trends are visible at a glance.
- Hovering shows a cursor and tooltip with the value at that point
- System metrics are now sampled every 2 seconds (down from 5) for more responsive charts.
-
Bookmarkable Views (Deep Linking):
- The active view is now reflected in the URL hash (e.g.
#stats,#source_editor), so views can be bookmarked, shared,
and navigated directly via URL. - Browser back/forward navigation and manual hash edits now switch the active view accordingly.
- The active view is now reflected in the URL hash (e.g.
-
Dev Container Support:
- Added a
.devcontainersetup so the project can be developed in a reproducible container (locally, on a remote Docker host, or in Codespaces). It pins Rust 1.89.0, adds the WASM and musl targets, and installstrunk,wasm-bindgen,cross,cargo-edit,mdbook, andmarkdownlint-cli2, forwarding the backend (8901) and frontend dev-server (9899) ports.
- Added a
-
UI Micro-Interactions:
- Cards now gently lift with a soft shadow on hover, buttons give a subtle press/ripple feedback when clicked,
and collapse/accordion chevrons smoothly rotate between open and closed states. All effects honor
prefers-reduced-motion: reduce.
- Cards now gently lift with a soft shadow on hover, buttons give a subtle press/ripple feedback when clicked,
and collapse/accordion chevrons smoothly rotate between open and closed states. All effects honor
-
Animated Theme Transitions:
- Switching themes now cross-dissolves colors, backgrounds, borders, and shadows instead of snapping instantly.
The transition is applied only during the switch (never during normal hover/interaction) and is fully disabled
for users who set
prefers-reduced-motion: reduce.
- Switching themes now cross-dissolves colors, backgrounds, borders, and shadows instead of snapping instantly.
The transition is applied only during the switch (never during normal hover/interaction) and is fully disabled
for users who set
-
Resilient Sidebar Initialization:
- The sidebar no longer panics if the global
windowobject is unavailable; resize handling and responsive collapse now degrade gracefully instead of crashing the app.
- The sidebar no longer panics if the global
-
Improved Screen-Reader Support:
- Toast notifications are now announced by screen readers via an
aria-liveregion (errors assertively, others politely). - The sidebar toggle button exposes its
aria-expandedstate and an accessible label, and the active navigation item is marked witharia-current="page".
- Toast notifications are now announced by screen readers via an
-
Table Empty-State Message:
- Paged/data tables now show a localized "No content" message in their empty state instead of just an icon, making it clearer when a query or filter returns no rows. (Table headers already stick to the top while scrolling.)
-
Debounced Filter Editor Input:
- Typing in the filter editor no longer re-parses and previews the filter on every keystroke; parsing and change notifications are now debounced, keeping the textarea responsive while editing large filters.
-
Persisted UI Preferences:
- The sidebar collapsed/expanded state and the stream-history table page size are now remembered across sessions (the active theme was already persisted), so the UI restores your last layout on reload.
-
Explicit Button Type On Shared Controls:
- The shared
IconButtonandTextButtonprimitives now render withtype="button", preventing accidental form submission when used inside forms.
- The shared
-
Disk-Space Alerts:
- Added a threshold-based disk-usage monitor (Normal / Warn / Critical) implemented in
backend/app/src/api/sys_usage.rs::DiskAlertMonitor. The background sampler ticks every 2 seconds (fixed, not configurable — seeSYSTEM_USAGE_INTERVALin that file) and the state machine decides when to emit aDiskAlertto the existing messaging channels (Telegram, Discord, Pushover, REST). - The state machine emits a
DiskAlertwhenever the level is non-Normal and (state_changedorrepeat_interval_secselapsed since the last notification). Long-running full-disk situations therefore re-notify periodically instead of going silent after the initial transition. - Configurable thresholds:
messaging.disk_alert.warn_percent(default80.0),messaging.disk_alert.critical_percent(default95.0), and the re-arm intervalmessaging.disk_alert.repeat_interval_secs(default3600, i.e. 1 hour). The sampling interval is not configurable. - Operators can opt out of disk-alert messages by simply not declaring a
disk_alertblock undermessaging, keeping the feature strictly opt-in and backward compatible. - Custom message templates per channel are supported via the standard
messaging.<channel>.templates.disk_alert_*Handlebars hooks (e.g.messaging.telegram.templates.disk_alert_warn,messaging.discord.templates.disk_alert_critical). A clear plain-text fallback is used when no template is configured.
- Added a threshold-based disk-usage monitor (Normal / Warn / Critical) implemented in
-
Shift+Click Range Selection For API-User Playlist Categories:
- In the API-user playlist editor, holding
Shiftwhile clicking a category now selects or deselects the whole range between the last anchor and the clicked item. - Plain mouse drag without
Shiftstill allows normal text selection, whileShift+dragno longer shows a brief browser text highlight before the range selection is applied.
- In the API-user playlist editor, holding
-
Descriptive Image Alt Text:
- Logo images on the login screen and sidebar now use the configured app title for their alt text, and playlist channel logos use the channel title, improving screen-reader accessibility.
-
Popup Menu Keyboard Dismissal:
- Popup menus now close when pressing
Escape, in addition to clicking outside, improving keyboard accessibility.
- Popup menus now close when pressing
-
Accessible Modal Dialogs:
- All dialogs (confirm, content, and custom) now expose
role="dialog"andaria-modal, trap keyboard focus within the dialog while open (Tab/Shift+Tab cycle through its controls), close onEscapewhen dismissable, and restore focus to the previously focused element when closed. Confirm dialogs also expose their title as an accessible label.
- All dialogs (confirm, content, and custom) now expose
-
Recoverable Error Boundary:
- Added an
ErrorBoundarycomponent that wraps each main view (dashboard, stats, streams, downloads, users, sources, playlists, EPG, RBAC, config) and the API-user playlist, so a recoverable failure shows a fallback with a retry button instead of leaving the section blank — and a failure in one view no longer affects the others. - Descendants can report a recoverable error through the boundary's context handle; retrying re-mounts the protected subtree so the user can recover without reloading the whole app.
- Added an
-
Runtime-Discovered UI Languages With RTL Support:
- The frontend now reads the available UI languages at runtime from an
assets/i18n/index.jsonmanifest, so adding a language only requires shipping a<code>.jsonlocale file and adding an entry to the manifest — no code change. - A language picker appears in the toolbar (next to the theme picker) whenever more than one language is available, and the chosen language is remembered across sessions.
- Each language declares its text direction (
ltr/rtl); the document direction is updated accordingly, providing baseline support for right-to-left languages such as Arabic.
- The frontend now reads the available UI languages at runtime from an
-
Confirmation For Destructive Download/Recording Actions:
- Cancelling a transfer/recording and removing a download entry now prompt a confirmation dialog before proceeding.
- The confirmation dialog focuses the safe (Cancel) action by default, consistent with other destructive actions (delete user, delete target, delete RBAC user/group).
-
Toast Notification UX Upgrade:
- Auto-dismiss toasts now show a countdown progress bar that reflects the remaining time before they disappear.
- Hovering a toast pauses both the dismiss timer and its progress bar, and resumes from where it left off on mouse leave.
- Error toasts gain a "copy details" action that copies the full message to the clipboard for easier bug reports.
- The progress bar and entrance animation respect the
prefers-reduced-motionaccessibility setting.
-
M3U Catchup / Archive Preservation And Proxying:
- Tuliprox now preserves standard M3U catchup/archive attributes during M3U import and export.
- Supported preserved attributes include:
catchupcatchup-dayscatchup-sourcecatchup-timecatchup-correctioncatchup-type- additional unknown
catchup-*attributes
- M3U catchup metadata is now stored under live stream properties and survives playlist rewrite/output generation.
- XMLTV
catchup-idis now imported, merged and exported alongside programme data. - Reverse-proxied M3U outputs can now expose local Tuliprox catchup/archive URLs instead of leaking provider archive URLs.
- Catchup URL templates now resolve indexed parameters and common player query forms such as
utc/lutcandutcstartwithoffsetorduration. - Native Flussonic archive playback supports HLS (
.m3u8) and MPEG-TS (.ts) paths. This includes flat archive requests used by TiviMate and nested Flussonic archive paths. - Live M3U entries that provide
timeshiftwithout a separate catchup block now use that value as Flussonic-style catchup metadata instead of being rejected as non-live archive requests. - Archive timestamps are retained when Tuliprox follows same-origin child HLS playlists. Segment, key and initialization URLs are left unchanged apart from normal relative URL resolution.
- Generated M3U playlists advertise the user's Tuliprox XMLTV endpoint through both
url-tvgandx-tvg-urlwhen server information is available. - Per-channel
#EXTVLCOPT:http-user-agentvalues are imported and used for upstream HLS and MPEG-TS requests. The directive is only written to playlists that still contain direct provider URLs.
-
Per-User Output Clusters: API proxy users can now be restricted to specific clusters on their assigned target via
output_clusters.- Supported values:
live,vod,series. - The filter is evaluated per user and limits which clusters are visible and deliverable for that account.
- At least one cluster should be selected if you want an active restriction.
- If no cluster is selected, the filter is treated as inactive and Tuliprox serves all clusters for that user.
- Supported values:
-
Input Resolve Filter: Added
resolve_filteroption to input configuration to selectively resolve only entries matching a filter expression. -
Input Probe Filter: Added
probe_filteroption to input configuration to selectively probe only entries matching a filter expression. -
Soft Connections And Soft Priority: API users can now be configured with
soft_connectionsandsoft_priority.- Soft connections allow a user to consume additional preemptible provider slots above
max_connections. soft_priorityis only applied while a connection is using a soft slot; once a regular slot becomes available again, the running connection
is promoted back toNormaland uses the user's normalpriority.- The soft-vs-normal classification is now preserved through provider-backed stream creation, HLS session handling, and shared live-stream reuse.
- The Web UI user editor now exposes both soft connection count and soft priority.
- The user DB schema is upgraded accordingly to persist the new fields.
- Soft connections allow a user to consume additional preemptible provider slots above
-
Download And Recording Manager: The Web UI download feature has been expanded into a provider-aware download/recording manager.
- VOD, series and episode downloads now use typed transfer snapshots across REST and websocket updates.
- Download state in the Web UI is websocket-driven after the initial snapshot instead of relying on repeated REST polling.
- Live entries can be scheduled as recordings through an
ffmpeg-based recording worker. - Download and recording tasks now respect provider capacity and join the normal priority/preemption model instead of bypassing provider limits.
- Download snapshots and actions are integrated into RBAC through the dedicated
download.readanddownload.writepermissions. - Background fairness controls were added under
video.download:reserve_slots_for_usersmax_background_per_providerdownload_priorityrecording_priority
- Transient retries now use exponential backoff with jitter and an explicit retry ceiling via:
retry_backoff_initial_secsretry_backoff_multiplierretry_backoff_max_secsretry_backoff_jitter_percentretry_max_attempts
- Transfer snapshots now expose
WaitingForCapacity,RetryWaiting,retry_attemptsandnext_retry_at. - Waiting transfers stay cancelable/pausable while they are blocked on provider capacity or retry backoff.
- The download scheduler and active worker now participate in config hot reloads and restart under updated
video.downloadsettings. - Corrupted persisted download state is renamed to a timestamped
*_corrupt.*.jsonbackup and no longer blocks server startup. - Playlist Explorer download and recording actions are hidden without
download.write, and duplicate queue requests now return the existing
task instead of creating a second entry. - The Playlist Explorer now supports:
- optional priority override for VOD/series/episode downloads
- start time, duration and optional priority override for live recordings
- Missed scheduled recordings are now terminalized during recovery/promotion instead of being replayed late.
- Preempted or retried live recordings continue with the remaining recording window instead of restarting with the full original duration.
-
QoS Aggregation Persistence: Added a new persisted QoS snapshot repository (
qos_snapshot.db+qos_snapshot_meta.db) in the storage directory. These files are maintained automatically by the QoS aggregation worker and become part of the local persistent runtime state. -
Stream History QoS Foundation: Stream history now captures structured QoS-relevant stream lifecycle data for later
reliability analysis and failover preparation.- Added
connect_failedas a first-class event type for startup failures before a stable session exists. - Added structured
failure_stageclassification (admission,provider_open,first_byte,streaming,session_reconnect). - Added
connect_failure_reasonfor startup/admission failures such as exhausted user/provider capacity. - Added structured provider failure metadata via
provider_error_classandprovider_http_status. - Added stable stream identity fields for cross-run QoS aggregation:
input_namestream_identity_keystream_url_hash
- Added shared-stream QoS markers:
shared_joined_existingshared_stream_id
- Stream disconnect history now stores meaningful
disconnect_reasonvalues:provider_errorprovider_closedpreemptedsession_expiredclient_closed
- Added
-
Connect Failure Recording: Admission and provider-open failure paths now write
connect_failedrecords into stream
history instead of only surfacing fallback responses. This includes exhausted user/provider capacity and provider-open/channel-unavailable style startup failures. -
QoS Snapshot Aggregator: Added a periodic QoS aggregation worker that reads stream history partitions and persists
compact QoS snapshots per stream identity.- Uses a dedicated B+Tree snapshot repository.
- Maintains rolling
24h,7d, and30dwindows from daily buckets. - Runs outside the streaming hotpath and processes history incrementally in the background.
-
QoS Snapshot Tooling:
- Added
--dbqto inspect the QoS snapshot database from the CLI DB viewer. - Added backend QoS snapshot read endpoints for summary/detail access.
- QoS snapshot API supports both JSON and CBOR responses depending on the request
Acceptheader. - The Web UI now shows QoS summary/detail data alongside stream history and requests QoS data via CBOR.
- Added
-
QoS Configuration: Added a dedicated
reverse_proxy.qos_aggregationconfiguration block to control the periodic aggregator. -
HLS session expiry now emits a disconnect history record with reason
session_expired. -
A minimal stdout logger is now initialized at the very start of the process so that errors during path resolution and early startup are always visible in the console.
-
Stream History Writer Block Bounds: Stream-history block headers now use the real min/max event timestamps of the
batch instead of assuming the first/last batch element is time-sorted. -
CLI Viewer Testability: The stream-history viewer no longer calls
process::exit()internally; exit handling now
happens inmain, making the path easier to test and reuse. -
Admission Failure Deduplication: Repeated admission failure response logic in
hls_api,m3u_api, andxtream_api
has been centralized into shared helpers. -
API User Network Access Restrictions: API proxy users can now be restricted by source CIDR ranges and/or GeoIP country codes via
network_access.- Matching any configured CIDR or any configured country is sufficient for access.
- Network access checks are centralized in the API user request context so denied requests stop before endpoint handling or upstream forwarding.
- Country-based checks require the GeoIP database. If GeoIP is unavailable, the secure default is to deny requests that did not match a configured CIDR.
- Operators can explicitly opt into allowing this GeoIP-unavailable country-rule case with
reverse_proxy.geoip.unavailable_policy: allow. - CIDR-only misses, unknown countries, and country mismatches still deny.
-
QoS Aggregation Efficiency:
- QoS snapshot listing does not rely on a full unbounded materialization path for filtered UI/API reads.
- Current-day QoS rebuilds are skipped when the history day is unchanged.
- Snapshot traversal APIs were reduced to a single repository traversal style to avoid duplicated code paths.
-
Connection Admission Rules: Added configurable admission strategies to
reverse_proxy.stream.admission_strategiesthat control how Tuliprox handles new stream requests when the user or provider connection limit is reached.evict_user_same_ip_oldest— evicts the oldest active connection from the same user and IP to make room.evict_user_same_ip_latest— evicts the newest active connection from the same user and IP to make room.evict_user_oldest— evicts the oldest active connection for the same user, regardless of IP.evict_user_latest— evicts the newest active connection for the same user, regardless of IP.grace_instant_stream— grants a grace period and immediately starts streaming.grace_hold_stream— grants a grace period but holds stream output until the grace check completes.- Strategies are evaluated in order; the first matching strategy wins and blocks later ones.
- Configuration rejects obviously shadowed orderings where
evict_user_oldestis placed beforeevict_user_same_ip_oldest, orevict_user_latestbeforeevict_user_same_ip_latest. grace_instant_streamandgrace_hold_streamare mutually exclusive.- Grace strategies require
grace_period_millis > 0. - Added comprehensive connection handling documentation covering failures, user-visible behavior, priorities, sessions, and reconnects.
- Added two new runtime-flow handbook pages:
- operator-facing current runtime flow
- developer-facing runtime internals and activity flow
-
Session Handling Boundary: HLS and catchup remain session-based for continuity and provider affinity, but regular TS/VOD/local playback is now enforced as socket-bound admission.
- A second non-HLS socket now counts as a second user connection even for the same user, IP, and stream.
- This prevents parallel TS/VOD sockets from being collapsed into one logical playback.
- Soft connections still work normally: once
max_connectionsis full, an additional socket may still be admitted asSoftwhensoft_connections > 0, and provider-side priority/preemption rules still apply afterward. - Admission-driven evictions now record the just-evicted session briefly so aggressive player reconnect loops cannot immediately evict the new winner back out again.
- This is not a full user cooldown: reconnects still succeed when a hard slot or soft slot is actually free, and switching to another channel is unaffected.
- For socket-bound TS/VOD/local playback, the anti-ping-pong protection uses a short same-user, same-IP, same-channel winner guard because those clients do not provide a stable reconnect session identifier.
- HLS activity refresh and cleanup dispatch were moved off the request/drop fast path so activity updates do not block segment responses and cleanup events are not silently lost when queues are temporarily full.
-
Provider URL Selection Strategy: Added
provider_url_selection_policyto provider definitions insource.yml.resume_last_working(default) — after failover, the provider continues using the last known working URL until it fails again.restart_from_first— after failover, the provider always tries from the first URL on the next request.
-
Structured Error Types: Replaced the old
TuliproxErrorKind-based error model with a typedTuliproxErrorenum usingthiserror. Each config domain now has its own variant (e.g.,ConfigStream,ConfigInput,ConfigSource,ConfigApiProxy,ProxyUser), making error messages precise and traceable. -
Web UI Landing Page: Added
landing_pagesetting toweb_uiconfig to choose the initial view after login. Supported values:dashboard,stats,streams,stream_history,downloads,users,config,source_editor,playlist_update,playlist_settings,playlist_explorer,playlist_epg,rbac. -
Stream Display Visibility Controls: Added optional
web_ui.stream_infoconfig to hide selected fields in the active stream display.- Supported flags:
hide_grouphide_iphide_countryhide_sharedhide_durationhide_bandwidthhide_transferredhide_playerhide_user_commenthide_epg
- If all flags are
false, the config is treated as absent and the default view remains unchanged. - Hidden
epgalso suppresses the per-stream EPG fetch/display work in the dashboard.
- Supported flags:
-
Runtime Config Report: Added opt-in startup dump of the complete effective runtime configuration.
log.runtime_config_report_enabled(defaultfalse) — enables the report.log.runtime_config_report_format—yaml(default) orjson.- Sensitive values (passwords, secrets, tokens, API keys) are automatically redacted.
- Includes prepared
config.yml,source.yml, loaded mappings/templates/api-proxy sections, and resolved paths.
-
CVD-Friendly Theme: Web UI now includes a CVD (color vision deficiency) friendly theme option.
-
Stream View:
- Displays the user comment in the stream view.
- Displays EPG information in the stream view.
-
Shared HLS Streams
- Added advanced video routing with HLS and TS support, including HLS provisioning polling and correct byte-range handling for TS output.
- Introduced live HLS reverse-proxy caching with lifecycle scheduling, garbage collection, and improved demand/prefetch backpressure.
- Added smarter HLS playback access/admission handling for consistent manifest responses.
⚙️ Optimizations
-
The effective admission strategy list is carried as
Arc<[AdmissionStrategy]>:GraceResolutionContextis stored onStreamInfoand travels with every clone of it, so aVec<AdmissionStrategy>field meant reallocating the list on each clone — andget_effective_admission_strategieshanded back a freshVecthat the context builder then cloned again. The list is immutable once resolved, so the context clone is now a refcount bump and the one allocation left is theArc::fromat resolution time. The strategy loop still takes a plain&[AdmissionStrategy], reached by deref, so slicing the remaining strategies is unchanged. -
Watch pattern matching walks the groups once: matching re-tested every configured pattern per group inside a filter. It now walks the groups once and records which patterns hit — which is also what makes
playlist.watch.unmatchedpossible. -
Notification templates are resolved and compiled once:
resolve_templatewrapped every template value in an input source and ran a full download attempt — once per message, per channel. Afile://template was re-read from disk and anhttp://one re-fetched over the network for every notification, and an inline Handlebars string paid for a download attempt too. The source is now classified once (inline / file / URL), local files revalidate on mtime so an edit applies immediately, remote documents cache on a 5-minute TTL, and compiled templates are kept in a registry so rendering is no longer a re-parse. When a remote template cannot be refreshed the cached copy is served rather than silently degrading to the built-in text. Config validation can now compile-check a template body without sending, surfacing a malformed template at load instead of leaving a per-send error and a plausible-looking fallback. -
Notification sends are concurrent and bounded: the outbox awaited channels in sequence and the shared HTTP client sets no request timeout, so one webhook host that accepted a connection and never answered could stall every pending notification — including the recording ones the outbox exists to protect. Sends now run concurrently per channel with a 30-second request timeout. The channel set (and the
reqwest::Clientbehind it) is built once and cached rather than reconstructed on every send. -
Filtered event subscriptions: every subscriber used to receive all event kinds and filter afterwards — after the broadcast channel had already cloned the message for it. A subscriber now declares a one-word mask and is never woken for what it did not ask for. The notification bridge is the first user: the four kinds it drops are the bulk of the traffic during a playlist refresh. The two largest payloads (the system-info and downloads samples) are carried behind
Arc, costing a refcount bump per receiver instead of a deep copy, and the last subscriber standing pays nothing. -
Event coalescing for payload-free nudges: the recording-changed nudge is emitted from six routes, twice back-to-back where deleting a recording also changes the rules, and once per item in a bulk operation — each one making every Web UI session re-fetch the same snapshot. Nudges that carry no payload are now coalesced in a 250 ms window measured from the last admitted send, so a sustained stream is throttled to one per window rather than one per burst and a later user action always produces a visible refresh. An event carrying a payload is never coalesced, however repetitive, because a dropped tick loses the message it carried.
-
Static dispatch through the messaging, event and processing paths: the notification layer's
Vec<Arc<dyn NotificationChannel>>and its boxed future per send, the event sink, the metadata update sink, and the playlist-update bootstrap were all trait objects behind heap-allocated futures. All of them are now static: channels dispatch through an enum the compiler turns into a direct call, and the sinks are type parameters whose absent case is a zero-sized no-op that compiles away entirely. The send path allocates nothing, and emitting an event on the playlist pipeline is a direct call rather than a vtable hop. Same delivery semantics, same 46 messaging tests. -
Playlist repository iterators no longer box: the hottest traversal in the repository returned
Box<dyn Iterator + Send>from three methods on a trait that is never used as a trait object, paying one allocation per call plus one uninlinable indirect call per playlist item — and the cluster skip-set filter boxed a second time on top. The adapter chains are replaced by named state machines and an enum per source kind, so a filtered traversal now allocates nothing at all. Two async methods dropBoxFuturefor plainasync fnat the same time. -
One shared M3U/Xtream playlist backend: the two raw-playlist iterators were transcriptions of one design — the same two read locks, the same blocking producer feeding a bounded channel, the same sorted-index reader — differing only in item type, storage subdirectory and error constructor. Those differences are now associated types and consts on a zero-sized marker, so 151 lines of duplicated logic became one implementation with entirely static dispatch. The one behavioural difference that was preserved rather than normalised (M3U holds its read lock for the consumer's lifetime, Xtream never did) is now named in the type rather than implicit.
-
Redundant
Arcclones dropped from the Xtream per-item parse loop: four fields wrapped a clone around an accessor that already returns an owned value, so each parsed Xtream stream paid four redundant atomic increment/decrement pairs on the playlist parse path. The workspace now has zeroArc::clone(&x.y())sites. -
Typed field access on playlist item headers: the mapper, sort and counter paths reached fields by string name, walking a chain of ~20 case-insensitive comparisons and returning an owned
Arc<str>— so readingchnoortypedid a heap allocation and an interner write lock, on every read, per item, per rule. Field access is now keyed on a typed enum with a borrowing read that never forces an allocation, and mapper counter fields are parsed once at config load instead of re-parsed per channel inside the counter loop. -
BPlusTree store path rewritten to stream to disk:
BPlusTree::storeno longer buffers a fullVec<[u8; PAGE_SIZE]>(up to ~270 MiB for large playlists) before writing. A newPageSinkhands out page ids and writes each finished page positionally to the destination file. Pages are written out of order — leaves are reserved before the overflow chains they point at — so positional writes are required.verify_fullre-reads the file before it is published, catching any write-ordering bug. Peak RAM during store drops by the fullVecsize; on a 70 000-entry playlist build this measured as ~270 MiB. -
Per-batch commit on playlist import: each
BATCH_SIZE(1000) entries are now committed to the BPlusTree before the next batch is read. Previously the whole import ran inside one transaction and accumulated adirty_pagesmap of every modified page between commits — bounded by total feed size. With per-batch commit,dirty_pagesis bounded byBATCH_SIZEpages plus the size of one batch in flight. Wall-clock cost is one extrafsyncper batch (~70 batches for 70 000 entries, ~7 ms total on SSD); peak RAM cost drops proportionally. -
Positional file write helper: new
write_all_at_offsetinbackend/btree/src/common.rsmirrors the existingread_exact_at_offset. UsesFileExt::write_all_aton Unix and an explicit short-write retry loop on Windows. Previously a bug in this area would have left the database file silently truncated or scrambled; the short-write loop is the same pattern used byread_exact_at_offset. -
Quick-XML read buffer pre-allocation:
parse_tvguidenow starts itsVec<u8>withwith_capacity(64 * 1024)instead ofVec::new(). The buffer is monotonically grown by quick-xml (it returns&buf[start..]between events; the caller does not clear it), and the largest single XML event in a typical XMLTV feed is a programme description that can grow to ~163 MiB. Starting at zero capacity triggered ~25 doubling reallocations along the way; starting at 64 KiB reduces the realloc chain and avoids the first costly zero-to-4 KiB jump. A regression test feeds a 200 KiB programme description and asserts the parser still produces the expected events. -
Stack-allocated JSON number parser:
serde_utils::deserialize_number_from_stringpreviously calledserde_json::Number::to_string()— a heap allocation per call. A newStackNumber(32-byte stack buffer) is filled viafmt::Writeand then parsed in place. The 32-byte size is the maximum render of anyserde_json::Number(-1.7976931348623157e-308is 24 bytes, so 32 leaves headroom); it deliberately takes&Numberrather thanimpl Displayto avoid being misused with a rawf64(whoseDisplayimpl writes 310 bytes forf64::MINand would silently truncate toNone). Affects everyEpgProgrammefield deserialized fromXtreamPlaylistItem— measurable on a 70 000-channel feed. -
UUID hashing skips the to_string allocation:
generate_provider_playlist_uuidandgenerate_local_playlist_uuidnow usePlaylistItemType::as_str()(a&'static str) instead ofto_string()(a fresh heapString). Same byte input, no allocation. A test (item_type_label_is_hash_stable_against_display) guards against a futureDisplayimpl that stops delegating toas_str()— the hash result is byte-equal to a hash built from the oldto_string()form, so all existing persisted UUIDs remain stable. -
Disk-based EPG processing (
disk_based_processing = true): each EPG source'sEpgMergeAccumulatornow drains directly into a tempBPlusTreeon disk viafinish_into_disk(path, source_priority, source_order), batched at 100 channels. The temp file is removed by aDiskEpgSourceDropguard, so a panic or early return cannot leak temp files. Amerge_epg_treesfunction does the multi-way merge at the end withO(n_sources + total_channels)complexity (per-channel priority resolution insideEpgMergeAccumulator::upsert_channel). The previous loss of per-source priority in the wire-up (set_attributes_if_preferred(0, 0, ...)) is fixed:DiskEpgSourcecarriessource_priorityandsource_order, andmerge_epg_treesreads them. Behaviour is gated onconfig.disk_based_processing; when false, the existing in-memoryflatten_tvguidepath is unchanged. On a 70 720-channel feed this reduced the EPG-parse phase peak from ~340 MiB to a per-source-batch bounded value. Note: the final mergedEpgstill materialises aVec<Arc<EpgChannel>>(the downstream consumers expect that shape); the constant-memory guarantee is on the write side only. A regression test for the wire-up path uses shared channel ids across two sources to exercise the priority-overrideOccupiedbranch and fails loudly if it ever breaks. -
Readiness hot path trimmed for frequent polling:
/readyis typically polled every few seconds by load balancers. The member→group lookup table is now derived once when the source config loads (SourcesConfig.group_lookup) instead of being rebuilt on every request; the saturation check consumes an iterator of provider slots and accumulates the few groups in a linearVecscan instead of allocating aHashMapper call; and the response is a typedReadyResponsestruct instead of aserde_json::json!DOM tree. The only remaining per-request work is the live connection walk, which cannot be cached. -
Health banner render allocations: provider rows now hold shared
Arc<str>names instead of freshly allocatedStrings, and the saturation check iterates the rows instead of cloning them into an intermediateVecon every render. Backend and frontend share oneCapacityGroup/build_group_lookupimplementation in thesharedcrate, so/readyand the banner can no longer drift apart in how they group inputs and aliases.
🐛 Fixes
-
Streaming and connection management: resolved silent async hang / deadlock during client kicks and concurrent stream load. Under concurrent stream traffic,
tuliproxwould occasionally stop logging and serving requests (the Web UI became unreachable and active streams dropped) while the container remained in a running state with near-zero CPU and memory usage. Thread inspection revealed Tokio worker threads parked infutex_waitorepoll_waitwith no crash or panic.Several interrelated issues contributed to this stall:
- Hyper's HTTP/1.1
graceful_shutdown()only prevents accepting subsequent requests on keep-alive connections; it does not abort active streaming response bodies. When a client was kicked while streaming live media, the server's serve loop waited indefinitely onconn.as_mut().awaitas long as the client continued reading bytes, postponing the associated upstream provider release and holding provider slots indefinitely. Forced socket closures now actively drop the connection transport (drop(conn)), terminating in-flight response bodies and triggering immediate provider cleanup. - Connection close signals were delivered via unaddressed broadcasts, meaning an unrelated receiver could report
delivery success even if no transport task listened for the target socket address, causing the fallback provider
cleanup on kicks to be skipped. A per-socket
SocketCloseState(Open/Closing) using oneshot channels now ensures targeted signal delivery, preserves provider allocations across duplicate kicks until transport termination, and reliably invokes fallback provider cleanup when unreceived. - In
ActiveUserManager, empty user records were removed by dropping and re-acquiring the write lock on the connections registry, causing severe lock thrashing and starvation against concurrent periodic tasks (such as active user logging). Empty user records are now removed atomically under the same lock acquisition. - In
SharedStreamManager, the shared registry lock guard is now explicitly dropped prior to secondary asynchronous cleanup and meter token cancellation.
- Hyper's HTTP/1.1
-
Provider priority was ignored and a second concurrent client failed with a source error while capacity was free. Provider-slot reservations were granted as soon as a playback opened a provider, so an HLS/DASH entry that only ever served a manifest — or a player that retried its manifest and gave up — still held a reservation for the whole
hls_session_ttl_secswindow. Because HLS entry session tokens carry a per-attempt suffix, every retry created a separate reservation under a separate owner. Those reservations were foreign to one another and to unrelated clients, so a higher-priority provider was skipped as reserved even after the active connection counters had reached zero and playback fell through to a lower-priority alias. Clients sharing one reverse-proxy socket hit this constantly, which looked like exhausted capacity or socket-based reservation collisions.Reservations are now provider slot leases with an explicit confirmation step. A lease starts unconfirmed, pins the provider for its own playback, and blocks nobody else. Only real media delivery confirms it — the first provider media byte forwarded to a client as an
OK/206media response. Manifest, HEAD, key, map and error fetches never confirm a lease — and only a confirmed lease with a configured reconnect window reserves capacity against other playbacks. Unconfirmed leases expire after a short startup deadline regardless of TTL, so abandoned starts and manifest-retry loops no longer accumulate. One owner holds at most one lease, so repeated requests of the same playback reuse their slot instead of stacking a reservation per attempt.Lease end is outcome-driven: a clean finish of a reconnect-capable playback (HLS, DASH, VOD, series, catchup) keeps the slot as an idle lease for its window, while provider failure, preemption, kick and timeout release capacity immediately. The
Skipping reserved provider ... for <SocketAddr>message is replaced by a structured decision log carrying the reason, current and maximum connections, foreign reserved slots, and the active/starting/idle slot split, so a fallback decision can be audited without inferring it from a socket address.Playback cleanup is now request-specific. Parallel segment, range and reconnect requests carry independent request identities, while a provider-binding generation prevents delayed cleanup from releasing a newer successor binding. Provider allocations are owned by drop guards until they are explicitly transferred, closing cancellation gaps during provider open, grace handling and HLS origin refresh.
Shared MPEG-TS subscribers now have identities independent of their transport socket, so clients sharing one reverse proxy connection cannot replace or cancel one another. Subscriber queues enforce byte and chunk budgets, slow clients have progress deadlines, and incomplete burst replay ends only the affected subscriber instead of skipping into live delivery. Cleanup admission is bounded by the new
reverse_proxy.stream.cleanup_queue_capacitysetting (default4096); mandatory HLS cleanup uses a reserved control lane. Graceful shutdown closes admission, releases active claims and provider leases, and waits for owned streaming and cleanup workers to finish. -
PTT title parsing: fixed panics on multi-byte UTF-8 character boundaries (e.g. en-dash
–). During title metadata parsing (such as background VOD/series metadata enrichment), previous match indices recorded from earlier handlers (e.g.year) could become stale after preceding handlers removed matched substrings in-place withreplace_range. In thevolumeshandlers, slicing&title[start_index..]using the stale index caused a worker thread panic (start byte index X is not a char boundary; it is inside '...') whenever the offset landed within a multi-byte UTF-8 sequence such as an en dash (–), em dash (—), or non-ASCII characters, crashing the background update worker. Slicing offsets involumesnow clamp and snap down to the nearest valid UTF-8 character boundary (is_char_boundary), andparser.rsstring operations (replace_rangeand title truncation) now explicitly verify char boundaries before slicing. -
Playlist update status reads no longer block an async request worker on filesystem I/O. The existing input status reader runs on the blocking pool; its status format and reload projection are unchanged.
-
Xtream disk processing preserves successful cluster downloads when another cluster request fails. Existing per-cluster quality checks still apply, while the failed cluster retains its previous data.
-
Stalker acquisition failures now identify the affected requested clusters. Portal, client, storage and handshake errors mark only requested, non-skipped clusters as failed instead of leaving their status unknown.
-
The Library catalog status initializes storage before its first read. A fresh installation reports zero catalog counts, while initialization failures and corrupt catalog data remain errors rather than appearing empty.
-
Trakt curation now requires an explicitly configured Client ID. Tuliprox no longer bundles or falls back to a shared Client ID. Blank or header-invalid
trakt.api.api_keyvalues now produce one target-scoped warning and skip only optional Trakt curation without making an HTTP request; other target processing continues. Trakt401,403,404, and429responses now have actionable, resource-aware messages, while independently successful lists and charts remain available. -
Empty playlist updates no longer replace previously published input or target data. A completely empty refresh is treated as a failed update and keeps the last usable playlist and its virtual-ID mapping intact. This prevents transient provider/download failures from making channels disappear or assigning different IDs when service recovers. An intentionally empty target must therefore be disabled or filtered operationally instead of being published as an empty refresh.
-
M3U alias failover now uses the allocated provider's opaque query credentials. When capacity allocation moved a stream from the primary input to an alias, a playlist URL carrying credentials such as
tokenorapi_keycould retain the primary provider's value because failover only understood base URLs and username/password credentials. Tuliprox now matches authentication-like query fields against the configured M3U account URL and replaces both key and value with those of the selected provider while preserving unrelated query parameters. Different key names such astokenandapi_keyare mapped only for an unambiguous one-to-one pair; ambiguous mappings fail closed. -
Admission: a request that ended up denied anyway could leave several other streams killed behind it. Eviction is destructive and is never rolled back, but the strategy loop would kick a target, find the retry still denied, and move straight on to the next eviction strategy. The loop now samples
user_connectionseither side of the kick. A kick that reduces the count is real progress and later strategies still run, which keeps the over-limit case — a hot-swapped config that loweredmax_connections— converging; a kick that frees nothing skips furtherEvictdecisions for the rest of the walk.Gracestrategies are still evaluated either way. -
Admission: a request that lost the queue race walked the eviction strategies on a stale count. The resolver read the admission state, then queued on the per-user gate, then used the snapshot it had taken before it queued — so it could evict a live connection to free a slot the winner had already released. The state is re-read after the gate (and after the empty-strategy check, so the uncontended path costs nothing extra) and the grace context captures the fresh kind. This does not close the wider check-then-register window: the slot is registered by the caller outside this gate, so two requests at
max_connections - 1can still both be admitted. -
Admission: a suppressed eviction misaligned the strategy index and replayed a grace strategy. The loop counted with a manual index incremented at the end of the body, and the eviction-reentry suppression arm exits via
continueand so skipped it, handing every later strategy an index one too low. That index is whatGraceResolutionContext.strategy_indexstores, and the post-grace fallback resumes atstrategy_index + 1: with[EvictUserOldest, GraceHoldStream]and the eviction suppressed, the grace recorded index 0, so the fallback slice restarted at the grace strategy itself and replayed it instead of moving past it. Now counted withiter().enumerate(), so the index cannot drift from the item. -
Events: watch payloads carried synthesized prose where a plugin expected channel titles. The watch handler truncated its own lists by pushing a sentence into them — "... 42 more added entries omitted" beside real channel titles, or "5000 entries added. Detailed list suppressed" replacing the list outright. That was legible to the text template and to nothing else: the payload is serialized straight to JSON for plugins, and a plugin has no way to tell a sentinel from a channel actually named that. The subject line had the same problem from the other side — it read
added.len(), so a suppressed change of five thousand announced itself as "1 channel(s) added".WatchChangesnow carriesadded_total,removed_totalandtruncated; the lists stay pure channel titles, the counts stay true whatever the lists carry, and the plain-text renderer spells the omission out itself.WatchChanges::newsets the totals from the lists, so a caller that is not truncating cannot get them out of step. -
Events: a failed local library scan reported itself as finished. The failure path emitted the same progress variant with
status: Error, and the taxonomy mapped that variant to the completion id unconditionally, so a failure reached operators as "A local library scan finished" at info severity. The id is now discriminated on the status the payload already carries — the way playlist updates have always discriminated on their state — and severity comes from the registry with no second table. The emitter was already reporting the status correctly; nothing downstream was reading it. See Breaking Changes for the subscription consequence. -
Messaging: an edited bot token, webhook URL or template did not take effect until a restart. The channel set and the compiled templates are cached so a notification does not rebuild every channel — and a fresh HTTP client with them — on every send, but nothing invalidated those caches on config reload. They are now invalidated when the config reloads.
-
Messaging: a successful playlist refresh with statistics notified twice. The run summary went straight to the notification layer as a second message while the bus carried the bare outcome, and both resolved to
playlist.update.completed— so one refresh produced two notifications, neither carrying the other's content, and a bus subscriber saw an outcome with no detail. The outcome, per-source statistics and aggregated error text are now one event emitted once at the end of the run. The WebSocket frame is unchanged, so the Web UI sees what it always did. -
Messaging: disk alerts were gated on somebody being subscribed by mail. The emission itself checked the subscription, so a plugin or any other consumer watching for disk pressure saw nothing unless an operator happened to want the same event on the same channel. The notification layer already drops unsubscribed events, so the check is gone.
-
Auth: changing a password invalidated nothing.
pwd_versionwas minted into every web token and checked in exactly one place — the refresh endpoint — so a token issued against the old password kept working on every guarded route until it expired. Together withtoken_ttl_mins: 0meaning ~100 years, a leaked token was effectively a permanent credential. The check now runs on every request whose principal is a web user, and rejectspwd_version: 0rather than treating it as "skip", which is how the refresh endpoint's own copy could be bypassed. Users will be signed out after a password change, which is the intent. -
Auth: revoking a permission had no effect until the token expired. The permission check read the snapshot minted into the token. The effective set is now the intersection of the claim with what the live config grants, so a revocation takes effect on the next request. A new grant still requires a refresh, because a token must never end up with more authority than it was issued with. Three permission paths that had drifted apart — one with no schema gate, no subject gate and no password-version check at all — now share one implementation.
-
Auth: the configured JWT issuer was never validated. Token validation checked expiry and nothing else, so
isswas decoration. It is now checked, including on the WebSocket paths, which previously carried a bare secret across task boundaries and so had no issuer to check against. -
Auth: an access token minted for one purpose was valid everywhere. Internal access tokens signed only a timestamp and a TTL, so any valid token verified at every place a token was accepted. The capability scope is now mixed into the keyed hash and is a compile-time constant on both sides, never caller-supplied. The token string format is unchanged.
-
Auth: renaming a user orphaned their recordings. The JWT subject was synthesised from the display name (
web:{username}/api:{username}), so a rename reassigned every recording the old subject owned to a principal that does not exist. Subjects now come from the identity registry, which was already built with persistence, bootstrap and a rename that preserves the id, and was simply never wired into the server. A corrupt registry refuses to start rather than inventing replacement ids. -
Auth: passwords typed at the terminal were left in memory. The interactive password generator left two plaintext
Strings sitting after it returned; they are now wiped, the same discipline already applied to a password arriving over HTTP. A dead duplicate of the credential type that was never declared in its crate root has been removed. -
Notifications: a typo'd webhook burned every retry attempt. Delivery outcomes could not distinguish "retry me" from "this URL is malformed and will fail identically forever", so a permanent failure ran the full exponential backoff before dead-lettering, and a
429was retried straight back into the rate limit it had just hit. -
Notifications: a newly added event kind was silently undiscoverable. Template discovery iterated a hardcoded variant list rather than the event registry, so a new kind's templates were never found — a failure with no error message. It now iterates the registry, and still finds legacy template filenames.
-
Events: several event kinds reached no WebSocket subscriber at all. The wire mapping was a hundred-line match nested three deep inside the socket loop, where a kind reaching no arm looked exactly like a kind deliberately ignored — and the test meant to catch that had been failing since the disk-alert event joined the bus. The mapping is now a pure function with tests that iterate every event kind, so a variant added later fails the tests instead of silently reaching nobody.
-
Events: a stream ending at shutdown lost its last window of transferred bytes. The meter sampler was cancelled in
Drop, which cannot await. The registry is now flushed explicitly at shutdown, after the connection manager, so the final batch reports what the streams actually transferred. -
Events: the bus dropped events under load with nothing to show for it. Capacity was hardcoded at 10 for everything, which a playlist refresh routinely outruns — the evidence was already in the tree, in a dedicated lag arm in the notification bridge and an entire resync recovery path in the WebSocket. Capacity is now configurable and defaults to 256, and drops are counted and reported at
GET /api/v1/events/stats. -
Config: mapper and counter field names were rejected for their casing. The allow-list was compared case-sensitively while the field accessor compared case-insensitively, so a mapper naming
NAMEwas rejected at config load even though writing it would have worked. Both now resolve through the same typed parse. Every previously valid config stays valid; some previously rejected ones are now accepted and behave correctly. -
Config: a config report listed only the first bad rule. Sort rules and target renames aggregate every child's error again, so a config with three bad rules reports all three in one pass rather than one round-trip at a time.
-
Stalker: a
403was retryable or not depending on which layer noticed it. The same refusal arrived as either a token rejection or a bad status, and callers were matching on variants to answer questions the variants were never organised around. Errors now carry a classification, and auth failures are deliberately not retryable so nothing loops on a rejected token. Provider redaction is also unified: the three unrelated answers to "what must never reach a log line" (error URLs, the debug-dump writer's inline key list, and the Xtream sanitizer) are now one module with one key list, and the JSON redaction walk catches nested keys, which the debug-dump writer's own copy never did. -
Web UI: disk-space alerts could not be enabled from Config → Messaging. The messaging form only included the
messaging.disk_alertblock when a threshold field was touched, and the save-time cleanup dropped the block whenever all thresholds still equalled their defaults — even though the block's presence is what enables the alerts. TheDiskAlertentry innotify_onis now the on/off switch: checking it writes the block (default thresholds when untouched), unchecking removes it, and default-valued blocks are no longer stripped on save. -
DVR: cancelling a recording could kill a different one.
cancel_recordingread the active slot, compared the uuid, then called the no-uuidcancel_active(). If ffmpeg finished in between and the queue promoted another recording, that innocent recording was cancelled instead. Now cancels by uuid. -
DVR: a disk-pressure sweep deleted the entire recording library. The stop condition compared a free-space measurement taken once per pass against the low watermark, ignoring the bytes the pass had already reclaimed, so it was constant for the whole pass — false on the first candidate and false forever. A single trigger therefore deleted every completed recording instead of just enough of them. The projected free space now folds in what has been reclaimed.
-
DVR: the recording module did not build on Windows.
utils::recording_pathscarried a blanket#![cfg(unix)], which erased the module and left every caller with unresolved imports. The gate is now scoped to the singleO_NOFOLLOWline it was needed for; the no-clobber and no-follow guarantees are carried bycreate_newandsymlink_metadata, which behave identically on all supported targets. -
DVR:
recording.enabled: falsewas only half-honoured. The REST routes refused requests while the rule scheduler kept materializing tasks and the WebSocket kept streaming recording data. All four gates — routes, scheduler, supervisors, socket — now share one predicate. -
DVR: WebSocket delta filtering dropped tasks under load. The visible-id set was built with
try_lock/try_readon all four queue guards and silently skipped whichever was contended, so recordings vanished from the client until the next full snapshot. It now waits for the same committed boundary the snapshot path uses. -
DVR: filenames were barely sanitized. Only
/and\were replaced, letting control characters, Windows-reserved characters, trailing dots/spaces, and BiDi override codepoints reach the path the muxer opens. Programme titles now pass through a single sanitizer that guarantees one safe path component. -
DVR: duplicate detection was bypassable. The key was
(url, start_at, duration_secs)ORfile_path. Thefile_pathhalf was dead (paths are disambiguated with a_Nsuffix, so they never match) andstart_atisnow.max(scheduled_start), so every request for a currently-airing programme produced a different key and could be booked repeatedly. Identity is now derived from the rule occurrence, or the programme and source per quota pool. -
DVR: a failed rule delete could lose upcoming recordings.
DELETE /rules/{id}?future=cancelcancelled the occurrences first; if the rule store then failed, the rule stayed and its recordings were gone. The cancelled occurrences are now restored from a pre-cancel snapshot. -
DVR:
recording_mut_atcould edit the wrong task. TheFinishedarm returned element 0 rather than the located index. Currently unreachable, but a latent trap for any future caller. -
DVR: a fatal ffmpeg error could loop until the window closed. Retryability was decided by substring-matching the whole stderr line, which includes the source URL, so a provider path containing e.g.
connection-refusedmade every failure look transient. URL-shaped tokens are now stripped before classification. -
DVR: deletion authorization and the state transition could disagree. The task was looked up, authorized, stamped, then looked up a second time, and the second lookup could see a different task. Authorization now runs inside the same mutation boundary that stamps it.
-
DVR: the
owner=task filter accepted arbitrary values for non-administrators. It returned an empty list rather than refusing, which read as if cross-owner queries were supported. Now403unless the caller is an administrator. -
DVR: an ineligible edit reported a misleading reason. Clearing rule provenance surfaced as
recording_invalid_state; it now has its ownrecording_provenance_immutablecode. -
DVR: task status was rendered as Rust debug output. The recording library showed
format!("{:?}", status), untranslated and inconsistent with the downloads view. Both now share one localized status pill, and every recording error code has a translated message in all shipped locales. -
DVR: the socket could not report an actionable refusal. A token predating a permission-schema bump produced an empty task list, indistinguishable from "you have no recordings", while REST correctly answered
recording_token_refresh_required. A newRecordingWsError { code }frame carries the reason. -
DVR:
NewEpisoderules never matched anything. The scheduler passes an empty EPG horizon to the planner, which matches those rules by walking programmes, so onlyWeeklyTimeslotrules could materialize. The horizon is still not wired, but the condition is now logged once per process instead of looking like a scheduler that found nothing. -
Mapper Regex Capture Results:
- Regex expressions now evaluate one complete match instead of flattening later matches into duplicate, unreachable capture keys.
- Every capture group from that match remains accessible by index (
.1through.n), and named groups remain accessible by both index and name. A single unnamed capture also supports.1without losing scalar use.
-
Media servers no longer show a duplicate movie for every extra provider listing (STRM
flatmode): underflat: truethe movie folder is deduplicated by TMDB id, but each file was still named after its own provider title. Providers routinely list the same film twice with the tag written differently (X [MULTI-SUB] - 2021vsX - 2021 [Multi Sub]), so the second listing landed in the first one's folder under a name that does not start with the folder name — exactly what Jellyfin/Emby require in order to group alternate versions. Jellyfin'sVideoListResolverthen abandons version grouping for the whole folder and shows one movie per file. Every listing that reuses a folder is now named after that folder, so the existingadd_quality_to_filenamesuffix (or the[Version id#N]collision suffix) distinguishes them and the media server shows a single movie with selectable versions. Applies to thejellyfin,embyandkodistyles. Note: this renames existing files inflatSTRM trees; withcleanup: truethe old names are removed on the next update. -
The provider category is no longer appended to STRM movie file names in
flatmode: it was added as a collision guard, but the only files that can now collide are versions of the same movie (same TMDB folder, same quality string), which the existing[Version id#N]pass already separates. Jellyfin and Emby render whatever follows the folder name as the version label, so the category leaked into the version picker; the label now reads as the quality alone. Items with no TMDB id still carry the category — it is what keeps their folder unique — and for thejellyfinandembystyles they now carry it in the file name too, so the name still starts with the folder name (it previously did not, which quietly broke version detection for those items).kodiis unchanged here: it has no filename-starts-with-folder-name convention. -
Two STRM versions of the same movie could silently overwrite each other when the name was very long: the
[Version id#N]suffix that tells colliding versions apart was appended last, and the writer then truncates the file stem to 250 characters — so for a long title the only distinguishing part was cut off and both versions resolved to the same path. The shared base is now trimmed instead, so the version label always survives. -
Quality tags no longer demote widescreen films a resolution tier:
MediaQualityclassified the resolution from the frame height alone. A letterboxed 2.40:1 film mastered at 1080p is 1920x796, so it was tagged720p HD; a 2.40:1 UHD master (3840x1600) was tagged1440p QHD. Resolution is now taken from the higher of the width-derived and height-derived tier, so scope films land in the tier they were mastered at. Height alone still decides when the width is unknown. Affectsadd_quality_to_filenameSTRM names. -
STRM files are no longer rewritten on every playlist update: authenticated tokens (STRM
/provider/resolve/…URLs and M3U catchup URLs) used a random IV, so re-encoding the same item produced a different token every run. That made every STRM file's content differ on each update, so the existinghas_strm_file_same_hashskip instrm_repositorynever matched and the whole STRM tree was rewritten every time (measured: ~28k files rewritten by a no-op update). The IV is now derived synthetically (SIV) from the secret, domain and payload, so the same item always encodes to the same token and unchanged STRM files are left alone. The token wire format is unchanged and the IV is still read from the token on decode, so tokens issued by older versions keep working — no STRM regeneration required. -
Playlist Cache Load Failures No Longer Silent: Xtream and M3U storage loads that fail due to corruption, version mismatch, or task panics now log an error before falling back to an empty playlist, instead of silently serving empty data. A genuinely missing storage file (first run) is logged at debug only, so normal startup stays quiet.
-
EPG Output Selection In Mixed Targets: Fixed ambiguous EPG file selection when a target exposes both Xtream and M3U outputs.
- Web UI playlist EPG and stream EPG APIs now explicitly prefer M3U EPG data and fall back to Xtream when M3U EPG is unavailable.
- Xtream short-EPG now explicitly resolves Xtream EPG data.
-
Playlist Series Info For Input/Custom Xtream: Completed
series_infohandling for input-based and custom Xtream playlist requests.- Input and custom Xtream requests now resolve series details via provider
series_idinstead of returning empty (204) responses. - Target-based
series_infobehavior is unchanged.
- Input and custom Xtream requests now resolve series details via provider
-
Async Local File Serving: The local-file stream handler now canonicalizes paths with
tokio::fs::canonicalizeinstead of the blockingstdcall, so the async runtime is no longer blocked while resolving the file path. -
Template Expansion Efficiency: Optimized
template.yml/template.dmulti-template expansion so sequence-style templates no longer duplicate unrelated entries during dependency resolution.- Sequence templates still resolve correctly and preserve order.
- Missing-template and cyclic-dependency validation remains unchanged.
- This reduces config/Web UI load cost for larger nested template collections.
-
Shutdown Diagnostics: Stream-history shutdown now reports dead worker situations instead of silently swallowing them.
-
Release Workflow Safety:
masterreleases now refuse to build non-release versions when the patch component is not0.- The release-version validation now runs before expensive build steps for an early exit.
-
provider:// Scheme: Fixed
provider://URL scheme resolution for failover scenarios. -
Log Level Change: Fixed runtime log level changes not taking effect.
-
API User Category Selection: Fixed API user category selection in the Web UI.
-
Refactored Playlist And EPG Explorer: Playlist Explorer and EPG Explorer have been refactored for improved reliability and UX.
-
HLS session info now reports accurate duration and total transferred data.
-
Removed open-ssl dependency
-
Infinite Fallback Video Behind Reverse Proxy: The 6 fallback custom videos (
channel_unavailable,user_connections_exhausted,provider_connections_exhausted,low_priority_preempted,user_account_expired,panel_api_provisioning) used to be served with an HTTP 200 OK header even when they represented a stream failure. A reverse proxy withproxy_intercept_errors on;therefore could not sever the socket, and the connection would hang open for hours ("ghost connections" / socket exhaustion under scraper load).- The new top-level
custom_stream_response_enabled: falseswitch turns the 6 fallback factories into no-op responses so the call sites return a real HTTP error code (default502 Bad Gateway,custom_stream_response_error_status: <4xx|5xx>) instead of the infinite MPEG-TS loop. - The default
trueis unchanged behaviour: the configured fallback video is still served. - All 6 factories are routed through a single helper (
create_video_stream) so the new behavior is centralized and applies uniformly without per-call-site changes.
- The new top-level
-
Readiness counted disabled providers as free capacity.
/readyand the health banner included disabled inputs and aliases in the capacity calculation — for example accounts that expired and were switched off during config preparation. A fully used setup could therefore still report spare slots through members that cannot accept connections. Readiness now only considers enabled inputs and aliases, matching the provider lineups that actually accept connections. -
/readycould report phantom readiness with unusable capacity. When no enabled input was left, the endpoint answeredinitializingor evenreadyinstead of503 exhausted; an empty enabled-provider slot list is now always treated as exhausted. The group capacity accumulator was also widened fromu16tousize, so groups whose members sum beyond 65 535 connections can no longer overflow into a wrong state. -
Health banner marked groups saturated although a fallback was idle. The banner derived its saturation slots only from providers with active connections, so an enabled alias without connections was invisible and its spare capacity ignored. Slots are now derived from the configured enabled members (missing live counts default to zero), matching the
/readyendpoint. -
Xtream: a VOD document could carry a blank
container_extension, and players appended.nullto the playback URL. The field is filled from the provider'sget_vod_streamsresponse, where a missing or null value collapses to an empty string, and only a per-itemget_vod_infofetch or an ffprobe run fills it in afterwards. A provider that omits it therefore left every VOD document carrying"", and a client building<stream_id>.<container_extension>has nothing to append — several render the blank as the literal stringnulland go on to request813563.null.get_vod_infoalready fell back to the extension carried by the item URL; the four document builders that did not — the stream-list document, both no-properties paths, and the resolved info document, which delegates to aStreamPropertiesmethod with no URL to consult — now share that fallback. A non-empty provider value still wins, and an item whose URL carries no extension either still reports the blank.create_vod_info_from_itemhad the fallback but kept the leading dot thatextract_extension_from_urlreturns, so a URL-derived extension was published as.mkvand would have built813563..mkv; it is stripped now. Series episodes are unchanged: their properties carry no provider URL at that layer.
⚙️ New Settings
-
source.yml (input
resource_policy): Added an optional per-input policy for private resource destinations.allowed_hosts(list of exact DNS names, default empty) andallowed_networks(list of private CIDR ranges, default empty) authorize a private address only together: the host name must match and the resolved address must fall inside one of the networks. An IP literal is authorized byallowed_networksalone. An absent or empty policy means public-only.- Invalid entries (scheme, path, port, wildcard, IP literal in
allowed_hosts; a range outside10.0.0.0/8,172.16.0.0/12,192.168.0.0/16, orfc00::/7) are rejected while the configuration is loaded.
-
Runtime diagnostics (environment variables):
TULIPROX_WATCHDOG(default unset = off) is a mode selector:1(true/on/yes/enabled) observes and logs stalls,2(restart) additionally exits the process after the stall persists so a supervisor restarts it.TULIPROX_WATCHDOG_HEARTBEAT_MS(default1000),TULIPROX_WATCHDOG_STALL_MS(default10000),TULIPROX_WATCHDOG_RELOG_MS(default30000) andTULIPROX_WATCHDOG_RESTART_GRACE_MS(default30000): heartbeat cadence, stall threshold, re-log interval and restart grace.TULIPROX_TOKIO_CONSOLE(default unset): set to1to start the console subscriber in atokio-consolebuild.
-
source.yml (target
options):- Added optional
clear_invalid_epg_ids(bool, defaultfalse) to clear unresolved live-channel EPG IDs after EPG matching and final mappings without removing playlist entries. The legacy namerequired_epgis accepted while reading existing configuration and is rewritten asclear_invalid_epg_idswhen serialized.
- Added optional
-
config.yml (
video.download.recording):- Added
enabled(bool, defaulttrue): master switch for the DVR. Whenfalsethe REST routes answer501 recording_disabled, the rule scheduler and supervisors idle, the WebSocket serves no recording data, and the sidebar entries are hidden. An absentrecording:block still means "defaults", so upgrading never silently disables a DVR that was already in use. - Added
container_format(mpegts|matroska|mp4, defaultmpegts): the muxer ffmpeg writes. Recordings were previously hard-coded to MPEG-TS regardless of the source codecs. MPEG-TS remains the default because it survives truncation — a recording killed mid-stream still plays. - Added
retention.sweep_interval_secs(u64, default3600): cadence of the age/count retention sweep, independent ofdisk.cleanup_interval_secs, which paces the watermark check. - Added a
notificationsblock governing the new lifecycle-notification outbox:outbox_buffer(default1024, fixed at startup),max_attempts(default6),backoff_initial_secs(default5), andbackoff_max_secs(default900). - Startup now warns when the DVR is enabled with no retention policy, no disk watermarks, and no quota, since nothing then bounds recording disk usage.
- Added
-
source.yml (target
options.epg_output):- Added optional
lowercase_ids(bool, defaultfalse) to canonicalize technical EPG IDs with ASCII lowercase consistently across visible M3Utvg-id, Xtreamepg_channel_id, XMLTV<channel id>/<programme channel>references, EPG API responses, and target EPG storage keys after a full target refresh. Disabled targets retain their existing source-case storage keys and ordering. - Added optional
lowercase_xmltv_display_names(bool, defaultfalse) to lowercase only XMLTV<display-name>values during serialization; playlist names and programme metadata remain unchanged, and no persisted rebuild is normally required. - Both options are disabled by default, so existing visible outputs remain unchanged. Changing
lowercase_idsrequires a full target refresh, and clients may need to re-index EPG data once after visible IDs change.
- Added optional
-
config.yml (main):
- Added
interner_gc_interval_secs: interval in seconds between background string interner GC checks. - Added
interner_gc_min_pool_size: minimum interned-string pool size required before background interner GC runs. - Added
custom_stream_response_enabled(bool, defaulttrue): whenfalse, the 6 fallback custom-video factories (channel_unavailable,user_connections_exhausted,provider_connections_exhausted,low_priority_preempted,user_account_expired,panel_api_provisioning) skip the configured MPEG-TS video and the call sites returncustom_stream_response_error_statusinstead of an infinite 200 OK loop. Use this behind a reverse proxy withproxy_intercept_errors on;to allow dead channels to be severed instead of pinning sockets open. The field lives in the main config (next tocustom_stream_response_path/custom_stream_response_timeout_secs) rather than underreverse_proxy.streambecause it is a custom-stream-response behaviour toggle, not a reverse-proxy behaviour setting. - Added
custom_stream_response_error_status(u16, default502): HTTP status code returned whencustom_stream_response_enabledisfalse. Must be a 4xx or 5xx code (ConfigDto::prepare()rejects anything else;0is silently clamped to the default502). Operators can match the code to their Nginxproxy_intercept_errors on;rules. - Added
event_channel_capacity(u32, default256, clamped to at least1): capacity of the internal event broadcast channel. It was previously hardcoded at10, which a playlist refresh routinely outruns — a subscriber that awaits I/O per event falls behind within one target. Drops are visible atGET /api/v1/events/stats.
- Added
-
config.yml (
reverse_proxy.stream):- Added
admission_strategies(optional list): ordered list of admission strategy rules. Available strategies:evict_user_same_ip_oldest,evict_user_same_ip_latest,evict_user_oldest,evict_user_latest,
grace_instant_stream,grace_hold_stream.
- Added
-
config.yml (
messaging):notify_onis now a list of glob patterns over dotted event ids (*,recording.*,provider.*.expired, and a leading!to exclude). LegacyMsgKindnames still parse and are normalized on the next save.- Added an
ntfychannel:url,topic, optionaltoken, optionaltemplates, optionalrouting. - Added a
gotifychannel:url,token, optionaltemplates, optionalrouting. - Added a
slackchannel:url(incoming webhook), optionaltemplates, optionalrouting. - Added a
commandchannel:program, optionalargs, optionaltimeout_secs, optionaltemplates, optionalrouting. The program is executed directly, not through a shell, and receives the event JSON on stdin. - Added
rest.signing_secret(optional): enables HMAC-SHA256 signing of{timestamp}.{body}, sent asX-Tuliprox-Signature. - Added an optional per-channel
routingblock on all eight channels. An absent block inherits the global subscription:notify_on(list of glob patterns): overrides the global subscription for this channel.min_severity(info|warn|error|critical): drops anything below it.quiet_hours(HH:MM-HH:MM, local time): notifications inside the window are deferred by the outbox, never dropped. An entry is only held while every still-pending channel is asleep.max_per_hour(u32): circuit breaker. On reaching it the channel sends one "suppressing further notifications" message and then goes quiet for the rest of the hour.dedup_window_secs(u64): suppresses a repeateddedup_keyfor this many seconds. Generalizes the disk alert'srepeat_interval_secs, which was previously available to nothing else.
- Templates are now supported on every channel including Pushover, keyed by event id wire name, and every template
receives a uniform
event.*context alongside every legacy top-level key, so templates written against the documented examples render identically.
-
config.yml (
messaging.disk_alert):- Added optional
disk_alertblock to enable disk-usage alerts via the existing messaging channels. The background monitor inbackend/app/src/api/sys_usage.rssamples the current working directory's mount on every fixed 2-second tick and feeds each sample to theDiskAlertMonitorstate machine (backend/app/src/api/sys_usage.rs::DiskAlertMonitor). - Fields:
warn_percent(f64, default80.0): percent-used at or above which theWarnlevel is reached. Must be in[0, 100].critical_percent(f64, default95.0): percent-used at or above which theCriticallevel is reached. Must be> warn_percentand in[0, 100]. Theprepare()step rejects values that violate these bounds.repeat_interval_secs(u64, default3600): re-arm interval in seconds. While the disk stays in the same alert state, the alert is re-sent after this many seconds. This is not the sampling interval — sampling is a fixed 2s and is not currently configurable. So if the disk is at 87% for 3 hours with the defaultrepeat_interval_secs: 3600, threeWarnnotifications are sent (one per hour), not one transition-only notification.
- The state machine emits a
DiskAlertwhenever the level is non-Normal and (state_changedorrearm_elapsed); the level-transition-only behaviour of the original prototype was intentionally removed because long-running full-disk situations were going unnoticed. - Templates can override the default text per channel via
messaging.<channel>.templates.disk_alert_warn/disk_alert_critical/disk_alert_normal.
- Added optional
-
api-proxy.yml (
user.credentials[]):- Added
output_clusters(optional list, default effective behaviorall): restricts a user tolive,vod, and/orserieson the assigned target. If no cluster is selected, the filter is inactive and all clusters are served.
- Added
-
config.yml (
web_ui):- Added
landing_page(optional, defaultdashboard): initial view after login. - Added optional
stream_infoblock to hide specific fields in the active stream display:hide_grouphide_iphide_countryhide_sharedhide_durationhide_bandwidthhide_transferredhide_playerhide_user_commenthide_epg
- Added
-
config.yml (
log):- Added
runtime_config_report_enabled(bool, defaultfalse): enables full runtime config dump at startup. - Added
runtime_config_report_format(yaml|json, defaultyaml): output format for the runtime config report.
- Added
-
source.yml (
providers):- Added
provider_url_selection_policy(resume_last_working|restart_from_first, defaultresume_last_working): controls URL selection behavior after provider failover.
- Added
-
config.yml (
reverse_proxy):- Added
qos_aggregation(optional) with:enabled(bool)interval_secs(u64)
- Added
-
config.yml (
reverse_proxy.geoip):- Added
unavailable_policy(deny|allow, defaultdeny). denykeeps country-basednetwork_accessrestrictions closed when GeoIP is disabled, missing, or not loaded.allowis an explicit risk acceptance that allows country-basednetwork_accessrestrictions only when GeoIP is unavailable. CIDR-only misses, unknown countries, and country mismatches still deny.
- Added
-
api-proxy.yml (
user.credentials[].network_access):- Added optional per-user network restrictions:
allowed_networks: CIDR ranges such as192.168.0.0/16or10.0.0.1/32.allowed_countries: ISO-style country codes resolved through GeoIP.
- The rules use OR semantics: any matching CIDR or country allows the request.
- Added optional per-user network restrictions:
🛠 Maintenance
-
Playlist curation now has a dedicated capability boundary: matching and ordered membership evaluation live in the source-neutral
tuliprox-curationkernel, while Trakt HTTP/JSON handling translates records at the edge and the category-scoped compatibility projector remains separate from membership identity. Existing category identity and matching rules remain unchanged; the target-wide selection entry above documents the intentional outcome changes. -
AdmissionRequestbundles the request-scoped admission arguments: five functions each threaded the same ten positional parameters, three of them consecutive barebools (use_session_admission, thenactivate_unbound_sessiona slot later). Call sites read..., true, Some(session_token), true, guard)— a shape where transposing two arguments still compiles and silently changes which admission check runs. One struct now names every field at the call site, and the comment that lived in the parameter list moved onto the field it documents. This removes three#[allow(clippy::too_many_arguments)]and oneclippy::too_many_lines. -
The empty-admission-strategy-list rule is stated rather than implied: the resolver matched on
admission_strategies.is_some()and then re-unwrapped withunwrap_or_default(), so the guard proved something the body checked again — and the rule that an explicitly empty list suppresses thegrace_period_millisfallback while an absent list does not was implicit in the arm ordering. Rewritten as a match onas_ref()with the distinction spelled out.Some(vec![])still means "no strategies", not "fall back to grace". -
Dead admission parameter and a doc block that described a decision that does not exist: the strategy loop took a
kind_for_exhaustedit never read (both callers construct the exhausted result themselves), and the doc block on the post-grace fallback listed aDenyrule althoughAdmissionDecisiononly hasNoMatch,GraceandEvict. -
The documented event table is complete again: eight events registered by the user-lifecycle and auth work —
user.created,user.updated,user.deleted, the fourauth.*decisions andstream.probe.failed— were never given a row, soevery_registered_event_appears_in_the_docs_tablehad been failing since before those events landed. The rows are generated from the descriptors, so severity and description match the registry exactly. A run of twenty-seven spaces left inside thestream.probe.faileddescription by a collapsed wrapped literal is normalised in the registry and the table together. -
The notification bridge stays a routing table: fourteen new events left
to_notificationdoing its own wording inline, at 213 lines. Each event's wording moved into a*_notificationbuilder beside the three that already existed, so the match is one arm per variant with no logic in it, and the "... N more not listed" wording has one home shared by both events that carry sampled lists rather than a copy in each. -
EventBusStats::Defaultis hand-written: the taxonomy crossed 32 kinds, and the derivedDefaultfor arrays stops there. -
Moved provider-specific M3U, Xtream and Stalker protocol code from the generic utility namespace into dedicated IPTV modules.
-
B+Tree v3 persistence engine:
- Consolidated the facade, v2 compatibility reader, v3 engine, migration, WAL, sorted index, and stress tests under
backend/btree/src/. - Added checksummed 4 KiB Slotted Pages, WAL-before-data in-place updates, verified atomic full replacement, typed v1/v2 startup migration, identity-bound sorted indexes, and corruption-reporting iterators.
- Reused mmap mappings, page validation, and decoded internal routes across cheap
BPlusTreeQueryclones while retaining request-local scratch buffers. - Kept stored values up to 512 bytes inline, avoiding one mostly empty 4 KiB overflow page per typical Xtream/M3U playlist entry and restoring compact full-scan behavior.
- Playlist APIs now coalesce small M3U, Xtream, HDHomeRun, XMLTV, JSON, and CBOR fragments into bounded 64 KiB response chunks instead of emitting one HTTP body frame per entry.
- Corrupt individual B+Tree values are logged and skipped when the iterator can safely continue; database-open and worker failures remain visible, and failed input-cache opens no longer replace existing Xtream persistence.
- Classified B+Tree read failures as repository errors
- Opening a corrupt existing Library database no longer silently produces an empty Library.
- Consolidated the facade, v2 compatibility reader, v3 engine, migration, WAL, sorted index, and stress tests under
-
Shared
FieldWrapperFor Form Inputs:- Extracted the repeated label / field-id /
tp__input-wrapperscaffolding from theInput,NumberInput, andTextAreaprimitives into a single sharedFieldWrappercomponent, reducing duplication while keeping the rendered markup and behavior unchanged.
- Extracted the repeated label / field-id /
-
Units and identity in the type system: several classes of value that were bare integers or strings now carry their meaning in their type, with no change to the serialized form and no config migration:
Millis/Secsfor HLS timing config, applied through the DTO → runtime hop rather than unwrapped at the boundary, so a millisecond value can no longer reach a seconds parameter — the two sat two lines apart in the same struct as bareu64s, andcache_durationandsession_idle_timeoutdid not carry their unit in their names at all.Bytesfor resolved byte sizes, so a parsed size is distinguishable from any otheru64and a runtime struct can no longer hold an unparsed size by accident.VirtualIdandProviderIdas real newtypes. The same store is keyed by a virtual id on the target path and a provider id on the input path, and nothing stopped a lookup in one key space using an id from the other. There is deliberately no implicit conversion in either direction. On-disk compatibility is pinned by a B+Tree codec test asserting a transparent newtype overu32encodes byte-for-byte identically and cross-reads in both directions, so existing databases are unaffected and there is no migration.- The Xtream store's key space is now a type parameter rather than a runtime tag matched per item inside the insert loop, so the two key spaces can no longer be swapped by passing the wrong enum variant.
-
One shape for config preparation and error reporting:
- A
Preparetrait replaces ~98 inherentprepare/validatemethods that had no agreed signature — some took nothing, some pattern templates, some a storage dir, a port or a boolean, returning four different result types. Because the shape was invisible, the recursive walk was hand-written at every level and a config struct that forgot to call its children failed silently at runtime rather than at compile time. Dispatch stays entirely static. TuliproxErrorsplits into aCopykind and a message. All 50 variants carried exactly one string, so it was a category tag beside a message encoded as an enum — costing a 50-arm accessor and a second 50-name list that had to be kept in sync by hand, and making the category impossible to compare, store or return on its own. The 755 construction sites are untouched.- A
Clockseam replaces 14 character-for-character copies ofcurrent_time_millisacross two crates. It is meant to be held as a generic parameter defaulted to a zero-sized type, never as a trait object; a test asserts owning one leaves a struct's layout unchanged.
- A
-
Single-sourced relations that were written down more than once:
- The item-type-to-cluster relation existed in three places with nothing keeping them in agreement, and its conversion
was total while returning a
Result— a phantom error that had spread defensive fallbacks to 17 call sites across four crates. All 17 drop their fallback. - Genre access was a four-arm match written out five times; it is now two methods.
- The mapper and counter field allow-lists are typed rather than string lists, which also collapsed a duplicated EPG channel-id entry that existed only to cover both accepted spellings.
- Three macros generating by-name field accessors are gone. One of them — an ~80-line prefix-matched lookup for Xtream cover and backdrop resources — turned out to have no callers at all and was deleted rather than ported.
- The item-type-to-cluster relation existed in three places with nothing keeping them in agreement, and its conversion
was total while returning a
-
Provider fetches have one shape: the three provider families were modelled three different ways and each returned a differently-shaped tuple, so the dispatcher was a ninety-line match whose eight arms hand-assembled a six-element tuple, padding fields their provider does not produce with literal zeros and then destructuring by position — two of the six elements were dead on arrival. One trait with one named result type replaces it, dispatch stays a statically dispatched match, and the two unsupported input types now carry their reason.
-
Stalker client seams and test coverage: the Stalker API client owned its HTTP client and read the system clock directly, which put every interesting decision it makes behind a live portal — the module docs conceded outright that no HTTP requests are issued from unit tests. Both are now type parameters defaulted to the production implementation, neither introducing a vtable or an allocation. Nine tests now cover paths that previously had no way to be reached at all, including a portal refusal hidden inside a
200 OK, an over-cap body being refused rather than buffered, endpoint-candidate failover in priority order, and a session ageing past its TTL on a clock that can be advanced. Expiry rules — session staleness, cookieMax-Age, and the Xtream account-expiry warning — now take the instant as a parameter rather than reading the clock, so the cookie boundary is asserted at the exact second it flips and the three-day expiry window has tests for all three branches instead of none. -
Stalker page arithmetic has one home: the rule for "is this the last catalog page" was written out four times and two copies had already drifted in how they measure progress against the advertised total. The two per-row-type page parsers collapse into one generic walk as well; they differed only in the row type and both hand-rolled the same four envelope shapes.
-
exec_processingtakes a run object: it had twelve positional parameters, seven of themOption, so a call site was a wall ofNones where the reader had to count commas and the compiler could not catch two same-typed arguments being swapped. The CLI path is now a single constructor call. -
Workspace dependency edges reduced from 78 to 75: the DVR crate no longer depends on the streaming-session runtime (the event bus was the only thing it wanted, and a trait bound is not a dependency), and neither the IPTV nor the processing crate depends on the messaging crate any more — emitting an event is not knowing how it is delivered.
3.3.0 (2026-04-02)
⚠️ Breaking Changes 3.3.0
-
working_dirinconfig.ymlrenamed tostorage_dir. -
Global Input Definitions: To align input definitions with the SourceEditor, inputs are now defined globally in the
inputssection of the config file. Each source can reference one or more inputs by their name in theinputsattribute. -
Data Format Migration: Due to heavy refactoring, the old data format is invalid. You need to clean your
datafolder and update the playlists. -
B+Tree Storage Format: Storage format has changed to a more efficient Slotted Page architecture.
- Index optimization: Added index to B+Tree to accelerate queries without tree traversal.
- TargetIdMapping Optimization: Refactored to use disk-based B+Tree operations, eliminating startup latency.
- B+Tree Header Metadata: Implemented efficient
BPlusTreeMetadataEnum to persistVirtualIdcounter directly in the database header. - Fast Initialization:
TargetIdMappingnow conditionally loads the tree, achieving near-instant startup for established databases.
-
Configuration Renames:
-
threadsattribute inconfig.ymlrenamed toprocess_parallel(boolean). -
Added mandatory
rewrite_secrettoreverse_proxyconfig for stable resource URLs. -
Removed
forced_retry_interval_secs. -
FFprobe settings moved from
video.*tometadata_update.ffprobe.*. -
metadata_update.ffprobe.analyze_durationandmetadata_update.ffprobe.live_analyze_durationnow require explicit unit suffixes (s|m|h|d). -
library.metadata.pathmoved tometadata_update.cache_path(defaultmetadata). The TMDB cache is now shared across all metadata resolution paths (Xtream VOD/Series and local library). Removepathfromlibrary.metadatain yourconfig.ymland set it undermetadata_updateinstead:# Before library: metadata: path: /data/library_metadata fallback_to_filename: true# After metadata_update: cache_path: /data/library_metadata # moved here library: metadata: fallback_to_filename: true
-
-
Input Batch URL Scheme: Batch input URLs now use the
batch://scheme instead offile://.file://is no longer accepted for batch CSV definitions. Update yoursource.yml:# Before inputs: - type: xtream_batch url: 'file:///home/tuliprox/config/batch.csv'# After inputs: - type: xtream_batch url: 'batch:///home/tuliprox/config/batch.csv'Local paths without a scheme (
/path/file.csv,./file.csv) continue to work. Thebatch://scheme clearly distinguishes batch alias files from providerfile://URLs. -
DNS Resolved Persistence:
dns.resolvedhas been removed fromsource.ymland theProviderDnsDto. Resolved IPs are now persisted separately in{storage_dir}/provider_dns_resolved.json. This eliminates hot-reload interference caused by DNS refresh cycles writing tosource.yml. DNS caches are automatically carried over during config hot-reloads. -
Input Batch Changes:
nameattribute is now mandatory for input type batch to ensure stable playlist UUIDs. -
Favorites Redesign: Replaced implicit
create_aliaswith explicitadd_favourite(group_name)script function.-
EpgSmartMatch: Field
name_prefixsyntax needs to be changed fromname_prefix: !suffix "."toname_prefix: { suffix: "." }. -
Sort: Sort can now use filter to sort specific entries.
sort: match_as_ascii: true rules: - target: group field: group filter: Input ~ "provider_1" order: asc - target: channel field: caption filter: Group ~ "!US_TNT_ENTERTAIN!" order: asc sequence: - "!CHAN_SEQ!" - '(?i)\bHD\b' - '(?i)\bSD\b' -
Trakt api config field
keyis nowapi_key. Addeduser_agentfield to Trakt api config -
resolve_vod_delay and resolve_series_delay are now merged as resolve_delay, added
probe_liveandprobe_live_interval_hoursfor live stream probing.# Before (deprecated) output: - type: xtream resolve_vod: true resolve_vod_delay: 500 resolve_series: true resolve_series_delay: 2# After (new consolidated) output: - type: xtream resolve_vod: true resolve_series: true resolve_delay: 2 # Single delay for all resolution types
-
🌟 New Features 3.3.0
- Role-Based Access Control (RBAC): Replaced the binary admin/non-admin model with fine-grained, group-based permissions.
- 14 permissions across 7 domains (
config,source,user,playlist,library,system,epg), each with independent.readand.writegrants. - Group management via
groups.txt— define custom roles (e.g.,viewer,source_manager) with specific permission sets. - Extended
user.txtformat — users can now be assigned to one or more groups (username:hash:group1,group2). Missing group field defaults toadminfor backward compatibility. - Compact JWT encoding — permissions are resolved at login and stored as a
u16bitmask in JWT claims. Backend middleware checks permissions via single-instruction bitwise tests. - Password-version tracking —
pwd_versionin JWT enables automatic token invalidation when a user's password changes. - Backend permission middleware — per-route
require_permission()guards replace the old blanket admin check. The backend is the security boundary. - RBAC management API — CRUD endpoints for web UI users and groups (
/api/v1/rbac/users,/api/v1/rbac/groups,/api/v1/rbac/permissions). - Frontend permission gating — UI elements (buttons, menu items, views) are cosmetically hidden based on the user's resolved permissions.
- RBAC admin panel — new Web UI page with tabbed user/group management, permission checkbox grid, and write-without-read warnings.
- No-access page — users with zero permissions see a friendly "no access" screen instead of an empty dashboard.
- Built-in
admingroup — reserved, always grants all permissions (*), cannot be deleted or modified.
- 14 permissions across 7 domains (
- User Connection Priority: API users now carry a
priorityfield (typei8, nice-style: lower value = higher priority, default0, probe127). When all provider slots are occupied and a higher-priority user connects, the lowest-priority active connection on that provider is evicted (oldest first when tied). Only connections with exactly one active listener are eligible for eviction; shared connections with multiple listeners are not interrupted. Equal priority never evicts equal priority — the new connection is rejected normally (with grace-period rules applied as before). Usermax_connectionslimits are unaffected. - Configurable Probe Priority: Stream-probe tasks (
probe_live,probe_vod,probe_series) now run with a configurable priority instead of a fixed internal constant. Setmetadata_update.probe.user_priority(default127, i.e. lowest priority) to control how aggressively active users can preempt probe connections. - User DB Schema Migration V3: The
api_user.dbfile is automatically upgraded to V3 format (addspriorityfield) on first startup. A.userdb_mergeto_v3guard file is created so config-driven user merges are skipped while the DB is the authoritative source. - Background Metadata Queue: Metadata resolution (VOD/Series) and stream analysis are now queued per input and processed in the background when provider connections are idle. This prevents "No Connections" errors for active users during playlist updates.
- Stream Probing: Added support for probing streams (
probe_live|vod|series) to determine codecs and resolution. This runs as a low-priority background task. - Discord Notifications: Support for Discord notifications via webhooks with optional Handlebars templates.
- Enhanced REST Messaging: Support for custom HTTP methods, headers, and Handlebars templating.
- Local Library Module: Comprehensive local video file scanning and metadata management.
- Recursive scanning, automatic classification, and NFO/TMDB metadata resolution.
- Incremental scanning and virtual ID management.
- Panel API Integration: Optional integration to renew expired input accounts or provision new accounts to ensure a minimum valid input accounts.
- Playlist Caching: Added
cache_durationto inputs, allowing configurable provider playlist cache times during subsequent updates (e.g.,60s,5m12h,1d). - Staged Cluster Source Routing: Added per-cluster staged routing for Xtream inputs.
You can now decide cluster-wise whether
live/vod/seriesis loaded from staged input, main input, or skipped. Skip flags (xtream_skip_live|vod|series) remain highest priority and always force skip. - Database Viewer: New CLI flags
--dbxand--dbmto inspect internal database content. - Home Directory Override: Added
--home(-H) CLI argument to set the base directory for config, data, backup, and downloads. - Added
disk_based_processing: (boolean, defaultfalse) toconfig.yml. When enabled, input playlists are processed from disk instead of memory. - User-Agent
default_user_agent: Ensures that outgoing requests always pass a default user agent. - FFprobe Integration: Added capability to probe streams for codec, resolution, HDR (HDR10/HLG/DV), and audio channels using
ffprobe. Probing strictly respects provider connection limits. If no slot is available (considering user limits), the item is skipped to prevent provider bans. - Metadata Fallback: Automatically fetches missing TMDB IDs and release dates via the TMDB API if the provider data is incomplete.
- Streaming: Added
grace_period_hold_streamconfiguration option to delay stream output until grace period connection checks are completed. - Provider Failover & Rotation: Tuliprox supports robust failover mechanisms for streaming providers.
You can use the special
provider://<provider_name>/...URL scheme in your configurations. Tuliprox will automatically resolve this to the current active URL of the specified provider. If the current URL fails (e.g., 5xx error, timeout), Tuliprox automatically rotates to the next available URL for that provider. It tracks failures and prevents infinite loops by limiting attempts to the number of available URLs. - Added
epg_request_timeshift: [-+]hh:mm or TimeZone, exampleEurope/Paris,America/New_York,-2:30(-2h30m),+0:15(15m),2(2h),:30(30m),:3(3m) - Extended scheduler to support
Local Libraryscans. Scheduler can now trigger automatic library scans alongside playlist updates. - Centralized Pattern Templates: Added a global template collection that is loaded from
config.yml -> template_path(file or directory) and shared across sources and mappings. - Template Backward Compatibility: Existing inline templates in
source.ymlandmapping.ymlare still loaded and merged during read/validation. - Template-Aware Hot Reload: File watcher now tracks template files/directories and reapplies sources/mappings when templates change.
- Setup Validation Improvement: Setup mode validates source configuration against the global template collection and persists template definitions separately.
- Added
-T, --templateto overridetemplate_pathon startup. - Metadata Update Runtime Config: Metadata worker intervals, retry/backoff limits, queue sizing, and probe cooldowns are now configurable through
a dedicated
metadata_updateconfig block. - Unified Metadata Retry State: Replaced probe-only retry persistence with a single
metadata_retry_state.dbper input. A single record per item now stores retry/cooldown state forresolve,probe, andtmdb. - TMDB No-Match Cooldown: Added explicit TMDB cooldown handling. When TMDB resolve completes successfully but returns no match, TMDB reasons are suppressed for that item during cooldown to prevent endless requeue loops.
- HLS/Catchup Provider Reservations: Added short-lived provider-account reservations for HLS and catchup playback so follow-up requests can stay
on the same provider account without holding a real provider slot open between requests. New config fields:
reverse_proxy.stream.hls_session_ttl_secs(default15) andreverse_proxy.stream.catchup_session_ttl_secs(default45). - Channel Switch Friendly Reservations: HLS/catchup reservations can now be taken over immediately by a new stream from the same client identity, so channel switching does not have to wait for the reservation TTL to expire.
- Custom Stream Response Timeout: Added support to limit how long custom fallback stream responses are served.
Set
config.custom_stream_response_timeout_secsto a value> 0to auto-stop these streams after N seconds. If unset or0, custom responses are streamed without timeout. - Added
reverse_proxy.stream.metrics_enabledto enable per-stream bandwidth and transferred-bytes metrics in the Web UI streams view.
🐛 Fixes 3.3.0
- Resolve Task Cooldown Persistence: Resolve retry exhaustion is now persisted and consulted before enqueueing, so unresolved VOD/Series entries are no longer recreated on every playlist refresh only to be skipped later in the worker.
- Probe Handle Capacity Leak: Fixed provider-slot leaks when internal probe tasks timed out, were dropped, or were preempted. Capacity is now released reliably even when the underlying probe task does not complete normally.
- Immediate Probe Preemption: Higher-priority stream requests now cancel lower-priority probe tasks immediately instead of leaving a grace window where the probe could continue holding upstream resources.
- Anonymous Socket Cleanup: Tracked anonymous incoming sockets are now pruned automatically after a TTL so stale UI/API keepalive registrations do not remain visible forever in active-socket statistics.
⚙️ New Settings 3.3.0
- config.yml (
web_ui.auth):- Added
groupfile(optional, default:groups.txtin same directory asuserfile): path to the RBAC group definitions file.
- Added
user.txt(extended format, backward compatible):- Format is now
username:argon2_hash[:group1,group2,...]. The optional third field assigns group memberships. Missing third field defaults to theadmingroup for full backward compatibility.
- Format is now
groups.txt(new file):- Defines permission groups in
group_name:permission1,permission2,...format. Theadmingroup is built-in and cannot be defined here. See the configuration docs for the full permission list.
- Defines permission groups in
- api-proxy.yml / Web UI (user):
- Added
priority(i8, default0) to user credentials. Lower value = higher priority (nice-style). Configurable via Web UI user editor. Negative values are valid and represent higher-than-default priority.
- Added
- config.yml:
- Added
custom_stream_response_timeout_secs(u32, default0): maximum duration in seconds for custom stream response videos.0disables the timeout and keeps existing behavior. - Added
metadata_update.probe.user_priority(i8, default127): priority assigned to probe connections. Probe tasks run at the lowest priority by default; reduce this value to give probes more connection access. - Added
metadata_update(optional) with grouped sections:log,resolve,probe,ffprobe,tmdb. - Added
metadata_update.cache_path(defaultmetadata): shared storage directory for TMDB cache and metadata files (moved fromlibrary.metadata.path). - Added
metadata_update.no_change_cache_ttl_secs(default3600): TTL in seconds for the no-change deduplication cache used by background metadata resolve tasks. - Added
metadata_update.tmdb.cooldown(default7d) for successful TMDB no-match cooldown behavior. - Added
metadata_update.ffprobe.enabled(default: false),metadata_update.ffprobe.timeout, and ffprobe probe/analyze size settings. metadata_update.ffprobe.analyze_durationandmetadata_update.ffprobe.live_analyze_durationrequire explicit unit suffixes (s|m|h|d).- FFprobe settings are configured under
metadata_update.ffprobe(not undervideo). - Added
metadata_update.probe_fairness_resolve_burst(default200) to control fairness between resolve and probe tasks. After N consecutive resolve-domain tasks, one pending probe-domain task is prioritized to avoid probe starvation. - Added
reverse_proxy.stream.hls_session_ttl_secs(u64, default15): keeps a short-lived provider-account reservation for HLS sessions. - Added
reverse_proxy.stream.catchup_session_ttl_secs(u64, default45): keeps a short-lived provider-account reservation for catchup sessions and seek/reconnect flows. - Added
template_path(optional): path to a template file (template.yml) or directory (template.dstyle).
- Added
- source.yml (input options):
- Added
resolve_tmdb: Triggers TMDB lookup if ID is missing. - Added
probe_stream: Triggers ffprobe if technical info is missing. - Added
probe_delay: Delay between probe tasks (default50seconds). - Added
staged.enabled: Disables/enables the staged input. - Added
staged.live_source: Selects source for Live cluster (staged|input|skip). - Added
staged.vod_source: Selects source for VOD cluster (staged|input|skip). - Added
staged.series_source: Selects source for Series cluster (staged|input|skip). - Added staged validation rules:
- Cluster source rules apply only when
staged.enabled=true. - For Xtream main inputs with staged enabled, at least one cluster source must be
staged. - For staged type
m3u,vod_source=stagedandseries_source=stagedare rejected.
- Cluster source rules apply only when
- Added
- source.yml (target output):
- Added
probe_live: Enables background probing for Live TV streams (default disabled). - Added
probe_live_interval_hours: Sets the frequency for re-probing Live TV streams. - Added
resolve_background: Toggles background metadata resolution (defaulttrue). Set tofalsefor blocking, immediate resolution.
- Added
🛠 Optimizations 3.3.0
- Quality Tagging: Generates enhanced filename tags (e.g.,
[2160p 4K HEVC HDR TrueHD 7.1]) for STRM files based on analysis results. - Flat Grouping: When
flat: trueoption for STRM output is active, multiple versions (e.g., 4K and 1080p) of the same movie are now safely merged over all categories into a single folder based on TMDB ID, compatible with Jellyfin/Emby "Multi-Version" features.
⚙️ Engine & Storage Optimizations 3.3.0
- Slotted Page Architecture: Improved space utilization and support for variable-length keys.
- Adaptive LZ4 Compression: Optimized disk footprint for stored values.
- Atomic I/O Layer: Refactored for atomic writes and file locking, ensuring data integrity.
- B+Tree Compaction: Reclaim space after deletions or mass updates.
- Batch Upsert: Significantly higher throughput during mass inserts/updates.
- Persistent Value Caching: Implemented high-performance, thread-safe value caching
- Compressed Read Optimization: Caches decompressed values in memory to eliminate redundant decompression overhead during frequent queries.
- Packed Block Update Optimization: Caches exact byte offsets within 4KB blocks, enabling direct disk writes for same-size updates and bypassing expensive Read-Scan-Modify-Write cycles.
- Buffer Reuse: Introduced reusable serialization buffers in
BPlusTreeUpdateto minimize heap allocations during write operations. - Configurable Flush Policy: Added
Immediate,Batch, andNoneflush policies to optimize disk synchronization overhead. - Disk-Based Provider Processing: New
disk_based_processingconfig option massively reduces RAM usage by streaming playlist data from disk (BPlusTree) during updates. - String Interning: Implemented
Arc<str>string interning for playlist items to further reduce memory footprint. - Zero-Copy B+Tree Scan: Implemented zero-copy scanning for B+Tree internal nodes, significantly reducing heap allocations and improving random read throughput (up to 96k ops/sec).
- Optimized Key Lookups:
XtreamRepositoryandM3uRepositorynow use zero-copy queries foru32keys, enhancing performance for high-traffic endpoints.
🔍 Mapping & Filtering Enhancements 3.3.0
- Accent-Independent Matching: Integrated
match_as_asciiflag for robust text matching (e.g., "Cinema" matches "Cinéma"). - Deunicoding Support:
ValueProviderandValueAccessornow support on-the-fly deunicoding. - Flexible Sorting: Added
order: nonesupport to retain source order in mappings. - Mapper Loop enhancement: Updated
for_eachsyntax tovariable.for_each((key, value) => { ... }). Added support for_ignored variables in loop.
💻 WebUI & API 3.3.0
- Source Editor Integration: Redesigned UI for global input management and hot-reloading.
- Messaging Config View: New UI for configuring Discord and enhanced REST settings.
- Performance Monitoring: Added CPU usage display to the dashboard.
- Stream Table Enhancements: Added "Copy-To-Clipboard" functions and improved connection monitoring.
- Streams Table Episode Title: Stream rows now prefer explicit episode titles instead of falling back to the series name.
- UX Improvements: Implemented API-user category selection and better session tracking for HLS.
- Filter View: Compacted pretty printing for filters.
- Mapper View: Updated to support new
for_eachsyntax. - Added Stream Buffer settings (Enabled, Size) to Reverse Proxy configuration UI.
- Added TMDB settings (Rate Limit, Cache Duration, Language) and Metadata Formats (NFO support) to Library configuration UI.
- Introduced the Metadata Update config tab, with FFprobe controls relocated from Video into it.
- Playlist Explorer Resources: Channel logos (and non-local episode images) are loaded via authenticated same-origin resource endpoints so HTTP upstream assets still render behind HTTPS frontends.
- Local Library Episode Backgrounds: Local series episode
movie_imagevalues are now kept as direct TMDB image URLs in series info documents and rendered directly in Playlist Explorer.
🚀 Performance & Stability 3.3.0
- Deadlock Resolution: Fixed a potential deadlock in
ProviderLineupManager::reconcile_connectionsby refactoringDashMapiterations to use snapshots, preventing internal shard locks from being held during async lock acquisition. - Connection Reconciliation & GC: Resolved a critical issue where provider connection counters could leak or become stale during hot reloads. Added automatic garbage collection for unused provider records to prevent logical memory buildup.
- Full Async Runtime: Transitioned to
#[tokio::main]and async I/O throughout the entire application. - Non-Blocking Operations: Cache persistence, playlist exports, and config saves moved to async tasks to prevent runtime stalls.
- Zero-Copy Buffers: Reduced memory usage for shared stream burst buffers.
- Improved Connection Handling: Refactored provider registration to prevent zombie sockets and race conditions.
- HLS Session Tracking: Improved session matching to maintain correct active connection counts.
- Resource Cache: Avoid blocking runtime, async persistence, robust storage, incomplete downloads deleted.
- File Operations: Normalized FileLockManager paths, async playlist persistence, async JSON writers, async EPG exports, async config/API proxy saves, async video download queue.
- M3U Exports: Stream asynchronously.
- Logging: Detailed shared-stream/buffer/provider logging.
- Connection Failures: Explicit disconnect on registration failures.
- API User DB: Async persistence for user management APIs.
- Playlist Updates: Use Tokio tasks for reduced overhead.
- XMLTV Timeshift: Stream asynchronously.
- Healthcheck CLI: Uses async Reqwest client.
- Shared Stream Shutdown: Drops registry locks before releasing provider handles.
- EPG Icon URLs: Rewritten in reverse proxy mode.
- Short EPG: Served from local disk.
- EPG Memory Cache: Added target-scoped in-memory EPG cache (when
use_memory_cache=true) to reduce disk access for WebUI and short EPG lookups. - Client Requests: Extended debug logging for client requests and ID chain.
- XTream Fixes: Fixed series/catch-up lookups using
series-info virtual_id. - Cloudflare Header: Added
cloudflare_headerto reverse proxydisable_headersettings. - Kick Seconds:
kick_secsadded toconfig.yml web_uiconfig. - Improved connection handling for users with strict connection limits during streaming operations.
- Fixed streaming response handling for specific content types.
- Enhanced validation of response headers to prevent invalid values.
- Corrected request header prioritization logic.
- HLS-to-TS Fallback: Added optional non-HLS fallback path for live streams by forcing direct TS stream endpoints.
- Fix: Re-instated EPG Title Synchronization after playlist updates.
- Optimization: Significant EPG memory reduction.
- Optimization: Improved EPG parsing performance.
- EPG: Fixed XMLTV timeshift to correctly apply user-defined timezone offsets in the generated XML output.
- 407 Proxy Authentication Required fix.
⚙️ Messaging Refactoring 3.3.0
- Structured Messaging: Transitioned from JSON-string-based notifications to a strictly typed messaging pipeline.
- Backend Model Migration: Moved complex messaging models (
WatchChanges,ProcessingStats) from the shared crate to the backend to reduce shared-library overhead. - Unified API: Consolidated all notification types into a single, type-safe
send_messagefunction. - Template Improvements:
- Added per-message-type templates for Telegram, Discord, and REST messaging channels with support for Info, Stats, Error, and Watch notifications.
- Renamed template context fields for better clarity (e.g.,
event→processing). - Improved data accessibility in Handlebars templates with optimized context mapping (e.g.,
{{processing.stats}}or shorthand{{stats}}). - Implemented template loading from files and HTTP/HTTPS URIs with automatic discovery from configuration directories.
- Added UI components for managing per-type templates with textarea editor support.
3.2.0 (2025-11-14)
- Added
nameattribute to Staged Input. - Real-time active provider connection monitoring (dashboard + websocket)
- Source editor: block selection, batch-mode UI and automatic layout
- Fixed SSL certificate field binding in configuration view
- More robust connection-state and provider-handle management
- Streamlined event notifications and provider-count reporting
- Added configurable
reverse_proxy.resource_retry(UI + server) to tune max attempts, base delay, and exponential backoff multiplier for proxied resources. - Multi Strm outputs with same type is now allowed.
- Added new mapper function
pad(text | number, number, char, optional position: "<" | ">" | "^") - Added new mapper function
formatfor simple in-text replacement likeformat("Hello {}! Hello {}!", "Bob", "World") - Added
reverse_proxy.stream.shared_burst_buffer_mbto control shared-stream burst buffer size (default 12 MB). - Added
movieas alias forvodfor type filter. You can now useType = movieas an alternative toType = vod. - Fixed file locks to avoid race conditions on file operations
3.1.8 (2025-11-06)
-
Fixed HLS streaming issues caused by session eviction and incorrect headers.
-
Catchup stream fix cycling through multiple providers on play.
-
Custom streams fix and update webui stream info
-
Added TimeZone to
epg_timeshift: [-+]hh:mm or TimeZone, exampleEurope/Paris,America/New_York,-2:30(-2h30m),+0:15(15m),2(2h),:30(30m),:3(3m) If you use TimeZone the timeshift will change on Summer/Winter time if its applied in the TZ. -
Fixed: Mappings now automatically reload and reapply after configuration changes, preventing stale settings.
-
Search in Playlist Explorer now returns groups instead of matching flat channel list.
-
Added
use_memory_cacheattribute to target definition to hold playlist in memory to reduce disc access. Placing playlist into memory causes more RAM usage but reduces disk access. -
Added optional
filterattribute to Output (except HDHomerun-Output). Output filters are applied after all transformations have been performed, therefore, all filter contents must refer to the final state of the playlist. -
Added burst buffer to shared stream
-
Telegram message thread support. thread id can now be appended to chat-id like
chat-id:thread-id. -
Telegram supports markdown generation for structured json messages. simply set
markdown: truein telegram config. -
Added User-Stream-Connections Table to WebUI
-
Enhanced STRM output filenames to include detailed media quality info (e.g., 4K, HDR, x265, 5.1) for easy version distinction.
-
Added standardized SSDP (Simple Service Discovery Protocol) and the Proprietary HDHomeRun UDP Discovery Protocol (Port 65001)
-
Fixed some session handling issue
-
added
reverse_proxy.disabled_headerconfiguration Allows removing selected headers before forwarding requests when acting as a reverse proxy. Configure removal of the referer header, allX-*headers, and additional custom headers. -
!BREAKING_CHANGE!
disble_referer_headeris now part ofreverse_proxy.disabled_headerconfiguration -
UserTable: Copy credentials to clipboard from user table
-
UserTable: Kick user action from streams table
-
UserTable: Auto-generated username/password for new proxy users
-
Update process uses now streams for data processing.
3.1.7 (2025-10-10)
- Added Dark/Bright theme switch
- Resource proxy retries failed requests up to three times and respects the
Retry-Afterheader (falls back to 100 ms wait) to reduce transient HTTP errors (400, 408, 425, 429, 5xx) - Added
accept_insecure_ssl_certificatesoption inconfig.yml(for serving images over HTTPS without a valid SSL certificate) - VOD streams now use tmdbid from
get_vod_streamsif available, removing the need forresolve_vodin STRM generation - Fixed file length issue in STRM generation
- Fixed empty parentheses issue in series names
- Removed default sorting
- WebSocket now reconnects on disconnect; added WebSocket connection status icon in Web UI
- Added Playlist EPG view with timeline, channels,
nowline, and program details - EPG data can now be fetched from selected targets and custom URLs
- Faster, more reliable EPG loading via streaming and asynchronous processing, with reduced memory usage and better support for large or compressed guides.
- Invalid EPG text data fix
- Added new sidebar entry and icon for quick EPG access
- Added CBOR (binary JSON) support for large API data
3.1.6 (2025-09-01)
- EPG Config View added
- Fixed loading users for WebUI from user DB
- Fixed auto EPG for batch inputs
- Fixed EPG URL prepare
- Content Security Policies configurable via config, default OFF
- WebUI Config View editor for config.yml added
3.1.5 (2025-08-14)
- Hot reload for config
- New WebUI (currently only readonly)
- Fixed shared stream provider connection count
- Added hanging client connection release
- Added
replacebuilt-in function for mapper scripts - Added
token_ttl_minsto web_auth config to define auth token expiration duration. - Staged sources. Side-loading playlist. Load from staged, serve from provider.
- Fixed proxy config
- Added Content Security Policy to WebUI
3.1.4 (2025-06-17)
- share live stream refactored
- fixed active user count
- fixed hls streaming
- more logs sanitized
- added session key for session management
- added sleep timer
sleep_timer_minsto config.yml - added mapper script builtin function
templateto access template definitions.
station_prefix = template(concat("US_", station, "_PREFIX")),
If we assume the variable station contains the value WINK,
this script receives the template with the concatenated name US_WINK_PREFIX which should be defined in templates section,
and assigns it to the variable station_prefix.
- Extended STRM export functionality with:
- Support for various media tools (Kodi, Plex, Emby, Jellyfin), with consideration for recommended naming conventions and file organization.
- Optional flat directory structure via 'flat' parameter (nested folder structures are not supported by some media scanners).
- Added Trakt support for XC targets
- name: iptv-trakt-example
output:
- type: xtream
skip_live_direct_source: true
skip_video_direct_source: true
skip_series_direct_source: true
resolve_series: false
resolve_vod: false
trakt:
api:
key: <my Trakt Client ID>
version: 2
lists:
- user: "linaspurinis"
list_slug: "top-watched-movies-of-the-week"
category_name: "📈 Top Weekly Movies"
content_type: "vod"
fuzzy_match_threshold: 80
- user: "garycrawfordgc"
list_slug: "latest-tv-shows"
category_name: "📺 Latest TV Shows"
content_type: "series"
fuzzy_match_threshold: 80
3.1.3 (2025-06-06)
- Fixed xtream codes series info duplicate fields problem.
- Fixed series info container_extension problem.
- Mapper script can have blocks now.
For example, you want to write a
if then elseblock
# Maybe there is no station
station = @Caption ~ "(ABC)"
match {
station => {
# if block
# station exists
}
# optional any match as else block
_ => {
# else block
# station does not exists
}
}
- New BuiltIn Mapper function
first. When you use Regular expressions it could be that your match contains multiple results. The builtin functionfirstreturns the first match.
3.1.2 (2025-06-02)
- fixed input filter
- fixed epg fuzzy match
match_thresholddefault value - fixed
autoepg source
3.1.1 (2025-05-27)
- fixed m3u api hls handling
- during grace period no data is sent to client.
- splitted config file handling for accurate error messages
3.1.0 (2025-05-26)
- !BREAKING_CHANGE! mapper refactored, mapping can be written as a script with a custom DSL.
- !BREAKING_CHANGE!
tagsdefinition removed from new mapper. - !BREAKING_CHANGE! removed
suffixandprefixfrom input config. Use mapper with an input filter instead. - !BREAKING_CHANGE! custom_stream_response is now
custom_stream_response_path. The filename identifies the file inside the path- user_account_expired.ts
- provider_connections_exhausted.ts
- user_connections_exhausted.ts
- channel_unavailable.ts
user_account_expired.ts: Tuliprox will return a 403 Forbidden response for any playlist request if the user is expired. So this screen will only ever appear if someone tries to directly access a stream URL after their account has expired.
- !BREAKING_CHANGE! epg refactored
- url config is now renamed to sources
- Added
priority, priority isoptional auto_epgis now removed, useurl: autoinstead.- Added
logo_overrideto overwrite logo from epg.
Note: The priority value determines the importance or order of processing. Lower numbers mean higher priority. That is:
A priority of 0 is higher than 1. Negative numbers are allowed and represent even higher priority
epg:
sources:
- url: "auto"
priority: -2
logo_override: true
- url: "http://localhost:3001/xmltv.php?epg_id=1"
priority: -1
- url: "http://localhost:3001/xmltv.php?epg_id=2"
priority: 3
- url: "http://localhost:3001/xmltv.php?epg_id=3"
priority: 0
smart_match:
enabled: true
fuzzy_matching: true
match_threshold: 80
best_match_threshold: 99
name_prefix: !suffix "."
name_prefix_separator: [':', '|', '-']
strip : ["3840p", "uhd", "fhd", "hd", "sd", "4k", "plus", "raw"]
normalize_regex: '[^a-zA-Z0-9\-]'
-
Fixed mapper transform capitalize.
-
Auto hot reload for
mapping.ymlandapi-proxy.ymlTo enable setconfig_hot_reload: trueinconfig.yml -
Added config.d-style mapping support. You can now place multiple mapping files inside a directory like
mapping.dand specify it using the-moption, for example:-m /home/tuliprox/config/mapping.dThe files are loaded in alphanumeric order. Note: This is a lexicographic sort — som_10.ymlcomes beforem_2.ymlunless you name files carefully (e.g.,m_01.yml,m_02.yml, ...,m_10.yml). -
Added
mapping_pathtoconfig.yml. -
Added list template for sequences. List templates can only be used for sequences.
templates:
- name: CHAN_SEQ
value:
- '(?i)\bUHD\b'
- '(?i)\bFHD\b'
The template can now be used for sequence
sort:
groups:
order: asc
channels:
- field: caption
group_pattern: "!US_TNT_ENTERTAIN!"
order: asc
sequence:
- "!CHAN_SEQ!"
- '(?i)\bHD\b'
- '(?i)\bSD\b'
- added
disable_referer_headertoreverse_proxyconfig This option, when set totrue, prevents tuliprox from sending the Referer header in requests made when acting as a reverse proxy. This can be particularly useful when dealing with certain Xtream Codes providers that might restrict or behave differently based on the Referer header. Default isfalse.
reverse_proxy:
disable_referer_header: false
3.0.0 (2025-05-12)
- !BREAKING_CHANGE! user has now the attribute
ui_enabledto disable/enable web_ui for user. You need to migrate the user db if you have useduse_user_db:true. Set it tofalserun old tuliprox version, then update tuliprox and setuse_user_db:trueand start. - !BREAKING_CHANGE! all docker images have now tuliprox under
/app - !BREAKING CHANGE! bandwidth
throttle_kbpsattribute forreverse_proxy.streaminconfig.ymlis nowthrottleand supports units. Allowed units areKB/s,MB/s,KiB/s,MiB/s,kbps,mbps,Mibps. Default unit iskbps. - !BREAKING_CHANGE!
logconfigactive_clientsrenamed tolog_active_user - !BREAKING_CHANGE!
web_ui configrestructured and addeduser_ui_enabledattribute
web_ui:
enabled: true
user_ui_enabled: true
path:
auth:
enabled: true
issuer: tuliprox
secret: ef9ab256a8c0abe5de92c2e05ca92baa810472ab702ff1674e9248308ceeec92
userfile: user.txt
grace_period_millisdefault set to 300 milliseconds.grace_period_timeout_secsdefault set to 2 seconds.- Fixed user grace period
- Added
default_grace_period_timeout_secstoreverse_proxy.streamconfig. When grace_period granted, until thedefault_grace_period_timeout_secselapses no grace_period is granted again. - Added
methodattribute to input config. It can be set toGETorPOST. - Added optional
auto_epgfield toinput epg configfor auto-generating provider epg link. - Added rate limiting per IP. The burst_size defines the initial number of available connections,
while period_millis specifies the interval at which one connection is replenished.
If behind a proxy
x-forwarded-for,x-real-iporforwardedshould be set as header. The configuration below allows up to 10 connections initially and then replenishes 1 connection every 500 milliseconds.
reverse_proxy:
rate_limit:
enabled: true
period_millis: 500
burst_size: 10
- Multi epg processing/optimization, auto guessing/assigning epg id's
- Fixed hls redirect url issue
- Added
force_redirectto target config options. valid options arelive,vod,series
options: {ignore_logo: false, share_live_streams: false, force_redirect: [vod, series]}
epg:
url: ['http://localhost:3001/xmltv.php?epg_id=1', 'http://localhost:3001/xmltv.php?epg_id=2']
smart_match:
enabled: true
fuzzy_matching: true
match_threshold: 80
best_match_threshold: 99
name_prefix: !suffix "."
name_prefix_separator: [':', '|', '-']
strip : ["3840p", "uhd", "fhd", "hd", "sd", "4k", "plus", "raw"]
normalize_regex: '[^a-zA-Z0-9\-]'
match_thresholdis optional and if not set 80.
best_match_threshold is optional and if not set 99.
name_prefix can be ignore, suffix, prefix. For suffix and prefix you need to define a concat string.
strip : ["3840p", "uhd", "fhd", "hd", "sd", "4k", "plus", "raw"] this is the defualt
normalize_regex: [^a-zA-Z0-9\-] is the default
# single epg
url: 'https://localhost.com/epg.xml'
# multi local file epg
url: ['file:///${env:TULIPROX_HOME}/epg.xml', 'file:///${env:TULIPROX_HOME}/epg2.xml']
# multi url epg
url: ['http://localhost:3001/xmltv.php?epg_id=1', 'http://localhost:3001/xmltv.php?epg_id=2']
- Added
stripto input for auto epg matching, if not given["3840p", "uhd", "fhd", "hd", "sd", "4k", "plus", "raw"]is default When no matching epg_id is found, the display name is used to match a channel name. The given strings are stripped to get a better match. - Fixed chno assignment issue
- Redirect Proxy provider cycle implemented (m3u playlist only cycles when output param
mask_redirect_urlis set). - Reverse Proxy mode for user can now be a subset
reverse-> all reversereverse[live]-> only live reverse, vod and series redirectreverse[live,vod]-> series redirect, others reverse
/statusapi endpoint moved to/api/v1/statusfor auth protection- fixed multi provider VOD seek problem (provider cycle on seek request prevented playback)
- hdhomerun supports now basic auth like http://user:password@ip:port/lineup.json you need to enable auth in config
hdhomerun:
enabled: true
auth: true
devices:
- name: hdhr1
- A new filter field
captionhas been added. This field is used to bypass thetitle/nameissue. Ifcaptionis provided, its value is read fromtitleif available, otherwise fromname. When settingcaption, bothtitleandnameare updated.” - Counter has now an attribute
padding. Which fills the number like 001. - Added proxy configuration for all outgoing requests in
config.yml. supported http, https, socks5 proxies.
proxy:
url: socks5://192.168.1.6:8123
username: uname # <- optional basic auth
password: secret # <- optional basic auth
- Added support for regular expression-based sequence sorting. You can now sort both groups and channels using custom regex sequences.
sort:
groups:
order: asc
sequence:
- '^Freetv'
- '^Shopping'
- '^Entertainment'
- '^Sunrise'
channels:
- field: caption
group_pattern: '^Freetv'
order: asc
sequence:
- '(?P<c1>.*?)\bUHD\b'
- '(?P<c1>.*?)\bFHD\b'
- '(?P<c1>.*?)\bHD\b'
- '(?P<c1>.*?)\bSD\b'
In the example above, groups are sorted based on the specified sequence.
Channels within the Freetv group are first sorted by quality (as matched by the regex sequence), and then by the captured prefix.
To sort by specific parts of the content, use named capture groups such as c1, c2, c3, etc.
The numeric suffix indicates the priority: c1 is evaluated first, followed by c2, and so on.
- Added ip check config
- url # URL that may return both IPv4 and IPv6 in one response
- url_ipv4 # Dedicated URL to fetch only IPv4
- url_ipv6 # Dedicated URL to fetch only IPv6
- pattern_ipv4 # Optional regex pattern to extract IPv4
- pattern_ipv6 # Optional regex pattern to extract IPv6
ipcheck:
url_ipv4: https://ipinfo.io/ip
2.2.5 (2025-03-27)
- fixed web ui playlist regexp search
- added
web_ui_pathtoconfig.yml - added grace period
grace_period_millisattribute forreverse_proxy.streaminconfig.ymlIf you have a provider or a user where the max_connection attribute is greater than 0, a grace period can be given during the switchover. If this period is set too short, it may result in access being denied in some cases. The default is 1000 milliseconds (1sec). - added bandwidth
throttle_kbpsattribute forreverse_proxy.streaminconfig.yml
| Resolution | Framerate | Bitrate (kbps) | Quality |
|---|---|---|---|
| 480p (854x480) | 30 fps | 819–2.457 | Low-Quality |
| 720p (1280x720) | 30 fps | 2.457–5.737 | HD-Streams |
| 1080p (1920x1080) | 30 fps | 5.737–12.288 | Full-HD |
| 4K (3840x2160) | 30 fps | 20.480–49.152 | Ultra-HD |
2.2.4 (2025-03-24)
- fixed
connect_timeout_secs:0prevents connection initiation issue. - fixed
hdhomerunandstrmconfig check for non-existing username. - "Breaking CHANGE! Moved
connect_timeout_secsis global timeout and defiend in config root and notreverse_proxy.stream.
2.2.3 (2025-03-23)
- variable resolving for config files now for all settings
- hls reverse proxy implemented
- dash redirect implemented (reverse proxy not supported)
- !BREAKING CHANGE!
channel_unavailable_fileis now undercustom_stream_response, - New custom streams
user_connections_exhaustedandprovider_connections_exhaustedadded.
custom_stream_response:
channel_unavailable: /home/tuliprox/resources/channel_unavailable.ts
user_connections_exhausted: /home/tuliprox/resources/user_connections_exhausted.ts
provider_connections_exhausted: /home/tuliprox/resources/provider_connections_exhausted.ts
- input alias definition for same provider with same content but different credentials
- sources:
- inputs:
- type: xtream
name: my_provider
url: 'http://provider.net'
username: xyz
password: secret1
aliases:
- name: my_provider_2
url: 'http://provider.net'
username: abcd
password: secret2
targets:
- name: test
Input aliases can be defined as batches in csv files with ; separator.
There are 2 batch input types xtream_batch and m3u_batch.
XtreamBatch:
- sources:
- inputs:
- type: xtream_batch
url: 'file:///home/tuliprox/config/my_provider_batch.csv'
targets:
- name: test
#name;username;password;url;max_connections;priority
my_provider_1;user1;password1;http://my_provider_1.com:80;1;0
my_provider_2;user2;password2;http://my_provider_2.com:8080;1;0
M3uBatch:
- sources:
- inputs:
- type: m3u_batch
url: 'file:///home/tuliprox/config/my_provider_batch.csv'
targets:
- name: test
#url;max_connections;priority
http://my_provider_1.com:80/get_php?username=user1&password=password1;1;0
http://my_provider_2.com:8080/get_php?username=user2&password=password2;1;0
The Fields max_connections and priorityare optional.
max_connections will be set default to 1. This is different from yaml config where the default is 0=unlimited
- added two options to reverse proxy config
forced_retry_interval_secsandconnect_timeout_secsforced_retry_interval_secsforces every x seconds a reconnect to the provider,connect_timeout_secstries only x seconds for connection, if not successfully starts a retry.
2.2.2 (2025-03-12)
- !BREAKING CHANGE! Target options moved to specific target output definitions.
target options:
ignore_logo:true|false,share_live_streams:true|false,remove_duplicates:true|false,
target output type xtream:
skip_live_direct_source:true|false,skip_video_direct_source:true|false,skip_series_direct_source:true|false,resolve_series:true|false,resolve_series_delay: seconds,resolve_vod:true|false,resolve_vod_delay:true|false,
target output type m3u:
filename: optionalinclude_type_in_url:true|false,mask_redirect_url:true|false,
target output type strm:
directory: mandatory,username: optional,underscore_whitespace:true|false,cleanup:true|false,kodi_style:true|false,strm_props: optional, list of strings,
target output type hdhomerun:
device: mandatory,username: mandatory,use_output: optional,m3u|xtream
Example:
targets:
- name: xc_m3u
output:
- type: xtream
skip_live_direct_source: true,
skip_video_direct_source: true,
- type: m3u
- type: strm
directory: /tmp/kodi
- type: hdhomerun
username: hdhruser
device: hdhr1
use_output: xtream
options: {ignore_logo: false, share_live_streams: true, remove_duplicates: false}
- The Web UI now includes a login feature for playlist users, allowing them to set their groups for filtering and managing their own bouquet of groups. The playlist user can login with his credentials and can select the desired groups for his playlist.
- Added
user_config_dirtoconfig.yml. It is the storage path for user configurations (f.e. bouquets). - New Filter field
inputcan be used alongname,group,title,urlandtype. Input is aregexpfilter.input ~ "provider\-\d+" - New option
use_user_dbinapi-proxy.yml. The Playlist Users are stored inside the config fileapi-proxy.yml. When you set this option totruethe user are stored in a db file. This is a better choice if you have a lot of users. If you have only a few let it default tofalse - WebUI playlist browser with tree and gallery mode. Explore self hosted and provider playlists in browser.
- Added HdHomeRun tuner target for use with Plex/Emby/Jellyfin
2.2.1 (2025-02-14)
- Added more info to
/status. - Refactored unavailable channel replacement streaming.
- Fixed catch up saving.
- Updated readme for creation of unavailable channel video file with ffmpeg for mobiles.
- refactored stream sharing.
2.2.0 (2025-02-11)
- !BREAKING CHANGE! unique
inputnameis now mandatory, because rearranging thesource.ymlcould lead to wrong results without a playlist update. - !BREAKING_CHANGE!
log_sanitize_sensitive_infois now underlogsection assanitize_sensitive_info - !BREAKING_CHANGE! uuid generation for entries changed to
input.name+stream_id. Virtual id mapping changed. The new Virtual id is not a sequence anymore. - !BREAKING_CHANGE!
api-proxy.ymlserver config changed.
server:
- name: default
protocol: http
host: 192.169.1.9
port: '8901'
timezone: Europe/Paris
message: Welcome to tuliprox
- name: external
protocol: https
host: tuliprox.mydomain.tv
port: '443'
timezone: Europe/Paris
message: Welcome to tuliprox
path: tuliprox
- Added Active clients count (for reverse proxy mode users) which is now displayed in
/statusand can be logged with settingactive_clients: trueunderlogsection inconfig.yml - Fixed iptv player using live tv stream without
/live/context. - Added
log_leveltologconfig. Priority: CLI-Argument, Env-Var, Config, Default(info)
log:
sanitize_sensitive_info: false
active_clients: true
log_level: debug
update_on_boot: false
web_ui_enabled: true
- Added new option to
inputxtream_live_stream_without_extension. Default isfalse. Some providers don't like.tsextension, some providers need it. Now you can disable or enable it for a provider. - Aded new option to
inputxtream_live_stream_use_prefix.. Default istrue. Some providers don't like/live/prefix for streams, some providers need it. Now you can disable or enable it for a provider. - Added
pathtoapi-proxy.ymlserver config for simpler front reverse-proxy configuration (like nginx) - added
hlsrhandling. - fixed mapper counter not incrementing.
- adding
&type=m3u_plusat the end of anm3uurl wil trigger a download. Without it will only stream the result. kodistrmgeneration, does not delete root directory, avoids unchanged file creations.strmfiles now o get timestamp fromaddeddproperty if exists.- shared live stream implementation refactored.
- added optional user properties:
max_connections,status,exp_date(expiration date as unix seconds). If they exist they are checked whenconfig.ymluser_access_controlset to true., if you don't need them remove this fields fromapi-proxy.ymlAdded option inconfig.ymlthe optionuser_access_controlto activate the checks. Default is false. - Added option
channel_unavailable_fileinconfig.yml. If a provider stream is not available this file content is send instead.
update_on_boot: false
web_ui_enabled: true
channel_unavailable_file: /freeze_frame.ts
2.1.3 (2025-01-26)
- Hotfix 2.1.2, forgot to update the stream api code.
2.1.2 (2025-01-26)
Strmoutput has an additional optionstrm_props. These props are written to the strm file. You can add properties like#KODIPROP:seekable=true|false,#KODIPROP:inputstream=inputstream.ffmpegor"#KODIPROP:http-reconnect=true.- Fixed xtream affix-processed output.
log_sanitize_sensitive_infoadded toconfig.yml. Default istrue.- added
resource_rewrite_disabledtoreverse_proxyconfig to disable resource url rewrite. - Fixed series redirect proxy mode.
- Added
pushover.netconfig to messaging.
messaging:
pushover:
token: _required_
user: _required_
url: `optional`, default is https://api.pushover.net/1/messages.json
2.1.1 (2025-01-19)
- added new path
/statuswhich is an alias tohealthcheck - added memory usage to
/status - fixed VLC seeking problem when reconnect stream was enabled.
- duplicate field problem for xtream series/vod info fixed.
- fixed docker/build scripts
- fixed xtream live stream redirect bug
2.1.0 (2025-01-17)
- Watch files are now moved inside the
targetfolder. Move them manually fromwatch_<target_name>_<watched_group>.binto<target_name>/watch_<watched_group>.bin - No error log for xtream api when content is skipped with options
xtream_skip_[live|vod|series] - experimental: added live channel connection sharing in reverse proxy mode. To activate set
share_live_streamsin target options. - Added
infoandtmdb-idcaching for vod and series with optionsxtream_resolve_(series|vod). - The
kodiformat for movies can contain thetmdb-id(optional). To add thetmdb-idyou can set nowkodi_style,xtream_resolve_vod,xtream_resolve_vod_delay,xtream_resolve_seriesandxtream_resolve_series_delayto target options. kodioutput can now haveusernameattribute to use reverse proxy mode when combined withxtreamoutput.- Fixed webUI manual update for selected targets
- Added m3u logo url rewrite in
reverse proxymode or withm3u_mask_redirect_urloption. - BPlusTree compression changed from zlib to zstd.
- Breaking change: multi scheduler config with optional targets.
# sec min hour day of month month day of week year
schedules:
- schedule: "0 0 8 * * * *"
targets:
- vod_channels
- schedule: "0 0 10 * * * *"
targets:
- series_channels
- schedule: "0 0 20 * * * *"
- Stats have now target information
- Prevent simultaneous updates
- Added target options
remove_duplicatesto remove entries with sameurl. - Added reverse Proxy config to
config.yml config.ymlbackup_diris now defaultbackup. If you want to keep the old name setbackup_dir: .backup
reverse_proxy:
stream:
retry: true
buffer:
enabled: true
size: 1024
connect_timeout_secs: 5
cache:
size: 500MB
enabled: true
dir: ./cache
2.0.10 (2024-12-03)
- added Target Output Option
m3u_include_type_in_url, default false. This addslive,movie,seriesto the url of the stream in reverse proxy mode. - added Target Output Option
m3u_mask_redirect_url, default false. The urls are pointed to tuliprox in redirect mode. In stream request a redirect response is send. Usefully if you want to track calls in redirect mode. - fixed xtream api redirect url problem.
2.0.9 (2024-12-01)
- Fixed api proxy server url bug
2.0.8 (2024-11-27)
- The configured directories
data,backupandvideo-downloadare created when configured and do not exist. - set "actix_web::middleware::logger" to level
error - masking sensitive information in log
- hls support (m3u8 url, ignores proxy type, always redirect)
2.0.7 (2024-11-05)
- EPG is now first downloaded to disk instead of directly into memory, then processed using a SAX parser (slower but reduces memory usage from up to 2GB).
- Various code optimizations have been applied.
- Regular expression matching in log output is now set to trace level to prevent flooding the debug log.
- Processing stats now include a
tookfield indicating the processing time.
2.0.6 (2024-11-02)
-
breaking change virtual_id handling. You need to clear the data directory.
-
new content storage implementation with BPlusTree indexing.
-
api responses are now streamed directly from disk to avoid memory allocation.
-
fixed scheduler implementation to only wake up on scheduled times.
2.0.5(2024-10-16)
- input url supports now scheme
file://...(which is not necessary because file paths are supported). Gzip files are also supported. - sort takes now a sequence for channel values which has higher priority than sort order
- fixed error handling in filter parsing
NOTfilter is nownon greedy.NOT Name ~ "A" AND Group ~ "B"wasNOT (Name ~ "A" AND Group ~ "B"). Now it is(NOT Name ~ "A") AND Group ~ "B"- Implemented workaround for missing tvg-ID
2.0.4(2024-09-19)
- if Content type of file download is not set in header, the gzip encoding is checked through magic header.
- if source is m3u and stream id not a number, the entry is skipped and logged.
- prefix and suffix was applied wrong, fixed.
- epg timeshift, define timeshift api-proxy.yml for each user as
epg_timeshift: hh:mm, example-2:30,1:45,+0:15,2,:30,:3,2: - timeshift.php api implementation
- New Filter
typeadded can be uses asType = vodorType = liveorType = series - Counter in
mapping.yml. Each mapper can have counters to add counter to specific fields. - Added new mapper feature
transform.uppercase,lowercaseandcapitalizesupported. - Fixed parsing invalid m3u playlist entries like
tvg-logo="[""]"
2.0.3(2024-07-11)
- added
source-input-nameattribute to README - added
chnoto Playlist attributes. epg_channel_idmapping fixed
v2.0.2(2024-05-28)
- Added Encoding handling: gzip,deflate
- Fixed panic when
tvg-idis not set.
v2.0.1(2024-05-24)
- m3u playlists are not saved as plainfile, therefor m3u output filename is not mandatory, if given the plain m3u playlist is stored.
- Added
--healthcheckargument for docker - Added
catch-up/timeshiftapi forxtream
v2.0.0(2024-05-10)
- major version change due to massive changes
update_on_bootfor config, default is false, if true an update is run on startcategory_idfilter added to xtream api- Handling for m3u files without id and group information
- Added
panel_api.phpendpoint for xtream - Case insensitive filter syntax
- Xtream category_id fixes, to avoid category_id change when title not changes.
- Target options
xtream_skip_live_direct_sourceandxtream_skip_video_direct_sourceare now default true - added new target option
xtream_skip_series_direct_sourcedefault is true
- Added new options to input configuration.
xtream_skip_live,xtream_skip_vod,xtream_skip_series - Updated docker files, New Dockerfile with builder to build an image without installing rust or node environments.
- Generating xtream stream urls from m3u input.
- Reverse proxy implementation for m3u playlist.
- Mapper can now set
epg_channel_id. - Added environment variables for User Credentials
username,passwordandtokenin format${env:<EnvVarName>}where<EnvVarName>should be replaced. - Added
web_ui_enabledtoconfig.yml. Default istrue. Set tofalseto disable webui. - Added
web_authtoconfig.ymlstruct for web-ui-authentication is optional.enabled: default trueissuerissuer for jwt tokensecretsecret for jwt tokenuserfileoptional userfile with generated userfile in format "username: password" per file, default name is user.txt in config path
- Password generation argument --genpwd to generate passwords for userfile.
- Added env var
TULIPROX_LOGfor log level - Log Level has now module support like
tuliprox::util=error,tuliprox::filter=debug,tuliprox=debug - Multiple Xtream Sources merging into one target is now supported
v1.1.8(2024-03-06)
- Fixed WebUI Option-Select
- WebUI: added gallery view as second view for playlist
- Breaking change config path. The config path is now default ./config. You can provide a config path with the "-p" argument.
v1.1.7(2024-01-30)
- Renamed api-proxy.yml server info field
iptohost - Multiple server-config for xtream api. In api-proxy.yml assign server config to user
v1.1.6(2024-01-17)
- Watch filter are now regular expressions
- Fixed watch file not created problem
- UI responds immediately to update request
v1.1.5(2024-01-11)
- Changed api-proxy user default proxy type from
reversetoredirect - Added
xtream_resolve_seriesandxtream_resolve_series_delayoption form3utarget - Messaging calling rest endpoint added
- Messaging added 'Watch' option as OptIn
v1.1.4(2023-12-06)
- Breaking change,
config.ymlsplit intoconfig.ymlandsource.yml - Added
backup_dirproperty toconfig.ymlto store backups of changed config files. - Added regexp search in Web-UI
- Added config Web-UI
- Added xtream vod_info and series_info, stream seek.
- Added input options with attribute xtream_info_cache to cache get_vod_info and get_series_info on disc
- for xtream api added proxy types reverse and redirect to user credentials.
v1.1.3(2023-11-08)
- added new target options
xtream_skip_live_direct_sourcextream_skip_video_direct_source
- internal optimization/refactoring to avoid string cloning.
- new options for downloading media files from web-ui
organize_into_directoriesepisode_pattern
- Web-UI - Download View with multi download support
- Added WebSearch Url `web_search: '<\1> under video configuration.
v1.1.2(2023-11-03)
- Fixed epg for xtream
- Fixed some Web-UI Problems
- Added some convenience endpoints to rest api
v1.1.1(2023-10-31)
- Added scheduler to update lists in server mode.
- Added Xtream Cluster Live, Video, Series. M3u Playlist cluster guessing through video file endings.
- Added api-proxy config for xtream proxy, to define server info and user credentials
- Added Xtream Api Endpoints.
- Added M3u Api Endpoints.
- Added multiple input support
- Added Messaging with opt in message types [info, error, stats]
- Added Telegram message support
- Added Target watch for groups
- Fixed TLS problem with docker scratch
- Added simple stats
- Target Output is now a list of multiple output formats, !breaking change!
- RegExp captures can now be used in mapper attributes
- Added file download to a defined directory in config
- Refactored web-ui
- Added XMLTV support
Changes in config.yml
messaging:
notify_on:
- error
- info
- stats
telegram:
bot_token: '<your telegram bot token>'
chat_ids:
- <your telegram chat_id>
schedules:
- schedule: '0 0 0,8,18 * * * *'
api-proxy.yml
server:
protocol: http
ip: 192.168.9.3
http_port: 80
https_port:
rtmp_port:
timezone: Europe/Paris
message: Welcome to tuliprox
user:
- target: pl1
credentials:
- {username: x3452, password: ztrhgrGZrt83hjerter}
v1.0.1(2023-09-07)
- Refactored sorting. Sorting channels inside group now possible
v1.0.0(2023-04-27)
- Added target argument for command line.
tuliprox -t <target_name> -t <target_name>. Target names should be provided in the config. - Added filter to mapper definition.
- Refactored filter parsing.
- Fixed sort after mapping group names.
- Refactored mapping, fixed reading unmodified initial values in mapping loop from ValueProvider, because of cloned channel
v0.9.9(2023-03-20)
- Added optional 'enabled' property to input and target. Default is true.
- Fixed template dependency replacement.
- Added optional 'name' property to target. Default is 'default'.
- Added Dockerfile
- Added xtream support
- Breaking changes: config changes for input
v0.9.8(2023-02-25)
- Added new fields to mapping attributes and assignments
- "name"
- "title"
- "group"
- "id"
- "chno"
- "logo"
- "logo_small"
- "parent_code"
- "audio_track"
- "time_shift"
- "rec"
- "source"
- Added static suffix and prefix at inpupt source level
v0.9.7(2023-02-15)
- Breaking changes, mappings.yml refactored
- Added
threadsproperty to config, which executes different sources in threads. - WebUI: Added clipboard collector on left side
- Added templates to config to use in filters
- Added nested templates, templates can have references to other templates with
!name!. - Renamed Enum Constants
- M3u -> m3u,
- Strm -> strm
- FRM -> frm
- FMR -> fmr
- RFM -> rfm
- RMF -> rmf
- MFR -> mfr
- MRF -> mrf
- Group -> group (Not in filter regular expressions)
- Name -> name (Not in filter regular expressions)
- Title -> title (Not in filter regular expressions)
- Url -> url (Not in filter regular expressions)
- Discard -> discard
- Include -> include
- Asc -> asc
- Desc -> desc
v0.9.6(2023-01-14)
- Renamed
mappings.templatesattributekeytoname mappings.tagis now a struct- captures: List of captured variable names like
quality. - concat: if you have more than one captures defined this is the join string between them
- suffix: suffix for thge tag
- prefix: prefix for the tag
- captures: List of captured variable names like
v0.9.5(2023-01-13)
- Upgraded libraries, fixed serde_yaml v.0.8 empty string bug.
- Added Processing Pipe to target for filter, map and rename. Values are:
- FRM
- FMR
- RFM
- RMF
- MFR
- MRF default is FMR
- Added mapping parameter
match_as_ascii. Default isfalse. Iftruebefore regexp matching the matching text will be converted to ascii. unidecode
Added regexp templates to mapper:
mappings:
- id: France
tag: ""
match_as_ascii: true
templates:
- key: delimiter
value: '[\s_-]*'
- key: quality
value: '(?i)(?P<quality>HD|LQ|4K|UHD)?'
mapper:
- tvg_name: TF1 $quality
# https://regex101.com/r/UV233E/1
tvg_names:
- '^\s*(FR)?[: |]?TF1!delimiter!!quality!\s*$'
tvg_id: TF1.fr
tvg_chno: "1"
tvg_logo: https://emojipedia-us.s3.amazonaws.com/source/skype/289/shrimp_1f990.png
group_title:
- FR
- TNT
mappingattribute for target is now a list. You can assign multiple mapper to a target.
mapping:
- France
- Belgium
- Germany
v0.9.4(2023-01-12)
-
Added mappings. Mappings are defined in a file named
mapping.ymlor can be given by command line option-m.targethas now an optional fieldmappingwhich has the id of the mapping configuration. -
rename is now optional
v0.9.3(2022-04-21)
Strmoutput has an additional optionkodi_style. This option tries to guess the year, season and episode for kodi style names. https://kodi.wiki/view/Naming_video_files/TV_shows
v0.9.2(2022-04-05)
Strmoutput has an additional optioncleanup. This deletes the old directory given atfilename.
v0.9.1(2022-04-05)
- There are two types of targets
m3uandstrm. This can be set by theoutputattribute toStrmorM3u. If the attribute is not specifiedM3uis created by default.Strmoutput has an additional optionunderscore_whitespace. This replaces all whitespaces with_in the path.
v0.9.0(2022-04-04)
- Changed filter. Filter are now defined as filter statements. Url added to filter fields.
v0.8.0(2022-03-24)
- Changed configuration. It is now possible to handle multiple sources. Each input has its own targets.
v0.7.0(2022-01-20)
- Updated frontend libraries
- Added Search, currently only plain text search
v0.6.0(2021-12-29)
- Added options to target, currently only ignore_logo
- Added sorting to groups
v0.5.0(2021-10-15)
- Fixed: config input persistence filename was ignored
- Added working_dir to configuration
- relative web_root is now checked for existence in current path and working_dir.
v0.4.0(2021-10-08)
- Fixed server exit on playlist not found
- Added copy link to clipboard in playlist tree
v0.3.0(2021-10-08)
- Updated frontend packages
- Added linter for code checking
- Updated tree layout and added hover coloring
- Fixed Url Field could not be edited after drop down selection
- Added download on key-"Enter" press
v0.2.0(2021-10-07)
- Added simple WEB-UI
- Start in server mode
v0.1.0(2021-10-01)
- Initial project release