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