Main -> Dev - #2105
Merged
Merged
Main -> Dev#2105
Conversation
…ing them (#2006) `WatermarkSchema` and `ChannelIconSchema` used `.catch()` on several fields, so `PUT /channels/:id` answered 200 while storing something other than what was submitted: - `watermark.opacity` of 150, -10, 50.5 or "50" was stored as 100, fully opaque. - `watermark.fadeConfig[].programType: "movies"` became undefined, which means "no restriction" — so a fade rule the user scoped to one program type applied to every program on the channel. - `watermark.fadeConfig[].leadingEdge: "false"`, as a form serialiser sends it, became true, the opposite of what was asked for. - `icon.path: null` cleared the icon to "", `icon.width: -20` became 0, and `icon.position: "centre"` became "bottom-right". The `.catch()` calls could not simply be deleted. These schemas are also on channel *responses*, and fastify-type-provider-zod validates responses: the `icon` and `watermark` columns are raw JSON that drizzle casts but never validates, so tightening them in place would turn a legacy or malformed row into a hard 500 on GET /channels, GET /channels/:id, GET /channels/all/lineups, GET /channels/:id/lineup and GET /guide/channels. The server-local copies in db/schema/base.ts do not shield them — those are used for one default value and two type aliases, nothing else. So the schemas are split. StrictChannelIconSchema and StrictWatermarkSchema are used by SaveableChannelSchema, the request body; the lenient originals stay on the response path, with a comment saying why they must not be tightened. The strict variants use `.default()` exactly where the lenient ones use `.catch()`, so a *missing* field behaves identically and a partial icon or watermark is still accepted. Only an *invalid* value changes behaviour, from a silent substitution to a 400. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…off (#2010) `UpdateFeatureFlagsRequestSchema` was `FeatureFlagsSchema.partial()`, and every flag carries `.default(false)`. `.partial()` wraps a field's default rather than removing it, so all six flags arrived populated whatever the client actually sent: parse({ tonemapEnabled: true }) => { proxyArtwork: false, tonemapEnabled: true, webvttSidecarEnabled: false, disableSearchSnapshotInBackup: false, disableVulkan: false, disableVaapiPad: false } The handler then does `Object.assign(file.featureFlags, req.body)` over all six, so enabling one flag through the API silently turned every other flag off. The Features settings page submits the whole form, so the UI never showed it, but the route is published as a partial update and any client following the spec loses state. The body is now declared field by field with genuinely optional fields and no defaults. Zod omits an absent optional key from its output entirely, so the handler's Object.assign is already the right thing for this shape and does not change. Writing the fields out rather than deriving them introduces a drift hazard: a new flag added to FeatureFlagsSchema would not be updatable. A test asserts the two schemas cover exactly the same keys, and that none of the update fields carries a default. Found by the zod contract audit rather than by hand. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2031) (#2049) Copy-paste errors name the wrong media source in three places. Control flow is unaffected — only the message text: - Plex route guard said "not a Jellyfin server" when the source is not Plex (plexApi.ts, withPlexMediaSource). - The Emby client factory logged "updating Jellyfin media source user info" inside getEmbyApiClient (MediaSourceApiFactory.ts). - The Emby route guard read "is not a Emby server" (grammar: "an Emby"). Each guard/factory now names its own media source type, mirroring jellyfinApi.ts which already said "Jellyfin".
midRollUtil.ts has no production importer — the only file referencing it is its own test, midRollUtil.test.ts. The live implementation is midRollBreakRules.ts, imported at slotSchedulerUtil.ts:56, and the two implement genuinely different algorithms (midRollBreakRules supports fixed_interval/percentage/initial_then_interval rule types, a tailBuffer, and randomized break durations that midRollUtil has no concept of). midRollUtil.ts is also stale and incorrect: programQualifiesForMidRoll is a no-op that returns true for every content program regardless of config.programTypes. Deleting the module and its test removes a trap for anyone reviving it, leaving midRollBreakRules.ts as the single break-point authority. No behavior change — nothing in production imports these symbols.
) * fix(web): repair eleven cache invalidations that matched no query Generated hey-api query keys are a single-element array holding an object: [{ _id, baseURL, tags }]. TanStack matches keys by structural prefix, so a hand-written string key diverges at element 0 and matches nothing. Every site below invalidated successfully, logged nothing, and left the cache stale. The give-away is `['Channels']`: 'Channels' is the endpoint *tag*, not a key segment. ChannelDeleteDialog already did this correctly with getChannelsQueryKey(), so the two delete paths behaved differently. Sites, and what stayed stale: useCreateChannel new channel missing from the list ChannelsPage (delete) deleted row still on screen ChannelsPage (stream ev) session counts never update ChannelNowPlayingCard finished program shown indefinitely XmlTvSettingsPage settings cache stale after save SystemDebugPage x3 env/nvidia/vaapi checks inert on reclick TvGuide ignores xmltv regeneration events useUpdateLineup lineup queries missed useScanNow pre-scan metadata retained useScanNow had two stacked bugs: its predicate compared a Query instance against a key array, and one operand was an options object rather than a key. Both always false. Categorical events now invalidate by tag through invalidateTaggedQueries, which is what the tags exist for and what sixteen other call sites already do. Two sites stay key-scoped on purpose: the now-playing card runs a per-card timer where a tag would refetch every channel query on the page, and the two debug buttons each re-run exactly one query. Choosing a tag is not automatically safe -- the TV Guide fix nearly went out invalidating the 'Guide' tag, which belongs to the /api/guide/channels endpoints that page never calls; it renders from the channel lineup endpoints, tagged 'Channels'. The new tests drive a real QueryClient and pin every site to the query it must refresh, including a case asserting the old string key does not match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(web): refresh lingui catalogs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(web): refresh lingui catalogs after pre-commit formatting Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ry (#1982) * fix(api): contain troubleshoot file reads within session temp directory * fix(api): contain HLS fragment reads within session directory The /stream/channels/:id/:sessionType/:file handler builds file paths with join(session.workingDirectory, req.params.file) and passes them straight to fs.readFile, so a traversal sequence in the file param escapes the session directory. Add the same resolve + prefix guard used for the troubleshoot routes to the .vtt and .m3u8 read branches; sendFile already blocks escapes.
…2063) Set a fixed peak instead of letting ffmpeg re-derive it from each frame's mastering-display/CLL side data, which caused visible brightness pumping at scene cuts on the VAAPI (tonemap_opencl) and CPU tonemap fallback paths. Co-authored-by: Corey Vaillancourt <coreyjv@gmail.com>
Jellyfin person mapping used .map and kept credits with empty or missing Name values. Emby already filters those with seq.collect and isNonEmptyString. Mirror that guard for actor, writer, and director, and add a unit test. Fixes #2028 Co-authored-by: MrStewood <stewood@outlook.com>
…ed (#2017) tsconfig.build.json excluded ./src/**/*.test.ts(x), and that is the only config CI typechecks web through -- `turbo build --filter=@tunarr/web` runs `tsgo -p tsconfig.build.json --noEmit`. So no web test file has ever been typechecked. Four of the eleven had drifted far enough to stop compiling: 14 errors, all fixtures asserting against a ContentProgram shape the app no longer produces. `persisted`, `uniqueId`, `subtype`, `title` at the top level -- none of which exist on ContentProgram -- and in useAddBreaks a fixture with no `program` field at all, whose assertions then read a `title` the fixture itself invented. Those tests pass while testing a shape nothing in the app can produce. Dropping the exclude makes CI catch this, with no CI workflow change: `build` for web is already a typecheck, and vite bundling is a separate script unaffected by the include set. The four files now build their programs through the cast-free fixture factory, which is what keeps them honest when the schema next moves. useRemoveDuplicates additionally needed channelNumber/channelName on its redirect fixture and index on its custom-show fixture -- fields the function under test does not read, but the type requires. No test assertions were weakened; all 83 still pass. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SchemaBackedDbAdapter.read caught every error from the underlying adapter and returned null. lowdb's TextFile only returns null for ENOENT and throws for anything else, so that catch collapsed "this file could not be read" into "this file does not exist". With a defaultValue configured -- as LineupRepository does for every channel -- the null took the merge-with-defaults path, which succeeds, sets needsWriteFlush, and immediately writes the defaults back over the file that had just failed to read. A transient EACCES/EIO/EMFILE therefore erased the channel's programming during the failed read itself, leaving only a debug-level log line. The channel still appeared in the UI with an empty schedule and streams fell back to flex. Read errors now propagate. An absent file still initializes from defaults, a valid file is still returned untouched, and a file missing fields is still repaired by merging defaults in; all three are covered by tests so the fix cannot overshoot into never writing defaults. Two related fixes: - The constructor took an `adapter` parameter and then unconditionally overwrote it with `new TextFile(filename)`, so an injected adapter was silently discarded. The sync variant does not do this. Honouring it is also what makes the class testable. - saveChannelLineupDirect wrote with a plain fs.writeFile, which truncates before writing, so an interrupted write leaves a zero-length lineup file -- which the adapter path above then treats as empty and overwrites with defaults. It now goes through a new writeFileAtomic helper that writes a scratch file and renames it into place. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Deleting a folder from a local media source failed with "SqliteError: no such column: media_source_library". The library deletion in MediaSourceDB.updateMediaSource passed the whole MediaSourceLibrary table object to Drizzle's eq() instead of its mediaSourceId column, so the generated DELETE WHERE clause referenced the table name as a column identifier. Use MediaSourceLibrary.mediaSourceId. Add MediaSourceDB.test.ts covering library deletion and addition when updating a local media source's paths, so this class of query-builder error is caught by the test suite.
…2041) * fix: match ISO 639-2 bibliographic and terminological language codes ISO 639-2 assigns 20 languages two three-letter codes: a bibliographic (/B) code derived from the English name and a terminological (/T) code derived from the native name. German is both "ger" and "deu". Media servers and containers use them interchangeably. A German audio preference never matched a Jellyfin track, because: - LanguageService.getAlpha3TCode called alpha3TToAlpha2 in its /B branch, so it returned undefined for every /B code. This broke normalization in CelEvaluationService.matchLanguageInList, SubtitleStreamPicker and LocalSubtitlesService, and dropped the language of any sidecar or container track tagged with a /B code. - StreamSelectionEvaluator compared codes with raw string equality and never normalized either side. - SubtitleStreamPicker normalized the stream's code but compared it to a raw, unnormalized preference. - The FFmpeg settings language picker emitted /B codes while the channel subtitle picker emitted /T codes, so the two screens disagreed and the subtitle table could not resolve a display name for either. Adds LanguageService.codesMatch, which normalizes both sides to /T and falls back to a case-insensitive exact comparison for codes it cannot resolve, and routes all stream language matching through it. Stream language is now resolved by tag precedence (ISO 639-2 > ISO 639-1 > free-form), matching what buildCelContext already does. Both web pickers now emit /T. Preferences already stored as /B keep working through codesMatch and still resolve a display name, so no migration is needed. Fixes #1960 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): dedupe language options by ISO 639-1 code The options list suppressed already-selected languages by comparing ISO 639-2 codes while isOptionEqualToValue compared ISO 639-1 codes. Now that the picker emits terminological codes, a preference saved earlier as "ger" no longer matched the "deu" option, so German was offered again in the dropdown and could be selected twice. Both checks now key on the ISO 639-1 code, which has no bibliographic and terminological split. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: move conventional commits resolutions to pnpm-workspace * fix: use shared language matching in the troubleshoot report (#2042) The troubleshoot simulator resolved audio through the stream selector but re-implemented subtitle language matching inline with raw string comparison. After the selector learned to treat the two ISO 639-2 code sets as equivalent, the two disagreed: the report would claim "No subtitle found for languages: ger" for a file that playback selects a German subtitle for. This matters more than a normal display bug, because the troubleshoot report is what users paste into issues — #1960 was diagnosed from one. Extracts the lookup into findSubtitleForLanguages and routes it through the selector's streamMatchesLanguage, which is now exported. Also adds the missing iso6392 to the FFmpeg settings language-preference fallback; LanguagePreferenceSchema requires it, so the fallback produced a value that would fail validation. Refs #1960, #2026 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: remove duplicate overrides key * chore: regen translations --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…slot (#2069) * chore: update translation messages * fix(scheduling): apply per-slot pad override to every program in the slot The per-slot padMs override was only used for the first program in a slot. The loop that fills the remainder of the slot and the flex distributor both fell back to the schedule-level padMs. With a 30m schedule pad, a 6:00 slot overridden to a 5m pad, and 7m episodes, the first episode was padded to 10m but the second was padded to 30m. The slot then ran 40m into a 30m window, the cursor realigned to the next 30m mark at 7:00, and the following slot was 30m late. That exceeded max lateness, so it was flexed away entirely and never scheduled. Hoist the effective pad into slotPadMs and use it at all three sites. Introduced in 57b7f63 (#1614), which only patched one call site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(scheduling): use the shared pad helper in handleFixedDurationSlot The first program in a fixed-duration random slot inlined the same `padStyle === 'slot' ? 1 : padMs` expression that createPaddedProgram already computes, while every subsequent program went through the helper. Both resolved to the same value, so this is not a behavior change -- but it is the same duplication that let the time slot pad override drift out of sync in the first place. The local padStyle/padMs destructure is now unused, so it goes too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eduler (#2013) Every numeric field on a time slot schedule was a bare z.number() and slots had no minimum length, so several inputs reached the scheduler that it mishandles rather than rejects. Probing each one: startTime: 1234.567 an infinite loop. The scheduling loop is synchronous despite the enclosing async, so it blocks the event loop outright -- the test runner's own timeout never fires and the process has to be killed. The worker pool's timeoutPromise rejects the caller while the thread keeps burning a core, so N such requests permanently exhaust an N worker pool. padMs: 0 reports success and returns a lineup whose durations are all NaN. maxDays: -1 reports success and returns zero lineup items. maxDays: 0 returns half the expected items. slots: [] throws "Could not find a suitable slot", the same message an out-of-period offset produces, so the log cannot tell the two apart. Add StrictTimeSlotScheduleSchema and use it for the request body. TimeSlotScheduleSchema itself has to stay permissive. It also feeds LineupScheduleSchema inside CondensedChannelProgrammingSchema, which is both persisted and used to serialize channel responses, so tightening it would make a channel already holding one of these values fail response serialization and stop loading entirely -- worse than the bug. This is the same split already used for StrictChannelIconSchema. The bounds are startTime a whole number within the period, padMs and maxDays positive, latenessMs non-negative, and at least one slot. A test asserts the permissive schema still accepts every case the strict one rejects, so the distinction cannot be erased by accident later. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#2073) The Plex content hashes (movie/show/season/episode/artist/album/track) only folded title, dates and media/tag fields, never Label. Editing an item's labels in Plex therefore left the canonical id unchanged, so a label-only change never triggered a re-canonicalization. Fold each item's Label tags into the hash via a shared helper.
loadLineup returned Low.data directly, so every caller shared one mutable object with the lowdb cache. The guide build reads items and startTimeOffsets interleaved with awaited DB writes; a saveLineup landing in between rebound those properties underneath it, leaving the captured offsets indexed against a discarded items array. That either throws "General algorithm error, completely unexpected" or emits plausible but wrong airtimes. loadLineup and saveLineup now return a top-level copy. Every writer rebinds whole properties on Low.data rather than mutating the arrays in place, so a shallow copy is enough to pin a caller to the lineup it read, and it keeps loadAllLineups off the guide path's critical cost. createChannel returned getFileDb(...).data directly; route it through loadLineup so it gets the same treatment. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
createFillerIterators deduped slot filler definitions by fillerListId alone, but registered each iterator under `filler_<listId>_<order>` and looked it up by (fillerListId, fillerOrder). A filler list referenced at two different orders therefore only got an iterator for whichever order appeared first, and every slot using the other order silently resolved to no filler iterator at all -- no head, pre, post or tail, and no error. Dedupe on the list id and order together so each pair gets its own iterator. Refs #1984 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Point the slot-save skew plan's validation step at the new plan, which keeps lineup validation instead of removing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MkDocs publishes every file under docs/, nav or not, so the internal architecture review and candidate records were live on tunarr.com. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
This is included in v2026.9.0. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.