use super::DownloadOptions;
use super::client::WallflowClient as Client;
use super::filesystem::FilesystemHelper;
use super::traits::{Wallpaper, WallpaperDownloader};
use crate::config::Config;
use anyhow::{Context, Result, anyhow};
use async_trait::async_trait;
use chrono::Utc;
use rand::seq::SliceRandom;
use serde::Deserialize;
use std::path::Path;
use tracing::debug;
#[derive(Debug, Deserialize)]
struct WallhavenResponse {
data: Vec<WallhavenImage>,
}
#[derive(Debug, Deserialize)]
struct WallhavenImage {
path: String,
#[allow(dead_code)]
resolution: String,
}
pub struct WallhavenDownloader;
impl WallhavenDownloader {
fn purity_to_bitmask(purity: &[String]) -> String {
let sfw = purity.iter().any(|p| p.eq_ignore_ascii_case("sfw"));
let sketchy = purity.iter().any(|p| p.eq_ignore_ascii_case("sketchy"));
let nsfw = purity.iter().any(|p| p.eq_ignore_ascii_case("nsfw"));
if !sfw && !sketchy && !nsfw {
return "100".to_string();
}
format!("{}{}{}", sfw as u8, sketchy as u8, nsfw as u8)
}
fn categories_to_bitmask(categories: &[String]) -> String {
let general = categories.iter().any(|c| c.eq_ignore_ascii_case("general"));
let anime = categories.iter().any(|c| c.eq_ignore_ascii_case("anime"));
let people = categories.iter().any(|c| c.eq_ignore_ascii_case("people"));
if !general && !anime && !people {
return "100".to_string();
}
format!("{}{}{}", general as u8, anime as u8, people as u8)
}
fn build_search_query(config_q: &str, categories: &[String]) -> String {
let mut terms: Vec<String> = Vec::new();
if !config_q.is_empty() {
terms.push(config_q.to_string());
}
let api_categories = ["general", "anime", "people"];
for cat in categories {
if !api_categories.iter().any(|c| cat.eq_ignore_ascii_case(c)) {
terms.push(cat.clone());
}
}
terms.join(" ")
}
}
#[async_trait]
impl WallpaperDownloader for WallhavenDownloader {
async fn download(&self, config: &Config, query: &[String], opts: &DownloadOptions) -> Result<Wallpaper> {
let wallhaven_config = &config.sources.wallhaven;
let resolution = config.get_wallhaven_resolution()?;
let purity = Self::purity_to_bitmask(&wallhaven_config.purity);
let categories = Self::categories_to_bitmask(&wallhaven_config.categories);
let cli_query = query.join(" ");
let base_query = if cli_query.is_empty() { &wallhaven_config.q } else { &cli_query };
let search_query = Self::build_search_query(base_query, &wallhaven_config.categories);
let resolution_str = format!("{}x{}", resolution.width, resolution.height);
debug!(
"Searching Wallhaven: q='{}', resolution={}, purity={}, categories={}",
search_query, resolution_str, purity, categories
);
let client = Client::from(&config.advanced);
let mut request = client.get(&wallhaven_config.url).query(&[
("purity", purity.as_str()),
("categories", categories.as_str()),
("atleast", resolution_str.as_str()),
("sorting", "random"),
]);
if !search_query.is_empty() {
request = request.query(&[("q", search_query.as_str())]);
}
if let Some(api_key) = &wallhaven_config.api_key
&& !api_key.is_empty()
{
request = request.query(&[("apikey", api_key.as_str())]);
}
let response = request.send().await.context("Failed to send request to Wallhaven API")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
return Err(anyhow!("Wallhaven API request failed with status {}: {}", status, error_text));
}
let wallhaven_data: WallhavenResponse = response.json().await.context("Failed to parse Wallhaven API response")?;
if wallhaven_data.data.is_empty() {
return Err(anyhow!(
"No wallpapers found matching criteria (resolution: {}, purity: {}, categories: {})",
resolution_str,
purity,
categories
));
}
let image = wallhaven_data
.data
.choose(&mut rand::thread_rng())
.ok_or_else(|| anyhow!("Failed to select random wallpaper"))?;
debug!("Selected wallpaper: {}", image.path);
let image_response = client.get(&image.path).send().await.context("Failed to download wallpaper image")?;
if !image_response.status().is_success() {
return Err(anyhow!("Image download failed with status: {}", image_response.status()));
}
let bytes = image_response.bytes().await.context("Failed to read image data")?;
let file_extension = image
.path
.rsplit('.')
.next()
.and_then(|ext| {
let ext = ext.split('?').next().unwrap_or(ext);
if ext.len() <= 5 { Some(ext) } else { None }
})
.unwrap_or("jpg");
let filename = format!("{}_{}", self.source_name(), FilesystemHelper::make_file_suffix());
let download_dir = opts.output_dir.as_deref().unwrap_or(Path::new(&config.paths.downloads));
let file_path = download_dir.join(&filename).with_extension(file_extension);
if let Some(parent) = file_path.parent() {
tokio::fs::create_dir_all(parent).await.context("Failed to create download directory")?;
}
tokio::fs::write(&file_path, &bytes).await.context("Failed to save wallpaper image")?;
let wallpaper = Wallpaper {
file_path,
downloaded_at: Utc::now(),
source: self.source_name().to_string(),
};
debug!("Downloaded Wallhaven wallpaper: {:?}", wallpaper);
Ok(wallpaper)
}
fn source_name(&self) -> &'static str {
"wallhaven"
}
}