use crate::config::{get_config_dir, get_feature_dir};
use crate::feature::FeatureControl;
use crate::generated::{
GetWallpaperRequest, GetWallpaperResponse, SetWallpaperRequest, WallpaperUsecase, WpSetting,
};
use anyhow::{bail, Ok, Result};
use async_trait::async_trait;
use axum::http::response;
use chrono::Local;
use reqwest::{self, Client};
use serde::{Deserialize, Serialize};
use std::cell::Cell;
use std::ffi::CString;
use std::fs::{self, File};
use std::io::Cursor;
use std::io::{copy, Write};
use std::path::{self, Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tracing::{debug, error, info};
#[cfg(target_os = "windows")]
use winapi::um::winuser::{SystemParametersInfoA, SPIF_UPDATEINIFILE, SPI_SETDESKWALLPAPER};
#[cfg(target_os = "windows")]
fn windows_set_wallpaper(path: &Path) -> Result<()> {
let path_cstring = CString::new(path.to_str().unwrap())?;
let result = unsafe {
SystemParametersInfoA(
SPI_SETDESKWALLPAPER,
0,
path_cstring.as_ptr() as *mut _,
SPIF_UPDATEINIFILE,
)
};
if result == 0 {
bail!(std::io::Error::last_os_error())
} else {
Ok(())
}
}
fn gnome_set_wallpaper(path: &Path) -> Result<()> {
let output = Command::new("gsettings")
.arg("set")
.arg("org.gnome.desktop.background")
.arg("picture-uri")
.arg(format!("file://{}", path.to_str().unwrap()))
.output()
.expect("Failed to execute command");
if !output.status.success() {
bail!(
"Stdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
let output = Command::new("gsettings")
.arg("set")
.arg("org.gnome.desktop.background")
.arg("picture-uri-dark")
.arg(format!("file://{}", path.to_str().unwrap()))
.output()
.expect("Failed to execute command");
if !output.status.success() {
bail!(
"Stdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
Ok(())
}
fn set_wallpaper(path: &Path) -> Result<()> {
#[cfg(target_os = "macos")]
return macos_set_wallpaper(path);
#[cfg(target_os = "windows")]
return windows_set_wallpaper(path);
#[cfg(target_os = "linux")]
return gnome_set_wallpaper(path);
bail!("set wallpaper at unsupported platform")
}
fn macos_set_wallpaper(path: &Path) -> Result<()> {
let apple_script = format!(
r#"
tell application "System Events"
tell every desktop
set picture to "{}"
end tell
end tell
"#,
path.to_str().unwrap()
);
let output = Command::new("/usr/bin/osascript")
.arg("-e")
.arg(&apple_script)
.output()
.expect("Failed to execute command");
if !output.status.success() {
bail!(
"Stdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct GetWallpaperInfoResponse {
start_date: String,
end_date: String,
url: String,
copyright: String,
copyright_link: String,
}
fn get_wallpaper_id(start_date: String, mkt: &str, resolution: u32) -> String {
format!("{}-{}-{}", start_date, mkt, resolution)
}
async fn get_wallpaper(resolution: u32, region: &str) -> Result<GetWallpaperInfoResponse> {
let url = "https://bing.biturl.top";
let client = Client::new();
let response = client
.get(url)
.query(&[
("resolution", resolution.to_string()),
("format", "json".to_string()),
("index", "0".to_string()),
("mkt", region.to_string()),
])
.send()
.await?
.json::<GetWallpaperInfoResponse>()
.await?;
Ok(response)
}
pub struct WallpaperUsecaseImpl {
settings: RwLock<Option<WpSetting>>,
scheduler_handle: RwLock<Option<JoinHandle<()>>>,
}
impl WallpaperUsecaseImpl {
pub fn new() -> Self {
Self {
settings: Default::default(),
scheduler_handle: Default::default(),
}
}
}
impl Default for WpSetting {
fn default() -> Self {
Self {
res_w: Some(3840),
mkt: Some("en-US".to_string()),
}
}
}
#[async_trait]
impl WallpaperUsecase for WallpaperUsecaseImpl {
async fn set_wallpaper(&self, request: SetWallpaperRequest) -> Result<()> {
Ok(())
}
async fn get_wallpaper(&self, request: GetWallpaperRequest) -> Result<GetWallpaperResponse> {
let wallpaper_info = get_wallpaper(request.res_w, &request.mkt).await?;
let id = get_wallpaper_id(wallpaper_info.start_date, &request.mkt, request.res_w);
Ok(GetWallpaperResponse {
data: wallpaper_info.url,
id,
})
}
}
async fn download_to_file(url: &str, file_path: &Path) -> Result<()> {
let response = reqwest::get(url).await?;
let mut file = std::fs::File::create(file_path)?;
let mut content = Cursor::new(response.bytes().await?);
std::io::copy(&mut content, &mut file)?;
Ok(())
}
async fn refresh_wallpaper(res_w: u32, mkt: &String, img_dir: PathBuf) -> Result<()> {
let res = get_wallpaper(res_w, mkt).await?;
let id = get_wallpaper_id(res.start_date, mkt, res_w);
if !img_dir.exists() {
std::fs::create_dir_all(&img_dir)?;
}
let dst = img_dir.join(format!("{}.png", id));
if !dst.exists() {
download_to_file(&res.url, &dst).await?;
} else {
debug!("skip download wallpaper[id={}] since already existed", id);
}
set_wallpaper(&dst)?;
info!("refreshed wallpaper");
Ok(())
}
fn duration_until_2359() -> Duration {
let now = Local::now();
let end_of_day = now.date_naive().and_hms_opt(23, 59, 0).unwrap();
let duration_until_end_of_day = end_of_day.signed_duration_since(now.naive_local());
duration_until_end_of_day.to_std().unwrap()
}
#[async_trait]
impl FeatureControl for WallpaperUsecaseImpl {
fn name(&self) -> &str {
"wp"
}
async fn enable(&self, settings: Option<serde_json::Value>) -> Result<()> {
self.update(None, settings).await?;
let settings = match &*self.settings.read().await {
Some(settings) => {
settings.clone()
}
None => {
Default::default()
}
};
let img_dir = get_feature_dir(self.name());
let mut handle = self.scheduler_handle.write().await;
*handle = Some(tokio::spawn({
let mkt = settings.mkt.unwrap_or("en-US".to_string());
let img_dir = img_dir;
let res_w = settings.res_w.unwrap_or(3840);
async move {
loop {
match refresh_wallpaper(res_w, &mkt, img_dir.clone()).await {
std::result::Result::Ok(_) => {}
Err(err) => {
error!("failed to refresh wallpaper: {}", err)
}
};
let dur = duration_until_2359();
debug!("wait {:?} to refresh wallpaper", dur);
sleep(dur).await;
}
}
}));
Ok(())
}
async fn disable(&self) -> Result<()> {
Ok(())
}
async fn update(
&self,
old_settings: Option<serde_json::Value>,
settings: Option<serde_json::Value>,
) -> Result<()> {
let mut sl = self.settings.write().await;
*sl = if settings.is_none() {
None
} else {
serde_json::from_value(settings.unwrap())?
};
Ok(())
}
async fn lid_change(&self, open: bool) {}
}