changeset 118:39521c47c7a3

*: another huge megacommit, SORRY The torrents page works a lot better now Added the edit option to the anime list right click menu Vectorized currently playing files Available player and extensions are now loaded at runtime from files in (dotpath)/players.json and (dotpath)/extensions.json These paths are not permanent and will likely be moved to (dotpath)/recognition ... ... ...
author Paper <mrpapersonic@gmail.com>
date Tue, 07 Nov 2023 23:40:54 -0500
parents 2c1b6782e1d0
children 4eae379cb1ff
files CMakeLists.txt include/core/config.h include/core/filesystem.h include/core/session.h include/core/strings.h include/core/torrent.h include/gui/dialog/information.h include/gui/locale.h include/gui/pages/torrents.h include/gui/theme.h include/track/constants.h include/track/types.h rc/dark.qss src/core/config.cc src/core/filesystem.cc src/core/strings.cc src/gui/dialog/information.cc src/gui/dialog/settings.cc src/gui/locale.cc src/gui/pages/anime_list.cc src/gui/pages/torrents.cc src/gui/theme.cc src/gui/window.cc src/main.cc src/track/constants.cc src/track/media.cc src/track/types.cc
diffstat 27 files changed, 528 insertions(+), 92 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Tue Nov 07 16:06:17 2023 -0500
+++ b/CMakeLists.txt	Tue Nov 07 23:40:54 2023 -0500
@@ -105,6 +105,7 @@
 	# Tracking
 	src/track/constants.cc
 	src/track/media.cc
+	src/track/types.cc
 
 	# Qt resources
 	rc/icons.qrc
--- a/include/core/config.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/core/config.h	Tue Nov 07 23:40:54 2023 -0500
@@ -4,11 +4,13 @@
 #include "core/anime.h"
 #include "gui/theme.h"
 #include "gui/locale.h"
+#include <string>
+#include <vector>
 
 class Config {
 	public:
 		int Load();
-		int Save();
+		int Save() const;
 
 		Anime::Services service;
 		Theme::Theme theme;
--- a/include/core/filesystem.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/core/filesystem.h	Tue Nov 07 23:40:54 2023 -0500
@@ -22,9 +22,11 @@
 		std::string _path;
 };
 
-Path GetDotPath();     // %APPDATA%/minori, ~/Library/Application Support/minori, ~/.config/minori...
-Path GetConfigPath();  // (dotpath)/config.json
-Path GetAnimeDBPath(); // (dotpath)/anime/db.json
+Path GetDotPath();        // %APPDATA%/minori, ~/Library/Application Support/minori, ~/.config/minori...
+Path GetConfigPath();     // (dotpath)/config.json
+Path GetAnimeDBPath();    // (dotpath)/anime/db.json
+Path GetPlayersPath();    // (dotpath)/player.json
+Path GetExtensionsPath(); // (dotpath)/extensions.json
 
 } // namespace Filesystem
 
--- a/include/core/session.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/core/session.h	Tue Nov 07 23:40:54 2023 -0500
@@ -2,20 +2,27 @@
 #define __core__session_h
 
 #include "core/config.h"
+#include "track/types.h"
 #include "gui/locale.h"
 #include <QElapsedTimer>
 
 struct Session {
 	public:
-		Config config;
 		Session() { timer.start(); }
 		/* we literally *cannot* be lying to the user by doing this */
 		void IncrementRequests() { requests++; };
 		int GetRequests() { return requests; };
 		int uptime() { return timer.elapsed(); }
 
+		Config config;
+
+		struct {
+			std::vector<Track::Types::MediaPlayer> players;
+			std::vector<Track::Types::MediaExtension> extensions;
+		} recognition;
+
 	private:
-		int requests = 0;
+		uint32_t requests = 0;
 		QElapsedTimer timer;
 };
 
--- a/include/core/strings.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/core/strings.h	Tue Nov 07 23:40:54 2023 -0500
@@ -14,6 +14,7 @@
 /* Implode function: takes a vector of strings and turns it
    into a string, separated by delimiters. */
 std::string Implode(const std::vector<std::string>& vector, const std::string& delimiter);
+std::vector<std::string> Split(const std::string &text, const std::string& delimiter);
 
 /* Substring removal functions */
 std::string ReplaceAll(std::string string, const std::string& find, const std::string& replace);
--- a/include/core/torrent.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/core/torrent.h	Tue Nov 07 23:40:54 2023 -0500
@@ -4,11 +4,14 @@
 #include <string>
 #include <QDateTime>
 
-/* this will be moved into its own namespace if
+/* this is really just a fancy struct...
+
+   this will be moved into its own namespace if
    it's deemed necessary */
 class Torrent {
 	public:
 		std::string GetTitle() const { return _title; };
+		std::string GetCategory() const { return _category; };
 		std::string GetEpisode() const { return _episode; };
 		std::string GetGroup() const { return _group; };
 		size_t GetSize() const { return _size; };
@@ -23,6 +26,7 @@
 		QDateTime GetDate() const { return _date; };
 
 		void SetTitle(const std::string& title) { _title = title; };
+		void SetCategory(const std::string& category) { _category = category; };
 		void SetEpisode(const std::string& episode) { _episode = episode; };
 		void SetGroup(const std::string& group) { _group = group; };
 		void SetSize(const size_t size) { _size = size; };
@@ -38,6 +42,7 @@
 
 	private:
 		std::string _title;
+		std::string _category;
 		std::string _episode;
 		std::string _group;
 		size_t _size = 0;
--- a/include/gui/dialog/information.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/gui/dialog/information.h	Tue Nov 07 23:40:54 2023 -0500
@@ -10,7 +10,13 @@
 		Q_OBJECT
 
 	public:
-		InformationDialog(Anime::Anime& anime, std::function<void()> accept, QWidget* parent = nullptr);
+		enum Pages {
+			PAGE_MAIN_INFO,
+			PAGE_MY_LIST
+		};
+
+		InformationDialog(Anime::Anime& anime, std::function<void()> accept = {},
+			              enum Pages page = Pages::PAGE_MAIN_INFO, QWidget* parent = nullptr);
 
 	protected:
 		void showEvent(QShowEvent* event) override;
--- a/include/gui/locale.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/gui/locale.h	Tue Nov 07 23:40:54 2023 -0500
@@ -15,10 +15,10 @@
 public:
 	Locale();
 	Locale(const std::string& name);
-	QLocale GetLocale();
-	std::vector<QLocale> GetAvailableLocales();
+	QLocale GetLocale() const;
+	std::vector<QLocale> GetAvailableLocales() const;
 	void RefreshAvailableLocales(); // why would this ever be called?
-	bool IsLocaleAvailable(const QLocale& locale);
+	bool IsLocaleAvailable(const QLocale& locale) const;
 	bool SetActiveLocale(const QLocale& locale);
 
 private:
--- a/include/gui/pages/torrents.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/gui/pages/torrents.h	Tue Nov 07 23:40:54 2023 -0500
@@ -6,15 +6,6 @@
 #include <QAbstractListModel>
 #include <QSortFilterProxyModel>
 
-class TorrentModelItem : public Torrent {
-	public:
-		bool GetChecked() const { return _checked; };
-		void SetChecked(bool checked) { _checked = checked; };
-
-	private:
-		bool _checked = false;
-};
-
 class TorrentsPageListSortFilter final : public QSortFilterProxyModel {
 		Q_OBJECT
 
@@ -59,6 +50,15 @@
 		void RefreshTorrentList();
 
 	private:
+		class TorrentModelItem : public Torrent {
+			public:
+				bool GetChecked() const { return _checked; };
+				void SetChecked(bool checked) { _checked = checked; };
+
+			private:
+				bool _checked = false;
+		};
+
 		std::vector<TorrentModelItem> list;
 };
 
--- a/include/gui/theme.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/gui/theme.h	Tue Nov 07 23:40:54 2023 -0500
@@ -15,15 +15,15 @@
 	public:
 		Theme(Themes theme = Themes::OS);
 		void SetTheme(Themes theme);
-		Themes GetTheme();
-		bool IsInDarkTheme();
+		Themes GetTheme() const;
+		bool IsInDarkTheme() const;
 		void RepaintCurrentTheme();
 
 	private:
 		void SetToDarkTheme();
 		void SetToLightTheme();
 		void SetStyleSheet(Themes theme);
-		Themes GetCurrentOSTheme();
+		Themes GetCurrentOSTheme() const;
 		Themes theme;
 };
 
--- a/include/track/constants.h	Tue Nov 07 16:06:17 2023 -0500
+++ b/include/track/constants.h	Tue Nov 07 23:40:54 2023 -0500
@@ -3,7 +3,13 @@
 #include <string>
 #include <vector>
 
-extern const std::vector<std::string> media_extensions;
-extern const std::vector<std::string> media_players;
+namespace Track {
+namespace Constants {
+
+extern const std::vector<std::string> default_media_extensions;
+extern const std::vector<std::string> default_media_players;
+
+}
+}
 
 #endif // __track__constants_h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/include/track/types.h	Tue Nov 07 23:40:54 2023 -0500
@@ -0,0 +1,48 @@
+#ifndef __track__types_h
+#define __track__types_h
+
+#include <string>
+#include <vector>
+
+namespace Track {
+namespace Types {
+
+struct MediaPlayer {
+	public:
+		std::string GetName() const { return _name; };
+		std::string GetExecutable() const { return _executable; };
+		bool GetEnabled() const { return _enabled; };
+
+		void SetName(const std::string& name) { _name = name; };
+		void SetExecutable(const std::string& executable) { _executable = executable; };
+		void SetEnabled(const bool enabled) { _enabled = enabled; };
+
+	private:
+		std::string _name;
+		std::string _executable;
+		bool _enabled;
+};
+
+struct MediaExtension {
+	public:
+		std::string GetExtension() const { return _extension; };
+		bool GetEnabled() const { return _enabled; };
+
+		void SetExtension(const std::string& extension) { _extension = extension; };
+		void SetEnabled(const bool enabled) { _enabled = enabled; };
+
+	private:
+		std::string _extension;
+		bool _enabled;
+};
+
+void LoadPlayers(std::vector<MediaPlayer>& players);
+void LoadExtensions(std::vector<MediaExtension>& extensions);
+
+void SavePlayers(const std::vector<MediaPlayer>& players);
+void SaveExtensions(const std::vector<MediaExtension>& extensions);
+
+}
+}
+
+#endif // __track__types_h
\ No newline at end of file
--- a/rc/dark.qss	Tue Nov 07 16:06:17 2023 -0500
+++ b/rc/dark.qss	Tue Nov 07 23:40:54 2023 -0500
@@ -161,6 +161,7 @@
 
 QLineEdit {
 	background: transparent;
+	color: white;
 }
 
 QLineEdit:!read-only {
--- a/src/core/config.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/core/config.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -37,14 +37,14 @@
 	anilist.auth_token = INI::GetIniString(ini, "AniList", "Auth Token", "");
 	anilist.user_id = Strings::ToInt(INI::GetIniString(ini, "AniList", "User ID", ""), 0);
 
-	torrents.feed_link = INI::GetIniString(ini, "Torrents", "RSS Feed Link", "https://www.tokyotosho.info/rss.php?filter=1,11&zwnj=0");
+	torrents.feed_link = INI::GetIniString(ini, "Torrents", "RSS feed", "https://www.tokyotosho.info/rss.php?filter=1,11&zwnj=0");
 
 	theme.SetTheme(Translate::ToTheme(INI::GetIniString(ini, "Appearance", "Theme", "Default")));
 
 	return 0;
 }
 
-int Config::Save() {
+int Config::Save() const {
 	Filesystem::Path cfg_path = Filesystem::GetConfigPath();
 	if (!cfg_path.GetParent().Exists())
 		cfg_path.GetParent().CreateDirectories();
@@ -62,7 +62,7 @@
 	ini["AniList"]["Auth Token"] = anilist.auth_token;
 	ini["AniList"]["User ID"] = std::to_string(anilist.user_id);
 	ini["Appearance"]["Theme"] = Translate::ToString(theme.GetTheme());
-	ini["Torrents"]["RSS Feed Link"] = torrents.feed_link;
+	ini["Torrents"]["RSS feed"] = torrents.feed_link;
 
 	file.write(ini);
 
--- a/src/core/filesystem.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/core/filesystem.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -128,6 +128,22 @@
 	return ret;
 }
 
+Path GetPlayersPath(void) {
+	std::string ret = "";
+	ret += GetDotPath().GetPath();
+	if (!ret.empty())
+		ret += DELIM "players.json";
+	return ret;
+}
+
+Path GetExtensionsPath(void) {
+	std::string ret = "";
+	ret += GetDotPath().GetPath();
+	if (!ret.empty())
+		ret += DELIM "extensions.json";
+	return ret;
+}
+
 Path GetAnimeDBPath(void) {
 	std::string ret = "";
 	ret += GetDotPath().GetPath();
--- a/src/core/strings.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/core/strings.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -29,6 +29,19 @@
 	return out;
 }
 
+std::vector<std::string> Split(const std::string &text, const std::string& delimiter) {
+	std::vector<std::string> tokens;
+
+	std::size_t start = 0, end = 0;
+	while ((end = text.find(delimiter, start)) != std::string::npos) {
+		tokens.push_back(text.substr(start, end - start));
+		start = end + delimiter.length();
+	}
+	tokens.push_back(text.substr(start));
+
+	return tokens;
+}
+
 /* This function is really only used for cleaning up the synopsis of
    horrible HTML debris from AniList :) */
 std::string ReplaceAll(std::string string, const std::string& find, const std::string& replace) {
@@ -173,11 +186,11 @@
 
 uint64_t HumanReadableSizeToBytes(const std::string& str) {
 	const std::unordered_map<std::string, uint64_t> bytes_map = {
-		{"KB", 1 << 10},
-		{"MB", 1 << 20},
-		{"GB", 1 << 30},
-		{"TB", 1 << 40},
-		{"PB", 1 << 50} /* surely we won't need more than this */
+		{"KB", 1ull << 10},
+		{"MB", 1ull << 20},
+		{"GB", 1ull << 30},
+		{"TB", 1ull << 40},
+		{"PB", 1ull << 50} /* surely we won't need more than this */
 	};
 
 	for (const auto& suffix : bytes_map) {
@@ -195,8 +208,8 @@
 }
 
 std::string RemoveLeadingChars(std::string s, const char c) {
-    s.erase(0, std::min(s.find_first_not_of(c), s.size() - 1));
-    return s;
+	s.erase(0, std::min(s.find_first_not_of(c), s.size() - 1));
+	return s;
 }
 
 std::string RemoveTrailingChars(std::string s, const char c) {
--- a/src/gui/dialog/information.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/gui/dialog/information.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -39,7 +39,7 @@
 	anime.SetUserDateCompleted(_completed);
 }
 
-InformationDialog::InformationDialog(Anime::Anime& anime, std::function<void()> accept, QWidget* parent)
+InformationDialog::InformationDialog(Anime::Anime& anime, std::function<void()> accept, enum Pages page, QWidget* parent)
     : QDialog(parent) {
     /* ack. lots of brackets here, but MUCH, MUCH MUCH better than what it used to be */
 	setFixedSize(842, 613);
@@ -276,6 +276,7 @@
 
 					tabbed_widget->addTab(settings_widget, tr("My list and settings"));
 				}
+				tabbed_widget->setCurrentIndex(static_cast<int>(page));
 				main_layout->addWidget(tabbed_widget);
 				main_layout->setContentsMargins(0, 0, 0, 0);
 				main_layout->setSpacing(12);
--- a/src/gui/dialog/settings.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/gui/dialog/settings.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -30,6 +30,7 @@
 		pal.setColor(QPalette::WindowText, Qt::white);
 		page_title->setPalette(pal);
 	}
+
 	page_title->setAutoFillBackground(true);
 
 	page_title->setFixedHeight(23);
--- a/src/gui/locale.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/gui/locale.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -31,11 +31,11 @@
 	SetActiveLocale(QLocale(Strings::ToQString(name)));
 }
 
-QLocale Locale::GetLocale() {
+QLocale Locale::GetLocale() const {
 	return _locale;
 }
 
-std::vector<QLocale> Locale::GetAvailableLocales() {
+std::vector<QLocale> Locale::GetAvailableLocales() const {
 	return _available_translations;
 }
 
@@ -57,7 +57,7 @@
 	}
 }
 
-bool Locale::IsLocaleAvailable(const QLocale& locale) {
+bool Locale::IsLocaleAvailable(const QLocale& locale) const {
 	for (const QLocale& l : _available_translations)
 		if (l == locale)
 			return true;
--- a/src/gui/pages/anime_list.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/gui/pages/anime_list.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -108,13 +108,13 @@
 				case AL_TITLE: return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
 				case AL_PROGRESS:
 					return QString::number(list[index.row()].GetUserProgress()) + "/" +
-					       QString::number(list[index.row()].GetEpisodes());
+						   QString::number(list[index.row()].GetEpisodes());
 				case AL_EPISODES: return list[index.row()].GetEpisodes();
 				case AL_SCORE: return list[index.row()].GetUserScore();
 				case AL_TYPE: return Strings::ToQString(Translate::ToString(list[index.row()].GetFormat()));
 				case AL_SEASON:
 					return Strings::ToQString(Translate::ToString(list[index.row()].GetSeason())) + " " +
-					       QString::number(list[index.row()].GetAirDate().GetYear());
+						   QString::number(list[index.row()].GetAirDate().GetYear());
 				case AL_AVG_SCORE: return QString::number(list[index.row()].GetAudienceScore()) + "%";
 				case AL_STARTED: return list[index.row()].GetUserDateStarted().GetAsQDate();
 				case AL_COMPLETED: return list[index.row()].GetUserDateCompleted().GetAsQDate();
@@ -163,8 +163,8 @@
 }
 
 void AnimeListPageModel::RefreshList() {
-	bool has_children = !!rowCount(index(0));
-	if (!has_children) {
+	/* equivalent to hasChildren()... */
+	if (!rowCount(index(0))) {
 		beginInsertRows(QModelIndex(), 0, 0);
 		endInsertRows();
 	}
@@ -233,7 +233,8 @@
 		if (i == AnimeListPageModel::AL_TITLE)
 			continue;
 		const auto column_name =
-		    sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
+			sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
+
 		QAction* action = menu->addAction(column_name, this, [this, i](const bool checked) {
 			if (!checked && (VisibleColumnsCount() <= 1))
 				return;
@@ -245,6 +246,7 @@
 
 			// SaveSettings();
 		});
+
 		action->setCheckable(true);
 		action->setChecked(!tree_view->isColumnHidden(i));
 	}
@@ -266,9 +268,9 @@
 	menu->setToolTipsVisible(true);
 
 	AnimeListPageModel* source_model =
-	    reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
+		reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
 	const QItemSelection selection =
-	    sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+		sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
 
 	std::set<Anime::Anime*> animes;
 	for (const auto& index : selection.indexes()) {
@@ -281,8 +283,9 @@
 
 	menu->addAction(tr("Information"), [this, animes] {
 		for (auto& anime : animes) {
-			InformationDialog* dialog = new InformationDialog(
-			    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this);
+			InformationDialog* dialog = new InformationDialog(*anime, [this, anime] {
+				UpdateAnime(anime->GetId());
+			}, InformationDialog::PAGE_MAIN_INFO, this);
 
 			dialog->show();
 			dialog->raise();
@@ -290,6 +293,17 @@
 		}
 	});
 	menu->addSeparator();
+	menu->addAction(tr("Edit"), [this, animes] {
+		for (auto& anime : animes) {
+			InformationDialog* dialog = new InformationDialog(*anime, [this, anime] {
+				UpdateAnime(anime->GetId());
+			}, InformationDialog::PAGE_MY_LIST, this);
+
+			dialog->show();
+			dialog->raise();
+			dialog->activateWindow();
+		}
+	});
 	menu->addAction(tr("Delete from list..."), [this, animes] {
 		for (auto& anime : animes) {
 			RemoveAnime(anime->GetId());
@@ -301,19 +315,20 @@
 void AnimeListPage::ItemDoubleClicked() {
 	/* throw out any other garbage */
 	const QItemSelection selection =
-	    sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
+		sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
 	if (!selection.indexes().first().isValid()) {
 		return;
 	}
 
 	AnimeListPageModel* source_model =
-	    reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
+		reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
 
 	const QModelIndex index = source_model->index(selection.indexes().first().row());
 	Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
 
-	InformationDialog* dialog = new InformationDialog(
-	    *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this);
+	InformationDialog* dialog = new InformationDialog(*anime, [this, anime] {
+		UpdateAnime(anime->GetId());
+	}, InformationDialog::PAGE_MAIN_INFO, this);
 
 	dialog->show();
 	dialog->raise();
@@ -328,7 +343,7 @@
 void AnimeListPage::RefreshTabs() {
 	for (unsigned int i = 0; i < sort_models.size(); i++)
 		tab_bar->setTabText(i, Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
-		                           QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
+								   QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
 }
 
 void AnimeListPage::Refresh() {
@@ -423,7 +438,7 @@
 
 	for (unsigned int i = 0; i < sort_models.size(); i++) {
 		tab_bar->addTab(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
-		                QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
+						QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
 		sort_models[i] = new AnimeListPageSortFilter(tree_view);
 		sort_models[i]->setSourceModel(new AnimeListPageModel(this, Anime::ListStatuses[i]));
 		sort_models[i]->setSortRole(Qt::UserRole);
@@ -441,10 +456,10 @@
 
 	/* Enter & return keys */
 	connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
-	        &AnimeListPage::ItemDoubleClicked);
+			&AnimeListPage::ItemDoubleClicked);
 
 	connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
-	        &AnimeListPage::ItemDoubleClicked);
+			&AnimeListPage::ItemDoubleClicked);
 
 	tree_view->header()->setStretchLastSection(false);
 	tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
--- a/src/gui/pages/torrents.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/gui/pages/torrents.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -17,6 +17,12 @@
 #include <sstream>
 #include <algorithm>
 
+/* This file is very, very similar to the anime list page.
+
+   It differs from Taiga in that it uses tabs instead of
+   those "groups", but those are custom painted and a pain in the ass to
+   maintain over multiple platforms. */
+
 TorrentsPageListSortFilter::TorrentsPageListSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
 }
 
@@ -74,16 +80,38 @@
 		}
 		torrent.SetDescription(Strings::TextifySynopsis(item.child_value("description")));
 		{
+			/* Parse description... */
+			enum class Keys { SIZE, AUTHORIZED, SUBMITTER, COMMENT };
+
+			const std::unordered_map<std::string, Keys> KeyMap = {
+				{"Size", Keys::SIZE},
+				{"Authorized", Keys::AUTHORIZED},
+				{"Submitter", Keys::SUBMITTER},
+				{"Comment", Keys::COMMENT}
+			};
+
+			const std::string description = Strings::TextifySynopsis(item.child_value("description"));
+
 			/* Parse size from description */
-			std::istringstream descstream(torrent.GetDescription());
+			std::istringstream descstream(description);
 
 			for (std::string line; std::getline(descstream, line);) {
-				const std::string match = "Size: ";
-				size_t pos = line.find(match);
+				const size_t pos = line.find_first_of(':', 0);
+				if (pos == std::string::npos)
+					continue;
+
+				const std::string key = line.substr(0, pos);
+				const std::string value = line.substr(line.find_first_not_of(": ", pos));
 
-				if (!pos) {
-					const std::string size = line.substr(pos + match.length());
-					torrent.SetSize(Strings::HumanReadableSizeToBytes(size));
+				switch (KeyMap.at(key)) {
+					case Keys::COMMENT:
+						torrent.SetDescription(value);
+						break;
+					case Keys::SIZE:
+						torrent.SetSize(Strings::HumanReadableSizeToBytes(value));
+						break;
+					default:
+						break;
 				}
 			}
 		}
@@ -202,11 +230,6 @@
 				default: return data(index, Qt::DisplayRole);
 			}
 			break;
-		case Qt::CheckStateRole:
-			switch (index.column()) {
-				case 0: return item.GetChecked() ? Qt::Checked : Qt::Unchecked;
-				default: return {};
-			}
 		case Qt::SizeHintRole: {
 			switch (index.column()) {
 				default: {
@@ -243,12 +266,7 @@
 	if (!index.isValid())
 		return Qt::NoItemFlags;
 
-	const TorrentModelItem& item = list.at(index.row());
-
-	if (item.GetChecked() || index.column() == 0)
-		return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
-	else
-		return Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
+	return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
 }
 
 TorrentsPage::TorrentsPage(QWidget* parent) : QFrame(parent) {
--- a/src/gui/theme.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/gui/theme.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -27,11 +27,11 @@
 	this->theme = theme;
 }
 
-Themes Theme::GetTheme() {
+Themes Theme::GetTheme() const {
 	return theme;
 }
 
-bool Theme::IsInDarkTheme() {
+bool Theme::IsInDarkTheme() const {
 	if (theme != Themes::OS)
 		return (theme == Themes::DARK);
 #ifdef MACOSX
@@ -70,7 +70,7 @@
 		SetStyleSheet(Themes::LIGHT);
 }
 
-Themes Theme::GetCurrentOSTheme() {
+Themes Theme::GetCurrentOSTheme() const {
 #ifdef MACOSX
 	if (osx::DarkThemeAvailable())
 		return osx::IsInDarkTheme() ? Themes::DARK : Themes::LIGHT;
--- a/src/gui/window.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/gui/window.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -16,6 +16,7 @@
 #include "gui/widgets/sidebar.h"
 #include "services/services.h"
 #include "track/media.h"
+#include "track/types.h"
 #include <QActionGroup>
 #include <QApplication>
 #include <QDebug>
@@ -447,6 +448,8 @@
 
 void MainWindow::closeEvent(QCloseEvent* event) {
 	session.config.Save();
+	Track::Types::SavePlayers(session.recognition.players);
+	Track::Types::SaveExtensions(session.recognition.extensions);
 	event->accept();
 }
 
--- a/src/main.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/main.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -1,5 +1,6 @@
 #include "core/session.h"
 #include "gui/window.h"
+#include "track/types.h"
 #include <QApplication>
 #include <QStyleFactory>
 #include <QTranslator>
@@ -11,6 +12,8 @@
 	QApplication app(argc, argv);
 
 	session.config.Load();
+	Track::Types::LoadPlayers(session.recognition.players);
+	Track::Types::LoadExtensions(session.recognition.extensions);
 
 	MainWindow window;
 
--- a/src/track/constants.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/track/constants.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -1,11 +1,12 @@
 #include "track/constants.h"
 
-// clang-format off
-// https://github.com/llvm/llvm-project/issues/62676
-
 /* right now, these are just const vectors, but eventually
    I'll make a class to manage these and make them disableable */
-const std::vector<std::string> media_extensions = {
+
+namespace Track {
+namespace Constants {
+
+const std::vector<std::string> default_media_extensions = {
     "mkv",
     "mp4",
     "m4v", /* apple's stupid DRM thing */
@@ -46,7 +47,7 @@
     "mxf"
 };
 
-const std::vector<std::string> media_players = {
+const std::vector<std::string> default_media_players = {
 #ifdef MACOSX
     "VLC", "IINA", "QuickTime Player"
 #elif WIN32
@@ -55,4 +56,6 @@
     "vlc", "mpv", "mpc-qt"
 #endif
 };
-// clang-format on
+
+}
+}
--- a/src/track/media.cc	Tue Nov 07 16:06:17 2023 -0500
+++ b/src/track/media.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -4,29 +4,43 @@
 #include "anitomy/anitomy.h"
 #include "core/filesystem.h"
 #include "core/strings.h"
+#include "core/session.h"
 #include <string>
 #include <unordered_map>
 #include <vector>
+#include <iostream>
 
 namespace Track {
 namespace Media {
 
-Filesystem::Path GetCurrentPlaying() {
+std::vector<Filesystem::Path> GetCurrentlyPlayingFiles() {
 	/* getting all open files */
+	std::vector<Filesystem::Path> ret;
+
 	std::vector<int> pids = Animia::get_all_pids();
-	for (int i : pids) {
-		for (const std::string& player : media_players) {
-			if (Animia::get_process_name(i) != player)
+	for (int pid : pids) {
+		for (const Types::MediaPlayer& player : session.recognition.players) {
+			if (!player.GetEnabled() || Animia::get_process_name(pid) != player.GetExecutable())
 				continue;
-			for (const std::string& f : Animia::filter_system_files(Animia::get_open_files(i))) {
-				Filesystem::Path p(f);
-				for (const std::string& ext : media_extensions) {
-					if (p.Extension() == ext)
-						return p;
-				}
+
+			for (const std::string& file : Animia::filter_system_files(Animia::get_open_files(pid))) {
+				const Filesystem::Path path(file);
+
+				for (const Types::MediaExtension& ext : session.recognition.extensions)
+					if (path.Extension() == ext.GetExtension())
+						ret.push_back(path);
 			}
 		}
 	}
+
+	return ret;
+}
+
+Filesystem::Path GetCurrentPlaying() {
+	/* getting all open files */
+	std::vector<Filesystem::Path> paths = GetCurrentlyPlayingFiles();
+	if (paths.size())
+		return paths.at(0);
 	return Filesystem::Path();
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/track/types.cc	Tue Nov 07 23:40:54 2023 -0500
@@ -0,0 +1,270 @@
+#include "track/types.h"
+#include "core/filesystem.h"
+#include "core/json.h"
+#include <fstream>
+#include <vector>
+#include <string>
+
+using namespace nlohmann::literals::json_literals;
+
+namespace Track {
+namespace Types {
+
+static nlohmann::json default_players = {
+	{
+		{"name", "VLC"},
+#ifdef MACOSX
+		{"executable", "VLC"},
+#elif defined(WIN32)
+		{"executable", "vlc.exe"},
+#else
+		{"executable", "vlc"},
+#endif
+		{"enabled", true}
+	},
+	{
+		{"name", "mpv"},
+#ifdef WIN32
+		{"executable", "mpv.exe"},
+#else
+		{"executable", "mpv"},
+#endif
+		{"enabled", true}
+	},
+#ifdef WIN32
+	{
+		{"name", "MPC-HC x64"},
+		{"executable", "mpc-hc64.exe"},
+		{"enabled", true}
+	},
+	{
+		{"name", "MPC-HC"},
+		{"executable", "mpc-hc.exe"},
+		{"enabled", true}
+	},
+	{
+		{"name", "Windows Media Player"},
+		{"executable", "wmplayer.exe"},
+		{"enabled", true}
+	}
+#elif defined(MACOSX)
+	{
+		{"name", "IINA"},
+		{"executable", "IINA"},
+		{"enabled", true}
+	},
+	{
+		{"name", "QuickTime Player"},
+		{"executable", "QuickTime Player"},
+		{"enabled", false}
+	}
+#else
+	{
+		{"name", "MPC-Qt"},
+		{"executable", "mpc-qt"},
+		{"enabled", true}
+	}
+#endif
+};
+
+static nlohmann::json default_extensions = {
+	/* These are the four most common file extensions
+	   according to... me. */
+	{
+		{"extension", "mkv"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "mp4"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "m4v"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "avi"},
+		{"enabled", true}
+	},
+	/* Matroska's retarded inbred cousin */
+	{
+		{"extension", "webm"},
+		{"enabled", true}
+	},
+	/* QuickTime */
+	{
+		{"extension", "mov"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "qt"},
+		{"enabled", true}
+	},
+	/* MPEG transport stream */
+	{
+		{"extension", "mts"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "m2ts"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "ts"},
+		{"enabled", true}
+	},
+	/* MPEG-1 */
+	{
+		{"extension", "mpg"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "mpeg"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "mpe"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "mpv"},
+		{"enabled", true}
+	},
+	/* MPEG-2 */
+	{
+		{"extension", "m2v"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "mp2"},
+		{"enabled", true}
+	},
+	/* 3GPP */
+	{
+		{"extension", "3gp"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "3g2"},
+		{"enabled", true}
+	},
+	/* Windows Media */
+	{
+		{"extension", "asf"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "wmv"},
+		{"enabled", true}
+	},
+	/* Adobe Flash */
+	{
+		{"extension", "flv"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "swf"}, // lol
+		{"enabled", false}
+	},
+	/* Ogg Video */
+	{
+		{"extension", "ogv"},
+		{"enabled", true}
+	},
+	/* RealPlayer... LOL */
+	{
+		{"extension", "rm"},
+		{"enabled", true}
+	},
+	{
+		{"extension", "rmvb"},
+		{"enabled", true}
+	},
+	/* Nullsoft Streaming Video (Winamp) */
+	{
+		{"extension", "nsv"},
+		{"enabled", true}
+	},
+	/* Material Exchange Format (Sony) */
+	{
+		{"extension", "mxf"},
+		{"enabled", true}
+	},
+};
+
+void LoadPlayers(std::vector<MediaPlayer>& players) {
+	nlohmann::json json;
+	{	
+		std::ifstream is(Filesystem::GetPlayersPath().GetPath());
+		if (!is.is_open())
+			json = default_players;
+		else
+			is >> json;
+	}
+
+	players.reserve(json.size());
+	for (const auto& item : json) {
+		MediaPlayer player;
+		player.SetName(JSON::GetString(item, "/name"_json_pointer));
+		player.SetExecutable(JSON::GetString(item, "/executable"_json_pointer));
+		player.SetEnabled(JSON::GetBoolean(item, "/enabled"_json_pointer));
+		players.push_back(player);
+	}
+}
+
+void LoadExtensions(std::vector<MediaExtension>& extensions) {
+	nlohmann::json json;
+	{	
+		std::ifstream is(Filesystem::GetExtensionsPath().GetPath());
+		if (!is.is_open())
+			json = default_extensions;
+		else
+			is >> json;
+	}
+
+	extensions.reserve(json.size());
+	for (const auto& item : json) {
+		MediaExtension extension;
+		extension.SetExtension(JSON::GetString(item, "/extension"_json_pointer));
+		extension.SetEnabled(JSON::GetBoolean(item, "/enabled"_json_pointer));
+		extensions.push_back(extension);
+	}
+}
+
+void SavePlayers(const std::vector<MediaPlayer>& players) {
+	nlohmann::json json = {};
+	for (const auto& player : players) {
+		json.push_back({
+			{"name", player.GetName()},
+			{"executable", player.GetExecutable()},
+			{"enabled", player.GetEnabled()}
+		});
+	}
+
+	{
+		std::ofstream os(Filesystem::GetPlayersPath().GetPath());
+		if (!os.is_open())
+			return;
+		os << std::setw(4) << json << std::endl;
+	}
+}
+
+void SaveExtensions(const std::vector<MediaExtension>& extensions) {
+	nlohmann::json json = {};
+	for (const auto& extension : extensions) {
+		json.push_back({
+			{"extension", extension.GetExtension()},
+			{"enabled", extension.GetEnabled()}
+		});
+	}
+
+	{
+		std::ofstream os(Filesystem::GetExtensionsPath().GetPath());
+		if (!os.is_open())
+			return;
+		os << std::setw(4) << json << std::endl;
+	}
+}
+
+}
+}