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
323#[derive(
324 Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default,
325)]
326#[serde(rename_all = "camelCase")]
327#[strum(serialize_all = "camelCase")]
328pub enum RsRequestStatus {
329 #[default]
331 Unprocessed,
332 Processed,
334 NeedParsing,
336 RequireAdd,
340 Intermediate,
342 NeedFileSelection,
344 FinalPrivate,
346 FinalPublic,
348}
349
350#[derive(
351 Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default,
352)]
353#[serde(rename_all = "camelCase")]
354#[strum(serialize_all = "camelCase")]
355pub enum RsRequestMethod {
356 #[default]
357 Get,
358 Post,
359 Patch,
360 Delete,
361 Head,
362}
363
364#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
365#[serde(rename_all = "camelCase")]
366pub struct RsRequestFiles {
367 pub name: String,
368 pub size: u64,
369
370 pub mime: Option<String>,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub description: Option<String>,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub tags: Option<Vec<String>>,
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub people: Option<Vec<String>>,
377 #[serde(skip_serializing_if = "Option::is_none")]
378 pub albums: Option<Vec<String>>,
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub season: Option<u32>,
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub episode: Option<u32>,
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub language: Option<String>,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub resolution: Option<RsResolution>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub video_format: Option<RsVideoFormat>,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub videocodec: Option<RsVideoCodec>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub audio: Option<Vec<RsAudio>>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub quality: Option<u64>,
395
396 #[serde(skip_serializing_if = "Option::is_none")]
398 pub tags_lookup: Option<Vec<String>>,
399 #[serde(skip_serializing_if = "Option::is_none")]
400 pub people_lookup: Option<Vec<String>>,
401 #[serde(skip_serializing_if = "Option::is_none")]
402 pub albums_lookup: Option<Vec<String>>,
403}
404
405impl RsRequestFiles {
406 pub fn parse_filename(&mut self) {
407 let resolution = RsResolution::from_filename(&self.name);
408 if resolution != RsResolution::Unknown {
409 self.resolution = Some(resolution);
410 }
411 let video_format = RsVideoFormat::from_filename(&self.name);
412 if video_format != RsVideoFormat::Other {
413 self.video_format = Some(video_format);
414 }
415 let videocodec = RsVideoCodec::from_filename(&self.name);
416 if videocodec != RsVideoCodec::Unknown {
417 self.videocodec = Some(videocodec);
418 }
419 let audio = RsAudio::list_from_filename(&self.name);
420 if !audio.is_empty() {
421 self.audio = Some(audio);
422 }
423
424 let re = Regex::new(r"(?i)s(\d+)e(\d+)").unwrap();
425 if let Some(caps) = re.captures(&self.name) {
426 self.season = caps[1].parse::<u32>().ok();
427 self.episode = caps[2].parse::<u32>().ok();
428 }
429 }
430}
431
432#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
433#[serde(rename_all = "camelCase")]
434pub struct RsRequestPluginRequest {
435 pub request: RsRequest,
436 pub credential: Option<PluginCredential>,
437 pub params: Option<HashMap<String, CustomParamTypes>>,
438}
439
440#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
442#[serde(rename_all = "camelCase")]
443pub struct RsGroupDownload {
444 #[serde(default)]
446 pub group: bool,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub group_thumbnail_url: Option<String>,
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub group_filename: Option<String>,
451 #[serde(skip_serializing_if = "Option::is_none")]
452 pub group_mime: Option<String>,
453 pub requests: Vec<RsRequest>,
454
455 #[serde(skip_serializing_if = "Option::is_none")]
456 pub infos: Option<MediaForUpdate>,
457
458 #[serde(skip_serializing_if = "Option::is_none")]
459 pub match_type: Option<RsLookupMatchType>,
460}
461
462#[derive(
464 Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default,
465)]
466#[serde(rename_all = "camelCase")]
467#[strum(serialize_all = "camelCase")]
468pub enum RsProcessingStatus {
469 #[default]
470 Pending,
471 Processing,
472 Finished,
473 Error,
474 Paused,
475}
476
477#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
479#[serde(rename_all = "camelCase")]
480pub struct RsRequestAddResponse {
481 pub processing_id: String,
483 #[serde(default)]
485 pub status: RsProcessingStatus,
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub eta: Option<i64>,
489}
490
491#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
493#[serde(rename_all = "camelCase")]
494pub struct RsProcessingProgress {
495 pub processing_id: String,
497 pub progress: u32,
499 pub status: RsProcessingStatus,
501 #[serde(skip_serializing_if = "Option::is_none")]
503 pub error: Option<String>,
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub eta: Option<i64>,
507 #[serde(skip_serializing_if = "Option::is_none")]
509 pub request: Option<Box<RsRequest>>,
510}
511
512#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
514#[serde(rename_all = "camelCase")]
515pub struct RsProcessingActionRequest {
516 pub processing_id: String,
518 pub credential: Option<PluginCredential>,
520 pub params: Option<HashMap<String, CustomParamTypes>>,
522}
523
524#[cfg(test)]
525mod tests {
526
527 use self::error::RequestError;
528
529 use super::*;
530
531 #[test]
532 fn test_cookie_parsing() -> Result<(), RequestError> {
533 let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
534 assert!(parsed.domain == ".twitter.com".to_owned());
535 assert!(parsed.http_only == false);
536 assert!(parsed.path == "/".to_owned());
537 assert!(parsed.secure == true);
538 assert!(parsed.expiration == Some(1722364794.437907));
539 assert!(parsed.name == "kdt".to_owned());
540 assert!(parsed.value == "w1j".to_owned());
541 Ok(())
542 }
543
544 #[test]
545 fn test_cookie_parsing_no_expi() -> Result<(), RequestError> {
546 let parsed = RsCookie::from_str(".twitter.com;false;/;true;;kdt;w1j")?;
547 assert!(parsed.domain == ".twitter.com".to_owned());
548 assert!(parsed.http_only == false);
549 assert!(parsed.path == "/".to_owned());
550 assert!(parsed.secure == true);
551 assert!(parsed.expiration == None);
552 assert!(parsed.name == "kdt".to_owned());
553 assert!(parsed.value == "w1j".to_owned());
554 Ok(())
555 }
556
557 #[test]
558 fn test_netscape() -> Result<(), RequestError> {
559 let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
560 assert!(parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1722364794\tkdt\tw1j");
561 Ok(())
562 }
563 #[test]
564 fn test_netscape_doublequote() -> Result<(), RequestError> {
565 let parsed = RsCookie::from_str(
566 ".twitter.com;true;/;true;1726506480.700665;ads_prefs;\"HBESAAA=\"",
567 )?;
568 assert!(
569 parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1726506480\tads_prefs\t\"HBESAAA=\""
570 );
571 Ok(())
572 }
573
574 #[test]
575 fn test_parse_filename() -> Result<(), RequestError> {
576 let req = RsRequest {
577 url: "http://www.test.com/filename.mp4?toto=3".to_string(),
578 filename: Some("test.mkv".to_owned()),
579 ..Default::default()
580 };
581 assert_eq!(
582 req.filename_or_extract_from_url(),
583 Some("test.mkv".to_string())
584 );
585 let req = RsRequest {
586 url: "http://www.test.com/filename.mp4?toto=3".to_string(),
587 ..Default::default()
588 };
589 assert_eq!(
590 req.filename_or_extract_from_url(),
591 Some("filename.mp4".to_string()),
592 "We are expecting a filename from the url"
593 );
594 let req = RsRequest {
595 url: "http://www.test.com/notfilename?toto=3".to_string(),
596 ..Default::default()
597 };
598 assert_eq!(
599 req.filename_or_extract_from_url(),
600 None,
601 "Should return none as there is no filename with extensiopn in url"
602 );
603 let req = RsRequest {
604 url: "http://www.test.com/notfilename.toolong?toto=3".to_string(),
605 ..Default::default()
606 };
607 assert_eq!(
608 req.filename_or_extract_from_url(),
609 None,
610 "Should return none as too long after dot is not an extension"
611 );
612 let req = RsRequest {
613 url: "http://www.test.com/filename%20test.mp4?toto=3".to_string(),
614 ..Default::default()
615 };
616 assert_eq!(
617 req.filename_or_extract_from_url(),
618 Some("filename test.mp4".to_string()),
619 "Should decode URL-encoded filename"
620 );
621 Ok(())
622 }
623
624 #[test]
625 fn test_header() -> Result<(), RequestError> {
626 let parsed = vec![
627 RsCookie::from_str(
628 ".twitter.com;true;/;true;1726506480.700665;ads_prefs;\"HBESAAA=\"",
629 )?,
630 RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?,
631 ];
632 println!("header: {}", parsed.header_value());
633 assert!(parsed.header_value() == "ads_prefs=\"HBESAAA=\"; kdt=w1j");
634 Ok(())
635 }
636
637 #[test]
638 fn test_parse() -> Result<(), RequestError> {
639 let mut req = RsRequest {
640 filename: Some(
641 "Shogun.2024.S01E01.Anjin.1080p.VOSTFR.DSNP.WEB-DL.DDP5.1.H.264-NTb.mkv".to_owned(),
642 ),
643 ..Default::default()
644 };
645 req.parse_filename();
646 assert_eq!(req.season.unwrap(), 1);
647 assert_eq!(req.episode.unwrap(), 1);
648 assert_eq!(req.resolution.unwrap(), RsResolution::FullHD);
649 assert_eq!(req.videocodec.unwrap(), RsVideoCodec::H264);
650 assert_eq!(req.video_format.unwrap(), RsVideoFormat::Mkv);
651 assert_eq!(req.audio.unwrap().len(), 1);
652 Ok(())
653 }
654
655 #[test]
656 fn test_parse2() -> Result<(), RequestError> {
657 let mut req = RsRequest {
658 filename: Some("Shogun.2024.S01E05.MULTi.HDR.DV.2160p.WEB.H265-FW".to_owned()),
659 ..Default::default()
660 };
661 req.parse_filename();
662 assert_eq!(req.season.expect("a season"), 1);
663 assert_eq!(req.episode.expect("an episode"), 5);
664 assert_eq!(req.resolution.expect("a resolution"), RsResolution::UHD);
665 assert_eq!(req.videocodec.expect("a videocodec"), RsVideoCodec::H265);
666
667 Ok(())
668 }
669}