plugin_request_interfaces/
lib.rs1use 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 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 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 #[serde(skip_serializing_if = "Option::is_none")]
107 pub cookies: Option<Vec<RsCookie>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub files: Option<Vec<RsRequestFiles>>,
111 #[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 #[default]
185 Unprocessed,
186 NeedParsing,
188 RequireAdd,
192 Intermediate,
194 NeedFileSelection,
196 FinalPrivate,
198 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 #[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}