Mercurial > minori
view src/gui/pages/torrents.cc @ 367:8d45d892be88 default tip
*: instead of pugixml, use Qt XML features
this means we have one extra Qt dependency though...
author | Paper <paper@tflc.us> |
---|---|
date | Sun, 17 Nov 2024 22:55:47 -0500 |
parents | a0aa8c8c4307 |
children |
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 <QDomDocument> #include <algorithm> #include <fstream> #include <iostream> #include <sstream> #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) { } 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) { QDomDocument doc; QDomNode node; QDomNodeList node_nodes; { QString err; int err_ln; int err_col; if (!doc.setContent(ba, &err, &err_ln, &err_col)) { session.SetStatusBar(Strings::ToUtf8String(tr("Torrents: Failed to parse XML with error %1 at line %2, column %3").arg(err, QString::number(err_ln), QString::number(err_col)))); return; // peace out } } /* my extra special dumb hack. */ if (!rowCount(index(0))) { beginInsertRows(QModelIndex(), 0, 0); endInsertRows(); } beginResetModel(); list.clear(); node = doc; for (const auto& n : {"rss", "channel"}) { node = node.namedItem(n); if (node.isNull()) { std::cout << n << std::endl; goto end; } } if (!node.hasChildNodes()) { std::cout << "no child nodes" << std::endl; goto end; } node_nodes = node.childNodes(); for (int c = 0; c < node_nodes.count(); c++) { const QDomNode item = node_nodes.at(c); if (!item.isElement() || item.nodeName() != "item") continue; const QDomNode title = item.namedItem("title"); if (!title.isElement()) continue; const QDomNode description = item.namedItem("description"); if (!description.isElement()) continue; const QDomNode link = item.namedItem("link"); if (!link.isElement()) continue; const QDomNode guid = item.namedItem("guid"); if (!guid.isElement()) continue; const QDomNode pubDate = item.namedItem("pubDate"); if (!pubDate.isElement()) continue; TorrentModelItem torrent; torrent.SetFilename(Strings::ToUtf8String(title.toElement().text())); /* "title" == filename */ { anitomy::Anitomy anitomy; anitomy.Parse(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_s = Strings::ToUtf8String(description.toElement().text()); Strings::TextifySynopsis(description_s); ParseFeedDescription(description_s, torrent); torrent.SetLink(Strings::ToUtf8String(link.toElement().text())); torrent.SetGuid(Strings::ToUtf8String(guid.toElement().text())); torrent.SetDate(QDateTime::fromString(pubDate.toElement().text(), "ddd, dd MMM yyyy HH:mm:ss t")); list.push_back(torrent); } end: 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(); }