Mercurial > minori
view src/core/anime_db.cc @ 337:a7d4e5107531
dep/animone: REFACTOR ALL THE THINGS
1: animone now has its own syntax divergent from anisthesia,
making different platforms actually have their own sections
2: process names in animone are now called `comm' (this will
probably break things). this is what its called in bsd/linux
so I'm just going to use it everywhere
3: the X11 code now checks for the existence of a UTF-8 window title
and passes it if available
4: ANYTHING THATS NOT LINUX IS 100% UNTESTED AND CAN AND WILL BREAK!
I still actually need to test the bsd code. to be honest I'm probably
going to move all of the bsds into separate files because they're
all essentially different operating systems at this point
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Wed, 19 Jun 2024 12:51:15 -0400 |
parents | b5d6c27c308f |
children |
line wrap: on
line source
#include "core/anime_db.h" #include "core/anime.h" #include "core/filesystem.h" #include "core/json.h" #include "core/session.h" #include "core/strings.h" #include "gui/translate/anilist.h" #include "gui/translate/anime.h" #include <QDate> #include <fstream> #include <exception> #include <cstdlib> #include <iostream> #include <random> namespace Anime { size_t Database::GetTotalAnimeAmount() const { size_t total = 0; for (const auto& [id, anime] : items) if (anime.IsInUserList()) total++; return total; } size_t Database::GetListsAnimeAmount(ListStatus status) const { if (status == ListStatus::NotInList) 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() const { 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() const { 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() const { 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() const { 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() const { 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; } int Database::LookupAnimeTitle(const std::string& title) const { if (title.empty()) return 0; std::string title_n(title); Strings::NormalizeAnimeTitle(title_n); for (const auto& [id, anime] : items) { std::vector<std::string> synonyms(anime.GetTitleSynonyms()); synonyms.push_back(anime.GetUserPreferredTitle()); for (auto& synonym : synonyms) { Strings::NormalizeAnimeTitle(synonym); if (synonym == title_n) return id; } } return 0; } 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()}, {"synonyms", anime.GetTitleSynonyms()}, {"episodes", anime.GetEpisodes()}, {"airing_status", Translate::ToString(anime.GetAiringStatus())}, {"started_date", anime.GetStartedDate().GetAsAniListJson()}, {"completed_date", anime.GetCompletedDate().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 /* now for dynamically-filled stuff */ for (const auto& lang : TitleLanguages) { std::optional<std::string> title = anime.GetTitle(lang); if (title.has_value()) json["title"][Strings::ToLower(Translate::ToString(lang))] = title.value(); } for (const auto& service : Services) { std::optional<std::string> id = anime.GetServiceId(service); if (id.has_value()) json["ids"][Strings::ToLower(Translate::ToString(service))] = id.value(); } nlohmann::json user; if (GetListDataAsJSON(anime, user)) json.push_back({"list_data", user}); return true; } bool Database::GetDatabaseAsJSON(nlohmann::json& json) const { for (const auto& [id, anime] : items) { nlohmann::json anime_json = {}; GetAnimeAsJSON(anime, anime_json); json.push_back(anime_json); } return true; } bool Database::SaveDatabaseToDisk() const { 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); for (const auto& service : Services) { nlohmann::json::json_pointer p("/ids/" + Strings::ToLower(Translate::ToString(service))); if (json.contains(p) && json[p].is_string()) anime.SetServiceId(service, json[p].get<std::string>()); } for (const auto& lang : TitleLanguages) { nlohmann::json::json_pointer p("/title/" + Strings::ToLower(Translate::ToString(lang))); if (json.contains(p) && json[p].is_string()) anime.SetTitle(lang, json[p].get<std::string>()); } 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.SetStartedDate(Date(JSON::GetValue(json, "/started_date"_json_pointer))); anime.SetCompletedDate(Date(JSON::GetValue(json, "/completed_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; } int Database::GetUnusedId() const { std::uniform_int_distribution<int> distrib(1, INT_MAX); int res; do { res = distrib(session.gen); } while (items.count(res) && !res); return res; } int Database::LookupServiceId(Service service, const std::string& id_to_find) const { for (const auto& [id, anime] : items) { std::optional<std::string> service_id = anime.GetServiceId(service); if (!service_id) continue; if (service_id == id_to_find) return id; } return 0; } int Database::LookupServiceIdOrUnused(Service service, const std::string& id_to_find) const { int id = LookupServiceId(service, id_to_find); if (id) return id; return GetUnusedId(); } void Database::RemoveAllUserData() { for (auto& [id, anime] : items) { if (anime.IsInUserList()) anime.RemoveFromUserList(); } } std::vector<int> Database::GetAllAnimeForSeason(Season season) { std::vector<int> res; for (const auto& [id, anime] : items) { if (anime.GetSeason() == season) res.push_back(id); } return res; } Database db; } // namespace Anime