// This file Copyright © 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 // size_t #include #include #include #include #include #include #include #include #include #include #include #include // for tr_file_save() #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&) { /*default callback is a no-op */ }) { 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 constexpr auto Width = 16; static constexpr auto Height = 16; private: class InFlightData { public: InFlightData(IconFunc callback, std::string_view sitename) : callback_{ std::move(callback) } , sitename_{ sitename } { } InFlightData(InFlightData const&) = delete; InFlightData& operator=(InFlightData const&) = delete; [[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::scoped_lock{ responses_mutex_ }; auto tmp = decltype(responses_){}; std::swap(tmp, responses_); return tmp; } void add_response(std::string contents, long code) { auto lock = std::scoped_lock{ 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) const { 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_file_save(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_; };