tvdb - conn.go
1 // Package tvdb is a simple interface to the TVDB database of TV shows
2 package tvdb // import "vimagination.zapto.org/tvdb"
3
4 import (
5 "bytes"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "net/url"
12 "sync"
13 )
14
15 // Auth represents the information required to get a validated authentication
16 // token.
17 type Auth struct {
18 APIKey string `json:"apikey"`
19 Username string `json:"username,omitempty"`
20 UserKey string `json:"userkey,omitempty"`
21 }
22
23 type authResponse struct {
24 Token string `json:"token"`
25 Error string `json:"Error"`
26 }
27
28 var (
29 loginURL = makeURL("/login", "")
30 refreshURL = makeURL("/refresh_token", "")
31 )
32
33 var contentType = []string{
34 "application/json",
35 }
36
37 var loginHeaders = http.Header{
38 "Content-Type": contentType,
39 }
40
41 // Conn represents a connection to the TVDB database
42 type Conn struct {
43 headerMutex sync.RWMutex
44 headers http.Header
45 }
46
47 // Token creates a TVDB database connection using a pre-authorised token
48 func Token(t string) *Conn {
49 return &Conn{
50 headers: http.Header{
51 "Authorization": []string{
52 "Bearer " + t,
53 },
54 "Content-Type": contentType,
55 },
56 }
57 }
58
59 // Login creates a TVDB database connection using login credentials
60 func Login(a Auth) (*Conn, error) {
61 c := &Conn{
62 headers: loginHeaders,
63 }
64
65 var ar authResponse
66
67 if err := c.post(loginURL, a, &ar); err != nil {
68 return nil, err
69 }
70
71 if ar.Error != "" {
72 return nil, errors.New(ar.Error)
73 } else if ar.Token == "" {
74 return nil, ErrUnknownError
75 }
76
77 c.headers = http.Header{
78 "Authorization": []string{
79 "Bearer " + ar.Token,
80 },
81 "Content-Type": contentType,
82 }
83 return c, nil
84 }
85
86 // Token returns the current authentication token
87 func (c *Conn) Token() string {
88 a := c.headers.Get("Authorization")
89 if len(a) < 7 {
90 return ""
91 }
92 return a[7:]
93 }
94
95 // Refresh retrieves a new authentication token without having to use the login
96 // credentials. Each token only lasts 24 hours and refresh can only be used in
97 // that time-frame
98 func (c *Conn) Refresh() error {
99 var ar authResponse
100 err := c.get(refreshURL, &ar)
101 if err != nil {
102 return err
103 }
104
105 if ar.Error != "" {
106 return errors.New(ar.Error)
107 } else if ar.Token == "" {
108 return ErrUnknownError
109 }
110
111 c.headerMutex.Lock()
112 c.headers["Authorization"][0] = "Bearer " + ar.Token
113 c.headerMutex.Unlock()
114
115 return nil
116 }
117
118 // SetLanguage sets the language header used by some queries to return
119 // information in the requested language
120 func (c *Conn) SetLanguage(code string) {
121 c.headers.Set("Accept-Language", code)
122 }
123
124 func (c *Conn) get(u *url.URL, ret interface{}) error {
125 return c.do(http.MethodGet, u, nil, ret, nil)
126 }
127
128 func (c *Conn) post(u *url.URL, data interface{}, ret interface{}) error {
129 return c.do(http.MethodPost, u, data, ret, nil)
130 }
131
132 func (c *Conn) put(u *url.URL, ret interface{}) error {
133 return c.do(http.MethodPut, u, nil, ret, nil)
134 }
135
136 func (c *Conn) delete(u *url.URL, ret interface{}) error {
137 return c.do(http.MethodDelete, u, nil, ret, nil)
138 }
139
140 func (c *Conn) do(method string, u *url.URL, data interface{}, ret interface{}, headers http.Header) error {
141 r := http.Request{
142 URL: u,
143 Header: c.headers,
144 Method: method,
145 }
146
147 if method == http.MethodPost && data != nil {
148 var buf bytes.Buffer
149 if err := json.NewEncoder(&buf).Encode(data); err != nil {
150 return err
151 }
152 r.Body = io.NopCloser(&buf)
153 r.ContentLength = int64(buf.Len())
154 }
155 c.headerMutex.RLock()
156 resp, err := http.DefaultClient.Do(&r)
157 c.headerMutex.RUnlock()
158 if err != nil {
159 return fmt.Errorf("error making connection: %w", err)
160 }
161
162 switch resp.StatusCode {
163 case http.StatusOK:
164 case http.StatusNotFound:
165 return ErrNotFound
166 case http.StatusUnauthorized:
167 return ErrInvalidAuth
168 default:
169 return ErrUnknownError
170 }
171
172 if ret != nil {
173 if err = json.NewDecoder(resp.Body).Decode(ret); err != nil {
174 return fmt.Errorf("error decoding response: %w", err)
175 }
176 if err = resp.Body.Close(); err != nil {
177 return fmt.Errorf("error closing response body: %w", err)
178 }
179 }
180
181 for k := range headers {
182 headers[k] = resp.Header[k]
183 }
184
185 return nil
186 }
187
188 // Errors
189 var (
190 ErrInvalidAuth = errors.New("invalid credentials")
191 ErrUnknownError = errors.New("unknown error")
192 ErrNotFound = errors.New("not found")
193 )
194