@@ -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
119137async 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
141149async 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