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}