Skip to content

Commit d1e223b

Browse files
authored
Audiobookshelf improvements (#872)
* fix(backend): request all necessary information from ABS * chore(backend): remove newlines * feat(backend): send isbn service to ABS integration * refactor(backend): use default for getting details * feat(backend): get correct identifiers * feat(backend): get little bit more data * fix(backend): get progress for all * feat(backend): get correct progress of ABS media * fix(backend): no too much verbose logging * fix(backend): log err on integration progress fail * fix(frontend): display episode number * Revert "fix(frontend): display episode number" This reverts commit ad4bb3f. * refactor(backend): send database to integration service * feat(backend): handle ABS podcasts * fix(backend): get progress for ebooks * docs: update docs * refactor(backend): move into separate module * refactor(backend): add more attributes * refactor(backend): use new models for getting response * docs: improve ABS information * fix(backend): do not send finished in query params in podcast * refactor(backend): fn to get podcast number by name * feat(backend): import books correctly * feat(backend): import podcasts correctly * feat(backend): handle individual episode imports * fix(backend): import season correctly * build(backend): bump version * docs: info about manual podcast import * fix(backend): no extra handling * feat(backend): commit media before importing * feat(backend): commit media before integration * Revert "docs: info about manual podcast import" This reverts commit 8d31d90. * chore(backend): ignore more from instrument * feat(backend): get integrations working * feat(backend): commit media correctly * chore(backend): remove `?` * chore(backend): no need to instrument * chore(backend): send commit function to resolver * Revert "chore(backend): send commit function to resolver" This reverts commit 1a10228.
1 parent eef6b0d commit d1e223b

File tree

10 files changed

+399
-175
lines changed

10 files changed

+399
-175
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/backend/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ryot"
3-
version = "6.2.1"
3+
version = "6.2.2"
44
edition = "2021"
55
repository = "https://github.com/IgnisDa/ryot"
66
license = "GPL-3.0"
+142-64
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,49 @@
1+
use std::future::Future;
2+
13
use anyhow::anyhow;
24
use async_graphql::Result;
35
use data_encoding::BASE64;
46
use database::{ImportSource, MediaLot, MediaSource};
7+
use sea_orm::DatabaseConnection;
58
use serde::{Deserialize, Serialize};
69
use serde_json::json;
7-
use strum::Display;
8-
use surf::http::headers::AUTHORIZATION;
10+
use surf::{http::headers::AUTHORIZATION, Client};
911

1012
use crate::{
1113
importer::{ImportFailStep, ImportFailedItem, ImportResult},
12-
models::media::{
13-
ImportOrExportItemIdentifier, ImportOrExportMediaItem, ImportOrExportMediaItemSeen,
14+
miscellaneous::{audiobookshelf_models, itunes_podcast_episode_by_name},
15+
models::{
16+
media::{
17+
CommitMediaInput, ImportOrExportItemIdentifier, ImportOrExportMediaItem,
18+
ImportOrExportMediaItemSeen,
19+
},
20+
StringIdObject,
1421
},
22+
providers::google_books::GoogleBooksService,
1523
utils::get_base_http_client,
1624
};
1725

1826
use super::DeployUrlAndKeyImportInput;
1927

20-
#[derive(Debug, Serialize, Deserialize, Clone, Display)]
21-
#[serde(rename_all = "snake_case")]
22-
enum MediaType {
23-
Book,
24-
Podcast,
25-
}
26-
27-
#[derive(Debug, Serialize, Deserialize)]
28-
#[serde(rename_all = "camelCase")]
29-
pub struct LibraryListItemMetadata {
30-
asin: Option<String>,
31-
title: Option<String>,
32-
}
33-
34-
#[derive(Debug, Serialize, Deserialize)]
35-
#[serde(rename_all = "camelCase")]
36-
pub struct LibraryListItem {
37-
id: String,
38-
media_type: Option<MediaType>,
39-
name: Option<String>,
40-
metadata: Option<LibraryListItemMetadata>,
41-
media: Option<Box<LibraryListItem>>,
42-
}
43-
4428
#[derive(Debug, Serialize, Deserialize)]
4529
pub struct LibrariesListResponse {
46-
pub libraries: Vec<LibraryListItem>,
30+
pub libraries: Vec<audiobookshelf_models::Item>,
4731
}
4832

4933
#[derive(Debug, Serialize, Deserialize)]
5034
pub struct ListResponse {
51-
pub results: Vec<LibraryListItem>,
35+
pub results: Vec<audiobookshelf_models::Item>,
5236
}
5337

54-
pub async fn import(input: DeployUrlAndKeyImportInput) -> Result<ImportResult> {
38+
pub async fn import<F>(
39+
input: DeployUrlAndKeyImportInput,
40+
isbn_service: &GoogleBooksService,
41+
db: &DatabaseConnection,
42+
commit_metadata: impl Fn(CommitMediaInput) -> F,
43+
) -> Result<ImportResult>
44+
where
45+
F: Future<Output = Result<StringIdObject>>,
46+
{
5547
let mut media = vec![];
5648
let mut failed_items = vec![];
5749
let client = get_base_http_client(
@@ -66,55 +58,122 @@ pub async fn import(input: DeployUrlAndKeyImportInput) -> Result<ImportResult> {
6658
.await
6759
.unwrap();
6860
for library in libraries_resp.libraries {
69-
tracing::debug!("Importing library {:?}", library.name);
61+
tracing::debug!("Importing library {:?}", library.name.unwrap());
62+
let mut query = json!({ "expanded": "1" });
63+
if let Some(audiobookshelf_models::MediaType::Book) = library.media_type {
64+
query["filter"] = json!(format!("progress.{}", BASE64.encode(b"finished")));
65+
}
7066
let finished_items: ListResponse = client
7167
.get(&format!("libraries/{}/items", library.id))
72-
.query(&json!({ "filter": format!("progress.{}", BASE64.encode(b"finished")) }))
68+
.query(&query)
7369
.unwrap()
7470
.await
7571
.map_err(|e| anyhow!(e))?
7672
.body_json()
7773
.await
7874
.unwrap();
7975
for item in finished_items.results {
80-
let metadata = item.media.unwrap().metadata.unwrap();
81-
match item.media_type.unwrap() {
82-
MediaType::Book => {
83-
let lot = MediaLot::AudioBook;
84-
if let Some(asin) = metadata.asin {
85-
media.push(ImportOrExportMediaItem {
86-
internal_identifier: Some(ImportOrExportItemIdentifier::NeedsDetails(
87-
asin,
88-
)),
89-
lot,
90-
source: MediaSource::Audible,
91-
source_id: metadata.title.unwrap_or_default(),
92-
identifier: "".to_string(),
93-
seen_history: vec![ImportOrExportMediaItemSeen {
94-
provider_watched_on: Some(ImportSource::Audiobookshelf.to_string()),
95-
..Default::default()
96-
}],
97-
collections: vec![],
98-
reviews: vec![],
99-
})
100-
} else {
76+
let metadata = item.media.clone().unwrap().metadata;
77+
let title = metadata.title.clone();
78+
let (identifier, lot, source, episodes) = if Some("epub".to_string())
79+
== item.media.as_ref().unwrap().ebook_format
80+
{
81+
match &metadata.isbn {
82+
Some(isbn) => match isbn_service.id_from_isbn(isbn).await {
83+
Some(id) => (id, MediaLot::Book, MediaSource::GoogleBooks, None),
84+
_ => {
85+
failed_items.push(ImportFailedItem {
86+
error: Some("No Google Books ID found".to_string()),
87+
identifier: title,
88+
lot: None,
89+
step: ImportFailStep::InputTransformation,
90+
});
91+
continue;
92+
}
93+
},
94+
_ => {
10195
failed_items.push(ImportFailedItem {
102-
error: Some("No ASIN found".to_string()),
103-
identifier: metadata.title.unwrap_or_default(),
104-
lot: Some(lot),
96+
error: Some("No ISBN found".to_string()),
97+
identifier: title,
98+
lot: None,
10599
step: ImportFailStep::InputTransformation,
106100
});
101+
continue;
107102
}
108103
}
109-
s => {
110-
failed_items.push(ImportFailedItem {
111-
error: Some(format!("Import of {s:#?} media type is not supported yet")),
112-
identifier: metadata.title.unwrap_or_default(),
113-
lot: None,
114-
step: ImportFailStep::ItemDetailsFromSource,
104+
} else if let Some(asin) = metadata.asin.clone() {
105+
(asin, MediaLot::AudioBook, MediaSource::Audible, None)
106+
} else if let Some(itunes_id) = metadata.itunes_id.clone() {
107+
let item_details = get_item_details(&client, &item.id, None).await?;
108+
match item_details.media.and_then(|m| m.episodes) {
109+
Some(episodes) => {
110+
let lot = MediaLot::Podcast;
111+
let source = MediaSource::Itunes;
112+
let mut to_return = vec![];
113+
for episode in episodes {
114+
let episode_details =
115+
get_item_details(&client, &item.id, Some(episode.id.unwrap()))
116+
.await?;
117+
if let Some(true) =
118+
episode_details.user_media_progress.map(|u| u.is_finished)
119+
{
120+
commit_metadata(CommitMediaInput {
121+
identifier: itunes_id.clone(),
122+
lot,
123+
source,
124+
..Default::default()
125+
})
126+
.await
127+
.ok();
128+
if let Ok(Some(pe)) =
129+
itunes_podcast_episode_by_name(&episode.title, &itunes_id, db)
130+
.await
131+
{
132+
to_return.push(pe);
133+
}
134+
}
135+
}
136+
(itunes_id, lot, source, Some(to_return))
137+
}
138+
_ => {
139+
failed_items.push(ImportFailedItem {
140+
error: Some("No episodes found for podcast".to_string()),
141+
identifier: title,
142+
lot: Some(MediaLot::Podcast),
143+
step: ImportFailStep::ItemDetailsFromSource,
144+
});
145+
continue;
146+
}
147+
}
148+
} else {
149+
tracing::debug!("No ASIN, ISBN or iTunes ID found for item {:#?}", item);
150+
continue;
151+
};
152+
let mut seen_history = vec![];
153+
if let Some(podcasts) = episodes {
154+
for episode in podcasts {
155+
seen_history.push(ImportOrExportMediaItemSeen {
156+
provider_watched_on: Some(ImportSource::Audiobookshelf.to_string()),
157+
podcast_episode_number: Some(episode),
158+
..Default::default()
115159
});
116160
}
117-
}
161+
} else {
162+
seen_history.push(ImportOrExportMediaItemSeen {
163+
provider_watched_on: Some(ImportSource::Audiobookshelf.to_string()),
164+
..Default::default()
165+
});
166+
};
167+
media.push(ImportOrExportMediaItem {
168+
lot,
169+
source,
170+
source_id: metadata.title,
171+
identifier: "".to_string(),
172+
internal_identifier: Some(ImportOrExportItemIdentifier::NeedsDetails(identifier)),
173+
seen_history,
174+
collections: vec![],
175+
reviews: vec![],
176+
})
118177
}
119178
}
120179
Ok(ImportResult {
@@ -123,3 +182,22 @@ pub async fn import(input: DeployUrlAndKeyImportInput) -> Result<ImportResult> {
123182
..Default::default()
124183
})
125184
}
185+
186+
async fn get_item_details(
187+
client: &Client,
188+
id: &str,
189+
episode: Option<String>,
190+
) -> Result<audiobookshelf_models::Item> {
191+
let mut query = json!({ "expanded": "1", "include": "progress" });
192+
if let Some(episode) = episode {
193+
query["episode"] = json!(episode);
194+
}
195+
let item: audiobookshelf_models::Item = client
196+
.get(&format!("items/{}", id))
197+
.query(&query)?
198+
.await
199+
.map_err(|e| anyhow!(e))?
200+
.body_json()
201+
.await?;
202+
Ok(item)
203+
}

apps/backend/src/importer/mod.rs

+8-3
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,14 @@ impl ImporterService {
284284
)
285285
.await
286286
.unwrap(),
287-
ImportSource::Audiobookshelf => audiobookshelf::import(input.url_and_key.unwrap())
288-
.await
289-
.unwrap(),
287+
ImportSource::Audiobookshelf => audiobookshelf::import(
288+
input.url_and_key.unwrap(),
289+
&self.media_service.get_isbn_service().await.unwrap(),
290+
&self.media_service.db,
291+
|input| self.media_service.commit_metadata(input),
292+
)
293+
.await
294+
.unwrap(),
290295
ImportSource::Imdb => imdb::import(
291296
input.generic_csv.unwrap(),
292297
&self

0 commit comments

Comments
 (0)