diff --git a/extras/rpc-spec.md b/extras/rpc-spec.md index 1214ea41f..bc46b0a17 100644 --- a/extras/rpc-spec.md +++ b/extras/rpc-spec.md @@ -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 +| `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. diff --git a/gtk/DetailsDialog.cc b/gtk/DetailsDialog.cc index 16b399d8d..499f0cdbb 100644 --- a/gtk/DetailsDialog.cc +++ b/gtk/DetailsDialog.cc @@ -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(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{}; - auto announce_urls = std::vector{}; - auto tiers = std::vector{}; - 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(); } diff --git a/libtransmission/announce-list.cc b/libtransmission/announce-list.cc index 16db94858..42c6e0fec 100644 --- a/libtransmission/announce-list.cc +++ b/libtransmission/announce-list.cc @@ -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{}; + + 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; +} diff --git a/libtransmission/announce-list.h b/libtransmission/announce-list.h index adb4c141e..6614ceabf 100644 --- a/libtransmission/announce-list.h +++ b/libtransmission/announce-list.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -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; 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 tiers() const; - tr_tracker_tier_t nextTier() const; + [[nodiscard]] std::set 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 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(); diff --git a/libtransmission/quark.cc b/libtransmission/quark.cc index d4c98c027..6a2d1ba93 100644 --- a/libtransmission/quark.cc +++ b/libtransmission/quark.cc @@ -18,7 +18,7 @@ using namespace std::literals; namespace { -auto constexpr my_static = std::array{ ""sv, +auto constexpr my_static = std::array{ ""sv, "activeTorrentCount"sv, "activity-date"sv, "activityDate"sv, @@ -370,6 +370,7 @@ auto constexpr my_static = std::array{ ""sv, "totalSize"sv, "total_size"sv, "trackerAdd"sv, + "trackerList"sv, "trackerRemove"sv, "trackerReplace"sv, "trackerStats"sv, diff --git a/libtransmission/quark.h b/libtransmission/quark.h index 4696be27f..2afd6805e 100644 --- a/libtransmission/quark.h +++ b/libtransmission/quark.h @@ -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, diff --git a/libtransmission/rpcimpl.cc b/libtransmission/rpcimpl.cc index b190138c1..898cfe11b 100644 --- a/libtransmission/rpcimpl.cc +++ b/libtransmission/rpcimpl.cc @@ -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); } diff --git a/libtransmission/session.cc b/libtransmission/session.cc index e1dfc1eea..9533d0dd6 100644 --- a/libtransmission/session.cc +++ b/libtransmission/session.cc @@ -28,7 +28,6 @@ #include -// #define TR_SHOW_DEPRECATED #include "transmission.h" #include "announcer.h" diff --git a/libtransmission/torrent.cc b/libtransmission/torrent.cc index 30c2fb0cd..615abf29e 100644 --- a/libtransmission/torrent.cc +++ b/libtransmission/torrent.cc @@ -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()); +} + /** *** **/ diff --git a/libtransmission/torrent.h b/libtransmission/torrent.h index 71265e3cb..23e973d71 100644 --- a/libtransmission/torrent.h +++ b/libtransmission/torrent.h @@ -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 diff --git a/libtransmission/transmission.h b/libtransmission/transmission.h index 6917d6007..7b8f6b75c 100644 --- a/libtransmission/transmission.h +++ b/libtransmission/transmission.h @@ -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; diff --git a/libtransmission/utils.h b/libtransmission/utils.h index 24e3c4797..6d9449ad7 100644 --- a/libtransmission/utils.h +++ b/libtransmission/utils.h @@ -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(); diff --git a/macosx/Torrent.mm b/macosx/Torrent.mm index 42d5c4681..38dcc516f 100644 --- a/macosx/Torrent.mm +++ b/macosx/Torrent.mm @@ -8,7 +8,7 @@ #include #include -#include // tr_new() +#include // 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{}; - auto tiers = std::vector{}; - - 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{}; - auto tiers = std::vector{}; + auto new_list = std::string{}; + auto current_tier = std::optional{}; 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"); } diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index 4e701d138..24e91dc9c 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -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) diff --git a/qt/DetailsDialog.cc b/qt/DetailsDialog.cc index 1e9fab62b..f183bb7f1 100644 --- a/qt/DetailsDialog.cc +++ b/qt/DetailsDialog.cc @@ -29,6 +29,7 @@ #include #include // 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(); + 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 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" diff --git a/qt/DetailsDialog.h b/qt/DetailsDialog.h index 3539350da..1ac887e81 100644 --- a/qt/DetailsDialog.h +++ b/qt/DetailsDialog.h @@ -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 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_; diff --git a/qt/DetailsDialog.ui b/qt/DetailsDialog.ui index ced415af5..5be532815 100644 --- a/qt/DetailsDialog.ui +++ b/qt/DetailsDialog.ui @@ -527,9 +527,9 @@ - + - Edit Tracker + Edit Trackers Qt::ToolButtonIconOnly diff --git a/qt/Session.cc b/qt/Session.cc index 52a387760..307ce34d2 100644 --- a/qt/Session.cc +++ b/qt/Session.cc @@ -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 const& Session::getKeyNames(TorrentProperties prop }; // unchanging fields needed by the details dialog - static auto constexpr DetailInfoKeys = std::array{ + static auto constexpr DetailInfoKeys = std::array{ TR_KEY_comment, // TR_KEY_creator, // TR_KEY_dateCreated, // @@ -569,6 +578,7 @@ std::vector const& Session::getKeyNames(TorrentProperties prop TR_KEY_isPrivate, // TR_KEY_pieceCount, // TR_KEY_pieceSize, // + TR_KEY_trackerList, // TR_KEY_trackers, // }; diff --git a/qt/Session.h b/qt/Session.h index 4d6790062..48dde4b91 100644 --- a/qt/Session.h +++ b/qt/Session.h @@ -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 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 const& val); diff --git a/qt/Torrent.cc b/qt/Torrent.cc index 0bd026fb6..70c1ea2cd 100644 --- a/qt/Torrent.cc +++ b/qt/Torrent.cc @@ -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 diff --git a/qt/Torrent.h b/qt/Torrent.h index 6016f93cf..07dab668e 100644 --- a/qt/Torrent.h +++ b/qt/Torrent.h @@ -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(); diff --git a/qt/TrackersDialog.ui b/qt/TrackersDialog.ui new file mode 100644 index 000000000..21e43db8e --- /dev/null +++ b/qt/TrackersDialog.ui @@ -0,0 +1,68 @@ + + + TrackersDialog + + + + 0 + 0 + 500 + 450 + + + + true + + + Edit Trackers + + + + + + font-weight:bold + + + Tracker Announce URLs + + + + + + + 18 + + + + + To add another primary URL, add it after a blank line. + + + + + + + + + + To add a backup URL, add it on the line after the primary URL. + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/tests/libtransmission/announce-list-test.cc b/tests/libtransmission/announce-list-test.cc index 306bfc37f..32a848dfa 100644 --- a/tests/libtransmission/announce-list-test.cc +++ b/tests/libtransmission/announce-list-test.cc @@ -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)); +}