Skip to content

Commit 916c157

Browse files
docs: document the Tunarr API surface Programmarr depends on
Tunarr's author noted that API stability hasn't been a priority simply because few people have built on Tunarr, and that a trend of downstream tools would change that. He can't version what he doesn't know is load-bearing — so this is that list, from our side. Every endpoint we call, what we use it for, and the specific response fields we read. Plus the two request payload shapes that are effectively a contract (the programming/schedule body and the channel create body), and the behaviours we depend on that aren't endpoints at all: program state "missing" rather than removal, channel ids surviving programming updates, the shared id-space between /programs and /channels/{id}/programming, and channel numbers being encoded in the XMLTV ids. That last group matters most and is the least obvious from reading an API spec — the shared id-space in particular is what lets a downstream tool diff desired against actual without keeping its own state file. Doubles as our own coupling map: it's the list of places an upstream change can bite us, which we'd otherwise rediscover one bug at a time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 63fd48c commit 916c157

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ Start at the [project README](../README.md) for install + quick start. These doc
88
| [api.md](api.md) | Backend HTTP endpoint reference (Pipeline / Recipe / Tunarr / TMDB / Plex). |
99
| [live-channels-design.md](live-channels-design.md) | How auto-updating "live" channels work — scheduler, in-place updates, rationale. |
1010
| [tunarr-commercials-findings.md](tunarr-commercials-findings.md) | What does and doesn't work for commercials/filler in Tunarr (QSV mid-roll caveat). |
11+
| [tunarr-api-dependencies.md](tunarr-api-dependencies.md) | Every Tunarr endpoint Programmarr calls and the fields it reads — the blast radius of an upstream change. |
1112
| [ideas.md](ideas.md) | Planned/unbuilt ideas — not yet in the product. |
1213
| [archive/](archive/) | Completed build plans, kept for context. Historical — not current instructions. |

docs/tunarr-api-dependencies.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Tunarr API surface Programmarr depends on
2+
3+
Every Tunarr endpoint Programmarr calls, what it uses each one for, and the specific
4+
response fields it reads. Written for two audiences:
5+
6+
- **Tunarr maintainers**, as a concrete answer to "what would a breaking change break?"
7+
Anything on this list is load-bearing for at least one downstream tool.
8+
- **Programmarr contributors**, as the list of places an upstream change can bite us.
9+
10+
Verified against **Tunarr 1.3.13**. Programmarr does not gate on a version number — it
11+
probes for the endpoints it needs and reports what's missing, because a missing endpoint
12+
is directly observable and a minimum-version guess isn't.
13+
14+
## Endpoints
15+
16+
| Method | Endpoint | Used for | Response fields read |
17+
|---|---|---|---|
18+
| GET | `/api/version` | Diagnostics; shown in the UI and asked for in bug reports | `tunarr` |
19+
| GET | `/api/media-sources` | Find Plex sources and their libraries | `type`, `name`, `uri`, `libraries[].id`, `libraries[].name`, `libraries[].mediaType`, `libraries[].enabled` |
20+
| GET | `/api/media-libraries/{id}/programs` | Index the whole library — the core read | `id`, `program.title`, `program.show.uuid`, `program.show.title`, `program.showId`, `program.state`, `program.releaseDate` |
21+
| GET | `/api/transcode_configs` | Pick a transcode config when creating channels | `id`, `name` |
22+
| GET | `/api/channels` | List channels; look up by number | `id`, `number`, `name` |
23+
| GET | `/api/channels/{id}` | Read a full channel to edit its icon | whole object (round-tripped) |
24+
| POST | `/api/channels` | Create a channel | `id` |
25+
| PUT | `/api/channels/{id}` | Set/clear a channel icon | whole object (round-tripped) |
26+
| DELETE | `/api/channels/{id}` | Remove channels on a wipe-and-rebuild deploy ||
27+
| GET | `/api/channels/{id}/programming` | Read the current lineup — drives change detection and pre-delete backups | `programs` (dict keyed by program id), `lineup[].type`, `lineup[].id` |
28+
| POST | `/api/channels/{id}/programming` | Write a channel's schedule ||
29+
| POST | `/api/upload/image` | Upload generated channel badge art | `fileUrl` |
30+
| GET | `/api/filler-lists` | Populate the commercials picker | `id`, `name`, `contentCount` |
31+
| GET | `/api/xmltv.xml` | Plex DVR sync and the guide grid | channel ids of the form `C{number}.{...}.tunarr.com` |
32+
33+
## Request payload shapes we depend on
34+
35+
Two request bodies are effectively a contract. If either changes shape, Programmarr
36+
writes a channel that looks fine and plays nothing.
37+
38+
**`POST /api/channels/{id}/programming`** — the schedule payload
39+
(`channel_engine.build_schedule`):
40+
41+
```json
42+
{
43+
"type": "random",
44+
"programs": ["<program id>", "..."],
45+
"schedule": {
46+
"type": "random",
47+
"flexPreference": "end",
48+
"maxDays": 30,
49+
"padMs": 0,
50+
"padStyle": "episode",
51+
"randomDistribution": "uniform",
52+
"slots": [{ "type": "show|movie", "order": "next|chronological", "weight": 1 }]
53+
}
54+
}
55+
```
56+
57+
`padMs` opens the gaps that `fillerCollections` fills at playback — that's how
58+
Programmarr does commercials between shows.
59+
60+
**`POST /api/channels`** — the create body (`create.py:create_channel`) sets
61+
`transcodeConfigId`, `streamMode`, `groupTitle`, `guideMinimumDuration`,
62+
`fillerRepeatCooldown`, `fillerCollections`, `disableFillerOverlay`, `stealth`,
63+
`subtitlesEnabled`, `icon`, `offline`, `watermark`, `onDemand`.
64+
65+
## Behaviours we rely on, beyond the shapes
66+
67+
These aren't endpoints, but a change to any of them would be just as breaking:
68+
69+
- **`program.state == "missing"` marks unplayable content** rather than removing it.
70+
Programmarr counts non-missing programs to pick between duplicate copies of a show
71+
across libraries, so a dead duplicate can't shadow the real one.
72+
- **A channel's Tunarr `id` is stable across programming updates.** Auto-updating
73+
channels patch programming in place specifically so the id survives — recreating a
74+
channel breaks the Plex DVR mapping that points at it.
75+
- **`programs` in the programming response is keyed by program id**, and those ids match
76+
the ones from `/api/media-libraries/{id}/programs`. That shared id-space is what makes
77+
change detection possible without a state file.
78+
- **XMLTV channel ids encode the channel number** as the first dotted segment
79+
(`C10.97.tunarr.com` → channel 10). Plex DVR sync parses this.
80+
81+
## What would help most
82+
83+
Ranked by how much breakage it would prevent, not by effort:
84+
85+
1. **A stable id-space between `/programs` and `/channels/{id}/programming`.** More load-bearing than any single endpoint — it's what lets a tool diff desired against actual without keeping its own state.
86+
2. **Advance notice on the programming payload shape.** A silent change here fails quietly: the channel is created, the write is accepted, nothing plays.
87+
3. **`/api/media-sources` and `/api/media-libraries/{id}/programs` staying stable**, or changing behind a version. Everything else is recoverable; without these there's no library to read.
88+
4. **Anything version-ish on responses** — a header, a field, `/api/version` semantics that are safe to compare — so downstream tools can adapt instead of guessing.
89+
90+
Not asking for a frozen API. Knowing which parts are intentionally stable versus
91+
in-flux is worth more than stability itself.

0 commit comments

Comments
 (0)