comparison src/services/kitsu.cc @ 320:1b5c04268d6a

services/kitsu: ACTUALLY finish GetAnimeList there are some things the API just... doesn't provide. therefore we have to request the genres separately any time a new anime info box is opened...
author Paper <paper@paper.us.eu.org>
date Wed, 12 Jun 2024 19:49:19 -0400
parents d928ec7b6a0d
children 8141f409d52c
comparison
equal deleted inserted replaced
319:d928ec7b6a0d 320:1b5c04268d6a
121 return auth.access_token; 121 return auth.access_token;
122 } 122 }
123 123
124 /* ----------------------------------------------------------------------------- */ 124 /* ----------------------------------------------------------------------------- */
125 125
126 static std::optional<nlohmann::json> SendJSONAPIRequest(const std::string& path, const std::map<std::string, std::string>& params) { 126 static void AddAnimeFilters(std::map<std::string, std::string>& map) {
127 static const std::vector<std::string> fields = {
128 "abbreviatedTitles",
129 "averageRating",
130 "episodeCount",
131 "episodeLength",
132 "posterImage",
133 "startDate",
134 "status",
135 "subtype",
136 "titles",
137 "categories",
138 "synopsis",
139 "animeProductions",
140 };
141 static const std::string imploded = Strings::Implode(fields, ",");
142
143 map["fields[anime]"] = imploded;
144 map["fields[animeProductions]"] = "producer";
145 map["fields[categories]"] = "title";
146 map["fields[producers]"] = "name";
147 }
148
149 static void AddLibraryEntryFilters(std::map<std::string, std::string>& map) {
150 static const std::vector<std::string> fields = {
151 "anime",
152 "startedAt",
153 "finishedAt",
154 "notes",
155 "progress",
156 "ratingTwenty",
157 "reconsumeCount",
158 "reconsuming",
159 "status",
160 "updatedAt",
161 };
162 static const std::string imploded = Strings::Implode(fields, ",");
163
164 map["fields[libraryEntries]"] = imploded;
165 }
166
167 /* ----------------------------------------------------------------------------- */
168
169 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(); 170 std::optional<std::string> token = AccountAccessToken();
128 if (!token) 171 if (!token)
129 return std::nullopt; 172 return std::nullopt;
130 173
131 const std::vector<std::string> headers = { 174 const std::vector<std::string> headers = {
138 181
139 const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get)); 182 const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get));
140 if (response.empty()) 183 if (response.empty())
141 return std::nullopt; 184 return std::nullopt;
142 185
143 std::optional<nlohmann::json> result; 186 nlohmann::json json;
144 try { 187 try {
145 result = nlohmann::json::parse(response); 188 json = nlohmann::json::parse(response);
146 } catch (const std::exception& ex) { 189 } catch (const std::exception& ex) {
147 session.SetStatusBar(std::string("Kitsu: Failed to parse response with error \"") + ex.what() + "\"!"); 190 session.SetStatusBar(std::string("Kitsu: Failed to parse response with error \"") + ex.what() + "\"!");
148 return std::nullopt; 191 return std::nullopt;
149 } 192 }
150 193
151 const nlohmann::json& json = result.value();
152
153 if (json.contains("/errors"_json_pointer)) { 194 if (json.contains("/errors"_json_pointer)) {
154 for (const auto& item : json["/errors"]) 195 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; 196 std::cerr << "Kitsu: API returned error \"" << json["/errors/title"_json_pointer] << "\" with detail \"" << json["/errors/detail"] << std::endl;
156 197
157 session.SetStatusBar("Kitsu: Request failed with errors!"); 198 session.SetStatusBar("Kitsu: Request failed with errors!");
158 return std::nullopt; 199 return std::nullopt;
159 } 200 }
160 201
161 return result; 202 return json;
162 } 203 }
163 204
164 static void ParseTitleJson(Anime::Anime& anime, const nlohmann::json& json) { 205 static void ParseTitleJson(Anime::Anime& anime, const nlohmann::json& json) {
165 static const std::map<std::string, Anime::TitleLanguage> lookup = { 206 static const std::map<std::string, Anime::TitleLanguage> lookup = {
166 {"en", Anime::TitleLanguage::English}, 207 {"en", Anime::TitleLanguage::English},
202 return; 243 return;
203 244
204 anime.SetUserStatus(lookup.at(str)); 245 anime.SetUserStatus(lookup.at(str));
205 } 246 }
206 247
248 static void ParseSeriesStatus(Anime::Anime& anime, const std::string& str) {
249 static const std::map<std::string, Anime::SeriesStatus> lookup = {
250 {"current", Anime::SeriesStatus::Releasing},
251 {"finished", Anime::SeriesStatus::Finished},
252 {"tba", Anime::SeriesStatus::Hiatus}, // is this right?
253 {"unreleased", Anime::SeriesStatus::Cancelled},
254 {"upcoming", Anime::SeriesStatus::NotYetReleased},
255 };
256
257 if (lookup.find(str) == lookup.end())
258 return;
259
260 anime.SetAiringStatus(lookup.at(str));
261 }
262
207 static int ParseAnimeJson(const nlohmann::json& json) { 263 static int ParseAnimeJson(const nlohmann::json& json) {
208 static const std::string FAILED_TO_PARSE = "Kitsu: Failed to parse anime object!"; 264 static const std::string FAILED_TO_PARSE = "Kitsu: Failed to parse anime object!";
209 265
210 const std::string service_id = json["/id"_json_pointer].get<std::string>(); 266 const std::string service_id = json["/id"_json_pointer].get<std::string>();
211 if (service_id.empty()) { 267 if (service_id.empty()) {
219 } 275 }
220 276
221 const auto& attributes = json["/attributes"_json_pointer]; 277 const auto& attributes = json["/attributes"_json_pointer];
222 278
223 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); 279 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id);
224 if (!id) {
225 session.SetStatusBar(FAILED_TO_PARSE + " getting unused ID");
226 return 0;
227 }
228 280
229 Anime::Anime& anime = Anime::db.items[id]; 281 Anime::Anime& anime = Anime::db.items[id];
230 282
231 anime.SetId(id); 283 anime.SetId(id);
232 anime.SetServiceId(Anime::Service::Kitsu, service_id); 284 anime.SetServiceId(Anime::Service::Kitsu, service_id);
235 anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>()); 287 anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>());
236 288
237 if (attributes.contains("/titles"_json_pointer) && attributes["/titles"_json_pointer].is_object()) 289 if (attributes.contains("/titles"_json_pointer) && attributes["/titles"_json_pointer].is_object())
238 ParseTitleJson(anime, attributes["/titles"_json_pointer]); 290 ParseTitleJson(anime, attributes["/titles"_json_pointer]);
239 291
240 // FIXME: parse abbreviatedTitles for synonyms?? 292 if (attributes.contains("/abbreviatedTitles"_json_pointer) && attributes["/abbreviatedTitles"_json_pointer].is_array())
241 293 for (const auto& title : attributes["/abbreviatedTitles"_json_pointer])
242 if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_number()) 294 anime.AddTitleSynonym(title.get<std::string>());
243 anime.SetAudienceScore(JSON::GetNumber<double>(attributes, "/averageRating"_json_pointer)); 295
296 if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_string())
297 anime.SetAudienceScore(Strings::ToInt<double>(attributes["/averageRating"_json_pointer].get<std::string>()));
244 298
245 if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string()) 299 if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string())
246 anime.SetAirDate(attributes["/startDate"_json_pointer].get<std::string>()); 300 anime.SetAirDate(attributes["/startDate"_json_pointer].get<std::string>());
247 301
248 // TODO: endDate 302 // XXX endDate
249 303
250 if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string()) 304 if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string())
251 ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>()); 305 ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>());
306
307 if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string())
308 ParseSeriesStatus(anime, attributes["/status"_json_pointer].get<std::string>());
252 309
253 if (attributes.contains("/posterImage/original"_json_pointer) && attributes["/posterImage/original"_json_pointer].is_string()) 310 if (attributes.contains("/posterImage/original"_json_pointer) && attributes["/posterImage/original"_json_pointer].is_string())
254 anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>()); 311 anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>());
255 312
256 if (attributes.contains("/episodeCount"_json_pointer) && attributes["/episodeCount"_json_pointer].is_number()) 313 if (attributes.contains("/episodeCount"_json_pointer) && attributes["/episodeCount"_json_pointer].is_number())
328 anime.SetUserTimeUpdated(Time::ParseISO8601Time(attributes["/progressedAt"_json_pointer].get<std::string>())); 385 anime.SetUserTimeUpdated(Time::ParseISO8601Time(attributes["/progressedAt"_json_pointer].get<std::string>()));
329 386
330 return id; 387 return id;
331 } 388 }
332 389
390 static void ParseMetadataJson(Anime::Anime& anime, const nlohmann::json& json) {
391 std::vector<std::string> categories;
392 std::vector<std::string> producers;
393
394 for (const auto& item : json) {
395 std::string variant;
396 {
397 static const nlohmann::json::json_pointer p = "/type"_json_pointer;
398
399 if (!item.contains(p) || !item[p].is_string())
400 continue;
401
402 variant = item[p].get<std::string>();
403 }
404
405 /* now parse variants */
406 if (variant == "categories") {
407 static const nlohmann::json::json_pointer p = "/attributes/title"_json_pointer;
408
409 if (!item.contains(p) || !item[p].is_string())
410 continue;
411
412 categories.push_back(item[p].get<std::string>());
413 } else if (variant == "producers") {
414 static const nlohmann::json::json_pointer p = "/attributes/name"_json_pointer;
415
416 if (!item.contains(p) || !item[p].is_string())
417 continue;
418
419 producers.push_back(item[p].get<std::string>());
420 }
421 }
422
423 anime.SetGenres(categories);
424 anime.SetProducers(producers);
425 }
426
333 static bool ParseAnyJson(const nlohmann::json& json) { 427 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; 428 static const nlohmann::json::json_pointer required = "/type"_json_pointer;
350 if (!json.contains(required) && !json[required].is_string()) { 429 if (!json.contains(required) && !json[required].is_string()) {
351 session.SetStatusBar(std::string("Kitsu: Failed to parse generic object! (missing ") + required.to_string() + ")"); 430 session.SetStatusBar(std::string("Kitsu: Failed to parse generic object! (missing ") + required.to_string() + ")");
352 return 0; 431 return 0;
353 } 432 }
354 433
355 Variant variant = Variant::Unknown; 434 std::string variant = json["/type"_json_pointer].get<std::string>();
356 435
357 std::string json_type = json["/type"_json_pointer].get<std::string>(); 436 if (variant == "anime") {
358 437 return !!ParseAnimeJson(json);
359 if (lookup.find(json_type) != lookup.end()) 438 } else if (variant == "libraryEntries") {
360 variant = lookup.at(json_type); 439 return !!ParseLibraryJson(json);
361 440 } else if (variant == "categories" || variant == "producers") {
362 switch (variant) { 441 /* do nothing */
363 case Variant::Anime: 442 } else {
364 return !!ParseAnimeJson(json); 443 std::cerr << "Kitsu: received unknown type " << variant << std::endl;
365 case Variant::LibraryEntry: 444 }
366 return !!ParseLibraryJson(json); 445
367 /* ... */ 446 return true;
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 } 447 }
376 448
377 int GetAnimeList() { 449 int GetAnimeList() {
378 static constexpr int LIBRARY_MAX_SIZE = 500; 450 static constexpr int LIBRARY_MAX_SIZE = 500;
379 451
392 {"filter[kind]", "anime"}, 464 {"filter[kind]", "anime"},
393 {"include", "anime"}, 465 {"include", "anime"},
394 {"page[offset]", Strings::ToUtf8String(page)}, 466 {"page[offset]", Strings::ToUtf8String(page)},
395 {"page[limit]", Strings::ToUtf8String(LIBRARY_MAX_SIZE)} 467 {"page[limit]", Strings::ToUtf8String(LIBRARY_MAX_SIZE)}
396 }; 468 };
469 AddAnimeFilters(params);
470 AddLibraryEntryFilters(params);
397 471
398 Anime::db.RemoveAllUserData(); 472 Anime::db.RemoveAllUserData();
399 473
400 bool success = true; 474 bool success = true;
401 475
427 session.SetStatusBar("Kitsu: Successfully received library data!"); 501 session.SetStatusBar("Kitsu: Successfully received library data!");
428 502
429 return 1; 503 return 1;
430 } 504 }
431 505
506 bool RetrieveAnimeMetadata(int id) {
507 /* TODO: the genres should *probably* be a std::optional */
508 Anime::Anime& anime = Anime::db.items[id];
509 if (anime.GetGenres().size() > 0)
510 return false;
511
512 std::optional<std::string> service_id = anime.GetServiceId(Anime::Service::Kitsu);
513 if (!service_id)
514 return false;
515
516 session.SetStatusBar("Kitsu: Retrieving anime metadata...");
517
518 static const std::map<std::string, std::string> params = {
519 {"include", Strings::Implode({
520 "categories",
521 "animeProductions",
522 "animeProductions.producer",
523 }, ",")}
524 };
525
526 std::optional<nlohmann::json> response = SendJSONAPIRequest("/anime/" + service_id.value(), params);
527 if (!response)
528 return false;
529
530 const auto& json = response.value();
531
532 if (!json.contains("/included"_json_pointer) || !json["/included"_json_pointer].is_array()) {
533 session.SetStatusBar("Kitsu: Server returned bad data when trying to retrieve anime metadata!");
534 return false;
535 }
536
537 ParseMetadataJson(anime, json["/included"_json_pointer]);
538
539 session.SetStatusBar("Kitsu: Successfully retrieved anime metadata!");
540
541 return true;
542 }
543
432 /* unimplemented for now */ 544 /* unimplemented for now */
433 std::vector<int> Search(const std::string& search) { 545 std::vector<int> Search(const std::string& search) {
434 return {}; 546 return {};
435 } 547 }
436 548