1 #include "lib/spotify/api.hpp"
2
3 using namespace lib::spt;
4
api(lib::settings & settings,const lib::http_client & http_client)5 api::api(lib::settings &settings, const lib::http_client &http_client)
6 : settings(settings),
7 http(http_client)
8 {
9 }
10
refresh(bool force)11 void api::refresh(bool force)
12 {
13 constexpr long s_in_hour = 60 * 60;
14
15 if (!force
16 && lib::date_time::seconds_since_epoch() - settings.account.last_refresh < s_in_hour)
17 {
18 lib::log::dev("Last refresh was less than an hour ago, not refreshing access token");
19 last_auth = settings.account.last_refresh;
20 return;
21 }
22
23 // Make sure we have a refresh token
24 auto refresh_token = settings.account.refresh_token;
25 if (refresh_token.empty())
26 {
27 throw lib::spt::error("No refresh token", "token");
28 }
29
30 // Create form
31 auto post_data = lib::fmt::format("grant_type=refresh_token&refresh_token={}",
32 refresh_token);
33
34 // Create request
35 auto auth_header = lib::fmt::format("Basic {}",
36 lib::base64::encode(lib::fmt::format("{}:{}",
37 settings.account.client_id, settings.account.client_secret)));
38
39 // Send request
40 auto reply = request_refresh(post_data, auth_header);
41 if (reply.empty())
42 {
43 throw lib::spt::error("No response", "token");
44 }
45
46 // Parse as JSON
47 const auto json = nlohmann::json::parse(reply);
48
49 // Check if error
50 if (json.contains("error_description") || !json.contains("access_token"))
51 {
52 auto error = json.at("error_description").get<std::string>();
53 throw lib::spt::error(error.empty()
54 ? "No access token" : error, "token");
55 }
56
57 // Save as access token
58 last_auth = lib::date_time::seconds_since_epoch();
59 settings.account.last_refresh = last_auth;
60 settings.account.access_token = json.at("access_token").get<std::string>();
61 settings.save();
62 }
63
auth_headers()64 auto api::auth_headers() -> lib::headers
65 {
66 constexpr int secsInHour = 3600;
67
68 // See when last refresh was
69 auto last_refresh = lib::date_time::seconds_since_epoch() - last_auth;
70 if (last_refresh >= secsInHour)
71 {
72 lib::log::dev("Access token probably expired, refreshing");
73 try
74 {
75 refresh();
76 }
77 catch (const std::exception &e)
78 {
79 lib::log::error("Refresh failed: {}", e.what());
80 }
81 }
82
83 return {
84 {
85 "Authorization",
86 lib::fmt::format("Bearer {}", settings.account.access_token),
87 },
88 };
89 }
90
parse_json(const std::string & url,const std::string & data)91 auto api::parse_json(const std::string &url, const std::string &data) -> nlohmann::json
92 {
93 // No data, no response, no error
94 if (data.empty())
95 {
96 return nlohmann::json();
97 }
98
99 auto json = nlohmann::json::parse(data);
100
101 if (!lib::spt::error::is(json))
102 {
103 return json;
104 }
105
106 auto err = lib::spt::error::error_message(json);
107 lib::log::error("{} failed: {}", url, err);
108 throw lib::spt::error(err, url);
109 }
110
request_refresh(const std::string & post_data,const std::string & authorization)111 auto api::request_refresh(const std::string &post_data,
112 const std::string &authorization) -> std::string
113 {
114 return http.post("https://accounts.spotify.com/api/token", {
115 {"Content-Type", "application/x-www-form-urlencoded"},
116 {"Authorization", authorization},
117 }, post_data);
118 }
119
error_message(const std::string & url,const std::string & data)120 auto api::error_message(const std::string &url, const std::string &data) -> std::string
121 {
122 nlohmann::json json;
123 try
124 {
125 if (!data.empty())
126 {
127 json = nlohmann::json::parse(data);
128 }
129 }
130 catch (const std::exception &e)
131 {
132 lib::log::warn("{} failed: {}", url, e.what());
133 return std::string();
134 }
135
136 if (json.is_null() || !json.is_object() || !json.contains("error"))
137 {
138 return std::string();
139 }
140
141 auto message = json.at("error").at("message").get<std::string>();
142 if (!message.empty())
143 {
144 lib::log::error("{} failed: {}", url, message);
145 }
146 return message;
147 }
148
select_device(const std::vector<lib::spt::device> &,lib::callback<lib::spt::device> & callback)149 void api::select_device(const std::vector<lib::spt::device> &/*devices*/,
150 lib::callback<lib::spt::device> &callback)
151 {
152 callback(lib::spt::device());
153 }
154
to_uri(const std::string & type,const std::string & id)155 auto api::to_uri(const std::string &type, const std::string &id) -> std::string
156 {
157 return lib::strings::starts_with(id, "spotify:")
158 ? id
159 : lib::fmt::format("spotify:{}:{}", type, id);
160 }
161
to_id(const std::string & id)162 auto api::to_id(const std::string &id) -> std::string
163 {
164 auto i = lib::strings::last_index_of(id, ":");
165 return i >= 0
166 ? id.substr(i + 1)
167 : id;
168 }
169
to_full_url(const std::string & relative_url)170 auto api::to_full_url(const std::string &relative_url) -> std::string
171 {
172 return lib::fmt::format("https://api.spotify.com/v1/{}", relative_url);
173 }
174
follow_type_string(lib::follow_type type)175 auto api::follow_type_string(lib::follow_type type) -> std::string
176 {
177 switch (type)
178 {
179 case lib::follow_type::artist:
180 return "artist";
181
182 case lib::follow_type::user:
183 return "user";
184 }
185
186 return std::string();
187 }
188
set_current_device(const std::string & id)189 void api::set_current_device(const std::string &id)
190 {
191 settings.general.last_device = id;
192 settings.save();
193 }
194
get_current_device() const195 auto api::get_current_device() const -> const std::string &
196 {
197 return settings.general.last_device;
198 }
199
200 //region GET
201
get(const std::string & url,lib::callback<nlohmann::json> & callback)202 void api::get(const std::string &url, lib::callback<nlohmann::json> &callback)
203 {
204 http.get(to_full_url(url), auth_headers(),
205 [url, callback](const std::string &response)
206 {
207 try
208 {
209 callback(response.empty()
210 ? nlohmann::json()
211 : nlohmann::json::parse(response));
212 }
213 catch (const std::exception &e)
214 {
215 lib::log::error("{} failed: {}", url, e.what());
216 }
217 });
218 }
219
get_items(const std::string & url,const std::string & key,lib::callback<nlohmann::json> & callback)220 void api::get_items(const std::string &url, const std::string &key,
221 lib::callback<nlohmann::json> &callback)
222 {
223 constexpr size_t api_prefix_length = 27;
224
225 auto api_url = lib::strings::starts_with(url, "https://api.spotify.com/v1/")
226 ? url.substr(api_prefix_length)
227 : url;
228
229 get(api_url, [this, key, callback](const nlohmann::json &json)
230 {
231 if (!key.empty() && !json.contains(key))
232 {
233 lib::log::error(R"(no such key "{}" in "{}" ({}))", key, json.dump());
234 }
235
236 const auto &items = (key.empty() ? json : json.at(key)).at("items");
237 if (json.contains("next") && json.at("next").is_string())
238 {
239 const auto &next = json.at("next").get<std::string>();
240 get_items(next, key, [items, callback](const nlohmann::json &next)
241 {
242 callback(lib::json::combine(items, next));
243 });
244 return;
245 }
246 callback(items);
247 });
248 }
249
get_items(const std::string & url,lib::callback<nlohmann::json> & callback)250 void api::get_items(const std::string &url, lib::callback<nlohmann::json> &callback)
251 {
252 get_items(url, std::string(), callback);
253 }
254
255 //endregion
256
257 //region PUT
258
put(const std::string & url,const nlohmann::json & body,lib::callback<std::string> & callback)259 void api::put(const std::string &url, const nlohmann::json &body,
260 lib::callback<std::string> &callback)
261 {
262 auto header = auth_headers();
263 header["Content-Type"] = "application/json";
264
265 auto data = body.is_null()
266 ? std::string()
267 : body.dump();
268
269 http.put(to_full_url(url), data, header,
270 [this, url, body, callback](const std::string &response)
271 {
272 auto error = error_message(url, response);
273
274 if (lib::strings::contains(error, "No active device found")
275 || lib::strings::contains(error, "Device not found"))
276 {
277 devices([this, url, body, error, callback]
278 (const std::vector<lib::spt::device> &devices)
279 {
280 if (devices.empty())
281 {
282 if (callback)
283 {
284 callback(error);
285 }
286 }
287 else
288 {
289 this->select_device(devices, [this, url, body, callback, error]
290 (const lib::spt::device &device)
291 {
292 if (device.id.empty())
293 {
294 callback(error);
295 return;
296 }
297
298 // Remember old device to replace in new URL
299 const auto &old_device = settings.general.last_device;
300
301 this->set_device(device, [this, url, body, callback, device, old_device]
302 (const std::string &status)
303 {
304 if (status.empty())
305 {
306 this->put(lib::strings::replace_all(url, old_device, device.id),
307 body, callback);
308 }
309 });
310 });
311 }
312 });
313 }
314 else if (callback)
315 {
316 callback(error);
317 }
318 });
319 }
320
put(const std::string & url,lib::callback<std::string> & callback)321 void api::put(const std::string &url, lib::callback<std::string> &callback)
322 {
323 put(url, nlohmann::json(), callback);
324 }
325
326 //endregion
327
328 //region POST
329
post(const std::string & url,lib::callback<std::string> & callback)330 void api::post(const std::string &url, lib::callback<std::string> &callback)
331 {
332 auto headers = auth_headers();
333 headers["Content-Type"] = "application/x-www-form-urlencoded";
334
335 http.post(to_full_url(url), headers, [url, callback](const std::string &response)
336 {
337 callback(error_message(url, response));
338 });
339 }
340
341 //endregion
342
343 //region DELETE
344
del(const std::string & url,const nlohmann::json & json,lib::callback<std::string> & callback)345 void api::del(const std::string &url, const nlohmann::json &json,
346 lib::callback<std::string> &callback)
347 {
348 auto headers = auth_headers();
349 headers["Content-Type"] = "application/json";
350
351 auto data = json.is_null()
352 ? std::string()
353 : json.dump();
354
355 http.del(to_full_url(url), data, headers,
356 [url, callback](const std::string &response)
357 {
358 callback(error_message(url, response));
359 });
360 }
361
del(const std::string & url,lib::callback<std::string> & callback)362 void api::del(const std::string &url, lib::callback<std::string> &callback)
363 {
364 del(url, nlohmann::json(), callback);
365 }
366
367 //endregion
368