Skip to content

Commit 4e5aefe

Browse files
committed
Add nextory folder support
1 parent a940d05 commit 4e5aefe

4 files changed

Lines changed: 324 additions & 72 deletions

File tree

mlm_db/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,5 @@ pub mod ids {
300300
pub const GOODREADS: &str = "goodreads";
301301
pub const ISBN: &str = "isbn";
302302
pub const MAM: &str = "mam";
303+
pub const NEXTORY: &str = "nextory";
303304
}

server/src/linker/folder.rs

Lines changed: 212 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ async fn link_folder(
5757

5858
let mut audio_files = vec![];
5959
let mut ebook_files = vec![];
60-
let mut metadata_file = None;
60+
let mut metadata_files = vec![];
6161
// let mut cover_file = None;
6262
while let Some(entry) = entries.next_entry().await? {
6363
match entry.path().extension() {
64-
Some(ext) if ext == "json" => metadata_file = Some(entry),
64+
Some(ext) if ext == "json" => metadata_files.push(entry),
6565
// Some(ext) if ext == "jpg" || ext == "png" => cover_file = Some(entry),
6666
Some(ext)
6767
if config
@@ -83,22 +83,41 @@ async fn link_folder(
8383
}
8484
}
8585

86-
let Some(metadata_file) = metadata_file else {
86+
if metadata_files.is_empty() {
8787
warn!("Missing metadata file");
8888
return Ok(());
89-
};
89+
}
9090

91-
let json = read_to_string(metadata_file.path()).await?;
92-
if let Ok(libation_meta) = serde_json::from_str::<Libation>(&json) {
93-
trace!("Linking libation folder");
94-
let asin = libation_meta.asin.clone();
95-
let title = libation_meta.title.clone();
96-
let result =
97-
link_libation_folder(config, library, db, libation_meta, audio_files, ebook_files)
98-
.await;
99-
update_errored_torrent(db, ErroredTorrentId::Linker(asin), title, result).await;
91+
metadata_files.sort_by_key(|entry| entry.file_name());
92+
for metadata_file in metadata_files {
93+
let json = read_to_string(metadata_file.path()).await?;
94+
if let Ok(libation_meta) = serde_json::from_str::<Libation>(&json) {
95+
trace!("Linking libation folder");
96+
let asin = libation_meta.asin.clone();
97+
let title = libation_meta.title.clone();
98+
let result =
99+
link_libation_folder(config, library, db, libation_meta, audio_files, ebook_files)
100+
.await;
101+
update_errored_torrent(db, ErroredTorrentId::Linker(asin), title, result).await;
102+
return Ok(());
103+
}
104+
if let Some(nextory_meta) = parse_nextory_meta(&json) {
105+
trace!("Linking nextory folder");
106+
let id = nextory_torrent_id(nextory_meta.id);
107+
let title = nextory_meta.title.clone();
108+
let result =
109+
link_nextory_folder(config, library, db, nextory_meta, audio_files, ebook_files)
110+
.await;
111+
update_errored_torrent(db, ErroredTorrentId::Linker(id), title, result).await;
112+
return Ok(());
113+
}
100114
}
101115

116+
warn!(
117+
folder = folder.path().to_string_lossy().to_string(),
118+
"Unsupported metadata format"
119+
);
120+
102121
Ok(())
103122
}
104123

@@ -110,11 +129,10 @@ async fn link_libation_folder(
110129
audio_files: Vec<DirEntry>,
111130
ebook_files: Vec<DirEntry>,
112131
) -> Result<()> {
113-
let r = db.r_transaction()?;
114-
let existing_torrent: Option<Torrent> = r.get().primary(libation_meta.asin.clone())?;
115-
if existing_torrent.is_some() {
116-
return Ok(());
117-
}
132+
let torrent =
133+
build_libation_torrent(library, libation_meta, &audio_files, &ebook_files).await?;
134+
link_prepared_folder_torrent(config, library, db, torrent, audio_files, ebook_files).await
135+
}
118136

119137
async fn link_nextory_folder(
120138
config: &Config,
@@ -123,19 +141,9 @@ async fn link_nextory_folder(
123141
nextory_meta: NextoryRaw,
124142
audio_files: Vec<DirEntry>,
125143
ebook_files: Vec<DirEntry>,
126-
events: &crate::stats::Events,
127144
) -> Result<()> {
128145
let torrent = build_nextory_torrent(library, nextory_meta, &audio_files, &ebook_files).await?;
129-
link_prepared_folder_torrent(
130-
config,
131-
library,
132-
db,
133-
torrent,
134-
audio_files,
135-
ebook_files,
136-
events,
137-
)
138-
.await
146+
link_prepared_folder_torrent(config, library, db, torrent, audio_files, ebook_files).await
139147
}
140148

141149
async fn build_libation_torrent(
@@ -197,13 +205,7 @@ async fn build_libation_torrent(
197205
if libation_meta.format_type.starts_with("abridged") {
198206
flags.abridged = Some(true);
199207
}
200-
let mut size = 0;
201-
for file in &audio_files {
202-
size += file_size(&file.metadata().await?);
203-
}
204-
for file in &ebook_files {
205-
size += file_size(&file.metadata().await?);
206-
}
208+
let (size, filetypes) = folder_file_stats(audio_files, ebook_files).await?;
207209

208210
let meta = TorrentMeta {
209211
ids,
@@ -215,7 +217,7 @@ async fn build_libation_torrent(
215217
tags: vec![],
216218
language: Language::from_str(&libation_meta.language).ok(),
217219
flags: Some(FlagBits::new(flags.as_bitfield())),
218-
filetypes: vec!["m4b".to_string()],
220+
filetypes,
219221
num_files: audio_files.len() as u64,
220222
size: Size::from_bytes(size),
221223
title,
@@ -231,10 +233,103 @@ async fn build_libation_torrent(
231233
source: MetadataSource::File,
232234
uploaded_at: Timestamp::from(UtcDateTime::UNIX_EPOCH),
233235
};
234-
let meta = clean_meta(meta, "")?;
236+
build_torrent(library, libation_meta.asin, clean_meta(meta, "")?)
237+
}
238+
239+
async fn build_nextory_torrent(
240+
library: &Library,
241+
nextory_meta: NextoryRaw,
242+
audio_files: &[DirEntry],
243+
ebook_files: &[DirEntry],
244+
) -> Result<Torrent> {
245+
let mut series = vec![];
246+
if let Some(raw_series) = nextory_meta.series
247+
&& !raw_series.name.is_empty()
248+
{
249+
let sequence = nextory_meta
250+
.volume
251+
.map(|v| v.to_string())
252+
.unwrap_or_default();
253+
if let Ok(parsed) = Series::try_from((raw_series.name, sequence)) {
254+
series.push(parsed);
255+
}
256+
}
257+
if series.is_empty()
258+
&& let Some((name, num)) = parse_series_from_title(&nextory_meta.title)
259+
{
260+
series.push(Series {
261+
name: name.to_string(),
262+
entries: SeriesEntries::new(num.into_iter().map(SeriesEntry::Num).collect()),
263+
});
264+
}
265+
266+
let mut ids = BTreeMap::new();
267+
ids.insert(ids::NEXTORY.to_string(), nextory_meta.id.to_string());
268+
if let Some(isbn) = nextory_isbn(&nextory_meta.formats) {
269+
ids.insert(ids::ISBN.to_string(), isbn);
270+
}
271+
let (size, filetypes) = folder_file_stats(audio_files, ebook_files).await?;
235272

236-
let mut torrent = Torrent {
237-
id: libation_meta.asin.clone(),
273+
let description = if nextory_meta.description_full.is_empty() {
274+
nextory_meta.blurb
275+
} else {
276+
nextory_meta.description_full
277+
};
278+
let meta = TorrentMeta {
279+
ids,
280+
vip_status: None,
281+
cat: None,
282+
media_type: MediaType::Audiobook,
283+
main_cat: None,
284+
categories: vec![],
285+
tags: vec![],
286+
language: parse_nextory_language(&nextory_meta.language),
287+
flags: None,
288+
filetypes,
289+
num_files: audio_files.len() as u64,
290+
size: Size::from_bytes(size),
291+
title: nextory_meta.title,
292+
edition: None,
293+
description,
294+
authors: nextory_meta.authors.into_iter().map(|a| a.name).collect(),
295+
narrators: nextory_meta.narrators.into_iter().map(|n| n.name).collect(),
296+
series,
297+
source: MetadataSource::File,
298+
uploaded_at: Timestamp::from(UtcDateTime::UNIX_EPOCH),
299+
};
300+
build_torrent(
301+
library,
302+
nextory_torrent_id(nextory_meta.id),
303+
clean_meta(meta, "")?,
304+
)
305+
}
306+
307+
async fn folder_file_stats(
308+
audio_files: &[DirEntry],
309+
ebook_files: &[DirEntry],
310+
) -> Result<(u64, Vec<String>)> {
311+
let mut size = 0;
312+
let mut filetypes = vec![];
313+
for file in audio_files {
314+
size += file_size(&file.metadata().await?);
315+
if let Some(ext) = file.path().extension() {
316+
filetypes.push(ext.to_string_lossy().to_lowercase());
317+
}
318+
}
319+
for file in ebook_files {
320+
size += file_size(&file.metadata().await?);
321+
if let Some(ext) = file.path().extension() {
322+
filetypes.push(ext.to_string_lossy().to_lowercase());
323+
}
324+
}
325+
filetypes.sort();
326+
filetypes.dedup();
327+
Ok((size, filetypes))
328+
}
329+
330+
fn build_torrent(library: &Library, id: String, meta: TorrentMeta) -> Result<Torrent> {
331+
Ok(Torrent {
332+
id,
238333
id_is_hash: false,
239334
mam_id: meta.mam_id(),
240335
library_path: None,
@@ -244,12 +339,28 @@ async fn build_libation_torrent(
244339
selected_audio_format: None,
245340
selected_ebook_format: None,
246341
title_search: normalize_title(&meta.title),
247-
meta: meta.clone(),
342+
meta,
248343
created_at: Timestamp::now(),
249344
replaced_with: None,
250345
library_mismatch: None,
251346
client_status: None,
252-
};
347+
})
348+
}
349+
350+
async fn link_prepared_folder_torrent(
351+
config: &Config,
352+
library: &Library,
353+
db: &Database<'_>,
354+
mut torrent: Torrent,
355+
audio_files: Vec<DirEntry>,
356+
ebook_files: Vec<DirEntry>,
357+
) -> Result<()> {
358+
let torrent_id = torrent.id.clone();
359+
let r = db.r_transaction()?;
360+
let existing_torrent: Option<Torrent> = r.get().primary(torrent_id.clone())?;
361+
if existing_torrent.is_some() {
362+
return Ok(());
363+
}
253364

254365
let matches = find_matches(db, &torrent)?;
255366
if !matches.is_empty() {
@@ -263,7 +374,9 @@ async fn build_libation_torrent(
263374
}
264375

265376
if let Some(filter) = library.edition_filter()
266-
&& !filter.matches_meta(&meta).is_ok_and(|matches| matches)
377+
&& !filter
378+
.matches_meta(&torrent.meta)
379+
.is_ok_and(|matches| matches)
267380
{
268381
trace!("Skipping folder due to edition filter");
269382
return Ok(());
@@ -282,14 +395,20 @@ async fn build_libation_torrent(
282395
);
283396

284397
let library_path = if library.options().method != LibraryLinkMethod::NoLink {
285-
let Some(mut dir) = library_dir(config.exclude_narrator_in_library_dir, library, &meta)
286-
else {
398+
let Some(mut dir) = library_dir(
399+
config.exclude_narrator_in_library_dir,
400+
library,
401+
&torrent.meta,
402+
) else {
287403
bail!("Torrent has no author");
288404
};
289-
if config.exclude_narrator_in_library_dir && !meta.narrators.is_empty() && dir.exists() {
290-
dir = library_dir(false, library, &meta).unwrap();
405+
if config.exclude_narrator_in_library_dir
406+
&& !torrent.meta.narrators.is_empty()
407+
&& dir.exists()
408+
{
409+
dir = library_dir(false, library, &torrent.meta).unwrap();
291410
}
292-
let metadata = abs::create_metadata(&meta);
411+
let metadata = abs::create_metadata(&torrent.meta);
293412

294413
create_dir_all(&dir).await?;
295414
for file in audio_files {
@@ -339,7 +458,7 @@ async fn build_libation_torrent(
339458
write_event(
340459
db,
341460
Event::new(
342-
Some(libation_meta.asin),
461+
Some(torrent_id),
343462
None,
344463
EventType::Linked {
345464
linker: library.options().name.clone(),
@@ -462,3 +581,47 @@ pub struct LibationSeries {
462581
pub sequence: String,
463582
pub title: String,
464583
}
584+
585+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
586+
pub struct NextoryWrapped {
587+
pub raw: NextoryRaw,
588+
}
589+
590+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
591+
pub struct NextoryRaw {
592+
pub id: u64,
593+
pub title: String,
594+
#[serde(default)]
595+
pub blurb: String,
596+
#[serde(default)]
597+
pub description_full: String,
598+
pub language: String,
599+
#[serde(default)]
600+
pub volume: Option<f64>,
601+
#[serde(default)]
602+
pub series: Option<NextorySeries>,
603+
#[serde(default)]
604+
pub formats: Vec<NextoryFormat>,
605+
#[serde(default)]
606+
pub authors: Vec<NextoryName>,
607+
#[serde(default)]
608+
pub narrators: Vec<NextoryName>,
609+
}
610+
611+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
612+
pub struct NextorySeries {
613+
pub name: String,
614+
}
615+
616+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
617+
pub struct NextoryFormat {
618+
#[serde(rename = "type")]
619+
pub format_type: String,
620+
#[serde(default)]
621+
pub isbn: Option<String>,
622+
}
623+
624+
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
625+
pub struct NextoryName {
626+
pub name: String,
627+
}

0 commit comments

Comments
 (0)