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