2
|
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 /* NOTE: these should be in the qrc file */
|
|
113 #define QUERY "query ($id: Int) {\n" \
|
|
114 " MediaListCollection (userId: $id, type: ANIME) {\n" \
|
|
115 " lists {\n" \
|
|
116 " name\n" \
|
|
117 " entries {\n" \
|
|
118 " score\n" \
|
|
119 " notes\n" \
|
|
120 " progress\n" \
|
|
121 " startedAt {\n" \
|
|
122 " year\n" \
|
|
123 " month\n" \
|
|
124 " day\n" \
|
|
125 " }\n" \
|
|
126 " completedAt {\n" \
|
|
127 " year\n" \
|
|
128 " month\n" \
|
|
129 " day\n" \
|
|
130 " }\n" \
|
|
131 " updatedAt\n" \
|
|
132 " media {\n" \
|
|
133 " id\n" \
|
|
134 " title {\n" \
|
|
135 " romaji\n" \
|
|
136 " english\n" \
|
|
137 " native\n" \
|
|
138 " }\n" \
|
|
139 " format\n" \
|
|
140 " status\n" \
|
|
141 " averageScore\n" \
|
|
142 " season\n" \
|
|
143 " startDate {\n" \
|
|
144 " year\n" \
|
|
145 " month\n" \
|
|
146 " day\n" \
|
|
147 " }\n" \
|
|
148 " genres\n" \
|
|
149 " episodes\n" \
|
|
150 " duration\n" \
|
|
151 " synonyms\n" \
|
|
152 " description(asHtml: false)\n" \
|
|
153 " }\n" \
|
|
154 " }\n" \
|
|
155 " }\n" \
|
|
156 " }\n" \
|
|
157 "}\n"
|
|
158 nlohmann::json json = {
|
|
159 {"query", QUERY},
|
|
160 {"variables", {
|
|
161 {"id", id}
|
|
162 }}
|
|
163 };
|
|
164 /* TODO: do a try catch here, catch any json errors and then call
|
|
165 Authorize() if needed */
|
|
166 auto res = nlohmann::json::parse(SendRequest(json.dump()));
|
|
167 /* TODO: make sure that we actually need the wstring converter and see
|
|
168 if we can just get wide strings back from nlohmann::json */
|
|
169 for (const auto& list : res["data"]["MediaListCollection"]["lists"].items()) {
|
|
170 /* why are the .key() values strings?? */
|
|
171 int list_key = std::stoi(list.key());
|
|
172 AnimeList anime_list;
|
|
173 anime_list.name = StringUtils::Utf8ToWstr(JSON::GetString(list.value(), "name"));
|
|
174 for (const auto& entry : list.value()["entries"].items()) {
|
|
175 int entry_key = std::stoi(entry.key());
|
|
176 Anime anime;
|
|
177 anime.score = JSON::GetInt(entry.value(), "score");
|
|
178 anime.progress = JSON::GetInt(entry.value(), "progress");
|
|
179 anime.status = StringToAnimeWatchingMap[JSON::GetString(entry.value(), "status")];
|
|
180 anime.notes = StringUtils::Utf8ToWstr(JSON::GetString(entry.value(), "notes"));
|
|
181
|
|
182 anime.started.SetYear(JSON::GetInt(entry.value()["startedAt"], "year"));
|
|
183 anime.started.SetMonth(JSON::GetInt(entry.value()["startedAt"], "month"));
|
|
184 anime.started.SetDay(JSON::GetInt(entry.value()["startedAt"], "day"));
|
|
185
|
|
186 anime.completed.SetYear(JSON::GetInt(entry.value()["completedAt"], "year"));
|
|
187 anime.completed.SetMonth(JSON::GetInt(entry.value()["completedAt"], "month"));
|
|
188 anime.completed.SetDay(JSON::GetInt(entry.value()["completedAt"], "day"));
|
|
189
|
|
190 anime.updated = JSON::GetInt(entry.value(), "updatedAt");
|
|
191
|
|
192 anime.title.native = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "native"));
|
|
193 anime.title.english = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "english"));
|
|
194 anime.title.romaji = StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"]["title"], "romaji"));
|
|
195
|
|
196 anime.id = JSON::GetInt(entry.value()["media"], "id");
|
|
197 anime.episodes = JSON::GetInt(entry.value()["media"], "episodes");
|
|
198 anime.type = StringToAnimeFormatMap[JSON::GetString(entry.value()["media"], "format")];
|
|
199
|
|
200 anime.airing = StringToAnimeAiringMap[JSON::GetString(entry.value()["media"], "status")];
|
|
201
|
|
202 anime.air_date.SetYear(JSON::GetInt(entry.value()["media"]["startDate"], "year"));
|
|
203 anime.air_date.SetMonth(JSON::GetInt(entry.value()["media"]["startDate"], "month"));
|
|
204 anime.air_date.SetDay(JSON::GetInt(entry.value()["media"]["startDate"], "day"));
|
|
205
|
|
206 anime.audience_score = JSON::GetInt(entry.value()["media"], "averageScore");
|
|
207 anime.season = StringToAnimeSeasonMap[JSON::GetString(entry.value()["media"], "season")];
|
|
208 anime.duration = JSON::GetInt(entry.value()["media"], "duration");
|
|
209 anime.synopsis = StringUtils::TextifySynopsis(StringUtils::Utf8ToWstr(JSON::GetString(entry.value()["media"], "duration")));
|
|
210
|
|
211 if (entry.value()["media"]["genres"].is_array())
|
|
212 anime.genres = entry.value()["media"]["genres"].get<std::vector<std::string>>();
|
|
213 anime_list.Add(anime);
|
|
214 }
|
|
215 anime_lists->push_back(anime_list);
|
|
216 }
|
|
217 return 1;
|
|
218 #undef QUERY
|
|
219 }
|
|
220
|
|
221 int AniList::Authorize() {
|
|
222 if (session.config.anilist.auth_token.empty()) {
|
|
223 /* Prompt for PIN */
|
|
224 QDesktopServices::openUrl(QUrl("https://anilist.co/api/v2/oauth/authorize?client_id=" CLIENT_ID "&response_type=token"));
|
|
225 bool ok;
|
|
226 QString token = QInputDialog::getText(0, "Credentials needed!", "Please enter the code given to you after logging in to AniList:", QLineEdit::Normal, "", &ok);
|
|
227 if (ok && !token.isEmpty()) {
|
|
228 session.config.anilist.auth_token = token.toStdString();
|
|
229 } else { // fail
|
|
230 return 0;
|
|
231 }
|
|
232 }
|
|
233 return 1;
|
|
234 }
|