Skip to main content

rig_core/loaders/epub/
loader.rs

1use epub::doc::EpubDoc;
2
3use std::fs::File;
4use std::io::BufReader;
5use std::marker::PhantomData;
6use std::path::PathBuf;
7
8use super::RawTextProcessor;
9use super::errors::EpubLoaderError;
10use super::text_processors::TextProcessor;
11
12loadable_trait!(
13    Loadable,
14    EpubLoaderError,
15    EpubDoc<BufReader<File>>,
16    load,
17    load_with_path
18);
19
20impl Loadable for PathBuf {
21    fn load(self) -> Result<EpubDoc<BufReader<File>>, EpubLoaderError> {
22        EpubDoc::new(self).map_err(EpubLoaderError::EpubError)
23    }
24
25    fn load_with_path(self) -> Result<(PathBuf, EpubDoc<BufReader<File>>), EpubLoaderError> {
26        let contents = EpubDoc::new(&self).map_err(EpubLoaderError::EpubError);
27        Ok((self, contents?))
28    }
29}
30
31/// Iterator pipeline for synchronous EPUB loading and chapter processing.
32/// `P` selects text processing and defaults to [`RawTextProcessor`].
33/// Loading and processing errors are yielded unless filtered; unavailable chapter
34/// text is skipped.
35pub struct EpubFileLoader<'a, T, P = RawTextProcessor> {
36    iterator: Box<dyn Iterator<Item = T> + 'a>,
37    _processor: PhantomData<P>,
38}
39
40type EpubLoaded = Result<(PathBuf, EpubDoc<BufReader<File>>), EpubLoaderError>;
41
42impl<'a, P> EpubFileLoader<'a, Result<PathBuf, EpubLoaderError>, P> {
43    /// Loads each EPUB during iteration, yielding document-loading errors.
44    ///
45    /// ```no_run
46    /// use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
47    ///
48    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
49    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?.load().into_iter();
50    /// for result in content {
51    ///     match result {
52    ///         Ok(doc) => println!("{doc:?}"),
53    ///         Err(e) => eprintln!("Error reading epub: {e}"),
54    ///     }
55    /// }
56    /// # Ok(())
57    /// # }
58    /// ```
59    pub fn load(self) -> EpubFileLoader<'a, Result<EpubDoc<BufReader<File>>, EpubLoaderError>, P> {
60        EpubFileLoader {
61            iterator: Box::new(self.iterator.map(Loadable::load)),
62            _processor: PhantomData,
63        }
64    }
65
66    /// Loads each EPUB paired with its source path, yielding loading errors.
67    ///
68    /// ```no_run
69    /// use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
70    ///
71    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
72    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?.load_with_path().into_iter();
73    /// for result in content {
74    ///     match result {
75    ///         Ok((path, doc)) => println!("{path:?} {doc:?}"),
76    ///         Err(e) => eprintln!("Error reading epub: {e}"),
77    ///     }
78    /// }
79    /// # Ok(())
80    /// # }
81    /// ```
82    pub fn load_with_path(self) -> EpubFileLoader<'a, EpubLoaded, P> {
83        EpubFileLoader {
84            iterator: Box::new(self.iterator.map(Loadable::load_with_path)),
85            _processor: PhantomData,
86        }
87    }
88}
89
90impl<'a, P> EpubFileLoader<'a, Result<PathBuf, EpubLoaderError>, P>
91where
92    P: TextProcessor,
93{
94    /// Concatenates processed chapters without separators, yielding loading or
95    /// processing errors. Unavailable chapter text is skipped.
96    ///
97    /// ```no_run
98    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
99    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
100    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?.read().into_iter();
101    /// for result in content {
102    ///     match result {
103    ///         Ok(content) => println!("{content}"),
104    ///         Err(e) => eprintln!("Error reading epub: {e}"),
105    ///     }
106    /// }
107    /// # Ok(())
108    /// # }
109    /// ```
110    pub fn read(self) -> EpubFileLoader<'a, Result<String, EpubLoaderError>, P> {
111        EpubFileLoader {
112            iterator: Box::new(self.iterator.map(|res| {
113                let doc = res.load().map(EpubChapterIterator::<P>::from)?;
114
115                Ok(doc
116                    .into_iter()
117                    .collect::<Result<Vec<String>, EpubLoaderError>>()?
118                    .into_iter()
119                    .collect::<String>())
120            })),
121            _processor: PhantomData,
122        }
123    }
124
125    /// Pairs each path with concatenated processed chapters, yielding loading
126    /// or processing errors. Unavailable chapter text is skipped.
127    ///
128    /// ```no_run
129    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
130    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
131    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?.read_with_path().into_iter();
132    /// for result in content {
133    ///     match result {
134    ///         Ok((path, content)) => println!("{path:?} {content}"),
135    ///         Err(e) => eprintln!("Error reading epub: {e}"),
136    ///     }
137    /// }
138    /// # Ok(())
139    /// # }
140    /// ```
141    pub fn read_with_path(
142        self,
143    ) -> EpubFileLoader<'a, Result<(PathBuf, String), EpubLoaderError>, P> {
144        EpubFileLoader {
145            iterator: Box::new(self.iterator.map(|res| {
146                let (path, doc) = res.load_with_path()?;
147
148                let content = EpubChapterIterator::<P>::from(doc)
149                    .collect::<Result<Vec<String>, EpubLoaderError>>()?
150                    .into_iter()
151                    .collect::<String>();
152                Ok((path, content))
153            })),
154            _processor: PhantomData,
155        }
156    }
157}
158
159impl<'a, P> EpubFileLoader<'a, EpubDoc<BufReader<File>>, P>
160where
161    P: TextProcessor + 'a,
162{
163    /// Yields processed chapter results across documents in input order,
164    /// skipping unavailable chapter text.
165    ///
166    /// ```no_run
167    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
168    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
169    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?
170    ///     .load()
171    ///     .ignore_errors()
172    ///     .by_chapter()
173    ///     .into_iter();
174    /// for result in content {
175    ///     match result {
176    ///         Ok(chapter) => println!("{chapter}"),
177    ///         Err(e) => eprintln!("Error reading chapter: {e}"),
178    ///     }
179    /// }
180    /// # Ok(())
181    /// # }
182    /// ```
183    pub fn by_chapter(self) -> EpubFileLoader<'a, Result<String, EpubLoaderError>, P> {
184        EpubFileLoader {
185            iterator: Box::new(self.iterator.flat_map(EpubChapterIterator::<P>::from)),
186            _processor: PhantomData,
187        }
188    }
189}
190
191type ByChapter = (PathBuf, Vec<(usize, Result<String, EpubLoaderError>)>);
192impl<'a, P: TextProcessor> EpubFileLoader<'a, (PathBuf, EpubDoc<BufReader<File>>), P> {
193    /// Pairs each path with zero-based indices and chapter-processing results.
194    /// Indices count available chapter texts, not skipped source entries.
195    ///
196    /// ```no_run
197    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
198    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
199    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?
200    ///     .load_with_path()
201    ///     .ignore_errors()
202    ///     .by_chapter()
203    ///     .ignore_errors()
204    ///     .into_iter();
205    ///
206    /// for result in content {
207    ///     println!("{result:?}");
208    /// }
209    /// # Ok(())
210    /// # }
211    /// ```
212    pub fn by_chapter(self) -> EpubFileLoader<'a, ByChapter, P> {
213        EpubFileLoader {
214            iterator: Box::new(self.iterator.map(|doc| {
215                let (path, doc) = doc;
216
217                (
218                    path,
219                    EpubChapterIterator::<P>::from(doc)
220                        .enumerate()
221                        .collect::<Vec<_>>(),
222                )
223            })),
224            _processor: PhantomData,
225        }
226    }
227}
228
229impl<'a, P> EpubFileLoader<'a, ByChapter, P>
230where
231    P: TextProcessor,
232{
233    /// Drops failed chapter results, retaining paths and existing chapter indices.
234    /// Documents with no successful chapters remain.
235    ///
236    /// ```no_run
237    /// # use rig_core::loaders::{EpubFileLoader, RawTextProcessor};
238    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
239    /// let content = EpubFileLoader::<_, RawTextProcessor>::with_glob("tests/data/*.epub")?
240    ///     .load_with_path()
241    ///     .ignore_errors()
242    ///     .by_chapter()
243    ///     .ignore_errors();
244    /// for (_path, chapters) in content {
245    ///     println!("{}", chapters.len())
246    /// }
247    /// # Ok(())
248    /// # }
249    /// ```
250    pub fn ignore_errors(self) -> EpubFileLoader<'a, (PathBuf, Vec<(usize, String)>), P> {
251        EpubFileLoader {
252            iterator: Box::new(self.iterator.map(|(path, chapters)| {
253                let chapters = chapters
254                    .into_iter()
255                    .filter_map(|(idx, res)| res.ok().map(|content| (idx, content)))
256                    .collect::<Vec<_>>();
257                (path, chapters)
258            })),
259            _processor: PhantomData,
260        }
261    }
262}
263
264loader_scaffold!(EpubFileLoader, EpubLoaderError, dir: all_entries, extra: P);
265
266struct EpubChapterIterator<P> {
267    epub: EpubDoc<BufReader<File>>,
268    finished: bool,
269    _processor: PhantomData<P>,
270}
271
272impl<P> From<EpubDoc<BufReader<File>>> for EpubChapterIterator<P> {
273    fn from(epub: EpubDoc<BufReader<File>>) -> Self {
274        Self::new(epub)
275    }
276}
277
278impl<P> EpubChapterIterator<P> {
279    fn new(epub: EpubDoc<BufReader<File>>) -> Self {
280        Self {
281            epub,
282            finished: false,
283            _processor: PhantomData,
284        }
285    }
286}
287
288impl<P> Iterator for EpubChapterIterator<P>
289where
290    P: TextProcessor,
291{
292    type Item = Result<String, EpubLoaderError>;
293
294    fn next(&mut self) -> Option<Self::Item> {
295        if self.finished {
296            return None;
297        }
298
299        // Unavailable chapter text is skipped rather than yielded as an error.
300        while !self.finished {
301            let chapter = self.epub.get_current_str();
302
303            if !self.epub.go_next() {
304                self.finished = true;
305            }
306
307            if let Some((text, _)) = chapter {
308                return Some(
309                    P::process(&text)
310                        .map_err(|err| EpubLoaderError::TextProcessorError(Box::new(err))),
311                );
312            }
313        }
314
315        None
316    }
317}
318
319#[cfg(test)]
320mod tests;