2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2024 Radu Carpa <radu.carpa@cern.ch>
5 * Copyright (C) 2017 Mike Tzou (Chocobo1)
6 * Copyright (C) 2010 Christophe Dumez <chris@qbittorrent.org>
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 * In addition, as a special exception, the copyright holders give permission to
23 * link this program with the OpenSSL project's "OpenSSL" library (or with
24 * modified versions of it that use the same license as the "OpenSSL" library),
25 * and distribute the linked executables. You must obey the GNU General Public
26 * License in all respects for all of the code used other than "OpenSSL". If you
27 * modify file(s), you may extend this exception to your version of the file(s),
28 * but you are not obligated to do so. If you do not wish to do so, delete this
29 * exception statement from your version.
32 #include "torrentcreatordialog.h"
34 #include <QCloseEvent>
35 #include <QFileDialog>
36 #include <QMessageBox>
40 #include "base/bittorrent/session.h"
41 #include "base/bittorrent/torrentdescriptor.h"
42 #include "base/global.h"
43 #include "base/utils/fs.h"
44 #include "base/utils/misc.h"
45 #include "ui_torrentcreatordialog.h"
48 #define SETTINGS_KEY(name) u"TorrentCreator/" name
52 // When the root file/directory of the created torrent is a symlink, we want to keep the symlink name in the torrent.
53 // On Windows, however, QFileDialog::DontResolveSymlinks disables shortcuts (.lnk files) expansion, making it impossible to pick a file if its path contains a shortcut.
54 // As of NTFS symlinks, they don't seem to be resolved anyways.
56 const QFileDialog::Options FILE_DIALOG_OPTIONS
{QFileDialog::DontResolveSymlinks
};
58 const QFileDialog::Options FILE_DIALOG_OPTIONS
{};
62 TorrentCreatorDialog::TorrentCreatorDialog(QWidget
*parent
, const Path
&defaultPath
)
64 , m_ui(new Ui::TorrentCreatorDialog
)
66 , m_storeDialogSize(SETTINGS_KEY(u
"Size"_s
))
67 , m_storePieceSize(SETTINGS_KEY(u
"PieceSize"_s
))
68 , m_storePrivateTorrent(SETTINGS_KEY(u
"PrivateTorrent"_s
))
69 , m_storeStartSeeding(SETTINGS_KEY(u
"StartSeeding"_s
))
70 , m_storeIgnoreRatio(SETTINGS_KEY(u
"IgnoreRatio"_s
))
71 #ifdef QBT_USES_LIBTORRENT2
72 , m_storeTorrentFormat(SETTINGS_KEY(u
"TorrentFormat"_s
))
74 , m_storeOptimizeAlignment(SETTINGS_KEY(u
"OptimizeAlignment"_s
))
75 , m_paddedFileSizeLimit(SETTINGS_KEY(u
"PaddedFileSizeLimit"_s
))
77 , m_storeLastAddPath(SETTINGS_KEY(u
"LastAddPath"_s
))
78 , m_storeTrackerList(SETTINGS_KEY(u
"TrackerList"_s
))
79 , m_storeWebSeedList(SETTINGS_KEY(u
"WebSeedList"_s
))
80 , m_storeComments(SETTINGS_KEY(u
"Comments"_s
))
81 , m_storeLastSavePath(SETTINGS_KEY(u
"LastSavePath"_s
))
82 , m_storeSource(SETTINGS_KEY(u
"Source"_s
))
86 m_ui
->comboPieceSize
->addItem(tr("Auto"), 0);
87 for (int i
= 4; i
<= 17; ++i
)
89 const int size
= 1024 << i
;
90 const QString displaySize
= Utils::Misc::friendlyUnit(size
, false, 0);
91 m_ui
->comboPieceSize
->addItem(displaySize
, size
);
94 m_ui
->buttonBox
->button(QDialogButtonBox::Ok
)->setText(tr("Create Torrent"));
95 m_ui
->textInputPath
->setMode(FileSystemPathEdit::Mode::ReadOnly
);
97 connect(m_ui
->addFileButton
, &QPushButton::clicked
, this, &TorrentCreatorDialog::onAddFileButtonClicked
);
98 connect(m_ui
->addFolderButton
, &QPushButton::clicked
, this, &TorrentCreatorDialog::onAddFolderButtonClicked
);
99 connect(m_ui
->buttonBox
, &QDialogButtonBox::accepted
, this, &TorrentCreatorDialog::onCreateButtonClicked
);
100 connect(m_ui
->buttonBox
, &QDialogButtonBox::rejected
, this, &QDialog::reject
);
101 connect(m_ui
->buttonCalcTotalPieces
, &QPushButton::clicked
, this, &TorrentCreatorDialog::updatePiecesCount
);
102 connect(m_ui
->checkStartSeeding
, &QCheckBox::clicked
, m_ui
->checkIgnoreShareLimits
, &QWidget::setEnabled
);
105 updateInputPath(defaultPath
);
107 m_threadPool
.setMaxThreadCount(1);
109 #ifdef QBT_USES_LIBTORRENT2
110 m_ui
->checkOptimizeAlignment
->hide();
112 m_ui
->widgetTorrentFormat
->hide();
116 TorrentCreatorDialog::~TorrentCreatorDialog()
123 void TorrentCreatorDialog::updateInputPath(const Path
&path
)
125 if (path
.isEmpty()) return;
126 m_ui
->textInputPath
->setSelectedPath(path
);
127 updateProgressBar(0);
130 void TorrentCreatorDialog::onAddFolderButtonClicked()
132 const QString oldPath
= m_ui
->textInputPath
->selectedPath().data();
133 const Path path
{QFileDialog::getExistingDirectory(this, tr("Select folder")
134 , oldPath
, (QFileDialog::ShowDirsOnly
| FILE_DIALOG_OPTIONS
))};
135 updateInputPath(path
);
138 void TorrentCreatorDialog::onAddFileButtonClicked()
140 const QString oldPath
= m_ui
->textInputPath
->selectedPath().data();
141 const Path path
{QFileDialog::getOpenFileName(this, tr("Select file"), oldPath
, QString(), nullptr, FILE_DIALOG_OPTIONS
)};
142 updateInputPath(path
);
145 int TorrentCreatorDialog::getPieceSize() const
147 return m_ui
->comboPieceSize
->currentData().toInt();
150 #ifdef QBT_USES_LIBTORRENT2
151 BitTorrent::TorrentFormat
TorrentCreatorDialog::getTorrentFormat() const
153 switch (m_ui
->comboTorrentFormat
->currentIndex())
156 return BitTorrent::TorrentFormat::V2
;
158 return BitTorrent::TorrentFormat::Hybrid
;
160 return BitTorrent::TorrentFormat::V1
;
162 return BitTorrent::TorrentFormat::Hybrid
;
165 int TorrentCreatorDialog::getPaddedFileSizeLimit() const
167 const int value
= m_ui
->spinPaddedFileSizeLimit
->value();
168 return ((value
>= 0) ? (value
* 1024) : -1);
172 void TorrentCreatorDialog::dropEvent(QDropEvent
*event
)
174 event
->acceptProposedAction();
176 if (event
->mimeData()->hasUrls())
178 // only take the first one
179 const QUrl firstItem
= event
->mimeData()->urls().first();
181 (firstItem
.scheme().compare(u
"file", Qt::CaseInsensitive
) == 0)
182 ? firstItem
.toLocalFile() : firstItem
.toString()
184 updateInputPath(path
);
188 void TorrentCreatorDialog::dragEnterEvent(QDragEnterEvent
*event
)
190 if (event
->mimeData()->hasFormat(u
"text/plain"_s
) || event
->mimeData()->hasFormat(u
"text/uri-list"_s
))
191 event
->acceptProposedAction();
194 // Main function that create a .torrent file
195 void TorrentCreatorDialog::onCreateButtonClicked()
198 // Resolve the path in case it contains a shortcut (otherwise, the following usages will consider it invalid)
199 const Path inputPath
= Utils::Fs::toCanonicalPath(m_ui
->textInputPath
->selectedPath());
201 const Path inputPath
= m_ui
->textInputPath
->selectedPath();
205 if (!Utils::Fs::isReadable(inputPath
))
207 QMessageBox::critical(this, tr("Torrent creation failed"), tr("Reason: Path to file/folder is not readable."));
212 const Path lastSavePath
= (m_storeLastSavePath
.get(Utils::Fs::homePath()) / Path(inputPath
.filename() + u
".torrent"));
213 Path destPath
{QFileDialog::getSaveFileName(this, tr("Select where to save the new torrent"), lastSavePath
.data(), tr("Torrent Files (*.torrent)"))};
214 if (destPath
.isEmpty())
216 if (!destPath
.hasExtension(TORRENT_FILE_EXTENSION
))
217 destPath
+= TORRENT_FILE_EXTENSION
;
218 m_storeLastSavePath
= destPath
.parentPath();
220 // Disable dialog & set busy cursor
221 setInteractionEnabled(false);
222 setCursor(QCursor(Qt::WaitCursor
));
224 const QStringList trackers
= m_ui
->trackersList
->toPlainText().trimmed()
225 .replace(QRegularExpression(u
"\n\n[\n]+"_s
), u
"\n\n"_s
).split(u
'\n');
226 const BitTorrent::TorrentCreatorParams params
228 .isPrivate
= m_ui
->checkPrivate
->isChecked(),
229 #ifdef QBT_USES_LIBTORRENT2
230 .torrentFormat
= getTorrentFormat(),
232 .isAlignmentOptimized
= m_ui
->checkOptimizeAlignment
->isChecked(),
233 .paddedFileSizeLimit
= getPaddedFileSizeLimit(),
235 .pieceSize
= getPieceSize(),
236 .sourcePath
= inputPath
,
237 .torrentFilePath
= destPath
,
238 .comment
= m_ui
->txtComment
->toPlainText(),
239 .source
= m_ui
->lineEditSource
->text(),
240 .trackers
= trackers
,
241 .urlSeeds
= m_ui
->URLSeedsList
->toPlainText().split(u
'\n', Qt::SkipEmptyParts
)
244 auto *torrentCreator
= new BitTorrent::TorrentCreator(params
);
245 connect(this, &QDialog::rejected
, torrentCreator
, &BitTorrent::TorrentCreator::requestInterruption
);
246 connect(torrentCreator
, &BitTorrent::TorrentCreator::creationSuccess
, this, &TorrentCreatorDialog::handleCreationSuccess
);
247 connect(torrentCreator
, &BitTorrent::TorrentCreator::creationFailure
, this, &TorrentCreatorDialog::handleCreationFailure
);
248 connect(torrentCreator
, &BitTorrent::TorrentCreator::progressUpdated
, this, &TorrentCreatorDialog::updateProgressBar
);
250 // run the torrentCreator in a thread
251 m_threadPool
.start(torrentCreator
);
254 void TorrentCreatorDialog::handleCreationFailure(const QString
&msg
)
256 // Remove busy cursor
257 setCursor(QCursor(Qt::ArrowCursor
));
258 QMessageBox::information(this, tr("Torrent creation failed"), msg
);
259 setInteractionEnabled(true);
262 void TorrentCreatorDialog::handleCreationSuccess(const BitTorrent::TorrentCreatorResult
&result
)
264 setCursor(QCursor(Qt::ArrowCursor
));
265 setInteractionEnabled(true);
267 QMessageBox::information(this, tr("Torrent creator")
268 , u
"%1\n%2"_s
.arg(tr("Torrent created:"), result
.torrentFilePath
.toString()));
270 if (m_ui
->checkStartSeeding
->isChecked())
272 if (const auto loadResult
= BitTorrent::TorrentDescriptor::loadFromFile(result
.torrentFilePath
))
274 BitTorrent::AddTorrentParams params
;
275 params
.savePath
= result
.savePath
;
276 params
.skipChecking
= true;
277 if (m_ui
->checkIgnoreShareLimits
->isChecked())
279 params
.ratioLimit
= BitTorrent::Torrent::NO_RATIO_LIMIT
;
280 params
.seedingTimeLimit
= BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT
;
281 params
.inactiveSeedingTimeLimit
= BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT
;
283 params
.useAutoTMM
= false; // otherwise if it is on by default, it will overwrite `savePath` to the default save path
285 BitTorrent::Session::instance()->addTorrent(loadResult
.value(), params
);
289 const QString message
= tr("Add torrent to transfer list failed.") + u
'\n' + tr("Reason: \"%1\"").arg(loadResult
.error());
290 QMessageBox::critical(this, tr("Add torrent failed"), message
);
295 void TorrentCreatorDialog::updateProgressBar(int progress
)
297 m_ui
->progressBar
->setValue(progress
);
300 void TorrentCreatorDialog::updatePiecesCount()
302 const Path path
= m_ui
->textInputPath
->selectedPath();
303 #ifdef QBT_USES_LIBTORRENT2
304 const int count
= BitTorrent::TorrentCreator::calculateTotalPieces(
305 path
, getPieceSize(), getTorrentFormat());
307 const bool isAlignmentOptimized
= m_ui
->checkOptimizeAlignment
->isChecked();
308 const int count
= BitTorrent::TorrentCreator::calculateTotalPieces(path
309 , getPieceSize(), isAlignmentOptimized
, getPaddedFileSizeLimit());
311 m_ui
->labelTotalPieces
->setText(QString::number(count
));
314 void TorrentCreatorDialog::setInteractionEnabled(const bool enabled
) const
316 m_ui
->textInputPath
->setEnabled(enabled
);
317 m_ui
->addFileButton
->setEnabled(enabled
);
318 m_ui
->addFolderButton
->setEnabled(enabled
);
319 m_ui
->trackersList
->setEnabled(enabled
);
320 m_ui
->URLSeedsList
->setEnabled(enabled
);
321 m_ui
->txtComment
->setEnabled(enabled
);
322 m_ui
->lineEditSource
->setEnabled(enabled
);
323 m_ui
->comboPieceSize
->setEnabled(enabled
);
324 m_ui
->buttonCalcTotalPieces
->setEnabled(enabled
);
325 m_ui
->checkPrivate
->setEnabled(enabled
);
326 m_ui
->checkStartSeeding
->setEnabled(enabled
);
327 m_ui
->buttonBox
->button(QDialogButtonBox::Ok
)->setEnabled(enabled
);
328 m_ui
->checkIgnoreShareLimits
->setEnabled(enabled
&& m_ui
->checkStartSeeding
->isChecked());
329 #ifdef QBT_USES_LIBTORRENT2
330 m_ui
->widgetTorrentFormat
->setEnabled(enabled
);
332 m_ui
->checkOptimizeAlignment
->setEnabled(enabled
);
333 m_ui
->spinPaddedFileSizeLimit
->setEnabled(enabled
);
337 void TorrentCreatorDialog::saveSettings()
339 m_storeLastAddPath
= m_ui
->textInputPath
->selectedPath();
341 m_storePieceSize
= m_ui
->comboPieceSize
->currentIndex();
342 m_storePrivateTorrent
= m_ui
->checkPrivate
->isChecked();
343 m_storeStartSeeding
= m_ui
->checkStartSeeding
->isChecked();
344 m_storeIgnoreRatio
= m_ui
->checkIgnoreShareLimits
->isChecked();
345 #ifdef QBT_USES_LIBTORRENT2
346 m_storeTorrentFormat
= m_ui
->comboTorrentFormat
->currentIndex();
348 m_storeOptimizeAlignment
= m_ui
->checkOptimizeAlignment
->isChecked();
349 m_paddedFileSizeLimit
= m_ui
->spinPaddedFileSizeLimit
->value();
352 m_storeTrackerList
= m_ui
->trackersList
->toPlainText();
353 m_storeWebSeedList
= m_ui
->URLSeedsList
->toPlainText();
354 m_storeComments
= m_ui
->txtComment
->toPlainText();
355 m_storeSource
= m_ui
->lineEditSource
->text();
357 m_storeDialogSize
= size();
360 void TorrentCreatorDialog::loadSettings()
362 m_ui
->textInputPath
->setSelectedPath(m_storeLastAddPath
.get(Utils::Fs::homePath()));
364 m_ui
->comboPieceSize
->setCurrentIndex(m_storePieceSize
);
365 m_ui
->checkPrivate
->setChecked(m_storePrivateTorrent
);
366 m_ui
->checkStartSeeding
->setChecked(m_storeStartSeeding
);
367 m_ui
->checkIgnoreShareLimits
->setChecked(m_storeIgnoreRatio
);
368 m_ui
->checkIgnoreShareLimits
->setEnabled(m_ui
->checkStartSeeding
->isChecked());
369 #ifdef QBT_USES_LIBTORRENT2
370 m_ui
->comboTorrentFormat
->setCurrentIndex(m_storeTorrentFormat
.get(1));
372 m_ui
->checkOptimizeAlignment
->setChecked(m_storeOptimizeAlignment
.get(true));
373 m_ui
->spinPaddedFileSizeLimit
->setValue(m_paddedFileSizeLimit
.get(-1));
376 m_ui
->trackersList
->setPlainText(m_storeTrackerList
);
377 m_ui
->URLSeedsList
->setPlainText(m_storeWebSeedList
);
378 m_ui
->txtComment
->setPlainText(m_storeComments
);
379 m_ui
->lineEditSource
->setText(m_storeSource
);
381 if (const QSize dialogSize
= m_storeDialogSize
; dialogSize
.isValid())