From 9426dc2eebb81e313a45160812bcc34f78cdc3dc Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 27 Apr 2026 08:04:23 -0400 Subject: [PATCH] feat(config): add recommendation section (weighted shuffle weights) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BaseWeight, LikeBoost, RecencyWeight, SkipPenalty, JitterMagnitude, RecentlyPlayedHours, RadioSize, RadioSizeMax. Defaults match spec §6. --- config.example.yaml | 18 ++++++++++++++++++ internal/config/config.go | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index b9a2ed80..6cc062d9 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -45,3 +45,21 @@ events: # this threshold AND duration played (ms) is below the next threshold. skip_max_completion_ratio: 0.5 skip_max_duration_played_ms: 30000 + +recommendation: + # Base score every candidate gets before adjustments. Spec §6. + base_weight: 1.0 + # Bonus for tracks the user has liked (general_likes). + like_boost: 2.0 + # Multiplier on recency_decay (1.0 for tracks ≥ 30 days stale, 0 for fresh). + recency_weight: 1.0 + # Penalty multiplier on skip_ratio (skips/plays); 1.0 = full penalty. + skip_penalty: 1.0 + # ± random jitter applied to every candidate; breaks ties without dominating. + jitter_magnitude: 0.1 + # Hard-suppression window: tracks played within this many hours never appear. + recently_played_hours: 1 + # Default radio size when ?limit= is not specified. + radio_size: 50 + # Maximum allowed ?limit= (server caps to this regardless). + radio_size_max: 200 diff --git a/internal/config/config.go b/internal/config/config.go index af16e97d..2a6a2a39 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,13 +10,14 @@ import ( ) type Config struct { - Server ServerConfig `yaml:"server"` - Database DatabaseConfig `yaml:"database"` - Log LogConfig `yaml:"log"` - Auth AuthConfig `yaml:"auth"` - Library LibraryConfig `yaml:"library"` - Subsonic SubsonicConfig `yaml:"subsonic"` - Events EventsConfig `yaml:"events"` + Server ServerConfig `yaml:"server"` + Database DatabaseConfig `yaml:"database"` + Log LogConfig `yaml:"log"` + Auth AuthConfig `yaml:"auth"` + Library LibraryConfig `yaml:"library"` + Subsonic SubsonicConfig `yaml:"subsonic"` + Events EventsConfig `yaml:"events"` + Recommendation RecommendationConfig `yaml:"recommendation"` } type ServerConfig struct { @@ -63,6 +64,19 @@ type EventsConfig struct { SkipMaxDurationPlayedMs int `yaml:"skip_max_duration_played_ms"` } +// RecommendationConfig governs the M3 weighted-shuffle scoring (spec §6). +// All weights are operator-tunable; defaults match the spec recommendations. +type RecommendationConfig struct { + BaseWeight float64 `yaml:"base_weight"` + LikeBoost float64 `yaml:"like_boost"` + RecencyWeight float64 `yaml:"recency_weight"` + SkipPenalty float64 `yaml:"skip_penalty"` + JitterMagnitude float64 `yaml:"jitter_magnitude"` + RecentlyPlayedHours int `yaml:"recently_played_hours"` + RadioSize int `yaml:"radio_size"` + RadioSizeMax int `yaml:"radio_size_max"` +} + func Default() Config { return Config{ Server: ServerConfig{Address: ":4533"}, @@ -73,6 +87,16 @@ func Default() Config { SkipMaxCompletionRatio: 0.5, SkipMaxDurationPlayedMs: 30000, }, + Recommendation: RecommendationConfig{ + BaseWeight: 1.0, + LikeBoost: 2.0, + RecencyWeight: 1.0, + SkipPenalty: 1.0, + JitterMagnitude: 0.1, + RecentlyPlayedHours: 1, + RadioSize: 50, + RadioSizeMax: 200, + }, } }