| 1 | 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 } |