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