-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
256 lines (220 loc) · 6.65 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// Package tvdb provides a Go client implementation of The TVDB API v2
package tvdb
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"net/url"
)
var baseURL = url.URL{
Scheme: "https",
Host: "api.thetvdb.com",
}
type requestOption func(*http.Request)
// QueryOption provides a query option function.
type QueryOption func(*url.Values)
func withLanguage(language string) requestOption {
return func (req *http.Request) {
if len(language) > 0 {
req.Header.Set("Accept-Language", language)
}
}
}
// WithAiredSeasonNumber filters episodes matching the given season number.
//
// This function should be used with EpisodesBySeriesID.
func WithAiredSeasonNumber(seasonNumber int) QueryOption {
return withQueryIntOption("airedSeason", seasonNumber)
}
// WithAiredEpisodeNumber filters episodes matching the given episode number.
//
// This function should be used with EpisodesBySeriesID.
func WithAiredEpisodeNumber(episodeNumber int) QueryOption {
return withQueryIntOption("airedEpisode", episodeNumber)
}
// WithDVDSeasonNumber filters episodes matching the given DVD season number.
//
// This function should be used with EpisodesBySeriesID
func WithDVDSeasonNumber(seasonNumber int) QueryOption {
return withQueryIntOption("dvdSeason", seasonNumber)
}
// WithDVDEpisodeNumber filters episodes matching the given DVD episode number.
//
// This function should be used with EpisodesBySeriesID
func WithDVDEpisodeNumber(episodeNumber int) QueryOption {
return withQueryIntOption("dvdEpisode", episodeNumber)
}
// WithAbsoluteEpisodeNumber filters episodes matching the given absolute episode number.
//
// This function should be used with EpisodesBySeriesID
func WithAbsoluteEpisodeNumber(episodeNumber int) QueryOption {
return withQueryIntOption("absoluteNumber", episodeNumber)
}
func withQueryIntOption(name string, value int) QueryOption {
return func(v *url.Values) {
v.Set(name, fmt.Sprintf("%d", value))
}
}
func withQueryOption(name, value string) QueryOption {
return func(v *url.Values) {
v.Set(name, value)
}
}
// ClientOptions represents options for a TVDB client.
//
// Either APIKey or UserKey and Username are mandatory for login.
// Language provides a hint to return result in the given language and can be changed dynamically
// using the WithLanguage() helper.
type ClientOptions struct {
APIKey string
UserKey string
Username string
Language string
}
// Client to query the TVDB
type Client struct {
token string
tokenDate time.Time
httpClient *http.Client
options ClientOptions
}
// NewClient creates a new TVDB client.
//
// The function immediately tries to login and returns the handle of the new client.
func NewClient(options ClientOptions) (*Client, error) {
c := &Client{
httpClient: &http.Client{},
options: options,
}
err := c.login()
if err != nil {
return nil, fmt.Errorf("login failed, %s", err)
}
return c, nil
}
// URL builds a request URL for the given path and options.
func (c *Client) URL(path string, options ...QueryOption) url.URL {
ret := baseURL
ret.Path = path
if len(options) > 0 {
q := ret.Query()
for _, opt := range options {
opt(&q)
}
ret.RawQuery = q.Encode()
}
return ret
}
// Languages returns a list of languages supported by The TVDB.
func (c *Client) Languages() ([]Language, error) {
var data LanguageData
fullURL := c.URL("/languages")
if err := c.get(fullURL, &data); err != nil {
return data.Data, err
}
if len(data.Error) > 0 {
return data.Data, fmt.Errorf(data.Error)
}
return data.Data, nil
}
// Token returns the JWT used for authentication.
func (c *Client) Token() string {
return c.token
}
// Options returns a copy of the options used by the client.
func (c *Client) Options() ClientOptions {
return c.options
}
// WithLanguage updates the client default language and returns a shallow copy of the client handle.
func (c *Client) WithLanguage(language string) *Client {
c.options.Language = language
return c
}
// SeriesByID returns a single series information given a TVDB ID.
func (c *Client) SeriesByID(id int) (*Series, error) {
var data SeriesData
fullURL := c.URL(fmt.Sprintf("/series/%d", id))
if err := c.get(fullURL, &data, withLanguage(c.options.Language)); err != nil {
return nil, err
}
return data.Data, nil
}
// EpisodesBySeriesID returns a list of episodes for the given series ID, optionally filtered by filters.
//
// When no filters are provided, this function returns all the episodes of the series.
// Filters can be used to get, for example, all the episodes from a given season, or a single episode in a season.
func (c *Client) EpisodesBySeriesID(seriesID int,filters ...QueryOption) ([]Episode, error) {
uri := fmt.Sprintf("/series/%d/episodes", seriesID)
if len(filters) > 0 {
uri += "/query"
}
var data SeriesEpisodesData
var episodes []Episode
last := 2
for page := 1; page < last; page++ {
filtersWithPage := append(filters, withQueryIntOption("page", page))
fullURL := c.URL(uri, filtersWithPage...)
if err := c.get(fullURL, &data, withLanguage(c.options.Language)); err != nil {
return nil, err
}
last = data.Pages.Last
episodes = append(episodes, data.Data...)
}
return episodes, nil
}
// SearchSeriesByName returns a list of series matching name.
//
// Results are returned in the configured client language.
func (c *Client) SearchSeriesByName(name string) ([]SeriesSearchResult, error) {
var result SeriesSearchResults
fullURL := c.URL("/search/series", withQueryOption("name", name))
err := c.get(fullURL, &result, withLanguage(c.options.Language))
return result.Data, err
}
func (c *Client) login() error {
loginData := map[string]string{
"apiKey": c.options.APIKey,
"userKey": c.options.UserKey,
"username": c.options.Username,
}
postData, err := json.Marshal(loginData)
if err != nil {
return err
}
loginURL := c.URL("/login")
res, err := c.httpClient.Post(loginURL.String(), "application/json", bytes.NewReader(postData))
if err != nil {
return err
}
decoder := json.NewDecoder(res.Body)
defer res.Body.Close()
var token Token
err = decoder.Decode(&token); if err != nil {
return err
}
if len(token.Error) > 0 {
return fmt.Errorf("%s", token.Error)
}
c.token = token.Value
return nil
}
func (c *Client) get(url url.URL, out interface{}, options ...requestOption) error {
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return err
}
for _, optFunc := range options {
optFunc(req)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
req.Header.Set("Accept", "application/json")
res, err := c.httpClient.Do(req)
if err != nil {
return err
}
decoder := json.NewDecoder(res.Body)
defer res.Body.Close()
return decoder.Decode(out)
}