Skip to content

Delete stale generated files on same-path content change instead of renaming - #7158

Open
lurkfueh wants to merge 7 commits into
stashapp:developfrom
lurkfueh:fix/stale-generated-files-on-rescan
Open

lurkfueh wants to merge 7 commits into
stashapp:developfrom
lurkfueh:fix/stale-generated-files-on-rescan

Conversation

@lurkfueh

@lurkfueh lurkfueh commented Aug 14, 2026 •

Copy link
Copy Markdown

Description

Deletes generated content when a file is modified (i.e. trimmed) but the file path remains the same. Also modifies the UI scene player to track media fingerprint instead of just scene id when deciding to update the player.

Related Issue

Fixes #7155

Testing

  • built the executable in wsl and tested the changes I made on windows
  • Confirmed that generated content is deleted after file modification and rescan
  • Confirmed that after a refetch, the player will pick up changes to rescanned file
  • Added tests to pkg/scene/scan_test.go covering my changes

Screenshots

Checklist

  • I have read and understood the Contributing document.
  • I have read and understood the AI Usage Policy document.
  • I have made corresponding changes to the documentation (if applicable).

AI Usage Disclosure

  • I have used AI tools to assist with this pull request, and I have disclosed the tools and how I used them below.
    used claude to make the code changes, tested and verified that the changes work

Additional Context

Note, this does not touch transcodes (I think they should be removed also but want to hear from maintainers)

…enaming

When a scene's file changes at the same path, the scan handler called
MigrateHash, which renames the old hash's sprite/preview/transcode files
onto the new hash's path instead of regenerating them. Every generator's
"does this already exist" check then found a file already sitting there
and skipped, leaving stale pre-edit content wearing the new hash's name -
and the cover (a DB blob, never touched by MigrateHash) stayed stale too.

Add InvalidateGeneratedFiles, used only for the same-path content-change
case: deletes the old hash's generated files instead of renaming them,
and clears the scene's cover. MigrateHash itself is unchanged and still
renames for its real use case (the hash-naming-algorithm migration task,
where content is unchanged). With the stale files/cover actually gone,
every generator's existing exists-check works correctly on its own for
any future regeneration path - scan, manual Generate, or scheduled.

Also fixes ScenePlayer never refreshing a scene's video source/duration
after such an edit: its player-(re)init effect only fired on scene ID
change, so an in-place file edit under the same scene ID left the player
showing the pre-edit duration/sources indefinitely.

Fixes stashapp#7155

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@lurkfueh
lurkfueh marked this pull request as ready for review August 14, 2026 07:33

@Gykes Gykes 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.

Just a quick pass through. No actual testing was done.

Comment thread pkg/scene/scan.go
// so stale content doesn't pass as valid for the new content
InvalidateGeneratedFiles(h.Paths, oldHash)

for _, s := range existing {

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.

What about secondary files? I think this would clear for every scene even if it's the secondary file that gets modified, is that expected? I think this should be guarded to the primary file only, no?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Could I get more context on what secondary files are or a pointer to what file to start at?

Comment thread pkg/scene/migrate_hash.go Outdated
// where the old hash's generated files are no longer valid and should be
// deleted rather than renamed onto the new hash.
func InvalidateGeneratedFiles(p *paths.Paths, hash string) {
removeSceneFolder(filepath.Join(p.Generated.Markers, hash))

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.

Is this not essentially the same as line 67?

Comment thread pkg/scene/migrate_hash.go
scenePaths := p.Scene
removeSceneFile(scenePaths.GetVideoPreviewPath(hash))
removeSceneFile(scenePaths.GetWebpPreviewPath(hash))
removeSceneFile(scenePaths.GetTranscodePath(hash))

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.

This removes transcodes but your PR description said you're not touching them yet.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

my bad, but do we want to remove them? I think they should be removed since they would now differ from the source.

Comment thread ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx Outdated
lurkfueh and others added 5 commits September 26, 2026 02:16
…rprint comparison

Responds to Gykes' review on stashapp#7158 plus follow-up findings from re-reviewing
the same changes:

- Guard both InvalidateGeneratedFiles and the cover-clear on whether the
  file that changed is actually a scene's primary file. Sprite/preview/
  transcode/cover are generated from a scene's primary file, keyed on its
  hash - editing a secondary file (or a scene's primary file that happens
  to share a hash with another scene's untouched primary file, e.g. two
  files that started out as byte-identical duplicates) must not delete
  content that's still valid for the unchanged primary file.

- Defer InvalidateGeneratedFiles to a post-commit hook instead of running
  it synchronously inside the DB transaction. Deleting from disk isn't
  part of the SQL transaction and can't be rolled back with it - if a
  later step in the same Handle() call fails, the DB now correctly keeps
  believing the old hash's files are current, because they were never
  touched. Registered before the existing ScanGenerator hook so deletion
  still runs before regeneration (post-commit hooks run in registration
  order).

- migrate_hash.go: drop the redundant removeSceneFolder call (same path
  as the one three lines later), and extract the shared "check exists,
  tolerating IsNotExist" logic (existsForAction) used by all five
  exists-then-act call sites instead of duplicating it.

- ScenePlayer.tsx: replace the fingerprint-join comparison with
  fingerprintsMatch, comparing only fingerprint types present in both the
  previous and current file snapshots. Fixes the false positive Gykes
  flagged (a phash finishing generation in the background looking like a
  content edit and resetting playback position) without reintroducing
  the false positive of a size/mod_time-based key (touch, a non-
  timestamp-preserving copy, or a backup restore bumping those without
  changing content) - fingerprints are actual content hashes, immune to
  both. Also short-circuits to "unchanged" when both snapshots have no
  fingerprints at all, so a scene whose file hasn't finished fingerprinting
  yet doesn't get its player reset on every unrelated re-render (e.g.
  periodic resume_time saves during normal playback).

- New tests: TestHandle_ContentChangedAtSamePath_SecondaryFile,
  _SharedHashSibling, and _RollbackDoesNotDeleteFiles (forces a later
  failure and asserts the generated files survive). Existing tests
  switched from mocks.Database.WithTxnCtx (which always rolls back by
  design) to a new withCommittedTxn helper so the post-commit deletion
  path is actually exercised.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Condensed fingerprintsMatch's docblock and the effect's guard comment -
same information, no restatement/rationale-per-sentence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same two spots as the ScenePlayer.tsx trim - the isPrimary guard and the
post-commit hook explanation were both over-explained.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
migrate_hash.go: merge removeSceneFile/removeSceneFolder into one
removePath helper, and use fsutil.RemoveDir instead of raw os.RemoveAll.

scan_test.go: extract newTestScanHandler and seedStaleFile, shared by
all four TestHandle_ContentChangedAtSamePath* tests; hoist the
oldHash/newHash literals (identical across three of them) to package
consts.

ScenePlayer.tsx: merge the sceneId/fileFingerprints refs - they were
always read, written, and reset together - into one loadedSource ref.
Rewrite fingerprintsMatch as a single loop instead of a Map plus an
intermediate filtered array.

All within code this PR added; nothing pre-existing restructured beyond
what the review-feedback commit already touched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deliberate pass over every comment this PR added - condensing the rest
that were still over-explained (doc comments restating the same point
across multiple sentences, test comments repeating what the adjacent
one already said).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Stale sprite/preview/cover/transcode after a same path file content change

2 participants