Mercurial > minori
view src/gui/pages/torrents.cc @ 327:b5d6c27c308f
anime: refactor Anime::SeriesSeason to Season class
ToLocalString has also been altered to take in both season
and year because lots of locales actually treat formatting
seasons differently! most notably is Russian which adds a
suffix at the end to notate seasons(??)
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Thu, 13 Jun 2024 01:49:18 -0400 |
parents | 9a88e1725fd2 |
children | a0aa8c8c4307 |
line wrap: on
line source
#include "gui/pages/torrents.h" #include "core/filesystem.h" #include "core/http.h" #include "core/session.h" #include "core/strings.h" #include "gui/widgets/text.h" #include "track/media.h" #include <QByteArray> #include <QDataStream> #include <QDebug> #include <QHeaderView> #include <QThread> #include <QToolBar> #include <QTreeView> #include <QVBoxLayout> #include <QtGlobal> #include <algorithm> #include <fstream> #include <iostream> #include <sstream> #include "anitomy/anitomy.h" #include "pugixml.hpp" /* 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) { } void TorrentsPageListModel::DownloadTorrents(QItemSelection selection) { const auto indexes = selection.indexes(); for (const auto& index : indexes) { /* a torrent file IS literally text... */ const std::string link = list.at(index.row()).GetLink(); const std::string filename = list.at(index.row()).GetFilename() + ".torrent"; const std::filesystem::path torrents_dir = Filesystem::GetTorrentsPath(); std::filesystem::create_directories(torrents_dir); /* this sucks */ HTTP::RequestThread* thread = new HTTP::RequestThread(link, {}, "", HTTP::Type::Get, this); connect(thread, &HTTP::RequestThread::ReceivedData, this, [this, torrents_dir, filename](const QByteArray& data) { std::ofstream file(torrents_dir / filename, std::ofstream::out | std::ofstream::trunc); if (!file) return; // wat file.write(data.data(), data.size()); file.close(); }); connect(thread, &HTTP::RequestThread::finished, thread, &HTTP::RequestThread::deleteLater); thread->start(); } } QByteArray TorrentsPageListModel::DownloadTorrentList() { return HTTP::Request(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; case Keys::AUTHORIZED: if (torrent.GetGroup().empty() && value != "N/A") torrent.SetGroup(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))); std::string episode = Strings::ToUtf8String(elements.get(anitomy::kElementEpisodeNumber)); Strings::RemoveLeadingChars(episode, '0'); torrent.SetEpisode(episode); torrent.SetGroup(Strings::ToUtf8String(elements.get(anitomy::kElementReleaseGroup))); torrent.SetResolution(Strings::ToUtf8String(elements.get(anitomy::kElementVideoResolution))); } std::string description = item.child_value("description"); Strings::TextifySynopsis(description); ParseFeedDescription(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"); /* "Video" in Taiga */ case TL_SEEDERS: return tr("S"); case TL_LEECHERS: return tr("L"); case TL_DOWNLOADS: return tr("D"); 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_DOWNLOADS: 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 Strings::ToQString(Strings::BytesToHumanReadableSize(item.GetSize())); case TL_RESOLUTION: return Strings::ToQString(item.GetResolution()); case TL_SEEDERS: return item.GetSeeders(); case TL_LEECHERS: return item.GetLeechers(); case TL_DOWNLOADS: return item.GetDownloads(); 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.boundingRect(d).width(), 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_DOWNLOADS: 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] { Refresh(); }); } toolbar->addSeparator(); { toolbar->addAction(QIcon(":/icons/16x16/navigation-270-button.png"), tr("Download &marked torrents"), [this] { DownloadSelection(); }); } { 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); } { 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); } // set column sizes treeview->setColumnWidth(TorrentsPageListModel::TL_TITLE, 240); treeview->setColumnWidth(TorrentsPageListModel::TL_EPISODE, 60); treeview->setColumnWidth(TorrentsPageListModel::TL_GROUP, 100); treeview->setColumnWidth(TorrentsPageListModel::TL_SIZE, 70); treeview->setColumnWidth(TorrentsPageListModel::TL_RESOLUTION, 100); treeview->setColumnWidth(TorrentsPageListModel::TL_SEEDERS, 20); treeview->setColumnWidth(TorrentsPageListModel::TL_LEECHERS, 20); treeview->setColumnWidth(TorrentsPageListModel::TL_DOWNLOADS, 20); treeview->setColumnWidth(TorrentsPageListModel::TL_DESCRIPTION, 200); treeview->setColumnWidth(TorrentsPageListModel::TL_FILENAME, 200); treeview->setColumnWidth(TorrentsPageListModel::TL_RELEASEDATE, 190); treeview->header()->setStretchLastSection(false); layout->addWidget(treeview); } } void TorrentsPage::DownloadSelection() { if (!model) return; const QItemSelection selection = sort_model->mapSelectionToSource(treeview->selectionModel()->selection()); model->DownloadTorrents(selection); } void TorrentsPage::Refresh() { if (!model) return; HTTP::RequestThread* thread = new HTTP::RequestThread(session.config.torrents.feed_link); connect(thread, &HTTP::RequestThread::ReceivedData, this, [&](const QByteArray& ba) { /* This is to make sure we aren't in a different thread * messing around with GUI stuff */ treeview->setUpdatesEnabled(false); model->ParseTorrentList(ba); treeview->setUpdatesEnabled(true); }); connect(thread, &HTTP::RequestThread::finished, thread, &HTTP::RequestThread::deleteLater); thread->start(); }