Skip to main content

autopulse_service/settings/targets/
audiobookshelf.rs

1use 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    /// URL to the audiobookshelf instance
14    pub url: String,
15    // /// Authentication credentials
16    // pub auth: Auth,
17    /// API token for Audiobookshelf Server (see https://www.audiobookshelf.org/guides/api-keys)
18    pub token: String,
19    /// Rewrite path for the file
20    pub rewrite: Option<Rewrite>,
21    /// Path filter matched against the target-rewritten path.
22    #[serde(default)]
23    pub filter: PathFilter,
24    /// HTTP request options
25    #[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 login(&self) -> anyhow::Result<String> {
60    //     let client = self.get_client(None).await?;
61    //     let url = get_url(&self.url)?.join("login")?;
62
63    //     let res = client
64    //         .post(url)
65    //         .header("Content-Type", "application/json")
66    //         .json(&self.auth)
67    //         .perform()
68    //         .await?;
69
70    //     let body: AudiobookshelfLoginResponse = res.json().await?;
71
72    //     Ok(body.user.token)
73    // }
74
75    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                // audiobookshelf will scan for the changes so del/rename *should* be handled
86                // https://github.com/mikiher/audiobookshelf/blob/master/server/Watcher.js#L268
87                "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}