Mercurial > minori
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 |