Mercurial > minori
view src/core/anime_db.cc @ 229:adc20fa321c1
theme: force Fusion style on platforms other than Win32 or OS X
I was reluctant to do this, but most of the other styles just
look like pure shite regardless of whether I force a stylesheet
on them or not. KDE's style is actually hilariously bad paired
with my stylesheet, so I've decided to also make the stylesheet
Windows-specific as well, because that's really the only platform
where it makes sense in the first place.
| author | Paper <paper@paper.us.eu.org> |
|---|---|
| date | Wed, 10 Jan 2024 21:23:57 -0500 |
| parents | bc1ae1810855 |
| children | 4d461ef7d424 |
line wrap: on
line source
#include "core/anime_db.h" #include "core/anime.h" #include "core/strings.h" #include "core/json.h" #include "core/filesystem.h" #include "gui/translate/anime.h" #include "gui/translate/anilist.h" #include <QDate> #include <fstream> #include <iostream> #include <exception> namespace Anime { size_t Database::GetTotalAnimeAmount() { size_t total = 0; for (const auto& [id, anime] : items) if (anime.IsInUserList()) total++; return total; } size_t Database::GetListsAnimeAmount(ListStatus status) { if (status == ListStatus::NOT_IN_LIST) return 0; size_t total = 0; for (const auto& [id, anime] : items) if (anime.IsInUserList() && anime.GetUserStatus() == status) total++; return total; } size_t Database::GetTotalEpisodeAmount() { size_t total = 0; for (const auto& [id, anime] : items) if (anime.IsInUserList()) total += anime.GetUserRewatchedTimes() * anime.GetEpisodes() + anime.GetUserProgress(); return total; } /* Returns the total watched amount in minutes. */ size_t Database::GetTotalWatchedAmount() { size_t total = 0; for (const auto& [id, anime] : items) if (anime.IsInUserList()) total += anime.GetDuration() * anime.GetUserProgress() + anime.GetEpisodes() * anime.GetDuration() * anime.GetUserRewatchedTimes(); return total; } /* Returns the total planned amount in minutes. Note that we should probably limit progress to the amount of episodes, as AniList will let you set episode counts up to 32768. But that should rather be handled elsewhere. */ size_t Database::GetTotalPlannedAmount() { size_t total = 0; for (const auto& [id, anime] : items) if (anime.IsInUserList()) total += anime.GetDuration() * (anime.GetEpisodes() - anime.GetUserProgress()); return total; } /* In Taiga this is called the mean, but "average" is what's primarily used in conversation, at least in the U.S. */ double Database::GetAverageScore() { double avg = 0; size_t amt = 0; for (const auto& [id, anime] : items) { if (anime.IsInUserList() && anime.GetUserScore()) { avg += anime.GetUserScore(); amt++; } } return avg / amt; } double Database::GetScoreDeviation() { double squares_sum = 0, avg = GetAverageScore(); size_t amt = 0; for (const auto& [id, anime] : items) { if (anime.IsInUserList() && anime.GetUserScore()) { squares_sum += std::pow(static_cast<double>(anime.GetUserScore()) - avg, 2); amt++; } } return (amt > 0) ? std::sqrt(squares_sum / amt) : 0; } template<typename T, typename U> static T get_lowest_in_map(const std::unordered_map<T, U>& map) { if (map.size() <= 0) return 0; T id = 0; U ret = std::numeric_limits<U>::max(); for (const auto& t : map) { if (t.second < ret) { ret = t.second; id = t.first; } } return id; } /* * This fairly basic algorithm is only in effect because * there are some special cases, e.g. Another and Re:ZERO, where * we get the wrong match, so we have to create Advanced Techniques * to solve this * * This algorithm: * 1. searches each anime item for a match to the preferred title * AND all synonyms and marks those matches with * `synonym.length() - (synonym.find(needle) + needle.length());` * which should never be less than zero and will be zero if, and only if * the titles match exactly. * 2. returns the id of the match that is the lowest, which will most * definitely match anything that exactly matches the title of the * filename */ int Database::GetAnimeFromTitle(const std::string& title) { if (title.empty()) return 0; std::unordered_map<int, size_t> map; static const auto process_title = [&map](const Anime& anime, const std::string& title, const std::string& needle) -> bool { size_t ret = title.find(needle); if (ret == std::string::npos) return false; map[anime.GetId()] = title.length() - (ret + needle.length()); return true; }; for (const auto& [id, anime] : items) { if (process_title(anime, anime.GetUserPreferredTitle(), title)) continue; for (const auto& synonym : anime.GetTitleSynonyms()) if (process_title(anime, synonym, title)) continue; } return get_lowest_in_map(map); } static bool GetListDataAsJSON(const Anime& anime, nlohmann::json& json) { if (!anime.IsInUserList()) return false; // clang-format off json = { {"status", Translate::ToString(anime.GetUserStatus())}, {"progress", anime.GetUserProgress()}, {"score", anime.GetUserScore()}, {"started", anime.GetUserDateStarted().GetAsAniListJson()}, {"completed", anime.GetUserDateCompleted().GetAsAniListJson()}, {"private", anime.GetUserIsPrivate()}, {"rewatched_times", anime.GetUserRewatchedTimes()}, {"rewatching", anime.GetUserIsRewatching()}, {"updated", anime.GetUserTimeUpdated()}, {"notes", anime.GetUserNotes()} }; // clang-format on return true; } static bool GetAnimeAsJSON(const Anime& anime, nlohmann::json& json) { // clang-format off json = { {"id", anime.GetId()}, {"title", { {"native", anime.GetNativeTitle()}, {"romaji", anime.GetRomajiTitle()}, {"english", anime.GetEnglishTitle()} }}, {"synonyms", anime.GetTitleSynonyms()}, {"episodes", anime.GetEpisodes()}, {"airing_status", Translate::ToString(anime.GetAiringStatus())}, {"air_date", anime.GetAirDate().GetAsAniListJson()}, {"genres", anime.GetGenres()}, {"producers", anime.GetProducers()}, {"format", Translate::ToString(anime.GetFormat())}, {"season", Translate::ToString(anime.GetSeason())}, {"audience_score", anime.GetAudienceScore()}, {"synopsis", anime.GetSynopsis()}, {"duration", anime.GetDuration()}, {"poster_url", anime.GetPosterUrl()} }; // clang-format on nlohmann::json user; if (GetListDataAsJSON(anime, user)) json.push_back({"list_data", user}); return true; } bool Database::GetDatabaseAsJSON(nlohmann::json& json) { for (const auto& [id, anime] : items) { nlohmann::json anime_json = {}; GetAnimeAsJSON(anime, anime_json); json.push_back(anime_json); } return true; } bool Database::SaveDatabaseToDisk() { std::filesystem::path db_path = Filesystem::GetAnimeDBPath(); Filesystem::CreateDirectories(db_path); std::ofstream db_file(db_path); if (!db_file) return false; nlohmann::json json = {}; if (!GetDatabaseAsJSON(json)) return false; db_file << std::setw(4) << json << std::endl; return true; } static bool ParseAnimeUserInfoJSON(const nlohmann::json& json, Anime& anime) { if (!anime.IsInUserList()) anime.AddToUserList(); anime.SetUserStatus(Translate::ToListStatus(JSON::GetString<std::string>(json, "/status"_json_pointer, ""))); anime.SetUserProgress(JSON::GetNumber(json, "/progress"_json_pointer, 0)); anime.SetUserScore(JSON::GetNumber(json, "/score"_json_pointer, 0)); anime.SetUserDateStarted(Date(JSON::GetValue(json, "/started"_json_pointer))); anime.SetUserDateCompleted(Date(JSON::GetValue(json, "/completed"_json_pointer))); anime.SetUserIsPrivate(JSON::GetBoolean(json, "/private"_json_pointer, false)); anime.SetUserRewatchedTimes(JSON::GetNumber(json, "/rewatched_times"_json_pointer, 0)); anime.SetUserIsRewatching(JSON::GetBoolean(json, "/rewatching"_json_pointer, false)); anime.SetUserTimeUpdated(JSON::GetNumber(json, "/updated"_json_pointer, 0)); anime.SetUserNotes(JSON::GetString<std::string>(json, "/notes"_json_pointer, "")); return true; } static bool ParseAnimeInfoJSON(const nlohmann::json& json, Database& database) { int id = JSON::GetNumber(json, "/id"_json_pointer, 0); if (!id) return false; Anime& anime = database.items[id]; anime.SetId(id); anime.SetNativeTitle(JSON::GetString<std::string>(json, "/title/native"_json_pointer, "")); anime.SetRomajiTitle(JSON::GetString<std::string>(json, "/title/romaji"_json_pointer, "")); anime.SetEnglishTitle(JSON::GetString<std::string>(json, "/title/english"_json_pointer, "")); anime.SetTitleSynonyms(JSON::GetArray<std::vector<std::string>>(json, "/synonyms"_json_pointer, {})); anime.SetEpisodes(JSON::GetNumber(json, "/episodes"_json_pointer, 0)); anime.SetAiringStatus(Translate::ToSeriesStatus(JSON::GetString<std::string>(json, "/airing_status"_json_pointer, ""))); anime.SetAirDate(Date(JSON::GetValue(json, "/air_date"_json_pointer))); anime.SetGenres(JSON::GetArray<std::vector<std::string>>(json, "/genres"_json_pointer, {})); anime.SetProducers(JSON::GetArray<std::vector<std::string>>(json, "/producers"_json_pointer, {})); anime.SetFormat(Translate::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, ""))); anime.SetSeason(Translate::ToSeriesSeason(JSON::GetString<std::string>(json, "/season"_json_pointer, ""))); anime.SetAudienceScore(JSON::GetNumber(json, "/audience_score"_json_pointer, 0)); anime.SetSynopsis(JSON::GetString<std::string>(json, "/synopsis"_json_pointer, "")); anime.SetDuration(JSON::GetNumber(json, "/duration"_json_pointer, 0)); anime.SetPosterUrl(JSON::GetString<std::string>(json, "/poster_url"_json_pointer, "")); if (json.contains("/list_data"_json_pointer) && json.at("/list_data"_json_pointer).is_object()) ParseAnimeUserInfoJSON(json.at("/list_data"_json_pointer), anime); return true; } bool Database::ParseDatabaseJSON(const nlohmann::json& json) { for (const auto& anime_json : json) ParseAnimeInfoJSON(anime_json, *this); return true; } bool Database::LoadDatabaseFromDisk() { std::filesystem::path db_path = Filesystem::GetAnimeDBPath(); Filesystem::CreateDirectories(db_path); std::ifstream db_file(db_path); if (!db_file) return false; /* When parsing, do NOT throw exceptions */ nlohmann::json json; try { json = json.parse(db_file); } catch (std::exception const& ex) { std::cerr << "[anime/db] Failed to parse JSON! " << ex.what() << std::endl; return false; } if (!ParseDatabaseJSON(json)) /* How */ return false; return true; } Database db; } // namespace Anime
