Skip to content

馃悰 Fix stale thumbnail caching - #1380

Open
ajselzilic wants to merge 1 commit into
stumpapp:nightlyfrom
ajselzilic:codex/fix-thumbnail-cache
Open

ajselzilic wants to merge 1 commit into
stumpapp:nightlyfrom
ajselzilic:codex/fix-thumbnail-cache

Conversation

@ajselzilic

@ajselzilic ajselzilic commented Sep 4, 2026

Copy link
Copy Markdown

Closes #913

Related to #546 and #619

Regenerates Stump-generated thumbnails during forced library rebuilds and adds modification timestamps to media, series, and library thumbnail URLs. When a thumbnail changes, the related timestamp changes with it, giving the browser a new URL and bypassing the existing long-lived cache without disabling caching.

Relevant browser queries are refreshed after thumbnail uploads, selections, deletions, scans, and generation jobs. Thumbnail consumers, including tables and the File Explorer, now use the versioned URLs returned by GraphQL instead of rebuilding unversioned URLs locally.

New generated thumbnails include a .generated filename marker so Stump can distinguish them from custom uploads. This allows forced rebuilds to replace generated thumbnails without overwriting user-provided covers. A filename marker was chosen instead of a database migration because the distinction is only needed while managing the files, and existing thumbnails cannot be reliably classified after the fact. Existing unmarked thumbnails are therefore treated as custom or legacy files, while newly generated thumbnails can be safely replaced going forward.

While tracing the full thumbnail flow, this also fixes:

  • Library and series thumbnail selections updating the source book instead of the intended entity.
  • Thumbnail mutations returning stale entity data after an update.
  • Library thumbnail deletion leaving stale paths and metadata in the database.
  • Authenticated images displaying an outdated result when their source changes during an active request.
  • Failed authenticated image requests leaving the image without a fallback.

Library thumbnail cleanup now clears its database state transactionally. It also uses relational queries instead of passing every series and media ID as a separate SQL parameter, avoiding SQLite bind limits on large libraries.

@ajselzilic
ajselzilic force-pushed the codex/fix-thumbnail-cache branch from e632317 to 545437f Compare September 4, 2026 12:30
@ajselzilic ajselzilic changed the title Fix stale thumbnail caching 馃悰 Fix stale thumbnail caching Sep 4, 2026
@aaronleopold

Copy link
Copy Markdown
Collaborator

Hey, just wanted to say this has not slipped my radar. I'm hoping to have time closer to or over the weekend to give this a proper look

@aaronleopold aaronleopold left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I had a few comments, but otherwise I think this is on the right track. Thanks for working on this! Let me know if you have any questions

Comment thread core/src/filesystem/image/thumbnail/generate.rs
Comment on lines +63 to +72
pub is_custom: bool,
pub target: ThumbnailTarget,
}

#[derive(Debug, Clone)]
pub enum ThumbnailTarget {
Media,
Series(String),
Library(String),
}

@aaronleopold aaronleopold Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure I understand the move away from filename and towards an enum, rather than just making the callers set filename accordingly (e.g., mediaIdaryid.generated for a generated one or mediaId for custom).

I guess coming back to this after a little more review, it's intention is for the update clause. I think we can get away without the is_custom flag, though. I don't see why a custom thumbnail would even pass through the generate flow.

Separately, I think my decision to include database updates in the generate_book_thumbnail is one I am currently questioning. If we had e.g. generate_library_thumbnail that calls (renamed) generate_thumbnail_from_book then that library-specific one can handle the update call and be a bit cleaner to follow. Then the same would follow for generate_series_thumbnail/generate_book_thumbnail calls generate_thumbnail_from_book and handles their own update.

Comment on lines +339 to +348
if is_custom {
match &target {
ThumbnailTarget::Media => {
bump_media_thumbnail_fallbacks(conn, Some(&book.series_id)).await?
},
ThumbnailTarget::Series(id) => {
bump_series_thumbnail_fallbacks(conn, std::slice::from_ref(id)).await?
},
ThumbnailTarget::Library(_) => {},
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as above re: why would a custom thumb flow through to the generate flow?

.is_some_and(|stem| stem.ends_with(".generated"))
}

pub async fn bump_media_thumbnail_fallbacks<C>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I understand why these bumps were added, but I don't love how it works around an awkward point (image fallbacks and cache-friendly URLs). I think it's fine for now, just being a bit pedantic

Comment on lines +44 to +54
.then((data) => {
if (active) {
setLoadedImage({ src, token, url: URL.createObjectURL(data) })
}
})
.catch((error) => {
if (active) {
setLoadedImage({ src, token, url: src })
console.error('Failed to load authenticated image:', error)
}
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I generally prefer async/await, which is why originally I handled the local state in the fetchImage callback. If it was refactored for the active I think react-query has utilities to cancel in-flight queries instead

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.

2 participants