view src/gui/pages/torrents.cc @ 198:bc1ae1810855

dep/animia: switch from using classes to global functions the old idea was ok, but sort of hackish; this method doesn't use classes at all, and this way (especially important!) we can do wayland stuff AND x11 at the same time, which wasn't really possible without stupid workarounds in the other method
author Paper <mrpapersonic@gmail.com>
date Sun, 24 Dec 2023 02:59:42 -0500
parents 01d259b9c89f
children 53211cb1e7f5
line wrap: on
line source

#include "gui/pages/torrents.h"
#include "core/strings.h"
#include "core/http.h"
#include "core/session.h"
#include "gui/widgets/text.h"
#include "track/media.h"

#include <QVBoxLayout>
#include <QToolBar>
#include <QTreeView>
#include <QMainWindow>
#include <QByteArray>
#include <QDataStream>
#include <QThreadPool>
#include <QDebug>

#include <iostream>
#include <sstream>
#include <algorithm>

#include "pugixml.hpp"
#include "anitomy/anitomy.h"

/* 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) {
}

bool TorrentsPageListSortFilter::lessThan(const QModelIndex& l, const QModelIndex& r) const {
	QVariant left = sourceModel()->data(l, sortRole());
	QVariant right = sourceModel()->data(r, sortRole());

	switch (left.userType()) {
		case QMetaType::Int:
		case QMetaType::UInt:
		case QMetaType::LongLong:
		case QMetaType::ULongLong: return left.toInt() < right.toInt();
		case QMetaType::QDate: return left.toDate() < right.toDate();
		case QMetaType::QString:
		default: return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
	}
}

/* -------------------------------------------- */

TorrentsPageListModel::TorrentsPageListModel(QObject* parent) : QAbstractListModel(parent) {
}

QByteArray TorrentsPageListModel::DownloadTorrentList() {
	return HTTP::Get(session.config.torrents.feed_link);
}

void TorrentsPageListModel::ParseFeedDescription(const std::string& description, Torrent& torrent) {
	/* 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}
	};

	/* Parse size from description */
	std::istringstream descstream(description);

	for (std::string line; std::getline(descstream, line);) {
		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));

		switch (KeyMap.at(key)) {
			case Keys::COMMENT:
				torrent.SetDescription(value);
				break;
			case Keys::SIZE:
				torrent.SetSize(Strings::HumanReadableSizeToBytes(value));
				break;
			default:
				break;
		}
	}
}

void TorrentsPageListModel::ParseTorrentList(const QByteArray& ba) {
	std::istringstream stdstream(Strings::ToUtf8String(ba));

	pugi::xml_document doc;
	if (!doc.load(stdstream))
		return; // peace out

	/* my extra special dumb hack. */
	if (!rowCount(index(0))) {
		beginInsertRows(QModelIndex(), 0, 0);
		endInsertRows();
	}

	beginResetModel();

	list.clear();
	/* this is just an rss parser; it should be in a separate class... */
	for (pugi::xml_node item : doc.child("rss").child("channel").children("item")) {
		TorrentModelItem torrent;
		torrent.SetFilename(item.child_value("title")); /* "title" == filename */
		{
			anitomy::Anitomy anitomy;
			anitomy.Parse(Strings::ToWstring(torrent.GetFilename()));

			const auto& elements = anitomy.elements();

			/* todo: patch Anitomy so that it doesn't use wide strings */
			torrent.SetTitle(Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle)));
			torrent.SetEpisode(Strings::RemoveLeadingChars(Strings::ToUtf8String(elements.get(anitomy::kElementEpisodeNumber)), '0'));
			torrent.SetGroup(Strings::ToUtf8String(elements.get(anitomy::kElementReleaseGroup)));
			torrent.SetResolution(Strings::ToUtf8String(elements.get(anitomy::kElementVideoResolution)));
		}

		ParseFeedDescription(Strings::TextifySynopsis(item.child_value("description")), torrent);

		torrent.SetLink(item.child_value("link"));
		torrent.SetGuid(item.child_value("guid"));
		{
			const QString date_str = Strings::ToQString(item.child_value("pubDate"));
			torrent.SetDate(QDateTime::fromString(date_str, "ddd, dd MMM yyyy HH:mm:ss t"));
		}
		list.push_back(torrent);
	}

	endResetModel();
}

void TorrentsPageListModel::RefreshTorrentList() {
	ParseTorrentList(DownloadTorrentList());
}

int TorrentsPageListModel::rowCount(const QModelIndex& parent) const {
	return list.size();
	(void)(parent);
}

int TorrentsPageListModel::columnCount(const QModelIndex& parent) const {
	return NB_COLUMNS;
	(void)(parent);
}

QVariant TorrentsPageListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
	switch (role) {
		case Qt::DisplayRole: {
			switch (section) {
				case TL_TITLE: return tr("Anime title");
				case TL_EPISODE: return tr("Episode");
				case TL_GROUP: return tr("Group");
				case TL_SIZE: return tr("Size");
				case TL_RESOLUTION: return tr("Resolution"); /* this is named "Video" in Taiga */
				case TL_SEEDERS: return tr("Seeding"); /* named "S" in Taiga */
				case TL_LEECHERS: return tr("Leeching"); /* named "L" in Taiga */
				case TL_DOWNLOADERS: return tr("Downloading"); /* named "D" in Taiga */
				case TL_DESCRIPTION: return tr("Description");
				case TL_FILENAME: return tr("Filename");
				case TL_RELEASEDATE: return tr("Release date");
				default: return {};
			}
			break;
		}
		case Qt::TextAlignmentRole: {
			switch (section) {
				case TL_FILENAME:
				case TL_GROUP:
				case TL_DESCRIPTION:
				case TL_RESOLUTION:
				case TL_TITLE: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
				case TL_SEEDERS:
				case TL_LEECHERS:
				case TL_DOWNLOADERS:
				case TL_SIZE:
				case TL_EPISODE:
				case TL_RELEASEDATE: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
				default: return {};
			}
			break;
		}
	}
	return QAbstractListModel::headerData(section, orientation, role);
}

bool TorrentsPageListModel::setData(const QModelIndex& index, const QVariant& value, int role) {
	TorrentModelItem& item = list.at(index.row());

	if (index.column() == 0) {
		switch (role) {
			case Qt::EditRole:
				return false;
			case Qt::CheckStateRole:
				item.SetChecked(value.toBool());
				emit dataChanged(index, index);
				return true;
		}
	}

	return QAbstractItemModel::setData(index, value, role);
}

QVariant TorrentsPageListModel::data(const QModelIndex& index, int role) const {
	if (!index.isValid())
		return QVariant();

	const TorrentModelItem& item = list.at(index.row());

	switch (role) {
		case Qt::DisplayRole:
			switch (index.column()) {
				case TL_TITLE: return Strings::ToQString(item.GetTitle());
				case TL_EPISODE: return Strings::ToQString(item.GetEpisode());
				case TL_GROUP: return Strings::ToQString(item.GetGroup());
				case TL_SIZE: return session.config.locale.GetLocale().formattedDataSize(item.GetSize());
				case TL_RESOLUTION: return Strings::ToQString(item.GetResolution());
				case TL_SEEDERS: return item.GetSeeders();
				case TL_LEECHERS: return item.GetLeechers();
				case TL_DOWNLOADERS: return item.GetDownloaders();
				case TL_DESCRIPTION: return Strings::ToQString(item.GetDescription());
				case TL_FILENAME: return Strings::ToQString(item.GetFilename());
				case TL_RELEASEDATE: return item.GetDate();
				default: return "";
			}
			break;
		case Qt::UserRole:
			switch (index.column()) {
				case TL_EPISODE: return Strings::ToInt(item.GetEpisode(), -1);
				/* We have to use this to work around some stupid
				   "conversion ambiguous" error on Linux */
				case TL_SIZE: return QVariant::fromValue(item.GetSize());
				default: return data(index, Qt::DisplayRole);
			}
			break;
		case Qt::SizeHintRole: {
			switch (index.column()) {
				default: {
					/* max horizontal size of 100, height size = size of current font */
					const QString d = data(index, Qt::DisplayRole).toString();
					const QFontMetrics metric = QFontMetrics(QFont());

					return QSize(std::max(metric.horizontalAdvance(d), 100), metric.height());
				}
			}
			break;
		}
		case Qt::TextAlignmentRole:
			switch (index.column()) {
				case TL_FILENAME:
				case TL_GROUP:
				case TL_DESCRIPTION:
				case TL_RESOLUTION:
				case TL_TITLE: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
				case TL_SEEDERS:
				case TL_LEECHERS:
				case TL_DOWNLOADERS:
				case TL_SIZE:
				case TL_EPISODE:
				case TL_RELEASEDATE: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
				default: return {};
			}
			break;
	}
	return QVariant();
}

Qt::ItemFlags TorrentsPageListModel::flags(const QModelIndex& index) const {
	if (!index.isValid())
		return Qt::NoItemFlags;

	return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}

TorrentsPage::TorrentsPage(QWidget* parent) : QFrame(parent) {
	setFrameShape(QFrame::Box);
	setFrameShadow(QFrame::Sunken);

	QVBoxLayout* layout = new QVBoxLayout(this);
	layout->setContentsMargins(0, 0, 0, 0);
	layout->setSpacing(0);

	{
		/* Toolbar */
		QToolBar* toolbar = new QToolBar(this);
		toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
		toolbar->setIconSize(QSize(16, 16));
		toolbar->setMovable(false);

		{
			/* this needs to be stored somewhere to replicate Taiga's
			   "timer" feature */
			toolbar->addAction(QIcon(":/icons/16x16/arrow-circle-315.png"), tr("&Check new torrents"), [this]{
				QThreadPool::globalInstance()->start([this] {
					Refresh();
				});
			});
		}

		toolbar->addSeparator();

		{
			toolbar->addAction(QIcon(":/icons/16x16/navigation-270-button.png"), tr("Download &marked torrents"));
		}

		{
			toolbar->addAction(QIcon(":/icons/16x16/cross-button.png"), tr("&Discard all"));
		}

		toolbar->addSeparator();

		{
			toolbar->addAction(QIcon(":/icons/16x16/gear.png"), tr("&Settings"));
		}

		layout->addWidget(toolbar);
	}

	{
		QFrame* line = new QFrame(this);
		line->setFrameShape(QFrame::HLine);
		line->setFrameShadow(QFrame::Sunken);
		line->setLineWidth(1);
		layout->addWidget(line);
	}

	{
		QTreeView* treeview = new QTreeView(this);
		treeview->setUniformRowHeights(true);
		treeview->setAllColumnsShowFocus(false);
		treeview->setAlternatingRowColors(true);
		treeview->setSortingEnabled(true);
		treeview->setSelectionMode(QAbstractItemView::ExtendedSelection);
		treeview->setItemsExpandable(false);
		treeview->setRootIsDecorated(false);
		treeview->setContextMenuPolicy(Qt::CustomContextMenu);
		treeview->setFrameShape(QFrame::NoFrame);

		{
			sort_model = new TorrentsPageListSortFilter(treeview);
			model = new TorrentsPageListModel(treeview);
			sort_model->setSourceModel(model);
			sort_model->setSortRole(Qt::UserRole);
			sort_model->setSortCaseSensitivity(Qt::CaseInsensitive);
			treeview->setModel(sort_model);
		}

		layout->addWidget(treeview);
	}
}

void TorrentsPage::Refresh() {
	if (!model)
		return;
	model->RefreshTorrentList();
}

#include "gui/pages/moc_torrents.cpp"