Comment blocks in cmd/minstrel and 30 smaller internal packages, rewritten
to rule 32's standard with each citation read in context: task, milestone
and rule ids go, and so do the stories they were attached to ("used to",
"Before #N", measurements from one install, quotes, pointers to Fabled
Librarian's records and to docs/superpowers files that are not in the
repo). Where a story wrapped a real constraint, the constraint stays as a
present-tense fact.
Comments that had gone stale are corrected:
- taste.Config: the tuned values are overlaid by internal/recsettings;
it is no longer "a later follow-up".
- StorageConfig: the data dir holds art, caches and the stream secret,
not only playlist covers.
- the Subsonic album list and getUser comments describe what they do
today rather than a milestone that would change it.
Checked: the non-comment token stream of every changed file matches
HEAD, and gofmt -l is empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
2.8 KiB
Go
86 lines
2.8 KiB
Go
// Package coplay builds household co-play similarity edges. Minstrel is a
|
||
// single shared-library, multi-user server (no per-user library ACL), so the
|
||
// "household" is the whole instance's user set. A periodic worker recomputes
|
||
// artist–artist co-occurrence from play_events entirely in SQL (no external
|
||
// API) and stores the edges in artist_similarity under
|
||
// source='user_cooccurrence', which the radio/mix candidate pool consumes as
|
||
// a collaborative arm. Single-user servers produce no edges (the >= 2 co-player
|
||
// gate), so the feature is a graceful no-op there.
|
||
package coplay
|
||
|
||
import (
|
||
"context"
|
||
"log/slog"
|
||
"time"
|
||
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||
)
|
||
|
||
// minJaccard is the co-play edge floor: prune weak pairs and self-limit hub
|
||
// artists, whose large combined-audience denominators pull their Jaccard down.
|
||
// 0.1 keeps only pairs whose co-player overlap is a real fraction of their
|
||
// combined audience.
|
||
const minJaccard = 0.1
|
||
|
||
// Worker periodically rebuilds the household co-play edge set. Pure local SQL,
|
||
// so there are no rate limits — but the pair self-join is roughly
|
||
// O(users × artists_per_user²), so it ticks slowly: co-listening shifts over
|
||
// days, not minutes, and a frequent rebuild would burn CPU for no fresher
|
||
// signal.
|
||
type Worker struct {
|
||
pool *pgxpool.Pool
|
||
logger *slog.Logger
|
||
tick time.Duration
|
||
}
|
||
|
||
// NewWorker constructs a worker with the production default 6h tick.
|
||
func NewWorker(pool *pgxpool.Pool, logger *slog.Logger) *Worker {
|
||
return &Worker{pool: pool, logger: logger, tick: 6 * time.Hour}
|
||
}
|
||
|
||
// Run rebuilds once at startup (so edges exist without waiting a full tick),
|
||
// then every w.tick until ctx is cancelled. Runs in its own goroutine, so a
|
||
// slow initial rebuild never blocks boot.
|
||
func (w *Worker) Run(ctx context.Context) {
|
||
if err := w.rebuild(ctx); err != nil {
|
||
w.logger.Error("coplay: initial rebuild failed", "err", err)
|
||
}
|
||
t := time.NewTicker(w.tick)
|
||
defer t.Stop()
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-t.C:
|
||
if err := w.rebuild(ctx); err != nil {
|
||
w.logger.Error("coplay: rebuild failed", "err", err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// rebuild atomic-replaces the user_cooccurrence edge set inside one
|
||
// transaction — delete the old edges, recompute from current play history — so
|
||
// the candidate pool never observes a half-built set.
|
||
func (w *Worker) rebuild(ctx context.Context) error {
|
||
tx, err := w.pool.Begin(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer func() { _ = tx.Rollback(ctx) }()
|
||
q := dbq.New(tx)
|
||
if err := q.DeleteArtistCoplayEdges(ctx); err != nil {
|
||
return err
|
||
}
|
||
if err := q.InsertArtistCoplayEdges(ctx, minJaccard); err != nil {
|
||
return err
|
||
}
|
||
if err := tx.Commit(ctx); err != nil {
|
||
return err
|
||
}
|
||
w.logger.Debug("coplay: edges rebuilt")
|
||
return nil
|
||
}
|