diff --git a/internal/scrobble/threshold.go b/internal/scrobble/threshold.go new file mode 100644 index 00000000..a33ecd7e --- /dev/null +++ b/internal/scrobble/threshold.go @@ -0,0 +1,15 @@ +// Package scrobble owns the outbound-scrobble pipeline: threshold detection, +// the queue write path, and the worker goroutine. The HTTP client is in +// internal/scrobble/listenbrainz; this package consumes it. +package scrobble + +// Qualifies returns true if a closed play_event meets ListenBrainz's +// recommended scrobble threshold: ≥240 seconds played OR ≥50% of the +// track length played. +// +// The two thresholds are deliberately separate from M2's skip-detection +// thresholds (which serve a different purpose — internal engine signal +// for the scoring formula's skip penalty). +func Qualifies(durationPlayedMs int, completionRatio float64) bool { + return durationPlayedMs >= 240_000 || completionRatio >= 0.5 +} diff --git a/internal/scrobble/threshold_test.go b/internal/scrobble/threshold_test.go new file mode 100644 index 00000000..93d48b73 --- /dev/null +++ b/internal/scrobble/threshold_test.go @@ -0,0 +1,42 @@ +package scrobble + +import "testing" + +func TestQualifies_NeverPlayed(t *testing.T) { + if Qualifies(0, 0) { + t.Error("0/0 should not qualify") + } +} + +func TestQualifies_AtLeast240Seconds(t *testing.T) { + if !Qualifies(240_000, 0.0) { + t.Error("240_000 ms played should qualify regardless of ratio") + } +} + +func TestQualifies_AtLeastHalfRatio(t *testing.T) { + if !Qualifies(0, 0.5) { + t.Error("ratio=0.5 should qualify regardless of duration") + } +} + +func TestQualifies_BelowBoth(t *testing.T) { + if Qualifies(120_000, 0.49) { + t.Error("120s + 49% should not qualify") + } +} + +func TestQualifies_BoundaryJustUnder(t *testing.T) { + if Qualifies(239_999, 0.4999) { + t.Error("just-under both thresholds should not qualify") + } +} + +func TestQualifies_BoundaryExactly(t *testing.T) { + if !Qualifies(240_000, 0.4999) { + t.Error("exactly 240s should qualify") + } + if !Qualifies(239_999, 0.5) { + t.Error("exactly 50% should qualify") + } +}