Mercurial > minori
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 |