Skip to main content

autopulse_service/settings/triggers/
sportarr.rs

1use crate::settings::path_filter::PathFilter;
2use crate::settings::rewrite::Rewrite;
3use crate::settings::timer::{EventTimers, Timer};
4use crate::settings::triggers::{TriggerConfig, TriggerRequest};
5use autopulse_utils::join_path;
6use serde::{Deserialize, Serialize};
7
8#[derive(Serialize, Deserialize, Clone)]
9pub struct Sportarr {
10    /// Rewrite path
11    pub rewrite: Option<Rewrite>,
12    /// Timer settings
13    pub timer: Option<Timer>,
14    /// Targets to ignore
15    #[serde(default)]
16    pub excludes: Vec<String>,
17    /// Path filter matched against the rewritten file path.
18    #[serde(default)]
19    pub filter: PathFilter,
20    /// Event-specific timers
21    pub event_timers: Option<EventTimers>,
22}
23
24impl TriggerConfig for Sportarr {
25    fn rewrite(&self) -> Option<&Rewrite> {
26        self.rewrite.as_ref()
27    }
28
29    fn timer(&self) -> Option<&Timer> {
30        self.timer.as_ref()
31    }
32
33    fn excludes(&self) -> &Vec<String> {
34        &self.excludes
35    }
36
37    fn filter(&self) -> &PathFilter {
38        &self.filter
39    }
40
41    fn event_timers(&self) -> Option<&EventTimers> {
42        self.event_timers.as_ref()
43    }
44}
45
46#[derive(Deserialize, Clone)]
47#[serde(rename_all = "camelCase")]
48#[doc(hidden)]
49pub struct EpisodeFile {
50    pub relative_path: String,
51}
52
53/// `SeriesDelete` omits `path` because it removes metadata only.
54/// Unlike Sonarr, Sportarr paths must be optional.
55#[derive(Deserialize, Clone)]
56#[serde(rename_all = "camelCase")]
57#[doc(hidden)]
58pub struct Series {
59    pub path: Option<String>,
60}
61
62#[derive(Deserialize, Clone)]
63#[serde(tag = "eventType")]
64#[doc(hidden)]
65pub enum SportarrRequest {
66    #[serde(rename = "Download")]
67    #[serde(rename_all = "camelCase")]
68    Download {
69        episode_file: Option<EpisodeFile>,
70        #[serde(default)]
71        episode_files: Vec<EpisodeFile>,
72        #[serde(default)]
73        deleted_files: Vec<EpisodeFile>,
74        series: Series,
75    },
76    /// Rename webhooks identify the batch by `series.path`; rescan that directory.
77    #[serde(rename = "Rename")]
78    #[serde(rename_all = "camelCase")]
79    Rename { series: Series },
80    #[serde(rename = "SeriesDelete")]
81    #[serde(rename_all = "camelCase")]
82    SeriesDelete { series: Series },
83    /// Sportarr uses plural `deletedFiles`, unlike Sonarr's `episodeFile`.
84    #[serde(rename = "EpisodeFileDelete")]
85    #[serde(rename_all = "camelCase")]
86    EpisodeFileDelete {
87        #[serde(default)]
88        deleted_files: Vec<EpisodeFile>,
89        series: Series,
90    },
91    #[serde(rename = "Test")]
92    Test,
93    #[serde(other)]
94    Other,
95}
96
97impl TriggerRequest for SportarrRequest {
98    fn from_json(json: serde_json::Value) -> anyhow::Result<Self> {
99        serde_json::from_value(json).map_err(|e| anyhow::anyhow!(e))
100    }
101
102    fn paths(&self) -> Vec<(String, bool)> {
103        match self {
104            Self::EpisodeFileDelete {
105                deleted_files,
106                series,
107            } => {
108                let Some(series_path) = series.path.as_ref() else {
109                    return vec![];
110                };
111
112                deleted_files
113                    .iter()
114                    .map(|f| (join_path(series_path, &f.relative_path), false))
115                    .collect()
116            }
117            Self::Rename { series } => series
118                .path
119                .clone()
120                .map(|path| vec![(path, true)])
121                .unwrap_or_default(),
122            Self::SeriesDelete { series } => series
123                .path
124                .clone()
125                .map(|path| vec![(path, false)])
126                .unwrap_or_default(),
127            Self::Download {
128                episode_file,
129                episode_files,
130                series,
131                deleted_files,
132            } => {
133                let mut paths: Vec<(String, bool)> = vec![];
134
135                let Some(series_path) = series.path.as_ref() else {
136                    return paths;
137                };
138
139                if let Some(ef) = episode_file {
140                    paths.push((join_path(series_path, &ef.relative_path), true));
141                }
142
143                for ef in episode_files {
144                    paths.push((join_path(series_path, &ef.relative_path), true));
145                }
146
147                for file in deleted_files {
148                    paths.push((join_path(series_path, &file.relative_path), false));
149                }
150
151                paths
152            }
153            Self::Test | Self::Other => vec![],
154        }
155    }
156}