comparison src/services/kitsu.cc @ 369:47c9f8502269

*: clang-format all the things I've edited the formatting a bit. Now pointer asterisks (and reference ampersands) are on the variable instead of the type, as well as having newlines for function braces (but nothing else)
author Paper <paper@tflc.us>
date Fri, 25 Jul 2025 10:16:02 -0400
parents 7e97c566cce4
children
comparison
equal deleted inserted replaced
368:6d37a998cf91 369:47c9f8502269
1 #include "services/anilist.h"
2 #include "core/anime.h" 1 #include "core/anime.h"
3 #include "core/anime_db.h" 2 #include "core/anime_db.h"
3 #include "core/config.h"
4 #include "core/date.h" 4 #include "core/date.h"
5 #include "core/config.h"
6 #include "core/http.h" 5 #include "core/http.h"
7 #include "core/json.h" 6 #include "core/json.h"
8 #include "core/session.h" 7 #include "core/session.h"
9 #include "core/strings.h" 8 #include "core/strings.h"
10 #include "core/time.h" 9 #include "core/time.h"
11 #include "gui/translate/anilist.h" 10 #include "gui/translate/anilist.h"
11 #include "services/anilist.h"
12 12
13 #include <QByteArray> 13 #include <QByteArray>
14 #include <QDate> 14 #include <QDate>
15 #include <QDesktopServices> 15 #include <QDesktopServices>
16 #include <QInputDialog> 16 #include <QInputDialog>
33 33
34 namespace Services { 34 namespace Services {
35 namespace Kitsu { 35 namespace Kitsu {
36 36
37 /* This nifty little function basically handles authentication AND reauthentication. */ 37 /* This nifty little function basically handles authentication AND reauthentication. */
38 static bool SendAuthRequest(const nlohmann::json& data) { 38 static bool SendAuthRequest(const nlohmann::json &data)
39 static const std::vector<std::string> headers = { 39 {
40 {"Content-Type: application/json"} 40 static const std::vector<std::string> headers = {{"Content-Type: application/json"}};
41 }; 41
42 42 const std::string ret =
43 const std::string ret = Strings::ToUtf8String(HTTP::Request(std::string(OAUTH_PATH), headers, data.dump(), HTTP::Type::Post)); 43 Strings::ToUtf8String(HTTP::Request(std::string(OAUTH_PATH), headers, data.dump(), HTTP::Type::Post));
44 if (ret.empty()) { 44 if (ret.empty()) {
45 session.SetStatusBar(Strings::Translate("Kitsu: Request returned empty data!")); 45 session.SetStatusBar(Strings::Translate("Kitsu: Request returned empty data!"));
46 return false; 46 return false;
47 } 47 }
48 48
49 nlohmann::json result; 49 nlohmann::json result;
50 try { 50 try {
51 result = nlohmann::json::parse(ret, nullptr, false); 51 result = nlohmann::json::parse(ret, nullptr, false);
52 } catch (const std::exception& ex) { 52 } catch (const std::exception &ex) {
53 session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed to parse authorization data with error \"{}\""), ex.what())); 53 session.SetStatusBar(
54 fmt::format(Strings::Translate("Kitsu: Failed to parse authorization data with error \"{}\""), ex.what()));
54 return false; 55 return false;
55 } 56 }
56 57
57 if (result.contains("/error"_json_pointer)) { 58 if (result.contains("/error"_json_pointer)) {
58 session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed with error \"{}\"!"), result["/error"_json_pointer].get<std::string>())); 59 session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed with error \"{}\"!"),
60 result["/error"_json_pointer].get<std::string>()));
59 return false; 61 return false;
60 } 62 }
61 63
62 const std::vector<nlohmann::json::json_pointer> required = { 64 const std::vector<nlohmann::json::json_pointer> required = {
63 "/access_token"_json_pointer, 65 "/access_token"_json_pointer, "/created_at"_json_pointer, "/expires_in"_json_pointer,
64 "/created_at"_json_pointer, 66 "/refresh_token"_json_pointer, "/scope"_json_pointer, "/token_type"_json_pointer};
65 "/expires_in"_json_pointer, 67
66 "/refresh_token"_json_pointer, 68 for (const auto &ptr : required) {
67 "/scope"_json_pointer,
68 "/token_type"_json_pointer
69 };
70
71 for (const auto& ptr : required) {
72 if (!result.contains(ptr)) { 69 if (!result.contains(ptr)) {
73 session.SetStatusBar(Strings::Translate("Kitsu: Authorization request returned bad data!")); 70 session.SetStatusBar(Strings::Translate("Kitsu: Authorization request returned bad data!"));
74 return false; 71 return false;
75 } 72 }
76 } 73 }
77 74
78 session.config.auth.kitsu.access_token = result["/access_token"_json_pointer].get<std::string>(); 75 session.config.auth.kitsu.access_token = result["/access_token"_json_pointer].get<std::string>();
79 session.config.auth.kitsu.access_token_expiration 76 session.config.auth.kitsu.access_token_expiration = result["/created_at"_json_pointer].get<Time::Timestamp>() +
80 = result["/created_at"_json_pointer].get<Time::Timestamp>() 77 result["/expires_in"_json_pointer].get<Time::Timestamp>();
81 + result["/expires_in"_json_pointer].get<Time::Timestamp>();
82 session.config.auth.kitsu.refresh_token = result["/refresh_token"_json_pointer].get<std::string>(); 78 session.config.auth.kitsu.refresh_token = result["/refresh_token"_json_pointer].get<std::string>();
83 79
84 /* the next two are not that important */ 80 /* the next two are not that important */
85 81
86 return true; 82 return true;
87 } 83 }
88 84
89 static bool RefreshAccessToken(void) { 85 static bool RefreshAccessToken(void)
86 {
90 const nlohmann::json request = { 87 const nlohmann::json request = {
91 {"grant_type", "refresh_token"}, 88 {"grant_type", "refresh_token" },
92 {"refresh_token", session.config.auth.kitsu.refresh_token}, 89 {"refresh_token", session.config.auth.kitsu.refresh_token},
93 }; 90 };
94 91
95 if (!SendAuthRequest(request)) 92 if (!SendAuthRequest(request))
96 return false; 93 return false;
97 94
98 return true; 95 return true;
99 } 96 }
100 97
101 /* ----------------------------------------------------------------------------- */ 98 /* ----------------------------------------------------------------------------- */
102 99
103 static std::optional<std::string> AccountAccessToken() { 100 static std::optional<std::string> AccountAccessToken()
104 auto& auth = session.config.auth.kitsu; 101 {
102 auto &auth = session.config.auth.kitsu;
105 103
106 if (Time::GetSystemTime() >= session.config.auth.kitsu.access_token_expiration) 104 if (Time::GetSystemTime() >= session.config.auth.kitsu.access_token_expiration)
107 if (!RefreshAccessToken()) 105 if (!RefreshAccessToken())
108 return std::nullopt; 106 return std::nullopt;
109 107
110 return auth.access_token; 108 return auth.access_token;
111 } 109 }
112 110
113 /* ----------------------------------------------------------------------------- */ 111 /* ----------------------------------------------------------------------------- */
114 112
115 static void AddAnimeFilters(std::map<std::string, std::string>& map) { 113 static void AddAnimeFilters(std::map<std::string, std::string> &map)
114 {
116 static const std::vector<std::string> fields = { 115 static const std::vector<std::string> fields = {
117 "abbreviatedTitles", 116 "abbreviatedTitles", "averageRating", "episodeCount", "episodeLength", "posterImage",
118 "averageRating", 117 "startDate", "status", "subtype", "titles", "categories",
119 "episodeCount", 118 "synopsis", "animeProductions",
120 "episodeLength",
121 "posterImage",
122 "startDate",
123 "status",
124 "subtype",
125 "titles",
126 "categories",
127 "synopsis",
128 "animeProductions",
129 }; 119 };
130 static const std::string imploded = Strings::Implode(fields, ","); 120 static const std::string imploded = Strings::Implode(fields, ",");
131 121
132 map["fields[anime]"] = imploded; 122 map["fields[anime]"] = imploded;
133 map["fields[animeProductions]"] = "producer"; 123 map["fields[animeProductions]"] = "producer";
134 map["fields[categories]"] = "title"; 124 map["fields[categories]"] = "title";
135 map["fields[producers]"] = "name"; 125 map["fields[producers]"] = "name";
136 } 126 }
137 127
138 static void AddLibraryEntryFilters(std::map<std::string, std::string>& map) { 128 static void AddLibraryEntryFilters(std::map<std::string, std::string> &map)
129 {
139 static const std::vector<std::string> fields = { 130 static const std::vector<std::string> fields = {
140 "anime", 131 "anime", "startedAt", "finishedAt", "notes", "progress",
141 "startedAt", 132 "ratingTwenty", "reconsumeCount", "reconsuming", "status", "updatedAt",
142 "finishedAt",
143 "notes",
144 "progress",
145 "ratingTwenty",
146 "reconsumeCount",
147 "reconsuming",
148 "status",
149 "updatedAt",
150 }; 133 };
151 static const std::string imploded = Strings::Implode(fields, ","); 134 static const std::string imploded = Strings::Implode(fields, ",");
152 135
153 map["fields[libraryEntries]"] = imploded; 136 map["fields[libraryEntries]"] = imploded;
154 } 137 }
155 138
156 /* ----------------------------------------------------------------------------- */ 139 /* ----------------------------------------------------------------------------- */
157 140
158 static std::optional<nlohmann::json> SendJSONAPIRequest(const std::string& path, const std::map<std::string, std::string>& params = {}) { 141 static std::optional<nlohmann::json> SendJSONAPIRequest(const std::string &path,
142 const std::map<std::string, std::string> &params = {})
143 {
159 std::optional<std::string> token = AccountAccessToken(); 144 std::optional<std::string> token = AccountAccessToken();
160 if (!token) 145 if (!token)
161 return std::nullopt; 146 return std::nullopt;
162 147
163 const std::vector<std::string> headers = { 148 const std::vector<std::string> headers = {"Accept: application/vnd.api+json",
164 "Accept: application/vnd.api+json", 149 "Authorization: Bearer " + token.value(),
165 "Authorization: Bearer " + token.value(), 150 "Content-Type: application/vnd.api+json"};
166 "Content-Type: application/vnd.api+json"
167 };
168 151
169 const std::string url = HTTP::EncodeParamsList(std::string(BASE_API_PATH) + path, params); 152 const std::string url = HTTP::EncodeParamsList(std::string(BASE_API_PATH) + path, params);
170 153
171 const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get)); 154 const std::string response = Strings::ToUtf8String(HTTP::Request(url, headers, "", HTTP::Type::Get));
172 if (response.empty()) 155 if (response.empty())
173 return std::nullopt; 156 return std::nullopt;
174 157
175 nlohmann::json json; 158 nlohmann::json json;
176 try { 159 try {
177 json = nlohmann::json::parse(response); 160 json = nlohmann::json::parse(response);
178 } catch (const std::exception& ex) { 161 } catch (const std::exception &ex) {
179 session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed to parse response with error \"{}\""), ex.what())); 162 session.SetStatusBar(
163 fmt::format(Strings::Translate("Kitsu: Failed to parse response with error \"{}\""), ex.what()));
180 return std::nullopt; 164 return std::nullopt;
181 } 165 }
182 166
183 if (json.contains("/errors"_json_pointer)) { 167 if (json.contains("/errors"_json_pointer)) {
184 for (const auto& item : json["/errors"]) 168 for (const auto &item : json["/errors"])
185 std::cerr << "Kitsu: API returned error \"" << json["/errors/title"_json_pointer] << "\" with detail \"" << json["/errors/detail"] << std::endl; 169 std::cerr << "Kitsu: API returned error \"" << json["/errors/title"_json_pointer] << "\" with detail \""
170 << json["/errors/detail"] << std::endl;
186 171
187 session.SetStatusBar(Strings::Translate("Kitsu: Request failed with errors!")); 172 session.SetStatusBar(Strings::Translate("Kitsu: Request failed with errors!"));
188 return std::nullopt; 173 return std::nullopt;
189 } 174 }
190 175
191 return json; 176 return json;
192 } 177 }
193 178
194 static void ParseTitleJson(Anime::Anime& anime, const nlohmann::json& json) { 179 static void ParseTitleJson(Anime::Anime &anime, const nlohmann::json &json)
180 {
195 static const std::map<std::string, Anime::TitleLanguage> lookup = { 181 static const std::map<std::string, Anime::TitleLanguage> lookup = {
196 {"en", Anime::TitleLanguage::English}, 182 {"en", Anime::TitleLanguage::English},
197 {"en_jp", Anime::TitleLanguage::Romaji}, 183 {"en_jp", Anime::TitleLanguage::Romaji },
198 {"ja_jp", Anime::TitleLanguage::Native} 184 {"ja_jp", Anime::TitleLanguage::Native }
199 }; 185 };
200 186
201 for (const auto& [string, title] : lookup) 187 for (const auto &[string, title] : lookup)
202 if (json.contains(string)) 188 if (json.contains(string))
203 anime.SetTitle(title, json[string].get<std::string>()); 189 anime.SetTitle(title, json[string].get<std::string>());
204 } 190 }
205 191
206 static void ParseSubtype(Anime::Anime& anime, const std::string& str) { 192 static void ParseSubtype(Anime::Anime &anime, const std::string &str)
193 {
207 static const std::map<std::string, Anime::SeriesFormat> lookup = { 194 static const std::map<std::string, Anime::SeriesFormat> lookup = {
208 {"ONA", Anime::SeriesFormat::Ona}, 195 {"ONA", Anime::SeriesFormat::Ona },
209 {"OVA", Anime::SeriesFormat::Ova}, 196 {"OVA", Anime::SeriesFormat::Ova },
210 {"TV", Anime::SeriesFormat::Tv}, 197 {"TV", Anime::SeriesFormat::Tv },
211 {"movie", Anime::SeriesFormat::Movie}, 198 {"movie", Anime::SeriesFormat::Movie },
212 {"music", Anime::SeriesFormat::Music}, 199 {"music", Anime::SeriesFormat::Music },
213 {"special", Anime::SeriesFormat::Special} 200 {"special", Anime::SeriesFormat::Special}
214 }; 201 };
215 202
216 if (lookup.find(str) == lookup.end()) 203 if (lookup.find(str) == lookup.end())
217 return; 204 return;
218 205
219 anime.SetFormat(lookup.at(str)); 206 anime.SetFormat(lookup.at(str));
220 } 207 }
221 208
222 static void ParseListStatus(Anime::Anime& anime, const std::string& str) { 209 static void ParseListStatus(Anime::Anime &anime, const std::string &str)
210 {
223 static const std::map<std::string, Anime::ListStatus> lookup = { 211 static const std::map<std::string, Anime::ListStatus> lookup = {
224 {"completed", Anime::ListStatus::Completed}, 212 {"completed", Anime::ListStatus::Completed},
225 {"current", Anime::ListStatus::Current}, 213 {"current", Anime::ListStatus::Current },
226 {"dropped", Anime::ListStatus::Dropped}, 214 {"dropped", Anime::ListStatus::Dropped },
227 {"on_hold", Anime::ListStatus::Paused}, 215 {"on_hold", Anime::ListStatus::Paused },
228 {"planned", Anime::ListStatus::Planning} 216 {"planned", Anime::ListStatus::Planning }
229 }; 217 };
230 218
231 if (lookup.find(str) == lookup.end()) 219 if (lookup.find(str) == lookup.end())
232 return; 220 return;
233 221
234 anime.SetUserStatus(lookup.at(str)); 222 anime.SetUserStatus(lookup.at(str));
235 } 223 }
236 224
237 static void ParseSeriesStatus(Anime::Anime& anime, const std::string& str) { 225 static void ParseSeriesStatus(Anime::Anime &anime, const std::string &str)
226 {
238 static const std::map<std::string, Anime::SeriesStatus> lookup = { 227 static const std::map<std::string, Anime::SeriesStatus> lookup = {
239 {"current", Anime::SeriesStatus::Releasing}, 228 {"current", Anime::SeriesStatus::Releasing },
240 {"finished", Anime::SeriesStatus::Finished}, 229 {"finished", Anime::SeriesStatus::Finished },
241 {"tba", Anime::SeriesStatus::Hiatus}, // is this right? 230 {"tba", Anime::SeriesStatus::Hiatus }, // is this right?
242 {"unreleased", Anime::SeriesStatus::Cancelled}, 231 {"unreleased", Anime::SeriesStatus::Cancelled },
243 {"upcoming", Anime::SeriesStatus::NotYetReleased}, 232 {"upcoming", Anime::SeriesStatus::NotYetReleased},
244 }; 233 };
245 234
246 if (lookup.find(str) == lookup.end()) 235 if (lookup.find(str) == lookup.end())
247 return; 236 return;
248 237
249 anime.SetAiringStatus(lookup.at(str)); 238 anime.SetAiringStatus(lookup.at(str));
250 } 239 }
251 240
252 static int ParseAnimeJson(const nlohmann::json& json) { 241 static int ParseAnimeJson(const nlohmann::json &json)
242 {
253 const std::string FAILED_TO_PARSE = Strings::Translate("Kitsu: Failed to parse anime object! {}"); 243 const std::string FAILED_TO_PARSE = Strings::Translate("Kitsu: Failed to parse anime object! {}");
254 244
255 const std::string service_id = json["/id"_json_pointer].get<std::string>(); 245 const std::string service_id = json["/id"_json_pointer].get<std::string>();
256 if (service_id.empty()) { 246 if (service_id.empty()) {
257 session.SetStatusBar(fmt::format(FAILED_TO_PARSE, "(/id)")); 247 session.SetStatusBar(fmt::format(FAILED_TO_PARSE, "(/id)"));
261 if (!json.contains("/attributes"_json_pointer)) { 251 if (!json.contains("/attributes"_json_pointer)) {
262 session.SetStatusBar(fmt::format(FAILED_TO_PARSE, "(/attributes)")); 252 session.SetStatusBar(fmt::format(FAILED_TO_PARSE, "(/attributes)"));
263 return 0; 253 return 0;
264 } 254 }
265 255
266 const auto& attributes = json["/attributes"_json_pointer]; 256 const auto &attributes = json["/attributes"_json_pointer];
267 257
268 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); 258 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id);
269 259
270 Anime::Anime& anime = Anime::db.items[id]; 260 Anime::Anime &anime = Anime::db.items[id];
271 261
272 anime.SetId(id); 262 anime.SetId(id);
273 anime.SetServiceId(Anime::Service::Kitsu, service_id); 263 anime.SetServiceId(Anime::Service::Kitsu, service_id);
274 264
275 if (attributes.contains("/synopsis"_json_pointer) && attributes["/synopsis"_json_pointer].is_string()) 265 if (attributes.contains("/synopsis"_json_pointer) && attributes["/synopsis"_json_pointer].is_string())
276 anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>()); 266 anime.SetSynopsis(attributes["/synopsis"_json_pointer].get<std::string>());
277 267
278 if (attributes.contains("/titles"_json_pointer) && attributes["/titles"_json_pointer].is_object()) 268 if (attributes.contains("/titles"_json_pointer) && attributes["/titles"_json_pointer].is_object())
279 ParseTitleJson(anime, attributes["/titles"_json_pointer]); 269 ParseTitleJson(anime, attributes["/titles"_json_pointer]);
280 270
281 if (attributes.contains("/abbreviatedTitles"_json_pointer) && attributes["/abbreviatedTitles"_json_pointer].is_array()) 271 if (attributes.contains("/abbreviatedTitles"_json_pointer) &&
282 for (const auto& title : attributes["/abbreviatedTitles"_json_pointer]) 272 attributes["/abbreviatedTitles"_json_pointer].is_array())
273 for (const auto &title : attributes["/abbreviatedTitles"_json_pointer])
283 anime.AddTitleSynonym(title.get<std::string>()); 274 anime.AddTitleSynonym(title.get<std::string>());
284 275
285 if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_string()) 276 if (attributes.contains("/averageRating"_json_pointer) && attributes["/averageRating"_json_pointer].is_string())
286 anime.SetAudienceScore(Strings::ToInt<double>(attributes["/averageRating"_json_pointer].get<std::string>())); 277 anime.SetAudienceScore(Strings::ToInt<double>(attributes["/averageRating"_json_pointer].get<std::string>()));
287 278
288 if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string()) 279 if (attributes.contains("/startDate"_json_pointer) && attributes["/startDate"_json_pointer].is_string())
289 anime.SetStartedDate(attributes["/startDate"_json_pointer].get<std::string>()); 280 anime.SetStartedDate(attributes["/startDate"_json_pointer].get<std::string>());
290 281
291 anime.SetCompletedDate(attributes.contains("/endDate"_json_pointer) && attributes["/endDate"_json_pointer].is_string() 282 anime.SetCompletedDate(attributes.contains("/endDate"_json_pointer) &&
292 ? attributes["/endDate"_json_pointer].get<std::string>() 283 attributes["/endDate"_json_pointer].is_string()
293 : anime.GetStartedDate()); 284 ? attributes["/endDate"_json_pointer].get<std::string>()
285 : anime.GetStartedDate());
294 286
295 if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string()) 287 if (attributes.contains("/subtype"_json_pointer) && attributes["/subtype"_json_pointer].is_string())
296 ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>()); 288 ParseSubtype(anime, attributes["/subtype"_json_pointer].get<std::string>());
297 289
298 if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string()) 290 if (attributes.contains("/status"_json_pointer) && attributes["/status"_json_pointer].is_string())
299 ParseSeriesStatus(anime, attributes["/status"_json_pointer].get<std::string>()); 291 ParseSeriesStatus(anime, attributes["/status"_json_pointer].get<std::string>());
300 292
301 if (attributes.contains("/posterImage/original"_json_pointer) && attributes["/posterImage/original"_json_pointer].is_string()) 293 if (attributes.contains("/posterImage/original"_json_pointer) &&
294 attributes["/posterImage/original"_json_pointer].is_string())
302 anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>()); 295 anime.SetPosterUrl(attributes["/posterImage/original"_json_pointer].get<std::string>());
303 296
304 if (attributes.contains("/episodeCount"_json_pointer) && attributes["/episodeCount"_json_pointer].is_number()) 297 if (attributes.contains("/episodeCount"_json_pointer) && attributes["/episodeCount"_json_pointer].is_number())
305 anime.SetEpisodes(attributes["/episodeCount"_json_pointer].get<int>()); 298 anime.SetEpisodes(attributes["/episodeCount"_json_pointer].get<int>());
306 299
308 anime.SetDuration(attributes["/episodeLength"_json_pointer].get<int>()); 301 anime.SetDuration(attributes["/episodeLength"_json_pointer].get<int>());
309 302
310 return id; 303 return id;
311 } 304 }
312 305
313 static int ParseLibraryJson(const nlohmann::json& json) { 306 static int ParseLibraryJson(const nlohmann::json &json)
307 {
314 static const std::vector<nlohmann::json::json_pointer> required = { 308 static const std::vector<nlohmann::json::json_pointer> required = {
315 "/id"_json_pointer, 309 "/id"_json_pointer,
316 "/relationships/anime/data/id"_json_pointer, 310 "/relationships/anime/data/id"_json_pointer,
317 "/attributes"_json_pointer, 311 "/attributes"_json_pointer,
318 }; 312 };
319 313
320 for (const auto& ptr : required) { 314 for (const auto &ptr : required) {
321 if (!json.contains(ptr)) { 315 if (!json.contains(ptr)) {
322 session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed to parse library object! (missing {})"), ptr.to_string())); 316 session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed to parse library object! (missing {})"),
317 ptr.to_string()));
323 return 0; 318 return 0;
324 } 319 }
325 } 320 }
326 321
327 std::string service_id = json["/relationships/anime/data/id"_json_pointer].get<std::string>(); 322 std::string service_id = json["/relationships/anime/data/id"_json_pointer].get<std::string>();
330 return 0; 325 return 0;
331 } 326 }
332 327
333 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id); 328 int id = Anime::db.LookupServiceIdOrUnused(Anime::Service::Kitsu, service_id);
334 329
335 const auto& attributes = json["/attributes"_json_pointer]; 330 const auto &attributes = json["/attributes"_json_pointer];
336 331
337 const std::string library_id = json["/id"_json_pointer].get<std::string>(); 332 const std::string library_id = json["/id"_json_pointer].get<std::string>();
338 333
339 Anime::Anime& anime = Anime::db.items[id]; 334 Anime::Anime &anime = Anime::db.items[id];
340 335
341 anime.AddToUserList(); 336 anime.AddToUserList();
342 337
343 anime.SetId(id); 338 anime.SetId(id);
344 anime.SetServiceId(Anime::Service::Kitsu, service_id); 339 anime.SetServiceId(Anime::Service::Kitsu, service_id);
345 340
346 anime.SetUserId(library_id); 341 anime.SetUserId(library_id);
347 342
348 if (attributes.contains("/startedAt"_json_pointer) && attributes["/startedAt"_json_pointer].is_string()) 343 if (attributes.contains("/startedAt"_json_pointer) && attributes["/startedAt"_json_pointer].is_string())
349 anime.SetUserDateStarted(Date(Time::ParseISO8601Time(attributes["/startedAt"_json_pointer].get<std::string>()))); 344 anime.SetUserDateStarted(
345 Date(Time::ParseISO8601Time(attributes["/startedAt"_json_pointer].get<std::string>())));
350 346
351 if (attributes.contains("/finishedAt"_json_pointer) && attributes["/finishedAt"_json_pointer].is_string()) 347 if (attributes.contains("/finishedAt"_json_pointer) && attributes["/finishedAt"_json_pointer].is_string())
352 anime.SetUserDateCompleted(Date(Time::ParseISO8601Time(attributes["/finishedAt"_json_pointer].get<std::string>()))); 348 anime.SetUserDateCompleted(
349 Date(Time::ParseISO8601Time(attributes["/finishedAt"_json_pointer].get<std::string>())));
353 350
354 if (attributes.contains("/notes"_json_pointer) && attributes["/notes"_json_pointer].is_string()) 351 if (attributes.contains("/notes"_json_pointer) && attributes["/notes"_json_pointer].is_string())
355 anime.SetUserNotes(attributes["/notes"_json_pointer].get<std::string>()); 352 anime.SetUserNotes(attributes["/notes"_json_pointer].get<std::string>());
356 353
357 if (attributes.contains("/progress"_json_pointer) && attributes["/progress"_json_pointer].is_number()) 354 if (attributes.contains("/progress"_json_pointer) && attributes["/progress"_json_pointer].is_number())
376 anime.SetUserTimeUpdated(Time::ParseISO8601Time(attributes["/progressedAt"_json_pointer].get<std::string>())); 373 anime.SetUserTimeUpdated(Time::ParseISO8601Time(attributes["/progressedAt"_json_pointer].get<std::string>()));
377 374
378 return id; 375 return id;
379 } 376 }
380 377
381 static void ParseMetadataJson(Anime::Anime& anime, const nlohmann::json& json) { 378 static void ParseMetadataJson(Anime::Anime &anime, const nlohmann::json &json)
379 {
382 std::vector<std::string> categories; 380 std::vector<std::string> categories;
383 std::vector<std::string> producers; 381 std::vector<std::string> producers;
384 382
385 for (const auto& item : json) { 383 for (const auto &item : json) {
386 std::string variant; 384 std::string variant;
387 { 385 {
388 static const nlohmann::json::json_pointer p = "/type"_json_pointer; 386 static const nlohmann::json::json_pointer p = "/type"_json_pointer;
389 387
390 if (!item.contains(p) || !item[p].is_string()) 388 if (!item.contains(p) || !item[p].is_string())
413 411
414 anime.SetGenres(categories); 412 anime.SetGenres(categories);
415 anime.SetProducers(producers); 413 anime.SetProducers(producers);
416 } 414 }
417 415
418 static bool ParseAnyJson(const nlohmann::json& json) { 416 static bool ParseAnyJson(const nlohmann::json &json)
417 {
419 static const nlohmann::json::json_pointer required = "/type"_json_pointer; 418 static const nlohmann::json::json_pointer required = "/type"_json_pointer;
420 if (!json.contains(required) && !json[required].is_string()) { 419 if (!json.contains(required) && !json[required].is_string()) {
421 session.SetStatusBar(fmt::format(Strings::Translate("Kitsu: Failed to generic object! (missing {})"), required.to_string())); 420 session.SetStatusBar(
421 fmt::format(Strings::Translate("Kitsu: Failed to generic object! (missing {})"), required.to_string()));
422 return 0; 422 return 0;
423 } 423 }
424 424
425 std::string variant = json["/type"_json_pointer].get<std::string>(); 425 std::string variant = json["/type"_json_pointer].get<std::string>();
426 426
435 } 435 }
436 436
437 return true; 437 return true;
438 } 438 }
439 439
440 int GetAnimeList() { 440 int GetAnimeList()
441 {
441 static constexpr int LIBRARY_MAX_SIZE = 500; 442 static constexpr int LIBRARY_MAX_SIZE = 500;
442 443
443 const auto& auth = session.config.auth.kitsu; 444 const auto &auth = session.config.auth.kitsu;
444 445
445 if (auth.user_id.empty()) { 446 if (auth.user_id.empty()) {
446 session.SetStatusBar(Strings::Translate("Kitsu: User ID is unavailable!")); 447 session.SetStatusBar(Strings::Translate("Kitsu: User ID is unavailable!"));
447 return 0; 448 return 0;
448 } 449 }
449 450
450 int page = 0; 451 int page = 0;
451 bool have_next_page = true; 452 bool have_next_page = true;
452 453
453 std::map<std::string, std::string> params = { 454 std::map<std::string, std::string> params = {
454 {"filter[user_id]", auth.user_id}, 455 {"filter[user_id]", auth.user_id },
455 {"filter[kind]", "anime"}, 456 {"filter[kind]", "anime" },
456 {"include", "anime"}, 457 {"include", "anime" },
457 {"page[offset]", Strings::ToUtf8String(page)}, 458 {"page[offset]", Strings::ToUtf8String(page) },
458 {"page[limit]", Strings::ToUtf8String(LIBRARY_MAX_SIZE)} 459 {"page[limit]", Strings::ToUtf8String(LIBRARY_MAX_SIZE)}
459 }; 460 };
460 AddAnimeFilters(params); 461 AddAnimeFilters(params);
461 AddLibraryEntryFilters(params); 462 AddLibraryEntryFilters(params);
462 463
463 Anime::db.RemoveAllUserData(); 464 Anime::db.RemoveAllUserData();
464 465
467 while (have_next_page) { 468 while (have_next_page) {
468 std::optional<nlohmann::json> response = SendJSONAPIRequest("/library-entries", params); 469 std::optional<nlohmann::json> response = SendJSONAPIRequest("/library-entries", params);
469 if (!response) 470 if (!response)
470 return 0; 471 return 0;
471 472
472 const nlohmann::json& root = response.value(); 473 const nlohmann::json &root = response.value();
473 474
474 if (root.contains("/next"_json_pointer) && root["/next"_json_pointer].is_number()) { 475 if (root.contains("/next"_json_pointer) && root["/next"_json_pointer].is_number()) {
475 page += root["/next"_json_pointer].get<int>(); 476 page += root["/next"_json_pointer].get<int>();
476 if (page <= 0) 477 if (page <= 0)
477 have_next_page = false; 478 have_next_page = false;
478 } else have_next_page = false; 479 } else
479 480 have_next_page = false;
480 for (const auto& item : root["/data"_json_pointer]) 481
482 for (const auto &item : root["/data"_json_pointer])
481 if (!ParseLibraryJson(item)) 483 if (!ParseLibraryJson(item))
482 success = false; 484 success = false;
483 485
484 for (const auto& variant : root["/included"_json_pointer]) 486 for (const auto &variant : root["/included"_json_pointer])
485 if (!ParseAnyJson(variant)) 487 if (!ParseAnyJson(variant))
486 success = false; 488 success = false;
487 489
488 params["page[offset]"] = Strings::ToUtf8String(page); 490 params["page[offset]"] = Strings::ToUtf8String(page);
489 } 491 }
492 session.SetStatusBar(Strings::Translate("Kitsu: Successfully received library data!")); 494 session.SetStatusBar(Strings::Translate("Kitsu: Successfully received library data!"));
493 495
494 return 1; 496 return 1;
495 } 497 }
496 498
497 bool RetrieveAnimeMetadata(int id) { 499 bool RetrieveAnimeMetadata(int id)
500 {
498 /* TODO: the genres should *probably* be a std::optional */ 501 /* TODO: the genres should *probably* be a std::optional */
499 Anime::Anime& anime = Anime::db.items[id]; 502 Anime::Anime &anime = Anime::db.items[id];
500 if (anime.GetGenres().size() > 0 && anime.GetProducers().size()) 503 if (anime.GetGenres().size() > 0 && anime.GetProducers().size())
501 return false; 504 return false;
502 505
503 std::optional<std::string> service_id = anime.GetServiceId(Anime::Service::Kitsu); 506 std::optional<std::string> service_id = anime.GetServiceId(Anime::Service::Kitsu);
504 if (!service_id) 507 if (!service_id)
505 return false; 508 return false;
506 509
507 session.SetStatusBar(Strings::Translate("Kitsu: Retrieving anime metadata...")); 510 session.SetStatusBar(Strings::Translate("Kitsu: Retrieving anime metadata..."));
508 511
509 static const std::map<std::string, std::string> params = { 512 static const std::map<std::string, std::string> params = {
510 {"include", Strings::Implode({ 513 {"include", Strings::Implode(
511 "categories", 514 {
512 "animeProductions", 515 "categories",
513 "animeProductions.producer", 516 "animeProductions",
514 }, ",")} 517 "animeProductions.producer",
515 }; 518 }, ",")}
519 };
516 520
517 std::optional<nlohmann::json> response = SendJSONAPIRequest("/anime/" + service_id.value(), params); 521 std::optional<nlohmann::json> response = SendJSONAPIRequest("/anime/" + service_id.value(), params);
518 if (!response) 522 if (!response)
519 return false; 523 return false;
520 524
521 const auto& json = response.value(); 525 const auto &json = response.value();
522 526
523 if (!json.contains("/included"_json_pointer) || !json["/included"_json_pointer].is_array()) { 527 if (!json.contains("/included"_json_pointer) || !json["/included"_json_pointer].is_array()) {
524 session.SetStatusBar(Strings::Translate("Kitsu: Server returned bad data when trying to retrieve anime metadata!")); 528 session.SetStatusBar(
529 Strings::Translate("Kitsu: Server returned bad data when trying to retrieve anime metadata!"));
525 return false; 530 return false;
526 } 531 }
527 532
528 ParseMetadataJson(anime, json["/included"_json_pointer]); 533 ParseMetadataJson(anime, json["/included"_json_pointer]);
529 534
531 536
532 return true; 537 return true;
533 } 538 }
534 539
535 /* unimplemented for now */ 540 /* unimplemented for now */
536 std::vector<int> Search(const std::string& search) { 541 std::vector<int> Search(const std::string &search)
542 {
537 return {}; 543 return {};
538 } 544 }
539 545
540 bool GetSeason(Anime::Season season) { 546 bool GetSeason(Anime::Season season)
547 {
541 return false; 548 return false;
542 } 549 }
543 550
544 int UpdateAnimeEntry(int id) { 551 int UpdateAnimeEntry(int id)
552 {
545 return 0; 553 return 0;
546 } 554 }
547 555
548 bool AuthorizeUser(const std::string& email, const std::string& password) { 556 bool AuthorizeUser(const std::string &email, const std::string &password)
557 {
549 const nlohmann::json body = { 558 const nlohmann::json body = {
550 {"grant_type", "password"}, 559 {"grant_type", "password" },
551 {"username", email}, 560 {"username", email },
552 {"password", HTTP::UrlEncode(password)} 561 {"password", HTTP::UrlEncode(password)}
553 }; 562 };
554 563
555 if (!SendAuthRequest(body)) 564 if (!SendAuthRequest(body))
556 return false; 565 return false;
557 566
558 static const std::map<std::string, std::string> params = { 567 static const std::map<std::string, std::string> params = {
559 {"filter[self]", "true"} 568 {"filter[self]", "true"}
560 }; 569 };
561 570
562 std::optional<nlohmann::json> response = SendJSONAPIRequest("/users", params); 571 std::optional<nlohmann::json> response = SendJSONAPIRequest("/users", params);
563 if (!response) 572 if (!response)
564 return false; // whuh? 573 return false; // whuh?
565 574
566 const nlohmann::json& json = response.value(); 575 const nlohmann::json &json = response.value();
567 576
568 if (!json.contains("/data/0/id"_json_pointer)) { 577 if (!json.contains("/data/0/id"_json_pointer)) {
569 session.SetStatusBar(Strings::Translate("Kitsu: Failed to retrieve user ID!")); 578 session.SetStatusBar(Strings::Translate("Kitsu: Failed to retrieve user ID!"));
570 return false; 579 return false;
571 } 580 }