1use std::{collections::HashMap, str::FromStr};
2
3use regex::Regex;
4use serde_json::Value;
5use urlencoding::decode;
6use crate::{PluginCredential, RsFileType, RsVideoFormat};
7use crate::{RsAudio, RsResolution, RsVideoCodec};
8use serde::{Deserialize, Serialize};
9use strum_macros::EnumString;
10
11pub mod error;
12
13#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
14#[serde(rename_all = "camelCase")]
15pub struct RsCookie {
16 pub domain: String,
17 pub http_only: bool,
18 pub path: String,
19 pub secure: bool,
20 pub expiration: Option<f64>,
21 pub name: String,
22 pub value: String,
23}
24
25impl FromStr for RsCookie {
26 type Err = error::RequestError;
27 fn from_str(line: &str) -> Result<Self, Self::Err> {
28 let mut splitted = line.split(';');
30 Ok(RsCookie {
31 domain: splitted.next().ok_or(error::RequestError::UnableToParseCookieString("domain".to_owned(), line.to_owned()))?.to_owned(),
32 http_only: "true" == splitted.next().ok_or(error::RequestError::UnableToParseCookieString("http_only".to_owned(), line.to_owned()))?,
33 path: splitted.next().ok_or(error::RequestError::UnableToParseCookieString("path".to_owned(), line.to_owned()))?.to_owned(),
34 secure: "true" == splitted.next().ok_or(error::RequestError::UnableToParseCookieString("secure".to_owned(), line.to_owned()))?,
35 expiration: {
36 let t = splitted.next().ok_or(error::RequestError::UnableToParseCookieString("expiration".to_owned(), line.to_owned()))?.to_owned();
37 if t.is_empty() {
38 None
39 } else {
40 Some(t.parse().map_err(|_| error::RequestError::UnableToParseCookieString("expiration parsing".to_owned(), line.to_owned()))?)
41 }
42 },
43 name: splitted.next().ok_or(error::RequestError::UnableToParseCookieString("name".to_owned(), line.to_owned()))?.to_owned(),
44 value: splitted.next().ok_or(error::RequestError::UnableToParseCookieString("value".to_owned(), line.to_owned()))?.to_owned() })
45 }
46}
47
48impl RsCookie {
49 pub fn netscape(&self) -> String {
50 let second = if self.domain.starts_with('.') {
51 "TRUE"
52 } else {
53 "FALSE"
54 };
55 let secure = if self.secure {
56 "TRUE"
57 } else {
58 "FALSE"
59 };
60 let expiration = if let Some(expiration) = self.expiration {
61 (expiration as u32).to_string()
62 } else {
63 "".to_owned()
64 };
65 format!("{}\t{}\t{}\t{}\t{}\t{}\t{}", self.domain, second, self.path, secure, expiration, self.name, self.value)
67 }
68
69 pub fn header(&self) -> String {
70 format!("{}={}", self.name, self.value)
71 }
72}
73
74pub trait RsCookies {
75 fn header_value(&self) -> String;
76 fn headers(&self) -> (String, String);
77}
78
79impl RsCookies for Vec<RsCookie> {
80 fn header_value(&self) -> String {
81 self.iter().map(|t| t.header()).collect::<Vec<String>>().join("; ")
82 }
83
84 fn headers(&self) -> (String, String) {
85 ("cookie".to_owned(), self.iter().map(|t| t.header()).collect::<Vec<String>>().join("; "))
86 }
87}
88
89#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
90#[serde(rename_all = "camelCase")]
91pub struct RsRequest {
92 pub upload_id: Option<String>,
93 pub url: String,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub mime: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub size: Option<u64>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub filename: Option<String>,
100 #[serde(default)]
101 pub status: RsRequestStatus,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub plugin_id: Option<String>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub plugin_name: Option<String>,
106
107 #[serde(default)]
110 pub permanent: bool,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub instant: Option<bool>,
114
115 pub json_body: Option<Value>,
116 #[serde(default)]
117 pub method: RsRequestMethod,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub referer: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub headers: Option<Vec<(String, String)>>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub cookies: Option<Vec<RsCookie>>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub files: Option<Vec<RsRequestFiles>>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub selected_file: Option<String>,
132
133
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub description: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub tags: Option<Vec<String>>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub people: Option<Vec<String>>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub albums: Option<Vec<String>>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub season: Option<u32>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub episode: Option<u32>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub movie: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub language: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub resolution: Option<RsResolution>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub video_format: Option<RsVideoFormat>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub videocodec: Option<RsVideoCodec>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub audio: Option<Vec<RsAudio>>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub quality: Option<u64>,
160
161 #[serde(default)]
162 pub ignore_origin_duplicate: bool,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub thumbnail_url: Option<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub origin_url: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub title: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub kind: Option<RsFileType>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub tags_lookup: Option<Vec<String>>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub people_lookup: Option<Vec<String>>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub albums_lookup: Option<Vec<String>>,
181}
182
183impl RsRequest {
184 pub fn set_cookies(&mut self, cookies: Vec<RsCookie>) {
185 let mut existing = if let Some(headers) = &self.headers {
186 headers.to_owned()
187 } else{
188 vec![]
189 };
190 existing.push(cookies.headers());
191 self.headers = Some(existing);
192 }
193
194 pub fn filename_or_extract_from_url(&self) -> Option<String> {
195 if self.filename.is_some() {
196 self.filename.clone()
197 } else {
198 self.url.split('/')
199 .last()
200 .and_then(|segment| {
201 segment.split('?')
202 .next()
203 .filter(|s| !s.is_empty())
204 .map(|s| s.to_string())
205 })
206 .and_then(|potential| {
207 let extension = potential.split('.').map(|t| t.to_string()).collect::<Vec<String>>();
208 if extension.len() > 1 && extension.last().unwrap_or(&"".to_string()).len() > 2 && extension.last().unwrap_or(&"".to_string()).len() < 5{
209 let decoded = decode(&potential).map(|x| x.into_owned()).unwrap_or(potential); Some(decoded)
211 } else {
212 None
213 }
214 })
215 }
216
217 }
218
219 pub fn parse_filename(&mut self) {
220 if let Some(filename) = &self.filename {
221 let resolution = RsResolution::from_filename(filename);
222 if resolution != RsResolution::Unknown {
223 self.resolution = Some(resolution);
224 }
225 let video_format = RsVideoFormat::from_filename(filename);
226 if video_format != RsVideoFormat::Other {
227 self.video_format = Some(video_format);
228 }
229 let videocodec = RsVideoCodec::from_filename(filename);
230 if videocodec != RsVideoCodec::Unknown {
231 self.videocodec = Some(videocodec);
232 }
233 let audio = RsAudio::list_from_filename(filename);
234 if !audio.is_empty() {
235 self.audio = Some(audio);
236 }
237
238 let re = Regex::new(r"(?i)s(\d+)e(\d+)").unwrap();
239 if let Some(caps) = re.captures(filename) {
240 self.season = caps[1].parse::<u32>().ok();
241 self.episode = caps[2].parse::<u32>().ok();
242 }
243 }
244
245 }
246
247 pub fn parse_subfilenames(&mut self) {
248 if let Some(ref mut files) = self.files {
249 for file in files {
250 file.parse_filename();
251 }
252 }
253 }
254}
255
256#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display,EnumString, Default)]
257#[serde(rename_all = "camelCase")]
258#[strum(serialize_all = "camelCase")]
259pub enum RsRequestStatus {
260 #[default]
262 Unprocessed,
263 Processed,
265 NeedParsing,
267 RequireAdd,
271 Intermediate,
273 NeedFileSelection,
275 FinalPrivate,
277 FinalPublic
279}
280
281
282#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display,EnumString, Default)]
283#[serde(rename_all = "camelCase")]
284#[strum(serialize_all = "camelCase")]
285pub enum RsRequestMethod {
286
287 #[default]
288 Get,
289 Post,
290 Patch,
291 Delete,
292 Head,
293
294}
295
296
297
298#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
299#[serde(rename_all = "camelCase")]
300pub struct RsRequestFiles {
301 pub name: String,
302 pub size: u64,
303
304 pub mime: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub description: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub tags: Option<Vec<String>>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub people: Option<Vec<String>>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub albums: Option<Vec<String>>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub season: Option<u32>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub episode: Option<u32>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub language: Option<String>,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub resolution: Option<RsResolution>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub video_format: Option<RsVideoFormat>,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub videocodec: Option<RsVideoCodec>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub audio: Option<Vec<RsAudio>>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub quality: Option<u64>,
329
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub tags_lookup: Option<Vec<String>>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub people_lookup: Option<Vec<String>>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub albums_lookup: Option<Vec<String>>,
337}
338
339impl RsRequestFiles {
340 pub fn parse_filename(&mut self) {
341 let resolution = RsResolution::from_filename(&self.name);
342 if resolution != RsResolution::Unknown {
343 self.resolution = Some(resolution);
344 }
345 let video_format = RsVideoFormat::from_filename(&self.name);
346 if video_format != RsVideoFormat::Other {
347 self.video_format = Some(video_format);
348 }
349 let videocodec = RsVideoCodec::from_filename(&self.name);
350 if videocodec != RsVideoCodec::Unknown {
351 self.videocodec = Some(videocodec);
352 }
353 let audio = RsAudio::list_from_filename(&self.name);
354 if !audio.is_empty() {
355 self.audio = Some(audio);
356 }
357
358 let re = Regex::new(r"(?i)s(\d+)e(\d+)").unwrap();
359 if let Some(caps) = re.captures(&self.name) {
360 self.season = caps[1].parse::<u32>().ok();
361 self.episode = caps[2].parse::<u32>().ok();
362 }
363 }
364}
365
366#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
367#[serde(rename_all = "camelCase")]
368pub struct RsRequestPluginRequest {
369 pub request: RsRequest,
370 pub credential: Option<PluginCredential>,
371 pub params: Option<HashMap<String, String>>,
372}
373
374#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
376#[serde(rename_all = "camelCase")]
377pub struct RsGroupDownload {
378 #[serde(default)]
380 pub group: bool,
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub group_thumbnail_url: Option<String>,
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub group_filename: Option<String>,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub group_mime: Option<String>,
387 pub requests: Vec<RsRequest>,
388}
389
390#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default)]
392#[serde(rename_all = "camelCase")]
393#[strum(serialize_all = "camelCase")]
394pub enum RsProcessingStatus {
395 #[default]
396 Pending,
397 Processing,
398 Finished,
399 Error,
400 Paused,
401}
402
403#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
405#[serde(rename_all = "camelCase")]
406pub struct RsRequestAddResponse {
407 pub processing_id: String,
409 #[serde(default)]
411 pub status: RsProcessingStatus,
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub eta: Option<i64>,
415}
416
417#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
419#[serde(rename_all = "camelCase")]
420pub struct RsProcessingProgress {
421 pub processing_id: String,
423 pub progress: u32,
425 pub status: RsProcessingStatus,
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub error: Option<String>,
430 #[serde(skip_serializing_if = "Option::is_none")]
432 pub eta: Option<i64>,
433 #[serde(skip_serializing_if = "Option::is_none")]
435 pub request: Option<Box<RsRequest>>,
436}
437
438#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
440#[serde(rename_all = "camelCase")]
441pub struct RsProcessingActionRequest {
442 pub processing_id: String,
444 pub credential: Option<PluginCredential>,
446 pub params: Option<HashMap<String, String>>,
448}
449
450#[cfg(test)]
451mod tests {
452
453 use self::error::RequestError;
454
455 use super::*;
456
457 #[test]
458 fn test_cookie_parsing() -> Result<(), RequestError> {
459 let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
460 assert!(parsed.domain == ".twitter.com".to_owned());
461 assert!(parsed.http_only == false);
462 assert!(parsed.path == "/".to_owned());
463 assert!(parsed.secure == true);
464 assert!(parsed.expiration == Some(1722364794.437907));
465 assert!(parsed.name == "kdt".to_owned());
466 assert!(parsed.value == "w1j".to_owned());
467 Ok(())
468 }
469
470 #[test]
471 fn test_cookie_parsing_no_expi() -> Result<(), RequestError> {
472 let parsed = RsCookie::from_str(".twitter.com;false;/;true;;kdt;w1j")?;
473 assert!(parsed.domain == ".twitter.com".to_owned());
474 assert!(parsed.http_only == false);
475 assert!(parsed.path == "/".to_owned());
476 assert!(parsed.secure == true);
477 assert!(parsed.expiration == None);
478 assert!(parsed.name == "kdt".to_owned());
479 assert!(parsed.value == "w1j".to_owned());
480 Ok(())
481 }
482
483 #[test]
484 fn test_netscape() -> Result<(), RequestError> {
485 let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
486 assert!(parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1722364794\tkdt\tw1j");
487 Ok(())
488 }
489 #[test]
490 fn test_netscape_doublequote() -> Result<(), RequestError> {
491 let parsed = RsCookie::from_str(".twitter.com;true;/;true;1726506480.700665;ads_prefs;\"HBESAAA=\"")?;
492 assert!(parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1726506480\tads_prefs\t\"HBESAAA=\"");
493 Ok(())
494 }
495
496 #[test]
497 fn test_parse_filename() -> Result<(), RequestError> {
498 let req = RsRequest {url: "http://www.test.com/filename.mp4?toto=3".to_string(), filename: Some("test.mkv".to_owned()), ..Default::default()};
499 assert_eq!(req.filename_or_extract_from_url(), Some("test.mkv".to_string()));
500 let req = RsRequest {url: "http://www.test.com/filename.mp4?toto=3".to_string(), ..Default::default()};
501 assert_eq!(req.filename_or_extract_from_url(), Some("filename.mp4".to_string()), "We are expecting a filename from the url");
502 let req = RsRequest {url: "http://www.test.com/notfilename?toto=3".to_string(), ..Default::default()};
503 assert_eq!(req.filename_or_extract_from_url(), None, "Should return none as there is no filename with extensiopn in url");
504 let req = RsRequest {url: "http://www.test.com/notfilename.toolong?toto=3".to_string(), ..Default::default()};
505 assert_eq!(req.filename_or_extract_from_url(), None, "Should return none as too long after dot is not an extension");
506 let req = RsRequest {url: "http://www.test.com/filename%20test.mp4?toto=3".to_string(), ..Default::default()};
507 assert_eq!(req.filename_or_extract_from_url(), Some("filename test.mp4".to_string()), "Should decode URL-encoded filename");
508 Ok(())
509 }
510
511
512 #[test]
513 fn test_header() -> Result<(), RequestError> {
514 let parsed = vec![RsCookie::from_str(".twitter.com;true;/;true;1726506480.700665;ads_prefs;\"HBESAAA=\"")?, RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?];
515 println!("header: {}", parsed.header_value());
516 assert!(parsed.header_value() == "ads_prefs=\"HBESAAA=\"; kdt=w1j");
517 Ok(())
518 }
519
520 #[test]
521 fn test_parse() -> Result<(), RequestError> {
522 let mut req = RsRequest { filename: Some("Shogun.2024.S01E01.Anjin.1080p.VOSTFR.DSNP.WEB-DL.DDP5.1.H.264-NTb.mkv".to_owned()), ..Default::default()};
523 req.parse_filename();
524 assert_eq!(req.season.unwrap(), 1);
525 assert_eq!(req.episode.unwrap(), 1);
526 assert_eq!(req.resolution.unwrap(), RsResolution::FullHD);
527 assert_eq!(req.videocodec.unwrap(), RsVideoCodec::H264);
528 assert_eq!(req.video_format.unwrap(), RsVideoFormat::Mkv);
529 assert_eq!(req.audio.unwrap().len(), 1);
530 Ok(())
531 }
532
533 #[test]
534 fn test_parse2() -> Result<(), RequestError> {
535 let mut req = RsRequest { filename: Some("Shogun.2024.S01E05.MULTi.HDR.DV.2160p.WEB.H265-FW".to_owned()), ..Default::default()};
536 req.parse_filename();
537 assert_eq!(req.season.expect("a season"), 1);
538 assert_eq!(req.episode.expect("an episode"), 5);
539 assert_eq!(req.resolution.expect("a resolution"), RsResolution::UHD);
540 assert_eq!(req.videocodec.expect("a videocodec"), RsVideoCodec::H265);
541
542 Ok(())
543 }
544}
545