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 #ifdef QBT_USES_LIBTORRENT2
88 for (int i
= 4; i
<= 18; ++i
)
90 for (int i
= 4; i
<= 17; ++i
)
93 const int size
= 1024 << i
;
94 const QString displaySize
= Utils::Misc::friendlyUnit(size
, false, 0);
95 m_ui
->comboPieceSize
->addItem(displaySize
, size
);
98 m_ui
->buttonBox
->button(QDialogButtonBox::Ok
)->setText(tr("Create Torrent"));
99 m_ui
->textInputPath
->setMode(FileSystemPathEdit::Mode::ReadOnly
);
101 connect(m_ui
->addFileButton
, &QPushButton::clicked
, this, &TorrentCreatorDialog::onAddFileButtonClicked
);
102 connect(m_ui
->addFolderButton
, &QPushButton::clicked
, this, &TorrentCreatorDialog::onAddFolderButtonClicked
);
103 connect(m_ui
->buttonBox
, &QDialogButtonBox::accepted
, this, &TorrentCreatorDialog::onCreateButtonClicked
);
104 connect(m_ui
->buttonBox
, &QDialogButtonBox::rejected
, this, &QDialog::reject
);
105 connect(m_ui
->buttonCalcTotalPieces
, &QPushButton::clicked
, this, &TorrentCreatorDialog::updatePiecesCount
);
106 connect(m_ui
->checkStartSeeding
, &QCheckBox::clicked
, m_ui
->checkIgnoreShareLimits
, &QWidget::setEnabled
);
109 updateInputPath(defaultPath
);
111 m_threadPool
.setMaxThreadCount(1);
113 #ifdef QBT_USES_LIBTORRENT2
114 m_ui
->checkOptimizeAlignment
->hide();
116 m_ui
->widgetTorrentFormat
->hide();
120 TorrentCreatorDialog::~TorrentCreatorDialog()
127 void TorrentCreatorDialog::updateInputPath(const Path
&path
)
129 if (path
.isEmpty()) return;
130 m_ui
->textInputPath
->setSelectedPath(path
);
131 updateProgressBar(0);
134 void TorrentCreatorDialog::onAddFolderButtonClicked()
136 const QString oldPath
= m_ui
->textInputPath
->selectedPath().data();
137 const Path path
{QFileDialog::getExistingDirectory(this, tr("Select folder")
138 , oldPath
, (QFileDialog::ShowDirsOnly
| FILE_DIALOG_OPTIONS
))};
139 updateInputPath(path
);
142 void TorrentCreatorDialog::onAddFileButtonClicked()
144 const QString oldPath
= m_ui
->textInputPath
->selectedPath().data();
145 const Path path
{QFileDialog::getOpenFileName(this, tr("Select file"), oldPath
, QString(), nullptr, FILE_DIALOG_OPTIONS
)};
146 updateInputPath(path
);
149 int TorrentCreatorDialog::getPieceSize() const
151 return m_ui
->comboPieceSize
->currentData().toInt();
154 #ifdef QBT_USES_LIBTORRENT2
155 BitTorrent::TorrentFormat
TorrentCreatorDialog::getTorrentFormat() const
157 switch (m_ui
->comboTorrentFormat
->currentIndex())
160 return BitTorrent::TorrentFormat::V2
;
162 return BitTorrent::TorrentFormat::Hybrid
;
164 return BitTorrent::TorrentFormat::V1
;
166 return BitTorrent::TorrentFormat::Hybrid
;
169 int TorrentCreatorDialog::getPaddedFileSizeLimit() const
171 const int value
= m_ui
->spinPaddedFileSizeLimit
->value();
172 return ((value
>= 0) ? (value
* 1024) : -1);
176 void TorrentCreatorDialog::dropEvent(QDropEvent
*event
)
178 event
->acceptProposedAction();
180 if (event
->mimeData()->hasUrls())
182 // only take the first one
183 const QUrl firstItem
= event
->mimeData()->urls().first();
185 (firstItem
.scheme().compare(u
"file", Qt::CaseInsensitive
) == 0)
186 ? firstItem
.toLocalFile() : firstItem
.toString()
188 updateInputPath(path
);
192 void TorrentCreatorDialog::dragEnterEvent(QDragEnterEvent
*event
)
194 if (event
->mimeData()->hasFormat(u
"text/plain"_s
) || event
->mimeData()->hasFormat(u
"text/uri-list"_s
))
195 event
->acceptProposedAction();
198 // Main function that create a .torrent file
199 void TorrentCreatorDialog::onCreateButtonClicked()
202 // Resolve the path in case it contains a shortcut (otherwise, the following usages will consider it invalid)
203 const Path inputPath
= Utils::Fs::toCanonicalPath(m_ui
->textInputPath
->selectedPath());
205 const Path inputPath
= m_ui
->textInputPath
->selectedPath();
209 if (!Utils::Fs::isReadable(inputPath
))
211 QMessageBox::critical(this, tr("Torrent creation failed"), tr("Reason: Path to file/folder is not readable."));
216 const Path lastSavePath
= (m_storeLastSavePath
.get(Utils::Fs::homePath()) / Path(inputPath
.filename() + u
".torrent"));
217 Path destPath
{QFileDialog::getSaveFileName(this, tr("Select where to save the new torrent"), lastSavePath
.data(), tr("Torrent Files (*.torrent)"))};
218 if (destPath
.isEmpty())
220 if (!destPath
.hasExtension(TORRENT_FILE_EXTENSION
))
221 destPath
+= TORRENT_FILE_EXTENSION
;
222 m_storeLastSavePath
= destPath
.parentPath();
224 // Disable dialog & set busy cursor
225 setInteractionEnabled(false);
226 setCursor(QCursor(Qt::WaitCursor
));
228 const QStringList trackers
= m_ui
->trackersList
->toPlainText().trimmed()
229 .replace(QRegularExpression(u
"\n\n[\n]+"_s
), u
"\n\n"_s
).split(u
'\n');
230 const BitTorrent::TorrentCreatorParams params
232 .isPrivate
= m_ui
->checkPrivate
->isChecked(),
233 #ifdef QBT_USES_LIBTORRENT2
234 .torrentFormat
= getTorrentFormat(),
236 .isAlignmentOptimized
= m_ui
->checkOptimizeAlignment
->isChecked(),
237 .paddedFileSizeLimit
= getPaddedFileSizeLimit(),
239 .pieceSize
= getPieceSize(),
240 .sourcePath
= inputPath
,
241 .torrentFilePath
= destPath
,
242 .comment
= m_ui
->txtComment
->toPlainText(),
243 .source
= m_ui
->lineEditSource
->text(),
244 .trackers
= trackers
,
245 .urlSeeds
= m_ui
->URLSeedsList
->toPlainText().split(u
'\n', Qt::SkipEmptyParts
)
248 auto *torrentCreator
= new BitTorrent::TorrentCreator(params
);
249 connect(this, &QDialog::rejected
, torrentCreator
, &BitTorrent::TorrentCreator::requestInterruption
);
250 connect(torrentCreator
, &BitTorrent::TorrentCreator::creationSuccess
, this, &TorrentCreatorDialog::handleCreationSuccess
);
251 connect(torrentCreator
, &BitTorrent::TorrentCreator::creationFailure
, this, &TorrentCreatorDialog::handleCreationFailure
);
252 connect(torrentCreator
, &BitTorrent::TorrentCreator::progressUpdated
, this, &TorrentCreatorDialog::updateProgressBar
);
254 // run the torrentCreator in a thread
255 m_threadPool
.start(torrentCreator
);
258 void TorrentCreatorDialog::handleCreationFailure(const QString
&msg
)
260 // Remove busy cursor
261 setCursor(QCursor(Qt::ArrowCursor
));
262 QMessageBox::information(this, tr("Torrent creation failed"), msg
);
263 setInteractionEnabled(true);
266 void TorrentCreatorDialog::handleCreationSuccess(const BitTorrent::TorrentCreatorResult
&result
)
268 setCursor(QCursor(Qt::ArrowCursor
));
269 setInteractionEnabled(true);
271 QMessageBox::information(this, tr("Torrent creator")
272 , u
"%1\n%2"_s
.arg(tr("Torrent created:"), result
.torrentFilePath
.toString()));
274 if (m_ui
->checkStartSeeding
->isChecked())
276 if (const auto loadResult
= BitTorrent::TorrentDescriptor::loadFromFile(result
.torrentFilePath
))
278 BitTorrent::AddTorrentParams params
;
279 params
.savePath
= result
.savePath
;
280 params
.skipChecking
= true;
281 if (m_ui
->checkIgnoreShareLimits
->isChecked())
283 params
.ratioLimit
= BitTorrent::Torrent::NO_RATIO_LIMIT
;
284 params
.seedingTimeLimit
= BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT
;
285 params
.inactiveSeedingTimeLimit
= BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT
;
287 params
.useAutoTMM
= false; // otherwise if it is on by default, it will overwrite `savePath` to the default save path
289 BitTorrent::Session::instance()->addTorrent(loadResult
.value(), params
);
293 const QString message
= tr("Add torrent to transfer list failed.") + u
'\n' + tr("Reason: \"%1\"").arg(loadResult
.error());
294 QMessageBox::critical(this, tr("Add torrent failed"), message
);
299 void TorrentCreatorDialog::updateProgressBar(int progress
)
301 m_ui
->progressBar
->setValue(progress
);
304 void TorrentCreatorDialog::updatePiecesCount()
306 const Path path
= m_ui
->textInputPath
->selectedPath();
307 #ifdef QBT_USES_LIBTORRENT2
308 const int count
= BitTorrent::TorrentCreator::calculateTotalPieces(
309 path
, getPieceSize(), getTorrentFormat());
311 const bool isAlignmentOptimized
= m_ui
->checkOptimizeAlignment
->isChecked();
312 const int count
= BitTorrent::TorrentCreator::calculateTotalPieces(path
313 , getPieceSize(), isAlignmentOptimized
, getPaddedFileSizeLimit());
315 m_ui
->labelTotalPieces
->setText(QString::number(count
));
318 void TorrentCreatorDialog::setInteractionEnabled(const bool enabled
) const
320 m_ui
->textInputPath
->setEnabled(enabled
);
321 m_ui
->addFileButton
->setEnabled(enabled
);
322 m_ui
->addFolderButton
->setEnabled(enabled
);
323 m_ui
->trackersList
->setEnabled(enabled
);
324 m_ui
->URLSeedsList
->setEnabled(enabled
);
325 m_ui
->txtComment
->setEnabled(enabled
);
326 m_ui
->lineEditSource
->setEnabled(enabled
);
327 m_ui
->comboPieceSize
->setEnabled(enabled
);
328 m_ui
->buttonCalcTotalPieces
->setEnabled(enabled
);
329 m_ui
->checkPrivate
->setEnabled(enabled
);
330 m_ui
->checkStartSeeding
->setEnabled(enabled
);
331 m_ui
->buttonBox
->button(QDialogButtonBox::Ok
)->setEnabled(enabled
);
332 m_ui
->checkIgnoreShareLimits
->setEnabled(enabled
&& m_ui
->checkStartSeeding
->isChecked());
333 #ifdef QBT_USES_LIBTORRENT2
334 m_ui
->widgetTorrentFormat
->setEnabled(enabled
);
336 m_ui
->checkOptimizeAlignment
->setEnabled(enabled
);
337 m_ui
->spinPaddedFileSizeLimit
->setEnabled(enabled
);
341 void TorrentCreatorDialog::saveSettings()
343 m_storeLastAddPath
= m_ui
->textInputPath
->selectedPath();
345 m_storePieceSize
= m_ui
->comboPieceSize
->currentIndex();
346 m_storePrivateTorrent
= m_ui
->checkPrivate
->isChecked();
347 m_storeStartSeeding
= m_ui
->checkStartSeeding
->isChecked();
348 m_storeIgnoreRatio
= m_ui
->checkIgnoreShareLimits
->isChecked();
349 #ifdef QBT_USES_LIBTORRENT2
350 m_storeTorrentFormat
= m_ui
->comboTorrentFormat
->currentIndex();
352 m_storeOptimizeAlignment
= m_ui
->checkOptimizeAlignment
->isChecked();
353 m_paddedFileSizeLimit
= m_ui
->spinPaddedFileSizeLimit
->value();
356 m_storeTrackerList
= m_ui
->trackersList
->toPlainText();
357 m_storeWebSeedList
= m_ui
->URLSeedsList
->toPlainText();
358 m_storeComments
= m_ui
->txtComment
->toPlainText();
359 m_storeSource
= m_ui
->lineEditSource
->text();
361 m_storeDialogSize
= size();
364 void TorrentCreatorDialog::loadSettings()
366 m_ui
->textInputPath
->setSelectedPath(m_storeLastAddPath
.get(Utils::Fs::homePath()));
368 m_ui
->comboPieceSize
->setCurrentIndex(m_storePieceSize
);
369 m_ui
->checkPrivate
->setChecked(m_storePrivateTorrent
);
370 m_ui
->checkStartSeeding
->setChecked(m_storeStartSeeding
);
371 m_ui
->checkIgnoreShareLimits
->setChecked(m_storeIgnoreRatio
);
372 m_ui
->checkIgnoreShareLimits
->setEnabled(m_ui
->checkStartSeeding
->isChecked());
373 #ifdef QBT_USES_LIBTORRENT2
374 m_ui
->comboTorrentFormat
->setCurrentIndex(m_storeTorrentFormat
.get(1));
376 m_ui
->checkOptimizeAlignment
->setChecked(m_storeOptimizeAlignment
.get(true));
377 m_ui
->spinPaddedFileSizeLimit
->setValue(m_paddedFileSizeLimit
.get(-1));
380 m_ui
->trackersList
->setPlainText(m_storeTrackerList
);
381 m_ui
->URLSeedsList
->setPlainText(m_storeWebSeedList
);
382 m_ui
->txtComment
->setPlainText(m_storeComments
);
383 m_ui
->lineEditSource
->setText(m_storeSource
);
385 if (const QSize dialogSize
= m_storeDialogSize
; dialogSize
.isValid())