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 |
