comparison src/core/anime_db.cc @ 202:71832ffe425a

animia: re-add kvm fd source this is all being merged from my wildly out-of-date laptop. SORRY! in other news, I edited the CI file to install the wayland client as well, so the linux CI build might finally get wayland stuff.
author Paper <paper@paper.us.eu.org>
date Tue, 02 Jan 2024 06:05:06 -0500
parents bc1ae1810855
children 4d461ef7d424
comparison
equal deleted inserted replaced
201:8f6f8dd2eb23 202:71832ffe425a
1 #include "core/anime_db.h" 1 #include "core/anime_db.h"
2 #include "core/anime.h" 2 #include "core/anime.h"
3 #include "core/strings.h" 3 #include "core/strings.h"
4 #include <QDebug> 4 #include "core/json.h"
5 #include "core/filesystem.h"
6
7 #include "gui/translate/anime.h"
8 #include "gui/translate/anilist.h"
9
10 #include <QDate>
11
12 #include <fstream>
13
14 #include <iostream>
15 #include <exception>
5 16
6 namespace Anime { 17 namespace Anime {
7 18
8 int Database::GetTotalAnimeAmount() { 19 size_t Database::GetTotalAnimeAmount() {
9 int total = 0; 20 size_t total = 0;
10 for (const auto& a : items) { 21
11 if (a.second.IsInUserList()) 22 for (const auto& [id, anime] : items)
23 if (anime.IsInUserList())
12 total++; 24 total++;
13 } 25
14 return total; 26 return total;
15 } 27 }
16 28
17 int Database::GetListsAnimeAmount(ListStatus status) { 29 size_t Database::GetListsAnimeAmount(ListStatus status) {
18 if (status == ListStatus::NOT_IN_LIST) 30 if (status == ListStatus::NOT_IN_LIST)
19 return 0; 31 return 0;
20 int total = 0; 32
21 for (const auto& a : items) { 33 size_t total = 0;
22 if (a.second.IsInUserList() && a.second.GetUserStatus() == status) 34
35 for (const auto& [id, anime] : items)
36 if (anime.IsInUserList() && anime.GetUserStatus() == status)
23 total++; 37 total++;
24 } 38
25 return total; 39 return total;
26 } 40 }
27 41
28 int Database::GetTotalEpisodeAmount() { 42 size_t Database::GetTotalEpisodeAmount() {
29 int total = 0; 43 size_t total = 0;
30 for (const auto& a : items) { 44
31 if (a.second.IsInUserList()) { 45 for (const auto& [id, anime] : items)
32 total += a.second.GetUserRewatchedTimes() * a.second.GetEpisodes(); 46 if (anime.IsInUserList())
33 total += a.second.GetUserProgress(); 47 total += anime.GetUserRewatchedTimes() * anime.GetEpisodes() + anime.GetUserProgress();
34 } 48
35 }
36 return total; 49 return total;
37 } 50 }
38 51
39 /* Returns the total watched amount in minutes. */ 52 /* Returns the total watched amount in minutes. */
40 int Database::GetTotalWatchedAmount() { 53 size_t Database::GetTotalWatchedAmount() {
41 int total = 0; 54 size_t total = 0;
42 for (const auto& a : items) { 55
43 if (a.second.IsInUserList()) { 56 for (const auto& [id, anime] : items)
44 total += a.second.GetDuration() * a.second.GetUserProgress(); 57 if (anime.IsInUserList())
45 total += a.second.GetEpisodes() * a.second.GetDuration() * a.second.GetUserRewatchedTimes(); 58 total += anime.GetDuration() * anime.GetUserProgress()
46 } 59 + anime.GetEpisodes() * anime.GetDuration() * anime.GetUserRewatchedTimes();
47 } 60
48 return total; 61 return total;
49 } 62 }
50 63
51 /* Returns the total planned amount in minutes. 64 /* Returns the total planned amount in minutes.
52 Note that we should probably limit progress to the 65 Note that we should probably limit progress to the
53 amount of episodes, as AniList will let you 66 amount of episodes, as AniList will let you
54 set episode counts up to 32768. But that should 67 set episode counts up to 32768. But that should
55 rather be handled elsewhere. */ 68 rather be handled elsewhere. */
56 int Database::GetTotalPlannedAmount() { 69 size_t Database::GetTotalPlannedAmount() {
57 int total = 0; 70 size_t total = 0;
58 for (const auto& a : items) { 71
59 if (a.second.IsInUserList()) 72 for (const auto& [id, anime] : items)
60 total += a.second.GetDuration() * (a.second.GetEpisodes() - a.second.GetUserProgress()); 73 if (anime.IsInUserList())
61 } 74 total += anime.GetDuration() * (anime.GetEpisodes() - anime.GetUserProgress());
62 return total; 75
63 } 76 return total;
64 77 }
65 /* In Taiga this is called a mean, but "average" is 78
79 /* In Taiga this is called the mean, but "average" is
66 what's primarily used in conversation, at least 80 what's primarily used in conversation, at least
67 in the U.S. */ 81 in the U.S. */
68 double Database::GetAverageScore() { 82 double Database::GetAverageScore() {
69 double avg = 0; 83 double avg = 0;
70 int amt = 0; 84 size_t amt = 0;
71 for (const auto& a : items) { 85
72 if (a.second.IsInUserList() && a.second.GetUserScore()) { 86 for (const auto& [id, anime] : items) {
73 avg += a.second.GetUserScore(); 87 if (anime.IsInUserList() && anime.GetUserScore()) {
88 avg += anime.GetUserScore();
74 amt++; 89 amt++;
75 } 90 }
76 } 91 }
77 return avg / amt; 92 return avg / amt;
78 } 93 }
79 94
80 double Database::GetScoreDeviation() { 95 double Database::GetScoreDeviation() {
81 double squares_sum = 0, avg = GetAverageScore(); 96 double squares_sum = 0, avg = GetAverageScore();
82 int amt = 0; 97 size_t amt = 0;
83 for (const auto& a : items) { 98
84 if (a.second.IsInUserList() && a.second.GetUserScore()) { 99 for (const auto& [id, anime] : items) {
85 squares_sum += std::pow(static_cast<double>(a.second.GetUserScore()) - avg, 2); 100 if (anime.IsInUserList() && anime.GetUserScore()) {
101 squares_sum += std::pow(static_cast<double>(anime.GetUserScore()) - avg, 2);
86 amt++; 102 amt++;
87 } 103 }
88 } 104 }
105
89 return (amt > 0) ? std::sqrt(squares_sum / amt) : 0; 106 return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
90 } 107 }
91 108
92 template <typename T, typename U> 109 template<typename T, typename U>
93 static T get_lowest_in_map(const std::unordered_map<T, U>& map) { 110 static T get_lowest_in_map(const std::unordered_map<T, U>& map) {
94 if (map.size() <= 0) 111 if (map.size() <= 0)
95 return 0; 112 return 0;
96 T id; 113
114 T id = 0;
97 U ret = std::numeric_limits<U>::max(); 115 U ret = std::numeric_limits<U>::max();
98 for (const auto& t : map) { 116 for (const auto& t : map) {
99 if (t.second < ret) { 117 if (t.second < ret) {
100 ret = t.second; 118 ret = t.second;
101 id = t.first; 119 id = t.first;
102 } 120 }
103 } 121 }
104 return id; 122 return id;
105 } 123 }
106 124
107 /* This is really fugly but WHO CARES :P 125 /*
108 126 * This fairly basic algorithm is only in effect because
109 This sort of ""advanced"" algorithm is only in effect because 127 * there are some special cases, e.g. Another and Re:ZERO, where
110 there are some special cases, e.g. Another and Re:ZERO, where 128 * we get the wrong match, so we have to create Advanced Techniques
111 we get the wrong match, so we have to create Advanced Techniques 129 * to solve this
112 to solve this 130 *
113 131 * This algorithm:
114 This algorithm: 132 * 1. searches each anime item for a match to the preferred title
115 1. searches each anime item for a match to the preferred title 133 * AND all synonyms and marks those matches with
116 AND all synonyms and marks those matches with 134 * `synonym.length() - (synonym.find(needle) + needle.length());`
117 `synonym.length() - (synonym.find(needle) + needle.length());` 135 * which should never be less than zero and will be zero if, and only if
118 which, on a title that exactly matches, will be 0 136 * the titles match exactly.
119 2. returns the id of the match that is the lowest, which will most 137 * 2. returns the id of the match that is the lowest, which will most
120 definitely match anything that exactly matches the title of the 138 * definitely match anything that exactly matches the title of the
121 filename */ 139 * filename
140 */
122 int Database::GetAnimeFromTitle(const std::string& title) { 141 int Database::GetAnimeFromTitle(const std::string& title) {
123 if (title.empty()) 142 if (title.empty())
124 return 0; 143 return 0;
125 std::unordered_map<int, long long> map; 144
126 for (const auto& a : items) { 145 std::unordered_map<int, size_t> map;
127 long long ret = a.second.GetUserPreferredTitle().find(title); 146
128 if (ret != static_cast<long long>(std::string::npos)) { 147 static const auto process_title = [&map](const Anime& anime, const std::string& title, const std::string& needle) -> bool {
129 map[a.second.GetId()] = a.second.GetUserPreferredTitle().length() - (ret + title.length()); 148 size_t ret = title.find(needle);
149 if (ret == std::string::npos)
150 return false;
151
152 map[anime.GetId()] = title.length() - (ret + needle.length());
153 return true;
154 };
155
156 for (const auto& [id, anime] : items) {
157 if (process_title(anime, anime.GetUserPreferredTitle(), title))
130 continue; 158 continue;
131 } 159
132 for (const auto& synonym : a.second.GetTitleSynonyms()) { 160 for (const auto& synonym : anime.GetTitleSynonyms())
133 ret = synonym.find(title); 161 if (process_title(anime, synonym, title))
134 if (ret != static_cast<long long>(std::string::npos)) {
135 map[a.second.GetId()] = synonym.length() - (ret + title.length());
136 continue; 162 continue;
137 } 163 }
138 } 164
139 }
140 return get_lowest_in_map(map); 165 return get_lowest_in_map(map);
141 } 166 }
142 167
168 static bool GetListDataAsJSON(const Anime& anime, nlohmann::json& json) {
169 if (!anime.IsInUserList())
170 return false;
171
172 // clang-format off
173 json = {
174 {"status", Translate::ToString(anime.GetUserStatus())},
175 {"progress", anime.GetUserProgress()},
176 {"score", anime.GetUserScore()},
177 {"started", anime.GetUserDateStarted().GetAsAniListJson()},
178 {"completed", anime.GetUserDateCompleted().GetAsAniListJson()},
179 {"private", anime.GetUserIsPrivate()},
180 {"rewatched_times", anime.GetUserRewatchedTimes()},
181 {"rewatching", anime.GetUserIsRewatching()},
182 {"updated", anime.GetUserTimeUpdated()},
183 {"notes", anime.GetUserNotes()}
184 };
185 // clang-format on
186
187 return true;
188 }
189
190 static bool GetAnimeAsJSON(const Anime& anime, nlohmann::json& json) {
191 // clang-format off
192 json = {
193 {"id", anime.GetId()},
194 {"title", {
195 {"native", anime.GetNativeTitle()},
196 {"romaji", anime.GetRomajiTitle()},
197 {"english", anime.GetEnglishTitle()}
198 }},
199 {"synonyms", anime.GetTitleSynonyms()},
200 {"episodes", anime.GetEpisodes()},
201 {"airing_status", Translate::ToString(anime.GetAiringStatus())},
202 {"air_date", anime.GetAirDate().GetAsAniListJson()},
203 {"genres", anime.GetGenres()},
204 {"producers", anime.GetProducers()},
205 {"format", Translate::ToString(anime.GetFormat())},
206 {"season", Translate::ToString(anime.GetSeason())},
207 {"audience_score", anime.GetAudienceScore()},
208 {"synopsis", anime.GetSynopsis()},
209 {"duration", anime.GetDuration()},
210 {"poster_url", anime.GetPosterUrl()}
211 };
212 // clang-format on
213
214 nlohmann::json user;
215 if (GetListDataAsJSON(anime, user))
216 json.push_back({"list_data", user});
217
218 return true;
219 }
220
221 bool Database::GetDatabaseAsJSON(nlohmann::json& json) {
222 for (const auto& [id, anime] : items) {
223 nlohmann::json anime_json = {};
224 GetAnimeAsJSON(anime, anime_json);
225 json.push_back(anime_json);
226 }
227
228 return true;
229 }
230
231 bool Database::SaveDatabaseToDisk() {
232 std::filesystem::path db_path = Filesystem::GetAnimeDBPath();
233 Filesystem::CreateDirectories(db_path);
234
235 std::ofstream db_file(db_path);
236 if (!db_file)
237 return false;
238
239 nlohmann::json json = {};
240 if (!GetDatabaseAsJSON(json))
241 return false;
242
243 db_file << std::setw(4) << json << std::endl;
244 return true;
245 }
246
247 static bool ParseAnimeUserInfoJSON(const nlohmann::json& json, Anime& anime) {
248 if (!anime.IsInUserList())
249 anime.AddToUserList();
250
251 anime.SetUserStatus(Translate::ToListStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, "")));
252 anime.SetUserProgress(JSON::GetNumber(json, "/progress"_json_pointer, 0));
253 anime.SetUserScore(JSON::GetNumber(json, "/score"_json_pointer, 0));
254 anime.SetUserDateStarted(Date(JSON::GetValue(json, "/started"_json_pointer)));
255 anime.SetUserDateCompleted(Date(JSON::GetValue(json, "/completed"_json_pointer)));
256 anime.SetUserIsPrivate(JSON::GetBoolean(json, "/private"_json_pointer, false));
257 anime.SetUserRewatchedTimes(JSON::GetNumber(json, "/rewatched_times"_json_pointer, 0));
258 anime.SetUserIsRewatching(JSON::GetBoolean(json, "/rewatching"_json_pointer, false));
259 anime.SetUserTimeUpdated(JSON::GetNumber(json, "/updated"_json_pointer, 0));
260 anime.SetUserNotes(JSON::GetString<std::string>(json, "/notes"_json_pointer, ""));
261
262 return true;
263 }
264
265 static bool ParseAnimeInfoJSON(const nlohmann::json& json, Database& database) {
266 int id = JSON::GetNumber(json, "/id"_json_pointer, 0);
267 if (!id)
268 return false;
269
270 Anime& anime = database.items[id];
271
272 anime.SetId(id);
273 anime.SetNativeTitle(JSON::GetString<std::string>(json, "/title/native"_json_pointer, ""));
274 anime.SetRomajiTitle(JSON::GetString<std::string>(json, "/title/romaji"_json_pointer, ""));
275 anime.SetEnglishTitle(JSON::GetString<std::string>(json, "/title/english"_json_pointer, ""));
276 anime.SetTitleSynonyms(JSON::GetArray<std::vector<std::string>>(json, "/synonyms"_json_pointer, {}));
277 anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0));
278 anime.SetAiringStatus(Translate::ToSeriesStatus(JSON::GetString<std::string>(json, "/airing_status"_json_pointer, "")));
279 anime.SetAirDate(Date(JSON::GetValue(json, "/air_date"_json_pointer)));
280 anime.SetGenres(JSON::GetArray<std::vector<std::string>>(json, "/genres"_json_pointer, {}));
281 anime.SetProducers(JSON::GetArray<std::vector<std::string>>(json, "/producers"_json_pointer, {}));
282 anime.SetFormat(Translate::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, "")));
283 anime.SetSeason(Translate::ToSeriesSeason(JSON::GetString<std::string>(json, "/season"_json_pointer, "")));
284 anime.SetAudienceScore(JSON::GetNumber(json, "/audience_score"_json_pointer, 0));
285 anime.SetSynopsis(JSON::GetString<std::string>(json, "/synopsis"_json_pointer, ""));
286 anime.SetDuration(JSON::GetNumber(json, "/duration"_json_pointer, 0));
287 anime.SetPosterUrl(JSON::GetString<std::string>(json, "/poster_url"_json_pointer, ""));
288
289 if (json.contains("/list_data"_json_pointer) && json.at("/list_data"_json_pointer).is_object())
290 ParseAnimeUserInfoJSON(json.at("/list_data"_json_pointer), anime);
291
292 return true;
293 }
294
295 bool Database::ParseDatabaseJSON(const nlohmann::json& json) {
296 for (const auto& anime_json : json)
297 ParseAnimeInfoJSON(anime_json, *this);
298
299 return true;
300 }
301
302 bool Database::LoadDatabaseFromDisk() {
303 std::filesystem::path db_path = Filesystem::GetAnimeDBPath();
304 Filesystem::CreateDirectories(db_path);
305
306 std::ifstream db_file(db_path);
307 if (!db_file)
308 return false;
309
310 /* When parsing, do NOT throw exceptions */
311 nlohmann::json json;
312 try {
313 json = json.parse(db_file);
314 } catch (std::exception const& ex) {
315 std::cerr << "[anime/db] Failed to parse JSON! " << ex.what() << std::endl;
316 return false;
317 }
318
319 if (!ParseDatabaseJSON(json)) /* How */
320 return false;
321
322 return true;
323 }
324
143 Database db; 325 Database db;
144 326
145 } // namespace Anime 327 } // namespace Anime