pf_cmd/
lib.rs

1use clap::Parser;
2use futures_util::pin_mut;
3use futures_util::StreamExt;
4use regex::Regex;
5
6fn validate_date(val: &str) -> Result<String, String> {
7    let datetime_regex = Regex::new(
8        r"^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$",
9    )
10    .unwrap();
11
12    if datetime_regex.is_match(val) {
13        Ok(val.to_string())
14    } else {
15        Err(String::from("Invalid date format. Use YYYY-MM-DDTHH:MM:SS plus optional fractional seconds plus an optional timezone specifier in the form Z, +XX, -XX, +XX:XX, or -XX:XX (cf. <https://core.trac.wordpress.org/ticket/41032>)."))
16    }
17}
18
19/// Scans WordPress websites to find videos.
20///
21/// Supported MIME types: video/mp4 and video/quicktime (.mov).
22#[derive(Parser)]
23#[command(name = "pf", author, version, about)]
24pub struct Opt {
25    /// WordPress base URL (e.g. <http://example.com>).
26    pub url: String,
27
28    /// Result set published before a given date (cf. <https://core.trac.wordpress.org/ticket/41032>).
29    #[arg(long, value_parser=validate_date)]
30    pub before: Option<String>,
31
32    /// Result set modified before a given date (cf. <https://core.trac.wordpress.org/ticket/41032>).
33    #[arg(long, value_parser=validate_date)]
34    pub modified_before: Option<String>,
35
36    /// Result set published after a given date (cf. <https://core.trac.wordpress.org/ticket/41032>).
37    #[arg(long, value_parser=validate_date)]
38    pub after: Option<String>,
39
40    /// Result set modified after a given date (cf. <https://core.trac.wordpress.org/ticket/41032>).
41    #[arg(long, value_parser=validate_date)]
42    pub modified_after: Option<String>,
43
44    /// Ensures result set excludes specific IDs.
45    #[arg(long)]
46    pub exclude: Vec<u16>,
47
48    /// Ensures result set excludes specific category IDs.
49    #[arg(long)]
50    pub categories_exclude: Vec<u16>,
51
52    /// Ensures result set excludes to specific tag IDs.
53    #[arg(long)]
54    pub tags_exclude: Vec<u16>,
55}
56
57impl Opt {
58    /// Converts the `Opt` struct to a `FinderConfig` struct.
59    fn to_finder_config(&self) -> pf_lib::FinderConfig {
60        pf_lib::FinderConfig {
61            url: self.url.clone(),
62            target: pf_lib::FinderTarget::Posts {
63                categories_exclude: self.categories_exclude.clone(),
64                tags_exclude: self.tags_exclude.clone(),
65            },
66            before: self.before.clone(),
67            modified_before: self.modified_before.clone(),
68            after: self.after.clone(),
69            modified_after: self.modified_after.clone(),
70            exclude: self.exclude.clone(),
71        }
72    }
73}
74
75/// Runs the `pf` command.
76pub async fn run(opt: Opt) -> Result<(), Box<dyn std::error::Error>> {
77    let mut config = opt.to_finder_config();
78    print_stream(&config).await?;
79    config.target = pf_lib::FinderTarget::Media;
80    print_stream(&config).await?;
81    Ok(())
82}
83
84/// Consumes and prints the `find` stream.
85async fn print_stream(config: &pf_lib::FinderConfig) -> Result<(), Box<dyn std::error::Error>> {
86    let stream = pf_lib::find(config);
87    pin_mut!(stream);
88    while let Some(res) = stream.next().await {
89        match res {
90            Ok(url) => println!("{}", url),
91            Err(e) => eprintln!("{}", e),
92        }
93    }
94    Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_validate_date() {
103        assert!(validate_date("2023-01-01T00:00:00").is_ok());
104        assert!(validate_date("2023-01-01T00:00:00Z").is_ok());
105        assert!(validate_date("2023-01-01T00:00:00+00:00").is_ok());
106        assert!(validate_date("2023-01-01T00:00:00-00:00").is_ok());
107        assert!(validate_date("2023-01-01T00:00:00+00").is_ok());
108        assert!(validate_date("2023-01-01T00:00:00-00").is_ok());
109        assert!(validate_date("2023-01-01T00:00:00+00:00").is_ok());
110        assert!(validate_date("2023-01-01T00:00:00-00:00").is_ok());
111        assert!(validate_date("2023-01-01T00:00:00.123Z").is_ok());
112        assert!(validate_date("2023-01-01T00:00:00.123+00:00").is_ok());
113        assert!(validate_date("2023-01-01T00:00:00.123-00:00").is_ok());
114        assert!(validate_date("2023-01-01T00:00:00.123+00").is_ok());
115        assert!(validate_date("2023-01-01T00:00:00.123-00").is_ok());
116        assert!(validate_date("2023-01-01T00:00:00.123+00:00").is_ok());
117        assert!(validate_date("2023-01-01T00:00:00.123-00:00").is_ok());
118        assert!(validate_date("2023-01-01").is_err());
119        assert!(validate_date("2023-01-01T00:00:00+").is_err());
120        assert!(validate_date("2023-01-01T00:00:00-").is_err());
121        assert!(validate_date("2023-01-01T00:00:00+00:").is_err());
122        assert!(validate_date("2023-01-01T00:00:00-00:").is_err());
123        assert!(validate_date("2023-01-01T00:00:00+00:00:00").is_err());
124    }
125
126    #[test]
127    fn test_opt_parsing() {
128        let args = vec![
129            "pf",
130            "http://example.com",
131            "--before",
132            "2023-01-01T00:00:00",
133            "--modified-before",
134            "2023-01-01T00:00:00",
135            "--after",
136            "2023-01-01T00:00:00",
137            "--modified-after",
138            "2023-01-01T00:00:00",
139            "--exclude",
140            "1",
141            "--exclude",
142            "2",
143            "--categories-exclude",
144            "3",
145            "--tags-exclude",
146            "4",
147        ];
148        let opt = Opt::parse_from(args);
149        assert_eq!(opt.url, "http://example.com");
150        assert_eq!(opt.before, Some("2023-01-01T00:00:00".to_string()));
151        assert_eq!(opt.modified_before, Some("2023-01-01T00:00:00".to_string()));
152        assert_eq!(opt.after, Some("2023-01-01T00:00:00".to_string()));
153        assert_eq!(opt.modified_after, Some("2023-01-01T00:00:00".to_string()));
154        assert_eq!(opt.exclude, vec![1, 2]);
155        assert_eq!(opt.categories_exclude, vec![3]);
156        assert_eq!(opt.tags_exclude, vec![4]);
157    }
158
159    #[test]
160    fn test_to_finder_config() {
161        let opt = Opt {
162            url: "http://example.com".to_string(),
163            before: Some("2023-01-01T00:00:00".to_string()),
164            modified_before: Some("2023-01-01T00:00:00".to_string()),
165            after: Some("2023-01-01T00:00:00".to_string()),
166            modified_after: Some("2023-01-01T00:00:00".to_string()),
167            exclude: vec![1, 2],
168            categories_exclude: vec![3],
169            tags_exclude: vec![4],
170        };
171        let config = opt.to_finder_config();
172        assert_eq!(config.url, "http://example.com");
173        assert_eq!(config.before, Some("2023-01-01T00:00:00".to_string()));
174        assert_eq!(
175            config.modified_before,
176            Some("2023-01-01T00:00:00".to_string())
177        );
178        assert_eq!(config.after, Some("2023-01-01T00:00:00".to_string()));
179        assert_eq!(
180            config.modified_after,
181            Some("2023-01-01T00:00:00".to_string())
182        );
183        assert_eq!(config.exclude, vec![1, 2]);
184        assert_eq!(
185            config.target,
186            pf_lib::FinderTarget::Posts {
187                categories_exclude: vec![3],
188                tags_exclude: vec![4],
189            }
190        );
191    }
192
193    #[tokio::test]
194    async fn test_print_stream() {
195        let config = pf_lib::FinderConfig {
196            url: "http://example.com".to_string(),
197            target: pf_lib::FinderTarget::Posts {
198                categories_exclude: vec![],
199                tags_exclude: vec![],
200            },
201            before: None,
202            modified_before: None,
203            after: None,
204            modified_after: None,
205            exclude: vec![],
206        };
207        let result = print_stream(&config).await;
208        assert!(result.is_ok());
209    }
210}