autopulse_service/settings/targets/
audiobookshelf.rs1use super::{Request, RequestBuilderPerform};
2use crate::settings::path_filter::PathFilter;
3use crate::settings::rewrite::Rewrite;
4use crate::settings::targets::TargetProcess;
5use autopulse_database::models::ScanEvent;
6use autopulse_utils::get_url;
7use reqwest::header;
8use serde::{Deserialize, Serialize};
9use tracing::{debug, error};
10
11#[derive(Clone, Deserialize, Serialize)]
12pub struct Audiobookshelf {
13 pub url: String,
15 pub token: String,
19 pub rewrite: Option<Rewrite>,
21 #[serde(default)]
23 pub filter: PathFilter,
24 #[serde(default)]
26 pub request: Request,
27}
28
29#[derive(Debug, Deserialize)]
30pub struct LibraryFolder {
31 #[serde(rename = "fullPath")]
32 pub full_path: String,
33 #[serde(rename = "libraryId")]
34 pub library_id: String,
35}
36
37#[derive(Debug, Deserialize)]
38pub struct Library {
39 pub folders: Vec<LibraryFolder>,
40}
41
42#[derive(Debug, Deserialize)]
43pub struct LibrariesResponse {
44 pub libraries: Vec<Library>,
45}
46
47impl Audiobookshelf {
48 async fn get_client(&self) -> anyhow::Result<reqwest::Client> {
49 let mut headers = header::HeaderMap::new();
50
51 headers.insert("Authorization", format!("Bearer {}", self.token).parse()?);
52
53 self.request
54 .client_builder(headers)
55 .build()
56 .map_err(Into::into)
57 }
58
59 async fn scan(&self, ev: &ScanEvent, library_id: String) -> anyhow::Result<()> {
76 let client = self.get_client().await?;
77 let url = get_url(&self.url)?.join("api/watcher/update")?;
78
79 client
80 .post(url)
81 .header("Content-Type", "application/json")
82 .json(&serde_json::json!({
83 "libraryId": library_id,
84 "path": ev.get_path(&self.rewrite),
85 "type": "add"
88 }))
89 .perform()
90 .await
91 .map(|_| ())
92 }
93
94 async fn get_libraries(&self) -> anyhow::Result<Vec<Library>> {
95 let client = self.get_client().await?;
96
97 let url = get_url(&self.url)?.join("api/libraries")?;
98
99 let res = client.get(url).perform().await?;
100
101 let body: LibrariesResponse = res.json().await?;
102
103 Ok(body.libraries)
104 }
105
106 async fn choose_library(
107 &self,
108 ev: &ScanEvent,
109 libraries: &[Library],
110 ) -> anyhow::Result<Option<String>> {
111 for library in libraries {
112 for folder in library.folders.iter() {
113 if ev.get_path(&self.rewrite).starts_with(&folder.full_path) {
114 debug!("found library: {}", folder.library_id);
115 return Ok(Some(folder.library_id.clone()));
116 }
117 }
118 }
119
120 Ok(None)
121 }
122}
123
124impl TargetProcess for Audiobookshelf {
125 async fn process(&self, evs: &[&ScanEvent]) -> anyhow::Result<Vec<String>> {
126 let mut succeeded = Vec::new();
127
128 let libraries = self.get_libraries().await?;
129
130 if libraries.is_empty() {
131 error!("no libraries found");
132 return Ok(succeeded);
133 }
134
135 for ev in evs {
136 match self.choose_library(ev, &libraries).await {
137 Ok(Some(library_id)) => {
138 if let Err(e) = self.scan(ev, library_id).await {
139 error!("failed to scan audiobookshelf: {}", e);
140 } else {
141 succeeded.push(ev.id.clone());
142 }
143 }
144 Ok(None) => {
145 error!("no library found for {}", ev.get_path(&self.rewrite));
146 }
147 Err(e) => {
148 error!("failed to choose library: {}", e);
149 }
150 }
151 }
152
153 Ok(succeeded)
154 }
155}