chilen_backend/music_lib/indexer/
mod.rs1pub 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#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum IndexingIntensity {
27 #[default]
29 Fast,
30 Balanced,
32 Lightweight,
34}
35
36#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum CacheMode {
39 #[cfg_attr(test, default)]
44 #[cfg(test)]
45 Disabled,
46 #[cfg_attr(not(test), default)]
47 UseCache,
49 RebuildCache,
51}
52
53impl IndexingIntensity {
54 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}