autopulse_service/settings/triggers/mod.rs
1/// Autoscan - autoscan compatibility trigger
2///
3/// Provides a trigger with compatibility for applications that are built for autoscan
4///
5/// # Example
6///
7/// ```yml
8/// triggers:
9/// my_autoscan:
10/// type: autoscan
11/// ```
12///
13/// or
14///
15/// ```yml
16/// triggers:
17/// my_autoscan:
18/// type: autoscan
19/// rewrite:
20/// from: "/downloads/all"
21/// to: "/all"
22/// timer:
23/// wait: 30
24/// excludes: [ "ignored_target" ]
25/// ```
26///
27/// See [`Autoscan`] for all options
28pub mod autoscan;
29/// Lidarr - Lidarr trigger
30///
31/// This trigger is used to process a file from Lidarr
32///
33/// # Example
34///
35/// ```yml
36/// triggers:
37/// my_lidarr:
38/// type: lidarr
39/// ```
40///
41/// or
42///
43/// ```yml
44/// triggers:
45/// my_lidarr:
46/// type: lidarr
47/// rewrite:
48/// from: "/downloads/music"
49/// to: "/music"
50/// timer:
51/// wait: 30
52/// excludes: [ "ignored_target" ]
53/// ```
54///
55/// See [`Lidarr`] for all options
56pub mod lidarr;
57/// Manual - Manual trigger
58///
59/// This trigger is used to manually process a file. Often used when implementing a custom trigger
60///
61/// Note: A default route of `/triggers/manual` is provided
62///
63/// # Example
64///
65/// ```yml
66/// triggers:
67/// my_manual:
68/// type: manual
69/// ```
70///
71/// or
72///
73/// ```yml
74/// triggers:
75/// my_manual:
76/// type: manual
77/// rewrite:
78/// from: "/downloads/stuff"
79/// to: "/stuff"
80/// timer:
81/// wait: 30
82/// excludes: [ "ignored_target" ]
83/// ```
84///
85/// See [`Manual`] for all options
86/// and
87/// [`ManualQueryParams`](manual::ManualQueryParams) for query parameters
88pub mod manual;
89/// Notify - Notify trigger
90///
91/// Cross-platform monitoring for a directory to process based on file events
92///
93/// # Example
94///
95/// ```yml
96/// triggers:
97/// my_notify:
98/// type: notify
99/// paths:
100/// - "/path/to/monitor"
101/// ```
102///
103/// or
104///
105/// ```yml
106/// triggers:
107/// my_notify:
108/// type: notify
109/// paths:
110/// - "/downloads"
111/// recursive: false
112/// rewrite:
113/// from: "/downloads"
114/// to: "/media"
115/// timer:
116/// wait: 30
117/// excludes: [ "ignored_target" ]
118/// ```
119///
120/// See [`Notify`] for all options
121pub mod notify;
122/// Radarr - Radarr trigger
123///
124/// This trigger is used to process a file from Radarr
125///
126/// # Example
127///
128/// ```yml
129/// triggers:
130/// my_radarr:
131/// type: radarr
132/// ```
133///
134/// or
135///
136/// ```yml
137/// triggers:
138/// my_radarr:
139/// type: radarr
140/// rewrite:
141/// from: "/downloads/movies"
142/// to: "/movies"
143/// timer:
144/// wait: 30
145/// excludes: [ "ignored_target" ]
146/// ```
147///
148/// See [`Radarr`] for all options
149pub mod radarr;
150/// Readarr - Readarr trigger
151///
152/// This trigger is used to process a file from Readarr
153///
154/// # Example
155///
156/// ```yml
157/// triggers:
158/// my_readarr:
159/// type: readarr
160/// ```
161///
162/// or
163///
164/// ```yml
165/// triggers:
166/// my_readarr:
167/// type: readarr
168/// rewrite:
169/// from: "/downloads/books"
170/// to: "/books"
171/// timer:
172/// wait: 30
173/// excludes: [ "ignored_target" ]
174/// ```
175///
176/// See [`Readarr`] for all options
177pub mod readarr;
178/// Sonarr - Sonarr trigger
179///
180/// This trigger is used to process a file from Sonarr
181///
182/// # Example
183///
184/// ```yml
185/// triggers:
186/// my_sonarr:
187/// type: sonarr
188/// ```
189///
190/// or
191///
192/// ```yml
193/// triggers:
194/// my_sonarr:
195/// type: sonarr
196/// rewrite:
197/// from: "/downloads/shows"
198/// to: "/shows"
199/// timer:
200/// wait: 30
201/// excludes: [ "ignored_target" ]
202/// ```
203///
204/// See [`Sonarr`] for all options
205pub mod sonarr;
206/// Sportarr - Sportarr trigger
207///
208/// This trigger is used to process a file from Sportarr
209///
210/// # Example
211///
212/// ```yml
213/// triggers:
214/// my_sportarr:
215/// type: sportarr
216/// ```
217///
218/// or
219///
220/// ```yml
221/// triggers:
222/// my_sportarr:
223/// type: sportarr
224/// rewrite:
225/// from: "/downloads"
226/// to: "/sports"
227/// timer:
228/// wait: 30
229/// excludes: [ "ignored_target" ]
230/// ```
231///
232/// See [`Sportarr`] for all options
233pub mod sportarr;
234/// Tdarr - Tdarr trigger
235///
236/// Processes JSON notifications with `path`, `original_path`, and/or `dir`.
237/// Also accepts `?path=` (with an optional `hash`) or `?dir=` query parameters.
238///
239/// # Example
240///
241/// ```yml
242/// triggers:
243/// my_tdarr:
244/// type: tdarr
245/// ```
246///
247/// or
248///
249/// ```yml
250/// triggers:
251/// my_tdarr:
252/// type: tdarr
253/// rewrite:
254/// from: "^/tdarr-media"
255/// to: "/media"
256/// timer:
257/// wait: 30
258/// excludes: [ "ignored_target" ]
259/// ```
260///
261/// # Flows (recommended)
262///
263/// After **Replace Original File**, add a **Send Web Request** node:
264///
265/// - Method: `post`
266/// - URL: `http://<autopulse-host>:2875/triggers/my_tdarr`
267/// - Headers: `Content-Type: application/json`, `Authorization: Basic <base64 user:pass>`
268/// using autopulse's auth credentials.
269///
270/// Body:
271///
272/// ```json
273/// {"path": "{{{args.inputFileObj._id}}}", "original_path": "{{{args.originalLibraryFile._id}}}"}
274/// ```
275///
276/// Set **Output 2 Status Codes** (e.g. `400-599`) and **Output 2 On Network Error**,
277/// and route that output so notification failures do not fail the transcode job.
278/// Prefer the body over query templating: Tdarr does not URL-encode template values.
279/// Paths containing `"` or Windows backslashes still need proper JSON escaping.
280///
281/// # Classic plugin: Trigger Plex_Autoscan (TD01)
282///
283/// TD01 always calls `/triggers/manual?dir=<parent dir>/`. Replace the default manual
284/// trigger with:
285///
286/// ```yml
287/// triggers:
288/// manual:
289/// type: tdarr
290/// ```
291///
292/// Set the plugin's address/port to autopulse (default `2875`) and username/password
293/// to autopulse's auth credentials; its defaults `Batman` / `SecretPassword` are sent
294/// unless changed.
295///
296/// If autopulse also has a Tdarr target, set `excludes: [<your tdarr target>]` using
297/// its configured target name to prevent re-queue loops.
298///
299/// See [`Tdarr`] for all options and [`TdarrRequest`] for the JSON body
300pub mod tdarr;
301
302use crate::settings::path_filter::PathFilter;
303use crate::settings::timer::EventTimers;
304use crate::settings::timer::Timer;
305use crate::settings::{rewrite::Rewrite, triggers::autoscan::Autoscan};
306use serde::{Deserialize, Serialize};
307use {
308 lidarr::{Lidarr, LidarrRequest},
309 manual::Manual,
310 notify::Notify,
311 radarr::{Radarr, RadarrRequest},
312 readarr::{Readarr, ReadarrRequest},
313 sonarr::{Sonarr, SonarrRequest},
314 sportarr::{Sportarr, SportarrRequest},
315 tdarr::{Tdarr, TdarrRequest},
316};
317
318pub trait TriggerRequest {
319 fn from_json(json: serde_json::Value) -> anyhow::Result<Self>
320 where
321 Self: Sized;
322
323 // where the bool represents whether to check found status
324 fn paths(&self) -> Vec<(String, bool)>;
325}
326
327pub trait TriggerConfig {
328 fn rewrite(&self) -> Option<&Rewrite>;
329 fn timer(&self) -> Option<&Timer>;
330 fn excludes(&self) -> &Vec<String>;
331 fn filter(&self) -> &PathFilter;
332 fn event_timers(&self) -> Option<&EventTimers> {
333 None
334 }
335}
336
337#[derive(Serialize, Deserialize)]
338#[serde(rename_all = "lowercase")]
339pub enum TriggerType {
340 Manual,
341 Autoscan,
342 Radarr,
343 Bazarr,
344 Sonarr,
345 Sportarr,
346 Tdarr,
347 Lidarr,
348 Readarr,
349 Notify,
350}
351
352#[derive(Serialize, Deserialize, Clone)]
353#[serde(tag = "type", rename_all = "lowercase")]
354pub enum Trigger {
355 Manual(Manual),
356 Autoscan(Autoscan),
357 Bazarr(Manual),
358 Radarr(Radarr),
359 Sonarr(Sonarr),
360 Sportarr(Sportarr),
361 Tdarr(Tdarr),
362 Lidarr(Lidarr),
363 Readarr(Readarr),
364 Notify(Notify),
365}
366
367impl Trigger {
368 fn as_config(&self) -> &dyn TriggerConfig {
369 match self {
370 Self::Manual(trigger) | Self::Bazarr(trigger) => trigger,
371 Self::Autoscan(trigger) => trigger,
372 Self::Radarr(trigger) => trigger,
373 Self::Sonarr(trigger) => trigger,
374 Self::Sportarr(trigger) => trigger,
375 Self::Tdarr(trigger) => trigger,
376 Self::Lidarr(trigger) => trigger,
377 Self::Readarr(trigger) => trigger,
378 Self::Notify(trigger) => trigger,
379 }
380 }
381
382 pub fn get_rewrite(&self) -> Option<&Rewrite> {
383 self.as_config().rewrite()
384 }
385
386 pub fn get_timer(&self, event_name: Option<String>) -> Timer {
387 let config = self.as_config();
388 let mut base_timer = config.timer().cloned().unwrap_or_default();
389
390 let event_specific_timer = event_name
391 .as_ref()
392 .and_then(|event| config.event_timers().and_then(|timers| timers.get(event)));
393
394 if let Some(event_timer) = event_specific_timer {
395 base_timer = base_timer.chain(event_timer);
396 }
397
398 base_timer
399 }
400
401 pub fn paths(&self, body: serde_json::Value) -> anyhow::Result<(String, Vec<(String, bool)>)> {
402 let event_name = body["eventType"].as_str().unwrap_or("unknown").to_string();
403
404 let paths = match &self {
405 Self::Sonarr(_) => Ok(SonarrRequest::from_json(body)?.paths()),
406 Self::Sportarr(_) => Ok(SportarrRequest::from_json(body)?.paths()),
407 Self::Tdarr(_) => Ok(TdarrRequest::from_json(body)?.paths()),
408 Self::Radarr(_) => Ok(RadarrRequest::from_json(body)?.paths()),
409 Self::Lidarr(_) => Ok(LidarrRequest::from_json(body)?.paths()),
410 Self::Readarr(_) => Ok(ReadarrRequest::from_json(body)?.paths()),
411 Self::Manual(_) | Self::Notify(_) | Self::Autoscan(_) | Self::Bazarr(_) => {
412 Err(anyhow::anyhow!("Manual trigger does not have paths"))
413 }
414 }?;
415
416 Ok((event_name, paths))
417 }
418
419 pub fn excludes(&self) -> &Vec<String> {
420 self.as_config().excludes()
421 }
422
423 pub fn should_process_path(&self, path: &str) -> bool {
424 self.as_config().filter().allows(path)
425 }
426}