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 |
