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