/* * This file Copyright (C) 2012-2015 Mnemosyne LLC * * It may be used under the GNU GPL versions 2 or 3 * or any future license endorsed by Mnemosyne LLC. * */ #include #include #include #include #include #include #include "FaviconCache.h" /*** **** ***/ FaviconCache::FaviconCache() : nam_(new QNetworkAccessManager(this)) { 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 markUrlAsScraped(QString const& url_str) { auto skip_file = QFile(getScrapedFile()); if (skip_file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) { skip_file.write(url_str.toUtf8()); skip_file.write("\n"); } } } // unnamed namespace void FaviconCache::ensureCacheDirHasBeenScanned() { static bool has_been_scanned = false; if (has_been_scanned) { 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 auto skip_file = QFile(getScrapedFile()); if (skip_file.open(QIODevice::ReadOnly | QIODevice::Text)) { while (!skip_file.atEnd()) { auto const url = QString::fromUtf8(skip_file.readLine()).trimmed(); auto const key = getKey(QUrl{ url }); keys_.insert({ url, key }); pixmaps_.try_emplace(key); } } // load the cached favicons auto cache_dir = QDir(getCacheDir()); cache_dir.mkpath(cache_dir.absolutePath()); QStringList const files = cache_dir.entryList(QDir::Files | QDir::Readable); for (auto const& file : files) { QPixmap pixmap(cache_dir.absoluteFilePath(file)); if (!pixmap.isNull()) { pixmaps_[file] = scale(pixmap); } } } /*** **** ***/ QString FaviconCache::getDisplayName(Key const& key) { auto name = key; name[0] = name.at(0).toTitleCase(); return name; } FaviconCache::Key FaviconCache::getKey(QUrl const& url) { auto host = url.host(); // remove tld auto const suffix = url.topLevelDomain(); host.truncate(host.size() - suffix.size()); // remove subdomain auto const pos = host.indexOf(QLatin1Char('.')); return pos < 0 ? host : host.remove(0, pos + 1); } FaviconCache::Key FaviconCache::getKey(QString const& displayName) { return displayName.toLower(); } QSize FaviconCache::getIconSize() { return QSize(16, 16); } QPixmap FaviconCache::find(Key const& key) { ensureCacheDirHasBeenScanned(); return pixmaps_[key]; } FaviconCache::Key FaviconCache::add(QString const& url_str) { ensureCacheDirHasBeenScanned(); // find or add this url's key auto k_it = keys_.find(url_str); if (k_it != keys_.end()) { return k_it->second; } auto const url = QUrl { url_str }; auto const key = getKey(url); keys_.insert({ url_str, key }); // Try to download a favicon if we don't have one. // Add a placeholder to prevent repeat downloads. if (pixmaps_.try_emplace(key).second) { markUrlAsScraped(url_str); auto const scrape = [this](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); nam_->get(QNetworkRequest(path)); } } }; // tracker.domain.com auto host = url.host(); scrape(host); auto const delim = QStringLiteral("."); auto const has_subdomain = host.count(delim) > 1; if (has_subdomain) { auto const original_subdomain = host.left(host.indexOf(delim)); host.remove(0, original_subdomain.size() + 1); // domain.com scrape(host); auto const www = QStringLiteral("www"); if (original_subdomain != www) { // www.domain.com scrape(QStringLiteral("%1.%2").arg(www).arg(host)); } } } return key; } void FaviconCache::onRequestFinished(QNetworkReply* reply) { auto const key = getKey(reply->url()); QPixmap pixmap; QByteArray const content = reply->readAll(); if (reply->error() == QNetworkReply::NoError) { pixmap.loadFromData(content); } if (!pixmap.isNull()) { // save it in memory... pixmaps_[key] = scale(pixmap); // save it on disk... QDir cache_dir(getCacheDir()); cache_dir.mkpath(cache_dir.absolutePath()); QFile file(cache_dir.absoluteFilePath(key)); file.open(QIODevice::WriteOnly); file.write(content); file.close(); // notify listeners emit pixmapReady(key); } reply->deleteLater(); }