diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 4265c5aa8..42bca388a 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -21,7 +21,6 @@ add_library(qbt_base STATIC bittorrent/loadtorrentparams.h bittorrent/ltqbitarray.h bittorrent/lttypecast.h - bittorrent/magneturi.h bittorrent/nativesessionextension.h bittorrent/nativetorrentextension.h bittorrent/peeraddress.h @@ -36,6 +35,7 @@ add_library(qbt_base STATIC bittorrent/torrentcontenthandler.h bittorrent/torrentcontentlayout.h bittorrent/torrentcreatorthread.h + bittorrent/torrentdescriptor.h bittorrent/torrentimpl.h bittorrent/torrentinfo.h bittorrent/tracker.h @@ -120,7 +120,6 @@ add_library(qbt_base STATIC bittorrent/filterparserthread.cpp bittorrent/infohash.cpp bittorrent/ltqbitarray.cpp - bittorrent/magneturi.cpp bittorrent/nativesessionextension.cpp bittorrent/nativetorrentextension.cpp bittorrent/peeraddress.cpp @@ -132,6 +131,7 @@ add_library(qbt_base STATIC bittorrent/torrent.cpp bittorrent/torrentcontenthandler.cpp bittorrent/torrentcreatorthread.cpp + bittorrent/torrentdescriptor.cpp bittorrent/torrentimpl.cpp bittorrent/torrentinfo.cpp bittorrent/tracker.cpp diff --git a/src/base/bittorrent/magneturi.cpp b/src/base/bittorrent/magneturi.cpp deleted file mode 100644 index b70bbdf78..000000000 --- a/src/base/bittorrent/magneturi.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * In addition, as a special exception, the copyright holders give permission to - * link this program with the OpenSSL project's "OpenSSL" library (or with - * modified versions of it that use the same license as the "OpenSSL" library), - * and distribute the linked executables. You must obey the GNU General Public - * License in all respects for all of the code used other than "OpenSSL". If you - * modify file(s), you may extend this exception to your version of the file(s), - * but you are not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - */ - -#include "magneturi.h" - -#include -#include -#include -#include - -#include - -#include "base/global.h" -#include "infohash.h" - -namespace -{ - // BEP9 Extension for Peers to Send Metadata Files - - bool isV1Hash(const QString &string) - { - // There are 2 representations for BitTorrent v1 info hash: - // 1. 40 chars hex-encoded string - // == 20 (SHA-1 length in bytes) * 2 (each byte maps to 2 hex characters) - // 2. 32 chars Base32 encoded string - // == 20 (SHA-1 length in bytes) * 1.6 (the efficiency of Base32 encoding) - const int V1_HEX_SIZE = SHA1Hash::length() * 2; - const int V1_BASE32_SIZE = SHA1Hash::length() * 1.6; - - return ((((string.size() == V1_HEX_SIZE)) - && !string.contains(QRegularExpression(u"[^0-9A-Fa-f]"_s))) - || ((string.size() == V1_BASE32_SIZE) - && !string.contains(QRegularExpression(u"[^2-7A-Za-z]"_s)))); - } - - bool isV2Hash(const QString &string) - { - // There are 1 representation for BitTorrent v2 info hash: - // 1. 64 chars hex-encoded string - // == 32 (SHA-2 256 length in bytes) * 2 (each byte maps to 2 hex characters) - const int V2_HEX_SIZE = SHA256Hash::length() * 2; - - return (string.size() == V2_HEX_SIZE) - && !string.contains(QRegularExpression(u"[^0-9A-Fa-f]"_s)); - } -} - -using namespace BitTorrent; - -const int magnetUriId = qRegisterMetaType(); - -MagnetUri::MagnetUri(const QString &source) - : m_url(source) -{ - if (source.isEmpty()) return; - - if (isV2Hash(source)) - m_url = u"magnet:?xt=urn:btmh:1220" + source; // 0x12 0x20 is the "multihash format" tag for the SHA-256 hashing scheme. - else if (isV1Hash(source)) - m_url = u"magnet:?xt=urn:btih:" + source; - - lt::error_code ec; - lt::parse_magnet_uri(m_url.toStdString(), m_addTorrentParams, ec); - if (ec) return; - - m_valid = true; - -#ifdef QBT_USES_LIBTORRENT2 - m_infoHash = m_addTorrentParams.info_hashes; -#else - m_infoHash = m_addTorrentParams.info_hash; -#endif - - m_name = QString::fromStdString(m_addTorrentParams.name); - - m_trackers.reserve(static_cast(m_addTorrentParams.trackers.size())); - int tier = 0; - auto tierIter = m_addTorrentParams.tracker_tiers.cbegin(); - for (const std::string &url : m_addTorrentParams.trackers) - { - if (tierIter != m_addTorrentParams.tracker_tiers.cend()) - tier = *tierIter++; - - m_trackers.append({QString::fromStdString(url), tier}); - } - - m_urlSeeds.reserve(static_cast(m_addTorrentParams.url_seeds.size())); - for (const std::string &urlSeed : m_addTorrentParams.url_seeds) - m_urlSeeds.append(QString::fromStdString(urlSeed)); -} - -bool MagnetUri::isValid() const -{ - return m_valid; -} - -InfoHash MagnetUri::infoHash() const -{ - return m_infoHash; -} - -QString MagnetUri::name() const -{ - return m_name; -} - -QVector MagnetUri::trackers() const -{ - return m_trackers; -} - -QVector MagnetUri::urlSeeds() const -{ - return m_urlSeeds; -} - -QString MagnetUri::url() const -{ - return m_url; -} - -lt::add_torrent_params MagnetUri::addTorrentParams() const -{ - return m_addTorrentParams; -} diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index ba4e23f44..81370375c 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -58,8 +58,8 @@ enum DeleteOption namespace BitTorrent { class InfoHash; - class MagnetUri; class Torrent; + class TorrentDescriptor; class TorrentID; class TorrentInfo; struct CacheStatus; @@ -441,10 +441,9 @@ namespace BitTorrent virtual bool isKnownTorrent(const InfoHash &infoHash) const = 0; virtual bool addTorrent(const QString &source, const AddTorrentParams ¶ms = {}) = 0; - virtual bool addTorrent(const MagnetUri &magnetUri, const AddTorrentParams ¶ms = {}) = 0; - virtual bool addTorrent(const TorrentInfo &torrentInfo, const AddTorrentParams ¶ms = {}) = 0; + virtual bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms = {}) = 0; virtual bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteOption::DeleteTorrent) = 0; - virtual bool downloadMetadata(const MagnetUri &magnetUri) = 0; + virtual bool downloadMetadata(const TorrentDescriptor &torrentDescr) = 0; virtual bool cancelDownloadMetadata(const TorrentID &id) = 0; virtual void recursiveTorrentDownload(const TorrentID &id) = 0; diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index a6c2efb07..5d36e6e34 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -101,10 +101,10 @@ #include "filterparserthread.h" #include "loadtorrentparams.h" #include "lttypecast.h" -#include "magneturi.h" #include "nativesessionextension.h" #include "portforwarderimpl.h" #include "resumedatastorage.h" +#include "torrentdescriptor.h" #include "torrentimpl.h" #include "tracker.h" @@ -2250,14 +2250,22 @@ void SessionImpl::handleDownloadFinished(const Net::DownloadResult &result) { case Net::DownloadStatus::Success: emit downloadFromUrlFinished(result.url); - if (const nonstd::expected loadResult = TorrentInfo::load(result.data); loadResult) + if (const auto loadResult = TorrentDescriptor::load(result.data)) addTorrent(loadResult.value(), m_downloadedTorrents.take(result.url)); else LogMsg(tr("Failed to load torrent. Reason: \"%1\"").arg(loadResult.error()), Log::WARNING); break; case Net::DownloadStatus::RedirectedToMagnet: emit downloadFromUrlFinished(result.url); - addTorrent(MagnetUri(result.magnet), m_downloadedTorrents.take(result.url)); + if (const auto parseResult = TorrentDescriptor::parse(result.magnetURI)) + { + addTorrent(parseResult.value(), m_downloadedTorrents.take(result.url)); + } + else + { + LogMsg(tr("Failed to load torrent. The request was redirected to invalid Magnet URI. Reason: \"%1\"") + .arg(parseResult.error()), Log::WARNING); + } break; default: emit downloadFromUrlFailed(result.url, result.errorString); @@ -2592,13 +2600,12 @@ bool SessionImpl::addTorrent(const QString &source, const AddTorrentParams ¶ return true; } - const MagnetUri magnetUri {source}; - if (magnetUri.isValid()) - return addTorrent(magnetUri, params); + if (const auto parseResult = TorrentDescriptor::parse(source)) + return addTorrent(parseResult.value(), params); const Path path {source}; TorrentFileGuard guard {path}; - const nonstd::expected loadResult = TorrentInfo::loadFromFile(path); + const auto loadResult = TorrentDescriptor::loadFromFile(path); if (!loadResult) { LogMsg(tr("Failed to load torrent. Source: \"%1\". Reason: \"%2\"").arg(source, loadResult.error()), Log::WARNING); @@ -2609,23 +2616,12 @@ bool SessionImpl::addTorrent(const QString &source, const AddTorrentParams ¶ return addTorrent(loadResult.value(), params); } -bool SessionImpl::addTorrent(const MagnetUri &magnetUri, const AddTorrentParams ¶ms) -{ - if (!isRestored()) - return false; - - if (!magnetUri.isValid()) - return false; - - return addTorrent_impl(magnetUri, params); -} - -bool SessionImpl::addTorrent(const TorrentInfo &torrentInfo, const AddTorrentParams ¶ms) +bool SessionImpl::addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms) { if (!isRestored()) return false; - return addTorrent_impl(torrentInfo, params); + return addTorrent_impl(torrentDescr, params); } LoadTorrentParams SessionImpl::initLoadTorrentParams(const AddTorrentParams &addTorrentParams) @@ -2690,12 +2686,12 @@ LoadTorrentParams SessionImpl::initLoadTorrentParams(const AddTorrentParams &add } // Add a torrent to the BitTorrent session -bool SessionImpl::addTorrent_impl(const std::variant &source, const AddTorrentParams &addTorrentParams) +bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorrentParams &addTorrentParams) { Q_ASSERT(isRestored()); - const bool hasMetadata = std::holds_alternative(source); - const auto infoHash = (hasMetadata ? std::get(source).infoHash() : std::get(source).infoHash()); + const bool hasMetadata = (source.info().has_value()); + const auto infoHash = source.infoHash(); const auto id = TorrentID::fromInfoHash(infoHash); // alternative ID can be useful to find existing torrent in case if hybrid torrent was added by v1 info hash @@ -2712,7 +2708,7 @@ bool SessionImpl::addTorrent_impl(const std::variant &so if (hasMetadata) { // Trying to set metadata to existing torrent in case if it has none - torrent->setMetadata(std::get(source)); + torrent->setMetadata(*source.info()); } if (!isMergeTrackersEnabled()) @@ -2721,29 +2717,16 @@ bool SessionImpl::addTorrent_impl(const std::variant &so return false; } - const bool isPrivate = torrent->isPrivate() || (hasMetadata && std::get(source).isPrivate()); + const bool isPrivate = torrent->isPrivate() || (hasMetadata && source.info()->isPrivate()); if (isPrivate) { LogMsg(tr("Detected an attempt to add a duplicate torrent. Trackers cannot be merged because it is a private torrent. Torrent: %1").arg(torrent->name())); return false; } - if (hasMetadata) - { - const TorrentInfo &torrentInfo = std::get(source); - - // merge trackers and web seeds - torrent->addTrackers(torrentInfo.trackers()); - torrent->addUrlSeeds(torrentInfo.urlSeeds()); - } - else - { - const MagnetUri &magnetUri = std::get(source); - - // merge trackers and web seeds - torrent->addTrackers(magnetUri.trackers()); - torrent->addUrlSeeds(magnetUri.urlSeeds()); - } + // merge trackers and web seeds + torrent->addTrackers(source.trackers()); + torrent->addUrlSeeds(source.urlSeeds()); LogMsg(tr("Detected an attempt to add a duplicate torrent. Trackers are merged from new source. Torrent: %1").arg(torrent->name())); return false; @@ -2759,6 +2742,7 @@ bool SessionImpl::addTorrent_impl(const std::variant &so LoadTorrentParams loadTorrentParams = initLoadTorrentParams(addTorrentParams); lt::add_torrent_params &p = loadTorrentParams.ltAddTorrentParams; + p = source.ltAddTorrentParams(); bool isFindingIncompleteFiles = false; @@ -2767,7 +2751,7 @@ bool SessionImpl::addTorrent_impl(const std::variant &so if (hasMetadata) { - const TorrentInfo &torrentInfo = std::get(source); + const TorrentInfo &torrentInfo = *source.info(); Q_ASSERT(addTorrentParams.filePaths.isEmpty() || (addTorrentParams.filePaths.size() == torrentInfo.filesCount())); @@ -2779,8 +2763,7 @@ bool SessionImpl::addTorrent_impl(const std::variant &so { const Path originalRootFolder = Path::findRootFolder(filePaths); const auto originalContentLayout = (originalRootFolder.isEmpty() - ? TorrentContentLayout::NoSubfolder - : TorrentContentLayout::Subfolder); + ? TorrentContentLayout::NoSubfolder : TorrentContentLayout::Subfolder); if (loadTorrentParams.contentLayout != originalContentLayout) { if (loadTorrentParams.contentLayout == TorrentContentLayout::NoSubfolder) @@ -2843,13 +2826,10 @@ bool SessionImpl::addTorrent_impl(const std::variant &so p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(addTorrentParams.filePriorities[i]); } - p.ti = torrentInfo.nativeInfo(); + Q_ASSERT(p.ti); } else { - const MagnetUri &magnetUri = std::get(source); - p = magnetUri.addTorrentParams(); - if (loadTorrentParams.name.isEmpty() && !p.name.empty()) loadTorrentParams.name = QString::fromStdString(p.name); } @@ -3009,19 +2989,20 @@ void SessionImpl::invokeAsync(std::function func) // Add a torrent to libtorrent session in hidden mode // and force it to download its metadata -bool SessionImpl::downloadMetadata(const MagnetUri &magnetUri) +bool SessionImpl::downloadMetadata(const TorrentDescriptor &torrentDescr) { - if (!magnetUri.isValid()) + Q_ASSERT(!torrentDescr.info().has_value()); + if (Q_UNLIKELY(torrentDescr.info().has_value())) return false; - const InfoHash infoHash = magnetUri.infoHash(); + const InfoHash infoHash = torrentDescr.infoHash(); // We should not add torrent if it's already // processed or adding to session if (isKnownTorrent(infoHash)) return false; - lt::add_torrent_params p = magnetUri.addTorrentParams(); + lt::add_torrent_params p = torrentDescr.ltAddTorrentParams(); if (isAddTrackersEnabled()) { @@ -5281,8 +5262,7 @@ void SessionImpl::recursiveTorrentDownload(const TorrentID &id) AddTorrentParams params; // Passing the save path along to the sub torrent file params.savePath = torrent->savePath(); - const nonstd::expected loadResult = TorrentInfo::loadFromFile(torrentFullpath); - if (loadResult) + if (const auto loadResult = TorrentDescriptor::loadFromFile(torrentFullpath)) { addTorrent(loadResult.value(), params); } diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index b6222160c..b1754dccb 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -30,7 +30,6 @@ #pragma once #include -#include #include #include @@ -76,9 +75,9 @@ namespace Net namespace BitTorrent { class InfoHash; - class MagnetUri; class ResumeDataStorage; class Torrent; + class TorrentDescriptor; class TorrentImpl; class Tracker; struct LoadTorrentParams; @@ -416,10 +415,9 @@ namespace BitTorrent bool isKnownTorrent(const InfoHash &infoHash) const override; bool addTorrent(const QString &source, const AddTorrentParams ¶ms = {}) override; - bool addTorrent(const MagnetUri &magnetUri, const AddTorrentParams ¶ms = {}) override; - bool addTorrent(const TorrentInfo &torrentInfo, const AddTorrentParams ¶ms = {}) override; + bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms = {}) override; bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteTorrent) override; - bool downloadMetadata(const MagnetUri &magnetUri) override; + bool downloadMetadata(const TorrentDescriptor &torrentDescr) override; bool cancelDownloadMetadata(const TorrentID &id) override; void recursiveTorrentDownload(const TorrentID &id) override; @@ -532,7 +530,7 @@ namespace BitTorrent void endStartup(ResumeSessionContext *context); LoadTorrentParams initLoadTorrentParams(const AddTorrentParams &addTorrentParams); - bool addTorrent_impl(const std::variant &source, const AddTorrentParams &addTorrentParams); + bool addTorrent_impl(const TorrentDescriptor &source, const AddTorrentParams &addTorrentParams); void updateSeedingLimitTimer(); void exportTorrentFile(const Torrent *torrent, const Path &folderPath); diff --git a/src/base/bittorrent/torrentdescriptor.cpp b/src/base/bittorrent/torrentdescriptor.cpp new file mode 100644 index 000000000..ef91682e4 --- /dev/null +++ b/src/base/bittorrent/torrentdescriptor.cpp @@ -0,0 +1,232 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015-2023 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "torrentdescriptor.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "base/global.h" +#include "base/preferences.h" +#include "base/utils/io.h" +#include "infohash.h" +#include "trackerentry.h" + +namespace +{ + // BEP9 Extension for Peers to Send Metadata Files + + bool isV1Hash(const QString &string) + { + // There are 2 representations for BitTorrent v1 info hash: + // 1. 40 chars hex-encoded string + // == 20 (SHA-1 length in bytes) * 2 (each byte maps to 2 hex characters) + // 2. 32 chars Base32 encoded string + // == 20 (SHA-1 length in bytes) * 1.6 (the efficiency of Base32 encoding) + const int V1_HEX_SIZE = SHA1Hash::length() * 2; + const int V1_BASE32_SIZE = SHA1Hash::length() * 1.6; + + return ((((string.size() == V1_HEX_SIZE)) && !string.contains(QRegularExpression(u"[^0-9A-Fa-f]"_s))) + || ((string.size() == V1_BASE32_SIZE) && !string.contains(QRegularExpression(u"[^2-7A-Za-z]"_s)))); + } + + bool isV2Hash(const QString &string) + { + // There are 1 representation for BitTorrent v2 info hash: + // 1. 64 chars hex-encoded string + // == 32 (SHA-2 256 length in bytes) * 2 (each byte maps to 2 hex characters) + const int V2_HEX_SIZE = SHA256Hash::length() * 2; + + return (string.size() == V2_HEX_SIZE) && !string.contains(QRegularExpression(u"[^0-9A-Fa-f]"_s)); + } + + lt::load_torrent_limits loadTorrentLimits() + { + const auto *pref = Preferences::instance(); + + lt::load_torrent_limits limits; + limits.max_buffer_size = static_cast(pref->getTorrentFileSizeLimit()); + limits.max_decode_depth = pref->getBdecodeDepthLimit(); + limits.max_decode_tokens = pref->getBdecodeTokenLimit(); + + return limits; + } +} + +const int TORRENTDESCRIPTOR_TYPEID = qRegisterMetaType(); + +nonstd::expected +BitTorrent::TorrentDescriptor::load(const QByteArray &data) noexcept +try +{ + return TorrentDescriptor(lt::load_torrent_buffer(lt::span(data.data(), data.size()), loadTorrentLimits())); +} +catch (const lt::system_error &err) +{ + return nonstd::make_unexpected(QString::fromLocal8Bit(err.what())); +} + +nonstd::expected +BitTorrent::TorrentDescriptor::loadFromFile(const Path &path) noexcept +try +{ + return TorrentDescriptor(lt::load_torrent_file(path.toString().toStdString(), loadTorrentLimits())); +} +catch (const lt::system_error &err) +{ + return nonstd::make_unexpected(QString::fromLocal8Bit(err.what())); +} + +nonstd::expected +BitTorrent::TorrentDescriptor::parse(const QString &str) noexcept +try +{ + QString magnetURI = str; + if (isV2Hash(str)) + magnetURI = u"magnet:?xt=urn:btmh:1220" + str; // 0x12 0x20 is the "multihash format" tag for the SHA-256 hashing scheme. + else if (isV1Hash(str)) + magnetURI = u"magnet:?xt=urn:btih:" + str; + + return TorrentDescriptor(lt::parse_magnet_uri(magnetURI.toStdString())); +} +catch (const lt::system_error &err) +{ + return nonstd::make_unexpected(QString::fromLocal8Bit(err.what())); +} + +nonstd::expected BitTorrent::TorrentDescriptor::saveToFile(const Path &path) const +try +{ + const lt::entry torrentEntry = lt::write_torrent_file(m_ltAddTorrentParams); + const nonstd::expected result = Utils::IO::saveToFile(path, torrentEntry); + if (!result) + return result.get_unexpected(); + + return {}; +} +catch (const lt::system_error &err) +{ + return nonstd::make_unexpected(QString::fromLocal8Bit(err.what())); +} + +BitTorrent::TorrentDescriptor::TorrentDescriptor(lt::add_torrent_params ltAddTorrentParams) + : m_ltAddTorrentParams {std::move(ltAddTorrentParams)} +{ + if (m_ltAddTorrentParams.ti && m_ltAddTorrentParams.ti->is_valid()) + m_info.emplace(*m_ltAddTorrentParams.ti); +} + +BitTorrent::InfoHash BitTorrent::TorrentDescriptor::infoHash() const +{ +#ifdef QBT_USES_LIBTORRENT2 + return m_ltAddTorrentParams.info_hashes; +#else + return m_ltAddTorrentParams.info_hash; +#endif +} + +QString BitTorrent::TorrentDescriptor::name() const +{ + return m_info ? m_info->name() : QString::fromStdString(m_ltAddTorrentParams.name); +} + +QDateTime BitTorrent::TorrentDescriptor::creationDate() const +{ + return ((m_ltAddTorrentParams.ti->creation_date() != 0) + ? QDateTime::fromSecsSinceEpoch(m_ltAddTorrentParams.ti->creation_date()) : QDateTime()); +} + +QString BitTorrent::TorrentDescriptor::creator() const +{ + return QString::fromStdString(m_ltAddTorrentParams.ti->creator()); +} + +QString BitTorrent::TorrentDescriptor::comment() const +{ + return QString::fromStdString(m_ltAddTorrentParams.ti->comment()); +} + +const std::optional &BitTorrent::TorrentDescriptor::info() const +{ + return m_info; +} + +void BitTorrent::TorrentDescriptor::setTorrentInfo(TorrentInfo torrentInfo) +{ + if (!torrentInfo.isValid()) + { + m_info.reset(); + m_ltAddTorrentParams.ti.reset(); + } + else + { + m_info = std::move(torrentInfo); + m_ltAddTorrentParams.ti = torrentInfo.nativeInfo(); +#ifdef QBT_USES_LIBTORRENT2 + m_ltAddTorrentParams.info_hashes = m_ltAddTorrentParams.ti->info_hashes(); +#else + m_ltAddTorrentParams.info_hash = m_ltAddTorrentParams.ti->info_hash(); +#endif + } +} + +QVector BitTorrent::TorrentDescriptor::trackers() const +{ + QVector ret; + ret.reserve(static_cast(m_ltAddTorrentParams.trackers.size())); + std::size_t i = 0; + for (const std::string &tracker : m_ltAddTorrentParams.trackers) + ret.append({QString::fromStdString(tracker), m_ltAddTorrentParams.tracker_tiers[i++]}); + + return ret; +} + +QVector BitTorrent::TorrentDescriptor::urlSeeds() const +{ + QVector urlSeeds; + urlSeeds.reserve(static_cast(m_ltAddTorrentParams.url_seeds.size())); + + for (const std::string &nativeURLSeed : m_ltAddTorrentParams.url_seeds) + urlSeeds.append(QUrl(QString::fromStdString(nativeURLSeed))); + + return urlSeeds; +} + +const libtorrent::add_torrent_params &BitTorrent::TorrentDescriptor::ltAddTorrentParams() const +{ + return m_ltAddTorrentParams; +} diff --git a/src/base/bittorrent/magneturi.h b/src/base/bittorrent/torrentdescriptor.h similarity index 55% rename from src/base/bittorrent/magneturi.h rename to src/base/bittorrent/torrentdescriptor.h index f0becd129..86f9059c3 100644 --- a/src/base/bittorrent/magneturi.h +++ b/src/base/bittorrent/torrentdescriptor.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2015-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -28,40 +28,57 @@ #pragma once +#include + #include -#include -#include -#include +#include +#include + +#include "base/3rdparty/expected.hpp" +#include "base/path.h" +#include "torrentdescriptor.h" +#include "torrentinfo.h" -#include "infohash.h" -#include "trackerentry.h" +class QByteArray; +class QDateTime; +class QString; +class QUrl; namespace BitTorrent { - class MagnetUri + class InfoHash; + struct TrackerEntry; + + class TorrentDescriptor { public: - explicit MagnetUri(const QString &source = {}); + TorrentDescriptor() = default; - bool isValid() const; InfoHash infoHash() const; QString name() const; + QDateTime creationDate() const; + QString creator() const; + QString comment() const; QVector trackers() const; QVector urlSeeds() const; - QString url() const; + const std::optional &info() const; - lt::add_torrent_params addTorrentParams() const; + void setTorrentInfo(TorrentInfo torrentInfo); + + static nonstd::expected load(const QByteArray &data) noexcept; + static nonstd::expected loadFromFile(const Path &path) noexcept; + static nonstd::expected parse(const QString &str) noexcept; + nonstd::expected saveToFile(const Path &path) const; + + const lt::add_torrent_params <AddTorrentParams() const; private: - bool m_valid = false; - QString m_url; - InfoHash m_infoHash; - QString m_name; - QVector m_trackers; - QVector m_urlSeeds; - lt::add_torrent_params m_addTorrentParams; + explicit TorrentDescriptor(lt::add_torrent_params ltAddTorrentParams); + + lt::add_torrent_params m_ltAddTorrentParams; + std::optional m_info; }; } -Q_DECLARE_METATYPE(BitTorrent::MagnetUri) +Q_DECLARE_METATYPE(BitTorrent::TorrentDescriptor) diff --git a/src/base/bittorrent/torrentinfo.cpp b/src/base/bittorrent/torrentinfo.cpp index 71b82138e..4a0cd050c 100644 --- a/src/base/bittorrent/torrentinfo.cpp +++ b/src/base/bittorrent/torrentinfo.cpp @@ -51,7 +51,7 @@ using namespace BitTorrent; -const int torrentInfoId = qRegisterMetaType(); +const int TORRENTINFO_TYPEID = qRegisterMetaType(); TorrentInfo::TorrentInfo(const lt::torrent_info &nativeInfo) : m_nativeInfo {std::make_shared(nativeInfo)} @@ -401,6 +401,34 @@ TorrentInfo::PieceRange TorrentInfo::filePieces(const int fileIndex) const return makeInterval(beginIdx, endIdx); } +bool TorrentInfo::matchesInfoHash(const InfoHash &otherInfoHash) const +{ + if (!isValid()) + return false; + + const InfoHash thisInfoHash = infoHash(); + + if (thisInfoHash.v1().isValid() && otherInfoHash.v1().isValid() + && (thisInfoHash.v1() != otherInfoHash.v1())) + { + return false; + } + + if (thisInfoHash.v2().isValid() && otherInfoHash.v2().isValid() + && (thisInfoHash.v2() != otherInfoHash.v2())) + { + return false; + } + + if (!thisInfoHash.v1().isValid() && otherInfoHash.v1().isValid()) + return false; + + if (!thisInfoHash.v2().isValid() && otherInfoHash.v2().isValid()) + return false; + + return true; +} + int TorrentInfo::fileIndex(const Path &filePath) const { // the check whether the object is valid is not needed here diff --git a/src/base/bittorrent/torrentinfo.h b/src/base/bittorrent/torrentinfo.h index e9b467939..abca51978 100644 --- a/src/base/bittorrent/torrentinfo.h +++ b/src/base/bittorrent/torrentinfo.h @@ -93,6 +93,8 @@ namespace BitTorrent PieceRange filePieces(const Path &filePath) const; PieceRange filePieces(int fileIndex) const; + bool matchesInfoHash(const InfoHash &otherInfoHash) const; + std::shared_ptr nativeInfo() const; QVector nativeIndexes() const; diff --git a/src/base/net/downloadhandlerimpl.cpp b/src/base/net/downloadhandlerimpl.cpp index 58648714b..fa9275e3f 100644 --- a/src/base/net/downloadhandlerimpl.cpp +++ b/src/base/net/downloadhandlerimpl.cpp @@ -199,11 +199,9 @@ void Net::DownloadHandlerImpl::handleRedirection(const QUrl &newUrl) // Redirect to magnet workaround if (newUrlString.startsWith(u"magnet:", Qt::CaseInsensitive)) { - qDebug("Magnet redirect detected."); m_result.status = Net::DownloadStatus::RedirectedToMagnet; - m_result.magnet = newUrlString; + m_result.magnetURI = newUrlString; m_result.errorString = tr("Redirected to magnet URI"); - finish(); return; } diff --git a/src/base/net/downloadmanager.h b/src/base/net/downloadmanager.h index 441780bd1..263ed700b 100644 --- a/src/base/net/downloadmanager.h +++ b/src/base/net/downloadmanager.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015, 2018 Vladimir Golovnev + * Copyright (C) 2015-2023 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -103,7 +103,7 @@ namespace Net QString errorString; QByteArray data; Path filePath; - QString magnet; + QString magnetURI; }; class DownloadHandler : public QObject diff --git a/src/base/rss/rss_autodownloader.cpp b/src/base/rss/rss_autodownloader.cpp index 5347784ab..5e877ad78 100644 --- a/src/base/rss/rss_autodownloader.cpp +++ b/src/base/rss/rss_autodownloader.cpp @@ -41,8 +41,8 @@ #include #include -#include "../bittorrent/magneturi.h" #include "../bittorrent/session.h" +#include "../bittorrent/torrentdescriptor.h" #include "../asyncfilestorage.h" #include "../global.h" #include "../logger.h" @@ -474,7 +474,7 @@ void AutoDownloader::processJob(const QSharedPointer &job) const auto torrentURL = job->articleData.value(Article::KeyTorrentURL).toString(); BitTorrent::Session::instance()->addTorrent(torrentURL, rule.addTorrentParams()); - if (BitTorrent::MagnetUri(torrentURL).isValid()) + if (BitTorrent::TorrentDescriptor::parse(torrentURL)) { if (Feed *feed = Session::instance()->feedByURL(job->feedURL)) { diff --git a/src/base/torrentfileswatcher.cpp b/src/base/torrentfileswatcher.cpp index d645be135..97bf26d22 100644 --- a/src/base/torrentfileswatcher.cpp +++ b/src/base/torrentfileswatcher.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2021 Vladimir Golovnev + * Copyright (C) 2021-2023 Vladimir Golovnev * Copyright (C) 2010 Christian Kandeler, Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -44,11 +44,9 @@ #include #include "base/algorithm.h" -#include "base/bittorrent/magneturi.h" #include "base/bittorrent/torrentcontentlayout.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrent.h" -#include "base/bittorrent/torrentinfo.h" #include "base/exceptions.h" #include "base/global.h" #include "base/logger.h" @@ -99,8 +97,7 @@ public slots: void removeWatchedFolder(const Path &path); signals: - void magnetFound(const BitTorrent::MagnetUri &magnetURI, const BitTorrent::AddTorrentParams &addTorrentParams); - void torrentFound(const BitTorrent::TorrentInfo &torrentInfo, const BitTorrent::AddTorrentParams &addTorrentParams); + void torrentFound(const BitTorrent::TorrentDescriptor &torrentDescr, const BitTorrent::AddTorrentParams &addTorrentParams); private: void onTimeout(); @@ -159,7 +156,6 @@ void TorrentFilesWatcher::initWorker() m_asyncWorker = new TorrentFilesWatcher::Worker; - connect(m_asyncWorker, &TorrentFilesWatcher::Worker::magnetFound, this, &TorrentFilesWatcher::onMagnetFound); connect(m_asyncWorker, &TorrentFilesWatcher::Worker::torrentFound, this, &TorrentFilesWatcher::onTorrentFound); m_asyncWorker->moveToThread(m_ioThread.get()); @@ -332,16 +328,10 @@ void TorrentFilesWatcher::removeWatchedFolder(const Path &path) } } -void TorrentFilesWatcher::onMagnetFound(const BitTorrent::MagnetUri &magnetURI - , const BitTorrent::AddTorrentParams &addTorrentParams) +void TorrentFilesWatcher::onTorrentFound(const BitTorrent::TorrentDescriptor &torrentDescr + , const BitTorrent::AddTorrentParams &addTorrentParams) { - BitTorrent::Session::instance()->addTorrent(magnetURI, addTorrentParams); -} - -void TorrentFilesWatcher::onTorrentFound(const BitTorrent::TorrentInfo &torrentInfo - , const BitTorrent::AddTorrentParams &addTorrentParams) -{ - BitTorrent::Session::instance()->addTorrent(torrentInfo, addTorrentParams); + BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams); } TorrentFilesWatcher::Worker::Worker() @@ -438,7 +428,10 @@ void TorrentFilesWatcher::Worker::processFolder(const Path &path, const Path &wa while (!file.atEnd()) { const auto line = QString::fromLatin1(file.readLine()).trimmed(); - emit magnetFound(BitTorrent::MagnetUri(line), addTorrentParams); + if (const auto parseResult = BitTorrent::TorrentDescriptor::parse(line)) + emit torrentFound(parseResult.value(), addTorrentParams); + else + LogMsg(tr("Invalid Magnet URI. URI: %1. Reason: %2").arg(line, parseResult.error()), Log::WARNING); } file.close(); @@ -446,7 +439,7 @@ void TorrentFilesWatcher::Worker::processFolder(const Path &path, const Path &wa } else { - LogMsg(tr("Magnet file too big. File: %1").arg(file.errorString())); + LogMsg(tr("Magnet file too big. File: %1").arg(file.errorString()), Log::WARNING); } } else @@ -456,10 +449,9 @@ void TorrentFilesWatcher::Worker::processFolder(const Path &path, const Path &wa } else { - const nonstd::expected result = BitTorrent::TorrentInfo::loadFromFile(filePath); - if (result) + if (const auto loadResult = BitTorrent::TorrentDescriptor::loadFromFile(filePath)) { - emit torrentFound(result.value(), addTorrentParams); + emit torrentFound(loadResult.value(), addTorrentParams); Utils::Fs::removeFile(filePath); } else @@ -496,8 +488,7 @@ void TorrentFilesWatcher::Worker::processFailedTorrents() if (!torrentPath.exists()) return true; - const nonstd::expected result = BitTorrent::TorrentInfo::loadFromFile(torrentPath); - if (result) + if (const auto loadResult = BitTorrent::TorrentDescriptor::loadFromFile(torrentPath)) { BitTorrent::AddTorrentParams addTorrentParams = options.addTorrentParams; if (torrentPath != watchedFolderPath) @@ -515,7 +506,7 @@ void TorrentFilesWatcher::Worker::processFailedTorrents() } } - emit torrentFound(result.value(), addTorrentParams); + emit torrentFound(loadResult.value(), addTorrentParams); Utils::Fs::removeFile(torrentPath); return true; diff --git a/src/base/torrentfileswatcher.h b/src/base/torrentfileswatcher.h index be7f42ce9..1c67ecf52 100644 --- a/src/base/torrentfileswatcher.h +++ b/src/base/torrentfileswatcher.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2021 Vladimir Golovnev + * Copyright (C) 2021-2023 Vladimir Golovnev * Copyright (C) 2010 Christian Kandeler, Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -32,16 +32,12 @@ #include #include "base/bittorrent/addtorrentparams.h" +#include "base/bittorrent/torrentdescriptor.h" #include "base/path.h" #include "base/utils/thread.h" class QThread; -namespace BitTorrent -{ - class MagnetUri; -} - /* * Watches the configured directories for new .torrent files in order * to add torrents to BitTorrent session. Supports Network File System @@ -72,8 +68,7 @@ signals: void watchedFolderRemoved(const Path &path); private slots: - void onMagnetFound(const BitTorrent::MagnetUri &magnetURI, const BitTorrent::AddTorrentParams &addTorrentParams); - void onTorrentFound(const BitTorrent::TorrentInfo &torrentInfo, const BitTorrent::AddTorrentParams &addTorrentParams); + void onTorrentFound(const BitTorrent::TorrentDescriptor &torrentDescr, const BitTorrent::AddTorrentParams &addTorrentParams); private: explicit TorrentFilesWatcher(QObject *parent = nullptr); diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index 98667c31c..df3087b0e 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -45,7 +45,6 @@ #include "base/bittorrent/downloadpriority.h" #include "base/bittorrent/infohash.h" -#include "base/bittorrent/magneturi.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrent.h" #include "base/bittorrent/torrentcontenthandler.h" @@ -139,18 +138,18 @@ class AddNewTorrentDialog::TorrentContentAdaptor final : public BitTorrent::TorrentContentHandler { public: - TorrentContentAdaptor(BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths + TorrentContentAdaptor(const BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths , QVector &filePriorities) : m_torrentInfo {torrentInfo} , m_filePaths {filePaths} , m_filePriorities {filePriorities} { - Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); + Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == m_torrentInfo.filesCount())); m_originalRootFolder = Path::findRootFolder(m_torrentInfo.filePaths()); m_currentContentLayout = (m_originalRootFolder.isEmpty() - ? BitTorrent::TorrentContentLayout::NoSubfolder - : BitTorrent::TorrentContentLayout::Subfolder); + ? BitTorrent::TorrentContentLayout::NoSubfolder + : BitTorrent::TorrentContentLayout::Subfolder); if (!m_filePriorities.isEmpty()) m_filePriorities.resize(filesCount(), BitTorrent::DownloadPriority::Normal); @@ -197,10 +196,10 @@ public: Q_ASSERT(!m_filePaths.isEmpty()); const auto originalContentLayout = (m_originalRootFolder.isEmpty() - ? BitTorrent::TorrentContentLayout::NoSubfolder - : BitTorrent::TorrentContentLayout::Subfolder); + ? BitTorrent::TorrentContentLayout::NoSubfolder + : BitTorrent::TorrentContentLayout::Subfolder); const auto newContentLayout = ((contentLayout == BitTorrent::TorrentContentLayout::Original) - ? originalContentLayout : contentLayout); + ? originalContentLayout : contentLayout); if (newContentLayout != m_currentContentLayout) { if (newContentLayout == BitTorrent::TorrentContentLayout::NoSubfolder) @@ -210,8 +209,7 @@ public: else { const auto rootFolder = ((originalContentLayout == BitTorrent::TorrentContentLayout::Subfolder) - ? m_originalRootFolder - : m_filePaths.at(0).removedExtension()); + ? m_originalRootFolder : m_filePaths.at(0).removedExtension()); Path::addRootFolder(m_filePaths, rootFolder); } @@ -262,7 +260,7 @@ public: } private: - BitTorrent::TorrentInfo &m_torrentInfo; + const BitTorrent::TorrentInfo &m_torrentInfo; PathList &m_filePaths; QVector &m_filePriorities; Path m_originalRootFolder; @@ -489,12 +487,7 @@ void AddNewTorrentDialog::show(const QString &source, const BitTorrent::AddTorre return; } - const BitTorrent::MagnetUri magnetUri {source}; - const bool isLoaded = magnetUri.isValid() - ? dlg->loadMagnet(magnetUri) - : dlg->loadTorrentFile(source); - - if (isLoaded) + if (dlg->loadTorrent(source)) dlg->QDialog::show(); else delete dlg; @@ -505,42 +498,56 @@ void AddNewTorrentDialog::show(const QString &source, QWidget *parent) show(source, BitTorrent::AddTorrentParams(), parent); } -bool AddNewTorrentDialog::loadTorrentFile(const QString &source) +bool AddNewTorrentDialog::loadTorrent(const QString &source) { - const Path decodedPath {source.startsWith(u"file://", Qt::CaseInsensitive) - ? QUrl::fromEncoded(source.toLocal8Bit()).toLocalFile() - : source}; - - const nonstd::expected result = BitTorrent::TorrentInfo::loadFromFile(decodedPath); - if (!result) + if (const auto parseResult = BitTorrent::TorrentDescriptor::parse(source)) + { + m_torrentDescr = parseResult.value(); + return loadTorrentImpl(); + } + else if (source.startsWith(u"magnet:", Qt::CaseInsensitive)) { RaisedMessageBox::critical(this, tr("Invalid torrent") - , tr("Failed to load the torrent: %1.\nError: %2", "Don't remove the '\n' characters. They insert a newline.") - .arg(decodedPath.toString(), result.error())); + , tr("Failed to load the torrent: %1.\nError: %2").arg(source, parseResult.error())); return false; } - m_torrentInfo = result.value(); - m_torrentGuard = std::make_unique(decodedPath); + const Path decodedPath {source.startsWith(u"file://", Qt::CaseInsensitive) + ? QUrl::fromEncoded(source.toLocal8Bit()).toLocalFile() : source}; + + if (const auto loadResult = BitTorrent::TorrentDescriptor::loadFromFile(decodedPath)) + { + m_torrentDescr = loadResult.value(); + m_torrentGuard = std::make_unique(decodedPath); - return loadTorrentImpl(); + return loadTorrentImpl(); + } + else + { + RaisedMessageBox::critical(this, tr("Invalid torrent") + , tr("Failed to load the torrent: %1.\nError: %2", "Don't remove the '\n' characters. They insert a newline.") + .arg(decodedPath.toString(), loadResult.error())); + return false; + } } bool AddNewTorrentDialog::loadTorrentImpl() { - const BitTorrent::InfoHash infoHash = m_torrentInfo.infoHash(); + const BitTorrent::InfoHash infoHash = m_torrentDescr.infoHash(); // Prevent showing the dialog if download is already present const auto *btSession = BitTorrent::Session::instance(); if (btSession->isKnownTorrent(infoHash)) { - BitTorrent::Torrent *const torrent = btSession->findTorrent(infoHash); - if (torrent) + if (BitTorrent::Torrent *torrent = btSession->findTorrent(infoHash)) { - // Trying to set metadata to existing torrent in case if it has none - torrent->setMetadata(m_torrentInfo); + if (hasMetadata()) + { + // Trying to set metadata to existing torrent in case if it has none + torrent->setMetadata(*m_torrentDescr.info()); + } - if (torrent->isPrivate() || m_torrentInfo.isPrivate()) + if (torrent->isPrivate() || (hasMetadata() && m_torrentDescr.info()->isPrivate())) { RaisedMessageBox::warning(this, tr("Torrent is already present"), tr("Torrent '%1' is already in the transfer list. Trackers cannot be merged because it is a private torrent.").arg(torrent->name()), QMessageBox::Ok); } @@ -557,8 +564,8 @@ bool AddNewTorrentDialog::loadTorrentImpl() if (mergeTrackers) { - torrent->addTrackers(m_torrentInfo.trackers()); - torrent->addUrlSeeds(m_torrentInfo.urlSeeds()); + torrent->addTrackers(m_torrentDescr.trackers()); + torrent->addUrlSeeds(m_torrentDescr.urlSeeds()); } } } @@ -570,85 +577,35 @@ bool AddNewTorrentDialog::loadTorrentImpl() return false; } - m_ui->labelInfohash1Data->setText(m_torrentInfo.infoHash().v1().isValid() ? m_torrentInfo.infoHash().v1().toString() : tr("N/A")); - m_ui->labelInfohash2Data->setText(m_torrentInfo.infoHash().v2().isValid() ? m_torrentInfo.infoHash().v2().toString() : tr("N/A")); - setupTreeview(); - TMMChanged(m_ui->comboTTM->currentIndex()); - - return true; -} + m_ui->labelInfohash1Data->setText(infoHash.v1().isValid() ? infoHash.v1().toString() : tr("N/A")); + m_ui->labelInfohash2Data->setText(infoHash.v2().isValid() ? infoHash.v2().toString() : tr("N/A")); -bool AddNewTorrentDialog::loadMagnet(const BitTorrent::MagnetUri &magnetUri) -{ - if (!magnetUri.isValid()) + if (hasMetadata()) { - RaisedMessageBox::critical(this, tr("Invalid magnet link"), tr("This magnet link was not recognized")); - return false; + setupTreeview(); } - - m_torrentGuard = std::make_unique(); - - const BitTorrent::InfoHash infoHash = magnetUri.infoHash(); - - // Prevent showing the dialog if download is already present - auto *btSession = BitTorrent::Session::instance(); - if (btSession->isKnownTorrent(infoHash)) + else { - BitTorrent::Torrent *const torrent = btSession->findTorrent(infoHash); - if (torrent) - { - if (torrent->isPrivate()) - { - RaisedMessageBox::warning(this, tr("Torrent is already present"), tr("Torrent '%1' is already in the transfer list. Trackers haven't been merged because it is a private torrent.").arg(torrent->name()), QMessageBox::Ok); - } - else - { - bool mergeTrackers = btSession->isMergeTrackersEnabled(); - if (Preferences::instance()->confirmMergeTrackers()) - { - const QMessageBox::StandardButton btn = RaisedMessageBox::question(this, tr("Torrent is already present") - , tr("Torrent '%1' is already in the transfer list. Do you want to merge trackers from new source?").arg(torrent->name()) - , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes); - mergeTrackers = (btn == QMessageBox::Yes); - } + connect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &AddNewTorrentDialog::updateMetadata); - if (mergeTrackers) - { - torrent->addTrackers(magnetUri.trackers()); - torrent->addUrlSeeds(magnetUri.urlSeeds()); - } - } - } - else - { - RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Magnet link is already queued for processing."), QMessageBox::Ok); - } - - return false; + // Set dialog title + const QString torrentName = m_torrentDescr.name(); + setWindowTitle(torrentName.isEmpty() ? tr("Magnet link") : torrentName); + updateDiskSpaceLabel(); + BitTorrent::Session::instance()->downloadMetadata(m_torrentDescr); + setMetadataProgressIndicator(true, tr("Retrieving metadata...")); } - connect(btSession, &BitTorrent::Session::metadataDownloaded, this, &AddNewTorrentDialog::updateMetadata); - - // Set dialog title - const QString torrentName = magnetUri.name(); - setWindowTitle(torrentName.isEmpty() ? tr("Magnet link") : torrentName); - - updateDiskSpaceLabel(); TMMChanged(m_ui->comboTTM->currentIndex()); - btSession->downloadMetadata(magnetUri); - setMetadataProgressIndicator(true, tr("Retrieving metadata...")); - m_ui->labelInfohash1Data->setText(magnetUri.infoHash().v1().isValid() ? magnetUri.infoHash().v1().toString() : tr("N/A")); - m_ui->labelInfohash2Data->setText(magnetUri.infoHash().v2().isValid() ? magnetUri.infoHash().v2().toString() : tr("N/A")); - - m_magnetURI = magnetUri; return true; } void AddNewTorrentDialog::showEvent(QShowEvent *event) { QDialog::showEvent(event); - if (!isTopLevel()) return; + if (!isTopLevel()) + return; activateWindow(); raise(); @@ -661,12 +618,13 @@ void AddNewTorrentDialog::updateDiskSpaceLabel() if (hasMetadata()) { + const auto torrentInfo = *m_torrentDescr.info(); const QVector &priorities = m_contentAdaptor->filePriorities(); - Q_ASSERT(priorities.size() == m_torrentInfo.filesCount()); + Q_ASSERT(priorities.size() == torrentInfo.filesCount()); for (int i = 0; i < priorities.size(); ++i) { if (priorities[i] > BitTorrent::DownloadPriority::Ignored) - torrentSize += m_torrentInfo.fileSize(i); + torrentSize += torrentInfo.fileSize(i); } } @@ -734,28 +692,32 @@ void AddNewTorrentDialog::contentLayoutChanged() void AddNewTorrentDialog::saveTorrentFile() { Q_ASSERT(hasMetadata()); + if (Q_UNLIKELY(!hasMetadata())) + return; + + const auto torrentInfo = *m_torrentDescr.info(); const QString filter {tr("Torrent file (*%1)").arg(TORRENT_FILE_EXTENSION)}; Path path {QFileDialog::getSaveFileName(this, tr("Save as torrent file") - , QDir::home().absoluteFilePath(m_torrentInfo.name() + TORRENT_FILE_EXTENSION) - , filter)}; + , QDir::home().absoluteFilePath(torrentInfo.name() + TORRENT_FILE_EXTENSION) + , filter)}; if (path.isEmpty()) return; if (!path.hasExtension(TORRENT_FILE_EXTENSION)) path += TORRENT_FILE_EXTENSION; - const nonstd::expected result = m_torrentInfo.saveToFile(path); + const auto result = m_torrentDescr.saveToFile(path); if (!result) { QMessageBox::critical(this, tr("I/O Error") - , tr("Couldn't export torrent metadata file '%1'. Reason: %2.").arg(path.toString(), result.error())); + , tr("Couldn't export torrent metadata file '%1'. Reason: %2.").arg(path.toString(), result.error())); } } bool AddNewTorrentDialog::hasMetadata() const { - return m_torrentInfo.isValid(); + return m_torrentDescr.info().has_value(); } void AddNewTorrentDialog::populateSavePaths() @@ -882,12 +844,10 @@ void AddNewTorrentDialog::accept() setEnabled(!m_ui->checkBoxNeverShow->isChecked()); // Add torrent - if (!hasMetadata()) - BitTorrent::Session::instance()->addTorrent(m_magnetURI, m_torrentParams); - else - BitTorrent::Session::instance()->addTorrent(m_torrentInfo, m_torrentParams); + BitTorrent::Session::instance()->addTorrent(m_torrentDescr, m_torrentParams); - m_torrentGuard->markAsAddedToSession(); + if (m_torrentGuard) + m_torrentGuard->markAsAddedToSession(); QDialog::accept(); } @@ -896,7 +856,7 @@ void AddNewTorrentDialog::reject() if (!hasMetadata()) { setMetadataProgressIndicator(false); - BitTorrent::Session::instance()->cancelDownloadMetadata(m_magnetURI.infoHash().toTorrentID()); + BitTorrent::Session::instance()->cancelDownloadMetadata(m_torrentDescr.infoHash().toTorrentID()); } QDialog::reject(); @@ -906,12 +866,13 @@ void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata { Q_ASSERT(metadata.isValid()); - if (metadata.infoHash() != m_magnetURI.infoHash()) return; + if (!metadata.matchesInfoHash(m_torrentDescr.infoHash())) + return; disconnect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &AddNewTorrentDialog::updateMetadata); // Good to go - m_torrentInfo = metadata; + m_torrentDescr.setTorrentInfo(metadata); setMetadataProgressIndicator(true, tr("Parsing metadata...")); // Update UI @@ -919,7 +880,7 @@ void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata setMetadataProgressIndicator(false, tr("Metadata retrieval complete")); m_ui->buttonSave->setVisible(true); - if (m_torrentInfo.infoHash().v2().isValid()) + if (m_torrentDescr.infoHash().v2().isValid()) { m_ui->buttonSave->setEnabled(false); m_ui->buttonSave->setToolTip(tr("Cannot create v2 torrent until its data is fully downloaded.")); @@ -941,16 +902,18 @@ void AddNewTorrentDialog::setupTreeview() return; // Set dialog title - setWindowTitle(m_torrentInfo.name()); + setWindowTitle(m_torrentDescr.name()); + + const auto &torrentInfo = *m_torrentDescr.info(); // Set torrent information - m_ui->labelCommentData->setText(Utils::Misc::parseHtmlLinks(m_torrentInfo.comment().toHtmlEscaped())); - m_ui->labelDateData->setText(!m_torrentInfo.creationDate().isNull() ? QLocale().toString(m_torrentInfo.creationDate(), QLocale::ShortFormat) : tr("Not available")); + m_ui->labelCommentData->setText(Utils::Misc::parseHtmlLinks(torrentInfo.comment().toHtmlEscaped())); + m_ui->labelDateData->setText(!torrentInfo.creationDate().isNull() ? QLocale().toString(torrentInfo.creationDate(), QLocale::ShortFormat) : tr("Not available")); if (m_torrentParams.filePaths.isEmpty()) - m_torrentParams.filePaths = m_torrentInfo.filePaths(); + m_torrentParams.filePaths = torrentInfo.filePaths(); - m_contentAdaptor = new TorrentContentAdaptor(m_torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities); + m_contentAdaptor = new TorrentContentAdaptor(torrentInfo, m_torrentParams.filePaths, m_torrentParams.filePriorities); const auto contentLayout = static_cast(m_ui->contentLayoutComboBox->currentIndex()); m_contentAdaptor->applyContentLayout(contentLayout); @@ -964,7 +927,7 @@ void AddNewTorrentDialog::setupTreeview() if (priorities[i] == BitTorrent::DownloadPriority::Ignored) continue; - if (BitTorrent::Session::instance()->isFilenameExcluded(m_torrentInfo.filePath(i).filename())) + if (BitTorrent::Session::instance()->isFilenameExcluded(torrentInfo.filePath(i).filename())) priorities[i] = BitTorrent::DownloadPriority::Ignored; } @@ -983,35 +946,43 @@ void AddNewTorrentDialog::handleDownloadFinished(const Net::DownloadResult &down switch (downloadResult.status) { case Net::DownloadStatus::Success: + if (const auto loadResult = BitTorrent::TorrentDescriptor::load(downloadResult.data)) { - const nonstd::expected result = BitTorrent::TorrentInfo::load(downloadResult.data); - if (!result) - { - RaisedMessageBox::critical(this, tr("Invalid torrent"), tr("Failed to load from URL: %1.\nError: %2") - .arg(downloadResult.url, result.error())); - return; - } - - m_torrentInfo = result.value(); - m_torrentGuard = std::make_unique(); - - if (loadTorrentImpl()) - open(); - else - deleteLater(); + m_torrentDescr = loadResult.value(); + } + else + { + RaisedMessageBox::critical(this, tr("Invalid torrent") + , tr("Failed to load from URL: %1.\nError: %2").arg(downloadResult.url, loadResult.error())); + deleteLater(); + return; } break; case Net::DownloadStatus::RedirectedToMagnet: - if (loadMagnet(BitTorrent::MagnetUri(downloadResult.magnet))) - open(); + if (const auto parseResult = BitTorrent::TorrentDescriptor::parse(downloadResult.magnetURI)) + { + m_torrentDescr = parseResult.value(); + } else + { + RaisedMessageBox::critical(this, tr("Invalid torrent") + , tr("Failed to load torrent. The request was redirected to invalid Magnet URI.\nError: %1") + .arg(parseResult.error())); deleteLater(); + return; + } break; default: - RaisedMessageBox::critical(this, tr("Download Error"), - tr("Cannot download '%1': %2").arg(downloadResult.url, downloadResult.errorString)); + RaisedMessageBox::critical(this, tr("Download Error") + , tr("Cannot download '%1': %2").arg(downloadResult.url, downloadResult.errorString)); deleteLater(); + return; } + + if (loadTorrentImpl()) + open(); + else + deleteLater(); } void AddNewTorrentDialog::TMMChanged(int index) @@ -1046,5 +1017,6 @@ void AddNewTorrentDialog::TMMChanged(int index) void AddNewTorrentDialog::doNotDeleteTorrentClicked(bool checked) { - m_torrentGuard->setAutoRemove(!checked); + if (m_torrentGuard) + m_torrentGuard->setAutoRemove(!checked); } diff --git a/src/gui/addnewtorrentdialog.h b/src/gui/addnewtorrentdialog.h index 2a830e14b..c36735d32 100644 --- a/src/gui/addnewtorrentdialog.h +++ b/src/gui/addnewtorrentdialog.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2022 Vladimir Golovnev + * Copyright (C) 2022-2023 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -34,8 +34,7 @@ #include #include "base/bittorrent/addtorrentparams.h" -#include "base/bittorrent/magneturi.h" -#include "base/bittorrent/torrentinfo.h" +#include "base/bittorrent/torrentdescriptor.h" #include "base/path.h" #include "base/settingvalue.h" @@ -98,9 +97,8 @@ private: explicit AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inParams, QWidget *parent); - bool loadTorrentFile(const QString &source); + bool loadTorrent(const QString &source); bool loadTorrentImpl(); - bool loadMagnet(const BitTorrent::MagnetUri &magnetUri); void populateSavePaths(); void loadState(); void saveState(); @@ -113,8 +111,7 @@ private: Ui::AddNewTorrentDialog *m_ui = nullptr; TorrentContentAdaptor *m_contentAdaptor = nullptr; - BitTorrent::MagnetUri m_magnetURI; - BitTorrent::TorrentInfo m_torrentInfo; + BitTorrent::TorrentDescriptor m_torrentDescr; int m_savePathIndex = -1; int m_downloadPathIndex = -1; bool m_useDownloadPath = false; diff --git a/src/gui/torrentcreatordialog.cpp b/src/gui/torrentcreatordialog.cpp index 83609e6d9..687df05ec 100644 --- a/src/gui/torrentcreatordialog.cpp +++ b/src/gui/torrentcreatordialog.cpp @@ -36,7 +36,7 @@ #include #include "base/bittorrent/session.h" -#include "base/bittorrent/torrentinfo.h" +#include "base/bittorrent/torrentdescriptor.h" #include "base/global.h" #include "base/utils/fs.h" #include "ui_torrentcreatordialog.h" @@ -252,8 +252,8 @@ void TorrentCreatorDialog::handleCreationSuccess(const Path &path, const Path &b if (m_ui->checkStartSeeding->isChecked()) { // Create save path temp data - const nonstd::expected result = BitTorrent::TorrentInfo::loadFromFile(path); - if (!result) + const auto loadResult = BitTorrent::TorrentDescriptor::loadFromFile(path); + if (!loadResult) { QMessageBox::critical(this, tr("Torrent creation failed"), tr("Reason: Created torrent is invalid. It won't be added to download list.")); return; @@ -270,7 +270,7 @@ void TorrentCreatorDialog::handleCreationSuccess(const Path &path, const Path &b } params.useAutoTMM = false; // otherwise if it is on by default, it will overwrite `savePath` to the default save path - BitTorrent::Session::instance()->addTorrent(result.value(), params); + BitTorrent::Session::instance()->addTorrent(loadResult.value(), params); } QMessageBox::information(this, tr("Torrent creator") , u"%1\n%2"_s.arg(tr("Torrent created:"), path.toString())); diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 798619e92..6104f5289 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2018-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -45,7 +45,7 @@ #include "base/bittorrent/peerinfo.h" #include "base/bittorrent/session.h" #include "base/bittorrent/torrent.h" -#include "base/bittorrent/torrentinfo.h" +#include "base/bittorrent/torrentdescriptor.h" #include "base/bittorrent/trackerentry.h" #include "base/global.h" #include "base/logger.h" @@ -739,14 +739,14 @@ void TorrentsController::addAction() const DataMap torrents = data(); for (auto it = torrents.constBegin(); it != torrents.constEnd(); ++it) { - const nonstd::expected result = BitTorrent::TorrentInfo::load(it.value()); - if (!result) + if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value())) { - throw APIError(APIErrorType::BadData - , tr("Error: '%1' is not a valid torrent file.").arg(it.key())); + partialSuccess |= BitTorrent::Session::instance()->addTorrent(loadResult.value(), addTorrentParams); + } + else + { + throw APIError(APIErrorType::BadData, tr("Error: '%1' is not a valid torrent file.").arg(it.key())); } - - partialSuccess |= BitTorrent::Session::instance()->addTorrent(result.value(), addTorrentParams); } if (partialSuccess)