refactor(download): rename Want/Request to Request/Download, unify downloads flow, add auto-download guardrails
Build & publish Arch package / arch-package (push) Successful in 2m2s
Search index maintenance / maintain-index (push) Successful in 7s

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
This commit is contained in:
2026-08-10 14:35:57 -04:00
co-authored by Claude Sonnet 5
parent cbd82a5a74
commit 65333857e2
62 changed files with 4067 additions and 2524 deletions
+11 -11
View File
@@ -201,9 +201,9 @@ func (e ytEntry) link() string {
// how yt-dlp is actually useful for albums — a single "full album"
// video is one file and cannot be imported as tracks. Without a
// tracklist it falls back to returning the top individual results.
func (y *ytDlp) Search(ctx context.Context, req Request) ([]Candidate, error) {
if len(req.Expected) > 0 {
c, err := y.assembleAlbum(ctx, req)
func (y *ytDlp) Search(ctx context.Context, dl Download) ([]Candidate, error) {
if len(dl.Expected) > 0 {
c, err := y.assembleAlbum(ctx, dl)
if err != nil {
return nil, err
}
@@ -213,7 +213,7 @@ func (y *ytDlp) Search(ctx context.Context, req Request) ([]Candidate, error) {
}
}
entries, err := y.search(ctx, req.SearchText(), ytSearchCount)
entries, err := y.search(ctx, dl.SearchText(), ytSearchCount)
if err != nil {
return nil, err
}
@@ -257,7 +257,7 @@ func (y *ytDlp) Search(ctx context.Context, req Request) ([]Candidate, error) {
// threshold decides whether what arrived is enough.
func (y *ytDlp) assembleAlbum(
ctx context.Context,
req Request,
dl Download,
) (Candidate, error) {
type hit struct {
index int
@@ -272,10 +272,10 @@ func (y *ytDlp) assembleAlbum(
group, gctx := errgroup.WithContext(ctx)
group.SetLimit(ytTrackConcurrency)
for i, track := range req.Expected {
for i, track := range dl.Expected {
group.Go(func() error {
query := strings.TrimSpace(
req.Artist + " " + track.Title,
dl.Artist + " " + track.Title,
)
entries, err := y.search(gctx, query, 1)
@@ -300,11 +300,11 @@ func (y *ytDlp) assembleAlbum(
}
c := Candidate{
ID: "ytdlp:album:" + req.ID,
ID: "ytdlp:album:" + dl.ID,
Kind: KindYtDlp,
Protocol: ProtocolDirect,
Title: req.Album,
Artist: req.Artist,
Title: dl.Album,
Artist: dl.Artist,
Origin: "yt-dlp (assembled per track)",
Health: 0.75,
Payload: map[string]string{},
@@ -312,7 +312,7 @@ func (y *ytDlp) assembleAlbum(
}
for _, h := range hits {
track := req.Expected[h.index]
track := dl.Expected[h.index]
link := h.entry.link()
if link == "" {