diff --git a/src/base/bittorrent/infohash.cpp b/src/base/bittorrent/infohash.cpp index 7fa78b233..36e3fa271 100644 --- a/src/base/bittorrent/infohash.cpp +++ b/src/base/bittorrent/infohash.cpp @@ -28,6 +28,8 @@ #include "infohash.h" +#include + const int TorrentIDTypeId = qRegisterMetaType(); BitTorrent::InfoHash::InfoHash(const WrappedType &nativeHash) diff --git a/src/base/bittorrent/infohash.h b/src/base/bittorrent/infohash.h index f6f3101a1..2953b9fcf 100644 --- a/src/base/bittorrent/infohash.h +++ b/src/base/bittorrent/infohash.h @@ -33,7 +33,6 @@ #endif #include -#include #include #include "base/digest32.h" diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index 453013233..cb5b78bd8 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -34,6 +34,7 @@ #include #include +#include "base/3rdparty/expected.hpp" #include "base/pathfwd.h" #include "base/tagset.h" #include "abstractfilestorage.h" @@ -300,6 +301,7 @@ namespace BitTorrent virtual void clearPeers() = 0; virtual QString createMagnetURI() const = 0; + virtual nonstd::expected exportToFile(const Path &path) const = 0; TorrentID id() const; bool isResumed() const; diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 76c4bdfdd..7417e1aee 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -53,6 +54,7 @@ #include "base/logger.h" #include "base/preferences.h" #include "base/utils/fs.h" +#include "base/utils/io.h" #include "base/utils/string.h" #include "common.h" #include "downloadpriority.h" @@ -2232,6 +2234,37 @@ QString TorrentImpl::createMagnetURI() const return QString::fromStdString(lt::make_magnet_uri(m_nativeHandle)); } +nonstd::expected TorrentImpl::exportToFile(const Path &path) const +{ + if (!hasMetadata()) + return nonstd::make_unexpected(tr("Missing metadata")); + + try + { +#ifdef QBT_USES_LIBTORRENT2 + const std::shared_ptr completeTorrentInfo = m_nativeHandle.torrent_file_with_hashes(); + const std::shared_ptr torrentInfo = {completeTorrentInfo ? completeTorrentInfo : info().nativeInfo()}; +#else + const std::shared_ptr torrentInfo = info().nativeInfo(); +#endif + auto creator = lt::create_torrent(*torrentInfo); + + for (const TrackerEntry &entry : asConst(trackers())) + creator.add_tracker(entry.url.toStdString(), entry.tier); + + const lt::entry torrentEntry = creator.generate(); + const nonstd::expected result = Utils::IO::saveToFile(path, torrentEntry); + if (!result) + return result.get_unexpected(); + } + catch (const lt::system_error &err) + { + return nonstd::make_unexpected(QString::fromLocal8Bit(err.what())); + } + + return {}; +} + void TorrentImpl::prioritizeFiles(const QVector &priorities) { if (!hasMetadata()) return; diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index 84918a65a..d7cb894de 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -227,6 +227,7 @@ namespace BitTorrent void clearPeers() override; QString createMagnetURI() const override; + nonstd::expected exportToFile(const Path &path) const; bool needSaveResumeData() const; diff --git a/src/base/bittorrent/torrentinfo.cpp b/src/base/bittorrent/torrentinfo.cpp index 71ec11486..cd2954280 100644 --- a/src/base/bittorrent/torrentinfo.cpp +++ b/src/base/bittorrent/torrentinfo.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include "base/global.h" #include "base/path.h" diff --git a/src/base/bittorrent/torrentinfo.h b/src/base/bittorrent/torrentinfo.h index a357c4e2e..0d4127cec 100644 --- a/src/base/bittorrent/torrentinfo.h +++ b/src/base/bittorrent/torrentinfo.h @@ -32,6 +32,7 @@ #include #include +#include #include "base/3rdparty/expected.hpp" #include "base/indexrange.h" diff --git a/src/gui/transferlistwidget.cpp b/src/gui/transferlistwidget.cpp index a55c22d28..f7da93b50 100644 --- a/src/gui/transferlistwidget.cpp +++ b/src/gui/transferlistwidget.cpp @@ -47,6 +47,7 @@ #include "base/bittorrent/trackerentry.h" #include "base/global.h" #include "base/logger.h" +#include "base/path.h" #include "base/preferences.h" #include "base/torrentfilter.h" #include "base/utils/compare.h" @@ -757,6 +758,57 @@ void TransferListWidget::editTorrentTrackers() trackerDialog->open(); } +void TransferListWidget::exportTorrent() +{ + if (getSelectedTorrents().isEmpty()) + return; + + auto fileDialog = new QFileDialog(this, tr("Choose folder to save exported .torrent files")); + fileDialog->setAttribute(Qt::WA_DeleteOnClose); + fileDialog->setFileMode(QFileDialog::Directory); + fileDialog->setOptions(QFileDialog::ShowDirsOnly); + connect(fileDialog, &QFileDialog::fileSelected, this, [this](const QString &dir) + { + const QVector torrents = getSelectedTorrents(); + if (torrents.isEmpty()) + return; + + const Path savePath {dir}; + if (!savePath.exists()) + return; + + const QString errorMsg = tr("Export .torrent file failed. Torrent: \"%1\". Save path: \"%2\". Reason: \"%3\""); + + bool hasError = false; + for (const BitTorrent::Torrent *torrent : torrents) + { + const Path filePath = savePath / Path(torrent->name() + u".torrent"); + if (filePath.exists()) + { + LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), tr("A file with the same name already exists")) , Log::WARNING); + hasError = true; + continue; + } + + const nonstd::expected result = torrent->exportToFile(filePath); + if (!result) + { + LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), result.error()) , Log::WARNING); + hasError = true; + continue; + } + } + + if (hasError) + { + QMessageBox::warning(this, tr("Export .torrent file error") + , tr("Errors occured when exporting .torrent files. Check execution log for details.")); + } + }); + + fileDialog->open(); +} + void TransferListWidget::confirmRemoveAllTagsForSelection() { QMessageBox::StandardButton response = QMessageBox::question( @@ -906,6 +958,8 @@ void TransferListWidget::displayListMenu() connect(actionAutoTMM, &QAction::triggered, this, &TransferListWidget::setSelectedAutoTMMEnabled); auto *actionEditTracker = new QAction(UIThemeManager::instance()->getIcon(u"edit-rename"_qs), tr("Edit trackers..."), listMenu); connect(actionEditTracker, &QAction::triggered, this, &TransferListWidget::editTorrentTrackers); + auto *actionExportTorrent = new QAction(UIThemeManager::instance()->getIcon(u"edit-copy"_qs), tr("Export .torrent..."), listMenu); + connect(actionExportTorrent, &QAction::triggered, this, &TransferListWidget::exportTorrent); // End of actions // Enable/disable pause/start action given the DL state @@ -1161,8 +1215,7 @@ void TransferListWidget::displayListMenu() queueMenu->addAction(actionBottomQueuePos); } - QMenu *copySubMenu = listMenu->addMenu( - UIThemeManager::instance()->getIcon(u"edit-copy"_qs), tr("Copy")); + QMenu *copySubMenu = listMenu->addMenu(UIThemeManager::instance()->getIcon(u"edit-copy"_qs), tr("Copy")); copySubMenu->addAction(actionCopyName); copySubMenu->addAction(actionCopyHash1); actionCopyHash1->setEnabled(hasInfohashV1); @@ -1171,6 +1224,9 @@ void TransferListWidget::displayListMenu() copySubMenu->addAction(actionCopyMagnetLink); copySubMenu->addAction(actionCopyID); + actionExportTorrent->setToolTip(u"Exported torrent is not necessarily the same as the imported"_qs); + listMenu->addAction(actionExportTorrent); + listMenu->popup(QCursor::pos()); } diff --git a/src/gui/transferlistwidget.h b/src/gui/transferlistwidget.h index 8ead6b5d5..c30e09c20 100644 --- a/src/gui/transferlistwidget.h +++ b/src/gui/transferlistwidget.h @@ -34,9 +34,9 @@ #include #include "base/bittorrent/infohash.h" -#include "base/path.h" class MainWindow; +class Path; class TransferListModel; class TransferListSortModel; @@ -124,6 +124,7 @@ private: QVector getSelectedTorrents() const; void askAddTagsForSelection(); void editTorrentTrackers(); + void exportTorrent(); void confirmRemoveAllTagsForSelection(); QStringList askTagsForSelection(const QString &dialogTitle); void applyToSelectedTorrents(const std::function &fn);