Skip to main content

autopulse_service/settings/targets/
fileflows.rs

1use super::{Request, RequestBuilderPerform};
2use crate::settings::path_filter::PathFilter;
3use crate::settings::rewrite::Rewrite;
4use crate::settings::targets::TargetProcess;
5use anyhow::Context;
6use autopulse_database::models::ScanEvent;
7use autopulse_utils::{get_url, RuntimePath};
8use reqwest::header;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12use tracing::{debug, error, trace};
13
14#[derive(Serialize, Deserialize, Clone)]
15pub struct FileFlows {
16    /// URL to the `FileFlows` server
17    pub url: String,
18    /// Rewrite path for the file
19    pub rewrite: Option<Rewrite>,
20    /// Path filter matched against the target-rewritten path.
21    #[serde(default)]
22    pub filter: PathFilter,
23    /// HTTP request options
24    #[serde(default)]
25    pub request: Request,
26}
27
28#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)]
29#[doc(hidden)]
30#[serde(rename_all = "PascalCase")]
31struct FileFlowsFlow {
32    uid: String,
33}
34
35#[derive(Deserialize, Clone, Eq, PartialEq, Hash, Debug)]
36#[doc(hidden)]
37#[serde(rename_all = "PascalCase")]
38struct FileFlowsLibrary {
39    uid: String,
40    enabled: bool,
41    path: Option<String>,
42    flow: Option<FileFlowsFlow>,
43}
44
45// #[derive(Serialize)]
46// #[doc(hidden)]
47// #[serde(rename_all = "PascalCase")]
48// struct FileFlowsRescanLibraryRequest {
49//     uids: Vec<String>,
50// }
51
52#[derive(Serialize, Debug)]
53#[doc(hidden)]
54#[serde(rename_all = "PascalCase")]
55struct FileFlowsManuallyAddRequest {
56    flow_uid: String,
57    files: Vec<String>,
58    #[serde(default)]
59    custom_variables: HashMap<String, String>,
60}
61
62#[derive(Serialize)]
63#[doc(hidden)]
64#[serde(rename_all = "PascalCase")]
65struct FileFlowsSearchRequest {
66    path: String,
67    limit: u32, // set to 1
68}
69
70#[derive(Serialize, Default, Debug)]
71#[doc(hidden)]
72#[serde(rename_all = "PascalCase")]
73struct FileFlowsReprocessRequest {
74    uids: Vec<String>,
75    custom_variables: HashMap<String, String>,
76    mode: u8,
77    flow: Option<Value>,
78    node: Option<Value>,
79    bottom_of_queue: bool,
80}
81
82#[derive(Deserialize, Clone, Eq, PartialEq, Hash, Debug)]
83#[doc(hidden)]
84#[serde(rename_all = "PascalCase")]
85struct FileFlowsLibraryFile {
86    uid: String,
87    flow_uid: String,
88    name: String, // filename, maybe use output_path later..
89}
90
91// How to "scan" a file in fileflows
92// First, get the libraries
93// Group files with their library
94// if the library disabled- error
95// Next get each file and check their status
96// If they are processed, send a reprocess request individually
97// For the rest, send a manual-add request, again still in a group with their library
98
99fn path_in_library(path: &str, library_path: &str) -> bool {
100    RuntimePath::new(path).starts_with(RuntimePath::new(library_path))
101}
102
103impl FileFlows {
104    fn get_client(&self) -> anyhow::Result<reqwest::Client> {
105        self.request
106            .client_builder(header::HeaderMap::new())
107            .build()
108            .map_err(Into::into)
109    }
110
111    async fn get_libraries(&self) -> anyhow::Result<Vec<FileFlowsLibrary>> {
112        let client = self.get_client()?;
113        let url = get_url(&self.url)?.join("api/library")?;
114
115        let res = client.get(url).perform().await?;
116
117        let libraries: Vec<FileFlowsLibrary> = res.json().await?;
118
119        Ok(libraries)
120    }
121
122    async fn get_library_file(
123        &self,
124        ev: &ScanEvent,
125    ) -> anyhow::Result<Option<FileFlowsLibraryFile>> {
126        let client = self.get_client()?;
127
128        let url = get_url(&self.url)?.join("api/library-file/search")?;
129        let req = FileFlowsSearchRequest {
130            path: ev.get_path(&self.rewrite),
131            limit: 1,
132        };
133
134        let res = client.post(url).json(&req).perform().await?;
135
136        let files: Vec<FileFlowsLibraryFile> = res.json().await?;
137
138        Ok(files.first().cloned())
139    }
140
141    async fn reprocess_library_file(&self, evs: Vec<&FileFlowsLibraryFile>) -> anyhow::Result<()> {
142        let client = self.get_client()?;
143
144        let url = get_url(&self.url)?.join("api/library-file/reprocess")?;
145
146        let req = FileFlowsReprocessRequest {
147            uids: evs.iter().map(|ev| ev.uid.clone()).collect(),
148            ..Default::default()
149        };
150
151        client.post(url).json(&req).perform().await.map(|_| ())
152    }
153
154    async fn manually_add_files(
155        &self,
156        library: &FileFlowsLibrary,
157        files: Vec<&ScanEvent>,
158    ) -> anyhow::Result<()> {
159        let client = self.get_client()?;
160
161        let url = get_url(&self.url)?.join("api/library-file/manually-add")?;
162
163        let flow = library
164            .flow
165            .as_ref()
166            .context("library has no flow configured")?;
167
168        let req = FileFlowsManuallyAddRequest {
169            flow_uid: flow.uid.clone(),
170            files: files.iter().map(|ev| ev.get_path(&self.rewrite)).collect(),
171            custom_variables: HashMap::new(),
172        };
173
174        client.post(url).json(&req).perform().await.map(|_| ())
175    }
176
177    // async fn rescan_library(&self, libraries: &FileFlowsLibrary) -> anyhow::Result<()> {
178    //     let client = self.get_client()?;
179
180    //     let url = get_url(&self.url)?.join("/api/library/rescan")?;
181
182    //     let req = FileFlowsRescanLibraryRequest {
183    //         uids: vec![libraries.uid.clone()],
184    //     };
185
186    //     let res = client.put(url.to_string()).json(&req).send().await?;
187
188    //     if res.status().is_success() {
189    //         Ok(())
190    //     } else {
191    //         let body = res.text().await?;
192    //         Err(anyhow::anyhow!("failed to send rescan: {}", body))
193    //     }
194    // }
195
196    // No longer in fileflows..
197    // async fn scan(&self, ev: &ScanEvent, library: &FileFlowsLibrary) -> anyhow::Result<()> {
198    //     let client = self.get_client()?;
199
200    //     let mut url = get_url(&self.url)?.join("/api/library-file/process-file")?;
201
202    //     url.query_pairs_mut().append_pair("filename", &ev.file_path);
203
204    //     let res = client.post(url.to_string()).send().await?;
205
206    //     if res.status().is_success() {
207    //         Ok(())
208    //     } else {
209    //         let body = res.text().await?;
210    //         Err(anyhow::anyhow!("failed to send scan: {}", body))
211    //     }
212    // }
213}
214
215impl TargetProcess for FileFlows {
216    async fn process(&self, evs: &[&ScanEvent]) -> anyhow::Result<Vec<String>> {
217        let mut succeeded = Vec::new();
218        let libraries = self
219            .get_libraries()
220            .await
221            .context("failed to get libraries")?;
222
223        let mut to_scan: HashMap<FileFlowsLibrary, Vec<&ScanEvent>> = HashMap::new();
224
225        for library in libraries {
226            let files = evs
227                .iter()
228                .filter_map(|ev| {
229                    let event_path = ev.get_path(&self.rewrite);
230                    let library_path = library.path.as_deref()?;
231
232                    path_in_library(&event_path, library_path).then_some(*ev)
233                })
234                .collect::<Vec<_>>();
235
236            if files.is_empty() {
237                continue;
238            }
239
240            if !library.enabled {
241                error!(
242                    "library '{}' is disabled but {} files will fail to scan",
243                    library.uid,
244                    files.len()
245                );
246                continue;
247            }
248
249            to_scan.insert(library, files);
250        }
251
252        for (library, evs) in to_scan {
253            let mut library_files = HashMap::new();
254
255            for ev in evs {
256                let event_path = ev.get_path(&self.rewrite);
257                if RuntimePath::new(&event_path).is_directory() {
258                    succeeded.push(ev.id.clone());
259                    continue;
260                }
261
262                match self.get_library_file(ev).await {
263                    Ok(file) => {
264                        library_files.insert(ev, file);
265                    }
266                    Err(e) => {
267                        error!("failed to get library file: {}", e);
268                        library_files.insert(ev, None);
269                    }
270                }
271            }
272
273            let (processed, not_processed): (Vec<_>, Vec<_>) =
274                library_files.iter().partition(|(_, file)| file.is_some());
275
276            trace!(
277                "library {} has {} processed and {} not processed files",
278                library.uid,
279                processed.len(),
280                not_processed.len()
281            );
282
283            if !processed.is_empty() {
284                match self
285                    .reprocess_library_file(
286                        processed
287                            .iter()
288                            .filter_map(|(_, file)| file.as_ref())
289                            .collect(),
290                    )
291                    .await
292                {
293                    Ok(()) => {
294                        for (ev, _) in &processed {
295                            debug!("reprocessed file: {}", ev.get_path(&self.rewrite));
296                        }
297                        succeeded.extend(processed.iter().map(|(ev, _)| ev.id.clone()));
298                    }
299                    Err(e) => error!("failed to reprocess files: {}", e),
300                }
301            }
302
303            if !not_processed.is_empty() {
304                match self
305                    .manually_add_files(
306                        &library,
307                        not_processed.iter().map(|(ev, _)| **ev).collect(),
308                    )
309                    .await
310                {
311                    Ok(()) => {
312                        for (ev, _) in &not_processed {
313                            debug!("manually added file: {}", ev.get_path(&self.rewrite));
314                        }
315                        succeeded.extend(not_processed.iter().map(|(ev, _)| ev.id.clone()));
316                    }
317                    Err(e) => error!("failed to manually add files: {}", e),
318                }
319            }
320        }
321
322        Ok(succeeded)
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn matches_windows_fileflows_library_by_runtime_components() {
332        assert!(path_in_library(
333            r"D:\MEDIA\Incoming\Film\Film.mkv",
334            r"d:\media\incoming"
335        ));
336        assert!(!path_in_library(
337            r"D:\Media\Incoming-Archive\Film.mkv",
338            r"D:\Media\Incoming"
339        ));
340        assert!(!path_in_library(
341            r"D:\Media\Incoming\Film.mkv",
342            "/Media/Incoming"
343        ));
344    }
345}