configs/server-dockers/jellyfin-news/jellyfin.go
server-dockers/jellyfin-news/jellyfin.goRaw
1package main
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "net/url"
9 "strconv"
10 "strings"
11 "time"
12)
13
14type Client struct {
15 baseURL string
16 apiKey string
17 userID string
18 http *http.Client
19}
20
21func New(baseURL, apiKey, userID string, httpClient *http.Client) (*Client, error) {
22 baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
23 if _, err := url.ParseRequestURI(baseURL); err != nil || !(strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://")) {
24 return nil, fmt.Errorf("invalid JELLYFIN_URL %q", baseURL)
25 }
26 if strings.TrimSpace(apiKey) == "" {
27 return nil, fmt.Errorf("JELLYFIN_API_KEY is required")
28 }
29 if httpClient == nil {
30 httpClient = &http.Client{Timeout: 30 * time.Second}
31 }
32 return &Client{baseURL: baseURL, apiKey: apiKey, userID: strings.TrimSpace(userID), http: httpClient}, nil
33}
34
35type response struct {
36 Items []item `json:"Items"`
37 TotalRecordCount int `json:"TotalRecordCount"`
38}
39
40type item struct {
41 Type string `json:"Type"`
42 Name string `json:"Name"`
43 SeriesID string `json:"SeriesId"`
44 SeriesName string `json:"SeriesName"`
45 ParentIndexNumber *int `json:"ParentIndexNumber"`
46 IndexNumber *int `json:"IndexNumber"`
47 ProductionYear *int `json:"ProductionYear"`
48 DateCreated string `json:"DateCreated"`
49}
50
51// AddedBetween returns movies and episodes whose Jellyfin DateCreated is in [start, end).
52func (c *Client) AddedBetween(ctx context.Context, start, end time.Time) ([]Item, error) {
53 const pageSize = 200
54 var result []Item
55 for offset := 0; ; offset += pageSize {
56 params := url.Values{
57 "IncludeItemTypes": {"Movie,Episode"},
58 "Recursive": {"true"},
59 "Fields": {"DateCreated"},
60 "SortBy": {"DateCreated"},
61 "SortOrder": {"Descending"},
62 "EnableImages": {"false"},
63 "EnableUserData": {"false"},
64 "EnableTotalRecordCount": {"true"},
65 "StartIndex": {strconv.Itoa(offset)},
66 "Limit": {strconv.Itoa(pageSize)},
67 }
68 if c.userID != "" {
69 params.Set("UserId", c.userID)
70 }
71 req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/Items?"+params.Encode(), nil)
72 if err != nil {
73 return nil, err
74 }
75 req.Header.Set("X-Emby-Token", c.apiKey)
76 resp, err := c.http.Do(req)
77 if err != nil {
78 return nil, fmt.Errorf("query Jellyfin: %w", err)
79 }
80 var page response
81 decodeErr := json.NewDecoder(resp.Body).Decode(&page)
82 resp.Body.Close()
83 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
84 return nil, fmt.Errorf("Jellyfin returned HTTP %d", resp.StatusCode)
85 }
86 if decodeErr != nil {
87 return nil, fmt.Errorf("decode Jellyfin response: %w", decodeErr)
88 }
89 pastStart := false
90 for _, raw := range page.Items {
91 created, err := time.Parse(time.RFC3339Nano, raw.DateCreated)
92 if err != nil {
93 continue
94 }
95 if created.Before(start) {
96 pastStart = true
97 continue
98 }
99 if !created.Before(end) {
100 continue
101 }
102 result = append(result, Item{
103 Type: raw.Type, Name: raw.Name, SeriesID: raw.SeriesID,
104 SeriesName: raw.SeriesName, SeasonNumber: raw.ParentIndexNumber,
105 EpisodeNumber: raw.IndexNumber, ProductionYear: raw.ProductionYear,
106 })
107 }
108 if pastStart || len(page.Items) < pageSize || (page.TotalRecordCount > 0 && offset+len(page.Items) >= page.TotalRecordCount) {
109 break
110 }
111 }
112 return result, nil
113}