Mercurial > minori
diff 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 |
line wrap: on
line diff
--- a/src/core/anime_db.cc Sun Nov 19 19:13:28 2023 -0500 +++ b/src/core/anime_db.cc Tue Jan 02 06:05:06 2024 -0500 @@ -1,50 +1,63 @@ #include "core/anime_db.h" #include "core/anime.h" #include "core/strings.h" -#include <QDebug> +#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 { -int Database::GetTotalAnimeAmount() { - int total = 0; - for (const auto& a : items) { - if (a.second.IsInUserList()) +size_t Database::GetTotalAnimeAmount() { + size_t total = 0; + + for (const auto& [id, anime] : items) + if (anime.IsInUserList()) total++; - } + return total; } -int Database::GetListsAnimeAmount(ListStatus status) { +size_t Database::GetListsAnimeAmount(ListStatus status) { if (status == ListStatus::NOT_IN_LIST) return 0; - int total = 0; - for (const auto& a : items) { - if (a.second.IsInUserList() && a.second.GetUserStatus() == status) + + size_t total = 0; + + for (const auto& [id, anime] : items) + if (anime.IsInUserList() && anime.GetUserStatus() == status) total++; - } + return total; } -int Database::GetTotalEpisodeAmount() { - int total = 0; - for (const auto& a : items) { - if (a.second.IsInUserList()) { - total += a.second.GetUserRewatchedTimes() * a.second.GetEpisodes(); - total += a.second.GetUserProgress(); - } - } +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. */ -int Database::GetTotalWatchedAmount() { - int total = 0; - for (const auto& a : items) { - if (a.second.IsInUserList()) { - total += a.second.GetDuration() * a.second.GetUserProgress(); - total += a.second.GetEpisodes() * a.second.GetDuration() * a.second.GetUserRewatchedTimes(); - } - } +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; } @@ -53,24 +66,26 @@ amount of episodes, as AniList will let you set episode counts up to 32768. But that should rather be handled elsewhere. */ -int Database::GetTotalPlannedAmount() { - int total = 0; - for (const auto& a : items) { - if (a.second.IsInUserList()) - total += a.second.GetDuration() * (a.second.GetEpisodes() - a.second.GetUserProgress()); - } +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 a mean, but "average" is +/* 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; - int amt = 0; - for (const auto& a : items) { - if (a.second.IsInUserList() && a.second.GetUserScore()) { - avg += a.second.GetUserScore(); + size_t amt = 0; + + for (const auto& [id, anime] : items) { + if (anime.IsInUserList() && anime.GetUserScore()) { + avg += anime.GetUserScore(); amt++; } } @@ -79,21 +94,24 @@ double Database::GetScoreDeviation() { double squares_sum = 0, avg = GetAverageScore(); - int amt = 0; - for (const auto& a : items) { - if (a.second.IsInUserList() && a.second.GetUserScore()) { - squares_sum += std::pow(static_cast<double>(a.second.GetUserScore()) - avg, 2); + 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> +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; + + T id = 0; U ret = std::numeric_limits<U>::max(); for (const auto& t : map) { if (t.second < ret) { @@ -104,40 +122,204 @@ return id; } -/* This is really fugly but WHO CARES :P - - This sort of ""advanced"" 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, on a title that exactly matches, will be 0 - 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 */ +/* + * 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, long long> map; - for (const auto& a : items) { - long long ret = a.second.GetUserPreferredTitle().find(title); - if (ret != static_cast<long long>(std::string::npos)) { - map[a.second.GetId()] = a.second.GetUserPreferredTitle().length() - (ret + title.length()); + + 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 : a.second.GetTitleSynonyms()) { - ret = synonym.find(title); - if (ret != static_cast<long long>(std::string::npos)) { - map[a.second.GetId()] = synonym.length() - (ret + title.length()); + + 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 get_lowest_in_map(map); + + 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;