comparison src/services/kitsu.cc @ 319:d928ec7b6a0d

services/kitsu: implement GetAnimeList() it finally works!
author Paper <paper@paper.us.eu.org>
date Wed, 12 Jun 2024 17:52:26 -0400
parents b1f4d1867ab1
children 1b5c04268d6a
comparison
equal deleted inserted replaced
318:3b355fa948c7 319:d928ec7b6a0d
121 return auth.access_token; 121 return auth.access_token;
122 } 122 }
123 123
124 /* ----------------------------------------------------------------------------- */ 124 /* ----------------------------------------------------------------------------- */
125 125
126 static std::optional<std::string> SendRequest(const std::string& path, const std::map<std::string, std::string>& params) { 126 static std::optional<nlohmann::json> SendJSONAPIRequest(const std::string& path, const std::map<std::string, std::string>& params) {
127 std::optional<std::string> token = AccountAccessToken(); 127 std::optional<std::string> token = AccountAccessToken();
128 if (!token) 128 if (!token)
129 return std::nullopt; 129 return std::nullopt;
130 130
131 const std::vector<std::string> headers = { 131 const std::vector<std::string> headers = {
134 "Content-Type: application/vnd.api+json" 134 "Content-Type: application/vnd.api+json"
135 }; 135 };
136 136
137 const std::string url = HTTP::EncodeParamsList(std::string(BASE_API_PATH) + path, params); 137 const std::string url = HTTP::EncodeParamsList(std::string(BASE_API_PATH) + path, params);
138 138
139 return Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get)); 139 const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get));
140 if (response.empty())
141 return std::nullopt;
142
143 std::optional<nlohmann::json> result;
144 try {
145 result = nlohmann::json::parse(response);
146 } catch (const std::exception& ex) {
147 session.SetStatusBar(std::string("Kitsu: Failed to parse response with error \"") + ex.what() + "\"!");
148 return std::nullopt;
149 }
150
151 const nlohmann::json& json = result.value();
152
153 if (json.contains("/errors"_json_pointer)) {
154 for (const auto& item : json["/errors"])
155 std::cerr << "Kitsu: API returned error \"" << json["/errors/title"_json_pointer] << "\" with detail \"" << json["/errors/detail"] << std::endl;
156
157 session.SetStatusBar("Kitsu: Request failed with errors!");
158 return std::nullopt;
159 }
160
161 return result;
140 } 162 }
141 163
142 static void ParseTitleJson(Anime::Anime& anime, const nlohmann::json& json) { 164 static void ParseTitleJson(Anime::Anime& anime, const nlohmann::json& json) {
143 static const std::map<std::string, Anime::TitleLanguage> lookup = { 165 static const std::map<std::string, Anime::TitleLanguage> lookup = {
144 {"en", Anime::TitleLanguage::English}, 166 {"en", Anime::TitleLanguage::English},
165 return; 187 return;
166 188
167 anime.SetFormat(lookup.at(str)); 189 anime.SetFormat(lookup.at(str));
168 } 190 }
169 191
170 static const std::string FAILED_TO_PARSE = "Kitsu: Failed to parse anime object!"; 192 static void ParseListStatus(Anime::Anime& anime, const std::string& str) {
193 static const std::map<std::string, Anime::ListStatus> lookup = {
194 {"completed", Anime::ListStatus::Completed},
195 {"current", Anime::ListStatus::Current},
196 {"dropped", Anime::ListStatus::Dropped},
197 {"on_hold", Anime::ListStatus::Paused},
198 {"planned", Anime::ListStatus::Planning}
199 };
200
201 if (lookup.find(str) == lookup.end())
202 return;
203
204 anime.SetUserStatus(lookup.at(str));
205 }
171 206
172 static int ParseAnimeJson(const nlohmann::json& json) { 207 static int ParseAnimeJson(const nlohmann::json& json) {
208 static const std::string FAILED_TO_PARSE = "Kitsu: Failed to parse anime object!";
209
173 const std::string service_id = json["/id"_json_pointer].get<std::string>(); 210 const std::string service_id = json["/id"_json_pointer].get<std::string>();
174 if (service_id.empty()) { 211 if (service_id.empty()) {
175 session.SetStatusBar(FAILED_TO_PARSE); 212 session.SetStatusBar(FAILED_TO_PARSE + " (/id)");
176 return 0; 213 return 0;
177 } 214 }
178 215
179 if (!json.contains("/attributes"_json_pointer)) { 216 if (!json.contains("/attributes"_json_pointer)) {
180 session.SetStatusBar(FAILED_TO_PARSE); 217 session.SetStatusBar(FAILED_TO_PARSE + " (/attributes)");
181 return 0; 218 return 0;
182 } 219 }
183 220
184 const auto& attributes = json["/attributes"_json_pointer]; 221 const auto& attributes = json["/attributes"_json_pointer];
185 222
186 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); 223 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id);
187 if (!id) { 224 if (!id) {
188 session.SetStatusBar(FAILED_TO_PARSE); 225 session.SetStatusBar(FAILED_TO_PARSE + " getting unused ID");
189 return 0; 226 return 0;
190 } 227 }
191 228
192 Anime::Anime& anime = Anime::db.items[id]; 229 Anime::Anime& anime = Anime::db.items[id];
193 230
194 anime.SetId(id); 231 anime.SetId(id);
195 anime.SetServiceId(Anime::Service::Kitsu, service_id); 232 anime.SetServiceId(Anime::Service::Kitsu, service_id);
196 anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>()); 233
197 ParseTitleJson(anime, attributes["/titles"_json_pointer]); 234 if (attributes.contains("/synopsis"_json_pointer) && attributes["/synopsis"_json_pointer].is_string())
235 anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>());
236
237 if (attributes.contains("/titles"_json_pointer) && attributes["/titles"_json_pointer].is_object())
238 ParseTitleJson(anime, attributes["/titles"_json_pointer]);
198 239
199 // FIXME: parse abbreviatedTitles for synonyms?? 240 // FIXME: parse abbreviatedTitles for synonyms??
200 241
201 anime.SetAudienceScore(JSON::GetNumber<double>(attributes, "/averageRating"_json_pointer)); 242 if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_number())
202 243 anime.SetAudienceScore(JSON::GetNumber<double>(attributes, "/averageRating"_json_pointer));
203 if (attributes.contains("/startDate"_json_pointer)) 244
245 if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string())
204 anime.SetAirDate(attributes["/startDate"_json_pointer].get<std::string>()); 246 anime.SetAirDate(attributes["/startDate"_json_pointer].get<std::string>());
205 247
206 // TODO: endDate 248 // TODO: endDate
207 249
208 if (attributes.contains("/subtype"_json_pointer)) 250 if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string())
209 ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>()); 251 ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>());
210 252
211 anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>()); 253 if (attributes.contains("/posterImage/original"_json_pointer) && attributes["/posterImage/original"_json_pointer].is_string())
212 anime.SetEpisodes(attributes["/episodeCount"_json_pointer].get<int>()); 254 anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>());
213 anime.SetDuration(attributes["/episodeLength"_json_pointer].get<int>()); 255
256 if (attributes.contains("/episodeCount"_json_pointer) && attributes["/episodeCount"_json_pointer].is_number())
257 anime.SetEpisodes(attributes["/episodeCount"_json_pointer].get<int>());
258
259 if (attributes.contains("/episodeLength"_json_pointer) && attributes["/episodeLength"_json_pointer].is_number())
260 anime.SetDuration(attributes["/episodeLength"_json_pointer].get<int>());
214 261
215 return id; 262 return id;
216 } 263 }
217 264
218 static int ParseLibraryJson(const nlohmann::json& json) { 265 static int ParseLibraryJson(const nlohmann::json& json) {
219 if (!json.contains("/relationships/anime/data"_json_pointer) 266 static const std::vector<nlohmann::json::json_pointer> required = {
220 || !json.contains("/attributes"_json_pointer) 267 "/id"_json_pointer,
221 || !json.contains("/id"_json_pointer)) { 268 "/relationships/anime/data/id"_json_pointer,
269 "/attributes"_json_pointer,
270 };
271
272 for (const auto& ptr : required) {
273 if (!json.contains(ptr)) {
274 session.SetStatusBar(std::string("Kitsu: Failed to parse library object! (missing ") + ptr.to_string() + ")");
275 return 0;
276 }
277 }
278
279 std::string service_id = json["/relationships/anime/data/id"_json_pointer].get<std::string>();
280 if (service_id.empty()) {
222 session.SetStatusBar("Kitsu: Failed to parse library object!"); 281 session.SetStatusBar("Kitsu: Failed to parse library object!");
223 return 0; 282 return 0;
224 } 283 }
225 284
226 int id = ParseAnimeJson(json["/relationships/anime/data"_json_pointer]); 285 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id);
227 if (!id)
228 return 0;
229 286
230 const auto& attributes = json["/attributes"_json_pointer]; 287 const auto& attributes = json["/attributes"_json_pointer];
231 288
232 const std::string library_id = json["/id"_json_pointer].get<std::string>(); 289 const std::string library_id = json["/id"_json_pointer].get<std::string>();
233 290
234 Anime::Anime& anime = Anime::db.items[id]; 291 Anime::Anime& anime = Anime::db.items[id];
235 292
236 anime.AddToUserList(); 293 anime.AddToUserList();
237 294
295 anime.SetId(id);
296 anime.SetServiceId(Anime::Service::Kitsu, service_id);
297
238 anime.SetUserId(library_id); 298 anime.SetUserId(library_id);
239 anime.SetUserDateStarted(Date(attributes["/startedAt"_json_pointer].get<std::string>())); 299
240 anime.SetUserDateCompleted(Date(attributes["/finishedAt"_json_pointer].get<std::string>())); 300 if (attributes.contains("/startedAt"_json_pointer) && attributes["/startedAt"_json_pointer].is_string())
241 anime.SetUserNotes(attributes["/notes"_json_pointer].get<std::string>()); 301 anime.SetUserDateStarted(Date(Time::ParseISO8601Time(attributes["/startedAt"_json_pointer].get<std::string>())));
242 anime.SetUserProgress(attributes["/progress"_json_pointer].get<int>()); 302
243 anime.SetUserScore(attributes["/ratingTwenty"_json_pointer].get<int>() * 5); 303 if (attributes.contains("/finishedAt"_json_pointer) && attributes["/finishedAt"_json_pointer].is_string())
244 anime.SetUserIsPrivate(attributes["/private"_json_pointer].get<bool>()); 304 anime.SetUserDateCompleted(Date(Time::ParseISO8601Time(attributes["/finishedAt"_json_pointer].get<std::string>())));
245 anime.SetUserRewatchedTimes(attributes["/reconsumeCount"_json_pointer].get<int>()); 305
246 anime.SetUserIsRewatching(attributes["/reconsuming"_json_pointer].get<bool>()); /* "reconsuming". really? */ 306 if (attributes.contains("/notes"_json_pointer) && attributes["/notes"_json_pointer].is_string())
247 // anime.SetUserStatus(); 307 anime.SetUserNotes(attributes["/notes"_json_pointer].get<std::string>());
248 // anime.SetUserLastUpdated(); 308
309 if (attributes.contains("/progress"_json_pointer) && attributes["/progress"_json_pointer].is_number())
310 anime.SetUserProgress(attributes["/progress"_json_pointer].get<int>());
311
312 if (attributes.contains("/ratingTwenty"_json_pointer) && attributes["/ratingTwenty"_json_pointer].is_number())
313 anime.SetUserScore(attributes["/ratingTwenty"_json_pointer].get<int>() * 5);
314
315 if (attributes.contains("/private"_json_pointer) && attributes["/private"_json_pointer].is_boolean())
316 anime.SetUserIsPrivate(attributes["/private"_json_pointer].get<bool>());
317
318 if (attributes.contains("/reconsumeCount"_json_pointer) && attributes["/reconsumeCount"_json_pointer].is_number())
319 anime.SetUserRewatchedTimes(attributes["/reconsumeCount"_json_pointer].get<int>());
320
321 if (attributes.contains("/reconsuming"_json_pointer) && attributes["/reconsuming"_json_pointer].is_boolean())
322 anime.SetUserIsRewatching(attributes["/reconsuming"_json_pointer].get<bool>()); /* lmfao "reconsuming" */
323
324 if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string())
325 ParseListStatus(anime, attributes["/status"_json_pointer].get<std::string>());
326
327 if (attributes.contains("/progressedAt"_json_pointer) && attributes["/progressedAt"_json_pointer].is_string())
328 anime.SetUserTimeUpdated(Time::ParseISO8601Time(attributes["/progressedAt"_json_pointer].get<std::string>()));
249 329
250 return id; 330 return id;
251 } 331 }
252 332
333 static bool ParseAnyJson(const nlohmann::json& json) {
334 enum class Variant {
335 Unknown,
336 Anime,
337 LibraryEntry,
338 Category,
339 Producer,
340 };
341
342 static const std::map<std::string, Variant> lookup = {
343 {"anime", Variant::Anime},
344 {"libraryEntries", Variant::LibraryEntry},
345 {"category", Variant::Category},
346 {"producers", Variant::Producer}
347 };
348
349 static const nlohmann::json::json_pointer required = "/type"_json_pointer;
350 if (!json.contains(required) && !json[required].is_string()) {
351 session.SetStatusBar(std::string("Kitsu: Failed to parse generic object! (missing ") + required.to_string() + ")");
352 return 0;
353 }
354
355 Variant variant = Variant::Unknown;
356
357 std::string json_type = json["/type"_json_pointer].get<std::string>();
358
359 if (lookup.find(json_type) != lookup.end())
360 variant = lookup.at(json_type);
361
362 switch (variant) {
363 case Variant::Anime:
364 return !!ParseAnimeJson(json);
365 case Variant::LibraryEntry:
366 return !!ParseLibraryJson(json);
367 /* ... */
368 case Variant::Category:
369 case Variant::Producer:
370 return true;
371 default:
372 std::cerr << "Kitsu: received unknown type " << json_type << std::endl;
373 return true;
374 }
375 }
376
253 int GetAnimeList() { 377 int GetAnimeList() {
254 return 0; 378 static constexpr int LIBRARY_MAX_SIZE = 500;
379
380 const auto& auth = session.config.auth.kitsu;
381
382 if (auth.user_id.empty()) {
383 session.SetStatusBar("Kitsu: User ID is unavailable!");
384 return 0;
385 }
386
387 int page = 0;
388 bool have_next_page = true;
389
390 std::map<std::string, std::string> params = {
391 {"filter[user_id]", auth.user_id},
392 {"filter[kind]", "anime"},
393 {"include", "anime"},
394 {"page[offset]", Strings::ToUtf8String(page)},
395 {"page[limit]", Strings::ToUtf8String(LIBRARY_MAX_SIZE)}
396 };
397
398 Anime::db.RemoveAllUserData();
399
400 bool success = true;
401
402 while (have_next_page) {
403 std::optional<nlohmann::json> response = SendJSONAPIRequest("/library-entries", params);
404 if (!response)
405 return 0;
406
407 const nlohmann::json& root = response.value();
408
409 if (root.contains("/next"_json_pointer) && root["/next"_json_pointer].is_number()) {
410 page += root["/next"_json_pointer].get<int>();
411 if (page <= 0)
412 have_next_page = false;
413 } else have_next_page = false;
414
415 for (const auto& item : root["/data"_json_pointer])
416 if (!ParseLibraryJson(item))
417 success = false;
418
419 for (const auto& variant : root["/included"_json_pointer])
420 if (!ParseAnyJson(variant))
421 success = false;
422
423 params["page[offset]"] = Strings::ToUtf8String(page);
424 }
425
426 if (success)
427 session.SetStatusBar("Kitsu: Successfully received library data!");
428
429 return 1;
255 } 430 }
256 431
257 /* unimplemented for now */ 432 /* unimplemented for now */
258 std::vector<int> Search(const std::string& search) { 433 std::vector<int> Search(const std::string& search) {
259 return {}; 434 return {};
279 454
280 static const std::map<std::string, std::string> params = { 455 static const std::map<std::string, std::string> params = {
281 {"filter[self]", "true"} 456 {"filter[self]", "true"}
282 }; 457 };
283 458
284 std::optional<std::string> response = SendRequest("/users", params); 459 std::optional<nlohmann::json> response = SendJSONAPIRequest("/users", params);
285 if (!response) 460 if (!response)
286 return false; // whuh? 461 return false; // whuh?
287 462
288 nlohmann::json json; 463 const nlohmann::json& json = response.value();
289 try {
290 json = nlohmann::json::parse(response.value());
291 } catch (const std::exception& ex) {
292 session.SetStatusBar(std::string("Kitsu: Failed to parse user data with error \"") + ex.what() + "\"!");
293 return false;
294 }
295 464
296 if (!json.contains("/data/0/id"_json_pointer)) { 465 if (!json.contains("/data/0/id"_json_pointer)) {
297 session.SetStatusBar("Kitsu: Failed to retrieve user ID!"); 466 session.SetStatusBar("Kitsu: Failed to retrieve user ID!");
298 return false; 467 return false;
299 } 468 }
300 469
301 session.SetStatusBar("Kitsu: Successfully retrieved user data!"); 470 session.SetStatusBar("Kitsu: Successfully authorized user!");
302 session.config.auth.kitsu.user_id = json["/data/0/id"_json_pointer].get<std::string>(); 471 session.config.auth.kitsu.user_id = json["/data/0/id"_json_pointer].get<std::string>();
303 472
304 return true; 473 return true;
305 } 474 }
306 475