diff --git a/internal/acoustid/client.go b/internal/acoustid/client.go index dbf1f42d..4cbfe5f3 100644 --- a/internal/acoustid/client.go +++ b/internal/acoustid/client.go @@ -39,8 +39,8 @@ const ( // minInterval keeps to AcoustID's 3 requests a second with headroom: one // every 400ms is 2.5 a second. minInterval = 400 * time.Millisecond - // lookupTimeout bounds one request, connection included. A lookup answers in well under a second; 20s is the line past which slow - // has become never. + // lookupTimeout bounds one request, connection included. A lookup answers + // in well under a second; 20s is the line past which slow has become never. lookupTimeout = 20 * time.Second // bodyLimit caps the response read. A lookup with meta=recordings for a // popular song runs to tens of kilobytes. diff --git a/internal/acquire/fanout.go b/internal/acquire/fanout.go index 35260078..c4f607c8 100644 --- a/internal/acquire/fanout.go +++ b/internal/acquire/fanout.go @@ -10,8 +10,8 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/indexer" ) -// Searching every source at once. A source that is down narrows a search rather than failing it: the -// results come back with the list of who did not answer. +// Searching every source at once. A source that is down narrows a search rather +// than failing it: the results come back with the list of who did not answer. // SourceFailure is one source that could not be reached. type SourceFailure struct { diff --git a/internal/acquire/pace.go b/internal/acquire/pace.go index 8b8bb575..fb2352e3 100644 --- a/internal/acquire/pace.go +++ b/internal/acquire/pace.go @@ -8,8 +8,9 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/indexer" ) -// SearchPace is the one Pacer every search shares. Process-wide because a Fanout is built fresh for each -// search; a Pacer per Fanout would start empty and pace nothing. +// SearchPace is the one Pacer every search shares. Process-wide because a +// Fanout is built fresh for each search; a Pacer per Fanout would start empty +// and pace nothing. var SearchPace = &Pacer{} // holdWithoutRetryAfter is how long a source that answered 429 without saying diff --git a/internal/api/admin_acoustid.go b/internal/api/admin_acoustid.go index 6a3a433a..69c88f17 100644 --- a/internal/api/admin_acoustid.go +++ b/internal/api/admin_acoustid.go @@ -11,7 +11,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) -// AcoustID recording-id lookup, admin side (M401 #3922). +// AcoustID recording-id lookup, admin side. // acoustIDUnsettledLimit caps the admin list of tracks a lookup could not // settle. It is a list to read, not to page through. @@ -96,7 +96,8 @@ type acoustIDSettingsReq struct { APIKey *string `json:"api_key"` } -// handleUpdateAcoustIDSettings implements PUT /api/admin/library/acoustid-settings. +// handleUpdateAcoustIDSettings implements PUT +// /api/admin/library/acoustid-settings. func (h *handlers) handleUpdateAcoustIDSettings(w http.ResponseWriter, r *http.Request) { var req acoustIDSettingsReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -143,9 +144,9 @@ type acoustIDUnsettledResp struct { LookedUpAt time.Time `json:"looked_up_at"` } -// handleListUnsettledAcoustID implements GET /api/admin/library/acoustid/unsettled: -// the tracks a lookup could not settle (ambiguous or failed), so they can be -// found and tagged by hand. +// handleListUnsettledAcoustID implements GET +// /api/admin/library/acoustid/unsettled: the tracks a lookup could not settle +// (ambiguous or failed), so they can be found and tagged by hand. func (h *handlers) handleListUnsettledAcoustID(w http.ResponseWriter, r *http.Request) { rows, err := dbq.New(h.pool).ListUnsettledAcoustIDLookups(r.Context(), acoustIDUnsettledLimit) if err != nil { diff --git a/internal/api/admin_acquisition.go b/internal/api/admin_acquisition.go index e857d24a..f6144e61 100644 --- a/internal/api/admin_acquisition.go +++ b/internal/api/admin_acquisition.go @@ -13,7 +13,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) -// Indexers and download clients, admin side (M502 #5518). Rows the operator +// Indexers and download clients, admin side. Rows an admin // adds and configures; nothing is seeded. Secrets are write-only: no response // carries one, only whether it is set. @@ -79,9 +79,10 @@ func (h *handlers) handleListAcquisitionServices(w http.ResponseWriter, r *http. writeJSON(w, http.StatusOK, map[string]any{"services": out}) } -// handleCreateAcquisitionService implements POST /api/admin/acquisition/services -// with {"protocol": "prowlarr"|"torznab"|"qbittorrent"|"exchange"}. The row -// starts switched off, with no address. +// handleCreateAcquisitionService implements POST +// /api/admin/acquisition/services with {"protocol": +// "prowlarr"|"torznab"|"qbittorrent"|"exchange"}. The row starts switched off, +// with no address. func (h *handlers) handleCreateAcquisitionService(w http.ResponseWriter, r *http.Request) { var body struct { Protocol string `json:"protocol"` diff --git a/internal/api/admin_cover_sources.go b/internal/api/admin_cover_sources.go index 5c9c1171..924eca90 100644 --- a/internal/api/admin_cover_sources.go +++ b/internal/api/admin_cover_sources.go @@ -70,7 +70,8 @@ type updateCoverSourceResp struct { VersionBumped bool `json:"version_bumped"` } -// handleUpdateCoverSource implements PATCH /api/admin/cover-sources/{provider_id}. +// handleUpdateCoverSource implements PATCH +// /api/admin/cover-sources/{provider_id}. func (h *handlers) handleUpdateCoverSource(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "provider_id") @@ -128,7 +129,8 @@ type testCoverSourceResp struct { Error string `json:"error,omitempty"` } -// handleTestCoverSource implements POST /api/admin/cover-sources/{provider_id}/test. +// handleTestCoverSource implements POST +// /api/admin/cover-sources/{provider_id}/test. func (h *handlers) handleTestCoverSource(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "provider_id") diff --git a/internal/api/admin_cover_sources_test.go b/internal/api/admin_cover_sources_test.go index 3a0de4e7..0da0d4f9 100644 --- a/internal/api/admin_cover_sources_test.go +++ b/internal/api/admin_cover_sources_test.go @@ -103,7 +103,8 @@ func TestAdminListCoverSources_ReturnsRegisteredProviders(t *testing.T) { if p.APIKeySet { t.Error("api_key_set = true, want false (no key set)") } - // apiTestAlbumProvider implements AlbumCoverProvider but not TestableProvider. + // apiTestAlbumProvider implements AlbumCoverProvider but not + // TestableProvider. if p.Testable { t.Error("testable = true, want false") } diff --git a/internal/api/admin_coverage.go b/internal/api/admin_coverage.go index 7bbe259c..851d32c7 100644 --- a/internal/api/admin_coverage.go +++ b/internal/api/admin_coverage.go @@ -39,11 +39,12 @@ func (h *handlers) handleGetLibraryCoverage(w http.ResponseWriter, r *http.Reque }) } -// fingerprintCoverageResp is the wire shape for GET /api/admin/library/fingerprints. -// fingerprinted + rejected + pending = total. Missing tracks are not counted: -// there is no file to fingerprint. Enabled travels with the counts because with -// fingerprinting off (#3913) pending never shrinks, and a gauge that implies -// progress would be promising work nothing is doing. +// fingerprintCoverageResp is the wire shape for GET +// /api/admin/library/fingerprints. fingerprinted + rejected + pending = total. +// Missing tracks are not counted: there is no file to fingerprint. Enabled +// travels with the counts because with fingerprinting off pending never +// shrinks, and a gauge that implies progress would be promising work nothing is +// doing. type fingerprintCoverageResp struct { Total int64 `json:"total"` Fingerprinted int64 `json:"fingerprinted"` @@ -70,7 +71,7 @@ func (h *handlers) fingerprintCoverage(ctx context.Context) (fingerprintCoverage } // handleGetFingerprintCoverage implements GET /api/admin/library/fingerprints: -// how far the fingerprint backfill (#3908) has got. The backfill is its own +// how far the fingerprint backfill has got. The backfill is its own // worker spanning many passes, with no scan run to attach a tally to, so its // progress is read live here. Always 200; zeros on an empty library. func (h *handlers) handleGetFingerprintCoverage(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/admin_coverage_test.go b/internal/api/admin_coverage_test.go index 4fbd4e0c..4a42c647 100644 --- a/internal/api/admin_coverage_test.go +++ b/internal/api/admin_coverage_test.go @@ -80,11 +80,9 @@ func TestAdminLibraryCoverage_MixedRowsReturnCorrectBuckets(t *testing.T) { mbid5 := "55555555-5555-5555-5555-555555555555" mbid6 := "66666666-6666-6666-6666-666666666666" mbid7 := "77777777-7777-7777-7777-777777777777" - // Cover every cover_art_source value the migrations allow so a future - // addition without updating the rollup query trips this test — - // regression guard for drift #557, the gap that hid #556 (deezer + - // lastfm omission introduced by migration 0020 but never wired into - // the rollup whitelist). + // Cover every cover_art_source value the migrations allow, so a source + // added to the CHECK but not to the rollup query's whitelist trips this + // test. rows := []seed{ {title: "WithArtSidecar", source: &sourceSidecar, mbid: &mbid1}, // with_art {title: "WithArtMbcaa", source: &sourceMbcaa, mbid: &mbid2}, // with_art diff --git a/internal/api/admin_covers.go b/internal/api/admin_covers.go index d354ce31..b9856f23 100644 --- a/internal/api/admin_covers.go +++ b/internal/api/admin_covers.go @@ -19,8 +19,9 @@ type adminCoverRefetchResp struct { CoverArtSource *string `json:"cover_art_source"` } -// handleAdminAlbumRefetchCover implements POST /api/admin/albums/{id}/cover/refetch. -// Synchronously clears + re-runs the enricher for one album. +// handleAdminAlbumRefetchCover implements POST +// /api/admin/albums/{id}/cover/refetch. Synchronously clears + re-runs the +// enricher for one album. func (h *handlers) handleAdminAlbumRefetchCover(w http.ResponseWriter, r *http.Request) { albumID, ok := parseUUID(chi.URLParam(r, "id")) if !ok { @@ -51,9 +52,9 @@ type adminBulkRefetchResp struct { Started bool `json:"started"` } -// handleAdminBulkRefetchCovers implements POST /api/admin/covers/refetch-missing. -// Returns immediately; the drain runs UNBOUNDED in a background goroutine -// (#388 — no global cap; remote providers self-throttle per provider, local +// handleAdminBulkRefetchCovers implements POST +// /api/admin/covers/refetch-missing. Returns immediately; the drain runs +// UNBOUNDED in a background goroutine (remote providers pace themselves, local // sources run at disk speed). The count is unknowable synchronously, so the // response just acknowledges the drain started. func (h *handlers) handleAdminBulkRefetchCovers(w http.ResponseWriter, _ *http.Request) { diff --git a/internal/api/admin_duplicates.go b/internal/api/admin_duplicates.go index 87260c05..970478f6 100644 --- a/internal/api/admin_duplicates.go +++ b/internal/api/admin_duplicates.go @@ -18,7 +18,7 @@ import ( // duplicateMemberView is one copy in a proposed duplicate group. LikeCount and // PlayCount span every user: the report is admin-only, and what a copy carries -// is the fact the operator weighs when choosing which to keep. +// is the fact an admin weighs when choosing which to keep. type duplicateMemberView struct { TrackID string `json:"track_id"` Title string `json:"title"` @@ -36,13 +36,13 @@ type duplicateMemberView struct { DiscNumber *int32 `json:"disc_number"` TrackNumber *int32 `json:"track_number"` // StreamURL lets the review play each copy, to hear whether they are the - // same song (#6168). + // same song. StreamURL string `json:"stream_url"` } // duplicateGroupView is one proposal. SurvivorTrackID and SurvivorReason are // the copy the report proposes keeping and the rule that chose it -// (library.ProposeSurvivor) — a default the merge (#3911) lets the operator +// (library.ProposeSurvivor) — a default the merge lets an admin // override. type duplicateGroupView struct { ID string `json:"id"` @@ -52,7 +52,7 @@ type duplicateGroupView struct { SurvivorTrackID string `json:"survivor_track_id"` SurvivorReason string `json:"survivor_reason"` // Class is the resolver's verdict (library.DuplicateClass), null until its - // first pass; ResolveNote is why it left the group for the operator. + // first pass; ResolveNote is why it left the group for an admin. Class *string `json:"class"` ResolveNote *string `json:"resolve_note"` Members []duplicateMemberView `json:"members"` @@ -78,8 +78,8 @@ type duplicateViewCounts struct { Resolved int64 `json:"resolved"` } -// Report views (M498). cross_release groups are the same song on different -// releases, kept on purpose; everything else pending needs the operator. +// Report views. cross_release groups are the same song on different +// releases, kept on purpose; everything else pending needs an admin. const ( duplicateViewReview = "review" duplicateViewCrossRelease = "cross_release" @@ -97,12 +97,13 @@ type adminDuplicatesResponse struct { Groups []duplicateGroupView `json:"groups"` } -// handleListDuplicates implements GET /api/admin/library/duplicates (#3912). -// ?view=review (the default) or cross_release picks the tab (M498). +// handleListDuplicates implements GET /api/admin/library/duplicates. +// ?view=review (the default) or cross_release picks the tab. // // Read-only. The sweep's state and the fingerprint backfill's progress travel // with the groups because an empty report means three different things — still -// fingerprinting, never swept, or swept and clean — and the page has to say which. +// fingerprinting, never swept, or swept and clean — and the page has to say +// which. func (h *handlers) handleListDuplicates(w http.ResponseWriter, r *http.Request) { limit, offset, err := parsePaging(r.URL.Query()) if err != nil { @@ -203,7 +204,7 @@ type resolvedActivityResponse struct { } // handleListResolvedDuplicates implements GET -// /api/admin/library/duplicates/resolved (M498): what the resolver did by +// /api/admin/library/duplicates/resolved: what the resolver did by // itself, newest first, read from the audit log so it outlives the groups. func (h *handlers) handleListResolvedDuplicates(w http.ResponseWriter, r *http.Request) { limit, offset, err := parsePaging(r.URL.Query()) @@ -332,7 +333,7 @@ func (h *handlers) handleRunDuplicateSweep(w http.ResponseWriter, _ *http.Reques // handleDismissDuplicateGroup implements POST // /api/admin/library/duplicates/{id}/dismiss: "these are not duplicates". The // sweep keeps the dismissal and will not propose that set of tracks again. On a -// cross-release group it also undoes the song link (M498 #5438): the copies +// cross-release group it also undoes the song link: the copies // stop sharing likes from here on. 404 duplicate_group_not_pending when the // group was already resolved or is gone. func (h *handlers) handleDismissDuplicateGroup(w http.ResponseWriter, r *http.Request) { @@ -380,7 +381,7 @@ type mergeDuplicateRequest struct { } // mergeDuplicateResponse reports what the merge removed. RemovedPaths are files -// deleted from disk; the operator reads them to know exactly what went. +// deleted from disk; an admin reads them to know exactly what went. type mergeDuplicateResponse struct { SurvivorTrackID string `json:"survivor_track_id"` RemovedPaths []string `json:"removed_paths"` @@ -389,9 +390,10 @@ type mergeDuplicateResponse struct { // mergeRequestBodyLimit bounds the request body. It holds one id. const mergeRequestBodyLimit = 1 << 16 -// handleMergeDuplicateGroup implements POST /api/admin/library/duplicates/{id}/merge -// (#3911): keep one copy, move the others' likes, plays and playlist entries onto -// it, and delete the others' files and rows. +// handleMergeDuplicateGroup implements POST +// /api/admin/library/duplicates/{id}/merge: keep one copy, move the others' +// likes, plays and playlist entries onto it, and delete the others' files and +// rows. // // Errors: // - 409 library_not_writable / 500 file_delete_failed when a file could not be @@ -450,7 +452,7 @@ func (h *handlers) handleMergeDuplicateGroup(w http.ResponseWriter, r *http.Requ writeJSON(w, http.StatusOK, resp) } -// refetchDuplicateRequest is the operator's answer for one recording filling +// refetchDuplicateRequest is an admin's answer for one recording filling // several of an album's slots. keep_track_id is the copy whose title the // recording is; empty means they cannot say, and every copy goes. type refetchDuplicateRequest struct { @@ -466,9 +468,9 @@ type refetchDuplicateResponse struct { } // handleRefetchDuplicateGroup implements POST -// /api/admin/library/duplicates/{id}/refetch (#6184): settle a group of one -// recording in several slots of an album the way the resolver would, with the -// operator's answer. The copies in slots that are not the recording's are +// /api/admin/library/duplicates/{id}/refetch: settle a group of one +// recording in several slots of an album the way the resolver would, with an +// admin's answer. The copies in slots that are not the recording's are // removed and marked missing, the release they came from is set aside, and the // album is wanted again. Merge is the wrong action there: it keeps one title // whether or not it is the audio's, and fetches nothing. diff --git a/internal/api/admin_duplicates_test.go b/internal/api/admin_duplicates_test.go index 74c2c00f..ba869714 100644 --- a/internal/api/admin_duplicates_test.go +++ b/internal/api/admin_duplicates_test.go @@ -64,7 +64,7 @@ func TestFoldDuplicateGroups(t *testing.T) { } } -// M498: the proposal shown beside a group follows the resolver's rule. The copy +// the proposal shown beside a group follows the resolver's rule. The copy // whose tags fit the album is proposed even when the other is larger (only a // lossless copy outranks it). func TestFoldDuplicateGroups_ProposesTheCopyWhoseTagsFit(t *testing.T) { diff --git a/internal/api/admin_fingerprint_settings.go b/internal/api/admin_fingerprint_settings.go index d21bbbc0..cf27c54e 100644 --- a/internal/api/admin_fingerprint_settings.go +++ b/internal/api/admin_fingerprint_settings.go @@ -10,15 +10,15 @@ import ( ) // fingerprintSettingsBody is the wire shape for GET and PUT -// /api/admin/library/fingerprint-settings (M400 #3913). The threshold travels as -// the bit-error rate the matcher uses; the card presents it as a match percentage. +// /api/admin/library/fingerprint-settings. The threshold travels as the +// bit-error rate the matcher uses; the card presents it as a match percentage. type fingerprintSettingsBody struct { Enabled bool `json:"enabled"` ChromaprintLengthSec int32 `json:"chromaprint_length_sec"` AcousticMaxBitErrorRate float64 `json:"acoustic_max_bit_error_rate"` BackfillConcurrency int32 `json:"backfill_concurrency"` SweepIntervalHours int32 `json:"sweep_interval_hours"` - // AutoResolve is a pointer so a client that predates it (M498) and leaves + // AutoResolve is a pointer so a client that predates it and leaves // it out keeps the saved value rather than switching the resolver off. AutoResolve *bool `json:"auto_resolve"` } @@ -34,16 +34,18 @@ func fingerprintSettingsBodyOf(s library.FingerprintSettings) fingerprintSetting } } -// handleGetFingerprintSettings implements GET /api/admin/library/fingerprint-settings. +// handleGetFingerprintSettings implements GET +// /api/admin/library/fingerprint-settings. func (h *handlers) handleGetFingerprintSettings(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, fingerprintSettingsBodyOf(h.fingerprintSettings.Get())) } -// handleUpdateFingerprintSettings implements PUT /api/admin/library/fingerprint-settings. +// handleUpdateFingerprintSettings implements PUT +// /api/admin/library/fingerprint-settings. // -// A whole-row write. A body that leaves a field out decodes it as zero, which no -// field accepts, so a partial save is refused rather than zeroing what it omitted. -// auto_resolve is the exception: left out, it keeps its saved value. +// A whole-row write. A body that leaves a field out decodes it as zero, which +// no field accepts, so a partial save is refused rather than zeroing what it +// omitted. auto_resolve is the exception: left out, it keeps its saved value. // The saved settings reach the scanner and both workers at once: they share the // service instance. func (h *handlers) handleUpdateFingerprintSettings(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/admin_import.go b/internal/api/admin_import.go index 3535ab84..207283ef 100644 --- a/internal/api/admin_import.go +++ b/internal/api/admin_import.go @@ -13,7 +13,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/lifecycle" ) -// Downloads and their imports (M502 #5522): the Wanted view's Activity tab, +// Downloads and their imports: the Wanted view's Activity tab, // a person's "import anyway", "refuse" and "retry", and the Blocked tab. // downloadView is one grab and what its import made of it. @@ -41,7 +41,7 @@ type downloadView struct { Files []string `json:"files"` Skipped int `json:"skipped"` RehomedFrom string `json:"rehomed_from,omitempty"` - // EndDetail says why an ended download ended (#5523). + // EndDetail says why an ended download ended. EndDetail string `json:"end_detail"` // Seeding: imported and still sharing; Ratio its share ratio as last // read; SeedReleasedAt when it was removed once it had shared enough. @@ -113,7 +113,7 @@ type downloadsPage struct { Items []downloadView `json:"items"` Limit int `json:"limit"` Offset int `json:"offset"` - // ClientsDown are the download clients not answering now (#5523). + // ClientsDown are the download clients not answering now. ClientsDown []clientDownView `json:"clients_down"` } @@ -239,7 +239,7 @@ type releaseBlockView struct { Detail string `json:"detail"` BlockedAt string `json:"blocked_at"` // ExpiresAt is when a release given up on may be taken again; null is - // for good (#5523). + // for good. ExpiresAt *string `json:"expires_at"` WantTitle string `json:"want_title"` WantArtist string `json:"want_artist"` diff --git a/internal/api/admin_library_folders.go b/internal/api/admin_library_folders.go index a70ccf66..0c2a4745 100644 --- a/internal/api/admin_library_folders.go +++ b/internal/api/admin_library_folders.go @@ -11,7 +11,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) -// The library's folders (#6129): where the scan, the watcher and the import +// The library's folders: where the scan, the watcher and the import // look for music, set here rather than only by MINSTREL_LIBRARY_SCAN_PATHS. type libraryFolderView struct { diff --git a/internal/api/admin_library_missing.go b/internal/api/admin_library_missing.go index 26d2f0df..30a3a4a8 100644 --- a/internal/api/admin_library_missing.go +++ b/internal/api/admin_library_missing.go @@ -29,12 +29,12 @@ type missingTrackView struct { LastPlayedAt *string `json:"last_played_at"` } -// reacquisitionStateView is what the wanted list says about one album -// (milestone #290, M502 #5520): when it was wanted back, and when the library +// reacquisitionStateView is what the wanted list says about one album: when +// it was wanted back, and when the library // held it again. A missing album with no want yet is still inside the grace // window, or MusicBrainz cannot name it. Want is that want as the wanted list // sends it, so the Missing page can show its search and download and act on -// it (M504). +// it. type reacquisitionStateView struct { WantedAt *string `json:"wanted_at"` HeldAt *string `json:"held_at"` @@ -43,11 +43,11 @@ type reacquisitionStateView struct { // missingGroupView is a directory's worth of missing tracks. // -// Grouping is the whole ergonomic argument for this surface. The case that -// produced #2523 was three reorganised albums showing up as ~40 individually -// missing files; presented flat that reads as forty problems, presented by -// folder it reads as three. MissingSince is the EARLIEST mark in the group, -// so a directory sorts and reads by when it first went away. +// Grouping is the whole ergonomic argument for this surface: a reorganised +// album shows up as dozens of individually missing files, which presented flat +// reads as dozens of problems and presented by folder reads as one. +// MissingSince is the EARLIEST mark in the group, so a directory sorts and +// reads by when it first went away. type missingGroupView struct { Directory string `json:"directory"` MissingSince string `json:"missing_since"` @@ -72,8 +72,8 @@ type adminMissingResponse struct { // // Read-only by design. Nothing on this surface deletes a track: a missing file // keeps its row, its history and its likes because it may come back, and if it -// comes back renamed the scanner adopts it (#2528). The surface exists so an -// operator can SEE what the library has lost and act on it deliberately. +// comes back renamed the scanner adopts it. The surface exists so an admin +// can SEE what the library has lost and act on it deliberately. func (h *handlers) handleListMissingTracks(w http.ResponseWriter, r *http.Request) { limit, offset, err := parsePaging(r.URL.Query()) if err != nil { @@ -215,7 +215,7 @@ func (h *handlers) attachReacquisitionState( continue } // Held before its files went (every library album is on the list, - // held, since #5567): not wanted back yet, which the sweep does once + // held): not wanted back yet, which the sweep does once // the grace window has passed. if w.FulfilledAt.Valid && !w.FulfilledAt.Time.After(dirLost[groups[i].Directory]) { continue diff --git a/internal/api/admin_library_missing_wants_test.go b/internal/api/admin_library_missing_wants_test.go index 35a4d621..6710580d 100644 --- a/internal/api/admin_library_missing_wants_test.go +++ b/internal/api/admin_library_missing_wants_test.go @@ -38,8 +38,8 @@ func seedMissingWant(t *testing.T, h *handlers, trackID, albumID pgtype.UUID, gr } // A folder's album that was held before its files went is not wanted back -// yet (every library album is on the list, held, since #5567); an open want -// comes with its search, so the Missing page can show it and act on it (M504). +// yet (every library album is on the list, held); an open want comes with +// its search, so the Missing page can show it and act on it. func TestListMissingTracks_WantState(t *testing.T) { h, pool := testHandlers(t) truncateLibrary(t, pool) diff --git a/internal/api/admin_loudness.go b/internal/api/admin_loudness.go index 3df88ffc..2e022651 100644 --- a/internal/api/admin_loudness.go +++ b/internal/api/admin_loudness.go @@ -9,9 +9,9 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) -// loudnessCoverageResp is the wire shape for GET /api/admin/library/loudness -// (M464 #4995). measured + silent + unreadable + pending = total; missing -// tracks are not counted. Enabled travels with the counts because with +// loudnessCoverageResp is the wire shape for GET /api/admin/library/loudness. +// measured + silent + unreadable + pending = total; missing tracks are not +// counted. Enabled travels with the counts because with // analysis off, pending never shrinks, and a gauge implying progress would be // promising work nothing is doing. type loudnessCoverageResp struct { @@ -57,14 +57,15 @@ func loudnessSettingsBodyOf(s library.LoudnessSettings) loudnessSettingsBody { } } -// handleGetLoudnessSettings implements GET /api/admin/library/loudness-settings. +// handleGetLoudnessSettings implements GET +// /api/admin/library/loudness-settings. func (h *handlers) handleGetLoudnessSettings(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, loudnessSettingsBodyOf(h.loudnessSettings.Get())) } -// handleUpdateLoudnessSettings implements PUT /api/admin/library/loudness-settings. -// A whole-row write: a body that leaves out the concurrency decodes it as zero, -// which is refused rather than saved. +// handleUpdateLoudnessSettings implements PUT +// /api/admin/library/loudness-settings. A whole-row write: a body that leaves +// out the concurrency decodes it as zero, which is refused rather than saved. func (h *handlers) handleUpdateLoudnessSettings(w http.ResponseWriter, r *http.Request) { var req loudnessSettingsBody if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/internal/api/admin_network.go b/internal/api/admin_network.go index e2e2a4be..b37d73d3 100644 --- a/internal/api/admin_network.go +++ b/internal/api/admin_network.go @@ -14,7 +14,7 @@ type networkSettingsResp struct { TrustedProxyHops int `json:"trusted_proxy_hops"` MaxHops int `json:"max_hops"` // DetectedClientIP is what the CURRENT setting resolves this very request - // to. It's the difference between a number the operator has to reason + // to. It's the difference between a number an admin has to reason // about and one they can verify: set the value, reload, and check the // address matches the machine you're sitting at. DetectedClientIP string `json:"detected_client_ip"` @@ -79,7 +79,7 @@ func (h *handlers) handleUpdateNetworkSettings(w http.ResponseWriter, r *http.Re } // Echo the payload recomputed under the NEW value, so the card can show // immediately what the change did to this request's own address rather - // than making the operator reload to find out. + // than making an admin reload to find out. writeJSON(w, http.StatusOK, h.networkSettingsPayload(r)) } diff --git a/internal/api/admin_notification_email.go b/internal/api/admin_notification_email.go index c4916856..3e750e5e 100644 --- a/internal/api/admin_notification_email.go +++ b/internal/api/admin_notification_email.go @@ -11,7 +11,7 @@ import ( ) // notificationEmailBody is the wire shape for GET and PUT -// /api/admin/notification-email (M489 #5346): when the grouped emails go out. +// /api/admin/notification-email: when the grouped emails go out. type notificationEmailBody struct { SummaryHour int32 `json:"summary_hour"` BatchWindowMinutes int32 `json:"batch_window_minutes"` diff --git a/internal/api/admin_quarantine.go b/internal/api/admin_quarantine.go index 6949d234..07ab3202 100644 --- a/internal/api/admin_quarantine.go +++ b/internal/api/admin_quarantine.go @@ -85,7 +85,8 @@ type actionResultView struct { AffectedUsers int32 `json:"affected_users"` } -// handleResolveQuarantine implements POST /api/admin/quarantine/{track_id}/resolve. +// handleResolveQuarantine implements POST +// /api/admin/quarantine/{track_id}/resolve. func (h *handlers) handleResolveQuarantine(w http.ResponseWriter, r *http.Request) { admin, ok := auth.UserFromContext(r.Context()) if !ok { @@ -115,7 +116,8 @@ func (h *handlers) handleResolveQuarantine(w http.ResponseWriter, r *http.Reques }) } -// handleDeleteQuarantineFile implements POST /api/admin/quarantine/{track_id}/delete-file. +// handleDeleteQuarantineFile implements POST +// /api/admin/quarantine/{track_id}/delete-file. func (h *handlers) handleDeleteQuarantineFile(w http.ResponseWriter, r *http.Request) { admin, ok := auth.UserFromContext(r.Context()) if !ok { @@ -129,8 +131,8 @@ func (h *handlers) handleDeleteQuarantineFile(w http.ResponseWriter, r *http.Req } action, err := h.quarantine.DeleteFile(r.Context(), id, admin.ID) if err != nil { - // Written in the enveloped shape, not writeAdminJSONErr's bare code: the - // message is the part that tells the operator which directory and uid. + // Written in the enveloped shape, not writeAdminJSONErr's bare code: + // the message is the part that tells an admin which directory and uid. if apiErr, ok := fileRemoveAPIError(err); ok { logFileRemoveFailure(h.logger, apiErr, "track_id", uuidToString(id)) writeErr(w, apiErr) diff --git a/internal/api/admin_reacquisition.go b/internal/api/admin_reacquisition.go index 7fe1cc57..01d841c1 100644 --- a/internal/api/admin_reacquisition.go +++ b/internal/api/admin_reacquisition.go @@ -10,12 +10,12 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/reacquisition" ) -// reacquisitionSettingsResp is the admin card's payload (milestone #290). +// reacquisitionSettingsResp is the admin card's payload. // // Carries more than the stored settings: [UnnameableAlbums] is the count of // albums with missing files that can never be wanted automatically because // neither they nor their artist has an MBID. The card states that number -// rather than leaving the operator to wonder why some rows never get wanted — +// rather than leaving an admin to wonder why some rows never get wanted — // a feature that silently does nothing for part of its input reads as broken. type reacquisitionSettingsResp struct { Enabled bool `json:"enabled"` @@ -25,12 +25,14 @@ type reacquisitionSettingsResp struct { UnnameableAlbums int64 `json:"unnameable_albums"` } -// handleGetReacquisitionSettings implements GET /api/admin/library/reacquisition. +// handleGetReacquisitionSettings implements GET +// /api/admin/library/reacquisition. func (h *handlers) handleGetReacquisitionSettings(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, h.reacquisitionPayload(r)) } -// handleUpdateReacquisitionSettings implements PUT /api/admin/library/reacquisition. +// handleUpdateReacquisitionSettings implements PUT +// /api/admin/library/reacquisition. func (h *handlers) handleUpdateReacquisitionSettings(w http.ResponseWriter, r *http.Request) { var req reacquisitionSettingsResp if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/internal/api/admin_recommendation_trends.go b/internal/api/admin_recommendation_trends.go index d9e8922c..4d306b9c 100644 --- a/internal/api/admin_recommendation_trends.go +++ b/internal/api/admin_recommendation_trends.go @@ -1,6 +1,6 @@ -// Admin recommendation-trends endpoint (#1251): weekly per-surface -// outcome series with tuning-audit markers — the verify half of the -// tune→verify loop the tuning lab (#1250) opens. Aggregated across +// Admin recommendation-trends endpoint: weekly per-surface outcome +// series with tuning-audit markers — the verify half of the tune→verify +// loop the tuning card opens. Aggregated across // all users because the knobs are global; rows carry rates only. package api diff --git a/internal/api/admin_recommendation_tuning.go b/internal/api/admin_recommendation_tuning.go index 2d52633e..d5e69799 100644 --- a/internal/api/admin_recommendation_tuning.go +++ b/internal/api/admin_recommendation_tuning.go @@ -1,8 +1,8 @@ -// Admin recommendation-tuning endpoints (#1250): the defaults- -// discovery lab. GET returns current values + shipped defaults for +// Admin recommendation-tuning endpoints: the lab for finding good +// defaults. GET returns current values + shipped defaults for // every scope; PATCH applies a partial update to one scope; reset // restores a scope to shipped defaults. Every change writes an audit -// row (consumed by the metrics trend view, #1251). +// row (consumed by the metrics trend view). package api import ( @@ -67,7 +67,7 @@ func tasteRespFrom(t recsettings.TasteTuning) tasteTuningResp { } } -// discoverTuningResp is the Discover scope on the wire (#2377). +// discoverTuningResp is the Discover scope on the wire. type discoverTuningResp struct { TagOverlapWeight float64 `json:"tag_overlap_weight"` SnoozeDays float64 `json:"snooze_days"` @@ -113,7 +113,8 @@ func (h *handlers) tuningSnapshot() tuningSnapshot { return out } -// handleGetRecommendationTuning implements GET /api/admin/recommendation-tuning. +// handleGetRecommendationTuning implements GET +// /api/admin/recommendation-tuning. func (h *handlers) handleGetRecommendationTuning(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, h.tuningSnapshot()) } diff --git a/internal/api/admin_requests.go b/internal/api/admin_requests.go index ae7f5b99..4fd395eb 100644 --- a/internal/api/admin_requests.go +++ b/internal/api/admin_requests.go @@ -90,15 +90,15 @@ func requesterNames(ctx context.Context, q *dbq.Queries, rows []dbq.Request) map return names } -// approveRequestBody is the optional JSON body for POST /api/admin/requests/:id/approve. -// profile_id picks the release-type profile an artist request is followed -// on; omitted, the default profile. +// approveRequestBody is the optional JSON body for POST +// /api/admin/requests/:id/approve. profile_id picks the release-type profile an +// artist request is followed on; omitted, the default profile. type approveRequestBody struct { ProfileID string `json:"profile_id"` } // handleApproveRequest implements POST /api/admin/requests/:id/approve. The -// request goes on the wanted list (M502 #5520): an album or track request +// request goes on the wanted list: an album or track request // wants its album, an artist request follows the artist. func (h *handlers) handleApproveRequest(w http.ResponseWriter, r *http.Request) { admin, ok := auth.UserFromContext(r.Context()) @@ -147,7 +147,8 @@ func (h *handlers) handleApproveRequest(w http.ResponseWriter, r *http.Request) writeJSON(w, http.StatusOK, requestViewFrom(row)) } -// rejectRequestBody is the optional JSON body for POST /api/admin/requests/:id/reject. +// rejectRequestBody is the optional JSON body for POST +// /api/admin/requests/:id/reject. type rejectRequestBody struct { Notes string `json:"notes"` } diff --git a/internal/api/admin_requests_test.go b/internal/api/admin_requests_test.go index 780f7cc1..734eadfc 100644 --- a/internal/api/admin_requests_test.go +++ b/internal/api/admin_requests_test.go @@ -55,8 +55,9 @@ func seedPendingArtistRequest(t *testing.T, h *handlers, user dbq.User, artistMB return createArtistRequest(t, h, user, artistMBID, artistName) } -// TestHandleListAdminRequests_DefaultStatus_Pending seeds 2 pending + 1 rejected -// and verifies that GET without ?status returns only the 2 pending rows. +// TestHandleListAdminRequests_DefaultStatus_Pending seeds 2 pending + 1 +// rejected and verifies that GET without ?status returns only the 2 pending +// rows. func TestHandleListAdminRequests_DefaultStatus_Pending(t *testing.T) { h, _ := testHandlers(t) resetRequests(t, h) @@ -96,8 +97,8 @@ func TestHandleListAdminRequests_DefaultStatus_Pending(t *testing.T) { } } -// TestHandleListAdminRequests_StatusFilter verifies GET ?status=rejected returns -// only rejected rows. +// TestHandleListAdminRequests_StatusFilter verifies GET ?status=rejected +// returns only rejected rows. func TestHandleListAdminRequests_StatusFilter(t *testing.T) { h, _ := testHandlers(t) resetRequests(t, h) diff --git a/internal/api/admin_scan.go b/internal/api/admin_scan.go index c2b5854e..93e3bf63 100644 --- a/internal/api/admin_scan.go +++ b/internal/api/admin_scan.go @@ -76,8 +76,8 @@ type scanTriggerResp struct { // handleTriggerScan implements POST /api/admin/scan/run. // Returns 202 on success, 409 if a scan is already in flight (and not stale), // 500 on DB errors. The lazy reaper inside library.TryStartScan handles -// stale (>1h) in-flight rows automatically — the operator no longer needs -// to manually clear stuck rows after a server crash. +// stale (>1h) in-flight rows automatically, so a row left stuck by a +// server crash needs no manual clearing. func (h *handlers) handleTriggerScan(w http.ResponseWriter, _ *http.Request) { bgCtx := context.Background() started, existing, err := library.TryStartScan( diff --git a/internal/api/admin_search.go b/internal/api/admin_search.go index 4d7488ad..aaebdb50 100644 --- a/internal/api/admin_search.go +++ b/internal/api/admin_search.go @@ -18,7 +18,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/wanted" ) -// Searching for wants and grabbing releases (M502 #5521): the schedule's +// Searching for wants and grabbing releases: the schedule's // settings, each want's candidates, and a person's Search, Grab and "never // this one". @@ -33,17 +33,17 @@ type searchSettingsView struct { Tiers []release.TierInfo `json:"tiers"` LastPassAt *string `json:"last_pass_at"` LastPass json.RawMessage `json:"last_pass"` - // ImportRoot is the library folder new music is filed into (#5522); "" + // ImportRoot is the library folder new music is filed into; "" // is the first of ScanPaths, the library's folders. ImportRoot string `json:"import_root"` ScanPaths []string `json:"scan_paths"` - // The download lifecycle (#5523): seed an imported download to this + // The download lifecycle: seed an imported download to this // ratio or for this many days; give up on one nobody shares after this // many hours. 0 is: remove once imported, no time limit, never. SeedRatio float32 `json:"seed_ratio"` SeedDays int32 `json:"seed_days"` StallHours int32 `json:"stall_hours"` - // UpgradeUntil is the tier a held album is upgraded to (#5524); "" is + // UpgradeUntil is the tier a held album is upgraded to; "" is // no quality upgrades. UpgradeUntil string `json:"upgrade_until"` } diff --git a/internal/api/admin_status.go b/internal/api/admin_status.go index 76f73fbd..17c6f8fa 100644 --- a/internal/api/admin_status.go +++ b/internal/api/admin_status.go @@ -10,7 +10,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/logging" ) -// The admin Overview's status sections (M504): after the verdict, who is +// The admin Overview's status sections: after the verdict, who is // listening, what is downloading, what the library holds and how much room // it has, and how the server itself is doing. One read for the page, polled // while it is open. diff --git a/internal/api/admin_status_test.go b/internal/api/admin_status_test.go index 8ead2f1c..4e2bcc07 100644 --- a/internal/api/admin_status_test.go +++ b/internal/api/admin_status_test.go @@ -31,7 +31,7 @@ func openPlay(t *testing.T, h *handlers, user, track pgtype.UUID, ago string) { } } -// The Overview's status (M504): a play still inside its track's length is +// The Overview's status: a play still inside its track's length is // listening now; one long past it, never closed by its client, is not. func TestAdminStatus_ListeningAndServer(t *testing.T) { h, pool := testHandlers(t) diff --git a/internal/api/admin_summary.go b/internal/api/admin_summary.go index f04aac48..9a870497 100644 --- a/internal/api/admin_summary.go +++ b/internal/api/admin_summary.go @@ -15,9 +15,9 @@ type adminQueueView struct { Since *string `json:"since"` } -// adminSummaryView is what the admin rail and the Overview's verdict read -// (#5605): every queue an admin acts on, in one response, so each admin page -// pays one cheap read for its rail. +// adminSummaryView is what the admin rail and the Overview's verdict read: +// every queue an admin acts on, in one response, so each admin page pays one +// cheap read for its rail. type adminSummaryView struct { Requests adminQueueView `json:"requests"` Reports adminQueueView `json:"reports"` diff --git a/internal/api/admin_summary_test.go b/internal/api/admin_summary_test.go index 0d1abc3c..f6970512 100644 --- a/internal/api/admin_summary_test.go +++ b/internal/api/admin_summary_test.go @@ -32,7 +32,7 @@ func getAdminSummary(t *testing.T, h *handlers, user dbq.User) (int, adminSummar } // The summary counts each queue an admin acts on and says since when the -// oldest item has waited; an empty queue has no since (#5605). +// oldest item has waited; an empty queue has no since. func TestAdminSummary_CountsTheQueuesAndSinceWhen(t *testing.T) { h, _ := testHandlers(t) admin := seedUser(t, h.pool, "admin", "pw", true) @@ -64,8 +64,7 @@ func TestAdminSummary_CountsTheQueuesAndSinceWhen(t *testing.T) { } // Duplicates count what needs review: a cross-release group is kept and -// linked by the resolver and waits on nobody (operator, 2026-10-19: the -// headline said 3,479 when 536 needed review). +// linked by the resolver and waits on nobody. func TestAdminSummary_DuplicatesCountOnlyWhatNeedsReview(t *testing.T) { h, _ := testHandlers(t) truncateLibrary(t, h.pool) diff --git a/internal/api/admin_suspect_sources.go b/internal/api/admin_suspect_sources.go index 30125395..8e5a0e90 100644 --- a/internal/api/admin_suspect_sources.go +++ b/internal/api/admin_suspect_sources.go @@ -12,7 +12,7 @@ import ( ) // suspectTrackView is one flagged track. FilePath is the evidence: the -// markers are read from its basename, and the operator needs to see it to +// markers are read from its basename, and an admin needs to see it to // judge whether the flag is right. type suspectTrackView struct { TrackID string `json:"track_id"` @@ -27,11 +27,11 @@ type suspectTrackView struct { TrackNumber *int32 `json:"track_number"` Markers []string `json:"markers"` // Verdict is "suspect" while the track is held back from radio and the - // mixes, "fine" once the operator let it back, null before the resolver's - // first pass (M498 #5439). VerdictAt is when it was last set. + // mixes, "fine" once an admin let it back, null before the resolver's + // first pass. VerdictAt is when it was last set. Verdict *string `json:"verdict"` VerdictAt *string `json:"verdict_at"` - // Replacing: the album is on the wanted list for a clean copy (#5524); + // Replacing: the album is on the wanted list for a clean copy; // SearchedAt is when one was last searched for, null before the first. Replacing bool `json:"replacing"` SearchedAt *string `json:"searched_at"` @@ -57,8 +57,8 @@ type adminSuspectSourcesResponse struct { // // A marker is a reason to look, not proof: a band can title a song "Music // Video". The duplicate resolver holds a flagged track back from radio and the -// mixes and merges it away where a clean copy exists (M498 #5439); this report -// is where the operator sees that and can say a track is fine. +// mixes and merges it away where a clean copy exists; this report +// is where an admin sees that and can say a track is fine. func (h *handlers) handleListSuspectSources(w http.ResponseWriter, r *http.Request) { limit, offset, err := parsePaging(r.URL.Query()) if err != nil { diff --git a/internal/api/admin_tag_sources.go b/internal/api/admin_tag_sources.go index cc9b534b..412a1a42 100644 --- a/internal/api/admin_tag_sources.go +++ b/internal/api/admin_tag_sources.go @@ -15,7 +15,7 @@ import ( // tagSourceResp is the wire shape for one tag-enrichment provider in the // GET /api/admin/tag-sources list. Mirrors the cover-sources admin surface // (admin_cover_sources.go) — a separate, independent settings card so a new -// tag source is added without touching art settings (#1490). +// tag source is added without touching art settings. type tagSourceResp struct { ID string `json:"id"` DisplayName string `json:"display_name"` @@ -114,7 +114,8 @@ type testTagSourceResp struct { Error string `json:"error,omitempty"` } -// handleTestTagSource implements POST /api/admin/tag-sources/{provider_id}/test. +// handleTestTagSource implements POST +// /api/admin/tag-sources/{provider_id}/test. func (h *handlers) handleTestTagSource(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "provider_id") diff --git a/internal/api/admin_tracks.go b/internal/api/admin_tracks.go index 4d312ab7..e944a257 100644 --- a/internal/api/admin_tracks.go +++ b/internal/api/admin_tracks.go @@ -10,7 +10,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/tracks" ) -// removeTrackResponse is the wire shape from spec §5 (M7 #372). The two +// removeTrackResponse is DELETE /api/admin/tracks/{id}'s answer. The two // optional fields are omitted unless the corresponding cleanup actually // happened, so the response stays minimal in the common case. type removeTrackResponse struct { @@ -21,9 +21,9 @@ type removeTrackResponse struct { // handleRemoveTrack implements DELETE /api/admin/tracks/{id}. // -// Admin-only (gated by auth.RequireAdmin on the /admin route group). Deletes the -// file, then the DB row, and runs the album/artist cascade tidy-up — and deletes -// nothing at all when the file cannot be removed (#3918). +// Admin-only (gated by auth.RequireAdmin on the /admin route group). Deletes +// the file, then the DB row, and runs the album/artist cascade tidy-up — and +// deletes nothing at all when the file cannot be removed. // // The codes this handler emits are not_found, // library_not_writable (409) and file_delete_failed when the file could not be diff --git a/internal/api/admin_tracks_test.go b/internal/api/admin_tracks_test.go index 7ee49da0..0dd9a4cb 100644 --- a/internal/api/admin_tracks_test.go +++ b/internal/api/admin_tracks_test.go @@ -107,10 +107,8 @@ func seedTrackForRemoveTest(t *testing.T, h *handlers, unique, trackMbid, albumM } // TestHandleRemoveTrack_NonAdmin403 verifies the RequireAdmin middleware -// rejects non-admin callers. Asserts the existing middleware shape -// ({"error":"not_authorized"}) — spec §5 says `unauthorized`, but the -// shared middleware emits not_authorized today, and this handler doesn't -// own that envelope. +// rejects non-admin callers, with the shared middleware's +// {"error":"not_authorized"} envelope, which this handler doesn't own. func TestHandleRemoveTrack_NonAdmin403(t *testing.T) { h, pool := testHandlers(t) truncateLibrary(t, pool) diff --git a/internal/api/admin_wanted.go b/internal/api/admin_wanted.go index 9f2f5ece..0cb28b80 100644 --- a/internal/api/admin_wanted.go +++ b/internal/api/admin_wanted.go @@ -19,7 +19,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/wanted" ) -// The wanted list's admin surface (M502 #5520): wants, follows and +// The wanted list's admin surface: wants, follows and // release-type profiles. // wantView is one row of GET /api/admin/wanted. @@ -40,10 +40,10 @@ type wantView struct { HeldAt *string `json:"held_at"` ImageURL string `json:"image_url"` // Upgrade: why a held album is wanted again, quality or rip, and - // HeldQuality what the library's copy is (#5524). + // HeldQuality what the library's copy is. Upgrade string `json:"upgrade,omitempty"` HeldQuality string `json:"held_quality,omitempty"` - // Search is the schedule's last look; Grab the download running (#5521). + // Search is the schedule's last look; Grab the download running. Search *wantSearchView `json:"search"` Grab *wantGrabView `json:"grab"` } @@ -58,7 +58,7 @@ type wantSearchView struct { } // wantGrabView is the want's latest download, running or ended, and what -// its import made of it (#5522). +// its import made of it. type wantGrabView struct { ID string `json:"id"` Name string `json:"name"` @@ -76,19 +76,19 @@ type wantGrabView struct { } // wantCounts sizes each tab of the Wanted page. Artists: how many artists -// the list holds albums of, its main page (#5572). +// the list holds albums of, its main page. type wantCounts struct { Artists int64 `json:"artists"` - // Unmatched: library albums waiting to be matched by hand (#5579). + // Unmatched: library albums waiting to be matched by hand. Unmatched int64 `json:"unmatched"` Wanted int64 `json:"wanted"` Missing int64 `json:"missing"` Held int64 `json:"held"` - // Upgrades: held albums wanted for a better copy (#5524). + // Upgrades: held albums wanted for a better copy. Upgrades int64 `json:"upgrades"` Following int64 `json:"following"` // Activity: downloads running; Attention: of those, the ones waiting on - // a person; Blocked: releases set aside (#5522). + // a person; Blocked: releases set aside. Activity int64 `json:"activity"` Attention int64 `json:"attention"` Blocked int64 `json:"blocked"` @@ -100,13 +100,13 @@ type wantsPage struct { Limit int `json:"limit"` Offset int `json:"offset"` Counts wantCounts `json:"counts"` - // Library is the one-time listing of the library's albums (#5567). + // Library is the one-time listing of the library's albums. Library libraryListingView `json:"library"` // Artist heads one artist's page, when the list is narrowed to them. Artist *wantArtistView `json:"artist,omitempty"` } -// wantArtistView is one artist on the Wanted view's main page (#5572): how +// wantArtistView is one artist on the Wanted view's main page: how // many of their albums the list holds, of how many. type wantArtistView struct { ArtistMBID string `json:"artist_mbid"` @@ -132,16 +132,15 @@ type wantArtistsPage struct { } // libraryListingView: whether the library's albums are all on the list -// yet, and how many could not be matched to MusicBrainz (#5569); those are -// listed in the Collection's Unmatched section (#5579). +// yet, and how many could not be matched to MusicBrainz; those are +// listed in the Collection's Unmatched section. type libraryListingView struct { ListedAt *string `json:"listed_at"` Unlisted int32 `json:"unlisted"` } -// unmatchedAlbumView is one album in the Collection's Unmatched section -// (#5579): why no MusicBrainz album matched it, or that a person left it -// off. +// unmatchedAlbumView is one album in the Collection's Unmatched section: why +// no MusicBrainz album matched it, or that a person left it off. type unmatchedAlbumView struct { AlbumID string `json:"album_id"` Title string `json:"title"` @@ -200,7 +199,7 @@ func wantArtistViewOf(row dbq.ListWantArtistsRow) wantArtistView { } // handleListWantArtists implements GET /api/admin/wanted/artists?q=&offset=: -// the Wanted view's main page, every artist on the list A–Z (#5572). +// the Wanted view's main page, every artist on the list A–Z. func (h *handlers) handleListWantArtists(w http.ResponseWriter, r *http.Request) { offset := offsetParam(r) q := dbq.New(h.pool) @@ -224,12 +223,12 @@ func (h *handlers) handleListWantArtists(w http.ResponseWriter, r *http.Request) writeJSON(w, http.StatusOK, out) } -// handleListWants implements -// GET /api/admin/wanted?state=wanted|missing|upgrade|held|all&artist=&q=&offset=. -// wanted is everything open but missing files; upgrade the held albums -// wanted for a better copy (#5524). Each list runs artist by -// artist, A–Z, like the library (#5572); q narrows it to an artist or -// album, and artist to one artist's page, which it then heads. +// handleListWants implements GET +// /api/admin/wanted?state=wanted|missing|upgrade|held|all&artist=&q=&offset=. +// wanted is everything open but missing files; upgrade the held albums wanted +// for a better copy. Each list runs artist by artist, A–Z, like the library; q +// narrows it to an artist or album, and artist to one artist's page, which it +// then heads. func (h *handlers) handleListWants(w http.ResponseWriter, r *http.Request) { state := r.URL.Query().Get("state") if state == "" { @@ -329,7 +328,7 @@ func wantViewOf(row dbq.ListWantsRow) wantView { return v } -// wantTracksView is an album's track list for the Wanted view (#5572): the +// wantTracksView is an album's track list for the Wanted view: the // library's own tracks for an album it holds or held, else MusicBrainz's // standard edition of what is wanted. type wantTracksView struct { @@ -687,7 +686,8 @@ func (h *handlers) handleUpdateReleaseProfile(w http.ResponseWriter, r *http.Req }) } -// handleDeleteReleaseProfile implements DELETE /api/admin/release-profiles/{id}. +// handleDeleteReleaseProfile implements DELETE +// /api/admin/release-profiles/{id}. func (h *handlers) handleDeleteReleaseProfile(w http.ResponseWriter, r *http.Request) { id, ok := requireURLUUID(w, r, "id") if !ok { @@ -726,7 +726,7 @@ func deref(s *string) string { } // handleListUnmatched implements GET /api/admin/wanted/unmatched: the -// Collection's Unmatched section (#5579), the albums no MusicBrainz album +// Collection's Unmatched section, the albums no MusicBrainz album // matched and, after them, the ones a person left off. func (h *handlers) handleListUnmatched(w http.ResponseWriter, r *http.Request) { rows, err := dbq.New(h.pool).ListUnmatchedLibraryAlbums(r.Context(), unmatchedShown) @@ -793,8 +793,9 @@ func (h *handlers) handleMatchUnmatched(w http.ResponseWriter, r *http.Request) Agree: m.Agree, Tracks: m.Tracks}) } -// handleLeaveUnmatchedOff implements POST /api/admin/wanted/unmatched/{id}/leave-off, -// and handlePutUnmatchedBack its undoing, POST …/{id}/put-back. +// handleLeaveUnmatchedOff implements POST +// /api/admin/wanted/unmatched/{id}/leave-off, and handlePutUnmatchedBack its +// undoing, POST …/{id}/put-back. func (h *handlers) handleLeaveUnmatchedOff(w http.ResponseWriter, r *http.Request) { h.setUnmatched(w, r, h.wanted.LeaveAlbumOff) } diff --git a/internal/api/admin_wanted_test.go b/internal/api/admin_wanted_test.go index ad6adf3c..e80a2edb 100644 --- a/internal/api/admin_wanted_test.go +++ b/internal/api/admin_wanted_test.go @@ -67,7 +67,7 @@ func listWanted(t *testing.T, h *handlers, user dbq.User, q string) wantsPage { return page } -// The list runs like the library (#5572): artists A–Z by the library's sort +// The list runs like the library: artists A–Z by the library's sort // name where the library holds the artist, each artist's albums oldest // first; q narrows it to an artist or an album. func TestListWants_ArtistsAToZThenTheirAlbumsByDate(t *testing.T) { @@ -109,7 +109,7 @@ func TestListWants_ArtistsAToZThenTheirAlbumsByDate(t *testing.T) { } } -// The Collection's catalogue (#5572): every artist on the list A–Z, each +// The Collection's catalogue: every artist on the list A–Z, each // with how many of their albums are held; an artist's page lists every album // of theirs, held or not, under that artist. func TestListWantArtists_CatalogueAndAnArtistsPage(t *testing.T) { @@ -292,7 +292,7 @@ func TestWantTracks_HeldAlbumShowsTheLibrarysTracks(t *testing.T) { } } -// The Unmatched section (#5579): the albums matching could not place, a +// The Unmatched section: the albums matching could not place, a // local band's album left off and put back, and a link that names no // MusicBrainz album refused before MusicBrainz is asked. func TestUnmatched_ListLeaveOffPutBack(t *testing.T) { diff --git a/internal/api/api.go b/internal/api/api.go index 45d85b2c..bb222fe6 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1,5 +1,5 @@ // Package api implements Minstrel's native JSON surface under /api. It is -// consumed by the built-in web SPA and (eventually) the Flutter client. +// consumed by the built-in web SPA and the Android app. // Subsonic-compatible endpoints under /rest are intentionally separate — // see internal/subsonic — and the two packages must not depend on each other. package api @@ -105,15 +105,13 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev // query token (UPnP / Sonos path; see streamAuthOk). The // middleware attaches user to context when a valid cookie / // bearer is present but does NOT 401 on absence; the handler's - // own streamAuthOk performs the actual auth check. See the - // design at - // docs/superpowers/specs/2026-06-03-android-output-picker-upnp-design.md. + // own streamAuthOk performs the actual auth check. api.With(auth.OptionalUser(pool, logger)).Get("/tracks/{id}/stream", h.handleGetStream) // Extension-bearing alias so Sonos's URL probe can identify the // audio format from the path. The {ext} param is consumed by chi - // and ignored by the handler (which keys off {id}). See task #610. + // and ignored by the handler (which keys off {id}). api.With(auth.OptionalUser(pool, logger)).Get("/tracks/{id}/stream.{ext}", h.handleGetStream) - // The leveled stream for Sonos/UPnP (M464 #5001): session or a + // The leveled stream for Sonos/UPnP: session or a // leveled token, like the plain stream. api.With(auth.OptionalUser(pool, logger)).Get("/tracks/{id}/leveled.flac", h.handleGetLeveledStream) @@ -154,7 +152,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev authed.Get("/albums/{id}/cover", h.handleGetCover) authed.Get("/library/shuffle", h.handleLibraryShuffle) authed.Get("/library/albums", h.handleListLibraryAlbums) - // Browse indexes (#367). Genre filtering rides + // Browse indexes. Genre filtering rides // /library/albums?genre= rather than a path segment, because raw // ID3 genres contain slashes ("Rock/Pop") that a path can't carry. authed.Get("/library/genres", h.handleListGenres) @@ -169,8 +167,8 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev authed.Get("/search", h.handleSearch) authed.Get("/radio", h.handleRadio) authed.Get("/discover/suggestions", h.handleListSuggestions) - // Snooze = "not right now", time-boxed and self-expiring - // (#2374). Not a dislike — see the migration for why. + // Snooze = "not right now", time-boxed and self-expiring. + // Not a dislike — see the migration for why. authed.Post("/discover/suggestions/{mbid}/snooze", h.handleSnoozeSuggestion) authed.Delete("/discover/suggestions/{mbid}/snooze", h.handleUnsnoozeSuggestion) authed.Get("/discover/snoozes", h.handleListSuggestionSnoozes) @@ -178,10 +176,8 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev authed.Get("/home/index", h.handleGetHomeIndex) authed.Post("/events", h.handleEvents) authed.Get("/events/stream", h.handleEventsStream) - // UPnP / Sonos cast slice: issue a short-lived HMAC stream URL - // the speaker can fetch without the user's session. See the - // design at - // docs/superpowers/specs/2026-06-03-android-output-picker-upnp-design.md. + // UPnP / Sonos casting: issue a short-lived HMAC stream URL the + // speaker can fetch without the user's session. authed.Post("/cast/stream-token", h.handleCastStreamToken) authed.Post("/likes/tracks/{id}", h.handleLikeTrack) authed.Delete("/likes/tracks/{id}", h.handleUnlikeTrack) @@ -194,7 +190,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev authed.Get("/likes/artists", h.handleListLikedArtists) authed.Get("/likes/ids", h.handleGetLikedIDs) - // The release catalogue (M502 #5519): request search asks + // The release catalogue: request search asks // MusicBrainz, and art for what isn't held comes through // Minstrel from the enabled cover-art sources. authed.Get("/catalogue/search", h.handleCatalogueSearch) @@ -213,13 +209,13 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev // load failures). Admin-only inbox; any user can report. authed.Post("/playback-errors", h.handleReportPlaybackError) - // Device diagnostics ingest (M9). Any signed-in user can + // Device diagnostics ingest. Any signed-in user can // POST a batch, but events are only stored when the // account's debug_mode_enabled flag is on (handler no-ops // otherwise). Admin views live under /admin/diagnostics. authed.Post("/diagnostics", h.handleReportDiagnostics) - // Self-hosted in-app update channel (#397). Auth-gated to + // Self-hosted in-app update channel. Auth-gated to // prevent anonymous bandwidth abuse on the APK stream; // /apk additionally per-user rate-limited. authed.Get("/client/version", h.handleClientVersion) @@ -236,14 +232,15 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev admin.Delete("/acquisition/services/{code}/api-key", h.handleSetAcquisitionSecret("api_key")) admin.Put("/acquisition/services/{code}/password", h.handleSetAcquisitionSecret("password")) admin.Delete("/acquisition/services/{code}/password", h.handleSetAcquisitionSecret("password")) - // The queues that wait on an admin, for the rail and the Overview (#5605). + // The queues that wait on an admin, for the rail and the + // Overview. admin.Get("/summary", h.handleGetAdminSummary) admin.Get("/status", h.handleGetAdminStatus) admin.Get("/requests", h.handleListAdminRequests) admin.Post("/requests/{id}/approve", h.handleApproveRequest) admin.Post("/requests/{id}/reject", h.handleRejectRequest) - // The wanted list (M502 #5520): what Minstrel is looking + // The wanted list: what Minstrel is looking // for, the artists it follows, and the release-type // profiles that decide which of their releases count. admin.Get("/wanted", h.handleListWants) @@ -253,7 +250,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev admin.Post("/wanted/unmatched/{id}/leave-off", h.handleLeaveUnmatchedOff) admin.Post("/wanted/unmatched/{id}/put-back", h.handlePutUnmatchedBack) admin.Delete("/wanted/{id}", h.handleRemoveWant) - // Searching and grabbing (M502 #5521). + // Searching and grabbing. admin.Get("/acquisition/settings", h.handleGetSearchSettings) admin.Put("/acquisition/settings", h.handlePutSearchSettings) admin.Get("/wanted/{id}/candidates", h.handleListCandidates) @@ -294,12 +291,12 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev admin.Put("/network-settings", h.handleUpdateNetworkSettings) // Policy for wanting back an album whose files went missing - // (#290). Sits beside the missing-files list it governs, - // because the operator meets it on that surface. + // Sits beside the missing-files list it governs, because an + // admin meets it on that surface. admin.Get("/library/reacquisition", h.handleGetReacquisitionSettings) admin.Put("/library/reacquisition", h.handleUpdateReacquisitionSettings) - // Where the library lives (#6129). A changed folder moves its + // Where the library lives. A changed folder moves its // tracks rather than re-adding them. admin.Get("/library/folders", h.handleGetLibraryFolders) admin.Put("/library/folders", h.handlePutLibraryFolders) @@ -309,7 +306,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev // Sits under /library rather than /tracks because what it // reports is a property of the library's relationship to disk, // and because the destructive /tracks/{id} route above must - // not be mistaken for it (#2527). + // not be mistaken for it. admin.Get("/library/missing", h.handleListMissingTracks) admin.Get("/library/suspect-sources", h.handleListSuspectSources) admin.Put("/library/suspect-sources/{id}", h.handleSetSuspectSourceVerdict) @@ -325,9 +322,10 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev admin.Put("/library/acoustid-settings", h.handleUpdateAcoustIDSettings) admin.Post("/library/acoustid/run", h.handleRunAcoustIDLookup) admin.Get("/library/acoustid/unsettled", h.handleListUnsettledAcoustID) - // Duplicates report (#3912): proposals from the duplicate sweep, a - // trigger to sweep now, dismissal, and the merge (#3911), which deletes - // the removed copies' files after moving their history onto the kept one. + // Duplicates report: proposals from the duplicate sweep, a + // trigger to sweep now, dismissal, and the merge, which deletes + // the removed copies' files after moving their history onto the + // kept one. admin.Get("/library/duplicates", h.handleListDuplicates) admin.Get("/library/duplicates/resolved", h.handleListResolvedDuplicates) admin.Post("/library/duplicates/sweep", h.handleRunDuplicateSweep) @@ -346,7 +344,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev admin.Put("/users/{id}/auto-approve", h.handleAdminAutoApproveToggle) admin.Put("/users/{id}/debug-mode", h.handleAdminDebugModeToggle) - // Device diagnostics timeline + device overview (M9). + // Device diagnostics timeline + device overview. admin.Get("/diagnostics", h.handleListAdminDiagnostics) admin.Get("/diagnostics/devices", h.handleListAdminDiagnosticDevices) @@ -366,12 +364,12 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev admin.Get("/notification-email", h.handleGetNotificationEmail) admin.Put("/notification-email", h.handleUpdateNotificationEmail) - // Recommendation tuning lab (#1250): scoring-weight + // Recommendation tuning: scoring-weight // profiles + taste-build knobs, DB-backed, live effect. admin.Get("/recommendation-tuning", h.handleGetRecommendationTuning) admin.Patch("/recommendation-tuning/{scope}", h.handlePatchRecommendationTuning) admin.Post("/recommendation-tuning/{scope}/reset", h.handleResetRecommendationTuning) - // Weekly outcome trends + knob-turn markers (#1251). + // Weekly outcome trends + knob-turn markers. admin.Get("/recommendation-trends", h.handleGetRecommendationTrends) }) @@ -398,7 +396,7 @@ type handlers struct { recSettings *recsettings.Service rng func() float64 // librarySize memoises the track count that sizes the candidate pool - // (#3880). Held here rather than counted per request: the count is a + // Held here rather than counted per request: the count is a // full table scan, and library size only moves when a scan runs. librarySize *recommendation.LibrarySize requests *requests.Service @@ -413,47 +411,47 @@ type handlers struct { dataDir string mailer mailer.Sender eventbus *eventbus.Bus - // notifier writes the notifications inbox (M489). Nil-safe: a nil + // notifier writes the notifications inbox. Nil-safe: a nil // notifier records nothing, which is what most handler tests want. notifier *notifications.Notifier playlistScheduler *playlists.Scheduler // reacqSettings is the DB-backed policy for auto re-acquisition of - // missing files (milestone #290) — grace window, backoff, attempt caps. + // missing files — grace window, backoff, attempt caps. // Cached in the service, so the admin card reads it without a query. reacqSettings *reacquisition.SettingsService - // fingerprintSettings is the fingerprinting policy (M400 #3913), the same + // fingerprintSettings is the fingerprinting policy, the same // instance the scanner and the fingerprint workers read, so a save from the // admin card reaches them without a restart. Nil serves the defaults. fingerprintSettings *library.FingerprintSettingsService - // loudnessSettings is the loudness analysis policy (M464 #4995), the same + // loudnessSettings is the loudness analysis policy, the same // instance the loudness backfill reads. Nil serves the defaults. loudnessSettings *library.LoudnessSettingsService - // acoustIDLookup is the AcoustID lookup worker (M401): its settings, its + // acoustIDLookup is the AcoustID lookup worker: its settings, its // status and "look up now". Nil serves the defaults (off). acoustIDLookup *library.AcoustIDLookupWorker - // acquisition is the operator's indexers and download clients (M502). + // acquisition is the configured indexers and download clients. acquisition *acquire.Store // catalogue is the release catalogue read from MusicBrainz, and - // catalogueArt the images for what isn't held (M502 #5519). + // catalogueArt the images for what isn't held. catalogue *catalogue.Service catalogueArt *catalogue.Art - // wanted is the wanted list: follows, release-type profiles and wants - // (M502 #5520). Its worker runs in main; the API reads and edits. + // wanted is the wanted list: follows, release-type profiles and wants. + // Its worker runs in main; the API reads and edits. wanted *wanted.Service - // searcher searches for wants and grabs releases for them (#5521). + // searcher searches for wants and grabs releases for them. searcher *wanted.Searcher - // importer refuses a download at a person's word (#5522). The import + // importer refuses a download at a person's word. The import // loop itself runs in main; this one only acts on a request. importer *importer.Importer - // lifecycle gives up on a download at a person's word (#5523); its loop + // lifecycle gives up on a download at a person's word; its loop // runs in main. lifecycle *lifecycle.Service // catalogueSearches reuses recent MusicBrainz answers; nil asks every // time, which is what handler tests get. catalogueSearches *searchCache - // leveled renders the leveled streams handed to Sonos/UPnP speakers - // (M464 #5001). Nil when its cache directory cannot be made: a level - // request then gets the plain stream. + // leveled renders the leveled streams handed to Sonos/UPnP speakers. Nil + // when its cache directory cannot be made: a level request then gets the + // plain stream. leveled *library.LeveledRenderer // setupToken must accompany the first registration while no users exist // (see auth.SetupToken). requireSetupToken is set by Mount, the only @@ -473,13 +471,10 @@ type handlers struct { // middleware on every request and edited from the admin network card. netSettings *netsettings.Service // streamSecret is the HMAC key used by SignStreamToken / - // VerifyStreamToken to authenticate the UPnP-speaker stream path - // (see internal/api/stream_token.go and the design at - // docs/superpowers/specs/2026-06-03-android-output-picker-upnp-design.md). - // nil in slice 1; slice 2 wires the env-var-with-app_preferences- - // fallback loader. A nil secret leaves the cookie path intact and - // makes the token path unreachable (HMAC of empty key won't match - // anything a client mints), which is the desired slice-1 default. + // VerifyStreamToken to authenticate the UPnP-speaker stream path (see + // internal/api/stream_token.go), from config.Config.StreamSecret. A nil + // secret leaves the cookie path intact and makes the token path + // unreachable (HMAC of empty key won't match anything a client mints). streamSecret []byte } diff --git a/internal/api/auth.go b/internal/api/auth.go index a36dea87..89bcc13f 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -140,7 +140,7 @@ func (h *handlers) handleLogin(w http.ResponseWriter, r *http.Request) { Value: token, Path: "/", HttpOnly: true, - Secure: auth.IsHTTPS(r, h.netSettings.Hops()), // plain HTTP and LAN stay functional (rule 94) + Secure: auth.IsHTTPS(r, h.netSettings.Hops()), // plain HTTP and LAN stay functional SameSite: http.SameSiteStrictMode, MaxAge: int(sessionCookieMaxAge.Seconds()), }) diff --git a/internal/api/auth_forgot.go b/internal/api/auth_forgot.go index e85fdc91..57baf6b7 100644 --- a/internal/api/auth_forgot.go +++ b/internal/api/auth_forgot.go @@ -130,15 +130,15 @@ func (h *handlers) sendResetEmail(ctx context.Context, user dbq.User) error { return h.mailer.Send(ctx, *user.Email, mailer.ResetEmailSubject, textBody, htmlBody) } -// errNoPublicURL stops a reset email from going out before the operator has +// errNoPublicURL stops a reset email from going out before an admin has // said where Minstrel lives. It surfaces in the log, not the response, which // stays the same opaque 200 either way. var errNoPublicURL = errors.New("forgot-password: no public URL set (Admin › Email & network › Public address); reset email not sent") -// buildResetURL builds the emailed link from the operator-set public URL. +// buildResetURL builds the emailed link from an admin-set public URL. // It deliberately ignores the request: the Host header is whatever the -// requester sent, and building from it let a forged Host plant a real reset -// token on a link to someone else's server. +// requester sent, and building from it would let a forged Host plant a real +// reset token on a link to someone else's server. func buildResetURL(publicURL, token string) (string, error) { if publicURL == "" { return "", errNoPublicURL diff --git a/internal/api/auth_register.go b/internal/api/auth_register.go index eec2fd72..02d142f5 100644 --- a/internal/api/auth_register.go +++ b/internal/api/auth_register.go @@ -175,7 +175,8 @@ func (h *handlers) handleRegister(w http.ResponseWriter, r *http.Request) { return } - // Atomic invite redeem (if applicable). Now we have user.ID for redeemed_by. + // Atomic invite redeem (if applicable). Now we have user.ID for + // redeemed_by. if usedInviteToken != "" { rows, rerr := q.RedeemInvite(r.Context(), dbq.RedeemInviteParams{ Token: usedInviteToken, @@ -214,7 +215,7 @@ func (h *handlers) handleRegister(w http.ResponseWriter, r *http.Request) { Value: sessionToken, Path: "/", HttpOnly: true, - Secure: auth.IsHTTPS(r, h.netSettings.Hops()), // plain HTTP and LAN stay functional (rule 94) + Secure: auth.IsHTTPS(r, h.netSettings.Hops()), // plain HTTP and LAN stay functional SameSite: http.SameSiteStrictMode, MaxAge: int(sessionCookieMaxAge.Seconds()), }) diff --git a/internal/api/auth_register_test.go b/internal/api/auth_register_test.go index d64de07e..34b51587 100644 --- a/internal/api/auth_register_test.go +++ b/internal/api/auth_register_test.go @@ -158,7 +158,8 @@ func TestRegister_OpenMode_NoTokenNeeded(t *testing.T) { h, pool := testHandlers(t) seedUser(t, pool, "preexisting", "pw", true) // Switch to open mode; restore invite_only afterwards so other tests are - // not affected (registration_settings is a singleton not truncated by ResetDB). + // not affected (registration_settings is a singleton not truncated by + // ResetDB). if _, err := pool.Exec(context.Background(), "UPDATE registration_settings SET mode = 'open' WHERE id = true"); err != nil { t.Fatalf("set mode: %v", err) diff --git a/internal/api/auth_test.go b/internal/api/auth_test.go index f7386567..a09a1a0a 100644 --- a/internal/api/auth_test.go +++ b/internal/api/auth_test.go @@ -77,7 +77,7 @@ func testHandlers(t *testing.T) (*handlers, *pgxpool.Pool) { // seedUser creates a test user. The supplied username is automatically // prefixed with dbtest.TestUserPrefix so dbtest.ResetDB cleans it up -// without touching the operator's admin row. Callers that need to send +// without touching the developer's admin row. Callers that need to send // the username in HTTP bodies or compare against API responses must // include the same prefix in their literals. func seedUser(t *testing.T, pool *pgxpool.Pool, username, password string, isAdmin bool) dbq.User { diff --git a/internal/api/cast_token.go b/internal/api/cast_token.go index 8930f2c5..2705948c 100644 --- a/internal/api/cast_token.go +++ b/internal/api/cast_token.go @@ -20,7 +20,7 @@ const ( type castTokenRequest struct { TrackID string `json:"trackId"` ExpSeconds int `json:"expSeconds,omitempty"` - // Level asks for the leveled stream (M464 #5001): the track rendered at + // Level asks for the leveled stream: the track rendered at // the user's loudness gain. AsAlbum says the track is playing as part of // its album in order, which picks album gain in auto mode; only the // client holding the queue knows it. @@ -68,7 +68,7 @@ func mimeForFormat(format string) string { // extension (Content-Type header alone is insufficient) -- without a // recognizable extension, Sonos reports TrackDuration=0 and seeks // trigger auto-advance because every position past 0 looks past-the- -// end. Defaults to "mp3" for unknown formats. See task #610. +// end. Defaults to "mp3" for unknown formats. func extForFormat(format string) string { switch strings.ToLower(strings.TrimSpace(format)) { case "mp3", "mpeg": @@ -101,9 +101,6 @@ func extForFormat(format string) string { // // expSeconds is clamped to [60, 86400]; default 21600 (6h) — long enough // to play through any typical track without re-minting mid-playback. -// -// Part of the output-picker UPnP slice. See -// docs/superpowers/specs/2026-06-03-android-output-picker-upnp-design.md. func (h *handlers) handleCastStreamToken(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { diff --git a/internal/api/cast_token_test.go b/internal/api/cast_token_test.go index 526f50ed..b08bd1f2 100644 --- a/internal/api/cast_token_test.go +++ b/internal/api/cast_token_test.go @@ -54,7 +54,7 @@ func TestCastStreamToken_HappyPath(t *testing.T) { t.Fatalf("URL missing stream path: %s", resp.URL) } // Stream URL must carry a file extension so Sonos's URL probe can - // identify the audio format (see task #610). Track seeded above is + // identify the audio format. Track seeded above is // .flac via seedTrack's default file_format. if !strings.Contains(resp.URL, "/stream.flac?") { t.Fatalf("URL missing file-extension segment: %s", resp.URL) diff --git a/internal/api/catalogue.go b/internal/api/catalogue.go index cf4c0658..9b883600 100644 --- a/internal/api/catalogue.go +++ b/internal/api/catalogue.go @@ -36,9 +36,10 @@ type catalogueSearchResult struct { Requested bool `json:"requested"` } -// handleCatalogueSearch implements GET /api/catalogue/search?q=&kind=artist|album|track -// (M502 #5519), asking MusicBrainz directly. An unreachable MusicBrainz is a -// 503 the page shows as such (rule 164). +// handleCatalogueSearch implements GET +// /api/catalogue/search?q=&kind=artist|album|track asking MusicBrainz directly. +// An unreachable MusicBrainz is a 503 the page shows as such, not an empty +// result. func (h *handlers) handleCatalogueSearch(w http.ResponseWriter, r *http.Request) { kind := strings.TrimSpace(r.URL.Query().Get("kind")) switch kind { @@ -250,7 +251,7 @@ func (h *handlers) catalogueInLibrary(ctx context.Context, kind, mbid string) (b // handleCatalogueArt implements GET /api/catalogue/art/{kind}/{mbid} for an // artist or a release group Minstrel doesn't hold. The image comes from the -// operator's enabled cover-art sources and is kept on disk, so the browser +// enabled cover-art sources and is kept on disk, so the browser // never fetches a third-party host. A 404 shows the placeholder. func (h *handlers) handleCatalogueArt(w http.ResponseWriter, r *http.Request) { kind, mbid := chi.URLParam(r, "kind"), chi.URLParam(r, "mbid") diff --git a/internal/api/catalogue_test.go b/internal/api/catalogue_test.go index 6495ebec..0dfdf7f6 100644 --- a/internal/api/catalogue_test.go +++ b/internal/api/catalogue_test.go @@ -199,7 +199,8 @@ func TestCatalogueArt_ServesTheImageOrAPlaceholder404(t *testing.T) { if w := get("/api/catalogue/art/artist/" + searchArtistMBID + "?name=Radiohead"); w.Code != http.StatusOK || w.Header().Get("Content-Type") != "image/png" { t.Errorf("artist: %d %q", w.Code, w.Header().Get("Content-Type")) } - // No album source is enabled: a 404, which the page shows as its placeholder. + // No album source is enabled: a 404, which the page shows as its + // placeholder. if w := get("/api/catalogue/art/release-group/" + searchGroupMBID); w.Code != http.StatusNotFound { t.Errorf("release group with no source: %d", w.Code) } diff --git a/internal/api/client_assets.go b/internal/api/client_assets.go index 5f7e8c39..d35dd66d 100644 --- a/internal/api/client_assets.go +++ b/internal/api/client_assets.go @@ -1,6 +1,6 @@ package api -// In-app update endpoints (#397). The Android APK ships bundled with +// In-app update endpoints. The Android APK ships bundled with // the server image so the client can self-update without an external // app store. CI sequencing bakes the APK + sidecar version file into // /app/client/ at image build time. @@ -13,11 +13,8 @@ package api // download every 60s. Real install flows fire one download per // update; anything tighter is scripted/abusive. // -// Returns 404 gracefully when the APK isn't present (dev environments, -// pre-CI-wiring); the client treats 404 as "no update channel available." -// -// (These paragraphs said "the Flutter client" until 2026-09-10. That client -// was deleted in v2026.08.18 — the Android app is the only one now.) +// Returns 404 gracefully when the APK isn't present (a local build); the +// client treats 404 as "no update channel available." import ( "encoding/json" @@ -96,10 +93,10 @@ func clientAPKAllowDownload(userID string, now time.Time) time.Duration { // installed over that?" — the name never does. // - Channel is a SIBLING FIELD, never a suffix inside the name. // -// JSON rather than a positional line on purpose. The obvious growth path for -// the old one-value file was " ", which a first-space split -// silently mangles the moment a third field appears: the code stops parsing, -// and the reader falls back to name comparison WITHOUT erroring. +// JSON rather than a positional line on purpose: a " " line split +// on the first space silently mangles the moment a third field appears — the +// code stops parsing, and the reader falls back to name comparison WITHOUT +// erroring. type clientVersionSidecar struct { Name string `json:"name"` // Pointer, not int64: absent must stay distinguishable from zero. An diff --git a/internal/api/convert.go b/internal/api/convert.go index 2ea8b1da..a48ba868 100644 --- a/internal/api/convert.go +++ b/internal/api/convert.go @@ -55,9 +55,9 @@ func yearFromDate(d pgtype.Date) int { return d.Time.Year() } -// durationMsToSec converts millisecond durations (as stored in tracks.duration_ms) -// to seconds, rounding to nearest. Callers display seconds; sub-second precision -// is not useful in UI. +// durationMsToSec converts millisecond durations (as stored in +// tracks.duration_ms) to seconds, rounding to nearest. Callers display seconds; +// sub-second precision is not useful in UI. func durationMsToSec(ms int32) int { return int((ms + 500) / 1000) } @@ -79,7 +79,7 @@ func streamURL(trackID pgtype.UUID) string { // cast tokens. Sonos's URL probe gates duration detection on a recognizable // audio file extension; the bare `/stream` shape reports TrackDuration=0 // and breaks seek/auto-advance. The bare /stream route stays mounted as an -// alias for legacy / web / Subsonic clients. See task #610. +// alias for web and Subsonic clients. func streamURLWithExt(trackID pgtype.UUID, ext string) string { return streamURL(trackID) + "." + ext } diff --git a/internal/api/errors.go b/internal/api/errors.go index 50bcc297..71b81d3c 100644 --- a/internal/api/errors.go +++ b/internal/api/errors.go @@ -51,7 +51,7 @@ func writeRateLimited(w http.ResponseWriter, wait time.Duration) { } // writeErrWithLog logs the error at Error level and writes the response. -// Use for 500-class errors where the operator needs the cause in logs. +// Use for 500-class errors whose cause belongs in the logs. func writeErrWithLog(w http.ResponseWriter, logger *slog.Logger, msg string, err error) { if logger != nil { logger.Error(msg, "err", err) diff --git a/internal/api/events.go b/internal/api/events.go index 191d1688..5946d78b 100644 --- a/internal/api/events.go +++ b/internal/api/events.go @@ -22,16 +22,16 @@ type eventRequest struct { At *string `json:"at"` ClientID *string `json:"client_id"` // Source tags which surface a play_started came from. "for_you" - // / "discover" feed the system-playlist rotation dedup (#415); + // / "discover" feed the system-playlist rotation dedup; // absent / "" for library, user-playlist, radio, Subsonic. Source *string `json:"source"` - // DeviceClass is the client's self-reported device class (#1551), + // DeviceClass is the client's self-reported device class, // normalized + stored per play to condition radio on the current device. DeviceClass *string `json:"device_class"` } // knownDeviceClasses whitelists the device classes the context-affinity facet -// buckets by (#1551). A client sends one; anything unrecognized normalizes to +// buckets by. A client sends one; anything unrecognized normalizes to // "other", and empty/absent to "" (stored NULL → excluded from the device // dimension). Kept permissive (no DB CHECK) so a new client class is one // whitelist entry, not a migration. @@ -66,9 +66,9 @@ const eventFutureSkew = 5 * time.Minute // clampEventTime bounds a client-supplied event timestamp to sanity: // no earlier than the account's creation (offline replays can // legitimately be days old, but no play can predate the user) and no -// later than now + a small skew allowance. Unbounded client clocks -// previously let a skewed device write arbitrarily old plays, which -// poisoned Rediscover's "not played in 6 months" ordering (#1246). +// later than now + a small skew allowance. An unbounded client clock would +// let a skewed device write arbitrarily old plays, poisoning Rediscover's +// "not played in 6 months" ordering. func clampEventTime(at, userCreatedAt, now time.Time) time.Time { if at.Before(userCreatedAt) { return userCreatedAt @@ -157,11 +157,11 @@ func (h *handlers) handleEventPlayStarted( } // handleEventPlayOffline records a complete play that happened with -// no/flaky connectivity, replayed from the Flutter offline mutation -// queue (#426 part B). `at` is the original (client) play-start time -// — parsed by handleEvents from req.At — so history lands on the -// real timeline, not replay time. duration_played_ms drives the skip -// rule; source advances #415 rotation for system-playlist plays. +// no or flaky connectivity, replayed from a client's offline queue. +// `at` is the original (client) play-start time — parsed by handleEvents +// from req.At — so history lands on the real timeline, not replay time. +// duration_played_ms drives the skip rule; source advances the +// system-playlist rotation. func (h *handlers) handleEventPlayOffline( w http.ResponseWriter, r *http.Request, user dbq.User, req eventRequest, at time.Time, clientID string, diff --git a/internal/api/events_publish.go b/internal/api/events_publish.go index 7c6eb69f..40eadb46 100644 --- a/internal/api/events_publish.go +++ b/internal/api/events_publish.go @@ -1,7 +1,7 @@ -// Small helpers for posting live events to the SSE bus (#392). Producers -// call these from their handler success paths so the wire shape stays in -// one place. Each helper is a no-op when h.eventbus is nil (older callers -// or tests that don't construct a bus); production always has one. +// Small helpers for posting live events to the SSE bus. Producers call +// these from their handler success paths so the wire shape stays in one +// place. Each helper is a no-op when h.eventbus is nil (tests that don't +// construct a bus); production always has one. package api diff --git a/internal/api/events_stream.go b/internal/api/events_stream.go index 9af3d8d6..f9fccf8f 100644 --- a/internal/api/events_stream.go +++ b/internal/api/events_stream.go @@ -1,6 +1,6 @@ // Live event stream — Server-Sent Events at GET /api/events/stream. // -// Clients (today: Flutter) subscribe to be notified of state changes +// The web app and the Android app subscribe to be notified of state changes // elsewhere in the system (likes, request status flips, quarantine // resolutions, scan progress, playlist mutations) so static screens // can invalidate cached data without explicit user action. diff --git a/internal/api/file_remove_error.go b/internal/api/file_remove_error.go index 3b2441c2..f9cfb670 100644 --- a/internal/api/file_remove_error.go +++ b/internal/api/file_remove_error.go @@ -10,17 +10,17 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) -// fileRemoveAPIError answers a delete that could not reach the track's file -// (#3918). Both delete endpoints use it, so the operator gets the same +// fileRemoveAPIError answers a delete that could not reach the track's file. +// Both delete endpoints use it, so an admin gets the same // explanation from the admin remove-track action and from quarantine's Delete // file. // // The unwritable case is a 409 rather than a 500 because nothing is broken: the // request conflicts with how the library is mounted, and the fix is the -// operator's. The message names the directory — removal writes to the parent, +// host's. The message names the directory — removal writes to the parent, // not the file — and the uid/gid the process runs as, which is the half of a // permission problem invisible from the host. Every case says nothing was -// deleted, because that is exactly what the operator will be worried about. +// deleted, because that is exactly what an admin will be worried about. func fileRemoveAPIError(err error) (*apierror.Error, bool) { var fre *library.FileRemoveError if !errors.As(err, &fre) { @@ -46,7 +46,7 @@ func fileRemoveAPIError(err error) (*apierror.Error, bool) { } // logFileRemoveFailure records a delete that could not reach its file. An -// unwritable library is an environment fact the operator can fix, so it is a +// unwritable library is an environment fact an admin can fix, so it is a // Warn; anything else is a real fault. func logFileRemoveFailure(logger *slog.Logger, apiErr *apierror.Error, attrs ...any) { attrs = append(attrs, "code", apiErr.Code, "err", apiErr.Cause) diff --git a/internal/api/file_remove_error_test.go b/internal/api/file_remove_error_test.go index b4317f92..f0925865 100644 --- a/internal/api/file_remove_error_test.go +++ b/internal/api/file_remove_error_test.go @@ -72,7 +72,7 @@ func TestFileRemoveAPIError(t *testing.T) { } // The unwritable answer must name the DIRECTORY. Removal needs write access to -// the parent, so a message naming the file would send the operator to fix the +// the parent, so a message naming the file would send an admin to fix the // wrong permissions. The directory is a prefix of the file path, which is why a // plain "contains the directory" check could never catch that regression. func TestFileRemoveAPIError_NotWritableNamesTheDirectoryNotTheFile(t *testing.T) { diff --git a/internal/api/home.go b/internal/api/home.go index 0e2daec5..22014c84 100644 --- a/internal/api/home.go +++ b/internal/api/home.go @@ -59,15 +59,14 @@ func (h *handlers) handleGetHome(w http.ResponseWriter, r *http.Request) { // handleGetHomeIndex implements GET /api/home/index. Returns the same // five sections /api/home computes, but as flat slices of entity IDs -// rather than denormalized objects. Drives the Flutter per-item +// rather than denormalized objects. Drives the Android per-item // rendering path — the client hydrates each tile against the // per-entity endpoints, so this response is tiny (a few KB at most) // and the cold-visit round-trip is correspondingly short. // // Shares the recommendation.HomeData computation with /api/home; the -// DB cost is identical. A future optimisation could add ID-only -// query variants, but the JSON savings already shrink the wire -// payload by roughly an order of magnitude on populated libraries. +// DB cost is identical, and the wire payload is roughly an order of +// magnitude smaller on populated libraries. func (h *handlers) handleGetHomeIndex(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { diff --git a/internal/api/leveled_stream.go b/internal/api/leveled_stream.go index 6c01f708..30e9fb3b 100644 --- a/internal/api/leveled_stream.go +++ b/internal/api/leveled_stream.go @@ -21,7 +21,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) -// The leveled stream (M464 #5001): a track rendered with the user's loudness +// The leveled stream: a track rendered with the user's loudness // gain applied, for the Sonos and UPnP speakers that fetch their own audio. // See internal/library/leveled.go for how it is rendered. @@ -49,7 +49,7 @@ func VerifyLeveledStreamToken(secret []byte, trackID string, exp int64, g librar } // leveledStreamPath is the leveled stream's path. It ends in .flac because -// Sonos reads the format from the URL's extension (task #610). +// Sonos reads the format from the URL's extension. func leveledStreamPath(trackID pgtype.UUID) string { return "/api/tracks/" + uuidToString(trackID) + "/leveled.flac" } diff --git a/internal/api/library.go b/internal/api/library.go index b2641af6..685bf952 100644 --- a/internal/api/library.go +++ b/internal/api/library.go @@ -119,7 +119,8 @@ func (h *handlers) handleGetArtist(w http.ResponseWriter, r *http.Request) { } refs := make([]AlbumRef, 0, len(rows)) for _, row := range rows { - // durationSec=0: not aggregated for nested album lists per spec data flow. + // durationSec=0: not aggregated for nested album lists per spec data + // flow. refs = append(refs, albumRefFrom(row.Album, artist.Name, int(row.TrackCount), 0)) } genres, err := q.ListGenresForArtist(r.Context(), artist.ID) @@ -257,7 +258,7 @@ func (h *handlers) handleGetArtistTopTracks(w http.ResponseWriter, r *http.Reque // handleLibraryShuffle implements GET /api/library/shuffle?limit=N — // the online source for the client's always-present "Shuffle all" -// (#427 S4). N random tracks across the whole library, per-user +// N random tracks across the whole library, per-user // quarantine filtered. limit defaults to 100, clamped to 1..500. // Offline, the client shuffles its local cache instead and never // calls this. diff --git a/internal/api/library_albums.go b/internal/api/library_albums.go index f480d311..e57f0e8b 100644 --- a/internal/api/library_albums.go +++ b/internal/api/library_albums.go @@ -39,11 +39,11 @@ type yearFilter struct { // the SPA infinite-scrolls against this endpoint via TanStack // createInfiniteQuery. // -// Optional filters (#367): `genre` and `year_from`/`year_to`. +// Optional filters: `genre` and `year_from`/`year_to`. // // Genre arrives as a QUERY parameter rather than a path segment on purpose. -// Raw ID3 genres routinely contain a slash — "Rock/Pop" is a real tag, and -// the one the task itself cites — which cannot survive a path segment: Go +// Raw ID3 genres routinely contain a slash — "Rock/Pop" is a real tag — +// which cannot survive a path segment: Go // normalises %2F and the router would split the value into two segments. func (h *handlers) handleListLibraryAlbums(w http.ResponseWriter, r *http.Request) { limit, offset, err := parsePaging(r.URL.Query()) diff --git a/internal/api/library_browse.go b/internal/api/library_browse.go index e4c98d85..ce65632e 100644 --- a/internal/api/library_browse.go +++ b/internal/api/library_browse.go @@ -7,18 +7,13 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) -// genreCount is one row of the genre browse index (#367). +// genreCount is one row of the genre browse index. // // Genres are the tag's own strings, split on [;,] but otherwise untouched — no // case folding and no synonym mapping. So "Rock" and "rock" can both appear, -// as can "Rock/Pop" alongside "Rock" and "Pop". That's deliberate for v1: the +// as can "Rock/Pop" alongside "Rock" and "Pop". That's deliberate: the // alternative is a normalisation table to invent and maintain, and the raw // spread has to be visible before anyone can judge whether it's a problem. -// -// The first look at that spread found it dominated by welded tokens like -// "Alternative RockRock" — the scanner's own bug, not the operator's tagging -// (#2499). Judge the "is a taxonomy needed" question (#2468) only against a -// library re-scanned since that fix. type genreCount struct { Genre string `json:"genre"` TrackCount int `json:"track_count"` diff --git a/internal/api/library_browse_test.go b/internal/api/library_browse_test.go index 1b591a5d..9d8988fe 100644 --- a/internal/api/library_browse_test.go +++ b/internal/api/library_browse_test.go @@ -83,10 +83,9 @@ func TestParseYearFilter(t *testing.T) { } } -// The crux of #367: a track tagged "Rock;Pop" must be reachable from BOTH -// genres. An exact-string match — which is what ListAlbumsByGenre did before -// this task — makes every multi-genre track invisible from either of its -// genres, so the index would list a genre whose page is empty. +// A track tagged "Rock;Pop" must be reachable from BOTH genres. An +// exact-string match makes every multi-genre track invisible from either of +// its genres, so the index would list a genre whose page is empty. func TestListGenres_SplitsMultiGenreTags(t *testing.T) { h, pool := testHandlers(t) artist := seedArtist(t, pool, "Genre Splitter") @@ -121,7 +120,7 @@ func TestListGenres_SplitsMultiGenreTags(t *testing.T) { // Splitting produces leading spaces on every fragment after the first, and // showing " Pop" as a genre distinct from "Pop" would be a bug. Trimming is a -// repair for our own splitting, not normalisation of the operator's tags. +// repair for our own splitting, not normalisation of the library's tags. func TestListGenres_TrimsFragmentWhitespace(t *testing.T) { h, pool := testHandlers(t) artist := seedArtist(t, pool, "Spacey Tags") @@ -223,8 +222,8 @@ func TestListLibraryAlbums_GenreWithSlashSurvives(t *testing.T) { func TestListLibraryAlbums_YearRangeFilter(t *testing.T) { h, pool := testHandlers(t) artist := seedArtist(t, pool, "Chronology") - // Each album needs a playable track: since #2702 the year axis lists only - // albums with something to play, matching what the genre axis already did. + // Each album needs a playable track: the year axis, like the genre axis, + // lists only albums with something to play. for _, a := range []struct { title string year int @@ -341,10 +340,9 @@ func keysOf(m map[string]bool) []string { return out } -// #2702: an album whose every file has gone leaves the year axis, matching -// what the genre axis already did. Before this the two disagreed — the same -// album was absent from genre and still listed under its year, where opening -// it found nothing playable. +// An album whose every file has gone leaves the year axis, as it leaves the +// genre axis: otherwise the same album would be absent from its genre and +// still listed under its year, where opening it finds nothing playable. func TestListAlbumYears_ExcludesFullyMissingAlbums(t *testing.T) { h, pool := testHandlers(t) q := dbq.New(pool) @@ -387,8 +385,8 @@ func TestListAlbumYears_ExcludesFullyMissingAlbums(t *testing.T) { } } -// The index, the list and the count must agree. #367 needed care for exactly -// this reason at the genre level: if they diverge, a year leads to an empty +// The index, the list and the count must agree, as at the genre level: if +// they diverge, a year leads to an empty // page or "Load more" promises rows that never arrive. func TestListLibraryAlbums_YearFilterAndCountAgreeOnMissing(t *testing.T) { h, pool := testHandlers(t) diff --git a/internal/api/library_sync_views.go b/internal/api/library_sync_views.go index c9038c3c..f3cbd994 100644 --- a/internal/api/library_sync_views.go +++ b/internal/api/library_sync_views.go @@ -3,8 +3,8 @@ package api // Wire shapes for /api/library/sync upserts. The sqlc-generated row // structs (dbq.Artist, dbq.Album, dbq.Track, dbq.Playlist) have no // JSON tags (sqlc.yaml: emit_json_tags=false), so a raw json.Marshal -// of them produces PascalCase field names that the Flutter sync -// controller (which reads snake_case keys) can't parse. +// of them produces PascalCase field names, and clients read snake_case +// keys. // // These view structs add the JSON tag layer + flatten pgtype.UUID and // pgtype.Timestamptz / Date into strings, mirroring the pattern @@ -50,7 +50,7 @@ type albumSyncView struct { ReleaseDate *string `json:"release_date"` CoverArtPath *string `json:"cover_art_path"` Mbid *string `json:"mbid"` - // AlbumGain and AlbumPeak are the album's ReplayGain 2.0 values (#4997): + // AlbumGain and AlbumPeak are the album's ReplayGain 2.0 values: // dB to the -18 LUFS reference, and a linear peak. Null until every track // on the album is measured. AlbumGain *float32 `json:"album_gain"` @@ -87,20 +87,20 @@ type trackSyncView struct { FilePath string `json:"file_path"` FileFormat string `json:"file_format"` Genre *string `json:"genre"` - // Missing reports that the file is currently absent from disk (#2704). + // Missing reports that the file is currently absent from disk. // // Shipped as state rather than filtered out of the feed, because a // missing file is expected to come back: the scanner clears the mark - // when it does, and adopts the row if it returns under a new name - // (#2528). Dropping the row instead would mean a delete-and-recreate on - // every client for what is often a transient unmount, churning caches - // and discarding the identity #2528 works to preserve. + // when it does, and adopts the row if it returns under a new name. + // Dropping the row instead would mean a delete-and-recreate on every + // client for what is often a transient unmount, churning caches and + // discarding the track's identity. // // A bool rather than the timestamp: clients need it to decide whether a // track is playable, which is a yes/no. The "gone since" clock is an - // operator concern and lives on the admin surface. + // admin concern and lives on the admin surface. Missing bool `json:"missing"` - // TrackGain and TrackPeak are the track's ReplayGain 2.0 values (#4997). + // TrackGain and TrackPeak are the track's ReplayGain 2.0 values. // Null until the track is measured. The album's pair rides on the album // view, since it changes when the album does, not when this track does. TrackGain *float32 `json:"track_gain"` diff --git a/internal/api/library_sync_views_test.go b/internal/api/library_sync_views_test.go index fe627d45..0000d109 100644 --- a/internal/api/library_sync_views_test.go +++ b/internal/api/library_sync_views_test.go @@ -81,9 +81,9 @@ func TestTrackSyncView_WireKeys(t *testing.T) { }) } -// The mark is what the client filters on, so a wrong value here silently -// re-introduces #2704: a present track marked missing vanishes from the -// client's library, a missing one stays playable and fails at the speaker. +// The mark is what the client filters on, so a wrong value here is silent: a +// present track marked missing vanishes from the client's library, a missing +// one stays playable and fails at the speaker. func TestTrackSyncView_MissingReflectsTheMark(t *testing.T) { present := dbq.Track{ID: validUUID, AlbumID: validUUID, ArtistID: validUUID} if toTrackSyncView(present, library.ReplayGain{}).Missing { @@ -97,7 +97,7 @@ func TestTrackSyncView_MissingReflectsTheMark(t *testing.T) { } } -// The gains are what Android levels playback by, offline included (#4997): +// The gains are what Android levels playback by, offline included: // the track's pair rides on the track view and the album's on the album view. func TestSyncViews_CarryReplayGain(t *testing.T) { gain, peak := float32(-3.25), float32(0.9441) diff --git a/internal/api/library_test.go b/internal/api/library_test.go index 7333664d..ec40a475 100644 --- a/internal/api/library_test.go +++ b/internal/api/library_test.go @@ -169,7 +169,8 @@ func TestHandleGetAlbum_NoTracks(t *testing.T) { if len(got.Tracks) != 0 { t.Errorf("tracks len = %d, want 0", len(got.Tracks)) } - // Year omitempty: releaseYear=0 => Year=0 => field should be absent from JSON + // Year omitempty: releaseYear=0 => Year=0 => field should be absent from + // JSON if got.Year != 0 { t.Errorf("year = %d, want 0", got.Year) } @@ -226,7 +227,8 @@ func TestHandleGetArtist_HappyPath(t *testing.T) { if len(got.Albums) != 2 { t.Fatalf("albums len = %d, want 2", len(got.Albums)) } - // ListAlbumsByArtist orders by release_date then sort_title; Abbey Road (1969) first. + // ListAlbumsByArtist orders by release_date then sort_title; Abbey Road + // (1969) first. if got.Albums[0].Title != "Abbey Road" { t.Errorf("first album = %q, want Abbey Road", got.Albums[0].Title) } @@ -475,10 +477,10 @@ func TestRoutesRegisteredInMount(t *testing.T) { "/api/tracks/00000000-0000-0000-0000-000000000001", "/api/tracks/00000000-0000-0000-0000-000000000001/stream", "/api/search?q=x", - // Browse indexes (#367). + // Browse indexes. "/api/library/genres", "/api/library/years", - // Admin surfaces for the missing-file lifecycle (#2527). Both must + // Admin surfaces for the missing-file lifecycle. Both must // 401 at the middleware rather than 404 — they live inside the // admin group, so reaching the auth check is what proves they are // wired. diff --git a/internal/api/likes.go b/internal/api/likes.go index aad2a663..955ea06a 100644 --- a/internal/api/likes.go +++ b/internal/api/likes.go @@ -16,9 +16,9 @@ import ( // logLikeChange writes a library_changes row for a like / unlike. Best- // effort: a failure here means the next sync may miss the row, but the -// HTTP response still succeeds. Cheap consequence — operator's other -// device will see the like once it manually pulls or once a future -// like/unlike on the same entity emits a fresh log row. +// HTTP response still succeeds. Cheap consequence — the user's other +// device sees the like once it manually pulls, or once a later like/unlike +// on the same entity emits a fresh log row. func (h *handlers) logLikeChange(r *http.Request, et syncpkg.EntityType, userID, entityID pgtype.UUID, op syncpkg.Op) { if err := syncpkg.LogChange(r.Context(), h.pool, et, syncpkg.EncodeLikeID(syncpkg.FormatUUID(userID), syncpkg.FormatUUID(entityID)), op); err != nil { @@ -60,7 +60,7 @@ func (h *handlers) handleLikeTrack(w http.ResponseWriter, r *http.Request) { if slices.Contains(liked, id) { _ = playevents.CaptureContextualLikeIfPlaying(r.Context(), q, user.ID, id, h.logger) } - // The like is on the song (M498 #5438): every copy it reached changes on + // The like is on the song: every copy it reached changes on // the client too. for _, t := range withTrack(id, liked) { h.logLikeChange(r, syncpkg.EntityLikeTrack, user.ID, t, syncpkg.OpUpsert) diff --git a/internal/api/me.go b/internal/api/me.go index bc33cd8f..231626ac 100644 --- a/internal/api/me.go +++ b/internal/api/me.go @@ -10,20 +10,14 @@ import ( // handleGetMe returns the authenticated user's profile shape — id, // username, display_name, email, is_admin. Mirrors the response shape -// of PUT /api/me/profile so the Android Settings → Profile screen -// (and Flutter's equivalent) can read its starting state from a GET -// before the user has done a first save. +// of PUT /api/me/profile so the Android Settings → Profile screen can +// read its starting state from a GET before the user has done a first +// save. // -// Drift #578 fix: this previously emitted the narrower UserView -// (id, username, is_admin only). Android's MeApi.getProfile() deser- -// ialised into MyProfileWire and saw display_name=null, email=null -// on every read — wiping the form fields on every Settings open. -// Saving from that blank state then submitted empty strings, which -// handleUpdateMyProfile treats as "clear to NULL" — DESTROYING the -// user's stored display_name + email. Returning the full profile -// shape here closes the loop. Web (User type narrower than this -// response) keeps working via TypeScript structural typing — the -// extra fields are ignored. +// The full profile matters: a form seeded with display_name=null and +// email=null would, on save, submit empty strings, which +// handleUpdateMyProfile treats as "clear to NULL" — destroying the stored +// values. The web app's narrower User type ignores the extra fields. func (h *handlers) handleGetMe(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok { diff --git a/internal/api/me_normalization.go b/internal/api/me_normalization.go index 4b41b9fe..0b1f9061 100644 --- a/internal/api/me_normalization.go +++ b/internal/api/me_normalization.go @@ -11,7 +11,7 @@ import ( ) // GET /api/me/normalization returns the caller's loudness-normalization -// preference, or the defaults when they have never set one (M464 #4998). +// preference, or the defaults when they have never set one. func (h *handlers) handleGetMyNormalization(w http.ResponseWriter, r *http.Request) { caller, ok := requireUser(w, r) if !ok { diff --git a/internal/api/me_notifications.go b/internal/api/me_notifications.go index 8c7ae485..48d078e5 100644 --- a/internal/api/me_notifications.go +++ b/internal/api/me_notifications.go @@ -18,7 +18,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/notifications" ) -// The notifications inbox (M489). Rows are written by internal/notifications; +// The notifications inbox. Rows are written by internal/notifications; // this surface lists them, counts the unread, marks them read, and holds each // user's per-kind settings. @@ -125,8 +125,8 @@ func (h *handlers) handleListMyNotifications(w http.ResponseWriter, r *http.Requ writeJSON(w, http.StatusOK, out) } -// handleMyUnreadNotificationCount implements GET /api/me/notifications/unread-count, -// the cheap call behind the badge. +// handleMyUnreadNotificationCount implements GET +// /api/me/notifications/unread-count, the cheap call behind the badge. func (h *handlers) handleMyUnreadNotificationCount(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { @@ -165,7 +165,8 @@ func (h *handlers) handleMarkMyNotificationRead(w http.ResponseWriter, r *http.R w.WriteHeader(http.StatusNoContent) } -// handleMarkAllMyNotificationsRead implements POST /api/me/notifications/read-all. +// handleMarkAllMyNotificationsRead implements POST +// /api/me/notifications/read-all. func (h *handlers) handleMarkAllMyNotificationsRead(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { diff --git a/internal/api/me_profile.go b/internal/api/me_profile.go index ce5f64cf..fb14f19e 100644 --- a/internal/api/me_profile.go +++ b/internal/api/me_profile.go @@ -37,7 +37,7 @@ type meProfileResp struct { IsAdmin bool `json:"is_admin"` // DebugModeEnabled is the account's diagnostics opt-in. The client // reads it here to decide whether to run the on-device diagnostics - // reporter (M9). Admin-set; the client also has a local OFF switch. + // reporter. Admin-set; the client also has a local OFF switch. DebugModeEnabled bool `json:"debug_mode_enabled"` } diff --git a/internal/api/me_recommendation_metrics.go b/internal/api/me_recommendation_metrics.go index 7716d9f3..ae2a796e 100644 --- a/internal/api/me_recommendation_metrics.go +++ b/internal/api/me_recommendation_metrics.go @@ -21,7 +21,7 @@ const ( recMetricsLowVolume = 20 ) -// Surface intents (milestone #127): each family is judged against its +// Surface intents: each family is judged against its // job, not one global bar — discovery mixes are EXPECTED to run higher // skip rates than the go-to surfaces. const ( @@ -40,15 +40,15 @@ type surfaceMetric struct { AvgCompletion float64 `json:"avg_completion"` // mean completion ratio, [0,1] LowConfidence bool `json:"low_confidence"` // plays < recMetricsLowVolume // SkipDelta / CompletionDelta are this row's difference from the manual - // baseline WITH its margin of error (#2495). nil on the baseline row + // baseline WITH its margin of error. nil on the baseline row // itself, and whenever the samples are too thin for a margin to mean // anything. Computed server-side so both clients read the same arithmetic - // instead of each re-deriving it — and so `low_confidence` is no longer - // mistaken for a decision threshold, which it never was. + // instead of each re-deriving it. `low_confidence` is a display + // threshold, not a decision one. SkipDelta *metricDelta `json:"skip_delta,omitempty"` CompletionDelta *metricDelta `json:"completion_delta,omitempty"` // Breakdown splits the family into the pick-kind populations its - // builder stamped (#1249, generalized #1270): For You's taste/fresh, + // builder stamped: For You's taste/fresh, // Discover's buckets, tier1-3 for tiered mixes — plus earlier plays // that predate attribution. Present only when the family has at // least one attributed play; the parent row remains the sum of its @@ -128,7 +128,7 @@ type familyAccum struct { // reduces to a single weighted division at the end. completionSum float64 // completionSqSum is the sum of squared completion ratios, which is what - // makes the variance mergeable across raw source rows (#2495). Standard + // makes the variance mergeable across raw source rows. Standard // deviations cannot be combined; sums of squares add exactly. completionSqSum float64 } diff --git a/internal/api/me_recommendation_metrics_test.go b/internal/api/me_recommendation_metrics_test.go index a5a6946e..4199fb5b 100644 --- a/internal/api/me_recommendation_metrics_test.go +++ b/internal/api/me_recommendation_metrics_test.go @@ -23,7 +23,7 @@ func newMetricsRouter(h *handlers) chi.Router { // seedSourcedPlay inserts a play_event with an explicit source + completion + // skip flag. A nil source inserts NULL (manual library play → baseline). -// pickKind is the For You taste/fresh attribution (#1249); nil everywhere +// pickKind is the For You taste/fresh attribution; nil everywhere // except attributed for_you plays. func seedSourcedPlay( t *testing.T, h *handlers, userID, trackID, sessionID pgtype.UUID, @@ -169,7 +169,7 @@ func TestBucketMetricsResponse_ForYouBreakdown(t *testing.T) { } func TestBucketMetricsResponse_DiscoverBucketBreakdown(t *testing.T) { - // Provenance is standard (#1270): Discover's bucket stamps surface as + // Provenance is standard: Discover's bucket stamps surface as // a breakdown exactly like For You's taste/fresh — this is what makes // the bucket allocation judgeable instead of a guess. src := func(s string) *string { return &s } @@ -225,7 +225,7 @@ func TestRecommendationMetrics_BucketsWithBaseline(t *testing.T) { radioA := "radio:11111111-1111-1111-1111-111111111111" taste, fresh := "taste", "fresh" // for_you: 3 plays, 1 skipped; completions 1.0, 0.95, 0.05 → mean 0.6667. - // Two attributed as taste picks, the skipped one as a fresh pick (#1249). + // Two attributed as taste picks, the skipped one as a fresh pick. seedSourcedPlay(t, h, user.ID, tk.ID, session, &forYou, &taste, 1.0, false) seedSourcedPlay(t, h, user.ID, tk.ID, session, &forYou, &taste, 0.95, false) seedSourcedPlay(t, h, user.ID, tk.ID, session, &forYou, &fresh, 0.05, true) @@ -265,7 +265,7 @@ func TestRecommendationMetrics_BucketsWithBaseline(t *testing.T) { if fy.AvgCompletion < 0.66 || fy.AvgCompletion > 0.67 { t.Errorf("for_you avg_completion = %.4f, want ~0.6667", fy.AvgCompletion) } - // Pick-kind attribution surfaces as the For You breakdown (#1249). + // Pick-kind attribution surfaces as the For You breakdown. if len(fy.Breakdown) != 2 { t.Fatalf("for_you breakdown rows = %d, want 2 (taste, fresh)", len(fy.Breakdown)) } diff --git a/internal/api/me_sessions_test.go b/internal/api/me_sessions_test.go index 8f9c3d6d..5473e608 100644 --- a/internal/api/me_sessions_test.go +++ b/internal/api/me_sessions_test.go @@ -49,8 +49,8 @@ func withURLParam(r *http.Request, key, value string) *http.Request { return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx)) } -// The rule #47 assertion. A delete keyed only on session id would let any -// household member revoke any other member's session by id — this pins that +// Household data stays per-user: a delete keyed only on session id would let +// any household member revoke any other member's session by id — this pins that // the user scope is actually in the WHERE clause and not just intended. func TestRevokeMySession_CannotRevokeAnotherUsersSession(t *testing.T) { h, pool := testHandlers(t) diff --git a/internal/api/me_subsonic_password.go b/internal/api/me_subsonic_password.go index 62143d33..6a0009fe 100644 --- a/internal/api/me_subsonic_password.go +++ b/internal/api/me_subsonic_password.go @@ -15,8 +15,7 @@ import ( // itself, so it is stored readable (migration 0003). That is why Minstrel // generates it rather than letting the user choose one: a generated value // can never be a login password reused from somewhere else, so a leaked -// users table gives up access to this server's /rest API and nothing more -// (M462 #5026). +// users table gives up access to this server's /rest API and nothing more. const subsonicPasswordBytes = 18 // 24 base64url characters diff --git a/internal/api/me_system_playlists.go b/internal/api/me_system_playlists.go index 91903927..a4c3cde4 100644 --- a/internal/api/me_system_playlists.go +++ b/internal/api/me_system_playlists.go @@ -19,9 +19,10 @@ type systemPlaylistsStatusResp struct { LastError *string `json:"last_error"` } -// handleGetSystemPlaylistsStatus implements GET /api/me/system-playlists-status. -// Returns the caller's system_playlist_runs row, or zero-values when no row -// exists yet (i.e. the user has never had a build attempted). +// handleGetSystemPlaylistsStatus implements GET +// /api/me/system-playlists-status. Returns the caller's system_playlist_runs +// row, or zero-values when no row exists yet (i.e. the user has never had a +// build attempted). func (h *handlers) handleGetSystemPlaylistsStatus(w http.ResponseWriter, r *http.Request) { caller, ok := requireUser(w, r) if !ok { diff --git a/internal/api/me_test.go b/internal/api/me_test.go index a9515e6c..680b9827 100644 --- a/internal/api/me_test.go +++ b/internal/api/me_test.go @@ -37,13 +37,10 @@ func TestHandleGetMe_ReturnsAuthenticatedUser(t *testing.T) { } } -// Drift #578 regression guard: a user whose profile fields ARE set -// must see them in the /api/me response. Previously this handler -// emitted the narrower UserView so Android Settings → Profile saw -// display_name=null and email=null on every read, wiped the form -// fields, and submitting from that blank state destroyed the user's -// stored values via the "empty string clears to NULL" semantics of -// PUT /api/me/profile. +// A user whose profile fields ARE set must see them in the /api/me +// response: a Settings → Profile form seeded without them would, on save, +// clear the stored values through PUT /api/me/profile's "empty string +// clears to NULL". func TestHandleGetMe_IncludesDisplayNameAndEmail(t *testing.T) { h, pool := testHandlers(t) user := seedUser(t, pool, "alice", "hunter2", false) diff --git a/internal/api/me_timezone.go b/internal/api/me_timezone.go index 60cfe06e..edd316c8 100644 --- a/internal/api/me_timezone.go +++ b/internal/api/me_timezone.go @@ -1,11 +1,8 @@ // PUT /api/me/timezone — accept the client's IANA timezone and store -// it on the user row. The system-playlist scheduler (#392 Half B) -// reads this column to fire each user's daily 03:00 build in their -// local time. -// -// Scheduler.Refresh(ctx, userID) is invoked in a later commit (Task 3) -// so a timezone change takes effect synchronously rather than waiting -// for the hourly reconciliation pass. +// it on the user row. The system-playlist scheduler reads this column +// to fire each user's daily 03:00 build in their local time. The +// handler calls Scheduler.Refresh so a change takes effect at once rather +// than at the hourly reconciliation pass. package api diff --git a/internal/api/media.go b/internal/api/media.go index a4009a44..4a577a05 100644 --- a/internal/api/media.go +++ b/internal/api/media.go @@ -125,17 +125,14 @@ func (h *handlers) handleGetCover(w http.ResponseWriter, r *http.Request) { // stream — either via the standard session path (cookie OR bearer token // resolved to a user-in-context by auth.OptionalUser, the middleware // the route is wrapped with in api.go) OR via a valid short-lived HMAC -// token in the ?token=&exp= query (UPnP / Sonos path, new for the -// output-picker UPnP slice). +// token in the ?token=&exp= query (the UPnP / Sonos path). // // The token path lets network speakers fetch the stream URL without -// carrying the user's session cookie — they cannot. See the design at -// docs/superpowers/specs/2026-06-03-android-output-picker-upnp-design.md. +// carrying the user's session cookie — they cannot. // -// When h.streamSecret is nil (slice-1 default, until slice 2 wires the -// loader), the token path always rejects because the HMAC of an empty -// key won't match any token a client could mint. The session path keeps -// working. +// When h.streamSecret is nil the token path always rejects, because the +// HMAC of an empty key won't match any token a client could mint. The +// session path keeps working. func (h *handlers) streamAuthOk(r *http.Request, trackID string) bool { if _, ok := auth.UserFromContext(r.Context()); ok { return true @@ -194,9 +191,9 @@ func (h *handlers) handleGetStream(w http.ResponseWriter, r *http.Request) { w.Header().Set("Accept-Ranges", "bytes") // Track bytes are immutable for a given track id (the scanner // indexes by file_path; re-encoded files take new ids). One-year - // max-age + immutable lets the client cache (LockCachingAudioSource - // on the Flutter side, browser cache on web) skip even the - // conditional GET on repeat plays. + // max-age + immutable lets the client cache (Media3's cache on Android, + // the browser cache on web) skip even the conditional GET on repeat + // plays. w.Header().Set("Cache-Control", "private, max-age=31536000, immutable") http.ServeContent(w, r, filepath.Base(track.FilePath), info.ModTime(), f) } diff --git a/internal/api/metrics_significance.go b/internal/api/metrics_significance.go index 4bc1d048..fdfcfdf7 100644 --- a/internal/api/metrics_significance.go +++ b/internal/api/metrics_significance.go @@ -2,27 +2,19 @@ package api import "math" -// Uncertainty on the deltas the recommendation-metrics card shows (#2495). +// Uncertainty on the deltas the recommendation-metrics card shows. // -// Why this exists: the card had exactly one volume threshold, -// recMetricsLowVolume = 20, and it was doing two jobs. Twenty plays is enough to -// be worth DISPLAYING — below that a skip rate is anecdote — but it is nowhere -// near enough to ACT on. Detecting the ~13pp differences that actually matter -// needs roughly 133 plays per arm for 80% power at α=0.05. +// recMetricsLowVolume = 20 answers "is this worth DISPLAYING" — below that a +// skip rate is anecdote — but twenty plays is nowhere near enough to ACT on. +// Detecting the ~13pp differences that actually matter needs roughly 133 +// plays per arm for 80% power at α=0.05, and two arms of 60-70 plays each +// can look like a bold difference while sitting at p ≈ 0.06. // -// So Discover's taste-matched (59 plays) and random-unheard (70) both rendered as -// full-confidence rows with a bold delta beside them, and that comparison sits at -// p ≈ 0.06. The card said "signal"; the arithmetic said "maybe". It led directly -// to a recommendation the data didn't support, and any reader with the same -// numbers would have made the same call. -// -// The fix is to publish the margin of error next to the delta and flag when the -// delta is smaller than it — i.e. not distinguishable from zero. Computed here, -// server-side, so both clients agree rather than each re-deriving it. -// -// recMetricsLowVolume stays exactly as it was. This is a second, independent -// signal, not a replacement: "too thin to show" and "too thin to act on" are -// different questions and deserve different answers. +// So each delta carries its margin of error, and a flag when the delta is +// smaller than it — not distinguishable from zero. Computed here, +// server-side, so both clients agree rather than each re-deriving it. "Too +// thin to show" and "too thin to act on" are different questions and get +// different answers. // deltaZ is the two-sided 95% normal critical value. Normal rather than // Student's t: at the sample sizes where a delta is worth acting on (n in the @@ -50,7 +42,8 @@ type metricDelta struct { // proportionDelta compares two rates (skips/plays) as a two-proportion // difference. Returns nil when either sample is empty, or when either rate is // degenerate (0 or 1) — a rate with no observed variation has an SE of 0 on its -// side, which would report a spuriously narrow margin rather than an honest one. +// side, which would report a spuriously narrow margin rather than an honest +// one. func proportionDelta(rate1 float64, n1 int64, rate2 float64, n2 int64) *metricDelta { if n1 <= 0 || n2 <= 0 { return nil @@ -98,8 +91,8 @@ func newDelta(deltaPP, marginPP float64) *metricDelta { } // sampleVariance recovers the sample variance from the aggregates the SQL -// returns. sum is mean×n rather than a selected column, which keeps the query to -// one extra expression. +// returns. sum is mean×n rather than a selected column, which keeps the query +// to one extra expression. // // The subtraction can go very slightly negative through floating-point // cancellation when every observation is identical, so the result is clamped — diff --git a/internal/api/metrics_significance_test.go b/internal/api/metrics_significance_test.go index c8e39ca8..f07e33f9 100644 --- a/internal/api/metrics_significance_test.go +++ b/internal/api/metrics_significance_test.go @@ -6,9 +6,8 @@ import ( ) func TestProportionDelta_ReproducesTheDiscoverCase(t *testing.T) { - // The comparison that motivated #2495: Discover taste-matched (59 plays, - // 15.3% skip) vs random-unheard (70 plays, 28.6%). A 13.3pp gap that the old - // card rendered as a confident coloured number, sitting at p ≈ 0.06. + // 59 plays at 15.3% skip vs 70 at 28.6%: a 13.3pp gap that looks + // confident and sits at p ≈ 0.06. d := proportionDelta(0.153, 59, 0.286, 70) if d == nil { t.Fatal("expected a delta for two real samples") @@ -16,8 +15,8 @@ func TestProportionDelta_ReproducesTheDiscoverCase(t *testing.T) { if math.Abs(d.DeltaPP-(-13.3)) > 0.1 { t.Errorf("DeltaPP = %.2f, want ≈ -13.3", d.DeltaPP) } - // This is the assertion the whole task exists for: at these sample sizes the - // margin swallows the difference. + // This is the assertion the whole task exists for: at these sample sizes + // the margin swallows the difference. if d.Distinguishable { t.Errorf("13.3pp on n=59/70 reported as distinguishable (margin %.2f) — "+ "this is exactly the false confidence #2495 set out to remove", d.MarginPP) @@ -70,7 +69,8 @@ func TestProportionDelta_DegenerateRates(t *testing.T) { if d := proportionDelta(1, 50, 1, 50); d != nil { t.Errorf("both rates 1 produced a delta: %+v", d) } - // One degenerate side is still informative — the other side carries variance. + // One degenerate side is still informative — the other side carries + // variance. if d := proportionDelta(0, 200, 0.3, 200); d == nil { t.Error("one degenerate rate should still yield a delta") } diff --git a/internal/api/notify_producers.go b/internal/api/notify_producers.go index cc2c1d2a..3cd2e5fc 100644 --- a/internal/api/notify_producers.go +++ b/internal/api/notify_producers.go @@ -11,7 +11,7 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/requests" ) -// Helpers for the handlers that produce notifications (M489). Every call is +// Helpers for the handlers that produce notifications. Every call is // NotifyLogged: a notification never fails the action that caused it. // userLabel is how a user is named to someone else: their display name, or @@ -74,7 +74,7 @@ func (h *handlers) trackLabel(ctx context.Context, trackID pgtype.UUID) string { } // notifyPlaybackErrors tells admins a track failed to play, naming it and who -// it failed for (M542). The notice coalesces: while it is unread each report +// it failed for. The notice coalesces: while it is unread each report // adds one to its count and its name to the front of the list. func (h *handlers) notifyPlaybackErrors(ctx context.Context, trackID pgtype.UUID, user dbq.User) { p := notifications.Payload{Count: 1} diff --git a/internal/api/notify_producers_test.go b/internal/api/notify_producers_test.go index 3fa6abd0..bee25be8 100644 --- a/internal/api/notify_producers_test.go +++ b/internal/api/notify_producers_test.go @@ -15,8 +15,8 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/notifications" ) -// inboxItem is one thing the notifier left for one user: kind and decoded payload, -// newest first. +// inboxItem is one thing the notifier left for one user: kind and decoded +// payload, newest first. type inboxItem struct { Kind string Payload notifications.Payload diff --git a/internal/api/playlists.go b/internal/api/playlists.go index eab70042..a1b22da7 100644 --- a/internal/api/playlists.go +++ b/internal/api/playlists.go @@ -60,7 +60,7 @@ type playlistTrackView struct { DurationSec int32 `json:"duration_sec"` StreamURL *string `json:"stream_url"` AddedAt string `json:"added_at"` - // Unavailable marks an entry whose file is missing from disk (#2527). + // Unavailable marks an entry whose file is missing from disk. // The track is still a real, known track — its history and likes are // intact and the file may come back — so the entry keeps its place and // its text; clients render it greyed out and skip over it on playback. @@ -316,8 +316,9 @@ func (h *handlers) handleAppendTracks(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, playlistDetailToView(detail)) } -// handleRemovePlaylistTrack implements DELETE /api/playlists/{id}/tracks/{position}. -// Owner only. Returns the post-mutation detail like AppendTracks. +// handleRemovePlaylistTrack implements DELETE +// /api/playlists/{id}/tracks/{position}. Owner only. Returns the post-mutation +// detail like AppendTracks. func (h *handlers) handleRemovePlaylistTrack(w http.ResponseWriter, r *http.Request) { caller, ok := requireUser(w, r) if !ok { diff --git a/internal/api/playlists_system_shuffle.go b/internal/api/playlists_system_shuffle.go index 84be5c1a..a1315c93 100644 --- a/internal/api/playlists_system_shuffle.go +++ b/internal/api/playlists_system_shuffle.go @@ -14,15 +14,14 @@ import ( "git.fabledsword.com/bvandeusen/minstrel/internal/playlists" ) -// Generic registry-driven system-playlist endpoints (#411 R2), -// replacing the former per-kind handlers. {kind} is the raw +// Generic registry-driven system-playlist endpoints. {kind} is the raw // system_variant ('for_you' | 'discover' | future singleton kinds); // only singleton kinds (playlists.RefreshableSystemKind) are // addressable here — songs_like_artist is multi-per-user and played // by playlist id, so it 404s. // // GET /api/playlists/system/{kind}/shuffle — the caller's playlist -// for that kind with tracks in rotation-aware order (#415): unheard +// for that kind with tracks in rotation-aware order: unheard // this rotation first (shuffled), then heard; resets when exhausted. // Same JSON shape as GET /api/playlists/{id} so clients reuse track // parsing. Intentionally uncached — it varies per play, unlike the @@ -73,7 +72,7 @@ func (h *handlers) handleSystemPlaylistRefresh(w http.ResponseWriter, r *http.Re writeErr(w, apierror.InternalMsg("build failed", err)) return } - // #968: announce the rebuild so the user's other clients refresh. + // Announce the rebuild so the user's other clients refresh. h.publishSystemRebuilt(user.ID) q := dbq.New(h.pool) v := kind diff --git a/internal/api/playlists_system_test.go b/internal/api/playlists_system_test.go index 621775fd..7664fbf4 100644 --- a/internal/api/playlists_system_test.go +++ b/internal/api/playlists_system_test.go @@ -10,8 +10,7 @@ import ( "github.com/go-chi/chi/v5" ) -// Generic registry-driven system-playlist endpoints (#411 R2). -// Replaces the former per-kind refresh handler tests. +// Generic registry-driven system-playlist endpoints. func newSystemPlaylistRouter(h *handlers) chi.Router { r := chi.NewRouter() r.Route("/api", func(api chi.Router) { diff --git a/internal/api/playlists_test.go b/internal/api/playlists_test.go index 1c77d1fa..0930a2e3 100644 --- a/internal/api/playlists_test.go +++ b/internal/api/playlists_test.go @@ -571,13 +571,15 @@ func TestListPlaylists_BadKindReturns400(t *testing.T) { } // TestEditEndpoints_403OnSystemPlaylist verifies that all five edit verbs -// return 403 system_playlist_readonly when the target playlist has kind='system'. +// return 403 system_playlist_readonly when the target playlist has +// kind='system'. func TestEditEndpoints_403OnSystemPlaylist(t *testing.T) { h, pool := testHandlers(t) u := seedUser(t, pool, "sysedit", "pw", false) // Seed a system playlist directly via SQL (BuildSystemPlaylists requires - // play_events seeding which we already exercise in playlists package tests). + // play_events seeding which we already exercise in playlists package + // tests). var sid string if err := pool.QueryRow(context.Background(), ` INSERT INTO playlists (user_id, name, description, is_public, kind, system_variant) diff --git a/internal/api/quarantine.go b/internal/api/quarantine.go index de1332db..091d24e7 100644 --- a/internal/api/quarantine.go +++ b/internal/api/quarantine.go @@ -71,7 +71,7 @@ func (h *handlers) handleFlag(w http.ResponseWriter, r *http.Request) { // Broadcast: the flagging user's other clients invalidate their // Hidden tab; admins' clients invalidate their quarantine queue. h.publishQuarantineEvent("quarantine.flagged", user.ID, trackID, true) - // Admins review flags (M489). An admin flagging a track needs no notice + // Admins review flags. An admin flagging a track needs no notice // of their own flag. h.notifier.NotifyLogged(r.Context(), notifications.KindQuarantineFlagged, notifications.ToAdmins(user.ID), notifications.Payload{Name: h.trackLabel(r.Context(), trackID), Actor: userLabel(user), Reason: quarantineReasonLabel(body.Reason)}.Map()) diff --git a/internal/api/radio.go b/internal/api/radio.go index d8558bb8..641ceb58 100644 --- a/internal/api/radio.go +++ b/internal/api/radio.go @@ -82,16 +82,14 @@ func (h *handlers) handleRadio(w http.ResponseWriter, r *http.Request) { } currentVec := loadCurrentSessionVector(r, q, user.ID, h.logger) - // Condition on the current device (#1551): the latest play's device is a + // Condition on the current device: the latest play's device is a // cheap, request-free proxy for what the user is on right now. currentVec.DeviceClass = latestDeviceClass(r.Context(), q, user.ID, h.logger) exclude := parseExcludeParam(r.URL.Query().Get("exclude")) - // Size the pool to the library (#3880). A fixed ~170 candidates samples a - // shrinking fraction of a growing collection, which is what made the - // recommendations feel less relevant as the library grew. Degrades to the - // base limits if the count is unavailable — never fails the request over a - // sizing hint. + // Size the pool to the library: a fixed ~170 candidates samples a shrinking + // fraction of a growing collection. Degrades to the base limits if the + // count is unavailable — never fails the request over a sizing hint. librarySize := h.librarySize.Get(r.Context(), func(ctx context.Context) (int64, error) { return recommendation.CountLibraryTracks(ctx, q) }) @@ -101,7 +99,7 @@ func (h *handlers) handleRadio(w http.ResponseWriter, r *http.Request) { candidates, err := recommendation.LoadCandidatesFromSimilarity( r.Context(), q, user.ID, seedID, h.recCfg.RecentlyPlayedHours, currentVec, exclude, limits, - // A fresh seed per request (#3889): radio is a new session each time + // A fresh seed per request: radio is a new session each time // and SHOULD draw differently. The system mixes are the surfaces that // promise repeatability; this is not one of them. strconv.FormatInt(time.Now().UnixNano(), 36), @@ -119,11 +117,11 @@ func (h *handlers) handleRadio(w http.ResponseWriter, r *http.Request) { } } - // Scoring weights come from the DB-backed tuning lab (#1250) — + // Scoring weights come from the DB-backed tuning — // read per request so an admin change takes effect live. weights := h.recSettings.Weights(recsettings.ScopeRadio) - // Diversity caps (#3882). Radio had none while every sibling surface did, - // which is how a whole session could come back from one artist. Scaled to + // Diversity caps, so a whole session cannot come back from one artist. + // Scaled to // the requested length so a 20-track radio and a 200-track one are capped // alike; Shuffle relaxes them rather than returning a short radio. // @@ -177,7 +175,7 @@ func loadCurrentSessionVector(r *http.Request, q *dbq.Queries, userID pgtype.UUI } // latestDeviceClass returns the device_class of the user's most recent play as -// the "current device" for context conditioning (#1551), or "" when unknown +// the "current device" for context conditioning, or "" when unknown // (no plays yet, or the latest play predates device capture). Best-effort: a // lookup failure yields a device-agnostic ("") affinity cell. func latestDeviceClass(ctx context.Context, q *dbq.Queries, userID pgtype.UUID, logger *slog.Logger) string { diff --git a/internal/api/replay_gain.go b/internal/api/replay_gain.go index 38009db1..c2807031 100644 --- a/internal/api/replay_gain.go +++ b/internal/api/replay_gain.go @@ -19,8 +19,8 @@ type replayGainResp struct { Items map[string]library.ReplayGain `json:"items"` } -// handleGetReplayGain implements GET /api/tracks/replay-gain?ids=a,b,c (M464 -// #4997): ReplayGain 2.0 values (dB to -18 LUFS, linear peaks) for each track, +// handleGetReplayGain implements GET /api/tracks/replay-gain?ids=a,b,c: +// ReplayGain 2.0 values (dB to -18 LUFS, linear peaks) for each track, // and for its album when the whole album is measured. // // A lookup rather than fields on TrackRef. Tracks reach the player through diff --git a/internal/api/requests.go b/internal/api/requests.go index 58c1920f..887da75b 100644 --- a/internal/api/requests.go +++ b/internal/api/requests.go @@ -150,7 +150,7 @@ func (h *handlers) handleCreateRequest(w http.ResponseWriter, r *http.Request) { return } - // Auto-approve flow (#355 / #376 U2.5): when an admin has set the user's + // Auto-approve: when an admin has set the user's // auto_approve_requests flag, the new request goes straight onto the // wanted list as if approved. A failure leaves it pending for an admin, // the same as a manual approval hitting the same error. The approver is @@ -168,10 +168,10 @@ func (h *handlers) handleCreateRequest(w http.ResponseWriter, r *http.Request) { } } - // A new request still pending after any auto-approval waits on an admin - // (M489). One that deduped into a request already in flight was - // announced when it was first made, and the requester, if they are an - // admin themselves, needs no notice of their own request. + // A new request still pending after any auto-approval waits on an admin. + // One that deduped into a request already in flight was announced when it + // was first made, and the requester, if they are an admin themselves, + // needs no notice of their own request. if created && row.Status == dbq.RequestStatusPending { p := requestPayload(row) p.Actor = userLabel(user) @@ -182,7 +182,8 @@ func (h *handlers) handleCreateRequest(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, requestViewFrom(row)) } -// handleListRequests implements GET /api/requests — returns caller's own requests. +// handleListRequests implements GET /api/requests — returns caller's own +// requests. func (h *handlers) handleListRequests(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { diff --git a/internal/api/requests_test.go b/internal/api/requests_test.go index 4e72cf96..3e922eec 100644 --- a/internal/api/requests_test.go +++ b/internal/api/requests_test.go @@ -27,12 +27,14 @@ func newRequestsRouter(h *handlers) chi.Router { return r } -// withUser injects a user into the request context the same way RequireUser does. +// withUser injects a user into the request context the same way RequireUser +// does. func withUser(req *http.Request, user dbq.User) *http.Request { return req.WithContext(context.WithValue(req.Context(), userCtxKeyForTest(), user)) } -// doCreateRequest fires POST /api/requests with the given JSON body as the given user. +// doCreateRequest fires POST /api/requests with the given JSON body as the +// given user. func doCreateRequest(h *handlers, user dbq.User, body string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/api/requests", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") @@ -69,8 +71,8 @@ func doCancelRequest(h *handlers, user dbq.User, id pgtype.UUID) *httptest.Respo return w } -// createArtistRequest is a convenience wrapper that seeds a valid artist request -// via the handler and returns the parsed response. +// createArtistRequest is a convenience wrapper that seeds a valid artist +// request via the handler and returns the parsed response. func createArtistRequest(t *testing.T, h *handlers, user dbq.User, artistMBID, artistName string) requestView { t.Helper() body := fmt.Sprintf(`{"kind":"artist","artist_mbid":%q,"artist_name":%q}`, artistMBID, artistName) @@ -87,7 +89,7 @@ func createArtistRequest(t *testing.T, h *handlers, user dbq.User, artistMBID, a // TestHandleCreateRequest_AutoApprove_GoesStraightOnTheWantedList verifies // that a user with auto_approve_requests=true gets their request approved on -// creation (#355 / #376 U2.5): an artist request follows the artist at once. +// creation: an artist request follows the artist at once. func TestHandleCreateRequest_AutoApprove_GoesStraightOnTheWantedList(t *testing.T) { h, pool := testHandlers(t) resetRequests(t, h) @@ -239,7 +241,8 @@ func TestHandleListRequests_OnlyOwnRequests(t *testing.T) { } } -// TestHandleGetRequest_OwnReturns200 verifies a caller can fetch their own request. +// TestHandleGetRequest_OwnReturns200 verifies a caller can fetch their own +// request. func TestHandleGetRequest_OwnReturns200(t *testing.T) { h, pool := testHandlers(t) resetRequests(t, h) @@ -351,10 +354,11 @@ func TestHandleCancelRequest_NonPending_409(t *testing.T) { t.Fatalf("first cancel: status = %d, want 200; body = %s", w1.Code, w1.Body.String()) } - // Second cancel on a now-rejected row → ErrNotPending → 409. - // The cancel SQL checks user_id = $2 AND status = 'pending'; zero rows → ErrNotPending. - // For this test we use dbq directly to reject via admin so Cancel returns ErrNotPending - // rather than the ownership miss. We already did first cancel so row is rejected. + // Second cancel on a now-rejected row → ErrNotPending → 409. The cancel SQL + // checks user_id = $2 AND status = 'pending'; zero rows → ErrNotPending. + // For this test we use dbq directly to reject via admin so Cancel returns + // ErrNotPending rather than the ownership miss. We already did first cancel + // so row is rejected. w2 := doCancelRequest(h, alice, rv.ID) if w2.Code != http.StatusConflict { t.Fatalf("second cancel: status = %d, want 409; body = %s", w2.Code, w2.Body.String()) @@ -383,7 +387,8 @@ func TestHandleCancelRequest_WrongUser_409(t *testing.T) { rv := createArtistRequest(t, h, alice, "cancel-wrong-user-mbid", "Alice's Artist") - // Bob tries to cancel Alice's request: SQL WHERE user_id=bob AND status=pending → 0 rows. + // Bob tries to cancel Alice's request: SQL WHERE user_id=bob AND + // status=pending → 0 rows. w := doCancelRequest(h, bob, rv.ID) if w.Code != http.StatusConflict { t.Fatalf("status = %d, want 409; body = %s", w.Code, w.Body.String()) diff --git a/internal/api/search.go b/internal/api/search.go index d0ba6575..a0fa2765 100644 --- a/internal/api/search.go +++ b/internal/api/search.go @@ -127,7 +127,8 @@ func (h *handlers) resolveAlbumRefs(ctx context.Context, q *dbq.Queries, albums h.logger.Error("api: count tracks for album failed", "err", cerr) return nil, cerr } - // durationSec=0: not aggregated for search album refs per spec data flow. + // durationSec=0: not aggregated for search album refs per spec data + // flow. items = append(items, albumRefFrom(a, names[uuidToString(a.ArtistID)], int(count), 0)) } return items, nil @@ -135,7 +136,7 @@ func (h *handlers) resolveAlbumRefs(ctx context.Context, q *dbq.Queries, albums // resolveTrackRefs builds TrackRefs from dbq.Track rows: looks up the // parent album + artist by id for each track (pair of queries per track). -// At limit=200 this is 400 queries worst case — acceptable for M1.5. +// At limit=200 this is 400 queries worst case. func (h *handlers) resolveTrackRefs(ctx context.Context, q *dbq.Queries, tracks []dbq.Track) ([]TrackRef, error) { items := make([]TrackRef, 0, len(tracks)) for _, t := range tracks { diff --git a/internal/api/stream_token.go b/internal/api/stream_token.go index 9f1d70f9..b43fc1a2 100644 --- a/internal/api/stream_token.go +++ b/internal/api/stream_token.go @@ -15,10 +15,9 @@ import ( // trackID is the canonical track UUID. exp is the Unix time after which // the token is invalid; the handler checks exp before serving. // -// The secret comes from MINSTREL_STREAM_SECRET (or its auto-generated + -// persisted fallback in app_preferences). The loader lands in slice 2 -// (POST /api/cast/stream-token); this file is the primitive both the -// loader and the stream handler share. +// The secret comes from MINSTREL_STREAM_SECRET, or its auto-generated +// fallback persisted under the data directory. This file is the primitive +// the token endpoint and the stream handler share. func SignStreamToken(secret []byte, trackID string, exp int64) string { mac := hmac.New(sha256.New, secret) // hash.Hash.Write is documented as never returning an error. diff --git a/internal/api/suggestions.go b/internal/api/suggestions.go index cb1a814c..96cca9dd 100644 --- a/internal/api/suggestions.go +++ b/internal/api/suggestions.go @@ -23,13 +23,13 @@ type suggestionView struct { Score float64 `json:"score"` Attribution []seedContributionView `json:"attribution"` // MatchedTags are the candidate's tags that overlap the user's taste - // profile, strongest first (#2377) — the "matches: shoegaze, melancholic" + // profile, strongest first — the "matches: shoegaze, melancholic" // line. Omitted when empty, which is common: tag coverage for - // out-of-library artists is permanently partial (#2376), and the card - // falls back to the seed attribution it has always shown. + // out-of-library artists is permanently partial, and the card falls back + // to the seed attribution. MatchedTags []string `json:"matched_tags,omitempty"` // ImageURL is Minstrel's catalogue art endpoint for the candidate - // (out-of-library artists have no local art row, M502 #5519). It + // (out-of-library artists have no local art row). It // answers 404 when no enabled source has an image, and the client // falls back to a placeholder. ImageURL string `json:"image_url,omitempty"` @@ -75,7 +75,7 @@ func (h *handlers) handleListSuggestions(w http.ResponseWriter, r *http.Request) } // Read the tuned weight per request so an admin change takes effect on the - // next refresh, no restart (rule #25). + // next refresh, no restart. tagWeight := h.recSettings.Discover().TagOverlapWeight suggestions, err := recommendation.SuggestArtists( r.Context(), h.pool, user.ID, halfLife, limit, tagWeight) @@ -107,8 +107,8 @@ func (h *handlers) handleListSuggestions(w http.ResponseWriter, r *http.Request) } // maxSnoozeDays caps a client-supplied duration. The DEFAULT is not here: it's -// a DB-backed knob on the admin tuning card (rule #25), read per request via -// recSettings.Discover().SnoozeDays. See #2377. +// a DB-backed knob on the admin tuning card, read per request via +// recSettings.Discover().SnoozeDays. const maxSnoozeDays = 365.0 // snoozeRequest is the POST body. Both fields are optional in the JSON sense @@ -136,8 +136,8 @@ type snoozeView struct { // snoozing an already-snoozed candidate extends it rather than conflicting. // // This is NOT negative feedback. It records no verdict on the artist and is -// never read by internal/taste — see 0049_suggestion_snoozes.up.sql for the -// rule #101 reasoning. Returns 204. +// never read by internal/taste — see 0049_suggestion_snoozes.up.sql for why. +// Returns 204. func (h *handlers) handleSnoozeSuggestion(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) if !ok { @@ -222,7 +222,7 @@ func (h *handlers) handleUnsnoozeSuggestion(w http.ResponseWriter, r *http.Reque // // The un-snooze surface needs this: a parked candidate is by definition // absent from the suggestion deck, so without a list there is no way to -// reach the DELETE above. Scoped to the caller (rule #47). Expired rows are +// reach the DELETE above. Scoped to the caller. Expired rows are // already filtered by the query — the hourly gc sweep only reclaims space. func (h *handlers) handleListSuggestionSnoozes(w http.ResponseWriter, r *http.Request) { user, ok := requireUser(w, r) @@ -248,7 +248,7 @@ func (h *handlers) handleListSuggestionSnoozes(w http.ResponseWriter, r *http.Re } // resolveSuggestionArt points each suggestion's ImageURL at Minstrel's -// catalogue art endpoint (M502 #5519). Nothing is fetched here: the image is +// catalogue art endpoint. Nothing is fetched here: the image is // looked up from the enabled cover-art sources when the browser asks for it, // and kept, so listing suggestions makes no outbound call. A candidate with // no MBID keeps an empty ImageURL and the client renders its placeholder. diff --git a/internal/api/types.go b/internal/api/types.go index 31af1d17..b68594c5 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -7,7 +7,7 @@ import ( ) // LoginRequest is the POST /api/auth/login body. Kept boring on purpose — -// web and Flutter both send the same payload. +// web and Android send the same payload. type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` @@ -15,7 +15,8 @@ type LoginRequest struct { // LoginResponse carries the opaque session token AND the authenticated user // so the SPA can hydrate its auth store in one round-trip. The token is also -// set as a cookie; SPAs ignore the body token, Flutter uses it as bearer. +// set as a cookie; the SPA ignores the body token, a native client may send +// it as a bearer. type LoginResponse struct { Token string `json:"token"` User UserView `json:"user"` @@ -82,7 +83,7 @@ type TrackRef struct { DiscNumber int `json:"disc_number,omitempty"` DurationSec int `json:"duration_sec"` StreamURL string `json:"stream_url"` - // Unavailable reports that the file is missing from disk (#2704). + // Unavailable reports that the file is missing from disk. // // Present on every TrackRef rather than only where it can be true: the // selection surfaces (recommendation, discover, mixes, browse) filter @@ -99,11 +100,11 @@ type TrackRef struct { type ArtistDetail struct { ArtistRef Albums []AlbumRef `json:"albums"` - // Genres carried by this artist's tracks, for quick-jump chips (#367). + // Genres carried by this artist's tracks, for quick-jump chips. // Always non-nil at JSON so the client can iterate without a null check. Genres []string `json:"genres"` - // MBID is the artist's MusicBrainz id, which following is keyed by - // (M502 #5520). Empty when the tags never named one. + // MBID is the artist's MusicBrainz id, which following is keyed by. + // Empty when the tags never named one. MBID string `json:"mbid"` } @@ -111,7 +112,7 @@ type ArtistDetail struct { type AlbumDetail struct { AlbumRef Tracks []TrackRef `json:"tracks"` - // Genres carried by this album's tracks, for quick-jump chips (#367). + // Genres carried by this album's tracks, for quick-jump chips. // Derived from the tracks rather than stored on the album, because genre // lives on tracks and an album's tracks can disagree. Non-nil at JSON. Genres []string `json:"genres"` diff --git a/internal/config/config.go b/internal/config/config.go index a7d978fa..645a21e3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -112,9 +112,10 @@ func Default() Config { RadioSizeMax: 200, }, Library: LibraryConfig{ - // On by default so a fresh install populates its library on - // first boot with no manual step. Scans are incremental (unchanged files skipped by - // mtime), so every later restart is just a cheap directory walk. + // On by default so a fresh install populates its library on first + // boot with no manual step. Scans are incremental (unchanged files + // skipped by mtime), so every later restart is just a cheap + // directory walk. ScanOnStartup: true, CoverArtFromMBCAA: true, }, @@ -151,13 +152,15 @@ const streamSecretBytes = 64 const streamSecretFile = "stream_secret" // resolveStreamSecret populates cfg.StreamSecret using, in order: -// 1. MINSTREL_STREAM_SECRET env var (base64-url encoded — accepts either padded or raw form). +// 1. MINSTREL_STREAM_SECRET env var (base64-url encoded — accepts either +// padded or raw form). // 2. /stream_secret if present (auto-generated on a // previous boot; survives restarts so signed URLs stay valid). // 3. Auto-generate streamSecretBytes random bytes, persist them to that // file at 0600, then use them. // -// Logs (only) when an auto-generation happens, so it shows on first boot. Never logs the contents. +// Logs (only) when an auto-generation happens, so it shows on first boot. Never +// logs the contents. // // The loader runs before slog is initialized in cmd/minstrel/main.go; // log.Printf is the project's pre-logger convention. diff --git a/internal/playlists/system_mixes_db_test.go b/internal/playlists/system_mixes_db_test.go index 0445b058..556150f1 100644 --- a/internal/playlists/system_mixes_db_test.go +++ b/internal/playlists/system_mixes_db_test.go @@ -150,9 +150,9 @@ func TestListDeepCutsTracks_SkippedTracksExcluded(t *testing.T) { } func TestListOnThisDayTracks_WrapsYearBoundary(t *testing.T) { - // The day-of-year window wraps the year: plain ABS would make Dec 28 vs - // Jan 3 read as 359 days apart. The build date parameter anchors "today" so the wrap - // is testable at any run date. + // The day-of-year window wraps the year: plain ABS would make Dec 28 vs Jan + // 3 read as 359 days apart. The build date parameter anchors "today" so the + // wrap is testable at any run date. pool := newPool(t) q := dbq.New(pool) u := seedUser(t, pool, "otd1") diff --git a/internal/playsessions/service.go b/internal/playsessions/service.go index 3b3d2d4b..9489e48b 100644 --- a/internal/playsessions/service.go +++ b/internal/playsessions/service.go @@ -1,6 +1,6 @@ -// Package playsessions implements the listening-session lifecycle: a session is a contiguous sequence of plays by a user with no -// gap longer than the configured timeout (default 30 minutes) between -// events. +// Package playsessions implements the listening-session lifecycle: a session is +// a contiguous sequence of plays by a user with no gap longer than the +// configured timeout (default 30 minutes) between events. package playsessions import ( @@ -34,7 +34,8 @@ func FindOrCreate( return pgtype.UUID{}, err } if err == nil { - // "Within window" includes the exact-boundary case: gap <= timeout extends. + // "Within window" includes the exact-boundary case: gap <= timeout + // extends. gap := eventTime.Sub(prev.LastEventAt.Time) if gap <= timeout { if err := q.TouchPlaySessionLastEvent(ctx, dbq.TouchPlaySessionLastEventParams{ diff --git a/internal/recommendation/candidates.go b/internal/recommendation/candidates.go index ace767cb..faf67cf0 100644 --- a/internal/recommendation/candidates.go +++ b/internal/recommendation/candidates.go @@ -190,9 +190,10 @@ func LoadCandidatesFromSimilarity( Limit_5: int32(limits.RandomFill), Limit_6: int32(limits.TasteOverlap), Limit_7: int32(limits.UserCoplay), - // The randomised arms order by md5(id || this) rather than random(), - // so the caller decides whether the draw repeats: a per-(user, day) seed for the system - // mixes that promise daily determinism, a fresh one per radio request. + // The randomised arms order by md5(id || this) rather than random(), so + // the caller decides whether the draw repeats: a per-(user, day) seed + // for the system mixes that promise daily determinism, a fresh one per + // radio request. Column12: orderSeed, }) if err != nil { diff --git a/internal/server/release_version_test.go b/internal/server/release_version_test.go index d4be48b6..20407f30 100644 --- a/internal/server/release_version_test.go +++ b/internal/server/release_version_test.go @@ -181,9 +181,9 @@ var jobBoundary = regexp.MustCompile(`\n\n [^ \n]`) // // The naive cut — "up to the next `- name:`" — silently returns an EMPTY body // for the last step in a job, and every assertion over it then passes -// vacuously: a check that reads as coverage while asserting on nothing. Cutting at a blank line followed by -// job-level indentation handles the last-step case, which is exactly where -// `Build and push` sits. +// vacuously: a check that reads as coverage while asserting on nothing. Cutting +// at a blank line followed by job-level indentation handles the last-step case, +// which is exactly where `Build and push` sits. func stepBody(t *testing.T, yaml, name string) string { t.Helper() i := strings.Index(yaml, "- name: "+name) diff --git a/internal/server/requestlog_test.go b/internal/server/requestlog_test.go index 24ecf3b4..29af63b8 100644 --- a/internal/server/requestlog_test.go +++ b/internal/server/requestlog_test.go @@ -109,9 +109,10 @@ func TestRequestLog_AttributesPresent(t *testing.T) { } } -// Behind a proxy, the access log's `remote` must be the client rather than the proxy, and must agree with what the -// Active-sessions surface records for the same request. Logs and UI -// disagreeing about who connected is worse than either being wrong alone. +// Behind a proxy, the access log's `remote` must be the client rather than the +// proxy, and must agree with what the Active-sessions surface records for the +// same request. Logs and UI disagreeing about who connected is worse than +// either being wrong alone. func TestRequestLog_RemoteHonoursTrustedProxyDepth(t *testing.T) { cases := []struct { name string diff --git a/internal/wanted/match.go b/internal/wanted/match.go index a536299b..c1fe3fa8 100644 --- a/internal/wanted/match.go +++ b/internal/wanted/match.go @@ -18,11 +18,11 @@ import ( // Matching a library album to its MusicBrainz release group. // -// Music from any mainstream source was matched against MusicBrainz on its -// way in, so nearly every album has a group. The ids its tags carry are -// taken as they are. Without them, the groups called what the album is called, from -// the artist's discography and then from a search, are checked against the -// album's own tracks, and the one its tracks agree with is taken. +// Music from any mainstream source was matched against MusicBrainz on its way +// in, so nearly every album has a group. The ids its tags carry are taken as +// they are. Without them, the groups called what the album is called, from the +// artist's discography and then from a search, are checked against the album's +// own tracks, and the one its tracks agree with is taken. // Match methods, as library_album_matches records them. const (