arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
jellyfin-tools/jellyfin-year-review/internal/generate/personality.go
jellyfin-year-review/internal/generate/personality.goRaw
 1package generate
 2
 3import "reelreview/internal/model"
 4
 5// personality uses ordered, evidence-based gates; the first match wins. The final
 6// balanced label applies only after the same minimum-data gate as every other type.
 7func personality(r model.Review, exact bool) *model.Personality {
 8	if !exact || r.Records < 10 || r.Seconds < 18000 {
 9		return nil
10	}
11	sum := func(a, b int) float64 {
12		n := 0.0
13		for i := a; i < b; i++ {
14			n += r.Hourly[i]
15		}
16		return n
17	}
18	minutes := r.Seconds / 60
19	night := (sum(0, 5) + sum(23, 24)) / minutes
20	early := sum(5, 10) / minutes
21	weekend := (r.Weekdays[0] + r.Weekdays[6]) / minutes
22	repeated := float64(r.Records-r.UniqueTitles) / float64(r.Records)
23	maxMonth := 0.0
24	for _, v := range r.Monthly {
25		if v > maxMonth {
26			maxMonth = v
27		}
28	}
29	rules := []struct {
30		name, why string
31		match     bool
32	}{
33		{"Night Owl", "At least 40% of your watch time falls between 23:00 and 05:00.", night >= .4},
34		{"Early Bird", "At least 40% of your watch time falls between 05:00 and 10:00.", early >= .4},
35		{"Weekend Warrior", "At least 60% of your watch time falls on weekends.", weekend >= .6},
36		{"Marathoner", "Your longest run includes at least six watched hours.", r.Marathon != nil && r.Marathon.Watched >= 21600},
37		{"Binge Watcher", "TV dominates your viewing and your longest run spans at least three records and three watched hours.", r.TVSeconds/r.Seconds >= .7 && r.Marathon != nil && r.Marathon.Records >= 3 && r.Marathon.Watched >= 10800},
38		{"Movie Buff", "Movies account for at least 80% of your watch time.", r.MovieSeconds/r.Seconds >= .8},
39		{"Series Devotee", "TV accounts for at least 80% of your watch time.", r.TVSeconds/r.Seconds >= .8},
40		{"Comfort Rewatcher", "At least half your records revisit a previously seen title or episode.", repeated >= .5},
41		{"Genre Explorer", "You watched at least six known genres with meaningful watch time.", knownGenres(r) >= 6},
42		{"Prime-Time Regular", "At least half your watch time falls between 19:00 and 23:00.", sum(19, 23)/minutes >= .5},
43		{"Daily Viewer", "You watched on at least 180 different dates.", r.ActiveDays >= 180},
44		{"Seasonal Viewer", "At least 40% of your watch time falls in one month.", maxMonth/minutes >= .4},
45		{"Short-Session Viewer", "Your average uninterrupted record lasts at most 25 minutes.", r.Seconds/float64(r.Records) <= 1500},
46		{"Balanced Viewer", "Your viewing spans several habits without one of the stronger patterns dominating.", true},
47	}
48	for _, v := range rules {
49		if v.match {
50			return &model.Personality{Name: v.name, Explanation: v.why}
51		}
52	}
53	return nil
54}
55func knownGenres(r model.Review) int {
56	n := 0
57	for _, g := range r.Genres {
58		if g.Name != "Unknown" && g.Seconds >= r.Seconds*.03 {
59			n++
60		}
61	}
62	return n
63}