arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
jellyfin-tools/jellyfin-year-review/internal/model/model.go
jellyfin-year-review/internal/model/model.goRaw
  1// Package model defines the private source snapshot and public-to-owner review v1 contracts.
  2package model
  3
  4import (
  5	"encoding/hex"
  6	"fmt"
  7	"strings"
  8	"time"
  9)
 10
 11const Version = 1
 12
 13// ID converts Jellyfin UUIDs to lowercase, hyphen-free identity keys.
 14func ID(s string) (string, error) {
 15	s = strings.ToLower(strings.ReplaceAll(s, "-", ""))
 16	if len(s) != 32 {
 17		return "", fmt.Errorf("invalid Jellyfin UUID")
 18	}
 19	if _, err := hex.DecodeString(s); err != nil {
 20		return "", fmt.Errorf("invalid Jellyfin UUID")
 21	}
 22	return s, nil
 23}
 24
 25type User struct {
 26	ID   string `json:"id"`
 27	Name string `json:"name"`
 28}
 29type Item struct {
 30	ID          string            `json:"id"`
 31	Title       string            `json:"title"`
 32	Type        string            `json:"type"`
 33	SeriesID    string            `json:"seriesId,omitempty"`
 34	SeriesTitle string            `json:"seriesTitle,omitempty"`
 35	Year        int               `json:"year,omitempty"`
 36	Runtime     float64           `json:"runtimeSeconds,omitempty"`
 37	Genres      []string          `json:"genres"`
 38	Studios     []string          `json:"studios,omitempty"`
 39	Platforms   []string          `json:"platforms,omitempty"`
 40	ImageTag    string            `json:"imageTag,omitempty"`
 41	ProviderIDs map[string]string `json:"providerIds,omitempty"`
 42	Poster      string            `json:"poster,omitempty"`
 43}
 44
 45// Playback is a finalized activity row, not a progress update. Exact records describe
 46// uninterrupted watched intervals; position ranges describe actual media coverage.
 47type Playback struct {
 48	ID            string    `json:"id"`
 49	UserID        string    `json:"userId"`
 50	ItemID        string    `json:"itemId"`
 51	Start         time.Time `json:"start"`
 52	End           time.Time `json:"end"`
 53	ObservedAt    time.Time `json:"observedAt"`
 54	Seconds       float64   `json:"seconds"`
 55	Exact         bool      `json:"exactInterval"`
 56	PositionStart *float64  `json:"positionStart,omitempty"`
 57	PositionEnd   *float64  `json:"positionEnd,omitempty"`
 58	Device        string    `json:"device"`
 59	Client        string    `json:"client"`
 60}
 61type Snapshot struct {
 62	Version    int        `json:"schemaVersion"`
 63	CapturedAt time.Time  `json:"capturedAt"`
 64	Source     string     `json:"source"`
 65	Coverage   string     `json:"coverage"`
 66	Users      []User     `json:"users"`
 67	Items      []Item     `json:"items"`
 68	Playback   []Playback `json:"playback"`
 69}
 70type Availability struct {
 71	Available bool   `json:"available"`
 72	Reason    string `json:"reason,omitempty"`
 73}
 74type Named struct {
 75	Name    string  `json:"name"`
 76	Seconds float64 `json:"seconds"`
 77	Records int     `json:"records"`
 78}
 79
 80// Platform counts distinct watched movies and series associated with a service.
 81type Platform struct {
 82	Name   string `json:"name"`
 83	Movies int    `json:"movies"`
 84	Shows  int    `json:"shows"`
 85}
 86type Content struct {
 87	ID           string   `json:"id"`
 88	Title        string   `json:"title"`
 89	Poster       string   `json:"poster,omitempty"`
 90	PosterSource string   `json:"posterSource,omitempty"`
 91	Seconds      float64  `json:"seconds"`
 92	Records      int      `json:"records"`
 93	Months       []int    `json:"months,omitempty"`
 94	Completion   *float64 `json:"completionPercent,omitempty"`
 95}
 96type Marathon struct {
 97	Start   time.Time `json:"start"`
 98	End     time.Time `json:"end"`
 99	Watched float64   `json:"watchedSeconds"`
100	Elapsed float64   `json:"elapsedSeconds"`
101	Records int       `json:"records"`
102}
103type Personality struct {
104	Name        string `json:"name"`
105	Explanation string `json:"explanation"`
106}
107type Ranking struct {
108	Rank       int     `json:"rank"`
109	Cohort     int     `json:"cohort"`
110	Percentile float64 `json:"percentile"`
111	Average    float64 `json:"averageSeconds"`
112}
113type Rules struct {
114	MinimumSeconds     float64 `json:"minimumSeconds"`
115	CompletionPercent  float64 `json:"completionPercent"`
116	MarathonGapSeconds float64 `json:"marathonGapSeconds"`
117}
118type Review struct {
119	Rules              Rules                   `json:"rules"`
120	Version            int                     `json:"schemaVersion"`
121	User               User                    `json:"user"`
122	Year               int                     `json:"year"`
123	Timezone           string                  `json:"timezone"`
124	GeneratedAt        time.Time               `json:"generatedAt"`
125	Source             string                  `json:"source"`
126	Coverage           string                  `json:"coverage"`
127	Availability       map[string]Availability `json:"availability"`
128	Seconds            float64                 `json:"seconds"`
129	MovieSeconds       float64                 `json:"movieSeconds"`
130	TVSeconds          float64                 `json:"tvSeconds"`
131	Records            int                     `json:"records"`
132	UniqueTitles       int                     `json:"uniqueTitles"`
133	ActiveDays         int                     `json:"activeDays"`
134	Movies             []Content               `json:"movies"`
135	Shows              []Content               `json:"shows"`
136	Platforms          []Platform              `json:"platforms,omitempty"`
137	Abandoned          []Content               `json:"abandoned"`
138	Genres             []Named                 `json:"genres"`
139	Devices            []Named                 `json:"devices"`
140	Clients            []Named                 `json:"clients"`
141	Hourly             [24]float64             `json:"hourly"`
142	Weekdays           [7]float64              `json:"weekdays"`
143	Monthly            [12]float64             `json:"monthly"`
144	DistributionMetric string                  `json:"distributionMetric"`
145	Marathon           *Marathon               `json:"marathon"`
146	Personality        *Personality            `json:"personality"`
147	Ranking            *Ranking                `json:"ranking"`
148	Warnings           []string                `json:"warnings"`
149}
150type Manifest struct {
151	Version int              `json:"schemaVersion"`
152	Users   map[string][]int `json:"users"`
153}
154
155// Validate rejects incompatible, corrupted or incorrectly addressed documents.
156func (r Review) Validate(id string, year int) error {
157	if r.Version != Version || r.User.ID != id || r.Year != year || r.Seconds < 0 || r.Availability == nil {
158		return fmt.Errorf("invalid or unsupported review")
159	}
160	if _, err := time.LoadLocation(r.Timezone); err != nil {
161		return fmt.Errorf("invalid review timezone")
162	}
163	return nil
164}