Ranks the streams behind your Dispatcharr channels, best first, by measuring them rather than trusting their names.
It fetches your channels and streams, works out which streams belong to which
channel, probes each one with ffprobe/ffmpeg, and writes the ranked order
back. One container, one volume.
One channel, its streams in the order Dispatcharr currently serves them, and
what each one actually is. The stream named 1080p at #2 measures 720p; the one
named UHD at #5 is a black screen; #6 is dead. Matches update as you type, and
nothing is written until you say so.
docker run -d -p 127.0.0.1:3456:3456 -v podium-data:/app/data \
-e DISPATCHARR_URL=http://dispatcharr:9191 \
-e DISPATCHARR_API_KEY=... \
ghcr.io/lpukatch/podium:latestThat publishes the port on loopback only. To reach Podium from another machine
drop the 127.0.0.1:, and read who can reach it
first — there is no login, and it holds a Dispatcharr credential.
Or docker compose up -d with the compose file in this
repo.
On first start it writes an empty rules file and comes up with nothing to check.
Open the UI on :3456, pick a group and start adding channels — or
import rules you already have.
If your lineups are already set and you only want them kept in the right order,
you do not have to describe them twice: set a group to
assigned and Podium ranks every channel in it
off the streams Dispatcharr has already given it, no rules to write.
Podium needs no credentials to boot. If you would rather not put an API key in your environment, start it bare and enter one in Settings.
Important
It starts in dry-run, wherever you run it. Reordering a channel is a write
with no undo, so Podium watches and records first. Set PODIUM_DRY_RUN=false
once you have seen a few passes you agree with.
Provider stream names lie. A stream advertised as [1080p] turns out to be
720p; one advertised as 1080p turns out to deliver 193kbps of black screen. The
only way to rank them honestly is to look.
Looking is expensive, though — every probe spends a connection you are paying for, against a provider that will cut you off for using too many at once. Most of Podium is about spending that budget well.
Provider lanes, not a channel queue. Providers have different concurrency limits. Checking channel-by-channel means one saturated provider stalls everything while the others sit idle, so Podium treats a stream as the unit of work and gives each provider its own lane. That takes hours off a full pass on a multi-provider catalogue. Extra logins saved as profiles on a Dispatcharr M3U account get lanes of their own too, and Podium pools them: their connection caps add up, and each stream is drawn by one login, so a 3-connection login beside a 2-connection one gets through the catalogue five streams at a time.
Caches against stream_hash. Provider stream sets barely change between
runs, so most probing is repeated for nothing. Podium re-probes only when
Dispatcharr's stream_hash changes or the verdict ages out. (updated_at is
useless for this: the M3U refresh bumps it on every stream every few minutes.)
Paces against a freshness target, not a clock. "Every channel checked within 24 hours, using only spare provider capacity, and stopping the moment somebody starts watching" is what you actually want. A nightly batch hammers every provider whether or not anyone is streaming, and if it overruns it is still going at breakfast.
The target is a floor, though, and the afternoon of a big game is when that shows: everything can be comfortably inside the window and still have been measured at four in the morning. A group — or the whole catalogue — can be queued for a re-check whatever the cache says. It queues rather than probes, so the pass that does the work is still the paced one, and calling it off puts the existing verdicts straight back in service.
All three show up on one page: each provider lane drains against its own limit, the library tiles say how much of the catalogue is still inside the freshness target, and channels whose EPG says the match has not kicked off yet are held back rather than probed.
Resolution, bitrate, fps and codec, scored and weighted, with your own rule order winning first. Dead streams sink to the bottom, and so do streams below a bitrate floor or showing a black screen — a slate is not a fallback.
Bitrate is measured, not read from the container: live TS/HLS almost never declares one. Podium reads a few seconds of the stream, which also gives it the black-screen check from the same read, so it costs one provider connection rather than two.
There is deliberately no loop detection. Catching a loop means watching for at least one loop period — around 120s per stream against the ~1s the other checks cost — for a failure far rarer than dead, black or throttled.
Podium has no login: it manages one Dispatcharr on your own network, and
accounts would be ceremony. It does refuse the two things that can reach it
without being on your network — a page you are visiting driving your browser at
your LAN address, and a hostname that resolves to it. So cross-site writes are
rejected, and it only answers to private addresses and names (plus anything you
put in PODIUM_ALLOWED_HOSTS).
That is not the same as being safe to expose. Anyone who can reach the port can
reorder your channels, and it holds a Dispatcharr credential. Keep it on your
network, and set PODIUM_AUTH_TOKEN if it goes anywhere else —
the details are here.
| guide | what is in it |
|---|---|
| Usage | Matching streams to channels, section qualifiers, group policy, ranking, and importing existing rules |
| Quality priors and the Teamarr export | Ranking a stream nobody has measured yet, from what its account and its name have been worth before — and exporting that as Teamarr scoring rules, or checking the rules you already run against it |
| Configuration | Every environment variable, who can reach it, concurrency limits, and how to keep a settled install quiet |
| Name miner | Learning regex rules from stream names instead of a hand-picked vocabulary — thresholds, guards, and the evidence behind them |
| Contributing | Development setup, and how PRs land given the mirror |
npm install
npm run dev # UI on :3456
npm run worker # the paced loop
npm run test:runffmpeg on PATH matters for the tests: without it they exercise the probe
parser but not the real ffprobe path.
Issues and pull requests are welcome. Development happens on a self-hosted Forgejo instance and GitHub is a push mirror, so a PR is landed by replaying its commits upstream rather than merged on GitHub — see CONTRIBUTING.md for what that means in practice.
Images: :latest and :X.Y.Z are releases; :main tracks the tip of main and
is rebuilt on every push. amd64 only for now.
MIT — see LICENSE.


