rust_paper/
lib.rs

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}