-
-
Notifications
You must be signed in to change notification settings - Fork 671
fix(scan): let update metadata scans refresh artwork, and record uploads explicitly #4160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sdornan
wants to merge
5
commits into
rommapp:master
Choose a base branch
from
sdornan:fix/artwork-provenance-update-scan
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7a3b67e
fix(scan): let update scans replace provider-written artwork urls
sdornan 5289899
fix(scan): record hand-supplied artwork explicitly
sdornan 1b282d9
fix(resources): stop recording screenshots that never landed
sdornan 93ef336
fix(roms): accumulate lock changes, and release the manual lock too
sdornan a2a2c09
test(resources): name the cover re-derivation test for what it checks
sdornan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| """Record which rom fields a user supplied by hand | ||
|
|
||
| Uploading artwork stores the file and clears `url_cover`, so a stored cover | ||
| path with no url was the only thing separating a hand-supplied cover from a | ||
| scraped one. That signal cannot survive: `get_cover` returns no path when the | ||
| file is missing, and the scan writes that straight back to `path_cover_s`, so a | ||
| single scan run while the resources volume is unavailable erases it. The next | ||
| scan then reads the row as having no cover at all, adopts the provider url, and | ||
| replaces the user's cover with provider art once storage returns. | ||
|
|
||
| `locked_fields` records the same fact durably, independent of what is on disk. | ||
|
|
||
| The backfill is the load-bearing part: existing uploads are recognisable only by | ||
| the old inferred marker, and reading it once here is the last chance to do so. | ||
| Without it the first scan after upgrading would replace every uploaded cover in | ||
| every library. | ||
|
|
||
| Manuals are deliberately not backfilled. An uploaded manual and a scraped one | ||
| share a path and neither clears `url_manual`, so nothing distinguishes them and | ||
| any guess would be wrong for half the rows. Manuals stay pinned by the scan, and | ||
| uploads made from here on are marked as they happen. | ||
|
|
||
| Revision ID: 0108_roms_locked_fields | ||
| Revises: 0107_roms_dedup_cover_index | ||
| Create Date: 2026-08-08 00:00:00.000000 | ||
|
|
||
| """ | ||
|
|
||
| import sqlalchemy as sa | ||
| from alembic import op | ||
|
|
||
| from utils.database import CustomJSON | ||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision = "0108_roms_locked_fields" | ||
| down_revision = "0107_roms_dedup_cover_index" | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def _roms_table() -> sa.TableClause: | ||
| return sa.table( | ||
| "roms", | ||
| sa.column("path_cover_s", sa.Text), | ||
| sa.column("url_cover", sa.Text), | ||
| sa.column("locked_fields", CustomJSON()), | ||
| ) | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| with op.batch_alter_table("roms", schema=None) as batch_op: | ||
| batch_op.add_column( | ||
| sa.Column("locked_fields", CustomJSON(), nullable=True), | ||
| if_not_exists=True, | ||
| ) | ||
|
|
||
| roms = _roms_table() | ||
| connection = op.get_bind() | ||
|
|
||
| connection.execute(roms.update().values(locked_fields=[])) | ||
|
|
||
| # A stored cover path with an empty url is the pre-migration marker for an | ||
| # upload. Values go through the JSON type rather than a literal so each | ||
| # dialect serialises its own way. | ||
| connection.execute( | ||
| roms.update() | ||
| .where( | ||
| sa.and_( | ||
| roms.c.path_cover_s.isnot(None), | ||
| roms.c.path_cover_s != "", | ||
| sa.or_(roms.c.url_cover.is_(None), roms.c.url_cover == ""), | ||
| ) | ||
| ) | ||
| .values(locked_fields=["url_cover"]) | ||
| ) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| with op.batch_alter_table("roms", schema=None) as batch_op: | ||
| batch_op.drop_column("locked_fields", if_exists=True) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a ROM with an uploaded cover is unmatched, the endpoint clears its cover paths and URL but leaves
locked_fieldsintact. The next scan sees the survivingurl_coverlock and discards the resolved provider URL, leaving the ROM coverless until the user uploads another cover or explicitly supplies a source URL.Knowledge Base Used: ROM Scanning Flow
Prompt To Fix With AI
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does not reproduce, so leaving this open for a maintainer rather than resolving it myself.
The premise is right:
unmatch_metadataclearspath_cover_s,path_cover_landurl_coverwhile leavinglocked_fieldsintact. The conclusion does not follow, because unmatching never deletes the cover files —remove_coveris only called on theremove_cover=truebranch ofupdate_rom.So the next scan holds
url_coverat"",get_coverskips the download, and then re-derives both paths from disk:The rom keeps its cover, which is intended: unmatching drops provider matches, and a hand-uploaded cover is not one.
A genuinely coverless rom needs the files gone as well as the paths, which is the deliberate "my upload is mine" behaviour with documented exits (remove the cover, or name a different source). Since this is one
remove_covercall away from becoming true,test_cover_with_no_source_url_is_rederived_from_disknow pins it.Full reasoning in #4160 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the thorough write-up and for tracing it through. The key point I missed:
get_coverre-derives paths from disk, so clearingpath_cover_sin the DB doesn't make the cover disappear as long as the files are still there. The lock holds correctly in that state, and the rom keeps its cover.The
test_locked_cover_survives_unmatchtest is exactly the right guardrail — it pins the one-remove_cover-call-away cliff edge so that distinction doesn't get accidentally collapsed later. Happy to defer to a maintainer on whether to resolve the original comment.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the detailed walkthrough — the reasoning is sound. The file-vs-path distinction is the key bit I missed:
get_coverre-deriving from disk means the lock pointing aturl_cover = ""is inert rather than blocking, because the paths come back from the filesystem check regardless.The pinning test makes the one-
remove_cover-call-away edge case explicit, which is exactly the right guard to have here. Happy to mark this resolved.