docs: server comments say what the code does, not how it came to be (#6277)
Comment blocks in cmd/minstrel and 30 smaller internal packages, rewritten
to rule 32's standard with each citation read in context: task, milestone
and rule ids go, and so do the stories they were attached to ("used to",
"Before #N", measurements from one install, quotes, pointers to Fabled
Librarian's records and to docs/superpowers files that are not in the
repo). Where a story wrapped a real constraint, the constraint stays as a
present-tense fact.
Comments that had gone stale are corrected:
- taste.Config: the tuned values are overlaid by internal/recsettings;
it is no longer "a later follow-up".
- StorageConfig: the data dir holds art, caches and the stream secret,
not only playlist covers.
- the Subsonic album list and getUser comments describe what they do
today rather than a milestone that would change it.
Checked: the non-comment token stream of every changed file matches
HEAD, and gofmt -l is empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,14 +55,12 @@ func runAdmin(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// adminResetPassword resets a user's login password (password_hash). It
|
||||
// recovers a locked-out operator when the bootstrap password was missed or
|
||||
// the DB volume was recreated (Fable #321) without DB surgery.
|
||||
// adminResetPassword resets a user's login password (password_hash), so an
|
||||
// admin who is locked out can get back in without editing the database.
|
||||
//
|
||||
// It deliberately leaves subsonic_password alone. That column is stored in
|
||||
// plain text, and it used to receive the new login password here, so any
|
||||
// account recovered this way had its login password readable in the database
|
||||
// (#5026). The Subsonic password is generated separately in Settings.
|
||||
// It leaves subsonic_password alone: that column is stored in plain text, so
|
||||
// writing the login password there would make it readable in the database.
|
||||
// The Subsonic password is generated separately in Settings.
|
||||
func adminResetPassword(args []string) error {
|
||||
fs := flag.NewFlagSet("admin reset-password", flag.ContinueOnError)
|
||||
configPath := fs.String("config", os.Getenv("MINSTREL_CONFIG"), "path to YAML config file")
|
||||
|
||||
+54
-67
@@ -128,7 +128,7 @@ func run() error {
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Fingerprinting settings (M400 #3913): one instance, shared by the scanner,
|
||||
// Fingerprinting settings: one instance, shared by the scanner,
|
||||
// the fingerprint backfill, the duplicate sweep and the admin API, so a save
|
||||
// reaches all of them without a restart. A load failure is logged, not fatal:
|
||||
// the service falls back to the shipped defaults.
|
||||
@@ -136,9 +136,9 @@ func run() error {
|
||||
if fpErr != nil {
|
||||
logger.Warn("fingerprint settings: using defaults", "err", fpErr)
|
||||
}
|
||||
// The library's folders (#6129): the ones saved in Settings, or
|
||||
// The library's folders: the ones saved in Settings, or
|
||||
// MINSTREL_LIBRARY_SCAN_PATHS until any are. A load failure falls back to
|
||||
// env, which is what the server read before the setting existed.
|
||||
// the env value.
|
||||
roots, rootsSaved, rootsErr := library.LoadRoots(ctx, pool, cfg.Library.ScanPaths)
|
||||
switch {
|
||||
case rootsErr != nil:
|
||||
@@ -149,14 +149,14 @@ func run() error {
|
||||
}
|
||||
scanner := library.New(pool, logger, roots, fpSettings)
|
||||
|
||||
// Loudness analysis settings (M464 #4995): shared by the loudness backfill
|
||||
// Loudness analysis settings: shared by the loudness backfill
|
||||
// and the admin API, and falls back to the defaults like the above.
|
||||
loudSettings, loudErr := library.NewLoudnessSettingsService(ctx, pool)
|
||||
if loudErr != nil {
|
||||
logger.Warn("loudness settings: using defaults", "err", loudErr)
|
||||
}
|
||||
|
||||
// AcoustID lookup settings (M401): shared by the lookup worker and the
|
||||
// AcoustID lookup settings: shared by the lookup worker and the
|
||||
// admin API. A load failure leaves the lookup off.
|
||||
acoustIDSettings, acoustIDErr := library.NewAcoustIDSettingsService(ctx, pool)
|
||||
if acoustIDErr != nil {
|
||||
@@ -194,10 +194,9 @@ func run() error {
|
||||
coverEnricher := coverart.NewEnricher(pool, logger.With("component", "coverart"), coverSettings)
|
||||
coverEnricher.DataDir = cfg.Storage.DataDir
|
||||
|
||||
// Recommendation tuning lab (#1250): seeds shipped defaults on first
|
||||
// boot and pushes the daily_mix weights + taste config into package
|
||||
// playlists — must precede the scheduler so the first builds score
|
||||
// with the operator's tuned values, not the pre-push literals.
|
||||
// Recommendation tuning: seeds the shipped defaults on first boot and
|
||||
// pushes the daily-mix weights and taste config into package playlists.
|
||||
// It must precede the scheduler, so the first builds use the tuned values.
|
||||
recSettings, err := recsettings.New(ctx, pool, logger.With("component", "recsettings"))
|
||||
if err != nil {
|
||||
logger.Error("recommendation settings service init failed", "err", err)
|
||||
@@ -213,7 +212,7 @@ func run() error {
|
||||
logger.With("component", "scan_run"),
|
||||
library.RunScanConfig{
|
||||
BackfillCap: 5000,
|
||||
EnrichCap: -1, // #388: no global cap; per-provider rate limits
|
||||
EnrichCap: -1, // no global cap; each provider paces itself
|
||||
ArtistEnrichCap: -1,
|
||||
DataDir: cfg.Storage.DataDir,
|
||||
},
|
||||
@@ -223,14 +222,13 @@ func run() error {
|
||||
}()
|
||||
} else {
|
||||
// No startup file walk, but still run backfill + enrich so cover
|
||||
// progress doesn't stall just because the operator disabled
|
||||
// scan-on-startup.
|
||||
// art keeps arriving with scan-on-startup off.
|
||||
go func() {
|
||||
if _, err := library.RunScan(ctx, pool, nil, coverEnricher,
|
||||
logger.With("component", "scan_run"),
|
||||
library.RunScanConfig{
|
||||
BackfillCap: 5000,
|
||||
EnrichCap: -1, // #388: no global cap; per-provider rate limits
|
||||
EnrichCap: -1, // no global cap; each provider paces itself
|
||||
ArtistEnrichCap: -1,
|
||||
DataDir: cfg.Storage.DataDir,
|
||||
},
|
||||
@@ -240,46 +238,44 @@ func run() error {
|
||||
}()
|
||||
}
|
||||
|
||||
// Start the ListenBrainz scrobble worker. Per spec §M4a, runs every 30s
|
||||
// and drains up to 50 pending rows per tick. Per-user gating happens
|
||||
// inside the worker (rows from disabled users are skipped).
|
||||
// ListenBrainz scrobble worker: every 30s, drains up to 50 pending rows.
|
||||
// Rows from users with scrobbling off are skipped inside the worker.
|
||||
scrobbleWorker := scrobble.NewWorker(pool, listenbrainz.NewClient(), logger.With("component", "scrobble"))
|
||||
go scrobbleWorker.Run(ctx)
|
||||
|
||||
// Start the similarity ingest worker. Per spec §M4b, runs every 1h, drains
|
||||
// up to 5 played tracks + 5 played artists per tick, weekly re-fetch cap
|
||||
// per row. Public LB endpoints — no token required.
|
||||
// Similarity ingest worker: hourly, fetches ListenBrainz similarity for up
|
||||
// to 5 played tracks and 5 played artists, each re-fetched at most weekly.
|
||||
// The endpoints are public, so no token is needed.
|
||||
similarityWorker := similarity.NewWorker(pool, listenbrainz.NewClient(), logger.With("component", "similarity"))
|
||||
go similarityWorker.Run(ctx)
|
||||
|
||||
// Start the household co-play worker (#1533). Recomputes artist–artist
|
||||
// Household co-play worker: recomputes artist–artist
|
||||
// co-occurrence edges (source='user_cooccurrence') from play_events every
|
||||
// 6h — a collaborative candidate arm for the radio/mix pools. Pure local
|
||||
// SQL, no external calls; empty on single-user servers.
|
||||
go coplay.NewWorker(pool, logger.With("component", "coplay")).Run(ctx)
|
||||
|
||||
// Fingerprint backfill (M400 #3908): fingerprints the tracks the scan never
|
||||
// will — everything imported before fingerprinting existed, and rows derived
|
||||
// by an older method. A worker of its own rather than a scan stage; see
|
||||
// internal/library/fingerprint_backfill.go for why.
|
||||
// Fingerprint backfill: fingerprints the tracks the scan skips (unchanged
|
||||
// files, and rows fingerprinted by an older method). A worker of its own
|
||||
// rather than a scan stage; see internal/library/fingerprint_backfill.go.
|
||||
go library.NewFingerprintBackfillWorker(pool, logger.With("component", "fingerprint_backfill"), fpSettings).Run(ctx)
|
||||
|
||||
// Loudness backfill (M464 #4995): measures every track's loudness for
|
||||
// Loudness backfill: measures every track's loudness for
|
||||
// normalization, new tracks included; the scan only drops a changed file's
|
||||
// measurement. See internal/library/loudness_backfill.go.
|
||||
go library.NewLoudnessBackfillWorker(pool, logger.With("component", "loudness_backfill"), loudSettings).Run(ctx)
|
||||
|
||||
// AcoustID lookup (M401): fills the recording MBIDs untagged tracks lack,
|
||||
// so the similarity arms can see them. Idle until the operator adds a key
|
||||
// and switches it on. See internal/library/acoustid_lookup.go.
|
||||
// AcoustID lookup: fills the recording MBIDs untagged tracks lack, so the
|
||||
// similarity arms can see them. Idle until an admin adds a key and
|
||||
// switches it on. See internal/library/acoustid_lookup.go.
|
||||
go acoustIDLookup.Run(ctx)
|
||||
|
||||
// Duplicate sweep (M400 #3910): proposes groups of tracks holding one
|
||||
// Duplicate sweep: proposes groups of tracks holding one
|
||||
// recording, from the fingerprints above. Sweeps only when fingerprints have
|
||||
// changed since the last sweep.
|
||||
go library.NewDuplicateSweepWorker(pool, logger.With("component", "duplicate_sweep"), fpSettings).Run(ctx)
|
||||
|
||||
// Start the tag-enrichment worker (#1490). Reconciles the compiled-in
|
||||
// Tag-enrichment worker: reconciles the compiled-in
|
||||
// tag providers with tag_provider_settings, bumps the sources version if
|
||||
// the provider set changed (re-opening settled rows), then drains tracks
|
||||
// needing folksonomy tags on a periodic tick. Standalone (not in the file
|
||||
@@ -298,54 +294,50 @@ func run() error {
|
||||
tagEnricher := tags.NewEnricher(pool, logger.With("component", "tags"), tagSettings)
|
||||
go tags.NewWorker(tagEnricher, logger.With("component", "tags")).Run(ctx)
|
||||
|
||||
// Start the GC worker. Runs every 1h and sweeps lifecycle tables
|
||||
// that have no writer-side close path or retention policy:
|
||||
// orphan play_events, stale play_sessions, expired
|
||||
// scrobble_queue failures, stuck system_playlist_runs, expired
|
||||
// password_resets. Each sweep is idempotent — a row that's
|
||||
// already clean is a no-op. Addresses drift audit findings
|
||||
// #565 #566 #567 #574 #575 (Scribe parent #552).
|
||||
// GC worker: hourly, sweeps the tables that have no writer-side close
|
||||
// path or retention policy: orphan play_events, stale play_sessions,
|
||||
// expired scrobble_queue failures, stuck system_playlist_runs and expired
|
||||
// password_resets. Each sweep is idempotent.
|
||||
gcWorker := gc.NewWorker(pool, logger.With("component", "gc"))
|
||||
go gcWorker.Run(ctx)
|
||||
|
||||
// The request reconciler completes approved requests once the library
|
||||
// holds what they asked for (M502 #5520).
|
||||
// Live-event bus shared between SSE subscribers (api.Mount) and
|
||||
// background workers that publish (reconciler today; scanner later).
|
||||
// Constructed before any service that publishes so they all share the
|
||||
// same instance.
|
||||
// Live-event bus shared between SSE subscribers (api.Mount) and the
|
||||
// background workers that publish. Built before any of them, so they all
|
||||
// share one instance.
|
||||
bus := eventbus.New()
|
||||
// Scan-run lifecycle events use a package-level setter rather than
|
||||
// threading the bus through RunScan + TryStartScan + the Scheduler
|
||||
// + every test caller. Per-process singleton, set once at startup.
|
||||
library.SetEventBus(bus)
|
||||
// The notifications inbox's one writer (M489), shared by every
|
||||
// The notifications inbox's one writer, shared by every
|
||||
// background producer started here. The API builds its own over the
|
||||
// same pool and bus.
|
||||
notifier := notifications.New(pool, bus, logger.With("component", "notifications"))
|
||||
|
||||
// The request reconciler completes approved requests once the library
|
||||
// holds what they asked for.
|
||||
requestReconciler := requests.NewReconciler(pool, logger.With("component", "requests"), bus)
|
||||
requestReconciler.SetNotifier(notifier)
|
||||
library.SetNotifier(notifier)
|
||||
go requestReconciler.Run(ctx)
|
||||
|
||||
// Release catalogue (M502 #5519): re-reads discographies older than a
|
||||
// Release catalogue: re-reads discographies older than a
|
||||
// week from MusicBrainz, a batch an hour, through the process-wide
|
||||
// MusicBrainz pacer it shares with tag enrichment.
|
||||
cat := catalogue.New(pool, nil, logger.With("component", "catalogue"))
|
||||
go cat.Run(ctx)
|
||||
|
||||
// The wanted list (M502 #5520): reads followed artists' discographies
|
||||
// The wanted list: reads followed artists' discographies
|
||||
// daily for new releases and marks wants held once the library has them.
|
||||
go wanted.New(pool, cat, logger.With("component", "wanted")).Run(ctx)
|
||||
|
||||
// Search and grab (M502 #5521): every ten minutes, searches the
|
||||
// Search and grab: every ten minutes, searches the
|
||||
// configured indexers for wants due, paced by each source's
|
||||
// searches-per-hour, and hands the best release to the download client
|
||||
// when auto-grab is on.
|
||||
go wanted.NewSearcher(pool, cat, logger.With("component", "search")).Run(ctx)
|
||||
|
||||
// Import with verification (M502 #5522): every minute, finished
|
||||
// Import with verification: every minute, finished
|
||||
// downloads are checked against their album (AcoustID, lengths, tags)
|
||||
// and filed into the library with MusicBrainz tags, or refused.
|
||||
var printer importer.Printer
|
||||
@@ -356,13 +348,13 @@ func run() error {
|
||||
imp.DataDir = cfg.Storage.DataDir
|
||||
go imp.Run(ctx)
|
||||
|
||||
// Download lifecycle (M502 #5523): every ten minutes, a download that
|
||||
// Download lifecycle: every ten minutes, a download that
|
||||
// failed or that nobody shares is retried, then given up and its album
|
||||
// searched again; an imported one seeds, then is removed.
|
||||
go lifecycle.New(pool, logger.With("component", "lifecycle")).Run(ctx)
|
||||
|
||||
// Duplicate resolver (M498, M502): classifies the sweep's groups and, with
|
||||
// the operator's auto-resolve on, merges the copies that are safe to merge.
|
||||
// Duplicate resolver: classifies the sweep's groups and, with auto-resolve
|
||||
// on, merges the copies that are safe to merge.
|
||||
// Started after SetNotifier so its summaries reach admins. It asks the
|
||||
// catalogue for the track list of an album whose wrong slot it must
|
||||
// decide, a few a pass. See internal/library/duplicate_resolve.go.
|
||||
@@ -371,15 +363,12 @@ func run() error {
|
||||
TrackLists: cat.RefreshReleaseGroup, Identify: acoustIDLookup.IdentifyRecordings,
|
||||
}).Run(ctx)
|
||||
|
||||
// Missing-file re-acquisition (milestone #290, M502 #5520). Wants back
|
||||
// albums whose files have been gone longer than the grace window; the
|
||||
// search schedule looks for them. Hourly tick — the grace window is
|
||||
// measured in hours.
|
||||
// Missing-file re-acquisition: wants back albums whose files have been
|
||||
// gone longer than the grace window, for the search schedule to find.
|
||||
// Hourly, because the grace window is measured in hours.
|
||||
//
|
||||
// A settings-load failure is logged, not fatal: NewSettingsService always
|
||||
// returns a usable service holding the defaults, and running on defaults
|
||||
// is far better than dropping the feature because the database hiccuped
|
||||
// during boot.
|
||||
// returns a usable service holding the defaults.
|
||||
reacqSettings, reacqErr := reacquisition.NewSettingsService(
|
||||
ctx, pool, logger.With("component", "reacquisition"))
|
||||
if reacqErr != nil {
|
||||
@@ -387,18 +376,18 @@ func run() error {
|
||||
}
|
||||
go reacquisition.NewSweeper(pool, reacqSettings, logger.With("component", "reacquisition")).Run(ctx)
|
||||
|
||||
// library_changes compactor (#357 follow-up). Daily tick; deletes
|
||||
// library_changes compactor: daily, deletes
|
||||
// rows older than the configured retention so the change-log table
|
||||
// doesn't grow unbounded. Clients that drop offline longer than
|
||||
// retention hit the /api/library/sync 410 fallback and resync.
|
||||
libraryChangesCompactor := syncpkg.NewCompactor(pool, logger.With("component", "library_changes_compactor"))
|
||||
go libraryChangesCompactor.Run(ctx)
|
||||
|
||||
// Notifications inbox retention (M489). Daily: read rows go after 90
|
||||
// Notifications inbox retention, daily: read rows go after 90
|
||||
// days, anything at all after a year.
|
||||
go notifications.NewRetention(pool, logger.With("component", "notifications_retention")).Run(ctx)
|
||||
|
||||
// Notifications by email (M489 #5346), grouped, never one per event: new
|
||||
// Notifications by email, grouped, never one per event: new
|
||||
// music as a daily summary at a local hour, everything else batched an
|
||||
// hour (admin-configurable) after the first item.
|
||||
go notifications.NewDigest(
|
||||
@@ -407,10 +396,8 @@ func run() error {
|
||||
logger.With("component", "notification_digest"),
|
||||
).Run(ctx)
|
||||
|
||||
// Per-user system-playlist scheduler (#392 Half B). Fires each
|
||||
// active user's daily build at 03:00 in their stored timezone.
|
||||
// Replaces the 24h-anchored cron loop (removed in the next commit
|
||||
// of this arc).
|
||||
// Per-user system-playlist scheduler: runs each active user's daily
|
||||
// build at 03:00 in their stored timezone.
|
||||
playlistScheduler, err := playlists.NewScheduler(
|
||||
pool,
|
||||
logger.With("component", "playlist_scheduler"),
|
||||
@@ -439,7 +426,7 @@ func run() error {
|
||||
|
||||
scanCfg := library.RunScanConfig{
|
||||
BackfillCap: 5000,
|
||||
EnrichCap: -1, // #388: no global cap; per-provider rate limits
|
||||
EnrichCap: -1, // no global cap; each provider paces itself
|
||||
ArtistEnrichCap: -1,
|
||||
DataDir: cfg.Storage.DataDir,
|
||||
}
|
||||
@@ -468,7 +455,7 @@ func run() error {
|
||||
srv.LoudnessSettings = loudSettings
|
||||
srv.AcoustIDLookup = acoustIDLookup
|
||||
// The sweeper above holds this same instance, so a save from the admin
|
||||
// card changes what it does on its next tick (#3936).
|
||||
// card changes what it does on its next tick.
|
||||
srv.ReacqSettings = reacqSettings
|
||||
srv.StreamSecret = cfg.StreamSecret
|
||||
httpServer := &http.Server{
|
||||
|
||||
Reference in New Issue
Block a user