comparison src/anilist.cpp @ 1:1ae666fdf9e2

*: initial commit
author Paper <mrpapersonic@gmail.com>
date Tue, 08 Aug 2023 19:49:15 -0400
parents
children 23d0d9319a00
comparison
equal deleted inserted replaced
0:5a76e1b94163 1:1ae666fdf9e2
1 #include "window.h"
2 #include "json.h"
3 #include <curl/curl.h>
4 #include <chrono>
5 #include <exception>
6 #include <format>
7 #include "anilist.h"
8 #include "anime.h"
9 #include "config.h"
10 #include "string_utils.h"
11 #define CLIENT_ID "13706"
12
13 size_t AniList::CurlWriteCallback(void *contents, size_t size, size_t nmemb, void *userdata) {
14 ((std::string*)userdata)->append((char*)contents, size * nmemb);
15 return size * nmemb;
16 }
17
18 std::string AniList::SendRequest(std::string data) {
19 struct curl_slist *list = NULL;
20 std::string userdata;
21 curl = curl_easy_init();
22 if (curl) {
23 list = curl_slist_append(list, "Accept: application/json");
24 list = curl_slist_append(list, "Content-Type: application/json");
25 std::string bearer = "Authorization: Bearer " + session.config.anilist.auth_token;
26 list = curl_slist_append(list, bearer.c_str());
27 curl_easy_setopt(curl, CURLOPT_URL, "https://graphql.anilist.co");
28 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
29 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
30 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &userdata);
31 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback);
32 /* FIXME: This sucks. When using HTTPS, we should ALWAYS make sure that our peer
33 is actually valid. I assume the best way to go about this would be to bundle a
34 certificate file, and if it's not found we should *prompt the user* and ask them
35 if it's okay to contact AniList WITHOUT verification. If so, we're golden, and this
36 flag will be set. If not, we should abort mission.
37
38 For this program, it's probably fine to just contact AniList without
39 HTTPS verification. However it should still be in the list of things to do... */
40 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE);
41 res = curl_easy_perform(curl);
42 curl_slist_free_all(list);
43 if (res != CURLE_OK) {
44 QMessageBox box(QMessageBox::Icon::Critical, "", QString("curl_easy_perform(curl) failed!: ") + QString(curl_easy_strerror(res)));
45 box.exec();
46 curl_easy_cleanup(curl);
47 return "";
48 }
49 curl_easy_cleanup(curl);
50 return userdata;
51 }
52 return "";
53 }
54
55 int AniList::GetUserId(std::string name) {
56 #define QUERY "query ($name: String) {\n" \
57 " User (name: $name) {\n" \
58 " id\n" \
59 " }\n" \
60 "}\n"
61 nlohmann::json json = {
62 {"query", QUERY},
63 {"variables", {
64 {"name", name}
65 }}
66 };
67 auto ret = nlohmann::json::parse(SendRequest(json.dump()));
68 return ret["data"]["User"]["id"].get<int>();
69 #undef QUERY
70 }
71
72 /* Maps to convert string forms to our internal enums */
73
74 std::map<std::string, enum AnimeWatchingStatus> StringToAnimeWatchingMap = {
75 {"CURRENT", CURRENT},
76 {"PLANNING", PLANNING},
77 {"COMPLETED", COMPLETED},
78 {"DROPPED", DROPPED},
79 {"PAUSED", PAUSED},
80 {"REPEATING", REPEATING}
81 };
82
83 std::map<std::string, enum AnimeAiringStatus> StringToAnimeAiringMap = {
84 {"FINISHED", FINISHED},
85 {"RELEASING", RELEASING},
86 {"NOT_YET_RELEASED", NOT_YET_RELEASED},
87 {"CANCELLED", CANCELLED},
88 {"HIATUS", HIATUS}
89 };
90
91 std::map<std::string, enum AnimeSeason> StringToAnimeSeasonMap = {
92 {"WINTER", WINTER},
93 {"SPRING", SPRING},
94 {"SUMMER", SUMMER},
95 {"FALL", FALL}
96 };
97
98 std::map<std::string, enum AnimeFormat> StringToAnimeFormatMap = {
99 {"TV", TV},
100 {"TV_SHORT", TV_SHORT},
101 {"MOVIE", MOVIE},
102 {"SPECIAL", SPECIAL},
103 {"OVA", OVA},
104 {"ONA", ONA},
105 {"MUSIC", MUSIC},
106 {"MANGA", MANGA},
107 {"NOVEL", NOVEL},
108 {"ONE_SHOT", ONE_SHOT}
109 };
110
111 int AniList::UpdateAnimeList(std::vector<AnimeList>* anime_lists, int id) {
112 #define QUERY "query ($id: Int) {\n" \
113 " MediaListCollection (userId: $id, type: ANIME) {\n" \
114 " lists {\n" \
115 " name\n" \
116 " entries {\n" \
117 " score\n" \
118 " notes\n" \
119 " progress\n" \
120 " startedAt {\n" \
121 " year\n" \
122 " month\n" \
123 " day\n" \
124 " }\n" \
125 " completedAt {\n" \
126 " year\n" \
127 " month\n" \
128 " day\n" \
129 " }\n" \
130 " media {\n" \
131 " id\n" \
132 " title {\n" \
133 " userPreferred\n" \
134 " }\n" \
135 " format\n" \
136 " status\n" \
137 " averageScore\n" \
138 " season\n" \
139 " startDate {\n" \
140 " year\n" \
141 " month\n" \
142 " day\n" \
143 " }\n" \
144 " genres\n" \
145 " episodes\n" \
146 " duration\n" \
147 " synonyms\n" \
148 " description(asHtml: false)\n" \
149 " }\n" \
150 " }\n" \
151 " }\n" \
152 " }\n" \
153 "}\n"
154 nlohmann::json json = {
155 {"query", QUERY},
156 {"variables", {
157 {"id", id}
158 }}
159 };
160 /* TODO: do a try catch here, catch any json errors and then call
161 Authorize() if needed */
162 auto res = nlohmann::json::parse(SendRequest(json.dump()));
163 /* TODO: make sure that we actually need the wstring converter and see
164 if we can just get wide strings back from nlohmann::json */
165 for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
166 /* why are the .key() values strings?? */
167 int list_key = std::stoi(list.key());
168 AnimeList anime_list;
169 anime_list.name = StringUtils::Utf8ToWstr(list.value()["name"].get<std::string>());
170 for (const auto& entry : list.value()["entries"].items()) {
171 int entry_key = std::stoi(entry.key());
172 Anime anime;
173 anime.score = entry.value()["score"].get<int>();
174 anime.progress = entry.value()["progress"].get<int>();
175 if (entry.value()["status"].is_string())
176 anime.status = StringToAnimeWatchingMap[entry.value()["status"].get<std::string>()];
177 if (entry.value()["notes"].is_string())
178 anime.notes = StringUtils::Utf8ToWstr(entry.value()["notes"].get<std::string>());
179
180 if (ANILIST_DATE_IS_VALID(entry.value()["startedAt"]))
181 anime.started = ANILIST_DATE_TO_YMD(entry.value()["startedAt"]);
182 if (ANILIST_DATE_IS_VALID(entry.value()["completedAt"]))
183 anime.completed = ANILIST_DATE_TO_YMD(entry.value()["completedAt"]);
184
185 anime.title = StringUtils::Utf8ToWstr(entry.value()["media"]["title"]["userPreferred"].get<std::string>());
186 anime.id = entry.value()["media"]["id"].get<int>();
187 if (!entry.value()["media"]["episodes"].is_null())
188 anime.episodes = entry.value()["media"]["episodes"].get<int>();
189 else // hasn't aired yet
190 anime.episodes = 0;
191
192 if (!entry.value()["media"]["format"].is_null())
193 anime.type = StringToAnimeFormatMap[entry.value()["media"]["format"].get<std::string>()];
194
195 anime.airing = StringToAnimeAiringMap[entry.value()["media"]["status"].get<std::string>()];
196
197 if (ANILIST_DATE_IS_VALID(entry.value()["media"]["startDate"]))
198 anime.air_date = ANILIST_DATE_TO_YMD(entry.value()["media"]["startDate"]);
199
200 if (entry.value()["media"]["averageScore"].is_number())
201 anime.audience_score = entry.value()["media"]["averageScore"].get<int>();
202
203 if (entry.value()["media"]["season"].is_string())
204 anime.season = StringToAnimeSeasonMap[entry.value()["media"]["season"].get<std::string>()];
205
206 if (entry.value()["media"]["duration"].is_number())
207 anime.duration = entry.value()["media"]["duration"].get<int>();
208 else
209 anime.duration = 0;
210
211 if (entry.value()["media"]["genres"].is_array())
212 anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>();
213 if (entry.value()["media"]["description"].is_string())
214 anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(entry.value()["media"]["description"].get<std::string>()));
215 anime_list.Add(anime);
216 }
217 anime_lists->push_back(anime_list);
218 }
219 return 1;
220 }
221
222 int AniList::Authorize() {
223 if (session.config.anilist.auth_token.empty()) {
224 /* Prompt for PIN */
225 QDesktopServices::openUrl(QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token"));
226 bool ok;
227 QString token = QInputDialog::getText(0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, "", &ok);
228 if (ok && !token.isEmpty()) {
229 session.config.anilist.auth_token = token.toStdString();
230 } else { // fail
231 return 0;
232 }
233 }
234 return 1;
235 }