comparison src/core/anime_db.cc @ 369:47c9f8502269

*: clang-format all the things I've edited the formatting a bit. Now pointer asterisks (and reference ampersands) are on the variable instead of the type, as well as having newlines for function braces (but nothing else)
author Paper <paper@tflc.us>
date Fri, 25 Jul 2025 10:16:02 -0400
parents b5d6c27c308f
children
comparison
equal deleted inserted replaced
368:6d37a998cf91 369:47c9f8502269
10 10
11 #include <QDate> 11 #include <QDate>
12 12
13 #include <fstream> 13 #include <fstream>
14 14
15 #include <cstdlib>
15 #include <exception> 16 #include <exception>
16 #include <cstdlib>
17 #include <iostream> 17 #include <iostream>
18 #include <random> 18 #include <random>
19 19
20 namespace Anime { 20 namespace Anime {
21 21
22 size_t Database::GetTotalAnimeAmount() const { 22 size_t Database::GetTotalAnimeAmount() const
23 size_t total = 0; 23 {
24 24 size_t total = 0;
25 for (const auto& [id, anime] : items) 25
26 for (const auto &[id, anime] : items)
26 if (anime.IsInUserList()) 27 if (anime.IsInUserList())
27 total++; 28 total++;
28 29
29 return total; 30 return total;
30 } 31 }
31 32
32 size_t Database::GetListsAnimeAmount(ListStatus status) const { 33 size_t Database::GetListsAnimeAmount(ListStatus status) const
34 {
33 if (status == ListStatus::NotInList) 35 if (status == ListStatus::NotInList)
34 return 0; 36 return 0;
35 37
36 size_t total = 0; 38 size_t total = 0;
37 39
38 for (const auto& [id, anime] : items) 40 for (const auto &[id, anime] : items)
39 if (anime.IsInUserList() && anime.GetUserStatus() == status) 41 if (anime.IsInUserList() && anime.GetUserStatus() == status)
40 total++; 42 total++;
41 43
42 return total; 44 return total;
43 } 45 }
44 46
45 size_t Database::GetTotalEpisodeAmount() const { 47 size_t Database::GetTotalEpisodeAmount() const
46 size_t total = 0; 48 {
47 49 size_t total = 0;
48 for (const auto& [id, anime] : items) 50
51 for (const auto &[id, anime] : items)
49 if (anime.IsInUserList()) 52 if (anime.IsInUserList())
50 total += anime.GetUserRewatchedTimes() * anime.GetEpisodes() + anime.GetUserProgress(); 53 total += anime.GetUserRewatchedTimes() * anime.GetEpisodes() + anime.GetUserProgress();
51 54
52 return total; 55 return total;
53 } 56 }
54 57
55 /* Returns the total watched amount in minutes. */ 58 /* Returns the total watched amount in minutes. */
56 size_t Database::GetTotalWatchedAmount() const { 59 size_t Database::GetTotalWatchedAmount() const
57 size_t total = 0; 60 {
58 61 size_t total = 0;
59 for (const auto& [id, anime] : items) 62
63 for (const auto &[id, anime] : items)
60 if (anime.IsInUserList()) 64 if (anime.IsInUserList())
61 total += anime.GetDuration() * anime.GetUserProgress() + 65 total += anime.GetDuration() * anime.GetUserProgress() +
62 anime.GetEpisodes() * anime.GetDuration() * anime.GetUserRewatchedTimes(); 66 anime.GetEpisodes() * anime.GetDuration() * anime.GetUserRewatchedTimes();
63 67
64 return total; 68 return total;
67 /* Returns the total planned amount in minutes. 71 /* Returns the total planned amount in minutes.
68 Note that we should probably limit progress to the 72 Note that we should probably limit progress to the
69 amount of episodes, as AniList will let you 73 amount of episodes, as AniList will let you
70 set episode counts up to 32768. But that should 74 set episode counts up to 32768. But that should
71 rather be handled elsewhere. */ 75 rather be handled elsewhere. */
72 size_t Database::GetTotalPlannedAmount() const { 76 size_t Database::GetTotalPlannedAmount() const
73 size_t total = 0; 77 {
74 78 size_t total = 0;
75 for (const auto& [id, anime] : items) 79
80 for (const auto &[id, anime] : items)
76 if (anime.IsInUserList()) 81 if (anime.IsInUserList())
77 total += anime.GetDuration() * (anime.GetEpisodes() - anime.GetUserProgress()); 82 total += anime.GetDuration() * (anime.GetEpisodes() - anime.GetUserProgress());
78 83
79 return total; 84 return total;
80 } 85 }
81 86
82 /* In Taiga this is called the mean, but "average" is 87 /* In Taiga this is called the mean, but "average" is
83 what's primarily used in conversation, at least 88 what's primarily used in conversation, at least
84 in the U.S. */ 89 in the U.S. */
85 double Database::GetAverageScore() const { 90 double Database::GetAverageScore() const
91 {
86 double avg = 0; 92 double avg = 0;
87 size_t amt = 0; 93 size_t amt = 0;
88 94
89 for (const auto& [id, anime] : items) { 95 for (const auto &[id, anime] : items) {
90 if (anime.IsInUserList() && anime.GetUserScore()) { 96 if (anime.IsInUserList() && anime.GetUserScore()) {
91 avg += anime.GetUserScore(); 97 avg += anime.GetUserScore();
92 amt++; 98 amt++;
93 } 99 }
94 } 100 }
95 return avg / amt; 101 return avg / amt;
96 } 102 }
97 103
98 double Database::GetScoreDeviation() const { 104 double Database::GetScoreDeviation() const
105 {
99 double squares_sum = 0, avg = GetAverageScore(); 106 double squares_sum = 0, avg = GetAverageScore();
100 size_t amt = 0; 107 size_t amt = 0;
101 108
102 for (const auto& [id, anime] : items) { 109 for (const auto &[id, anime] : items) {
103 if (anime.IsInUserList() && anime.GetUserScore()) { 110 if (anime.IsInUserList() && anime.GetUserScore()) {
104 squares_sum += std::pow(static_cast<double>(anime.GetUserScore()) - avg, 2); 111 squares_sum += std::pow(static_cast<double>(anime.GetUserScore()) - avg, 2);
105 amt++; 112 amt++;
106 } 113 }
107 } 114 }
108 115
109 return (amt > 0) ? std::sqrt(squares_sum / amt) : 0; 116 return (amt > 0) ? std::sqrt(squares_sum / amt) : 0;
110 } 117 }
111 118
112 int Database::LookupAnimeTitle(const std::string& title) const { 119 int Database::LookupAnimeTitle(const std::string &title) const
120 {
113 if (title.empty()) 121 if (title.empty())
114 return 0; 122 return 0;
115 123
116 std::string title_n(title); 124 std::string title_n(title);
117 Strings::NormalizeAnimeTitle(title_n); 125 Strings::NormalizeAnimeTitle(title_n);
118 126
119 for (const auto& [id, anime] : items) { 127 for (const auto &[id, anime] : items) {
120 std::vector<std::string> synonyms(anime.GetTitleSynonyms()); 128 std::vector<std::string> synonyms(anime.GetTitleSynonyms());
121 synonyms.push_back(anime.GetUserPreferredTitle()); 129 synonyms.push_back(anime.GetUserPreferredTitle());
122 130
123 for (auto& synonym : synonyms) { 131 for (auto &synonym : synonyms) {
124 Strings::NormalizeAnimeTitle(synonym); 132 Strings::NormalizeAnimeTitle(synonym);
125 if (synonym == title_n) 133 if (synonym == title_n)
126 return id; 134 return id;
127 } 135 }
128 } 136 }
129 137
130 return 0; 138 return 0;
131 } 139 }
132 140
133 static bool GetListDataAsJSON(const Anime& anime, nlohmann::json& json) { 141 static bool GetListDataAsJSON(const Anime &anime, nlohmann::json &json)
142 {
134 if (!anime.IsInUserList()) 143 if (!anime.IsInUserList())
135 return false; 144 return false;
136 145
137 // clang-format off 146 // clang-format off
138 json = { 147 json = {
150 // clang-format on 159 // clang-format on
151 160
152 return true; 161 return true;
153 } 162 }
154 163
155 static bool GetAnimeAsJSON(const Anime& anime, nlohmann::json& json) { 164 static bool GetAnimeAsJSON(const Anime &anime, nlohmann::json &json)
165 {
156 // clang-format off 166 // clang-format off
157 json = { 167 json = {
158 {"id", anime.GetId()}, 168 {"id", anime.GetId()},
159 {"synonyms", anime.GetTitleSynonyms()}, 169 {"synonyms", anime.GetTitleSynonyms()},
160 {"episodes", anime.GetEpisodes()}, 170 {"episodes", anime.GetEpisodes()},
171 {"poster_url", anime.GetPosterUrl()} 181 {"poster_url", anime.GetPosterUrl()}
172 }; 182 };
173 // clang-format on 183 // clang-format on
174 184
175 /* now for dynamically-filled stuff */ 185 /* now for dynamically-filled stuff */
176 for (const auto& lang : TitleLanguages) { 186 for (const auto &lang : TitleLanguages) {
177 std::optional<std::string> title = anime.GetTitle(lang); 187 std::optional<std::string> title = anime.GetTitle(lang);
178 if (title.has_value()) 188 if (title.has_value())
179 json["title"][Strings::ToLower(Translate::ToString(lang))] = title.value(); 189 json["title"][Strings::ToLower(Translate::ToString(lang))] = title.value();
180 } 190 }
181 191
182 for (const auto& service : Services) { 192 for (const auto &service : Services) {
183 std::optional<std::string> id = anime.GetServiceId(service); 193 std::optional<std::string> id = anime.GetServiceId(service);
184 if (id.has_value()) 194 if (id.has_value())
185 json["ids"][Strings::ToLower(Translate::ToString(service))] = id.value(); 195 json["ids"][Strings::ToLower(Translate::ToString(service))] = id.value();
186 } 196 }
187 197
190 json.push_back({"list_data", user}); 200 json.push_back({"list_data", user});
191 201
192 return true; 202 return true;
193 } 203 }
194 204
195 bool Database::GetDatabaseAsJSON(nlohmann::json& json) const { 205 bool Database::GetDatabaseAsJSON(nlohmann::json &json) const
196 for (const auto& [id, anime] : items) { 206 {
207 for (const auto &[id, anime] : items) {
197 nlohmann::json anime_json = {}; 208 nlohmann::json anime_json = {};
198 GetAnimeAsJSON(anime, anime_json); 209 GetAnimeAsJSON(anime, anime_json);
199 json.push_back(anime_json); 210 json.push_back(anime_json);
200 } 211 }
201 212
202 return true; 213 return true;
203 } 214 }
204 215
205 bool Database::SaveDatabaseToDisk() const { 216 bool Database::SaveDatabaseToDisk() const
217 {
206 std::filesystem::path db_path = Filesystem::GetAnimeDBPath(); 218 std::filesystem::path db_path = Filesystem::GetAnimeDBPath();
207 Filesystem::CreateDirectories(db_path); 219 Filesystem::CreateDirectories(db_path);
208 220
209 std::ofstream db_file(db_path); 221 std::ofstream db_file(db_path);
210 if (!db_file) 222 if (!db_file)
216 228
217 db_file << std::setw(4) << json << std::endl; 229 db_file << std::setw(4) << json << std::endl;
218 return true; 230 return true;
219 } 231 }
220 232
221 static bool ParseAnimeUserInfoJSON(const nlohmann::json& json, Anime& anime) { 233 static bool ParseAnimeUserInfoJSON(const nlohmann::json &json, Anime &anime)
234 {
222 if (!anime.IsInUserList()) 235 if (!anime.IsInUserList())
223 anime.AddToUserList(); 236 anime.AddToUserList();
224 237
225 anime.SetUserStatus(Translate::ToListStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""))); 238 anime.SetUserStatus(Translate::ToListStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, "")));
226 anime.SetUserProgress(JSON::GetNumber(json, "/progress"_json_pointer, 0)); 239 anime.SetUserProgress(JSON::GetNumber(json, "/progress"_json_pointer, 0));
234 anime.SetUserNotes(JSON::GetString<std::string>(json, "/notes"_json_pointer, "")); 247 anime.SetUserNotes(JSON::GetString<std::string>(json, "/notes"_json_pointer, ""));
235 248
236 return true; 249 return true;
237 } 250 }
238 251
239 static bool ParseAnimeInfoJSON(const nlohmann::json& json, Database& database) { 252 static bool ParseAnimeInfoJSON(const nlohmann::json &json, Database &database)
253 {
240 int id = JSON::GetNumber(json, "/id"_json_pointer, 0); 254 int id = JSON::GetNumber(json, "/id"_json_pointer, 0);
241 if (!id) 255 if (!id)
242 return false; 256 return false;
243 257
244 Anime& anime = database.items[id]; 258 Anime &anime = database.items[id];
245 259
246 anime.SetId(id); 260 anime.SetId(id);
247 for (const auto& service : Services) { 261 for (const auto &service : Services) {
248 nlohmann::json::json_pointer p("/ids/" + Strings::ToLower(Translate::ToString(service))); 262 nlohmann::json::json_pointer p("/ids/" + Strings::ToLower(Translate::ToString(service)));
249 263
250 if (json.contains(p) && json[p].is_string()) 264 if (json.contains(p) && json[p].is_string())
251 anime.SetServiceId(service, json[p].get<std::string>()); 265 anime.SetServiceId(service, json[p].get<std::string>());
252 } 266 }
253 267
254 for (const auto& lang : TitleLanguages) { 268 for (const auto &lang : TitleLanguages) {
255 nlohmann::json::json_pointer p("/title/" + Strings::ToLower(Translate::ToString(lang))); 269 nlohmann::json::json_pointer p("/title/" + Strings::ToLower(Translate::ToString(lang)));
256 270
257 if (json.contains(p) && json[p].is_string()) 271 if (json.contains(p) && json[p].is_string())
258 anime.SetTitle(lang, json[p].get<std::string>()); 272 anime.SetTitle(lang, json[p].get<std::string>());
259 } 273 }
276 ParseAnimeUserInfoJSON(json.at("/list_data"_json_pointer), anime); 290 ParseAnimeUserInfoJSON(json.at("/list_data"_json_pointer), anime);
277 291
278 return true; 292 return true;
279 } 293 }
280 294
281 bool Database::ParseDatabaseJSON(const nlohmann::json& json) { 295 bool Database::ParseDatabaseJSON(const nlohmann::json &json)
282 for (const auto& anime_json : json) 296 {
297 for (const auto &anime_json : json)
283 ParseAnimeInfoJSON(anime_json, *this); 298 ParseAnimeInfoJSON(anime_json, *this);
284 299
285 return true; 300 return true;
286 } 301 }
287 302
288 bool Database::LoadDatabaseFromDisk() { 303 bool Database::LoadDatabaseFromDisk()
304 {
289 std::filesystem::path db_path = Filesystem::GetAnimeDBPath(); 305 std::filesystem::path db_path = Filesystem::GetAnimeDBPath();
290 Filesystem::CreateDirectories(db_path); 306 Filesystem::CreateDirectories(db_path);
291 307
292 std::ifstream db_file(db_path); 308 std::ifstream db_file(db_path);
293 if (!db_file) 309 if (!db_file)
295 311
296 /* When parsing, do NOT throw exceptions */ 312 /* When parsing, do NOT throw exceptions */
297 nlohmann::json json; 313 nlohmann::json json;
298 try { 314 try {
299 json = json.parse(db_file); 315 json = json.parse(db_file);
300 } catch (std::exception const& ex) { 316 } catch (std::exception const &ex) {
301 std::cerr << "[anime/db] Failed to parse JSON! " << ex.what() << std::endl; 317 std::cerr << "[anime/db] Failed to parse JSON! " << ex.what() << std::endl;
302 return false; 318 return false;
303 } 319 }
304 320
305 if (!ParseDatabaseJSON(json)) /* How */ 321 if (!ParseDatabaseJSON(json)) /* How */
306 return false; 322 return false;
307 323
308 return true; 324 return true;
309 } 325 }
310 326
311 int Database::GetUnusedId() const { 327 int Database::GetUnusedId() const
328 {
312 std::uniform_int_distribution<int> distrib(1, INT_MAX); 329 std::uniform_int_distribution<int> distrib(1, INT_MAX);
313 int res; 330 int res;
314 331
315 do { 332 do {
316 res = distrib(session.gen); 333 res = distrib(session.gen);
317 } while (items.count(res) && !res); 334 } while (items.count(res) && !res);
318 335
319 return res; 336 return res;
320 } 337 }
321 338
322 int Database::LookupServiceId(Service service, const std::string& id_to_find) const { 339 int Database::LookupServiceId(Service service, const std::string &id_to_find) const
323 for (const auto& [id, anime] : items) { 340 {
341 for (const auto &[id, anime] : items) {
324 std::optional<std::string> service_id = anime.GetServiceId(service); 342 std::optional<std::string> service_id = anime.GetServiceId(service);
325 if (!service_id) 343 if (!service_id)
326 continue; 344 continue;
327 345
328 if (service_id == id_to_find) 346 if (service_id == id_to_find)
330 } 348 }
331 349
332 return 0; 350 return 0;
333 } 351 }
334 352
335 int Database::LookupServiceIdOrUnused(Service service, const std::string& id_to_find) const { 353 int Database::LookupServiceIdOrUnused(Service service, const std::string &id_to_find) const
354 {
336 int id = LookupServiceId(service, id_to_find); 355 int id = LookupServiceId(service, id_to_find);
337 if (id) 356 if (id)
338 return id; 357 return id;
339 358
340 return GetUnusedId(); 359 return GetUnusedId();
341 } 360 }
342 361
343 void Database::RemoveAllUserData() { 362 void Database::RemoveAllUserData()
344 for (auto& [id, anime] : items) { 363 {
364 for (auto &[id, anime] : items) {
345 if (anime.IsInUserList()) 365 if (anime.IsInUserList())
346 anime.RemoveFromUserList(); 366 anime.RemoveFromUserList();
347 } 367 }
348 } 368 }
349 369
350 std::vector<int> Database::GetAllAnimeForSeason(Season season) { 370 std::vector<int> Database::GetAllAnimeForSeason(Season season)
371 {
351 std::vector<int> res; 372 std::vector<int> res;
352 373
353 for (const auto& [id, anime] : items) { 374 for (const auto &[id, anime] : items) {
354 if (anime.GetSeason() == season) 375 if (anime.GetSeason() == season)
355 res.push_back(id); 376 res.push_back(id);
356 } 377 }
357 378
358 return res; 379 return res;