Skip to main content

cadmus_core/task/
thumbnail.rs

1//! Background task that extracts book cover thumbnails.
2
3use std::sync::mpsc::Sender;
4
5use crate::db::Database;
6use crate::device::CURRENT_DEVICE;
7use crate::document::open;
8use crate::library::Library;
9use crate::settings::Settings;
10use crate::task::{BackgroundTask, ShutdownSignal, TaskId};
11use crate::unit::scale_by_dpi;
12use crate::view::Event;
13use crate::view::BIG_BAR_HEIGHT;
14
15/// Runs thumbnail extraction for missing book previews in a library (or all libraries when `library_index` is `None`).
16pub struct ThumbnailExtractionTask {
17    database: Database,
18    settings: Settings,
19    /// Which library to process. `None` means all configured libraries.
20    library_index: Option<usize>,
21}
22
23impl ThumbnailExtractionTask {
24    pub fn new(database: Database, settings: Settings, library_index: Option<usize>) -> Self {
25        Self {
26            database,
27            settings,
28            library_index,
29        }
30    }
31
32    #[cfg_attr(feature = "tracing", tracing::instrument(skip(hub, shutdown, self)))]
33    fn run_for_index(&self, index: usize, hub: &Sender<Event>, shutdown: &ShutdownSignal) {
34        let lib_settings = match self.settings.libraries.get(index) {
35            Some(s) => s,
36            None => {
37                tracing::warn!(
38                    library_index = index,
39                    "library index out of range, skipping"
40                );
41                return;
42            }
43        };
44
45        let library = match Library::new(&lib_settings.path, &self.database, &lib_settings.name) {
46            Ok(lib) => lib,
47            Err(e) => {
48                tracing::error!(error = %e, library_index = index, "failed to open library for thumbnail extraction");
49                return;
50            }
51        };
52
53        let books = match library.db.books_without_thumbnails(library.library_id) {
54            Ok(books) => books,
55            Err(e) => {
56                tracing::error!(error = %e, library_id = library.library_id, "failed to query books without thumbnails");
57                return;
58            }
59        };
60
61        if books.is_empty() {
62            tracing::debug!(
63                library_id = library.library_id,
64                "no missing thumbnails for library"
65            );
66            return;
67        }
68
69        tracing::info!(
70            library_id = library.library_id,
71            count = books.len(),
72            "starting thumbnail extraction for library"
73        );
74
75        let dpi = CURRENT_DEVICE.dpi;
76        let big_height = scale_by_dpi(BIG_BAR_HEIGHT, dpi) as i32;
77        let th = big_height;
78        let tw = 3 * th / 4;
79
80        for (fp, path) in books {
81            if shutdown.should_stop() {
82                tracing::info!("thumbnail extraction task shutdown requested, stopping");
83                return;
84            }
85
86            let full_path = library.home.join(&path);
87            tracing::debug!(path = %path.display(), "extracting thumbnail");
88
89            match open(&full_path)
90                .and_then(|mut doc| {
91                    doc.preview_pixmap(tw as f32, th as f32, CURRENT_DEVICE.color_samples())
92                })
93                .and_then(|pixmap| pixmap.to_png_bytes().ok())
94            {
95                Some(bytes) => {
96                    if let Err(e) = library.db.save_thumbnail(fp, &bytes) {
97                        tracing::error!(error = %e, path = %path.display(), "failed to save thumbnail to database");
98                    } else {
99                        hub.send(Event::RefreshBookPreview(path)).ok();
100                    }
101                }
102                None => {
103                    tracing::warn!(path = %path.display(), "failed to extract preview for book");
104                }
105            }
106        }
107    }
108}
109
110impl BackgroundTask for ThumbnailExtractionTask {
111    fn id(&self) -> TaskId {
112        TaskId::ThumbnailExtraction
113    }
114
115    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
116    fn run(&mut self, hub: &Sender<Event>, shutdown: &ShutdownSignal) {
117        match self.library_index {
118            Some(index) => {
119                self.run_for_index(index, hub, shutdown);
120            }
121            None => {
122                for index in 0..self.settings.libraries.len() {
123                    if shutdown.should_stop() {
124                        return;
125                    }
126                    self.run_for_index(index, hub, shutdown);
127                }
128            }
129        }
130    }
131
132    fn finished_event(&self) -> Option<Event> {
133        Some(Event::ThumbnailExtractionFinished {
134            library_index: self.library_index,
135        })
136    }
137}