feat: implement scanner similarity scoring

This commit is contained in:
2026-07-19 18:14:49 +03:00
parent edf9c1d1f8
commit 92aafaab05
3 changed files with 191 additions and 4 deletions

View File

@@ -62,9 +62,9 @@
- [x] run tests — must pass before task 3
### Task 3: Implement similarity scoring in `internal/scanner`
- [ ] create `internal/scanner/scanner.go` with `Similarity(a, b string) float64` using `normalize.NormalizeString` + `fuzzy.Ratio` (normalized to 0.01.0); define `IsMatch(a, b string, threshold float64) bool`
- [ ] write tests `internal/scanner/scanner_test.go` (table-driven): exact match → 1.0, `(Remastered)` / year variants still match above 0.85, clearly different titles → below threshold, empty-string handling
- [ ] run tests — must pass before task 4
- [x] create `internal/scanner/scanner.go` with `Similarity(a, b string) float64` using `normalize.NormalizeString` + `fuzzy.LevenshteinDistance` (normalized to 0.01.0); define `IsMatch(a, b string, threshold float64) bool`
- [x] write tests `internal/scanner/scanner_test.go` (table-driven): exact match → 1.0, `(Remastered)` / year variants still match above 0.85, clearly different titles → below threshold, empty-string handling
- [x] run tests — must pass before task 4
### Task 4: Implement the diff engine (missing-release detection)
- [ ] add `type MissingRelease struct { RGID, ArtistID, Title, Type, ReleaseDate string }` in `internal/scanner`
@@ -99,7 +99,7 @@
- `bracketRe` = `\[[^\]]*\]` , `parenRe` = `\([^)]*\)`, `yearRe` = `\b(1[0-9]{3}|2[0-9]{3})\b`, `spaceRe` = `\s+`.
- `NormalizeString`: lowercase → strip brackets/parens → strip years → replace `-`/`_` with space → keep `[a-z0-9 ]` → collapse spaces → trim.
- `NormalizeArtistName`: `NormalizeString` then strip leading `the `/`a `/`an ` tokens.
- **Similarity**: `fuzzy.Ratio(normalize(a), normalize(b))` returns an int 0100; `Similarity` returns `float64(ratio)/100.0`. `IsMatch` returns `Similarity(a,b,threshold) >= threshold`.
- **Similarity**: `fuzzy.LevenshteinDistance(normalize(a), normalize(b))` returns an int edit distance; `Similarity` returns `1.0 - float64(dist)/float64(maxLen)` (clamped to [0.0, 1.0]). Empty/whitespace-only inputs normalize to empty and score 0.0 (no false match). `IsMatch` returns `Similarity(a,b) >= threshold`. Note: `fuzzy.Ratio` does not exist in `lithammer/fuzzysearch` v1.1.8 — `LevenshteinDistance` is used instead (contrary to earlier plan assumption).
- **Diff algorithm**: per external release (filtered by `!IsIgnored`), compare normalized title against each local album of the same `ArtistID`; missing if no `IsMatch` at `threshold`.
- **Config contract**: `threshold` passed from `config.Scanner.FuzzyThreshold` (default 0.85). Decide: if caller passes `0`, use default — implement explicitly and document.
- **No new DB tables** in this plan (compute-only per user decision).