You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bump version to v0.2.1 — channel protection and offset features
## Channel protection (--protect flag)
Users can now preserve specific Tunarr channels during a deploy instead of
always nuking everything. Protected channel numbers are passed to create.py
via --protect N1,N2,... and skipped during the deletion phase.
- create.py: added --protect NUMS argparse flag; delete_channels() accepts a
protect=set[int] and skips those numbers, printing "Preserving #N name (protected)"
- pipeline_router.py: deploy endpoint accepts protected= query param and no_delete bool;
DeployRequest Pydantic model adds protected_numbers: list[int] and no_delete: bool;
deploy-selective passes --protect and --no-delete to create.py as needed
## Channel offset (--start flag)
generate_no_ai.py now accepts --start N (default 10) which offsets all block
ranges by N-10, so users can leave lower channel numbers free for channels
they want to keep. The web UI passes this automatically based on the user's
selection in the Channel Planner step.
- generate_no_ai.py: --start N flag; all hardcoded block numbers (TV Marathons
at 10, TV Blocks at 20, Movies at 30–49, Franchise at 50–69, Specialty at 70–71)
now add offset; renamed loop var start -> yr_start to avoid shadowing
- pipeline_router.py: get_prompt accepts start param and replaces block range
strings in PROMPT.md (**10–19** etc.) and example numbers; run_no_ai passes
--start N when start != 10
## Channel Planner UI (AI path step 2, formerly "LLM Handoff")
The LLM Handoff step was renamed to Channel Planner and now serves as the
decision point for what to do with existing channels before generating a new
lineup. This makes the workflow feel more intentional: see your current lineup,
decide what to keep, then generate channels that fit around what you're keeping.
- Shows all current Tunarr channels as a scrollable checklist on mount
- All channels checked by default (keep all); uncheck to clear and replace
- Summary text turns yellow with explicit "cleared / rebuilt as new stations"
language when any channel will be deleted
- "Channels start at" auto-calculates from the highest checked channel,
rounded up to the nearest 10 (e.g. highest kept is #24 → start at 30).
Auto-updates live as checkboxes are toggled. Resets to 1 when all unchecked.
- Checked channel numbers are passed up to AIPath as protectedNums and down
to DeployStep as inheritedProtectedNums, bypassing the per-deploy panel
## Deploy step simplification
Removed the "Replace existing channels / Add new channels only" SegmentedControl
toggle. The checkbox model in Channel Planner subsumes both modes:
- All checked = keep all (equivalent to "add new only")
- None checked = replace all (equivalent to "replace existing")
- Mix = granular per-channel control the old toggle couldn't offer
- DeployStep: removed deployMode prop/state/SegmentedControl; added
inheritedProtectedNums?: number[] prop; shows inherited protection summary
when provided (AI path) or own protection panel when not (No-AI/Collections)
- Protection panel in Deploy (No-AI/Collections path): channels not being
redeployed are protected by default; channels being redeployed are not
- Conflict detection unchanged: red highlight + disabled deploy button when a
protected number collides with a deploy channel number
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+24-10Lines changed: 24 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -166,6 +166,7 @@ See `config.json.example` for the template.
166
166
- Auto-generates decade channels (year filtering) and genre channels (genre tag matching)
167
167
- Auto-generates TV marathon channels for shows with 50+ episodes
168
168
- Writes placeholder entries for franchise/themed channels (user fills manually)
169
+
-`--start N` — offsets all block ranges by `N - 10` (default: 10). E.g. `--start 30` shifts TV Marathons to 30–39, TV Blocks to 40–49, etc., leaving lower numbers free for pre-existing channels. Passed automatically by the web UI.
169
170
- Output: `channels.json`
170
171
171
172
**`generate_from_collections.py`** (Option C — Plex collections as channels)
@@ -180,6 +181,7 @@ See `config.json.example` for the template.
180
181
- Reads `channels.json`
181
182
- Indexes Tunarr library (exact title matching, case-insensitive)
182
183
- Deletes all existing channels then creates new ones (use `--from N` to scope to channels >= N, preserving lower channels and their custom images)
184
+
-`--protect N1,N2,...` — comma-separated channel numbers to skip during deletion; these channels remain in Tunarr untouched regardless of scope. Printed as "Preserving #N name (protected)" during the run.
183
185
- Builds Tunarr random-schedule payloads (30-day rolling window — channels loop forever, no dead air)
184
186
- Output: channels live in Tunarr
185
187
@@ -270,15 +272,15 @@ A title can appear on multiple channels — this is intentional and expected.
270
272
| POST |`/api/pipeline/export`| SSE-stream `export.py`; JSON body `{"no_crossref": bool, "movie_sections": ["1","2"], "tv_sections": ["3"]}` — sections are Plex section keys; `null` = auto-detect, `[]` = skip that type |
271
273
| GET |`/api/pipeline/csv`| Download `plex_library.csv`|
| GET |`/api/pipeline/prompt`| Fetch `PROMPT.md` with `{TARGET}`and preferences injected |
275
+
| GET |`/api/pipeline/prompt`| Fetch `PROMPT.md` with `{TARGET}`, preferences, and `start` (block offset) injected; query params: `target`, `preferences`, `start`|
274
276
| POST |`/api/pipeline/validate`| Parse/validate LLM output (file upload or raw text), write `channels.json`|
275
-
| POST |`/api/pipeline/no-ai`| SSE-stream `generate_no_ai.py`|
277
+
| POST |`/api/pipeline/no-ai`| SSE-stream `generate_no_ai.py`; query param `start=N` passed as `--start N`|
276
278
| GET |`/api/pipeline/collections`| Fetch all Plex collections (id, name, count, section, summary, has_poster) |
277
279
| GET |`/api/pipeline/collections/{id}/poster`| Proxy Plex collection poster image |
278
280
| POST |`/api/pipeline/collections/apply`| Write selected collections into `channels.json`|
279
281
| POST |`/api/pipeline/probe`| SSE-stream `create.py --probe`|
280
-
| POST |`/api/pipeline/deploy`| SSE-stream `create.py` (full deploy) |
281
-
| POST |`/api/pipeline/deploy-selective`|Filter channels by `DeploySelection[]`, write`deploy_temp.json`, SSE-stream`create.py --json deploy_temp.json`|
282
+
| POST |`/api/pipeline/deploy`| SSE-stream `create.py`; query params: `protected` (comma-separated channel numbers to preserve), `no_delete` (bool) |
283
+
| POST |`/api/pipeline/deploy-selective`|JSON body `DeployRequest{selections, protected_numbers, no_delete}`; filters channels.json to selected entries, writes`deploy_temp.json`, SSE-streams`create.py --json deploy_temp.json [--protect N1,N2,...] [--no-delete]`|
282
284
| POST |`/api/pipeline/images`| SSE-stream `fetch_images.py --apply`|
283
285
| POST |`/api/pipeline/sync`| SSE-stream `sync_plex.py`|
284
286
@@ -293,15 +295,27 @@ A title can appear on multiple channels — this is intentional and expected.
293
295
294
296
**AI Path (6 steps):**
295
297
1.**Export** — fetches Plex library list on mount; shows "Libraries to scan" card with checkboxes grouped by Movies / TV Shows (all checked by default, supports multi-select across libraries of the same type); runs `export.py` with selected section keys; shows compact stats card on success; manual Continue
3.**Add Plex Collections** — fetches collections on mount; 2-column grid with poster, name, count, editable channel number, checkbox (all checked by default); applies selections to `channels.json`
298
-
4.**Deploy** — probe explainer card; runs probe; after probe completes: 2-column layout [terminal | scrollable channel review card with checkboxes + editable channel numbers]; selective deploy via `/pipeline/deploy-selective`
298
+
2.**Channel Planner** — shows current Tunarr lineup as a scrollable checkable list (all checked by default = keep; uncheck = delete and replace with new station); auto-calculates "Channels start at" from the highest checked channel number, rounded up to the nearest 10 (e.g. highest checked is #24 → start at 30); summary line turns yellow and uses explicit "cleared / rebuilt" language when channels will be deleted; config card (target channel count NumberInput + "Channels start at" NumberInput + theme Textarea); side-by-side prompt copy + CSV download; paste/upload LLM output; post-validate results card showing channel breakdown; "Add Plex Collections" or "Skip to Deploy" buttons. Checked channel numbers are passed to Deploy step as `inheritedProtectedNums`.
299
+
3.**Add Plex Collections** — fetches collections on mount; list with poster, name, count, editable channel number, checkbox (all checked by default); applies selections to `channels.json`
300
+
4.**Deploy** — probe explainer card; shows note about how many channels will be kept (from Channel Planner); runs probe; after probe completes: 2-column layout [terminal | scrollable channel review card with checkboxes + editable channel numbers]; conflict detection highlights red when a deploy number collides with a protected number; selective deploy via `/pipeline/deploy-selective` with `protected_numbers` from Channel Planner
4.**Deploy** — same as AI Deploy step but with its own protection panel (no inherited protection): shows all existing Tunarr channels after probe, with channels NOT being redeployed checked (protected) by default; user adjusts before deploying
309
+
5.**Fetch Images** — same as AI path
310
+
6.**Sync Plex** — same as AI path
311
+
312
+
**Collections Path (4 steps):** Collections → Deploy (with own protection panel) → Fetch Images → Sync Plex (no export or LLM).
313
+
314
+
**Channel protection model:**
315
+
- AI path: user decides which existing channels to keep in the **Channel Planner** step, before even generating the LLM prompt. Checked = protected (passed to deploy as `inheritedProtectedNums`). Deploy step shows a summary and skips the per-deploy panel.
316
+
- No-AI / Collections path: user decides in the **Deploy** step's own protection panel after the probe. Channels not in the current deploy are protected by default; channels being redeployed are unprotected by default.
317
+
- In both cases, protected channel numbers are passed to `create.py` via `--protect N1,N2,...`.
318
+
- Conflict detection: if a protected number equals a deploy channel's `deployNumber`, the row highlights red and the deploy button is disabled until resolved (renumber or unprotect).
305
319
306
320
**`deploy_temp.json`:** Written by `/pipeline/deploy-selective` when the user excludes channels from a deploy session. The original `channels.json` is not modified — only the channels the user chose are deployed.
Copy file name to clipboardExpand all lines: README.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -147,6 +147,8 @@ All three paths end with a probe (dry run) → deploy to Tunarr. The app streams
147
147
148
148
### Also included
149
149
150
+
- **Channel protection** — before deploying, choose which existing Tunarr channels to keep. Checked = preserved; unchecked = cleared and rebuilt as new stations. The "Channels start at" number auto-adjusts to leave room for whatever you keep.
151
+
- **Channel offset** — set a starting channel number so new channels don't collide with ones you want to keep. Auto-calculated from your highest protected channel, rounded to the nearest 10.
150
152
- **Library picker** — choose which Plex libraries to scan (Movies, 4K Movies, Kids TV, etc.) before each export; supports mixing multiple libraries of the same type
151
153
- **Channel logo fetching** — pulls TMDB clearlogos for single-show/movie channels
152
154
- **Plex DVR sync** — maps new channels into the Plex Live TV guide automatically
0 commit comments