arnaucube
arnaucube/configsPublic Self-hosted repository
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}