all repos

rss-tools @ 50b546dbf3e0cf36ff5e6d5a75916206a125213e

get rss feed from sources that(i need and) dont provide one

rss-tools/sources/moviefeed/api.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix moviefeed, 28 days ago
1
package moviefeed
2
3
import (
4
	"encoding/json"
5
	"fmt"
6
	"log/slog"
7
	"net/http"
8
	"net/url"
9
	"strings"
10
	"time"
11
)
12
13
const (
14
	dateFormat       = "2006-01-02"
15
	tmdbBaseURL      = "https://api.themoviedb.org/3"
16
	tmdbImageBaseURL = "https://image.tmdb.org/t/p/w500"
17
)
18
19
type tmdbShow struct {
20
	ID           int    `json:"id"`
21
	Name         string `json:"name"`
22
	Overview     string `json:"overview"`
23
	FirstAirDate string `json:"first_air_date"`
24
}
25
26
type tmdbShowDetails struct {
27
	tmdbShow
28
	NumberOfSeasons int `json:"number_of_seasons"`
29
}
30
31
type TMDBEpisode struct {
32
	ID            int    `json:"id"`
33
	Name          string `json:"name"`
34
	Overview      string `json:"overview"`
35
	AirDate       string `json:"air_date"`
36
	EpisodeNumber int    `json:"episode_number"`
37
	SeasonNumber  int    `json:"season_number"`
38
	StillPath     string `json:"still_path"`
39
	ShowName      string
40
	ShowID        string
41
}
42
43
type tmdbFindResponse struct {
44
	TvResults []tmdbShow `json:"tv_results"`
45
}
46
47
type tmdbSeasonResponse struct {
48
	Episodes []TMDBEpisode `json:"episodes"`
49
}
50
51
type TMDBAPI struct {
52
	apiKey string
53
	client *http.Client
54
}
55
56
func NewTMDBAPI(apiKey string, client *http.Client) *TMDBAPI {
57
	return &TMDBAPI{
58
		apiKey: apiKey,
59
		client: client,
60
	}
61
}
62
63
func (a *TMDBAPI) FetchEpisodesForShow(showID string) ([]TMDBEpisode, error) {
64
	tmdbID, err := a.getTMDBID(showID)
65
	if err != nil {
66
		return nil, err
67
	}
68
69
	show, err := makeRequest[tmdbShowDetails](a, "/tv/%s", tmdbID)
70
	if err != nil {
71
		return nil, err
72
	}
73
74
	if show.NumberOfSeasons == 0 {
75
		return []TMDBEpisode{}, nil
76
	}
77
78
	var allEpisodes []TMDBEpisode
79
	season := show.NumberOfSeasons
80
	seasonData, err := makeRequest[tmdbSeasonResponse](a, "/tv/%s/season/%d", tmdbID, season)
81
	if err != nil {
82
		slog.Warn("failed to fetch season", "season", season, "show", tmdbID, "err", err)
83
	} else {
84
		for _, ep := range seasonData.Episodes {
85
			ep.ShowName = show.Name
86
			ep.ShowID = tmdbID
87
			allEpisodes = append(allEpisodes, ep)
88
		}
89
	}
90
91
	return filterRecentEpisodes(allEpisodes), nil
92
}
93
94
func (a *TMDBAPI) getTMDBID(showID string) (string, error) {
95
	if strings.HasPrefix(showID, "tt") {
96
		result, err := makeRequest[tmdbFindResponse](a, "/find/%s?external_source=imdb_id", showID)
97
		if err != nil {
98
			return "", err
99
		}
100
101
		if len(result.TvResults) == 0 {
102
			return "", fmt.Errorf("no TMDB show found for IMDB ID %s", showID)
103
		}
104
105
		return fmt.Sprintf("%d", result.TvResults[0].ID), nil
106
	}
107
	return showID, nil
108
}
109
110
func makeRequest[T any](a *TMDBAPI, endpoint string, args ...any) (*T, error) {
111
	u, err := url.Parse(fmt.Sprintf(tmdbBaseURL+endpoint, args...))
112
	if err != nil {
113
		return nil, fmt.Errorf("failed to parse URL: %w", err)
114
	}
115
	q := u.Query()
116
	q.Set("api_key", a.apiKey)
117
	u.RawQuery = q.Encode()
118
119
	slog.Info("external API request", "endpoint", u.String())
120
	resp, err := a.client.Get(u.String())
121
	if err != nil {
122
		return nil, fmt.Errorf("failed to fetch %s: %w", endpoint, err)
123
	}
124
	defer func() {
125
		if err := resp.Body.Close(); err != nil {
126
			slog.Error("failed to close response body", "err", err)
127
		}
128
	}()
129
130
	var result T
131
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
132
		return nil, fmt.Errorf("failed to decode response: %w", err)
133
	}
134
135
	return &result, nil
136
}
137
138
func filterRecentEpisodes(episodes []TMDBEpisode) []TMDBEpisode {
139
	var recent []TMDBEpisode
140
	now := time.Now()
141
	cutoff := now.AddDate(0, 0, -30)
142
143
	for _, ep := range episodes {
144
		if ep.AirDate == "" {
145
			continue
146
		}
147
148
		airDate, err := time.Parse(dateFormat, ep.AirDate)
149
		if err != nil {
150
			continue
151
		}
152
153
		if airDate.Before(now) && airDate.After(cutoff) {
154
			recent = append(recent, ep)
155
		}
156
	}
157
	return recent
158
}