fm/app/
thumbnailer.rs

1use std::{
2    collections::VecDeque,
3    fs::create_dir_all,
4    path::{Path, PathBuf},
5    sync::{
6        atomic::{AtomicBool, Ordering},
7        Arc,
8    },
9    thread::{self, JoinHandle},
10    time::Duration,
11};
12
13use parking_lot::Mutex;
14
15use crate::modes::Thumbnail;
16use crate::{common::TMP_THUMBNAILS_DIR, log_info};
17
18/// Video thumbnail builder.
19///
20/// Store videos paths to be thumbnailed in a thread safe vector.
21/// Keep track of a bunch of workers.
22///
23/// The first available workers dequeue a path and build its thumbnails.
24///
25/// A bunch of video paths can be added to the collection.
26/// They should all be _videos_ which can be thumbnailed.
27#[derive(Debug)]
28pub struct ThumbnailManager {
29    queue: Arc<Mutex<VecDeque<PathBuf>>>,
30    is_empty: Arc<AtomicBool>,
31    _workers: Vec<Worker>,
32}
33
34impl Default for ThumbnailManager {
35    fn default() -> Self {
36        Self::create_thumbnail_dir_if_not_exist();
37        let num_workers = Self::default_thread_count();
38        let mut _workers = Vec::with_capacity(num_workers);
39        let queue = Arc::new(Mutex::new(VecDeque::new()));
40        let is_empty = Arc::new(AtomicBool::new(true));
41        for id in 0..num_workers {
42            _workers.push(Worker::new(id, queue.clone(), is_empty.clone()));
43        }
44
45        Self {
46            queue,
47            _workers,
48            is_empty,
49        }
50    }
51}
52
53impl ThumbnailManager {
54    fn create_thumbnail_dir_if_not_exist() {
55        if Path::new(TMP_THUMBNAILS_DIR).exists() {
56            return;
57        }
58        if let Err(error) = create_dir_all(TMP_THUMBNAILS_DIR) {
59            log_info!("Coudln't create {TMP_THUMBNAILS_DIR}. Error: {error}");
60        }
61    }
62
63    fn default_thread_count() -> usize {
64        thread::available_parallelism()
65            .map(|it| it.get().checked_sub(6).unwrap_or(1))
66            .unwrap_or(1)
67    }
68
69    /// Add all received files to the queue.
70    ///
71    /// They will be dealt with by the first available worker.
72    pub fn enqueue(&self, mut videos: VecDeque<PathBuf>) {
73        let mut locked_queue = self.queue.lock();
74        // TODO remove when satisfied
75        log_info!(
76            "Enqueuing {len} videos to thumbnail queue",
77            len = videos.len()
78        );
79        locked_queue.append(&mut videos);
80        drop(locked_queue);
81        self.is_empty.store(false, Ordering::SeqCst);
82    }
83
84    /// Clear the queue.
85    ///
86    /// Remove all videos awaiting to be thumbnailed from the queue.
87    pub fn clear(&self) {
88        let mut locked_queue = self.queue.lock();
89        locked_queue.clear();
90        drop(locked_queue);
91    }
92}
93
94#[derive(Debug)]
95pub struct Worker {
96    _handle: JoinHandle<()>,
97}
98
99impl Worker {
100    fn new(id: usize, queue: Arc<Mutex<VecDeque<PathBuf>>>, is_empty: Arc<AtomicBool>) -> Self {
101        let _handle = thread::spawn(move || Self::runner(id, queue, is_empty));
102        Self { _handle }
103    }
104
105    fn runner(id: usize, queue: Arc<Mutex<VecDeque<PathBuf>>>, is_empty: Arc<AtomicBool>) {
106        loop {
107            Self::advance_queue(id, &queue, &is_empty);
108            thread::sleep(Duration::from_millis(10));
109        }
110    }
111
112    fn advance_queue(id: usize, queue: &Arc<Mutex<VecDeque<PathBuf>>>, is_empty: &Arc<AtomicBool>) {
113        if is_empty.load(Ordering::SeqCst) {
114            return;
115        }
116        let mut locked_queue = queue.lock();
117        let Some(path) = locked_queue.pop_front() else {
118            return;
119        };
120        if locked_queue.is_empty() {
121            is_empty.store(true, Ordering::SeqCst);
122        }
123        drop(locked_queue);
124        log_info!("Worker {id} received task {p}", p = path.display());
125        Self::make_thumbnail(id, path);
126    }
127
128    fn make_thumbnail(id: usize, path: PathBuf) {
129        match Thumbnail::create_video(&path.to_string_lossy()) {
130            Ok(_) => log_info!("worker {id} thumbnail built successfully"),
131            Err(e) => log_info!("worker {id} error building thumbnail {e}"),
132        }
133    }
134}