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
89 lines
3.2 KiB
Go
89 lines
3.2 KiB
Go
package download
|
|
|
|
import "time"
|
|
|
|
// UserConfig is the download subsystem's slice of the TOML config file.
|
|
// Provider connections are not here — they live in the database, keyed
|
|
// by row, because there can be many of them and they change through the
|
|
// settings UI rather than by hand-editing.
|
|
type UserConfig struct {
|
|
// PathTemplate lays out imported files under the library root.
|
|
// Tokens: {albumartist} {artist} {album} {year} {track} {disc}
|
|
// {title}. Empty falls back to DefaultPathTemplate.
|
|
PathTemplate string `toml:"PathTemplate"`
|
|
|
|
// AutoPick lets a single high-confidence, high-quality candidate
|
|
// download without asking. Off by default: an unattended download
|
|
// that picks wrong puts the wrong files in the library, and the
|
|
// ranking has to earn that trust on a given user's sources first.
|
|
AutoPick bool `toml:"AutoPick"`
|
|
|
|
// MaxConcurrent bounds simultaneous transfers across all providers.
|
|
// Per-provider limits sit underneath it and are set on the provider
|
|
// itself, since the right number depends on what is on the other
|
|
// end: one Soulseek peer, or a usenet server built for parallelism.
|
|
MaxConcurrent int `toml:"MaxConcurrent"`
|
|
|
|
// WantedIntervalMinutes is how often the wanted list is reconciled:
|
|
// artist subscriptions expanded, owned items retired, due wants
|
|
// searched for. Zero uses the default.
|
|
WantedIntervalMinutes int `toml:"WantedIntervalMinutes"`
|
|
|
|
// WantedBatch bounds how many wants one reconcile pass searches
|
|
// for. A large list should be worked through steadily rather than
|
|
// in one burst that every provider sees as a flood.
|
|
WantedBatch int `toml:"WantedBatch"`
|
|
|
|
// MinFileSizeMB, MaxFileSizeMB and PreferredFileSizeMB bound and
|
|
// nudge what auto-pick (interactive or via the request list) may
|
|
// grab without asking. Zero on any of them is permissive: see
|
|
// AutoDownloadPrefs.
|
|
MinFileSizeMB int `toml:"MinFileSizeMB"`
|
|
MaxFileSizeMB int `toml:"MaxFileSizeMB"`
|
|
PreferredFileSizeMB int `toml:"PreferredFileSizeMB"`
|
|
|
|
// AllowedFormats restricts auto-pick to these formats. Empty means
|
|
// no restriction. Values are Format strings ("flac", "mp3", ...).
|
|
AllowedFormats []string `toml:"AllowedFormats"`
|
|
}
|
|
|
|
// AutoDownloadPrefs converts the persisted guardrail fields to the
|
|
// runtime type Manager and the ranker consume.
|
|
func (c *UserConfig) AutoDownloadPrefs() AutoDownloadPrefs {
|
|
formats := make([]Format, 0, len(c.AllowedFormats))
|
|
for _, f := range c.AllowedFormats {
|
|
formats = append(formats, Format(f))
|
|
}
|
|
|
|
return AutoDownloadPrefs{
|
|
MinSizeMB: c.MinFileSizeMB,
|
|
MaxSizeMB: c.MaxFileSizeMB,
|
|
PreferredSizeMB: c.PreferredFileSizeMB,
|
|
AllowedFormats: formats,
|
|
}
|
|
}
|
|
|
|
// ApplyDefaults fills unset fields.
|
|
func (c *UserConfig) ApplyDefaults() {
|
|
if c.PathTemplate == "" {
|
|
c.PathTemplate = DefaultPathTemplate
|
|
}
|
|
|
|
if c.MaxConcurrent <= 0 {
|
|
c.MaxConcurrent = defaultConcurrency
|
|
}
|
|
|
|
if c.WantedIntervalMinutes <= 0 {
|
|
c.WantedIntervalMinutes = int(defaultReconcileInterval / time.Minute)
|
|
}
|
|
|
|
if c.WantedBatch <= 0 {
|
|
c.WantedBatch = defaultDueBatch
|
|
}
|
|
}
|
|
|
|
// WantedInterval is the reconcile interval as a duration.
|
|
func (c *UserConfig) WantedInterval() time.Duration {
|
|
return time.Duration(c.WantedIntervalMinutes) * time.Minute
|
|
}
|