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