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