1use std::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
103 #[serde(default)]
106 pub permanent: bool,
107
108 pub json_body: Option<Value>,
109 #[serde(default)]
110 pub method: RsRequestMethod,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub referer: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub headers: Option<Vec<(String, String)>>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub cookies: Option<Vec<RsCookie>>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub files: Option<Vec<RsRequestFiles>>,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub selected_file: Option<String>,
125
126
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub description: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub tags: Option<Vec<String>>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub people: Option<Vec<String>>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub albums: Option<Vec<String>>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub season: Option<u32>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub episode: Option<u32>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub language: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub resolution: Option<RsResolution>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub video_format: Option<RsVideoFormat>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub videocodec: Option<RsVideoCodec>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub audio: Option<Vec<RsAudio>>,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub quality: Option<u64>,
151
152 #[serde(default)]
153 pub ignore_origin_duplicate: bool,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub thumbnail_url: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub origin_url: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub title: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub kind: Option<RsFileType>,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub tags_lookup: Option<Vec<String>>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub people_lookup: Option<Vec<String>>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub albums_lookup: Option<Vec<String>>,
172}
173
174impl RsRequest {
175 pub fn set_cookies(&mut self, cookies: Vec<RsCookie>) {
176 let mut existing = if let Some(headers) = &self.headers {
177 headers.to_owned()
178 } else{
179 vec![]
180 };
181 existing.push(cookies.headers());
182 self.headers = Some(existing);
183 }
184
185 pub fn filename_or_extract_from_url(&self) -> Option<String> {
186 if self.filename.is_some() {
187 self.filename.clone()
188 } else {
189 self.url.split('/')
190 .last()
191 .and_then(|segment| {
192 segment.split('?')
193 .next()
194 .filter(|s| !s.is_empty())
195 .map(|s| s.to_string())
196 })
197 .and_then(|potential| {
198 let extension = potential.split('.').map(|t| t.to_string()).collect::<Vec<String>>();
199 if extension.len() > 1 && extension.last().unwrap_or(&"".to_string()).len() > 2 && extension.last().unwrap_or(&"".to_string()).len() < 5{
200 let decoded = decode(&potential).map(|x| x.into_owned()).unwrap_or(potential); Some(decoded)
202 } else {
203 None
204 }
205 })
206 }
207
208 }
209
210 pub fn parse_filename(&mut self) {
211 if let Some(filename) = &self.filename {
212 let resolution = RsResolution::from_filename(filename);
213 if resolution != RsResolution::Unknown {
214 self.resolution = Some(resolution);
215 }
216 let video_format = RsVideoFormat::from_filename(filename);
217 if video_format != RsVideoFormat::Other {
218 self.video_format = Some(video_format);
219 }
220 let videocodec = RsVideoCodec::from_filename(filename);
221 if videocodec != RsVideoCodec::Unknown {
222 self.videocodec = Some(videocodec);
223 }
224 let audio = RsAudio::list_from_filename(filename);
225 if !audio.is_empty() {
226 self.audio = Some(audio);
227 }
228
229 let re = Regex::new(r"(?i)s(\d+)e(\d+)").unwrap();
230 if let Some(caps) = re.captures(filename) {
231 self.season = caps[1].parse::<u32>().ok();
232 self.episode = caps[2].parse::<u32>().ok();
233 }
234 }
235
236 }
237
238 pub fn parse_subfilenames(&mut self) {
239 if let Some(ref mut files) = self.files {
240 for file in files {
241 file.parse_filename();
242 }
243 }
244 }
245}
246
247#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display,EnumString, Default)]
248#[serde(rename_all = "camelCase")]
249#[strum(serialize_all = "camelCase")]
250pub enum RsRequestStatus {
251 #[default]
253 Unprocessed,
254 Processed,
256 NeedParsing,
258 RequireAdd,
262 Intermediate,
264 NeedFileSelection,
266 FinalPrivate,
268 FinalPublic
270}
271
272
273#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display,EnumString, Default)]
274#[serde(rename_all = "camelCase")]
275#[strum(serialize_all = "camelCase")]
276pub enum RsRequestMethod {
277
278 #[default]
279 Get,
280 Post,
281 Patch,
282 Delete,
283 Head,
284
285}
286
287
288
289#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
290#[serde(rename_all = "camelCase")]
291pub struct RsRequestFiles {
292 pub name: String,
293 pub size: u64,
294
295 pub mime: Option<String>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub description: Option<String>,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub tags: Option<Vec<String>>,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub people: Option<Vec<String>>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub albums: Option<Vec<String>>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub season: Option<u32>,
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub episode: Option<u32>,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub language: Option<String>,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub resolution: Option<RsResolution>,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 pub video_format: Option<RsVideoFormat>,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub videocodec: Option<RsVideoCodec>,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub audio: Option<Vec<RsAudio>>,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub quality: Option<u64>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub tags_lookup: Option<Vec<String>>,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub people_lookup: Option<Vec<String>>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub albums_lookup: Option<Vec<String>>,
328}
329
330impl RsRequestFiles {
331 pub fn parse_filename(&mut self) {
332 let resolution = RsResolution::from_filename(&self.name);
333 if resolution != RsResolution::Unknown {
334 self.resolution = Some(resolution);
335 }
336 let video_format = RsVideoFormat::from_filename(&self.name);
337 if video_format != RsVideoFormat::Other {
338 self.video_format = Some(video_format);
339 }
340 let videocodec = RsVideoCodec::from_filename(&self.name);
341 if videocodec != RsVideoCodec::Unknown {
342 self.videocodec = Some(videocodec);
343 }
344 let audio = RsAudio::list_from_filename(&self.name);
345 if !audio.is_empty() {
346 self.audio = Some(audio);
347 }
348
349 let re = Regex::new(r"(?i)s(\d+)e(\d+)").unwrap();
350 if let Some(caps) = re.captures(&self.name) {
351 self.season = caps[1].parse::<u32>().ok();
352 self.episode = caps[2].parse::<u32>().ok();
353 }
354 }
355}
356
357#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
358#[serde(rename_all = "camelCase")]
359pub struct RsRequestPluginRequest {
360 pub request: RsRequest,
361 pub credential: Option<PluginCredential>,
362}
363
364#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
366#[serde(rename_all = "camelCase")]
367pub struct RsGroupDownload {
368 #[serde(default)]
370 pub group: bool,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub group_thumbnail_url: Option<String>,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub group_filename: Option<String>,
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub group_mime: Option<String>,
377 pub requests: Vec<RsRequest>,
378}
379
380
381
382#[cfg(test)]
383mod tests {
384
385 use self::error::RequestError;
386
387 use super::*;
388
389 #[test]
390 fn test_cookie_parsing() -> Result<(), RequestError> {
391 let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
392 assert!(parsed.domain == ".twitter.com".to_owned());
393 assert!(parsed.http_only == false);
394 assert!(parsed.path == "/".to_owned());
395 assert!(parsed.secure == true);
396 assert!(parsed.expiration == Some(1722364794.437907));
397 assert!(parsed.name == "kdt".to_owned());
398 assert!(parsed.value == "w1j".to_owned());
399 Ok(())
400 }
401
402 #[test]
403 fn test_cookie_parsing_no_expi() -> Result<(), RequestError> {
404 let parsed = RsCookie::from_str(".twitter.com;false;/;true;;kdt;w1j")?;
405 assert!(parsed.domain == ".twitter.com".to_owned());
406 assert!(parsed.http_only == false);
407 assert!(parsed.path == "/".to_owned());
408 assert!(parsed.secure == true);
409 assert!(parsed.expiration == None);
410 assert!(parsed.name == "kdt".to_owned());
411 assert!(parsed.value == "w1j".to_owned());
412 Ok(())
413 }
414
415 #[test]
416 fn test_netscape() -> Result<(), RequestError> {
417 let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
418 assert!(parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1722364794\tkdt\tw1j");
419 Ok(())
420 }
421 #[test]
422 fn test_netscape_doublequote() -> Result<(), RequestError> {
423 let parsed = RsCookie::from_str(".twitter.com;true;/;true;1726506480.700665;ads_prefs;\"HBESAAA=\"")?;
424 assert!(parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1726506480\tads_prefs\t\"HBESAAA=\"");
425 Ok(())
426 }
427
428 #[test]
429 fn test_parse_filename() -> Result<(), RequestError> {
430 let req = RsRequest {url: "http://www.test.com/filename.mp4?toto=3".to_string(), filename: Some("test.mkv".to_owned()), ..Default::default()};
431 assert_eq!(req.filename_or_extract_from_url(), Some("test.mkv".to_string()));
432 let req = RsRequest {url: "http://www.test.com/filename.mp4?toto=3".to_string(), ..Default::default()};
433 assert_eq!(req.filename_or_extract_from_url(), Some("filename.mp4".to_string()), "We are expecting a filename from the url");
434 let req = RsRequest {url: "http://www.test.com/notfilename?toto=3".to_string(), ..Default::default()};
435 assert_eq!(req.filename_or_extract_from_url(), None, "Should return none as there is no filename with extensiopn in url");
436 let req = RsRequest {url: "http://www.test.com/notfilename.toolong?toto=3".to_string(), ..Default::default()};
437 assert_eq!(req.filename_or_extract_from_url(), None, "Should return none as too long after dot is not an extension");
438 let req = RsRequest {url: "http://www.test.com/filename%20test.mp4?toto=3".to_string(), ..Default::default()};
439 assert_eq!(req.filename_or_extract_from_url(), Some("filename test.mp4".to_string()), "Should decode URL-encoded filename");
440 Ok(())
441 }
442
443
444 #[test]
445 fn test_header() -> Result<(), RequestError> {
446 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")?];
447 println!("header: {}", parsed.header_value());
448 assert!(parsed.header_value() == "ads_prefs=\"HBESAAA=\"; kdt=w1j");
449 Ok(())
450 }
451
452 #[test]
453 fn test_parse() -> Result<(), RequestError> {
454 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()};
455 req.parse_filename();
456 assert_eq!(req.season.unwrap(), 1);
457 assert_eq!(req.episode.unwrap(), 1);
458 assert_eq!(req.resolution.unwrap(), RsResolution::FullHD);
459 assert_eq!(req.videocodec.unwrap(), RsVideoCodec::H264);
460 assert_eq!(req.video_format.unwrap(), RsVideoFormat::Mkv);
461 assert_eq!(req.audio.unwrap().len(), 1);
462 Ok(())
463 }
464
465 #[test]
466 fn test_parse2() -> Result<(), RequestError> {
467 let mut req = RsRequest { filename: Some("Shogun.2024.S01E05.MULTi.HDR.DV.2160p.WEB.H265-FW".to_owned()), ..Default::default()};
468 req.parse_filename();
469 assert_eq!(req.season.expect("a season"), 1);
470 assert_eq!(req.episode.expect("an episode"), 5);
471 assert_eq!(req.resolution.expect("a resolution"), RsResolution::UHD);
472 assert_eq!(req.videocodec.expect("a videocodec"), RsVideoCodec::H265);
473
474 Ok(())
475 }
476}
477