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