|
| 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