plugin_request_interfaces/
lib.rs

1use std::str::FromStr;
2
3use regex::Regex;
4pub use rs_plugin_common_interfaces::PluginCredential;
5use rs_plugin_common_interfaces::{RsAudio, RsResolution, RsVideoCodec};
6use serde::{Deserialize, Serialize};
7use strum_macros::EnumString;
8
9pub mod error;
10
11#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
12#[serde(rename_all = "camelCase")] 
13pub struct RsCookie {
14    pub domain: String,
15    pub http_only: bool,
16    pub path: String,
17    pub secure: bool,
18    pub expiration: Option<f64>,
19    pub name: String,
20    pub value: String,
21}
22
23impl FromStr for RsCookie {
24    type Err = error::RequestError;
25    fn from_str(line: &str) -> Result<Self, Self::Err> {
26        //let [domain, httpOnly, path, secure, expiration, name, value ] = line.split(';');
27        let mut splitted = line.split(';');
28        Ok(RsCookie { 
29            domain: splitted.next().ok_or(error::RequestError::UnableToParseCookieString("domain".to_owned(), line.to_owned()))?.to_owned(), 
30            http_only: "true" == splitted.next().ok_or(error::RequestError::UnableToParseCookieString("http_only".to_owned(), line.to_owned()))?.to_owned(), 
31            path: splitted.next().ok_or(error::RequestError::UnableToParseCookieString("path".to_owned(), line.to_owned()))?.to_owned(), 
32            secure: "true" == splitted.next().ok_or(error::RequestError::UnableToParseCookieString("secure".to_owned(), line.to_owned()))?.to_owned(), 
33            expiration: {
34                let t = splitted.next().ok_or(error::RequestError::UnableToParseCookieString("expiration".to_owned(), line.to_owned()))?.to_owned();
35                if t == "" {
36                    None  
37                } else {
38                    Some(t.parse().map_err(|_| error::RequestError::UnableToParseCookieString("expiration parsing".to_owned(), line.to_owned()))?)
39                }
40            }, 
41            name: splitted.next().ok_or(error::RequestError::UnableToParseCookieString("name".to_owned(), line.to_owned()))?.to_owned(), 
42            value: splitted.next().ok_or(error::RequestError::UnableToParseCookieString("value".to_owned(), line.to_owned()))?.to_owned() })
43    }
44}
45
46impl  RsCookie {
47    pub fn netscape(&self) -> String {
48        let second = if self.domain.starts_with(".") {
49            "TRUE"
50        } else {
51            "FALSE"
52        };
53        let secure = if self.secure {
54            "TRUE"
55        } else {
56            "FALSE"
57        };
58        let expiration = if let Some(expiration) = self.expiration {
59           (expiration as u32).to_string()
60        } else {
61            "".to_owned()
62        };
63        //return [domain, domain.startsWith('.') ? 'TRUE' : 'FALSE', path, secure ? 'TRUE' : 'FALSE', expiration.split('.')[0], name, value].join('\t')
64        format!("{}\t{}\t{}\t{}\t{}\t{}\t{}", self.domain, second, self.path, secure, expiration, self.name, self.value)
65    }
66
67    pub fn header(&self) -> String {
68        format!("{}={}", self.name, self.value)
69    }
70}
71
72pub trait RsCookies {
73    fn header_value(&self) -> String;
74    fn headers(&self) -> (String, String);
75}
76
77impl RsCookies for Vec<RsCookie> {
78    fn header_value(&self) -> String {
79        self.iter().map(|t| t.header()).collect::<Vec<String>>().join("; ")
80    }
81
82    fn headers(&self) -> (String, String) {
83        ("cookie".to_owned(), self.iter().map(|t| t.header()).collect::<Vec<String>>().join("; "))
84    }
85}
86
87#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
88#[serde(rename_all = "camelCase")] 
89pub struct RsRequest {
90    pub upload_id: Option<String>,
91    pub url: String,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub mime: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub size: Option<u64>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub filename: Option<String>,
98    #[serde(default)]
99    pub status: RsRequestStatus,
100    
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub referer: Option<String>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub headers: Option<Vec<(String, String)>>,
105    /// some downloader like YTDL require detailed cookies. You can create Header equivalent  with `headers` fn on the vector
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub cookies: Option<Vec<RsCookie>>,
108    /// If must choose between multiple files. Recall plugin with a `selected_file` containing one of the name in this list to get link
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub files: Option<Vec<RsRequestFiles>>,
111    /// one of the `files` selected for download
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub selected_file: Option<String>,
114
115    
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub description: Option<String>,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub tags: Option<Vec<String>>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub people: Option<Vec<String>>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub albums: Option<Vec<String>>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub season: Option<u32>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub episode: Option<u32>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub language: Option<String>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub resolution: Option<RsResolution>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub videocodec: Option<RsVideoCodec>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub audio: Option<Vec<RsAudio>>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub quality: Option<u64>,
138}
139
140impl RsRequest {
141    pub fn set_cookies(&mut self, cookies: Vec<RsCookie>) {
142        let mut existing = if let Some(headers) = &self.headers {
143            headers.to_owned()
144        } else{
145            vec![]
146        };
147        existing.push(cookies.headers());
148        self.headers = Some(existing);
149    }
150
151    pub fn parse_filename(&mut self) {
152        if let Some(filename) = &self.filename {
153            let resolution = RsResolution::from_filename(filename);
154            if resolution != RsResolution::Unknown {
155                self.resolution = Some(resolution);
156            }
157            let videocodec = RsVideoCodec::from_filename(filename);
158            if videocodec != RsVideoCodec::Unknown {
159                self.videocodec = Some(videocodec);
160            }
161            let audio = RsAudio::list_from_filename(filename);
162            if audio.len() > 0 {
163                self.audio = Some(audio);
164            }
165
166            let re = Regex::new(r"(?i)s(\d+)e(\d+)").unwrap();
167            match re.captures(filename) {
168                Some(caps) => {
169                    self.season = caps[1].parse::<u32>().ok();
170                    self.episode = caps[2].parse::<u32>().ok();
171                }
172                None => (),
173            }
174        }
175 
176    }
177}
178
179#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display,EnumString, Default)]
180#[serde(rename_all = "camelCase")] 
181#[strum(serialize_all = "camelCase")]
182pub enum RsRequestStatus {
183    /// No plugin yet processed this request
184    #[default]
185	Unprocessed,
186    ///if remain in this state after all plugin it will go through YtDl to try to extract medias
187    NeedParsing,
188    /// Link can be processed but first need to be added to the service and downloaded
189    ///   -First call this plugin again with `add` method
190    ///   -Check status and once ready call `process` again
191    RequireAdd,
192    /// Other plugin can process it
193    Intermediate,
194    /// Multiple files found, current plugin need to be recalled with a `selected_file``
195    NeedFileSelection,
196    /// `url` is ready but should be proxied by the server as it contains sensitive informations (like token)
197    FinalPrivate,
198    /// `url` is ready and can be directly sent to _any_ user directly (using redirect)
199    FinalPublic
200}
201
202
203
204#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
205#[serde(rename_all = "camelCase")] 
206pub struct RsRequestFiles {
207    pub name: String,
208    pub size: u64,
209    pub mime: Option<String>,
210}
211
212#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
213#[serde(rename_all = "camelCase")] 
214pub struct RsRequestPluginRequest {
215    pub request: RsRequest,
216    pub credential: Option<PluginCredential>,
217    ///Plugin should only send back request that are usable long term and can be saved to the DB for later use
218    #[serde(default)]
219    pub savable: bool
220}
221
222
223
224#[cfg(test)]
225mod tests {
226
227    use self::error::RequestError;
228
229    use super::*;
230
231    #[test]
232    fn test_cookie_parsing() -> Result<(), RequestError> {
233        let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
234        assert!(parsed.domain == ".twitter.com".to_owned());
235        assert!(parsed.http_only == false);
236        assert!(parsed.path == "/".to_owned());
237        assert!(parsed.secure == true);
238        assert!(parsed.expiration == Some(1722364794.437907));
239        assert!(parsed.name == "kdt".to_owned());
240        assert!(parsed.value == "w1j".to_owned());
241        Ok(())
242    }
243    
244    #[test]
245    fn test_cookie_parsing_no_expi() -> Result<(), RequestError> {
246        let parsed = RsCookie::from_str(".twitter.com;false;/;true;;kdt;w1j")?;
247        assert!(parsed.domain == ".twitter.com".to_owned());
248        assert!(parsed.http_only == false);
249        assert!(parsed.path == "/".to_owned());
250        assert!(parsed.secure == true);
251        assert!(parsed.expiration == None);
252        assert!(parsed.name == "kdt".to_owned());
253        assert!(parsed.value == "w1j".to_owned());
254        Ok(())
255    }
256
257    #[test]
258    fn test_netscape() -> Result<(), RequestError> {
259        let parsed = RsCookie::from_str(".twitter.com;false;/;true;1722364794.437907;kdt;w1j")?;
260        assert!(parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1722364794\tkdt\tw1j");
261        Ok(())
262    }
263    #[test]
264    fn test_netscape_doublequote() -> Result<(), RequestError> {
265        let parsed = RsCookie::from_str(".twitter.com;true;/;true;1726506480.700665;ads_prefs;\"HBESAAA=\"")?;
266        assert!(parsed.netscape() == ".twitter.com\tTRUE\t/\tTRUE\t1726506480\tads_prefs\t\"HBESAAA=\"");
267        Ok(())
268    }
269
270    #[test]
271    fn test_header() -> Result<(), RequestError> {
272        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")?];
273        println!("header: {}", parsed.header_value());
274        assert!(parsed.header_value() == "ads_prefs=\"HBESAAA=\"; kdt=w1j");
275        Ok(())
276    }
277
278    #[test]
279    fn test_parse() -> Result<(), RequestError> {
280        let mut req = RsRequest { filename: Some("Shogun.2024.S01E01.Anjin.1080p.VOSTFR.DSNP.WEB-DL.DDP5.1.H.264-NTb".to_owned()), ..Default::default()};
281        req.parse_filename();
282        assert_eq!(req.season.unwrap(), 1);
283        assert_eq!(req.episode.unwrap(), 1);
284        assert_eq!(req.resolution.unwrap(), RsResolution::FullHD);
285        assert_eq!(req.videocodec.unwrap(), RsVideoCodec::H264);
286        assert_eq!(req.audio.unwrap().len(), 1);
287        Ok(())
288    }
289}