Skip to content

fix: preserve metadata overrides and upload job state#167

Open
Audionut wants to merge 1 commit into
mainfrom
fix/metadata-override
Open

fix: preserve metadata overrides and upload job state#167
Audionut wants to merge 1 commit into
mainfrom
fix/metadata-override

Conversation

@Audionut

@Audionut Audionut commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added support for a new season-episode release override in the CLI and app, with help text and alias support.
    • Introduced metadata override editing in the UI, including fields and toggles for common metadata settings.
  • Bug Fixes

    • Explicitly chosen category now takes priority over inferred category values.
    • Metadata overrides are now applied consistently across preview, preparation, screenshots, uploads, and tracker workflows.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a --season-episode CLI flag (UseSeasonEpisode) to ReleaseNameOverrides, propagates it through the override signature cache key, and fixes TMDB/category flag precedence. Separately, it introduces MetadataOverrides as a first-class parameter threaded through the entire stack—TypeScript types and normalization helpers, the runtime bridge, webserver routes, backend methods, guiapp Wails methods, job structs, and frontend state/hooks/UI. Upload-job cleanup errors now record without changing terminal job status.

Changes

CLI UseSeasonEpisode flag and override signature keying

Layer / File(s) Summary
CLI flag, request building, and core signature
cmd/upbrr/cli_options.go, cmd/upbrr/main.go, internal/core/core.go, cmd/upbrr/cli_options_test.go, internal/core/description_builder_test.go
UseSeasonEpisode added to cliOptions and releaseOverrideInput; --season-episode/--use-season-episode flags registered and mapped; help text updated; ReleaseNameOverrides.UseSeasonEpisode set from visited["season-episode"]; TMDB category inference gated on !visited["category"]; overrideSignature appends useSeasonEpisode=<bool>; two CLI tests and one signature test added.

MetadataOverrides end-to-end propagation

Layer / File(s) Summary
Type definitions and normalization helpers
gui/frontend/src/types.ts, gui/frontend/src/utils/helpers.ts, gui/frontend/src/utils/index.ts, gui/frontend/src/utils/helpers.test.ts, pkg/api/core.go, pkg/api/services.go
MetadataOverrides, MetadataOverrideEditState, and MetadataOverrideTouchedState types defined; normalizeMetadataOverrides added and normalizeReleaseOverrides extended with additional fields; re-exported from utils; api.Request.MetadataOverrides and MetadataOverrides struct doc-commented; normalization tests added.
Browser runtime bridge
gui/frontend/src/utils/runtime.ts, gui/frontend/src/utils/runtime.test.ts
All affected globalThis.go.guiapp.App bridge wrappers gain metadataOverrides: unknown parameter and forward it as MetadataOverrides in call payloads; runtime bridge tests updated for new argument positions and payloads.
Webserver routes, backend, and job structs
internal/webserver/app_routes.go, internal/webserver/backend.go, internal/webserver/jobs.go, internal/webserver/backend_repo_test.go
All /api/app/* request structs include MetadataOverrides; backend.go methods accept and inject metadataOverrides into api.Request; dupeCheckJob, trackerUploadJob, and trackerUploadRetryRequest store metadataOverrides; StartDupeCheck and StartTrackerUpload signatures extended; retry path preserves overrides; backend tests updated.
Guiapp Wails methods and upload job cleanup
internal/guiapp/app.go, internal/guiapp/dupe_jobs.go, internal/guiapp/upload_jobs.go, internal/guiapp/run_options_test.go, internal/guiapp/app_core_test.go
All Wails-exposed App methods gain metadataOverrides api.MetadataOverrides and set api.Request.MetadataOverrides; trackerUploadJob and dupeCheckJob extended; recordTrackerUploadCleanupError introduced so cleanup failures record error message without changing completed/success terminal status; test assertions updated for new signatures and cleanup behavior.
Frontend state, hooks, and UI controls
gui/frontend/src/app.tsx, gui/frontend/src/hooks/useScreenshots.ts, gui/frontend/src/hooks/useUploadImages.ts, gui/frontend/src/pages/input/index.tsx, gui/frontend/src/pages/menu_images/index.tsx, gui/frontend/src/app.test.ts
app.tsx adds metadataEdits/metadataTouched React state, computes metadataOverrideState, wires it into all backend call sites and hooks, adds clearTrackerUploadJob helper; InputPage gains a "Metadata overrides" section with distributor/originalLanguage text inputs and auto/yes/no selects for PersonalRelease, Commentary, WebDV, StreamOptimized, Anime; MenuImagesPage passes metadataOverrides to ImportMenuImages; hooks updated to accept and forward metadataOverrideState; app tests updated for new argument indices and new metadata override/upload-job test cases.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • autobrr/upbrr#126: Shares code-level overlap around TMDBMetadata localized title handling, which interacts with the MetadataOverrides inference semantics introduced here.
  • autobrr/upbrr#153: Touches the same CLI tracker-auth flow and dupe-check RPC plumbing that this PR extends with metadataOverrides propagation.

Poem

🐇 A rabbit hops through layers deep,
Overrides to carry, secrets to keep.
--season-episode? Yes, I say!
Metadata facts all forwarded today.
From bridge to backend, each method grows—
The warren expands wherever code flows! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: preserving metadata overrides and improving upload job state handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/metadata-override

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gui/frontend/src/app.tsx`:
- Around line 1443-1476: The metadata override builder in metadataOverrideState
is still serializing cleared text fields as empty strings. Update the
Distributor and OriginalLanguage handling so touched-but-empty edits are treated
as unset (not emitted in overrides), letting normalizeMetadataOverrides drop
them and revert to auto behavior. Use the existing metadataTouched,
metadataEdits, and normalizeMetadataOverrides flow to ensure only non-empty text
overrides are written.

In `@gui/frontend/src/hooks/useScreenshots.ts`:
- Around line 144-145: The screenshot follow-up mutations in useScreenshots are
using the live metadataOverrideState at click time, which can drift from the
snapshot used when the screenshots were loaded. Capture and reuse the same
normalized override snapshot from the initial load path (the call that uses
normalizeMetadataOverrides in useScreenshots) for the save/delete/update flows,
and thread that snapshot through the relevant handlers instead of reading
current overrides again. Make sure the handlers around the follow-up calls in
useScreenshots all reference the same loaded override state so the UI plan and
the mutation target stay aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 62da0244-2c1d-469b-b193-fc8cd46688c1

📥 Commits

Reviewing files that changed from the base of the PR and between a93a008 and a8c2a83.

📒 Files selected for processing (28)
  • cmd/upbrr/cli_options.go
  • cmd/upbrr/cli_options_test.go
  • cmd/upbrr/main.go
  • gui/frontend/src/app.test.ts
  • gui/frontend/src/app.tsx
  • gui/frontend/src/hooks/useScreenshots.ts
  • gui/frontend/src/hooks/useUploadImages.ts
  • gui/frontend/src/pages/input/index.tsx
  • gui/frontend/src/pages/menu_images/index.tsx
  • gui/frontend/src/types.ts
  • gui/frontend/src/utils/helpers.test.ts
  • gui/frontend/src/utils/helpers.ts
  • gui/frontend/src/utils/index.ts
  • gui/frontend/src/utils/runtime.test.ts
  • gui/frontend/src/utils/runtime.ts
  • internal/core/core.go
  • internal/core/description_builder_test.go
  • internal/guiapp/app.go
  • internal/guiapp/app_core_test.go
  • internal/guiapp/dupe_jobs.go
  • internal/guiapp/run_options_test.go
  • internal/guiapp/upload_jobs.go
  • internal/webserver/app_routes.go
  • internal/webserver/backend.go
  • internal/webserver/backend_repo_test.go
  • internal/webserver/jobs.go
  • pkg/api/core.go
  • pkg/api/services.go

Comment thread gui/frontend/src/app.tsx
Comment on lines +1443 to +1476
const metadataOverrideState = useMemo(() => {
const overrides: MetadataOverrides = {};
if (metadataTouched.distributor) {
overrides.Distributor = metadataEdits.distributor.trim();
}
if (metadataTouched.originalLanguage) {
overrides.OriginalLanguage = metadataEdits.originalLanguage.trim();
}
const personalRelease = parseBoolOverrideEditValue(metadataEdits.personalRelease);
const commentary = parseBoolOverrideEditValue(metadataEdits.commentary);
const webDV = parseBoolOverrideEditValue(metadataEdits.webDV);
const streamOptimized = parseBoolOverrideEditValue(metadataEdits.streamOptimized);
const anime = parseBoolOverrideEditValue(metadataEdits.anime);
if (metadataTouched.personalRelease && personalRelease !== null) {
overrides.PersonalRelease = personalRelease;
}
if (metadataTouched.commentary && commentary !== null) {
overrides.Commentary = commentary;
}
if (metadataTouched.webDV && webDV !== null) {
overrides.WebDV = webDV;
}
if (metadataTouched.streamOptimized && streamOptimized !== null) {
overrides.StreamOptimized = streamOptimized;
}
if (metadataTouched.anime && anime !== null) {
overrides.Anime = anime;
}
return {
overrides,
dirty: Object.keys(overrides).length > 0,
invalid: false,
};
}, [metadataEdits, metadataTouched]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't serialize cleared text overrides as empty strings.

Line 1446 and Line 1449 still write "" once a text field has been touched, and normalizeMetadataOverrides only drops null/undefined. After a user clears Distributor or Original language, later refreshes/uploads keep sending a blank override instead of reverting to auto.

Suggested fix
   const metadataOverrideState = useMemo(() => {
     const overrides: MetadataOverrides = {};
     if (metadataTouched.distributor) {
-      overrides.Distributor = metadataEdits.distributor.trim();
+      const distributor = metadataEdits.distributor.trim();
+      if (distributor) {
+        overrides.Distributor = distributor;
+      }
     }
     if (metadataTouched.originalLanguage) {
-      overrides.OriginalLanguage = metadataEdits.originalLanguage.trim();
+      const originalLanguage = metadataEdits.originalLanguage.trim();
+      if (originalLanguage) {
+        overrides.OriginalLanguage = originalLanguage;
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const metadataOverrideState = useMemo(() => {
const overrides: MetadataOverrides = {};
if (metadataTouched.distributor) {
overrides.Distributor = metadataEdits.distributor.trim();
}
if (metadataTouched.originalLanguage) {
overrides.OriginalLanguage = metadataEdits.originalLanguage.trim();
}
const personalRelease = parseBoolOverrideEditValue(metadataEdits.personalRelease);
const commentary = parseBoolOverrideEditValue(metadataEdits.commentary);
const webDV = parseBoolOverrideEditValue(metadataEdits.webDV);
const streamOptimized = parseBoolOverrideEditValue(metadataEdits.streamOptimized);
const anime = parseBoolOverrideEditValue(metadataEdits.anime);
if (metadataTouched.personalRelease && personalRelease !== null) {
overrides.PersonalRelease = personalRelease;
}
if (metadataTouched.commentary && commentary !== null) {
overrides.Commentary = commentary;
}
if (metadataTouched.webDV && webDV !== null) {
overrides.WebDV = webDV;
}
if (metadataTouched.streamOptimized && streamOptimized !== null) {
overrides.StreamOptimized = streamOptimized;
}
if (metadataTouched.anime && anime !== null) {
overrides.Anime = anime;
}
return {
overrides,
dirty: Object.keys(overrides).length > 0,
invalid: false,
};
}, [metadataEdits, metadataTouched]);
const metadataOverrideState = useMemo(() => {
const overrides: MetadataOverrides = {};
if (metadataTouched.distributor) {
const distributor = metadataEdits.distributor.trim();
if (distributor) {
overrides.Distributor = distributor;
}
}
if (metadataTouched.originalLanguage) {
const originalLanguage = metadataEdits.originalLanguage.trim();
if (originalLanguage) {
overrides.OriginalLanguage = originalLanguage;
}
}
const personalRelease = parseBoolOverrideEditValue(metadataEdits.personalRelease);
const commentary = parseBoolOverrideEditValue(metadataEdits.commentary);
const webDV = parseBoolOverrideEditValue(metadataEdits.webDV);
const streamOptimized = parseBoolOverrideEditValue(metadataEdits.streamOptimized);
const anime = parseBoolOverrideEditValue(metadataEdits.anime);
if (metadataTouched.personalRelease && personalRelease !== null) {
overrides.PersonalRelease = personalRelease;
}
if (metadataTouched.commentary && commentary !== null) {
overrides.Commentary = commentary;
}
if (metadataTouched.webDV && webDV !== null) {
overrides.WebDV = webDV;
}
if (metadataTouched.streamOptimized && streamOptimized !== null) {
overrides.StreamOptimized = streamOptimized;
}
if (metadataTouched.anime && anime !== null) {
overrides.Anime = anime;
}
return {
overrides,
dirty: Object.keys(overrides).length > 0,
invalid: false,
};
}, [metadataEdits, metadataTouched]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/frontend/src/app.tsx` around lines 1443 - 1476, The metadata override
builder in metadataOverrideState is still serializing cleared text fields as
empty strings. Update the Distributor and OriginalLanguage handling so
touched-but-empty edits are treated as unset (not emitted in overrides), letting
normalizeMetadataOverrides drop them and revert to auto behavior. Use the
existing metadataTouched, metadataEdits, and normalizeMetadataOverrides flow to
ensure only non-empty text overrides are written.

Comment on lines +144 to 145
normalizeMetadataOverrides(metadataOverrideState?.overrides || {}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reuse the same override snapshot for screenshot follow-up calls.

Line 144 fetches the plan under the current metadata overrides, but Line 255, Line 422, Line 450, and Line 471 reuse whatever override state is current when the user clicks save/delete. The parent app does not invalidate the loaded screenshot state on override edits, so the UI can show plan A while these mutations are sent against plan B.

Suggested direction
+  const planMetadataOverridesRef = useRef<MetadataOverrides>({});
+
   const loadScreenshotPlan = useCallback(
     async (revealSelections = false): Promise<ScreenshotPlan | null> => {
+      const normalizedMetadataOverrides = normalizeMetadataOverrides(
+        metadataOverrideState?.overrides || {},
+      );
       setScreenshotsError("");
       const fetcher = globalThis.go?.guiapp?.App?.FetchScreenshotPlan;
@@
         const result = await fetcher(
           path.trim(),
           normalizeOverrides(idOverrideState?.overrides || {}),
           normalizeReleaseOverrides(releaseOverrideState?.overrides || {}),
-          normalizeMetadataOverrides(metadataOverrideState?.overrides || {}),
+          normalizedMetadataOverrides,
         );
+        planMetadataOverridesRef.current = normalizedMetadataOverrides;
@@
         await saver(
           path.trim(),
           normalizeOverrides(idOverrideState?.overrides || {}),
           normalizeReleaseOverrides(releaseOverrideState?.overrides || {}),
-          normalizeMetadataOverrides(metadataOverrideState?.overrides || {}),
+          planMetadataOverridesRef.current,
           next.map((entry) => entry.image),
         );

Also applies to: 255-262, 422-478

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/frontend/src/hooks/useScreenshots.ts` around lines 144 - 145, The
screenshot follow-up mutations in useScreenshots are using the live
metadataOverrideState at click time, which can drift from the snapshot used when
the screenshots were loaded. Capture and reuse the same normalized override
snapshot from the initial load path (the call that uses
normalizeMetadataOverrides in useScreenshots) for the save/delete/update flows,
and thread that snapshot through the relevant handlers instead of reading
current overrides again. Make sure the handlers around the follow-up calls in
useScreenshots all reference the same loaded override state so the UI plan and
the mutation target stay aligned.

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