3d375adab10f5fad410373677e05b1bec8a32d74
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3d375adab1 |
feat(downloads): bound auto-pick by bitrate, and take a good copy
Three faults, one subsystem, and the middle one is why a request that looked obviously satisfiable came back refused. **The guardrails were in megabytes, which cannot mean anything.** 300 MB is a generous FLAC single and a suspiciously small boxset, and whoever fills the field in has no idea which release the pipeline will apply it to. `MinKbps`/`MaxKbps`/`PreferredKbps` are the same statement divided by how long the music is, so one number holds across a nine-minute EP and a three-hour opera. The runtime comes from `Download.Expected`, which every anchored request already carries, so this costs no lookup; the rate is audio bytes over that, falling back to the mean stated per-file bitrate when the runtime is unknown. Artwork is excluded from the numerator, or a folder with 30 MB of scans reads as a better rip. An unknown runtime *passes* the window rather than failing it: the window is a statement about quality, and refusing everything the moment MusicBrainz is missing a track length would be a silent embargo. `MaxFileSizeMB` survives as a separate ceiling, still in megabytes on purpose -- it is a question about disk space, and it has to apply to a candidate whose bitrate cannot be worked out at all. **Auto-pick required daylight over the runner-up**, 0.08 on the combined score, and so fired hardest in the case it was never written for: a popular album turns up five *correct* copies, all matching the tracklist at 95%+ and differing only in format and seeders, their scores land within a point of each other, and it refused forever on the grounds that the choice was the user's. It was not. There was no question about what to fetch, only about which copy -- and abundance is the condition under which that matters least. A candidate no longer has to beat the field, only clear the bars on its own terms; where several do, ranking puts the one closest to the preferred bitrate first. That tie-break needed the preference to carry weight or it would have been decorative in a new unit: `BitrateFit` was 0.05 against format's 0.42, so asking for 320 and being handed a FLAC every time was the designed behaviour. When a preference is set the weights shift to fit 0.40 / format 0.20 / bitrate 0.10, taking it off the two heuristics that exist as stand-ins for the preference the user has now given. Health and priority are untouched. And the fit spans 0.5 to 1.0 rather than 0 to 1, so a preference can promote the copy that matches it and can never push the others under `minQuality` -- turning "I like 320" into "never take anything else" silently is what `MinKbps`/`MaxKbps` are for, out loud. **And a refusal quoted numbers that passed.** The request list built its message from `ranked[0]` -- the best candidate *before* the guardrails and before the lead check -- so a request killed by the size window, or by having too many good copies, reported "best of 12 found is not a confident enough match (match 96%, quality 88%)". `AutoPickVeto` names the gate that actually refused, and `AutoPickable` is that returning empty. Existing configs: the old `MinFileSizeMB`/`PreferredFileSizeMB` are not migrated. A number meaning "300 MB" cannot be reinterpreted as a rate without knowing the album it was aimed at, so carrying it over would be inventing an intent nobody expressed. Those two fall back to no window, which is the permissive default and what a fresh install gets; `MaxFileSizeMB` carries over unchanged, because a ceiling on bytes still means exactly what it did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MeQt5hgXg5YGoNZQ9ozG7L |
||
|
|
409bfd5e89 |
test(download): wait for the work, not for the state that precedes it
TestManagerEndToEndAutoPick waits for StateComplete and then asserts
that staging was released and the library was rescanned. Those happen
*after* the state is recorded: manager.go sets StateComplete, then
satisfies the request, then releases staging, then scans. So waiting on
the state is not waiting on either assertion, and on a loaded machine
the worker is descheduled in between and the test reads the world one
step too early:
manager_test.go:209: staging not released: 1 dirs remain
manager_test.go:218: library scans = 0, want 1
It passed alone every time and failed three times under a full-suite
run, which is the signature of a test race rather than a broken
manager — nothing here is wrong except what the test chose to wait on.
It blocks pushes, since the pre-push hook is exactly the loaded run.
It polls for the side effects now, through the waitFor this package
already has and already uses for the same reason one file over
(service_test.go waits for a request to become satisfied after the same
StateComplete).
Not reproduced on demand: eight spinners and -count=5 did not provoke
it with or without the fix, so this rests on the ordering being plain
in the code rather than on a red-to-green demonstration.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh
|
||
|
|
65333857e2 |
refactor(download): rename Want/Request to Request/Download, unify downloads flow, add auto-download guardrails
The durable "I asked for this" record was called Want, and the one-shot search-and-grab attempt was called Request — names that didn't match what either actually did. Want is now Request, and the old Request/Item is now Download/DownloadItem, with a table-rename migration (download_wants -> download_requests, old download_requests -> download_downloads) safe against both fresh installs and existing data. Every anchored manual download now upserts/reuses a durable Request before running, so a "download now" that finds nothing is picked up by the background reconciler automatically instead of just failing with no trace — the gap that caused this session's repeated "no candidates found" failures on the same album. Also adds auto-download guardrails (file-size min/max with a preferred target, allowed file types) that gate what the pipeline may grab unattended, live-editable from a new settings section. The frontend's wanted-view becomes downloads-view, with a new Downloads tab showing attempt/transfer history that previously had no UI at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS |
||
|
|
cbd82a5a74 |
feat: autotag mixed-bag splitting, search relevance fixes, and multi-library download imports
Autotag: detect "junk drawer" folders with no artist/album consensus and split them into synthetic per-cluster groups instead of forcing one match on an unrelated pile of tracks; repair tagging_items rows left behind by a prior scan orphan-cleanup gap. Explore: fix an exact artist-name search being drowned out by its own catalog entries in intent-prior scoring, and prune stale in_library bookkeeping left behind when a referenced library row is deleted. Download: fix a multi-library regression where every import failed with "no library root configured" — the importer resolved the library root from a legacy single-library config field that nothing populates in the current multi-library model. It now resolves the destination library per-request from the request's own library_id. Also widen the Soulseek search window (12s -> 20s), measured against real request history to be missing available peers on live queries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS |
||
|
|
e190fd75b9 |
feat: data lifecycle rewrite, download clients, wanted list, and central catalog index
Ships the fresh-start schema cleanup: rebuilt explore catalog index pipeline (dump import, artifact fetch/build, incremental listen-count refresh), a new download subsystem (Lidarr/Prowlarr/qBittorrent/SABnzbd/ slskd/yt-dlp providers, staging, reconciliation, wanted list), and the supporting schema/query/store changes across backend and frontend. Also includes two smaller follow-ups: bump the central index's rebuild-after cadence from 90 to 180 days, and remove the Explore "library only" online/offline toggle entirely (frontend-only, no backend counterpart) rather than carry unused UI/state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS |