1#![deny(
2 rustdoc::broken_intra_doc_links,
3 rustdoc::private_intra_doc_links,
4 rustdoc::bare_urls,
5 missing_docs
6)]
7#![warn(rustdoc::unescaped_backticks)]
8use std::fmt::{format, Debug, Formatter};
89use std::fs;
90use std::fs::{create_dir_all, File};
91use std::io::{Cursor, Read, Write};
92use std::path::{Path, PathBuf};
93
94use directories::BaseDirs;
95#[cfg(feature = "epub")]
96use epub_builder::{EpubBuilder, Error, ZipLibrary};
97use fs_extra::dir::{move_dir, CopyOptions};
98#[cfg(feature = "epub")]
99use image::EncodableLayout;
100use image::{ImageError, ImageFormat};
101use libwebnovel::backends::BackendError;
102use libwebnovel::{Backend, Backends, Chapter, ChapterParseError};
103use log::{debug, error, info, trace, warn};
104use serde::{Deserialize, Serialize};
105use thiserror::Error;
106use url::Url;
107
108#[derive(Error, Debug)]
110pub enum LibraryError {
111 #[error(transparent)]
113 IoError(#[from] std::io::Error),
114 #[error(transparent)]
116 TomlDeserializationError(#[from] toml::de::Error),
117 #[error(transparent)]
119 TomlSerializationError(#[from] toml::ser::Error),
120 #[error(transparent)]
122 UrlParseError(#[from] url::ParseError),
123 #[error(transparent)]
125 ChapterParseError(#[from] ChapterParseError),
126 #[error(transparent)]
128 BackendError(#[from] BackendError),
129 #[error(transparent)]
131 ImageError(#[from] ImageError),
132 #[error(transparent)]
134 Infallible(#[from] std::convert::Infallible),
135 #[error(transparent)]
137 FsError(#[from] fs_extra::error::Error),
138 #[error("No novel with URL \"{0}\"was found.")]
140 NoSuchNovel(Url),
141 #[error("{0}")]
143 ParseError(String),
144 #[error("Novel at url {0} is already in our watchlist")]
146 NovelAlreadyPresent(Url),
147}
148
149#[derive(Serialize, Deserialize, Debug)]
172pub struct LocalLibrary {
173 #[serde(skip)]
174 config_path: PathBuf,
175 library_base_path: PathBuf,
176 novels: Vec<Novel>,
177}
178
179impl Default for LocalLibrary {
180 fn default() -> Self {
181 let dirs = BaseDirs::new().unwrap();
182 Self {
183 config_path: dirs.config_dir().join(env!("CARGO_PKG_NAME")).to_path_buf(),
184 library_base_path: dirs.data_dir().join(env!("CARGO_PKG_NAME")),
185 novels: Vec::new(),
186 }
187 }
188}
189
190impl LocalLibrary {
191 pub fn load(config_path: impl Into<PathBuf>) -> Result<Self, LibraryError> {
206 let config_path = config_path.into();
207 if !config_path.exists() {
208 info!("Could not find a configuration file, creating one with default values.");
209 return Ok(Self {
210 config_path,
211 ..Default::default()
212 });
213 }
214 let mut config_file = File::open(&config_path)?;
215 let mut config_str = String::new();
216 config_file.read_to_string(&mut config_str)?;
217 let mut config: Self = toml::from_str(&config_str)?;
218 config.config_path = config_path;
219 Ok(config)
220 }
221
222 pub fn set_base_path(
225 &mut self,
226 base_path: impl Into<PathBuf> + AsRef<Path>,
227 ) -> Result<(), LibraryError> {
228 if self.library_base_path.is_dir() {
229 create_dir_all(&base_path)?;
232 move_dir(
233 &self.library_base_path,
234 &base_path,
235 &CopyOptions {
236 overwrite: true,
237 ..CopyOptions::default()
238 },
239 )?;
240 }
241 self.library_base_path = base_path.into();
242 for novel in self.novels.iter_mut() {
243 novel.path = self
244 .library_base_path
245 .join(novel.backend.immutable_identifier()?);
246 }
247 self.persist()?;
248 Ok(())
249 }
250
251 pub fn persist(&self) -> Result<(), LibraryError> {
272 info!("Saving file to disk at path {}", self.config_path.display());
273 let toml = toml::to_string(self)?;
274 create_dir_all(self.config_path.parent().unwrap())?;
275 let mut file = File::create(&self.config_path)?;
276 file.write_all(toml.as_bytes())?;
277 Ok(())
278 }
279
280 pub fn base_path(&self) -> &Path {
282 self.library_base_path.as_path()
283 }
284
285 pub fn add(&mut self, url: &str) -> Result<String, LibraryError> {
288 let url = url.parse::<Url>()?;
289 if self.novels.iter().any(|n| n.url == url) {
290 return Err(LibraryError::NovelAlreadyPresent(url));
291 }
292 let novel = Novel::new(url, self.base_path())?;
293 let novel_title = novel.title()?;
294 self.novels.push(novel);
295 self.persist()?;
296 Ok(novel_title)
297 }
298
299 pub fn list(&self) -> Vec<Url> {
301 self.novels.iter().map(|novel| novel.url.clone()).collect()
302 }
303
304 pub fn update(&mut self) -> Result<(), Vec<LibraryError>> {
307 let mut errors = Vec::new();
308 for novel in self.novels.iter_mut() {
309 match novel.update() {
310 Ok(()) => {}
311 Err(e) => {
312 errors.push(e);
313 }
314 }
315 }
316 if !errors.is_empty() {
317 return Err(errors);
318 }
319 Ok(())
320 }
321
322 pub fn remove(&mut self, url: &str) -> Result<(), LibraryError> {
326 let url = Url::parse(url)?;
327 self.novels.retain(|novel| {
328 if novel.url == url {
329 let path = novel.novel_path();
330 if let Err(e) = fs::remove_dir_all(path) {
331 warn!("Failed to remove directory {}: {}", path.display(), e);
332 }
333 return false;
334 }
335 true
336 });
337 Ok(())
338 }
339
340 pub fn novels(&self) -> &Vec<Novel> {
342 &self.novels
343 }
344
345 pub fn novels_mut(&mut self) -> &mut Vec<Novel> {
347 &mut self.novels
348 }
349}
350
351#[derive(Serialize, Deserialize)]
356#[serde(try_from = "NovelConfig", into = "NovelConfig")]
357pub struct Novel {
358 url: Url,
359 path: PathBuf,
360 backend: Backends,
361 chapters: Vec<Chapter>,
362}
363
364impl Debug for Novel {
365 #[allow(dead_code)]
366 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
367 #[derive(Debug)]
368 struct Novel<'a> {
369 url: String,
370 path: &'a PathBuf,
371 backend: &'a Backends,
372 chapters: &'a Vec<Chapter>,
373 }
374 let Self {
375 url,
376 path,
377 backend,
378 chapters,
379 } = self;
380 Debug::fmt(
381 &Novel {
382 url: url.to_string(),
383 path,
384 backend,
385 chapters,
386 },
387 f,
388 )
389 }
390}
391
392impl Clone for Novel {
393 fn clone(&self) -> Self {
394 Self {
395 url: self.url.clone(),
396 path: self.path.clone(),
397 backend: Backends::new(self.url.as_ref()).unwrap(),
398 chapters: self.chapters.clone(),
399 }
400 }
401}
402
403pub struct NovelChapterUpdateIter<'a> {
437 novel: &'a mut Novel,
438 missing_chapters: Vec<usize>,
439 current_chapter_index: usize,
440}
441impl Debug for NovelChapterUpdateIter<'_> {
442 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
443 write!(
444 f,
445 "NovelChapterUpdateIter {}/{}:\n Chapter_Index={},\n Novel={},\n base_dir={}",
446 self.current_chapter_index,
447 self.missing_chapters.len(),
448 if self.current_chapter_index >= self.missing_chapters.len() {
449 "Out of bounds (may be normal if end of iter)".to_string()
450 } else {
451 self.missing_chapters[self.current_chapter_index].to_string()
452 },
453 self.novel.url,
454 self.novel.path.display(),
455 )
456 }
457}
458
459impl<'a> Iterator for NovelChapterUpdateIter<'a> {
460 type Item = Result<String, LibraryError>;
461
462 fn next(&mut self) -> Option<Self::Item> {
463 trace!("{:?}", self);
464 if self.current_chapter_index >= self.missing_chapters.len() {
465 return None;
466 }
467 let r = match self
468 .novel
469 .backend
470 .get_chapter(self.missing_chapters[self.current_chapter_index])
471 {
472 Ok(v) => match Novel::persist_chapter(&self.novel.path, &v) {
473 Ok(_) => {
474 let title = v.title().clone().unwrap();
475 self.novel.chapters.push(v);
476 Some(Ok(title))
477 }
478 Err(e) => Some(Err(e)),
479 },
480 Err(e) => Some(Err(LibraryError::from(e))),
481 };
482 self.current_chapter_index += 1;
483 r
484 }
485}
486
487impl<'a> ExactSizeIterator for NovelChapterUpdateIter<'a> {
488 fn len(&self) -> usize {
489 self.missing_chapters.len()
490 }
491}
492
493impl Novel {
494 pub fn new(url: impl Into<Url>, library_path: &Path) -> Result<Self, LibraryError> {
497 let url = url.into();
498 let mut novel = Self {
499 url: url.clone(),
500 path: Default::default(),
501 backend: Backends::new(url.as_ref())?,
502 chapters: vec![],
503 };
504 novel.path = library_path.join(novel.backend.immutable_identifier()?);
505 Ok(novel)
506 }
507
508 pub fn url(&self) -> &Url {
510 &self.url
511 }
512
513 pub fn title(&self) -> Result<String, LibraryError> {
515 Ok(self.backend.title()?)
516 }
517
518 pub fn get_chapter_storage_path(novel_path: &Path) -> PathBuf {
521 novel_path.join("chapters")
522 }
523
524 pub fn get_cover_storage_path(novel_path: &Path) -> PathBuf {
527 novel_path.join("cover.png")
528 }
529
530 pub fn chapter_storage_path(&self) -> PathBuf {
533 Self::get_chapter_storage_path(&self.path)
534 }
535
536 pub fn cover_storage_path(&self) -> PathBuf {
539 Self::get_cover_storage_path(&self.path)
540 }
541
542 fn persist_chapter(novel_path: &Path, chapter: &Chapter) -> Result<(), LibraryError> {
543 let path = Self::get_chapter_storage_path(novel_path);
544 if !path.exists() {
545 create_dir_all(&path)?;
546 }
547 let chapter_file_name = match chapter.title() {
548 None => {
549 format!("{}.html", chapter.index())
550 }
551 Some(title) => {
552 format!("{}-{}.html", chapter.index(), title)
553 }
554 };
555 let chapter_path = path.join(chapter_file_name);
556 let mut file = File::create(chapter_path)?;
557 file.write_all(chapter.to_string().as_bytes())?;
558 Ok(())
559 }
560
561 pub fn download_cover(&self) -> Result<(), LibraryError> {
564 let cover_path = self.path.join("cover.png");
565 if !cover_path.is_file() {
566 if !self.path.exists() {
567 create_dir_all(&self.path)?;
568 } else if cover_path.is_dir() {
569 warn!(
570 "{} is a directory! we will delete it in order to create the cover file",
571 cover_path.display()
572 );
573 fs::remove_dir_all(&cover_path)?;
575 }
576 let cover_data = self.backend.cover()?;
577 let data = match image::guess_format(&cover_data) {
578 Ok(format) => match format {
579 ImageFormat::Png => cover_data,
580 iformat => {
581 info!(
582 "Cover image is in {}. Converting to png.",
583 iformat.extensions_str()[0]
584 );
585 let cursor = Cursor::new(cover_data);
586 let img = image::load(cursor, iformat)?;
587 let mut buffer = Vec::new();
588 img.write_to(&mut Cursor::new(&mut buffer), ImageFormat::Png)?;
589 buffer
590 }
591 },
592 Err(e) => {
593 warn!("Error trying to guess image: {}", e);
596 cover_data
597 }
598 };
599 let mut f = File::create(cover_path)?;
600 f.write_all(&data)?;
601 }
602 Ok(())
603 }
604
605 pub fn update(&mut self) -> Result<(), LibraryError> {
608 self.load_local_chapters()?;
609
610 let _errors = self
613 .update_iter()
614 .filter(Result::is_err)
615 .map(|r| {
616 let err = r.unwrap_err();
617 warn!("{}", err);
618 err
619 })
620 .collect::<Vec<_>>();
621 self.consolidate_chapter_collection();
622 for chapter in &self.chapters {
625 Self::persist_chapter(&self.path, chapter)?;
626 }
627 Ok(())
628 }
629
630 pub fn consolidate_chapter_collection(&mut self) {
633 self.chapters.sort_by(self.backend.get_ordering_function());
635 self.chapters.dedup();
637 for (i, chapter) in self.chapters.iter_mut().enumerate() {
640 let chapter_index = i + 1;
642 if *chapter.index() != chapter_index {
643 warn!("There could be a conflict in chapter {}: index was expected to be {} but was {}. Setting chapter index to expectation",
644 chapter.title().clone().unwrap_or("<title_not_found>".to_string()),
645 chapter_index,
646 chapter.index()
647 );
648 chapter.set_index(chapter_index);
649 }
650 }
651 }
652
653 pub fn update_iter(&mut self) -> NovelChapterUpdateIter {
662 let missing_chapters = self.get_missing_chapters_indexes().unwrap();
663 debug!("Missing chapters: {:?}", missing_chapters);
664
665 NovelChapterUpdateIter {
666 novel: self,
667 missing_chapters,
668 current_chapter_index: 0,
669 }
670 }
671
672 pub fn get_missing_chapters_indexes(&self) -> Result<Vec<usize>, LibraryError> {
674 let local_chapters_tuples: Vec<(usize, String)> = self
676 .get_local_chapters()?
677 .iter()
678 .map(|c| c.try_into().unwrap())
679 .collect();
680 debug!("local chapters: {:?}", local_chapters_tuples);
681 let available_chapters = self.backend.get_chapter_list()?;
683 debug!("available chapters: {:?}", available_chapters);
684 Ok(available_chapters
686 .iter()
687 .filter(|c| {
688 for lc in &local_chapters_tuples {
689 if c.0 == lc.0 {
690 if c.1 == lc.1 {
693 return false;
695 }
696 warn!("distant chapter {:?} and local chapter {:?} have the same index but different titles! I won't download them.", c, lc);
699 return false;
700 }
701 }
702 debug!("Will download chapter {}: {}", c.0, c.1);
704 true
705 })
706 .map(|c| c.0)
707 .collect::<Vec<_>>())
708 }
709
710 pub fn get_local_chapter_count(&self) -> Result<usize, LibraryError> {
712 let path = self.chapter_storage_path();
713 if !path.exists() {
714 return Ok(0);
715 }
716 Ok(fs::read_dir(path)?.count())
718 }
719
720 pub fn get_remote_chapter_count(&self) -> Result<usize, LibraryError> {
722 Ok(self.backend.get_chapter_count()?)
723 }
724
725 pub fn load_local_chapters(&mut self) -> Result<(), LibraryError> {
728 self.chapters = self.get_local_chapters()?;
729 Ok(())
730 }
731
732 pub fn get_local_chapters(&self) -> Result<Vec<Chapter>, LibraryError> {
734 let path = self.chapter_storage_path();
735 if !path.exists() {
736 debug!(
737 "path {} doesn't exist, returning empty local chapter list",
738 path.display()
739 );
740 return Ok(Vec::new());
741 }
742 let mut chapters: Vec<Chapter> = Vec::new();
743 trace!("Reading directory {} contents", path.display());
744 let chapter_files = fs::read_dir(&path)?;
745 for chapter_file in chapter_files {
746 let chapter_file = chapter_file?;
747 let chapter_path = chapter_file.path();
748 trace!("looking at file {}", chapter_path.display());
749 let mut file = File::open(&chapter_path)?;
750 let mut content = String::new();
751 file.read_to_string(&mut content)?;
752 trace!("Parsing chapter from file {}", chapter_path.display());
753 let chapter = content.parse()?;
754 chapters.push(chapter);
755 }
756 Ok(chapters)
757 }
758
759 pub fn novel_path(&self) -> &Path {
761 &self.path
762 }
763
764 #[cfg(feature = "epub")]
767 pub fn generate_epub(&self) -> Result<PathBuf, EpubGenerationError> {
768 const TITLE_PAGE_HTML: &str = r#"
769 <html>
770 <head>
771 <style>
772
773 </style>
774 </head>
775 <body style="width: 30em; margin: auto; text-align: center;">
776 <h1>FICTION_TITLE</h1>
777 <p><img src="/cover.png"/></p>
778 </body>
779 </html>
780 "#;
781 let mut builder =
782 EpubBuilder::new(ZipLibrary::new()?).map_err(|e| EpubGenerationError::EpubError {
783 msg: "Could not create new builder".to_string(),
784 cause: e,
785 })?;
786 builder
787 .metadata("title", self.title()?)
788 .map_err(|e| EpubGenerationError::EpubError {
789 msg: "Could not set title".to_string(),
790 cause: e,
791 })?
792 .metadata("author", self.backend.get_authors()?.join(", "))
793 .map_err(|e| EpubGenerationError::EpubError {
794 msg: "could not set author metadata".to_string(),
795 cause: e,
796 })?
797 .epub_version(epub_builder::EpubVersion::V30);
798 builder.add_content(
799 epub_builder::EpubContent::new(
800 "cover.xhtml",
801 TITLE_PAGE_HTML
802 .replace("FICTION_TITLE", &self.title().unwrap())
803 .as_bytes(),
804 )
805 .reftype(epub_builder::ReferenceType::Cover),
806 )?;
807 let cover_path = self.cover_storage_path();
808 if !cover_path.is_file() {
809 self.download_cover()?;
810 }
811 let mut f = File::open(cover_path)?;
812 let mut data = Vec::new();
813 f.read_to_end(&mut data)?;
814 builder.add_cover_image("cover.png", data.as_bytes(), "image/png")?;
815 drop(data);
816 for chapter in &self.chapters {
817 let title = chapter.title().clone().unwrap();
818 builder.add_content(
819 epub_builder::EpubContent::new(
820 format!("{}.xhtml", title),
821 chapter.content().as_bytes(),
822 )
823 .title(format!("ch{}: {}", chapter.index(), title))
824 .reftype(epub_builder::ReferenceType::Text),
825 )?;
826 }
827 builder.inline_toc();
828 let epub_path = self.novel_path().join(format!("{}.epub3", self.title()?));
829 let mut f = File::create(&epub_path)?;
830 builder.generate(&mut f)?;
831 Ok(epub_path)
832 }
833}
834
835#[cfg(feature = "epub")]
837#[derive(Error, Debug)]
838pub enum EpubGenerationError {
839 #[error(transparent)]
841 LibraryError(#[from] LibraryError),
842 #[error("{msg}: {cause:?}")]
844 EpubError {
845 msg: String,
847 cause: epub_builder::Error,
849 },
850 #[error("{msg}: {cause:?}")]
852 IoError {
853 msg: String,
855 cause: std::io::Error,
857 },
858}
859
860#[cfg(feature = "epub")]
861impl From<epub_builder::Error> for EpubGenerationError {
862 fn from(value: Error) -> Self {
863 Self::EpubError {
864 msg: String::new(),
865 cause: value,
866 }
867 }
868}
869#[cfg(feature = "epub")]
870impl From<BackendError> for EpubGenerationError {
871 fn from(value: BackendError) -> Self {
872 EpubGenerationError::LibraryError(LibraryError::BackendError(value))
873 }
874}
875#[cfg(feature = "epub")]
876impl From<std::io::Error> for EpubGenerationError {
877 fn from(value: std::io::Error) -> Self {
878 Self::IoError {
879 msg: value.to_string(),
880 cause: value,
881 }
882 }
883}
884
885impl TryFrom<Url> for Novel {
886 type Error = LibraryError;
887
888 fn try_from(value: Url) -> Result<Self, Self::Error> {
890 Self::try_from(NovelConfig {
891 url: value,
892 path: Default::default(),
893 })
894 }
895}
896
897impl TryFrom<NovelConfig> for Novel {
898 type Error = LibraryError;
899
900 fn try_from(value: NovelConfig) -> Result<Self, Self::Error> {
901 let novel = Self {
902 url: value.url.clone(),
903 path: value.path.clone(),
904 backend: Backends::new(value.url.as_str())?,
905 chapters: vec![],
906 };
907 Ok(novel)
908 }
909}
910
911#[derive(Serialize, Deserialize, Clone, Debug)]
913struct NovelConfig {
914 url: Url,
915 path: PathBuf,
916}
917
918impl From<Novel> for NovelConfig {
919 fn from(value: Novel) -> Self {
920 Self {
921 url: value.url,
922 path: value.path,
923 }
924 }
925}