Skip to content

refactor(dupe): make duplicate checks evidence driven - #327

Open
Audionut wants to merge 15 commits into
mainfrom
refactor/evidence-driven-dupe-checking
Open

refactor(dupe): make duplicate checks evidence driven#327
Audionut wants to merge 15 commits into
mainfrom
refactor/evidence-driven-dupe-checking

Conversation

@Audionut

@Audionut Audionut commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Closes #325.

Supersedes #326. Builds on the tracker evidence and policy work in #316.

Summary

  • separate candidate discovery from duplicate policy with work-scoped search evidence and effective completeness
  • widen Unit3D, AZ-family, and standalone tracker searches so upload-slot filters cannot hide same-work candidates before evaluation
  • preserve native tracker facts, pagination state, wrong-work counts, and normal/pending overlap handling
  • centralize exact-file identity, pack precedence, HDR/media coexistence, and general fallback behavior under general/duplicate/v4
  • remove the unused SlotDifferencesOverrideGeneral configuration now that tracker-matched slot findings always take priority
  • add contract and regression coverage across the shared evaluator and every registered duplicate-search adapter family

Why

#325 exposed a byte-identical duplicate that passed because its resolved year differed from the tracker's name. The year comparison was one symptom of a broader flow problem: adapters mixed discovery with policy, discarded rows using the proposed upload's current slot, and reported endpoint exhaustion as complete even when the query did not prove same-work coverage. Once a candidate was filtered out, the evaluator could not recover it regardless of size, files, provider context, or tracker policy.

#326 addresses the reported year case with provider-aware name matching and metadata precedence. This PR instead fixes the boundary that allowed that case and related false negatives: searches return the broadest defensible same-work candidate set, evidence records what scope was actually searched, and the evaluator alone decides whether candidates conflict or coexist. This also follows the signal from #316, where tracker-specific disc and release-slot fixes showed that discovery filters and policy decisions were spread across adapters.

Behavior

  • authoritative provider-ID or tracker-group searches can prove a complete empty result; title fallbacks remain incomplete and require review
  • Unit3D consumes normal and pending endpoints, all advertised pages, filters only conflicting work IDs, and keeps the richer row when results overlap
  • BTN uses 100-result native pages and reported result totals, while daily episodes use one exact date-scoped request instead of enumerating broad title results
  • public search evidence includes work scope and wrong-work counts so API and WebUI consumers can explain effective incompleteness
  • AZ-family and standalone adapters no longer pre-filter by proposed quality, rip type, resolution, episode/pack shape, or other release-slot facts
  • exact file identity compares primary video basenames, either as equal video sets with agreeing sizes or a single proposed video contained in a larger candidate; auxiliary files and fuzzy names do not become exact identity
  • year or release-name styling differences no longer exclude otherwise relevant candidates from general policy evaluation
  • same-season pack versus episode direction applies when the search is provider-ID or tracker-group bound; title-fallback candidates fall through to review rather than directional precedence
  • HDR, edition, region, 3D, resolution, and media-class differences coexist only when supported by sufficient positive or complete evidence
  • coexists results remain retained evidence but stay outside the actionable duplicate list

Validation

  • make test-go
  • go test -race -v -timeout 20m ./internal/webserver/... ./pkg/api
  • make test-frontend
  • make lint
  • make logpolicy
  • make pathpolicy
  • make gofix-check-changed
  • git diff --check
  • pre-commit and pre-push hooks

Summary by CodeRabbit

  • New Features

    • Duplicate searches now report clearer completeness, search scope, pagination, and warning details.
    • API search evidence includes work scope and counts of excluded conflicting results.
    • Broader title and provider-ID matching improves duplicate discovery across trackers.
  • Bug Fixes

    • Improved pagination, HTTP failure, incomplete-search, deduplication, and conflicting-metadata handling.
    • Valid results are preserved when safety limits or pagination issues occur.
    • Duplicate detection better recognizes exact matches and season-pack relationships.

Broaden tracker searches to enumerate same-work candidates before policy evaluation.

Track work scope and effective completeness separately while preserving native candidate facts.

Centralize exact-file, pack, and general coexistence rules.

Refs #325
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Duplicate search handling now records work scope, effective completeness, wrong-work counts, pagination warnings, and structured results. Unit3D searches filter conflicting TMDB IDs and retain richer duplicates. Duplicate evaluation and tracker policies now use generalized identity, fallback, and precedence rules.

Changes

Duplicate search and evaluation

Layer / File(s) Summary
Search evidence contracts and propagation
internal/trackers/dupe/*, internal/trackers/projection.go, pkg/api/dupes.go, internal/webserver/openapi/*
Search results now expose work scope, effective completeness, wrong-work counts, and versioned search contracts.
Duplicate evaluation and policy rules
internal/trackers/dupe/evaluator.go, internal/trackers/dupe/findings.go, internal/trackers/definition.go, internal/trackers/registry.go, internal/trackers/impl/*/profile.go
Evaluation now compares primary video identity, supports authoritative season-pack containment, and applies generalized fallback findings. Same-slot fallback and inherited season-pack precedence configuration were removed.
Unit3D filtering and aggregation
internal/trackers/data/unit3d.go, internal/trackers/impl/unit3d/dupe.go, internal/trackers/data/*_test.go
Unit3D searches filter conflicting TMDB IDs, preserve entries across endpoint warnings, count omitted rows, deduplicate entries, and retain richer duplicates.
Tracker adapter search evidence
internal/trackers/impl/standalone/*/dupe.go, internal/trackers/impl/azfamily/dupe.go, internal/trackers/impl/standalone/internal/jsondupe/list.go
Tracker adapters now return structured search evidence, broader query selection, explicit work scope, and pagination metadata.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TrackerAdapter
  participant SearchEvidence
  participant DuplicateEvaluator
  TrackerAdapter->>SearchEvidence: return entries, work scope, and pagination metadata
  SearchEvidence->>DuplicateEvaluator: provide EffectiveComplete and WrongWorkCount
  DuplicateEvaluator->>DuplicateEvaluator: apply identity and policy findings
  DuplicateEvaluator-->>TrackerAdapter: return duplicate relations and evidence
Loading

Possibly related PRs

Poem

A rabbit checks each search row,
Keeps the richer facts in tow.
Wrong-work counts now clearly show,
Warnings mark the gaps below.
Duplicate rules sort the hay.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.71% 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
Linked Issues check ✅ Passed The changes prevent year or release-name differences from excluding exact file matches and route broader candidates to duplicate evaluation for issue #325.
Out of Scope Changes check ✅ Passed The tracker, policy, API, and test changes support the stated evidence-driven duplicate discovery and evaluation objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main refactor toward evidence-driven duplicate checks.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/evidence-driven-dupe-checking

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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/trackers/impl/standalone/hds/dupe.go (1)

103-115: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Detect an actual next page before marking the search incomplete.

Line 108 treats every pagination link with pages= as a next-page link. On the final page, a link to an earlier page can satisfy this condition. The loop then stops at Line 115 with complete == false, so a complete empty provider-ID search becomes blocked.

Accept only an explicit next control, or parse the target page and require it to be greater than page.

🤖 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 `@internal/trackers/impl/standalone/hds/dupe.go` around lines 103 - 115, Update
the next-page detection in the pagination loop around commonhttp.FirstNode so a
generic pages= link is not accepted as the next page. Require either an explicit
“Next”/“>>” control or parse the href and verify its target page is greater than
the current page variable, preserving complete=true when no actual next page
exists.
🧹 Nitpick comments (3)
internal/trackers/dupe/evaluator_test.go (1)

988-1003: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add negative coverage for generic shared file names.

TestEvaluatePartialCandidateFileSetWithExactBasenameIsExact uses the distinctive basename Example.Release.2026.mkv. The new overlap rule in exactCandidate also matches generic member names. Add a case that shares only a generic file such as sample.mkv and asserts that the relation is not api.DupeRelationExactDuplicate. This test gap shares one root cause with the overlap rule in internal/trackers/dupe/evaluator.go at Line 90.

🤖 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 `@internal/trackers/dupe/evaluator_test.go` around lines 988 - 1003, Extend
TestEvaluatePartialCandidateFileSetWithExactBasenameIsExact with negative
coverage using a generic shared filename such as sample.mkv, while keeping the
candidate file set partial. Assert that Evaluate does not return
api.DupeRelationExactDuplicate for this case, covering the generic-name overlap
behavior in exactCandidate.
internal/trackers/impl/dupe_policy_test.go (1)

205-208: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the remaining evidence fixtures with the new work-scope contract.

assertRelation and assertPTP now pass WorkScope: dupe.WorkScopeTrackerGroup. assertAREvaluation at Line 231 and assertRTF at Line 272 still pass dupe.SearchEvidence{Complete: true} with no work scope. Those evaluations are therefore effectively incomplete. The assertions still pass because they check only Candidates[0].Relation. Set the same work scope in those helpers to keep the fixtures consistent.

Also applies to: 375-378

🤖 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 `@internal/trackers/impl/dupe_policy_test.go` around lines 205 - 208, Update
the SearchEvidence fixtures in assertAREvaluation and assertRTF to include
WorkScope: dupe.WorkScopeTrackerGroup alongside Complete: true, matching
assertRelation and assertPTP so these evaluations remain complete under the new
work-scope contract.
internal/trackers/impl/azfamily/dupe.go (1)

165-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log pagination decisions.

Line 167 and Line 171 stop enumeration but emit no operator-visible warning. Log the blocked outcome with the tracker, page count, completion state, and decision reason. Do not log pageURL.

Log normal completion at DEBUG level.

As per coding guidelines, add operator-visible progress and decision-point logs. Use warnings for blocked outcomes and DEBUG for troubleshooting context.

🤖 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 `@internal/trackers/impl/azfamily/dupe.go` around lines 165 - 213, Add
operator-visible logs in the pagination loop: when the max-page safety bound or
repeated-page check stops enumeration, emit a warning containing the tracker,
page count, incomplete completion state, and decision reason without logging
pageURL. When pagination reaches normal completion, emit a DEBUG log with the
tracker, page count, and completed state, using the surrounding AZ-family
enumeration function’s existing logger.

Source: Coding guidelines

🤖 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 `@internal/trackers/data/unit3d.go`:
- Around line 463-468: Update the warning construction in the WrongWorkCount
handling to use “row” when WrongWorkCount equals 1 and “rows” otherwise,
preserving the existing message for plural counts. Update the exact assertion in
the Unit3D test to expect the corrected singular message.
- Around line 460-462: Update the result accumulation around dedupeUnit3DEntries
so entries from /api/torrents/filter and /api/torrents/pending use distinct
deduplication namespaces. Prefix or otherwise namespace pending entry IDs before
appending, ensuring dedupeUnit3DEntries removes only repeats from the same
endpoint and preserves distinct candidates with matching numeric IDs.

In `@internal/trackers/dupe/evaluator.go`:
- Around line 88-92: Update exactCandidate in
internal/trackers/dupe/evaluator.go at lines 88-92 to require full
target-file-set containment or otherwise exclude generic basename-only matches
such as sample.mkv. In internal/trackers/dupe/evaluator_test.go at lines
988-1003, add coverage where target and candidate share only sample.mkv and
assert the result is not api.DupeRelationExactDuplicate.

In `@internal/trackers/dupe/service.go`:
- Around line 558-574: Extend api.DupeSearchEvidence in dupes.go with WorkScope
and WrongWorkCount, then populate and preserve both fields in the
duplicate-search result and API/webui projection around the tracker completion
flow. Use the existing search.WorkScope and search.WrongWorkCount values so
consumers can identify effective incompleteness and excluded-row counts.

In `@internal/trackers/impl/azfamily/dupe.go`:
- Around line 207-211: Update the pagination flow around nextAZPage to validate
nextPage with the existing same-origin URL helper against site.baseURL before
the request adds loadedCookies; treat rejected or empty URLs as completion and
do not follow them. Add a test covering an external rel="next" URL.

In `@internal/trackers/impl/standalone/bt/dupe.go`:
- Line 171: Update processBTGroupPage and the related release-name extraction
flow so TV pack folder extraction is determined from the candidate page rather
than the uploaded release. Preserve the candidate page’s pack release name when
the first file is an episode, while retaining folder-based extraction for
folder-based TV packs. Add a regression test covering a TV pack whose first file
is an episode and asserting the pack release name is preserved.

In `@internal/trackers/impl/standalone/czt/dupe.go`:
- Around line 146-147: Update the comment above cztSearchQuery to accurately
describe the current precedence, noting that meta.Release.Title is selected
before exact upload/client names. Keep the query implementation unchanged and
remove the stale claim that exact names are preferred first.

In `@internal/trackers/impl/standalone/hdt/dupe.go`:
- Around line 53-57: Validate the resolved title query before performing tracker
searches: in internal/trackers/impl/standalone/hdt/dupe.go lines 53-57 and
internal/trackers/impl/standalone/is/dupe.go lines 62-65, return
dupe.NotRun(dupe.NotRunMissingMetadata, ...) when query is empty; otherwise
preserve the existing parameter setup and search flow.

---

Outside diff comments:
In `@internal/trackers/impl/standalone/hds/dupe.go`:
- Around line 103-115: Update the next-page detection in the pagination loop
around commonhttp.FirstNode so a generic pages= link is not accepted as the next
page. Require either an explicit “Next”/“>>” control or parse the href and
verify its target page is greater than the current page variable, preserving
complete=true when no actual next page exists.

---

Nitpick comments:
In `@internal/trackers/dupe/evaluator_test.go`:
- Around line 988-1003: Extend
TestEvaluatePartialCandidateFileSetWithExactBasenameIsExact with negative
coverage using a generic shared filename such as sample.mkv, while keeping the
candidate file set partial. Assert that Evaluate does not return
api.DupeRelationExactDuplicate for this case, covering the generic-name overlap
behavior in exactCandidate.

In `@internal/trackers/impl/azfamily/dupe.go`:
- Around line 165-213: Add operator-visible logs in the pagination loop: when
the max-page safety bound or repeated-page check stops enumeration, emit a
warning containing the tracker, page count, incomplete completion state, and
decision reason without logging pageURL. When pagination reaches normal
completion, emit a DEBUG log with the tracker, page count, and completed state,
using the surrounding AZ-family enumeration function’s existing logger.

In `@internal/trackers/impl/dupe_policy_test.go`:
- Around line 205-208: Update the SearchEvidence fixtures in assertAREvaluation
and assertRTF to include WorkScope: dupe.WorkScopeTrackerGroup alongside
Complete: true, matching assertRelation and assertPTP so these evaluations
remain complete under the new work-scope contract.
🪄 Autofix

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 Plus

Run ID: bb88ded3-7d94-4910-8f04-191518cd4d73

📥 Commits

Reviewing files that changed from the base of the PR and between 47bdb91 and 17f0f67.

📒 Files selected for processing (67)
  • internal/trackers/data/unit3d.go
  • internal/trackers/data/unit3d_fixture_test.go
  • internal/trackers/data/unit3d_test.go
  • internal/trackers/definition.go
  • internal/trackers/dupe/evaluator.go
  • internal/trackers/dupe/evaluator_test.go
  • internal/trackers/dupe/findings.go
  • internal/trackers/dupe/normalize.go
  • internal/trackers/dupe/service.go
  • internal/trackers/dupe/service_test.go
  • internal/trackers/dupe/set.go
  • internal/trackers/dupe/set_test.go
  • internal/trackers/dupe/types.go
  • internal/trackers/dupe/types_test.go
  • internal/trackers/impl/azfamily/definition.go
  • internal/trackers/impl/azfamily/dupe.go
  • internal/trackers/impl/azfamily/dupe_test.go
  • internal/trackers/impl/dupe_handlers_contract_test.go
  • internal/trackers/impl/dupe_policy_test.go
  • internal/trackers/impl/standalone/ant/dupe.go
  • internal/trackers/impl/standalone/ant/dupe_fixture_test.go
  • internal/trackers/impl/standalone/ant/dupe_test.go
  • internal/trackers/impl/standalone/ar/dupe.go
  • internal/trackers/impl/standalone/ar/dupe_test.go
  • internal/trackers/impl/standalone/asc/dupe.go
  • internal/trackers/impl/standalone/bhd/dupe.go
  • internal/trackers/impl/standalone/bhd/dupe_test.go
  • internal/trackers/impl/standalone/bjs/dupe.go
  • internal/trackers/impl/standalone/bjs/dupe_test.go
  • internal/trackers/impl/standalone/bt/dupe.go
  • internal/trackers/impl/standalone/btn/dupe.go
  • internal/trackers/impl/standalone/btn/dupe_test.go
  • internal/trackers/impl/standalone/czt/dupe.go
  • internal/trackers/impl/standalone/czt/dupe_test.go
  • internal/trackers/impl/standalone/dc/dupe.go
  • internal/trackers/impl/standalone/dc/dupe_test.go
  • internal/trackers/impl/standalone/ff/dupe.go
  • internal/trackers/impl/standalone/fl/dupe.go
  • internal/trackers/impl/standalone/gpw/dupe.go
  • internal/trackers/impl/standalone/gpw/dupe_test.go
  • internal/trackers/impl/standalone/hdb/dupe.go
  • internal/trackers/impl/standalone/hds/dupe.go
  • internal/trackers/impl/standalone/hdt/dupe.go
  • internal/trackers/impl/standalone/internal/jsondupe/list.go
  • internal/trackers/impl/standalone/is/dupe.go
  • internal/trackers/impl/standalone/mtv/dupe.go
  • internal/trackers/impl/standalone/mtv/dupe_test.go
  • internal/trackers/impl/standalone/mtv/profile.go
  • internal/trackers/impl/standalone/nbl/dupe.go
  • internal/trackers/impl/standalone/ptp/dupe.go
  • internal/trackers/impl/standalone/ptp/dupe_test.go
  • internal/trackers/impl/standalone/pts/dupe.go
  • internal/trackers/impl/standalone/rtf/dupe.go
  • internal/trackers/impl/standalone/spd/dupe.go
  • internal/trackers/impl/standalone/spd/dupe_test.go
  • internal/trackers/impl/standalone/thr/dupe.go
  • internal/trackers/impl/standalone/tl/dupe.go
  • internal/trackers/impl/unit3d/dupe.go
  • internal/trackers/impl/unit3d/sites/aither/profile.go
  • internal/trackers/impl/unit3d/sites/hhd/profile.go
  • internal/trackers/impl/unit3d/sites/lume/profile.go
  • internal/trackers/impl/unit3d/sites/otw/profile.go
  • internal/trackers/impl/unit3d/sites/sp/profile.go
  • internal/trackers/impl/unit3d/sites/ulcx/profile.go
  • internal/trackers/projection.go
  • internal/trackers/registry.go
  • internal/trackers/registry_test.go
💤 Files with no reviewable changes (3)
  • internal/trackers/impl/standalone/mtv/profile.go
  • internal/trackers/impl/azfamily/definition.go
  • internal/trackers/registry.go

Comment thread internal/trackers/data/unit3d.go
Comment on lines +463 to 468
if result.WrongWorkCount > 0 {
result.Warning = appendUnit3DWarning(
result.Warning,
fmt.Sprintf("Unit3D search omitted %d rows with conflicting TMDB IDs", result.WrongWorkCount),
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the singular form in the operator-visible warning.

For one omitted row the message reads Unit3D search omitted 1 rows with conflicting TMDB IDs. Use a singular form for one row. The assertion in internal/trackers/data/unit3d_test.go at Line 238 checks the exact substring omitted 1 rows with conflicting TMDB IDs, so update that test with the message.

🐛 Proposed fix
 	if result.WrongWorkCount > 0 {
+		rows := "rows"
+		if result.WrongWorkCount == 1 {
+			rows = "row"
+		}
 		result.Warning = appendUnit3DWarning(
 			result.Warning,
-			fmt.Sprintf("Unit3D search omitted %d rows with conflicting TMDB IDs", result.WrongWorkCount),
+			fmt.Sprintf("Unit3D search omitted %d %s with conflicting TMDB IDs", result.WrongWorkCount, rows),
 		)
 	}
📝 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
if result.WrongWorkCount > 0 {
result.Warning = appendUnit3DWarning(
result.Warning,
fmt.Sprintf("Unit3D search omitted %d rows with conflicting TMDB IDs", result.WrongWorkCount),
)
}
if result.WrongWorkCount > 0 {
rows := "rows"
if result.WrongWorkCount == 1 {
rows = "row"
}
result.Warning = appendUnit3DWarning(
result.Warning,
fmt.Sprintf("Unit3D search omitted %d %s with conflicting TMDB IDs", result.WrongWorkCount, rows),
)
}
🤖 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 `@internal/trackers/data/unit3d.go` around lines 463 - 468, Update the warning
construction in the WrongWorkCount handling to use “row” when WrongWorkCount
equals 1 and “rows” otherwise, preserving the existing message for plural
counts. Update the exact assertion in the Unit3D test to expect the corrected
singular message.

Comment thread internal/trackers/dupe/evaluator.go Outdated
Comment on lines 558 to +574
Search: api.DupeSearchEvidence{
Complete: search.Complete,
Complete: effectiveComplete,
Pages: search.Pages,
CandidateCount: len(candidates),
Scope: search.Scope,
Warnings: cloneNotes(search.Warnings),
},
}
s.logger.Infof(
"dupechecking: search tracker=%s state=completed candidates=%d complete=%t candidate_action=%t review_required=%t",
"dupechecking: search tracker=%s state=completed work_scope=%s received=%d evaluated=%d wrong_work=%d exhaustive=%t effective_complete=%t candidate_action=%t review_required=%t",
tracker,
search.WorkScope,
len(raw),
len(candidates),
search.WrongWorkCount,
search.Complete,
effectiveComplete,

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the public search-evidence schema and all projections/consumers.
ast-grep run --pattern 'type DupeSearchEvidence struct { $$$ }' --lang go internal
rg -n -C 4 '\bDupeSearchEvidence\b|\bWorkScope\b|\bWrongWorkCount\b' internal

Repository: autobrr/upbrr

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -u

echo "Tracked files count:"
git ls-files | wc -l

echo "Candidates by filename:"
git ls-files | rg '(^|/)dupe/(service|api).*\.go$|(^|/)api|internal/trackers/dupe|api\.go$' | head -200

echo "Search symbols:"
rg -n -C 3 'DupeSearchEvidence|EffectiveComplete|WorkScope|WrongWorkCount|Duplicate' . --glob '*.go' --glob '*.jsonschema' --glob '*.ts' --glob '*.tsx' --glob '*.js' | head -300

Repository: autobrr/upbrr

Length of output: 24428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "DupeSearchEvidence definition:"
sed -n '80,130p' pkg/api/dupes.go | cat -n

echo "DupeAssessmentResult Search evidence fields:"
sed -n '31,230p' pkg/api/workflow_contracts.go | cat -n

echo "internal/trackers/dupe/service.go relevant section:"
sed -n '520,590p' internal/trackers/dupe/service.go | cat -n

echo "Exact occurrences:"
rg -n -C 5 '\bu\.\b|\.Search\b|DupeSearchEvidence|Complete:\s*search\.EffectiveComplete|WorkScope|WrongWorkCount' pkg/api internal/webserver internal/trackers/dupe webui/src/api webui/src --glob '*.go' --glob '*.tsx' --glob '*.ts' | head -300

Repository: autobrr/upbrr

Length of output: 38763


Publish duplicate-search result constraints.

DupeSearchEvidence.Complete uses search.EffectiveComplete(), and EffectiveComplete() drops WorkScopeTitle and WorkScopeUnknown. Add WorkScope and WrongWorkCount to pkg/api/dupes.go:DupeSearchEvidence and preserve them in the API/webui projection. Consumers need these fields to tell why an exhaustive adapter result is effectively incomplete and how many rows were excluded.

🤖 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 `@internal/trackers/dupe/service.go` around lines 558 - 574, Extend
api.DupeSearchEvidence in dupes.go with WorkScope and WrongWorkCount, then
populate and preserve both fields in the duplicate-search result and API/webui
projection around the tracker completion flow. Use the existing search.WorkScope
and search.WrongWorkCount values so consumers can identify effective
incompleteness and excluded-row counts.

Comment thread internal/trackers/impl/azfamily/dupe.go
Comment thread internal/trackers/impl/standalone/bt/dupe.go
Comment thread internal/trackers/impl/standalone/czt/dupe.go Outdated
Comment thread internal/trackers/impl/standalone/hdt/dupe.go
Keep distinct Unit3D rows that reuse numeric IDs while still merging true overlaps by ID and name.

Prefer candidate folder names for folder-based BT packs and correct singular wrong-work diagnostics.
Reject off-origin AZ pagination URLs before cookies are attached and log pagination outcomes.

Distinguish earlier HDS page links from forward navigation.
Stop HDT and IS title fallback searches when no usable title exists.

Document CZT's title-first query precedence.
Mark AR and RTF policy fixtures with an authoritative work scope so completeness matches the new contract.
@Audionut

Audionut commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit embedded follow-ups: HDS forward-page detection and AZ pagination logging are fixed in 34197b3d; AR/RTF work-scope fixtures are fixed in 2aba0fd4. The generic-filename negative test was not added because any exact basename overlap is an explicit requirement of this refactor. The non-blocking docstring metric was not expanded into boilerplate for internal helpers; repository lint and documentation checks pass.

Any-single-shared-basename file identity over-blocked in three ways:
auxiliary companions (subtitles, nfo) that repeat their names across
distinct releases of one work established exact identity between
different resolutions; a proposed season pack was blocked as an exact
duplicate of a single existing episode instead of trumping it; and
known-conflicting sizes no longer vetoed identity for equal file sets.

File identity now compares primary video basenames only. Equal video
sets are exact when sizes agree or are unknown; a single-video proposal
contained in a larger candidate remains exact (existing packs and
collections cover the proposal, with no size comparison since the
candidate size describes more content); every other overlap defers to
pack precedence and slot policy. Partial candidate file lists stay
usable through the coverage rule.
… change

Scoping file identity to video content with pack-aware coverage changed
exact-identity behavior after the v3 bump was already published in this
branch. The policy ID feeds RuleFinding rule IDs and the duplicate
policy fingerprint that guards captured-assessment lineage, so the
semantic change must be visible to both.
The refactor's goal is that evidence records what scope was actually
searched, but WorkScope and WrongWorkCount only reached server logs.
Carry both on api.DupeSearchEvidence (workflow contracts regenerated) so
API and webui consumers can distinguish an exhaustive provider-bound
search from an incomplete title fallback and see how many rows were
excluded as conflicting works.
Season-pack containment replaced per-tracker opt-in precedence rules
with an unconditional general finding keyed only on season numbers and
content kind. On title-fallback searches the candidate set is not proven
to belong to the proposed work, so a different show sharing a season
number could surface existing_preferred and silently block a legitimate
upload.

Thread the search work scope into candidate findings and emit pack
containment only for provider-ID or tracker-group bound searches, where
same-work identity is authoritative. Title-fallback searches already
require review through effective completeness, which remains the correct
outcome for ambiguous candidates.
The single-file stem fallback compared any lone target file's stem to
the candidate release name, so a solitary auxiliary file (an nfo whose
stem matches the release) could establish exact identity — contradicting
the file-identity rule that auxiliary files never do. Gate the fallback
on the same video-extension check the file-set comparison uses.
…cope

Pack-containment evaluation now requires provider-ID or tracker-group
work binding; the shared per-tracker fixture ran without a work scope
and lost its season-pack direction expectations.
Slot-difference findings unconditionally take tracker-matched priority
since the evidence-driven refactor, leaving this opt-in flag with two
setters and no readers. Remove the field and its ar/lst assignments so
the policy surface matches actual behavior.
Cover the review-requested generic member-name cases: a shared generic
video basename between differing video sets, and identical generic
single-file sets with conflicting known sizes, neither of which may
establish exact identity.
@nitrobass24

Copy link
Copy Markdown
Collaborator

Pushed eight commits to the branch. Three lines in the description are now out of date because of them, and two changes aren't described at all. Flagging rather than editing your PR body.

Summary, line 4: "under general/duplicate/v3" is now v4 (586e79a). The bump is because 5c0e514 changed exact-identity semantics after v3 was already published on the branch, and the ID feeds RuleFinding rule IDs plus the DuplicatePolicyFingerprint that guards captured-assessment lineage (projection.go:406-414), so the semantics change needs to be visible to both.

Behavior, line 4: "any exact proposed/candidate file basename blocks, even when file lists or counts are partial" is no longer accurate. File identity now compares primary video basenames only (5c0e514, with the stem fallback gated the same way in acf76b9). Two concrete cases drove it:

  • A proposed season pack shared an episode basename with an existing single-episode upload, so it evaluated as exact_duplicate at priority 1000 and hard-blocked, overriding the pack precedence added in this same PR at 850.
  • A 2160p and a 1080p of the same work both shipping 2_English.srt evaluated as exact duplicates of each other, blocking a legitimate coexisting upload.

Both are covered by tests now (TestEvaluateSharedAuxiliaryFileIsNotExactIdentity, TestEvaluateSeasonPackProposalNotBlockedBySingleEpisodeFile), plus the generic-basename cases in 8d8290e. Partial candidate lists still work through the single-video coverage rule, so the #325 case still blocks: TestEvaluatePartialCandidateFileSetWithExactBasenameIsExact passes unchanged.

Suggested replacement: "exact file identity compares primary video basenames, either as equal video sets with agreeing sizes or a single proposed video contained in a larger candidate; auxiliary files and fuzzy names do not become exact identity"

Behavior, line 6: "same-season pack versus episode direction is universal" is now scope-gated (6758a35). collectPackContainmentFinding keys only on season number and content kind, with no title or work identity in contentScope, so on a title-fallback search a different show sharing a season number produced existing_preferred, which sets Blocks and silently stops a legitimate upload. It now runs only for provider-ID or tracker-group bound searches. Title-fallback candidates still reach review through effective completeness, so nothing silently passes.

Suggested replacement: "same-season pack versus episode direction applies when the search is provider-ID or tracker-group bound; title-fallback candidates fall through to review rather than directional precedence"

Not currently described:

  • api.DupeSearchEvidence now carries WorkScope and WrongWorkCount (851593c), with openapi and webui types regenerated. Previously both only reached server logs, so API and webui consumers could not tell an exhaustive provider-bound search from a title fallback, or see how many rows were dropped as wrong-work.
  • SlotDifferencesOverrideGeneral removed (1ce3650). Slot-difference findings take tracker-matched priority unconditionally after the refactor, so the flag had two setters (ar, lst) and no readers.

Comment on lines +116 to +122
warnings := []string{"BTN search is bounded to 50 results"}
return dupe.ResolvedWithSearch(entries, nil, dupe.SearchEvidence{
WorkScope: workScope,
Pages: 1,
Scope: "bounded_result_set",
Warnings: warnings,
})

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.

Didn't feel comfortable making a decision here, so flagging for you.

This literal leaves Complete at the zero value, and EffectiveComplete() (dupe/types.go:239-241) needs both halves:

return e.Complete && (e.WorkScope == WorkScopeProviderID || e.WorkScope == WorkScopeTrackerGroup)

so BTN never returns an effectively-complete search on any path, including the trackerID(meta) != "" case that sets WorkScopeTrackerGroup.

Downstream that becomes:

  • evaluator.go:79-81: !effectiveComplete sets RequiresAction = true regardless, even when the search came back with zero candidates.
  • workflow_dupes.go:349: result.HasDupes || !result.Search.Complete gives DupeDecisionPending plus RequiredActionReviewDuplicates.
  • onEvidence defaults to block for plain --unattended (releaseworkflow/composite_upload.go:295-302), so the lane blocks on every run, including for releases with nothing on the tracker.
  • dupe_check/index.tsx:65-70 gates its risk-acknowledgement banner on search.complete === false, and the backend synthesizes an insufficient_evidence match for display (workflow_dupes.go:299-318), so a clean release on one of these trackers renders as "1 potential dupe · review" with a danger-toned candidate that has no actual duplicate behind it.

isTV at line 45 rejects everything except TV, so this covers all of BTN's supported content. Pack precedence takes a hit too, since collectPackContainmentFinding only runs for provider/tracker-group scope and the imdb/tvdb/searchstr branches don't qualify.

The part I didn't want to just "fix": the "BTN search is bounded to 50 results" warning is accurate. The API really is capped, so Complete: false is honest rather than an oversight. But a capped endpoint can never prove exhaustion, so under the current model BTN is permanently unusable in unattended flows. Feels like the model is missing a way to say "we got everything the API will give us, bound to one work" separately from "we stopped early or couldn't bind the work".

Two groups land in the same place for different reasons:

  • Complete never set, so effective completeness is impossible regardless of scope: asc, bjs, bt, btn, ff, fl, hdt, is, pts (grep -c "Complete:" internal/trackers/impl/standalone/*/dupe.go returns 0 for each).
  • WorkScope hardcoded to title with no other branch, so effective completeness and pack precedence are both out of reach even when enumeration is genuinely exhaustive: ar (ar/dupe.go:150), czt (czt/dupe.go:139), tl (tl/dupe.go:85). All three handle TV.

So whatever gets decided here applies to about half the registered adapters.

Options as I see them:

  1. Adapters set Complete: true once they've enumerated everything available and the search is work-bound, keeping the warning for the cap.
  2. Add a third state (something like bounded) so consumers can tell capped enumeration apart from genuine incompleteness.
  3. Leave it and document that these trackers always need review.

t.Fatalf("unexpected ASC entries: %#v", entries)
}
},
scope: dupe.WorkScopeProviderID,

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.

Pinning scope/enumerated/effective per adapter is a good addition, and the values that are here match the adapters (I checked each one). The gap is coverage: this table pins 11 of the 25 registered dupe adapters.

Pinned: ASC, BT, FL, FF, BJS, HDS, HDT, IS, PTS, THR, TL.

Not pinned: ANT, AZ-family, BHD, BTN, CZT, DC, GPW, HDB, MTV, NBL, PTP, RTF, SPD, Unit3D.

BTN is the one I'd most want covered, since it can never reach EffectiveComplete on any path (see my other comment) and nothing in the suite would catch that today. Making the table total would also mean that whichever way the bounded-vs-incomplete question goes, the blast radius shows up as one test diff instead of being discovered per tracker later.

Related, in dupe_policy_test.go: assertRelation for AR (line 208-211) and assertRTF (line 278-281) both evaluate under WorkScope: dupe.WorkScopeTrackerGroup, but neither adapter can emit that scope at runtime. ar/dupe.go:150 sets WorkScopeTitle unconditionally, and rtfWorkScope (rtf/dupe.go:155-160) returns only WorkScopeProviderID or WorkScopeTitle. That matters for the AR season-pack subtests specifically: pack containment only runs for provider/tracker-group scope, so those assertions pass under a scope AR never produces, while the real AR path can't reach pack precedence at all. PTP's use of WorkScopeTrackerGroup at line 384-386 is correct, its adapter really does bind a torrent group.

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

🧹 Nitpick comments (4)
internal/trackers/impl/standalone/btn/dupe.go (3)

216-241: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Resolve trackerID(meta) once.

The switch calls trackerID(meta) for the case test and again for the assignment. One local variable removes the duplicate call and keeps the two values identical.

♻️ Proposed refactor
 	title := searchTitle(meta)
 	date, daily := btnDailyDate(meta.DailyEpisodeDate)
+	groupID := trackerID(meta)
 	filter := make(map[string]any)
 	workScope := dupe.WorkScopeUnknown
 	switch {
-	case trackerID(meta) != "":
+	case groupID != "":
 		workScope = dupe.WorkScopeTrackerGroup
-		filter["id"] = trackerID(meta)
+		filter["id"] = groupID
🤖 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 `@internal/trackers/impl/standalone/btn/dupe.go` around lines 216 - 241, In
btnDupeFilter, resolve trackerID(meta) once into a local variable before the
switch, then use that variable for both the case condition and filter assignment
while preserving the existing workScope and filter behavior.

69-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant maxPages fallback.

deps.MaxPages(100) already returns 100 when no policy value applies, so s.maxPages is never <= 0 for the constructed adapter. The local fallback duplicates the default in two places. If you keep the guard for zero-value structs built in tests, move the constant to a package-level identifier so both sites stay in sync.

♻️ Proposed refactor
+const btnDupeMaxPages = 100
+
-	maxPages := s.maxPages
-	if maxPages <= 0 {
-		maxPages = 100
-	}
+	maxPages := s.maxPages
+	if maxPages <= 0 {
+		maxPages = btnDupeMaxPages
+	}

Then use deps.MaxPages(btnDupeMaxPages) in newDuplicateAdapter.

🤖 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 `@internal/trackers/impl/standalone/btn/dupe.go` around lines 69 - 79, Remove
the local maxPages fallback in the duplicate-tracking flow and rely on the
adapter’s configured value. If zero-value test structs must remain supported,
define a package-level default identifier and reuse it in both this
initialization and newDuplicateAdapter’s deps.MaxPages call.

80-144: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add decision-point logging to the pagination loop.

The BTN adapter drops its logger (_ = logger), so this multi-request loop emits no operator-visible evidence. Sibling adapters log the same decisions: internal/trackers/impl/standalone/hdb/dupe.go logs pages, complete, and per-page failure codes, and internal/trackers/impl/standalone/ar/dupe.go logs pages, advertised pages, accepted results, and the final decision. Without those logs, a truncated or warning-bearing BTN search cannot be diagnosed from logs alone.

Store the logger on dupeSearcher and add stable key/value fields for the per-page failure branch and the final outcome. Do not log the token or the filter values that carry it.

As per coding guidelines: "Keep logging levels purposeful, add operator-visible progress and decision-point logs, use warnings for blocked outcomes, DEBUG for troubleshooting context, TRACE for detailed flow, and stable key/value-style fields."

🤖 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 `@internal/trackers/impl/standalone/btn/dupe.go` around lines 80 - 144, Update
dupeSearcher to retain the provided logger instead of discarding it, then
instrument the BTN pagination loop with stable key/value logging for per-page
failure details and the final search outcome, including pages and
completion/warning state as appropriate. Add operator-visible progress or
decision-point logs at purposeful levels, using warnings for blocked or
truncated outcomes and DEBUG/TRACE for troubleshooting detail. Do not include
the token or filter values in any log fields.

Source: Coding guidelines

internal/trackers/impl/standalone/btn/dupe_test.go (1)

255-320: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the partial-failure path.

The new pagination tests cover the success path and the daily one-shot path. They do not cover the branch where a later page fails after page 1 succeeded. That branch keeps the already collected entries and sets the warning "BTN search stopped after a partial request failure". The mock transport already returns an error for any request beyond the supplied response sequence, so a single-response sequence with a reported total of 3 exercises the branch directly.

Add a test that supplies one page with fewer rows than the reported count, then asserts that the entries survive, Complete is false, and the partial-failure warning is present.

🤖 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 `@internal/trackers/impl/standalone/btn/dupe_test.go` around lines 255 - 320,
Add a test alongside TestBTNHandlerPaginatesUntilReportedTotal using a single
mock response reporting 3 results but containing fewer rows, so the next
pagination request fails. Assert the collected entries are preserved,
SearchEvidence().Complete is false, and the warnings include exactly “BTN search
stopped after a partial request failure”; also verify the expected initial
request parameters if needed.
🤖 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.

Nitpick comments:
In `@internal/trackers/impl/standalone/btn/dupe_test.go`:
- Around line 255-320: Add a test alongside
TestBTNHandlerPaginatesUntilReportedTotal using a single mock response reporting
3 results but containing fewer rows, so the next pagination request fails.
Assert the collected entries are preserved, SearchEvidence().Complete is false,
and the warnings include exactly “BTN search stopped after a partial request
failure”; also verify the expected initial request parameters if needed.

In `@internal/trackers/impl/standalone/btn/dupe.go`:
- Around line 216-241: In btnDupeFilter, resolve trackerID(meta) once into a
local variable before the switch, then use that variable for both the case
condition and filter assignment while preserving the existing workScope and
filter behavior.
- Around line 69-79: Remove the local maxPages fallback in the
duplicate-tracking flow and rely on the adapter’s configured value. If
zero-value test structs must remain supported, define a package-level default
identifier and reuse it in both this initialization and newDuplicateAdapter’s
deps.MaxPages call.
- Around line 80-144: Update dupeSearcher to retain the provided logger instead
of discarding it, then instrument the BTN pagination loop with stable key/value
logging for per-page failure details and the final search outcome, including
pages and completion/warning state as appropriate. Add operator-visible progress
or decision-point logs at purposeful levels, using warnings for blocked or
truncated outcomes and DEBUG/TRACE for troubleshooting detail. Do not include
the token or filter values in any log fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd40ca58-363b-4e4c-98d0-5e275b2f320e

📥 Commits

Reviewing files that changed from the base of the PR and between 8d8290e and 6aa83ab.

📒 Files selected for processing (2)
  • internal/trackers/impl/standalone/btn/dupe.go
  • internal/trackers/impl/standalone/btn/dupe_test.go

@Audionut

Audionut commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all four CodeRabbit nitpicks in 43943690: BTN now retains and uses its logger for page failures and final completion decisions, removes the redundant runtime max-page fallback, resolves the tracker group ID once, and covers preservation of first-page entries when a later page request fails.

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.

Dupe check misses exact duplicates when the resolved title year differs from the tracker's (e.g. 2002 vs 2003)

2 participants