1
0
Fork 0
mirror of https://github.com/transmission/transmission synced 2025-01-30 10:52:00 +00:00

feat: add generic favicon_cache class for reuse between Qt, GTK app (#5294)

This commit is contained in:
Charles Kerr 2023-04-22 21:11:16 -05:00 committed by GitHub
parent 9d2507c7e3
commit 1b6b85989c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 385 additions and 441 deletions

View file

@ -13,7 +13,6 @@ target_sources(${TR_NAME}-gtk
Dialogs.cc
Dialogs.h
FaviconCache.cc
FaviconCache.h
FileList.cc
FileList.h
FilterBar.cc

View file

@ -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<Gdk::Pixbuf> const& pixbuf, Gtk::TreeRowReference& reference)
void favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> 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<tr_torrent*> 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<tr_torrent*> 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); });
}
}

View file

@ -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 <libtransmission/transmission.h>
#include <libtransmission/web-utils.h>
#include <libtransmission/web.h> // tr_sessionFetch()
#include <libtransmission/favicon-cache.h>
#include <gdkmm/pixbuf.h>
#include <giomm/memoryinputstream.h>
#include <glibmm/error.h>
#include <glibmm/fileutils.h>
#include <glibmm/main.h>
#include <glibmm/miscutils.h>
#include <fmt/core.h>
using Icon = Glib::RefPtr<Gdk::Pixbuf>;
#include <array>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <glib/gstdio.h> /* g_remove() */
using namespace std::literals;
namespace
template<>
Icon FaviconCache<Icon>::create_from_file(std::string_view filename) const
{
constexpr auto TimeoutSecs = 15s;
constexpr auto ImageTypes = std::array<std::string_view, 4>{ "ico"sv, "png"sv, "gif"sv, "jpg"sv };
struct favicon_data
{
tr_session* session = nullptr;
std::function<void(Glib::RefPtr<Gdk::Pixbuf> 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<Gdk::Pixbuf> 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<Icon>::create_from_data(void const* data, size_t datalen) const
{
return code != 0 && code <= 500;
}
bool favicon_web_done_idle_cb(std::unique_ptr<favicon_data> fav)
{
Glib::RefPtr<Gdk::Pixbuf> 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<favicon_data*>(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<favicon_data>(fav)); });
}
} // namespace
void gtr_get_favicon(
tr_session* session,
std::string const& host,
std::function<void(Glib::RefPtr<Gdk::Pixbuf> 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<favicon_data>();
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<void(Glib::RefPtr<Gdk::Pixbuf> const&)> const& pixbuf_ready_func)
template<>
std::string FaviconCache<Icon>::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<Icon>::add_to_ui_thread(std::function<void()> idlefunc)
{
Glib::signal_idle().connect_once([idlefunc = std::move(idlefunc)]() { idlefunc(); });
}

View file

@ -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 <libtransmission/transmission.h>
#include <gdkmm/pixbuf.h>
#include <glibmm/refptr.h>
#include <glibmm/ustring.h>
#include <functional>
#include <string>
void gtr_get_favicon(
tr_session* session,
std::string const& host,
std::function<void(Glib::RefPtr<Gdk::Pixbuf> const&)> const& pixbuf_ready_func);
void gtr_get_favicon_from_url(
tr_session* session,
Glib::ustring const& url,
std::function<void(Glib::RefPtr<Gdk::Pixbuf> const&)> const& pixbuf_ready_func);

View file

@ -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<Gdk::Pixbuf> const& pixbuf, Gtk::TreeModel::Path const& path);
void favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> 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<Gdk::Pixbuf> const& pixbuf, Gtk::TreeModel::Path const& path)
void FilterBar::Impl::favicon_ready_cb(Glib::RefPtr<Gdk::Pixbuf> 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<std::string, std::string>{};
auto site_to_host_and_announce = std::map<std::string, std::pair<std::string, std::string>>{};
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<int>(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

View file

@ -147,6 +147,11 @@ public:
return signal_torrents_changed_;
}
[[nodiscard]] constexpr auto& favicon_cache()
{
return favicon_cache_;
}
private:
Glib::RefPtr<Session> get_core_ptr() const;
@ -209,6 +214,8 @@ private:
Glib::RefPtr<SortListModel<Torrent>> sorted_model_;
Glib::RefPtr<TorrentSorter> sorter_ = TorrentSorter::create();
tr_session* session_ = nullptr;
FaviconCache<Glib::RefPtr<Gdk::Pixbuf>> favicon_cache_;
};
Glib::RefPtr<Session> 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<Torrent>::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<Glib::RefPtr<Gdk::Pixbuf>>& 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);

View file

@ -8,9 +8,11 @@
#include "Torrent.h"
#include <libtransmission/transmission.h>
#include <libtransmission/favicon-cache.h>
#include <libtransmission/tr-macros.h>
#include <libtransmission/variant.h>
#include <gdkmm/pixbuf.h>
#include <giomm/file.h>
#include <giomm/listmodel.h>
#include <glibmm/object.h>
@ -58,6 +60,8 @@ public:
tr_torrent* find_torrent(tr_torrent_id_t id) const;
FaviconCache<Glib::RefPtr<Gdk::Pixbuf>>& favicon_cache() const;
/******
*******
******/

View file

@ -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

View file

@ -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 <array>
#include <chrono>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <vector>
#include <fmt/core.h>
#include <libtransmission/file.h>
#include <libtransmission/utils.h>
#include <libtransmission/web-utils.h>
#include <libtransmission/web.h>
template<typename Icon>
class FaviconCache
{
public:
using IconFunc = std::function<void(Icon const*)>;
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<int, 2>{};
ports[n_ports++] = 80;
if (url->port != 80)
{
ports[n_ports++] = url->port;
}
auto in_flight = std::make_shared<InFlightData>(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<std::pair<std::string, long>> responses_;
tr_web::Mediator mediator_;
std::unique_ptr<tr_web> 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<void()> 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<FaviconCache::InFlightData> 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<std::string /*sitename*/, Icon, std::less<>> icons_;
};

View file

@ -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.

View file

@ -656,11 +656,6 @@ void Application::onNotificationActionInvoked(quint32 /* notification_id */, QSt
}
#endif
FaviconCache& Application::faviconCache()
{
return favicons_;
}
/***
****
***/

View file

@ -13,10 +13,11 @@
#include <QRegularExpression>
#include <QTimer>
#include <QTranslator>
#include <QWeakPointer>
#include <libtransmission/tr-macros.h>
#include <libtransmission/favicon-cache.h>
#include "FaviconCache.h"
#include "Typedefs.h"
#include "Utils.h" // std::hash<QString>
@ -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<Application>{ 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<QPixmap> favicon_cache_;
QString const config_name_ = QStringLiteral("transmission");
QString const display_name_ = QStringLiteral("transmission-qt");

View file

@ -21,7 +21,6 @@ target_sources(${TR_NAME}-qt
DetailsDialog.cc
DetailsDialog.h
FaviconCache.cc
FaviconCache.h
FileTreeDelegate.cc
FileTreeDelegate.h
FileTreeItem.cc

View file

@ -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 <array>
#include <libtransmission/favicon-cache.h>
#include <QDir>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QApplication>
#include <QPixmap>
#include <QStandardPaths>
#include "FaviconCache.h"
using Icon = QPixmap;
/***
****
***/
FaviconCache::FaviconCache()
: nam_(new QNetworkAccessManager(this))
template<>
Icon FaviconCache<Icon>::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<Icon>::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<uchar const*>(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<Icon>::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<Icon>::add_to_ui_thread(std::function<void()> 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<QString, 2>{
QStringLiteral("http"),
QStringLiteral("https"),
};
auto const suffixes = std::array<QString, 5>{
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);
}

View file

@ -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 <unordered_map>
#include <QObject>
#include <QPixmap>
#include <QString>
#include "Utils.h" // std::hash<QString>
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<QString /*sitename*/, QPixmap> pixmaps_;
};

View file

@ -16,7 +16,6 @@
#include <QStandardItemModel>
#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<uint64_t>(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<size_t>(count)), FilterBarComboBox::CountStringRole);
i->setData(icon, Qt::DecorationRole);
i->setData(trApp->find_favicon(sitename), Qt::DecorationRole);
i->setData(static_cast<int>(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

View file

@ -15,7 +15,6 @@
#include <libtransmission/tr-macros.h>
#include "FaviconCache.h"
#include "Torrent.h"
#include "Typedefs.h"

View file

@ -359,5 +359,5 @@ QString Torrent::getError() const
QPixmap TrackerStat::getFavicon() const
{
return trApp->faviconCache().find(sitename);
return trApp->find_favicon(sitename);
}

View file

@ -23,7 +23,6 @@
#include <libtransmission/quark.h>
#include <libtransmission/tr-macros.h>
#include "FaviconCache.h"
#include "IconCache.h"
#include "Speed.h"

View file

@ -10,8 +10,8 @@
#include <QTextDocument>
#include <libtransmission/web-utils.h>
#include <libtransmission/favicon-cache.h>
#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<QPixmap>::Width, FaviconCache<QPixmap>::Height };
QRect base_rect(top_left, QSize(width, 0));

View file

@ -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;