transmission/libtransmission/favicon-cache.h

250 lines
7.3 KiB
C++

// 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 <array>
#include <chrono>
#include <cstddef> // size_t
#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> // for tr_file_save()
#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&) { /*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<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 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<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) 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<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_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<std::string /*sitename*/, Icon, std::less<>> icons_;
};