An intelligent Xtream Codes API proxy that inspects actual video/audio streams using FFprobe and HLS manifest parsing to filter channels based on their real audio tracks and languages.
π‘ Why this proxy is different:
Unlike traditional IPTV proxies that only filter text/titles via Regex,xtream-filter-proxyperforms Deep Stream Inspection. It actively verifies the actual underlying audio streams (e.g., German, English, Multi-Audio) before delivering content to your player.
Works with any Xtream-Codes client, e.g. Smarters Pro, TiviMate, or anything else offering an "Xtream Codes API" login type. Nothing player-specific is required.
Note: This project only filters and forwards what your own provider account already serves you. It contains no content, no provider list, and no way to access anything you aren't already paying for.
- Active Audio Track Detection: Uses FFprobe / Stream Probing to identify and filter by real audio tracks (
ger,eng,und, etc.), bypassing incomplete or wrong provider title tags. - Xtream Codes API Compatibility: Works seamlessly as a drop-in proxy between your IPTV provider and any Xtream-compatible player (TiviMate, IPTV Smarters, etc.).
- Smart Caching & Probing: Analyzes streams in the background to minimize server load and avoid connection limits with your provider.
- Custom Regex & Title Filtering: Optional traditional filtering by categories, channel names, and tags.
- How it works
- Quick start (Docker)
- Raspberry Pi setup
- Connecting your player
- Configuration
- Web UI
- How audio-language detection works (and its limits)
- Running without Docker
- Updating
- Troubleshooting
- Tests
- API Requests: The proxy intercepts requests from your IPTV client to the Xtream Codes API.
- Stream Probing: Instead of relying solely on channel titles like
|DE|or[EN], the proxy fetches stream headers and analyzes audio metadata using FFprobe / HLS manifest parsing. - Filtering: Channels without the requested audio languages/tracks are hidden or filtered out automatically.
- Playback: Cleaned stream URLs are passed to your IPTV client.
- You point your player at this proxy's URL instead of the provider's.
- List endpoints (
get_live_streams,get_vod_streams,get_series, and the*_categoriesvariants) are served entirely from a local SQLite cache and filtered in-process. No upstream request happens on these calls, so browsing stays fast even with 150k+ VOD entries. - A background sync job periodically refreshes that cache from the provider's list endpoints and detects new/removed titles.
- A crawler worker fetches per-title audio-track metadata
(
get_vod_info/get_series_info, with an optionalffprobefallback) so the language filter has something to match against. - Stream playback (
/live/,/movie/,/series/) is a 302 redirect to the real provider URL β no video traffic passes through the proxy, so it adds no bandwidth cost and no transcoding load. - Login and unrecognized actions are passed through to upstream unmodified, so player features you don't filter keep working.
ββββββββββββββ Xtream API ββββββββββββββββββββ Xtream API ββββββββββββ
β Player β βββββββββββββββΊ β Filter Proxy β ββββββββββββββΊ β Provider β
β (Smarters β βββββββββββββββ β (this project) β ββββββββββββββ β β
β TiviMate) β filtered lists β SQLite + rules β full lists ββββββββββββ
ββββββββββββββ ββββββββββββββββββββ
β β²
ββββββββββββββ video streams (302 redirect, direct) βββββββββββββββ
Requirements: Docker with the Compose plugin.
git clone https://github.com/stefan-werning/xtream-filter-proxy.git
cd xtream-filter-proxy
mkdir -p data
cp config.example.yaml data/config.yaml
# edit data/config.yaml: set upstream.base_url / username / password
docker compose up -dOpen http://<host>:8080/ for the Web UI.
The container reads its config from data/config.yaml (the data/
directory is a bind mount, so the config and the SQLite database live on the
host and survive rebuilds). If that file doesn't exist on first start, the
bundled example is copied there automatically β but then you still have to
put your real credentials in and restart.
data/andconfig.yamlare git-ignored, so your credentials never end up in a commit.
Runs comfortably on a Pi 3, 4 or 5 with 64-bit Raspberry Pi OS (Debian-based). A Pi 3 with 1 GB RAM is enough β the proxy itself is light; only the initial crawl takes a while (see the note at the end).
1. Install Docker (skip if already present):
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker "$USER"Log out and back in (or reboot) so the group change takes effect. Verify
with docker ps β it should work without sudo.
2. Clone and configure:
git clone https://github.com/stefan-werning/xtream-filter-proxy.git
cd xtream-filter-proxy
mkdir -p data
cp config.example.yaml data/config.yaml
nano data/config.yaml # set upstream.base_url / username / password3. Build and start:
docker compose up -dThe first build takes roughly 5β10 minutes on a Pi 3 (it installs
ffmpeg for the ffprobe fallback) and a couple of minutes on a Pi 4/5.
Later rebuilds are much faster thanks to Docker's layer cache.
4. Verify:
docker compose logs -f # Ctrl-C to stop following
curl -s localhost:8080/api/crawler/statusThen open http://<pi-ip>:8080/ from any device on your network.
- Autostart is already handled:
restart: unless-stoppedindocker-compose.ymlbrings the container back after a reboot or crash. - Wired Ethernet is preferable to Wi-Fi for a device that's syncing catalogs and (optionally) probing streams around the clock, but Wi-Fi works.
- Use a decent power supply. A Pi 3 wants a solid 5 V / 2.5 A supply; undervoltage shows up as random SD-card corruption, which is not fun with a database on it.
- Shutdown is graceful by design.
stop_grace_period: 140sindocker-compose.ymlmatches uvicorn's--timeout-graceful-shutdown 130sodocker compose downnever kills the crawler mid-probe. That matters if your provider only allows one concurrent connection: a hard kill can leave that single slot stuck as "in use" upstream for a while. - The initial crawl is slow, on purpose. With
max_connections: 1the crawler probes one title at a time and waits for the connection slot; tens of thousands of titles can take days of wall-clock time. Usecrawl_scheduleto confine it to hours you don't watch TV, and use the Categories tab: Excluded and Always deliver categories are both skipped by the crawler, so narrowing down to the categories that actually need probing is by far the biggest speedup available.
In your player, add a playlist/profile of type Xtream Codes API (the exact wording differs per app) with:
| Field | Value |
|---|---|
| Server / Portal URL | http://<host>:8080 |
| Username | anything (see below) |
| Password | anything (see below) |
About the credentials: the proxy does not verify them. It always
authenticates upstream using the credentials from config.yaml. Any values
work β using your real provider credentials just keeps things recognizable.
All settings live in config.yaml. See
config.example.yaml for the full annotated
reference.
Most settings are also editable from the Web UI's Settings page. The UI writes changes back to the very same file β there is no second, hidden configuration store. Filter changes apply immediately; crawler timing parameters take effect on the worker's next cycle.
The listen address and port are not taken from config.yaml β they come
from how uvicorn is started (the ports: mapping in docker-compose.yml, or
the --port flag when running directly).
| Section | What it controls |
|---|---|
upstream |
Provider URL, credentials, timeout, user-agent |
database |
SQLite file path |
ffprobe |
Enable/disable the ffprobe fallback prober (on by default), its timeout_seconds, binary path |
crawler |
request_delay_seconds, reserve_slots (set 0 on a single-connection account or ffprobe never runs), ffprobe_cooldown_seconds (min gap between ffprobe runs so the panel can free the connection β raise if you see repeated error (exit code 1)), slot_recheck_seconds, sync_interval_minutes, retention (purge_after_days) and log rotation (log_max_age_days, log_max_rows) |
crawl_schedule |
Day/time windows the crawler may run in (or enabled: false to run continuously) |
title_filters |
Per-kind (live/vod/series) include/exclude regex lists, matched against the title (optionally also the category name) |
category_filters |
Per-kind excluded_ids (hidden, not probed) and always_deliver_ids (delivered as-is, filters skipped, not probed) β easiest to manage from the Categories tab |
audio_filters |
Per-kind (vod/series only) include/exclude regex matched against each probed audio track, plus on_unknown |
- An item passes if it matches at least one
includepattern (orincludeis empty) and matches noexcludepattern. excludealways wins overinclude.- All regexes are case-sensitive β write
(?i)inline if you want case-insensitive matching, e.g.(?i)\bgerman\b. - Invalid regexes are rejected on save, with an error message.
audio_filters.on_unknowndecides what happens to titles the crawler hasn't successfully probed yet:keep(default) β show them until proven otherwise. Nothing disappears by accident while the crawl is still running.dropβ show only titles with a confirmed matching audio track. Gives a clean list immediately, but hides a lot until the crawl catches up.
Reachable at http://<host>:8080/.
- Dashboard β crawler status, per-kind progress, ETA, pause/resume,
manual sync, retry failed probes, reset all probes, recent log with
explanations of each status term. Updates live over Server-Sent Events
(
/api/events) β status changes and new log lines appear as the crawler produces them, with no polling. Falls back to polling if the SSE connection can't be established. - Settings β upstream, crawler tuning (incl. an "Advanced timing"
section for the ffprobe/slot knobs), crawl schedule, all filter regex
fields. Everything here writes straight back to
config.yaml; the retention/log-rotation knobs are file-only. - Categories β set each category to Filtered (normal), Excluded (hidden, not probed), or Always deliver (every title delivered as-is, filters skipped, not probed β for a category you know is all in your language, e.g. a provider's "DE - β¦" section).
- Filter Preview β try a title/audio regex combination against the live cache and see counts plus sample titles from both sides, without touching the saved config.
- Catalog β searchable table of cached items with probe status and
detected audio tracks. Re-probe a single item, or β with a status filter
active β re-probe all matching items at once (e.g. every
no_audio_infoonce the provider's metadata has improved; confirmed results for other statuses are left alone). Also "always show" overrides that bypass all filters for a specific title. - Delivered List β exactly what your player receives right now for the selected type, after all filters. Useful for verifying that a filter did what you expected.
After changing filters, refresh the playlist inside your player β these apps cache the catalog locally and won't pick up changes until you do.
This is the most fragile part of the system, by nature of the ecosystem it talks to. Two sources are used, in this order:
- The provider API (
get_vod_info/get_series_info) β fast, no connection slot needed, but not always present or complete. ffprobefallback (on by default; needs theffprobebinary and a free connection slot) β opens a real connection to the stream and reads its header. Accurate, but slow and it occupies one of your account's connection slots while it runs. Turn it off in Settings if you'd rather rely on API metadata only.
Known limitations:
- Not all panels expose audio metadata via the API. Many omit
audio/streamsentirely. Ifffprobeis off, or can't get a usable stream URL for the title (some series responses carry no episode list), such items fall back toon_unknownβ ano_audio_inforesult then means "couldn't determine", not "definitely has no audio". - Language tags are free text set by the uploader β
ger,deu,german,Deutsch, or nothing at all. The regex approach exists precisely so you can adapt to whatever vocabulary your provider uses; there is no universal normalization. - Series are probed once, using the first episode with usable audio info; the result is applied to the whole series. If audio composition genuinely varies episode to episode, this can miss it.
- ffprobe consumes a real provider connection for the duration of the
probe. On accounts with a low
max_connections(some issue exactly one), the crawler's slot check (reserve_slots) may correctly refuse to probe rather than risk interrupting a live stream β that's intentional, not a bug. - Some API responses are incomplete rather than absent β e.g. one audio
track reported when the file actually has several. When the API result
doesn't match your include rules, the crawler deliberately re-checks with
ffprobe instead of trusting it (status
deferredin the UI).
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp config.example.yaml config.yaml
# edit config.yaml
uvicorn app.main:app --host 0.0.0.0 --port 8080 --timeout-graceful-shutdown 130Requires Python 3.11+. The ffprobe fallback is on by default; install
ffprobe (from FFmpeg) for it, or set ffprobe.enabled: false. The app
detects the binary at runtime and skips that step if it's missing, so a
missing ffprobe is harmless β you just lose the fallback. (The Docker
image bundles it.)
Run a single uvicorn worker (the default). The crawler and the live
dashboard updates assume one process; with --workers N > 1 you'd get N
crawlers fighting over the connection slot and the SSE stream would only
see events from whichever worker handled a given request.
Convenience scripts are included: ./start.sh, ./stop.sh, ./restart.sh.
They run the app on port 8099 by default (override with PORT=8080 ./start.sh) and write logs to data/uvicorn.log. stop.sh waits for a
running probe to finish rather than killing it, for the connection-slot
reason described above.
cd xtream-filter-proxy
git pull
docker compose build
docker compose up -dYour data/ directory (config + database) is untouched by this.
Nothing in the database is irreplaceable β items/categories come back
on the next sync from the provider β but the crawl results (which
titles have been probed, and their audio tracks) are not: on a
single-connection account, re-probing tens of thousands of titles takes
days. Worth backing up, especially on a Pi where the SD card is the
weak point.
backup.sh takes a consistent snapshot with sqlite3 β¦ "VACUUM INTO" β
one atomic read transaction, already compacted, and it never writes to the
live DB or its WAL, so it's safe to run while the crawler is going.
It then integrity-checks and gzips the snapshot (~30β50 MB), keeps a few
copies locally, and optionally pushes to a remote target (skipped silently
if unreachable).
Needs the sqlite3 CLI (apt install sqlite3).
cp backup.env.example backup.env
# edit backup.env: set REMOTE_DIR (an already-mounted path), or
# SMB_HOST / SMB_SHARE / SMB_SUBDIR for a CIFS share.
./backup.sh # test run β check data/backup.logThen add it to cron (as the user that owns data/):
15 4 * * * /home/you/xtream-filter-proxy/backup.shThe SMB push mounts the share per run, so the cron user needs passwordless
sudo mount / sudo umount / sudo mkdir. On a dedicated box, a line in
/etc/sudoers.d/:
you ALL=(root) NOPASSWD: /usr/bin/mount, /usr/bin/umount, /usr/bin/mkdir
Older NAS boxes (e.g. a Synology DS21x) only offer guest access over SMB1
β set SMB_OPTS=guest,vers=1.0,uid=1000,gid=1000 in that case. SMB1 is
fine on a trusted LAN for a backup target.
To restore: stop the container, gunzip -c proxy-YYYYMMDD-HHMMSS.db.gz > data/proxy.db (remove any stale proxy.db-wal / proxy.db-shm), start
again.
The player still shows the unfiltered list.
These apps cache aggressively. Refresh the playlist in the app; if that
doesn't help, delete the playlist entry and re-add it. Check the proxy log
(docker compose logs) β if you see only a bare player_api.php login and
no get_vod_streams call, the player is serving you its own cache and never
asked the proxy for the catalog.
Everything is hidden / the list is nearly empty.
Most likely audio_filters.on_unknown: drop combined with a crawl that
hasn't finished yet. Switch to keep, or check the Dashboard's "hidden by β¦"
breakdown to see which filter stage is removing items.
The crawler says outside window.
That's crawl_schedule doing its job β it's outside the configured hours.
Adjust the window in Settings, or set schedule to off.
Probes keep failing with error.
Usually a busy or unreachable provider. They're retried automatically with
backoff; "Retry failed probes" on the Dashboard requeues them all at once.
Everything is slow on a Pi.
Expected during the initial crawl. Exclude categories you don't need, and
confine crawling to off-hours via crawl_schedule.
source .venv/bin/activate
pip install -r requirements.txt pytest pytest-asyncio
python -m pytest tests/ -vtests/test_audio_parser.py covers the audio-track parser against several
realistic (and some deliberately malformed) get_vod_info response shapes,
since that parsing code is the most error-prone part of the project.