Mercurial > minori
comparison src/services/anilist.cpp @ 15:cde8f67a7c7d
*: update, megacommit :)
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Tue, 19 Sep 2023 22:36:08 -0400 |
parents | fc1bf97c528b |
children | 2743011a6042 |
comparison
equal
deleted
inserted
replaced
14:a29c9402faf0 | 15:cde8f67a7c7d |
---|---|
3 #include "core/anime_db.h" | 3 #include "core/anime_db.h" |
4 #include "core/config.h" | 4 #include "core/config.h" |
5 #include "core/json.h" | 5 #include "core/json.h" |
6 #include "core/session.h" | 6 #include "core/session.h" |
7 #include "core/strings.h" | 7 #include "core/strings.h" |
8 #include "gui/translate/anilist.h" | |
8 #include <QDesktopServices> | 9 #include <QDesktopServices> |
9 #include <QInputDialog> | 10 #include <QInputDialog> |
10 #include <QLineEdit> | 11 #include <QLineEdit> |
11 #include <QMessageBox> | 12 #include <QMessageBox> |
12 #include <QUrl> | 13 #include <QUrl> |
14 #include <curl/curl.h> | 15 #include <curl/curl.h> |
15 #include <exception> | 16 #include <exception> |
16 #include <format> | 17 #include <format> |
17 #define CLIENT_ID "13706" | 18 #define CLIENT_ID "13706" |
18 | 19 |
19 using nlohmann::literals::operator "" _json_pointer; | 20 using nlohmann::literals::operator"" _json_pointer; |
20 | 21 |
21 namespace Services::AniList { | 22 namespace Services::AniList { |
22 | 23 |
23 class Account { | 24 class Account { |
24 public: | 25 public: |
25 std::string Username() const { return session.config.anilist.username; } | 26 std::string Username() const { return session.config.anilist.username; } |
26 void SetUsername(std::string const& username) { session.config.anilist.username = username; } | 27 void SetUsername(std::string const& username) { |
28 session.config.anilist.username = username; | |
29 } | |
27 | 30 |
28 int UserId() const { return session.config.anilist.user_id; } | 31 int UserId() const { return session.config.anilist.user_id; } |
29 void SetUserId(const int id) { session.config.anilist.user_id = id; } | 32 void SetUserId(const int id) { session.config.anilist.user_id = id; } |
30 | 33 |
31 std::string AuthToken() const { return session.config.anilist.auth_token; } | 34 std::string AuthToken() const { return session.config.anilist.auth_token; } |
32 void SetAuthToken(std::string const& auth_token) { session.config.anilist.auth_token = auth_token; } | 35 void SetAuthToken(std::string const& auth_token) { |
36 session.config.anilist.auth_token = auth_token; | |
37 } | |
33 | 38 |
34 bool Authenticated() const { return !AuthToken().empty(); } | 39 bool Authenticated() const { return !AuthToken().empty(); } |
35 }; | 40 }; |
36 | 41 |
37 static Account account; | 42 static Account account; |
45 std::string SendRequest(std::string data) { | 50 std::string SendRequest(std::string data) { |
46 struct curl_slist* list = NULL; | 51 struct curl_slist* list = NULL; |
47 std::string userdata; | 52 std::string userdata; |
48 CURL* curl = curl_easy_init(); | 53 CURL* curl = curl_easy_init(); |
49 if (curl) { | 54 if (curl) { |
55 std::string bearer = "Authorization: Bearer " + account.AuthToken(); | |
50 list = curl_slist_append(list, "Accept: application/json"); | 56 list = curl_slist_append(list, "Accept: application/json"); |
51 list = curl_slist_append(list, "Content-Type: application/json"); | 57 list = curl_slist_append(list, "Content-Type: application/json"); |
52 std::string bearer = "Authorization: Bearer " + account.AuthToken(); | |
53 list = curl_slist_append(list, bearer.c_str()); | 58 list = curl_slist_append(list, bearer.c_str()); |
54 curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co"); | 59 curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co"); |
55 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); | 60 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); |
56 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); | 61 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); |
57 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata); | 62 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata); |
61 CURLcode res = curl_easy_perform(curl); | 66 CURLcode res = curl_easy_perform(curl); |
62 curl_slist_free_all(list); | 67 curl_slist_free_all(list); |
63 curl_easy_cleanup(curl); | 68 curl_easy_cleanup(curl); |
64 if (res != CURLE_OK) { | 69 if (res != CURLE_OK) { |
65 QMessageBox box(QMessageBox::Icon::Critical, "", | 70 QMessageBox box(QMessageBox::Icon::Critical, "", |
66 QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res))); | 71 QString("curl_easy_perform(curl) failed!: ") + |
72 QString(curl_easy_strerror(res))); | |
67 box.exec(); | 73 box.exec(); |
68 return ""; | 74 return ""; |
69 } | 75 } |
70 return userdata; | 76 return userdata; |
71 } | 77 } |
72 return ""; | 78 return ""; |
73 } | 79 } |
74 | 80 |
75 /* TODO: Move to Translate */ | 81 void ParseListStatus(std::string status, Anime::Anime& anime) { |
76 | 82 std::unordered_map<std::string, Anime::ListStatus> map = { |
77 std::map<std::string, Anime::ListStatus> AniListStringToAnimeWatchingMap = { | 83 {"CURRENT", Anime::ListStatus::CURRENT }, |
78 {"CURRENT", Anime::ListStatus::CURRENT }, | 84 {"PLANNING", Anime::ListStatus::PLANNING }, |
79 {"PLANNING", Anime::ListStatus::PLANNING }, | 85 {"COMPLETED", Anime::ListStatus::COMPLETED}, |
80 {"COMPLETED", Anime::ListStatus::COMPLETED}, | 86 {"DROPPED", Anime::ListStatus::DROPPED }, |
81 {"DROPPED", Anime::ListStatus::DROPPED }, | 87 {"PAUSED", Anime::ListStatus::PAUSED } |
82 {"PAUSED", Anime::ListStatus::PAUSED }, | 88 }; |
83 {"REPEATING", Anime::ListStatus::CURRENT} | 89 |
84 }; | 90 if (status == "REPEATING") { |
85 | 91 anime.SetUserIsRewatching(true); |
86 std::map<Anime::ListStatus, std::string> AniListAnimeWatchingToStringMap = { | 92 anime.SetUserStatus(Anime::ListStatus::CURRENT); |
87 {Anime::ListStatus::CURRENT, "CURRENT" }, | 93 return; |
88 {Anime::ListStatus::PLANNING, "PLANNING" }, | 94 } |
89 {Anime::ListStatus::COMPLETED, "COMPLETED"}, | 95 |
90 {Anime::ListStatus::DROPPED, "DROPPED" }, | 96 if (!map.contains(status)) { |
91 {Anime::ListStatus::PAUSED, "PAUSED" } | 97 anime.SetUserStatus(Anime::ListStatus::NOT_IN_LIST); |
92 }; | 98 return; |
93 | 99 } |
94 std::map<std::string, Anime::SeriesStatus> AniListStringToAnimeAiringMap = { | 100 |
95 {"FINISHED", Anime::SeriesStatus::FINISHED }, | 101 anime.SetUserStatus(map[status]); |
96 {"RELEASING", Anime::SeriesStatus::RELEASING }, | 102 } |
97 {"NOT_YET_RELEASED", Anime::SeriesStatus::NOT_YET_RELEASED}, | 103 |
98 {"CANCELLED", Anime::SeriesStatus::CANCELLED }, | 104 std::string ListStatusToString(const Anime::Anime& anime) { |
99 {"HIATUS", Anime::SeriesStatus::HIATUS } | 105 std::unordered_map<Anime::ListStatus, std::string> map = { |
100 }; | 106 {Anime::ListStatus::CURRENT, "CURRENT" }, |
101 | 107 {Anime::ListStatus::PLANNING, "PLANNING" }, |
102 std::map<std::string, Anime::SeriesSeason> AniListStringToAnimeSeasonMap = { | 108 {Anime::ListStatus::COMPLETED, "COMPLETED"}, |
103 {"WINTER", Anime::SeriesSeason::WINTER}, | 109 {Anime::ListStatus::DROPPED, "DROPPED" }, |
104 {"SPRING", Anime::SeriesSeason::SPRING}, | 110 {Anime::ListStatus::PAUSED, "PAUSED" } |
105 {"SUMMER", Anime::SeriesSeason::SUMMER}, | 111 }; |
106 {"FALL", Anime::SeriesSeason::FALL } | 112 |
107 }; | 113 if (anime.GetUserIsRewatching()) |
108 | 114 return "REWATCHING"; |
109 std::map<std::string, enum Anime::SeriesFormat> AniListStringToAnimeFormatMap = { | 115 |
110 {"TV", Anime::SeriesFormat::TV }, | 116 if (!map.contains(anime.GetUserStatus())) |
111 {"TV_SHORT", Anime::SeriesFormat::TV_SHORT}, | 117 return "CURRENT"; |
112 {"MOVIE", Anime::SeriesFormat::MOVIE }, | 118 return map[anime.GetUserStatus()]; |
113 {"SPECIAL", Anime::SeriesFormat::SPECIAL }, | 119 } |
114 {"OVA", Anime::SeriesFormat::OVA }, | |
115 {"ONA", Anime::SeriesFormat::ONA }, | |
116 {"MUSIC", Anime::SeriesFormat::MUSIC }, | |
117 {"MANGA", Anime::SeriesFormat::MANGA }, | |
118 {"NOVEL", Anime::SeriesFormat::NOVEL }, | |
119 {"ONE_SHOT", Anime::SeriesFormat::ONE_SHOT} | |
120 }; | |
121 | 120 |
122 Date ParseDate(const nlohmann::json& json) { | 121 Date ParseDate(const nlohmann::json& json) { |
123 Date date; | 122 Date date; |
124 if (json.contains("/year"_json_pointer) && json.at("/year"_json_pointer).is_number()) | 123 if (json.contains("/year"_json_pointer) && json.at("/year"_json_pointer).is_number()) |
125 date.SetYear(JSON::GetInt(json, "/year"_json_pointer)); | 124 date.SetYear(JSON::GetInt(json, "/year"_json_pointer)); |
152 anime.SetId(id); | 151 anime.SetId(id); |
153 | 152 |
154 ParseTitle(json.at("/title"_json_pointer), anime); | 153 ParseTitle(json.at("/title"_json_pointer), anime); |
155 | 154 |
156 anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer)); | 155 anime.SetEpisodes(JSON::GetInt(json, "/episodes"_json_pointer)); |
157 anime.SetFormat(AniListStringToAnimeFormatMap[JSON::GetString(json, "/format"_json_pointer)]); | 156 anime.SetFormat(Translate::AniList::ToSeriesFormat(JSON::GetString(json, "/format"_json_pointer))); |
158 | 157 |
159 anime.SetAiringStatus(AniListStringToAnimeAiringMap[JSON::GetString(json, "/status"_json_pointer)]); | 158 anime.SetAiringStatus(Translate::AniList::ToSeriesStatus(JSON::GetString(json, "/status"_json_pointer))); |
160 | 159 |
161 anime.SetAirDate(ParseDate(json["/startDate"_json_pointer])); | 160 anime.SetAirDate(ParseDate(json["/startDate"_json_pointer])); |
162 | 161 |
163 anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer)); | 162 anime.SetAudienceScore(JSON::GetInt(json, "/averageScore"_json_pointer)); |
164 anime.SetSeason(AniListStringToAnimeSeasonMap[JSON::GetString(json, "/season"_json_pointer)]); | 163 anime.SetSeason(Translate::AniList::ToSeriesSeason(JSON::GetString(json, "/season"_json_pointer))); |
165 anime.SetDuration(JSON::GetInt(json, "/duration"_json_pointer)); | 164 anime.SetDuration(JSON::GetInt(json, "/duration"_json_pointer)); |
166 anime.SetSynopsis(Strings::TextifySynopsis(JSON::GetString(json, "/description"_json_pointer))); | 165 anime.SetSynopsis(Strings::TextifySynopsis(JSON::GetString(json, "/description"_json_pointer))); |
167 | 166 |
168 if (json.contains("/genres"_json_pointer) && json["/genres"_json_pointer].is_array()) | 167 if (json.contains("/genres"_json_pointer) && json["/genres"_json_pointer].is_array()) |
169 anime.SetGenres(json["/genres"_json_pointer].get<std::vector<std::string>>()); | 168 anime.SetGenres(json["/genres"_json_pointer].get<std::vector<std::string>>()); |
179 | 178 |
180 anime.AddToUserList(); | 179 anime.AddToUserList(); |
181 | 180 |
182 anime.SetUserScore(JSON::GetInt(json, "/score"_json_pointer)); | 181 anime.SetUserScore(JSON::GetInt(json, "/score"_json_pointer)); |
183 anime.SetUserProgress(JSON::GetInt(json, "/progress"_json_pointer)); | 182 anime.SetUserProgress(JSON::GetInt(json, "/progress"_json_pointer)); |
184 anime.SetUserStatus(AniListStringToAnimeWatchingMap[JSON::GetString(json, "/status"_json_pointer)]); | 183 ParseListStatus(JSON::GetString(json, "/status"_json_pointer), anime); |
185 anime.SetUserNotes(JSON::GetString(json, "/notes"_json_pointer)); | 184 anime.SetUserNotes(JSON::GetString(json, "/notes"_json_pointer)); |
186 | 185 |
187 anime.SetUserDateStarted(ParseDate(json["/startedAt"_json_pointer])); | 186 anime.SetUserDateStarted(ParseDate(json["/startedAt"_json_pointer])); |
188 anime.SetUserDateCompleted(ParseDate(json["/completedAt"_json_pointer])); | 187 anime.SetUserDateCompleted(ParseDate(json["/completedAt"_json_pointer])); |
189 | 188 |
200 } | 199 } |
201 | 200 |
202 int GetAnimeList() { | 201 int GetAnimeList() { |
203 /* NOTE: these should be in the qrc file */ | 202 /* NOTE: these should be in the qrc file */ |
204 const std::string query = "query ($id: Int) {\n" | 203 const std::string query = "query ($id: Int) {\n" |
205 " MediaListCollection (userId: $id, type: ANIME) {\n" | 204 " MediaListCollection (userId: $id, type: ANIME) {\n" |
206 " lists {\n" | 205 " lists {\n" |
207 " name\n" | 206 " name\n" |
208 " entries {\n" | 207 " entries {\n" |
209 " score\n" | 208 " score\n" |
210 " notes\n" | 209 " notes\n" |
211 " status\n" | 210 " status\n" |
212 " progress\n" | 211 " progress\n" |
213 " startedAt {\n" | 212 " startedAt {\n" |
214 " year\n" | 213 " year\n" |
215 " month\n" | 214 " month\n" |
216 " day\n" | 215 " day\n" |
217 " }\n" | 216 " }\n" |
218 " completedAt {\n" | 217 " completedAt {\n" |
219 " year\n" | 218 " year\n" |
220 " month\n" | 219 " month\n" |
221 " day\n" | 220 " day\n" |
222 " }\n" | 221 " }\n" |
223 " updatedAt\n" | 222 " updatedAt\n" |
224 " media {\n" | 223 " media {\n" |
225 " id\n" | 224 " id\n" |
226 " title {\n" | 225 " title {\n" |
227 " romaji\n" | 226 " romaji\n" |
228 " english\n" | 227 " english\n" |
229 " native\n" | 228 " native\n" |
230 " }\n" | 229 " }\n" |
231 " format\n" | 230 " format\n" |
232 " status\n" | 231 " status\n" |
233 " averageScore\n" | 232 " averageScore\n" |
234 " season\n" | 233 " season\n" |
235 " startDate {\n" | 234 " startDate {\n" |
236 " year\n" | 235 " year\n" |
237 " month\n" | 236 " month\n" |
238 " day\n" | 237 " day\n" |
239 " }\n" | 238 " }\n" |
240 " genres\n" | 239 " genres\n" |
241 " episodes\n" | 240 " episodes\n" |
242 " duration\n" | 241 " duration\n" |
243 " synonyms\n" | 242 " synonyms\n" |
244 " description(asHtml: false)\n" | 243 " description(asHtml: false)\n" |
245 " }\n" | 244 " }\n" |
246 " }\n" | 245 " }\n" |
247 " }\n" | 246 " }\n" |
248 " }\n" | 247 " }\n" |
249 "}\n"; | 248 "}\n"; |
250 // clang-format off | 249 // clang-format off |
251 nlohmann::json json = { | 250 nlohmann::json json = { |
252 {"query", query}, | 251 {"query", query}, |
253 {"variables", { | 252 {"variables", { |
254 {"id", account.UserId()} | 253 {"id", account.UserId()} |
267 } | 266 } |
268 | 267 |
269 int UpdateAnimeEntry(const Anime::Anime& anime) { | 268 int UpdateAnimeEntry(const Anime::Anime& anime) { |
270 /** | 269 /** |
271 * possible values: | 270 * possible values: |
272 * | 271 * |
273 * int mediaId, | 272 * int mediaId, |
274 * MediaListStatus status, | 273 * MediaListStatus status, |
275 * float score, | 274 * float score, |
276 * int scoreRaw, | 275 * int scoreRaw, |
277 * int progress, | 276 * int progress, |
283 * bool hiddenFromStatusLists, | 282 * bool hiddenFromStatusLists, |
284 * string[] customLists, | 283 * string[] customLists, |
285 * float[] advancedScores, | 284 * float[] advancedScores, |
286 * Date startedAt, | 285 * Date startedAt, |
287 * Date completedAt | 286 * Date completedAt |
288 **/ | 287 **/ |
289 const std::string query = | 288 const std::string query = "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, " |
290 "mutation ($media_id: Int, $progress: Int, $status: MediaListStatus, $score: Int, $notes: String) {\n" | 289 "$score: Int, $notes: String) {\n" |
291 " SaveMediaListEntry (mediaId: $media_id, progress: $progress, status: $status, scoreRaw: $score, notes: " | 290 " SaveMediaListEntry (mediaId: $media_id, progress: $progress, " |
292 "$notes) {\n" | 291 "status: $status, scoreRaw: $score, notes: " |
293 " id\n" | 292 "$notes) {\n" |
294 " }\n" | 293 " id\n" |
295 "}\n"; | 294 " }\n" |
295 "}\n"; | |
296 // clang-format off | 296 // clang-format off |
297 nlohmann::json json = { | 297 nlohmann::json json = { |
298 {"query", query}, | 298 {"query", query}, |
299 {"variables", { | 299 {"variables", { |
300 {"media_id", anime.GetId()}, | 300 {"media_id", anime.GetId()}, |
301 {"progress", anime.GetUserProgress()}, | 301 {"progress", anime.GetUserProgress()}, |
302 {"status", AniListAnimeWatchingToStringMap[anime.GetUserStatus()]}, | 302 {"status", ListStatusToString(anime)}, |
303 {"score", anime.GetUserScore()}, | 303 {"score", anime.GetUserScore()}, |
304 {"notes", anime.GetUserNotes()} | 304 {"notes", anime.GetUserNotes()} |
305 }} | 305 }} |
306 }; | 306 }; |
307 // clang-format on | 307 // clang-format on |
315 return account.UserId(); | 315 return account.UserId(); |
316 } | 316 } |
317 | 317 |
318 int AuthorizeUser() { | 318 int AuthorizeUser() { |
319 /* Prompt for PIN */ | 319 /* Prompt for PIN */ |
320 QDesktopServices::openUrl( | 320 QDesktopServices::openUrl(QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID |
321 QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token")); | 321 "&response_type=token")); |
322 bool ok; | 322 bool ok; |
323 QString token = QInputDialog::getText( | 323 QString token = QInputDialog::getText( |
324 0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, | 324 0, "Credentials needed!", |
325 "", &ok); | 325 "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, "", |
326 &ok); | |
326 if (ok && !token.isEmpty()) | 327 if (ok && !token.isEmpty()) |
327 account.SetAuthToken(token.toStdString()); | 328 account.SetAuthToken(token.toStdString()); |
328 else { // fail | 329 else // fail |
329 return 0; | 330 return 0; |
330 } | |
331 const std::string query = "query {\n" | 331 const std::string query = "query {\n" |
332 " Viewer {\n" | 332 " Viewer {\n" |
333 " id\n" | 333 " id\n" |
334 " name\n" | 334 " name\n" |
335 " mediaListOptions {\n" | 335 " mediaListOptions {\n" |
336 " scoreFormat\n" | 336 " scoreFormat\n" |
337 " }\n" | 337 " }\n" |
338 " }\n" | 338 " }\n" |
339 "}\n"; | 339 "}\n"; |
340 nlohmann::json json = { | 340 nlohmann::json json = { |
341 {"query", query} | 341 {"query", query} |
342 }; | 342 }; |
343 auto ret = nlohmann::json::parse(SendRequest(json.dump())); | 343 auto ret = nlohmann::json::parse(SendRequest(json.dump())); |
344 ParseUser(json["Viewer"]); | 344 ParseUser(json["Viewer"]); |
345 return 1; | 345 return 1; |
346 } | 346 } |
347 | 347 |