XtreamFilter is a Docker-first Xtream Codes proxy and media workflow tool for IPTV libraries. It lets you combine multiple providers, apply per-source filters, expose merged or dedicated Xtream endpoints, browse the resulting catalog in a web UI, organize content with custom categories, monitor series and movies, and download VOD or episodes to local storage.
- Full Xtream Codes proxy for merged and per-source playback
- Multi-source support with dedicated routes per provider
- Merged playlists and virtual IDs to avoid source collisions
- Optional stream proxy mode to hide upstream URLs from clients
- Per-source filters for live, VOD, and series groups/channels
- Web UI for source management, browsing, downloads, categories, and monitoring
- Rich Browse page with search, groups, source filters, sorting, ratings, and preview playback
- Manual and automatic custom categories with pattern matching, recent-content filters, and Telegram notifications
- Download cart for movies and series episodes with progress tracking and retry tools
- Jellyfin/Kodi-friendly output with
.nfofiles and poster artwork - Series monitoring for new episodes, scoped seasons, full-series capture, and backfill
- Movie monitoring by title or TMDB ID with per-source and per-category restrictions
- Download scheduling, throttling, player-profile emulation, and Telegram download notifications
- Version check endpoint and in-app update awareness
docker run -d \
--name xtreamfilter \
-p 8080:5000 \
-v ./data:/data \
-v ./downloads:/downloads \
--restart unless-stopped \
-e TZ=Europe/Paris \
-e PUID=1000 \
-e PGID=1000 \
spanishst/xtreamfilter:latestservices:
xtreamfilter:
image: spanishst/xtreamfilter:latest
container_name: xtreamfilter
ports:
- "8080:5000"
volumes:
- ./data:/data
- ./downloads:/downloads
restart: unless-stopped
environment:
- TZ=Europe/Paris
- PUID=1000
- PGID=1000Start it with:
docker compose up -dgit clone https://github.com/SpanishST/xtreamfilter.git
cd xtreamfilter
docker compose up --build -d- Open
http://localhost:8080 - Add one or more Xtream sources
- Configure the dedicated route for each source
- Adjust filters for live, VOD, and series as needed
- Refresh the cache
- Use the displayed Xtream or M3U URLs in your IPTV client
The container supports PUID and PGID environment variables to control file ownership on mounted volumes.
On startup the entrypoint script:
- Remaps the internal
appuserto the specified UID/GID - Creates
/dataand/downloadsif they do not exist - Runs
chown -Ron/datato fix ownership (important when upgrading from a version that ran as root) - Runs
chown(non-recursive) on/downloadsso the directory itself is writable - Checks that
/downloadsis writable by the target user - Drops privileges via
gosubefore starting the application
The /downloads directory is only chowned non-recursively to stay SMB/NFS safe. Existing files inside are not touched. If some files are root-owned from a previous install, fix them manually:
sudo chown -R 1000:1000 ./downloadsMatch them to the owner of the host directories:
# Find the UID/GID of your host user
id -u # UID
id -g # PGIDThen pass them to the container:
environment:
- PUID=1000
- PGID=1000If you previously ran the container without PUID/PGID (or as root), existing files under /data will be owned by root. The entrypoint automatically runs chown -R on /data on every start to reassign ownership to the configured PUID/PGID. For large databases this may cause a brief delay on first startup after the change.
The /downloads directory itself is chowned non-recursively so it stays writable. Existing root-owned files inside are not touched. If needed, fix them manually:
sudo chown -R 1000:1000 ./downloadsIf chown fails (for example on NFS with root_squash or a read-only bind mount) the entrypoint tests whether the target user can write to /data. If not, the container falls back to running as root so it can still start. Check docker logs for a warning if you suspect this is happening.
Make sure the host directories you mount are writable by the target UID/GID:
# Example: grant ownership to UID 1000
sudo chown -R 1000:1000 ./data ./downloadsEach provider is configured as a separate source with:
- Name
- Host
- Username and password
- Dedicated route
- Optional group prefix
- Maximum connections
- Per-source filters
Dedicated routes are important because upstream providers can reuse the same IDs for streams, VOD items, or series.
XtreamFilter exposes content in two ways:
- Merged access: all enabled sources appear under one virtualized endpoint
- Dedicated access: each source keeps its own Xtream-compatible endpoint
Runtime data is stored under /data, including:
config.jsonfor source and option settingsapp.dbfor cache indexes, categories, monitoring state, and related persisted data- cache and download metadata used by the UI and background jobs
Recommended when you want one combined playlist across all enabled sources.
Server: http://YOUR_SERVER_IP:8080/merged
Username: proxy
Password: proxy
Filtered endpoint:
Server: http://YOUR_SERVER_IP:8080/<route>
Username: <provider username>
Password: <provider password>
Unfiltered endpoint:
Server: http://YOUR_SERVER_IP:8080/<route>/full
Username: <provider username>
Password: <provider password>
| URL | Description |
|---|---|
/playlist.m3u |
Merged playlist using virtual IDs |
/<route>/playlist.m3u |
Filtered playlist for one source |
/<route>/full/playlist.m3u |
Unfiltered playlist for one source |
When proxy mode is enabled, clients stream through XtreamFilter instead of receiving upstream redirect URLs directly.
- Hide upstream server URLs from clients
- Centralize stream access through one server
- Improve playback stability on problematic sources
- Keep client configuration simple when upstream URLs change
curl http://localhost:8080/api/options/proxy
curl -X POST http://localhost:8080/api/options/proxy \
-H "Content-Type: application/json" \
-d '{"enabled": true}'Filters are configured per source and per content type.
livevodseries
groupschannels
| Mode | Description |
|---|---|
contains |
Value appears anywhere |
not_contains |
Value must not appear |
starts_with |
Name begins with value |
ends_with |
Name ends with value |
exact |
Exact match |
regex |
Regular expression |
all |
Match everything |
- Exclude all, then whitelist selected content
- Keep only specific streaming-service groups
- Exclude adult content globally
- Apply separate rules for live vs VOD vs series
The application builds and refreshes a local cache of upstream categories and content.
- Default cache TTL:
3600seconds - Refresh runs in the background
- Progress is visible in the UI
- Cache survives restarts
- Automatic categories refresh after cache refresh
- Last refresh time
- Next refresh estimate
- Cache validity
- Per-source item counts
- Refresh progress and current step
The Browse page is available at /browse.
- Search by title
- TMDB-prefixed search such as
tmdb:12345 - Filter by content type, source, group, and custom category
- Rating and recency filters for VOD and series
- Sorting for catalog exploration
- Group dropdown populated dynamically from the current source/type selection
- Built-in playback preview for live, VOD, and playable episodes
- Add-to-cart and add-to-category actions directly from the result cards
- Browse-from-monitor links for both series and movies
The built-in player supports:
- MPEG-TS and HLS playback
- Audio track switching when available
- Keyboard shortcuts
- Episode playback from the series browser
Custom categories help you organize content across all sources.
Manual categories are curated item by item.
Typical flow:
- Create a category in manual mode
- Select accepted content types
- Browse the catalog and use the
+button to link items - Use the same control again to unlink them later
Automatic categories are built from pattern rules.
You can configure:
- Category name and icon
- Accepted content types
- Pattern list
- Pattern logic:
andoror - Recently-added window
- Whether source filters should also apply
- Telegram notification per category
Automatic categories refresh when cache refresh runs, and can also be refreshed manually.
- New movies from the last 7 days
- 4K content
- Provider-specific highlights
- Hand-picked favorites
- Monitoring-only custom channels used to restrict movie or series checks
Telegram is used for:
- automatic category notifications
- monitoring notifications
- optional download notifications
Jellyfin can also be used to trigger a full library scan after successful downloads.
- Create a bot with @BotFather
- Get your chat ID
- Configure token and chat ID in the UI or API
- Send a test notification
| Endpoint | Method | Description |
|---|---|---|
/api/config/telegram |
GET |
Get Telegram settings with masked token |
/api/config/telegram |
POST |
Update Telegram settings |
/api/config/telegram/test |
POST |
Send a basic test notification |
/api/config/telegram/test-diff |
POST |
Send a sample category-style notification |
| Endpoint | Method | Description |
|---|---|---|
/api/config/jellyfin |
GET |
Get Jellyfin settings with masked API key |
/api/config/jellyfin |
POST |
Update Jellyfin base URL, API key, and trigger settings |
/api/config/jellyfin/test |
POST |
Validate the configured Jellyfin connection |
The download workflow is exposed through the Browse page and the Cart page.
- Single VOD movie
- Single series episode
- Full season
- Entire series
Downloads are persisted and processed sequentially.
Item states include:
queueddownloadingcompletedfailedcancelledmove_failed
- Retry failed or cancelled items
- Retry all failed items
- Resume move step when the temp-to-final move failed
- Track current speed, ETA-related speed, and pause state
- Optional Telegram notifications when queueing or completing downloads
- Optional Jellyfin full-library refresh after completed downloads
- Duplicate prevention for active queued/downloading entries
- Crash recovery for interrupted download state
Download options are configurable from the UI and API:
- Bandwidth limit
- Periodic pause interval and pause duration
- Player-profile emulation
- Burst reconnect behavior
- Per-day download schedule windows
If a download schedule is enabled, automatic monitoring downloads wait until the configured window is open.
Downloads are organized into media-library-friendly folders and include metadata.
Example layout:
<download_path>/
├── Films/
│ └── Movie Name/
│ ├── Movie Name.mp4
│ ├── Movie Name.nfo
│ └── poster.jpg
└── Series/
└── Show Name/
├── tvshow.nfo
├── poster.jpg
├── S01/
│ ├── Show Name S01E01 - Episode Title.mkv
│ └── Show Name S01E01 - Episode Title.nfo
└── S02/
└── Show Name S02E01 - Episode Title.mp4
| Endpoint | Method | Description |
|---|---|---|
/api/cart |
GET |
List cart items |
/api/cart |
POST |
Add movie, episode, season, or full-series items |
/api/cart/{item_id} |
DELETE |
Remove one item |
/api/cart/{item_id}/retry |
POST |
Retry a failed, cancelled, or move-failed item |
/api/cart/{item_id}/move |
POST |
Retry only the final move step for a move_failed item |
/api/cart/retry-all |
POST |
Retry all failed/cancelled/move-failed items |
/api/cart/clear |
POST |
Clear items by mode |
/api/cart/start |
POST |
Start the worker manually |
/api/cart/cancel |
POST |
Request cancellation of the active download |
/api/cart/status |
GET |
Queue and active-download status |
/api/cart/active-source-downloads |
GET |
Count active downloads by source |
/api/cart/series-episodes/{source_id}/{series_id} |
GET |
Fetch season/episode structure for a series |
| Endpoint | Method | Description |
|---|---|---|
/api/options/download_path |
GET, POST |
Get or set the final download directory |
/api/options/download_temp_path |
GET, POST |
Get or set the temp directory |
/api/options/test_path |
POST |
Validate write access to a path |
/api/options/download_throttle |
GET, POST |
Get or set throttling, pause, and profile options |
/api/options/player_profiles |
GET |
List supported player profiles |
/api/options/download_notifications |
GET, POST |
Get or set Telegram download notification options |
/api/options/download_schedule |
GET, POST |
Get or set the day-by-day download schedule |
Jellyfin refresh settings are available from the main Settings page and use Jellyfin's server-wide /Library/Refresh API.
The Monitor page is available at /monitor and includes separate tabs for series and movies.
Series monitoring is designed for new-episode detection and optional download automation.
new_only: snapshot the current set as known and only react to future episodesseason: watch one season onlyall: treat all discovered episodes as candidates
downloadnotifyboth
- Multi-source matching
- Optional restriction to selected sources
- Optional restriction to custom categories used as monitoring channels
- Episode preview grouped by season
- Enable or disable without deleting the monitor entry
- Backfill support when editing an existing monitor entry
Backfill can queue:
- all known episodes
- all known episodes from one season
- no backfill
Movie monitoring watches for a VOD title to become available.
- Add a movie from the Monitor page
- Search by title or
tmdb:<id> - Optionally rename the local canonical title used for downloads
- Restrict the search to selected sources
- Optionally restrict those sources to selected VOD categories
- Optionally restrict matching further through custom categories
- Choose whether the result should notify, download, or do both
When a matching movie is found, it is marked as found or downloaded depending on the configured action and whether it was queued or already present on disk.
Monitoring checks run:
- after cache refresh in the background loop
- when manually triggered through
/api/monitor/check
| Endpoint | Method | Description |
|---|---|---|
/api/monitor |
GET |
List monitored series |
/api/monitor |
POST |
Add a monitored series |
/api/monitor/{id} |
PUT |
Update a monitored series |
/api/monitor/{id} |
DELETE |
Delete a monitored series |
/api/monitor/{id}/episodes |
GET |
Preview detected episodes and their status |
/api/monitor/series-meta/{source_id}/{series_id} |
GET |
Fetch series metadata used by the UI |
/api/monitor/check |
POST |
Trigger a manual monitoring run |
/api/monitor/movies |
GET |
List monitored movies |
/api/monitor/movies |
POST |
Add a monitored movie |
/api/monitor/movies/{movie_id} |
PUT |
Update a monitored movie |
/api/monitor/movies/{movie_id} |
DELETE |
Delete a monitored movie |
/api/monitor/movie-lookup |
GET |
Search the VOD cache by title or TMDB ID for movie setup |
/api/monitor/custom-categories |
GET |
List custom categories usable as monitoring channels |
/api/monitor/vod-categories |
GET |
List VOD categories grouped by enabled source |
| Route | Description |
|---|---|
/merged/player_api.php |
Merged Xtream API for all enabled sources |
/merged/live/{username}/{password}/{stream_id} |
Merged live stream route |
/merged/movie/{username}/{password}/{stream_id} |
Merged movie stream route |
/merged/series/{username}/{password}/{stream_id} |
Merged series stream route |
/player_api.php |
Root Xtream API helper route |
/full/player_api.php |
Root unfiltered Xtream helper route |
/{source_route}/player_api.php |
Filtered Xtream API for one dedicated source |
/{source_route}/full/player_api.php |
Unfiltered Xtream API for one dedicated source |
/{source_route}/playlist.m3u |
Filtered M3U playlist for one source |
/playlist.m3u |
Merged M3U playlist |
/merged/xmltv.php |
Merged XMLTV/EPG output |
| Route | Description |
|---|---|
/ |
Main configuration UI |
/browse |
Catalog browser |
/cart |
Download cart and queue UI |
/monitor |
Series and movie monitoring UI |
| Route | Method | Description |
|---|---|---|
/health |
GET |
Liveness check |
/api/version |
GET |
Current version, latest release, and update availability |
| Endpoint | Method | Description |
|---|---|---|
/api/sources |
GET |
List sources |
/api/sources |
POST |
Create a source |
/api/sources/{source_id} |
GET |
Get one source |
/api/sources/{source_id} |
PUT |
Update one source |
/api/sources/{source_id} |
DELETE |
Delete one source |
/api/sources/{source_id}/filters |
GET |
Get source filters |
/api/sources/{source_id}/filters |
POST |
Replace source filters |
/api/sources/{source_id}/filters/add |
POST |
Add one filter rule |
/api/sources/{source_id}/filters/delete |
POST |
Delete one filter rule |
| Endpoint | Method | Description |
|---|---|---|
/api/cache/status |
GET |
Cache status and counts |
/api/cache/refresh |
POST |
Trigger a background refresh |
/api/cache/cancel-refresh |
POST |
Clear refresh state |
/api/cache/clear |
POST |
Clear the cached data |
| Endpoint | Method | Description |
|---|---|---|
/groups |
GET |
Lightweight group list for a content type and source |
/channels |
GET |
Lightweight channel/item list |
/api/browse |
GET |
Main browse/search endpoint |
/api/browse/groups |
GET |
Group list with counts for current source/type |
| Endpoint | Method | Description |
|---|---|---|
/api/categories |
GET |
List categories with full data |
/api/categories |
POST |
Create a category |
/api/categories/summary |
GET |
Lightweight category list for nav and quick state |
/api/categories/{category_id} |
GET |
Get one category |
/api/categories/{category_id} |
PUT |
Update one category |
/api/categories/{category_id} |
DELETE |
Delete one category |
/api/categories/{category_id}/items |
POST |
Add an item to a manual category |
/api/categories/{category_id}/items/{content_type}/{source_id}/{item_id} |
DELETE |
Remove an item from a manual category |
/api/categories/refresh |
POST |
Refresh automatic categories |
| Endpoint | Method | Description |
|---|---|---|
/api/options |
GET, POST |
Get or update the options object |
/api/options/proxy |
GET, POST |
Get or set proxy mode |
/api/options/refresh_interval |
GET, POST |
Get or set background refresh interval |
Configuration is primarily stored in /data/config.json.
Important areas:
sources: provider definitions and per-source filterscontent_types: global enablement of live, VOD, and seriesoptions.proxy_streams: stream proxy toggleoptions.telegram: Telegram credentials and enablementoptions.jellyfin: Jellyfin base URL, API key, and refresh triggersoptions.download_pathandoptions.download_temp_path: file-system destinationsoptions.download_*: throttling, pause, profile, notifications, and scheduling
Example shape:
{
"sources": [
{
"id": "abc12345",
"name": "Provider A",
"host": "http://provider.example.com",
"username": "user",
"password": "pass",
"enabled": true,
"prefix": "[A] ",
"route": "providera",
"max_connections": 1,
"filters": {
"live": { "groups": [], "channels": [] },
"vod": { "groups": [], "channels": [] },
"series": { "groups": [], "channels": [] }
}
}
],
"content_types": {
"live": true,
"vod": true,
"series": true
},
"options": {
"proxy_streams": true,
"refresh_interval": 3600,
"telegram": {
"enabled": false,
"bot_token": "",
"chat_id": ""
},
"jellyfin": {
"enabled": false,
"base_url": "http://jellyfin:8096",
"api_key": "",
"trigger_file": true,
"trigger_queue": true
},
"download_path": "/downloads",
"download_temp_path": "/downloads/.tmp"
}
}Run locally without Docker:
pip install fastapi uvicorn[standard] httpx jinja2 python-multipart lxml rapidfuzz packaging aiosqlite
uvicorn app.main:app --host 0.0.0.0 --port 5000 --reloadThe application will then be available at http://localhost:5000.
uv run pytest tests/ -v

