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 |