favicon: Support -uri/favicon.ico

Fixes #270
This commit is contained in:
Bilal Elmoussaoui 2022-04-17 00:38:17 +02:00
parent aaf5411a30
commit 65c6ec9bb7
7 changed files with 453 additions and 660 deletions

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ builddir/
.flatpak-builder/
app/
vendor/
*.flatpak

975
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,6 @@ version = "0.1.0"
adw = {package = "libadwaita", version = "0.1.0"}
anyhow = "1.0"
ashpd = {version = "0.3", features = ["feature_pipewire", "feature_gtk4"]}
async-std = "1.10"
binascii = "0.1"
diesel = {version = "1.4", features = ["sqlite", "r2d2"]}
diesel_migrations = {version = "1.4", features = ["sqlite"]}
@ -32,7 +31,8 @@ rust-argon2 = "1.0"
secret-service = "2.0"
serde = "1.0"
serde_json = "1.0"
surf = "2.3"
reqwest = "0.11"
tokio = { version = "1.0", features = ["rt-multi-thread", "fs", "io-util"] }
unicase = "2.6"
url = "2.2"
zbar-rust = "0.0"

View file

@ -23,14 +23,14 @@ const SUPPORTED_RELS: [&[u8]; 7] = [
#[derive(Debug)]
pub enum FaviconError {
Surf(surf::Error),
Reqwest(reqwest::Error),
Url(url::ParseError),
NoResults,
}
impl From<surf::Error> for FaviconError {
fn from(e: surf::Error) -> Self {
Self::Surf(e)
impl From<reqwest::Error> for FaviconError {
fn from(e: reqwest::Error) -> Self {
Self::Reqwest(e)
}
}
@ -51,9 +51,16 @@ impl std::fmt::Display for FaviconError {
}
}
#[derive(Debug)]
pub struct Favicon(Vec<Url>);
impl std::fmt::Debug for Favicon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|u| u.as_str()))
.finish()
}
}
impl Favicon {
pub async fn find_best(&self) -> Option<&Url> {
let mut largest_size = 0;
@ -67,22 +74,22 @@ impl Favicon {
}
}
}
best
best.or_else(|| self.0.get(0))
}
pub async fn size(&self, url: &Url) -> Option<(u32, u32)> {
let mut response = CLIENT.get(url).await.ok()?;
let response = CLIENT.get(url.as_str()).send().await.ok()?;
let ext = std::path::Path::new(url.path())
.extension()
.map(|e| e.to_str().unwrap())?;
if ext == "svg" {
let body = response.body_string().await.ok()?;
let body = response.text().await.ok()?;
self.svg_dimensions(body)
} else {
let bytes = response.body_bytes().await.ok()?;
let bytes = response.bytes().await.ok()?;
let mut image = ImageReader::new(Cursor::new(bytes));
@ -106,13 +113,17 @@ pub struct FaviconScrapper;
impl FaviconScrapper {
pub async fn from_url(url: Url) -> Result<Favicon, FaviconError> {
let mut res = CLIENT.get(&url).header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15").await?;
let body = res.body_string().await?;
let res = CLIENT.get(url.as_str())
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15")
.send()
.await?;
let body = res.text().await?;
let mut reader = quick_xml::Reader::from_str(&body);
reader.check_end_names(false);
reader.trim_markup_names_in_closing_tags(true);
let icons = Self::from_reader(&mut reader, &url);
let mut icons = Self::from_reader(&mut reader, &url);
icons.push(url.join("favicon.ico")?);
if icons.is_empty() {
return Err(FaviconError::NoResults);
}

View file

@ -13,8 +13,9 @@ mod provider;
mod provider_sorter;
mod providers;
pub static CLIENT: Lazy<surf::Client> =
Lazy::new(|| surf::Client::new().with(surf::middleware::Redirect::default()));
pub static CLIENT: Lazy<reqwest::Client> = Lazy::new(reqwest::Client::new);
pub static RUNTIME: Lazy<tokio::runtime::Runtime> =
Lazy::new(|| tokio::runtime::Runtime::new().unwrap());
pub use self::{
account::Account,

View file

@ -7,7 +7,6 @@ use crate::{
schema::providers,
};
use anyhow::Result;
use async_std::prelude::*;
use core::cmp::Ordering;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use glib::{Cast, StaticType, ToValue};
@ -17,6 +16,7 @@ use std::{
str::FromStr,
string::ToString,
};
use tokio::io::AsyncWriteExt;
use unicase::UniCase;
use url::Url;
@ -351,36 +351,55 @@ impl Provider {
.expect("Failed to create provider")
}
pub async fn favicon(&self) -> Result<gio::File, Box<dyn std::error::Error>> {
if let Some(ref website) = self.website() {
let website_url = Url::parse(website)?;
pub async fn favicon(
website: String,
name: String,
id: u32,
) -> Result<gio::File, Box<dyn std::error::Error>> {
let website_url = Url::parse(&website)?;
let favicon = FaviconScrapper::from_url(website_url).await?;
log::debug!("Found the following icons {:#?} for {}", favicon, name);
let icon_name = format!("{}_{}", self.id(), self.name().replace(' ', "_"));
let icon_name = format!("{}_{}", id, name.replace(' ', "_"));
let icon_name = glib::base64_encode(icon_name.as_bytes());
let cache_path = FAVICONS_PATH.join(icon_name.as_str());
let mut dest = async_std::fs::File::create(cache_path.clone()).await?;
log::debug!("Trying to find the highest resolution favicon");
if let Some(best_favicon) = favicon.find_best().await {
let mut res = CLIENT.get(best_favicon).await?;
let body = res.body_bytes().await?;
log::debug!("Best favicon found is {}", best_favicon);
let res = CLIENT.get(best_favicon.as_str()).send().await?;
let body = res.bytes().await?;
// TODO This check might fail. One should look for a more robust
// solution that does not involve trying to decode each favicon.
if best_favicon.as_str().ends_with(".ico") {
let ico = image::load_from_memory_with_format(&body, image::ImageFormat::Ico)?;
let cache_path = if best_favicon.as_str().ends_with(".ico") {
log::debug!("Found a .ico favicon, converting to PNG");
let cache_path = FAVICONS_PATH.join(format!("{}.png", icon_name.as_str()));
let mut dest = tokio::fs::File::create(cache_path.clone()).await?;
if let Ok(ico) = image::load_from_memory_with_format(&body, image::ImageFormat::Ico)
{
let mut cursor = std::io::Cursor::new(vec![]);
ico.write_to(&mut cursor, image::ImageOutputFormat::Png)?;
dest.write_all(cursor.get_ref()).await?;
} else {
log::debug!("It seems to not be a .ICO favicon, fallback to PNG");
dest.write_all(&body).await?;
}
return Ok(gio::File::for_path(cache_path));
}
}
cache_path
} else {
let cache_path = FAVICONS_PATH.join(icon_name.as_str());
let mut dest = tokio::fs::File::create(cache_path.clone()).await?;
dest.write_all(&body).await?;
cache_path
};
Ok(gio::File::for_path(cache_path))
} else {
Err(Box::new(FaviconError::NoResults))
}
}
pub fn id(&self) -> u32 {
self.imp().id.get()

View file

@ -1,7 +1,8 @@
use crate::models::Provider;
use crate::models::RUNTIME;
use glib::{clone, Receiver, Sender};
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk_macros::{send, spawn};
use gtk_macros::send;
pub enum ImageAction {
Ready(gio::File),
@ -171,15 +172,34 @@ impl ProviderImage {
fn fetch(&self) {
let imp = self.imp();
if let Some(provider) = self.provider() {
let sender = imp.sender.clone();
imp.stack.set_visible_child_name("loading");
imp.spinner.start();
spawn!(async move {
match provider.favicon().await {
Ok(file) => send!(sender, ImageAction::Ready(file)),
Err(_) => send!(sender, ImageAction::Failed),
if let Some(website) = provider.website() {
let id = provider.id();
let name = provider.name();
let (sender, receiver) = futures::channel::oneshot::channel();
RUNTIME.spawn(async move {
match Provider::favicon(website, name, id).await {
Ok(file) => {
sender.send(Some(file)).unwrap();
}
Err(err) => {
log::error!("Failed to load favicon {}", err);
sender.send(None).unwrap();
}
}
});
glib::MainContext::default().spawn_local(clone!(@weak self as this => async move {
let imp = this.imp();
let response = receiver.await.unwrap();
if let Some(file) = response {
send!(imp.sender.clone(), ImageAction::Ready(file));
} else {
send!(imp.sender.clone(), ImageAction::Failed);
}
}));
}
}
}