-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathsoundcloudapi.go
249 lines (215 loc) · 6.49 KB
/
soundcloudapi.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
package soundcloudapi
import (
"io"
"net/http"
"strings"
"github.com/pkg/errors"
)
// API is a wrapper for the SoundCloud private API used internally for soundcloud.com
type API struct {
client *client
StripMobilePrefix bool
ConvertFirebaseURLs bool
}
// APIOptions are the options for creating an API struct
type APIOptions struct {
ClientID string // optional and a new one will be fetched if not provided
HTTPClient *http.Client // the HTTP client to make requests with
StripMobilePrefix bool // whether or not to convert mobile URLs to regular URLs
ConvertFirebaseURLs bool // whether or not to convert SoundCloud firebase URLs to regular URLs
}
// New returns a pointer to a new SoundCloud API struct.
func New(options APIOptions) (*API, error) {
if options.ClientID == "" {
var err error
options.ClientID, err = FetchClientID()
if err != nil {
return nil, errors.Wrap(err, "Failed to initiaze SounCloudAPI")
}
}
if options.HTTPClient == nil {
options.HTTPClient = http.DefaultClient
}
return &API{
client: newClient(options.ClientID, options.HTTPClient),
StripMobilePrefix: options.StripMobilePrefix,
ConvertFirebaseURLs: options.ConvertFirebaseURLs,
}, nil
}
// SetClientID sets the client ID
func (sc *API) SetClientID(clientID string) {
sc.client.clientID = clientID
}
// ClientID returns the client ID
func (sc *API) ClientID() string {
return sc.client.clientID
}
// GetTrackInfo returns the info for the track given tracks
//
// If URL is supplied, it will return the info for a single track given by that url.
// If an array of ids is supplied, it will return an array of track info.
//
// WARNING: Private tracks will not be fetched unless options.PlaylistID and options.PlaylistSecretToken
// are provided.
func (sc *API) GetTrackInfo(options GetTrackInfoOptions) ([]Track, error) {
if options.URL != "" {
url, err := sc.prepareURL(options.URL)
if err != nil {
return nil, err
}
options.URL = url
id := ExtractIDFromPersonalizedTrackURL(options.URL)
if id != -1 {
return sc.client.getTrackInfo(GetTrackInfoOptions{ID: []int64{id}})
}
}
return sc.client.getTrackInfo(options)
}
// GetPlaylistInfo returns the info for a playlist
func (sc *API) GetPlaylistInfo(url string) (Playlist, error) {
return sc.client.getPlaylistInfo(StripMobilePrefix(url))
}
// DownloadTrack downloads the track specified by the given Transcoding's URL to dst
func (sc *API) DownloadTrack(transcoding Transcoding, dst io.Writer) error {
url, err := sc.prepareURL(transcoding.URL)
if err != nil {
return err
}
u, err := sc.client.getMediaURL(url)
if err != nil {
return err
}
if strings.Contains(transcoding.URL, "progressive") {
// Progressive download
err = sc.client.downloadProgressive(u, dst)
} else {
// HLS download
err = sc.client.downloadHLS(u, dst)
}
return err
}
// GetLikes returns a PaginatedQuery with the Collection field member as a list of tracks
func (sc *API) GetLikes(options GetLikesOptions) (*PaginatedQuery, error) {
url, err := sc.prepareURL(options.ProfileURL)
if err != nil {
return nil, err
}
options.ProfileURL = url
return sc.client.getLikes(options)
}
// Search returns a PaginatedQuery for searching a specific query
func (sc *API) Search(options SearchOptions) (*PaginatedQuery, error) {
return sc.client.search(options)
}
// GetUser returns a User
func (sc *API) GetUser(options GetUserOptions) (User, error) {
url, err := sc.prepareURL(options.ProfileURL)
if err != nil {
return User{}, err
}
options.ProfileURL = url
return sc.client.getUser(options)
}
// GetDownloadURL retuns the URL to download a track. This is useful if you want to implement your own
// downloading algorithm.
// If the track has a publicly available download link, that link will be preferred and the streamType parameter will be ignored.
// streamType can be either "hls" or "progressive", defaults to "progressive"
func (sc *API) GetDownloadURL(url string, streamType string) (string, error) {
url, err := sc.prepareURL(url)
if err != nil {
return "", err
}
streamType = strings.ToLower(streamType)
if streamType == "" {
streamType = "progressive"
}
if IsURL(url, false, false) && !IsPlaylistURL(url) {
info, err := sc.client.getTrackInfo(GetTrackInfoOptions{
URL: url,
})
if err != nil {
return "", err
}
if len(info) == 0 {
return "", errors.New("Could not find a track with that URL")
}
if info[0].Downloadable && info[0].HasDownloadsLeft {
downloadURL, err := sc.client.getDownloadURL(info[0].ID)
if err != nil {
return "", err
}
return downloadURL, nil
}
for _, transcoding := range info[0].Media.Transcodings {
if strings.ToLower(transcoding.Format.Protocol) == streamType {
mediaURL, err := sc.client.getMediaURL(transcoding.URL)
if err != nil {
return "", err
}
return mediaURL, nil
}
}
mediaURL, err := sc.client.getMediaURL(info[0].Media.Transcodings[0].URL)
if err != nil {
return "", err
}
return mediaURL, nil
}
return "", errors.New("URL is not a track URL")
}
func (sc *API) prepareURL(url string) (string, error) {
if sc.StripMobilePrefix {
if IsMobileURL(url) {
url = StripMobilePrefix(url)
}
}
if sc.ConvertFirebaseURLs {
if IsFirebaseURL(url) {
var err error
url, err = ConvertFirebaseLink(url)
if err != nil {
return "", errors.Wrap(err, "Failed to convert Firebase URL")
}
}
}
if IsNewMobileURL(url) {
var err error
url, err = sc.ConvertNewMobileURL(url)
if err != nil {
return "", errors.Wrap(err, "failed to convert new mobile url")
}
}
return url, nil
}
func (sc *API) ConvertNewMobileURL(url string) (string, error) {
client := new(http.Client)
type urlResp struct {
url *string
err error
}
urlChan := make(chan urlResp, 1)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
u := req.URL.String()
if IsURL(u, false, false) {
urlChan <- urlResp{url: &u, err: nil}
}
return nil
}
_, err := client.Get(url)
select {
case urlR := <-urlChan:
if urlR.url == nil {
return "", errors.New("unable to retrieve redirect url for new mobile url")
}
return *urlR.url, nil
default:
if err != nil {
return "", errors.Wrap(err, "failed to get redirect url")
}
return "", errors.New("new mobile url is supposed to have redirects")
}
}
// IsURL is a shorthand for IsURL(url, sc.StripMobilePrefix, sc.ConvertFirebaseURLs)
func (sc *API) IsURL(url string) bool {
return IsURL(url, sc.StripMobilePrefix, sc.ConvertFirebaseURLs)
}