arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
jellyfin-tools/jellyfin-year-review/internal/generate/platforms.go
jellyfin-year-review/internal/generate/platforms.goRaw
  1package generate
  2
  3import (
  4	"sort"
  5	"strings"
  6
  7	"reelreview/internal/model"
  8)
  9
 10// platformName folds common service aliases without treating production studios
 11// as evidence of streaming availability.
 12func platformName(name string) string {
 13	switch strings.ToLower(strings.Join(strings.Fields(name), " ")) {
 14	case "netflix", "netflix studios":
 15		return "Netflix"
 16	case "hbo", "hbo max", "max", "home box office", "hbo films":
 17		return "HBO"
 18	case "apple tv", "apple tv+", "appletv", "appletv+", "apple tv plus", "apple original films", "apple studios":
 19		return "Apple TV+"
 20	case "amazon", "amazon prime", "amazon prime video", "prime video", "amazon studios", "amazon mgm studios":
 21		return "Prime Video"
 22	case "disney+", "disney plus":
 23		return "Disney+"
 24	case "hulu":
 25		return "Hulu"
 26	case "paramount+", "paramount plus", "cbs all access":
 27		return "Paramount+"
 28	case "peacock":
 29		return "Peacock"
 30	case "showtime":
 31		return "Showtime"
 32	case "starz":
 33		return "Starz"
 34	case "discovery+", "discovery plus":
 35		return "Discovery+"
 36	case "amc+", "amc plus":
 37		return "AMC+"
 38	case "crunchyroll":
 39		return "Crunchyroll"
 40	}
 41	return ""
 42}
 43
 44// itemPlatforms prefers explicit snapshot associations; only recognized service
 45// names are inferred from Jellyfin studios. Unknown studios remain unclassified.
 46func itemPlatforms(item model.Item) []string {
 47	seen := map[string]string{}
 48	for _, name := range item.Platforms {
 49		name = strings.Join(strings.Fields(name), " ")
 50		if canonical := platformName(name); canonical != "" {
 51			name = canonical
 52		}
 53		if name != "" {
 54			key := strings.ToLower(name)
 55			if previous, ok := seen[key]; !ok || name < previous {
 56				seen[key] = name
 57			}
 58		}
 59	}
 60	if len(seen) == 0 {
 61		for _, studio := range item.Studios {
 62			if name := platformName(studio); name != "" {
 63				seen[strings.ToLower(name)] = name
 64			}
 65		}
 66	}
 67	names := []string{}
 68	for _, name := range seen {
 69		names = append(names, name)
 70	}
 71	return names
 72}
 73
 74// platforms consumes the already filtered distinct titles, so episodes, resumes,
 75// and repeat viewings never multiply a title's contribution to a service.
 76func platforms(movies, shows []model.Content, items map[string]model.Item) []model.Platform {
 77	counts := map[string]*model.Platform{}
 78	for kind, titles := range [][]model.Content{movies, shows} {
 79		for _, title := range titles {
 80			names := itemPlatforms(items[title.ID])
 81			if len(names) == 0 {
 82				names = []string{"Other"}
 83			}
 84			for _, name := range names {
 85				key := strings.ToLower(name)
 86				row := counts[key]
 87				if row == nil {
 88					row = &model.Platform{Name: name}
 89					counts[key] = row
 90				}
 91				if name < row.Name {
 92					row.Name = name
 93				}
 94				if kind == 0 {
 95					row.Movies++
 96				} else {
 97					row.Shows++
 98				}
 99			}
100		}
101	}
102	out := []model.Platform{}
103	for _, row := range counts {
104		out = append(out, *row)
105	}
106	sort.Slice(out, func(i, j int) bool {
107		a, b := out[i], out[j]
108		if (a.Name == "Other") != (b.Name == "Other") {
109			return b.Name == "Other"
110		}
111		if a.Movies+a.Shows != b.Movies+b.Shows {
112			return a.Movies+a.Shows > b.Movies+b.Shows
113		}
114		return a.Name < b.Name
115	})
116	return out
117}