all repos

rss-tools @ master

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
imrpove movies id parsing, 9 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 tmdbSearchResponse struct {
48
	Results []tmdbShow `json:"results"`
49
}
50
51
type tmdbSeasonResponse struct {
52
	Episodes []TMDBEpisode `json:"episodes"`
53
}
54
55
type TMDBAPI struct {
56
	apiKey string
57
	client *http.Client
58
}
59
60
func NewTMDBAPI(apiKey string, client *http.Client) *TMDBAPI {
61
	return &TMDBAPI{
62
		apiKey: apiKey,
63
		client: client,
64
	}
65
}
66
67
func (a *TMDBAPI) FetchEpisodesForShow(showID string) ([]TMDBEpisode, error) {
68
	tmdbID, err := a.getTMDBID(showID)
69
	if err != nil {
70
		return nil, err
71
	}
72
73
	show, err := makeRequest[tmdbShowDetails](a, "/tv/%s", tmdbID)
74
	if err != nil {
75
		return nil, err
76
	}
77
78
	if show.NumberOfSeasons == 0 {
79
		return []TMDBEpisode{}, nil
80
	}
81
82
	var allEpisodes []TMDBEpisode
83
	season := show.NumberOfSeasons
84
	seasonData, err := makeRequest[tmdbSeasonResponse](a, "/tv/%s/season/%d", tmdbID, season)
85
	if err != nil {
86
		slog.Warn("failed to fetch season", "season", season, "show", tmdbID, "err", err)
87
	} else {
88
		for _, ep := range seasonData.Episodes {
89
			ep.ShowName = show.Name
90
			ep.ShowID = tmdbID
91
			allEpisodes = append(allEpisodes, ep)
92
		}
93
	}
94
95
	return filterRecentEpisodes(allEpisodes), nil
96
}
97
98
func (a *TMDBAPI) getTMDBID(showID string) (string, error) {
99
	if strings.HasPrefix(showID, "tt") {
100
		result, err := makeRequest[tmdbFindResponse](a, "/find/%s?external_source=imdb_id", showID)
101
		if err != nil {
102
			return "", err
103
		}
104
105
		if len(result.TvResults) == 0 {
106
			return "", fmt.Errorf("no TMDB show found for IMDB ID %s", showID)
107
		}
108
109
		return fmt.Sprintf("%d", result.TvResults[0].ID), nil
110
	}
111
	return showID, nil
112
}
113
114
func (a *TMDBAPI) SearchShow(query string) (*tmdbShow, error) {
115
	encoded := url.QueryEscape(query)
116
	result, err := makeRequest[tmdbSearchResponse](a, "/search/tv?query=%s", encoded)
117
	if err != nil {
118
		return nil, err
119
	}
120
	if len(result.Results) == 0 {
121
		return nil, fmt.Errorf("no TMDB show found for %q", query)
122
	}
123
	return &result.Results[0], nil
124
}
125
126
func makeRequest[T any](a *TMDBAPI, endpoint string, args ...any) (*T, error) {
127
	u, err := url.Parse(fmt.Sprintf(tmdbBaseURL+endpoint, args...))
128
	if err != nil {
129
		return nil, fmt.Errorf("failed to parse URL: %w", err)
130
	}
131
	q := u.Query()
132
	q.Set("api_key", a.apiKey)
133
	u.RawQuery = q.Encode()
134
135
	slog.Info("external API request", "endpoint", u.String())
136
	resp, err := a.client.Get(u.String())
137
	if err != nil {
138
		return nil, fmt.Errorf("failed to fetch %s: %w", endpoint, err)
139
	}
140
	defer func() {
141
		if err := resp.Body.Close(); err != nil {
142
			slog.Error("failed to close response body", "err", err)
143
		}
144
	}()
145
146
	var result T
147
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
148
		return nil, fmt.Errorf("failed to decode response: %w", err)
149
	}
150
151
	return &result, nil
152
}
153
154
func filterRecentEpisodes(episodes []TMDBEpisode) []TMDBEpisode {
155
	var recent []TMDBEpisode
156
	now := time.Now()
157
	cutoff := now.AddDate(0, 0, -30)
158
159
	for _, ep := range episodes {
160
		if ep.AirDate == "" {
161
			continue
162
		}
163
164
		airDate, err := time.Parse(dateFormat, ep.AirDate)
165
		if err != nil {
166
			continue
167
		}
168
169
		if airDate.Before(now) && airDate.After(cutoff) {
170
			recent = append(recent, ep)
171
		}
172
	}
173
	return recent
174
}