/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2015 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * 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 "torrenthandle.h" #include #include #ifdef Q_OS_WIN #include #endif #include #include #include #include #include #include #include #include #if (LIBTORRENT_VERSION_NUM >= 10200) #include #endif #include #include #include #include #include #include #include #include "base/global.h" #include "base/logger.h" #include "base/preferences.h" #include "base/profile.h" #include "base/tristatebool.h" #include "base/utils/fs.h" #include "base/utils/string.h" #include "downloadpriority.h" #include "peerinfo.h" #include "private/ltunderlyingtype.h" #include "session.h" #include "trackerentry.h" const QString QB_EXT {QStringLiteral(".!qB")}; using namespace BitTorrent; namespace { #if (LIBTORRENT_VERSION_NUM < 10200) using LTDownloadPriority = int; using LTFileIndex = int; using LTPieceIndex = int; using LTQueuePosition = int; #else using LTDownloadPriority = lt::download_priority_t; using LTFileIndex = lt::file_index_t; using LTPieceIndex = lt::piece_index_t; using LTQueuePosition = lt::queue_position_t; #endif std::vector toLTDownloadPriorities(const QVector &priorities) { std::vector out; std::transform(priorities.cbegin(), priorities.cend() , std::back_inserter(out), [](BitTorrent::DownloadPriority priority) { return static_cast( static_cast>(priority)); }); return out; } using ListType = lt::entry::list_type; ListType setToEntryList(const QSet &input) { ListType entryList; for (const QString &setValue : input) entryList.emplace_back(setValue.toStdString()); return entryList; } } // AddTorrentData CreateTorrentParams::CreateTorrentParams() : restored(false) , disableTempPath(false) , sequential(false) , firstLastPiecePriority(false) , hasSeedStatus(false) , skipChecking(false) , hasRootFolder(true) , forced(false) , paused(false) , uploadLimit(-1) , downloadLimit(-1) , ratioLimit(TorrentHandle::USE_GLOBAL_RATIO) , seedingTimeLimit(TorrentHandle::USE_GLOBAL_SEEDING_TIME) { } CreateTorrentParams::CreateTorrentParams(const AddTorrentParams ¶ms) : restored(false) , name(params.name) , category(params.category) , tags(params.tags) , savePath(params.savePath) , disableTempPath(params.disableTempPath) , sequential(params.sequential) , firstLastPiecePriority(params.firstLastPiecePriority) , hasSeedStatus(params.skipChecking) // do not react on 'torrent_finished_alert' when skipping , skipChecking(params.skipChecking) , hasRootFolder(params.createSubfolder == TriStateBool::Undefined ? Session::instance()->isCreateTorrentSubfolder() : params.createSubfolder == TriStateBool::True) , forced(params.addForced == TriStateBool::True) , paused(params.addPaused == TriStateBool::Undefined ? Session::instance()->isAddTorrentPaused() : params.addPaused == TriStateBool::True) , uploadLimit(params.uploadLimit) , downloadLimit(params.downloadLimit) , filePriorities(params.filePriorities) , ratioLimit(params.ignoreShareLimits ? TorrentHandle::NO_RATIO_LIMIT : TorrentHandle::USE_GLOBAL_RATIO) , seedingTimeLimit(params.ignoreShareLimits ? TorrentHandle::NO_SEEDING_TIME_LIMIT : TorrentHandle::USE_GLOBAL_SEEDING_TIME) { bool useAutoTMM = (params.useAutoTMM == TriStateBool::Undefined ? !Session::instance()->isAutoTMMDisabledByDefault() : params.useAutoTMM == TriStateBool::True); if (useAutoTMM) savePath = ""; else if (savePath.trimmed().isEmpty()) savePath = Session::instance()->defaultSavePath(); } // TorrentHandle const qreal TorrentHandle::USE_GLOBAL_RATIO = -2.; const qreal TorrentHandle::NO_RATIO_LIMIT = -1.; const int TorrentHandle::USE_GLOBAL_SEEDING_TIME = -2; const int TorrentHandle::NO_SEEDING_TIME_LIMIT = -1; const qreal TorrentHandle::MAX_RATIO = 9999.; const int TorrentHandle::MAX_SEEDING_TIME = 525600; TorrentHandle::TorrentHandle(Session *session, const lt::torrent_handle &nativeHandle, const CreateTorrentParams ¶ms) : QObject(session) , m_session(session) , m_nativeHandle(nativeHandle) , m_state(TorrentState::Unknown) , m_renameCount(0) , m_useAutoTMM(params.savePath.isEmpty()) , m_name(params.name) , m_savePath(Utils::Fs::toNativePath(params.savePath)) , m_category(params.category) , m_tags(params.tags) , m_hasSeedStatus(params.hasSeedStatus) , m_ratioLimit(params.ratioLimit) , m_seedingTimeLimit(params.seedingTimeLimit) , m_tempPathDisabled(params.disableTempPath) , m_hasMissingFiles(false) , m_hasRootFolder(params.hasRootFolder) , m_needsToSetFirstLastPiecePriority(false) , m_needsToStartForced(params.forced) , m_pauseWhenReady(params.paused) { if (m_useAutoTMM) m_savePath = Utils::Fs::toNativePath(m_session->categorySavePath(m_category)); updateStatus(); m_hash = InfoHash(m_nativeStatus.info_hash); // NB: the following two if statements are present because we don't want // to set either sequential download or first/last piece priority to false // if their respective flags in data are false when a torrent is being // resumed. This is because, in that circumstance, this constructor is // called with those flags set to false, even if the torrent was set to // download sequentially or have first/last piece priority enabled when // its resume data was saved. These two settings are restored later. But // if we set them to false now, both will erroneously not be restored. if (!params.restored || params.sequential) setSequentialDownload(params.sequential); if (!params.restored || params.firstLastPiecePriority) setFirstLastPiecePriority(params.firstLastPiecePriority); if (!params.restored && hasMetadata()) { if (filesCount() == 1) m_hasRootFolder = false; } if (!hasMetadata()) { // There is nothing to prepare if (!m_pauseWhenReady) { // Resume torrent because it was added in "resumed" state // but it's actually paused during initialization. m_startupState = Starting; resume_impl(m_needsToStartForced); } else { m_startupState = Started; m_pauseWhenReady = false; } } } TorrentHandle::~TorrentHandle() {} bool TorrentHandle::isValid() const { return m_nativeHandle.is_valid(); } InfoHash TorrentHandle::hash() const { return m_hash; } QString TorrentHandle::name() const { QString name = m_name; if (name.isEmpty()) name = QString::fromStdString(m_nativeStatus.name); if (name.isEmpty() && hasMetadata()) name = QString::fromStdString(m_torrentInfo.nativeInfo()->orig_files().name()); if (name.isEmpty()) name = m_hash; return name; } QDateTime TorrentHandle::creationDate() const { return m_torrentInfo.creationDate(); } QString TorrentHandle::creator() const { return m_torrentInfo.creator(); } QString TorrentHandle::comment() const { return m_torrentInfo.comment(); } bool TorrentHandle::isPrivate() const { return m_torrentInfo.isPrivate(); } qlonglong TorrentHandle::totalSize() const { return m_torrentInfo.totalSize(); } // get the size of the torrent without the filtered files qlonglong TorrentHandle::wantedSize() const { return m_nativeStatus.total_wanted; } qlonglong TorrentHandle::completedSize() const { return m_nativeStatus.total_wanted_done; } qlonglong TorrentHandle::incompletedSize() const { return (m_nativeStatus.total_wanted - m_nativeStatus.total_wanted_done); } qlonglong TorrentHandle::pieceLength() const { return m_torrentInfo.pieceLength(); } qlonglong TorrentHandle::wastedSize() const { return (m_nativeStatus.total_failed_bytes + m_nativeStatus.total_redundant_bytes); } QString TorrentHandle::currentTracker() const { return QString::fromStdString(m_nativeStatus.current_tracker); } QString TorrentHandle::savePath(bool actual) const { if (actual) return Utils::Fs::toUniformPath(nativeActualSavePath()); else return Utils::Fs::toUniformPath(m_savePath); } QString TorrentHandle::rootPath(bool actual) const { if ((filesCount() > 1) && !hasRootFolder()) 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 TorrentHandle::contentPath(bool actual) const { if (filesCount() == 1) return QDir(savePath(actual)).absoluteFilePath(filePath(0)); else if (hasRootFolder()) return rootPath(actual); else return savePath(actual); } bool TorrentHandle::isAutoTMMEnabled() const { return m_useAutoTMM; } void TorrentHandle::setAutoTMMEnabled(bool enabled) { if (m_useAutoTMM == enabled) return; m_useAutoTMM = enabled; m_session->handleTorrentSavingModeChanged(this); if (m_useAutoTMM) move_impl(m_session->categorySavePath(m_category), true); } bool TorrentHandle::hasRootFolder() const { return m_hasRootFolder; } QString TorrentHandle::nativeActualSavePath() const { return QString::fromStdString(m_nativeStatus.save_path); } bool TorrentHandle::isAutoManaged() const { #if (LIBTORRENT_VERSION_NUM < 10200) return m_nativeStatus.auto_managed; #else return bool {m_nativeStatus.flags & lt::torrent_flags::auto_managed}; #endif } void TorrentHandle::setAutoManaged(const bool enable) { #if (LIBTORRENT_VERSION_NUM < 10200) m_nativeHandle.auto_managed(enable); #else if (enable) m_nativeHandle.set_flags(lt::torrent_flags::auto_managed); else m_nativeHandle.unset_flags(lt::torrent_flags::auto_managed); #endif } QVector TorrentHandle::trackers() const { const std::vector announces = m_nativeHandle.trackers(); QVector entries; entries.reserve(announces.size()); for (const lt::announce_entry &tracker : announces) entries << tracker; return entries; } QHash TorrentHandle::trackerInfos() const { return m_trackerInfos; } void TorrentHandle::addTrackers(const QVector &trackers) { const QVector currentTrackers = this->trackers(); QVector newTrackers; newTrackers.reserve(trackers.size()); for (const TrackerEntry &tracker : trackers) { if (!currentTrackers.contains(tracker)) { m_nativeHandle.add_tracker(tracker.nativeEntry()); newTrackers << tracker; } } if (!newTrackers.isEmpty()) m_session->handleTorrentTrackersAdded(this, newTrackers); } void TorrentHandle::replaceTrackers(const QVector &trackers) { QVector currentTrackers = this->trackers(); QVector newTrackers; newTrackers.reserve(trackers.size()); std::vector announces; for (const TrackerEntry &tracker : trackers) { announces.emplace_back(tracker.nativeEntry()); if (!currentTrackers.removeOne(tracker)) newTrackers << tracker; } m_nativeHandle.replace_trackers(announces); 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); } } QList TorrentHandle::urlSeeds() const { QList urlSeeds; const std::set seeds = m_nativeHandle.url_seeds(); for (const std::string &urlSeed : seeds) urlSeeds.append(QUrl(urlSeed.c_str())); return urlSeeds; } void TorrentHandle::addUrlSeeds(const QList &urlSeeds) { QList addedUrlSeeds; for (const QUrl &urlSeed : urlSeeds) { if (addUrlSeed(urlSeed)) addedUrlSeeds << urlSeed; } if (!addedUrlSeeds.isEmpty()) m_session->handleTorrentUrlSeedsAdded(this, addedUrlSeeds); } void TorrentHandle::removeUrlSeeds(const QList &urlSeeds) { QList removedUrlSeeds; for (const QUrl &urlSeed : urlSeeds) { if (removeUrlSeed(urlSeed)) removedUrlSeeds << urlSeed; } if (!removedUrlSeeds.isEmpty()) m_session->handleTorrentUrlSeedsRemoved(this, removedUrlSeeds); } bool TorrentHandle::addUrlSeed(const QUrl &urlSeed) { QList seeds = urlSeeds(); if (seeds.contains(urlSeed)) return false; m_nativeHandle.add_url_seed(urlSeed.toString().toStdString()); return true; } bool TorrentHandle::removeUrlSeed(const QUrl &urlSeed) { QList seeds = urlSeeds(); if (!seeds.contains(urlSeed)) return false; m_nativeHandle.remove_url_seed(urlSeed.toString().toStdString()); return true; } bool TorrentHandle::connectPeer(const PeerAddress &peerAddress) { lt::error_code ec; const lt::address addr = lt::address::from_string(peerAddress.ip.toString().toStdString(), ec); if (ec) return false; const boost::asio::ip::tcp::endpoint ep(addr, peerAddress.port); m_nativeHandle.connect_peer(ep); return true; } bool TorrentHandle::needSaveResumeData() const { return m_nativeHandle.need_save_resume_data(); } void TorrentHandle::saveResumeData() { m_nativeHandle.save_resume_data(); } int TorrentHandle::filesCount() const { return m_torrentInfo.filesCount(); } int TorrentHandle::piecesCount() const { return m_torrentInfo.piecesCount(); } int TorrentHandle::piecesHave() const { return m_nativeStatus.num_pieces; } qreal TorrentHandle::progress() const { if (!isChecking()) { if (!m_nativeStatus.total_wanted) return 0.; if (m_nativeStatus.total_wanted_done == m_nativeStatus.total_wanted) return 1.; const qreal progress = static_cast(m_nativeStatus.total_wanted_done) / m_nativeStatus.total_wanted; Q_ASSERT((progress >= 0.f) && (progress <= 1.f)); return progress; } return m_nativeStatus.progress; } QString TorrentHandle::category() const { return m_category; } bool TorrentHandle::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; } QSet TorrentHandle::tags() const { return m_tags; } bool TorrentHandle::hasTag(const QString &tag) const { return m_tags.contains(tag); } bool TorrentHandle::addTag(const QString &tag) { if (!Session::isValidTag(tag)) return false; if (!hasTag(tag)) { if (!m_session->hasTag(tag)) if (!m_session->addTag(tag)) return false; m_tags.insert(tag); m_session->handleTorrentTagAdded(this, tag); return true; } return false; } bool TorrentHandle::removeTag(const QString &tag) { if (m_tags.remove(tag)) { m_session->handleTorrentTagRemoved(this, tag); return true; } return false; } void TorrentHandle::removeAllTags() { for (const QString &tag : asConst(tags())) removeTag(tag); } QDateTime TorrentHandle::addedTime() const { return QDateTime::fromSecsSinceEpoch(m_nativeStatus.added_time); } qreal TorrentHandle::ratioLimit() const { return m_ratioLimit; } int TorrentHandle::seedingTimeLimit() const { return m_seedingTimeLimit; } QString TorrentHandle::filePath(int index) const { return m_torrentInfo.filePath(index); } QString TorrentHandle::fileName(int index) const { if (!hasMetadata()) return {}; return Utils::Fs::fileName(filePath(index)); } qlonglong TorrentHandle::fileSize(int index) const { return m_torrentInfo.fileSize(index); } // Return a list of absolute paths corresponding // to all files in a torrent QStringList TorrentHandle::absoluteFilePaths() const { if (!hasMetadata()) return {}; const QDir saveDir(savePath(true)); QStringList res; for (int i = 0; i < filesCount(); ++i) res << Utils::Fs::expandPathAbs(saveDir.absoluteFilePath(filePath(i))); return res; } QStringList TorrentHandle::absoluteFilePathsUnwanted() const { if (!hasMetadata()) return {}; const QDir saveDir(savePath(true)); #if (LIBTORRENT_VERSION_NUM < 10200) const std::vector fp = m_nativeHandle.file_priorities(); #else const std::vector fp = m_nativeHandle.get_file_priorities(); #endif QStringList res; for (int i = 0; i < static_cast(fp.size()); ++i) { if (fp[i] == LTDownloadPriority {0}) { const QString path = Utils::Fs::expandPathAbs(saveDir.absoluteFilePath(filePath(i))); if (path.contains(".unwanted")) res << path; } } return res; } QVector TorrentHandle::filePriorities() const { #if (LIBTORRENT_VERSION_NUM < 10200) const std::vector fp = m_nativeHandle.file_priorities(); #else const std::vector fp = m_nativeHandle.get_file_priorities(); #endif QVector ret; std::transform(fp.cbegin(), fp.cend(), std::back_inserter(ret), [](LTDownloadPriority priority) { return static_cast(LTUnderlyingType {priority}); }); return ret; } TorrentInfo TorrentHandle::info() const { return m_torrentInfo; } bool TorrentHandle::isPaused() const { #if (LIBTORRENT_VERSION_NUM < 10200) return (m_nativeStatus.paused && !isAutoManaged()); #else return ((m_nativeStatus.flags & lt::torrent_flags::paused) && !isAutoManaged()); #endif } bool TorrentHandle::isResumed() const { return !isPaused(); } bool TorrentHandle::isQueued() const { #if (LIBTORRENT_VERSION_NUM < 10200) return (m_nativeStatus.paused && isAutoManaged()); #else return ((m_nativeStatus.flags & lt::torrent_flags::paused) && isAutoManaged()); #endif } bool TorrentHandle::isChecking() const { return ((m_nativeStatus.state == lt::torrent_status::checking_files) || (m_nativeStatus.state == lt::torrent_status::checking_resume_data)); } bool TorrentHandle::isDownloading() const { return m_state == TorrentState::Downloading || m_state == TorrentState::DownloadingMetadata || m_state == TorrentState::StalledDownloading || m_state == TorrentState::CheckingDownloading || m_state == TorrentState::PausedDownloading || m_state == TorrentState::QueuedDownloading || m_state == TorrentState::ForcedDownloading; } bool TorrentHandle::isUploading() const { return m_state == TorrentState::Uploading || m_state == TorrentState::StalledUploading || m_state == TorrentState::CheckingUploading || m_state == TorrentState::QueuedUploading || m_state == TorrentState::ForcedUploading; } bool TorrentHandle::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 TorrentHandle::isActive() const { if (m_state == TorrentState::StalledDownloading) return (uploadPayloadRate() > 0); return m_state == TorrentState::DownloadingMetadata || m_state == TorrentState::Downloading || m_state == TorrentState::ForcedDownloading || m_state == TorrentState::Uploading || m_state == TorrentState::ForcedUploading || m_state == TorrentState::Moving; } bool TorrentHandle::isInactive() const { return !isActive(); } bool TorrentHandle::isErrored() const { return m_state == TorrentState::MissingFiles || m_state == TorrentState::Error; } bool TorrentHandle::isSeed() const { // Affected by bug http://code.rasterbar.com/libtorrent/ticket/402 //bool result; //result = m_nativeHandle.is_seed()); //return result; // May suffer from approximation problems //return (progress() == 1.); // This looks safe return ((m_nativeStatus.state == lt::torrent_status::finished) || (m_nativeStatus.state == lt::torrent_status::seeding)); } bool TorrentHandle::isForced() const { #if (LIBTORRENT_VERSION_NUM < 10200) return (!m_nativeStatus.paused && !isAutoManaged()); #else return (!(m_nativeStatus.flags & lt::torrent_flags::paused) && !isAutoManaged()); #endif } bool TorrentHandle::isSequentialDownload() const { #if (LIBTORRENT_VERSION_NUM < 10200) return m_nativeStatus.sequential_download; #else return bool {m_nativeStatus.flags & lt::torrent_flags::sequential_download}; #endif } bool TorrentHandle::hasFirstLastPiecePriority() const { if (!hasMetadata()) return m_needsToSetFirstLastPiecePriority; #if (LIBTORRENT_VERSION_NUM < 10200) const std::vector filePriorities = nativeHandle().file_priorities(); #else const std::vector filePriorities = nativeHandle().get_file_priorities(); #endif for (int i = 0; i < static_cast(filePriorities.size()); ++i) { if (filePriorities[i] <= LTDownloadPriority {0}) continue; const TorrentInfo::PieceRange extremities = info().filePieces(i); const LTDownloadPriority firstPiecePrio = nativeHandle().piece_priority(LTPieceIndex {extremities.first()}); const LTDownloadPriority lastPiecePrio = nativeHandle().piece_priority(LTPieceIndex {extremities.last()}); return ((firstPiecePrio == LTDownloadPriority {7}) && (lastPiecePrio == LTDownloadPriority {7})); } return false; } TorrentState TorrentHandle::state() const { return m_state; } void TorrentHandle::updateState() { if (m_nativeStatus.state == lt::torrent_status::checking_resume_data) { m_state = TorrentState::CheckingResumeData; } else if (isMoveInProgress()) { m_state = TorrentState::Moving; } else if (isPaused()) { if (hasMissingFiles()) m_state = TorrentState::MissingFiles; else if (hasError()) m_state = TorrentState::Error; else m_state = isSeed() ? TorrentState::PausedUploading : TorrentState::PausedDownloading; } else { if (m_session->isQueueingSystemEnabled() && isQueued() && !isChecking()) { m_state = isSeed() ? TorrentState::QueuedUploading : TorrentState::QueuedDownloading; } else { switch (m_nativeStatus.state) { case lt::torrent_status::finished: case lt::torrent_status::seeding: if (isForced()) m_state = TorrentState::ForcedUploading; else m_state = m_nativeStatus.upload_payload_rate > 0 ? TorrentState::Uploading : TorrentState::StalledUploading; break; case lt::torrent_status::allocating: m_state = TorrentState::Allocating; break; case lt::torrent_status::checking_files: m_state = m_hasSeedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading; break; case lt::torrent_status::downloading_metadata: m_state = TorrentState::DownloadingMetadata; break; case lt::torrent_status::downloading: if (isForced()) m_state = TorrentState::ForcedDownloading; else m_state = m_nativeStatus.download_payload_rate > 0 ? TorrentState::Downloading : TorrentState::StalledDownloading; break; default: qWarning("Unrecognized torrent status, should not happen!!! status was %d", m_nativeStatus.state); m_state = TorrentState::Unknown; } } } } bool TorrentHandle::hasMetadata() const { return m_nativeStatus.has_metadata; } bool TorrentHandle::hasMissingFiles() const { return m_hasMissingFiles; } bool TorrentHandle::hasError() const { #if (LIBTORRENT_VERSION_NUM < 10200) return (m_nativeStatus.paused && m_nativeStatus.errc); #else return ((m_nativeStatus.flags & lt::torrent_flags::paused) && m_nativeStatus.errc); #endif } bool TorrentHandle::hasFilteredPieces() const { #if (LIBTORRENT_VERSION_NUM < 10200) const std::vector pp = m_nativeHandle.piece_priorities(); #else const std::vector pp = m_nativeHandle.get_piece_priorities(); #endif return std::any_of(pp.cbegin(), pp.cend(), [](const LTDownloadPriority priority) { return (priority == LTDownloadPriority {0}); }); } int TorrentHandle::queuePosition() const { if (m_nativeStatus.queue_position < LTQueuePosition {0}) return 0; return static_cast(m_nativeStatus.queue_position) + 1; } QString TorrentHandle::error() const { return QString::fromStdString(m_nativeStatus.errc.message()); } qlonglong TorrentHandle::totalDownload() const { return m_nativeStatus.all_time_download; } qlonglong TorrentHandle::totalUpload() const { return m_nativeStatus.all_time_upload; } qlonglong TorrentHandle::activeTime() const { #if (LIBTORRENT_VERSION_NUM < 10200) return m_nativeStatus.active_time; #else return lt::total_seconds(m_nativeStatus.active_duration); #endif } qlonglong TorrentHandle::finishedTime() const { #if (LIBTORRENT_VERSION_NUM < 10200) return m_nativeStatus.finished_time; #else return lt::total_seconds(m_nativeStatus.finished_duration); #endif } qlonglong TorrentHandle::seedingTime() const { #if (LIBTORRENT_VERSION_NUM < 10200) return m_nativeStatus.seeding_time; #else return lt::total_seconds(m_nativeStatus.seeding_duration); #endif } qulonglong TorrentHandle::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 TorrentHandle::filesProgress() const { std::vector fp; m_nativeHandle.file_progress(fp, lt::torrent_handle::piece_granularity); const int count = static_cast(fp.size()); QVector result; result.reserve(count); for (int i = 0; i < count; ++i) { const qlonglong size = fileSize(i); if ((size <= 0) || (fp[i] == size)) result << 1; else result << (fp[i] / static_cast(size)); } return result; } int TorrentHandle::seedsCount() const { return m_nativeStatus.num_seeds; } int TorrentHandle::peersCount() const { return m_nativeStatus.num_peers; } int TorrentHandle::leechsCount() const { return (m_nativeStatus.num_peers - m_nativeStatus.num_seeds); } int TorrentHandle::totalSeedsCount() const { return (m_nativeStatus.num_complete > 0) ? m_nativeStatus.num_complete : m_nativeStatus.list_seeds; } int TorrentHandle::totalPeersCount() const { const int peers = m_nativeStatus.num_complete + m_nativeStatus.num_incomplete; return (peers > 0) ? peers : m_nativeStatus.list_peers; } int TorrentHandle::totalLeechersCount() const { return (m_nativeStatus.num_incomplete > 0) ? m_nativeStatus.num_incomplete : (m_nativeStatus.list_peers - m_nativeStatus.list_seeds); } int TorrentHandle::completeCount() const { // additional info: https://github.com/qbittorrent/qBittorrent/pull/5300#issuecomment-267783646 return m_nativeStatus.num_complete; } int TorrentHandle::incompleteCount() const { // additional info: https://github.com/qbittorrent/qBittorrent/pull/5300#issuecomment-267783646 return m_nativeStatus.num_incomplete; } QDateTime TorrentHandle::lastSeenComplete() const { if (m_nativeStatus.last_seen_complete > 0) return QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete); else return {}; } QDateTime TorrentHandle::completedTime() const { if (m_nativeStatus.completed_time > 0) return QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time); else return {}; } qlonglong TorrentHandle::timeSinceUpload() const { #if (LIBTORRENT_VERSION_NUM < 10200) return m_nativeStatus.time_since_upload; #else return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_upload); #endif } qlonglong TorrentHandle::timeSinceDownload() const { #if (LIBTORRENT_VERSION_NUM < 10200) return m_nativeStatus.time_since_download; #else return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_download); #endif } qlonglong TorrentHandle::timeSinceActivity() const { const qlonglong upTime = timeSinceUpload(); const qlonglong downTime = timeSinceDownload(); return ((upTime < 0) != (downTime < 0)) ? std::max(upTime, downTime) : std::min(upTime, downTime); } int TorrentHandle::downloadLimit() const { return m_nativeHandle.download_limit(); } int TorrentHandle::uploadLimit() const { return m_nativeHandle.upload_limit(); } bool TorrentHandle::superSeeding() const { #if (LIBTORRENT_VERSION_NUM < 10200) return m_nativeStatus.super_seeding; #else return bool {m_nativeStatus.flags & lt::torrent_flags::super_seeding}; #endif } QList TorrentHandle::peers() const { QList peers; std::vector nativePeers; m_nativeHandle.get_peer_info(nativePeers); for (const lt::peer_info &peer : nativePeers) peers << PeerInfo(this, peer); return peers; } QBitArray TorrentHandle::pieces() const { QBitArray result(m_nativeStatus.pieces.size()); for (int i = 0; i < m_nativeStatus.pieces.size(); ++i) result.setBit(i, m_nativeStatus.pieces.get_bit(LTPieceIndex {i})); return result; } QBitArray TorrentHandle::downloadingPieces() const { QBitArray result(piecesCount()); std::vector queue; m_nativeHandle.get_download_queue(queue); for (const lt::partial_piece_info &info : queue) #if (LIBTORRENT_VERSION_NUM < 10200) result.setBit(info.piece_index); #else result.setBit(LTUnderlyingType {info.piece_index}); #endif return result; } QVector TorrentHandle::pieceAvailability() const { std::vector avail; m_nativeHandle.piece_availability(avail); return QVector::fromStdVector(avail); } qreal TorrentHandle::distributedCopies() const { return m_nativeStatus.distributed_copies; } qreal TorrentHandle::maxRatio() const { if (m_ratioLimit == USE_GLOBAL_RATIO) return m_session->globalMaxRatio(); return m_ratioLimit; } int TorrentHandle::maxSeedingTime() const { if (m_seedingTimeLimit == USE_GLOBAL_SEEDING_TIME) return m_session->globalMaxSeedingMinutes(); return m_seedingTimeLimit; } qreal TorrentHandle::realRatio() const { const boost::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 boost::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.0 : MAX_RATIO; const qreal ratio = upload / static_cast(download); Q_ASSERT(ratio >= 0.0); return (ratio > MAX_RATIO) ? MAX_RATIO : ratio; } int TorrentHandle::uploadPayloadRate() const { return m_nativeStatus.upload_payload_rate; } int TorrentHandle::downloadPayloadRate() const { return m_nativeStatus.download_payload_rate; } qlonglong TorrentHandle::totalPayloadUpload() const { return m_nativeStatus.total_payload_upload; } qlonglong TorrentHandle::totalPayloadDownload() const { return m_nativeStatus.total_payload_download; } int TorrentHandle::connectionsCount() const { return m_nativeStatus.num_connections; } int TorrentHandle::connectionsLimit() const { return m_nativeStatus.connections_limit; } qlonglong TorrentHandle::nextAnnounce() const { return lt::total_seconds(m_nativeStatus.next_announce); } void TorrentHandle::setName(const QString &name) { if (m_name != name) { m_name = name; m_session->handleTorrentNameChanged(this); } } bool TorrentHandle::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->handleTorrentCategoryChanged(this, oldCategory); if (m_useAutoTMM) { if (!m_session->isDisableAutoTMMWhenCategoryChanged()) move_impl(m_session->categorySavePath(m_category), true); else setAutoTMMEnabled(false); } } return true; } void TorrentHandle::move(QString path) { if (m_startupState != Started) return; m_useAutoTMM = false; m_session->handleTorrentSavingModeChanged(this); path = Utils::Fs::toUniformPath(path.trimmed()); if (path.isEmpty()) path = m_session->defaultSavePath(); if (!path.endsWith('/')) path += '/'; move_impl(path, false); } void TorrentHandle::move_impl(QString path, bool overwrite) { if (path == savePath()) return; path = Utils::Fs::toNativePath(path); if (!useTempPath()) { moveStorage(path, overwrite); } else { m_savePath = path; m_session->handleTorrentSavePathChanged(this); } } void TorrentHandle::forceReannounce(int index) { m_nativeHandle.force_reannounce(0, index); } void TorrentHandle::forceDHTAnnounce() { m_nativeHandle.force_dht_announce(); } void TorrentHandle::forceRecheck() { if (m_startupState != Started) return; if (!hasMetadata()) return; m_nativeHandle.force_recheck(); m_unchecked = false; if (isPaused()) { #if (LIBTORRENT_VERSION_NUM < 10200) m_nativeHandle.stop_when_ready(true); #else m_nativeHandle.set_flags(lt::torrent_flags::stop_when_ready); #endif setAutoManaged(true); m_pauseWhenReady = true; } } void TorrentHandle::setSequentialDownload(const bool enable) { #if (LIBTORRENT_VERSION_NUM < 10200) m_nativeHandle.set_sequential_download(enable); m_nativeStatus.sequential_download = enable; // prevent return cached value #else 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 } #endif saveResumeData(); } void TorrentHandle::toggleSequentialDownload() { setSequentialDownload(!isSequentialDownload()); } void TorrentHandle::setFirstLastPiecePriority(const bool enabled) { setFirstLastPiecePriorityImpl(enabled); } void TorrentHandle::setFirstLastPiecePriorityImpl(const bool enabled, const QVector &updatedFilePrio) { // Download first and last pieces first for every file in the torrent if (!hasMetadata()) { m_needsToSetFirstLastPiecePriority = enabled; return; } #if (LIBTORRENT_VERSION_NUM < 10200) const std::vector filePriorities = !updatedFilePrio.isEmpty() ? toLTDownloadPriorities(updatedFilePrio) : nativeHandle().file_priorities(); std::vector piecePriorities = nativeHandle().piece_priorities(); #else const std::vector filePriorities = !updatedFilePrio.isEmpty() ? toLTDownloadPriorities(updatedFilePrio) : nativeHandle().get_file_priorities(); std::vector piecePriorities = nativeHandle().get_piece_priorities(); #endif // 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 < static_cast(filePriorities.size()); ++index) { const LTDownloadPriority filePrio = filePriorities[index]; if (filePrio <= LTDownloadPriority {0}) continue; // Determine the priority to set const LTDownloadPriority newPrio = enabled ? LTDownloadPriority {7} : filePrio; 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] = newPrio; piecePriorities[extremities.last() - i] = newPrio; } } m_nativeHandle.prioritize_pieces(piecePriorities); LogMsg(tr("Download first and last piece first: %1, torrent: '%2'") .arg((enabled ? tr("On") : tr("Off")), name())); saveResumeData(); } void TorrentHandle::toggleFirstLastPiecePriority() { setFirstLastPiecePriority(!hasFirstLastPiecePriority()); } void TorrentHandle::pause() { if (m_startupState != Started) return; if (m_pauseWhenReady) return; if (isChecking()) { m_pauseWhenReady = true; return; } if (isPaused()) return; setAutoManaged(false); m_nativeHandle.pause(); // Libtorrent doesn't emit a torrent_paused_alert when the // torrent is queued (no I/O) // We test on the cached m_nativeStatus if (isQueued()) m_session->handleTorrentPaused(this); } void TorrentHandle::resume(bool forced) { if (m_startupState != Started) return; m_pauseWhenReady = false; resume_impl(forced); } void TorrentHandle::resume_impl(bool forced) { if (hasError()) m_nativeHandle.clear_error(); if (m_hasMissingFiles) { m_hasMissingFiles = false; m_nativeHandle.force_recheck(); } setAutoManaged(!forced); m_nativeHandle.resume(); } void TorrentHandle::moveStorage(const QString &newPath, bool overwrite) { if (isMoveInProgress()) { qDebug("enqueue move storage to %s", qUtf8Printable(newPath)); m_moveStorageInfo.queuedPath = newPath; m_moveStorageInfo.queuedOverwrite = overwrite; } else { const QString oldPath = nativeActualSavePath(); if (QDir(oldPath) == QDir(newPath)) return; qDebug("move storage: %s to %s", qUtf8Printable(oldPath), qUtf8Printable(newPath)); // Actually move the storage #if (LIBTORRENT_VERSION_NUM < 10200) m_nativeHandle.move_storage(newPath.toUtf8().constData() , (overwrite ? lt::always_replace_files : lt::dont_replace)); #else m_nativeHandle.move_storage(newPath.toUtf8().constData() , (overwrite ? lt::move_flags_t::always_replace_files : lt::move_flags_t::dont_replace)); #endif m_moveStorageInfo.oldPath = oldPath; m_moveStorageInfo.newPath = newPath; updateState(); } } void TorrentHandle::renameFile(const int index, const QString &name) { if (m_startupState != Started) return; m_oldPath[index].push_back(filePath(index)); ++m_renameCount; m_nativeHandle.rename_file(LTFileIndex {index}, Utils::Fs::toNativePath(name).toStdString()); } bool TorrentHandle::saveTorrentFile(const QString &path) { if (!m_torrentInfo.isValid()) return false; #if (LIBTORRENT_VERSION_NUM < 10200) const lt::create_torrent torrentCreator = lt::create_torrent(*(m_torrentInfo.nativeInfo()), true); #else const lt::create_torrent torrentCreator = lt::create_torrent(*(m_torrentInfo.nativeInfo())); #endif const lt::entry torrentEntry = torrentCreator.generate(); QVector out; lt::bencode(std::back_inserter(out), torrentEntry); QFile torrentFile(path); if (!out.empty() && torrentFile.open(QIODevice::WriteOnly)) return (torrentFile.write(&out[0], out.size()) == out.size()); return false; } void TorrentHandle::handleStateUpdate(const lt::torrent_status &nativeStatus) { updateStatus(nativeStatus); } void TorrentHandle::handleStorageMovedAlert(const lt::storage_moved_alert *p) { if (!isMoveInProgress()) { qWarning() << "Unexpected " << Q_FUNC_INFO << " call."; return; } const QString newPath(p->storage_path()); if (newPath != m_moveStorageInfo.newPath) { qWarning() << Q_FUNC_INFO << ": New path doesn't match a path in a queue."; return; } LogMsg(tr("Successfully moved torrent: %1. New path: %2").arg(name(), m_moveStorageInfo.newPath)); const QDir oldDir {m_moveStorageInfo.oldPath}; if ((oldDir == QDir(m_session->torrentTempPath(info()))) && (oldDir != 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 qDebug() << "Removing torrent temp folder:" << m_moveStorageInfo.oldPath; Utils::Fs::smartRemoveEmptyFolderTree(m_moveStorageInfo.oldPath); } m_moveStorageInfo.newPath.clear(); updateStatus(); if (!m_moveStorageInfo.queuedPath.isEmpty()) { moveStorage(m_moveStorageInfo.queuedPath, m_moveStorageInfo.queuedOverwrite); m_moveStorageInfo.queuedPath.clear(); } if (!useTempPath()) { m_savePath = newPath; m_session->handleTorrentSavePathChanged(this); } while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) m_moveFinishedTriggers.takeFirst()(); } void TorrentHandle::handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert *p) { if (!isMoveInProgress()) { qWarning() << "Unexpected " << Q_FUNC_INFO << " call."; return; } LogMsg(tr("Could not move torrent: '%1'. Reason: %2") .arg(name(), QString::fromStdString(p->message())), Log::CRITICAL); m_moveStorageInfo.newPath.clear(); updateStatus(); if (!m_moveStorageInfo.queuedPath.isEmpty()) { moveStorage(m_moveStorageInfo.queuedPath, m_moveStorageInfo.queuedOverwrite); m_moveStorageInfo.queuedPath.clear(); } while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) m_moveFinishedTriggers.takeFirst()(); } void TorrentHandle::handleTrackerReplyAlert(const lt::tracker_reply_alert *p) { const QString trackerUrl(p->tracker_url()); qDebug("Received a tracker reply from %s (Num_peers = %d)", qUtf8Printable(trackerUrl), p->num_peers); // Connection was successful now. Remove possible old errors m_trackerInfos[trackerUrl] = {{}, p->num_peers}; m_session->handleTorrentTrackerReply(this, trackerUrl); } void TorrentHandle::handleTrackerWarningAlert(const lt::tracker_warning_alert *p) { const QString trackerUrl = p->tracker_url(); const QString message = p->warning_message(); // Connection was successful now but there is a warning message m_trackerInfos[trackerUrl].lastMessage = message; // Store warning message m_session->handleTorrentTrackerWarning(this, trackerUrl); } void TorrentHandle::handleTrackerErrorAlert(const lt::tracker_error_alert *p) { const QString trackerUrl = p->tracker_url(); const QString message = p->error_message(); m_trackerInfos[trackerUrl].lastMessage = message; m_session->handleTorrentTrackerError(this, trackerUrl); } void TorrentHandle::handleTorrentCheckedAlert(const lt::torrent_checked_alert *p) { Q_UNUSED(p); qDebug("\"%s\" have just finished checking", qUtf8Printable(name())); if (m_startupState == Preparing) { if (!m_pauseWhenReady) { if (!m_hasMissingFiles) { // Resume torrent because it was added in "resumed" state // but it's actually paused during initialization. m_startupState = Starting; resume_impl(m_needsToStartForced); } else { // Torrent that has missing files is paused. m_startupState = Started; } } else { m_startupState = Started; m_pauseWhenReady = 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 TorrentHandle::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(); 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 TorrentHandle::handleTorrentPausedAlert(const lt::torrent_paused_alert *p) { Q_UNUSED(p); if (m_startupState == Started) { if (!m_pauseWhenReady) { updateStatus(); m_speedMonitor.reset(); m_session->handleTorrentPaused(this); } else { m_pauseWhenReady = false; } } } void TorrentHandle::handleTorrentResumedAlert(const lt::torrent_resumed_alert *p) { Q_UNUSED(p); if (m_startupState == Started) m_session->handleTorrentResumed(this); else if (m_startupState == Starting) m_startupState = Started; } void TorrentHandle::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p) { #if (LIBTORRENT_VERSION_NUM < 10200) const bool useDummyResumeData = !(p && p->resume_data); lt::entry dummyEntry; lt::entry &resumeData = useDummyResumeData ? dummyEntry : *(p->resume_data); #else const bool useDummyResumeData = !p; lt::entry resumeData = useDummyResumeData ? lt::entry() : lt::write_resume_data(p->params); #endif if (useDummyResumeData) { resumeData["qBt-magnetUri"] = toMagnetUri().toStdString(); resumeData["paused"] = isPaused(); resumeData["auto_managed"] = isAutoManaged(); // Both firstLastPiecePriority and sequential need to be stored in the // resume data if there is no metadata, otherwise they won't be // restored if qBittorrent quits before the metadata are retrieved: resumeData["qBt-firstLastPiecePriority"] = hasFirstLastPiecePriority(); resumeData["qBt-sequential"] = isSequentialDownload(); } else { const auto savePath = resumeData.find_key("save_path")->string(); resumeData["save_path"] = Profile::instance().toPortablePath(QString::fromStdString(savePath)).toStdString(); } resumeData["qBt-savePath"] = m_useAutoTMM ? "" : Profile::instance().toPortablePath(m_savePath).toStdString(); resumeData["qBt-ratioLimit"] = static_cast(m_ratioLimit * 1000); resumeData["qBt-seedingTimeLimit"] = m_seedingTimeLimit; resumeData["qBt-category"] = m_category.toStdString(); resumeData["qBt-tags"] = setToEntryList(m_tags); resumeData["qBt-name"] = m_name.toStdString(); resumeData["qBt-seedStatus"] = m_hasSeedStatus; resumeData["qBt-tempPathDisabled"] = m_tempPathDisabled; resumeData["qBt-queuePosition"] = (static_cast(nativeHandle().queue_position()) + 1); // qBt starts queue at 1 resumeData["qBt-hasRootFolder"] = m_hasRootFolder; if (m_pauseWhenReady) { // We need to redefine these values when torrent starting/rechecking // in "paused" state since native values can be logically wrong // (torrent can be not paused and auto_managed when it is checking). resumeData["paused"] = true; resumeData["auto_managed"] = false; } m_session->handleTorrentResumeDataReady(this, resumeData); } void TorrentHandle::handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *p) { // if torrent has no metadata we should save dummy fastresume data // containing Magnet URI and qBittorrent own resume data only if (p->error.value() == lt::errors::no_metadata) { handleSaveResumeDataAlert(nullptr); } else { LogMsg(tr("Save resume data failed. Torrent: \"%1\", error: \"%2\"") .arg(name(), QString::fromStdString(p->error.message())), Log::CRITICAL); m_session->handleTorrentResumeDataFailed(this); } } void TorrentHandle::handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *p) { 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', pausing it.").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::CRITICAL); } } void TorrentHandle::handleFileRenamedAlert(const lt::file_renamed_alert *p) { // We don't really need to call updateStatus() in this place. // All we need to do is make sure we have a valid instance of the TorrentInfo object. m_torrentInfo = TorrentInfo {m_nativeHandle.torrent_file()}; // 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 const QString oldFilePath = m_oldPath[LTUnderlyingType {p->index}].takeFirst(); const QString newFilePath = Utils::Fs::toUniformPath(p->new_name()); if (m_oldPath[LTUnderlyingType {p->index}].isEmpty()) m_oldPath.remove(LTUnderlyingType {p->index}); QVector oldPathParts = oldFilePath.splitRef('/', QString::SkipEmptyParts); oldPathParts.removeLast(); // drop file name part QVector newPathParts = newFilePath.splitRef('/', QString::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, QLatin1String("/"))); oldPathParts.removeLast(); } --m_renameCount; while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) m_moveFinishedTriggers.takeFirst()(); if (isPaused() && (m_renameCount == 0)) saveResumeData(); // otherwise the new path will not be saved } void TorrentHandle::handleFileRenameFailedAlert(const lt::file_rename_failed_alert *p) { LogMsg(tr("File rename failed. Torrent: \"%1\", file: \"%2\", reason: \"%3\"") .arg(name(), filePath(LTUnderlyingType {p->index}) , QString::fromStdString(p->error.message())), Log::WARNING); m_oldPath[LTUnderlyingType {p->index}].removeFirst(); if (m_oldPath[LTUnderlyingType {p->index}].isEmpty()) m_oldPath.remove(LTUnderlyingType {p->index}); --m_renameCount; while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty()) m_moveFinishedTriggers.takeFirst()(); if (isPaused() && (m_renameCount == 0)) saveResumeData(); // otherwise the new path will not be saved } void TorrentHandle::handleFileCompletedAlert(const lt::file_completed_alert *p) { // We don't really need to call updateStatus() in this place. // All we need to do is make sure we have a valid instance of the TorrentInfo object. m_torrentInfo = TorrentInfo {m_nativeHandle.torrent_file()}; qDebug("A file completed download in torrent \"%s\"", qUtf8Printable(name())); if (m_session->isAppendExtensionEnabled()) { QString name = filePath(LTUnderlyingType {p->index}); if (name.endsWith(QB_EXT)) { const QString oldName = name; name.chop(QB_EXT.size()); qDebug("Renaming %s to %s", qUtf8Printable(oldName), qUtf8Printable(name)); renameFile(LTUnderlyingType {p->index}, name); } } } void TorrentHandle::handleMetadataReceivedAlert(const lt::metadata_received_alert *p) { Q_UNUSED(p); qDebug("Metadata received for torrent %s.", qUtf8Printable(name())); updateStatus(); if (m_session->isAppendExtensionEnabled()) manageIncompleteFiles(); if (!m_hasRootFolder) m_torrentInfo.stripRootFolder(); if (filesCount() == 1) m_hasRootFolder = false; m_session->handleTorrentMetadataReceived(this); if (isPaused()) { // XXX: Unfortunately libtorrent-rasterbar does not send a torrent_paused_alert // and the torrent can be paused when metadata is received m_speedMonitor.reset(); m_session->handleTorrentPaused(this); } // If first/last piece priority was specified when adding this torrent, we can set it // now that we have metadata: if (m_needsToSetFirstLastPiecePriority) { setFirstLastPiecePriority(true); m_needsToSetFirstLastPiecePriority = false; } } void TorrentHandle::handlePerformanceAlert(const lt::performance_alert *p) const { LogMsg((tr("Performance alert: ") + QString::fromStdString(p->message())) , Log::INFO); } void TorrentHandle::handleTempPathChanged() { adjustActualSavePath(); } void TorrentHandle::handleCategorySavePathChanged() { if (m_useAutoTMM) move_impl(m_session->categorySavePath(m_category), true); } void TorrentHandle::handleAppendExtensionToggled() { if (!hasMetadata()) return; manageIncompleteFiles(); } void TorrentHandle::handleAlert(const lt::alert *a) { switch (a->type()) { case lt::file_renamed_alert::alert_type: handleFileRenamedAlert(static_cast(a)); break; case lt::file_rename_failed_alert::alert_type: handleFileRenameFailedAlert(static_cast(a)); break; case lt::file_completed_alert::alert_type: handleFileCompletedAlert(static_cast(a)); break; case lt::torrent_finished_alert::alert_type: handleTorrentFinishedAlert(static_cast(a)); break; case lt::save_resume_data_alert::alert_type: handleSaveResumeDataAlert(static_cast(a)); break; case lt::save_resume_data_failed_alert::alert_type: handleSaveResumeDataFailedAlert(static_cast(a)); break; case lt::storage_moved_alert::alert_type: handleStorageMovedAlert(static_cast(a)); break; case lt::storage_moved_failed_alert::alert_type: handleStorageMovedFailedAlert(static_cast(a)); break; case lt::torrent_paused_alert::alert_type: handleTorrentPausedAlert(static_cast(a)); break; case lt::torrent_resumed_alert::alert_type: handleTorrentResumedAlert(static_cast(a)); break; case lt::tracker_error_alert::alert_type: handleTrackerErrorAlert(static_cast(a)); break; case lt::tracker_reply_alert::alert_type: handleTrackerReplyAlert(static_cast(a)); break; case lt::tracker_warning_alert::alert_type: handleTrackerWarningAlert(static_cast(a)); break; case lt::metadata_received_alert::alert_type: handleMetadataReceivedAlert(static_cast(a)); break; case lt::fastresume_rejected_alert::alert_type: handleFastResumeRejectedAlert(static_cast(a)); break; case lt::torrent_checked_alert::alert_type: handleTorrentCheckedAlert(static_cast(a)); break; case lt::performance_alert::alert_type: handlePerformanceAlert(static_cast(a)); break; } } void TorrentHandle::manageIncompleteFiles() { const bool isAppendExtensionEnabled = m_session->isAppendExtensionEnabled(); const QVector 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)) { const QString newName = name + QB_EXT; qDebug() << "Renaming" << name << "to" << newName; renameFile(i, newName); } } else { if (name.endsWith(QB_EXT)) { const QString oldName = name; name.chop(QB_EXT.size()); qDebug() << "Renaming" << oldName << "to" << name; renameFile(i, name); } } } } void TorrentHandle::adjustActualSavePath() { if (!isMoveInProgress()) adjustActualSavePath_impl(); else m_moveFinishedTriggers.append([this]() { adjustActualSavePath_impl(); }); } void TorrentHandle::adjustActualSavePath_impl() { QString path; if (!useTempPath()) { // Disabling temp dir // Moving all torrents to their destination folder path = savePath(); } else { // Moving all downloading torrents to temporary folder path = m_session->torrentTempPath(info()); qDebug() << "Moving torrent to its temporary folder:" << path; } moveStorage(Utils::Fs::toNativePath(path), true); } lt::torrent_handle TorrentHandle::nativeHandle() const { return m_nativeHandle; } void TorrentHandle::updateTorrentInfo() { if (!hasMetadata()) return; m_torrentInfo = TorrentInfo(m_nativeStatus.torrent_file.lock()); } bool TorrentHandle::isMoveInProgress() const { return !m_moveStorageInfo.newPath.isEmpty(); } bool TorrentHandle::useTempPath() const { return !m_tempPathDisabled && m_session->isTempPathEnabled() && !(isSeed() || m_hasSeedStatus); } void TorrentHandle::updateStatus() { updateStatus(m_nativeHandle.status()); } void TorrentHandle::updateStatus(const lt::torrent_status &nativeStatus) { m_nativeStatus = nativeStatus; updateState(); updateTorrentInfo(); // 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; m_speedMonitor.addSample({nativeStatus.download_payload_rate , nativeStatus.upload_payload_rate}); } void TorrentHandle::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->handleTorrentShareLimitChanged(this); } } void TorrentHandle::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->handleTorrentShareLimitChanged(this); } } void TorrentHandle::setUploadLimit(const int limit) { m_nativeHandle.set_upload_limit(limit); } void TorrentHandle::setDownloadLimit(const int limit) { m_nativeHandle.set_download_limit(limit); } void TorrentHandle::setSuperSeeding(const bool enable) { #if (LIBTORRENT_VERSION_NUM < 10200) m_nativeHandle.super_seeding(enable); #else if (enable) m_nativeHandle.set_flags(lt::torrent_flags::super_seeding); else m_nativeHandle.unset_flags(lt::torrent_flags::super_seeding); #endif } void TorrentHandle::flushCache() { m_nativeHandle.flush_cache(); } QString TorrentHandle::toMagnetUri() const { return QString::fromStdString(lt::make_magnet_uri(m_nativeHandle)); } void TorrentHandle::prioritizeFiles(const QVector &priorities) { if (!hasMetadata()) return; if (priorities.size() != filesCount()) return; // Save first/last piece first option state const bool firstLastPieceFirst = hasFirstLastPiecePriority(); // Reset 'm_hasSeedStatus' if needed in order to react again to // 'torrent_finished_alert' and eg show tray notifications const QVector progress = filesProgress(); const QVector 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; } } qDebug() << Q_FUNC_INFO << "Changing files priorities..."; m_nativeHandle.prioritize_files(toLTDownloadPriorities(priorities)); qDebug() << Q_FUNC_INFO << "Moving unwanted files to .unwanted folder and conversely..."; const QString spath = savePath(true); for (int i = 0; i < priorities.size(); ++i) { const QString filepath = filePath(i); // Move unwanted files to a .unwanted subfolder if (priorities[i] == DownloadPriority::Ignored) { const QString oldAbsPath = QDir(spath).absoluteFilePath(filepath); const QString parentAbsPath = Utils::Fs::branchPath(oldAbsPath); // Make sure the file does not already exists if (QDir(parentAbsPath).dirName() != ".unwanted") { const QString unwantedAbsPath = parentAbsPath + "/.unwanted"; const QString newAbsPath = unwantedAbsPath + '/' + Utils::Fs::fileName(filepath); qDebug() << "Unwanted path is" << unwantedAbsPath; if (QFile::exists(newAbsPath)) { qWarning() << "File" << newAbsPath << "already exists at destination."; continue; } const bool created = QDir().mkpath(unwantedAbsPath); qDebug() << "unwanted folder was created:" << created; #ifdef Q_OS_WIN if (created) { // Hide the folder on Windows qDebug() << "Hiding folder (Windows)"; std::wstring winPath = Utils::Fs::toNativePath(unwantedAbsPath).toStdWString(); DWORD dwAttrs = ::GetFileAttributesW(winPath.c_str()); bool ret = ::SetFileAttributesW(winPath.c_str(), dwAttrs | FILE_ATTRIBUTE_HIDDEN); Q_ASSERT(ret != 0); Q_UNUSED(ret); } #endif QString parentPath = Utils::Fs::branchPath(filepath); if (!parentPath.isEmpty() && !parentPath.endsWith('/')) parentPath += '/'; renameFile(i, parentPath + ".unwanted/" + Utils::Fs::fileName(filepath)); } } // Move wanted files back to their original folder if (priorities[i] > DownloadPriority::Ignored) { 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"); } } } // Restore first/last piece first option if necessary if (firstLastPieceFirst) setFirstLastPiecePriorityImpl(true, priorities); } QVector TorrentHandle::availableFileFractions() const { const int filesCount = this->filesCount(); if (filesCount < 0) return {}; const QVector piecesAvailability = pieceAvailability(); // libtorrent returns empty array for seeding only torrents if (piecesAvailability.empty()) return QVector(filesCount, -1.); QVector 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 (int piece = filePieces.first(); piece <= filePieces.last(); ++piece) { availablePieces += (piecesAvailability[piece] > 0) ? 1 : 0; } res.push_back(static_cast(availablePieces) / filePieces.size()); } return res; }