-
Notifications
You must be signed in to change notification settings - Fork 5
/
xkcd.go
167 lines (143 loc) · 3.92 KB
/
xkcd.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
package xkcd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strconv"
)
// BaseURL is the default base URL for the xkcd JSON API.
const BaseURL = "https://xkcd.com"
type Client struct {
HTTPClient *http.Client
BaseURL string
}
// NewClient constructs a client using http.DefaultClient and the default
// base URL. The returned client is ready for use.
func NewClient() *Client {
return &Client{
HTTPClient: http.DefaultClient,
BaseURL: BaseURL,
}
}
type comicResponse struct {
Alt string `json:"alt"`
Day string `json:"day"`
Img string `json:"img"`
Link string `json:"link"`
Month string `json:"month"`
News string `json:"news"`
Num int `json:"num"`
SafeTitle string `json:"safe_title"`
Title string `json:"title"`
Transcript string `json:"transcript"`
Year string `json:"year"`
}
// Comic contains information about an xkcd comic.
type Comic struct {
Alt string
Day int
ImageURL string
URL string
Month int
News string
Number int
SafeTitle string
Title string
Transcript string
Year int
}
// StatusError is returned when a bad response status code is received
// from the API.
type StatusError struct {
Code int
}
var _ error = StatusError{}
func (e StatusError) Error() string {
return fmt.Sprintf("bad response status code: %d", e.Code)
}
// Get returns the xkcd comic for the given comic number.
func (c *Client) Get(ctx context.Context, number int) (Comic, error) {
return c.do(ctx, fmt.Sprintf("/%d/info.0.json", number))
}
// Latest returns the latest xkcd comic.
func (c *Client) Latest(ctx context.Context) (Comic, error) {
return c.do(ctx, fmt.Sprintf("/info.0.json"))
}
// Image returns the image data for the given comic number and the value of the
// image response's Content-Type header.
func (c *Client) Image(ctx context.Context, number int) (io.Reader, string, error) {
comic, err := c.Get(ctx, number)
if err != nil {
return nil, "", err
}
req, err := http.NewRequest("GET", comic.ImageURL, nil)
if err != nil {
return nil, "", fmt.Errorf("failed to build image request: %s", err)
}
req = req.WithContext(ctx)
rsp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, "", fmt.Errorf("failed to do image request: %s", err)
}
defer drainAndClose(rsp.Body)
if rsp.StatusCode != 200 {
return nil, "", StatusError{Code: rsp.StatusCode}
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, rsp.Body); err != nil {
return nil, "", fmt.Errorf("failed to do copy image: %s", err)
}
return &buf, rsp.Header.Get("Content-Type"), nil
}
func (c *Client) do(ctx context.Context, reqPath string) (Comic, error) {
req, err := http.NewRequest("GET", c.BaseURL+reqPath, nil)
if err != nil {
return Comic{}, fmt.Errorf("failed to build request: %v", err)
}
req = req.WithContext(ctx)
rsp, err := c.HTTPClient.Do(req)
if err != nil {
return Comic{}, fmt.Errorf("failed to do request: %v", err)
}
defer drainAndClose(rsp.Body)
if rsp.StatusCode != 200 {
return Comic{}, StatusError{Code: rsp.StatusCode}
}
var cr comicResponse
if err := json.NewDecoder(rsp.Body).Decode(&cr); err != nil {
return Comic{}, fmt.Errorf("failed to json-unmarshal response: %v", err)
}
d, err := strconv.Atoi(cr.Day)
if err != nil {
return Comic{}, fmt.Errorf("failed to parse day: %v", err)
}
m, err := strconv.Atoi(cr.Month)
if err != nil {
return Comic{}, fmt.Errorf("failed to parse month: %v", err)
}
y, err := strconv.Atoi(cr.Year)
if err != nil {
return Comic{}, fmt.Errorf("failed to parse year: %v", err)
}
return Comic{
Alt: cr.Alt,
Day: d,
ImageURL: cr.Img,
URL: cr.Link,
Month: m,
News: cr.News,
Number: cr.Num,
SafeTitle: cr.SafeTitle,
Title: cr.Title,
Transcript: cr.Transcript,
Year: y,
}, nil
}
func drainAndClose(r io.ReadCloser) {
io.Copy(ioutil.Discard, r)
r.Close()
}