diff --git a/src/base/bittorrent/trackerentry.h b/src/base/bittorrent/trackerentry.h index fcaf9af7d5d..e2930e0a92d 100644 --- a/src/base/bittorrent/trackerentry.h +++ b/src/base/bittorrent/trackerentry.h @@ -44,10 +44,10 @@ namespace BitTorrent public: enum Status { - NotContacted, - Working, - Updating, - NotWorking + NotContacted = 1, + Working = 2, + Updating = 3, + NotWorking = 4 }; TrackerEntry(const QString &url); diff --git a/src/gui/properties/trackerlistwidget.cpp b/src/gui/properties/trackerlistwidget.cpp index f2c85fbda9a..c27494968b4 100644 --- a/src/gui/properties/trackerlistwidget.cpp +++ b/src/gui/properties/trackerlistwidget.cpp @@ -97,23 +97,23 @@ TrackerListWidget::TrackerListWidget(PropertiesWidget *properties) insertTopLevelItem(2, m_LSDItem); setRowColor(2, QColor("grey")); // Set static items alignment - m_DHTItem->setTextAlignment(COL_RECEIVED, (Qt::AlignRight | Qt::AlignVCenter)); - m_PEXItem->setTextAlignment(COL_RECEIVED, (Qt::AlignRight | Qt::AlignVCenter)); - m_LSDItem->setTextAlignment(COL_RECEIVED, (Qt::AlignRight | Qt::AlignVCenter)); - m_DHTItem->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); - m_PEXItem->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); - m_LSDItem->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); m_DHTItem->setTextAlignment(COL_PEERS, (Qt::AlignRight | Qt::AlignVCenter)); m_PEXItem->setTextAlignment(COL_PEERS, (Qt::AlignRight | Qt::AlignVCenter)); m_LSDItem->setTextAlignment(COL_PEERS, (Qt::AlignRight | Qt::AlignVCenter)); + m_DHTItem->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); + m_PEXItem->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); + m_LSDItem->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); + m_DHTItem->setTextAlignment(COL_LEECHES, (Qt::AlignRight | Qt::AlignVCenter)); + m_PEXItem->setTextAlignment(COL_LEECHES, (Qt::AlignRight | Qt::AlignVCenter)); + m_LSDItem->setTextAlignment(COL_LEECHES, (Qt::AlignRight | Qt::AlignVCenter)); m_DHTItem->setTextAlignment(COL_DOWNLOADED, (Qt::AlignRight | Qt::AlignVCenter)); m_PEXItem->setTextAlignment(COL_DOWNLOADED, (Qt::AlignRight | Qt::AlignVCenter)); m_LSDItem->setTextAlignment(COL_DOWNLOADED, (Qt::AlignRight | Qt::AlignVCenter)); // Set header alignment headerItem()->setTextAlignment(COL_TIER, (Qt::AlignRight | Qt::AlignVCenter)); - headerItem()->setTextAlignment(COL_RECEIVED, (Qt::AlignRight | Qt::AlignVCenter)); - headerItem()->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); headerItem()->setTextAlignment(COL_PEERS, (Qt::AlignRight | Qt::AlignVCenter)); + headerItem()->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); + headerItem()->setTextAlignment(COL_LEECHES, (Qt::AlignRight | Qt::AlignVCenter)); headerItem()->setTextAlignment(COL_DOWNLOADED, (Qt::AlignRight | Qt::AlignVCenter)); // Set hotkeys m_editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut); @@ -245,15 +245,15 @@ void TrackerListWidget::clear() m_trackerItems.clear(); m_DHTItem->setText(COL_STATUS, ""); m_DHTItem->setText(COL_SEEDS, ""); - m_DHTItem->setText(COL_PEERS, ""); + m_DHTItem->setText(COL_LEECHES, ""); m_DHTItem->setText(COL_MSG, ""); m_PEXItem->setText(COL_STATUS, ""); m_PEXItem->setText(COL_SEEDS, ""); - m_PEXItem->setText(COL_PEERS, ""); + m_PEXItem->setText(COL_LEECHES, ""); m_PEXItem->setText(COL_MSG, ""); m_LSDItem->setText(COL_STATUS, ""); m_LSDItem->setText(COL_SEEDS, ""); - m_LSDItem->setText(COL_PEERS, ""); + m_LSDItem->setText(COL_LEECHES, ""); m_LSDItem->setText(COL_MSG, ""); } @@ -314,11 +314,11 @@ void TrackerListWidget::loadStickyItems(BitTorrent::TorrentHandle *const torrent } m_DHTItem->setText(COL_SEEDS, QString::number(seedsDHT)); - m_DHTItem->setText(COL_PEERS, QString::number(peersDHT)); + m_DHTItem->setText(COL_LEECHES, QString::number(peersDHT)); m_PEXItem->setText(COL_SEEDS, QString::number(seedsPeX)); - m_PEXItem->setText(COL_PEERS, QString::number(peersPeX)); + m_PEXItem->setText(COL_LEECHES, QString::number(peersPeX)); m_LSDItem->setText(COL_SEEDS, QString::number(seedsLSD)); - m_LSDItem->setText(COL_PEERS, QString::number(peersLSD)); + m_LSDItem->setText(COL_LEECHES, QString::number(peersLSD)); } void TrackerListWidget::loadTrackers() @@ -366,21 +366,15 @@ void TrackerListWidget::loadTrackers() break; } - item->setText(COL_RECEIVED, QString::number(data.numPeers)); -#if LIBTORRENT_VERSION_NUM >= 10000 + item->setText(COL_PEERS, QString::number(data.numPeers)); item->setText(COL_SEEDS, (entry.nativeEntry().scrape_complete > -1) ? QString::number(entry.nativeEntry().scrape_complete) : tr("N/A")); - item->setText(COL_PEERS, (entry.nativeEntry().scrape_incomplete > -1) ? QString::number(entry.nativeEntry().scrape_incomplete) : tr("N/A")); + item->setText(COL_LEECHES, (entry.nativeEntry().scrape_incomplete > -1) ? QString::number(entry.nativeEntry().scrape_incomplete) : tr("N/A")); item->setText(COL_DOWNLOADED, (entry.nativeEntry().scrape_downloaded > -1) ? QString::number(entry.nativeEntry().scrape_downloaded) : tr("N/A")); -#else - item->setText(COL_SEEDS, tr("N/A")); - item->setText(COL_PEERS, tr("N/A")); - item->setText(COL_DOWNLOADED, tr("N/A")); -#endif item->setTextAlignment(COL_TIER, (Qt::AlignRight | Qt::AlignVCenter)); - item->setTextAlignment(COL_RECEIVED, (Qt::AlignRight | Qt::AlignVCenter)); - item->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); item->setTextAlignment(COL_PEERS, (Qt::AlignRight | Qt::AlignVCenter)); + item->setTextAlignment(COL_SEEDS, (Qt::AlignRight | Qt::AlignVCenter)); + item->setTextAlignment(COL_LEECHES, (Qt::AlignRight | Qt::AlignVCenter)); item->setTextAlignment(COL_DOWNLOADED, (Qt::AlignRight | Qt::AlignVCenter)); } // Remove old trackers @@ -534,9 +528,9 @@ void TrackerListWidget::showTrackerListMenu(QPoint) QAction *delAct = nullptr; QAction *editAct = nullptr; if (!getSelectedTrackerItems().isEmpty()) { + editAct = menu.addAction(GuiIconProvider::instance()->getIcon("edit-rename"),tr("Edit tracker URL...")); delAct = menu.addAction(GuiIconProvider::instance()->getIcon("list-remove"), tr("Remove tracker")); copyAct = menu.addAction(GuiIconProvider::instance()->getIcon("edit-copy"), tr("Copy tracker URL")); - editAct = menu.addAction(GuiIconProvider::instance()->getIcon("edit-rename"),tr("Edit selected tracker URL")); } QAction *reannounceSelAct = nullptr; QAction *reannounceAllAct = nullptr; @@ -592,9 +586,9 @@ QStringList TrackerListWidget::headerLabels() "#" , tr("URL") , tr("Status") - , tr("Received") - , tr("Seeds") , tr("Peers") + , tr("Seeds") + , tr("Leeches") , tr("Downloaded") , tr("Message") }; diff --git a/src/gui/properties/trackerlistwidget.h b/src/gui/properties/trackerlistwidget.h index 9d0ae9da1f9..f8b28c547b7 100644 --- a/src/gui/properties/trackerlistwidget.h +++ b/src/gui/properties/trackerlistwidget.h @@ -54,9 +54,9 @@ class TrackerListWidget : public QTreeWidget COL_TIER, COL_URL, COL_STATUS, - COL_RECEIVED, - COL_SEEDS, COL_PEERS, + COL_SEEDS, + COL_LEECHES, COL_DOWNLOADED, COL_MSG, diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index c0482b41145..6edbe1d310a 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -40,6 +40,7 @@ #include #include "base/bittorrent/filepriority.h" +#include "base/bittorrent/peerinfo.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrenthandle.h" #include "base/bittorrent/torrentinfo.h" @@ -56,8 +57,12 @@ // Tracker keys const char KEY_TRACKER_URL[] = "url"; const char KEY_TRACKER_STATUS[] = "status"; +const char KEY_TRACKER_TIER[] = "tier"; const char KEY_TRACKER_MSG[] = "msg"; -const char KEY_TRACKER_PEERS[] = "num_peers"; +const char KEY_TRACKER_PEERS_COUNT[] = "num_peers"; +const char KEY_TRACKER_SEEDS_COUNT[] = "num_seeds"; +const char KEY_TRACKER_LEECHES_COUNT[] = "num_leeches"; +const char KEY_TRACKER_DOWNLOADED_COUNT[] = "num_downloaded"; // Web seed keys const char KEY_WEBSEED_URL[] = "url"; @@ -114,17 +119,83 @@ namespace void applyToTorrents(const QStringList &hashes, const std::function &func) { if ((hashes.size() == 1) && (hashes[0] == QLatin1String("all"))) { - for (BitTorrent::TorrentHandle *torrent : asConst(BitTorrent::Session::instance()->torrents())) + for (BitTorrent::TorrentHandle *const torrent : asConst(BitTorrent::Session::instance()->torrents())) func(torrent); } else { for (const QString &hash : hashes) { - BitTorrent::TorrentHandle *torrent = BitTorrent::Session::instance()->findTorrent(hash); + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); if (torrent) func(torrent); } } } + + QVariantList getStickyTrackers(const BitTorrent::TorrentHandle *const torrent) + { + uint seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0; + for (const BitTorrent::PeerInfo &peer : torrent->peers()) { + if (peer.isConnecting()) continue; + + if (peer.isSeed()) { + if (peer.fromDHT()) + ++seedsDHT; + if (peer.fromPeX()) + ++seedsPeX; + if (peer.fromLSD()) + ++seedsLSD; + } + else { + if (peer.fromDHT()) + ++leechesDHT; + if (peer.fromPeX()) + ++leechesPeX; + if (peer.fromLSD()) + ++leechesLSD; + } + } + + const int working = static_cast(BitTorrent::TrackerEntry::Working); + const int disabled = 0; + + const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")}; + const bool isTorrentPrivate = torrent->isPrivate(); + + const QVariantMap dht { + {KEY_TRACKER_URL, "** [DHT] **"}, + {KEY_TRACKER_TIER, ""}, + {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : "")}, + {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isDHTEnabled() && !isTorrentPrivate) ? working : disabled)}, + {KEY_TRACKER_PEERS_COUNT, 0}, + {KEY_TRACKER_DOWNLOADED_COUNT, 0}, + {KEY_TRACKER_SEEDS_COUNT, seedsDHT}, + {KEY_TRACKER_LEECHES_COUNT, leechesDHT} + }; + + const QVariantMap pex { + {KEY_TRACKER_URL, "** [PeX] **"}, + {KEY_TRACKER_TIER, ""}, + {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : "")}, + {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isPeXEnabled() && !isTorrentPrivate) ? working : disabled)}, + {KEY_TRACKER_PEERS_COUNT, 0}, + {KEY_TRACKER_DOWNLOADED_COUNT, 0}, + {KEY_TRACKER_SEEDS_COUNT, seedsPeX}, + {KEY_TRACKER_LEECHES_COUNT, leechesPeX} + }; + + const QVariantMap lsd { + {KEY_TRACKER_URL, "** [LSD] **"}, + {KEY_TRACKER_TIER, ""}, + {KEY_TRACKER_MSG, (isTorrentPrivate ? privateMsg : "")}, + {KEY_TRACKER_STATUS, ((BitTorrent::Session::instance()->isLSDEnabled() && !isTorrentPrivate) ? working : disabled)}, + {KEY_TRACKER_PEERS_COUNT, 0}, + {KEY_TRACKER_DOWNLOADED_COUNT, 0}, + {KEY_TRACKER_SEEDS_COUNT, seedsLSD}, + {KEY_TRACKER_LEECHES_COUNT, leechesLSD} + }; + + return QVariantList {dht, pex, lsd}; + } } // Returns all the torrents in JSON format. @@ -295,39 +366,37 @@ void TorrentsController::propertiesAction() // The dictionary keys are: // - "url": Tracker URL // - "status": Tracker status -// - "num_peers": Tracker peer count +// - "tier": Tracker tier +// - "num_peers": Number of peers this torrent is currently connected to +// - "num_seeds": Number of peers that have the whole file +// - "num_leeches": Number of peers that are still downloading +// - "num_downloaded": Tracker downloaded count // - "msg": Tracker message (last) void TorrentsController::trackersAction() { checkParams({"hash"}); const QString hash {params()["hash"]}; - QVariantList trackerList; - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + const BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); if (!torrent) throw APIError(APIErrorType::NotFound); + QVariantList trackerList = getStickyTrackers(torrent); + QHash trackersData = torrent->trackerInfos(); for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers())) { - QVariantMap trackerDict; - trackerDict[KEY_TRACKER_URL] = tracker.url(); const BitTorrent::TrackerInfo data = trackersData.value(tracker.url()); - QString status; - switch (tracker.status()) { - case BitTorrent::TrackerEntry::NotContacted: - status = tr("Not contacted yet"); break; - case BitTorrent::TrackerEntry::Updating: - status = tr("Updating..."); break; - case BitTorrent::TrackerEntry::Working: - status = tr("Working"); break; - case BitTorrent::TrackerEntry::NotWorking: - status = tr("Not working"); break; - } - trackerDict[KEY_TRACKER_STATUS] = status; - trackerDict[KEY_TRACKER_PEERS] = data.numPeers; - trackerDict[KEY_TRACKER_MSG] = data.lastMessage.trimmed(); - trackerList.append(trackerDict); + trackerList << QVariantMap { + {KEY_TRACKER_URL, tracker.url()}, + {KEY_TRACKER_TIER, tracker.tier()}, + {KEY_TRACKER_STATUS, static_cast(tracker.status())}, + {KEY_TRACKER_PEERS_COUNT, data.numPeers}, + {KEY_TRACKER_MSG, data.lastMessage.trimmed()}, + {KEY_TRACKER_SEEDS_COUNT, tracker.nativeEntry().scrape_complete}, + {KEY_TRACKER_LEECHES_COUNT, tracker.nativeEntry().scrape_incomplete}, + {KEY_TRACKER_DOWNLOADED_COUNT, tracker.nativeEntry().scrape_downloaded} + }; } setResult(QJsonArray::fromVariantList(trackerList)); @@ -532,15 +601,82 @@ void TorrentsController::addTrackersAction() const QString hash = params()["hash"]; BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) { - QList trackers; - for (QString url : asConst(params()["urls"].split('\n'))) { - url = url.trimmed(); - if (!url.isEmpty()) - trackers << url; + if (!torrent) + throw APIError(APIErrorType::NotFound); + + QList trackers; + for (const QString &urlStr : asConst(params()["urls"].split('\n'))) { + const QUrl url {urlStr.trimmed()}; + if (url.isValid()) + trackers << url.toString(); + } + torrent->addTrackers(trackers); +} + +void TorrentsController::editTrackerAction() +{ + checkParams({"hash", "origUrl", "newUrl"}); + + const QString hash = params()["hash"]; + const QString origUrl = params()["origUrl"]; + const QString newUrl = params()["newUrl"]; + + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + const QUrl origTrackerUrl(origUrl); + const QUrl newTrackerUrl(newUrl); + if (origTrackerUrl == newTrackerUrl) + return; + if (!newTrackerUrl.isValid()) + throw APIError(APIErrorType::BadParams, "New tracker URL is invalid"); + + QList trackers = torrent->trackers(); + bool match = false; + for (BitTorrent::TrackerEntry &tracker : trackers) { + const QUrl trackerUrl(tracker.url()); + if (trackerUrl == newTrackerUrl) + throw APIError(APIErrorType::Conflict, "New tracker URL already exists"); + if (trackerUrl == origTrackerUrl) { + match = true; + BitTorrent::TrackerEntry newTracker(newTrackerUrl.toString()); + newTracker.setTier(tracker.tier()); + tracker = newTracker; } - torrent->addTrackers(trackers); } + if (!match) + throw APIError(APIErrorType::Conflict, "Tracker not found"); + + torrent->replaceTrackers(trackers); + if (!torrent->isPaused()) + torrent->forceReannounce(); +} + +void TorrentsController::removeTrackersAction() +{ + checkParams({"hash", "urls"}); + + const QString hash = params()["hash"]; + const QStringList urls = params()["urls"].split('|'); + + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + QList remainingTrackers; + const QList trackers = torrent->trackers(); + for (const BitTorrent::TrackerEntry &entry : trackers) { + if (!urls.contains(entry.url())) + remainingTrackers.push_back(entry); + } + + if (remainingTrackers.size() == trackers.size()) + throw APIError(APIErrorType::Conflict, "No trackers were removed"); + + torrent->replaceTrackers(remainingTrackers); + if (!torrent->isPaused()) + torrent->forceReannounce(); } void TorrentsController::pauseAction() @@ -548,7 +684,7 @@ void TorrentsController::pauseAction() checkParams({"hashes"}); const QStringList hashes = params()["hashes"].split('|'); - applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->pause(); }); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *const torrent) { torrent->pause(); }); } void TorrentsController::resumeAction() @@ -556,7 +692,7 @@ void TorrentsController::resumeAction() checkParams({"hashes"}); const QStringList hashes = params()["hashes"].split('|'); - applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->resume(); }); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *const torrent) { torrent->resume(); }); } void TorrentsController::filePrioAction() @@ -606,7 +742,7 @@ void TorrentsController::uploadLimitAction() QVariantMap map; for (const QString &hash : hashes) { int limit = -1; - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + const BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); if (torrent) limit = torrent->uploadLimit(); map[hash] = limit; @@ -623,7 +759,7 @@ void TorrentsController::downloadLimitAction() QVariantMap map; for (const QString &hash : hashes) { int limit = -1; - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + const BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); if (torrent) limit = torrent->downloadLimit(); map[hash] = limit; @@ -641,7 +777,7 @@ void TorrentsController::setUploadLimitAction() limit = -1; const QStringList hashes {params()["hashes"].split('|')}; - applyToTorrents(hashes, [limit](BitTorrent::TorrentHandle *torrent) { torrent->setUploadLimit(limit); }); + applyToTorrents(hashes, [limit](BitTorrent::TorrentHandle *const torrent) { torrent->setUploadLimit(limit); }); } void TorrentsController::setDownloadLimitAction() @@ -653,7 +789,7 @@ void TorrentsController::setDownloadLimitAction() limit = -1; const QStringList hashes {params()["hashes"].split('|')}; - applyToTorrents(hashes, [limit](BitTorrent::TorrentHandle *torrent) { torrent->setDownloadLimit(limit); }); + applyToTorrents(hashes, [limit](BitTorrent::TorrentHandle *const torrent) { torrent->setDownloadLimit(limit); }); } void TorrentsController::setShareLimitsAction() @@ -664,7 +800,7 @@ void TorrentsController::setShareLimitsAction() const qlonglong seedingTimeLimit = params()["seedingTimeLimit"].toLongLong(); const QStringList hashes = params()["hashes"].split('|'); - applyToTorrents(hashes, [ratioLimit, seedingTimeLimit](BitTorrent::TorrentHandle *torrent) + applyToTorrents(hashes, [ratioLimit, seedingTimeLimit](BitTorrent::TorrentHandle *const torrent) { torrent->setRatioLimit(ratioLimit); torrent->setSeedingTimeLimit(seedingTimeLimit); @@ -676,7 +812,7 @@ void TorrentsController::toggleSequentialDownloadAction() checkParams({"hashes"}); const QStringList hashes {params()["hashes"].split('|')}; - applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->toggleSequentialDownload(); }); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *const torrent) { torrent->toggleSequentialDownload(); }); } void TorrentsController::toggleFirstLastPiecePrioAction() @@ -684,7 +820,7 @@ void TorrentsController::toggleFirstLastPiecePrioAction() checkParams({"hashes"}); const QStringList hashes {params()["hashes"].split('|')}; - applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->toggleFirstLastPiecePriority(); }); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *const torrent) { torrent->toggleFirstLastPiecePriority(); }); } void TorrentsController::setSuperSeedingAction() @@ -693,7 +829,7 @@ void TorrentsController::setSuperSeedingAction() const bool value {parseBool(params()["value"], false)}; const QStringList hashes {params()["hashes"].split('|')}; - applyToTorrents(hashes, [value](BitTorrent::TorrentHandle *torrent) { torrent->setSuperSeeding(value); }); + applyToTorrents(hashes, [value](BitTorrent::TorrentHandle *const torrent) { torrent->setSuperSeeding(value); }); } void TorrentsController::setForceStartAction() @@ -702,7 +838,7 @@ void TorrentsController::setForceStartAction() const bool value {parseBool(params()["value"], false)}; const QStringList hashes {params()["hashes"].split('|')}; - applyToTorrents(hashes, [value](BitTorrent::TorrentHandle *torrent) { torrent->resume(value); }); + applyToTorrents(hashes, [value](BitTorrent::TorrentHandle *const torrent) { torrent->resume(value); }); } void TorrentsController::deleteAction() @@ -711,7 +847,7 @@ void TorrentsController::deleteAction() const QStringList hashes {params()["hashes"].split('|')}; const bool deleteFiles {parseBool(params()["deleteFiles"], false)}; - applyToTorrents(hashes, [deleteFiles](BitTorrent::TorrentHandle *torrent) + applyToTorrents(hashes, [deleteFiles](BitTorrent::TorrentHandle *const torrent) { BitTorrent::Session::instance()->deleteTorrent(torrent->hash(), deleteFiles); }); @@ -779,7 +915,7 @@ void TorrentsController::setLocationAction() if (!QFileInfo(newLocation).isWritable()) throw APIError(APIErrorType::AccessDenied, tr("Cannot write to directory")); - applyToTorrents(hashes, [newLocation](BitTorrent::TorrentHandle *torrent) + applyToTorrents(hashes, [newLocation](BitTorrent::TorrentHandle *const torrent) { LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"") .arg(torrent->name(), Utils::Fs::toNativePath(torrent->savePath()), Utils::Fs::toNativePath(newLocation))); @@ -812,7 +948,7 @@ void TorrentsController::setAutoManagementAction() const QStringList hashes {params()["hashes"].split('|')}; const bool isEnabled {parseBool(params()["enable"], false)}; - applyToTorrents(hashes, [isEnabled](BitTorrent::TorrentHandle *torrent) + applyToTorrents(hashes, [isEnabled](BitTorrent::TorrentHandle *const torrent) { torrent->setAutoTMMEnabled(isEnabled); }); @@ -823,7 +959,7 @@ void TorrentsController::recheckAction() checkParams({"hashes"}); const QStringList hashes {params()["hashes"].split('|')}; - applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->forceRecheck(); }); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *const torrent) { torrent->forceRecheck(); }); } void TorrentsController::reannounceAction() @@ -831,7 +967,7 @@ void TorrentsController::reannounceAction() checkParams({"hashes"}); const QStringList hashes {params()["hashes"].split('|')}; - applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->forceReannounce(); }); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *const torrent) { torrent->forceReannounce(); }); } void TorrentsController::setCategoryAction() @@ -841,7 +977,7 @@ void TorrentsController::setCategoryAction() const QStringList hashes {params()["hashes"].split('|')}; const QString category {params()["category"].trimmed()}; - applyToTorrents(hashes, [category](BitTorrent::TorrentHandle *torrent) + applyToTorrents(hashes, [category](BitTorrent::TorrentHandle *const torrent) { if (!torrent->setCategory(category)) throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h index bf4caf8f539..2f467a22e24 100644 --- a/src/webui/api/torrentscontroller.h +++ b/src/webui/api/torrentscontroller.h @@ -59,6 +59,8 @@ private slots: void addAction(); void deleteAction(); void addTrackersAction(); + void editTrackerAction(); + void removeTrackersAction(); void filePrioAction(); void uploadLimitAction(); void downloadLimitAction(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index cfe09bcbfa8..3ca0aaf907f 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -43,9 +43,9 @@ #include "base/utils/net.h" #include "base/utils/version.h" -constexpr Utils::Version API_VERSION {2, 1, 1}; -constexpr int COMPAT_API_VERSION = 22; -constexpr int COMPAT_API_VERSION_MIN = 21; +constexpr Utils::Version API_VERSION {2, 2, 0}; +constexpr int COMPAT_API_VERSION = 23; +constexpr int COMPAT_API_VERSION_MIN = 23; class APIController; class WebApplication; diff --git a/src/webui/www/private/addtrackers.html b/src/webui/www/private/addtrackers.html index 52f26e0b1db..88322f492c3 100644 --- a/src/webui/www/private/addtrackers.html +++ b/src/webui/www/private/addtrackers.html @@ -9,6 +9,17 @@ + + + + + +
+
+

QBT_TR(Tracker URL:)QBT_TR[CONTEXT=TrackerListWidget]

+
+ +
+
+ +
+ + + diff --git a/src/webui/www/private/index.html b/src/webui/www/private/index.html index a0426125787..b8d5c525b28 100644 --- a/src/webui/www/private/index.html +++ b/src/webui/www/private/index.html @@ -158,6 +158,12 @@

qBittorrent Web User Interface QBT_TR(Pause torrents)QBT_TR[CONTEXT=CategoryFilterWidget] QBT_TR(Pause torrents)QBT_TR[CONTEXT=CategoryFilterWidget]
  • QBT_TR(Delete torrents)QBT_TR[CONTEXT=CategoryFilterWidget] QBT_TR(Delete torrents)QBT_TR[CONTEXT=CategoryFilterWidget]
  • +
    diff --git a/src/webui/www/private/properties_content.html b/src/webui/www/private/properties_content.html index ecd56f4d817..2ec80038d9d 100644 --- a/src/webui/www/private/properties_content.html +++ b/src/webui/www/private/properties_content.html @@ -84,10 +84,14 @@ - + + - - + + + + + diff --git a/src/webui/www/private/scripts/prop-trackers.js b/src/webui/www/private/scripts/prop-trackers.js index ba170846817..84d05315392 100644 --- a/src/webui/www/private/scripts/prop-trackers.js +++ b/src/webui/www/private/scripts/prop-trackers.js @@ -2,9 +2,10 @@ var trackersDynTable = new Class({ initialize: function() {}, - setup: function(table) { + setup: function(table, contextMenu) { this.table = $(table); this.rows = new Hash(); + this.contextMenu = contextMenu; }, removeRow: function(url) { @@ -32,7 +33,7 @@ var trackersDynTable = new Class({ }, insertRow: function(row) { - var url = row[0]; + var url = row[1]; if (this.rows.has(url)) { var tableRow = this.rows.get(url); this.updateRow(tableRow, row); @@ -46,11 +47,13 @@ var trackersDynTable = new Class({ td.set('html', row[i]); td.injectInside(tr); } + this.contextMenu.addTarget(tr); tr.injectInside(this.table); - }, + } }); var current_hash = ""; +var selectedTracker = ""; var loadTrackersDataTimer; var loadTrackersData = function() { @@ -61,13 +64,13 @@ var loadTrackersData = function() { } var new_hash = torrentsTable.getCurrentTorrentHash(); if (new_hash === "") { - tTable.removeAllRows(); + torrentTrackersTable.removeAllRows(); clearTimeout(loadTrackersDataTimer); loadTrackersDataTimer = loadTrackersData.delay(10000); return; } if (new_hash != current_hash) { - tTable.removeAllRows(); + torrentTrackersTable.removeAllRows(); current_hash = new_hash; } var url = new URI('api/v2/torrents/trackers?hash=' + current_hash); @@ -82,21 +85,44 @@ var loadTrackersData = function() { }, onSuccess: function(trackers) { $('error_div').set('html', ''); + torrentTrackersTable.removeAllRows(); + if (trackers) { // Update Trackers data trackers.each(function(tracker) { - var row = []; - row.length = 4; - row[0] = escapeHtml(tracker.url); - row[1] = tracker.status; - row[2] = tracker.num_peers; - row[3] = escapeHtml(tracker.msg); - tTable.insertRow(row); + var status; + switch (tracker.status) { + case 0: + status = "QBT_TR(Disabled)QBT_TR[CONTEXT=TrackerListWidget]"; + break; + case 1: + status = "QBT_TR(Not contacted yet)QBT_TR[CONTEXT=TrackerListWidget]"; + break; + case 2: + status = "QBT_TR(Working)QBT_TR[CONTEXT=TrackerListWidget]"; + break; + case 3: + status = "QBT_TR(Updating...)QBT_TR[CONTEXT=TrackerListWidget]"; + break; + case 4: + status = "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]"; + break; + } + + var row = [ + tracker.tier, + escapeHtml(tracker.url), + status, + tracker.num_peers, + (tracker.num_seeds >= 0) ? tracker.num_seeds : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", + (tracker.num_leeches >= 0) ? tracker.num_leeches : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", + (tracker.num_downloaded >= 0) ? tracker.num_downloaded : "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]", + escapeHtml(tracker.msg) + ]; + + torrentTrackersTable.insertRow(row); }); } - else { - tTable.removeAllRows(); - } clearTimeout(loadTrackersDataTimer); loadTrackersDataTimer = loadTrackersData.delay(10000); } @@ -108,11 +134,42 @@ var updateTrackersData = function() { loadTrackersData(); }; -tTable = new trackersDynTable(); -tTable.setup($('trackersTable')); +var torrentTrackersContextMenu = new ContextMenu({ + targets: '.torrentTrackersMenuTarget', + menu: 'torrentTrackersMenu', + actions: { + AddTracker: function(element, ref) { + addTrackerFN(); + }, + EditTracker: function(element, ref) { + editTrackerFN(element); + }, + RemoveTracker: function(element, ref) { + removeTrackerFN(element); + } + }, + offsets: { + x: -15, + y: 2 + }, + onShow: function() { + var element = this.options.element; + selectedTracker = element; + if (element.childNodes[1].innerText.indexOf("** [") === 0) { + this.hideItem('EditTracker'); + this.hideItem('RemoveTracker'); + this.hideItem('CopyTrackerUrl'); + } + else { + this.showItem('EditTracker'); + this.showItem('RemoveTracker'); + this.showItem('CopyTrackerUrl'); + } + this.options.element.firstChild.click(); + } +}); -// Add trackers code -$('addTrackersPlus').addEvent('click', function addTrackerDlg() { +var addTrackerFN = function() { if (current_hash.length === 0) return; new MochaUI.Window({ id: 'trackersPage', @@ -126,6 +183,62 @@ $('addTrackersPlus').addEvent('click', function addTrackerDlg() { paddingVertical: 0, paddingHorizontal: 0, width: 500, - height: 250 + height: 250, + onCloseComplete: function() { + updateTrackersData(); + } + }); +}; + +var editTrackerFN = function(element) { + if (current_hash.length === 0) return; + + var trackerUrl = encodeURIComponent(element.childNodes[1].innerText); + new MochaUI.Window({ + id: 'trackersPage', + title: "QBT_TR(Tracker editing)QBT_TR[CONTEXT=TrackerListWidget]", + loadMethod: 'iframe', + contentURL: 'edittracker.html?hash=' + current_hash + '&url=' + trackerUrl, + scrollbars: true, + resizable: false, + maximizable: false, + closable: true, + paddingVertical: 0, + paddingHorizontal: 0, + width: 500, + height: 150, + onCloseComplete: function() { + updateTrackersData(); + } }); +}; + +var removeTrackerFN = function(element) { + if (current_hash.length === 0) return; + + var trackerUrl = element.childNodes[1].innerText; + new Request({ + url: 'api/v2/torrents/removeTrackers', + method: 'post', + data: { + hash: current_hash, + urls: trackerUrl + }, + onSuccess: function() { + updateTrackersData(); + } + }).send(); +}; + +torrentTrackersTable = new trackersDynTable(); +torrentTrackersTable.setup($('trackersTable'), torrentTrackersContextMenu); + +new ClipboardJS('#CopyTrackerUrl', { + text: function(trigger) { + if (selectedTracker) { + var url = selectedTracker.childNodes[1].innerText; + selectedTracker = ""; + return url; + } + } }); diff --git a/src/webui/www/webui.qrc b/src/webui/www/webui.qrc index 999fa25a4d7..2c7559cd836 100644 --- a/src/webui/www/webui.qrc +++ b/src/webui/www/webui.qrc @@ -11,6 +11,7 @@ private/css/Window.cssprivate/download.htmlprivate/downloadlimit.html + private/edittracker.htmlprivate/filters.htmlprivate/index.htmlprivate/installsearchplugin.html
    QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget] Add TrackersQBT_TR(#)QBT_TR[CONTEXT=TrackerListWidget]QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget] QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]QBT_TR(Downloaded)QBT_TR[CONTEXT=TrackerListWidget]QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]