engine

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
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

func ExploreWeights(level int) Weights

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL