1use std::{collections::HashMap, str::FromStr};
2
3use crate::{PluginCredential, RsFileType, RsVideoFormat};
4use crate::{RsAudio, RsResolution, RsVideoCodec};
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use strum_macros::EnumString;
9use urlencoding::decode;
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
32 .next()
33 .ok_or(error::RequestError::UnableToParseCookieString(
34 "domain".to_owned(),
35 line.to_owned(),
36 ))?
37 .to_owned(),
38 http_only: "true"
39 == splitted
40 .next()
41 .ok_or(error::RequestError::UnableToParseCookieString(
42 "http_only".to_owned(),
43 line.to_owned(),
44 ))?,
45 path: splitted
46 .next()
47 .ok_or(error::RequestError::UnableToParseCookieString(
48 "path".to_owned(),
49 line.to_owned(),
50 ))?
51 .to_owned(),
52 secure: "true"
53 == splitted
54 .next()
55 .ok_or(error::RequestError::UnableToParseCookieString(
56 "secure".to_owned(),
57 line.to_owned(),
58 ))?,
59 expiration: {
60 let t = splitted
61 .next()
62 .ok_or(error::RequestError::UnableToParseCookieString(
63 "expiration".to_owned(),
64 line.to_owned(),
65 ))?
66 .to_owned();
67 if t.is_empty() {
68 None
69 } else {
70 Some(t.parse().map_err(|_| {
71 error::RequestError::UnableToParseCookieString(
72 "expiration parsing".to_owned(),
73 line.to_owned(),
74 )
75 })?)
76 }
77 },
78 name: splitted
79 .next()
80 .ok_or(error::RequestError::UnableToParseCookieString(
81 "name".to_owned(),
82 line.to_owned(),
83 ))?
84 .to_owned(),
85 value: splitted
86 .next()
87 .ok_or(error::RequestError::UnableToParseCookieString(
88 "value".to_owned(),
89 line.to_owned(),
90 ))?
91 .to_owned(),
92 })
93 }
94}
95
96impl RsCookie {
97 pub fn netscape(&self) -> String {
98 let second = if self.domain.starts_with('.') {
99 "TRUE"
100 } else {
101 "FALSE"
102 };
103 let secure = if self.secure { "TRUE" } else { "FALSE" };
104 let expiration = if let Some(expiration) = self.expiration {
105 (expiration as u32).to_string()
106 } else {
107 "".to_owned()
108 };
109 format!(
111 "{}\t{}\t{}\t{}\t{}\t{}\t{}",
112 self.domain, second, self.path, secure, expiration, self.name, self.value
113 )
114 }
115
116 pub fn header(&self) -> String {
117 format!("{}={}", self.name, self.value)
118 }
119}
120
121pub trait RsCookies {
122 fn header_value(&self) -> String;
123 fn headers(&self) -> (String, String);
124}
125
126impl RsCookies for Vec<RsCookie> {
127 fn header_value(&self) -> String {
128 self.iter()
129 .map(|t| t.header())
130 .collect::<Vec<String>>()
131 .join("; ")
132 }
133
134 fn headers(&self) -> (String, String) {
135 (
136 "cookie".to_owned(),
137 self.iter()
138 .map(|t| t.header())
139 .collect::<Vec<String>>()
140 .join("; "),
141 )
142 }
143}
144
145#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
146#[serde(rename_all = "camelCase")]
147pub struct RsRequest {
148 pub upload_id: Option<String>,
149 pub url: String,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub mime: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub size: Option<u64>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub filename: Option<String>,
156 #[serde(default)]
157 pub status: RsRequestStatus,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub plugin_id: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub plugin_name: Option<String>,
162
163 #[serde(default)]
166 pub permanent: bool,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub instant: Option<bool>,
170
171 pub json_body: Option<Value>,
172 #[serde(default)]
173 pub method: RsRequestMethod,
174
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub referer: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub headers: Option<Vec<(String, String)>>,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub cookies: Option<Vec<RsCookie>>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub files: Option<Vec<RsRequestFiles>>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub selected_file: Option<String>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub description: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub tags: Option<Vec<String>>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub people: Option<Vec<String>>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub albums: Option<Vec<String>>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub season: Option<u32>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub episode: Option<u32>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub movie: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub language: Option<String>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub resolution: Option<RsResolution>,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub video_format: Option<RsVideoFormat>,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub videocodec: Option<RsVideoCodec>,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub audio: Option<Vec<RsAudio>>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub quality: Option<u64>,
215
216 #[serde(default)]
217 pub ignore_origin_duplicate: bool,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub thumbnail_url: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub origin_url: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub title: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub kind: Option<RsFileType>,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub tags_lookup: Option<Vec<String>>,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub people_lookup: Option<Vec<String>>,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub albums_lookup: Option<Vec<String>>,
236}
237
238impl RsRequest {
239 pub fn set_cookies(&mut self, cookies: Vec<RsCookie>) {
240 let mut existing = if let Some(headers) = &self.headers {
241 headers.to_owned()
242 } else {
243 vec![]
244 };
245 existing.push(cookies.headers());
246 self.headers = Some(existing);
247 }
248
249 pub fn filename_or_extract_from_url(&self) -> Option<String> {
250 if self.filename.is_some() {
251 self.filename.clone()
252 } else {
253 self.url
254 .split('/')
255 .last()
256 .and_then(|segment| {
257 segment
258 .split('?')
259 .next()
260 .filter(|s| !s.is_empty())
261 .map(|s| s.to_string())
262 })
263 .and_then(|potential| {
264 let extension = potential
265 .split('.')
266 .map(|t| t.to_string())
267 .collect::<Vec<String>>();
268 if extension.len() > 1
269 && extension.last().unwrap_or(&"".to_string()).len() > 2
270 && extension.last().unwrap_or(&"".to_string()).len() < 5
271 {
272 let decoded = decode(&potential)
273 .map(|x| x.into_owned())
274 .unwrap_or(potential); Some(decoded)
276 } else {
277 None
278 }
279 })
280 }
281 }
282
283 pub fn parse_filename(&mut self) {
284 if let Some(filename) = &self.filename {
285 let resolution = RsResolution::from_filename(filename);
286 if resolution != RsResolution::Unknown {
287 self.resolution = Some(resolution);
288 }
289 let video_format = RsVideoFormat::from_filename(filename);
290 if video_format != RsVideoFormat::Other {
291 self.video_format = Some(video_format);
292 }
293 let videocodec = RsVideoCodec::from_filename(filename);
294 if videocodec != RsVideoCodec::Unknown {
295 self.videocodec = Some(videocodec);
296 }
297 let audio = RsAudio::list_from_filename(filename);
298 if !audio.is_empty() {
299 self.audio = Some(audio);
300 }
301
302 let re = Regex::new(r"(?i)s(\d+)e(\d+)").unwrap();
303 if let Some(caps) = re.captures(filename) {
304 self.season = caps[1].parse::<u32>().ok();
305 self.episode = caps[2].parse::<u32>().ok();
306 }
307 }
308 }
309
310 pub fn parse_subfilenames(&mut self) {
311 if let Some(ref mut files) = self.files {
312 for file in files {
313 file.parse_filename();
314 }
315 }
316 }
317}
318
319#[derive(
320 Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default,
321)]
322#[serde(rename_all = "camelCase")]
323#[strum(serialize_all = "camelCase")]
324pub enum RsRequestStatus {
325 #[default]
327 Unprocessed,
328 Processed,
330 NeedParsing,
332 RequireAdd,
336 Intermediate,
338 NeedFileSelection,
340 FinalPrivate,
342 FinalPublic,
344}
345
346#[derive(
347 Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default,
348)]
349#[serde(rename_all = "camelCase")]
350#[strum(serialize_all = "camelCase")]
351pub enum RsRequestMethod {
352 #[default]
353 Get,
354 Post,
355 Patch,
356 Delete,
357 Head,
358}
359
360#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
361#[serde(rename_all = "camelCase")]
362pub struct RsRequestFiles {
363 pub name: String,
364 pub size: u64,
365
366 pub mime: Option<String>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub description: Option<String>,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub tags: Option<Vec<String>>,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub people: Option<Vec<String>>,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub albums: Option<Vec<String>>,
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub season: Option<u32>,
377 #[serde(skip_serializing_if = "Option::is_none")]
378 pub episode: Option<u32>,
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub language: Option<String>,
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub resolution: Option<RsResolution>,
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub video_format: Option<RsVideoFormat>,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub videocodec: Option<RsVideoCodec>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub audio: Option<Vec<RsAudio>>,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub quality: Option<u64>,
391
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub tags_lookup: Option<Vec<String>>,
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub people_lookup: Option<Vec<String>>,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 pub albums_lookup: Option<Vec<String>>,
399}
400
401impl RsRequestFiles {
402 pub fn parse_filename(&mut self) {
403 let resolution = RsResolution::from_filename(&self.name);
404 if resolution != RsResolution::Unknown {
405 self.resolution = Some(resolution);
406 }
407 let video_format = RsVideoFormat::from_filename(&self.name);
408 if video_format != RsVideoFormat::Other {
409 self.video_format = Some(video_format);
410 }
411 let videocodec = RsVideoCodec::from_filename(&self.name);
412 if videocodec != RsVideoCodec::Unknown {
413 self.videocodec = Some(videocodec);
414 }
415 let audio = RsAudio::list_from_filename(&self.name);
416 if !audio.is_empty() {
417 self.audio = Some(audio);
418 }
419
420 let re = Regex::new(r"(?i)s(\d+)e(\d+)").unwrap();
421 if let Some(caps) = re.captures(&self.name) {
422 self.season = caps[1].parse::<u32>().ok();
423 self.episode = caps[2].parse::<u32>().ok();
424 }
425 }
426}
427
428#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
429#[serde(rename_all = "camelCase")]
430pub struct RsRequestPluginRequest {
431 pub request: RsRequest,
432 pub credential: Option<PluginCredential>,
433 pub params: Option<HashMap<String, String>>,
434}
435
436#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
438#[serde(rename_all = "camelCase")]
439pub struct RsGroupDownload {
440 #[serde(default)]
442 pub group: bool,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub group_thumbnail_url: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub group_filename: Option<String>,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub group_mime: Option<String>,
449 pub requests: Vec<RsRequest>,
450}
451
452#[derive(
454 Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default,
455)]
456#[serde(rename_all = "camelCase")]
457#[strum(serialize_all = "camelCase")]
458pub enum RsProcessingStatus {
459 #[default]
460 Pending,
461 Processing,
462 Finished,
463 Error,
464 Paused,
465}
466
467#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
469#[serde(rename_all = "camelCase")]
470pub struct RsRequestAddResponse {
471 pub processing_id: String,
473 #[serde(default)]
475 pub status: RsProcessingStatus,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub eta: Option<i64>,
479}
480
481#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
483#[serde(rename_all = "camelCase")]
484pub struct RsProcessingProgress {
485 pub processing_id: String,
487 pub progress: u32,
489 pub status: RsProcessingStatus,
491 #[serde(skip_serializing_if = "Option::is_none")]
493 pub error: Option<String>,
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub eta: Option<i64>,
497 #[serde(skip_serializing_if = "Option::is_none")]
499 pub request: Option<Box<RsRequest>>,
500}
501
502#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
504#[serde(rename_all = "camelCase")]
505pub struct RsProcessingActionRequest {
506 pub processing_id: String,
508 pub credential: Option<PluginCredential>,
510 pub params: Option<HashMap<String, String>>,
512}
513
514#[cfg(test)]
515mod tests {
516
517 use self::error::RequestError;
518
519 use super::*;
520
521 #[test]
522 fn test_cookie_parsing() -> Result<(), RequestError> {
523 let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
524 assert!(parsed.domain == ".twitter.com".to_owned());
525 assert!(parsed.http_only == false);
526 assert!(parsed.path == "/".to_owned());
527 assert!(parsed.secure == true);
528 assert!(parsed.expiration == Some(1722364794.437907));
529 assert!(parsed.name == "kdt".to_owned());
530 assert!(parsed.value == "w1j".to_owned());
531 Ok(())
532 }
533
534 #[test]
535 fn test_cookie_parsing_no_expi() -> Result<(), RequestError> {
536 let parsed = RsCookie::from_str(".twitter.com;false;/;true;;kdt;w1j")?;
537 assert!(parsed.domain == ".twitter.com".to_owned());
538 assert!(parsed.http_only == false);
539 assert!(parsed.path == "/".to_owned());
540 assert!(parsed.secure == true);
541 assert!(parsed.expiration == None);
542 assert!(parsed.name == "kdt".to_owned());
543 assert!(parsed.value == "w1j".to_owned());
544 Ok(())
545 }
546
547 #[test]
548 fn test_netscape() -> Result<(), RequestError> {
549 let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
550 assert!(parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1722364794\tkdt\tw1j");
551 Ok(())
552 }
553 #[test]
554 fn test_netscape_doublequote() -> Result<(), RequestError> {
555 let parsed = RsCookie::from_str(
556 ".twitter.com;true;/;true;1726506480.700665;ads_prefs;\"HBESAAA=\"",
557 )?;
558 assert!(
559 parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1726506480\tads_prefs\t\"HBESAAA=\""
560 );
561 Ok(())
562 }
563
564 #[test]
565 fn test_parse_filename() -> Result<(), RequestError> {
566 let req = RsRequest {
567 url: "http://www.test.com/filename.mp4?toto=3".to_string(),
568 filename: Some("test.mkv".to_owned()),
569 ..Default::default()
570 };
571 assert_eq!(
572 req.filename_or_extract_from_url(),
573 Some("test.mkv".to_string())
574 );
575 let req = RsRequest {
576 url: "http://www.test.com/filename.mp4?toto=3".to_string(),
577 ..Default::default()
578 };
579 assert_eq!(
580 req.filename_or_extract_from_url(),
581 Some("filename.mp4".to_string()),
582 "We are expecting a filename from the url"
583 );
584 let req = RsRequest {
585 url: "http://www.test.com/notfilename?toto=3".to_string(),
586 ..Default::default()
587 };
588 assert_eq!(
589 req.filename_or_extract_from_url(),
590 None,
591 "Should return none as there is no filename with extensiopn in url"
592 );
593 let req = RsRequest {
594 url: "http://www.test.com/notfilename.toolong?toto=3".to_string(),
595 ..Default::default()
596 };
597 assert_eq!(
598 req.filename_or_extract_from_url(),
599 None,
600 "Should return none as too long after dot is not an extension"
601 );
602 let req = RsRequest {
603 url: "http://www.test.com/filename%20test.mp4?toto=3".to_string(),
604 ..Default::default()
605 };
606 assert_eq!(
607 req.filename_or_extract_from_url(),
608 Some("filename test.mp4".to_string()),
609 "Should decode URL-encoded filename"
610 );
611 Ok(())
612 }
613
614 #[test]
615 fn test_header() -> Result<(), RequestError> {
616 let parsed = vec![
617 RsCookie::from_str(
618 ".twitter.com;true;/;true;1726506480.700665;ads_prefs;\"HBESAAA=\"",
619 )?,
620 RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?,
621 ];
622 println!("header: {}", parsed.header_value());
623 assert!(parsed.header_value() == "ads_prefs=\"HBESAAA=\"; kdt=w1j");
624 Ok(())
625 }
626
627 #[test]
628 fn test_parse() -> Result<(), RequestError> {
629 let mut req = RsRequest {
630 filename: Some(
631 "Shogun.2024.S01E01.Anjin.1080p.VOSTFR.DSNP.WEB-DL.DDP5.1.H.264-NTb.mkv".to_owned(),
632 ),
633 ..Default::default()
634 };
635 req.parse_filename();
636 assert_eq!(req.season.unwrap(), 1);
637 assert_eq!(req.episode.unwrap(), 1);
638 assert_eq!(req.resolution.unwrap(), RsResolution::FullHD);
639 assert_eq!(req.videocodec.unwrap(), RsVideoCodec::H264);
640 assert_eq!(req.video_format.unwrap(), RsVideoFormat::Mkv);
641 assert_eq!(req.audio.unwrap().len(), 1);
642 Ok(())
643 }
644
645 #[test]
646 fn test_parse2() -> Result<(), RequestError> {
647 let mut req = RsRequest {
648 filename: Some("Shogun.2024.S01E05.MULTi.HDR.DV.2160p.WEB.H265-FW".to_owned()),
649 ..Default::default()
650 };
651 req.parse_filename();
652 assert_eq!(req.season.expect("a season"), 1);
653 assert_eq!(req.episode.expect("an episode"), 5);
654 assert_eq!(req.resolution.expect("a resolution"), RsResolution::UHD);
655 assert_eq!(req.videocodec.expect("a videocodec"), RsVideoCodec::H265);
656
657 Ok(())
658 }
659}