1use anyhow::{Context, Result};
2use futures::{stream::FuturesUnordered, StreamExt};
3use lazy_static::lazy_static;
4use serde_json::Value;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::time::Duration;
8use tokio::fs::{create_dir_all, File, OpenOptions};
9use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
10use tokio::sync::Mutex;
11use tokio::time::sleep;
12
13mod config;
14mod helper;
15mod lock;
16
17use lock::LockFile;
18
19const WALLHEAVEN_API: &str = "https://wallhaven.cc/api/v1/w";
20
21lazy_static! {
22 static ref MAX_RETRY: u32 = 3;
23}
24
25#[derive(Clone)]
26pub struct RustPaper {
27 config: config::Config,
28 config_folder: PathBuf,
29 wallpapers: Vec<String>,
30 wallpapers_list_file_location: PathBuf,
31 lock_file: Arc<Mutex<Option<LockFile>>>,
32}
33
34impl RustPaper {
35 pub async fn new() -> Result<Self> {
36 let config: config::Config =
37 confy::load("rust-paper", "config").context(" Failed to load configuration")?;
38
39 let config_folder = helper::get_folder_path().context(" Failed to get folder path")?;
40
41 tokio::try_join!(
42 create_dir_all(&config_folder),
43 create_dir_all(&config.save_location)
44 )?;
45
46 let wallpapers_list_file_location = config_folder.join("wallpapers.lst");
47 let wallpapers = load_wallpapers(&wallpapers_list_file_location).await?;
48
49 let lock_file = Arc::new(Mutex::new(config.integrity.then(LockFile::default)));
50
51 Ok(Self {
52 config,
53 config_folder,
54 wallpapers,
55 wallpapers_list_file_location,
56 lock_file,
57 })
58 }
59
60 pub async fn sync(&self) -> Result<()> {
61 let tasks: FuturesUnordered<_> = self
62 .wallpapers
63 .iter()
64 .map(|wallpaper| {
65 let config = self.config.clone();
66 let lock_file = Arc::clone(&self.lock_file);
67 let wallpaper = wallpaper.clone();
68
69 tokio::spawn(
70 async move { process_wallpaper(&config, &lock_file, &wallpaper).await },
71 )
72 })
73 .collect();
74
75 tasks
76 .for_each(|result| async {
77 if let Err(e) = result.expect("Task panicked") {
78 eprintln!(" Error processing wallpaper: {}", e);
79 }
80 })
81 .await;
82
83 Ok(())
84 }
85
86 pub async fn add(&mut self, new_wallpapers: &mut Vec<String>) -> Result<()> {
87 *new_wallpapers = new_wallpapers
88 .iter()
89 .map(|wall| {
90 if helper::is_url(wall) {
91 wall.split('/')
92 .last()
93 .unwrap_or_default()
94 .split('?')
95 .next()
96 .unwrap_or_default()
97 .to_string()
98 } else {
99 wall.to_string()
100 }
101 })
102 .collect();
103
104 self.wallpapers
105 .extend(new_wallpapers.iter().flat_map(|s| helper::to_array(s)));
106 self.wallpapers.sort_unstable();
107 self.wallpapers.dedup();
108 update_wallpaper_list(&self.wallpapers, &self.wallpapers_list_file_location).await
109 }
110}
111
112async fn update_wallpaper_list(list: &[String], file_path: &Path) -> Result<()> {
113 let file = OpenOptions::new()
114 .write(true)
115 .create(true)
116 .truncate(true)
117 .open(file_path)
118 .await?;
119
120 let mut writer = BufWriter::new(file);
121
122 for wallpaper in list {
123 writer.write_all(wallpaper.as_bytes()).await?;
124 writer.write_all(b"\n").await?;
125 }
126
127 writer.flush().await?;
128 Ok(())
129}
130
131async fn process_wallpaper(
132 config: &config::Config,
133 lock_file: &Arc<Mutex<Option<LockFile>>>,
134 wallpaper: &str,
135) -> Result<()> {
136 let save_location = Path::new(&config.save_location);
137 if let Some(existing_path) = find_existing_image(save_location, wallpaper).await? {
138 if config.integrity {
139 if check_integrity(&existing_path, &wallpaper, &lock_file).await? {
140 println!(
141 " Skipping {}: already exists and integrity check passed",
142 wallpaper
143 );
144 return Ok(());
145 }
146 println!(
147 " Integrity check failed for {}: re-downloading",
148 wallpaper
149 );
150 } else {
151 println!(" Skipping {}: already exists", wallpaper);
152 return Ok(());
153 }
154 }
155
156 let wallhaven_img_link = format!("{}/{}", WALLHEAVEN_API, wallpaper.trim());
157 let curl_data = retry_get_curl_content(&wallhaven_img_link).await?;
158 let res: Value = serde_json::from_str(&curl_data)?;
159
160 if let Some(error) = res.get("error") {
161 eprintln!("Error : {}", error);
162 return Err(anyhow::anyhow!(" API error: {}", error));
163 }
164
165 let image_location = download_and_save(&res, wallpaper, &config.save_location).await?;
166
167 if config.integrity {
168 let mut lock_file = lock_file.lock().await;
169 if let Some(ref mut lock_file) = *lock_file {
170 let image_sha256 = helper::calculate_sha256(&image_location).await?;
171 lock_file.add(wallpaper.to_string(), image_location, image_sha256)?;
172 }
173 }
174
175 println!(" Downloaded {}", wallpaper);
176 Ok(())
177}
178
179async fn load_wallpapers(file_path: &Path) -> Result<Vec<String>> {
180 if !file_path.exists() {
181 File::create(file_path).await?;
182 return Ok(vec![]);
183 }
184
185 let file = File::open(file_path).await?;
186 let reader = BufReader::new(file);
187 let mut lines = Vec::new();
188 let mut lines_stream = reader.lines();
189
190 while let Some(line) = lines_stream.next_line().await? {
191 lines.extend(helper::to_array(&line));
192 }
193
194 Ok(lines)
195}
196
197async fn find_existing_image(save_location: &Path, wallpaper: &str) -> Result<Option<PathBuf>> {
198 let mut entries = tokio::fs::read_dir(save_location).await?;
199 while let Some(entry) = entries.next_entry().await? {
200 let path = entry.path();
201 if path.file_stem().and_then(|s| s.to_str()) == Some(wallpaper) {
202 return Ok(Some(path));
203 }
204 }
205 Ok(None)
206}
207
208async fn check_integrity(
209 existing_path: &Path,
210 wallpaper: &str,
211 lock_file: &Arc<Mutex<Option<LockFile>>>,
212) -> Result<bool> {
213 let lock_file = lock_file.lock().await;
214 if let Some(ref lock_file) = *lock_file {
215 let existing_image_sha256 = helper::calculate_sha256(existing_path).await?;
216 Ok(lock_file.contains(wallpaper, &existing_image_sha256))
217 } else {
218 Ok(false)
219 }
220}
221
222async fn download_and_save(api_data: &Value, id: &str, save_location: &str) -> Result<String> {
223 let img_link = api_data
224 .get("data")
225 .and_then(|data| data.get("path"))
226 .and_then(Value::as_str)
227 .ok_or_else(|| anyhow::anyhow!(" Failed to get image link from API response"))?;
228 helper::download_image(&img_link, id, save_location).await
229}
230
231async fn retry_get_curl_content(url: &str) -> Result<String> {
232 for retry_count in 0..*MAX_RETRY {
233 match helper::get_curl_content(url).await {
234 Ok(content) => return Ok(content),
235 Err(e) if retry_count + 1 < *MAX_RETRY => {
236 eprintln!(
237 "Error fetching content (attempt {}): {}. Retrying...",
238 retry_count + 1,
239 e
240 );
241 sleep(Duration::from_secs(1)).await;
242 }
243 Err(e) => return Err(e),
244 }
245 }
246 unreachable!()
247}