-
Notifications
You must be signed in to change notification settings - Fork 1
/
client.go
246 lines (205 loc) · 6.29 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
package odesli
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"time"
)
const (
APIPath = "https://api.song.link/v1-alpha.1"
LinksPath = APIPath + "/links"
)
const DefaultHTTPTimeout = 60 * time.Second
type GetLinksRequest struct {
// The unique identifier of the streaming entity, e.g.`1443109064` which is an
// iTunes ID.If `url` is not provided, you must provide `platform`, `type` and
// `id`.
ID string
// The URL of a valid song or album from any of our supported platforms. It is
// safest to encode the URL, e.g. with `encodeURIComponent()`.
URL string
// Two-letter country code. Specifies the country/location we use when searching
// streaming catalogs. Optional. Defaults to `US`.
UserCountry string
// The platform of the entity you'd like to match. See above section for
// supported platforms. If `url` is not provided, you must provide `platform`,
// `type` and `id`.
Platform Platform
// The type of streaming entity. We support `song` and `album`. If `url` is not
// provided, you must provide `platform`, `type` and `id`.
Type EntityType
}
func (r GetLinksRequest) GetURLValues() url.Values {
v := url.Values{}
if r.ID != "" {
v.Set("id", r.ID)
}
if r.URL != "" {
v.Set("url", r.URL)
}
if r.UserCountry != "" {
v.Set("userCountry", r.UserCountry)
}
if r.Platform != "" {
v.Set("platform", string(r.Platform))
}
if r.Type != "" {
v.Set("type", string(r.Type))
}
return v
}
type GetLinksResponse struct {
// The unique ID for the input entity that was supplied in the request. The data
// for this entity, such as title, artistName, etc. will be found in an object at
// `nodesByUniqueId[entityUniqueId]`
EntityUniqueID string `json:"entityUniqueId,omitempty"`
// The userCountry query param that was supplied in the request. It signals
// the country/availability we use to query the streaming platforms. Defaults
// to 'US' if no userCountry supplied in the request.
//
// NOTE: As a fallback, our service may respond with matches that were found in a
// locale other than the userCountry supplied
UserCountry string `json:"userCountry,omitempty"`
// A URL that will render the Songlink page for this entity
PageUrl string `json:"pageUrl,omitempty"`
// A collection of objects. Each key is a platform, and each value is an
// object that contains data for linking to the match
LinksByPlatform map[Platform]LinkByPlatform
// A collection of objects. Each key is a unique identifier for a streaming
// entity, and each value is an object that contains data for that entity, such
// as `title`, `artistName`, `thumbnailUrl`, etc.
EntitiesByUniqueId map[string]Entity `json:"entitiesByUniqueId,omitempty"`
}
type Entity struct {
// This is the unique identifier on the streaming platform/API provider
Id string `json:"id,omitempty"`
Type EntityType `json:"type,omitempty"`
Title string `json:"title,omitempty"`
ArtistName string `json:"artistName,omitempty"`
ThumbnailUrl string `json:"thumbnailUrl,omitempty"`
ThumbnailWidth int `json:"thumbnailWidth,omitempty"`
ThumbnailHeight int `json:"thumbnailHeight,omitempty"`
// The API provider that powered this match. Useful if you'd like to use
// this entity's data to query the API directly
ApiProvider APIProvider `json:"apiProvider,omitempty"`
// An array of platforms that are "powered" by this entity. E.g. an entity
// from Apple Music will generally have a `platforms` array of
// `["appleMusic", "itunes"]` since both those platforms/links are derived
// from this single entity
Platforms []Platform `json:"platforms,omitempty"`
}
func (e *Entity) UnmarshalJSON(data []byte) error {
type Alias Entity
aux := &struct {
Id interface{} `json:"id"` // unmarshal as string or number as priority field for id
*Alias
}{
Alias: (*Alias)(e),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
switch id := aux.Id.(type) {
case string:
e.Id = id
case int:
e.Id = strconv.Itoa(id)
case int64:
e.Id = strconv.FormatInt(id, 10)
case float32:
e.Id = strconv.FormatFloat(float64(id), 'f', -1, 64)
case float64:
e.Id = strconv.FormatFloat(id, 'f', -1, 64)
default:
return fmt.Errorf("unexpected type for id: %T", id)
}
return nil
}
type LinkByPlatform struct {
// The unique ID for this entity. Use it to look up data about this entity
// at `entitiesByUniqueId[entityUniqueId]`
EntityUniqueId string `json:"entityUniqueId"`
// The URL for this match
Url string `json:"url"`
// The native app URI that can be used on mobile devices to open this
// entity directly in the native app
NativeAppUriMobile string `json:"nativeAppUriMobile"`
// The native app URI that can be used on desktop devices to open this
// entity directly in the native app
NativeAppUriDesktop string `json:"nativeAppUriDesktop"`
}
type API interface {
GetLinks(ctx context.Context, req GetLinksRequest) (GetLinksResponse, error)
}
type ClientOption struct {
APIToken string
Debug bool
}
type Client struct {
client *http.Client
debug bool
}
func NewClient(opt ClientOption) (*Client, error) {
var rt http.RoundTripper
t := &http.Transport{}
if opt.APIToken != "" {
rt = TransportWithAPIToken(t, opt.APIToken)
} else {
rt = t
}
return &Client{
client: &http.Client{
Transport: rt,
Timeout: DefaultHTTPTimeout,
},
debug: opt.Debug,
}, nil
}
func (c *Client) GetLinks(ctx context.Context, r GetLinksRequest) (GetLinksResponse, error) {
path := fmt.Sprint(LinksPath, "?", r.GetURLValues().Encode())
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
path,
nil,
)
if err != nil {
return GetLinksResponse{}, err
}
if c.debug {
log.Printf("request: %s", req.URL.String())
}
resp, err := c.client.Do(req)
if err != nil {
return GetLinksResponse{}, err
}
defer func() { _ = resp.Body.Close() }()
err = checkResponse(resp)
if err != nil {
return GetLinksResponse{}, err
}
res := GetLinksResponse{}
body, err := io.ReadAll(resp.Body)
if err != nil {
return GetLinksResponse{}, err
}
if c.debug {
log.Printf("body: %s", string(body))
}
err = json.Unmarshal(body, &res)
if err != nil {
return GetLinksResponse{}, err
}
return res, nil
}
func checkResponse(resp *http.Response) error {
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
return fmt.Errorf("unexpected response: %s", resp.Status)
}