configs/server-dockers/jellyfin-news/digest.go
server-dockers/jellyfin-news/digest.goRaw
1package main
2
3import (
4 "fmt"
5 "sort"
6 "strings"
7 "time"
8)
9
10// Item is the subset of Jellyfin item metadata needed by a digest.
11type Item struct {
12 Type string
13 Name string
14 SeriesID string
15 SeriesName string
16 SeasonNumber *int
17 EpisodeNumber *int
18 ProductionYear *int
19}
20
21// Labels contains every translatable phrase used in generated recap text.
22type Labels struct {
23 WeeklyUpdate string
24 MonthlyUpdate string
25 Movies string
26 Shows string
27 Season string
28 Specials string
29 Episode string
30 Episodes string
31 UnknownShow string
32 NoWeeklyItems string
33 NoMonthlyItems string
34}
35
36func DefaultLabels() Labels {
37 return Labels{
38 WeeklyUpdate: "jellyfin weekly update",
39 MonthlyUpdate: "jellyfin monthly update",
40 Movies: "Movies",
41 Shows: "Shows",
42 Season: "Season",
43 Specials: "Specials",
44 Episode: "episode",
45 Episodes: "episodes",
46 UnknownShow: "Unknown show",
47 NoWeeklyItems: "No new movies or show episodes this week.",
48 NoMonthlyItems: "No new movies or show episodes last month.",
49 }
50}
51
52// FormatWeek creates a plain-text weekly message suitable for copying and sharing.
53func FormatWeek(start, end time.Time, items []Item, labels Labels) string {
54 title := start.Format("2006-01-02") + " - " + end.Format("2006-01-02") + " " + labels.WeeklyUpdate + ":"
55 return format(title, labels.NoWeeklyItems, items, labels)
56}
57
58// FormatMonth creates a recap for a calendar month.
59func FormatMonth(month time.Time, items []Item, labels Labels) string {
60 return format(month.Format("2006-01")+" "+labels.MonthlyUpdate+":", labels.NoMonthlyItems, items, labels)
61}
62
63func format(title, emptyMessage string, items []Item, labels Labels) string {
64 movies := make([]Item, 0)
65 type seasonKey struct {
66 seriesID, seriesName string
67 season int
68 }
69 seasons := make(map[seasonKey]map[int]struct{})
70
71 for _, item := range items {
72 switch item.Type {
73 case "Movie":
74 movies = append(movies, item)
75 case "Episode":
76 if item.SeasonNumber == nil || item.EpisodeNumber == nil {
77 continue
78 }
79 name := strings.TrimSpace(item.SeriesName)
80 if name == "" {
81 name = labels.UnknownShow
82 }
83 key := seasonKey{seriesID: item.SeriesID, seriesName: name, season: *item.SeasonNumber}
84 if seasons[key] == nil {
85 seasons[key] = make(map[int]struct{})
86 }
87 seasons[key][*item.EpisodeNumber] = struct{}{}
88 }
89 }
90
91 sort.Slice(movies, func(i, j int) bool {
92 return strings.ToLower(movies[i].Name) < strings.ToLower(movies[j].Name)
93 })
94 keys := make([]seasonKey, 0, len(seasons))
95 for key := range seasons {
96 keys = append(keys, key)
97 }
98 sort.Slice(keys, func(i, j int) bool {
99 a, b := strings.ToLower(keys[i].seriesName), strings.ToLower(keys[j].seriesName)
100 if a == b {
101 return keys[i].season < keys[j].season
102 }
103 return a < b
104 })
105
106 var out strings.Builder
107 out.WriteString(title)
108 out.WriteByte('\n')
109 if len(movies) == 0 && len(keys) == 0 {
110 out.WriteByte('\n')
111 out.WriteString(emptyMessage)
112 return out.String()
113 }
114 if len(movies) > 0 {
115 fmt.Fprintf(&out, "\n%s:\n", labels.Movies)
116 for _, movie := range movies {
117 fmt.Fprintf(&out, "• %s", movie.Name)
118 if movie.ProductionYear != nil {
119 fmt.Fprintf(&out, " (%d)", *movie.ProductionYear)
120 }
121 out.WriteByte('\n')
122 }
123 }
124 if len(keys) > 0 {
125 fmt.Fprintf(&out, "\n%s:\n", labels.Shows)
126 for _, key := range keys {
127 episodes := make([]int, 0, len(seasons[key]))
128 for episode := range seasons[key] {
129 episodes = append(episodes, episode)
130 }
131 sort.Ints(episodes)
132 season := fmt.Sprintf("%s %d", labels.Season, key.season)
133 if key.season == 0 {
134 season = labels.Specials
135 }
136 label := labels.Episodes
137 if len(episodes) == 1 {
138 label = labels.Episode
139 }
140 fmt.Fprintf(&out, "• %s — %s, %s %s\n", key.seriesName, season, label, ranges(episodes))
141 }
142 }
143 return strings.TrimRight(out.String(), "\n")
144}
145
146func ranges(numbers []int) string {
147 parts := make([]string, 0)
148 for i := 0; i < len(numbers); {
149 j := i
150 for j+1 < len(numbers) && numbers[j+1] == numbers[j]+1 {
151 j++
152 }
153 if i == j {
154 parts = append(parts, fmt.Sprint(numbers[i]))
155 } else {
156 parts = append(parts, fmt.Sprintf("%d-%d", numbers[i], numbers[j]))
157 }
158 i = j + 1
159 }
160 return strings.Join(parts, ", ")
161}