Skip to main content

chilen_backend/music_lib/indexer/
mod.rs

1pub mod covers;
2
3use std::{
4    collections::HashSet,
5    num::NonZero,
6    path::PathBuf,
7    sync::{
8        Mutex, RwLock,
9        atomic::{AtomicUsize, Ordering},
10    },
11};
12
13use log::{error, info, trace, warn};
14use serde::{Deserialize, Serialize};
15use walkdir::WalkDir;
16
17use crate::{
18    Error,
19    music_lib::{self, MUSIC_DIR, Track},
20};
21
22/// Defines the intensity level for resource allocation during indexing.
23///
24/// The indexer will always allocate at least one CPU core.
25#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum IndexingIntensity {
27    /// Use all available CPU cores.
28    #[default]
29    Fast,
30    /// Use half the available CPU cores.
31    Balanced,
32    /// Use a quarter of the available CPU cores.
33    Lightweight,
34}
35
36/// Track cover art caching mode used when indexing the music library.
37#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum CacheMode {
39    /// Don't attempt to obtain cover art images for indexed tracks.
40    ///
41    /// This should only be used for testing.
42    ///
43    #[cfg_attr(test, default)]
44    #[cfg(test)]
45    Disabled,
46    #[cfg_attr(not(test), default)]
47    /// Use cached cover art images when possible.
48    UseCache,
49    /// Discard cached cover art images and extract them when indexing.
50    RebuildCache,
51}
52
53impl IndexingIntensity {
54    /// Returns the percentage of available CPU cores the indexer should use.
55    fn multiplier(&self) -> f32 {
56        match self {
57            IndexingIntensity::Fast => 1.0,
58            IndexingIntensity::Balanced => 0.5,
59            IndexingIntensity::Lightweight => 0.25,
60        }
61    }
62}
63
64#[cfg_attr(test, derive(Default))]
65#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66pub struct Config {
67    pub cache_mode: CacheMode,
68    pub indexing_intensity: IndexingIntensity,
69    pub covers: covers::Config,
70}
71
72fn index_files(files: Vec<PathBuf>, config: music_lib::Config) -> Vec<Track> {
73    let total = files.len();
74    let mut tracks = Mutex::new(Vec::with_capacity(files.len()));
75    let next_index = AtomicUsize::new(0);
76    let completed = AtomicUsize::new(0);
77    let last_sent = AtomicUsize::new(0);
78
79    let report_every = (total / 100).max(1);
80    let covers_lookup_set: RwLock<HashSet<PathBuf>> = RwLock::new(HashSet::new());
81
82    crate::send_event(crate::Event::LoadProgressChanged(
83        music_lib::state::Progress::Indexing { progress: 0.0 },
84    ));
85
86    std::thread::scope(|s| {
87        let mult = config.indexer.indexing_intensity.multiplier();
88        let available_threads = std::thread::available_parallelism().map_or(1, |v| {
89            (NonZero::get(v) as f32 * mult)
90                .max(1.0)
91                .min(files.len() as f32) as u32
92        });
93        trace!("Starting {available_threads} indexing threads");
94
95        for _ in 0..available_threads {
96            s.spawn(|| {
97                while let Some(file) = files.get(next_index.fetch_add(1, Ordering::Relaxed)) {
98                    let tagged_file = match lofty::read_from_path(file) {
99                        Ok(tagged_file) => tagged_file,
100                        Err(e) => {
101                            match e.kind() {
102                                lofty::error::ErrorKind::UnknownFormat => {
103                                    info!("Likely not an audio file: {file:?}");
104                                }
105                                _ => {
106                                    warn!("Could not get tags from file {file:?}: {e}");
107                                }
108                            }
109                            continue;
110                        }
111                    };
112
113                    let track =
114                        match Track::new(file.clone(), &tagged_file, &config, &covers_lookup_set) {
115                            Ok(track) => track,
116                            Err(e) => {
117                                warn!("Could not create a track struct: {e}");
118                                continue;
119                            }
120                        };
121
122                    let done = completed.fetch_add(1, Ordering::Relaxed) + 1;
123                    let should_send = last_sent
124                        .try_update(Ordering::Relaxed, Ordering::Relaxed, |last| {
125                            if done >= last + report_every {
126                                Some(done)
127                            } else {
128                                None
129                            }
130                        })
131                        .is_ok();
132
133                    if should_send {
134                        let progress = done as f32 / total as f32;
135
136                        crate::send_event(crate::Event::LoadProgressChanged(
137                            music_lib::state::Progress::Indexing { progress },
138                        ));
139                    }
140
141                    tracks.lock().unwrap().push(track);
142                }
143            });
144        }
145    });
146
147    tracks.get_mut().unwrap().shrink_to_fit();
148    tracks.into_inner().unwrap()
149}
150
151pub(crate) fn index(config: music_lib::Config) -> Result<Vec<Track>, Error> {
152    #[cfg(debug_assertions)]
153    {
154        warn!(
155            "\x1b[1mINDEXER RUNNING IN DEBUG MODE\x1b[0m, image decoding will be EXTREMELY SLOW. Consider running Chilen in release mode for the initial indexing",
156        );
157    }
158
159    let music_dir = MUSIC_DIR.read().unwrap().clone().unwrap();
160
161    match music_dir.is_dir() {
162        true => {
163            trace!("Indexing directory: {music_dir:?}");
164        }
165        false => {
166            error!("Music library path is not a directory!");
167            return Err(Error::NotADirectory(music_dir));
168        }
169    }
170
171    crate::send_event(crate::Event::LoadProgressChanged(
172        music_lib::state::Progress::FindingTracks,
173    ));
174
175    let files: Vec<_> = WalkDir::new(music_dir)
176        .into_iter()
177        .filter_map(|result| {
178            let entry = match result {
179                Ok(entry) => entry,
180                Err(e) => {
181                    warn!("Error while trying to access the file: {e}");
182                    return None;
183                }
184            };
185
186            match entry.metadata() {
187                Ok(meta) => {
188                    if meta.is_file() {
189                        Some(PathBuf::from(entry.path()))
190                    } else {
191                        None
192                    }
193                }
194                Err(e) => {
195                    warn!("Could not get path metadata: {e}");
196                    None
197                }
198            }
199        })
200        .collect();
201
202    Ok(index_files(files, config))
203}