changeset 174:f88eda79c60a

anime/db: add some more json functionality, still doesn't compile :/
author Paper <mrpapersonic@gmail.com>
date Wed, 29 Nov 2023 13:53:56 -0500
parents de0a8d2f28b3
children 9b10175be389
files include/core/anime_db.h include/core/date.h include/core/json.h include/gui/translate/anime.h src/core/anime_db.cc src/core/date.cc src/core/json.cc src/gui/translate/anime.cc src/gui/window.cc
diffstat 9 files changed, 301 insertions(+), 77 deletions(-) [+]
line wrap: on
line diff
--- a/include/core/anime_db.h	Tue Nov 28 13:53:54 2023 -0500
+++ b/include/core/anime_db.h	Wed Nov 29 13:53:56 2023 -0500
@@ -21,6 +21,13 @@
 		int GetAnimeFromTitle(const std::string& title);
 
 		bool GetDatabaseAsJSON(nlohmann::json& json);
+		bool SaveDatabaseToDisk();
+
+		bool ParseDatabaseJSON(const nlohmann::json& json);
+		bool LoadDatabaseFromFile();
+
+	protected:
+		bool ParseAnimeInfoJSON(const nlohmann::json& json);
 };
 
 extern Database db;
--- a/include/core/date.h	Tue Nov 28 13:53:54 2023 -0500
+++ b/include/core/date.h	Wed Nov 29 13:53:56 2023 -0500
@@ -1,7 +1,7 @@
 #ifndef __core__date_h
 #define __core__date_h
 
-#include "json.h"
+#include "core/json.h"
 #include <QDate>
 #include <cstdint>
 
@@ -11,6 +11,7 @@
 		Date(unsigned int y);
 		Date(unsigned int y, unsigned int m, unsigned int d);
 		Date(const QDate& date);
+		Date(const nlohmann::json& json);
 		bool IsValid() const;
 		void SetYear(unsigned int y);
 		void SetMonth(unsigned int m);
@@ -29,8 +30,7 @@
 		bool operator>=(const Date& other) const;
 
 	private:
-		/* note: it might be worth it to change these all to int, as
-		   large bit precisions aren't exactly useful here... */
+		/* this implementation sucks and we should really use a struct instead */
 		std::shared_ptr<unsigned int> year;
 		std::shared_ptr<unsigned int> month;
 		std::shared_ptr<unsigned int> day;
--- a/include/core/json.h	Tue Nov 28 13:53:54 2023 -0500
+++ b/include/core/json.h	Wed Nov 29 13:53:56 2023 -0500
@@ -5,10 +5,32 @@
 
 namespace JSON {
 
-std::string GetString(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, std::string def = "");
-int GetInt(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, int def = 0);
-bool GetBoolean(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, bool def = false);
-double GetDouble(nlohmann::json const& json, nlohmann::json::json_pointer const& ptr, double def = 0);
+template<typename T = std::string>
+T GetString(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, T def) {
+	if (json.contains(ptr) && json[ptr].is_string())
+		return json[ptr].get<T>();
+	else
+		return def;
+}
+
+template<typename T = int>
+T GetNumber(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, T def = 0) {
+	if (json.contains(ptr) && json[ptr].is_number())
+		return json[ptr].get<T>();
+	else
+		return def;
+}
+
+template<typename T = std::vector<std::string>>
+T GetArray(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, T def = 0) {
+	if (json.contains(ptr) && json[ptr].is_array())
+		return json[ptr].get<T>();
+	else
+		return def;
+}
+
+nlohmann::json GetValue(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr);
+bool GetBoolean(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, bool def = false);
 
 } // namespace JSON
 
--- a/include/gui/translate/anime.h	Tue Nov 28 13:53:54 2023 -0500
+++ b/include/gui/translate/anime.h	Wed Nov 29 13:53:56 2023 -0500
@@ -7,8 +7,20 @@
 std::string ToString(const Anime::SeriesSeason season);
 std::string ToString(const Anime::SeriesStatus status);
 std::string ToString(const Anime::Services service);
+std::string ToString(const Anime::TitleLanguage language);
+
+std::string ToLocalString(const Anime::ListStatus status);
+std::string ToLocalString(const Anime::SeriesFormat format);
+std::string ToLocalString(const Anime::SeriesSeason season);
+std::string ToLocalString(const Anime::SeriesStatus status);
+std::string ToLocalString(const Anime::Services service);
+std::string ToLocalString(const Anime::TitleLanguage language);
+
+Anime::ListStatus ToListStatus(const std::string& str);
+Anime::SeriesFormat ToSeriesFormat(const std::string& str);
+Anime::SeriesSeason ToSeriesSeason(const std::string& str);
+Anime::SeriesStatus ToSeriesStatus(const std::string& str);
 Anime::Services ToService(const std::string& str);
-std::string ToString(const Anime::TitleLanguage language);
 Anime::TitleLanguage ToLanguage(const std::string& str);
 
 } // namespace Translate
--- a/src/core/anime_db.cc	Tue Nov 28 13:53:54 2023 -0500
+++ b/src/core/anime_db.cc	Wed Nov 29 13:53:56 2023 -0500
@@ -2,8 +2,12 @@
 #include "core/anime.h"
 #include "core/strings.h"
 #include "core/json.h"
+#include "core/filesystem.h"
 
-#include <QDebug>
+#include "gui/translate/anime.h"
+#include "gui/translate/anilist.h"
+
+#include <fstream>
 
 namespace Anime {
 
@@ -158,14 +162,16 @@
 		{"status", Translate::ToString(anime.GetUserStatus())},
 		{"progress", anime.GetUserProgress()},
 		{"score", anime.GetUserScore()},
-		//{"started", anime.GetUserDateStarted()},
-		//{"completed", anime.GetUserDateCompleted()},
+		{"started", anime.GetUserDateStarted().GetAsAniListJson()},
+		{"completed", anime.GetUserDateCompleted().GetAsAniListJson()},
 		{"private", anime.GetUserIsPrivate()},
 		{"rewatched_times", anime.GetUserRewatchedTimes()},
 		{"rewatching", anime.GetUserIsRewatching()},
 		{"updated", anime.GetUserTimeUpdated()},
 		{"notes", anime.GetUserNotes()}
 	};
+
+	return true;
 }
 
 static bool GetAnimeAsJSON(const Anime& anime, nlohmann::json& json) {
@@ -178,17 +184,16 @@
 		}},
 		{"synonyms", anime.GetTitleSynonyms()},
 		{"episodes", anime.GetEpisodes()},
-		{"airing_status", anime.GetAiringStatus()},
-		{"air_date", anime.GetAirDate()},
+		{"airing_status", Translate::ToString(anime.GetAiringStatus())},
+		{"air_date", anime.GetAirDate().GetAsAniListJson()},
 		{"genres", anime.GetGenres()},
 		{"producers", anime.GetProducers()},
-		{"format", anime.GetFormat()},
-		{"season", anime.GetSeason()},
+		{"format", Translate::ToString(anime.GetFormat())},
+		{"season", Translate::ToString(anime.GetSeason())},
 		{"audience_score", anime.GetAudienceScore()},
 		{"synopsis", anime.GetSynopsis()},
 		{"duration", anime.GetDuration()},
-		{"poster_url", anime.GetPosterUrl()},
-		{"service_url", anime.GetServiceUrl()}
+		{"poster_url", anime.GetPosterUrl()}
 	};
 
 	nlohmann::json user;
@@ -200,10 +205,101 @@
 
 bool Database::GetDatabaseAsJSON(nlohmann::json& json) {
 	for (const auto& [id, anime] : items) {
-		nlohmann::json anime_json;
+		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.SetUserStatus(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.SetUserDateStarted(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, ""));
+	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;
+}
+
+bool Database::ParseAnimeInfoJSON(const nlohmann::json& json) {
+	int id = JSON::GetNumber(json, "/id"_json_pointer, 0);
+	if (!id)
+		return false;
+
+	Anime& anime = 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::AniList::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::AniList::ToSeriesFormat(JSON::GetString<std::string>(json, "/format"_json_pointer, "")));
+	anime.SetSeason(Translate::AniList::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.at("/list_data").is_dictionary())
+		ParseAnimeUserInfoJSON(json, anime);
+
+	return true;
+}
+
+bool Database::ParseDatabaseJSON(const nlohmann::json& json) {
+	for (const auto& anime_json : json)
+		ParseAnimeInfoJSON(anime_json);
+
+	return true;
+}
+
+bool Database::LoadDatabaseFromFile() {
+	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 = json.parse(db_file, nullptr, false);
+	if (json.is_discarded())
+		return false; /* Give up */
+
+	if (!ParseDatabaseJSON(json)) /* How */
+		return false;
+
 	return true;
 }
 
--- a/src/core/date.cc	Tue Nov 28 13:53:54 2023 -0500
+++ b/src/core/date.cc	Wed Nov 29 13:53:56 2023 -0500
@@ -29,6 +29,15 @@
 	SetDay(date.day());
 }
 
+Date::Date(const nlohmann::json& json) {
+	if (json.contains("year") && json.at("year").is_number())
+		SetYear(json.at("year").get<unsigned int>());
+	if (json.contains("month") && json.at("month").is_number())
+		SetMonth(json.at("month").get<unsigned int>());
+	if (json.contains("day") && json.at("day").is_number())
+		SetDay(json.at("day").get<unsigned int>());
+}
+
 void Date::VoidYear() {
 	year.reset();
 }
--- a/src/core/json.cc	Tue Nov 28 13:53:54 2023 -0500
+++ b/src/core/json.cc	Wed Nov 29 13:53:56 2023 -0500
@@ -2,18 +2,11 @@
 
 namespace JSON {
 
-std::string GetString(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, std::string def) {
-	if (json.contains(ptr) && json[ptr].is_string())
-		return json[ptr].get<std::string>();
+nlohmann::json GetValue(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr) {
+	if (json.contains(ptr))
+		return json.at(ptr);
 	else
-		return def;
-}
-
-int GetInt(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, int def) {
-	if (json.contains(ptr) && json[ptr].is_number())
-		return json[ptr].get<int>();
-	else
-		return def;
+		return nlohmann::json();
 }
 
 bool GetBoolean(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, bool def) {
@@ -23,11 +16,4 @@
 		return def;
 }
 
-double GetDouble(const nlohmann::json& json, const nlohmann::json::json_pointer& ptr, double def) {
-	if (json.contains(ptr) && json[ptr].is_number())
-		return json[ptr].get<double>();
-	else
-		return def;
-}
-
 } // namespace JSON
--- a/src/gui/translate/anime.cc	Tue Nov 28 13:53:54 2023 -0500
+++ b/src/gui/translate/anime.cc	Wed Nov 29 13:53:56 2023 -0500
@@ -7,64 +7,96 @@
 
 std::string ToString(const Anime::ListStatus status) {
 	switch (status) {
-		case Anime::ListStatus::CURRENT: return Strings::ToUtf8String(QCoreApplication::tr("Currently watching"));
-		case Anime::ListStatus::PLANNING: return Strings::ToUtf8String(QCoreApplication::tr("Plan to watch"));
-		case Anime::ListStatus::COMPLETED: return Strings::ToUtf8String(QCoreApplication::tr("Completed"));
-		case Anime::ListStatus::DROPPED: return Strings::ToUtf8String(QCoreApplication::tr("Dropped"));
-		case Anime::ListStatus::PAUSED: return Strings::ToUtf8String(QCoreApplication::tr("On hold"));
+		case Anime::ListStatus::CURRENT: return "Currently watching";
+		case Anime::ListStatus::PLANNING: return "Plan to watch";
+		case Anime::ListStatus::COMPLETED: return "Completed";
+		case Anime::ListStatus::DROPPED: return "Dropped";
+		case Anime::ListStatus::PAUSED: return "On hold";
 		default:
-		case Anime::ListStatus::NOT_IN_LIST: return Strings::ToUtf8String(QCoreApplication::tr("Not in list"));
+		case Anime::ListStatus::NOT_IN_LIST: return "Not in list";
 	}
 }
 
 std::string ToString(const Anime::SeriesFormat format) {
 	switch (format) {
-		case Anime::SeriesFormat::TV: return Strings::ToUtf8String(QCoreApplication::tr("TV"));
-		case Anime::SeriesFormat::TV_SHORT: return Strings::ToUtf8String(QCoreApplication::tr("TV short"));
-		case Anime::SeriesFormat::OVA: return Strings::ToUtf8String(QCoreApplication::tr("OVA"));
-		case Anime::SeriesFormat::MOVIE: return Strings::ToUtf8String(QCoreApplication::tr("Movie"));
-		case Anime::SeriesFormat::SPECIAL: return Strings::ToUtf8String(QCoreApplication::tr("Special"));
-		case Anime::SeriesFormat::ONA: return Strings::ToUtf8String(QCoreApplication::tr("ONA"));
-		case Anime::SeriesFormat::MUSIC: return Strings::ToUtf8String(QCoreApplication::tr("Music"));
+		case Anime::SeriesFormat::TV: return "TV";
+		case Anime::SeriesFormat::TV_SHORT: return "TV short";
+		case Anime::SeriesFormat::OVA: return "OVA";
+		case Anime::SeriesFormat::MOVIE: return "Movie";
+		case Anime::SeriesFormat::SPECIAL: return "Special";
+		case Anime::SeriesFormat::ONA: return "ONA";
+		case Anime::SeriesFormat::MUSIC: return "Music";
 		default:
-		case Anime::SeriesFormat::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+		case Anime::SeriesFormat::UNKNOWN: return "Unknown";
 	}
 }
 
 std::string ToString(const Anime::SeriesSeason season) {
 	switch (season) {
-		case Anime::SeriesSeason::WINTER: return Strings::ToUtf8String(QCoreApplication::tr("Winter"));
-		case Anime::SeriesSeason::SUMMER: return Strings::ToUtf8String(QCoreApplication::tr("Summer"));
-		case Anime::SeriesSeason::FALL: return Strings::ToUtf8String(QCoreApplication::tr("Fall"));
-		case Anime::SeriesSeason::SPRING: return Strings::ToUtf8String(QCoreApplication::tr("Spring"));
+		case Anime::SeriesSeason::WINTER: return "Winter";
+		case Anime::SeriesSeason::SUMMER: return "Summer";
+		case Anime::SeriesSeason::FALL: return "Fall";
+		case Anime::SeriesSeason::SPRING: return "Spring";
 		default:
-		case Anime::SeriesSeason::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+		case Anime::SeriesSeason::UNKNOWN: return "Unknown";
 	}
 }
 
 std::string ToString(const Anime::SeriesStatus status) {
 	switch (status) {
-		case Anime::SeriesStatus::RELEASING: return Strings::ToUtf8String(QCoreApplication::tr("Currently airing"));
-		case Anime::SeriesStatus::FINISHED: return Strings::ToUtf8String(QCoreApplication::tr("Finished airing"));
-		case Anime::SeriesStatus::NOT_YET_RELEASED: return Strings::ToUtf8String(QCoreApplication::tr("Not yet aired"));
-		case Anime::SeriesStatus::CANCELLED: return Strings::ToUtf8String(QCoreApplication::tr("Cancelled"));
-		case Anime::SeriesStatus::HIATUS: return Strings::ToUtf8String(QCoreApplication::tr("On hiatus"));
+		case Anime::SeriesStatus::RELEASING: return "Currently airing";
+		case Anime::SeriesStatus::FINISHED: return "Finished airing";
+		case Anime::SeriesStatus::NOT_YET_RELEASED: return "Not yet aired";
+		case Anime::SeriesStatus::CANCELLED: return "Cancelled";
+		case Anime::SeriesStatus::HIATUS: return "On hiatus";
 		default:
-		case Anime::SeriesStatus::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+		case Anime::SeriesStatus::UNKNOWN: "Unknown";
 	}
 }
 
-std::string ToString(const Anime::Services service) {
-	switch (service) {
-		case Anime::Services::ANILIST: return Strings::ToUtf8String(QCoreApplication::tr("AniList"));
-		default:
-		case Anime::Services::NONE:    return Strings::ToUtf8String(QCoreApplication::tr("None"));
-	}
+Anime::SeriesStatus ToListStatus(const std::string& str) {
+	const std::unordered_map<std::string, Anime::SeriesStatus> map = {
+	    {"Currently watching", Anime::SeriesSeason::CURRENT},
+	    {"Plan to watch", Anime::SeriesSeason::PLANNING},
+	    {"Completed", Anime::SeriesSeason::COMPLETED},
+	    {"Dropped", Anime::SeriesSeason::DROPPED},
+	    {"On hold", Anime::SeriesSeason::PAUSED}
+	};
+
+	if (map.find(str) == map.end())
+		return Anime::SeriesStatus::NOT_IN_LIST;
+	return map.at(str);
+}
+
+Anime::SeriesStatus ToSeriesStatus(const std::string& str) {
+	const std::unordered_map<std::string, Anime::SeriesStatus> map = {
+	    {"Currently airing", Anime::SeriesSeason::RELEASING},
+	    {"Finished airing", Anime::SeriesSeason::FINISHED},
+	    {"Not yet aired", Anime::SeriesSeason::NOT_YET_RELEASED},
+	    {"Cancelled", Anime::SeriesSeason::CANCELLED},
+	    {"On hiatus", Anime::SeriesSeason::HIATUS}
+	};
+
+	if (map.find(str) == map.end())
+		return Anime::SeriesStatus::UNKNOWN;
+	return map.at(str);
+}
+
+Anime::SeriesSeason ToSeriesSeason(const std::string& str) {
+	const std::unordered_map<std::string, Anime::SeriesSeason> map = {
+	    {"Winter", Anime::SeriesSeason::WINTER},
+	    {"Summer", Anime::SeriesSeason::SUMMER},
+	    {"Fall", Anime::SeriesSeason::FALL},
+	    {"Spring", Anime::SeriesSeason::SPRING}
+	};
+
+	if (map.find(str) == map.end())
+		return Anime::SeriesSeason::UNKNOWN;
+	return map.at(str);
 }
 
 Anime::Services ToService(const std::string& str) {
 	const std::unordered_map<std::string, Anime::Services> map = {
-	    {"None",    Anime::Services::NONE   },
 	    {"AniList", Anime::Services::ANILIST}
 	};
 
@@ -73,15 +105,6 @@
 	return map.at(str);
 }
 
-std::string ToString(const Anime::TitleLanguage language) {
-	switch (language) {
-		case Anime::TitleLanguage::NATIVE:  return Strings::ToUtf8String(QCoreApplication::tr("Native"));
-		case Anime::TitleLanguage::ENGLISH: return Strings::ToUtf8String(QCoreApplication::tr("English"));
-		default:
-		case Anime::TitleLanguage::ROMAJI:  return Strings::ToUtf8String(QCoreApplication::tr("Romaji"));
-	}
-}
-
 Anime::TitleLanguage ToLanguage(const std::string& str) {
 	const std::unordered_map<std::string, Anime::TitleLanguage> map = {
 	    {"Romaji", Anime::TitleLanguage::ROMAJI},
@@ -94,4 +117,72 @@
 	return map.at(str);
 }
 
+/* Localized versions of ToString() functions */
+
+std::string ToLocalString(const Anime::ListStatus status) {
+	switch (status) {
+		case Anime::ListStatus::CURRENT: return Strings::ToUtf8String(QCoreApplication::tr("Currently watching"));
+		case Anime::ListStatus::PLANNING: return Strings::ToUtf8String(QCoreApplication::tr("Plan to watch"));
+		case Anime::ListStatus::COMPLETED: return Strings::ToUtf8String(QCoreApplication::tr("Completed"));
+		case Anime::ListStatus::DROPPED: return Strings::ToUtf8String(QCoreApplication::tr("Dropped"));
+		case Anime::ListStatus::PAUSED: return Strings::ToUtf8String(QCoreApplication::tr("On hold"));
+		default:
+		case Anime::ListStatus::NOT_IN_LIST: return Strings::ToUtf8String(QCoreApplication::tr("Not in list"));
+	}
+}
+
+std::string ToLocalString(const Anime::SeriesFormat format) {
+	switch (format) {
+		case Anime::SeriesFormat::TV: return Strings::ToUtf8String(QCoreApplication::tr("TV"));
+		case Anime::SeriesFormat::TV_SHORT: return Strings::ToUtf8String(QCoreApplication::tr("TV short"));
+		case Anime::SeriesFormat::OVA: return Strings::ToUtf8String(QCoreApplication::tr("OVA"));
+		case Anime::SeriesFormat::MOVIE: return Strings::ToUtf8String(QCoreApplication::tr("Movie"));
+		case Anime::SeriesFormat::SPECIAL: return Strings::ToUtf8String(QCoreApplication::tr("Special"));
+		case Anime::SeriesFormat::ONA: return Strings::ToUtf8String(QCoreApplication::tr("ONA"));
+		case Anime::SeriesFormat::MUSIC: return Strings::ToUtf8String(QCoreApplication::tr("Music"));
+		default:
+		case Anime::SeriesFormat::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+	}
+}
+
+std::string ToLocalString(const Anime::SeriesSeason season) {
+	switch (season) {
+		case Anime::SeriesSeason::WINTER: return Strings::ToUtf8String(QCoreApplication::tr("Winter"));
+		case Anime::SeriesSeason::SUMMER: return Strings::ToUtf8String(QCoreApplication::tr("Summer"));
+		case Anime::SeriesSeason::FALL: return Strings::ToUtf8String(QCoreApplication::tr("Fall"));
+		case Anime::SeriesSeason::SPRING: return Strings::ToUtf8String(QCoreApplication::tr("Spring"));
+		default:
+		case Anime::SeriesSeason::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+	}
+}
+
+std::string ToLocalString(const Anime::SeriesStatus status) {
+	switch (status) {
+		case Anime::SeriesStatus::RELEASING: return Strings::ToUtf8String(QCoreApplication::tr("Currently airing"));
+		case Anime::SeriesStatus::FINISHED: return Strings::ToUtf8String(QCoreApplication::tr("Finished airing"));
+		case Anime::SeriesStatus::NOT_YET_RELEASED: return Strings::ToUtf8String(QCoreApplication::tr("Not yet aired"));
+		case Anime::SeriesStatus::CANCELLED: return Strings::ToUtf8String(QCoreApplication::tr("Cancelled"));
+		case Anime::SeriesStatus::HIATUS: return Strings::ToUtf8String(QCoreApplication::tr("On hiatus"));
+		default:
+		case Anime::SeriesStatus::UNKNOWN: return Strings::ToUtf8String(QCoreApplication::tr("Unknown"));
+	}
+}
+
+std::string ToLocalString(const Anime::Services service) {
+	switch (service) {
+		case Anime::Services::ANILIST: return Strings::ToUtf8String(QCoreApplication::tr("AniList"));
+		default:
+		case Anime::Services::NONE:    return Strings::ToUtf8String(QCoreApplication::tr("None"));
+	}
+}
+
+std::string ToLocalString(const Anime::TitleLanguage language) {
+	switch (language) {
+		case Anime::TitleLanguage::NATIVE:  return Strings::ToUtf8String(QCoreApplication::tr("Native"));
+		case Anime::TitleLanguage::ENGLISH: return Strings::ToUtf8String(QCoreApplication::tr("English"));
+		default:
+		case Anime::TitleLanguage::ROMAJI:  return Strings::ToUtf8String(QCoreApplication::tr("Romaji"));
+	}
+}
+
 } // namespace Translate
--- a/src/gui/window.cc	Tue Nov 28 13:53:54 2023 -0500
+++ b/src/gui/window.cc	Wed Nov 29 13:53:56 2023 -0500
@@ -474,6 +474,7 @@
 
 void MainWindow::closeEvent(QCloseEvent* event) {
 	session.config.Save();
+	Anime::db.SaveDatabaseToDisk();
 	event->accept();
 }