Timeshift/catch-up TV plugin for Dispatcharr. Watch past TV programs (up to 7 days) via Xtream Codes providers.
Version: 1.1.9 GitHub: https://github.com/cedric-marcoux/dispatcharr_timeshift License: MIT
The easiest and most reliable way to install:
cd /path/to/dispatcharr/data/plugins/
git clone https://github.com/cedric-marcoux/dispatcharr_timeshift.git
docker compose restart dispatcharrThen enable the plugin in Dispatcharr Settings → Plugins.
Download dispatcharr_timeshift.zip from the Releases page, then import via Settings → Plugins → Import.
dispatcharr_timeshift-1.1.8/ or dispatcharr_timeshift-main/), which breaks Python imports. You must rename the folder:
cd /path/to/dispatcharr/data/plugins/
# If downloaded from a release tag (v1.1.8):
unzip dispatcharr_timeshift-1.1.8.zip
mv dispatcharr_timeshift-1.1.8 dispatcharr_timeshift
# If downloaded from main branch:
unzip dispatcharr_timeshift-main.zip
mv dispatcharr_timeshift-main dispatcharr_timeshift
# Fix permissions
chmod 644 dispatcharr_timeshift/*.py
chown 1000:1000 dispatcharr_timeshift/*
docker compose restart dispatcharrIf timeshift features don't appear after installation, your provider may not support timeshift (tv_archive). Check if your Xtream Codes provider offers catch-up/replay functionality.
- New feature: Catch-up Fallback Chain - Channels with multiple streams now support catch-up even when top-priority stream lacks it
- Previously: If top-priority stream (e.g., UHD) had no catch-up, entire channel was marked as unavailable
- Now: Plugin searches all streams in priority order to find one with
tv_archive=1 - IPTV clients now correctly see "Catch-up available" even when highest-quality stream lacks it
- Timeshift requests automatically use the catch-up-enabled stream (transparent to user)
- Maintains stream priority for live TV (top stream still preferred for live playback)
- Example: BBC One has Sky (no catch-up) and IPTV (with catch-up) → Users can now watch catch-up from IPTV stream
- Modified 3 functions across 2 files:
_patch_xc_get_live_streams(),_patch_xc_get_epg(),timeshift_proxy() - Backward compatible: Single-stream channels work identically
- Documentation: Improved installation instructions
- Method 1: Git clone (recommended) - most reliable method
- Method 2: Download release asset
dispatcharr_timeshift.zip(correct folder name) - Method 3: Manual ZIP with folder rename warning
- Explained why GitHub's default ZIP breaks Python imports (folder name includes version/branch)
- Bug fix: Export Plugin class in
__init__.py- Dispatcharr requires the Plugin class to be exported from
__init__.py - Without this, the plugin fails with "invalid plugin: missing plugin class"
- Added
from .plugin import Pluginand__all__ = ['Plugin']
- Dispatcharr requires the Plugin class to be exported from
- New feature: Debug Mode - Toggle in plugin settings to enable ultra-verbose logging
- Normal mode: Minimal logging (1 line per timeshift request + errors only)
- Debug mode: Detailed logs for every step (config loading, channel lookup, timestamp conversion, URL building, provider response)
- New feature: URL Format Selection - Choose between timeshift URL formats:
- Auto-detect (default): Tries Format A, falls back to Format B if 400 error
- Format A: Query string (
/streaming/timeshift.php?username=X&...) - Format B: Path-based (
/timeshift/user/pass/duration/time/id.ts) - Custom: User-defined template with placeholders
- New feature: Custom URL Template - For exotic providers with non-standard URLs
- Placeholders:
{server_url},{username},{password},{stream_id},{timestamp},{duration} - Only used when "Custom template" is selected in URL Format
- Placeholders:
- New feature: Timezone Dropdown - Provider timezone now uses a dropdown with 120 IANA timezone options
- Organized by region: UTC, Europe, Americas, Asia, Africa, Australia/Pacific
- Prevents typos and invalid timezone entries
- Code cleanup: Added
.strip()to all config values to prevent whitespace issues - Reduced log noise: Production logs now minimal, detailed info only in debug mode
- Bug fix: XMLTV EPG compatibility with Dispatcharr v0.14 (handles both HttpResponse and StreamingHttpResponse)
- Bug fix: Re-enabled UTC→Local timezone conversion for timeshift timestamps
- v1.1.4 incorrectly removed the conversion, causing wrong content to play
- Root cause: IPTV clients (iPlayTV, TiviMate, Televizo) use
start_timestamp(UTC unix timestamp) from EPG to construct timeshift URLs - The timestamp in the URL is therefore in UTC, but XC providers expect LOCAL time
- Now views.py correctly converts the timestamp from UTC to the configured timezone
- Example: User selects 18:00 Toronto show → Client sends 23:00 UTC → Plugin converts to 18:00 local → Provider plays correct content
Bug fix: Removed double timezone conversion- This was incorrect- This version broke timeshift for all non-UTC timezones
- Users experienced wrong content playing (offset by their timezone difference)
- Fixed in v1.1.5
- Bug fix: Timezone setting was not being read from database
- Plugin was using wrong attribute
config.configinstead ofconfig.settings - Timezone always defaulted to "Europe/Brussels" regardless of user setting
- Now correctly reads from Dispatcharr's PluginConfig.settings field
- Affects both timeshift URL conversion and EPG timestamp conversion
- Plugin was using wrong attribute
- Code cleanup: Removed dead code (
uninstall_hooks()and_restore_*()functions)- Dispatcharr never calls
plugin.run("disable"), so these functions were never executed
- Dispatcharr never calls
- Optimized diagnostics: Expensive DB queries in 404 handler now only run in DEBUG mode
- Reduces overhead on production systems
- Basic warning still logged at INFO level
- Minor fix: Removed unnecessary
if chunk:check in stream generator - Tested with Dispatcharr v0.14.0
- Dynamic EPG-based duration: Timeshift requests now use the actual programme duration from EPG
- Calculates duration from programme's
end_time - start_time - Adds 5-minute buffer for stream startup
- Falls back to 120 minutes if programme not found in EPG
- Caps at 8 hours maximum to prevent issues
- Prevents long movies from being cut off (was hardcoded to 2h)
- More efficient for short programmes (30-45 min)
- Calculates duration from programme's
- URL format fallback: Automatic detection and fallback for providers using different timeshift URL formats
- Format A (default):
/streaming/timeshift.php?username=X&password=Y&stream=Z&start=T&duration=N - Format B (fallback):
/timeshift/{username}/{password}/N/{timestamp}/{stream_id}.ts - Automatically tries Format B if Format A returns HTTP 400
- Caches working format per M3U account for session (no restart needed)
- Fixes "Provider returned 400" error for providers using path-based timeshift URLs
- Format A (default):
- Enhanced diagnostics: Improved "Channel not found" logging with detailed troubleshooting info
- Shows if stream exists but with wrong account type (need 'XC')
- Shows if stream has no channels assigned
- Shows total XC streams count (helps detect sync issues)
- Shows if channel exists but user lacks access level
- Bug fix: Fixed
AssertionError: .accepted_renderer not set on Responseerror- Replaced Django REST Framework
Responsewith DjangoJsonResponseinpatched_stream_xc() - This error occurred when channel lookup failed (404) or credentials were invalid (401)
- The issue was that DRF's Response requires a renderer, but the patched function is called from Django URL patterns, not through DRF's APIView
- Replaced Django REST Framework
- Enhanced logging: Improved error diagnostics with detailed context for troubleshooting
- API requests now log channel enhancement stats (e.g., "Enhanced 36/36 channels")
- EPG requests log channel lookups and program generation counts
- Authentication failures now specify the exact reason (missing xc_password, wrong password, unknown user)
- Provider errors include status code, content-type, and response body preview
- All errors include actionable diagnostic hints
Based on fixes from Lesthat's fork - thanks for the contributions!
- Multi-client support: Added compatibility for Snappier iOS and IPTVX
- EPG 404 fix: Fixed "not found" errors when clients request EPG data using provider stream IDs
- Data type fixes: Corrected JSON types for strict validation (Snappier iOS compatibility)
- EPG timezone fix: Programs now display at correct times (fixed +2h offset issue)
- XMLTV timezone: Converted timestamps for IPTVX compatibility
- Language setting: Configurable EPG language (27 European languages)
- Unique program IDs: Each program now has a timestamp-based unique ID
- User-Agent fix: Now uses the User-Agent configured in M3U account settings (TiviMate, VLC, etc.) instead of a hardcoded value
- Initial release
IMPORTANT: After enabling or disabling this plugin, you must refresh your source in your IPTV player (e.g., iPlayTV) for it to detect the timeshift/replay availability on channels.
- 100% Plugin Solution - No modification to Dispatcharr source code required
- Multi-Client Compatible - Works with iPlayTV, Snappier iOS, IPTVX, and other Xtream Codes clients
- Seek Support - Forward/rewind via HTTP Range headers
- Auto-install - Hooks install automatically on startup
- Hot Enable/Disable - Enable or disable without restarting Dispatcharr
- Timezone Conversion - Configurable timezone for accurate playback positioning
- Dispatcharr installed and running
- Xtream Codes provider with timeshift support (
tv_archive=1) - Channels synced with EPG data
- Copy
dispatcharr_timeshift/folder to Dispatcharr's/data/plugins/ - Restart Dispatcharr
- Enable in Dispatcharr UI: Settings > Plugins > Dispatcharr Timeshift
- Configure timezone if needed (defaults to Europe/Brussels)
Dispatcharr doesn't natively support timeshift. Adding this feature as a plugin presented several challenges:
- Catch-all URL pattern: Dispatcharr has a
<path:unused_path>pattern that catches all unmatched URLs - Stream ID mismatch: iPlayTV uses the
stream_idfrom the API for timeshift URLs, but Dispatcharr returns internal IDs - Timezone differences: iPlayTV sends UTC timestamps, but providers expect local time
- Multi-worker architecture: uWSGI runs multiple workers, each needs hooks installed
We use five monkey-patches to add timeshift without modifying Dispatcharr's source:
Problem: The Xtream Codes API response doesn't include tv_archive fields, so iPlayTV doesn't know which channels support timeshift.
Solution: Patch the function to:
- Add
tv_archiveandtv_archive_durationfrom stream'scustom_properties - Replace Dispatcharr's internal
stream_idwith the provider'sstream_id
# Before patch: stream_id = 42 (Dispatcharr internal ID)
# After patch: stream_id = 22371 (Provider's ID)
# tv_archive = 1
# tv_archive_duration = 7Problem: After changing stream_id to provider's ID in the API, live streaming breaks. iPlayTV requests /live/user/pass/22371.ts but Dispatcharr looks up Channel.objects.get(id=22371) which doesn't exist.
Solution: Patch stream_xc to first search by provider's stream_id in custom_properties, then fall back to internal ID lookup.
Additional Challenge: Simply patching the function in the module doesn't work because Django URL patterns keep a reference to the original function from import time. We must also update pattern.callback directly in urlpatterns.
Problem: After changing stream_id to provider's ID, EPG requests fail. Clients request EPG using provider's stream_id, but Dispatcharr looks up by internal ID.
Solution: Patch xc_get_epg to first search by provider's stream_id in custom_properties, then fall back to internal ID lookup. Also generates custom EPG with correct data types for strict clients like Snappier iOS.
Problem: IPTVX and some clients display EPG timestamps as-is without timezone conversion, causing programs to appear at wrong times.
Solution: Patch generate_epg to convert XMLTV timestamps from UTC to the configured local timezone.
Problem: Timeshift URLs like /timeshift/user/pass/155/2025-01-15:14-30/22371.ts are caught by Dispatcharr's catch-all pattern before any plugin URL can match.
Why Other Approaches Failed:
- URL pattern injection (
urlpatterns.insert) - Catch-all still matched - Middleware - Runs after URL resolution, too late
- ROOT_URLCONF replacement - Django caches settings at startup
Solution: Patch URLResolver.resolve() to intercept URLs BEFORE pattern matching happens.
iPlayTV Client
│
▼
/timeshift/user/pass/155/2025-01-15:14-30/22371.ts
│
▼
URLResolver.resolve [PATCHED] ─── Intercepts /timeshift/ URLs
│
▼
timeshift_proxy()
├── 1. Authenticate user (xc_password)
├── 2. Find channel by provider stream_id (22371)
├── 3. Check user access level
├── 4. Verify tv_archive support
├── 5. Convert timestamp UTC → Local timezone
├── 6. Get programme duration from EPG
└── 7. Proxy stream to client
│
▼
Provider: /streaming/timeshift.php?stream=22371&start=2025-01-15:14-30&duration={EPG_DURATION}
| Setting | Default | Description |
|---|---|---|
| Provider Timezone | Europe/Brussels | Timezone for timestamp conversion (IANA format) |
| EPG Language | en | Language code for EPG data (27 European languages available) |
| Debug Mode | Off | Enable ultra-verbose logging for troubleshooting |
| Catchup URL Format | Auto-detect | URL format for timeshift requests (see below) |
| Custom URL Template | (empty) | Custom URL with placeholders (only when "Custom" format selected) |
| Format | URL Pattern | When to Use |
|---|---|---|
| Auto-detect (default) | Tries A, falls back to B | Most providers - works automatically |
| Format A | /streaming/timeshift.php?username=X&password=Y&stream=Z&start=T&duration=N |
Standard XC providers |
| Format B | /timeshift/{user}/{pass}/{duration}/{timestamp}/{stream_id}.ts |
Some providers require this format |
| Custom | User-defined template | Exotic providers with non-standard URLs |
If your provider uses a non-standard URL format, select "Custom template" and use these placeholders:
| Placeholder | Value |
|---|---|
{server_url} |
Provider's server URL (without trailing slash) |
{username} |
M3U account username |
{password} |
M3U account password |
{stream_id} |
Provider's stream ID |
{timestamp} |
Programme start time (YYYY-MM-DD:HH-MM format, local timezone) |
{duration} |
Programme duration in minutes |
Example custom template:
{server_url}/catchup/{username}/{password}/{stream_id}/{timestamp}/{duration}.m3u8
iPlayTV sends timestamps in UTC (from EPG data), but Xtream Codes providers expect local time. Configure the timezone to match your provider's location.
Common values:
Europe/Brussels- Belgium, Western EuropeEurope/Paris- FranceAmerica/New_York- US EasternAmerica/Los_Angeles- US Pacific
- Open iPlayTV on Apple TV
- Add new source > Xtream Codes
- Configure:
- Server URL:
http://your-dispatcharr-ip:9191 - Username: Your Dispatcharr username
- Password: Your
xc_password(from user custom properties, NOT Django password)
- Server URL:
/timeshift/{username}/{password}/{epg_channel}/{timestamp}/{provider_stream_id}.ts
Example:
/timeshift/john/secret123/155/2025-01-15:14-30/22371.ts
Important: The parameter names are misleading due to how iPlayTV constructs URLs:
- Position 3 (
stream_idin pattern) = EPG channel number (NOT used) - Position 5 (
durationin pattern) = Provider's stream_id (USED for lookup)
Two different IDs are involved:
| ID Type | Example | Where Used |
|---|---|---|
| Dispatcharr Internal ID | 42 | Database primary key |
| Provider Stream ID | 22371 | Stored in stream.custom_properties.stream_id |
The plugin modifies the API to return provider's stream_id so iPlayTV builds correct timeshift URLs.
Dispatcharr runs with multiple uWSGI workers (separate processes). Each worker has its own memory space, so:
- Hooks must be installed in EACH worker independently
- The plugin auto-installs on first request to each worker
- Warm-up requests ensure all workers are ready (see Troubleshooting)
The plugin supports enabling/disabling without restarting Dispatcharr:
- Hooks are installed once at startup (regardless of plugin enabled state)
- Each hook checks the database
enabledflag at runtime before executing - When disabled, hooks pass through to original Dispatcharr functions
- No restart required - changes take effect immediately
Why this approach?
Dispatcharr's PluginManager only toggles the enabled flag in the database when you enable/disable a plugin. It does NOT call plugin.run("enable") or plugin.run("disable"). So we can't rely on those callbacks to install/uninstall hooks dynamically. Instead, hooks are always installed but check the enabled state per-request.
Note: After toggling the plugin, refresh your source in iPlayTV to see the updated channel list (with or without timeshift support).
dispatcharr_timeshift/
├── __init__.py # Package marker
├── plugin.py # Plugin metadata, settings, auto-install on startup
├── hooks.py # Five monkey-patches (API, live stream, EPG, XMLTV, URL resolver)
├── views.py # Timeshift proxy with timezone conversion
└── README.md # This file
The plugin proxies all streams through Dispatcharr. It does NOT pass the provider's catch-up URL directly to the client.
How it works:
- Client sends request to Dispatcharr:
/timeshift/user/pass/.../stream_id.ts - Plugin intercepts the request
- Dispatcharr fetches the stream from the XC provider
- Stream is proxied back to the client
VPN use case: If you need all XC provider traffic to go through a VPN, simply connect Dispatcharr to the VPN. All timeshift (and live) streams will be fetched through the VPN, while your clients connect directly to Dispatcharr without needing VPN apps.
Yes, but only the first stream (by priority order) determines timeshift availability. The plugin checks tv_archive from the first stream's custom_properties.
Timeshift will only appear if the first priority stream is from an Xtream Codes provider with tv_archive=1.
Example scenarios:
- Stream #1 is XC with timeshift → ✅ Timeshift appears
- Stream #1 is non-XC, Stream #2 is XC with timeshift → ❌ Timeshift does NOT appear
Important: For timeshift to actually work when playing, the XC stream with timeshift support must be the one being played. If a non-XC stream takes priority during playback, timeshift won't function even if shown in the channel list.
Recommendation: For channels where you want timeshift, ensure the XC stream with tv_archive=1 is set as the first priority stream.
No, catchup does not work when using Dispatcharr's M3U output.
Dispatcharr generates a "clean" M3U without catchup attributes:
#EXTINF:-1 tvg-id="10" tvg-name="|BE| LA UNE FHD" ...
http://dispatcharr:9191/proxy/ts/stream/uuid
For M3U-based catchup to work, the following attributes would be required:
catchup="default"
catchup-source="http://server/timeshift/user/pass/{stream_id}/{start}/{duration}.ts"
catchup-days="7"
The Timeshift plugin only works with:
- Xtream Codes API (
player_api.php) - addstv_archive=1to responses - Direct
/timeshift/...URL interception - IPTV clients that use the XC API (iPlayTV, TiviMate in XC mode, Snappier, etc.)
For M3U-based players like Emby Live TV, your options are:
- Use your provider's original M3U directly (bypass Dispatcharr for catchup)
- Connect via Xtream Codes API instead of M3U if the player supports it
- Request Dispatcharr to add catchup support in M3U export (feature request on their GitHub)
The Timeshift plugin patches the XC API layer, but Dispatcharr's M3U generator is a separate core component that doesn't include catchup metadata.
Ensure the "Provider Timezone" setting matches your provider's timezone. Most European providers use "Europe/Brussels" or similar. If programs appear 2 hours early or late, adjust the timezone setting accordingly.
This was fixed in v1.0.2. The issue occurred because clients use provider stream IDs for EPG requests, but Dispatcharr was looking up by internal IDs. Update to v1.0.2 or later.
Each uWSGI worker needs to install hooks on its first request. Warm up all workers:
for i in {1..10}; do curl -s http://localhost:9191/api/channels/ -o /dev/null; doneThe provider's stream_id must be stored in stream.custom_properties. This happens automatically during M3U sync for Xtream Codes providers. Try re-syncing your M3U account.
Check timezone configuration. If you request 13:00 news but get 11:00 content, the timezone offset is wrong. Adjust the "Provider Timezone" setting in plugin configuration.
This can happen if hooks aren't fully installed. The plugin patches both the stream_xc function AND the URL pattern callback. Restart Dispatcharr to ensure clean hook installation.
The plugin uses structured logging with different levels for easy troubleshooting:
# All timeshift logs
docker compose logs dispatcharr | grep -i timeshift
# Specific events
docker compose logs dispatcharr | grep "Timeshift.*API" # API enhancements (channel counts)
docker compose logs dispatcharr | grep "Timeshift.*EPG" # EPG lookups and generation
docker compose logs dispatcharr | grep "Timeshift.*Live" # Live stream lookups
docker compose logs dispatcharr | grep "Timeshift.*Request" # Incoming timeshift requests
docker compose logs dispatcharr | grep "Timeshift.*Auth" # Authentication issues
docker compose logs dispatcharr | grep "Timeshift.*Provider" # Provider communication errorsLog levels (Normal mode):
INFO: One line per timeshift request ([Timeshift] TF1 @ 2025-01-15:14-30)ERROR: Failures requiring attention (auth failed, channel not found, provider errors)
Log levels (Debug mode - enable in plugin settings):
- All of the above, plus:
- Detailed config loading
- Channel search steps (provider_stream_id lookup, internal_id fallback)
- Stream properties and tv_archive status
- Timestamp conversion details (UTC → Local)
- URL format selection and built URL
- Request headers and provider response status
- Each step is logged with
=== REQUEST START ===and=== REQUEST END ===markers
- Worker warm-up required: Each uWSGI worker must handle at least one request to install hooks
- XC providers only: Only works with Xtream Codes type M3U accounts
- EPG required for accurate duration: Without EPG data, falls back to 120 minutes
We explored several approaches before settling on monkey-patching:
- URL pattern injection - Failed because catch-all pattern matches first
- Middleware - Failed because it runs after URL resolution
- ROOT_URLCONF replacement - Failed because Django caches settings
- Django signals - No suitable signal for URL interception
- Monkey-patching URLResolver.resolve - Works!
When patching a view function, you must also patch the URL pattern's callback:
# This alone is NOT enough:
proxy_views.stream_xc = patched_stream_xc
# Must also update URL patterns:
for pattern in main_urls.urlpatterns:
if pattern.callback == _original_stream_xc:
pattern.callback = patched_stream_xcDjango resolves function references at import time and stores them in pattern.callback. Patching the module doesn't affect already-resolved patterns.
MIT License - See LICENSE file for details.