ca99bedb1a2a042a49232ad6a1d6725bb2bc28cf
Second cold cross-family (Codex, BLOCKED) re-review of b90f8a3b found the
regex/bracket-tracking guard scanner still defeated in five ways, and two new
High-severity races introduced by the F3/F4 fixes. Addressed as a further
follow-up (b90f8a3b left untouched).
GUARD REWRITE (per the review's explicit direction — stop patching the regex,
use the compiler):
- New `web/src/api/pageSizeScan.ts`: `scanPageSizeSites` parses each file with
`ts.createSourceFile` and walks the real AST for `pageSize`
PropertyAssignment/ShorthandPropertyAssignment nodes inside an
ObjectLiteralExpression. This eliminates categorically (not case-by-case):
- M-3: comments and string/template CONTENTS are never revisited as code,
so a `'https://...'` string can't be misread as an unterminated string
that swallows the rest of the file.
- M-4: an object literal nested in a ternary, `??`, or JSX expression
container is still found — the walk visits every descendant node
regardless of the syntactic context above the ObjectLiteralExpression.
- M-5: template-literal interpolations are real AST children, not opaque
text.
- L-7: a type literal (`type P = { pageSize: 100 }`), an interface
PropertySignature, and a destructuring ObjectBindingPattern (parameter
or nested) are structurally different node kinds from
ObjectLiteralExpression — excluded by kind, not by a
preceding-character heuristic a stray `{`/`(`/`,` could fool.
`getLineAndCharacterOfPosition` gives exact line+column (fixes M-6 identity
granularity) instead of the prior line-only identity.
- `pageSizeCallSites.guard.test.ts` now imports the shared scanner; identity
is `file:line:column:kind:value`, compared as a MULTISET (count, not
membership) in both directions.
- Both directions (unregistered / stale) are computed and folded into ONE
thrown Error so a failure always shows the complete picture in one run,
addressing the line-churn "second direction never renders" concern.
- New `pageSizeScan.test.ts`: a FIXTURE test (inline source strings, no repo
scan) pinning the exact discovered set for every case the review named —
comment-in-string, string containing the literal text `pageSize: 100`,
template interpolation, ternary, `??`, JSX container, same-line duplicates,
parameter/nested destructuring, a type literal, an interface property, a
forwarded call expression, a React dependency array. This is what actually
protects the scanner going forward — the guard test alone only ever proved
today's snapshot of real call sites, never the scanner's handling of input
classes it hadn't happened to encounter yet.
- Re-verified both original plants (a duplicate at-cap call in an
already-registered file, and a new file with both a literal and a
shorthand site) against the rewritten scanner; both still fail with the
new combined-direction message. Also verified a run with BOTH directions
simultaneously non-empty renders both in one report.
HIGH-1 (ChannelBuilder.tsx useLibraryBrowse, now web/src/builder/libraryBrowse.ts):
`reqId` identifies a query GENERATION, not an individual fetch — a page-0
refresh and a "Load more" append can be outstanding simultaneously under the
same reqId (query changes while an append is in flight for the new
generation). Whichever settled first used to clear `loadingMore`, letting a
second click fire an out-of-order/duplicate page fetch. Fixed with a
per-fetch `fetchId` plus a `loadingFetchIdRef`/`loadingFetchReqIdRef` pair:
only the fetch that OWNS the currently-displayed spinner can clear it; a
same-generation page-0 refresh leaves a same-generation append's spinner
alone, while a page-0 refresh for a NEW generation still retires an
abandoned OLDER-generation append's spinner (preserving the original #650 F3
fix). Reproduced the exact interleaving from the review in a new test
(gate B's page-0 and page-1 fetches independently, click "Load more" while
B's page-0 is still in flight) and confirmed it fails without the fix
(button re-enables while the append is still pending).
HIGH-2 (same file): the append-failure rollback mutated whatever
`pageRef.current` currently held, rather than the specific page THIS fetch
requested — under an overlapping-append race, a later page's success
followed by an earlier page's failure could roll the cursor back past
already-appended progress, corrupting a retry into refetching a duplicate.
Fixed with a compare-and-set guard (`if (pageRef.current === pageNum)`) so
the rollback only fires when nothing has advanced the cursor since. Since
this overlap is UI-unreachable once HIGH-1's single-flight disabling is
wired up (verified empirically: two synchronous fireEvent.click calls in RTL
only produce one request, since act() flushes the disabling render between
them), the regression test drives `useLibraryBrowse` directly via
`renderHook` (now exported) to force the exact interleaving and confirms it
fails without the fix (page 2 gets duplicated, page 3 never requested).
Extracted `useLibraryBrowse` (plus `loadCollections`/`loadLibraryItems`/
`BrowseState`/the media-type const arrays) into a new non-JSX module
`web/src/builder/libraryBrowse.ts` — exporting a hook from a .tsx file
tripped `react-refresh/only-export-components`; this also makes the hook
importable by `renderHook` without pulling in the whole screen component.
No server-side/C# change. Full local gate: lint clean, tsc clean, full
vitest run 110 files / 1051 tests passed (one LibrariesScreen.test.tsx
flake reproduced under full-suite parallel load, confirmed pre-existing and
unrelated — passes in isolation, never touched that file).
Merge pull request 'fix(649): point the ENFORCED review-verdict gate at the shared PR-file enumeration' (#666) from fix/649-enforced-verdict-guard into main
Merge pull request 'fix(649): point the ENFORCED review-verdict gate at the shared PR-file enumeration' (#666) from fix/649-enforced-verdict-guard into main
Merge pull request 'chore(deps): update dependency humanizer.core to 3.0.10' (#328) from renovate/humanizer-monorepo into main
ErsatzTV
ErsatzTV lets you transform your media library into a personalized, live TV experience - complete with EPG, channel scheduling, and seamless streaming to all your devices. Rediscover your content, your way.
How It Works
- Install ErsatzTV: Download and set up the server on your system.
- Add Your Media: Connect your media libraries and collections.
- Create Channels: Design and schedule your own live channels.
- Stream Anywhere: Watch on any device with IPTV and EPG support.
Key Features
- Custom channels: Create and schedule your own live TV channels.
- IPTV & EPG: Stream with IPTV and Electronic Program Guide support.
- Hardware Transcoding: High-performance streaming with hardware acceleration (NVENC, QSV, VAAPI, AMF, VideoToolbox)
- Media Server Integration: Connect Plex, Jellyfin, Emby and more.
- Music & Subtitles: Mix music videos and enjoy subtitle support.
- Open Source: Free, open, and community-driven project.
Documentation
Documentation is available at ersatztv.org.
License
This project is inspired by pseudotv-plex and the dizquetv fork and is released under the zlib license.
