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