Skip to content

Second refactoring review: reconnect race, keychain off the runtime, typed cast state, module cleanup - #16

Open
df49b9cd wants to merge 1 commit into
mainfrom
refactor/review-2
Open

df49b9cd wants to merge 1 commit into
mainfrom
refactor/review-2

Conversation

@df49b9cd

@df49b9cd df49b9cd commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Fixes all 36 findings of a second whole-repo refactoring review: 21 in the Rust backend and CI, 15 in the frontend. Two of them were real bugs; the rest is mostly finishing what #12 started.

Bugs

  • Live reconnect race (player/mod.rs). A dropped live stream is reopened after a 0.5–2.5 s backoff on a detached thread, which used to reload the URL it captured no matter what had happened since. Switching channel during the backoff brought the old channel back. Sessions now carry a generation number, and the thread reloads only if the session is still the same one, checked under the session lock so a new load can't slip in between.
  • macOS keychain blocking the runtime (secrets.rs). The macOS backend is async fn, but it called the blocking std::process::Command::output() without ever awaiting, so security unlock-keychain waiting on a prompt held a tokio worker and the timed() wrapper could never fire. The security calls now run on the blocking pool. Error::KeyringLocked replaces matching on the error message.

Backend

  • Detail pages. series_detail (292 lines) and movie_detail (157) are split into named steps, with tests for resume_point:

    • series_text, watch_history, copy_tracks, latest_watch_state, build_seasons, resume_point
    • choose_movie_copy, movie_resume_position

    The repeated work/item lookups (movie_by_key, series_by_key, …) and the episode-history filter live in catalog/works.rs, and details_of returns a named Details.

  • Grouping. rebuild_works (231 lines) is split into remap_search_matches, work_row and collect_facets; six copies of "push if not already there" become push_unique.

  • Typed state.

    • The cast status is a CastState enum instead of &'static str. on_media_status goes through a pure media_transition, with tests including the three live reloads.
    • SourceKind is FromSql/ToSql, so an unknown kind in the database is now an error instead of silently counting as Xtream.
    • Work preferences use ItemRef instead of (i64, String).
  • Shared state.

    • TMDB progress moves from a static Mutex plus a separate AtomicBool into App, with check-and-set under one lock.
    • HTTP clients are cached per user agent (App::client) instead of built on every detail, guide and sync request; playback's URL-only Xtream shares one idle client.
  • Modules.

    • No use super::*; outside test modules.
    • cast, catalog, sources and works keep only mod declarations and re-exports; their items moved to types.rs / keys.rs leaves.
  • Smaller.

    • One user-agent constant and one TMDB_KIND SQL fragment (it was written out 6 times).
    • Error::msg takes String; versions::unavailable takes Option<&Value>.
    • The background jobs move into spawn_background_jobs with a # Panics section on run().
    • The let _ = &cast; no-op lines are deleted, and assertion-roulette tests are labelled.

Frontend

  • Shared helpers.
    • The player's resolution, channel and language labels use lib/format.
    • ROUTES covers every page.
    • Shared useSources / useSeriesDetail hooks and a queryKeys module.
    • isAiring / currentAndUpcoming (tested), usePagedQuery for the three infinite lists, useFolds, and NavRow / FoldHeader / SearchInput in ui.tsx.
  • Correctness.
    • useTimeout clears the Live preview, channel-digit, notice and flash timers on unmount, so a preview can no longer start after leaving Live TV.
    • User actions (favorites, version picks, sync, clear artwork cache) go through backgroundWrite (renamed from playerWrite) or a mutation with an error.
    • The player's mpv writes go through casting-aware control methods.
  • Structure. PlayerPage is about 200 lines (was about 345), with useNotice, useTransport, TopBar and BottomBar. WorkKind / HistoryKind replace the retyped unions, and StartPage moved to lib/routes.ts.
  • Tests. New lib/play.test.ts and onEvent cases; Live.test.ts became lib/liveLists.test.ts.

CI

  • The four build jobs share .github/actions/setup: system packages or Homebrew tools, toolchains, the media-engine cache with the same keys as before, and rust-cache. The bundle trigger condition is written once as a YAML anchor.

Visible changes

  • Small known video sizes show "SD" in the player instead of e.g. "576p".
  • Track language names come from the system (Intl), e.g. "Norwegian Bokmål".
  • Live TV's "Up next" no longer lists a guide entry that overlaps the running show.

Testing

  • scripts/check.sh passes: fmt, clippy -D warnings plus the unsafe lint, 95 Rust tests (2 ignored), tsc, 77 vitest tests (was 65), third-party notices.
  • Headless app (scripts/headless.sh) on a debug build of this branch, with a local M3U playlist:
    • The source synced (2 channels, 1 movie, 1 series).
    • Every page loaded with no console errors.
    • Marking S1E1 watched moved resume and Up next to S1E2.
    • Movie details, the favorite toggle and a remembered fold on Live TV all worked.
  • Not tested yet:
    • The macOS keychain change: the Linux machine has no macOS target, so check-macos here is its first compile.
    • The CI composite action: it is checked only as YAML, and this PR's run is its first real test.
    • Playback, including the reconnect fix: there was no live stream to play.

…typed cast state, module cleanup

Fixes all 36 findings of a second whole-repo refactoring review (21 backend
and CI, 15 frontend).

Backend
- A delayed live-stream reconnect only reloads while its session is still
  the current one (generation check under the session lock); switching
  channel during the backoff used to bring the old channel back.
- The macOS keychain backend runs `security` on the blocking pool, so an
  unlock prompt can't stall a runtime worker and `timed()` can give up.
  Error::KeyringLocked replaces matching on the message.
- series_detail/movie_detail split into named steps (resume_point,
  build_seasons, watch_history, choose_movie_copy, ...); shared
  movie/series lookups and the episode-history filter in catalog/works.rs;
  details_of returns a named Details.
- rebuild_works split into remap_search_matches / work_row /
  collect_facets with push_unique.
- CastState enum instead of &'static str; on_media_status goes through a
  pure, tested media_transition.
- SourceKind is FromSql/ToSql (unknown kinds are an error, not Xtream);
  ItemRef for work preferences; versions::unavailable takes Option<&Value>.
- TMDB progress is App state (one lock, no separate AtomicBool); HTTP
  clients are shared per user agent (App::client); one user-agent constant;
  one TMDB_KIND SQL fragment; Error::msg takes String.
- No glob imports outside test modules; cast/catalog/sources/works keep
  only mod declarations and re-exports, their items moved to types.rs /
  keys.rs leaves.
- lib.rs background jobs in spawn_background_jobs; labelled assertions.

Frontend
- Player labels use lib/format; ROUTES covers every page; shared
  useSources/useSeriesDetail and queryKeys; isAiring/currentAndUpcoming;
  usePagedQuery, useFolds, useTimeout (timers cleared on unmount); NavRow,
  FoldHeader, SearchInput; WorkKind/HistoryKind; casting-aware control
  methods; backgroundWrite (was playerWrite) on user actions; new tests
  for play.ts and the player store's onEvent.

CI
- The four jobs share .github/actions/setup; the bundle trigger is one
  YAML anchor.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant