docs: plan-era "v1", "Task N" and "Phase N" wording restated as facts (#6277)
release / govulncheck (push) Successful in 25s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m47s
release / integration (push) Successful in 4m35s
release / android (push) Successful in 7m5s
release / Build signed APK (releases and dev) (push) Successful in 7m16s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m1s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 25s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m47s
release / integration (push) Successful in 4m35s
release / android (push) Successful in 7m5s
release / Build signed APK (releases and dev) (push) Successful in 7m16s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m1s
release / Verify release artifacts (tag releases only) (push) Skipped
The marker list behind the earlier server and web passes did not catch "for v1", "Task 5", "Phase 2" or "today: Flutter". Those comments now say what the code does. Corrected: coverart said "both v1 providers" ship on. There are four providers (MBCAA, TheAudioDB, Deezer, Last.fm), and Last.fm, which needs a key, ships off. Non-comment tokens unchanged against HEAD: 10 Go files, 7 TS/Svelte files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,9 +41,8 @@ func TestLibrarySync_AfterArtistUpsert_ReturnsHydratedPayload(t *testing.T) {
|
|||||||
truncateLibrary(t, pool)
|
truncateLibrary(t, pool)
|
||||||
u := seedUser(t, pool, "alice", "x", false)
|
u := seedUser(t, pool, "alice", "x", false)
|
||||||
|
|
||||||
// Seed an artist + write a change row in the same tx — that's the
|
// Seed an artist + write a change row in the same tx — the invariant
|
||||||
// invariant the scanner wiring (Task 5) will enforce, and the
|
// library writes keep, and the handler relies on.
|
||||||
// handler relies on.
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
tx, err := pool.Begin(ctx)
|
tx, err := pool.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,8 +121,8 @@ func TestLibrarySync_DeleteIsReflected(t *testing.T) {
|
|||||||
u := seedUser(t, pool, "alice", "x", false)
|
u := seedUser(t, pool, "alice", "x", false)
|
||||||
|
|
||||||
// Just write a delete change row directly (no underlying mutation).
|
// Just write a delete change row directly (no underlying mutation).
|
||||||
// Scanner wiring (Task 5) is what couples real deletes to LogChange;
|
// Library writes couple real deletes to LogChange; here we test the
|
||||||
// here we test the handler reflects what's in the log.
|
// handler reflects what's in the log.
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
tx, _ := pool.Begin(ctx)
|
tx, _ := pool.Begin(ctx)
|
||||||
_ = sync.LogChange(ctx, tx, sync.EntityArtist, "deadbeef-dead-beef-dead-beefdeadbeef", sync.OpDelete)
|
_ = sync.LogChange(ctx, tx, sync.EntityArtist, "deadbeef-dead-beef-dead-beefdeadbeef", sync.OpDelete)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type updateProfileReq struct {
|
|||||||
// nil = leave unchanged; non-nil empty string = clear; non-nil
|
// nil = leave unchanged; non-nil empty string = clear; non-nil
|
||||||
// non-empty = update to that value. Note: distinguishing
|
// non-empty = update to that value. Note: distinguishing
|
||||||
// omit-vs-clear at JSON-decode time requires custom unmarshaling
|
// omit-vs-clear at JSON-decode time requires custom unmarshaling
|
||||||
// or a sentinel; for v1 we use the simpler "empty string clears"
|
// or a sentinel; we use the simpler "empty string clears"
|
||||||
// semantics: any field present in the JSON body is applied.
|
// semantics: any field present in the JSON body is applied.
|
||||||
DisplayName *string `json:"display_name"`
|
DisplayName *string `json:"display_name"`
|
||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type Provider interface {
|
|||||||
RequiresAPIKey() bool
|
RequiresAPIKey() bool
|
||||||
|
|
||||||
// DefaultEnabled is whether a freshly-installed Minstrel ships
|
// DefaultEnabled is whether a freshly-installed Minstrel ships
|
||||||
// with this provider on. Both v1 providers return true.
|
// with this provider on. Providers that need an API key ship off.
|
||||||
DefaultEnabled() bool
|
DefaultEnabled() bool
|
||||||
|
|
||||||
// Configure receives the admin's settings (api_key, enabled
|
// Configure receives the admin's settings (api_key, enabled
|
||||||
@@ -102,8 +102,8 @@ type ArtistArtProvider interface {
|
|||||||
|
|
||||||
// TestableProvider is an opt-in capability: providers that can answer
|
// TestableProvider is an opt-in capability: providers that can answer
|
||||||
// "is my config working?" without performing a full enrichment cycle.
|
// "is my config working?" without performing a full enrichment cycle.
|
||||||
// Used by the admin Test-Connection button. v1: both MBCAA and
|
// Used by the admin Test-Connection button. MBCAA and TheAudioDB
|
||||||
// TheAudioDB implement this.
|
// implement this.
|
||||||
type TestableProvider interface {
|
type TestableProvider interface {
|
||||||
Provider
|
Provider
|
||||||
TestConnection(ctx context.Context) error
|
TestConnection(ctx context.Context) error
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const (
|
|||||||
|
|
||||||
// theAudioDBMinPeriod enforces TheAudioDB's documented free/test
|
// theAudioDBMinPeriod enforces TheAudioDB's documented free/test
|
||||||
// rate limit (2 req/sec). Patreon supporter keys allow higher
|
// rate limit (2 req/sec). Patreon supporter keys allow higher
|
||||||
// rates; we don't differentiate in v1 — operators with supporter
|
// rates; we don't differentiate — supporter keys get the same
|
||||||
// keys experience the conservative 2 req/sec ceiling.
|
// conservative 2 req/sec ceiling.
|
||||||
theAudioDBMinPeriod = 500 * time.Millisecond
|
theAudioDBMinPeriod = 500 * time.Millisecond
|
||||||
|
|
||||||
// theAudioDBMaxRetries is the number of retry attempts on 429 /
|
// theAudioDBMaxRetries is the number of retry attempts on 429 /
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Package eventbus is an in-process publish/subscribe bus for real-time
|
// Package eventbus is an in-process publish/subscribe bus for real-time
|
||||||
// events the server emits to subscribed clients (today: Flutter via SSE).
|
// events the server emits to subscribed clients (the web and Android apps,
|
||||||
|
// over SSE).
|
||||||
//
|
//
|
||||||
// Writers (likes handler, requests reconciler, scanner, etc.) call Publish
|
// Writers (likes handler, requests reconciler, scanner, etc.) call Publish
|
||||||
// to announce a state change. Each connected SSE client holds a
|
// to announce a state change. Each connected SSE client holds a
|
||||||
@@ -13,9 +14,9 @@
|
|||||||
// - No persistence. A client that connects after an event was emitted
|
// - No persistence. A client that connects after an event was emitted
|
||||||
// will not see that event. SSE clients are expected to fetch a fresh
|
// will not see that event. SSE clients are expected to fetch a fresh
|
||||||
// snapshot via the normal /api/* endpoints on (re)connect.
|
// snapshot via the normal /api/* endpoints on (re)connect.
|
||||||
// - No cross-process broadcast. v1 runs as a single Go binary; a future
|
// - No cross-process broadcast. Minstrel runs as a single Go binary; a
|
||||||
// multi-instance deployment will need a real broker (NATS / Redis
|
// multi-instance deployment would need a real broker (NATS / Redis
|
||||||
// pub-sub / etc.) but that's out of scope here.
|
// pub-sub / etc.).
|
||||||
package eventbus
|
package eventbus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ func (s *SMTPSender) Send(ctx context.Context, to, subject, textBody, htmlBody s
|
|||||||
// We respect the use_tls flag: if true, we require STARTTLS via
|
// We respect the use_tls flag: if true, we require STARTTLS via
|
||||||
// smtp.SendMail's default behavior (which negotiates STARTTLS when
|
// smtp.SendMail's default behavior (which negotiates STARTTLS when
|
||||||
// the server advertises it). For implicit-TLS (port 465 style),
|
// the server advertises it). For implicit-TLS (port 465 style),
|
||||||
// we'd need a custom dial — skip that complexity for v1 and rely on
|
// we'd need a custom dial; instead this relies on STARTTLS via port
|
||||||
// STARTTLS via port 587 (the default).
|
// 587 (the default).
|
||||||
dialCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
dialCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_ = dialCtx // smtp.SendMail doesn't accept ctx; the timeout is enforced via Dialer below
|
_ = dialCtx // smtp.SendMail doesn't accept ctx; the timeout is enforced via Dialer below
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ type CandidateSourceLimits struct {
|
|||||||
UserCoplay int
|
UserCoplay int
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultCandidateSourceLimits returns the v1 hardcoded constants per spec.
|
// DefaultCandidateSourceLimits returns the hardcoded per-source limits.
|
||||||
func DefaultCandidateSourceLimits() CandidateSourceLimits {
|
func DefaultCandidateSourceLimits() CandidateSourceLimits {
|
||||||
return CandidateSourceLimits{
|
return CandidateSourceLimits{
|
||||||
LBSimilar: 30,
|
LBSimilar: 30,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package recommendation
|
package recommendation
|
||||||
|
|
||||||
// SimilarityWeights balances the per-axis contribution to the weighted Jaccard
|
// SimilarityWeights balances the per-axis contribution to the weighted Jaccard
|
||||||
// score. v1 hardcodes the defaults — operators cannot tune via YAML. If
|
// score. The defaults are hardcoded; there is no config key for them.
|
||||||
// telemetry justifies it, expose under recommendation.similarity.* later.
|
|
||||||
type SimilarityWeights struct {
|
type SimilarityWeights struct {
|
||||||
TagsWeight float64
|
TagsWeight float64
|
||||||
ArtistsWeight float64
|
ArtistsWeight float64
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ func parseUUID(s string) (pgtype.UUID, bool) {
|
|||||||
return u, u.Valid
|
return u, u.Valid
|
||||||
}
|
}
|
||||||
|
|
||||||
// MusicFolder matches Subsonic's /rest/getMusicFolders. v1 always reports a
|
// MusicFolder matches Subsonic's /rest/getMusicFolders. Minstrel always
|
||||||
// single folder because Minstrel has no multi-library concept yet.
|
// reports a single folder: it has no multi-library concept.
|
||||||
type MusicFolder struct {
|
type MusicFolder struct {
|
||||||
XMLName xml.Name `json:"-" xml:"musicFolder"`
|
XMLName xml.Name `json:"-" xml:"musicFolder"`
|
||||||
ID int `json:"id" xml:"id,attr"`
|
ID int `json:"id" xml:"id,attr"`
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func TestRegisterDuplicatePanics(t *testing.T) {
|
|||||||
Register(&fakeProvider{id: "dup"})
|
Register(&fakeProvider{id: "dup"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBuiltinProvidersRegister confirms the two v1 sources register with
|
// TestBuiltinProvidersRegister confirms the two built-in sources register with
|
||||||
// the expected identities/defaults. Runs against a fresh registry so it is
|
// the expected identities/defaults. Runs against a fresh registry so it is
|
||||||
// independent of test ordering: re-register constructed instances.
|
// independent of test ordering: re-register constructed instances.
|
||||||
func TestBuiltinProvidersRegister(t *testing.T) {
|
func TestBuiltinProvidersRegister(t *testing.T) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export const BROWSE_PAGE_SIZE = 50;
|
|||||||
|
|
||||||
// Genres are the raw ID3 strings, split on [;,] server-side but otherwise
|
// Genres are the raw ID3 strings, split on [;,] server-side but otherwise
|
||||||
// untouched — no case folding, no synonym mapping. So "Rock" and "rock" can
|
// untouched — no case folding, no synonym mapping. So "Rock" and "rock" can
|
||||||
// both appear, as can "Rock/Pop" beside "Rock" and "Pop". Deliberate for v1:
|
// both appear, as can "Rock/Pop" beside "Rock" and "Pop". Deliberate: the
|
||||||
// the raw spread has to be visible before anyone can judge whether it needs
|
// raw spread has to be visible before anyone can judge whether it needs
|
||||||
// normalising.
|
// normalising.
|
||||||
export type GenreCount = { genre: string; track_count: number };
|
export type GenreCount = { genre: string; track_count: number };
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
// The server-side filter hides this candidate on next refetch.
|
// The server-side filter hides this candidate on next refetch.
|
||||||
await client.invalidateQueries({ queryKey: qk.suggestions() });
|
await client.invalidateQueries({ queryKey: qk.suggestions() });
|
||||||
} catch {
|
} catch {
|
||||||
// Swallow for v1; the SPA will refetch on next mount and the card
|
// Swallowed: the SPA will refetch on next mount and the card
|
||||||
// stays requestable so the user can retry.
|
// stays requestable so the user can retry.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export function useEventsDispatcher(): void {
|
|||||||
openTrackId = trackId;
|
openTrackId = trackId;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort; missed events are OK in v1.
|
// Best-effort; a missed event is acceptable.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ describe('player store — basic actions', () => {
|
|||||||
// play anyway — tested via report later. Here, simulate the paused state:
|
// play anyway — tested via report later. Here, simulate the paused state:
|
||||||
reportTimeUpdate(0);
|
reportTimeUpdate(0);
|
||||||
// manually put us in paused state by calling togglePlay twice isn't clean;
|
// manually put us in paused state by calling togglePlay twice isn't clean;
|
||||||
// set via the state-report API in Task 2. Instead, test the simpler case:
|
// it is set via the state-report API. Instead, test the simpler case:
|
||||||
// playQueue then togglePlay = still 'loading' (intent is to play).
|
// playQueue then togglePlay = still 'loading' (intent is to play).
|
||||||
togglePlay(); // from 'loading' — inverts intent; store moves to 'paused' intent
|
togglePlay(); // from 'loading' — inverts intent; store moves to 'paused' intent
|
||||||
expect(player.state).toBe('paused');
|
expect(player.state).toBe('paused');
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Phase 2 prefetch: when the current track passes the halfway mark,
|
// Prefetch: when the current track passes the halfway mark,
|
||||||
// start loading the next track's bytes into a hidden <audio> element.
|
// start loading the next track's bytes into a hidden <audio> element.
|
||||||
// The browser HTTP cache + audio buffer warm-up means the swap on
|
// The browser HTTP cache + audio buffer warm-up means the swap on
|
||||||
// track-end is gap-free on slow networks. Reset src when no next
|
// track-end is gap-free on slow networks. Reset src when no next
|
||||||
|
|||||||
@@ -94,8 +94,8 @@
|
|||||||
// Surface the new request on /requests right away (+ start its poll).
|
// Surface the new request on /requests right away (+ start its poll).
|
||||||
queryClient.invalidateQueries({ queryKey: qk.myRequests() });
|
queryClient.invalidateQueries({ queryKey: qk.myRequests() });
|
||||||
} catch {
|
} catch {
|
||||||
// Swallow for v1: error toasts land in a later UX pass. The card stays
|
// Swallowed, with no toast: the card stays in 'requestable' so the
|
||||||
// in 'requestable' so the user can retry.
|
// user can retry.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
await unflagTrack(trackID);
|
await unflagTrack(trackID);
|
||||||
await client.invalidateQueries({ queryKey: qk.myQuarantine() });
|
await client.invalidateQueries({ queryKey: qk.myQuarantine() });
|
||||||
} catch {
|
} catch {
|
||||||
// Silent in v1; the SPA will refetch on next mount.
|
// Silent; the SPA will refetch on next mount.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user