A photo manager for VRChat.
1use regex::Regex; 2use reqwest; 3use serde::Serialize; 4use serde_json::{Error, Value}; 5use std::{fs, io::Write, path, time::Duration}; 6use tauri::Emitter; 7 8#[derive(Clone, Serialize)] 9struct PhotoUploadMeta { 10 photos_uploading: usize, 11 photos_total: usize, 12} 13 14pub fn sync_photos(token: String, path: path::PathBuf, window: tauri::Window) { 15 let sync_lock_path = dirs::config_dir() 16 .unwrap() 17 .join("PhazeDev/VRChatPhotoManager/.sync_lock"); 18 19 match fs::metadata(&sync_lock_path) { 20 Ok(_) => { 21 return; 22 } 23 Err(_) => {} 24 } 25 26 fs::write(&sync_lock_path, "Currently Syncing").unwrap(); 27 28 match fs::metadata(&path) { 29 Ok(_) => {} 30 Err(_) => { 31 fs::create_dir(&path).unwrap(); 32 } 33 }; 34 35 let mut photos: Vec<String> = Vec::new(); 36 37 for folder in fs::read_dir(&path).unwrap() { 38 let f = folder.unwrap(); 39 40 if f.metadata().unwrap().is_dir() { 41 match fs::read_dir(f.path()) { 42 Ok(dir) => { 43 for photo in dir { 44 let p = photo.unwrap(); 45 46 let re1 = Regex::new(r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}.png").unwrap(); 47 let re2 = Regex::new( 48 r"(?m)VRChat_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}.[0-9]{3}_[0-9]{4}x[0-9]{4}_wrld_[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}.png").unwrap(); 49 50 if re1.is_match(p.file_name().to_str().unwrap()) 51 || re2.is_match(p.file_name().to_str().unwrap()) 52 { 53 photos.push(p.file_name().into_string().unwrap()); 54 } 55 } 56 } 57 Err(_) => {} 58 } 59 } 60 } 61 62 let body = reqwest::blocking::get(format!( 63 "https://photos-cdn.phazed.xyz/api/v1/photos/exists?token={}", 64 &token 65 )) 66 .unwrap() 67 .text() 68 .unwrap(); 69 70 let body: Value = serde_json::from_str(&body).unwrap(); 71 72 let mut photos_to_upload: Vec<String> = Vec::new(); 73 let uploaded_photos = body["files"].as_array().unwrap(); 74 75 let photos_len = photos.len(); 76 77 for photo in &photos { 78 let mut found_photo = false; 79 80 for uploaded_photo in uploaded_photos { 81 if photo == uploaded_photo.as_str().unwrap() { 82 found_photo = true; 83 break; 84 } 85 } 86 87 if !found_photo { 88 photos_to_upload.push(photo.clone()); 89 } 90 } 91 92 window 93 .emit( 94 "photos-upload-meta", 95 PhotoUploadMeta { 96 photos_uploading: photos_to_upload.len(), 97 photos_total: photos_len, 98 }, 99 ) 100 .unwrap(); 101 102 let mut photos_left = photos_to_upload.len(); 103 104 let client = reqwest::blocking::Client::new(); 105 106 loop { 107 match photos_to_upload.pop() { 108 Some(photo) => { 109 let folder_name = photo.clone().replace("VRChat_", ""); 110 let mut folder_name = folder_name.split("-"); 111 let folder_name = format!( 112 "{}-{}", 113 folder_name.nth(0).unwrap(), 114 folder_name.nth(0).unwrap() 115 ); 116 117 let full_path = format!("{}\\{}\\{}", path.to_str().unwrap(), folder_name, photo); 118 let file = fs::File::open(full_path); 119 120 match file { 121 Ok(file) => { 122 let res = client 123 .put(format!( 124 "https://photos-cdn.phazed.xyz/api/v1/photos?token={}", 125 &token 126 )) 127 .header("Content-Type", "image/png") 128 .header("filename", photo) 129 .body(file) 130 .timeout(Duration::from_secs(120)) 131 .send() 132 .unwrap() 133 .text() 134 .unwrap(); 135 136 let res: Result<Value, Error> = serde_json::from_str(&res); 137 138 match res { 139 Ok(res) => { 140 if !res["ok"].as_bool().unwrap() { 141 println!("Failed to upload: {}", res["error"].as_str().unwrap()); 142 143 window 144 .emit("sync-failed", res["error"].as_str().unwrap()) 145 .unwrap(); 146 147 break; 148 } 149 } 150 Err(err) => { 151 dbg!(err); 152 } 153 } 154 } 155 Err(_) => {} 156 } 157 158 photos_left -= 1; 159 window 160 .emit( 161 "photos-upload-meta", 162 PhotoUploadMeta { 163 photos_uploading: photos_left, 164 photos_total: photos_len, 165 }, 166 ) 167 .unwrap(); 168 } 169 None => { 170 break; 171 } 172 } 173 } 174 175 println!("Finished Uploading."); 176 let mut photos_to_download: Vec<String> = Vec::new(); 177 178 for photo in uploaded_photos { 179 let mut found_photo = false; 180 let photo = photo.as_str().unwrap().to_string(); 181 182 for uploaded_photo in &photos { 183 if &photo == uploaded_photo { 184 found_photo = true; 185 break; 186 } 187 } 188 189 if !found_photo { 190 photos_to_download.push(photo); 191 } 192 } 193 194 photos_to_download.reverse(); 195 196 let photos_len = photos_to_download.len(); 197 let mut photos_left = photos_to_download.len(); 198 199 loop { 200 match photos_to_download.pop() { 201 Some(photo) => { 202 let folder_name = photo.clone().replace("VRChat_", ""); 203 let mut folder_name = folder_name.split("-"); 204 let folder_name = format!( 205 "{}-{}", 206 folder_name.nth(0).unwrap(), 207 folder_name.nth(0).unwrap() 208 ); 209 210 let full_path = format!("{}/{}/{}", path.to_str().unwrap(), folder_name, photo); 211 212 let res = client 213 .get(format!( 214 "https://photos-cdn.phazed.xyz/api/v1/photos?token={}&photo={}", 215 &token, &photo 216 )) 217 .timeout(Duration::from_secs(120)) 218 .send() 219 .unwrap() 220 .bytes(); 221 222 match res { 223 Ok(res) => { 224 let folder_path = format!("{}/{}", path.to_str().unwrap(), folder_name); 225 match fs::metadata(&folder_path) { 226 Ok(_) => {} 227 Err(_) => { 228 fs::create_dir(folder_path).unwrap(); 229 } 230 } 231 232 let mut file = fs::File::create(full_path).unwrap(); 233 file.write_all(&res).unwrap(); 234 } 235 Err(err) => { 236 dbg!(err); 237 } 238 } 239 240 photos_left -= 1; 241 window 242 .emit( 243 "photos-download-meta", 244 PhotoUploadMeta { 245 photos_uploading: photos_left, 246 photos_total: photos_len, 247 }, 248 ) 249 .unwrap(); 250 } 251 None => { 252 break; 253 } 254 } 255 } 256 257 println!("Finished Downloading."); 258 259 fs::remove_file(&sync_lock_path).unwrap(); 260 window.emit("sync-finished", "h").unwrap(); 261}