Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultWeights = Weights{
Similarity: 0.40,
Popularity: 0.20,
ArtistNovelty: 0.35,
Serendipity: 0.05,
}
DefaultWeights are tuned to surface discovery over same-artist safety.
The ArtistNovelty signal is the key fix for the "filter niche" problem: without it, high-similarity same-artist tracks dominate because nothing rewards stepping outside the seed artist's orbit. With it, a track by a genuinely different artist at 0.75 similarity beats a same-artist track at 1.0 similarity — which is closer to how good human curation feels.
Functions ¶
This section is empty.
Types ¶
type Candidate ¶
type Candidate struct {
Track string
Artist string
Score float64
Path string // discovery path, shown in dev mode
Signals []Signal // per-signal breakdown, populated when dev mode is on
// Playback metadata — populated by the ytmusic layer, not the scorer.
// Empty on candidates straight from the recommender; resolved lazily
// at play/preload time.
VideoID string // YouTube Music video id (skips the yt-dlp search step)
DurationSec int // native track length in seconds (0 = unknown)
ArtURL string // album-art thumbnail URL (preferred over Last.fm)
// Source/playback routing.
Source string // "youtube" (default/empty), "subsonic", "library"
StreamURL string // direct, already-playable audio URL — when set, playback
}
Candidate is a track under consideration for recommendation.
type DataSource ¶
type DataSource interface {
GetSimilarTracks(artist, track string, limit int) ([]lastfm.SimilarTrack, error)
GetTrackTags(artist, track string) ([]string, error)
GetSimilarArtists(artist string, limit int) ([]lastfm.SimilarArtist, error)
GetArtistTopTracks(artist string, limit int) ([]lastfm.TopTrack, error)
}
DataSource abstracts where music similarity data comes from. *lastfm.Client, *store.Hybrid, and any test double all satisfy this interface.
type Recommender ¶
type Recommender struct {
Weights Weights
Dev bool
// DeepCuts skips the top 3 most-played tracks per artist expansion and
// takes the next 5 instead — surfaces album cuts over radio staples.
DeepCuts bool
// ExcludeArtists is a set of lowercase artist names to omit entirely
// from the candidate pool before scoring. Populated from --no-artist.
ExcludeArtists map[string]bool
// Affinity boosts candidates whose artist you've liked/played (lowercase
// artist → 0..1). Populated from the local library so recs lean toward
// what you already love. Applied as a small additive bonus when scoring.
Affinity map[string]float64
// contains filtered or unexported fields
}
Recommender is the core engine. Create one per request; it holds no state between calls.
func New ¶
func New(src DataSource, dev bool) *Recommender
func (*Recommender) Recommend ¶
func (r *Recommender) Recommend(artist, track string, n int) ([]Candidate, error)
Recommend returns the top n recommendations for the given seed track. It fans out multiple Last.fm API calls concurrently, scores candidates, applies per-artist diversity caps, and returns ranked results.
func (*Recommender) SeedTags ¶
func (r *Recommender) SeedTags(artist, track string) []string
SeedTags returns the genre/mood tags of the seed track, for display in dev mode.
type Signal ¶
type Signal struct {
Name string
Raw float64 // normalized 0–1 value
Weight float64
Score float64 // Raw × Weight contribution to final score
Note string
}
Signal is a single scored factor in a recommendation, exposed in dev mode.
type Weights ¶
type Weights struct {
Similarity float64 // Last.fm graph match score
Popularity float64 // anti-popularity: higher for niche artists
ArtistNovelty float64 // 1.0 for different artist, 0.0 for seed-family tracks
Serendipity float64 // deterministic jitter to prevent total determinism
}
Weights controls how each signal contributes to the final score. All weights should sum to 1.0 for interpretable scores.
func ExploreWeights ¶
ExploreWeights returns weights for a given discovery level.
0 — safe: similarity dominates, results stay close to the seed 5 — default: matches DefaultWeights exactly 10 — wild: novelty and anti-popularity take over, genre-crossing results
All returned weight sets sum to 1.0.