- Enhanced the `formatApiError` function to better format failed API responses, preventing raw HTML error pages from being displayed in toasts. JSON error bodies are now prioritized for user-friendly messages, while HTML and empty bodies are collapsed to a concise status line. Long plain-text bodies are truncated for readability.
- Updated tests to cover new error formatting behavior, ensuring accurate extraction of error messages from various JSON structures.
- Updated the catch-up stats UI to advance to the next EPG programme when playback continues past the original programme's end, improving user experience.
- Enhanced the `get_programme_info` function to resolve the guide entry based on the playhead position, ensuring accurate display of the current programme.
- Introduced new utility functions for computing catch-up playback position and checking if the playhead is outside the displayed programme window, optimizing session management.
- Added tests to validate the new functionality and ensure correct behaviour of the catch-up features.
- Introduced a new endpoint `POST /api/catchup/sessions/<session_id>/position/` for native clients to report their playhead position and pause state during catch-up sessions.
- Updated the catch-up session handling to include a `paused` flag, allowing accurate tracking of playback state without seeking the provider stream.
- Enhanced the Redis storage mechanism to accommodate the new position and pause data, ensuring real-time updates for admin stats.
- Added tests to validate the new position reporting functionality and its integration with existing catch-up features.
- Introduced an optional `duration` parameter for catch-up sessions, allowing clients to specify the programme length in minutes. This value is preferred over EPG-derived durations and includes a buffer for provider lag.
- Updated the API views and serializers to accept and process the new `duration` field.
- Enhanced the catch-up proxy implementation to utilize the client-supplied duration, improving playback accuracy.
- Added tests to validate the new duration handling and ensure proper integration with existing functionality.
- Implemented a new `LockedSpectacularAPIView` to handle schema generation, ensuring it no longer fails due to concurrent gevent requests. The schema builds are now single-flight per process and cached in Django's cache, allowing all workers to share the result.
- Updated the `urls.py` to use the new view for schema generation, enhancing stability and performance.
- Improved multi-provider failover by preferring streams with sufficient catch-up days.
- Added new helper functions for calculating programme age and ordering catch-up streams.
- Removed deprecated XMLTV settings from the frontend and backend.
- Updated tests to validate new stream ordering logic and catch-up functionality.
Updated the catch-up timestamp normalization to reflect XC client specifications. Introduced a new mechanism to handle near-EOF duration probes, ensuring that playback statistics are preserved without reanchoring to the end of the file. Additionally, improved the handling of presentation lengths in the timeshift proxy to align with XC behavior. Updated tests to validate these changes and ensure robust playback functionality.
Three tests encoded the machine's timezone or locale into their
expectations and fail on any box east of UTC (observed on UTC+12/13;
CI's UTC and US-locale dev machines never see it):
- dateTimeUtils isSame: compared 10:00Z/11:00Z on the 15th as 'same
day', but isSame works on local calendar days and no UTC instant
falls on the same date in every timezone (at UTC+13 those are 23:00
on the 15th and 00:00 on the 16th). Use timezone-less datetimes,
which mean the same calendar day everywhere.
- RecordingUtils toDateString: formatted a UTC-noon instant and
expected the UTC date, but toDateString renders local time (UTC+12
renders the next day). Construct the Date with local components.
- SeriesModalUtils getEpisodeAirdate: the code renders the viewer's
locale via toLocaleDateString() by design, but the test hardcoded
the en-US shape (it tolerated the timezone day-shift with /1[4|5]/
yet not the day/month order). Compute the expectation with the same
API so the test asserts the wiring, not a specific locale.
No production code changes.
Failed API responses whose body is not JSON - Django's HTML "Server
Error (500)" page, nginx's HTML 502/504 pages when the backend is down
or timing out - were interpolated verbatim into the error toast,
showing users a wall of markup.
request() deliberately keeps the raw text when JSON.parse fails, and
errorNotification() rendered `${status} - ${body}` unconditionally.
Route the body through a new formatApiError() helper (in utils.js so it
is unit-testable without importing the store-heavy api module):
- JSON object bodies keep their existing pretty-printed formatting
- markup and empty bodies collapse to the response's own status line:
the declared Content-Type decides what counts as markup (body
sniffing only as fallback when the header is missing), and the label
comes from the fetch Response's statusText - the protocol's reason
phrase, defined for every status code, rather than a hardcoded status
map. HTTP/2+ transmits no reason phrase, so an empty statusText falls
back to a generic label. request() already attaches the Response to
the error, so no call-site changes are needed.
- the full suppressed body goes to console.debug so the markup stays
available for troubleshooting (deliberate, not a leftover debug
statement)
- plain-text bodies are truncated at 200 chars as a backstop
- errors without a status keep the error.message fallback
Added a new file entry to .gitignore for .uwsgi-reload and modified uwsgi.debug.ini to implement a touch-based worker reload mechanism, replacing the previous py-autoreload setting. This change enhances the management of API worker reloads during development.
Clients that build catch-up requests in QUERY layout (e.g. Open-TV /
Fred TV: /streaming/timeshift.php?username=...&stream=...&start=...)
had no matching urlpattern, so the request silently fell through to
the frontend's <path:unused_path> catch-all and got served index.html
(200 OK, wrong content) instead of reaching the proxy — no error, no
log line, the client just fails to play.
The PATH-style layout (timeshift/<user>/<pass>/<stream_id>/<ts>/<dur>)
already worked; QUERY-style autodetection already existed for outgoing
provider requests (helpers.build_timeshift_url_format_a/_b) but was
never mirrored to the incoming route.
Split timeshift_proxy into a shared _timeshift_proxy_impl plus two thin
entry points (PATH-style timeshift_proxy, new QUERY-style
timeshift_proxy_query) so both incoming layouts are recognized.
Reported against the predecessor plugin as dispatcharr_timeshift#10;
reproduced identically against dispatcharr:dev (confirmed via nginx
access log: a QUERY-style request returned 200 with a response body
exactly matching the size of frontend/dist/index.html).
Updated the session handling mechanism to improve user experience during reconnects. The first request without a `session_id` now receives a `301` redirect with a minted `session_id`, while reconnects that omit `session_id` but match an existing pool entry are served immediately without a redirect. Additionally, refined playback logic for plain GET requests to restart from byte 0, aligning with provider behavior. Updated tests to reflect these changes and ensure proper session reuse and playback anchoring.
- Added entry for extended frontend unit tests covering plugin, backup, and settings components.
- Updated Backup Manager to ensure the "Created" column refreshes when date/time format preferences change.
Updated the stream switching mechanism in the proxy server to ensure that the `stream_id` is persisted correctly and that the requesting worker waits for confirmation from the owning worker in multi-worker deployments. Improved error handling for stream switch failures, returning appropriate HTTP status codes (502/504) based on the confirmation status. Additionally, refined the metadata update process to handle cases where the requested URL is already in use, ensuring a successful response without unnecessary operations. This change addresses issues with stale metadata and enhances the robustness of the stream switching feature. (Fixes#1412)
Updated the session matching logic to improve handling of fresh sessions. Introduced a new parameter, `fresh_session`, to skip adopting idle exact-media pools when a new session ID is provided. This prevents unnecessary reconnections to previously abandoned programme slots. Added tests to verify that fresh sessions correctly skip idle matches while retaining busy exact-media sessions and allowing channel hops. This change enhances the robustness of the timeshift feature and improves user experience during session transitions.
Add end-to-end catch-up support for XC clients and native apps: provider
proxy with failover and per-viewer session pooling, REST session minting
for tokenless playback URLs, catch-up admin stats, combined connection
stats, and Stats UI with dedicated cards plus websocket updates.
Includes Redis namespace consolidation under timeshift:* (dropping legacy
timeshift_ id prefixes), dedicated catch-up stop by session_id, and
cleaner channel/client metadata split for stats keys.
Closes#133
Updated the EPG generation logic to ensure that channels without a channel number do not cause crashes in the XMLTV export. Implemented a deferred assignment mechanism for channels with null numbers, allowing them to receive valid integers during processing. Added tests to verify that the XMLTV output remains stable and valid even when channels are unnumbered.
Implemented logic to abort the VOD refresh process when the provider returns no categories, preserving existing group selections. This change prevents unintended deletions of category relations and ensures that accounts retain their current settings during transient provider outages or API issues. Updated logging to reflect the abort condition for better traceability.
Updated the handling of channels in the live streams setup to ensure that channels without a channel number do not cause crashes. Implemented logic to assign a valid number to these channels during processing. Added tests to verify that the system correctly assigns numbers to channels with null values, ensuring stability in the live streams feature.
Added a helper function to close old database connections in the `timeshift_proxy` view before returning any HTTP responses. This change prevents potential database connection leaks and ensures that connections are properly managed during the request lifecycle. Updated tests to verify that connections are closed appropriately in various response scenarios.
Also stores provider_tz_name with session pool in redis to avoid an extra database call.
Enhanced the `/output/epg` cold rebuild process to prevent freezing of the gevent uWSGI worker. The `_stream_build` function now yields control to the gevent hub after processing each cached chunk, allowing other requests to be handled concurrently. This change improves responsiveness during CPU-bound XMLTV generation. Additionally, introduced a new utility function, `_cooperative_yield`, to facilitate yielding in CPU-bound loops.
Self-review follow-ups on the position fix:
- When the position actually moves, drop content_length/serving_range from
the pool entry — they describe the PREVIOUS position's file, and keeping
them would feed the near-EOF/displacement heuristics another programme's
size (a metadata probe near the new file's EOF could displace live
playback, or a genuine scrub could be misjudged as a probe). The next
successful open repopulates both.
- Guard the update under the pool lock and skip it when the entry has
vanished (Redis restart/eviction) — a bare HSET would resurrect a
partial, TTL-less hash that answers every later request for that
session_id with 503.
- Align the reuse-path timezone lookup with the fresh path
(is_active=True on the default-profile filter).
Three more tests: byte state dropped on move / kept on same-position
update, vanished entry never resurrected, tz fallback to the reserved
profile when no active default exists.
Clients (TiviMate) keep the ?session_id= query when they rebuild the seek
URL with a new start timestamp, so every timestamp-jump FF/RW landed on
the reused session's STORED provider_timestamp — playback snapped back to
the position the session was created for (the rewind anchor), 100%
reproducible: two requests with different start values on one session
returned byte-identical streams.
The reuse path now always recomputes the provider timestamp from the
REQUESTED one (the provider zone is a property of the account — read from
the default profile's server_info, same as the fresh path) and moves the
pool descriptor (media_id + provider_timestamp) to the position actually
served, so fingerprint matching and same-channel displacement keep
comparing against reality. Slot continuity is unchanged.
Regression tests: reused session serves the requested timestamp (unit),
descriptor follows the seek (unit), and an end-to-end timestamp-jump with
the same session_id reaches the new position through timeshift_proxy.