autopulse_service/settings/targets/
audiobookshelf.rs

1use 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    /// 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    /// HTTP request options
21    #[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 login(&self) -> anyhow::Result<String> {
56    //     let client = self.get_client(None).await?;
57    //     let url = get_url(&self.url)?.join("login")?;
58
59    //     let res = client
60    //         .post(url)
61    //         .header("Content-Type", "application/json")
62    //         .json(&self.auth)
63    //         .perform()
64    //         .await?;
65
66    //     let body: AudiobookshelfLoginResponse = res.json().await?;
67
68    //     Ok(body.user.token)
69    // }
70
71    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                // audiobookshelf will scan for the changes so del/rename *should* be handled
82                // https://github.com/mikiher/audiobookshelf/blob/master/server/Watcher.js#L268
83                "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}