// This file Copyright © 2022 Mnemosyne LLC. // It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. #include "Torrent.h" #include "IconCache.h" #include "Utils.h" #include #include #include #include #include #include #include using namespace std::string_view_literals; namespace { template Glib::Value& column_value_cast(Glib::ValueBase& value, Gtk::TreeModelColumn const& /*column*/) { return static_cast&>(value); } template>> void update_cache_value(T& value, U&& new_value, Torrent::ChangeFlags& changes, Torrent::ChangeFlag flag) { if (value != new_value) { value = std::forward(new_value); changes.set(flag); } } template>> void update_cache_value(T& value, U new_value, T epsilon, Torrent::ChangeFlags& changes, Torrent::ChangeFlag flag) { if (std::fabs(value - new_value) >= epsilon) { value = new_value; changes.set(flag); } } unsigned int build_torrent_trackers_hash(tr_torrent const& torrent) { auto hash = uint64_t(0); for (auto i = size_t(0), n = tr_torrentTrackerCount(&torrent); i < n; ++i) { for (auto const ch : std::string_view{ tr_torrentTracker(&torrent, i).announce }) { hash = (hash << 4) ^ (hash >> 28) ^ ch; } } return hash; } std::string_view get_mime_type(tr_torrent const& torrent) { auto const n_files = tr_torrentFileCount(&torrent); if (n_files == 0) { return UnknownMimeType; } if (n_files > 1) { return DirectoryMimeType; } auto const name = std::string_view(tr_torrentFile(&torrent, 0).name); return name.find('/') != std::string_view::npos ? DirectoryMimeType : tr_get_mime_type_for_filename(name); } } // namespace Torrent::Columns::Columns() { add(self); add(name_collated); } class Torrent::Impl { public: struct Cache { Glib::ustring error_message; Glib::ustring name; Glib::ustring name_collated; std::string_view mime_type; uint64_t have_unchecked = {}; uint64_t have_valid = {}; uint64_t left_until_done = {}; uint64_t size_when_done = {}; uint64_t total_size = {}; uint64_t uploaded_ever = {}; size_t queue_position = {}; time_t added_date = {}; time_t eta = {}; tr_torrent_activity activity = {}; unsigned int trackers = {}; int active_peer_count = {}; int active_peers_down = {}; int active_peers_up = {}; int error_code = {}; uint16_t peers_connected = {}; uint16_t peers_getting_from_us = {}; uint16_t peers_sending_to_us = {}; uint16_t webseeds_sending_to_us = {}; float activity_percent_done = {}; float metadata_percent_complete = {}; float percent_complete = {}; float percent_done = {}; float ratio = {}; float recheck_progress = {}; float seed_ratio = {}; float seed_ratio_percent_done = {}; float speed_down = {}; float speed_up = {}; tr_priority_t priority = {}; bool active = {}; bool finished = {}; bool has_metadata = {}; bool has_seed_ratio = {}; bool stalled = {}; }; public: Impl(Torrent& torrent, tr_torrent* raw_torrent); tr_torrent* get_raw_torrent() { return raw_torrent_; } Cache& get_cache() { return cache_; } ChangeFlags update_cache(); void notify_property_changes(ChangeFlags changes) const; void get_value(int column, Glib::ValueBase& value) const; [[nodiscard]] Glib::RefPtr get_icon() const; [[nodiscard]] Glib::ustring get_short_status_text() const; [[nodiscard]] Glib::ustring get_long_progress_text() const; [[nodiscard]] Glib::ustring get_long_status_text() const; private: [[nodiscard]] Glib::ustring get_short_transfer_text() const; [[nodiscard]] Glib::ustring get_error_text() const; [[nodiscard]] Glib::ustring get_activity_text() const; private: Torrent& torrent_; tr_torrent* const raw_torrent_; Cache cache_; }; Torrent::Impl::Impl(Torrent& torrent, tr_torrent* raw_torrent) : torrent_(torrent) , raw_torrent_(raw_torrent) { if (raw_torrent_ != nullptr) { update_cache(); } } Torrent::ChangeFlags Torrent::Impl::update_cache() { auto result = ChangeFlags(); auto const* const stats = tr_torrentStat(raw_torrent_); g_return_val_if_fail(stats != nullptr, Torrent::ChangeFlags()); auto seed_ratio = 0.0; auto const has_seed_ratio = tr_torrentGetSeedRatio(raw_torrent_, &seed_ratio); update_cache_value(cache_.name, tr_torrentName(raw_torrent_), result, ChangeFlag::NAME); update_cache_value(cache_.speed_up, stats->pieceUploadSpeed_KBps, 0.01F, result, ChangeFlag::SPEED_UP); update_cache_value(cache_.speed_down, stats->pieceDownloadSpeed_KBps, 0.01F, result, ChangeFlag::SPEED_DOWN); update_cache_value(cache_.active_peers_up, stats->peersGettingFromUs, result, ChangeFlag::ACTIVE_PEERS_UP); update_cache_value( cache_.active_peers_down, stats->peersSendingToUs + stats->webseedsSendingToUs, result, ChangeFlag::ACTIVE_PEERS_DOWN); update_cache_value(cache_.recheck_progress, stats->recheckProgress, 0.01F, result, ChangeFlag::RECHECK_PROGRESS); update_cache_value( cache_.active, stats->peersSendingToUs > 0 || stats->peersGettingFromUs > 0 || stats->activity == TR_STATUS_CHECK, result, ChangeFlag::ACTIVE); update_cache_value(cache_.activity, stats->activity, result, ChangeFlag::ACTIVITY); update_cache_value( cache_.activity_percent_done, std::clamp( stats->activity == TR_STATUS_SEED && has_seed_ratio ? stats->seedRatioPercentDone : stats->percentDone, 0.0F, 1.0F), 0.01F, result, ChangeFlag::PERCENT_DONE); update_cache_value(cache_.finished, stats->finished, result, ChangeFlag::FINISHED); update_cache_value(cache_.priority, tr_torrentGetPriority(raw_torrent_), result, ChangeFlag::PRIORITY); update_cache_value(cache_.queue_position, stats->queuePosition, result, ChangeFlag::QUEUE_POSITION); update_cache_value(cache_.trackers, build_torrent_trackers_hash(*raw_torrent_), result, ChangeFlag::TRACKERS); update_cache_value(cache_.error_code, stats->error, result, ChangeFlag::ERROR_CODE); update_cache_value(cache_.error_message, stats->errorString, result, ChangeFlag::ERROR_MESSAGE); update_cache_value( cache_.active_peer_count, stats->peersSendingToUs + stats->peersGettingFromUs + stats->webseedsSendingToUs, result, ChangeFlag::ACTIVE_PEER_COUNT); update_cache_value(cache_.mime_type, get_mime_type(*raw_torrent_), result, ChangeFlag::MIME_TYPE); update_cache_value(cache_.has_metadata, tr_torrentHasMetadata(raw_torrent_), result, ChangeFlag::HAS_METADATA); update_cache_value(cache_.stalled, stats->isStalled, result, ChangeFlag::STALLED); update_cache_value(cache_.ratio, stats->ratio, 0.01F, result, ChangeFlag::RATIO); update_cache_value(cache_.added_date, stats->addedDate, result, ChangeFlag::ADDED_DATE); update_cache_value(cache_.eta, stats->eta, result, ChangeFlag::ETA); update_cache_value(cache_.percent_complete, stats->percentComplete, 0.01F, result, ChangeFlag::PERCENT_COMPLETE); update_cache_value( cache_.seed_ratio_percent_done, stats->seedRatioPercentDone, 0.01F, result, ChangeFlag::SEED_RATIO_PERCENT_DONE); update_cache_value(cache_.total_size, tr_torrentTotalSize(raw_torrent_), result, ChangeFlag::TOTAL_SIZE); update_cache_value(cache_.has_seed_ratio, has_seed_ratio, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.have_unchecked, stats->haveUnchecked, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.have_valid, stats->haveValid, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.left_until_done, stats->leftUntilDone, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.percent_done, stats->percentDone, 0.01F, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.seed_ratio, static_cast(seed_ratio), 0.01F, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.size_when_done, stats->sizeWhenDone, result, ChangeFlag::LONG_PROGRESS); update_cache_value(cache_.uploaded_ever, stats->uploadedEver, result, ChangeFlag::LONG_PROGRESS); update_cache_value( cache_.metadata_percent_complete, stats->metadataPercentComplete, 0.01F, result, ChangeFlag::LONG_STATUS); update_cache_value(cache_.peers_connected, stats->peersConnected, result, ChangeFlag::LONG_STATUS); update_cache_value(cache_.peers_getting_from_us, stats->peersGettingFromUs, result, ChangeFlag::LONG_STATUS); update_cache_value(cache_.peers_sending_to_us, stats->peersSendingToUs, result, ChangeFlag::LONG_STATUS); update_cache_value(cache_.webseeds_sending_to_us, stats->webseedsSendingToUs, result, ChangeFlag::LONG_STATUS); if (result.test(ChangeFlag::NAME)) { cache_.name_collated = fmt::format("{}\t{}", cache_.name.lowercase(), tr_torrentView(raw_torrent_).hash_string); } return result; } void Torrent::Impl::notify_property_changes(ChangeFlags changes) const { // Updating the model triggers off resort/refresh, so don't notify unless something's actually changed if (changes.none()) { return; } gtr_object_notify_emit(torrent_); } void Torrent::Impl::get_value(int column, Glib::ValueBase& value) const { static auto const& columns = get_columns(); if (column == columns.self.index()) { column_value_cast(value, columns.self).set(&torrent_); } else if (column == columns.name_collated.index()) { column_value_cast(value, columns.name_collated).set(cache_.name_collated); } } Glib::RefPtr Torrent::Impl::get_icon() const { return gtr_get_mime_type_icon(cache_.mime_type); } Glib::ustring Torrent::Impl::get_short_status_text() const { switch (cache_.activity) { case TR_STATUS_STOPPED: return cache_.finished ? _("Finished") : _("Paused"); case TR_STATUS_CHECK_WAIT: return _("Queued for verification"); case TR_STATUS_DOWNLOAD_WAIT: return _("Queued for download"); case TR_STATUS_SEED_WAIT: return _("Queued for seeding"); case TR_STATUS_CHECK: return fmt::format( // xgettext:no-c-format _("Verifying local data ({percent_done}% tested)"), fmt::arg("percent_done", tr_truncd(cache_.recheck_progress * 100.0, 1))); case TR_STATUS_DOWNLOAD: case TR_STATUS_SEED: return fmt::format( FMT_STRING("{:s} {:s}"), get_short_transfer_text(), fmt::format(_("Ratio: {ratio}"), fmt::arg("ratio", tr_strlratio(cache_.ratio)))); default: return {}; } } Glib::ustring Torrent::Impl::get_long_progress_text() const { Glib::ustring gstr; bool const isDone = cache_.left_until_done == 0; auto const haveTotal = cache_.have_unchecked + cache_.have_valid; bool const isSeed = cache_.have_valid >= cache_.total_size; if (!isDone) // downloading { // 50 MB of 200 MB (25%) gstr += fmt::format( _("{current_size} of {complete_size} ({percent_done}%)"), fmt::arg("current_size", tr_strlsize(haveTotal)), fmt::arg("complete_size", tr_strlsize(cache_.size_when_done)), fmt::arg("percent_done", tr_strpercent(cache_.percent_done * 100.0))); } else if (!isSeed && cache_.has_seed_ratio) // partial seed, seed ratio { // 50 MB of 200 MB (25%), uploaded 30 MB (Ratio: X%, Goal: Y%) gstr += fmt::format( // xgettext:no-c-format _("{current_size} of {complete_size} ({percent_complete}%), uploaded {uploaded_size} (Ratio: {ratio}, Goal: {seed_ratio})"), fmt::arg("current_size", tr_strlsize(haveTotal)), fmt::arg("complete_size", tr_strlsize(cache_.total_size)), fmt::arg("percent_complete", tr_strpercent(cache_.percent_complete * 100.0)), fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)), fmt::arg("ratio", tr_strlratio(cache_.ratio)), fmt::arg("seed_ratio", tr_strlratio(cache_.seed_ratio))); } else if (!isSeed) // partial seed, no seed ratio { gstr += fmt::format( // xgettext:no-c-format _("{current_size} of {complete_size} ({percent_complete}%), uploaded {uploaded_size} (Ratio: {ratio})"), fmt::arg("current_size", tr_strlsize(haveTotal)), fmt::arg("complete_size", tr_strlsize(cache_.total_size)), fmt::arg("percent_complete", tr_strpercent(cache_.percent_complete * 100.0)), fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)), fmt::arg("ratio", tr_strlratio(cache_.ratio))); } else if (cache_.has_seed_ratio) // seed, seed ratio { gstr += fmt::format( _("{complete_size}, uploaded {uploaded_size} (Ratio: {ratio}, Goal: {seed_ratio})"), fmt::arg("complete_size", tr_strlsize(cache_.total_size)), fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)), fmt::arg("ratio", tr_strlratio(cache_.ratio)), fmt::arg("seed_ratio", tr_strlratio(cache_.seed_ratio))); } else // seed, no seed ratio { gstr += fmt::format( _("{complete_size}, uploaded {uploaded_size} (Ratio: {ratio})"), fmt::arg("complete_size", tr_strlsize(cache_.total_size)), fmt::arg("uploaded_size", tr_strlsize(cache_.uploaded_ever)), fmt::arg("ratio", tr_strlratio(cache_.ratio))); } // add time remaining when applicable if (cache_.activity == TR_STATUS_DOWNLOAD || (cache_.has_seed_ratio && cache_.activity == TR_STATUS_SEED)) { gstr += " - "; if (cache_.eta < 0) { gstr += _("Remaining time unknown"); } else { gstr += tr_format_time_left(cache_.eta); } } return gstr; } Glib::ustring Torrent::Impl::get_long_status_text() const { auto status_str = get_error_text(); if (status_str.empty()) { status_str = get_activity_text(); } switch (cache_.activity) { case TR_STATUS_CHECK_WAIT: case TR_STATUS_CHECK: case TR_STATUS_DOWNLOAD_WAIT: case TR_STATUS_SEED_WAIT: case TR_STATUS_STOPPED: break; default: if (auto const buf = get_short_transfer_text(); !std::empty(buf)) { status_str += fmt::format(FMT_STRING(" - {:s}"), buf); } } return status_str; } Glib::ustring Torrent::Impl::get_short_transfer_text() const { if (cache_.has_metadata && cache_.active_peers_down > 0) { return fmt::format( _("{download_speed} ▼ {upload_speed} ▲"), fmt::arg("upload_speed", tr_formatter_speed_KBps(cache_.speed_up)), fmt::arg("download_speed", tr_formatter_speed_KBps(cache_.speed_down))); } if (cache_.has_metadata && cache_.active_peers_up > 0) { return fmt::format(_("{upload_speed} ▲"), fmt::arg("upload_speed", tr_formatter_speed_KBps(cache_.speed_up))); } if (cache_.stalled) { return _("Stalled"); } return {}; } Glib::ustring Torrent::Impl::get_error_text() const { switch (cache_.error_code) { case TR_STAT_TRACKER_WARNING: return fmt::format(_("Tracker warning: '{warning}'"), fmt::arg("warning", cache_.error_message)); case TR_STAT_TRACKER_ERROR: return fmt::format(_("Tracker Error: '{error}'"), fmt::arg("error", cache_.error_message)); case TR_STAT_LOCAL_ERROR: return fmt::format(_("Local error: '{error}'"), fmt::arg("error", cache_.error_message)); default: return {}; } } Glib::ustring Torrent::Impl::get_activity_text() const { switch (cache_.activity) { case TR_STATUS_STOPPED: case TR_STATUS_CHECK_WAIT: case TR_STATUS_CHECK: case TR_STATUS_DOWNLOAD_WAIT: case TR_STATUS_SEED_WAIT: return get_short_status_text(); case TR_STATUS_DOWNLOAD: if (!cache_.has_metadata) { return fmt::format( ngettext( // xgettext:no-c-format "Downloading metadata from {active_count} connected peer ({percent_done}% done)", "Downloading metadata from {active_count} connected peers ({percent_done}% done)", cache_.peers_connected), fmt::arg("active_count", cache_.peers_connected), fmt::arg("percent_done", tr_strpercent(cache_.metadata_percent_complete * 100.0))); } if (cache_.peers_sending_to_us != 0 && cache_.webseeds_sending_to_us != 0) { return fmt::format( ngettext( "Downloading from {active_count} of {connected_count} connected peer and webseed", "Downloading from {active_count} of {connected_count} connected peers and webseeds", cache_.peers_connected + cache_.webseeds_sending_to_us), fmt::arg("active_count", cache_.peers_sending_to_us + cache_.webseeds_sending_to_us), fmt::arg("connected_count", cache_.peers_connected + cache_.webseeds_sending_to_us)); } if (cache_.webseeds_sending_to_us != 0) { return fmt::format( ngettext( "Downloading from {active_count} webseed", "Downloading from {active_count} webseeds", cache_.webseeds_sending_to_us), fmt::arg("active_count", cache_.webseeds_sending_to_us)); } return fmt::format( ngettext( "Downloading from {active_count} of {connected_count} connected peer", "Downloading from {active_count} of {connected_count} connected peers", cache_.peers_connected), fmt::arg("active_count", cache_.peers_sending_to_us), fmt::arg("connected_count", cache_.peers_connected)); case TR_STATUS_SEED: return fmt::format( ngettext( "Seeding to {active_count} of {connected_count} connected peer", "Seeding to {active_count} of {connected_count} connected peers", cache_.peers_connected), fmt::arg("active_count", cache_.peers_getting_from_us), fmt::arg("connected_count", cache_.peers_connected)); default: g_assert_not_reached(); return {}; } } Torrent::Torrent() : Glib::ObjectBase(typeid(Torrent)) { } Torrent::Torrent(tr_torrent* torrent) : Glib::ObjectBase(typeid(Torrent)) , impl_(std::make_unique(*this, torrent)) { g_assert(torrent != nullptr); } Glib::ustring const& Torrent::get_name_collated() const { return impl_->get_cache().name_collated; } tr_torrent_id_t Torrent::get_id() const { return tr_torrentId(impl_->get_raw_torrent()); } tr_torrent& Torrent::get_underlying() const { return *impl_->get_raw_torrent(); } float Torrent::get_speed_up() const { return impl_->get_cache().speed_up; } float Torrent::get_speed_down() const { return impl_->get_cache().speed_down; } int Torrent::get_active_peers_up() const { return impl_->get_cache().active_peers_up; } int Torrent::get_active_peers_down() const { return impl_->get_cache().active_peers_down; } float Torrent::get_recheck_progress() const { return impl_->get_cache().recheck_progress; } bool Torrent::get_active() const { return impl_->get_cache().active; } tr_torrent_activity Torrent::get_activity() const { return impl_->get_cache().activity; } bool Torrent::get_finished() const { return impl_->get_cache().finished; } tr_priority_t Torrent::get_priority() const { return impl_->get_cache().priority; } size_t Torrent::get_queue_position() const { return impl_->get_cache().queue_position; } unsigned int Torrent::get_trackers() const { return impl_->get_cache().trackers; } int Torrent::get_error_code() const { return impl_->get_cache().error_code; } Glib::ustring const& Torrent::get_error_message() const { return impl_->get_cache().error_message; } int Torrent::get_active_peer_count() const { return impl_->get_cache().active_peer_count; } uint64_t Torrent::get_total_size() const { return impl_->get_cache().total_size; } float Torrent::get_ratio() const { return impl_->get_cache().ratio; } time_t Torrent::get_eta() const { return impl_->get_cache().eta; } time_t Torrent::get_added_date() const { return impl_->get_cache().added_date; } float Torrent::get_percent_complete() const { return impl_->get_cache().percent_complete; } float Torrent::get_seed_ratio_percent_done() const { return impl_->get_cache().seed_ratio_percent_done; } Glib::RefPtr Torrent::get_icon() const { return impl_->get_icon(); } Glib::ustring Torrent::get_name() const { return impl_->get_cache().name; } float Torrent::get_percent_done() const { return impl_->get_cache().activity_percent_done; } Glib::ustring Torrent::get_short_status_text() const { return impl_->get_short_status_text(); } Glib::ustring Torrent::get_long_progress_text() const { return impl_->get_long_progress_text(); } Glib::ustring Torrent::get_long_status_text() const { return impl_->get_long_status_text(); } bool Torrent::get_sensitive() const { return impl_->get_cache().activity != TR_STATUS_STOPPED; } Torrent::ChangeFlags Torrent::update() { auto result = impl_->update_cache(); impl_->notify_property_changes(result); return result; } Glib::RefPtr Torrent::create(tr_torrent* torrent) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) return Glib::make_refptr_for_instance(new Torrent(torrent)); } Torrent::Columns const& Torrent::get_columns() { static Columns const columns; return columns; } int Torrent::get_item_id(Glib::RefPtr const& item) { if (auto const torrent = gtr_ptr_dynamic_cast(item); torrent != nullptr) { return torrent->get_id(); } return 0; } void Torrent::get_item_value(Glib::RefPtr const& item, int column, Glib::ValueBase& value) { if (auto const torrent = gtr_ptr_dynamic_cast(item); torrent != nullptr) { torrent->impl_->get_value(column, value); } } int Torrent::compare_by_id(Glib::RefPtr const& lhs, Glib::RefPtr const& rhs) { return gtr_compare_generic(lhs->get_id(), rhs->get_id()); } bool Torrent::less_by_id(Glib::RefPtr const& lhs, Glib::RefPtr const& rhs) { return lhs->get_id() < rhs->get_id(); }