mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2024-11-22 09:16:05 +03:00
Merge pull request #14466 from glassez/sha1hash
Improve "info hash" handling
This commit is contained in:
commit
6b3c6c12ff
17 changed files with 188 additions and 133 deletions
|
@ -339,7 +339,7 @@ void Application::runExternalProgram(const BitTorrent::Torrent *torrent) const
|
||||||
program.replace("%C", QString::number(torrent->filesCount()));
|
program.replace("%C", QString::number(torrent->filesCount()));
|
||||||
program.replace("%Z", QString::number(torrent->totalSize()));
|
program.replace("%Z", QString::number(torrent->totalSize()));
|
||||||
program.replace("%T", torrent->currentTracker());
|
program.replace("%T", torrent->currentTracker());
|
||||||
program.replace("%I", torrent->hash());
|
program.replace("%I", torrent->hash().toString());
|
||||||
|
|
||||||
Logger *logger = Logger::instance();
|
Logger *logger = Logger::instance();
|
||||||
logger->addMessage(tr("Torrent: %1, running external program, command: %2").arg(torrent->name(), program));
|
logger->addMessage(tr("Torrent: %1, running external program, command: %2").arg(torrent->name(), program));
|
||||||
|
|
|
@ -32,6 +32,7 @@ add_library(qbt_base STATIC
|
||||||
bittorrent/torrentinfo.h
|
bittorrent/torrentinfo.h
|
||||||
bittorrent/tracker.h
|
bittorrent/tracker.h
|
||||||
bittorrent/trackerentry.h
|
bittorrent/trackerentry.h
|
||||||
|
digest32.h
|
||||||
exceptions.h
|
exceptions.h
|
||||||
filesystemwatcher.h
|
filesystemwatcher.h
|
||||||
global.h
|
global.h
|
||||||
|
|
|
@ -31,6 +31,7 @@ HEADERS += \
|
||||||
$$PWD/bittorrent/torrentinfo.h \
|
$$PWD/bittorrent/torrentinfo.h \
|
||||||
$$PWD/bittorrent/tracker.h \
|
$$PWD/bittorrent/tracker.h \
|
||||||
$$PWD/bittorrent/trackerentry.h \
|
$$PWD/bittorrent/trackerentry.h \
|
||||||
|
$$PWD/digest32.h \
|
||||||
$$PWD/exceptions.h \
|
$$PWD/exceptions.h \
|
||||||
$$PWD/filesystemwatcher.h \
|
$$PWD/filesystemwatcher.h \
|
||||||
$$PWD/global.h \
|
$$PWD/global.h \
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
|
* Copyright (C) 2015, 2021 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -28,68 +28,14 @@
|
||||||
|
|
||||||
#include "infohash.h"
|
#include "infohash.h"
|
||||||
|
|
||||||
#include <QByteArray>
|
const int InfoHashTypeId = qRegisterMetaType<BitTorrent::InfoHash>();
|
||||||
#include <QHash>
|
|
||||||
|
|
||||||
using namespace BitTorrent;
|
BitTorrent::InfoHash BitTorrent::InfoHash::fromString(const QString &hashString)
|
||||||
|
|
||||||
const int InfoHashTypeId = qRegisterMetaType<InfoHash>();
|
|
||||||
|
|
||||||
InfoHash::InfoHash(const lt::sha1_hash &nativeHash)
|
|
||||||
: m_valid(true)
|
|
||||||
, m_nativeHash(nativeHash)
|
|
||||||
{
|
{
|
||||||
const QByteArray raw = QByteArray::fromRawData(nativeHash.data(), length());
|
return {SHA1Hash::fromString(hashString)};
|
||||||
m_hashString = QString::fromLatin1(raw.toHex());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InfoHash::InfoHash(const QString &hashString)
|
uint BitTorrent::qHash(const BitTorrent::InfoHash &key, const uint seed)
|
||||||
: m_valid(false)
|
|
||||||
{
|
{
|
||||||
if (hashString.size() != (length() * 2))
|
return ::qHash(std::hash<InfoHash::UnderlyingType>()(key), seed);
|
||||||
return;
|
|
||||||
|
|
||||||
const QByteArray raw = QByteArray::fromHex(hashString.toLatin1());
|
|
||||||
if (raw.size() != length()) // QByteArray::fromHex() will skip over invalid characters
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_valid = true;
|
|
||||||
m_hashString = hashString;
|
|
||||||
m_nativeHash.assign(raw.constData());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool InfoHash::isValid() const
|
|
||||||
{
|
|
||||||
return m_valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoHash::operator lt::sha1_hash() const
|
|
||||||
{
|
|
||||||
return m_nativeHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoHash::operator QString() const
|
|
||||||
{
|
|
||||||
return m_hashString;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BitTorrent::operator==(const InfoHash &left, const InfoHash &right)
|
|
||||||
{
|
|
||||||
return (static_cast<lt::sha1_hash>(left)
|
|
||||||
== static_cast<lt::sha1_hash>(right));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BitTorrent::operator!=(const InfoHash &left, const InfoHash &right)
|
|
||||||
{
|
|
||||||
return !(left == right);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BitTorrent::operator<(const InfoHash &left, const InfoHash &right)
|
|
||||||
{
|
|
||||||
return static_cast<lt::sha1_hash>(left) < static_cast<lt::sha1_hash>(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint BitTorrent::qHash(const InfoHash &key, const uint seed)
|
|
||||||
{
|
|
||||||
return ::qHash((std::hash<lt::sha1_hash> {})(key), seed);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
|
* Copyright (C) 2015, 2021 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -28,41 +28,25 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <libtorrent/sha1_hash.hpp>
|
#include <QHash>
|
||||||
|
|
||||||
#include <QMetaType>
|
#include <QMetaType>
|
||||||
#include <QString>
|
|
||||||
|
#include "base/digest32.h"
|
||||||
|
|
||||||
|
using SHA1Hash = Digest32<160>;
|
||||||
|
using SHA256Hash = Digest32<256>;
|
||||||
|
|
||||||
namespace BitTorrent
|
namespace BitTorrent
|
||||||
{
|
{
|
||||||
class InfoHash
|
class InfoHash : public SHA1Hash
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
InfoHash() = default;
|
using SHA1Hash::SHA1Hash;
|
||||||
InfoHash(const lt::sha1_hash &nativeHash);
|
|
||||||
InfoHash(const QString &hashString);
|
|
||||||
InfoHash(const InfoHash &other) = default;
|
|
||||||
|
|
||||||
static constexpr int length()
|
static InfoHash fromString(const QString &hashString);
|
||||||
{
|
|
||||||
return lt::sha1_hash::size();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isValid() const;
|
|
||||||
|
|
||||||
operator lt::sha1_hash() const;
|
|
||||||
operator QString() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool m_valid = false;
|
|
||||||
lt::sha1_hash m_nativeHash;
|
|
||||||
QString m_hashString;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
bool operator==(const InfoHash &left, const InfoHash &right);
|
uint qHash(const InfoHash &key, const uint seed);
|
||||||
bool operator!=(const InfoHash &left, const InfoHash &right);
|
|
||||||
bool operator<(const InfoHash &left, const InfoHash &right);
|
|
||||||
uint qHash(const InfoHash &key, uint seed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(BitTorrent::InfoHash)
|
Q_DECLARE_METATYPE(BitTorrent::InfoHash)
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
bool isBitTorrentInfoHash(const QString &string)
|
bool isSHA1Hash(const QString &string)
|
||||||
{
|
{
|
||||||
// There are 2 representations for BitTorrent info hash:
|
// There are 2 representations for BitTorrent info hash:
|
||||||
// 1. 40 chars hex-encoded string
|
// 1. 40 chars hex-encoded string
|
||||||
|
@ -65,7 +65,7 @@ MagnetUri::MagnetUri(const QString &source)
|
||||||
{
|
{
|
||||||
if (source.isEmpty()) return;
|
if (source.isEmpty()) return;
|
||||||
|
|
||||||
if (isBitTorrentInfoHash(source))
|
if (isSHA1Hash(source))
|
||||||
m_url = QLatin1String("magnet:?xt=urn:btih:") + source;
|
m_url = QLatin1String("magnet:?xt=urn:btih:") + source;
|
||||||
|
|
||||||
lt::error_code ec;
|
lt::error_code ec;
|
||||||
|
|
|
@ -1798,7 +1798,7 @@ bool Session::deleteTorrent(const InfoHash &hash, const DeleteOption deleteOptio
|
||||||
TorrentImpl *const torrent = m_torrents.take(hash);
|
TorrentImpl *const torrent = m_torrents.take(hash);
|
||||||
if (!torrent) return false;
|
if (!torrent) return false;
|
||||||
|
|
||||||
qDebug("Deleting torrent with hash: %s", qUtf8Printable(torrent->hash()));
|
qDebug("Deleting torrent with hash: %s", qUtf8Printable(torrent->hash().toString()));
|
||||||
emit torrentAboutToBeRemoved(torrent);
|
emit torrentAboutToBeRemoved(torrent);
|
||||||
|
|
||||||
// Remove it from session
|
// Remove it from session
|
||||||
|
@ -1852,8 +1852,8 @@ bool Session::deleteTorrent(const InfoHash &hash, const DeleteOption deleteOptio
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove it from torrent resume directory
|
// Remove it from torrent resume directory
|
||||||
const QString resumedataFile = QString::fromLatin1("%1.fastresume").arg(torrent->hash());
|
const QString resumedataFile = QString::fromLatin1("%1.fastresume").arg(torrent->hash().toString());
|
||||||
const QString metadataFile = QString::fromLatin1("%1.torrent").arg(torrent->hash());
|
const QString metadataFile = QString::fromLatin1("%1.torrent").arg(torrent->hash().toString());
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
|
||||||
QMetaObject::invokeMethod(m_resumeDataSavingManager, [this, resumedataFile, metadataFile]()
|
QMetaObject::invokeMethod(m_resumeDataSavingManager, [this, resumedataFile, metadataFile]()
|
||||||
{
|
{
|
||||||
|
@ -2250,7 +2250,7 @@ bool Session::downloadMetadata(const MagnetUri &magnetUri)
|
||||||
if (m_downloadedMetadata.contains(hash)) return false;
|
if (m_downloadedMetadata.contains(hash)) return false;
|
||||||
|
|
||||||
qDebug("Adding torrent to preload metadata...");
|
qDebug("Adding torrent to preload metadata...");
|
||||||
qDebug(" -> Hash: %s", qUtf8Printable(hash));
|
qDebug(" -> Hash: %s", qUtf8Printable(hash.toString()));
|
||||||
qDebug(" -> Name: %s", qUtf8Printable(name));
|
qDebug(" -> Name: %s", qUtf8Printable(name));
|
||||||
|
|
||||||
lt::add_torrent_params p = magnetUri.addTorrentParams();
|
lt::add_torrent_params p = magnetUri.addTorrentParams();
|
||||||
|
@ -2266,7 +2266,7 @@ bool Session::downloadMetadata(const MagnetUri &magnetUri)
|
||||||
p.max_connections = maxConnectionsPerTorrent();
|
p.max_connections = maxConnectionsPerTorrent();
|
||||||
p.max_uploads = maxUploadsPerTorrent();
|
p.max_uploads = maxUploadsPerTorrent();
|
||||||
|
|
||||||
const QString savePath = Utils::Fs::tempPath() + static_cast<QString>(hash);
|
const QString savePath = Utils::Fs::tempPath() + hash.toString();
|
||||||
p.save_path = Utils::Fs::toNativePath(savePath).toStdString();
|
p.save_path = Utils::Fs::toNativePath(savePath).toStdString();
|
||||||
|
|
||||||
// Forced start
|
// Forced start
|
||||||
|
@ -2299,7 +2299,7 @@ void Session::exportTorrentFile(const Torrent *torrent, TorrentExportFolder fold
|
||||||
((folder == TorrentExportFolder::Finished) && !finishedTorrentExportDirectory().isEmpty()));
|
((folder == TorrentExportFolder::Finished) && !finishedTorrentExportDirectory().isEmpty()));
|
||||||
|
|
||||||
const QString validName = Utils::Fs::toValidFileSystemName(torrent->name());
|
const QString validName = Utils::Fs::toValidFileSystemName(torrent->name());
|
||||||
const QString torrentFilename = QString::fromLatin1("%1.torrent").arg(torrent->hash());
|
const QString torrentFilename = QString::fromLatin1("%1.torrent").arg(torrent->hash().toString());
|
||||||
QString torrentExportFilename = QString::fromLatin1("%1.torrent").arg(validName);
|
QString torrentExportFilename = QString::fromLatin1("%1.torrent").arg(validName);
|
||||||
const QString torrentPath = QDir(m_resumeFolderPath).absoluteFilePath(torrentFilename);
|
const QString torrentPath = QDir(m_resumeFolderPath).absoluteFilePath(torrentFilename);
|
||||||
const QDir exportPath(folder == TorrentExportFolder::Regular ? torrentExportDirectory() : finishedTorrentExportDirectory());
|
const QDir exportPath(folder == TorrentExportFolder::Regular ? torrentExportDirectory() : finishedTorrentExportDirectory());
|
||||||
|
@ -2372,7 +2372,7 @@ void Session::saveTorrentsQueue() const
|
||||||
// We require actual (non-cached) queue position here!
|
// We require actual (non-cached) queue position here!
|
||||||
const int queuePos = static_cast<LTUnderlyingType<lt::queue_position_t>>(torrent->nativeHandle().queue_position());
|
const int queuePos = static_cast<LTUnderlyingType<lt::queue_position_t>>(torrent->nativeHandle().queue_position());
|
||||||
if (queuePos >= 0)
|
if (queuePos >= 0)
|
||||||
queue[queuePos] = torrent->hash();
|
queue[queuePos] = torrent->hash().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray data;
|
QByteArray data;
|
||||||
|
@ -3852,7 +3852,7 @@ void Session::handleTorrentMetadataReceived(TorrentImpl *const torrent)
|
||||||
{
|
{
|
||||||
// Save metadata
|
// Save metadata
|
||||||
const QDir resumeDataDir {m_resumeFolderPath};
|
const QDir resumeDataDir {m_resumeFolderPath};
|
||||||
const QString torrentFileName {QString {"%1.torrent"}.arg(torrent->hash())};
|
const QString torrentFileName {QString {"%1.torrent"}.arg(torrent->hash().toString())};
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
torrent->info().saveToFile(resumeDataDir.absoluteFilePath(torrentFileName));
|
torrent->info().saveToFile(resumeDataDir.absoluteFilePath(torrentFileName));
|
||||||
|
@ -3932,7 +3932,7 @@ void Session::handleTorrentResumeDataReady(TorrentImpl *const torrent, const std
|
||||||
// Separated thread is used for the blocking IO which results in slow processing of many torrents.
|
// Separated thread is used for the blocking IO which results in slow processing of many torrents.
|
||||||
// Copying lt::entry objects around isn't cheap.
|
// Copying lt::entry objects around isn't cheap.
|
||||||
|
|
||||||
const QString filename = QString::fromLatin1("%1.fastresume").arg(torrent->hash());
|
const QString filename = QString::fromLatin1("%1.fastresume").arg(torrent->hash().toString());
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
|
||||||
QMetaObject::invokeMethod(m_resumeDataSavingManager
|
QMetaObject::invokeMethod(m_resumeDataSavingManager
|
||||||
, [this, filename, data]() { m_resumeDataSavingManager->save(filename, data); });
|
, [this, filename, data]() { m_resumeDataSavingManager->save(filename, data); });
|
||||||
|
@ -4010,7 +4010,7 @@ void Session::moveTorrentStorage(const MoveStorageJob &job) const
|
||||||
{
|
{
|
||||||
const InfoHash infoHash = job.torrentHandle.info_hash();
|
const InfoHash infoHash = job.torrentHandle.info_hash();
|
||||||
const TorrentImpl *torrent = m_torrents.value(infoHash);
|
const TorrentImpl *torrent = m_torrents.value(infoHash);
|
||||||
const QString torrentName = (torrent ? torrent->name() : QString {infoHash});
|
const QString torrentName = (torrent ? torrent->name() : infoHash.toString());
|
||||||
LogMsg(tr("Moving \"%1\" to \"%2\"...").arg(torrentName, job.path));
|
LogMsg(tr("Moving \"%1\" to \"%2\"...").arg(torrentName, job.path));
|
||||||
|
|
||||||
job.torrentHandle.move_storage(job.path.toUtf8().constData()
|
job.torrentHandle.move_storage(job.path.toUtf8().constData()
|
||||||
|
@ -4583,7 +4583,7 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle)
|
||||||
{
|
{
|
||||||
// Backup torrent file
|
// Backup torrent file
|
||||||
const QDir resumeDataDir {m_resumeFolderPath};
|
const QDir resumeDataDir {m_resumeFolderPath};
|
||||||
const QString torrentFileName {QString {"%1.torrent"}.arg(torrent->hash())};
|
const QString torrentFileName {QString::fromLatin1("%1.torrent").arg(torrent->hash().toString())};
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
torrent->info().saveToFile(resumeDataDir.absoluteFilePath(torrentFileName));
|
torrent->info().saveToFile(resumeDataDir.absoluteFilePath(torrentFileName));
|
||||||
|
@ -4921,7 +4921,7 @@ void Session::handleStorageMovedAlert(const lt::storage_moved_alert *p)
|
||||||
|
|
||||||
const InfoHash infoHash = currentJob.torrentHandle.info_hash();
|
const InfoHash infoHash = currentJob.torrentHandle.info_hash();
|
||||||
TorrentImpl *torrent = m_torrents.value(infoHash);
|
TorrentImpl *torrent = m_torrents.value(infoHash);
|
||||||
const QString torrentName = (torrent ? torrent->name() : QString {infoHash});
|
const QString torrentName = (torrent ? torrent->name() : infoHash.toString());
|
||||||
LogMsg(tr("\"%1\" is successfully moved to \"%2\".").arg(torrentName, newPath));
|
LogMsg(tr("\"%1\" is successfully moved to \"%2\".").arg(torrentName, newPath));
|
||||||
|
|
||||||
handleMoveTorrentStorageJobFinished();
|
handleMoveTorrentStorageJobFinished();
|
||||||
|
@ -4936,7 +4936,7 @@ void Session::handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert
|
||||||
|
|
||||||
const InfoHash infoHash = currentJob.torrentHandle.info_hash();
|
const InfoHash infoHash = currentJob.torrentHandle.info_hash();
|
||||||
TorrentImpl *torrent = m_torrents.value(infoHash);
|
TorrentImpl *torrent = m_torrents.value(infoHash);
|
||||||
const QString torrentName = (torrent ? torrent->name() : QString {infoHash});
|
const QString torrentName = (torrent ? torrent->name() : infoHash.toString());
|
||||||
const QString currentLocation = QString::fromStdString(p->handle.status(lt::torrent_handle::query_save_path).save_path);
|
const QString currentLocation = QString::fromStdString(p->handle.status(lt::torrent_handle::query_save_path).save_path);
|
||||||
const QString errorMessage = QString::fromStdString(p->message());
|
const QString errorMessage = QString::fromStdString(p->message());
|
||||||
LogMsg(tr("Failed to move \"%1\" from \"%2\" to \"%3\". Reason: %4.")
|
LogMsg(tr("Failed to move \"%1\" from \"%2\" to \"%3\". Reason: %4.")
|
||||||
|
|
|
@ -185,7 +185,7 @@ QString TorrentImpl::name() const
|
||||||
if (!name.isEmpty())
|
if (!name.isEmpty())
|
||||||
return name;
|
return name;
|
||||||
|
|
||||||
return m_hash;
|
return m_hash.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QDateTime TorrentImpl::creationDate() const
|
QDateTime TorrentImpl::creationDate() const
|
||||||
|
|
|
@ -295,7 +295,7 @@ void Tracker::processAnnounceRequest()
|
||||||
if (infoHashIter == queryParams.end())
|
if (infoHashIter == queryParams.end())
|
||||||
throw TrackerError("Missing \"info_hash\" parameter");
|
throw TrackerError("Missing \"info_hash\" parameter");
|
||||||
|
|
||||||
const InfoHash infoHash(infoHashIter->toHex());
|
const auto infoHash = InfoHash::fromString(infoHashIter->toHex());
|
||||||
if (!infoHash.isValid())
|
if (!infoHash.isValid())
|
||||||
throw TrackerError("Invalid \"info_hash\" parameter");
|
throw TrackerError("Invalid \"info_hash\" parameter");
|
||||||
|
|
||||||
|
|
121
src/base/digest32.h
Normal file
121
src/base/digest32.h
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2015, 2021 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
*
|
||||||
|
* 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 <libtorrent/sha1_hash.hpp>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
template <int N>
|
||||||
|
class Digest32
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using UnderlyingType = lt::digest32<N>;
|
||||||
|
|
||||||
|
Digest32() = default;
|
||||||
|
Digest32(const Digest32 &other) = default;
|
||||||
|
|
||||||
|
Digest32(const UnderlyingType &nativeDigest)
|
||||||
|
: m_valid {true}
|
||||||
|
, m_nativeDigest {nativeDigest}
|
||||||
|
{
|
||||||
|
const QByteArray raw = QByteArray::fromRawData(nativeDigest.data(), length());
|
||||||
|
m_hashString = QString::fromLatin1(raw.toHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int length()
|
||||||
|
{
|
||||||
|
return UnderlyingType::size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const
|
||||||
|
{
|
||||||
|
return m_valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
operator UnderlyingType() const
|
||||||
|
{
|
||||||
|
return m_nativeDigest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Digest32 fromString(const QString &digestString)
|
||||||
|
{
|
||||||
|
if (digestString.size() != (length() * 2))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const QByteArray raw = QByteArray::fromHex(digestString.toLatin1());
|
||||||
|
if (raw.size() != length()) // QByteArray::fromHex() will skip over invalid characters
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Digest32 result;
|
||||||
|
result.m_valid = true;
|
||||||
|
result.m_hashString = digestString;
|
||||||
|
result.m_nativeDigest.assign(raw.constData());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString toString() const
|
||||||
|
{
|
||||||
|
return m_hashString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_valid = false;
|
||||||
|
UnderlyingType m_nativeDigest;
|
||||||
|
QString m_hashString;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <int N>
|
||||||
|
bool operator==(const Digest32<N> &left, const Digest32<N> &right)
|
||||||
|
{
|
||||||
|
return (static_cast<typename Digest32<N>::UnderlyingType>(left)
|
||||||
|
== static_cast<typename Digest32<N>::UnderlyingType>(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <int N>
|
||||||
|
bool operator!=(const Digest32<N> &left, const Digest32<N> &right)
|
||||||
|
{
|
||||||
|
return !(left == right);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <int N>
|
||||||
|
bool operator<(const Digest32<N> &left, const Digest32<N> &right)
|
||||||
|
{
|
||||||
|
return static_cast<typename Digest32<N>::UnderlyingType>(left)
|
||||||
|
< static_cast<typename Digest32<N>::UnderlyingType>(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <int N>
|
||||||
|
uint qHash(const Digest32<N> &key, const uint seed)
|
||||||
|
{
|
||||||
|
return ::qHash(std::hash<typename Digest32<N>::UnderlyingType>()(key), seed);
|
||||||
|
}
|
|
@ -296,7 +296,7 @@ bool AddNewTorrentDialog::loadTorrentImpl()
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_ui->labelHashData->setText(infoHash);
|
m_ui->labelHashData->setText(infoHash.toString());
|
||||||
setupTreeview();
|
setupTreeview();
|
||||||
TMMChanged(m_ui->comboTTM->currentIndex());
|
TMMChanged(m_ui->comboTTM->currentIndex());
|
||||||
return true;
|
return true;
|
||||||
|
@ -348,7 +348,7 @@ bool AddNewTorrentDialog::loadMagnet(const BitTorrent::MagnetUri &magnetUri)
|
||||||
|
|
||||||
BitTorrent::Session::instance()->downloadMetadata(magnetUri);
|
BitTorrent::Session::instance()->downloadMetadata(magnetUri);
|
||||||
setMetadataProgressIndicator(true, tr("Retrieving metadata..."));
|
setMetadataProgressIndicator(true, tr("Retrieving metadata..."));
|
||||||
m_ui->labelHashData->setText(infoHash);
|
m_ui->labelHashData->setText(infoHash.toString());
|
||||||
|
|
||||||
m_magnetURI = magnetUri;
|
m_magnetURI = magnetUri;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -313,7 +313,7 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent)
|
||||||
// Save path
|
// Save path
|
||||||
updateSavePath(m_torrent);
|
updateSavePath(m_torrent);
|
||||||
// Hash
|
// Hash
|
||||||
m_ui->labelHashVal->setText(m_torrent->hash());
|
m_ui->labelHashVal->setText(m_torrent->hash().toString());
|
||||||
m_propListModel->model()->clear();
|
m_propListModel->model()->clear();
|
||||||
if (m_torrent->hasMetadata())
|
if (m_torrent->hasMetadata())
|
||||||
{
|
{
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
#include "base/torrentfilter.h"
|
#include "base/torrentfilter.h"
|
||||||
|
|
||||||
namespace BitTorrent
|
namespace BitTorrent
|
||||||
|
|
|
@ -496,7 +496,7 @@ void TransferListWidget::copySelectedHashes() const
|
||||||
{
|
{
|
||||||
QStringList torrentHashes;
|
QStringList torrentHashes;
|
||||||
for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
|
for (BitTorrent::Torrent *const torrent : asConst(getSelectedTorrents()))
|
||||||
torrentHashes << torrent->hash();
|
torrentHashes << torrent->hash().toString();
|
||||||
|
|
||||||
qApp->clipboard()->setText(torrentHashes.join('\n'));
|
qApp->clipboard()->setText(torrentHashes.join('\n'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent)
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{KEY_TORRENT_HASH, QString(torrent.hash())},
|
{KEY_TORRENT_HASH, QString(torrent.hash().toString())},
|
||||||
{KEY_TORRENT_NAME, torrent.name()},
|
{KEY_TORRENT_NAME, torrent.name()},
|
||||||
{KEY_TORRENT_MAGNET_URI, torrent.createMagnetURI()},
|
{KEY_TORRENT_MAGNET_URI, torrent.createMagnetURI()},
|
||||||
{KEY_TORRENT_SIZE, torrent.wantedSize()},
|
{KEY_TORRENT_SIZE, torrent.wantedSize()},
|
||||||
|
|
|
@ -471,7 +471,7 @@ void SyncController::maindataAction()
|
||||||
if (iterTorrents != lastResponse.end())
|
if (iterTorrents != lastResponse.end())
|
||||||
{
|
{
|
||||||
const QVariantHash lastResponseTorrents = iterTorrents->toHash();
|
const QVariantHash lastResponseTorrents = iterTorrents->toHash();
|
||||||
const auto iterHash = lastResponseTorrents.find(torrentHash);
|
const auto iterHash = lastResponseTorrents.find(torrentHash.toString());
|
||||||
|
|
||||||
if (iterHash != lastResponseTorrents.end())
|
if (iterHash != lastResponseTorrents.end())
|
||||||
{
|
{
|
||||||
|
@ -488,9 +488,9 @@ void SyncController::maindataAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers()))
|
for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers()))
|
||||||
trackers[tracker.url()] << torrentHash;
|
trackers[tracker.url()] << torrentHash.toString();
|
||||||
|
|
||||||
torrents[torrentHash] = map;
|
torrents[torrentHash.toString()] = map;
|
||||||
}
|
}
|
||||||
data["torrents"] = torrents;
|
data["torrents"] = torrents;
|
||||||
|
|
||||||
|
@ -541,7 +541,7 @@ void SyncController::torrentPeersAction()
|
||||||
auto lastResponse = sessionManager()->session()->getData(QLatin1String("syncTorrentPeersLastResponse")).toMap();
|
auto lastResponse = sessionManager()->session()->getData(QLatin1String("syncTorrentPeersLastResponse")).toMap();
|
||||||
auto lastAcceptedResponse = sessionManager()->session()->getData(QLatin1String("syncTorrentPeersLastAcceptedResponse")).toMap();
|
auto lastAcceptedResponse = sessionManager()->session()->getData(QLatin1String("syncTorrentPeersLastAcceptedResponse")).toMap();
|
||||||
|
|
||||||
const QString hash {params()["hash"]};
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
const BitTorrent::Torrent *torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
const BitTorrent::Torrent *torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
|
|
@ -127,8 +127,9 @@ namespace
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
for (const QString &hash : hashes)
|
for (const QString &hashString : hashes)
|
||||||
{
|
{
|
||||||
|
const auto hash = BitTorrent::InfoHash::fromString(hashString);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (torrent)
|
if (torrent)
|
||||||
func(torrent);
|
func(torrent);
|
||||||
|
@ -213,7 +214,7 @@ namespace
|
||||||
QVector<BitTorrent::InfoHash> infoHashes;
|
QVector<BitTorrent::InfoHash> infoHashes;
|
||||||
infoHashes.reserve(hashes.size());
|
infoHashes.reserve(hashes.size());
|
||||||
for (const QString &hash : hashes)
|
for (const QString &hash : hashes)
|
||||||
infoHashes << hash;
|
infoHashes << BitTorrent::InfoHash::fromString(hash);
|
||||||
return infoHashes;
|
return infoHashes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +260,7 @@ void TorrentsController::infoAction()
|
||||||
|
|
||||||
InfoHashSet hashSet;
|
InfoHashSet hashSet;
|
||||||
for (const QString &hash : hashes)
|
for (const QString &hash : hashes)
|
||||||
hashSet.insert(BitTorrent::InfoHash {hash});
|
hashSet.insert(BitTorrent::InfoHash::fromString(hash));
|
||||||
|
|
||||||
const TorrentFilter torrentFilter(filter, (hashes.isEmpty() ? TorrentFilter::AnyHash : hashSet), category);
|
const TorrentFilter torrentFilter(filter, (hashes.isEmpty() ? TorrentFilter::AnyHash : hashSet), category);
|
||||||
QVariantList torrentList;
|
QVariantList torrentList;
|
||||||
|
@ -371,7 +372,7 @@ void TorrentsController::propertiesAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash"});
|
requireParams({"hash"});
|
||||||
|
|
||||||
const QString hash {params()["hash"]};
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -442,7 +443,7 @@ void TorrentsController::trackersAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash"});
|
requireParams({"hash"});
|
||||||
|
|
||||||
const QString hash {params()["hash"]};
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -478,7 +479,7 @@ void TorrentsController::webseedsAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash"});
|
requireParams({"hash"});
|
||||||
|
|
||||||
const QString hash {params()["hash"]};
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -509,7 +510,7 @@ void TorrentsController::filesAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash"});
|
requireParams({"hash"});
|
||||||
|
|
||||||
const QString hash {params()["hash"]};
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -555,7 +556,7 @@ void TorrentsController::pieceHashesAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash"});
|
requireParams({"hash"});
|
||||||
|
|
||||||
const QString hash {params()["hash"]};
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -577,7 +578,7 @@ void TorrentsController::pieceStatesAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash"});
|
requireParams({"hash"});
|
||||||
|
|
||||||
const QString hash {params()["hash"]};
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -683,7 +684,7 @@ void TorrentsController::addTrackersAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash", "urls"});
|
requireParams({"hash", "urls"});
|
||||||
|
|
||||||
const QString hash = params()["hash"];
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -702,7 +703,7 @@ void TorrentsController::editTrackerAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash", "origUrl", "newUrl"});
|
requireParams({"hash", "origUrl", "newUrl"});
|
||||||
|
|
||||||
const QString hash = params()["hash"];
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
const QString origUrl = params()["origUrl"];
|
const QString origUrl = params()["origUrl"];
|
||||||
const QString newUrl = params()["newUrl"];
|
const QString newUrl = params()["newUrl"];
|
||||||
|
|
||||||
|
@ -745,7 +746,7 @@ void TorrentsController::removeTrackersAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash", "urls"});
|
requireParams({"hash", "urls"});
|
||||||
|
|
||||||
const QString hash = params()["hash"];
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -798,7 +799,7 @@ void TorrentsController::addPeersAction()
|
||||||
return torrent->connectPeer(peer);
|
return torrent->connectPeer(peer);
|
||||||
});
|
});
|
||||||
|
|
||||||
results[torrent->hash()] = QJsonObject
|
results[torrent->hash().toString()] = QJsonObject
|
||||||
{
|
{
|
||||||
{"added", peersAdded},
|
{"added", peersAdded},
|
||||||
{"failed", (peers.size() - peersAdded)}
|
{"failed", (peers.size() - peersAdded)}
|
||||||
|
@ -828,7 +829,7 @@ void TorrentsController::filePrioAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash", "id", "priority"});
|
requireParams({"hash", "id", "priority"});
|
||||||
|
|
||||||
const QString hash = params()["hash"];
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
const auto priority = static_cast<BitTorrent::DownloadPriority>(params()["priority"].toInt(&ok));
|
const auto priority = static_cast<BitTorrent::DownloadPriority>(params()["priority"].toInt(&ok));
|
||||||
if (!ok)
|
if (!ok)
|
||||||
|
@ -874,7 +875,7 @@ void TorrentsController::uploadLimitAction()
|
||||||
for (const QString &hash : hashes)
|
for (const QString &hash : hashes)
|
||||||
{
|
{
|
||||||
int limit = -1;
|
int limit = -1;
|
||||||
const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(BitTorrent::InfoHash::fromString(hash));
|
||||||
if (torrent)
|
if (torrent)
|
||||||
limit = torrent->uploadLimit();
|
limit = torrent->uploadLimit();
|
||||||
map[hash] = limit;
|
map[hash] = limit;
|
||||||
|
@ -892,7 +893,7 @@ void TorrentsController::downloadLimitAction()
|
||||||
for (const QString &hash : hashes)
|
for (const QString &hash : hashes)
|
||||||
{
|
{
|
||||||
int limit = -1;
|
int limit = -1;
|
||||||
const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
const BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(BitTorrent::InfoHash::fromString(hash));
|
||||||
if (torrent)
|
if (torrent)
|
||||||
limit = torrent->downloadLimit();
|
limit = torrent->downloadLimit();
|
||||||
map[hash] = limit;
|
map[hash] = limit;
|
||||||
|
@ -1064,7 +1065,7 @@ void TorrentsController::renameAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash", "name"});
|
requireParams({"hash", "name"});
|
||||||
|
|
||||||
const QString hash = params()["hash"];
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
QString name = params()["name"].trimmed();
|
QString name = params()["name"].trimmed();
|
||||||
|
|
||||||
if (name.isEmpty())
|
if (name.isEmpty())
|
||||||
|
@ -1251,7 +1252,7 @@ void TorrentsController::renameFileAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash", "oldPath", "newPath"});
|
requireParams({"hash", "oldPath", "newPath"});
|
||||||
|
|
||||||
const QString hash = params()["hash"];
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
@ -1273,7 +1274,7 @@ void TorrentsController::renameFolderAction()
|
||||||
{
|
{
|
||||||
requireParams({"hash", "oldPath", "newPath"});
|
requireParams({"hash", "oldPath", "newPath"});
|
||||||
|
|
||||||
const QString hash = params()["hash"];
|
const auto hash = BitTorrent::InfoHash::fromString(params()["hash"]);
|
||||||
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
|
||||||
if (!torrent)
|
if (!torrent)
|
||||||
throw APIError(APIErrorType::NotFound);
|
throw APIError(APIErrorType::NotFound);
|
||||||
|
|
Loading…
Reference in a new issue