view src/core/anime_db.cc @ 258:862d0d8619f6

*: HUUUGE changes animia has been renamed to animone, so instead of thinking of a health condition, you think of a beautiful flower :) I've also edited some of the code for animone, but I have no idea if it even works or not because I don't have a mac or windows machine lying around. whoops! ... anyway, all of the changes divergent from Anisthesia are now licensed under BSD. it's possible that I could even rewrite most of the code to where I don't even have to keep the MIT license, but that's thinking too far into the future I've been slacking off on implementing the anime seasons page, mostly out of laziness. I think I'd have to create another db file specifically for the seasons anyway, this code is being pushed *primarily* because the hard drive it's on is failing! yay :)
author Paper <paper@paper.us.eu.org>
date Mon, 01 Apr 2024 02:43:44 -0400
parents d14f8e0e40c3
children dd211ff68b36
line wrap: on
line source

#include "core/anime_db.h"
#include "core/anime.h"
#include "core/filesystem.h"
#include "core/json.h"
#include "core/strings.h"

#include "gui/translate/anilist.h"
#include "gui/translate/anime.h"

#include <QDate>

#include <fstream>

#include <exception>
#include <iostream>

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;
}

/*
 * TODO: separate this from the anime DB,
 * provide *some* sort of normalization
 */
int Database::GetAnimeFromTitle(const std::string& title) {
	if (title.empty())
		return 0;

	for (const auto& [id, anime] : items) {
		if (anime.GetUserPreferredTitle() == title)
			return id;

		for (const auto& synonym : anime.GetTitleSynonyms())
			if (synonym == title)
				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()},
		{"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