Skip to content

The Library shows a folder scan's books while it is open - #60

Merged
StaticHumStudio merged 3 commits into
masterfrom
claude/amazing-banach-85f268
Oct 2, 2026
Merged

StaticHumStudio merged 3 commits into
masterfrom
claude/amazing-banach-85f268

Conversation

@StaticHumStudio

@StaticHumStudio StaticHumStudio commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

What was broken

In local mode the Library tab loaded its list once and only looked again when a server sync record arrived, which local mode never writes. A first-run folder scan creates the folder's library row and imports its books at the very end. So if you opened Library while the scan ran, it read an empty database and kept saying "The Archive Stands Empty" until you left the tab and came back.

What changed

  • LibraryViewModel now watches the local catalog in Room (which folders exist, and which books each holds, live or archived). A changed folder list reloads the list the same way a server sync does (shouldReloadLibrariesAfterSync is reused). Any other change re-filters the shelf.
  • Playback progress is left out of the watch (new observeLocalCatalog query returns only id, library, and archived flag), so listening does not re-query the shelf every few seconds. Server mode does not subscribe at all.
  • Loads wait for the watch's first snapshot before reading, so a scan that lands between a load and the watch starting still arrives. A watch that starts later (a switch into local mode) checks its first snapshot instead of skipping it.
  • A catalog reload re-saves a library the user picked in the Library whose own settings write it cancelled. Any other saved choice, including one made in Settings, is left alone.
  • The instrumented test sources compile again. LibraryViewModelFilterRaceTest and HomeViewModelStartupInstrumentedTest had fallen behind constructor and saveAuthToken changes, so nothing under androidTest could build.

Tests

  • New LibraryLocalCatalogLiveUpdateTest (instrumented, real ViewModel on an in-memory Room database): a first scan while the Library is open, a rescan that adds a book, and a rescan that archives one. All three failed on master at the step after the scan's writes and pass with this change.
  • New LibraryLocalCatalogChangeTest (unit): reload versus re-filter decisions and the pending-pick rules.
  • Unit suite: 923 tests, 0 failures.
  • Instrumented suite: 16 run, 15 pass. The one failure is the stale template ExampleInstrumentedTest, which expects the package name without the .debug suffix and is unrelated.

Evidence

Emulator, debug build of this commit, the reported repro (clear data, onboarding, pick the Audiobooks folder, open Library while "Scanning folder..." shows). In both Noir and Bright themes the Library opens on the empty state and the nine books appear about a second later without leaving the tab.

Review

Codex (gpt-6.1-sol, high), three local rounds. Rounds 1 and 2 each raised two P2 timing issues, all fixed (the baseline ordering, the mode-switch first snapshot, and the two selection cases above). Round 3: no material findings.

Found along the way, filed separately

🤖 Generated with Claude Code

In local mode the Library loaded its list once and only looked again when
a server sync record arrived, which local mode never writes. A first-run
folder scan creates the folder's library row and imports its books at the
very end, so a Library opened mid-scan read an empty database and kept
saying "The Archive Stands Empty" until the tab was left and re-entered.

The Library now watches the local catalog in Room: which folders exist and
which books each holds, live or archived. A changed folder list reloads the
list the way a server sync already does, and any other change re-filters
the shelf. Playback progress stays out of the watch, so listening does not
re-query the shelf every few seconds.

Two edges are covered. Loads wait for the watch's first snapshot before
reading, so a scan that lands between a load and the watch starting still
arrives. A reload re-saves a library the user picked on this screen whose
own settings write it cancelled, and leaves any other saved choice alone.

The instrumented test sources compile again. Two fixtures had fallen
behind constructor and saveAuthToken signature changes, so nothing under
androidTest could build or run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T15:33:14.145664Z 86e2617 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fedba69245

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* table changes. Progress columns stay out so a caller that drops repeats
* hears about imports and archives, not position saves.
*/
@Query("SELECT Id AS id, LibraryId AS libraryId, ArchivedAt IS NOT NULL AS isArchived FROM AudioBooks WHERE IsLocal = 1")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track shelf metadata changes during rescans

When a rescan updates an existing file at the same path—for example after its title, author, cover, duration, or tracks change—the imported row keeps the same ID, library, and archive status. Room re-emits this query, but it produces the same LocalBookMembership set, so the downstream distinctUntilChanged() suppresses the event and the open shelf retains stale metadata and filter/group results until it is reopened. Include the shelf-relevant metadata in the observed snapshot, or otherwise distinguish rescan upserts from playback-only updates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3004cdf. The watched row (now LocalCatalogEntry) also carries title, author, narrator, covers, duration, added date, series, and genres, so a rescan that retags a book re-filters the open shelf. Progress columns stay out. New instrumented case aRescanThatRetitlesABookUpdatesTheOpenShelf failed before the change and passes on a physical device after it.

The catalog watch only compared which books existed and whether they were
archived. A rescan that re-reads the same file with a new title, author,
cover, or duration keeps the id, the folder, and the archive state, so the
open shelf kept the old details until it was reopened.

The watched row (now LocalCatalogEntry) also carries the fields the shelf
shows, searches, sorts, and groups on. Progress and position still stay
out, so playback saves do not re-query the shelf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@StaticHumStudio

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3004cdfbb2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +110 to +111
DurationSeconds AS durationSeconds, AddedAt AS addedAt,
SeriesName AS seriesName, SeriesSequence AS seriesSequence, GenresJson AS genresJson

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include chapter data in catalog snapshots

When a rescan changes the track/chapter layout without changing the projected metadata—for example, splitting one track into two with the same total duration—ChaptersJson changes but this snapshot remains equal, so distinctUntilChanged() suppresses the refilter and the open Library card keeps displaying the old chapter index/count until reopened. Fresh evidence after the earlier metadata fix is that this expanded projection still omits ChaptersJson; add it to the watched entry as another shelf-visible field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 86e2617, with one difference from the suggestion: the watched row carries length(ChaptersJson) rather than the JSON itself. The chapter list can be large, and the query re-runs on every playback progress write while the Library is open, so the length stands in for it. Any re-split that changes the chapter count changes the length. New instrumented case aRescanThatSplitsAChapterUpdatesTheOpenShelf (same total duration, one chapter becomes two) failed before and passes after on a physical device.

Library cards show "Ch N/M", and a rescan can change the track layout
without changing the title or total duration, so the open card kept the old
chapter count. The watched row now carries the length of the chapter list.
The length stands in for the list itself, which can be large, so playback
saves still do not read every chapter blob.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@StaticHumStudio

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86e2617a43

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

CoverPath AS coverPath, LocalCoverPath AS localCoverPath,
DurationSeconds AS durationSeconds, AddedAt AS addedAt,
SeriesName AS seriesName, SeriesSequence AS seriesSequence, GenresJson AS genresJson,
length(ChaptersJson) AS chaptersJsonLength

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare chapter content instead of only its length

When a rescan keeps the chapter count and total duration but moves a boundary between equal-width timestamps—for example, two 600-second tracks changing from a 300/300 split to 200/400—the serialized chapter JSON can retain exactly the same length. Fresh evidence after the earlier thread is this follow-up's length(ChaptersJson) projection: every projected field then remains equal, so distinctUntilChanged() suppresses the refilter and the open shelf continues calculating Ch N/M from the old boundaries until it is reopened. Observe a collision-resistant chapter signature or the chapter data needed by the card rather than its string length.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining this one. The case needs a rescan that keeps the chapter count, the total duration, and the exact serialized length while moving a boundary, and the only effect is the open card's "Ch N/M" index until the Library is reopened. Observing the chapter list itself means re-reading every local book's chapter blob on each playback progress write while the Library is open, which is the cost this watch is built to avoid, and SQLite on Android has no hash function to make a cheap signature. Length covers the realistic re-split (the chapter count changes).

@StaticHumStudio
StaticHumStudio merged commit eb3e96d into master Oct 2, 2026
1 check passed
@StaticHumStudio
StaticHumStudio deleted the claude/amazing-banach-85f268 branch October 2, 2026 15:37
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