Some work about adaptive color scheme for Web UI (PR #19901)
http://[316:c51a:62a3:8b9::4]/d4708/qBittorrent/src/branch/adaptive-webui
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2327 lines
70 KiB
2327 lines
70 KiB
/* |
|
* Bittorrent Client using Qt and libtorrent. |
|
* Copyright (C) 2015 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> |
|
#include <type_traits> |
|
|
|
#ifdef Q_OS_WIN |
|
#include <Windows.h> |
|
#endif |
|
|
|
#include <libtorrent/address.hpp> |
|
#include <libtorrent/alert_types.hpp> |
|
#include <libtorrent/magnet_uri.hpp> |
|
#include <libtorrent/session.hpp> |
|
#include <libtorrent/storage_defs.hpp> |
|
#include <libtorrent/time.hpp> |
|
|
|
#ifdef QBT_USES_LIBTORRENT2 |
|
#include <libtorrent/info_hash.hpp> |
|
#endif |
|
|
|
#include <QBitArray> |
|
#include <QDebug> |
|
#include <QDir> |
|
#include <QFile> |
|
#include <QStringList> |
|
#include <QUrl> |
|
|
|
#include "base/global.h" |
|
#include "base/logger.h" |
|
#include "base/preferences.h" |
|
#include "base/utils/fs.h" |
|
#include "base/utils/string.h" |
|
#include "common.h" |
|
#include "downloadpriority.h" |
|
#include "loadtorrentparams.h" |
|
#include "ltqhash.h" |
|
#include "lttypecast.h" |
|
#include "peeraddress.h" |
|
#include "peerinfo.h" |
|
#include "session.h" |
|
#include "trackerentry.h" |
|
|
|
using namespace BitTorrent; |
|
|
|
namespace |
|
{ |
|
lt::announce_entry makeNativeAnnouncerEntry(const QString &url, const int tier) |
|
{ |
|
lt::announce_entry entry {url.toStdString()}; |
|
entry.tier = tier; |
|
return entry; |
|
} |
|
|
|
#ifdef QBT_USES_LIBTORRENT2 |
|
TrackerEntry fromNativeAnnouncerEntry(const lt::announce_entry &nativeEntry |
|
, const lt::info_hash_t &hashes, const QMap<lt::tcp::endpoint, int> &trackerPeerCounts) |
|
#else |
|
TrackerEntry fromNativeAnnouncerEntry(const lt::announce_entry &nativeEntry |
|
, const QMap<lt::tcp::endpoint, int> &trackerPeerCounts) |
|
#endif |
|
{ |
|
TrackerEntry trackerEntry {QString::fromStdString(nativeEntry.url), nativeEntry.tier}; |
|
|
|
int numUpdating = 0; |
|
int numWorking = 0; |
|
int numNotWorking = 0; |
|
QString firstTrackerMessage; |
|
QString firstErrorMessage; |
|
#ifdef QBT_USES_LIBTORRENT2 |
|
const auto numEndpoints = static_cast<qsizetype>(nativeEntry.endpoints.size() * ((hashes.has_v1() && hashes.has_v2()) ? 2 : 1)); |
|
trackerEntry.endpoints.reserve(static_cast<decltype(trackerEntry.endpoints)::size_type>(numEndpoints)); |
|
for (const lt::announce_endpoint &endpoint : nativeEntry.endpoints) |
|
{ |
|
for (const auto protocolVersion : {lt::protocol_version::V1, lt::protocol_version::V2}) |
|
{ |
|
if (hashes.has(protocolVersion)) |
|
{ |
|
const lt::announce_infohash &infoHash = endpoint.info_hashes[protocolVersion]; |
|
|
|
TrackerEntry::EndpointStats trackerEndpoint; |
|
trackerEndpoint.protocolVersion = (protocolVersion == lt::protocol_version::V1) ? 1 : 2; |
|
trackerEndpoint.numPeers = trackerPeerCounts.value(endpoint.local_endpoint, -1); |
|
trackerEndpoint.numSeeds = infoHash.scrape_complete; |
|
trackerEndpoint.numLeeches = infoHash.scrape_incomplete; |
|
trackerEndpoint.numDownloaded = infoHash.scrape_downloaded; |
|
|
|
if (infoHash.updating) |
|
{ |
|
trackerEndpoint.status = TrackerEntry::Updating; |
|
++numUpdating; |
|
} |
|
else if (infoHash.fails > 0) |
|
{ |
|
trackerEndpoint.status = TrackerEntry::NotWorking; |
|
++numNotWorking; |
|
} |
|
else if (nativeEntry.verified) |
|
{ |
|
trackerEndpoint.status = TrackerEntry::Working; |
|
++numWorking; |
|
} |
|
else |
|
{ |
|
trackerEndpoint.status = TrackerEntry::NotContacted; |
|
} |
|
|
|
const QString trackerMessage = QString::fromStdString(infoHash.message); |
|
const QString errorMessage = QString::fromLocal8Bit(infoHash.last_error.message().c_str()); |
|
trackerEndpoint.message = (!trackerMessage.isEmpty() ? trackerMessage : errorMessage); |
|
|
|
trackerEntry.endpoints.append(trackerEndpoint); |
|
trackerEntry.numPeers = std::max(trackerEntry.numPeers, trackerEndpoint.numPeers); |
|
trackerEntry.numSeeds = std::max(trackerEntry.numSeeds, trackerEndpoint.numSeeds); |
|
trackerEntry.numLeeches = std::max(trackerEntry.numLeeches, trackerEndpoint.numLeeches); |
|
trackerEntry.numDownloaded = std::max(trackerEntry.numDownloaded, trackerEndpoint.numDownloaded); |
|
|
|
if (firstTrackerMessage.isEmpty()) |
|
firstTrackerMessage = trackerMessage; |
|
if (firstErrorMessage.isEmpty()) |
|
firstErrorMessage = errorMessage; |
|
} |
|
} |
|
} |
|
#else |
|
const auto numEndpoints = static_cast<qsizetype>(nativeEntry.endpoints.size()); |
|
trackerEntry.endpoints.reserve(static_cast<decltype(trackerEntry.endpoints)::size_type>(numEndpoints)); |
|
for (const lt::announce_endpoint &endpoint : nativeEntry.endpoints) |
|
{ |
|
TrackerEntry::EndpointStats trackerEndpoint; |
|
trackerEndpoint.numPeers = trackerPeerCounts.value(endpoint.local_endpoint, -1); |
|
trackerEndpoint.numSeeds = endpoint.scrape_complete; |
|
trackerEndpoint.numLeeches = endpoint.scrape_incomplete; |
|
trackerEndpoint.numDownloaded = endpoint.scrape_downloaded; |
|
|
|
if (endpoint.updating) |
|
{ |
|
trackerEndpoint.status = TrackerEntry::Updating; |
|
++numUpdating; |
|
} |
|
else if (endpoint.fails > 0) |
|
{ |
|
trackerEndpoint.status = TrackerEntry::NotWorking; |
|
++numNotWorking; |
|
} |
|
else if (nativeEntry.verified) |
|
{ |
|
trackerEndpoint.status = TrackerEntry::Working; |
|
++numWorking; |
|
} |
|
else |
|
{ |
|
trackerEndpoint.status = TrackerEntry::NotContacted; |
|
} |
|
|
|
const QString trackerMessage = QString::fromStdString(endpoint.message); |
|
const QString errorMessage = QString::fromLocal8Bit(endpoint.last_error.message().c_str()); |
|
trackerEndpoint.message = (!trackerMessage.isEmpty() ? trackerMessage : errorMessage); |
|
|
|
trackerEntry.endpoints.append(trackerEndpoint); |
|
trackerEntry.numPeers = std::max(trackerEntry.numPeers, trackerEndpoint.numPeers); |
|
trackerEntry.numSeeds = std::max(trackerEntry.numSeeds, trackerEndpoint.numSeeds); |
|
trackerEntry.numLeeches = std::max(trackerEntry.numLeeches, trackerEndpoint.numLeeches); |
|
trackerEntry.numDownloaded = std::max(trackerEntry.numDownloaded, trackerEndpoint.numDownloaded); |
|
|
|
if (firstTrackerMessage.isEmpty()) |
|
firstTrackerMessage = trackerMessage; |
|
if (firstErrorMessage.isEmpty()) |
|
firstErrorMessage = errorMessage; |
|
} |
|
#endif |
|
|
|
if (numEndpoints > 0) |
|
{ |
|
if (numUpdating > 0) |
|
{ |
|
trackerEntry.status = TrackerEntry::Updating; |
|
} |
|
else if (numWorking > 0) |
|
{ |
|
trackerEntry.status = TrackerEntry::Working; |
|
trackerEntry.message = firstTrackerMessage; |
|
} |
|
else if (numNotWorking == numEndpoints) |
|
{ |
|
trackerEntry.status = TrackerEntry::NotWorking; |
|
trackerEntry.message = (!firstTrackerMessage.isEmpty() ? firstTrackerMessage : firstErrorMessage); |
|
} |
|
} |
|
|
|
return trackerEntry; |
|
} |
|
|
|
void initializeStatus(lt::torrent_status &status, const lt::add_torrent_params ¶ms) |
|
{ |
|
status.flags = params.flags; |
|
status.active_duration = lt::seconds {params.active_time}; |
|
status.finished_duration = lt::seconds {params.finished_time}; |
|
status.seeding_duration = lt::seconds {params.seeding_time}; |
|
status.num_complete = params.num_complete; |
|
status.num_incomplete = params.num_incomplete; |
|
status.all_time_download = params.total_downloaded; |
|
status.all_time_upload = params.total_uploaded; |
|
status.added_time = params.added_time; |
|
status.last_seen_complete = params.last_seen_complete; |
|
status.last_download = lt::time_point {lt::seconds {params.last_download}}; |
|
status.last_upload = lt::time_point {lt::seconds {params.last_upload}}; |
|
status.completed_time = params.completed_time; |
|
status.save_path = params.save_path; |
|
status.connections_limit = params.max_connections; |
|
status.pieces = params.have_pieces; |
|
status.verified_pieces = params.verified_pieces; |
|
} |
|
} |
|
|
|
// TorrentImpl |
|
|
|
TorrentImpl::TorrentImpl(Session *session, lt::session *nativeSession |
|
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams ¶ms) |
|
: QObject(session) |
|
, m_session(session) |
|
, m_nativeSession(nativeSession) |
|
, 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(Utils::Fs::toNativePath(params.savePath)) |
|
, m_category(params.category) |
|
, m_tags(params.tags) |
|
, m_ratioLimit(params.ratioLimit) |
|
, m_seedingTimeLimit(params.seedingTimeLimit) |
|
, m_operatingMode(params.operatingMode) |
|
, m_contentLayout(params.contentLayout) |
|
, m_hasSeedStatus(params.hasSeedStatus) |
|
, m_hasFirstLastPiecePriority(params.firstLastPiecePriority) |
|
, m_useAutoTMM(params.savePath.isEmpty()) |
|
, m_isStopped(params.stopped) |
|
, m_ltAddTorrentParams(params.ltAddTorrentParams) |
|
{ |
|
if (m_useAutoTMM) |
|
m_savePath = Utils::Fs::toNativePath(m_session->categorySavePath(m_category)); |
|
|
|
if (m_ltAddTorrentParams.ti) |
|
{ |
|
// Initialize it only if torrent is added with metadata. |
|
// Otherwise it should be initialized in "Metadata received" handler. |
|
m_torrentInfo = TorrentInfo {m_nativeHandle.torrent_file()}; |
|
} |
|
|
|
initializeStatus(m_nativeStatus, m_ltAddTorrentParams); |
|
updateState(); |
|
|
|
if (hasMetadata()) |
|
applyFirstLastPiecePriority(m_hasFirstLastPiecePriority); |
|
|
|
// TODO: Remove the following upgrade code in v.4.4 |
|
// == BEGIN UPGRADE CODE == |
|
const QString spath = actualStorageLocation(); |
|
for (int i = 0; i < filesCount(); ++i) |
|
{ |
|
const QString filepath = filePath(i); |
|
// Move "unwanted" files back to their original folder |
|
const QString parentRelPath = Utils::Fs::branchPath(filepath); |
|
if (QDir(parentRelPath).dirName() == ".unwanted") |
|
{ |
|
const QString oldName = Utils::Fs::fileName(filepath); |
|
const QString newRelPath = Utils::Fs::branchPath(parentRelPath); |
|
if (newRelPath.isEmpty()) |
|
renameFile(i, oldName); |
|
else |
|
renameFile(i, QDir(newRelPath).filePath(oldName)); |
|
|
|
// Remove .unwanted directory if empty |
|
qDebug() << "Attempting to remove \".unwanted\" folder at " << QDir(spath + '/' + newRelPath).absoluteFilePath(".unwanted"); |
|
QDir(spath + '/' + newRelPath).rmdir(".unwanted"); |
|
} |
|
} |
|
// == END UPGRADE CODE == |
|
} |
|
|
|
TorrentImpl::~TorrentImpl() {} |
|
|
|
bool TorrentImpl::isValid() const |
|
{ |
|
return m_nativeHandle.is_valid(); |
|
} |
|
|
|
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_torrentInfo.creationDate(); |
|
} |
|
|
|
QString TorrentImpl::creator() const |
|
{ |
|
return m_torrentInfo.creator(); |
|
} |
|
|
|
QString TorrentImpl::comment() const |
|
{ |
|
return m_torrentInfo.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 |
|
{ |
|
return QString::fromStdString(m_nativeStatus.current_tracker); |
|
} |
|
|
|
QString TorrentImpl::savePath(bool actual) const |
|
{ |
|
if (actual) |
|
return Utils::Fs::toUniformPath(actualStorageLocation()); |
|
else |
|
return Utils::Fs::toUniformPath(m_savePath); |
|
} |
|
|
|
QString TorrentImpl::rootPath(bool actual) const |
|
{ |
|
if (!hasMetadata()) |
|
return {}; |
|
|
|
const QString firstFilePath = filePath(0); |
|
const int slashIndex = firstFilePath.indexOf('/'); |
|
if (slashIndex >= 0) |
|
return QDir(savePath(actual)).absoluteFilePath(firstFilePath.left(slashIndex)); |
|
else |
|
return QDir(savePath(actual)).absoluteFilePath(firstFilePath); |
|
} |
|
|
|
QString TorrentImpl::contentPath(const bool actual) const |
|
{ |
|
if (!hasMetadata()) |
|
return {}; |
|
|
|
if (filesCount() == 1) |
|
return QDir(savePath(actual)).absoluteFilePath(filePath(0)); |
|
|
|
if (m_torrentInfo.hasRootFolder()) |
|
return rootPath(actual); |
|
|
|
return savePath(actual); |
|
} |
|
|
|
bool TorrentImpl::isAutoTMMEnabled() const |
|
{ |
|
return m_useAutoTMM; |
|
} |
|
|
|
void TorrentImpl::setAutoTMMEnabled(bool enabled) |
|
{ |
|
if (m_useAutoTMM == enabled) return; |
|
|
|
m_useAutoTMM = enabled; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentSavingModeChanged(this); |
|
|
|
if (m_useAutoTMM) |
|
move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); |
|
} |
|
|
|
QString TorrentImpl::actualStorageLocation() const |
|
{ |
|
return QString::fromStdString(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); |
|
} |
|
|
|
QVector<TrackerEntry> TorrentImpl::trackers() const |
|
{ |
|
const std::vector<lt::announce_entry> nativeTrackers = m_nativeHandle.trackers(); |
|
|
|
QVector<TrackerEntry> entries; |
|
entries.reserve(static_cast<decltype(entries)::size_type>(nativeTrackers.size())); |
|
|
|
for (const lt::announce_entry &tracker : nativeTrackers) |
|
{ |
|
const QString trackerURL = QString::fromStdString(tracker.url); |
|
#ifdef QBT_USES_LIBTORRENT2 |
|
entries << fromNativeAnnouncerEntry(tracker, m_nativeHandle.info_hashes(), m_trackerPeerCounts[trackerURL]); |
|
#else |
|
entries << fromNativeAnnouncerEntry(tracker, m_trackerPeerCounts[trackerURL]); |
|
#endif |
|
} |
|
|
|
return entries; |
|
} |
|
|
|
void TorrentImpl::addTrackers(const QVector<TrackerEntry> &trackers) |
|
{ |
|
QSet<TrackerEntry> currentTrackers; |
|
for (const lt::announce_entry &entry : m_nativeHandle.trackers()) |
|
currentTrackers.insert({QString::fromStdString(entry.url), entry.tier}); |
|
|
|
QVector<TrackerEntry> newTrackers; |
|
newTrackers.reserve(trackers.size()); |
|
|
|
for (const TrackerEntry &tracker : trackers) |
|
{ |
|
if (!currentTrackers.contains(tracker)) |
|
{ |
|
m_nativeHandle.add_tracker(makeNativeAnnouncerEntry(tracker.url, tracker.tier)); |
|
newTrackers << tracker; |
|
} |
|
} |
|
|
|
if (!newTrackers.isEmpty()) |
|
{ |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentTrackersAdded(this, newTrackers); |
|
} |
|
} |
|
|
|
void TorrentImpl::replaceTrackers(const QVector<TrackerEntry> &trackers) |
|
{ |
|
QVector<TrackerEntry> currentTrackers = this->trackers(); |
|
|
|
QVector<TrackerEntry> newTrackers; |
|
newTrackers.reserve(trackers.size()); |
|
|
|
std::vector<lt::announce_entry> nativeTrackers; |
|
nativeTrackers.reserve(trackers.size()); |
|
|
|
for (const TrackerEntry &tracker : trackers) |
|
{ |
|
nativeTrackers.emplace_back(makeNativeAnnouncerEntry(tracker.url, tracker.tier)); |
|
|
|
if (!currentTrackers.removeOne(tracker)) |
|
newTrackers << tracker; |
|
} |
|
|
|
m_nativeHandle.replace_trackers(nativeTrackers); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
|
|
if (newTrackers.isEmpty() && currentTrackers.isEmpty()) |
|
{ |
|
// when existing tracker reorders |
|
m_session->handleTorrentTrackersChanged(this); |
|
} |
|
else |
|
{ |
|
if (!currentTrackers.isEmpty()) |
|
m_session->handleTorrentTrackersRemoved(this, currentTrackers); |
|
|
|
if (!newTrackers.isEmpty()) |
|
m_session->handleTorrentTrackersAdded(this, newTrackers); |
|
|
|
// 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(); |
|
} |
|
} |
|
|
|
QVector<QUrl> TorrentImpl::urlSeeds() const |
|
{ |
|
const std::set<std::string> currentSeeds = m_nativeHandle.url_seeds(); |
|
|
|
QVector<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; |
|
} |
|
|
|
void TorrentImpl::addUrlSeeds(const QVector<QUrl> &urlSeeds) |
|
{ |
|
const std::set<std::string> currentSeeds = m_nativeHandle.url_seeds(); |
|
|
|
QVector<QUrl> addedUrlSeeds; |
|
addedUrlSeeds.reserve(urlSeeds.size()); |
|
|
|
for (const QUrl &url : urlSeeds) |
|
{ |
|
const std::string nativeUrl = url.toString().toStdString(); |
|
if (currentSeeds.find(nativeUrl) == currentSeeds.end()) |
|
{ |
|
m_nativeHandle.add_url_seed(nativeUrl); |
|
addedUrlSeeds << url; |
|
} |
|
} |
|
|
|
if (!addedUrlSeeds.isEmpty()) |
|
{ |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentUrlSeedsAdded(this, addedUrlSeeds); |
|
} |
|
} |
|
|
|
void TorrentImpl::removeUrlSeeds(const QVector<QUrl> &urlSeeds) |
|
{ |
|
const std::set<std::string> currentSeeds = m_nativeHandle.url_seeds(); |
|
|
|
QVector<QUrl> removedUrlSeeds; |
|
removedUrlSeeds.reserve(urlSeeds.size()); |
|
|
|
for (const QUrl &url : urlSeeds) |
|
{ |
|
const std::string nativeUrl = url.toString().toStdString(); |
|
if (currentSeeds.find(nativeUrl) != currentSeeds.end()) |
|
{ |
|
m_nativeHandle.remove_url_seed(nativeUrl); |
|
removedUrlSeeds << url; |
|
} |
|
} |
|
|
|
if (!removedUrlSeeds.isEmpty()) |
|
{ |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentUrlSeedsRemoved(this, removedUrlSeeds); |
|
} |
|
} |
|
|
|
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_nativeHandle.need_save_resume_data(); |
|
} |
|
|
|
void TorrentImpl::saveResumeData() |
|
{ |
|
m_nativeHandle.save_resume_data(); |
|
m_session->handleTorrentSaveResumeDataRequested(this); |
|
} |
|
|
|
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; |
|
Q_ASSERT((progress >= 0.f) && (progress <= 1.f)); |
|
return progress; |
|
} |
|
|
|
QString TorrentImpl::category() const |
|
{ |
|
return m_category; |
|
} |
|
|
|
bool TorrentImpl::belongsToCategory(const QString &category) const |
|
{ |
|
if (m_category.isEmpty()) return category.isEmpty(); |
|
if (!Session::isValidCategoryName(category)) return false; |
|
|
|
if (m_category == category) return true; |
|
|
|
if (m_session->isSubcategoriesEnabled() && m_category.startsWith(category + '/')) |
|
return true; |
|
|
|
return false; |
|
} |
|
|
|
TagSet TorrentImpl::tags() const |
|
{ |
|
return m_tags; |
|
} |
|
|
|
bool TorrentImpl::hasTag(const QString &tag) const |
|
{ |
|
return m_tags.contains(tag); |
|
} |
|
|
|
bool TorrentImpl::addTag(const QString &tag) |
|
{ |
|
if (!Session::isValidTag(tag)) |
|
return false; |
|
if (hasTag(tag)) |
|
return false; |
|
|
|
if (!m_session->hasTag(tag)) |
|
{ |
|
if (!m_session->addTag(tag)) |
|
return false; |
|
} |
|
m_tags.insert(tag); |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentTagAdded(this, tag); |
|
return true; |
|
} |
|
|
|
bool TorrentImpl::removeTag(const QString &tag) |
|
{ |
|
if (m_tags.remove(tag)) |
|
{ |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentTagRemoved(this, tag); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
void TorrentImpl::removeAllTags() |
|
{ |
|
for (const QString &tag : asConst(tags())) |
|
removeTag(tag); |
|
} |
|
|
|
QDateTime TorrentImpl::addedTime() const |
|
{ |
|
return QDateTime::fromSecsSinceEpoch(m_nativeStatus.added_time); |
|
} |
|
|
|
qreal TorrentImpl::ratioLimit() const |
|
{ |
|
return m_ratioLimit; |
|
} |
|
|
|
int TorrentImpl::seedingTimeLimit() const |
|
{ |
|
return m_seedingTimeLimit; |
|
} |
|
|
|
QString TorrentImpl::filePath(const int index) const |
|
{ |
|
return m_torrentInfo.filePath(index); |
|
} |
|
|
|
qlonglong TorrentImpl::fileSize(const int index) const |
|
{ |
|
return m_torrentInfo.fileSize(index); |
|
} |
|
|
|
QStringList TorrentImpl::filePaths() const |
|
{ |
|
return m_torrentInfo.filePaths(); |
|
} |
|
|
|
// Return a list of absolute paths corresponding |
|
// to all files in a torrent |
|
QStringList TorrentImpl::absoluteFilePaths() const |
|
{ |
|
if (!hasMetadata()) return {}; |
|
|
|
const QDir saveDir {savePath(true)}; |
|
QStringList res; |
|
res.reserve(filesCount()); |
|
for (int i = 0; i < filesCount(); ++i) |
|
res << Utils::Fs::expandPathAbs(saveDir.absoluteFilePath(filePath(i))); |
|
return res; |
|
} |
|
|
|
QVector<DownloadPriority> TorrentImpl::filePriorities() const |
|
{ |
|
if (!hasMetadata()) |
|
return {}; |
|
|
|
const std::vector<lt::download_priority_t> fp = m_nativeHandle.get_file_priorities(); |
|
|
|
QVector<DownloadPriority> ret; |
|
ret.reserve(filesCount()); |
|
for (const lt::file_index_t nativeIndex : asConst(m_torrentInfo.nativeIndexes())) |
|
{ |
|
const auto priority = LT::fromNative(fp[LT::toUnderlyingType(nativeIndex)]); |
|
ret.append(priority); |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
TorrentInfo TorrentImpl::info() const |
|
{ |
|
return m_torrentInfo; |
|
} |
|
|
|
bool TorrentImpl::isPaused() const |
|
{ |
|
return m_isStopped; |
|
} |
|
|
|
bool TorrentImpl::isQueued() const |
|
{ |
|
// Torrent is Queued if it isn't in Paused state but paused internally |
|
return (!isPaused() |
|
&& (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 |
|
{ |
|
return m_state == TorrentState::Downloading |
|
|| m_state == TorrentState::DownloadingMetadata |
|
|| m_state == TorrentState::ForcedDownloadingMetadata |
|
|| m_state == TorrentState::StalledDownloading |
|
|| m_state == TorrentState::CheckingDownloading |
|
|| m_state == TorrentState::PausedDownloading |
|
|| m_state == TorrentState::QueuedDownloading |
|
|| m_state == TorrentState::ForcedDownloading; |
|
} |
|
|
|
bool TorrentImpl::isUploading() const |
|
{ |
|
return m_state == TorrentState::Uploading |
|
|| m_state == TorrentState::StalledUploading |
|
|| m_state == TorrentState::CheckingUploading |
|
|| m_state == TorrentState::QueuedUploading |
|
|| m_state == TorrentState::ForcedUploading; |
|
} |
|
|
|
bool TorrentImpl::isCompleted() const |
|
{ |
|
return m_state == TorrentState::Uploading |
|
|| m_state == TorrentState::StalledUploading |
|
|| m_state == TorrentState::CheckingUploading |
|
|| m_state == TorrentState::PausedUploading |
|
|| m_state == TorrentState::QueuedUploading |
|
|| m_state == TorrentState::ForcedUploading; |
|
} |
|
|
|
bool TorrentImpl::isActive() const |
|
{ |
|
if (m_state == TorrentState::StalledDownloading) |
|
return (uploadPayloadRate() > 0); |
|
|
|
return m_state == TorrentState::DownloadingMetadata |
|
|| m_state == TorrentState::ForcedDownloadingMetadata |
|
|| m_state == TorrentState::Downloading |
|
|| m_state == TorrentState::ForcedDownloading |
|
|| m_state == TorrentState::Uploading |
|
|| m_state == TorrentState::ForcedUploading |
|
|| m_state == TorrentState::Moving; |
|
} |
|
|
|
bool TorrentImpl::isInactive() const |
|
{ |
|
return !isActive(); |
|
} |
|
|
|
bool TorrentImpl::isErrored() const |
|
{ |
|
return m_state == TorrentState::MissingFiles |
|
|| m_state == TorrentState::Error; |
|
} |
|
|
|
bool TorrentImpl::isSeed() const |
|
{ |
|
return ((m_nativeStatus.state == lt::torrent_status::finished) |
|
|| (m_nativeStatus.state == lt::torrent_status::seeding)); |
|
} |
|
|
|
bool TorrentImpl::isForced() const |
|
{ |
|
return (!isPaused() && (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 (isPaused()) |
|
m_state = TorrentState::PausedDownloading; |
|
else if (m_session->isQueueingSystemEnabled() && isQueued()) |
|
m_state = TorrentState::QueuedDownloading; |
|
else |
|
m_state = isForced() ? TorrentState::ForcedDownloadingMetadata : TorrentState::DownloadingMetadata; |
|
} |
|
else if ((m_nativeStatus.state == lt::torrent_status::checking_files) |
|
&& (!isPaused() || (m_nativeStatus.flags & lt::torrent_flags::auto_managed) |
|
|| !(m_nativeStatus.flags & lt::torrent_flags::paused))) |
|
{ |
|
// If the torrent is not just in the "checking" state, but is being actually checked |
|
m_state = m_hasSeedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading; |
|
} |
|
else if (isSeed()) |
|
{ |
|
if (isPaused()) |
|
m_state = TorrentState::PausedUploading; |
|
else if (m_session->isQueueingSystemEnabled() && 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 (isPaused()) |
|
m_state = TorrentState::PausedDownloading; |
|
else if (m_session->isQueueingSystemEnabled() && 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 QString::fromStdString(m_nativeStatus.errc.message()); |
|
|
|
if (m_nativeStatus.flags & lt::torrent_flags::upload_mode) |
|
{ |
|
const QString writeErrorStr = tr("Couldn't write to file."); |
|
const QString uploadModeStr = tr("Torrent is currently in \"upload only\" mode."); |
|
const QString errorMessage = QString::fromLocal8Bit(m_lastFileError.error.message().c_str()); |
|
|
|
return writeErrorStr + QLatin1Char(' ') + errorMessage + QLatin1String(". ") + uploadModeStr; |
|
} |
|
|
|
return {}; |
|
} |
|
|
|
qlonglong TorrentImpl::totalDownload() const |
|
{ |
|
return m_nativeStatus.all_time_download; |
|
} |
|
|
|
qlonglong TorrentImpl::totalUpload() const |
|
{ |
|
return m_nativeStatus.all_time_upload; |
|
} |
|
|
|
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::seedingTime() const |
|
{ |
|
return lt::total_seconds(m_nativeStatus.seeding_duration); |
|
} |
|
|
|
qlonglong TorrentImpl::eta() const |
|
{ |
|
if (isPaused()) return MAX_ETA; |
|
|
|
const SpeedSampleAvg speedAverage = m_speedMonitor.average(); |
|
|
|
if (isSeed()) |
|
{ |
|
const qreal maxRatioValue = maxRatio(); |
|
const int maxSeedingTimeValue = maxSeedingTime(); |
|
if ((maxRatioValue < 0) && (maxSeedingTimeValue < 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) - seedingTime(); |
|
if (seedingTimeEta < 0) |
|
seedingTimeEta = 0; |
|
} |
|
|
|
return qMin(ratioEta, seedingTimeEta); |
|
} |
|
|
|
if (!speedAverage.download) return MAX_ETA; |
|
|
|
return (wantedSize() - completedSize()) / speedAverage.download; |
|
} |
|
|
|
QVector<qreal> TorrentImpl::filesProgress() const |
|
{ |
|
if (!hasMetadata()) |
|
return {}; |
|
|
|
std::vector<int64_t> fp; |
|
m_nativeHandle.file_progress(fp, lt::torrent_handle::piece_granularity); |
|
|
|
const int count = filesCount(); |
|
const auto nativeIndexes = m_torrentInfo.nativeIndexes(); |
|
QVector<qreal> result; |
|
result.reserve(count); |
|
for (int i = 0; i < count; ++i) |
|
{ |
|
const int64_t progress = fp[LT::toUnderlyingType(nativeIndexes[i])]; |
|
const qlonglong 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 > 0) ? 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 > 0) ? peers : m_nativeStatus.list_peers; |
|
} |
|
|
|
int TorrentImpl::totalLeechersCount() const |
|
{ |
|
return (m_nativeStatus.num_incomplete > 0) ? m_nativeStatus.num_incomplete : (m_nativeStatus.list_peers - m_nativeStatus.list_seeds); |
|
} |
|
|
|
int TorrentImpl::completeCount() const |
|
{ |
|
// additional info: https://github.com/qbittorrent/qBittorrent/pull/5300#issuecomment-267783646 |
|
return m_nativeStatus.num_complete; |
|
} |
|
|
|
int TorrentImpl::incompleteCount() const |
|
{ |
|
// additional info: https://github.com/qbittorrent/qBittorrent/pull/5300#issuecomment-267783646 |
|
return m_nativeStatus.num_incomplete; |
|
} |
|
|
|
QDateTime TorrentImpl::lastSeenComplete() const |
|
{ |
|
if (m_nativeStatus.last_seen_complete > 0) |
|
return QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete); |
|
else |
|
return {}; |
|
} |
|
|
|
QDateTime TorrentImpl::completedTime() const |
|
{ |
|
if (m_nativeStatus.completed_time > 0) |
|
return QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time); |
|
else |
|
return {}; |
|
} |
|
|
|
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); |
|
} |
|
|
|
int TorrentImpl::downloadLimit() const |
|
{ |
|
return m_nativeHandle.download_limit(); |
|
} |
|
|
|
int TorrentImpl::uploadLimit() const |
|
{ |
|
return m_nativeHandle.upload_limit(); |
|
} |
|
|
|
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); |
|
} |
|
|
|
QVector<PeerInfo> TorrentImpl::peers() const |
|
{ |
|
std::vector<lt::peer_info> nativePeers; |
|
m_nativeHandle.get_peer_info(nativePeers); |
|
|
|
QVector<PeerInfo> peers; |
|
peers.reserve(static_cast<decltype(peers)::size_type>(nativePeers.size())); |
|
|
|
for (const lt::peer_info &peer : nativePeers) |
|
peers << PeerInfo(this, peer); |
|
|
|
return peers; |
|
} |
|
|
|
QBitArray TorrentImpl::pieces() const |
|
{ |
|
QBitArray result(m_nativeStatus.pieces.size()); |
|
for (int i = 0; i < result.size(); ++i) |
|
{ |
|
if (m_nativeStatus.pieces[lt::piece_index_t {i}]) |
|
result.setBit(i, true); |
|
} |
|
return result; |
|
} |
|
|
|
QBitArray TorrentImpl::downloadingPieces() const |
|
{ |
|
QBitArray result(piecesCount()); |
|
|
|
std::vector<lt::partial_piece_info> queue; |
|
m_nativeHandle.get_download_queue(queue); |
|
|
|
for (const lt::partial_piece_info &info : queue) |
|
result.setBit(LT::toUnderlyingType(info.piece_index)); |
|
|
|
return result; |
|
} |
|
|
|
QVector<int> TorrentImpl::pieceAvailability() const |
|
{ |
|
std::vector<int> avail; |
|
m_nativeHandle.piece_availability(avail); |
|
|
|
return {avail.cbegin(), avail.cend()}; |
|
} |
|
|
|
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; |
|
} |
|
|
|
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 > MAX_RATIO) ? MAX_RATIO : ratio; |
|
} |
|
|
|
int TorrentImpl::uploadPayloadRate() const |
|
{ |
|
return m_nativeStatus.upload_payload_rate; |
|
} |
|
|
|
int TorrentImpl::downloadPayloadRate() const |
|
{ |
|
return 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); |
|
} |
|
|
|
void TorrentImpl::setName(const QString &name) |
|
{ |
|
if (m_name != name) |
|
{ |
|
m_name = name; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentNameChanged(this); |
|
} |
|
} |
|
|
|
bool TorrentImpl::setCategory(const QString &category) |
|
{ |
|
if (m_category != category) |
|
{ |
|
if (!category.isEmpty() && !m_session->categories().contains(category)) |
|
return false; |
|
|
|
const QString oldCategory = m_category; |
|
m_category = category; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentCategoryChanged(this, oldCategory); |
|
|
|
if (m_useAutoTMM) |
|
{ |
|
if (!m_session->isDisableAutoTMMWhenCategoryChanged()) |
|
move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); |
|
else |
|
setAutoTMMEnabled(false); |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void TorrentImpl::move(QString path) |
|
{ |
|
if (m_useAutoTMM) |
|
{ |
|
m_useAutoTMM = false; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentSavingModeChanged(this); |
|
} |
|
|
|
path = Utils::Fs::toUniformPath(path.trimmed()); |
|
if (path.isEmpty()) |
|
path = m_session->defaultSavePath(); |
|
if (!path.endsWith('/')) |
|
path += '/'; |
|
|
|
move_impl(path, MoveStorageMode::KeepExistingFiles); |
|
} |
|
|
|
void TorrentImpl::move_impl(QString path, const MoveStorageMode mode) |
|
{ |
|
if (path == savePath()) return; |
|
|
|
path = Utils::Fs::toNativePath(path); |
|
if (!useTempPath()) |
|
{ |
|
moveStorage(path, mode); |
|
} |
|
else |
|
{ |
|
m_savePath = path; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentSavePathChanged(this); |
|
} |
|
} |
|
|
|
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(); |
|
m_hasMissingFiles = false; |
|
m_unchecked = false; |
|
|
|
if (isPaused()) |
|
{ |
|
// When "force recheck" is applied on paused torrent, we temporarily resume it |
|
// (really we just allow libtorrent to resume it by enabling auto management for it). |
|
m_nativeHandle.set_flags(lt::torrent_flags::stop_when_ready | lt::torrent_flags::auto_managed); |
|
} |
|
} |
|
|
|
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 |
|
} |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
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())); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
void TorrentImpl::applyFirstLastPiecePriority(const bool enabled, const QVector<DownloadPriority> &updatedFilePrio) |
|
{ |
|
Q_ASSERT(hasMetadata()); |
|
|
|
// Download first and last pieces first for every file in the torrent |
|
|
|
const QVector<DownloadPriority> filePriorities = |
|
!updatedFilePrio.isEmpty() ? updatedFilePrio : this->filePriorities(); |
|
std::vector<lt::download_priority_t> piecePriorities = nativeHandle().get_piece_priorities(); |
|
|
|
// 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 index = 0; index < filePriorities.size(); ++index) |
|
{ |
|
const DownloadPriority filePrio = filePriorities[index]; |
|
if (filePrio <= DownloadPriority::Ignored) |
|
continue; |
|
|
|
// Determine the priority to set |
|
const DownloadPriority newPrio = enabled ? DownloadPriority::Maximum : filePrio; |
|
const auto piecePrio = static_cast<lt::download_priority_t>(static_cast<int>(newPrio)); |
|
const TorrentInfo::PieceRange extremities = info().filePieces(index); |
|
|
|
// worst case: AVI index = 1% of total file size (at the end of the file) |
|
const int nNumPieces = std::ceil(fileSize(index) * 0.01 / pieceLength()); |
|
for (int i = 0; i < nNumPieces; ++i) |
|
{ |
|
piecePriorities[extremities.first() + i] = piecePrio; |
|
piecePriorities[extremities.last() - i] = piecePrio; |
|
} |
|
} |
|
|
|
m_nativeHandle.prioritize_pieces(piecePriorities); |
|
} |
|
|
|
void TorrentImpl::fileSearchFinished(const QString &savePath, const QStringList &fileNames) |
|
{ |
|
endReceivedMetadataHandling(savePath, fileNames); |
|
} |
|
|
|
void TorrentImpl::endReceivedMetadataHandling(const QString &savePath, const QStringList &fileNames) |
|
{ |
|
lt::add_torrent_params &p = m_ltAddTorrentParams; |
|
|
|
const TorrentInfo torrentInfo {m_nativeHandle.torrent_file()}; |
|
const auto nativeIndexes = torrentInfo.nativeIndexes(); |
|
for (int i = 0; i < fileNames.size(); ++i) |
|
p.renamed_files[nativeIndexes[i]] = fileNames[i].toStdString(); |
|
p.save_path = Utils::Fs::toNativePath(savePath).toStdString(); |
|
p.ti = torrentInfo.nativeInfo(); |
|
|
|
const int internalFilesCount = p.ti->files().num_files(); // including .pad files |
|
// Use qBittorrent default priority rather than libtorrent's (4) |
|
p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal)); |
|
|
|
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; |
|
updateStatus(); |
|
prepareResumeData(p); |
|
|
|
m_session->handleTorrentMetadataReceived(this); |
|
} |
|
|
|
void TorrentImpl::reload() |
|
{ |
|
const auto queuePos = m_nativeHandle.queue_position(); |
|
|
|
m_nativeSession->remove_torrent(m_nativeHandle, lt::session::delete_partfile); |
|
|
|
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); |
|
} |
|
|
|
m_nativeHandle = m_nativeSession->add_torrent(p); |
|
m_nativeHandle.queue_position_set(queuePos); |
|
|
|
m_torrentInfo = TorrentInfo {m_nativeHandle.torrent_file()}; |
|
} |
|
|
|
void TorrentImpl::pause() |
|
{ |
|
if (!m_isStopped) |
|
{ |
|
m_isStopped = true; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentPaused(this); |
|
} |
|
|
|
if (m_maintenanceJob == MaintenanceJob::None) |
|
{ |
|
setAutoManaged(false); |
|
m_nativeHandle.pause(); |
|
|
|
m_speedMonitor.reset(); |
|
} |
|
} |
|
|
|
void TorrentImpl::resume(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>(m_nativeHandle.torrent_file()); |
|
reload(); |
|
updateStatus(); |
|
return; |
|
} |
|
|
|
if (m_isStopped) |
|
{ |
|
// Torrent may have been temporarily resumed to perform checking files |
|
// so we have to ensure it will not pause after checking is done. |
|
m_nativeHandle.unset_flags(lt::torrent_flags::stop_when_ready); |
|
|
|
m_isStopped = false; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentResumed(this); |
|
} |
|
|
|
if (m_maintenanceJob == MaintenanceJob::None) |
|
{ |
|
setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged); |
|
if (m_operatingMode == TorrentOperatingMode::Forced) |
|
m_nativeHandle.resume(); |
|
} |
|
} |
|
|
|
void TorrentImpl::moveStorage(const QString &newPath, const MoveStorageMode mode) |
|
{ |
|
if (m_session->addMoveTorrentStorageJob(this, newPath, mode)) |
|
{ |
|
m_storageIsMoving = true; |
|
updateStatus(); |
|
} |
|
} |
|
|
|
void TorrentImpl::renameFile(const int index, const QString &path) |
|
{ |
|
#ifndef QBT_USES_LIBTORRENT2 |
|
const QString oldPath = filePath(index); |
|
m_oldPath[index].push_back(oldPath); |
|
#endif |
|
++m_renameCount; |
|
m_nativeHandle.rename_file(m_torrentInfo.nativeIndexes()[index], Utils::Fs::toNativePath(path).toStdString()); |
|
} |
|
|
|
void TorrentImpl::handleStateUpdate(const lt::torrent_status &nativeStatus) |
|
{ |
|
updateStatus(nativeStatus); |
|
} |
|
|
|
void TorrentImpl::handleMoveStorageJobFinished(const bool hasOutstandingJob) |
|
{ |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_storageIsMoving = hasOutstandingJob; |
|
|
|
updateStatus(); |
|
const QString newPath = QString::fromStdString(m_nativeStatus.save_path); |
|
if (!useTempPath() && (newPath != m_savePath)) |
|
{ |
|
m_savePath = newPath; |
|
m_session->handleTorrentSavePathChanged(this); |
|
} |
|
|
|
if (!m_storageIsMoving) |
|
{ |
|
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>(m_nativeHandle.torrent_file()); |
|
reload(); |
|
updateStatus(); |
|
} |
|
|
|
while ((m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) |
|
m_moveFinishedTriggers.takeFirst()(); |
|
} |
|
} |
|
|
|
void TorrentImpl::handleTrackerReplyAlert(const lt::tracker_reply_alert *p) |
|
{ |
|
const QString trackerUrl = p->tracker_url(); |
|
m_trackerPeerCounts[trackerUrl][p->local_endpoint] = p->num_peers; |
|
|
|
m_session->handleTorrentTrackerReply(this, trackerUrl); |
|
} |
|
|
|
void TorrentImpl::handleTrackerWarningAlert(const lt::tracker_warning_alert *p) |
|
{ |
|
const QString trackerUrl = p->tracker_url(); |
|
m_session->handleTorrentTrackerWarning(this, trackerUrl); |
|
} |
|
|
|
void TorrentImpl::handleTrackerErrorAlert(const lt::tracker_error_alert *p) |
|
{ |
|
const QString trackerUrl = p->tracker_url(); |
|
|
|
// Starting with libtorrent 1.2.x each tracker has multiple local endpoints from which |
|
// an announce is attempted. Some endpoints might succeed while others might fail. |
|
// Emit the signal only if all endpoints have failed. |
|
const QVector<TrackerEntry> trackerList = trackers(); |
|
const auto iter = std::find_if(trackerList.cbegin(), trackerList.cend(), [&trackerUrl](const TrackerEntry &entry) |
|
{ |
|
return (entry.url == trackerUrl); |
|
}); |
|
if ((iter != trackerList.cend()) && (iter->status == TrackerEntry::NotWorking)) |
|
m_session->handleTorrentTrackerError(this, trackerUrl); |
|
} |
|
|
|
void TorrentImpl::handleTorrentCheckedAlert(const lt::torrent_checked_alert *p) |
|
{ |
|
Q_UNUSED(p); |
|
qDebug("\"%s\" have just finished checking.", qUtf8Printable(name())); |
|
|
|
|
|
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 (m_nativeHandle.need_save_resume_data()) |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
|
|
if (m_fastresumeDataRejected && !m_hasMissingFiles) |
|
m_fastresumeDataRejected = false; |
|
|
|
updateStatus(); |
|
|
|
if (!m_hasMissingFiles) |
|
{ |
|
if ((progress() < 1.0) && (wantedSize() > 0)) |
|
m_hasSeedStatus = false; |
|
else if (progress() == 1.0) |
|
m_hasSeedStatus = true; |
|
|
|
adjustActualSavePath(); |
|
manageIncompleteFiles(); |
|
} |
|
|
|
m_session->handleTorrentChecked(this); |
|
} |
|
|
|
void TorrentImpl::handleTorrentFinishedAlert(const lt::torrent_finished_alert *p) |
|
{ |
|
Q_UNUSED(p); |
|
qDebug("Got a torrent finished alert for \"%s\"", qUtf8Printable(name())); |
|
qDebug("Torrent has seed status: %s", m_hasSeedStatus ? "yes" : "no"); |
|
m_hasMissingFiles = false; |
|
if (m_hasSeedStatus) return; |
|
|
|
updateStatus(); |
|
m_hasSeedStatus = true; |
|
|
|
adjustActualSavePath(); |
|
manageIncompleteFiles(); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
|
|
const bool recheckTorrentsOnCompletion = Preferences::instance()->recheckTorrentsOnCompletion(); |
|
if (isMoveInProgress() || (m_renameCount > 0)) |
|
{ |
|
if (recheckTorrentsOnCompletion) |
|
m_moveFinishedTriggers.append([this]() { forceRecheck(); }); |
|
m_moveFinishedTriggers.append([this]() { m_session->handleTorrentFinished(this); }); |
|
} |
|
else |
|
{ |
|
if (recheckTorrentsOnCompletion && m_unchecked) |
|
forceRecheck(); |
|
m_session->handleTorrentFinished(this); |
|
} |
|
} |
|
|
|
void TorrentImpl::handleTorrentPausedAlert(const lt::torrent_paused_alert *p) |
|
{ |
|
Q_UNUSED(p); |
|
} |
|
|
|
void TorrentImpl::handleTorrentResumedAlert(const lt::torrent_resumed_alert *p) |
|
{ |
|
Q_UNUSED(p); |
|
} |
|
|
|
void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p) |
|
{ |
|
if (m_maintenanceJob == MaintenanceJob::HandleMetadata) |
|
{ |
|
m_ltAddTorrentParams = p->params; |
|
|
|
m_ltAddTorrentParams.have_pieces.clear(); |
|
m_ltAddTorrentParams.verified_pieces.clear(); |
|
|
|
TorrentInfo metadata = TorrentInfo {m_nativeHandle.torrent_file()}; |
|
metadata.setContentLayout(m_contentLayout); |
|
|
|
m_session->findIncompleteFiles(metadata, m_savePath); |
|
} |
|
else |
|
{ |
|
prepareResumeData(p->params); |
|
} |
|
} |
|
|
|
void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms) |
|
{ |
|
if (m_hasMissingFiles) |
|
{ |
|
const auto havePieces = m_ltAddTorrentParams.have_pieces; |
|
const auto unfinishedPieces = m_ltAddTorrentParams.unfinished_pieces; |
|
const auto verifiedPieces = m_ltAddTorrentParams.verified_pieces; |
|
|
|
// Update recent resume data but preserve existing progress |
|
m_ltAddTorrentParams = params; |
|
m_ltAddTorrentParams.have_pieces = havePieces; |
|
m_ltAddTorrentParams.unfinished_pieces = unfinishedPieces; |
|
m_ltAddTorrentParams.verified_pieces = verifiedPieces; |
|
} |
|
else |
|
{ |
|
// Update recent resume data |
|
m_ltAddTorrentParams = params; |
|
} |
|
|
|
m_ltAddTorrentParams.added_time = addedTime().toSecsSinceEpoch(); |
|
|
|
// 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; |
|
resumeData.name = m_name; |
|
resumeData.category = m_category; |
|
resumeData.savePath = m_useAutoTMM ? "" : m_savePath; |
|
resumeData.tags = m_tags; |
|
resumeData.contentLayout = m_contentLayout; |
|
resumeData.ratioLimit = m_ratioLimit; |
|
resumeData.seedingTimeLimit = m_seedingTimeLimit; |
|
resumeData.firstLastPiecePriority = m_hasFirstLastPiecePriority; |
|
resumeData.hasSeedStatus = m_hasSeedStatus; |
|
resumeData.stopped = m_isStopped; |
|
resumeData.operatingMode = m_operatingMode; |
|
resumeData.ltAddTorrentParams = m_ltAddTorrentParams; |
|
|
|
m_session->handleTorrentResumeDataReady(this, resumeData); |
|
} |
|
|
|
void TorrentImpl::handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *p) |
|
{ |
|
Q_UNUSED(p); |
|
Q_ASSERT_X(false, Q_FUNC_INFO, "This point should be unreachable since libtorrent 1.2.11"); |
|
} |
|
|
|
void TorrentImpl::handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *p) |
|
{ |
|
m_fastresumeDataRejected = true; |
|
|
|
if (p->error.value() == lt::errors::mismatching_file_size) |
|
{ |
|
// Mismatching file size (files were probably moved) |
|
m_hasMissingFiles = true; |
|
LogMsg(tr("File sizes mismatch for torrent '%1'. Cannot proceed further.").arg(name()), Log::CRITICAL); |
|
} |
|
else |
|
{ |
|
LogMsg(tr("Fast resume data was rejected for torrent '%1'. Reason: %2. Checking again...") |
|
.arg(name(), QString::fromStdString(p->message())), Log::WARNING); |
|
} |
|
} |
|
|
|
void TorrentImpl::handleFileRenamedAlert(const lt::file_renamed_alert *p) |
|
{ |
|
const int fileIndex = m_torrentInfo.nativeIndexes().indexOf(p->index); |
|
Q_ASSERT(fileIndex >= 0); |
|
|
|
// 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 |
|
#ifndef QBT_USES_LIBTORRENT2 |
|
const QString oldFilePath = m_oldPath[fileIndex].takeFirst(); |
|
if (m_oldPath[fileIndex].isEmpty()) |
|
m_oldPath.remove(fileIndex); |
|
#else |
|
const QString oldFilePath = Utils::Fs::toUniformPath(p->old_name()); |
|
#endif |
|
const QString newFilePath = Utils::Fs::toUniformPath(p->new_name()); |
|
|
|
QList<QStringView> oldPathParts = QStringView(oldFilePath).split('/', Qt::SkipEmptyParts); |
|
oldPathParts.removeLast(); // drop file name part |
|
QList<QStringView> newPathParts = QStringView(newFilePath).split('/', Qt::SkipEmptyParts); |
|
newPathParts.removeLast(); // drop file name part |
|
|
|
#if defined(Q_OS_WIN) |
|
const Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive; |
|
#else |
|
const Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive; |
|
#endif |
|
|
|
int pathIdx = 0; |
|
while ((pathIdx < oldPathParts.size()) && (pathIdx < newPathParts.size())) |
|
{ |
|
if (oldPathParts[pathIdx].compare(newPathParts[pathIdx], caseSensitivity) != 0) |
|
break; |
|
++pathIdx; |
|
} |
|
|
|
for (int i = (oldPathParts.size() - 1); i >= pathIdx; --i) |
|
{ |
|
QDir().rmdir(savePath() + Utils::String::join(oldPathParts, QString::fromLatin1("/"))); |
|
oldPathParts.removeLast(); |
|
} |
|
|
|
--m_renameCount; |
|
while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) |
|
m_moveFinishedTriggers.takeFirst()(); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
void TorrentImpl::handleFileRenameFailedAlert(const lt::file_rename_failed_alert *p) |
|
{ |
|
const int fileIndex = m_torrentInfo.nativeIndexes().indexOf(p->index); |
|
Q_ASSERT(fileIndex >= 0); |
|
|
|
LogMsg(tr("File rename failed. Torrent: \"%1\", file: \"%2\", reason: \"%3\"") |
|
.arg(name(), filePath(fileIndex), QString::fromLocal8Bit(p->error.message().c_str())), Log::WARNING); |
|
|
|
#ifndef QBT_USES_LIBTORRENT2 |
|
m_oldPath[fileIndex].removeFirst(); |
|
if (m_oldPath[fileIndex].isEmpty()) |
|
m_oldPath.remove(fileIndex); |
|
#endif |
|
|
|
--m_renameCount; |
|
while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) |
|
m_moveFinishedTriggers.takeFirst()(); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
void TorrentImpl::handleFileCompletedAlert(const lt::file_completed_alert *p) |
|
{ |
|
const int fileIndex = m_torrentInfo.nativeIndexes().indexOf(p->index); |
|
Q_ASSERT(fileIndex >= 0); |
|
|
|
qDebug("A file completed download in torrent \"%s\"", qUtf8Printable(name())); |
|
if (m_session->isAppendExtensionEnabled()) |
|
{ |
|
QString name = filePath(fileIndex); |
|
if (name.endsWith(QB_EXT)) |
|
{ |
|
const QString oldName = name; |
|
name.chop(QB_EXT.size()); |
|
qDebug("Renaming %s to %s", qUtf8Printable(oldName), qUtf8Printable(name)); |
|
renameFile(fileIndex, name); |
|
} |
|
} |
|
} |
|
|
|
void TorrentImpl::handleFileErrorAlert(const lt::file_error_alert *p) |
|
{ |
|
m_lastFileError = {p->error, p->op}; |
|
} |
|
|
|
#ifdef QBT_USES_LIBTORRENT2 |
|
void TorrentImpl::handleFilePrioAlert(const lt::file_prio_alert *) |
|
{ |
|
if (m_nativeHandle.need_save_resume_data()) |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
#endif |
|
|
|
void TorrentImpl::handleMetadataReceivedAlert(const lt::metadata_received_alert *p) |
|
{ |
|
Q_UNUSED(p); |
|
qDebug("Metadata received for torrent %s.", qUtf8Printable(name())); |
|
|
|
m_maintenanceJob = MaintenanceJob::HandleMetadata; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
void TorrentImpl::handlePerformanceAlert(const lt::performance_alert *p) const |
|
{ |
|
LogMsg((tr("Performance alert: ") + QString::fromStdString(p->message())) |
|
, Log::INFO); |
|
} |
|
|
|
void TorrentImpl::handleTempPathChanged() |
|
{ |
|
adjustActualSavePath(); |
|
} |
|
|
|
void TorrentImpl::handleCategorySavePathChanged() |
|
{ |
|
if (m_useAutoTMM) |
|
move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite); |
|
} |
|
|
|
void TorrentImpl::handleAppendExtensionToggled() |
|
{ |
|
if (!hasMetadata()) return; |
|
|
|
manageIncompleteFiles(); |
|
} |
|
|
|
void TorrentImpl::handleAlert(const lt::alert *a) |
|
{ |
|
switch (a->type()) |
|
{ |
|
#ifdef QBT_USES_LIBTORRENT2 |
|
case lt::file_prio_alert::alert_type: |
|
handleFilePrioAlert(static_cast<const lt::file_prio_alert*>(a)); |
|
break; |
|
#endif |
|
case lt::file_renamed_alert::alert_type: |
|
handleFileRenamedAlert(static_cast<const lt::file_renamed_alert*>(a)); |
|
break; |
|
case lt::file_rename_failed_alert::alert_type: |
|
handleFileRenameFailedAlert(static_cast<const lt::file_rename_failed_alert*>(a)); |
|
break; |
|
case lt::file_completed_alert::alert_type: |
|
handleFileCompletedAlert(static_cast<const lt::file_completed_alert*>(a)); |
|
break; |
|
case lt::file_error_alert::alert_type: |
|
handleFileErrorAlert(static_cast<const lt::file_error_alert*>(a)); |
|
break; |
|
case lt::torrent_finished_alert::alert_type: |
|
handleTorrentFinishedAlert(static_cast<const lt::torrent_finished_alert*>(a)); |
|
break; |
|
case lt::save_resume_data_alert::alert_type: |
|
handleSaveResumeDataAlert(static_cast<const lt::save_resume_data_alert*>(a)); |
|
break; |
|
case lt::save_resume_data_failed_alert::alert_type: |
|
handleSaveResumeDataFailedAlert(static_cast<const lt::save_resume_data_failed_alert*>(a)); |
|
break; |
|
case lt::torrent_paused_alert::alert_type: |
|
handleTorrentPausedAlert(static_cast<const lt::torrent_paused_alert*>(a)); |
|
break; |
|
case lt::torrent_resumed_alert::alert_type: |
|
handleTorrentResumedAlert(static_cast<const lt::torrent_resumed_alert*>(a)); |
|
break; |
|
case lt::tracker_error_alert::alert_type: |
|
handleTrackerErrorAlert(static_cast<const lt::tracker_error_alert*>(a)); |
|
break; |
|
case lt::tracker_reply_alert::alert_type: |
|
handleTrackerReplyAlert(static_cast<const lt::tracker_reply_alert*>(a)); |
|
break; |
|
case lt::tracker_warning_alert::alert_type: |
|
handleTrackerWarningAlert(static_cast<const lt::tracker_warning_alert*>(a)); |
|
break; |
|
case lt::metadata_received_alert::alert_type: |
|
handleMetadataReceivedAlert(static_cast<const lt::metadata_received_alert*>(a)); |
|
break; |
|
case lt::fastresume_rejected_alert::alert_type: |
|
handleFastResumeRejectedAlert(static_cast<const lt::fastresume_rejected_alert*>(a)); |
|
break; |
|
case lt::torrent_checked_alert::alert_type: |
|
handleTorrentCheckedAlert(static_cast<const lt::torrent_checked_alert*>(a)); |
|
break; |
|
case lt::performance_alert::alert_type: |
|
handlePerformanceAlert(static_cast<const lt::performance_alert*>(a)); |
|
break; |
|
} |
|
} |
|
|
|
void TorrentImpl::manageIncompleteFiles() |
|
{ |
|
const bool isAppendExtensionEnabled = m_session->isAppendExtensionEnabled(); |
|
const QVector<qreal> fp = filesProgress(); |
|
if (fp.size() != filesCount()) |
|
{ |
|
qDebug() << "skip manageIncompleteFiles because of invalid torrent meta-data or empty file-progress"; |
|
return; |
|
} |
|
|
|
for (int i = 0; i < filesCount(); ++i) |
|
{ |
|
QString name = filePath(i); |
|
if (isAppendExtensionEnabled && (fileSize(i) > 0) && (fp[i] < 1)) |
|
{ |
|
if (!name.endsWith(QB_EXT, Qt::CaseInsensitive)) |
|
{ |
|
const QString newName = name + QB_EXT; |
|
qDebug() << "Renaming" << name << "to" << newName; |
|
renameFile(i, newName); |
|
} |
|
} |
|
else |
|
{ |
|
if (name.endsWith(QB_EXT, Qt::CaseInsensitive)) |
|
{ |
|
const QString oldName = name; |
|
name.chop(QB_EXT.size()); |
|
qDebug() << "Renaming" << oldName << "to" << name; |
|
renameFile(i, name); |
|
} |
|
} |
|
} |
|
} |
|
|
|
void TorrentImpl::adjustActualSavePath() |
|
{ |
|
if (!isMoveInProgress()) |
|
adjustActualSavePath_impl(); |
|
else |
|
m_moveFinishedTriggers.append([this]() { adjustActualSavePath_impl(); }); |
|
} |
|
|
|
void TorrentImpl::adjustActualSavePath_impl() |
|
{ |
|
const bool needUseTempDir = useTempPath(); |
|
const QDir tempDir {m_session->torrentTempPath(info())}; |
|
const QDir currentDir {actualStorageLocation()}; |
|
const QDir targetDir {needUseTempDir ? tempDir : QDir {savePath()}}; |
|
|
|
if (targetDir == currentDir) return; |
|
|
|
if (!needUseTempDir) |
|
{ |
|
if ((currentDir == tempDir) && (currentDir != QDir {m_session->tempPath()})) |
|
{ |
|
// torrent without root folder still has it in its temporary save path |
|
// so its temp path isn't equal to temp path root |
|
const QString currentDirPath = currentDir.absolutePath(); |
|
m_moveFinishedTriggers.append([currentDirPath] |
|
{ |
|
qDebug() << "Removing torrent temp folder:" << currentDirPath; |
|
Utils::Fs::smartRemoveEmptyFolderTree(currentDirPath); |
|
}); |
|
} |
|
} |
|
|
|
moveStorage(Utils::Fs::toNativePath(targetDir.absolutePath()), MoveStorageMode::Overwrite); |
|
} |
|
|
|
lt::torrent_handle TorrentImpl::nativeHandle() const |
|
{ |
|
return m_nativeHandle; |
|
} |
|
|
|
bool TorrentImpl::isMoveInProgress() const |
|
{ |
|
return m_storageIsMoving; |
|
} |
|
|
|
bool TorrentImpl::useTempPath() const |
|
{ |
|
return m_session->isTempPathEnabled() && !(isSeed() || m_hasSeedStatus); |
|
} |
|
|
|
void TorrentImpl::updateStatus() |
|
{ |
|
updateStatus(m_nativeHandle.status()); |
|
} |
|
|
|
void TorrentImpl::updateStatus(const lt::torrent_status &nativeStatus) |
|
{ |
|
m_nativeStatus = nativeStatus; |
|
updateState(); |
|
|
|
m_speedMonitor.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; |
|
} |
|
} |
|
|
|
void TorrentImpl::setRatioLimit(qreal limit) |
|
{ |
|
if (limit < USE_GLOBAL_RATIO) |
|
limit = NO_RATIO_LIMIT; |
|
else if (limit > MAX_RATIO) |
|
limit = MAX_RATIO; |
|
|
|
if (m_ratioLimit != limit) |
|
{ |
|
m_ratioLimit = limit; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentShareLimitChanged(this); |
|
} |
|
} |
|
|
|
void TorrentImpl::setSeedingTimeLimit(int limit) |
|
{ |
|
if (limit < USE_GLOBAL_SEEDING_TIME) |
|
limit = NO_SEEDING_TIME_LIMIT; |
|
else if (limit > MAX_SEEDING_TIME) |
|
limit = MAX_SEEDING_TIME; |
|
|
|
if (m_seedingTimeLimit != limit) |
|
{ |
|
m_seedingTimeLimit = limit; |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
m_session->handleTorrentShareLimitChanged(this); |
|
} |
|
} |
|
|
|
void TorrentImpl::setUploadLimit(const int limit) |
|
{ |
|
if (limit == uploadLimit()) |
|
return; |
|
|
|
m_nativeHandle.set_upload_limit(limit); |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
void TorrentImpl::setDownloadLimit(const int limit) |
|
{ |
|
if (limit == downloadLimit()) |
|
return; |
|
|
|
m_nativeHandle.set_download_limit(limit); |
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
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); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
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); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
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); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
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); |
|
|
|
m_session->handleTorrentNeedSaveResumeData(this); |
|
} |
|
|
|
void TorrentImpl::flushCache() const |
|
{ |
|
m_nativeHandle.flush_cache(); |
|
} |
|
|
|
QString TorrentImpl::createMagnetURI() const |
|
{ |
|
return QString::fromStdString(lt::make_magnet_uri(m_nativeHandle)); |
|
} |
|
|
|
void TorrentImpl::prioritizeFiles(const QVector<DownloadPriority> &priorities) |
|
{ |
|
if (!hasMetadata()) return; |
|
|
|
Q_ASSERT(priorities.size() == filesCount()); |
|
|
|
// Reset 'm_hasSeedStatus' if needed in order to react again to |
|
// 'torrent_finished_alert' and eg show tray notifications |
|
const QVector<qreal> progress = filesProgress(); |
|
const QVector<DownloadPriority> oldPriorities = filePriorities(); |
|
for (int i = 0; i < oldPriorities.size(); ++i) |
|
{ |
|
if ((oldPriorities[i] == DownloadPriority::Ignored) |
|
&& (priorities[i] > DownloadPriority::Ignored) |
|
&& (progress[i] < 1.0)) |
|
{ |
|
m_hasSeedStatus = 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); |
|
|
|
// Restore first/last piece first option if necessary |
|
if (m_hasFirstLastPiecePriority) |
|
applyFirstLastPiecePriority(true, priorities); |
|
} |
|
|
|
QVector<qreal> TorrentImpl::availableFileFractions() const |
|
{ |
|
const int filesCount = this->filesCount(); |
|
if (filesCount <= 0) return {}; |
|
|
|
const QVector<int> piecesAvailability = pieceAvailability(); |
|
// libtorrent returns empty array for seeding only torrents |
|
if (piecesAvailability.empty()) return QVector<qreal>(filesCount, -1); |
|
|
|
QVector<qreal> res; |
|
res.reserve(filesCount); |
|
const TorrentInfo info = this->info(); |
|
for (int i = 0; i < filesCount; ++i) |
|
{ |
|
const TorrentInfo::PieceRange filePieces = info.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(); |
|
res.push_back(availability); |
|
} |
|
return res; |
|
}
|
|
|