mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-07-05 20:51:25 -07:00
2961 lines
90 KiB
C++
2961 lines
90 KiB
C++
/*
|
|
* Bittorrent Client using Qt and libtorrent.
|
|
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
|
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
|
*
|
|
* 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 "torrentimpl.h"
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
|
|
#ifdef Q_OS_WIN
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#include <libtorrent/address.hpp>
|
|
#include <libtorrent/session.hpp>
|
|
#include <libtorrent/storage_defs.hpp>
|
|
#include <libtorrent/time.hpp>
|
|
#include <libtorrent/write_resume_data.hpp>
|
|
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
#include <libtorrent/info_hash.hpp>
|
|
#endif
|
|
|
|
#include <QtSystemDetection>
|
|
#include <QByteArray>
|
|
#include <QCache>
|
|
#include <QDebug>
|
|
#include <QFuture>
|
|
#include <QPointer>
|
|
#include <QPromise>
|
|
#include <QSet>
|
|
#include <QStringList>
|
|
#include <QUrl>
|
|
|
|
#include "base/exceptions.h"
|
|
#include "base/global.h"
|
|
#include "base/logger.h"
|
|
#include "base/preferences.h"
|
|
#include "base/types.h"
|
|
#include "base/utils/fs.h"
|
|
#include "base/utils/io.h"
|
|
#include "base/utils/string.h"
|
|
#include "common.h"
|
|
#include "downloadpriority.h"
|
|
#include "extensiondata.h"
|
|
#include "filesearcher.h"
|
|
#include "loadtorrentparams.h"
|
|
#include "ltqbitarray.h"
|
|
#include "lttypecast.h"
|
|
#include "peeraddress.h"
|
|
#include "peerinfo.h"
|
|
#include "sessionimpl.h"
|
|
#include "trackerentry.h"
|
|
|
|
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
|
#include "base/utils/os.h"
|
|
#endif // Q_OS_MACOS || Q_OS_WIN
|
|
|
|
#ifndef QBT_USES_LIBTORRENT2
|
|
#include "customstorage.h"
|
|
#endif
|
|
|
|
using namespace BitTorrent;
|
|
|
|
namespace
|
|
{
|
|
lt::announce_entry makeNativeAnnounceEntry(const QString &url, const int tier)
|
|
{
|
|
lt::announce_entry entry {url.toStdString()};
|
|
entry.tier = tier;
|
|
return entry;
|
|
}
|
|
|
|
QString toString(const lt::tcp::endpoint <TCPEndpoint)
|
|
{
|
|
static QCache<lt::tcp::endpoint, QString> cache;
|
|
|
|
if (const QString *endpointName = cache.object(ltTCPEndpoint))
|
|
return *endpointName;
|
|
|
|
const auto endpointName = Utils::String::fromLatin1((std::ostringstream() << ltTCPEndpoint).str());
|
|
cache.insert(ltTCPEndpoint, new QString(endpointName));
|
|
return endpointName;
|
|
}
|
|
|
|
void updateTrackerEntryStatus(TrackerEntryStatus &trackerEntryStatus, const lt::announce_entry &nativeEntry
|
|
, const QSet<int> &btProtocols, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
|
|
{
|
|
Q_ASSERT(trackerEntryStatus.url == QString::fromStdString(nativeEntry.url));
|
|
|
|
trackerEntryStatus.tier = nativeEntry.tier;
|
|
|
|
const auto numEndpoints = static_cast<qsizetype>(nativeEntry.endpoints.size()) * btProtocols.size();
|
|
|
|
int numUpdating = 0;
|
|
int numWorking = 0;
|
|
int numNotWorking = 0;
|
|
int numTrackerError = 0;
|
|
int numUnreachable = 0;
|
|
|
|
for (const lt::announce_endpoint <AnnounceEndpoint : nativeEntry.endpoints)
|
|
{
|
|
const auto endpointName = toString(ltAnnounceEndpoint.local_endpoint);
|
|
|
|
for (const auto protocolVersion : btProtocols)
|
|
{
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
Q_ASSERT((protocolVersion == 1) || (protocolVersion == 2));
|
|
const auto ltProtocolVersion = (protocolVersion == 1) ? lt::protocol_version::V1 : lt::protocol_version::V2;
|
|
const lt::announce_infohash <AnnounceInfo = ltAnnounceEndpoint.info_hashes[ltProtocolVersion];
|
|
#else
|
|
Q_ASSERT(protocolVersion == 1);
|
|
const lt::announce_endpoint <AnnounceInfo = ltAnnounceEndpoint;
|
|
#endif
|
|
const QMap<int, int> &endpointUpdateInfo = updateInfo[ltAnnounceEndpoint.local_endpoint];
|
|
TrackerEndpointStatus &trackerEndpointStatus = trackerEntryStatus.endpoints[std::make_pair(endpointName, protocolVersion)];
|
|
|
|
trackerEndpointStatus.name = endpointName;
|
|
trackerEndpointStatus.btVersion = protocolVersion;
|
|
trackerEndpointStatus.numPeers = endpointUpdateInfo.value(protocolVersion, trackerEndpointStatus.numPeers);
|
|
trackerEndpointStatus.numSeeds = ltAnnounceInfo.scrape_complete;
|
|
trackerEndpointStatus.numLeeches = ltAnnounceInfo.scrape_incomplete;
|
|
trackerEndpointStatus.numDownloaded = ltAnnounceInfo.scrape_downloaded;
|
|
trackerEndpointStatus.nextAnnounceTime = ltAnnounceInfo.next_announce;
|
|
trackerEndpointStatus.minAnnounceTime = ltAnnounceInfo.min_announce;
|
|
|
|
if (ltAnnounceInfo.updating)
|
|
{
|
|
trackerEndpointStatus.isUpdating = true;
|
|
++numUpdating;
|
|
}
|
|
else
|
|
{
|
|
trackerEndpointStatus.isUpdating = false;
|
|
|
|
if (ltAnnounceInfo.fails > 0)
|
|
{
|
|
if (ltAnnounceInfo.last_error == lt::errors::tracker_failure)
|
|
{
|
|
trackerEndpointStatus.state = TrackerEndpointState::TrackerError;
|
|
++numTrackerError;
|
|
}
|
|
else if (ltAnnounceInfo.last_error == lt::errors::announce_skipped)
|
|
{
|
|
trackerEndpointStatus.state = TrackerEndpointState::Unreachable;
|
|
++numUnreachable;
|
|
}
|
|
else
|
|
{
|
|
trackerEndpointStatus.state = TrackerEndpointState::NotWorking;
|
|
++numNotWorking;
|
|
}
|
|
}
|
|
else if (nativeEntry.verified)
|
|
{
|
|
trackerEndpointStatus.state = TrackerEndpointState::Working;
|
|
++numWorking;
|
|
}
|
|
else
|
|
{
|
|
trackerEndpointStatus.state = TrackerEndpointState::NotContacted;
|
|
}
|
|
}
|
|
|
|
if (!ltAnnounceInfo.message.empty())
|
|
{
|
|
trackerEndpointStatus.message = QString::fromStdString(ltAnnounceInfo.message);
|
|
}
|
|
else if (ltAnnounceInfo.last_error)
|
|
{
|
|
trackerEndpointStatus.message = QString::fromLocal8Bit(ltAnnounceInfo.last_error.message());
|
|
}
|
|
else
|
|
{
|
|
trackerEndpointStatus.message.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (trackerEntryStatus.endpoints.size() > numEndpoints)
|
|
{
|
|
// remove outdated endpoints
|
|
trackerEntryStatus.endpoints.removeIf([&nativeEntry](const QHash<std::pair<QString, int>, TrackerEndpointStatus>::iterator &iter)
|
|
{
|
|
return std::none_of(nativeEntry.endpoints.cbegin(), nativeEntry.endpoints.cend()
|
|
, [&endpointName = std::get<0>(iter.key())](const auto &existingEndpoint)
|
|
{
|
|
return (endpointName == toString(existingEndpoint.local_endpoint));
|
|
});
|
|
});
|
|
}
|
|
|
|
if (numEndpoints > 0)
|
|
{
|
|
if (numUpdating > 0)
|
|
{
|
|
trackerEntryStatus.isUpdating = true;
|
|
}
|
|
else
|
|
{
|
|
trackerEntryStatus.isUpdating = false;
|
|
|
|
if (numWorking > 0)
|
|
{
|
|
trackerEntryStatus.state = TrackerEndpointState::Working;
|
|
}
|
|
else if (numTrackerError > 0)
|
|
{
|
|
trackerEntryStatus.state = TrackerEndpointState::TrackerError;
|
|
}
|
|
else if (numUnreachable == numEndpoints)
|
|
{
|
|
trackerEntryStatus.state = TrackerEndpointState::Unreachable;
|
|
}
|
|
else if ((numUnreachable + numNotWorking) == numEndpoints)
|
|
{
|
|
trackerEntryStatus.state = TrackerEndpointState::NotWorking;
|
|
}
|
|
}
|
|
}
|
|
|
|
trackerEntryStatus.numPeers = -1;
|
|
trackerEntryStatus.numSeeds = -1;
|
|
trackerEntryStatus.numLeeches = -1;
|
|
trackerEntryStatus.numDownloaded = -1;
|
|
trackerEntryStatus.nextAnnounceTime = {};
|
|
trackerEntryStatus.minAnnounceTime = {};
|
|
trackerEntryStatus.message.clear();
|
|
|
|
for (const TrackerEndpointStatus &endpointStatus : asConst(trackerEntryStatus.endpoints))
|
|
{
|
|
trackerEntryStatus.numPeers = std::max(trackerEntryStatus.numPeers, endpointStatus.numPeers);
|
|
trackerEntryStatus.numSeeds = std::max(trackerEntryStatus.numSeeds, endpointStatus.numSeeds);
|
|
trackerEntryStatus.numLeeches = std::max(trackerEntryStatus.numLeeches, endpointStatus.numLeeches);
|
|
trackerEntryStatus.numDownloaded = std::max(trackerEntryStatus.numDownloaded, endpointStatus.numDownloaded);
|
|
|
|
if (endpointStatus.state == trackerEntryStatus.state)
|
|
{
|
|
if ((trackerEntryStatus.nextAnnounceTime == AnnounceTimePoint()) || (trackerEntryStatus.nextAnnounceTime > endpointStatus.nextAnnounceTime))
|
|
{
|
|
trackerEntryStatus.nextAnnounceTime = endpointStatus.nextAnnounceTime;
|
|
trackerEntryStatus.minAnnounceTime = endpointStatus.minAnnounceTime;
|
|
if ((endpointStatus.state != TrackerEndpointState::Working)
|
|
|| !endpointStatus.message.isEmpty())
|
|
{
|
|
trackerEntryStatus.message = endpointStatus.message;
|
|
}
|
|
}
|
|
|
|
if (endpointStatus.state == TrackerEndpointState::Working)
|
|
{
|
|
if (trackerEntryStatus.message.isEmpty())
|
|
trackerEntryStatus.message = endpointStatus.message;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename Vector>
|
|
Vector resized(const Vector &inVector, const typename Vector::size_type size, const typename Vector::value_type &defaultValue)
|
|
{
|
|
Vector outVector = inVector;
|
|
outVector.resize(size, defaultValue);
|
|
return outVector;
|
|
}
|
|
|
|
// This is an imitation of limit normalization performed by libtorrent itself.
|
|
// We need perform it to keep cached values in line with the ones used by libtorrent.
|
|
int cleanLimitValue(const int value)
|
|
{
|
|
return ((value < 0) || (value == std::numeric_limits<int>::max())) ? 0 : value;
|
|
}
|
|
}
|
|
|
|
// TorrentImpl
|
|
|
|
TorrentImpl::TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, LoadTorrentParams params)
|
|
: Torrent(session)
|
|
, m_session(session)
|
|
, m_nativeHandle(nativeHandle)
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
, m_infoHash(m_nativeHandle.info_hashes())
|
|
#else
|
|
, m_infoHash(m_nativeHandle.info_hash())
|
|
#endif
|
|
, m_name(params.name)
|
|
, m_savePath(params.savePath)
|
|
, m_downloadPath(params.downloadPath)
|
|
, m_category(params.category)
|
|
, m_tags(params.tags)
|
|
, m_ratioLimit(params.ratioLimit)
|
|
, m_seedingTimeLimit(params.seedingTimeLimit)
|
|
, m_inactiveSeedingTimeLimit(params.inactiveSeedingTimeLimit)
|
|
, m_shareLimitAction(params.shareLimitAction)
|
|
, m_operatingMode(params.operatingMode)
|
|
, m_contentLayout(params.contentLayout)
|
|
, m_hasFinishedStatus(params.hasFinishedStatus)
|
|
, m_hasFirstLastPiecePriority(params.firstLastPiecePriority)
|
|
, m_useAutoTMM(params.useAutoTMM)
|
|
, m_isStopped(params.stopped)
|
|
, m_sslParams(params.sslParameters)
|
|
, m_ltAddTorrentParams(std::move(params.ltAddTorrentParams))
|
|
, m_downloadLimit(cleanLimitValue(m_ltAddTorrentParams.download_limit))
|
|
, m_uploadLimit(cleanLimitValue(m_ltAddTorrentParams.upload_limit))
|
|
{
|
|
if (m_ltAddTorrentParams.ti)
|
|
{
|
|
if (const std::time_t creationDate = m_ltAddTorrentParams.ti->creation_date(); creationDate > 0)
|
|
m_creationDate = QDateTime::fromSecsSinceEpoch(creationDate);
|
|
m_creator = QString::fromStdString(m_ltAddTorrentParams.ti->creator());
|
|
m_comment = QString::fromStdString(m_ltAddTorrentParams.ti->comment());
|
|
|
|
// Initialize it only if torrent is added with metadata.
|
|
// Otherwise it should be initialized in "Metadata received" handler.
|
|
m_torrentInfo = TorrentInfo(*m_ltAddTorrentParams.ti);
|
|
|
|
Q_ASSERT(m_filePaths.isEmpty());
|
|
Q_ASSERT(m_indexMap.isEmpty());
|
|
const int filesCount = m_torrentInfo.filesCount();
|
|
m_filePaths.reserve(filesCount);
|
|
m_indexMap.reserve(filesCount);
|
|
m_filePriorities.reserve(filesCount);
|
|
const std::vector<lt::download_priority_t> filePriorities =
|
|
resized(m_ltAddTorrentParams.file_priorities, m_ltAddTorrentParams.ti->num_files()
|
|
, LT::toNative(m_ltAddTorrentParams.file_priorities.empty() ? DownloadPriority::Normal : DownloadPriority::Ignored));
|
|
|
|
m_completedFiles.fill(static_cast<bool>(m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode), filesCount);
|
|
m_filesProgress.resize(filesCount);
|
|
|
|
for (int i = 0; i < filesCount; ++i)
|
|
{
|
|
const lt::file_index_t nativeIndex = m_torrentInfo.nativeIndexes().at(i);
|
|
m_indexMap[nativeIndex] = i;
|
|
|
|
const auto fileIter = m_ltAddTorrentParams.renamed_files.find(nativeIndex);
|
|
const Path filePath = ((fileIter != m_ltAddTorrentParams.renamed_files.end())
|
|
? makeUserPath(Path(fileIter->second)) : m_torrentInfo.filePath(i));
|
|
m_filePaths.append(filePath);
|
|
|
|
const auto priority = LT::fromNative(filePriorities[LT::toUnderlyingType(nativeIndex)]);
|
|
m_filePriorities.append(priority);
|
|
}
|
|
}
|
|
|
|
setStopCondition(params.stopCondition);
|
|
|
|
const auto *extensionData = static_cast<ExtensionData *>(m_ltAddTorrentParams.userdata);
|
|
m_trackerEntryStatuses.reserve(static_cast<decltype(m_trackerEntryStatuses)::size_type>(extensionData->trackers.size()));
|
|
for (const lt::announce_entry &announceEntry : extensionData->trackers)
|
|
m_trackerEntryStatuses.append({QString::fromStdString(announceEntry.url), announceEntry.tier});
|
|
m_urlSeeds.reserve(static_cast<decltype(m_urlSeeds)::size_type>(extensionData->urlSeeds.size()));
|
|
for (const std::string &urlSeed : extensionData->urlSeeds)
|
|
m_urlSeeds.append(QString::fromStdString(urlSeed));
|
|
m_nativeStatus = extensionData->status;
|
|
|
|
m_addedTime = QDateTime::fromSecsSinceEpoch(m_nativeStatus.added_time);
|
|
if (m_nativeStatus.completed_time > 0)
|
|
m_completedTime = QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time);
|
|
if (m_nativeStatus.last_seen_complete > 0)
|
|
m_lastSeenComplete = QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete);
|
|
|
|
if (hasMetadata())
|
|
updateProgress();
|
|
|
|
updateState();
|
|
|
|
if (hasMetadata())
|
|
applyFirstLastPiecePriority(m_hasFirstLastPiecePriority);
|
|
}
|
|
|
|
TorrentImpl::~TorrentImpl() = default;
|
|
|
|
bool TorrentImpl::isValid() const
|
|
{
|
|
return m_nativeHandle.is_valid();
|
|
}
|
|
|
|
Session *TorrentImpl::session() const
|
|
{
|
|
return m_session;
|
|
}
|
|
|
|
InfoHash TorrentImpl::infoHash() const
|
|
{
|
|
return m_infoHash;
|
|
}
|
|
|
|
QString TorrentImpl::name() const
|
|
{
|
|
if (!m_name.isEmpty())
|
|
return m_name;
|
|
|
|
if (hasMetadata())
|
|
return m_torrentInfo.name();
|
|
|
|
const QString name = QString::fromStdString(m_nativeStatus.name);
|
|
if (!name.isEmpty())
|
|
return name;
|
|
|
|
return id().toString();
|
|
}
|
|
|
|
QDateTime TorrentImpl::creationDate() const
|
|
{
|
|
return m_creationDate;
|
|
}
|
|
|
|
QString TorrentImpl::creator() const
|
|
{
|
|
return m_creator;
|
|
}
|
|
|
|
QString TorrentImpl::comment() const
|
|
{
|
|
return m_comment;
|
|
}
|
|
|
|
bool TorrentImpl::isPrivate() const
|
|
{
|
|
return m_torrentInfo.isPrivate();
|
|
}
|
|
|
|
qlonglong TorrentImpl::totalSize() const
|
|
{
|
|
return m_torrentInfo.totalSize();
|
|
}
|
|
|
|
// size without the "don't download" files
|
|
qlonglong TorrentImpl::wantedSize() const
|
|
{
|
|
return m_nativeStatus.total_wanted;
|
|
}
|
|
|
|
qlonglong TorrentImpl::completedSize() const
|
|
{
|
|
return m_nativeStatus.total_wanted_done;
|
|
}
|
|
|
|
qlonglong TorrentImpl::pieceLength() const
|
|
{
|
|
return m_torrentInfo.pieceLength();
|
|
}
|
|
|
|
qlonglong TorrentImpl::wastedSize() const
|
|
{
|
|
return (m_nativeStatus.total_failed_bytes + m_nativeStatus.total_redundant_bytes);
|
|
}
|
|
|
|
QString TorrentImpl::currentTracker() const
|
|
{
|
|
if (!m_nativeStatus.current_tracker.empty())
|
|
return QString::fromStdString(m_nativeStatus.current_tracker);
|
|
|
|
if (!m_trackerEntryStatuses.isEmpty())
|
|
return m_trackerEntryStatuses.constFirst().url;
|
|
|
|
return {};
|
|
}
|
|
|
|
Path TorrentImpl::savePath() const
|
|
{
|
|
return isAutoTMMEnabled() ? m_session->categorySavePath(category()) : m_savePath;
|
|
}
|
|
|
|
void TorrentImpl::setSavePath(const Path &path)
|
|
{
|
|
Q_ASSERT(!isAutoTMMEnabled());
|
|
if (isAutoTMMEnabled()) [[unlikely]]
|
|
return;
|
|
|
|
const Path basePath = m_session->useCategoryPathsInManualMode()
|
|
? m_session->categorySavePath(category()) : m_session->savePath();
|
|
const Path resolvedPath = (path.isAbsolute() ? path : (basePath / path));
|
|
if (resolvedPath == savePath())
|
|
return;
|
|
|
|
if (isFinished() || m_hasFinishedStatus || downloadPath().isEmpty())
|
|
{
|
|
moveStorage(resolvedPath, MoveStorageContext::ChangeSavePath);
|
|
}
|
|
else
|
|
{
|
|
m_savePath = resolvedPath;
|
|
m_session->handleTorrentSavePathChanged(this);
|
|
deferredRequestResumeData();
|
|
}
|
|
}
|
|
|
|
Path TorrentImpl::downloadPath() const
|
|
{
|
|
return isAutoTMMEnabled() ? m_session->categoryDownloadPath(category()) : m_downloadPath;
|
|
}
|
|
|
|
void TorrentImpl::setDownloadPath(const Path &path)
|
|
{
|
|
Q_ASSERT(!isAutoTMMEnabled());
|
|
if (isAutoTMMEnabled()) [[unlikely]]
|
|
return;
|
|
|
|
const Path basePath = m_session->useCategoryPathsInManualMode()
|
|
? m_session->categoryDownloadPath(category()) : m_session->downloadPath();
|
|
const Path resolvedPath = (path.isEmpty() || path.isAbsolute()) ? path : (basePath / path);
|
|
if (resolvedPath == m_downloadPath)
|
|
return;
|
|
|
|
const bool isIncomplete = !(isFinished() || m_hasFinishedStatus);
|
|
if (isIncomplete)
|
|
{
|
|
moveStorage((resolvedPath.isEmpty() ? savePath() : resolvedPath), MoveStorageContext::ChangeDownloadPath);
|
|
}
|
|
else
|
|
{
|
|
m_downloadPath = resolvedPath;
|
|
m_session->handleTorrentSavePathChanged(this);
|
|
deferredRequestResumeData();
|
|
}
|
|
}
|
|
|
|
Path TorrentImpl::rootPath() const
|
|
{
|
|
if (!hasMetadata())
|
|
return {};
|
|
|
|
const Path relativeRootPath = Path::findRootFolder(filePaths());
|
|
if (relativeRootPath.isEmpty())
|
|
return {};
|
|
|
|
return (actualStorageLocation() / relativeRootPath);
|
|
}
|
|
|
|
Path TorrentImpl::contentPath() const
|
|
{
|
|
if (!hasMetadata())
|
|
return {};
|
|
|
|
if (filesCount() == 1)
|
|
return (actualStorageLocation() / filePath(0));
|
|
|
|
const Path rootPath = this->rootPath();
|
|
return (rootPath.isEmpty() ? actualStorageLocation() : rootPath);
|
|
}
|
|
|
|
bool TorrentImpl::isAutoTMMEnabled() const
|
|
{
|
|
return m_useAutoTMM;
|
|
}
|
|
|
|
void TorrentImpl::setAutoTMMEnabled(bool enabled)
|
|
{
|
|
if (m_useAutoTMM == enabled)
|
|
return;
|
|
|
|
m_useAutoTMM = enabled;
|
|
if (!m_useAutoTMM)
|
|
{
|
|
m_savePath = m_session->categorySavePath(category());
|
|
m_downloadPath = m_session->categoryDownloadPath(category());
|
|
}
|
|
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentSavingModeChanged(this);
|
|
|
|
adjustStorageLocation();
|
|
}
|
|
|
|
Path TorrentImpl::actualStorageLocation() const
|
|
{
|
|
if (!hasMetadata())
|
|
return {};
|
|
|
|
return Path(m_nativeStatus.save_path);
|
|
}
|
|
|
|
void TorrentImpl::setAutoManaged(const bool enable)
|
|
{
|
|
if (enable)
|
|
m_nativeHandle.set_flags(lt::torrent_flags::auto_managed);
|
|
else
|
|
m_nativeHandle.unset_flags(lt::torrent_flags::auto_managed);
|
|
}
|
|
|
|
Path TorrentImpl::makeActualPath(int index, const Path &path) const
|
|
{
|
|
Path actualPath = path;
|
|
|
|
if (m_session->isAppendExtensionEnabled()
|
|
&& (fileSize(index) > 0) && !m_completedFiles.at(index))
|
|
{
|
|
actualPath += QB_EXT;
|
|
}
|
|
|
|
if (m_session->isUnwantedFolderEnabled()
|
|
&& (m_filePriorities[index] == DownloadPriority::Ignored))
|
|
{
|
|
const Path parentPath = actualPath.parentPath();
|
|
const QString fileName = actualPath.filename();
|
|
actualPath = parentPath / Path(UNWANTED_FOLDER_NAME) / Path(fileName);
|
|
}
|
|
|
|
return actualPath;
|
|
}
|
|
|
|
Path TorrentImpl::makeUserPath(const Path &path) const
|
|
{
|
|
Path userPath = path.removedExtension(QB_EXT);
|
|
|
|
const Path parentRelPath = userPath.parentPath();
|
|
if (parentRelPath.filename() == UNWANTED_FOLDER_NAME)
|
|
{
|
|
const QString fileName = userPath.filename();
|
|
const Path relPath = parentRelPath.parentPath();
|
|
userPath = relPath / Path(fileName);
|
|
}
|
|
|
|
return userPath;
|
|
}
|
|
|
|
QList<TrackerEntryStatus> TorrentImpl::trackers() const
|
|
{
|
|
return m_trackerEntryStatuses;
|
|
}
|
|
|
|
void TorrentImpl::addTrackers(QList<TrackerEntry> trackers)
|
|
{
|
|
trackers.removeIf([](const TrackerEntry &trackerEntry) { return trackerEntry.url.isEmpty(); });
|
|
|
|
QSet<TrackerEntry> currentTrackerSet;
|
|
currentTrackerSet.reserve(m_trackerEntryStatuses.size());
|
|
for (const TrackerEntryStatus &status : asConst(m_trackerEntryStatuses))
|
|
currentTrackerSet.insert({.url = status.url, .tier = status.tier});
|
|
|
|
const auto newTrackerSet = QSet<TrackerEntry>(trackers.cbegin(), trackers.cend()) - currentTrackerSet;
|
|
if (newTrackerSet.isEmpty())
|
|
return;
|
|
|
|
trackers = QList<TrackerEntry>(newTrackerSet.cbegin(), newTrackerSet.cend());
|
|
for (const TrackerEntry &tracker : asConst(trackers))
|
|
{
|
|
m_nativeHandle.add_tracker(makeNativeAnnounceEntry(tracker.url, tracker.tier));
|
|
m_trackerEntryStatuses.append({tracker.url, tracker.tier});
|
|
}
|
|
std::sort(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end()
|
|
, [](const TrackerEntryStatus &left, const TrackerEntryStatus &right) { return left.tier < right.tier; });
|
|
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentTrackersAdded(this, trackers);
|
|
}
|
|
|
|
void TorrentImpl::removeTrackers(const QStringList &trackers)
|
|
{
|
|
QStringList removedTrackers = trackers;
|
|
for (const QString &tracker : trackers)
|
|
{
|
|
if (!m_trackerEntryStatuses.removeOne({tracker}))
|
|
removedTrackers.removeOne(tracker);
|
|
}
|
|
|
|
std::vector<lt::announce_entry> nativeTrackers;
|
|
nativeTrackers.reserve(m_trackerEntryStatuses.size());
|
|
for (const TrackerEntryStatus &tracker : asConst(m_trackerEntryStatuses))
|
|
nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier));
|
|
|
|
if (!removedTrackers.isEmpty())
|
|
{
|
|
m_nativeHandle.replace_trackers(nativeTrackers);
|
|
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentTrackersRemoved(this, removedTrackers);
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::replaceTrackers(QList<TrackerEntry> trackers)
|
|
{
|
|
trackers.removeIf([](const TrackerEntry &trackerEntry) { return trackerEntry.url.isEmpty(); });
|
|
|
|
// Filter out duplicate trackers
|
|
const auto uniqueTrackers = QSet<TrackerEntry>(trackers.cbegin(), trackers.cend());
|
|
trackers = QList<TrackerEntry>(uniqueTrackers.cbegin(), uniqueTrackers.cend());
|
|
std::sort(trackers.begin(), trackers.end()
|
|
, [](const TrackerEntry &left, const TrackerEntry &right) { return left.tier < right.tier; });
|
|
|
|
std::vector<lt::announce_entry> nativeTrackers;
|
|
nativeTrackers.reserve(trackers.size());
|
|
m_trackerEntryStatuses.clear();
|
|
|
|
for (const TrackerEntry &tracker : trackers)
|
|
{
|
|
nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier));
|
|
m_trackerEntryStatuses.append({tracker.url, tracker.tier});
|
|
}
|
|
|
|
m_nativeHandle.replace_trackers(nativeTrackers);
|
|
|
|
// Clear the peer list if it's a private torrent since
|
|
// we do not want to keep connecting with peers from old tracker.
|
|
if (isPrivate())
|
|
clearPeers();
|
|
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentTrackersChanged(this);
|
|
}
|
|
|
|
QList<QUrl> TorrentImpl::urlSeeds() const
|
|
{
|
|
return m_urlSeeds;
|
|
}
|
|
|
|
void TorrentImpl::addUrlSeeds(const QList<QUrl> &urlSeeds)
|
|
{
|
|
m_session->invokeAsync([urlSeeds, session = m_session
|
|
, nativeHandle = m_nativeHandle
|
|
, thisTorrent = QPointer<TorrentImpl>(this)]
|
|
{
|
|
try
|
|
{
|
|
const std::set<std::string> nativeSeeds = nativeHandle.url_seeds();
|
|
QList<QUrl> currentSeeds;
|
|
currentSeeds.reserve(static_cast<decltype(currentSeeds)::size_type>(nativeSeeds.size()));
|
|
for (const std::string &urlSeed : nativeSeeds)
|
|
currentSeeds.append(QString::fromStdString(urlSeed));
|
|
|
|
QList<QUrl> addedUrlSeeds;
|
|
addedUrlSeeds.reserve(urlSeeds.size());
|
|
|
|
for (const QUrl &url : urlSeeds)
|
|
{
|
|
if (!currentSeeds.contains(url))
|
|
{
|
|
nativeHandle.add_url_seed(url.toString().toStdString());
|
|
addedUrlSeeds.append(url);
|
|
}
|
|
}
|
|
|
|
currentSeeds.append(addedUrlSeeds);
|
|
session->invoke([session, thisTorrent, currentSeeds, addedUrlSeeds]
|
|
{
|
|
if (!thisTorrent)
|
|
return;
|
|
|
|
thisTorrent->m_urlSeeds = currentSeeds;
|
|
if (!addedUrlSeeds.isEmpty())
|
|
{
|
|
thisTorrent->deferredRequestResumeData();
|
|
session->handleTorrentUrlSeedsAdded(thisTorrent, addedUrlSeeds);
|
|
}
|
|
});
|
|
}
|
|
catch (const std::exception &) {}
|
|
});
|
|
}
|
|
|
|
void TorrentImpl::removeUrlSeeds(const QList<QUrl> &urlSeeds)
|
|
{
|
|
m_session->invokeAsync([urlSeeds, session = m_session
|
|
, nativeHandle = m_nativeHandle
|
|
, thisTorrent = QPointer<TorrentImpl>(this)]
|
|
{
|
|
try
|
|
{
|
|
const std::set<std::string> nativeSeeds = nativeHandle.url_seeds();
|
|
QList<QUrl> currentSeeds;
|
|
currentSeeds.reserve(static_cast<decltype(currentSeeds)::size_type>(nativeSeeds.size()));
|
|
for (const std::string &urlSeed : nativeSeeds)
|
|
currentSeeds.append(QString::fromStdString(urlSeed));
|
|
|
|
QList<QUrl> removedUrlSeeds;
|
|
removedUrlSeeds.reserve(urlSeeds.size());
|
|
|
|
for (const QUrl &url : urlSeeds)
|
|
{
|
|
if (currentSeeds.removeOne(url))
|
|
{
|
|
nativeHandle.remove_url_seed(url.toString().toStdString());
|
|
removedUrlSeeds.append(url);
|
|
}
|
|
}
|
|
|
|
session->invoke([session, thisTorrent, currentSeeds, removedUrlSeeds]
|
|
{
|
|
if (!thisTorrent)
|
|
return;
|
|
|
|
thisTorrent->m_urlSeeds = currentSeeds;
|
|
|
|
if (!removedUrlSeeds.isEmpty())
|
|
{
|
|
thisTorrent->deferredRequestResumeData();
|
|
session->handleTorrentUrlSeedsRemoved(thisTorrent, removedUrlSeeds);
|
|
}
|
|
});
|
|
}
|
|
catch (const std::exception &) {}
|
|
});
|
|
}
|
|
|
|
void TorrentImpl::clearPeers()
|
|
{
|
|
m_nativeHandle.clear_peers();
|
|
}
|
|
|
|
bool TorrentImpl::connectPeer(const PeerAddress &peerAddress)
|
|
{
|
|
lt::error_code ec;
|
|
const lt::address addr = lt::make_address(peerAddress.ip.toString().toStdString(), ec);
|
|
if (ec) return false;
|
|
|
|
const lt::tcp::endpoint endpoint(addr, peerAddress.port);
|
|
try
|
|
{
|
|
m_nativeHandle.connect_peer(endpoint);
|
|
}
|
|
catch (const lt::system_error &err)
|
|
{
|
|
LogMsg(tr("Failed to add peer \"%1\" to torrent \"%2\". Reason: %3")
|
|
.arg(peerAddress.toString(), name(), QString::fromLocal8Bit(err.what())), Log::WARNING);
|
|
return false;
|
|
}
|
|
|
|
LogMsg(tr("Peer \"%1\" is added to torrent \"%2\"").arg(peerAddress.toString(), name()));
|
|
return true;
|
|
}
|
|
|
|
bool TorrentImpl::needSaveResumeData() const
|
|
{
|
|
return m_nativeStatus.need_save_resume;
|
|
}
|
|
|
|
void TorrentImpl::requestResumeData(const lt::resume_data_flags_t flags)
|
|
{
|
|
m_nativeHandle.save_resume_data(flags);
|
|
m_deferredRequestResumeDataInvoked = false;
|
|
|
|
m_session->handleTorrentResumeDataRequested(this);
|
|
}
|
|
|
|
void TorrentImpl::deferredRequestResumeData()
|
|
{
|
|
if (!m_deferredRequestResumeDataInvoked)
|
|
{
|
|
QMetaObject::invokeMethod(this, [this]
|
|
{
|
|
requestResumeData((m_maintenanceJob == MaintenanceJob::HandleMetadata)
|
|
? lt::torrent_handle::save_info_dict : lt::resume_data_flags_t());
|
|
}, Qt::QueuedConnection);
|
|
|
|
m_deferredRequestResumeDataInvoked = true;
|
|
}
|
|
}
|
|
|
|
int TorrentImpl::filesCount() const
|
|
{
|
|
return m_torrentInfo.filesCount();
|
|
}
|
|
|
|
int TorrentImpl::piecesCount() const
|
|
{
|
|
return m_torrentInfo.piecesCount();
|
|
}
|
|
|
|
int TorrentImpl::piecesHave() const
|
|
{
|
|
return m_nativeStatus.num_pieces;
|
|
}
|
|
|
|
qreal TorrentImpl::progress() const
|
|
{
|
|
if (isChecking())
|
|
return m_nativeStatus.progress;
|
|
|
|
if (m_nativeStatus.total_wanted == 0)
|
|
return 0.;
|
|
|
|
if (m_nativeStatus.total_wanted_done == m_nativeStatus.total_wanted)
|
|
return 1.;
|
|
|
|
const qreal progress = static_cast<qreal>(m_nativeStatus.total_wanted_done) / m_nativeStatus.total_wanted;
|
|
if ((progress < 0.f) || (progress > 1.f))
|
|
{
|
|
LogMsg(tr("Unexpected data detected. Torrent: %1. Data: total_wanted=%2 total_wanted_done=%3.")
|
|
.arg(name(), QString::number(m_nativeStatus.total_wanted), QString::number(m_nativeStatus.total_wanted_done))
|
|
, Log::WARNING);
|
|
}
|
|
|
|
return progress;
|
|
}
|
|
|
|
QString TorrentImpl::category() const
|
|
{
|
|
return m_category;
|
|
}
|
|
|
|
bool TorrentImpl::belongsToCategory(const QString &category) const
|
|
{
|
|
if (m_category.isEmpty())
|
|
return category.isEmpty();
|
|
|
|
if (m_category == category)
|
|
return true;
|
|
|
|
return (m_session->isSubcategoriesEnabled() && m_category.startsWith(category + u'/'));
|
|
}
|
|
|
|
TagSet TorrentImpl::tags() const
|
|
{
|
|
return m_tags;
|
|
}
|
|
|
|
bool TorrentImpl::hasTag(const Tag &tag) const
|
|
{
|
|
return m_tags.contains(tag);
|
|
}
|
|
|
|
bool TorrentImpl::addTag(const Tag &tag)
|
|
{
|
|
if (!tag.isValid())
|
|
return false;
|
|
if (hasTag(tag))
|
|
return false;
|
|
|
|
if (!m_session->hasTag(tag))
|
|
{
|
|
if (!m_session->addTag(tag))
|
|
return false;
|
|
}
|
|
m_tags.insert(tag);
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentTagAdded(this, tag);
|
|
return true;
|
|
}
|
|
|
|
bool TorrentImpl::removeTag(const Tag &tag)
|
|
{
|
|
if (m_tags.remove(tag))
|
|
{
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentTagRemoved(this, tag);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void TorrentImpl::removeAllTags()
|
|
{
|
|
for (const Tag &tag : asConst(tags()))
|
|
removeTag(tag);
|
|
}
|
|
|
|
QDateTime TorrentImpl::addedTime() const
|
|
{
|
|
return m_addedTime;
|
|
}
|
|
|
|
QDateTime TorrentImpl::completedTime() const
|
|
{
|
|
return m_completedTime;
|
|
}
|
|
|
|
QDateTime TorrentImpl::lastSeenComplete() const
|
|
{
|
|
return m_lastSeenComplete;
|
|
}
|
|
|
|
qlonglong TorrentImpl::activeTime() const
|
|
{
|
|
return lt::total_seconds(m_nativeStatus.active_duration);
|
|
}
|
|
|
|
qlonglong TorrentImpl::finishedTime() const
|
|
{
|
|
return lt::total_seconds(m_nativeStatus.finished_duration);
|
|
}
|
|
|
|
qlonglong TorrentImpl::timeSinceUpload() const
|
|
{
|
|
if (m_nativeStatus.last_upload.time_since_epoch().count() == 0)
|
|
return -1;
|
|
|
|
return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_upload);
|
|
}
|
|
|
|
qlonglong TorrentImpl::timeSinceDownload() const
|
|
{
|
|
if (m_nativeStatus.last_download.time_since_epoch().count() == 0)
|
|
return -1;
|
|
|
|
return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_download);
|
|
}
|
|
|
|
qlonglong TorrentImpl::timeSinceActivity() const
|
|
{
|
|
const qlonglong upTime = timeSinceUpload();
|
|
const qlonglong downTime = timeSinceDownload();
|
|
return ((upTime < 0) != (downTime < 0))
|
|
? std::max(upTime, downTime)
|
|
: std::min(upTime, downTime);
|
|
}
|
|
|
|
qreal TorrentImpl::ratioLimit() const
|
|
{
|
|
return m_ratioLimit;
|
|
}
|
|
|
|
int TorrentImpl::seedingTimeLimit() const
|
|
{
|
|
return m_seedingTimeLimit;
|
|
}
|
|
|
|
int TorrentImpl::inactiveSeedingTimeLimit() const
|
|
{
|
|
return m_inactiveSeedingTimeLimit;
|
|
}
|
|
|
|
Path TorrentImpl::filePath(const int index) const
|
|
{
|
|
Q_ASSERT(index >= 0);
|
|
Q_ASSERT(index < m_filePaths.size());
|
|
|
|
return m_filePaths.value(index, {});
|
|
}
|
|
|
|
Path TorrentImpl::actualFilePath(const int index) const
|
|
{
|
|
const QList<lt::file_index_t> nativeIndexes = m_torrentInfo.nativeIndexes();
|
|
|
|
Q_ASSERT(index >= 0);
|
|
Q_ASSERT(index < nativeIndexes.size());
|
|
if ((index < 0) || (index >= nativeIndexes.size()))
|
|
return {};
|
|
|
|
return Path(nativeTorrentInfo()->files().file_path(nativeIndexes[index]));
|
|
}
|
|
|
|
qlonglong TorrentImpl::fileSize(const int index) const
|
|
{
|
|
return m_torrentInfo.fileSize(index);
|
|
}
|
|
|
|
PathList TorrentImpl::filePaths() const
|
|
{
|
|
return m_filePaths;
|
|
}
|
|
|
|
PathList TorrentImpl::actualFilePaths() const
|
|
{
|
|
if (!hasMetadata())
|
|
return {};
|
|
|
|
PathList paths;
|
|
paths.reserve(filesCount());
|
|
|
|
const lt::file_storage files = nativeTorrentInfo()->files();
|
|
for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes()))
|
|
paths.emplaceBack(files.file_path(nativeIndex));
|
|
|
|
return paths;
|
|
}
|
|
|
|
QList<DownloadPriority> TorrentImpl::filePriorities() const
|
|
{
|
|
return m_filePriorities;
|
|
}
|
|
|
|
TorrentInfo TorrentImpl::info() const
|
|
{
|
|
return m_torrentInfo;
|
|
}
|
|
|
|
bool TorrentImpl::isStopped() const
|
|
{
|
|
return m_isStopped;
|
|
}
|
|
|
|
bool TorrentImpl::isQueued() const
|
|
{
|
|
if (!m_session->isQueueingSystemEnabled())
|
|
return false;
|
|
|
|
// Torrent is Queued if it isn't in Stopped state but paused internally
|
|
return (!isStopped()
|
|
&& (m_nativeStatus.flags & lt::torrent_flags::auto_managed)
|
|
&& (m_nativeStatus.flags & lt::torrent_flags::paused));
|
|
}
|
|
|
|
bool TorrentImpl::isChecking() const
|
|
{
|
|
return ((m_nativeStatus.state == lt::torrent_status::checking_files)
|
|
|| (m_nativeStatus.state == lt::torrent_status::checking_resume_data));
|
|
}
|
|
|
|
bool TorrentImpl::isDownloading() const
|
|
{
|
|
switch (m_state)
|
|
{
|
|
case TorrentState::Downloading:
|
|
case TorrentState::DownloadingMetadata:
|
|
case TorrentState::ForcedDownloadingMetadata:
|
|
case TorrentState::StalledDownloading:
|
|
case TorrentState::CheckingDownloading:
|
|
case TorrentState::StoppedDownloading:
|
|
case TorrentState::QueuedDownloading:
|
|
case TorrentState::ForcedDownloading:
|
|
return true;
|
|
default:
|
|
break;
|
|
};
|
|
|
|
return false;
|
|
}
|
|
|
|
bool TorrentImpl::isMoving() const
|
|
{
|
|
return m_state == TorrentState::Moving;
|
|
}
|
|
|
|
bool TorrentImpl::isUploading() const
|
|
{
|
|
switch (m_state)
|
|
{
|
|
case TorrentState::Uploading:
|
|
case TorrentState::StalledUploading:
|
|
case TorrentState::CheckingUploading:
|
|
case TorrentState::QueuedUploading:
|
|
case TorrentState::ForcedUploading:
|
|
return true;
|
|
default:
|
|
break;
|
|
};
|
|
|
|
return false;
|
|
}
|
|
|
|
bool TorrentImpl::isCompleted() const
|
|
{
|
|
switch (m_state)
|
|
{
|
|
case TorrentState::Uploading:
|
|
case TorrentState::StalledUploading:
|
|
case TorrentState::CheckingUploading:
|
|
case TorrentState::StoppedUploading:
|
|
case TorrentState::QueuedUploading:
|
|
case TorrentState::ForcedUploading:
|
|
return true;
|
|
default:
|
|
break;
|
|
};
|
|
|
|
return false;
|
|
}
|
|
|
|
bool TorrentImpl::isActive() const
|
|
{
|
|
switch (m_state)
|
|
{
|
|
case TorrentState::StalledDownloading:
|
|
return (uploadPayloadRate() > 0);
|
|
|
|
case TorrentState::DownloadingMetadata:
|
|
case TorrentState::ForcedDownloadingMetadata:
|
|
case TorrentState::Downloading:
|
|
case TorrentState::ForcedDownloading:
|
|
case TorrentState::Uploading:
|
|
case TorrentState::ForcedUploading:
|
|
case TorrentState::Moving:
|
|
return true;
|
|
|
|
default:
|
|
break;
|
|
};
|
|
|
|
return false;
|
|
}
|
|
|
|
bool TorrentImpl::isInactive() const
|
|
{
|
|
return !isActive();
|
|
}
|
|
|
|
bool TorrentImpl::isErrored() const
|
|
{
|
|
return ((m_state == TorrentState::MissingFiles)
|
|
|| (m_state == TorrentState::Error));
|
|
}
|
|
|
|
bool TorrentImpl::isFinished() const
|
|
{
|
|
return ((m_nativeStatus.state == lt::torrent_status::finished)
|
|
|| (m_nativeStatus.state == lt::torrent_status::seeding));
|
|
}
|
|
|
|
bool TorrentImpl::isForced() const
|
|
{
|
|
return (!isStopped() && (m_operatingMode == TorrentOperatingMode::Forced));
|
|
}
|
|
|
|
bool TorrentImpl::isSequentialDownload() const
|
|
{
|
|
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::sequential_download);
|
|
}
|
|
|
|
bool TorrentImpl::hasFirstLastPiecePriority() const
|
|
{
|
|
return m_hasFirstLastPiecePriority;
|
|
}
|
|
|
|
TorrentState TorrentImpl::state() const
|
|
{
|
|
return m_state;
|
|
}
|
|
|
|
void TorrentImpl::updateState()
|
|
{
|
|
if (m_nativeStatus.state == lt::torrent_status::checking_resume_data)
|
|
{
|
|
m_state = TorrentState::CheckingResumeData;
|
|
}
|
|
else if (isMoveInProgress())
|
|
{
|
|
m_state = TorrentState::Moving;
|
|
}
|
|
else if (hasMissingFiles())
|
|
{
|
|
m_state = TorrentState::MissingFiles;
|
|
}
|
|
else if (hasError())
|
|
{
|
|
m_state = TorrentState::Error;
|
|
}
|
|
else if (!hasMetadata())
|
|
{
|
|
if (isStopped())
|
|
m_state = TorrentState::StoppedDownloading;
|
|
else if (isQueued())
|
|
m_state = TorrentState::QueuedDownloading;
|
|
else
|
|
m_state = isForced() ? TorrentState::ForcedDownloadingMetadata : TorrentState::DownloadingMetadata;
|
|
}
|
|
else if ((m_nativeStatus.state == lt::torrent_status::checking_files) && !isStopped())
|
|
{
|
|
// If the torrent is not just in the "checking" state, but is being actually checked
|
|
m_state = m_hasFinishedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading;
|
|
}
|
|
else if (isFinished())
|
|
{
|
|
if (isStopped())
|
|
m_state = TorrentState::StoppedUploading;
|
|
else if (isQueued())
|
|
m_state = TorrentState::QueuedUploading;
|
|
else if (isForced())
|
|
m_state = TorrentState::ForcedUploading;
|
|
else if (m_nativeStatus.upload_payload_rate > 0)
|
|
m_state = TorrentState::Uploading;
|
|
else
|
|
m_state = TorrentState::StalledUploading;
|
|
}
|
|
else
|
|
{
|
|
if (isStopped())
|
|
m_state = TorrentState::StoppedDownloading;
|
|
else if (isQueued())
|
|
m_state = TorrentState::QueuedDownloading;
|
|
else if (isForced())
|
|
m_state = TorrentState::ForcedDownloading;
|
|
else if (m_nativeStatus.download_payload_rate > 0)
|
|
m_state = TorrentState::Downloading;
|
|
else
|
|
m_state = TorrentState::StalledDownloading;
|
|
}
|
|
}
|
|
|
|
bool TorrentImpl::hasMetadata() const
|
|
{
|
|
return m_torrentInfo.isValid();
|
|
}
|
|
|
|
bool TorrentImpl::hasMissingFiles() const
|
|
{
|
|
return m_hasMissingFiles;
|
|
}
|
|
|
|
bool TorrentImpl::hasError() const
|
|
{
|
|
return (m_nativeStatus.errc || (m_nativeStatus.flags & lt::torrent_flags::upload_mode));
|
|
}
|
|
|
|
int TorrentImpl::queuePosition() const
|
|
{
|
|
return static_cast<int>(m_nativeStatus.queue_position);
|
|
}
|
|
|
|
QString TorrentImpl::error() const
|
|
{
|
|
if (m_nativeStatus.errc)
|
|
return Utils::String::fromLocal8Bit(m_nativeStatus.errc.message());
|
|
|
|
if (m_nativeStatus.flags & lt::torrent_flags::upload_mode)
|
|
{
|
|
return tr("Couldn't write to file. Reason: \"%1\". Torrent is now in \"upload only\" mode.")
|
|
.arg(Utils::String::fromLocal8Bit(m_lastFileError.error.message()));
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
qlonglong TorrentImpl::totalDownload() const
|
|
{
|
|
return m_nativeStatus.all_time_download;
|
|
}
|
|
|
|
qlonglong TorrentImpl::totalUpload() const
|
|
{
|
|
return m_nativeStatus.all_time_upload;
|
|
}
|
|
|
|
qlonglong TorrentImpl::eta() const
|
|
{
|
|
if (isStopped()) return MAX_ETA;
|
|
|
|
const SpeedSampleAvg speedAverage = m_payloadRateMonitor.average();
|
|
|
|
if (isFinished())
|
|
{
|
|
const qreal maxRatioValue = maxRatio();
|
|
const int maxSeedingTimeValue = maxSeedingTime();
|
|
const int maxInactiveSeedingTimeValue = maxInactiveSeedingTime();
|
|
if ((maxRatioValue < 0) && (maxSeedingTimeValue < 0) && (maxInactiveSeedingTimeValue < 0)) return MAX_ETA;
|
|
|
|
qlonglong ratioEta = MAX_ETA;
|
|
|
|
if ((speedAverage.upload > 0) && (maxRatioValue >= 0))
|
|
{
|
|
|
|
qlonglong realDL = totalDownload();
|
|
if (realDL <= 0)
|
|
realDL = wantedSize();
|
|
|
|
ratioEta = ((realDL * maxRatioValue) - totalUpload()) / speedAverage.upload;
|
|
}
|
|
|
|
qlonglong seedingTimeEta = MAX_ETA;
|
|
|
|
if (maxSeedingTimeValue >= 0)
|
|
{
|
|
seedingTimeEta = (maxSeedingTimeValue * 60) - finishedTime();
|
|
if (seedingTimeEta < 0)
|
|
seedingTimeEta = 0;
|
|
}
|
|
|
|
qlonglong inactiveSeedingTimeEta = MAX_ETA;
|
|
|
|
if (maxInactiveSeedingTimeValue >= 0)
|
|
{
|
|
inactiveSeedingTimeEta = (maxInactiveSeedingTimeValue * 60) - timeSinceActivity();
|
|
inactiveSeedingTimeEta = std::max<qlonglong>(inactiveSeedingTimeEta, 0);
|
|
}
|
|
|
|
return std::min({ratioEta, seedingTimeEta, inactiveSeedingTimeEta});
|
|
}
|
|
|
|
if (!speedAverage.download) return MAX_ETA;
|
|
|
|
return (wantedSize() - completedSize()) / speedAverage.download;
|
|
}
|
|
|
|
QList<qreal> TorrentImpl::filesProgress() const
|
|
{
|
|
if (!hasMetadata())
|
|
return {};
|
|
|
|
const int count = m_filesProgress.size();
|
|
Q_ASSERT(count == filesCount());
|
|
if (count != filesCount()) [[unlikely]]
|
|
return {};
|
|
|
|
if (m_completedFiles.count(true) == count)
|
|
return QList<qreal>(count, 1);
|
|
|
|
QList<qreal> result;
|
|
result.reserve(count);
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
const int64_t progress = m_filesProgress.at(i);
|
|
const int64_t size = fileSize(i);
|
|
if ((size <= 0) || (progress == size))
|
|
result << 1;
|
|
else
|
|
result << (progress / static_cast<qreal>(size));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
int TorrentImpl::seedsCount() const
|
|
{
|
|
return m_nativeStatus.num_seeds;
|
|
}
|
|
|
|
int TorrentImpl::peersCount() const
|
|
{
|
|
return m_nativeStatus.num_peers;
|
|
}
|
|
|
|
int TorrentImpl::leechsCount() const
|
|
{
|
|
return (m_nativeStatus.num_peers - m_nativeStatus.num_seeds);
|
|
}
|
|
|
|
int TorrentImpl::totalSeedsCount() const
|
|
{
|
|
return (m_nativeStatus.num_complete > -1) ? m_nativeStatus.num_complete : m_nativeStatus.list_seeds;
|
|
}
|
|
|
|
int TorrentImpl::totalPeersCount() const
|
|
{
|
|
const int peers = m_nativeStatus.num_complete + m_nativeStatus.num_incomplete;
|
|
return (peers > -1) ? peers : m_nativeStatus.list_peers;
|
|
}
|
|
|
|
int TorrentImpl::totalLeechersCount() const
|
|
{
|
|
return (m_nativeStatus.num_incomplete > -1) ? m_nativeStatus.num_incomplete : (m_nativeStatus.list_peers - m_nativeStatus.list_seeds);
|
|
}
|
|
|
|
int TorrentImpl::downloadLimit() const
|
|
{
|
|
return m_downloadLimit;
|
|
}
|
|
|
|
int TorrentImpl::uploadLimit() const
|
|
{
|
|
return m_uploadLimit;
|
|
}
|
|
|
|
bool TorrentImpl::superSeeding() const
|
|
{
|
|
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::super_seeding);
|
|
}
|
|
|
|
bool TorrentImpl::isDHTDisabled() const
|
|
{
|
|
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_dht);
|
|
}
|
|
|
|
bool TorrentImpl::isPEXDisabled() const
|
|
{
|
|
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_pex);
|
|
}
|
|
|
|
bool TorrentImpl::isLSDDisabled() const
|
|
{
|
|
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_lsd);
|
|
}
|
|
|
|
QBitArray TorrentImpl::pieces() const
|
|
{
|
|
return m_pieces;
|
|
}
|
|
|
|
qreal TorrentImpl::distributedCopies() const
|
|
{
|
|
return m_nativeStatus.distributed_copies;
|
|
}
|
|
|
|
qreal TorrentImpl::maxRatio() const
|
|
{
|
|
if (m_ratioLimit == USE_GLOBAL_RATIO)
|
|
return m_session->globalMaxRatio();
|
|
|
|
return m_ratioLimit;
|
|
}
|
|
|
|
int TorrentImpl::maxSeedingTime() const
|
|
{
|
|
if (m_seedingTimeLimit == USE_GLOBAL_SEEDING_TIME)
|
|
return m_session->globalMaxSeedingMinutes();
|
|
|
|
return m_seedingTimeLimit;
|
|
}
|
|
|
|
int TorrentImpl::maxInactiveSeedingTime() const
|
|
{
|
|
if (m_inactiveSeedingTimeLimit == USE_GLOBAL_INACTIVE_SEEDING_TIME)
|
|
return m_session->globalMaxInactiveSeedingMinutes();
|
|
|
|
return m_inactiveSeedingTimeLimit;
|
|
}
|
|
|
|
qreal TorrentImpl::realRatio() const
|
|
{
|
|
const int64_t upload = m_nativeStatus.all_time_upload;
|
|
// special case for a seeder who lost its stats, also assume nobody will import a 99% done torrent
|
|
const int64_t download = (m_nativeStatus.all_time_download < (m_nativeStatus.total_done * 0.01))
|
|
? m_nativeStatus.total_done
|
|
: m_nativeStatus.all_time_download;
|
|
|
|
if (download == 0)
|
|
return (upload == 0) ? 0 : MAX_RATIO;
|
|
|
|
const qreal ratio = upload / static_cast<qreal>(download);
|
|
Q_ASSERT(ratio >= 0);
|
|
|
|
return ratio;
|
|
}
|
|
|
|
int TorrentImpl::uploadPayloadRate() const
|
|
{
|
|
// workaround: suppress the speed for Stopped state
|
|
return isStopped() ? 0 : m_nativeStatus.upload_payload_rate;
|
|
}
|
|
|
|
int TorrentImpl::downloadPayloadRate() const
|
|
{
|
|
// workaround: suppress the speed for Stopped state
|
|
return isStopped() ? 0 : m_nativeStatus.download_payload_rate;
|
|
}
|
|
|
|
qlonglong TorrentImpl::totalPayloadUpload() const
|
|
{
|
|
return m_nativeStatus.total_payload_upload;
|
|
}
|
|
|
|
qlonglong TorrentImpl::totalPayloadDownload() const
|
|
{
|
|
return m_nativeStatus.total_payload_download;
|
|
}
|
|
|
|
int TorrentImpl::connectionsCount() const
|
|
{
|
|
return m_nativeStatus.num_connections;
|
|
}
|
|
|
|
int TorrentImpl::connectionsLimit() const
|
|
{
|
|
return m_nativeStatus.connections_limit;
|
|
}
|
|
|
|
qlonglong TorrentImpl::nextAnnounce() const
|
|
{
|
|
return lt::total_seconds(m_nativeStatus.next_announce);
|
|
}
|
|
|
|
qreal TorrentImpl::popularity() const
|
|
{
|
|
// in order to produce floating-point numbers using `std::chrono::duration_cast`,
|
|
// we should use `qreal` as `Rep` to define the `months` duration
|
|
using months = std::chrono::duration<qreal, std::chrono::months::period>;
|
|
const auto activeMonths = std::chrono::duration_cast<months>(m_nativeStatus.active_duration).count();
|
|
return (activeMonths > 0) ? (realRatio() / activeMonths) : 0;
|
|
}
|
|
|
|
void TorrentImpl::setName(const QString &name)
|
|
{
|
|
if (m_name != name)
|
|
{
|
|
m_name = name;
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentNameChanged(this);
|
|
}
|
|
}
|
|
|
|
bool TorrentImpl::setCategory(const QString &category)
|
|
{
|
|
if (m_category != category)
|
|
{
|
|
if (!category.isEmpty() && !m_session->categories().contains(category))
|
|
return false;
|
|
|
|
if (m_session->isDisableAutoTMMWhenCategoryChanged())
|
|
{
|
|
// This should be done before changing the category name
|
|
// to prevent the torrent from being moved at the path of new category.
|
|
setAutoTMMEnabled(false);
|
|
}
|
|
|
|
const QString oldCategory = m_category;
|
|
m_category = category;
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentCategoryChanged(this, oldCategory);
|
|
|
|
if (m_useAutoTMM)
|
|
adjustStorageLocation();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void TorrentImpl::forceReannounce(const int index)
|
|
{
|
|
m_nativeHandle.force_reannounce(0, index);
|
|
}
|
|
|
|
void TorrentImpl::forceDHTAnnounce()
|
|
{
|
|
m_nativeHandle.force_dht_announce();
|
|
}
|
|
|
|
void TorrentImpl::forceRecheck()
|
|
{
|
|
if (!hasMetadata())
|
|
return;
|
|
|
|
m_nativeHandle.force_recheck();
|
|
|
|
// We have to force update the cached state, otherwise someone will be able to get
|
|
// an incorrect one during the interval until the cached state is updated in a regular way.
|
|
m_nativeStatus.state = lt::torrent_status::checking_resume_data;
|
|
m_nativeStatus.pieces.clear_all();
|
|
m_nativeStatus.num_pieces = 0;
|
|
m_ltAddTorrentParams.have_pieces.clear();
|
|
m_ltAddTorrentParams.verified_pieces.clear();
|
|
m_ltAddTorrentParams.unfinished_pieces.clear();
|
|
m_completedFiles.fill(false);
|
|
m_filesProgress.fill(0);
|
|
m_pieces.fill(false);
|
|
m_unchecked = false;
|
|
|
|
if (m_hasMissingFiles)
|
|
{
|
|
m_hasMissingFiles = false;
|
|
if (!isStopped())
|
|
{
|
|
setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged);
|
|
if (m_operatingMode == TorrentOperatingMode::Forced)
|
|
m_nativeHandle.resume();
|
|
}
|
|
}
|
|
|
|
if (isStopped())
|
|
{
|
|
// When "force recheck" is applied on Stopped torrent, we start them to perform checking
|
|
start();
|
|
m_stopCondition = StopCondition::FilesChecked;
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::setSequentialDownload(const bool enable)
|
|
{
|
|
if (enable)
|
|
{
|
|
m_nativeHandle.set_flags(lt::torrent_flags::sequential_download);
|
|
m_nativeStatus.flags |= lt::torrent_flags::sequential_download; // prevent return cached value
|
|
}
|
|
else
|
|
{
|
|
m_nativeHandle.unset_flags(lt::torrent_flags::sequential_download);
|
|
m_nativeStatus.flags &= ~lt::torrent_flags::sequential_download; // prevent return cached value
|
|
}
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::setFirstLastPiecePriority(const bool enabled)
|
|
{
|
|
if (m_hasFirstLastPiecePriority == enabled)
|
|
return;
|
|
|
|
m_hasFirstLastPiecePriority = enabled;
|
|
if (hasMetadata())
|
|
applyFirstLastPiecePriority(enabled);
|
|
|
|
LogMsg(tr("Download first and last piece first: %1, torrent: '%2'")
|
|
.arg((enabled ? tr("On") : tr("Off")), name()));
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::applyFirstLastPiecePriority(const bool enabled)
|
|
{
|
|
Q_ASSERT(hasMetadata());
|
|
|
|
// Download first and last pieces first for every file in the torrent
|
|
|
|
auto piecePriorities = std::vector<lt::download_priority_t>(m_torrentInfo.piecesCount(), LT::toNative(DownloadPriority::Ignored));
|
|
|
|
// Updating file priorities is an async operation in libtorrent, when we just updated it and immediately query it
|
|
// we might get the old/wrong values, so we rely on `updatedFilePrio` in this case.
|
|
for (int fileIndex = 0; fileIndex < m_filePriorities.size(); ++fileIndex)
|
|
{
|
|
const DownloadPriority filePrio = m_filePriorities[fileIndex];
|
|
if (filePrio <= DownloadPriority::Ignored)
|
|
continue;
|
|
|
|
// Determine the priority to set
|
|
const lt::download_priority_t piecePrio = LT::toNative(enabled ? DownloadPriority::Maximum : filePrio);
|
|
const TorrentInfo::PieceRange pieceRange = m_torrentInfo.filePieces(fileIndex);
|
|
|
|
// worst case: AVI index = 1% of total file size (at the end of the file)
|
|
const int numPieces = std::ceil(fileSize(fileIndex) * 0.01 / pieceLength());
|
|
for (int i = 0; i < numPieces; ++i)
|
|
{
|
|
piecePriorities[pieceRange.first() + i] = piecePrio;
|
|
piecePriorities[pieceRange.last() - i] = piecePrio;
|
|
}
|
|
|
|
const int firstPiece = pieceRange.first() + numPieces;
|
|
const int lastPiece = pieceRange.last() - numPieces;
|
|
for (int pieceIndex = firstPiece; pieceIndex <= lastPiece; ++pieceIndex)
|
|
piecePriorities[pieceIndex] = LT::toNative(filePrio);
|
|
}
|
|
|
|
m_nativeHandle.prioritize_pieces(piecePriorities);
|
|
}
|
|
|
|
TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
|
|
{
|
|
const auto it = std::find_if(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end()
|
|
, [&announceEntry](const TrackerEntryStatus &trackerEntryStatus)
|
|
{
|
|
return (trackerEntryStatus.url == QString::fromStdString(announceEntry.url));
|
|
});
|
|
|
|
Q_ASSERT(it != m_trackerEntryStatuses.end());
|
|
if (it == m_trackerEntryStatuses.end()) [[unlikely]]
|
|
return {};
|
|
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
QSet<int> btProtocols;
|
|
const auto &infoHashes = nativeHandle().info_hashes();
|
|
if (infoHashes.has(lt::protocol_version::V1))
|
|
btProtocols.insert(1);
|
|
if (infoHashes.has(lt::protocol_version::V2))
|
|
btProtocols.insert(2);
|
|
#else
|
|
const QSet<int> btProtocols {1};
|
|
#endif
|
|
|
|
::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo);
|
|
|
|
return *it;
|
|
}
|
|
|
|
void TorrentImpl::resetTrackerEntryStatuses()
|
|
{
|
|
for (TrackerEntryStatus &status : m_trackerEntryStatuses)
|
|
{
|
|
const QString tempUrl = status.url;
|
|
const int tempTier = status.tier;
|
|
|
|
status.clear();
|
|
status.url = tempUrl;
|
|
status.tier = tempTier;
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<const libtorrent::torrent_info> TorrentImpl::nativeTorrentInfo() const
|
|
{
|
|
Q_ASSERT(!m_nativeStatus.torrent_file.expired());
|
|
|
|
return m_nativeStatus.torrent_file.lock();
|
|
}
|
|
|
|
void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathList &fileNames)
|
|
{
|
|
Q_ASSERT(m_maintenanceJob == MaintenanceJob::HandleMetadata);
|
|
if (m_maintenanceJob != MaintenanceJob::HandleMetadata) [[unlikely]]
|
|
return;
|
|
|
|
Q_ASSERT(m_filePaths.isEmpty());
|
|
if (!m_filePaths.isEmpty()) [[unlikely]]
|
|
m_filePaths.clear();
|
|
|
|
lt::add_torrent_params &p = m_ltAddTorrentParams;
|
|
|
|
const std::shared_ptr<lt::torrent_info> metadata = std::const_pointer_cast<lt::torrent_info>(nativeTorrentInfo());
|
|
m_torrentInfo = TorrentInfo(*metadata);
|
|
m_filePriorities.reserve(filesCount());
|
|
const auto nativeIndexes = m_torrentInfo.nativeIndexes();
|
|
p.file_priorities = resized(p.file_priorities, metadata->files().num_files()
|
|
, LT::toNative(p.file_priorities.empty() ? DownloadPriority::Normal : DownloadPriority::Ignored));
|
|
|
|
m_completedFiles.fill(static_cast<bool>(p.flags & lt::torrent_flags::seed_mode), filesCount());
|
|
m_filesProgress.resize(filesCount());
|
|
updateProgress();
|
|
|
|
for (int i = 0; i < fileNames.size(); ++i)
|
|
{
|
|
const auto nativeIndex = nativeIndexes.at(i);
|
|
|
|
const Path &actualFilePath = fileNames.at(i);
|
|
p.renamed_files[nativeIndex] = actualFilePath.toString().toStdString();
|
|
|
|
const Path filePath = actualFilePath.removedExtension(QB_EXT);
|
|
m_filePaths.append(filePath);
|
|
|
|
m_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)]));
|
|
}
|
|
|
|
m_session->applyFilenameFilter(m_filePaths, m_filePriorities);
|
|
for (int i = 0; i < m_filePriorities.size(); ++i)
|
|
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(m_filePriorities[i]);
|
|
|
|
p.save_path = savePath.toString().toStdString();
|
|
p.ti = metadata;
|
|
|
|
if (stopCondition() == StopCondition::MetadataReceived)
|
|
{
|
|
m_stopCondition = StopCondition::None;
|
|
|
|
m_isStopped = true;
|
|
p.flags |= lt::torrent_flags::paused;
|
|
p.flags &= ~lt::torrent_flags::auto_managed;
|
|
|
|
m_session->handleTorrentStopped(this);
|
|
}
|
|
|
|
reload();
|
|
|
|
// If first/last piece priority was specified when adding this torrent,
|
|
// we should apply it now that we have metadata:
|
|
if (m_hasFirstLastPiecePriority)
|
|
applyFirstLastPiecePriority(true);
|
|
|
|
m_maintenanceJob = MaintenanceJob::None;
|
|
prepareResumeData(std::move(p));
|
|
|
|
m_session->handleTorrentMetadataReceived(this);
|
|
}
|
|
|
|
void TorrentImpl::reload()
|
|
{
|
|
try
|
|
{
|
|
lt::add_torrent_params p = m_ltAddTorrentParams;
|
|
p.flags |= lt::torrent_flags::update_subscribe
|
|
| lt::torrent_flags::override_trackers
|
|
| lt::torrent_flags::override_web_seeds;
|
|
|
|
if (m_isStopped)
|
|
{
|
|
p.flags |= lt::torrent_flags::paused;
|
|
p.flags &= ~lt::torrent_flags::auto_managed;
|
|
}
|
|
else if (m_operatingMode == TorrentOperatingMode::AutoManaged)
|
|
{
|
|
p.flags |= (lt::torrent_flags::auto_managed | lt::torrent_flags::paused);
|
|
}
|
|
else
|
|
{
|
|
p.flags &= ~(lt::torrent_flags::auto_managed | lt::torrent_flags::paused);
|
|
}
|
|
|
|
const auto queuePos = m_nativeHandle.queue_position();
|
|
|
|
m_nativeHandle = m_session->reloadTorrent(m_nativeHandle, std::move(p));
|
|
m_nativeStatus = static_cast<ExtensionData *>(m_nativeHandle.userdata())->status;
|
|
|
|
if (queuePos >= lt::queue_position_t {})
|
|
m_nativeHandle.queue_position_set(queuePos);
|
|
m_nativeStatus.queue_position = queuePos;
|
|
|
|
m_completedFiles.fill(false);
|
|
m_filesProgress.fill(0);
|
|
m_pieces.fill(false);
|
|
m_nativeStatus.pieces.clear_all();
|
|
m_nativeStatus.num_pieces = 0;
|
|
|
|
updateState();
|
|
}
|
|
catch (const lt::system_error &err)
|
|
{
|
|
throw RuntimeError(tr("Failed to reload torrent. Torrent: %1. Reason: %2")
|
|
.arg(id().toString(), QString::fromLocal8Bit(err.what())));
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::stop()
|
|
{
|
|
if (!m_isStopped)
|
|
{
|
|
m_stopCondition = StopCondition::None;
|
|
m_isStopped = true;
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentStopped(this);
|
|
}
|
|
|
|
if (m_maintenanceJob == MaintenanceJob::None)
|
|
{
|
|
setAutoManaged(false);
|
|
m_nativeHandle.pause();
|
|
|
|
m_payloadRateMonitor.reset();
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::start(const TorrentOperatingMode mode)
|
|
{
|
|
if (hasError())
|
|
{
|
|
m_nativeHandle.clear_error();
|
|
m_nativeHandle.unset_flags(lt::torrent_flags::upload_mode);
|
|
}
|
|
|
|
m_operatingMode = mode;
|
|
|
|
if (m_hasMissingFiles)
|
|
{
|
|
m_hasMissingFiles = false;
|
|
m_isStopped = false;
|
|
m_ltAddTorrentParams.ti = std::const_pointer_cast<lt::torrent_info>(nativeTorrentInfo());
|
|
reload();
|
|
return;
|
|
}
|
|
|
|
if (m_isStopped)
|
|
{
|
|
m_isStopped = false;
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentStarted(this);
|
|
}
|
|
|
|
if (m_maintenanceJob == MaintenanceJob::None)
|
|
{
|
|
setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged);
|
|
if (m_operatingMode == TorrentOperatingMode::Forced)
|
|
m_nativeHandle.resume();
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext context)
|
|
{
|
|
if (!hasMetadata())
|
|
{
|
|
if (context == MoveStorageContext::ChangeSavePath)
|
|
{
|
|
m_savePath = newPath;
|
|
m_session->handleTorrentSavePathChanged(this);
|
|
}
|
|
else if (context == MoveStorageContext::ChangeDownloadPath)
|
|
{
|
|
m_downloadPath = newPath;
|
|
m_session->handleTorrentSavePathChanged(this);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const auto mode = (context == MoveStorageContext::AdjustCurrentLocation)
|
|
? MoveStorageMode::Overwrite : MoveStorageMode::KeepExistingFiles;
|
|
if (m_session->addMoveTorrentStorageJob(this, newPath, mode, context))
|
|
{
|
|
if (!m_storageIsMoving)
|
|
{
|
|
m_storageIsMoving = true;
|
|
updateState();
|
|
m_session->handleTorrentStorageMovingStateChanged(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::renameFile(const int index, const Path &path)
|
|
{
|
|
Q_ASSERT((index >= 0) && (index < filesCount()));
|
|
if ((index < 0) || (index >= filesCount())) [[unlikely]]
|
|
return;
|
|
|
|
const Path targetActualPath = makeActualPath(index, path);
|
|
doRenameFile(index, targetActualPath);
|
|
}
|
|
|
|
void TorrentImpl::handleStateUpdate(const lt::torrent_status &nativeStatus)
|
|
{
|
|
updateStatus(nativeStatus);
|
|
}
|
|
|
|
void TorrentImpl::handleQueueingModeChanged()
|
|
{
|
|
updateState();
|
|
}
|
|
|
|
void TorrentImpl::handleMoveStorageJobFinished(const Path &path, const MoveStorageContext context, const bool hasOutstandingJob)
|
|
{
|
|
if (context == MoveStorageContext::ChangeSavePath)
|
|
m_savePath = path;
|
|
else if (context == MoveStorageContext::ChangeDownloadPath)
|
|
m_downloadPath = path;
|
|
m_storageIsMoving = hasOutstandingJob;
|
|
m_nativeStatus.save_path = path.toString().toStdString();
|
|
|
|
m_session->handleTorrentSavePathChanged(this);
|
|
deferredRequestResumeData();
|
|
|
|
if (!m_storageIsMoving)
|
|
{
|
|
updateState();
|
|
m_session->handleTorrentStorageMovingStateChanged(this);
|
|
|
|
if (m_hasMissingFiles)
|
|
{
|
|
// it can be moved to the proper location
|
|
m_hasMissingFiles = false;
|
|
m_ltAddTorrentParams.save_path = m_nativeStatus.save_path;
|
|
m_ltAddTorrentParams.ti = std::const_pointer_cast<lt::torrent_info>(nativeTorrentInfo());
|
|
reload();
|
|
}
|
|
|
|
while ((m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
|
|
std::invoke(m_moveFinishedTriggers.dequeue());
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::handleTorrentChecked()
|
|
{
|
|
if (!hasMetadata())
|
|
{
|
|
// The torrent is checked due to metadata received, but we should not process
|
|
// this event until the torrent is reloaded using the received metadata.
|
|
return;
|
|
}
|
|
|
|
if (stopCondition() == StopCondition::FilesChecked)
|
|
stop();
|
|
|
|
m_statusUpdatedTriggers.enqueue([this]()
|
|
{
|
|
qDebug("\"%s\" have just finished checking.", qUtf8Printable(name()));
|
|
|
|
if (!m_hasMissingFiles)
|
|
{
|
|
if ((progress() < 1.0) && (wantedSize() > 0))
|
|
m_hasFinishedStatus = false;
|
|
else if (progress() == 1.0)
|
|
m_hasFinishedStatus = true;
|
|
|
|
adjustStorageLocation();
|
|
manageActualFilePaths();
|
|
|
|
if (!isStopped())
|
|
{
|
|
// torrent is internally paused using NativeTorrentExtension after files checked
|
|
// so we need to resume it if there is no corresponding "stop condition" set
|
|
setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged);
|
|
if (m_operatingMode == TorrentOperatingMode::Forced)
|
|
m_nativeHandle.resume();
|
|
}
|
|
}
|
|
|
|
if (m_nativeStatus.need_save_resume)
|
|
deferredRequestResumeData();
|
|
|
|
m_session->handleTorrentChecked(this);
|
|
});
|
|
}
|
|
|
|
void TorrentImpl::handleTorrentFinished()
|
|
{
|
|
m_hasMissingFiles = false;
|
|
if (m_hasFinishedStatus)
|
|
return;
|
|
|
|
m_statusUpdatedTriggers.enqueue([this]()
|
|
{
|
|
adjustStorageLocation();
|
|
manageActualFilePaths();
|
|
|
|
deferredRequestResumeData();
|
|
|
|
const bool recheckTorrentsOnCompletion = Preferences::instance()->recheckTorrentsOnCompletion();
|
|
if (recheckTorrentsOnCompletion && m_unchecked)
|
|
{
|
|
forceRecheck();
|
|
}
|
|
else
|
|
{
|
|
m_hasFinishedStatus = true;
|
|
|
|
if (isMoveInProgress() || (m_renameCount > 0))
|
|
m_moveFinishedTriggers.enqueue([this] { m_session->handleTorrentFinished(this); });
|
|
else
|
|
m_session->handleTorrentFinished(this);
|
|
}
|
|
});
|
|
}
|
|
|
|
void TorrentImpl::handleSaveResumeData(lt::add_torrent_params params)
|
|
{
|
|
if (m_ltAddTorrentParams.url_seeds != params.url_seeds)
|
|
{
|
|
// URL seed list have been changed by libtorrent for some reason, so we need to update cached one.
|
|
// Unfortunately, URL seed list containing in "resume data" is generated according to different rules
|
|
// than the list we usually cache, so we have to request it from the appropriate source.
|
|
fetchURLSeeds().then(this, [this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; });
|
|
}
|
|
|
|
if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && params.ti)
|
|
{
|
|
Q_ASSERT(m_indexMap.isEmpty());
|
|
|
|
const auto isSeedMode = static_cast<bool>(m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode);
|
|
m_ltAddTorrentParams = std::move(params);
|
|
if (isSeedMode)
|
|
m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode;
|
|
|
|
m_ltAddTorrentParams.have_pieces.clear();
|
|
m_ltAddTorrentParams.verified_pieces.clear();
|
|
m_ltAddTorrentParams.unfinished_pieces.clear();
|
|
|
|
m_nativeStatus.torrent_file = m_ltAddTorrentParams.ti;
|
|
|
|
const auto metadata = TorrentInfo(*m_ltAddTorrentParams.ti);
|
|
|
|
const auto &renamedFiles = m_ltAddTorrentParams.renamed_files;
|
|
PathList filePaths = metadata.filePaths();
|
|
if (renamedFiles.empty() && (m_contentLayout != TorrentContentLayout::Original))
|
|
{
|
|
const Path originalRootFolder = Path::findRootFolder(filePaths);
|
|
const auto originalContentLayout = (originalRootFolder.isEmpty()
|
|
? TorrentContentLayout::NoSubfolder : TorrentContentLayout::Subfolder);
|
|
if (m_contentLayout != originalContentLayout)
|
|
{
|
|
if (m_contentLayout == TorrentContentLayout::NoSubfolder)
|
|
Path::stripRootFolder(filePaths);
|
|
else
|
|
Path::addRootFolder(filePaths, filePaths.at(0).removedExtension());
|
|
}
|
|
}
|
|
|
|
const auto nativeIndexes = metadata.nativeIndexes();
|
|
m_indexMap.reserve(filePaths.size());
|
|
for (int i = 0; i < filePaths.size(); ++i)
|
|
{
|
|
const auto nativeIndex = nativeIndexes.at(i);
|
|
m_indexMap[nativeIndex] = i;
|
|
|
|
if (const auto it = renamedFiles.find(nativeIndex); it != renamedFiles.cend())
|
|
filePaths[i] = Path(it->second);
|
|
}
|
|
|
|
m_session->findIncompleteFiles(savePath(), downloadPath(), filePaths).then(this
|
|
, [this](const FileSearchResult &result)
|
|
{
|
|
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
|
|
endReceivedMetadataHandling(result.savePath, result.fileNames);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
prepareResumeData(std::move(params));
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::prepareResumeData(lt::add_torrent_params params)
|
|
{
|
|
{
|
|
decltype(params.have_pieces) havePieces;
|
|
decltype(params.unfinished_pieces) unfinishedPieces;
|
|
decltype(params.verified_pieces) verifiedPieces;
|
|
|
|
// The resume data obtained from libtorrent contains an empty "progress" in the following cases:
|
|
// 1. when it was requested at a time when the initial resume data has not yet been checked,
|
|
// 2. when initial resume data was rejected
|
|
// We should preserve the initial "progress" in such cases.
|
|
const bool needPreserveProgress = m_hasMissingFiles
|
|
|| (!m_ltAddTorrentParams.have_pieces.empty() && params.have_pieces.empty());
|
|
const bool preserveSeedMode = !m_hasMissingFiles && !hasMetadata()
|
|
&& (m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode);
|
|
|
|
if (needPreserveProgress)
|
|
{
|
|
havePieces = std::move(m_ltAddTorrentParams.have_pieces);
|
|
unfinishedPieces = std::move(m_ltAddTorrentParams.unfinished_pieces);
|
|
verifiedPieces = std::move(m_ltAddTorrentParams.verified_pieces);
|
|
}
|
|
|
|
// Update recent resume data
|
|
m_ltAddTorrentParams = std::move(params);
|
|
|
|
if (needPreserveProgress)
|
|
{
|
|
m_ltAddTorrentParams.have_pieces = std::move(havePieces);
|
|
m_ltAddTorrentParams.unfinished_pieces = std::move(unfinishedPieces);
|
|
m_ltAddTorrentParams.verified_pieces = std::move(verifiedPieces);
|
|
}
|
|
|
|
if (preserveSeedMode)
|
|
m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode;
|
|
}
|
|
|
|
// We shouldn't save upload_mode flag to allow torrent operate normally on next run
|
|
m_ltAddTorrentParams.flags &= ~lt::torrent_flags::upload_mode;
|
|
|
|
LoadTorrentParams resumeData
|
|
{
|
|
.ltAddTorrentParams = m_ltAddTorrentParams,
|
|
.name = m_name,
|
|
.category = m_category,
|
|
.tags = m_tags,
|
|
.savePath = (!m_useAutoTMM ? m_savePath : Path()),
|
|
.downloadPath = (!m_useAutoTMM ? m_downloadPath : Path()),
|
|
.contentLayout = m_contentLayout,
|
|
.operatingMode = m_operatingMode,
|
|
.useAutoTMM = m_useAutoTMM,
|
|
.firstLastPiecePriority = m_hasFirstLastPiecePriority,
|
|
.hasFinishedStatus = m_hasFinishedStatus,
|
|
.stopped = m_isStopped,
|
|
.stopCondition = m_stopCondition,
|
|
.addToQueueTop = false,
|
|
.ratioLimit = m_ratioLimit,
|
|
.seedingTimeLimit = m_seedingTimeLimit,
|
|
.inactiveSeedingTimeLimit = m_inactiveSeedingTimeLimit,
|
|
.shareLimitAction = m_shareLimitAction,
|
|
.sslParameters = m_sslParams
|
|
};
|
|
|
|
m_session->handleTorrentResumeDataReady(this, std::move(resumeData));
|
|
}
|
|
|
|
void TorrentImpl::handleFastResumeRejected()
|
|
{
|
|
// Files were probably moved or storage isn't accessible
|
|
m_hasMissingFiles = true;
|
|
}
|
|
|
|
void TorrentImpl::handleFileRenamed(const lt::file_index_t nativeFileIndex, const Path &newActualFilePath, const Path &oldActualFilePath)
|
|
{
|
|
const int fileIndex = fileIndexFromNative(nativeFileIndex);
|
|
Q_ASSERT(fileIndex >= 0);
|
|
|
|
const Path oldFilePath = m_filePaths.at(fileIndex);
|
|
const Path newFilePath = makeUserPath(newActualFilePath);
|
|
|
|
// Check if ".!qB" extension or ".unwanted" folder was just added or removed
|
|
// We should compare path in a case sensitive manner even on case insensitive
|
|
// platforms since it can be renamed by only changing case of some character(s)
|
|
if (oldFilePath.data() == newFilePath.data())
|
|
{
|
|
// Remove empty ".unwanted" folders
|
|
const Path oldActualParentPath = oldActualFilePath.parentPath();
|
|
const Path newActualParentPath = newActualFilePath.parentPath();
|
|
if (newActualParentPath.filename() == UNWANTED_FOLDER_NAME)
|
|
{
|
|
if (oldActualParentPath.filename() != UNWANTED_FOLDER_NAME)
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
const std::wstring winPath = (actualStorageLocation() / newActualParentPath).toString().toStdWString();
|
|
const DWORD dwAttrs = ::GetFileAttributesW(winPath.c_str());
|
|
::SetFileAttributesW(winPath.c_str(), (dwAttrs | FILE_ATTRIBUTE_HIDDEN));
|
|
#endif
|
|
}
|
|
}
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
else if (oldActualParentPath.filename() == UNWANTED_FOLDER_NAME)
|
|
{
|
|
if (newActualParentPath.filename() != UNWANTED_FOLDER_NAME)
|
|
Utils::Fs::rmdir(actualStorageLocation() / oldActualParentPath);
|
|
}
|
|
#else
|
|
else
|
|
{
|
|
Utils::Fs::rmdir(actualStorageLocation() / newActualParentPath / Path(UNWANTED_FOLDER_NAME));
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
m_filePaths[fileIndex] = newFilePath;
|
|
|
|
// Remove empty leftover folders
|
|
// For example renaming "a/b/c" to "d/b/c", then folders "a/b" and "a" will
|
|
// be removed if they are empty
|
|
Path oldParentPath = oldFilePath.parentPath();
|
|
const Path commonBasePath = Path::commonPath(oldParentPath, newFilePath.parentPath());
|
|
while (oldParentPath != commonBasePath)
|
|
{
|
|
Utils::Fs::rmdir(actualStorageLocation() / oldParentPath);
|
|
oldParentPath = oldParentPath.parentPath();
|
|
}
|
|
}
|
|
|
|
--m_renameCount;
|
|
while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
|
|
m_moveFinishedTriggers.takeFirst()();
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::handleFileRenameFailed(const lt::file_index_t nativeFileIndex)
|
|
{
|
|
const int fileIndex = fileIndexFromNative(nativeFileIndex);
|
|
Q_ASSERT(fileIndex >= 0);
|
|
|
|
--m_renameCount;
|
|
while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
|
|
m_moveFinishedTriggers.takeFirst()();
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::handleFileCompleted(const lt::file_index_t nativeFileIndex)
|
|
{
|
|
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
|
|
return;
|
|
|
|
const int fileIndex = fileIndexFromNative(nativeFileIndex);
|
|
Q_ASSERT(fileIndex >= 0);
|
|
|
|
m_completedFiles.setBit(fileIndex);
|
|
|
|
const Path actualPath = actualFilePath(fileIndex);
|
|
|
|
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
|
// only apply Mark-of-the-Web to new download files
|
|
if (Preferences::instance()->isMarkOfTheWebEnabled()
|
|
&& (m_nativeStatus.state == lt::torrent_status::downloading))
|
|
{
|
|
const Path fullpath = actualStorageLocation() / actualPath;
|
|
Utils::OS::applyMarkOfTheWeb(fullpath);
|
|
}
|
|
#endif // Q_OS_MACOS || Q_OS_WIN
|
|
|
|
if (m_session->isAppendExtensionEnabled())
|
|
{
|
|
const Path path = filePath(fileIndex);
|
|
if (actualPath != path)
|
|
{
|
|
qDebug("Renaming %s to %s", qUtf8Printable(actualPath.toString()), qUtf8Printable(path.toString()));
|
|
doRenameFile(fileIndex, path);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::handleFileError(FileErrorInfo fileError)
|
|
{
|
|
m_lastFileError = std::move(fileError);
|
|
}
|
|
|
|
void TorrentImpl::handleMetadataReceived()
|
|
{
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
const InfoHash prevInfoHash = infoHash();
|
|
m_infoHash = InfoHash(m_nativeHandle.info_hashes());
|
|
if (prevInfoHash != infoHash())
|
|
m_session->handleTorrentInfoHashChanged(this, prevInfoHash);
|
|
#endif
|
|
|
|
m_maintenanceJob = MaintenanceJob::HandleMetadata;
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::handleCategoryOptionsChanged()
|
|
{
|
|
if (m_useAutoTMM)
|
|
adjustStorageLocation();
|
|
}
|
|
|
|
void TorrentImpl::handleAppendExtensionToggled()
|
|
{
|
|
if (!hasMetadata())
|
|
return;
|
|
|
|
manageActualFilePaths();
|
|
}
|
|
|
|
void TorrentImpl::handleUnwantedFolderToggled()
|
|
{
|
|
if (!hasMetadata())
|
|
return;
|
|
|
|
manageActualFilePaths();
|
|
}
|
|
|
|
void TorrentImpl::manageActualFilePaths()
|
|
{
|
|
const std::shared_ptr<const lt::torrent_info> nativeInfo = nativeTorrentInfo();
|
|
const lt::file_storage &nativeFiles = nativeInfo->files();
|
|
|
|
for (int i = 0; i < filesCount(); ++i)
|
|
{
|
|
const Path path = filePath(i);
|
|
|
|
const auto nativeIndex = m_torrentInfo.nativeIndexes().at(i);
|
|
const Path actualPath {nativeFiles.file_path(nativeIndex)};
|
|
const Path targetActualPath = makeActualPath(i, path);
|
|
if (actualPath != targetActualPath)
|
|
{
|
|
qDebug() << "Renaming" << actualPath.toString() << "to" << targetActualPath.toString();
|
|
doRenameFile(i, targetActualPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::adjustStorageLocation()
|
|
{
|
|
const Path downloadPath = this->downloadPath();
|
|
const Path targetPath = ((isFinished() || m_hasFinishedStatus || downloadPath.isEmpty()) ? savePath() : downloadPath);
|
|
|
|
if ((targetPath != actualStorageLocation()) || isMoveInProgress())
|
|
moveStorage(targetPath, MoveStorageContext::AdjustCurrentLocation);
|
|
}
|
|
|
|
void TorrentImpl::doRenameFile(const int index, const Path &path)
|
|
{
|
|
const QList<lt::file_index_t> nativeIndexes = m_torrentInfo.nativeIndexes();
|
|
|
|
Q_ASSERT(index >= 0);
|
|
Q_ASSERT(index < nativeIndexes.size());
|
|
if ((index < 0) || (index >= nativeIndexes.size())) [[unlikely]]
|
|
return;
|
|
|
|
++m_renameCount;
|
|
m_nativeHandle.rename_file(nativeIndexes[index], path.toString().toStdString());
|
|
}
|
|
|
|
lt::torrent_handle TorrentImpl::nativeHandle() const
|
|
{
|
|
return m_nativeHandle;
|
|
}
|
|
|
|
int TorrentImpl::fileIndexFromNative(const lt::file_index_t nativeFileIndex) const
|
|
{
|
|
return m_indexMap.value(nativeFileIndex, -1);
|
|
}
|
|
|
|
void TorrentImpl::setMetadata(const TorrentInfo &torrentInfo)
|
|
{
|
|
if (hasMetadata())
|
|
return;
|
|
|
|
m_session->invokeAsync([nativeHandle = m_nativeHandle, torrentInfo]
|
|
{
|
|
try
|
|
{
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
nativeHandle.set_metadata(torrentInfo.nativeInfo()->info_section());
|
|
#else
|
|
const std::shared_ptr<lt::torrent_info> nativeInfo = torrentInfo.nativeInfo();
|
|
nativeHandle.set_metadata(lt::span<const char>(nativeInfo->metadata().get(), nativeInfo->metadata_size()));
|
|
#endif
|
|
}
|
|
catch (const std::exception &) {}
|
|
});
|
|
}
|
|
|
|
Torrent::StopCondition TorrentImpl::stopCondition() const
|
|
{
|
|
return m_stopCondition;
|
|
}
|
|
|
|
void TorrentImpl::setStopCondition(const StopCondition stopCondition)
|
|
{
|
|
if (stopCondition == m_stopCondition)
|
|
return;
|
|
|
|
if (isStopped())
|
|
return;
|
|
|
|
if ((stopCondition == StopCondition::MetadataReceived) && hasMetadata())
|
|
return;
|
|
|
|
if ((stopCondition == StopCondition::FilesChecked) && hasMetadata() && !isChecking())
|
|
return;
|
|
|
|
m_stopCondition = stopCondition;
|
|
}
|
|
|
|
SSLParameters TorrentImpl::getSSLParameters() const
|
|
{
|
|
return m_sslParams;
|
|
}
|
|
|
|
void TorrentImpl::setSSLParameters(const SSLParameters &sslParams)
|
|
{
|
|
if (sslParams == getSSLParameters())
|
|
return;
|
|
|
|
m_sslParams = sslParams;
|
|
applySSLParameters();
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
bool TorrentImpl::applySSLParameters()
|
|
{
|
|
if (!m_sslParams.isValid())
|
|
return false;
|
|
|
|
m_nativeHandle.set_ssl_certificate_buffer(m_sslParams.certificate.toPem().toStdString()
|
|
, m_sslParams.privateKey.toPem().toStdString(), m_sslParams.dhParams.toStdString());
|
|
return true;
|
|
}
|
|
|
|
bool TorrentImpl::isMoveInProgress() const
|
|
{
|
|
return m_storageIsMoving;
|
|
}
|
|
|
|
void TorrentImpl::updateStatus(const lt::torrent_status &nativeStatus)
|
|
{
|
|
// Since libtorrent alerts are handled asynchronously there can be obsolete
|
|
// "state update" event reached here after torrent was reloaded in libtorrent.
|
|
// Just discard such events.
|
|
if (nativeStatus.handle != m_nativeHandle) [[unlikely]]
|
|
return;
|
|
|
|
const lt::torrent_status oldStatus = std::exchange(m_nativeStatus, nativeStatus);
|
|
|
|
if (m_nativeStatus.num_pieces != oldStatus.num_pieces)
|
|
updateProgress();
|
|
|
|
if (m_nativeStatus.completed_time != oldStatus.completed_time)
|
|
m_completedTime = (m_nativeStatus.completed_time > 0) ? QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time) : QDateTime();
|
|
|
|
if (m_nativeStatus.last_seen_complete != oldStatus.last_seen_complete)
|
|
m_lastSeenComplete = QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete);
|
|
|
|
updateState();
|
|
|
|
m_payloadRateMonitor.addSample({nativeStatus.download_payload_rate
|
|
, nativeStatus.upload_payload_rate});
|
|
|
|
if (hasMetadata())
|
|
{
|
|
// NOTE: Don't change the order of these conditionals!
|
|
// Otherwise it will not work properly since torrent can be CheckingDownloading.
|
|
if (isChecking())
|
|
m_unchecked = false;
|
|
else if (isDownloading())
|
|
m_unchecked = true;
|
|
}
|
|
|
|
while (!m_statusUpdatedTriggers.isEmpty())
|
|
std::invoke(m_statusUpdatedTriggers.dequeue());
|
|
}
|
|
|
|
void TorrentImpl::updateProgress()
|
|
{
|
|
Q_ASSERT(hasMetadata());
|
|
if (!hasMetadata()) [[unlikely]]
|
|
return;
|
|
|
|
Q_ASSERT(!m_filesProgress.isEmpty());
|
|
if (m_filesProgress.isEmpty()) [[unlikely]]
|
|
m_filesProgress.resize(filesCount());
|
|
|
|
const QBitArray oldPieces = std::exchange(m_pieces, LT::toQBitArray(m_nativeStatus.pieces));
|
|
const QBitArray newPieces = m_pieces ^ oldPieces;
|
|
|
|
const int64_t pieceSize = m_torrentInfo.pieceLength();
|
|
for (qsizetype index = 0; index < newPieces.size(); ++index)
|
|
{
|
|
if (!newPieces.at(index))
|
|
continue;
|
|
|
|
int64_t size = m_torrentInfo.pieceLength(index);
|
|
int64_t pieceOffset = index * pieceSize;
|
|
|
|
for (const int fileIndex : asConst(m_torrentInfo.fileIndicesForPiece(index)))
|
|
{
|
|
const int64_t fileOffsetInPiece = pieceOffset - m_torrentInfo.fileOffset(fileIndex);
|
|
const int64_t add = std::min<int64_t>((m_torrentInfo.fileSize(fileIndex) - fileOffsetInPiece), size);
|
|
|
|
m_filesProgress[fileIndex] += add;
|
|
|
|
size -= add;
|
|
if (size <= 0)
|
|
break;
|
|
|
|
pieceOffset += add;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::setRatioLimit(qreal limit)
|
|
{
|
|
if (limit < USE_GLOBAL_RATIO)
|
|
limit = NO_RATIO_LIMIT;
|
|
|
|
if (m_ratioLimit != limit)
|
|
{
|
|
m_ratioLimit = limit;
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentShareLimitChanged(this);
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::setSeedingTimeLimit(int limit)
|
|
{
|
|
if (limit < USE_GLOBAL_SEEDING_TIME)
|
|
limit = NO_SEEDING_TIME_LIMIT;
|
|
|
|
if (m_seedingTimeLimit != limit)
|
|
{
|
|
m_seedingTimeLimit = limit;
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentShareLimitChanged(this);
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
|
|
{
|
|
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME)
|
|
limit = NO_INACTIVE_SEEDING_TIME_LIMIT;
|
|
|
|
if (m_inactiveSeedingTimeLimit != limit)
|
|
{
|
|
m_inactiveSeedingTimeLimit = limit;
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentShareLimitChanged(this);
|
|
}
|
|
}
|
|
|
|
ShareLimitAction TorrentImpl::shareLimitAction() const
|
|
{
|
|
return m_shareLimitAction;
|
|
}
|
|
|
|
void TorrentImpl::setShareLimitAction(const ShareLimitAction action)
|
|
{
|
|
if (m_shareLimitAction != action)
|
|
{
|
|
m_shareLimitAction = action;
|
|
deferredRequestResumeData();
|
|
m_session->handleTorrentShareLimitChanged(this);
|
|
}
|
|
}
|
|
|
|
void TorrentImpl::setUploadLimit(const int limit)
|
|
{
|
|
const int cleanValue = cleanLimitValue(limit);
|
|
if (cleanValue == uploadLimit())
|
|
return;
|
|
|
|
m_uploadLimit = cleanValue;
|
|
m_nativeHandle.set_upload_limit(m_uploadLimit);
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::setDownloadLimit(const int limit)
|
|
{
|
|
const int cleanValue = cleanLimitValue(limit);
|
|
if (cleanValue == downloadLimit())
|
|
return;
|
|
|
|
m_downloadLimit = cleanValue;
|
|
m_nativeHandle.set_download_limit(m_downloadLimit);
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::setSuperSeeding(const bool enable)
|
|
{
|
|
if (enable == superSeeding())
|
|
return;
|
|
|
|
if (enable)
|
|
m_nativeHandle.set_flags(lt::torrent_flags::super_seeding);
|
|
else
|
|
m_nativeHandle.unset_flags(lt::torrent_flags::super_seeding);
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::setDHTDisabled(const bool disable)
|
|
{
|
|
if (disable == isDHTDisabled())
|
|
return;
|
|
|
|
if (disable)
|
|
m_nativeHandle.set_flags(lt::torrent_flags::disable_dht);
|
|
else
|
|
m_nativeHandle.unset_flags(lt::torrent_flags::disable_dht);
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::setPEXDisabled(const bool disable)
|
|
{
|
|
if (disable == isPEXDisabled())
|
|
return;
|
|
|
|
if (disable)
|
|
m_nativeHandle.set_flags(lt::torrent_flags::disable_pex);
|
|
else
|
|
m_nativeHandle.unset_flags(lt::torrent_flags::disable_pex);
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::setLSDDisabled(const bool disable)
|
|
{
|
|
if (disable == isLSDDisabled())
|
|
return;
|
|
|
|
if (disable)
|
|
m_nativeHandle.set_flags(lt::torrent_flags::disable_lsd);
|
|
else
|
|
m_nativeHandle.unset_flags(lt::torrent_flags::disable_lsd);
|
|
|
|
deferredRequestResumeData();
|
|
}
|
|
|
|
void TorrentImpl::flushCache() const
|
|
{
|
|
m_nativeHandle.flush_cache();
|
|
}
|
|
|
|
QString TorrentImpl::createMagnetURI() const
|
|
{
|
|
QString ret = u"magnet:?"_s;
|
|
|
|
const SHA1Hash infoHash1 = infoHash().v1();
|
|
if (infoHash1.isValid())
|
|
ret += u"xt=urn:btih:" + infoHash1.toString();
|
|
|
|
if (const SHA256Hash infoHash2 = infoHash().v2(); infoHash2.isValid())
|
|
{
|
|
if (infoHash1.isValid())
|
|
ret += u'&';
|
|
ret += u"xt=urn:btmh:1220" + infoHash2.toString();
|
|
}
|
|
|
|
if (const QString displayName = name(); displayName != id().toString())
|
|
ret += u"&dn=" + QString::fromLatin1(QUrl::toPercentEncoding(displayName));
|
|
|
|
if (hasMetadata())
|
|
ret += u"&xl=" + QString::number(totalSize());
|
|
|
|
for (const TrackerEntryStatus &tracker : asConst(trackers()))
|
|
ret += u"&tr=" + QString::fromLatin1(QUrl::toPercentEncoding(tracker.url));
|
|
|
|
for (const QUrl &urlSeed : asConst(urlSeeds()))
|
|
ret += u"&ws=" + urlSeed.toString(QUrl::FullyEncoded);
|
|
|
|
return ret;
|
|
}
|
|
|
|
nonstd::expected<lt::entry, QString> TorrentImpl::exportTorrent() const
|
|
{
|
|
if (!hasMetadata())
|
|
return nonstd::make_unexpected(tr("Missing metadata"));
|
|
|
|
try
|
|
{
|
|
[[maybe_unused]] const auto infoGuard = qScopeGuard([this] { m_ltAddTorrentParams.ti.reset(); });
|
|
m_ltAddTorrentParams.ti = info().nativeInfo();
|
|
return lt::write_torrent_file(m_ltAddTorrentParams);
|
|
}
|
|
catch (const lt::system_error &err)
|
|
{
|
|
return nonstd::make_unexpected(QString::fromLocal8Bit(err.what()));
|
|
}
|
|
}
|
|
|
|
nonstd::expected<QByteArray, QString> TorrentImpl::exportToBuffer() const
|
|
{
|
|
const nonstd::expected<lt::entry, QString> preparationResult = exportTorrent();
|
|
if (!preparationResult)
|
|
return preparationResult.get_unexpected();
|
|
|
|
// usually torrent size should be smaller than 1 MB,
|
|
// however there are >100 MB v2/hybrid torrent files out in the wild
|
|
QByteArray buffer;
|
|
buffer.reserve(1024 * 1024);
|
|
lt::bencode(std::back_inserter(buffer), preparationResult.value());
|
|
return buffer;
|
|
}
|
|
|
|
nonstd::expected<void, QString> TorrentImpl::exportToFile(const Path &path) const
|
|
{
|
|
const nonstd::expected<lt::entry, QString> preparationResult = exportTorrent();
|
|
if (!preparationResult)
|
|
return preparationResult.get_unexpected();
|
|
|
|
const nonstd::expected<void, QString> saveResult = Utils::IO::saveToFile(path, preparationResult.value());
|
|
if (!saveResult)
|
|
return saveResult.get_unexpected();
|
|
|
|
return {};
|
|
}
|
|
|
|
QFuture<QList<PeerInfo>> TorrentImpl::fetchPeerInfo() const
|
|
{
|
|
return invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo>
|
|
{
|
|
try
|
|
{
|
|
std::vector<lt::peer_info> nativePeers;
|
|
nativeHandle.get_peer_info(nativePeers);
|
|
QList<PeerInfo> peers;
|
|
peers.reserve(static_cast<decltype(peers)::size_type>(nativePeers.size()));
|
|
for (const lt::peer_info &peer : nativePeers)
|
|
peers.append(PeerInfo(peer, allPieces));
|
|
return peers;
|
|
}
|
|
catch (const std::exception &) {}
|
|
|
|
return {};
|
|
});
|
|
}
|
|
|
|
QFuture<QList<QUrl>> TorrentImpl::fetchURLSeeds() const
|
|
{
|
|
return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl>
|
|
{
|
|
try
|
|
{
|
|
const std::set<std::string> currentSeeds = nativeHandle.url_seeds();
|
|
QList<QUrl> urlSeeds;
|
|
urlSeeds.reserve(static_cast<decltype(urlSeeds)::size_type>(currentSeeds.size()));
|
|
for (const std::string &urlSeed : currentSeeds)
|
|
urlSeeds.append(QString::fromStdString(urlSeed));
|
|
return urlSeeds;
|
|
}
|
|
catch (const std::exception &) {}
|
|
|
|
return {};
|
|
});
|
|
}
|
|
|
|
QFuture<QList<int>> TorrentImpl::fetchPieceAvailability() const
|
|
{
|
|
return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int>
|
|
{
|
|
try
|
|
{
|
|
std::vector<int> piecesAvailability;
|
|
nativeHandle.piece_availability(piecesAvailability);
|
|
return QList<int>(piecesAvailability.cbegin(), piecesAvailability.cend());
|
|
}
|
|
catch (const std::exception &) {}
|
|
|
|
return {};
|
|
});
|
|
}
|
|
|
|
QFuture<QBitArray> TorrentImpl::fetchDownloadingPieces() const
|
|
{
|
|
return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray
|
|
{
|
|
try
|
|
{
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
const std::vector<lt::partial_piece_info> queue = nativeHandle.get_download_queue();
|
|
#else
|
|
std::vector<lt::partial_piece_info> queue;
|
|
nativeHandle.get_download_queue(queue);
|
|
#endif
|
|
QBitArray result;
|
|
result.resize(torrentInfo.piecesCount());
|
|
for (const lt::partial_piece_info &info : queue)
|
|
result.setBit(LT::toUnderlyingType(info.piece_index));
|
|
return result;
|
|
}
|
|
catch (const std::exception &) {}
|
|
|
|
return {};
|
|
});
|
|
}
|
|
|
|
QFuture<QList<qreal>> TorrentImpl::fetchAvailableFileFractions() const
|
|
{
|
|
return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal>
|
|
{
|
|
if (!torrentInfo.isValid() || (torrentInfo.filesCount() <= 0))
|
|
return {};
|
|
|
|
try
|
|
{
|
|
std::vector<int> piecesAvailability;
|
|
nativeHandle.piece_availability(piecesAvailability);
|
|
const int filesCount = torrentInfo.filesCount();
|
|
// libtorrent returns empty array for seeding only torrents
|
|
if (piecesAvailability.empty())
|
|
return QList<qreal>(filesCount, -1);
|
|
|
|
QList<qreal> result;
|
|
result.reserve(filesCount);
|
|
for (int i = 0; i < filesCount; ++i)
|
|
{
|
|
const TorrentInfo::PieceRange filePieces = torrentInfo.filePieces(i);
|
|
|
|
int availablePieces = 0;
|
|
for (const int piece : filePieces)
|
|
availablePieces += (piecesAvailability[piece] > 0) ? 1 : 0;
|
|
|
|
const qreal availability = filePieces.isEmpty()
|
|
? 1 // the file has no pieces, so it is available by default
|
|
: static_cast<qreal>(availablePieces) / filePieces.size();
|
|
result.append(availability);
|
|
}
|
|
return result;
|
|
}
|
|
catch (const std::exception &) {}
|
|
|
|
return {};
|
|
});
|
|
}
|
|
|
|
void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
|
|
{
|
|
if (!hasMetadata())
|
|
return;
|
|
|
|
Q_ASSERT(priorities.size() == filesCount());
|
|
|
|
// Reset 'm_hasSeedStatus' if needed in order to react again to
|
|
// "torrent finished" event and e.g. show tray notifications
|
|
const QList<DownloadPriority> oldPriorities = filePriorities();
|
|
for (int i = 0; i < oldPriorities.size(); ++i)
|
|
{
|
|
if ((oldPriorities[i] == DownloadPriority::Ignored)
|
|
&& (priorities[i] > DownloadPriority::Ignored)
|
|
&& !m_completedFiles.at(i))
|
|
{
|
|
m_hasFinishedStatus = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const int internalFilesCount = m_torrentInfo.nativeInfo()->files().num_files(); // including .pad files
|
|
auto nativePriorities = std::vector<lt::download_priority_t>(internalFilesCount, LT::toNative(DownloadPriority::Normal));
|
|
const auto nativeIndexes = m_torrentInfo.nativeIndexes();
|
|
for (int i = 0; i < priorities.size(); ++i)
|
|
nativePriorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(priorities[i]);
|
|
|
|
qDebug() << Q_FUNC_INFO << "Changing files priorities...";
|
|
m_nativeHandle.prioritize_files(nativePriorities);
|
|
|
|
m_filePriorities = priorities;
|
|
// Restore first/last piece first option if necessary
|
|
if (m_hasFirstLastPiecePriority)
|
|
applyFirstLastPiecePriority(true);
|
|
manageActualFilePaths();
|
|
}
|
|
|
|
template <typename Func>
|
|
QFuture<std::invoke_result_t<Func>> TorrentImpl::invokeAsync(Func &&func) const
|
|
{
|
|
QPromise<std::invoke_result_t<Func>> promise;
|
|
const auto future = promise.future();
|
|
promise.start();
|
|
m_session->invokeAsync([func = std::forward<Func>(func), promise = std::move(promise)]() mutable
|
|
{
|
|
promise.addResult(func());
|
|
promise.finish();
|
|
});
|
|
|
|
return future;
|
|
}
|