The Jellyfin client now asks for each video's people, content rating and genres too, so the guide shows a Jellyfin channel's programs as it does Schedules Direct's: the cast with their characters, the rating (R, TV-14), genres, when it first aired, and up to 1000 characters of the overview rather than 300. The community rating is left out: it's on a 10-point scale, not the guide's 4 stars.
- commit
- b1832427144bb0a1813f8fc378b73fa1c9b6e867
- parent
- 8e08183a288c2d63c3b0c35dd9f26e3675166dde
- author
- FelineStateMachine <226808055+FelineStateMachine@users.noreply.github.com>
- kind
- feat risk score 1.6 of 3, Jev's estimate
| M | internal/ | 34 | ++++++++++++++++++++++++++---- |
| M | internal/ | 7 | ++++-- |
| M | internal/ | 27 | +++++++++++++++++++----- |
| M | internal/ | 4 | ++++ |
| M | internal/ | 3 | ++- |
| M | internal/ | 4 | ++-- |
| M | internal/ | 3 | ++- |
| M | internal/ | 17 | ++++++++++++++- |
| M | internal/ | 7 | ++++++ |
| M | internal/ | 3 | ++- |
@@ -57,8 +57,13 @@ type Video struct {
5757 Premiere string // first aired, "2006-01-02", or "" when unknown5858 Year int5959 Overview string60- Runtime time.Duration61- Audio bool60+ // Cast lists the actors, as "Name (Role)", leads first; Rating is the61+ // content rating ("TV-14"); Genres as the library has them.62+ Cast []string63+ Rating string64+ Genres []string65+ Runtime time.Duration66+ Audio bool6267 // Image is artwork on the server, which loads without signing in: an6368 // episode's still or its series' backdrop, a movie's backdrop or poster.6469 Image string@@ -113,7 +118,13 @@ type dto struct {
113118 BackdropImageTags []string114119 ParentBackdropItemID string `json:"ParentBackdropItemId"`115120 ParentBackdropImageTags []string116- MediaSources []struct {121+ OfficialRating string122+ People []struct {123+ Name string124+ Role string125+ Type string // "Actor", "GuestStar", "Director"...126+ }127+ MediaSources []struct {117128 ID string `json:"Id"`118129 RunTimeTicks int64119130 MediaStreams []mediaStream@@ -321,7 +332,7 @@ func (c *Client) contents(ctx context.Context, id, kind string) ([]dto, error) {
321332 }322333 323334 func videoQuery(q url.Values) url.Values {324- q.Set("fields", "Overview,MediaSources")335+ q.Set("fields", "Overview,MediaSources,People,OfficialRating,Genres")325336 q.Set("excludeLocationTypes", "Virtual")326337 q.Set("enableUserData", "false")327338 return q@@ -353,7 +364,20 @@ func (c *Client) video(d dto) Video {
353364 v := Video{354365 ID: d.ID, Source: d.ID, Type: d.Type, Name: d.Name, Series: d.SeriesName, SeriesID: d.SeriesID,355366 Season: d.ParentIndexNumber, Episode: d.IndexNumber, Year: d.ProductionYear, Overview: d.Overview,356- Runtime: ticks(d.RunTimeTicks), Audio: true, Image: c.artwork(d),367+ Runtime: ticks(d.RunTimeTicks), Audio: true, Image: c.artwork(d), Rating: d.OfficialRating, Genres: d.Genres,368+ }369+ // Actors come in billing order, guest stars after them.370+ for _, kind := range []string{"Actor", "GuestStar"} {371+ for _, p := range d.People {372+ if p.Type != kind || p.Name == "" || len(v.Cast) == 6 {373+ continue374+ }375+ if p.Role != "" {376+ v.Cast = append(v.Cast, p.Name+" ("+p.Role+")")377+ } else {378+ v.Cast = append(v.Cast, p.Name)379+ }380+ }357381 }358382 if len(d.PremiereDate) >= len("2006-01-02") {359383 v.Premiere = d.PremiereDate[:len("2006-01-02")]@@ -28,7 +28,8 @@ func library() []jellyfintest.Item {
2828 {ID: "fut-1-9", Type: "Episode", Name: "Not Downloaded", Parent: "fut-1", Season: 1, Episode: 9, Missing: true},2929 {ID: "btas", Type: "Series", Name: "Batman: The Animated Series", Parent: "lib-shows", Year: 1992},3030 {ID: "btas-1-1", Type: "Episode", Name: "On Leather Wings", Parent: "btas", Season: 1, Episode: 1, Runtime: 22 * time.Minute},31- {ID: "thing82", Type: "Movie", Name: "The Thing", Parent: "lib-movies", Year: 1982, Runtime: 109 * time.Minute, Image: true},31+ {ID: "thing82", Type: "Movie", Name: "The Thing", Parent: "lib-movies", Year: 1982, Runtime: 109 * time.Minute, Image: true,32+ Rating: "R", Genres: []string{"Horror"}, Cast: []string{"Kurt Russell/MacReady", "Wilford Brimley/Blair", "Keith David/"}},3233 {ID: "thing11", Type: "Movie", Name: "The Thing", Parent: "lib-movies", Year: 2011, Runtime: 103 * time.Minute},3334 {ID: "giant", Type: "Movie", Name: "The Iron Giant", Parent: "lib-movies", Year: 1999, Runtime: 86 * time.Minute},3435 {ID: "sat", Type: "BoxSet", Name: "Saturday Morning", Children: []string{"btas", "giant"}},@@ -232,7 +233,9 @@ func TestVideos(t *testing.T) {
232233 if v := got["fut-1-2"]; v.Audio || !strings.Contains(v.Image, "/Items/fut/Images/Primary") {233234 t.Errorf("silent episode without a still = %+v", v)234235 }235- if v := got["thing82"]; v.Type != "Movie" || v.Year != 1982 || !strings.Contains(v.Image, "/Items/thing82/Images/Primary") {236+ if v := got["thing82"]; v.Type != "Movie" || v.Year != 1982 || !strings.Contains(v.Image, "/Items/thing82/Images/Primary") ||237+ v.Rating != "R" || !slices.Equal(v.Genres, []string{"Horror"}) ||238+ !slices.Equal(v.Cast, []string{"Kurt Russell (MacReady)", "Wilford Brimley (Blair)", "Keith David"}) {236239 t.Errorf("movie = %+v", v)237240 }238241 if v := got["giant"]; v.Image != "" {@@ -30,12 +30,15 @@ type Item struct {
3030 Premiere string // "2006-01-02"3131 Overview string3232 Genres []string33- Season int34- Episode int35- Runtime time.Duration36- Silent bool // no sound37- Image bool // has a primary image38- Missing bool // known to the server, with no file33+ // Cast are actors as "Name/Role"; Rating the content rating.34+ Cast []string35+ Rating string36+ Season int37+ Episode int38+ Runtime time.Duration39+ Silent bool // no sound40+ Image bool // has a primary image41+ Missing bool // known to the server, with no file3942 // Streams are a video's sound and subtitles, after its picture. Without4043 // them it has one AAC track, unless Silent.4144 Streams []Stream@@ -414,6 +417,18 @@ func (s *Server) dto(it Item, fields []string, userData bool) map[string]any {
414417 if slices.Contains(fields, "Genres") {415418 m["Genres"] = it.Genres416419 }420+ if slices.Contains(fields, "OfficialRating") && it.Rating != "" {421+ m["OfficialRating"] = it.Rating422+ }423+ if slices.Contains(fields, "People") && len(it.Cast) > 0 {424+ var people []map[string]string425+ people = append(people, map[string]string{"Name": "Someone Behind", "Type": "Director"})426+ for _, c := range it.Cast {427+ name, role, _ := strings.Cut(c, "/")428+ people = append(people, map[string]string{"Name": name, "Role": role, "Type": "Actor"})429+ }430+ m["People"] = people431+ }417432 switch it.Type {418433 case "Episode":419434 for _, p := range s.items {@@ -539,6 +539,10 @@ func (s *Service) custom() []CustomChannel {
539539 gp := guide.Program{540540 Start: p.Start, End: p.End, Title: p.Title, EpisodeTitle: p.Subtitle, Description: p.Description,541541 Season: p.Season, Episode: p.Episode, Image: p.Image, AudioLang: p.AudioLang,542+ OriginalAirDate: p.Aired, Cast: p.Cast, Rating: p.Rating, Genres: p.Genres,543+ }544+ if len(p.Date) >= 4 {545+ gp.Year = p.Date[:4]542546 }543547 if p.New {544548 gp.Flags = []string{"New"}@@ -8,6 +8,7 @@ import (
88 "os"99 "os/exec"1010 "path/filepath"11+ "reflect"1112 "slices"1213 "strconv"1314 "strings"@@ -218,7 +219,7 @@ func TestScheduledFolderGuide(t *testing.T) {
218219 }219220 // Without a schedule, the folder loops on the clock as ever.220221 writeFile(t, filepath.Join(f.Dir, DetailsFile), `{"callSign": "HOTD"}`)221- if progs := f.Programs(at(4, 9, 30), at(4, 12, 0)); !slices.Equal(progs, programs(f.items, at(4, 9, 30), at(4, 12, 0))) {222+ if progs := f.Programs(at(4, 9, 30), at(4, 12, 0)); !reflect.DeepEqual(progs, programs(f.items, at(4, 9, 30), at(4, 12, 0))) {222223 t.Errorf("without a schedule: %+v", progs)223224 }224225 // One that can't be read is logged and left out.@@ -420,8 +420,8 @@ func jellyfinItem(c *jellyfin.Client, v jellyfin.Video, mbps float64) (item, boo
420420 return item{}, false421421 }422422 it := item{423- Frames: frames, Audio: v.Audio, Title: v.Name, Description: firstParagraph(v.Overview, 300), Image: v.Image,424- Input: jellyfinInput(c, v, mbps),423+ Frames: frames, Audio: v.Audio, Title: v.Name, Description: firstParagraph(v.Overview, 1000), Image: v.Image,424+ Input: jellyfinInput(c, v, mbps), Aired: v.Premiere, Year: v.Year, Cast: v.Cast, Rating: v.Rating, Genres: v.Genres,425425 }426426 if v.Type == "Movie" {427427 it.Category = "Movie"@@ -10,6 +10,7 @@ import (
1010 "os"1111 "os/exec"1212 "path/filepath"13+ "reflect"1314 "slices"1415 "strings"1516 "sync"@@ -124,7 +125,7 @@ func TestJellyfinChannelResolvesNames(t *testing.T) {
124125 125126 // The same videos keep the same schedule.126127 cartoons.refresh()127- if again := cartoons.Programs(from, from.Add(6*time.Hour)); !slices.Equal(progs, again) {128+ if again := cartoons.Programs(from, from.Add(6*time.Hour)); !reflect.DeepEqual(progs, again) {128129 t.Error("schedule changed on refresh")129130 }130131 @@ -34,6 +34,12 @@ type item struct {
3434 Season string3535 Episode string3636 Image string37+ // Aired, Cast, Rating and Genres describe it further, as Program's.38+ Aired string39+ Year int40+ Cast []string41+ Rating string42+ Genres []string3743 // Input, when set, returns ffmpeg input options (ending "-i <url>") that3844 // play the video from offset in, for sources that seek on their side.3945 // Otherwise Path is read with a local seek. The stream's pace is set@@ -145,11 +151,20 @@ func programs(items []item, from, to time.Time) []Program {
145151 146152 // programOf is an item's guide entry, airing from start to end.147153 func programOf(it item, start, end time.Time) Program {148- return Program{154+ p := Program{149155 Start: start, End: end, Title: it.Title, Subtitle: it.Subtitle, Description: it.Description,150156 Category: it.Category, Season: it.Season, Episode: it.Episode, Image: it.Image,151157 AudioLang: it.AudioLang, Captions: it.Captions != nil,158+ Aired: it.Aired, Cast: it.Cast, Rating: it.Rating, Genres: it.Genres,152159 }160+ switch {161+ case p.Date != "":162+ case len(it.Aired) == len("2006-01-02"):163+ p.Date = strings.ReplaceAll(it.Aired, "-", "")164+ case it.Year > 0:165+ p.Date = strconv.Itoa(it.Year)166+ }167+ return p153168 }154169 155170 // lineup gives a looping channel's items, and its schedule (nil for none),@@ -24,6 +24,13 @@ type Program struct {
2424 // Date is when it was made, as XMLTV writes it: "2014", "202603" or2525 // "20140304".2626 Date string27+ // Aired is when it first aired ("2014-03-04"), Cast its actors as28+ // "Name (Role)", Rating its content rating ("TV-14") and Genres its29+ // genres, where the source knows them.30+ Aired string31+ Cast []string32+ Rating string33+ Genres []string2734 // AudioLang is the sound's language as tagged ("eng", "jpn"), "" when2835 // unknown, which is taken for English.2936 AudioLang string@@ -9,6 +9,7 @@ import (
99 "os"1010 "os/exec"1111 "path/filepath"12+ "reflect"1213 "slices"1314 "strconv"1415 "strings"@@ -461,7 +462,7 @@ func TestYouTubeGuide(t *testing.T) {
461462 }462463 // A restart reads the same schedule.463464 again := &YouTube{Num: "1.8", Title: "Northernlion", Dir: dir}464- if got := again.Programs(now, now.Add(12*time.Hour)); len(got) != len(progs) || got[5] != progs[5] {465+ if got := again.Programs(now, now.Add(12*time.Hour)); len(got) != len(progs) || !reflect.DeepEqual(got[5], progs[5]) {465466 t.Error("schedule differs after a restart")466467 }467468 }