1use std::{
2 ffi::OsStr,
3 path::{Path, PathBuf},
4};
5
6mod config;
7pub(crate) use config::Config;
8
9mod env;
10
11mod error;
12pub(crate) use error::Error;
13
14pub(crate) mod prelude;
15use prelude::*;
16
17async fn delay_until_next_update(config: &Config) -> Result<jiff::Span> {
18 let interval = reqwest::get(format!("{}/api/interval", config.server_url()))
19 .await
20 .map_err(Error::IntervalRequest)?
21 .text()
22 .await
23 .map_err(Error::IntervalRequest)?
24 .parse::<u64>()
25 .map_err(|_| Error::InvalidInterval)?;
26
27 let current_timestamp = jiff::Timestamp::now();
28 let next_timestamp = jiff::Timestamp::from_millisecond(
29 ((current_timestamp.as_millisecond() as f64 / (interval * 1000) as f64) + 1.0).floor()
30 as i64
31 * (interval * 1000) as i64,
32 )
33 .expect("calculation should always succeed");
34 Ok(next_timestamp - current_timestamp)
35}
36
37async fn current_digest(config: &Config) -> Result<String> {
38 let digest_response = reqwest::get(format!(
39 "{}/api/pool/{}/digest",
40 config.server_url(),
41 config.pool_name()
42 ))
43 .await
44 .map_err(|e| Error::DigestRequest(e.to_string()))?;
45
46 if !digest_response.status().is_success() {
47 return Err(Error::DigestRequest("response was not 200".to_string()));
48 }
49
50 digest_response
51 .text()
52 .await
53 .map_err(|e| Error::DigestRequest(e.to_string()))
54}
55
56fn find_wallpaper_path(wallpapers_path: &Path, digest: &str) -> Result<Option<PathBuf>> {
57 Ok(std::fs::read_dir(wallpapers_path)
58 .map_err(|e| Error::WallpaperList {
59 io_error: e,
60 wallpapers_path: wallpapers_path.display().to_string(),
61 })?
62 .collect::<Result<Vec<std::fs::DirEntry>, _>>()
63 .map_err(|e| Error::WallpaperList {
64 io_error: e,
65 wallpapers_path: wallpapers_path.display().to_string(),
66 })?
67 .iter()
68 .map(|dir_entry| dir_entry.path())
69 .find(|wallpaper_path| {
70 wallpaper_path
71 .file_stem()
72 .map(|file_stem| file_stem == OsStr::new(digest))
73 .unwrap_or(false)
74 }))
75}
76
77async fn download_current_wallpaper(
78 wallpapers_path: &Path,
79 config: &Config,
80 digest: &str,
81) -> Result<PathBuf> {
82 info!("downloading current wallpaper");
83 let wallpaper_response = reqwest::get(format!(
84 "{}/api/pool/{}/wallpaper",
85 config.server_url(),
86 config.pool_name()
87 ))
88 .await
89 .map_err(|e| Error::WallpaperRequest(e.to_string()))?;
90
91 if !wallpaper_response.status().is_success() {
92 return Err(Error::WallpaperRequest("response was not 200".to_string()));
93 }
94
95 let content_type = wallpaper_response
96 .headers()
97 .get("Content-Type")
98 .expect("should always have a Content-Type")
99 .to_str()
100 .expect("content-type should always be a valid &str")
101 .to_string();
102 let extension = content_type
103 .split("/")
104 .nth(1)
105 .expect("content-type should always contain a slash");
106 let wallpaper_path = wallpapers_path.join(format!("{}.{}", digest, extension));
107
108 let image_content = wallpaper_response
109 .bytes()
110 .await
111 .map_err(|e| Error::WallpaperRequest(e.to_string()))?;
112
113 std::fs::write(&wallpaper_path, image_content).map_err(Error::WallpaperWrite)?;
114
115 Ok(wallpaper_path)
116}
117
118fn set_wallpaper(wallpaper_path: &Path) -> Result<()> {
119 let exit_status = match std::env::var("XDG_CURRENT_DESKTOP")
120 .unwrap_or_default()
121 .as_str()
122 {
123 "GNOME" => {
124 let mut exit_status = std::process::Command::new("gsettings")
125 .arg("set")
126 .arg("org.gnome.desktop.background")
127 .arg("picture-uri")
128 .arg(format!("file://{}", wallpaper_path.display()))
129 .spawn()
130 .map_err(Error::WallpaperSetCommand)?
131 .wait()
132 .map_err(Error::WallpaperSetCommand)?;
133 if exit_status.success() {
134 exit_status = std::process::Command::new("gsettings")
135 .arg("set")
136 .arg("org.gnome.desktop.background")
137 .arg("picture-uri-dark")
138 .arg(format!("file://{}", wallpaper_path.display()))
139 .spawn()
140 .map_err(Error::WallpaperSetCommand)?
141 .wait()
142 .map_err(Error::WallpaperSetCommand)?;
143 }
144 exit_status
145 }
146 _ => std::process::Command::new("swww")
147 .arg("img")
148 .arg("--transition-type")
149 .arg("fade")
150 .arg("--transition-bezier")
151 .arg("0,0,1,1")
152 .arg(wallpaper_path)
153 .spawn()
154 .map_err(Error::WallpaperSetCommand)?
155 .wait()
156 .map_err(Error::WallpaperSetCommand)?,
157 };
158
159 if !exit_status.success() {
160 return Err(Error::WallpaperSet {
162 exit_code: exit_status.code().unwrap_or_default(),
163 });
164 }
165
166 Ok(())
167}
168
169async fn update_wallpaper(
170 config: &Config,
171 wallpapers_path: &Path,
172 last_digest: &str,
173) -> Result<String> {
174 let current_digest = current_digest(config).await?;
175 if current_digest == last_digest {
176 info!("the wallpaper did not change; skipping");
177 } else {
178 let wallpaper_path = match find_wallpaper_path(wallpapers_path, ¤t_digest)? {
179 Some(wallpaper_path) => wallpaper_path,
180 None => download_current_wallpaper(wallpapers_path, config, ¤t_digest).await?,
181 };
182 info!("setting a new wallpaper");
183 set_wallpaper(&wallpaper_path)?;
184 }
185
186 Ok(current_digest)
187}
188
189pub async fn run() -> Result<()> {
190 env::load_dotenv()?;
191
192 let data_home_path = xdg::BaseDirectories::with_prefix("mural-client")
193 .map_err(|_| Error::DataHome)?
194 .get_data_home();
195 let wallpapers_path = data_home_path.join("wallpapers");
196 let _ = std::fs::create_dir_all(&wallpapers_path);
197
198 let mut last_digest = String::new();
199 let mut delay = jiff::Span::new().seconds(5);
200
201 loop {
202 info!("updating wallpaper");
203 let config = Config::load()?;
204
205 last_digest = match update_wallpaper(&config, &wallpapers_path, &last_digest).await {
206 Ok(new_digest) => new_digest,
207 Err(e) => {
208 error!("updating wallpaper failed: {}", e);
209 last_digest
210 }
211 };
212
213 delay = match delay_until_next_update(&config).await {
214 Ok(new_delay) => new_delay,
215 Err(e) => {
216 error!("getting delay failed: {}", e);
217 delay
218 }
219 };
220 std::thread::sleep(std::time::Duration::from_millis(
221 delay
222 .total(jiff::Unit::Millisecond)
223 .expect("should only fail if the unit is bigger than hours") as u64
224 + 1,
225 ));
226 }
227}