diff --git a/gtk/CMakeLists.txt b/gtk/CMakeLists.txt index 82bef4bd3..7d956378d 100644 --- a/gtk/CMakeLists.txt +++ b/gtk/CMakeLists.txt @@ -13,7 +13,6 @@ target_sources(${TR_NAME}-gtk Dialogs.cc Dialogs.h FaviconCache.cc - FaviconCache.h FileList.cc FileList.h FilterBar.cc diff --git a/gtk/DetailsDialog.cc b/gtk/DetailsDialog.cc index 553491db1..9560b30c9 100644 --- a/gtk/DetailsDialog.cc +++ b/gtk/DetailsDialog.cc @@ -6,7 +6,6 @@ #include "DetailsDialog.h" #include "Actions.h" -#include "FaviconCache.h" // gtr_get_favicon() #include "FileList.h" #include "GtkCompat.h" #include "HigWorkarea.h" // GUI_PAD, GUI_PAD_BIG, GUI_PAD_SMALL @@ -2046,16 +2045,16 @@ tr_torrent* DetailsDialog::Impl::tracker_list_get_current_torrent() const namespace { -void favicon_ready_cb(Glib::RefPtr const& pixbuf, Gtk::TreeRowReference& reference) +void favicon_ready_cb(Glib::RefPtr const* pixbuf, Gtk::TreeRowReference& reference) { - if (pixbuf != nullptr) + if (pixbuf != nullptr && *pixbuf != nullptr) { auto const path = reference.get_path(); auto const model = reference.get_model(); if (auto const iter = model->get_iter(path); iter) { - (*iter)[tracker_cols.favicon] = pixbuf; + (*iter)[tracker_cols.favicon] = *pixbuf; } } } @@ -2067,7 +2066,6 @@ void DetailsDialog::Impl::refreshTracker(std::vector const& torrent std::ostringstream gstr; auto& hash = tracker_hash_; auto const& store = tracker_store_; - auto* session = core_->get_session(); bool const showScrape = scrape_check_->get_active(); /* step 1: get all the trackers */ @@ -2104,10 +2102,10 @@ void DetailsDialog::Impl::refreshTracker(std::vector const& torrent auto const p = store->get_path(iter); hash.try_emplace(gstr.str(), Gtk::TreeRowReference(store, p)); - gtr_get_favicon_from_url( - session, + core_->favicon_cache().load( tracker.announce, - [ref = Gtk::TreeRowReference(store, p)](auto const& pixbuf) mutable { favicon_ready_cb(pixbuf, ref); }); + [ref = Gtk::TreeRowReference(store, p)](auto const* pixbuf_refptr) mutable + { favicon_ready_cb(pixbuf_refptr, ref); }); } } diff --git a/gtk/FaviconCache.cc b/gtk/FaviconCache.cc index 069077f3d..0720704d0 100644 --- a/gtk/FaviconCache.cc +++ b/gtk/FaviconCache.cc @@ -3,164 +3,52 @@ // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. -#include "FaviconCache.h" - -#include "Utils.h" /* gtr_get_host_from_url() */ - -#include -#include -#include // tr_sessionFetch() +#include +#include +#include #include -#include #include #include -#include +using Icon = Glib::RefPtr; -#include -#include -#include -#include -#include - -#include /* g_remove() */ - -using namespace std::literals; - -namespace +template<> +Icon FaviconCache::create_from_file(std::string_view filename) const { - -constexpr auto TimeoutSecs = 15s; -constexpr auto ImageTypes = std::array{ "ico"sv, "png"sv, "gif"sv, "jpg"sv }; - -struct favicon_data -{ - tr_session* session = nullptr; - std::function const&)> func; - std::string host; - std::string contents; - size_t type = 0; - long code = 0; -}; - -Glib::ustring get_url(std::string const& host, size_t image_type) -{ - return fmt::format("http://{}/favicon.{}", host, ImageTypes.at(image_type)); -} - -std::string favicon_get_cache_dir() -{ - static std::string dir; - - if (dir.empty()) - { - dir = Glib::build_filename(Glib::get_user_cache_dir(), "transmission", "favicons"); - (void)g_mkdir_with_parents(dir.c_str(), 0777); - } - - return dir; -} - -std::string favicon_get_cache_filename(std::string const& host) -{ - return Glib::build_filename(favicon_get_cache_dir(), host); -} - -void favicon_save_to_cache(std::string const& host, std::string const& data) -{ - Glib::file_set_contents(favicon_get_cache_filename(host), data); -} - -Glib::RefPtr favicon_load_from_cache(std::string const& host) -{ - auto const filename = favicon_get_cache_filename(host); - try { - return Gdk::Pixbuf::create_from_file(filename, 16, 16, false); + return Gdk::Pixbuf::create_from_file(std::string{ filename }, Width, Height, false); } catch (Glib::Error const&) { - (void)g_remove(filename.c_str()); return {}; } } -void favicon_web_done_cb(tr_web::FetchResponse const& response); - -constexpr bool should_keep_trying(long code) +template<> +Icon FaviconCache::create_from_data(void const* data, size_t datalen) const { - return code != 0 && code <= 500; -} - -bool favicon_web_done_idle_cb(std::unique_ptr fav) -{ - Glib::RefPtr pixbuf; - - if (!fav->contents.empty()) /* we got something... try to make a pixbuf from it */ + try { - favicon_save_to_cache(fav->host, fav->contents); - pixbuf = favicon_load_from_cache(fav->host); + auto memory_stream = Gio::MemoryInputStream::create(); + memory_stream->add_data(data, datalen, nullptr); + return Gdk::Pixbuf::create_from_stream_at_scale(memory_stream, Width, Height, false); } - - if (pixbuf == nullptr && should_keep_trying(fav->code) && ++fav->type < ImageTypes.size()) /* keep trying */ + catch (Glib::Error const&) { - fav->contents.clear(); - auto* const session = fav->session; - auto const next_url = get_url(fav->host, fav->type); - tr_sessionFetch(session, { next_url.raw(), favicon_web_done_cb, fav.release(), TimeoutSecs }); - return false; - } - - // Not released into the next web request, means we're done trying (even if `pixbuf` is still invalid) - fav->func(pixbuf); - return false; -} - -void favicon_web_done_cb(tr_web::FetchResponse const& response) -{ - auto* const fav = static_cast(response.user_data); - fav->contents = response.body; - fav->code = response.status; - Glib::signal_idle().connect([fav]() { return favicon_web_done_idle_cb(std::unique_ptr(fav)); }); -} - -} // namespace - -void gtr_get_favicon( - tr_session* session, - std::string const& host, - std::function const&)> const& pixbuf_ready_func) -{ - auto pixbuf = favicon_load_from_cache(host); - - if (pixbuf != nullptr) - { - pixbuf_ready_func(pixbuf); - } - else - { - auto data = std::make_unique(); - data->session = session; - data->func = pixbuf_ready_func; - data->host = host; - tr_sessionFetch(session, { get_url(host, 0).raw(), favicon_web_done_cb, data.release(), TimeoutSecs }); + return {}; } } -void gtr_get_favicon_from_url( - tr_session* session, - Glib::ustring const& url, - std::function const&)> const& pixbuf_ready_func) +template<> +std::string FaviconCache::app_cache_dir() const { - if (auto const parsed_url = tr_urlParse(url.c_str()); parsed_url.has_value()) - { - auto const host = std::string{ parsed_url->host }; - gtr_get_favicon(session, host, pixbuf_ready_func); - } - else - { - pixbuf_ready_func({}); - } + return fmt::format("{:s}/{:s}", Glib::get_user_cache_dir(), "transmission"); +} + +template<> +void FaviconCache::add_to_ui_thread(std::function idlefunc) +{ + Glib::signal_idle().connect_once([idlefunc = std::move(idlefunc)]() { idlefunc(); }); } diff --git a/gtk/FaviconCache.h b/gtk/FaviconCache.h deleted file mode 100644 index 5b444a3c5..000000000 --- a/gtk/FaviconCache.h +++ /dev/null @@ -1,25 +0,0 @@ -// This file Copyright © 2012-2023 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. - -#pragma once - -#include - -#include -#include -#include - -#include -#include - -void gtr_get_favicon( - tr_session* session, - std::string const& host, - std::function const&)> const& pixbuf_ready_func); - -void gtr_get_favicon_from_url( - tr_session* session, - Glib::ustring const& url, - std::function const&)> const& pixbuf_ready_func); diff --git a/gtk/FilterBar.cc b/gtk/FilterBar.cc index 26970c6d8..9e5a2705e 100644 --- a/gtk/FilterBar.cc +++ b/gtk/FilterBar.cc @@ -5,7 +5,6 @@ #include "FilterBar.h" -#include "FaviconCache.h" // gtr_get_favicon() #include "FilterListModel.hh" #include "HigWorkarea.h" // GUI_PAD #include "ListModelAdapter.h" @@ -79,7 +78,7 @@ private: bool activity_filter_model_update(); bool tracker_filter_model_update(); - void favicon_ready_cb(Glib::RefPtr const& pixbuf, Gtk::TreeModel::Path const& path); + void favicon_ready_cb(Glib::RefPtr const* pixbuf, Gtk::TreeModel::Path const& path); void update_filter_models(Torrent::ChangeFlags changes); void update_filter_models_idle(Torrent::ChangeFlags changes); @@ -172,13 +171,13 @@ void FilterBar::Impl::tracker_model_update_count(Gtk::TreeModel::iterator const& } } -void FilterBar::Impl::favicon_ready_cb(Glib::RefPtr const& pixbuf, Gtk::TreeModel::Path const& path) +void FilterBar::Impl::favicon_ready_cb(Glib::RefPtr const* pixbuf, Gtk::TreeModel::Path const& path) { - if (pixbuf != nullptr) + if (pixbuf != nullptr && *pixbuf != nullptr) { if (auto const iter = tracker_model_->get_iter(path); iter) { - iter->set_value(tracker_filter_cols.pixbuf, pixbuf); + iter->set_value(tracker_filter_cols.pixbuf, *pixbuf); } } } @@ -190,6 +189,7 @@ bool FilterBar::Impl::tracker_filter_model_update() int count = 0; std::string host; std::string sitename; + std::string announce_url; bool operator<(site_info const& that) const { @@ -214,18 +214,19 @@ bool FilterBar::Impl::tracker_filter_model_update() auto const& raw_torrent = torrent->get_underlying(); - auto torrent_sites_and_hosts = std::map{}; + auto site_to_host_and_announce = std::map>{}; for (size_t j = 0, n = tr_torrentTrackerCount(&raw_torrent); j < n; ++j) { auto const view = tr_torrentTracker(&raw_torrent, j); - torrent_sites_and_hosts.try_emplace(std::data(view.sitename), view.host); + site_to_host_and_announce.try_emplace(std::data(view.sitename), view.host, view.announce); } - for (auto const& [sitename, host] : torrent_sites_and_hosts) + for (auto const& [sitename, host_and_announce] : site_to_host_and_announce) { auto& info = site_infos[sitename]; + info.host = host_and_announce.first; + info.announce_url = host_and_announce.second; info.sitename = sitename; - info.host = host; ++info.count; } @@ -299,10 +300,9 @@ bool FilterBar::Impl::tracker_filter_model_update() add->set_value(tracker_filter_cols.count, site.count); add->set_value(tracker_filter_cols.type, static_cast(TrackerType::HOST)); auto path = tracker_model_->get_path(add); - gtr_get_favicon( - core_->get_session(), - site.host, - [this, path](auto const& pixbuf) { favicon_ready_cb(pixbuf, path); }); + core_->favicon_cache().load( + site.announce_url, + [this, path](auto const* pixbuf) { favicon_ready_cb(pixbuf, path); }); ++i; } else // update row diff --git a/gtk/Session.cc b/gtk/Session.cc index 36fdb1ed7..002d09229 100644 --- a/gtk/Session.cc +++ b/gtk/Session.cc @@ -147,6 +147,11 @@ public: return signal_torrents_changed_; } + [[nodiscard]] constexpr auto& favicon_cache() + { + return favicon_cache_; + } + private: Glib::RefPtr get_core_ptr() const; @@ -209,6 +214,8 @@ private: Glib::RefPtr> sorted_model_; Glib::RefPtr sorter_ = TorrentSorter::create(); tr_session* session_ = nullptr; + + FaviconCache> favicon_cache_; }; Glib::RefPtr Session::Impl::get_core_ptr() const @@ -531,8 +538,8 @@ Session::Session(tr_session* session) Session::~Session() = default; Session::Impl::Impl(Session& core, tr_session* session) - : core_(core) - , session_(session) + : core_{ core } + , session_{ session } { raw_model_ = Gio::ListStore::create(); signal_torrents_changed_.connect(sigc::hide<0>(sigc::mem_fun(*sorter_.get(), &TorrentSorter::update))); @@ -1359,6 +1366,11 @@ tr_torrent* Session::find_torrent(tr_torrent_id_t id) const return tor; } +FaviconCache>& Session::favicon_cache() const +{ + return impl_->favicon_cache(); +} + void Session::open_folder(tr_torrent_id_t torrent_id) const { auto const* tor = find_torrent(torrent_id); diff --git a/gtk/Session.h b/gtk/Session.h index b010d04d8..850d249a6 100644 --- a/gtk/Session.h +++ b/gtk/Session.h @@ -8,9 +8,11 @@ #include "Torrent.h" #include +#include #include #include +#include #include #include #include @@ -58,6 +60,8 @@ public: tr_torrent* find_torrent(tr_torrent_id_t id) const; + FaviconCache>& favicon_cache() const; + /****** ******* ******/ diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index 5680ec2c7..e0f735eb4 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -50,6 +50,7 @@ target_sources(${TR_NAME} error-types.h error.cc error.h + favicon-cache.h file-piece-map.cc file-piece-map.h file-posix.cc @@ -304,6 +305,7 @@ if(INSTALL_LIB) FILES error-types.h error.h + favicon-cache.h file.h log.h makemeta.h diff --git a/libtransmission/favicon-cache.h b/libtransmission/favicon-cache.h new file mode 100644 index 000000000..066e1f966 --- /dev/null +++ b/libtransmission/favicon-cache.h @@ -0,0 +1,245 @@ +// This file Copyright © 2012-2023 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. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +template +class FaviconCache +{ +public: + using IconFunc = std::function; + + FaviconCache() + : cache_dir_{ app_cache_dir() } + , icons_dir_{ fmt::format("{:s}/{:s}", cache_dir_, "favicons") } + , scraped_sitenames_filename_{ fmt::format("{:s}/favicons-scraped.txt", cache_dir_) } + { + } + + [[nodiscard]] Icon const* find(std::string_view sitename) const noexcept + { + auto const iter = icons_.find(sitename); + return iter != std::end(icons_) ? &iter->second : nullptr; + } + + void load( + std::string_view url_in, + IconFunc callback = [](Icon const&) {}) + { + std::call_once(scan_once_flag_, &FaviconCache::scan_file_cache, this); + + auto const url = tr_urlParse(url_in); + if (!url) // invalid url? + { + callback({}); + return; + } + + // If we already have it, use it. + if (auto const* const icon = find(url->sitename); icon != nullptr) + { + callback(icon); + return; + } + + // We don't already have it, so fetch it. + // Add a placeholder to icons_ prevent repeat downloads. + icons_.try_emplace(std::string{ url->sitename }); + mark_site_as_scraped(url->sitename); + + // ports to try + auto n_ports = 0; + auto ports = std::array{}; + ports[n_ports++] = 80; + if (url->port != 80) + { + ports[n_ports++] = url->port; + } + + auto in_flight = std::make_shared(callback, url->sitename); + for (auto i = 0; i < n_ports; ++i) + { + for (auto const scheme : { "http", "https" }) + { + for (auto const suffix : { "ico", "png", "gif", "jpg" }) + { + auto on_fetch_response = [this, in_flight](auto const& response) + { + in_flight->add_response(response.body, response.status); + add_to_ui_thread([this, in_flight]() { check_responses(in_flight); }); + }; + + static constexpr auto TimeoutSecs = std::chrono::seconds{ 15 }; + auto const favicon_url = fmt::format("{:s}://{:s}:{:d}/favicon.{:s}", scheme, url->host, ports[i], suffix); + in_flight->web().fetch({ favicon_url, std::move(on_fetch_response), nullptr, TimeoutSecs }); + } + } + } + } + + static inline constexpr auto Width = 16; + static inline constexpr auto Height = 16; + +private: + class InFlightData + { + public: + InFlightData(IconFunc callback, std::string_view sitename) + : callback_{ std::move(callback) } + , sitename_{ sitename } + { + } + + [[nodiscard]] constexpr auto const& sitename() const noexcept + { + return sitename_; + } + + ~InFlightData() + { + invoke_callback(nullptr); // ensure it's called once, even if no icon + } + + void invoke_callback(Icon const* icon) + { + if (callback_) + { + callback_(icon); + callback_ = {}; + } + } + + [[nodiscard]] auto get_responses() + { + auto lock = std::lock_guard{ responses_mutex_ }; + + auto tmp = decltype(responses_){}; + std::swap(tmp, responses_); + return tmp; + } + + void add_response(std::string contents, long code) + { + auto lock = std::lock_guard{ responses_mutex_ }; + + responses_.emplace_back(std::move(contents), code); + } + + [[nodiscard]] auto& web() + { + return *web_; + } + + private: + IconFunc callback_; + std::string const sitename_; + + std::mutex responses_mutex_; + std::vector> responses_; + + tr_web::Mediator mediator_; + std::unique_ptr web_ = tr_web::create(mediator_); + }; + + [[nodiscard]] Icon create_from_file(std::string_view filename) const; + [[nodiscard]] Icon create_from_data(void const* data, size_t datalen) const; + [[nodiscard]] std::string app_cache_dir() const; + void add_to_ui_thread(std::function idlefunc); + + void scan_file_cache() + { + // ensure the folders exist + tr_sys_dir_create(cache_dir_, TR_SYS_DIR_CREATE_PARENTS, 0700); + tr_sys_dir_create(icons_dir_, TR_SYS_DIR_CREATE_PARENTS, 0700); + + // remember which hosts we've asked for a favicon so that we + // don't re-ask them every time we start a new session + if (auto ifs = std::ifstream{ scraped_sitenames_filename_ }; ifs.is_open()) + { + auto sitename = std::string{}; + while (std::getline(ifs, sitename)) + { + icons_.try_emplace(sitename); + } + } + + // load the cached favicons + for (auto const& sitename : tr_sys_dir_get_files(icons_dir_)) + { + auto const filename = fmt::format("{:s}/{:s}", icons_dir_, sitename); + + if (auto icon = create_from_file(filename); !icon) + { + tr_sys_path_remove(filename); + } + else + { + icons_[sitename] = std::move(icon); + } + } + } + + void mark_site_as_scraped(std::string_view sitename) + { + if (auto ofs = std::ofstream{ scraped_sitenames_filename_, std::ios_base::out | std::ios_base::app }; ofs.is_open()) + { + ofs << sitename << '\n'; + } + } + + void check_responses(std::shared_ptr in_flight) + { + for (auto const& [contents, code] : in_flight->get_responses()) + { + if (std::empty(contents) || code < 200 || code >= 300) + { + continue; + } + + auto const icon = create_from_data(std::data(contents), std::size(contents)); + if (!icon) + { + continue; + } + + // cache it in memory + auto& perm = icons_[in_flight->sitename()]; + perm = std::move(icon); + + // cache it on disk + tr_saveFile(fmt::format("{:s}/{:s}", icons_dir_, in_flight->sitename()), contents); + + // notify the user that we got it + in_flight->invoke_callback(&perm); + return; + } + } + + std::once_flag scan_once_flag_; + std::string const cache_dir_; + std::string const icons_dir_; + std::string const scraped_sitenames_filename_; + + std::map> icons_; +}; diff --git a/libtransmission/web.h b/libtransmission/web.h index 2111c6499..d77509749 100644 --- a/libtransmission/web.h +++ b/libtransmission/web.h @@ -156,7 +156,10 @@ public: func(response); } - [[nodiscard]] virtual time_t now() const = 0; + [[nodiscard]] virtual time_t now() const + { + return time(nullptr); + } }; // Note that tr_web does no management of the `mediator` reference. diff --git a/qt/Application.cc b/qt/Application.cc index 181d009eb..76dd2b1b5 100644 --- a/qt/Application.cc +++ b/qt/Application.cc @@ -656,11 +656,6 @@ void Application::onNotificationActionInvoked(quint32 /* notification_id */, QSt } #endif -FaviconCache& Application::faviconCache() -{ - return favicons_; -} - /*** **** ***/ diff --git a/qt/Application.h b/qt/Application.h index d821c924b..6dd48c1b5 100644 --- a/qt/Application.h +++ b/qt/Application.h @@ -13,10 +13,11 @@ #include #include #include +#include #include +#include -#include "FaviconCache.h" #include "Typedefs.h" #include "Utils.h" // std::hash @@ -44,7 +45,30 @@ public: return *interned_strings_.insert(in).first; } - FaviconCache& faviconCache(); + [[nodiscard]] QPixmap find_favicon(QString const& sitename) const + { + auto const key = sitename.toStdString(); + auto const* const icon = favicon_cache_.find(key); + return icon != nullptr ? *icon : QPixmap{}; + } + + void load_favicon(QString const& url) + { + auto weak_self = QPointer{ this }; + + favicon_cache_.load( + url.toStdString(), + [weak_self = std::move(weak_self)](QPixmap const* /*favicon_or_nullptr*/) + { + if (!weak_self.isNull()) + { + weak_self.data()->faviconsChanged(); + } + }); + } + +signals: + void faviconsChanged(); public slots: void addTorrent(AddData const&) const; @@ -82,7 +106,8 @@ private: time_t last_full_update_time_ = {}; QTranslator qt_translator_; QTranslator app_translator_; - FaviconCache favicons_; + + FaviconCache favicon_cache_; QString const config_name_ = QStringLiteral("transmission"); QString const display_name_ = QStringLiteral("transmission-qt"); diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index 2aa42a601..77fa1f047 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -21,7 +21,6 @@ target_sources(${TR_NAME}-qt DetailsDialog.cc DetailsDialog.h FaviconCache.cc - FaviconCache.h FileTreeDelegate.cc FileTreeDelegate.h FileTreeItem.cc diff --git a/qt/FaviconCache.cc b/qt/FaviconCache.cc index d64d87b97..964944f8e 100644 --- a/qt/FaviconCache.cc +++ b/qt/FaviconCache.cc @@ -1,211 +1,49 @@ -// This file Copyright © 2012-2023 Mnemosyne LLC. +// This file Copyright © 2023 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 +#include -#include -#include -#include -#include +#include +#include #include -#include "FaviconCache.h" +using Icon = QPixmap; -/*** -**** -***/ - -FaviconCache::FaviconCache() - : nam_(new QNetworkAccessManager(this)) +template<> +Icon FaviconCache::create_from_file(std::string_view filename) const // NOLINT(readability-identifier-naming) { - connect(nam_, &QNetworkAccessManager::finished, this, &FaviconCache::onRequestFinished); -} - -/*** -**** -***/ - -namespace -{ - -QPixmap scale(QPixmap const& pixmap) -{ - return pixmap.scaled(FaviconCache::getIconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation); -} - -QString getCacheDir() -{ - auto const base = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); - return QDir(base).absoluteFilePath(QStringLiteral("favicons")); -} - -QString getScrapedFile() -{ - auto const base = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); - return QDir(base).absoluteFilePath(QStringLiteral("favicons-scraped.txt")); -} - -void markSiteAsScraped(QString const& sitename) -{ - auto skip_file = QFile(getScrapedFile()); - if (skip_file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) + auto icon = QPixmap{}; + if (!icon.load(QString::fromUtf8(std::data(filename), std::size(filename)))) { - skip_file.write(sitename.toUtf8()); - skip_file.write("\n"); - } -} - -} // namespace - -void FaviconCache::ensureCacheDirHasBeenScanned() -{ - static bool has_been_scanned = false; - if (has_been_scanned) - { - return; + return {}; } - has_been_scanned = true; - - // remember which hosts we've asked for a favicon so that we - // don't re-ask them every time we start a new session - if (auto skip_file = QFile(getScrapedFile()); skip_file.open(QIODevice::ReadOnly | QIODevice::Text)) - { - while (!skip_file.atEnd()) - { - auto const sitename = QString::fromUtf8(skip_file.readLine()).trimmed(); - pixmaps_.try_emplace(sitename); - } - } - - // load the cached favicons - auto cache_dir = QDir(getCacheDir()); - cache_dir.mkpath(cache_dir.absolutePath()); - for (auto const& sitename : cache_dir.entryList(QDir::Files | QDir::Readable)) - { - QPixmap const pixmap(cache_dir.absoluteFilePath(sitename)); - if (!pixmap.isNull()) - { - pixmaps_[sitename] = scale(pixmap); - } - } + return icon.scaled({ Width, Height }, Qt::KeepAspectRatio, Qt::SmoothTransformation); } -/*** -**** -***/ - -QString FaviconCache::getDisplayName(QString const& sitename) +template<> +Icon FaviconCache::create_from_data(void const* data, size_t datalen) const // NOLINT(readability-identifier-naming) { - auto name = sitename; - if (!name.isEmpty()) + auto icon = QPixmap{}; + if (!icon.loadFromData(static_cast(data), datalen)) { - name.front() = name.front().toTitleCase(); + return {}; } - return name; + + return icon.scaled({ Width, Height }, Qt::KeepAspectRatio, Qt::SmoothTransformation); } -QSize FaviconCache::getIconSize() +template<> +std::string FaviconCache::app_cache_dir() const // NOLINT(readability-identifier-naming) { - return { 16, 16 }; + return QStandardPaths::writableLocation(QStandardPaths::CacheLocation).toStdString(); } -QPixmap FaviconCache::find(QString const& sitename) +template<> +void FaviconCache::add_to_ui_thread(std::function idlefunc) // NOLINT(readability-identifier-naming) { - ensureCacheDirHasBeenScanned(); - - return pixmaps_[sitename]; -} - -void FaviconCache::add(QString const& sitename, QString const& url_str) -{ - ensureCacheDirHasBeenScanned(); - - // Try to download a favicon if we don't have one. - // Add a placeholder to prevent repeat downloads. - if (auto const already_had_it = !pixmaps_.try_emplace(sitename).second; already_had_it) - { - return; - } - - markSiteAsScraped(sitename); - - auto const scrape = [this, sitename](auto const host) - { - auto const schemes = std::array{ - QStringLiteral("http"), - QStringLiteral("https"), - }; - auto const suffixes = std::array{ - QStringLiteral("gif"), // - QStringLiteral("ico"), // - QStringLiteral("jpg"), // - QStringLiteral("png"), // - QStringLiteral("svg"), // - }; - for (auto const& scheme : schemes) - { - for (auto const& suffix : suffixes) - { - auto const path = QStringLiteral("%1://%2/favicon.%3").arg(scheme).arg(host).arg(suffix); - auto request = QNetworkRequest(path); - request.setAttribute(QNetworkRequest::UserMax, sitename); - nam_->get(request); - } - } - }; - - // scrape tracker.domain.com - auto const host = QUrl(url_str).host(); - scrape(host); - - if (auto const idx = host.indexOf(sitename); idx != -1) - { - // scrape domain.com - auto const root = host.mid(idx); - if (root != host) - { - scrape(root); - } - - // scrape www.domain.com - if (auto const www = QStringLiteral("www.") + root; www != host) - { - scrape(www); - } - } -} - -void FaviconCache::onRequestFinished(QNetworkReply* reply) -{ - auto const content = reply->readAll(); - auto pixmap = QPixmap{}; - - if (reply->error() == QNetworkReply::NoError) - { - pixmap.loadFromData(content); - } - - if (!pixmap.isNull()) - { - auto sitename = reply->request().attribute(QNetworkRequest::UserMax).toString(); - - // save it in memory... - pixmaps_[sitename] = scale(pixmap); - - // save it on disk... - QDir const cache_dir(getCacheDir()); - cache_dir.mkpath(cache_dir.absolutePath()); - QFile file(cache_dir.absoluteFilePath(sitename)); - file.open(QIODevice::WriteOnly); - file.write(content); - file.close(); - - // notify listeners - emit pixmapReady(sitename); - } - - reply->deleteLater(); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + QMetaObject::invokeMethod(qApp, std::move(idlefunc), Qt::QueuedConnection); } diff --git a/qt/FaviconCache.h b/qt/FaviconCache.h deleted file mode 100644 index d298c7978..000000000 --- a/qt/FaviconCache.h +++ /dev/null @@ -1,47 +0,0 @@ -// This file Copyright © 2012-2023 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. - -#pragma once - -#include - -#include -#include -#include - -#include "Utils.h" // std::hash - -class QNetworkAccessManager; -class QNetworkReply; -class QUrl; - -class FaviconCache : public QObject -{ - Q_OBJECT - -public: - FaviconCache(); - - // This will emit a signal when (if) the icon becomes ready. - void add(QString const& sitename, QString const& url); - - // returns a cached pixmap, or a nullptr pixmap if there's no match in the cache - QPixmap find(QString const& sitename); - - static QString getDisplayName(QString const& sitename); - static QSize getIconSize(); - -signals: - void pixmapReady(QString const& sitename); - -private slots: - void onRequestFinished(QNetworkReply* reply); - -private: - void ensureCacheDirHasBeenScanned(); - - QNetworkAccessManager* nam_ = {}; - std::unordered_map pixmaps_; -}; diff --git a/qt/FilterBar.cc b/qt/FilterBar.cc index 91e4d2df6..9390d054c 100644 --- a/qt/FilterBar.cc +++ b/qt/FilterBar.cc @@ -16,7 +16,6 @@ #include #include "Application.h" -#include "FaviconCache.h" #include "FilterBarComboBox.h" #include "FilterBarComboBoxDelegate.h" #include "Filters.h" @@ -93,7 +92,7 @@ FilterBarComboBox* FilterBar::createActivityCombo() namespace { -QString getCountString(size_t n) +[[nodiscard]] auto getCountString(size_t n) { return QStringLiteral("%L1").arg(n); } @@ -102,6 +101,18 @@ Torrent::fields_t constexpr TrackerFields = { static_cast(1) << Torrent::TRACKER_STATS, }; +[[nodiscard]] auto displayName(QString const& sitename) +{ + auto name = sitename; + + if (!name.isEmpty()) + { + name.front() = name.front().toTitleCase(); + } + + return name; +} + auto constexpr ActivityFields = FilterMode::TorrentFields; } // namespace @@ -133,13 +144,12 @@ void FilterBar::refreshTrackers() auto update_tracker_item = [](QStandardItem* i, auto const& it) { auto const& [sitename, count] = *it; - auto const display_name = FaviconCache::getDisplayName(sitename); - auto const icon = trApp->faviconCache().find(sitename); + auto const display_name = displayName(sitename); i->setData(display_name, Qt::DisplayRole); i->setData(display_name, TRACKER_ROLE); i->setData(getCountString(static_cast(count)), FilterBarComboBox::CountStringRole); - i->setData(icon, Qt::DecorationRole); + i->setData(trApp->find_favicon(sitename), Qt::DecorationRole); i->setData(static_cast(count), FilterBarComboBox::CountRole); return i; @@ -243,7 +253,7 @@ FilterBar::FilterBar(Prefs& prefs, TorrentModel const& torrents, TorrentFilter c connect(&torrents_, &TorrentModel::rowsRemoved, this, &FilterBar::recountAllSoon); connect(&torrents_, &TorrentModel::torrentsChanged, this, &FilterBar::onTorrentsChanged); connect(&recount_timer_, &QTimer::timeout, this, &FilterBar::recount); - connect(&trApp->faviconCache(), &FaviconCache::pixmapReady, this, &FilterBar::recountTrackersSoon); + connect(trApp, &Application::faviconsChanged, this, &FilterBar::recountTrackersSoon); recountAllSoon(); is_bootstrapping_ = false; // NOLINT cppcoreguidelines-prefer-member-initializer diff --git a/qt/FilterBar.h b/qt/FilterBar.h index af7504a54..70e1be027 100644 --- a/qt/FilterBar.h +++ b/qt/FilterBar.h @@ -15,7 +15,6 @@ #include -#include "FaviconCache.h" #include "Torrent.h" #include "Typedefs.h" diff --git a/qt/Torrent.cc b/qt/Torrent.cc index e959bbe8a..0b2f38aa0 100644 --- a/qt/Torrent.cc +++ b/qt/Torrent.cc @@ -359,5 +359,5 @@ QString Torrent::getError() const QPixmap TrackerStat::getFavicon() const { - return trApp->faviconCache().find(sitename); + return trApp->find_favicon(sitename); } diff --git a/qt/Torrent.h b/qt/Torrent.h index ebc795678..fa070668a 100644 --- a/qt/Torrent.h +++ b/qt/Torrent.h @@ -23,7 +23,6 @@ #include #include -#include "FaviconCache.h" #include "IconCache.h" #include "Speed.h" diff --git a/qt/TrackerDelegate.cc b/qt/TrackerDelegate.cc index 3afb13282..0705d93df 100644 --- a/qt/TrackerDelegate.cc +++ b/qt/TrackerDelegate.cc @@ -10,8 +10,8 @@ #include #include +#include -#include "FaviconCache.h" #include "Formatter.h" #include "Torrent.h" #include "TrackerDelegate.h" @@ -58,7 +58,7 @@ ItemLayout::ItemLayout( QPoint const& top_left, int width) { - QSize const icon_size = FaviconCache::getIconSize(); + auto const icon_size = QSize{ FaviconCache::Width, FaviconCache::Height }; QRect base_rect(top_left, QSize(width, 0)); diff --git a/qt/VariantHelpers.cc b/qt/VariantHelpers.cc index dba1064e4..a768452ae 100644 --- a/qt/VariantHelpers.cc +++ b/qt/VariantHelpers.cc @@ -180,7 +180,7 @@ bool change(TrackerStat& setme, tr_variant const* value) if (site_changed && !setme.sitename.isEmpty() && !setme.announce.isEmpty()) { setme.announce = trApp->intern(setme.announce); - trApp->faviconCache().add(setme.sitename, setme.announce); + trApp->load_favicon(setme.announce); } return changed;