autopulse_service/settings/targets/
audiobookshelf.rs

1use 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    /// URL to the audiobookshelf instance
13    pub url: String,
14    // /// Authentication credentials
15    // pub auth: Auth,
16    /// API token for Audiobookshelf Server (see https://www.audiobookshelf.org/guides/api-keys)
17    pub token: String,
18    /// Rewrite path for the file
19    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        // if self.auth.enabled {
45        //     if let Some(token) = token {
46        //         headers.insert("Authorization", format!("Bearer {token}").parse()?);
47        //     }
48        // }
49        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 login(&self) -> anyhow::Result<String> {
59    //     let client = self.get_client(None).await?;
60    //     let url = get_url(&self.url)?.join("login")?;
61
62    //     let res = client
63    //         .post(url)
64    //         .header("Content-Type", "application/json")
65    //         .json(&self.auth)
66    //         .perform()
67    //         .await?;
68
69    //     let body: AudiobookshelfLoginResponse = res.json().await?;
70
71    //     Ok(body.user.token)
72    // }
73
74    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                // audiobookshelf will scan for the changes so del/rename *should* be handled
85                // https://github.com/mikiher/audiobookshelf/blob/master/server/Watcher.js#L268
86                "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}