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}