Skip to content

feat(slskd): single-source matching + fingerprint import verification - #65

Merged
chodeus merged 5 commits into
mainfrom
feat/slskd-single-source-matching
Jul 24, 2026
Merged

feat(slskd): single-source matching + fingerprint import verification#65
chodeus merged 5 commits into
mainfrom
feat/slskd-single-source-matching

Conversation

@chodeus

@chodeus chodeus commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Problem

For a monitored single/EP, the slskd indexer would match any album folder that merely contained the wanted track (track-title evidence forces the match), then download the whole album and keep the one file whose title matched. That produced:

  • full albums downloaded to extract one track (most of the files then fail to import),
  • multi-track singles stitched together from different users' shares / different albums, and
  • duplicate-track folders when retry-failover from a different peer accumulated in the same name-keyed local folder.

And imports were trusted on filename/tag title only — a wrong file whose name matched got tagged as the wanted track, with no content check.

Changes

Matching (indexer)

  • Pluck only the matched track(s) from a larger album share for a single/EP target instead of downloading the whole album. Gated so it only fires when the source is genuinely larger and every target track is matchable — a track whose title normalizes below the 4-char floor (e.g. The Endend) is never silently dropped; those sources download whole.
  • Reject a source that holds none of the wanted tracks for a single/EP.
  • Source coherence for multi-track singles/EPs: prefer a source holding every wanted track, downrank a partial one, and optionally reject it (new Require Coherent Single Source setting). Coverage is measured against titles that survive normalization, not the raw track count.
  • The recently-failed skip now hashes the plucked download set, so the id matches the real downloadId and a plucked single isn't re-offered every search after a failure.

Import verification (pre-import tagger)

  • Fingerprint-gate the title-driven fallback using Lidarr's bundled AcoustID/fpcalc: tag only when the audio resolves to the wanted MusicBrainz recording (old recording ids accepted, mirroring DistanceCalculator). Fail-safe — a file AcoustID can't identify, an outage, or a missing fpcalc falls back to the existing match rather than blocking the import.
  • New Verify Imports With Fingerprint download-client setting extends the check to every file. Both checks are scoped to slskd; Deezer/Tidal post-processing is unaffected.

Download client

  • On a retry superseding a failed attempt, delete the stale attempt's partial files so failover no longer interleaves two sources into one folder. Deferred until after a successful enqueue and time-bounded, so a failed enqueue never destroys content with no retry in flight and a slow slskd can't stall the grab.

Settings (defaults preserve current behaviour where it matters)

  • Pluck / coherence-prefer / title-fallback fingerprinting are on by default.
  • Require Coherent Single Source (indexer) and Verify Imports With Fingerprint (download client) are opt-in.

Testing

dotnet test293 pass, 0 fail (7 new: pluck, coverage-reject, coherent-vs-partial ranking, require-coherent, and the sub-4-char-title edge case).

Summary by CodeRabbit

  • New Features
    • Added optional fingerprint verification for completed downloads, including a title-based fallback gate to prevent tagging mismatched recordings.
    • Introduced advanced settings VerifyImportsWithFingerprint and RequireCoherentSingleSource to control verification and stricter single/EP source selection.
  • Bug Fixes
    • Improved recently-failed source matching to align with the actual selected download set.
    • Improved retry handling by safely cleaning up superseded attempts after successful retries.
    • Added a timeout for transfer deletion requests for better reliability.
  • Tests
    • Added coverage for single/EP plucking behavior, coherence preference/rejection, and fingerprint-gated selection.

Stops slskd single/EP grabs from hauling whole albums or stitching a
single together from files scattered across different sources, and adds
acoustic (AcoustID) verification so a wrong file whose filename merely
matches is rejected.

Matching (indexer):
- Pluck only the matched track(s) from a larger album share for a
  single/EP target instead of downloading the whole album — but only when
  the source is genuinely larger and every target track is matchable, so
  a track whose title normalizes below the 4-char floor (e.g. "The End"
  -> "end") is never silently dropped.
- Reject a source that holds none of the wanted tracks for a single/EP.
- Source coherence for multi-track singles/EPs: prefer a source holding
  every wanted track, downrank a partial one, and optionally reject it
  (new "Require Coherent Single Source" setting). Coverage is measured
  against the titles that survive normalization, not the raw track count.
- The recently-failed skip now hashes the plucked download set so the id
  matches the real downloadId (a plucked single is no longer re-offered
  every search after a failure).

Import verification (pre-import tagger):
- Fingerprint-gate the title-driven fallback using Lidarr's bundled
  AcoustID/fpcalc: tag only when the audio resolves to the wanted
  MusicBrainz recording (old recording ids accepted, mirroring Lidarr's
  own scorer). Fail-safe — a file AcoustID can't identify, an outage, or
  a missing fpcalc falls back to the existing match rather than blocking
  the import.
- New "Verify Imports With Fingerprint" download-client setting extends
  the check to every file. Both checks are scoped to slskd; the Deezer
  and Tidal post-processing paths are unaffected.

Download client:
- On a retry that supersedes a failed attempt, delete the stale attempt's
  partial files so failover no longer interleaves two sources into one
  name-keyed folder (mixed editions / duplicate track numbers). Deferred
  until after a successful enqueue and time-bounded, so a failed enqueue
  never destroys content with no retry in flight and a slow slskd can't
  stall the grab.

293 tests pass (7 new covering pluck, coverage-reject, coherence ranking,
require-coherent, and the sub-4-char-title edge case).
@coderabbitai

coderabbitai Bot commented Jul 24, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c2d73ad-7e97-4655-8f18-f46ef34cc505

📥 Commits

Reviewing files that changed from the base of the PR and between 2150f9d and 88e6fe5.

📒 Files selected for processing (1)
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs

📝 Walkthrough

Walkthrough

The PR adds fingerprint-gated completed-download tagging, deferred cleanup for superseded Soulseek attempts, and bounded transfer deletion. Small Soulseek targets now use matched-file narrowing and coherence scoring, with updated failure identity hashing and expanded tests.

Changes

Fingerprint-Gated Post-Processing

Layer / File(s) Summary
Fingerprint verification and tagging flow
src/Sleezer/Core/PostProcessing/PreImportTagger.cs, src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs, src/Sleezer/Download/Clients/Soulseek/SlskdProviderSettings.cs
Tagging accepts fingerprint controls, verifies recordings, skips definite mismatches, and includes title-fallback skips in its results.
Superseded download cleanup
src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs, src/Sleezer/Download/Clients/Soulseek/SlskdApiClient.cs
Retry handling defers stale-attempt cleanup, protects shared basenames, bounds cleanup to 20 seconds, and applies a 15-second delete request timeout.

Soulseek Source Selection

Layer / File(s) Summary
Small-target matching and coherent sources
src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs, src/Sleezer/Indexers/Soulseek/SlskdSettings.cs
Small singles and EPs match wanted titles to files, narrow download sets, apply coherence rules, and analyze quality on selected files.
Concrete download identity and selection coverage
src/Sleezer/Indexers/Soulseek/SlskdIndexerParser.cs, tests/Sleezer.Tests/SlskdSearchMatchingTests.cs
Recently-failed filtering hashes serialized download files, with tests covering plucking, coherence, ceilings, and unmatchable titles.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SlskdDownloadManager
  participant PreImportTagger
  participant IFingerprintingService
  participant WantedTrack
  SlskdDownloadManager->>PreImportTagger: Tag completed download with fingerprint flags
  PreImportTagger->>IFingerprintingService: VerifyRecording(file, track)
  IFingerprintingService-->>PreImportTagger: Verified, Mismatch, or Unverifiable
  PreImportTagger->>WantedTrack: Tag verified candidate
  PreImportTagger-->>SlskdDownloadManager: Return tagging counts including skipped files
Loading

Possibly related PRs

  • chodeus/sleezer#60: Implements the title-driven fallback path extended here with fingerprint gating.
  • chodeus/sleezer#62: Modifies the overlapping PreImportTagger title-fallback and tagging flow.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 clearly reflects the main changes: single-source matching and fingerprint-based import verification.
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 feat/slskd-single-source-matching

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

🧹 Nitpick comments (2)
src/Sleezer/Core/PostProcessing/PreImportTagger.cs (1)

61-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Condense the new narrative comment blocks.

  • src/Sleezer/Core/PostProcessing/PreImportTagger.cs#L61-L65: reduce to the fallback-vs-mismatch invariant.
  • src/Sleezer/Core/PostProcessing/PreImportTagger.cs#L89-L92: reduce to the old-recording-ID compatibility reason.
  • src/Sleezer/Core/PostProcessing/PreImportTagger.cs#L399-L404: reduce to the title-match fingerprint guard.
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs#L192-L196: reduce to the deferred-deletion invariant.
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs#L249-L255: reduce to the shared-folder cleanup risk.
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs#L1165-L1167: reduce to the cleanup timeout rationale.

As per path instructions, comments must be concise 1–2 line navigational/gotcha notes; added narrative essays should be flagged.

🤖 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 `@src/Sleezer/Core/PostProcessing/PreImportTagger.cs` around lines 61 - 65,
Condense the comments at PreImportTagger.cs lines 61-65, 89-92, and 399-404 to
concise 1–2 line notes covering respectively the fallback-vs-mismatch invariant,
old-recording-ID compatibility, and title-match fingerprint guard. Condense the
comments at SlskdDownloadManager.cs lines 192-196, 249-255, and 1165-1167 to
concise notes covering deferred deletion, shared-folder cleanup risk, and
cleanup timeout rationale; remove narrative explanations while preserving these
navigational/gotcha details.

Source: Path instructions

src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs (1)

160-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Condense the added narrative comments.

  • src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs#L160-L176: retain only the non-obvious plucking/coherence invariant.
  • src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs#L662-L668: keep concise constant intent.
  • src/Sleezer/Indexers/Soulseek/SlskdIndexerParser.cs#L151-L163: remove historical incident detail; retain the file-set hash invariant.

As per path instructions, comments must be navigational/gotcha-only and limited to 1–2 lines.

🤖 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 `@src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs` around lines 160 - 176,
Condense the comments at src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs lines
160-176 to retain only the non-obvious plucking/coherence invariant in 1–2
lines; at lines 662-668, keep only concise constant intent; and at
src/Sleezer/Indexers/Soulseek/SlskdIndexerParser.cs lines 151-163, remove
historical incident details while retaining the file-set hash invariant. No code
behavior changes are needed.

Source: Path instructions

🤖 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 `@src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs`:
- Around line 249-257: The superseded-attempt cleanup can delete files belonging
to the newly enqueued retry, and its delete tasks continue after the timeout.
Update CleanupSupersededAttemptsAsync and its callers at
src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs:249-257 and
:1163-1177 to propagate cancellation into each delete operation and ensure
timed-out cleanup cannot continue deleting files in the background; preserve
deferred cleanup after successful retry enqueue.

In `@src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs`:
- Around line 699-710: Update the title/file matching loop in the parser so each
wanted title is assigned to a distinct filename, preventing one file from
satisfying multiple overlapping titles; derive both coveredTitles and matched
from these one-to-one assignments. Preserve the requirement that every target
title is represented, and add a regression test covering overlapping titles such
as “Falling” and “Falling Slowly” to verify incomplete album shares are
rejected.
- Around line 181-185: Reject sources with no covered tracks unconditionally in
the matching branch around coveredTrackCount, removing the audioFileCount >
expectedTrackCount requirement while preserving the existing logging and
matchedSearchCriteria update. In tests/Sleezer.Tests/SlskdSearchMatchingTests.cs
lines 229-238, replace the fixture with an unrelated source whose album name
matches and whose file count equals the expected count, so the test verifies
this boundary; no other sites require changes.

---

Nitpick comments:
In `@src/Sleezer/Core/PostProcessing/PreImportTagger.cs`:
- Around line 61-65: Condense the comments at PreImportTagger.cs lines 61-65,
89-92, and 399-404 to concise 1–2 line notes covering respectively the
fallback-vs-mismatch invariant, old-recording-ID compatibility, and title-match
fingerprint guard. Condense the comments at SlskdDownloadManager.cs lines
192-196, 249-255, and 1165-1167 to concise notes covering deferred deletion,
shared-folder cleanup risk, and cleanup timeout rationale; remove narrative
explanations while preserving these navigational/gotcha details.

In `@src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs`:
- Around line 160-176: Condense the comments at
src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs lines 160-176 to retain only
the non-obvious plucking/coherence invariant in 1–2 lines; at lines 662-668,
keep only concise constant intent; and at
src/Sleezer/Indexers/Soulseek/SlskdIndexerParser.cs lines 151-163, remove
historical incident details while retaining the file-set hash invariant. No code
behavior changes are needed.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c508839-488b-4147-b265-e842ea0083c6

📥 Commits

Reviewing files that changed from the base of the PR and between d773257 and a137aba.

📒 Files selected for processing (7)
  • src/Sleezer/Core/PostProcessing/PreImportTagger.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdProviderSettings.cs
  • src/Sleezer/Indexers/Soulseek/SlskdIndexerParser.cs
  • src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs
  • src/Sleezer/Indexers/Soulseek/SlskdSettings.cs
  • tests/Sleezer.Tests/SlskdSearchMatchingTests.cs

Comment thread src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs Outdated
Comment thread src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs Outdated
Comment thread src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs Outdated
…afe cleanup

- MatchWantedTrackFiles: one-to-one title→file assignment so a lone
  "Falling Slowly" file can't satisfy both "Falling" and "Falling Slowly"
  and make an incomplete source look complete.
- Take the filename basename after normalizing backslashes — slskd paths
  are backslash-delimited, which Path.* doesn't split on Linux, so the
  folder name (often == the single title) was leaking into the track-title
  haystack and inflating coverage.
- Reject a zero-coverage source regardless of file count, so a same-sized
  source that only matched by name is no longer let through.
- Failover cleanup: protect the active retry's own basenames from deletion
  (a retry sharing the folder can't lose its files), keeping the deferred/
  bounded behaviour.
- Condense the added comments to short navigational notes.

Tests: 294 pass — adds overlapping-title and name-match-no-track
regressions and de-vacuums the zero-coverage test.
@chodeus

chodeus commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review in c43649a:

  • Overlapping titles (MatchWantedTrackFiles): now a one-to-one title→file assignment, so a lone Falling Slowly file can't satisfy both Falling and Falling Slowly. Added an overlap regression test.
  • Zero-coverage reject: dropped the audioFileCount > expectedTrackCount condition — a same-sized source that only matched by name is now rejected. Replaced the vacuous test with one that actually reaches the branch (name-matching folder, no track file).
  • Superseded cleanup: the retry's own basenames are now protected from deletion, so a retry sharing the same name-keyed folder can't lose its files; the deferred-after-enqueue + time-bound behaviour is kept.
  • Comments condensed to short navigational notes.

While writing the zero-coverage test it surfaced a real bug: slskd filenames are backslash-delimited, which Path.* doesn't split on Linux, so the folder name (usually == the single's title) was leaking into the track-title haystack and inflating coverage. Fixed by normalizing separators before taking the basename.

294 tests pass.

@chodeus

chodeus commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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: 3

🤖 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 `@src/Sleezer/Core/PostProcessing/PreImportTagger.cs`:
- Around line 61-100: Update VerifyRecording to accept the caller’s cancellation
token and thread it through the fingerprint verification flow, including
_fingerprintingService.Lookup or its cancellation-aware equivalent. Ensure
cancellation propagates promptly instead of being converted into
FingerprintVerdict.Unverifiable, while preserving existing handling for ordinary
lookup errors and verdicts.

In `@src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs`:
- Around line 1164-1178: Update CleanupSupersededAttemptsAsync and the
RemoveItemFilesAsync/DeleteTransferAsync call chain to accept and honor the
cleanup CancellationToken, so timed-out deletes are actually cancelled before
retry proceeds. Handle OperationCanceledException caused by the 20-second
timeout separately from other exceptions, and log non-timeout API or network
failures with their original error context rather than labeling them as
timeouts.

In `@src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs`:
- Around line 668-709: Update MatchWantedTrackFiles when constructing named so
it filters files through the existing audio-file predicate before title
matching. Ensure non-audio files cannot be claimed or added to matched, while
preserving the current one-to-one title matching behavior and audio-file
handling when OnlyAudioFiles is disabled.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 72d7d9fc-a007-4a06-bed5-04b3fc579f88

📥 Commits

Reviewing files that changed from the base of the PR and between d773257 and c43649a.

📒 Files selected for processing (7)
  • src/Sleezer/Core/PostProcessing/PreImportTagger.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdProviderSettings.cs
  • src/Sleezer/Indexers/Soulseek/SlskdIndexerParser.cs
  • src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs
  • src/Sleezer/Indexers/Soulseek/SlskdSettings.cs
  • tests/Sleezer.Tests/SlskdSearchMatchingTests.cs

Comment thread src/Sleezer/Core/PostProcessing/PreImportTagger.cs
Comment thread src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs
Comment thread src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs
- MatchWantedTrackFiles: match against audio files only, so a same-named
  .cue/.nfo can't be claimed (and then plucked) ahead of the real track.
- VerifyRecording: take a CancellationToken; run the non-cancellable
  IFingerprintingService.Lookup off-thread and stop waiting when the token
  trips, propagating cancellation instead of swallowing it — a slow
  fpcalc/AcoustID lookup no longer blocks the import loop past the
  post-processing timeout.
- Superseded-attempt cleanup: distinguish the time bound (retry files stay
  protected) from real API/network errors instead of logging both as a
  timeout, and bound each slskd delete request to 15s so detached deletes
  drain quickly. IHttpClient/Lookup take no token, so this is the feasible
  cancellation path.

295 tests pass (adds a non-audio-file pluck regression).

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs (1)

694-699: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Match title phrases, not arbitrary substrings.

Name.Contains(title) makes a wanted title like Home match Hometown. That false positive can mark an incomplete source as covered and pluck the wrong audio file. Match the normalized title as a whole token sequence, while still allowing suffixes such as Live.

Proposed fix
-                    if (claimed.Contains(i) || !named[i].Name.Contains(title))
+                    if (claimed.Contains(i) ||
+                        !($" {named[i].Name} ").Contains($" {title} ", StringComparison.Ordinal))
                         continue;
🤖 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 `@src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs` around lines 694 - 699,
Update the title comparison in the parsing loop over `titles` and `named` so it
matches the normalized title as a whole token sequence rather than using
arbitrary substring matching. Preserve valid suffix matches such as “Live”,
while preventing titles like “Home” from matching “Hometown” and incorrectly
claiming the source.
🤖 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 `@src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs`:
- Around line 681-683: Update IsAudioFile, used by the named selection in the
parser, so an empty Extension explicitly falls back to the filename-derived
extension using the same string.IsNullOrEmpty behavior as
GetMostCommonExtension; preserve the existing Extension value when non-empty.
Add a regression fixture covering a valid audio filename such as track.flac with
an empty Extension.

---

Outside diff comments:
In `@src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs`:
- Around line 694-699: Update the title comparison in the parsing loop over
`titles` and `named` so it matches the normalized title as a whole token
sequence rather than using arbitrary substring matching. Preserve valid suffix
matches such as “Live”, while preventing titles like “Home” from matching
“Hometown” and incorrectly claiming the source.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4adb9b77-7d99-418e-a010-c515d524fe1e

📥 Commits

Reviewing files that changed from the base of the PR and between c43649a and 0700068.

📒 Files selected for processing (5)
  • src/Sleezer/Core/PostProcessing/PreImportTagger.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdApiClient.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs
  • src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs
  • tests/Sleezer.Tests/SlskdSearchMatchingTests.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/Sleezer.Tests/SlskdSearchMatchingTests.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs
  • src/Sleezer/Core/PostProcessing/PreImportTagger.cs

Comment thread src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs
`f.Extension ?? Path.GetExtension(...)` only falls back on null, so an EMPTY
Extension left a valid track.flac unrecognized as audio — which, now that
MatchWantedTrackFiles filters on IsAudioFile, could drop every match and
reject an otherwise-valid small-target source. Use the same IsNullOrEmpty
filename fallback as GetMostCommonExtension.

296 tests pass (adds an empty-Extension fixture).
@chodeus

chodeus commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@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

Caution

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

⚠️ Outside diff range comments (2)
src/Sleezer/Core/PostProcessing/PreImportTagger.cs (1)

116-138: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cancellation is swallowed before it reaches the caller's "timed out" handling.

VerifyRecording (and the ct.ThrowIfCancellationRequested() calls in TagInternal) rethrow OperationCanceledException on cancellation — but the only entry point, TagCompletedDownloadAsync, catches it with a blanket catch (Exception ex) and converts it into a normal completed Task<TaggingResult> (TaggingResult(0, 0, 1)), logged via _logger.Error. This means SlskdDownloadManager.EnqueuePostProcess's dedicated catch (OperationCanceledException) { _logger.Warn("[post-process] Timed out for {ItemId}", ...); } never fires for a post-processing timeout that occurs during tagging — every cancellation is instead reported as a tagging failure with a full error stack trace. This negates the intent of the previously-fixed cancellation propagation (VerifyRecording rethrowing OperationCanceledException so the import loop stops promptly and is distinguishable from a real failure).

🐛 Proposed fix
     public Task<TaggingResult> TagCompletedDownloadAsync(
         ...
         try
         {
             TaggingResult result = TagInternal(album, artist, albumRelease, sourceId, completedFolderPath, confidenceThreshold, stripFeaturedArtists, verifyAllWithFingerprint, fingerprintTitleFallback, ct);
             return Task.FromResult(result);
         }
+        catch (OperationCanceledException)
+        {
+            throw;
+        }
         catch (Exception ex)
         {
             _logger.Error(ex, "Pre-import tagging failed for {SourceId}", sourceId);
             return Task.FromResult(new TaggingResult(0, 0, 1));
         }
     }
🤖 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 `@src/Sleezer/Core/PostProcessing/PreImportTagger.cs` around lines 116 - 138,
Update TagCompletedDownloadAsync so OperationCanceledException is rethrown to
the caller instead of being converted into TaggingResult(0, 0, 1) by the blanket
exception handler. Preserve the existing logging and failure result for
non-cancellation exceptions, allowing EnqueuePostProcess’s cancellation-specific
handling to receive tagging timeouts.
src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs (1)

1191-1229: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep local cleanup running if the remote delete fails

src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs:1195-1198 — the two DeleteTransferAsync awaits run before the local delete block, so one thrown delete skips cleanup for that file. In the superseded-attempts path, that leaves stale files behind when the transfer is already gone. Wrap each remote delete and continue to local cleanup.

🤖 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 `@src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs` around lines
1191 - 1229, The per-file cleanup flow in the Task.WhenAll block must continue
to local deletion when either _apiClient.DeleteTransferAsync call fails. Wrap
each remote delete await independently so failures are handled without aborting
the file’s cleanup, while preserving the existing delay and remove=true
behavior.
🧹 Nitpick comments (1)
src/Sleezer/Core/PostProcessing/PreImportTagger.cs (1)

295-302: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fingerprint mismatch skips are conflated with weak-match skips in the metric/log.

Both the album-level (295-301) and title-fallback (405-411) paths increment skipped, which is reported as TaggingResult.SkippedWeakMatch and logged under skipped_weak_match= (line 327). The doc-comment on TaggingResult (lines 109-114) defines SkippedWeakMatch specifically as "identification confidence below the threshold" — a fingerprint-confirmed different-recording is a materially different (and more actionable) outcome than a merely-weak match, but operators can no longer distinguish them from the summary log or the result tuple.

Consider a separate counter (e.g. FingerprintMismatch) or at least a distinct log line, so troubleshooting fingerprint-gated imports isn't muddied with ordinary weak-match skips.

Also applies to: 405-411

🤖 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 `@src/Sleezer/Core/PostProcessing/PreImportTagger.cs` around lines 295 - 302,
Separate fingerprint-confirmed mismatches from ordinary weak-match skips in the
pre-import tagging results. Update the album-level and title-fallback
fingerprint mismatch paths in the relevant tagging method to use a dedicated
counter/result field and distinct summary logging, while leaving
SkippedWeakMatch for confidence-threshold failures only. Ensure TaggingResult
and its documentation expose the new outcome consistently.
🤖 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 `@src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs`:
- Around line 1202-1204: Update the filename normalization in the cleanup logic
surrounding fileName to handle a null file.Filename consistently with the
equivalent computation near line 254. Use an empty-string fallback before
replacing path separators, preserving basename extraction and allowing local
cleanup to proceed without a null dereference.
- Around line 247-259: Change the superseded-attempt cleanup in the enqueue flow
to run fire-and-forget instead of awaiting CleanupSupersededAttemptsAsync.
Preserve the existing supersededAttempts guard and retryBasenames construction,
and follow the established CleanStaleDirectoriesAsync invocation pattern while
retaining the cleanup method’s own timeout and failure handling.

---

Outside diff comments:
In `@src/Sleezer/Core/PostProcessing/PreImportTagger.cs`:
- Around line 116-138: Update TagCompletedDownloadAsync so
OperationCanceledException is rethrown to the caller instead of being converted
into TaggingResult(0, 0, 1) by the blanket exception handler. Preserve the
existing logging and failure result for non-cancellation exceptions, allowing
EnqueuePostProcess’s cancellation-specific handling to receive tagging timeouts.

In `@src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs`:
- Around line 1191-1229: The per-file cleanup flow in the Task.WhenAll block
must continue to local deletion when either _apiClient.DeleteTransferAsync call
fails. Wrap each remote delete await independently so failures are handled
without aborting the file’s cleanup, while preserving the existing delay and
remove=true behavior.

---

Nitpick comments:
In `@src/Sleezer/Core/PostProcessing/PreImportTagger.cs`:
- Around line 295-302: Separate fingerprint-confirmed mismatches from ordinary
weak-match skips in the pre-import tagging results. Update the album-level and
title-fallback fingerprint mismatch paths in the relevant tagging method to use
a dedicated counter/result field and distinct summary logging, while leaving
SkippedWeakMatch for confidence-threshold failures only. Ensure TaggingResult
and its documentation expose the new outcome consistently.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e784a72a-7d60-432e-ab5a-5228adc8edf9

📥 Commits

Reviewing files that changed from the base of the PR and between d773257 and 2150f9d.

📒 Files selected for processing (8)
  • src/Sleezer/Core/PostProcessing/PreImportTagger.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdApiClient.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs
  • src/Sleezer/Download/Clients/Soulseek/SlskdProviderSettings.cs
  • src/Sleezer/Indexers/Soulseek/SlskdIndexerParser.cs
  • src/Sleezer/Indexers/Soulseek/SlskdItemsParser.cs
  • src/Sleezer/Indexers/Soulseek/SlskdSettings.cs
  • tests/Sleezer.Tests/SlskdSearchMatchingTests.cs

Comment thread src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs
Comment thread src/Sleezer/Download/Clients/Soulseek/SlskdDownloadManager.cs Outdated
- CleanupSupersededAttemptsAsync is now fire-and-forget — it self-bounds
  (20s), protects the retry's own basenames, and handles its own errors, so
  awaiting it only added up to 20s to a successful enqueue for no benefit.
- Null-guard file.Filename in RemoveItemFilesAsync's basename derivation,
  matching the retry-basename computation and the rest of the file.
@chodeus
chodeus merged commit 682112e into main Jul 24, 2026
3 checks passed
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