feat: add tr_torrentSetTrackerList(), tr_torrentGetTrackerList() (#2642)

Add a getter/setter for torrent announce URLs as text that can be copied
and pasted: one URL per line, with a blank line between tiers.

C API: tr_torrentGetTrackerList() and tr_torrentSetTrackerList()
RPC APi: `trackerList` in `torrent-get` and `torrent-set`.

This deprecates `trackerAdd`, `trackerRemove`, and `trackerEdit`
from the RPC API.
This commit is contained in:
Charles Kerr 2022-02-18 11:52:01 -06:00 committed by GitHub
parent 17cb155451
commit 02b6cc76d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 561 additions and 141 deletions

View File

@ -142,8 +142,8 @@ Request arguments:
| `bandwidthPriority` | number | this torrent's bandwidth tr_priority_t
| `downloadLimit` | number | maximum download speed (KBps)
| `downloadLimited` | boolean | true if `downloadLimit` is honored
| `files-wanted` | array | indices of file(s) to download
| `files-unwanted` | array | indices of file(s) to not download
| `files-wanted` | array | indices of file(s) to download
| `honorsSessionLimits` | boolean | true if session upload limits are honored
| `ids` | array | torrent list, as described in 3.1
| `labels` | array | array of string labels
@ -157,9 +157,10 @@ Request arguments:
| `seedIdleMode` | number | which seeding inactivity to use. See tr_idlelimit
| `seedRatioLimit` | double | torrent-level seeding ratio
| `seedRatioMode` | number | which ratio to use. See tr_ratiolimit
| `trackerAdd` | array | strings of announce URLs to add
| `trackerRemove` | array | ids of trackers to remove
| `trackerReplace` | array | pairs of <trackerId/new announce URLs>
| `trackerAdd` | array | **DEPRECATED** use trackerList instead
| `trackerList` | string | string of announce URLs, one per line, with a blank line between tiers
| `trackerRemove` | array | **DEPRECATED** use trackerList instead
| `trackerReplace` | array | **DEPRECATED** use trackerList instead
| `uploadLimit` | number | maximum upload speed (KBps)
| `uploadLimited` | boolean | true if `uploadLimit` is honored
@ -269,6 +270,7 @@ The 'source' column here corresponds to the data structure there.
| `startDate`| number| tr_stat
| `status`| number (see below)| tr_stat
| `trackers`| array (see below)| n/a
' `trackerList` | string | string of announce URLs, one per line, with a blank line between tiers
| `trackerStats`| array (see below)| n/a
| `totalSize`| number| tr_torrent_view
| `torrentFile`| string| tr_info
@ -531,6 +533,7 @@ Response arguments: `path`, `name`, and `id`, holding the torrent ID integer
| `config-dir` | string | location of transmission's configuration directory
| `dht-enabled` | boolean | true means allow dht in public torrents
| `download-dir` | string | default path to download torrents
| `download-dir-free-space` | number | **DEPRECATED** Use the `free-space` method instead.
| `download-queue-enabled` | boolean | if true, limit how many torrents can be downloaded at once
| `download-queue-size` | number | max number of torrents to download at once (see download-queue-enabled)
| `encryption` | string | `required`, `preferred`, `tolerated`
@ -935,6 +938,7 @@ Transmission 4.0.0 (`rpc-version-semver` 5.3.0, `rpc-version`: 17)
| Method | Description
|:---|:---
| `/upload` | :warning: undocumented `/upload` endpoint removed
| `session-get` | **DEPRECATED** `download-dir-free-space`. Use `free-space` instead.
| `free-space` | new return arg `total-capacity`
| `session-get` | new arg `rpc-version-semver`
| `session-get` | new arg `script-torrent-added-enabled`
@ -947,16 +951,9 @@ Transmission 4.0.0 (`rpc-version-semver` 5.3.0, `rpc-version`: 17)
| `torrent-get` | new arg `primary-mime-type`
| `torrent-get` | new arg `tracker.sitename`
| `torrent-get` | new arg `trackerStats.sitename`
| `torrent-get` | new arg `trackerList`
| `torrent-set` | new arg `trackerList`
| `torrent-set` | **DEPRECATED** `trackerAdd`. Use `trackerList` instead.
| `torrent-set` | **DEPRECATED** `trackerRemove`. Use `trackerList` instead.
| `torrent-set` | **DEPRECATED** `trackerReplace`. Use `trackerList` instead.
### 5.1. Upcoming Breakage
These features are deprecated:
1. session-get's 'download-dir-free-space' argument will be removed.
Its functionality has been superceded by the 'free-space' method.
2. HTTP POSTs to http://server:port/transmission/upload will fail.
This was an undocumented hack to allow web clients to add files without
client-side access to the file. This functionality is superceded by
using HTML5's FileReader object + the documented 'torrent-add' method.

View File

@ -2211,39 +2211,10 @@ void DetailsDialog::Impl::on_edit_trackers_response(int response, std::shared_pt
{
auto const torrent_id = GPOINTER_TO_INT(dialog->get_data(TORRENT_ID_KEY));
auto* const text_buffer = static_cast<Gtk::TextBuffer*>(dialog->get_data(TEXT_BUFFER_KEY));
tr_torrent* const tor = core_->find_torrent(torrent_id);
if (tor != nullptr)
if (auto* const tor = core_->find_torrent(torrent_id); tor != nullptr)
{
/* build the array of trackers */
auto const tracker_text = text_buffer->get_text(false);
std::istringstream tracker_strings(tracker_text);
auto announce_url_strings = std::vector<std::string>{};
auto announce_urls = std::vector<char const*>{};
auto tiers = std::vector<tr_tracker_tier_t>{};
auto tier = tr_tracker_tier_t{ 0 };
std::string str;
while (std::getline(tracker_strings, str))
{
if (str.empty())
{
++tier;
}
else
{
announce_url_strings.push_back(str);
tiers.push_back(tier);
}
}
std::transform(
std::begin(announce_url_strings),
std::end(announce_url_strings),
std::back_inserter(announce_urls),
[](auto const& url) { return url.c_str(); });
if (tr_torrentSetAnnounceList(tor, std::data(announce_urls), std::data(tiers), std::size(announce_urls)))
if (tr_torrentSetTrackerList(tor, text_buffer->get_text(false).c_str()))
{
refresh();
}

View File

@ -248,3 +248,62 @@ bool tr_announce_list::save(std::string const& torrent_file, tr_error** error) c
// save it
return tr_saveFile(torrent_file, contents, error);
}
bool tr_announce_list::parse(std::string_view text)
{
auto scratch = tr_announce_list{};
auto current_tier = tr_tracker_tier_t{ 0 };
auto current_tier_size = size_t{ 0 };
auto line = std::string_view{};
while (tr_strvSep(&text, &line, '\n'))
{
if (tr_strvEndsWith(line, '\r'))
{
line = line.substr(0, std::size(line) - 1);
}
line = tr_strvStrip(line);
if (std::empty(line))
{
if (current_tier_size > 0)
{
++current_tier;
current_tier_size = 0;
}
}
else if (scratch.add(line, current_tier))
{
++current_tier_size;
}
else
{
return false;
}
}
*this = scratch;
return true;
}
std::string tr_announce_list::toString() const
{
auto text = std::string{};
auto current_tier = std::optional<tr_tracker_tier_t>{};
for (auto const& tracker : *this)
{
if (current_tier && *current_tier != tracker.tier)
{
text += '\n';
}
text += tracker.announce.full;
text += '\n';
current_tier = tracker.tier;
}
return text;
}

View File

@ -8,6 +8,7 @@
#include <cstddef>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
@ -32,7 +33,7 @@ public:
tr_tracker_tier_t tier = 0;
tr_tracker_id_t id = 0;
int compare(tracker_info const& that) const // <=>
[[nodiscard]] int compare(tracker_info const& that) const // <=>
{
if (this->tier != that.tier)
{
@ -47,12 +48,12 @@ public:
return 0;
}
bool operator<(tracker_info const& that) const
[[nodiscard]] bool operator<(tracker_info const& that) const
{
return compare(that) < 0;
}
bool operator==(tracker_info const& that) const
[[nodiscard]] bool operator==(tracker_info const& that) const
{
return compare(that) == 0;
}
@ -62,29 +63,34 @@ private:
using trackers_t = std::vector<tracker_info>;
public:
auto begin() const
[[nodiscard]] auto begin() const
{
return std::begin(trackers_);
}
auto end() const
[[nodiscard]] auto end() const
{
return std::end(trackers_);
}
bool empty() const
[[nodiscard]] bool empty() const
{
return std::empty(trackers_);
}
size_t size() const
[[nodiscard]] size_t size() const
{
return std::size(trackers_);
}
tracker_info const& at(size_t i) const
[[nodiscard]] tracker_info const& at(size_t i) const
{
return trackers_.at(i);
}
std::set<tr_tracker_tier_t> tiers() const;
tr_tracker_tier_t nextTier() const;
[[nodiscard]] std::set<tr_tracker_tier_t> tiers() const;
[[nodiscard]] tr_tracker_tier_t nextTier() const;
bool add(std::string_view announce_url_sv)
{
@ -101,13 +107,21 @@ public:
return trackers_.clear();
}
/**
* Populate the announce list from a text string.
* - One announce URL per line
* - Blank line denotes a new tier
*/
bool parse(std::string_view text);
[[nodiscard]] std::string toString() const;
bool save(std::string const& torrent_file, tr_error** error = nullptr) const;
static std::optional<std::string> announceToScrape(std::string_view announce);
static tr_quark announceToScrape(tr_quark announce);
private:
tr_tracker_tier_t getTier(tr_tracker_tier_t tier, tr_url_parsed_t const& announce) const;
[[nodiscard]] tr_tracker_tier_t getTier(tr_tracker_tier_t tier, tr_url_parsed_t const& announce) const;
bool canAdd(tr_url_parsed_t const& announce);
static tr_tracker_id_t nextUniqueId();

View File

@ -18,7 +18,7 @@ using namespace std::literals;
namespace
{
auto constexpr my_static = std::array<std::string_view, 387>{ ""sv,
auto constexpr my_static = std::array<std::string_view, 388>{ ""sv,
"activeTorrentCount"sv,
"activity-date"sv,
"activityDate"sv,
@ -370,6 +370,7 @@ auto constexpr my_static = std::array<std::string_view, 387>{ ""sv,
"totalSize"sv,
"total_size"sv,
"trackerAdd"sv,
"trackerList"sv,
"trackerRemove"sv,
"trackerReplace"sv,
"trackerStats"sv,

View File

@ -373,6 +373,7 @@ enum
TR_KEY_totalSize,
TR_KEY_total_size,
TR_KEY_trackerAdd,
TR_KEY_trackerList,
TR_KEY_trackerRemove,
TR_KEY_trackerReplace,
TR_KEY_trackerStats,

View File

@ -777,6 +777,10 @@ static void initField(tr_torrent const* const tor, tr_stat const* const st, tr_v
addTrackers(tor, initme);
break;
case TR_KEY_trackerList:
tr_variantInitStr(initme, tor->trackerList());
break;
case TR_KEY_trackerStats:
{
auto const n = tr_torrentTrackerCount(tor);
@ -1258,6 +1262,14 @@ static char const* torrentSet(
errmsg = replaceTrackers(tor, tmp_variant);
}
if (std::string_view txt; errmsg == nullptr && tr_variantDictFindStrView(args_in, TR_KEY_trackerList, &txt))
{
if (!tor->setTrackerList(txt))
{
errmsg = "Invalid tracker list";
}
}
notify(session, TR_RPC_TORRENT_CHANGED, tor);
}

View File

@ -28,7 +28,6 @@
#include <libutp/utp.h>
// #define TR_SHOW_DEPRECATED
#include "transmission.h"
#include "announcer.h"

View File

@ -2028,42 +2028,51 @@ bool tr_torrent::checkPiece(tr_piece_index_t piece)
****
***/
bool tr_torrentSetAnnounceList(tr_torrent* tor, char const* const* announce_urls, tr_tracker_tier_t const* tiers, size_t n)
bool tr_torrent::setTrackerList(std::string_view text)
{
TR_ASSERT(tr_isTorrent(tor));
auto const lock = tor->unique_lock();
auto const lock = this->unique_lock();
auto announce_list = tr_announce_list();
if ((announce_list.set(announce_urls, tiers, n) == 0U) || !announce_list.save(tor->torrentFile()))
if (!announce_list.parse(text) || !announce_list.save(this->torrentFile()))
{
return false;
}
tor->metainfo_.announceList() = announce_list;
tor->markEdited();
this->metainfo_.announceList() = announce_list;
this->markEdited();
/* if we had a tracker-related error on this torrent,
* and that tracker's been removed,
* then clear the error */
if (tor->error == TR_STAT_TRACKER_WARNING || tor->error == TR_STAT_TRACKER_ERROR)
if (this->error == TR_STAT_TRACKER_WARNING || this->error == TR_STAT_TRACKER_ERROR)
{
auto const error_url = tor->error_announce_url;
auto const error_url = this->error_announce_url;
if (std::any_of(
std::begin(tor->announceList()),
std::end(tor->announceList()),
std::begin(this->announceList()),
std::end(this->announceList()),
[error_url](auto const& tracker) { return tracker.announce_str == error_url; }))
{
tr_torrentClearError(tor);
tr_torrentClearError(this);
}
}
/* tell the announcer to reload this torrent's tracker list */
tr_announcerResetTorrent(tor->session->announcer, tor);
tr_announcerResetTorrent(this->session->announcer, this);
return true;
}
bool tr_torrentSetTrackerList(tr_torrent* tor, char const* text)
{
return text != nullptr && tor->setTrackerList(text);
}
char* tr_torrentGetTrackerList(tr_torrent const* tor)
{
return tr_strvDup(tor->trackerList());
}
/**
***
**/

View File

@ -426,6 +426,13 @@ public:
return this->announceList().tiers();
}
[[nodiscard]] auto trackerList() const
{
return this->announceList().toString();
}
bool setTrackerList(std::string_view text);
/// METAINFO - WEBSEEDS
[[nodiscard]] auto webseedCount() const

View File

@ -1186,17 +1186,19 @@ char* tr_torrentGetMagnetLink(tr_torrent const* tor);
**/
/**
* @brief Modify a torrent's tracker list.
* Returns a newly-allocated string listing its tracker's announce URLs.
* One URL per line, with a blank line between tiers
*/
char* tr_torrentGetTrackerList(tr_torrent const* tor);
/**
* Sets a torrent's tracker list from a list of announce URLs with one
* URL per line and a blank line between tiers.
*
* This updates both the `torrent' object's tracker list
* and the metainfo file in tr_sessionGetConfigDir()'s torrent subdirectory.
*
* @param torrent The torrent whose tracker list is to be modified
* @param urls Array of n announce url strings
* @param tiers Array of n tier numbers for grouping 'urls' into tiers
* @param n the number of urls/tiers
*/
bool tr_torrentSetAnnounceList(tr_torrent* torrent, char const* const* announce_urls, tr_tracker_tier_t const* tiers, size_t n);
bool tr_torrentSetTrackerList(tr_torrent* tor, char const* text);
/**
***
@ -1599,8 +1601,8 @@ struct tr_stat
This ONLY counts piece data. */
float pieceDownloadSpeed_KBps;
#define TR_ETA_NOT_AVAIL -1
#define TR_ETA_UNKNOWN -2
#define TR_ETA_NOT_AVAIL (-1)
#define TR_ETA_UNKNOWN (-2)
/** If downloading, estimated number of seconds left until the torrent is done.
If seeding, estimated number of seconds left until seed ratio is reached. */
int eta;

View File

@ -114,7 +114,7 @@ void tr_timerAdd(struct event* timer, int seconds, int microseconds) TR_GNUC_NON
void tr_timerAddMsec(struct event* timer, int milliseconds) TR_GNUC_NONNULL(1);
/** @brief return the current date in milliseconds */
uint64_t tr_time_msec(void);
uint64_t tr_time_msec();
/** @brief sleep the specified number of milliseconds */
void tr_wait_msec(long int delay_milliseconds);
@ -360,13 +360,19 @@ constexpr std::string_view tr_strvSep(std::string_view* sv, char delim)
{
auto pos = sv->find(delim);
auto const ret = sv->substr(0, pos);
sv->remove_prefix(pos != sv->npos ? pos + 1 : std::size(*sv));
sv->remove_prefix(pos != std::string_view::npos ? pos + 1 : std::size(*sv));
return ret;
}
constexpr bool tr_strvSep(std::string_view* sv, std::string_view* token, char delim)
{
return !std::empty((*token = tr_strvSep(sv, delim)));
if (std::empty(*sv))
{
return false;
}
*token = tr_strvSep(sv, delim);
return true;
}
std::string_view tr_strvStrip(std::string_view sv);
@ -453,7 +459,7 @@ extern time_t __tr_current_time;
* to always be accurate. However, it is *much* faster when 100% accuracy
* isn't needed
*/
static inline time_t tr_time(void)
static inline time_t tr_time()
{
return __tr_current_time;
}
@ -542,4 +548,4 @@ char* tr_env_get_string(char const* key, char const* default_value);
****
***/
void tr_net_init(void);
void tr_net_init();

View File

@ -8,7 +8,7 @@
#include <libtransmission/error.h>
#include <libtransmission/log.h>
#include <libtransmission/utils.h> // tr_new()
#include <libtransmission/utils.h> // tr_free(), tr_strvJoin()
#import "Torrent.h"
#import "GroupsController.h"
@ -746,28 +746,18 @@ bool trashDataFile(char const* filename, tr_error** error)
new_tracker = [@"http://" stringByAppendingString:new_tracker];
}
auto urls = std::vector<char const*>{};
auto tiers = std::vector<tr_tracker_tier_t>{};
for (size_t i = 0, n = tr_torrentTrackerCount(fHandle); i < n; ++i)
{
auto const tracker = tr_torrentTracker(fHandle, i);
urls.push_back(tracker.announce);
tiers.push_back(tracker.tier);
}
urls.push_back(new_tracker.UTF8String);
tiers.push_back(std::empty(tiers) ? 0 : tiers.back() + 1);
BOOL const success = tr_torrentSetAnnounceList(fHandle, std::data(urls), std::data(tiers), std::size(urls));
char* old_list = tr_torrentGetTrackerList(fHandle);
auto new_list = tr_strvJoin(old_list, "\n\n", new_tracker.UTF8String);
BOOL const success = tr_torrentSetTrackerList(fHandle, new_list.c_str());
tr_free(old_list);
return success;
}
- (void)removeTrackers:(NSSet*)trackers
{
auto urls = std::vector<char const*>{};
auto tiers = std::vector<tr_tracker_tier_t>{};
auto new_list = std::string{};
auto current_tier = std::optional<tr_tracker_tier_t>{};
for (size_t i = 0, n = tr_torrentTrackerCount(fHandle); i < n; ++i)
{
@ -778,11 +768,18 @@ bool trashDataFile(char const* filename, tr_error** error)
continue;
}
urls.push_back(tracker.announce);
tiers.push_back(tracker.tier);
if (current_tier && *current_tier != tracker.tier)
{
new_list += '\n';
}
new_list += tracker.announce;
new_list += '\n';
current_tier = tracker.tier;
}
BOOL const success = tr_torrentSetAnnounceList(fHandle, std::data(urls), std::data(tiers), std::size(urls));
BOOL const success = tr_torrentSetTrackerList(fHandle, new_list.c_str());
NSAssert(success, @"Removing tracker addresses failed");
}

View File

@ -132,6 +132,7 @@ tr_qt_wrap_ui(${PROJECT_NAME}_UI_SOURCES
RelocateDialog.ui
SessionDialog.ui
StatsDialog.ui
TrackersDialog.ui
)
set(${PROJECT_NAME}_QRC_FILES application.qrc)

View File

@ -29,6 +29,7 @@
#include <libtransmission/transmission.h>
#include <libtransmission/utils.h> // tr_getRatio()
#include "BaseDialog.h"
#include "ColumnResizer.h"
#include "DetailsDialog.h"
#include "Formatter.h"
@ -43,6 +44,8 @@
#include "TrackerModelFilter.h"
#include "Utils.h"
#include "ui_TrackersDialog.h"
class Prefs;
class Session;
@ -53,6 +56,43 @@ class Session;
namespace
{
class TrackersDialog : public BaseDialog
{
Q_OBJECT
public:
explicit TrackersDialog(QString tracker_list, QWidget* parent = nullptr)
: BaseDialog{ parent }
{
ui_.setupUi(this);
ui_.trackerList->setPlainText(tracker_list);
connect(ui_.dialogButtons, &QDialogButtonBox::clicked, this, &TrackersDialog::onButtonBoxClicked);
}
signals:
void trackerListEdited(QString trackerList);
private slots:
void onButtonBoxClicked(QAbstractButton* button)
{
if (ui_.dialogButtons->standardButton(button) == QDialogButtonBox::Ok)
{
emit trackerListEdited(ui_.trackerList->toPlainText());
}
close();
}
private:
Ui::TrackersDialog ui_{};
QTimer timer_;
};
} // namespace
namespace
{
int constexpr DebounceIntervalMSec = 100;
int constexpr RefreshIntervalMSec = 4000;
@ -1014,6 +1054,7 @@ void DetailsDialog::refreshUI()
///
tracker_model_->refresh(model_, ids_);
ui_.editTrackersButton->setEnabled(std::size(ids_) == 1);
///
/// Peers tab
@ -1256,7 +1297,6 @@ void DetailsDialog::onBandwidthPriorityChanged(int index)
void DetailsDialog::onTrackerSelectionChanged()
{
int const selection_count = ui_.trackersView->selectionModel()->selectedRows().size();
ui_.editTrackerButton->setEnabled(selection_count == 1);
ui_.removeTrackerButton->setEnabled(selection_count > 0);
}
@ -1303,39 +1343,28 @@ void DetailsDialog::onAddTrackerClicked()
}
}
void DetailsDialog::onEditTrackerClicked()
void DetailsDialog::onTrackerListEdited(QString tracker_list)
{
QItemSelectionModel* selection_model = ui_.trackersView->selectionModel();
QModelIndexList selected_rows = selection_model->selectedRows();
assert(selected_rows.size() == 1);
QModelIndex i = selection_model->currentIndex();
auto const tracker_info = ui_.trackersView->model()->data(i, TrackerModel::TrackerRole).value<TrackerInfo>();
torrentSet(TR_KEY_trackerList, tracker_list);
}
bool ok = false;
QString const newval = QInputDialog::getText(
this,
tr("Edit URL "),
tr("Edit tracker announce URL:"),
QLineEdit::Normal,
tracker_info.st.announce,
&ok);
if (!ok)
void DetailsDialog::onEditTrackersClicked()
{
if (std::size(ids_) != 1)
{
// user pressed "cancel" -- noop
return;
}
else if (!QUrl(newval).isValid())
{
QMessageBox::warning(this, tr("Error"), tr("Invalid URL \"%1\"").arg(newval));
}
else
{
torrent_ids_t ids{ tracker_info.torrent_id };
QPair<int, QString> const id_url = qMakePair(tracker_info.st.id, newval);
torrentSet(ids, TR_KEY_trackerReplace, id_url);
auto const* const tor = model_.getTorrentFromId(*std::begin(ids_));
if (tor == nullptr)
{
return;
}
auto* dialog = new TrackersDialog(tor->trackerList(), this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
connect(dialog, &TrackersDialog::trackerListEdited, this, &DetailsDialog::onTrackerListEdited);
dialog->open();
}
void DetailsDialog::onRemoveTrackerClicked()
@ -1433,14 +1462,14 @@ void DetailsDialog::initTrackerTab()
auto& icons = IconCache::get();
ui_.addTrackerButton->setIcon(icons.getThemeIcon(QStringLiteral("list-add"), QStyle::SP_DialogOpenButton));
ui_.editTrackerButton->setIcon(icons.getThemeIcon(QStringLiteral("document-properties"), QStyle::SP_DesktopIcon));
ui_.editTrackersButton->setIcon(icons.getThemeIcon(QStringLiteral("document-properties"), QStyle::SP_DesktopIcon));
ui_.removeTrackerButton->setIcon(icons.getThemeIcon(QStringLiteral("list-remove"), QStyle::SP_TrashIcon));
ui_.showTrackerScrapesCheck->setChecked(prefs_.getBool(Prefs::SHOW_TRACKER_SCRAPES));
ui_.showBackupTrackersCheck->setChecked(prefs_.getBool(Prefs::SHOW_BACKUP_TRACKERS));
connect(ui_.addTrackerButton, &QAbstractButton::clicked, this, &DetailsDialog::onAddTrackerClicked);
connect(ui_.editTrackerButton, &QAbstractButton::clicked, this, &DetailsDialog::onEditTrackerClicked);
connect(ui_.editTrackersButton, &QAbstractButton::clicked, this, &DetailsDialog::onEditTrackersClicked);
connect(ui_.removeTrackerButton, &QAbstractButton::clicked, this, &DetailsDialog::onRemoveTrackerClicked);
connect(ui_.showBackupTrackersCheck, &QAbstractButton::clicked, this, &DetailsDialog::onShowBackupTrackersToggled);
connect(ui_.showTrackerScrapesCheck, &QAbstractButton::clicked, this, &DetailsDialog::onShowTrackerScrapesToggled);
@ -1545,3 +1574,5 @@ void DetailsDialog::onOpenRequested(QString const& path) const
}
}
}
#include "DetailsDialog.moc"

View File

@ -19,6 +19,7 @@
#include "Typedefs.h"
#include "ui_DetailsDialog.h"
#include "ui_TrackersDialog.h"
class QTreeWidgetItem;
@ -68,10 +69,11 @@ private slots:
// Tracker tab
void onTrackerSelectionChanged();
void onAddTrackerClicked();
void onEditTrackerClicked();
void onEditTrackersClicked();
void onRemoveTrackerClicked();
void onShowTrackerScrapesToggled(bool);
void onShowBackupTrackersToggled(bool);
void onTrackerListEdited(QString);
// Files tab
void onFilePriorityChanged(QSet<int> const& file_indices, int);
@ -124,6 +126,7 @@ private:
TorrentModel const& model_;
Ui::DetailsDialog ui_ = {};
Ui::TrackersDialog trackers_ui_ = {};
torrent_ids_t ids_;
QTimer model_timer_;

View File

@ -527,9 +527,9 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QToolButton" name="editTrackerButton">
<widget class="QToolButton" name="editTrackersButton">
<property name="toolTip">
<string>Edit Tracker</string>
<string>Edit Trackers</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonIconOnly</enum>

View File

@ -436,6 +436,15 @@ Session::Tag Session::torrentSet(torrent_ids_t const& ids, tr_quark const key, b
return torrentSetImpl(&args);
}
Session::Tag Session::torrentSet(torrent_ids_t const& ids, tr_quark const key, QString const& value)
{
tr_variant args;
tr_variantInitDict(&args, 2);
addOptionalIds(&args, ids);
dictAdd(&args, key, value);
return torrentSetImpl(&args);
}
Session::Tag Session::torrentSet(torrent_ids_t const& ids, tr_quark const key, QStringList const& value)
{
tr_variant args;
@ -561,7 +570,7 @@ std::vector<std::string_view> const& Session::getKeyNames(TorrentProperties prop
};
// unchanging fields needed by the details dialog
static auto constexpr DetailInfoKeys = std::array<tr_quark, 8>{
static auto constexpr DetailInfoKeys = std::array<tr_quark, 9>{
TR_KEY_comment, //
TR_KEY_creator, //
TR_KEY_dateCreated, //
@ -569,6 +578,7 @@ std::vector<std::string_view> const& Session::getKeyNames(TorrentProperties prop
TR_KEY_isPrivate, //
TR_KEY_pieceCount, //
TR_KEY_pieceSize, //
TR_KEY_trackerList, //
TR_KEY_trackers, //
};

View File

@ -86,6 +86,7 @@ public:
Tag torrentSet(torrent_ids_t const& ids, tr_quark const key, bool val);
Tag torrentSet(torrent_ids_t const& ids, tr_quark const key, int val);
Tag torrentSet(torrent_ids_t const& ids, tr_quark const key, double val);
Tag torrentSet(torrent_ids_t const& ids, tr_quark const key, QString const& val);
Tag torrentSet(torrent_ids_t const& ids, tr_quark const key, QList<int> const& val);
Tag torrentSet(torrent_ids_t const& ids, tr_quark const key, QStringList const& val);
Tag torrentSet(torrent_ids_t const& ids, tr_quark const key, QPair<int, QString> const& val);

View File

@ -224,6 +224,7 @@ Torrent::fields_t Torrent::update(tr_quark const* keys, tr_variant const* const*
HANDLE_KEY(startDate, start_date, START_DATE)
HANDLE_KEY(status, status, STATUS)
HANDLE_KEY(totalSize, total_size, TOTAL_SIZE)
HANDLE_KEY(trackerList, tracker_list, TRACKER_LIST)
HANDLE_KEY(trackerStats, tracker_stats, TRACKER_STATS)
HANDLE_KEY(trackers, tracker_stats, TRACKER_STATS)
HANDLE_KEY(uploadLimit, upload_limit, UPLOAD_LIMIT) // KB/s

View File

@ -201,6 +201,11 @@ public:
QString getError() const;
QString trackerList() const
{
return tracker_list_;
}
TorrentHash const& hash() const
{
return hash_;
@ -606,6 +611,7 @@ public:
STATUS,
TOTAL_SIZE,
TRACKER_STATS,
TRACKER_LIST,
UPLOADED_EVER,
UPLOAD_LIMIT,
UPLOAD_LIMITED,
@ -669,12 +675,13 @@ private:
double recheck_progress_ = {};
double seed_ratio_limit_ = {};
QString primary_mime_type_;
QString comment_;
QString creator_;
QString download_dir_;
QString error_string_;
QString name_;
QString primary_mime_type_;
QString tracker_list_;
// mutable because it's a lazy lookup
mutable QIcon icon_ = IconCache::get().fileIcon();

68
qt/TrackersDialog.ui Normal file
View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TrackersDialog</class>
<widget class="QDialog" name="TrackersDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>450</height>
</rect>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="windowTitle">
<string>Edit Trackers</string>
</property>
<layout class="QVBoxLayout" name="dialogLayout">
<item>
<widget class="QLabel" name="filesSectionLabel">
<property name="styleSheet">
<string notr="true">font-weight:bold</string>
</property>
<property name="text">
<string>Tracker Announce URLs</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="filesSectionLayout" columnstretch="0">
<property name="leftMargin">
<number>18</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>To add another primary URL, add it after a blank line.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPlainTextEdit" name="trackerList"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="destinationLabel">
<property name="text">
<string>To add a backup URL, add it on the line after the primary URL.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="dialogButtons">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -388,3 +388,226 @@ TEST_F(AnnounceListTest, save)
// cleanup
std::remove(test_file.c_str());
}
TEST_F(AnnounceListTest, SingleAnnounce)
{
auto announce_list = tr_announce_list{};
auto constexpr Text = "https://www.example.com/a/announce";
EXPECT_TRUE(announce_list.parse(Text));
EXPECT_EQ(1, std::size(announce_list));
EXPECT_EQ("https://www.example.com/a/announce", announce_list.at(0).announce.full);
}
TEST_F(AnnounceListTest, parseThreeTier)
{
auto announce_list = tr_announce_list{};
auto constexpr Text =
"https://www.example.com/a/announce\n"
"\n"
"https://www.example.com/b/announce\n"
"\n"
"https://www.example.com/c/announce"sv;
EXPECT_TRUE(announce_list.parse(Text));
EXPECT_EQ(3, std::size(announce_list));
EXPECT_EQ("https://www.example.com/a/announce", announce_list.at(0).announce.full);
EXPECT_EQ(0, announce_list.at(0).tier);
EXPECT_EQ("https://www.example.com/b/announce", announce_list.at(1).announce.full);
EXPECT_EQ(1, announce_list.at(1).tier);
EXPECT_EQ("https://www.example.com/c/announce", announce_list.at(2).announce.full);
EXPECT_EQ(2, announce_list.at(2).tier);
EXPECT_EQ(tr_strvJoin(Text, "\n"sv), announce_list.toString());
}
TEST_F(AnnounceListTest, parseThreeTierWithTrailingLf)
{
auto announce_list = tr_announce_list{};
auto constexpr Text =
"https://www.example.com/a/announce\n"
"\n"
"https://www.example.com/b/announce\n"
"\n"
"https://www.example.com/c/announce\n"sv;
EXPECT_TRUE(announce_list.parse(Text));
EXPECT_EQ(3, std::size(announce_list));
EXPECT_EQ("https://www.example.com/a/announce", announce_list.at(0).announce.full);
EXPECT_EQ(0, announce_list.at(0).tier);
EXPECT_EQ("https://www.example.com/b/announce", announce_list.at(1).announce.full);
EXPECT_EQ(1, announce_list.at(1).tier);
EXPECT_EQ("https://www.example.com/c/announce", announce_list.at(2).announce.full);
EXPECT_EQ(2, announce_list.at(2).tier);
EXPECT_EQ(Text, announce_list.toString());
}
TEST_F(AnnounceListTest, parseThreeTierWithExcessLf)
{
auto announce_list = tr_announce_list{};
auto constexpr Text =
"https://www.example.com/a/announce\n"
"\n"
"\n"
"\n"
"https://www.example.com/b/announce\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"https://www.example.com/c/announce\n"sv;
EXPECT_TRUE(announce_list.parse(Text));
EXPECT_EQ(3, std::size(announce_list));
EXPECT_EQ("https://www.example.com/a/announce", announce_list.at(0).announce.full);
EXPECT_EQ(0, announce_list.at(0).tier);
EXPECT_EQ("https://www.example.com/b/announce", announce_list.at(1).announce.full);
EXPECT_EQ(1, announce_list.at(1).tier);
EXPECT_EQ("https://www.example.com/c/announce", announce_list.at(2).announce.full);
EXPECT_EQ(2, announce_list.at(2).tier);
auto constexpr ExpectedText =
"https://www.example.com/a/announce\n"
"\n"
"https://www.example.com/b/announce\n"
"\n"
"https://www.example.com/c/announce\n"sv;
EXPECT_EQ(ExpectedText, announce_list.toString());
}
TEST_F(AnnounceListTest, parseThreeTierWithWhitespace)
{
auto announce_list = tr_announce_list{};
auto constexpr Text =
"https://www.example.com/a/announce \n"
"\n"
" \n"
"\n"
"https://www.example.com/b/announce\n"
"\n"
"\n"
"\n"
" https://www.example.com/c/announce\n"sv;
EXPECT_TRUE(announce_list.parse(Text));
EXPECT_EQ(3, std::size(announce_list));
EXPECT_EQ("https://www.example.com/a/announce", announce_list.at(0).announce.full);
EXPECT_EQ(0, announce_list.at(0).tier);
EXPECT_EQ("https://www.example.com/b/announce", announce_list.at(1).announce.full);
EXPECT_EQ(1, announce_list.at(1).tier);
EXPECT_EQ("https://www.example.com/c/announce", announce_list.at(2).announce.full);
EXPECT_EQ(2, announce_list.at(2).tier);
auto constexpr ExpectedText =
"https://www.example.com/a/announce\n"
"\n"
"https://www.example.com/b/announce\n"
"\n"
"https://www.example.com/c/announce\n"sv;
EXPECT_EQ(ExpectedText, announce_list.toString());
}
TEST_F(AnnounceListTest, parseThreeTierCrLf)
{
auto announce_list = tr_announce_list{};
auto constexpr Text =
"https://www.example.com/a/announce\r\n"
"\r\n"
"https://www.example.com/b/announce\r\n"
"\r\n"
"https://www.example.com/c/announce\r\n"sv;
EXPECT_TRUE(announce_list.parse(Text));
EXPECT_EQ(3, std::size(announce_list));
EXPECT_EQ("https://www.example.com/a/announce", announce_list.at(0).announce.full);
EXPECT_EQ(0, announce_list.at(0).tier);
EXPECT_EQ("https://www.example.com/b/announce", announce_list.at(1).announce.full);
EXPECT_EQ(1, announce_list.at(1).tier);
EXPECT_EQ("https://www.example.com/c/announce", announce_list.at(2).announce.full);
EXPECT_EQ(2, announce_list.at(2).tier);
auto constexpr ExpectedText =
"https://www.example.com/a/announce\n"
"\n"
"https://www.example.com/b/announce\n"
"\n"
"https://www.example.com/c/announce\n"sv;
EXPECT_EQ(ExpectedText, announce_list.toString());
}
TEST_F(AnnounceListTest, parseMultiTrackerInTier)
{
auto announce_list = tr_announce_list{};
auto constexpr Text =
"https://www.example.com/a/announce\n"
"https://www.example.com/b/announce\n"
"\n"
"https://www.example.com/c/announce\n"
"https://www.example.com/d/announce\n"
"https://www.example.com/e/announce\n"
"https://www.example.com/f/announce\n"
"\n"
"https://www.example.com/g/announce\n"
"https://www.example.com/h/announce\n"
"https://www.example.com/i/announce\n"sv;
EXPECT_TRUE(announce_list.parse(Text));
EXPECT_EQ(9, std::size(announce_list));
EXPECT_EQ("https://www.example.com/a/announce", announce_list.at(0).announce.full);
EXPECT_EQ(0, announce_list.at(0).tier);
EXPECT_EQ("https://www.example.com/b/announce", announce_list.at(1).announce.full);
EXPECT_EQ(0, announce_list.at(1).tier);
EXPECT_EQ("https://www.example.com/c/announce", announce_list.at(2).announce.full);
EXPECT_EQ(1, announce_list.at(2).tier);
EXPECT_EQ("https://www.example.com/d/announce", announce_list.at(3).announce.full);
EXPECT_EQ(1, announce_list.at(3).tier);
EXPECT_EQ("https://www.example.com/e/announce", announce_list.at(4).announce.full);
EXPECT_EQ(1, announce_list.at(4).tier);
EXPECT_EQ("https://www.example.com/f/announce", announce_list.at(5).announce.full);
EXPECT_EQ(1, announce_list.at(5).tier);
EXPECT_EQ("https://www.example.com/g/announce", announce_list.at(6).announce.full);
EXPECT_EQ(2, announce_list.at(6).tier);
EXPECT_EQ("https://www.example.com/h/announce", announce_list.at(7).announce.full);
EXPECT_EQ(2, announce_list.at(7).tier);
EXPECT_EQ("https://www.example.com/i/announce", announce_list.at(8).announce.full);
EXPECT_EQ(2, announce_list.at(8).tier);
EXPECT_EQ(Text, announce_list.toString());
}
TEST_F(AnnounceListTest, parseInvalidUrl)
{
auto announce_list = tr_announce_list{};
auto constexpr Text =
"siojfaiojf"
"\n"
"https://www.example.com/b/announce\n"
"\n"
"https://www.example.com/c/announce\n"sv;
EXPECT_FALSE(announce_list.parse(Text));
}
TEST_F(AnnounceListTest, parseDuplicateUrl)
{
auto announce_list = tr_announce_list{};
auto constexpr Text =
"https://www.example.com/a/announce\n"
"\r\n"
"https://www.example.com/a/announce"sv;
EXPECT_FALSE(announce_list.parse(Text));
}