diff --git a/dist/windows/config.nsh b/dist/windows/config.nsh index 6d35d2e42..ee3fc5145 100644 --- a/dist/windows/config.nsh +++ b/dist/windows/config.nsh @@ -111,7 +111,8 @@ RequestExecutionLevel user !define MUI_HEADERIMAGE !define MUI_COMPONENTSPAGE_NODESC ;!define MUI_ICON "qbittorrent.ico" -!define MUI_LICENSEPAGE_CHECKBOX +!define MUI_LICENSEPAGE_BUTTON $(^NextBtn) +!define MUI_LICENSEPAGE_TEXT_BOTTOM "$_CLICK" !define MUI_LANGDLL_ALLLANGUAGES ;-------------------------------- diff --git a/src/app/application.cpp b/src/app/application.cpp index e82df1168..cfb0dbd18 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -920,10 +920,10 @@ int Application::exec() m_desktopIntegration->showNotification(tr("Torrent added"), tr("'%1' was added.", "e.g: xxx.avi was added.").arg(torrent->name())); }); connect(m_addTorrentManager, &AddTorrentManager::addTorrentFailed, this - , [this](const QString &source, const QString &reason) + , [this](const QString &source, const BitTorrent::AddTorrentError &reason) { m_desktopIntegration->showNotification(tr("Add torrent failed") - , tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason)); + , tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason.message)); }); disconnect(m_desktopIntegration, &DesktopIntegration::activationRequested, this, &Application::createStartupProgressDialog); diff --git a/src/app/cmdoptions.cpp b/src/app/cmdoptions.cpp index f6946f68f..3cbf53e37 100644 --- a/src/app/cmdoptions.cpp +++ b/src/app/cmdoptions.cpp @@ -491,6 +491,12 @@ QString makeUsage(const QString &prgName) { const QString indentation {USAGE_INDENTATION, u' '}; +#if defined(Q_OS_WIN) + const QString noSplashCommand = u"set QBT_NO_SPLASH=1 && " + prgName; +#else + const QString noSplashCommand = u"QBT_NO_SPLASH=1 " + prgName; +#endif + const QString text = QCoreApplication::translate("CMD Options", "Usage:") + u'\n' + indentation + prgName + u' ' + QCoreApplication::translate("CMD Options", "[options] [( | )...]") + u'\n' @@ -542,7 +548,7 @@ QString makeUsage(const QString &prgName) "'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper " "case, '-' replaced with '_'). To pass flag values, set the variable to '1' or " "'TRUE'. For example, to disable the splash screen: "), 0) + u'\n' - + u"QBT_NO_SPLASH=1 " + prgName + u'\n' + + noSplashCommand + u'\n' + wrapText(QCoreApplication::translate("CMD Options", "Command line parameters take precedence over environment variables"), 0) + u'\n'; return text; diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 5d2f9cf0e..5bb0077a9 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(qbt_base STATIC applicationcomponent.h asyncfilestorage.h bittorrent/abstractfilestorage.h + bittorrent/addtorrenterror.h bittorrent/addtorrentparams.h bittorrent/announcetimepoint.h bittorrent/bandwidthscheduler.h diff --git a/src/base/addtorrentmanager.cpp b/src/base/addtorrentmanager.cpp index ce446a72b..14332098a 100644 --- a/src/base/addtorrentmanager.cpp +++ b/src/base/addtorrentmanager.cpp @@ -140,7 +140,7 @@ void AddTorrentManager::onSessionTorrentAdded(BitTorrent::Torrent *torrent) } } -void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason) +void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason) { if (const QString source = m_sourcesByInfoHash.take(infoHash); !source.isEmpty()) { @@ -154,7 +154,7 @@ void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &in void AddTorrentManager::handleAddTorrentFailed(const QString &source, const QString &reason) { LogMsg(tr("Failed to add torrent. Source: \"%1\". Reason: \"%2\"").arg(source, reason), Log::WARNING); - emit addTorrentFailed(source, reason); + emit addTorrentFailed(source, {BitTorrent::AddTorrentError::Other, reason}); } void AddTorrentManager::handleDuplicateTorrent(const QString &source @@ -187,7 +187,7 @@ void AddTorrentManager::handleDuplicateTorrent(const QString &source LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: %2. Result: %3") .arg(source, existingTorrent->name(), message)); - emit addTorrentFailed(source, message); + emit addTorrentFailed(source, {BitTorrent::AddTorrentError::DuplicateTorrent, message}); } void AddTorrentManager::setTorrentFileGuard(const QString &source, std::shared_ptr torrentFileGuard) diff --git a/src/base/addtorrentmanager.h b/src/base/addtorrentmanager.h index 0e2fc6f49..0621be7a2 100644 --- a/src/base/addtorrentmanager.h +++ b/src/base/addtorrentmanager.h @@ -35,6 +35,7 @@ #include #include "base/applicationcomponent.h" +#include "base/bittorrent/addtorrenterror.h" #include "base/bittorrent/addtorrentparams.h" #include "base/torrentfileguard.h" @@ -66,7 +67,7 @@ public: signals: void torrentAdded(const QString &source, BitTorrent::Torrent *torrent); - void addTorrentFailed(const QString &source, const QString &reason); + void addTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &reason); protected: bool addTorrentToSession(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr @@ -79,7 +80,7 @@ protected: private: void onDownloadFinished(const Net::DownloadResult &result); void onSessionTorrentAdded(BitTorrent::Torrent *torrent); - void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason); + void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason); bool processTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr , const BitTorrent::AddTorrentParams &addTorrentParams); diff --git a/src/base/bittorrent/addtorrenterror.h b/src/base/bittorrent/addtorrenterror.h new file mode 100644 index 000000000..a0afb4705 --- /dev/null +++ b/src/base/bittorrent/addtorrenterror.h @@ -0,0 +1,49 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include + +namespace BitTorrent +{ + struct AddTorrentError + { + enum Kind + { + DuplicateTorrent, + Other + }; + + Kind kind = Other; + QString message; + }; +} + +Q_DECLARE_METATYPE(BitTorrent::AddTorrentError) diff --git a/src/base/bittorrent/bencoderesumedatastorage.cpp b/src/base/bittorrent/bencoderesumedatastorage.cpp index 1e1083051..16a63207c 100644 --- a/src/base/bittorrent/bencoderesumedatastorage.cpp +++ b/src/base/bittorrent/bencoderesumedatastorage.cpp @@ -147,7 +147,7 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(cons const Path torrentFilePath = path() / Path(idString + u".torrent"); const qint64 torrentSizeLimit = Preferences::instance()->getTorrentFileSizeLimit(); - const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, torrentSizeLimit); + const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, -1); if (!resumeDataReadResult) return nonstd::make_unexpected(resumeDataReadResult.error().message); diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index f5c062172..d35b9839e 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -34,6 +34,7 @@ #include "base/pathfwd.h" #include "base/tagset.h" +#include "addtorrenterror.h" #include "addtorrentparams.h" #include "categoryoptions.h" #include "sharelimitaction.h" @@ -481,7 +482,7 @@ namespace BitTorrent signals: void startupProgressUpdated(int progress); - void addTorrentFailed(const InfoHash &infoHash, const QString &reason); + void addTorrentFailed(const InfoHash &infoHash, const AddTorrentError &reason); void allTorrentsFinished(); void categoryAdded(const QString &categoryName); void categoryRemoved(const QString &categoryName); diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 4dee5a911..da1419f49 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -467,9 +467,11 @@ SessionImpl::SessionImpl(QObject *parent) , m_additionalTrackers(BITTORRENT_SESSION_KEY(u"AdditionalTrackers"_s)) , m_isAddTrackersFromURLEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersFromURLEnabled"_s), false) , m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s)) - , m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r;}) - , m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s), -1, lowerLimited(-1)) - , m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s), -1, lowerLimited(-1)) + , m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r; }) + , m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s) + , Torrent::NO_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_SEEDING_TIME_LIMIT)) + , m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s) + , Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT)) , m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false) , m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false) , m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None) @@ -1220,7 +1222,7 @@ qreal SessionImpl::globalMaxRatio() const void SessionImpl::setGlobalMaxRatio(qreal ratio) { if (ratio < 0) - ratio = -1.; + ratio = Torrent::NO_RATIO_LIMIT; if (ratio != globalMaxRatio()) { @@ -1236,8 +1238,7 @@ int SessionImpl::globalMaxSeedingMinutes() const void SessionImpl::setGlobalMaxSeedingMinutes(int minutes) { - if (minutes < 0) - minutes = -1; + minutes = std::max(minutes, Torrent::NO_SEEDING_TIME_LIMIT); if (minutes != globalMaxSeedingMinutes()) { @@ -1253,7 +1254,7 @@ int SessionImpl::globalMaxInactiveSeedingMinutes() const void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes) { - minutes = std::max(minutes, -1); + minutes = std::max(minutes, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT); if (minutes != globalMaxInactiveSeedingMinutes()) { @@ -2312,19 +2313,19 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent) QString description; if (const qreal ratio = torrent->realRatio(); - (ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit)) + (ratioLimit >= 0) && (ratio >= ratioLimit)) { reached = true; description = tr("Torrent reached the share ratio limit."); } else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60; - (seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit)) + (seedingTimeLimit >= 0) && (seedingTimeInMinutes >= seedingTimeLimit)) { reached = true; description = tr("Torrent reached the seeding time limit."); } else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60; - (inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit)) + (inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit)) { reached = true; description = tr("Torrent reached the inactive seeding time limit."); @@ -2753,7 +2754,10 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr // We should not add the torrent if it is already // processed or is pending to add to session if (m_loadingTorrents.contains(id) || (infoHash.isHybrid() && m_loadingTorrents.contains(altID))) + { + emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, tr("Duplicate torrent")}); return false; + } if (Torrent *torrent = findTorrent(infoHash)) { @@ -2767,16 +2771,20 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr if (!isMergeTrackersEnabled()) { + const QString message = tr("Merging of trackers is disabled"); LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2") - .arg(torrent->name(), tr("Merging of trackers is disabled"))); + .arg(torrent->name(), message)); + emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message}); return false; } const bool isPrivate = torrent->isPrivate() || (hasMetadata && source.info()->isPrivate()); if (isPrivate) { + const QString message = tr("Trackers cannot be merged because it is a private torrent"); LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2") - .arg(torrent->name(), tr("Trackers cannot be merged because it is a private torrent"))); + .arg(torrent->name(), message)); + emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message}); return false; } @@ -2784,8 +2792,10 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr torrent->addTrackers(source.trackers()); torrent->addUrlSeeds(source.urlSeeds()); + const QString message = tr("Trackers are merged from new source"); LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2") - .arg(torrent->name(), tr("Trackers are merged from new source"))); + .arg(torrent->name(), message)); + emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message}); return false; } @@ -5707,7 +5717,9 @@ void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert) if (const auto loadingTorrentsIter = m_loadingTorrents.find(TorrentID::fromInfoHash(infoHash)) ; loadingTorrentsIter != m_loadingTorrents.end()) { - emit addTorrentFailed(infoHash, msg); + const AddTorrentError::Kind errorKind = (alert->error == lt::errors::duplicate_torrent) + ? AddTorrentError::DuplicateTorrent : AddTorrentError::Other; + emit addTorrentFailed(infoHash, {errorKind, msg}); m_loadingTorrents.erase(loadingTorrentsIter); } else if (const auto downloadedMetadataIter = m_downloadedMetadata.find(TorrentID::fromInfoHash(infoHash)) diff --git a/src/base/bittorrent/torrent.cpp b/src/base/bittorrent/torrent.cpp index c92fb9aea..b8098d77f 100644 --- a/src/base/bittorrent/torrent.cpp +++ b/src/base/bittorrent/torrent.cpp @@ -29,6 +29,8 @@ #include "torrent.h" +#include + #include #include "infohash.h" @@ -51,9 +53,7 @@ namespace BitTorrent const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2; const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1; - const qreal Torrent::MAX_RATIO = 9999; - const int Torrent::MAX_SEEDING_TIME = 525600; - const int Torrent::MAX_INACTIVE_SEEDING_TIME = 525600; + const qreal Torrent::MAX_RATIO = std::numeric_limits::infinity(); TorrentID Torrent::id() const { diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index d2eefbabf..72cbf0a38 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -132,8 +132,6 @@ namespace BitTorrent static const int NO_INACTIVE_SEEDING_TIME_LIMIT; static const qreal MAX_RATIO; - static const int MAX_SEEDING_TIME; - static const int MAX_INACTIVE_SEEDING_TIME; using TorrentContentHandler::TorrentContentHandler; diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 45cc7f3bf..96547fe77 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -1549,7 +1549,8 @@ qreal TorrentImpl::realRatio() const const qreal ratio = upload / static_cast(download); Q_ASSERT(ratio >= 0); - return (ratio > MAX_RATIO) ? MAX_RATIO : ratio; + + return ratio; } int TorrentImpl::uploadPayloadRate() const @@ -2712,8 +2713,6 @@ 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) { @@ -2727,8 +2726,6 @@ 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) { @@ -2742,8 +2739,6 @@ void TorrentImpl::setInactiveSeedingTimeLimit(int limit) { if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME) limit = NO_INACTIVE_SEEDING_TIME_LIMIT; - else if (limit > MAX_INACTIVE_SEEDING_TIME) - limit = MAX_SEEDING_TIME; if (m_inactiveSeedingTimeLimit != limit) { diff --git a/src/base/rss/rss_autodownloader.cpp b/src/base/rss/rss_autodownloader.cpp index a365a53ef..c40218457 100644 --- a/src/base/rss/rss_autodownloader.cpp +++ b/src/base/rss/rss_autodownloader.cpp @@ -375,10 +375,24 @@ void AutoDownloader::handleTorrentAdded(const QString &source) } } -void AutoDownloader::handleAddTorrentFailed(const QString &source) +void AutoDownloader::handleAddTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &error) { - m_waitingJobs.remove(source); - // TODO: Re-schedule job here. + const auto job = m_waitingJobs.take(source); + if (!job) + return; + + if (error.kind == BitTorrent::AddTorrentError::DuplicateTorrent) + { + if (Feed *feed = Session::instance()->feedByURL(job->feedURL)) + { + if (Article *article = feed->articleByGUID(job->articleData.value(Article::KeyId).toString())) + article->markAsRead(); + } + } + else + { + // TODO: Re-schedule job here. + } } void AutoDownloader::handleNewArticle(const Article *article) diff --git a/src/base/rss/rss_autodownloader.h b/src/base/rss/rss_autodownloader.h index e69877fb8..dcd752bcf 100644 --- a/src/base/rss/rss_autodownloader.h +++ b/src/base/rss/rss_autodownloader.h @@ -37,6 +37,7 @@ #include #include "base/applicationcomponent.h" +#include "base/bittorrent/addtorrenterror.h" #include "base/exceptions.h" #include "base/settingvalue.h" #include "base/utils/thread.h" @@ -111,7 +112,7 @@ namespace RSS private slots: void process(); void handleTorrentAdded(const QString &source); - void handleAddTorrentFailed(const QString &url); + void handleAddTorrentFailed(const QString &url, const BitTorrent::AddTorrentError &error); void handleNewArticle(const Article *article); void handleFeedURLChanged(Feed *feed, const QString &oldURL); diff --git a/src/base/utils/random.cpp b/src/base/utils/random.cpp index 57f84c753..bc0ec3392 100644 --- a/src/base/utils/random.cpp +++ b/src/base/utils/random.cpp @@ -42,7 +42,7 @@ uint32_t Utils::Random::rand(const uint32_t min, const uint32_t max) { - static RandomLayer layer; + static const RandomLayer layer; // new distribution is cheap: https://stackoverflow.com/a/19036349 std::uniform_int_distribution uniform(min, max); diff --git a/src/base/utils/randomlayer_linux.cpp b/src/base/utils/randomlayer_linux.cpp index 29b24631c..81310f3bb 100644 --- a/src/base/utils/randomlayer_linux.cpp +++ b/src/base/utils/randomlayer_linux.cpp @@ -27,6 +27,7 @@ */ #include +#include #include #include @@ -44,6 +45,27 @@ namespace RandomLayer() { + if (::getrandom(nullptr, 0, 0) < 0) + { + if (errno == ENOSYS) + { + // underlying kernel does not implement this system call + // fallback to `urandom` + m_randDev = fopen("/dev/urandom", "rb"); + if (!m_randDev) + qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno); + } + else + { + qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno); + } + } + } + + ~RandomLayer() + { + if (m_randDev) + fclose(m_randDev); } static constexpr result_type min() @@ -56,7 +78,15 @@ namespace return std::numeric_limits::max(); } - result_type operator()() + result_type operator()() const + { + if (!m_randDev) + return getRandomViaAPI(); + return getRandomViaFile(); + } + + private: + result_type getRandomViaAPI() const { const int RETRY_MAX = 3; @@ -68,10 +98,21 @@ namespace return buf; if (result < 0) - qFatal("getrandom() error. Reason: %s. Error code: %d.", std::strerror(errno), errno); + qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno); } qFatal("getrandom() failed. Reason: too many retries."); } + + result_type getRandomViaFile() const + { + result_type buf = 0; + if (fread(&buf, sizeof(buf), 1, m_randDev) == 1) + return buf; + + qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno); + } + + FILE *m_randDev = nullptr; }; } diff --git a/src/base/utils/randomlayer_other.cpp b/src/base/utils/randomlayer_other.cpp index fa7e61f38..4e794a1ae 100644 --- a/src/base/utils/randomlayer_other.cpp +++ b/src/base/utils/randomlayer_other.cpp @@ -46,7 +46,7 @@ namespace : m_randDev {fopen("/dev/urandom", "rb")} { if (!m_randDev) - qFatal("Failed to open /dev/urandom. Reason: %s. Error code: %d.", std::strerror(errno), errno); + qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno); } ~RandomLayer() @@ -67,10 +67,10 @@ namespace result_type operator()() const { result_type buf = 0; - if (fread(&buf, sizeof(buf), 1, m_randDev) != 1) - qFatal("Read /dev/urandom error. Reason: %s. Error code: %d.", std::strerror(errno), errno); + if (fread(&buf, sizeof(buf), 1, m_randDev) == 1) + return buf; - return buf; + qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno); } private: diff --git a/src/base/utils/randomlayer_win.cpp b/src/base/utils/randomlayer_win.cpp index 916ed6acf..946a26c94 100644 --- a/src/base/utils/randomlayer_win.cpp +++ b/src/base/utils/randomlayer_win.cpp @@ -60,7 +60,7 @@ namespace return std::numeric_limits::max(); } - result_type operator()() + result_type operator()() const { result_type buf = 0; const bool result = m_processPrng(reinterpret_cast(&buf), sizeof(buf)); diff --git a/src/base/utils/string.cpp b/src/base/utils/string.cpp index f3fc4d87e..2dc408c85 100644 --- a/src/base/utils/string.cpp +++ b/src/base/utils/string.cpp @@ -61,7 +61,12 @@ QString Utils::String::fromLocal8Bit(const std::string_view string) QString Utils::String::wildcardToRegexPattern(const QString &pattern) { +#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 1)) + return QRegularExpression::wildcardToRegularExpression(pattern + , (QRegularExpression::UnanchoredWildcardConversion | QRegularExpression::NonPathWildcardConversion)); +#else return QRegularExpression::wildcardToRegularExpression(pattern, QRegularExpression::UnanchoredWildcardConversion); +#endif } QStringList Utils::String::splitCommand(const QString &command) diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 71906f13b..4d124915f 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -3021,9 +3021,6 @@ Disable encryption: Only connect to peers without protocol encryption false - - 9998.000000000000000 - 0.050000000000000 @@ -3283,15 +3280,9 @@ Disable encryption: Only connect to peers without protocol encryption - - QAbstractSpinBox::ButtonSymbols::PlusMinus - 99 - - QAbstractSpinBox::StepType::DefaultStepType - diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp index d5c2b03a4..fe7870272 100644 --- a/src/gui/properties/propertieswidget.cpp +++ b/src/gui/properties/propertieswidget.cpp @@ -439,10 +439,10 @@ void PropertiesWidget::loadDynamicData() // Update ratio info const qreal ratio = m_torrent->realRatio(); - m_ui->labelShareRatioVal->setText(ratio > BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(ratio, 2)); + m_ui->labelShareRatioVal->setText(ratio >= BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(ratio, 2)); const qreal popularity = m_torrent->popularity(); - m_ui->labelPopularityVal->setText(popularity > BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(popularity, 2)); + m_ui->labelPopularityVal->setText(popularity >= BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(popularity, 2)); m_ui->labelSeedsVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)") .arg(QString::number(m_torrent->seedsCount()) diff --git a/src/gui/torrentsharelimitswidget.ui b/src/gui/torrentsharelimitswidget.ui index 9ada9ed67..13fbfe0a2 100644 --- a/src/gui/torrentsharelimitswidget.ui +++ b/src/gui/torrentsharelimitswidget.ui @@ -47,9 +47,6 @@ false - - 9998.000000000000000 - 0.050000000000000 diff --git a/src/gui/transferlistmodel.cpp b/src/gui/transferlistmodel.cpp index 7c827c1a6..0298db047 100644 --- a/src/gui/transferlistmodel.cpp +++ b/src/gui/transferlistmodel.cpp @@ -293,7 +293,7 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons if (hideValues && (value <= 0)) return {}; - return ((static_cast(value) == -1) || (value > BitTorrent::Torrent::MAX_RATIO)) + return ((static_cast(value) == -1) || (value >= BitTorrent::Torrent::MAX_RATIO)) ? C_INFINITY : Utils::String::fromDouble(value, 2); }; diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp index cd6b44653..f553f5c6b 100644 --- a/src/gui/transferlistwidget.cpp +++ b/src/gui/transferlistwidget.cpp @@ -311,10 +311,7 @@ void TransferListWidget::torrentDoubleClicked() case PREVIEW_FILE: if (torrentContainsPreviewableFiles(torrent)) { - auto *dialog = new PreviewSelectDialog(this, torrent); - dialog->setAttribute(Qt::WA_DeleteOnClose); - connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile); - dialog->show(); + openPreviewSelectDialog(torrent); } else { @@ -616,10 +613,7 @@ void TransferListWidget::previewSelectedTorrents() { if (torrentContainsPreviewableFiles(torrent)) { - auto *dialog = new PreviewSelectDialog(this, torrent); - dialog->setAttribute(Qt::WA_DeleteOnClose); - connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile); - dialog->show(); + openPreviewSelectDialog(torrent); } else { @@ -1448,3 +1442,13 @@ void TransferListWidget::wheelEvent(QWheelEvent *event) QTreeView::wheelEvent(event); // event delegated to base class } + +void TransferListWidget::openPreviewSelectDialog(const BitTorrent::Torrent *torrent) +{ + auto *dialog = new PreviewSelectDialog(this, torrent); + dialog->setAttribute(Qt::WA_DeleteOnClose); + // Qt::QueuedConnection is required to prevent a bug on wayland compositors where the preview won't open. + // It occurs when the window focus shifts immediately after TransferListWidget::previewFile has been called. + connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile, Qt::QueuedConnection); + dialog->show(); +} diff --git a/src/gui/transferlistwidget.h b/src/gui/transferlistwidget.h index c5b24ba24..097dd809a 100644 --- a/src/gui/transferlistwidget.h +++ b/src/gui/transferlistwidget.h @@ -123,6 +123,7 @@ private: void dragMoveEvent(QDragMoveEvent *event) override; void dropEvent(QDropEvent *event) override; void wheelEvent(QWheelEvent *event) override; + void openPreviewSelectDialog(const BitTorrent::Torrent *torrent); QModelIndex mapToSource(const QModelIndex &index) const; QModelIndexList mapToSource(const QModelIndexList &indexes) const; QModelIndex mapFromSource(const QModelIndex &index) const; diff --git a/src/webui/api/serialize/serialize_torrent.cpp b/src/webui/api/serialize/serialize_torrent.cpp index bc75a7698..1543170d4 100644 --- a/src/webui/api/serialize/serialize_torrent.cpp +++ b/src/webui/api/serialize/serialize_torrent.cpp @@ -96,7 +96,7 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent) const auto adjustRatio = [](const qreal ratio) -> qreal { - return (ratio > BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio; + return (ratio >= BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio; }; const auto getLastActivityTime = [&torrent]() -> qlonglong diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 324bb89d6..39e536867 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -522,8 +522,8 @@ void TorrentsController::propertiesAction() {KEY_PROP_SEEDS_TOTAL, torrent->totalSeedsCount()}, {KEY_PROP_PEERS, torrent->leechsCount()}, {KEY_PROP_PEERS_TOTAL, torrent->totalLeechersCount()}, - {KEY_PROP_RATIO, ((ratio > BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)}, - {KEY_PROP_POPULARITY, ((popularity > BitTorrent::Torrent::MAX_RATIO) ? -1 : popularity)}, + {KEY_PROP_RATIO, ((ratio >= BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)}, + {KEY_PROP_POPULARITY, ((popularity >= BitTorrent::Torrent::MAX_RATIO) ? -1 : popularity)}, {KEY_PROP_REANNOUNCE, torrent->nextAnnounce()}, {KEY_PROP_TOTAL_SIZE, torrent->totalSize()}, {KEY_PROP_PIECES_NUM, torrent->piecesCount()}, diff --git a/src/webui/www/private/index.html b/src/webui/www/private/index.html index da0b74d7e..f201d0efb 100644 --- a/src/webui/www/private/index.html +++ b/src/webui/www/private/index.html @@ -26,6 +26,7 @@ + diff --git a/src/webui/www/private/newcategory.html b/src/webui/www/private/newcategory.html index b2ec949fa..6e36e26d5 100644 --- a/src/webui/www/private/newcategory.html +++ b/src/webui/www/private/newcategory.html @@ -147,9 +147,9 @@ break; } }); - }); - window.qBittorrent.pathAutofill.attachPathAutofill(); + window.qBittorrent.pathAutofill.attachPathAutofill(); + }); diff --git a/src/webui/www/private/newfolder.html b/src/webui/www/private/newfolder.html index 30db4c450..bfbeceead 100644 --- a/src/webui/www/private/newfolder.html +++ b/src/webui/www/private/newfolder.html @@ -10,7 +10,6 @@ -
- +
diff --git a/src/webui/www/private/rename_files.html b/src/webui/www/private/rename_files.html index f8eefa009..514efa4f4 100644 --- a/src/webui/www/private/rename_files.html +++ b/src/webui/www/private/rename_files.html @@ -411,7 +411,7 @@

- +
@@ -425,7 +425,7 @@

- + +
 
diff --git a/src/webui/www/private/shareratio.html b/src/webui/www/private/shareratio.html index b106e72b8..10f5a0bd7 100644 --- a/src/webui/www/private/shareratio.html +++ b/src/webui/www/private/shareratio.html @@ -172,17 +172,17 @@
- +
- +
- +
diff --git a/src/webui/www/private/views/cookies.html b/src/webui/www/private/views/cookies.html index e7e301057..f6ffe4000 100644 --- a/src/webui/www/private/views/cookies.html +++ b/src/webui/www/private/views/cookies.html @@ -104,7 +104,7 @@ }; const deleteCookie = (element) => { - element.closest("tr").destroy(); + element.closest("tr").remove(); }; const save = () => { diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index 727eb318d..2cca336c6 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -2110,7 +2110,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD // Advanced Tab const updateNetworkInterfaces = (default_iface, default_iface_name) => { - [...document.getElementById("networkInterface").children].forEach((el) => { el.destroy(); }); + [...document.getElementById("networkInterface").children].forEach((el) => { el.remove(); }); fetch("api/v2/app/networkInterfaceList", { method: "GET", @@ -2139,7 +2139,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD }; const updateInterfaceAddresses = (iface, default_addr) => { - [...document.getElementById("optionalIPAddressToBind").children].forEach((el) => { el.destroy(); }); + [...document.getElementById("optionalIPAddressToBind").children].forEach((el) => { el.remove(); }); const url = new URL("api/v2/app/networkInterfaceAddressList", window.location); url.search = new URLSearchParams({ @@ -2876,22 +2876,22 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD } // Share Ratio Limiting - let max_ratio = -1; - if ($("max_ratio_checkbox").checked) { - max_ratio = Number($("max_ratio_value").value); - if (isNaN(max_ratio) || (max_ratio < 0) || (max_ratio > 9998)) { - alert("QBT_TR(Share ratio limit must be between 0 and 9998.)QBT_TR[CONTEXT=HttpServer]"); + let maxRatio = -1; + if (document.getElementById("maxRatioCheckbox").checked) { + maxRatio = Number(document.getElementById("maxRatioValue").value); + if (isNaN(maxRatio) || (maxRatio < 0)) { + alert("QBT_TR(Share ratio limit must not have a negative value.)QBT_TR[CONTEXT=HttpServer]"); return; } } settings["max_ratio_enabled"] = $("max_ratio_checkbox").checked; settings["max_ratio"] = max_ratio; - let max_seeding_time = -1; - if ($("max_seeding_time_checkbox").checked) { - max_seeding_time = Number($("max_seeding_time_value").value); - if (isNaN(max_seeding_time) || (max_seeding_time < 0) || (max_seeding_time > 525600)) { - alert("QBT_TR(Seeding time limit must be between 0 and 525600 minutes.)QBT_TR[CONTEXT=HttpServer]"); + let maxSeedingTime = -1; + if (document.getElementById("maxSeedingTimeCheckbox").checked) { + maxSeedingTime = Number(document.getElementById("maxSeedingTimeValue").value); + if (Number.isNaN(maxSeedingTime) || (maxSeedingTime < 0)) { + alert("QBT_TR(Seeding time limit must not have a negative value.)QBT_TR[CONTEXT=HttpServer]"); return; } } @@ -2899,11 +2899,11 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD settings["max_seeding_time"] = max_seeding_time; settings["max_ratio_act"] = Number($("max_ratio_act").value); - let max_inactive_seeding_time = -1; - if ($("max_inactive_seeding_time_checkbox").checked) { - max_inactive_seeding_time = Number($("max_inactive_seeding_time_value").value); - if (isNaN(max_inactive_seeding_time) || (max_inactive_seeding_time < 0) || (max_inactive_seeding_time > 525600)) { - alert("QBT_TR(Seeding time limit must be between 0 and 525600 minutes.)QBT_TR[CONTEXT=HttpServer]"); + let maxInactiveSeedingTime = -1; + if (document.getElementById("maxInactiveSeedingTimeCheckbox").checked) { + maxInactiveSeedingTime = Number(document.getElementById("maxInactiveSeedingTimeValue").value); + if (Number.isNaN(maxInactiveSeedingTime) || (maxInactiveSeedingTime < 0)) { + alert("QBT_TR(Seeding time limit must not have a negative value.)QBT_TR[CONTEXT=HttpServer]"); return; } } diff --git a/src/webui/www/private/views/properties.html b/src/webui/www/private/views/properties.html index e3cb42b1b..fbcaa1d53 100644 --- a/src/webui/www/private/views/properties.html +++ b/src/webui/www/private/views/properties.html @@ -1,4 +1,4 @@ -