src/library/watcher.rs (2637B)
1 use std::path::Path;
2 use std::time::{Duration, Instant};
3
4 use crossbeam_channel::Sender;
5 use notify::{RecommendedWatcher, RecursiveMode, Watcher, EventKind};
6
7 use crate::event::Event;
8 use crate::library::Library;
9
10 pub fn spawn_watcher(music_dir: &Path, event_tx: Sender<Event>) -> Option<RecommendedWatcher> {
11 let (notify_tx, notify_rx) = crossbeam_channel::unbounded();
12 let dir = music_dir.to_path_buf();
13
14 let mut watcher = RecommendedWatcher::new(
15 move |res: Result<notify::Event, notify::Error>| {
16 if let Ok(ev) = res {
17 let dominated = matches!(
18 ev.kind,
19 EventKind::Create(_) | EventKind::Remove(_) | EventKind::Modify(_)
20 );
21 if dominated {
22 let _ = notify_tx.send(());
23 }
24 }
25 },
26 notify::Config::default(),
27 ).ok()?;
28
29 watcher.watch(music_dir, RecursiveMode::Recursive).ok()?;
30
31 // Debounce thread. `pending` is None until a real filesystem event arrives,
32 // so startup does not look like activity — seeding it with `Instant::now()`
33 // used to fire a full rescan ~2s in with nothing having changed, and again
34 // at ~2.5s because the timestamp was never reset after scanning.
35 std::thread::spawn(move || {
36 let debounce = Duration::from_secs(2);
37 let mut pending: Option<Instant> = None;
38
39 loop {
40 match notify_rx.recv_timeout(Duration::from_millis(500)) {
41 Ok(()) => {
42 pending = Some(Instant::now());
43 }
44 Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
45 let Some(last_event) = pending else { continue };
46 if last_event.elapsed() < debounce {
47 continue;
48 }
49 // Drain anything that landed while we were deciding; if the
50 // batch is still arriving, wait for it to settle.
51 let mut had_events = false;
52 while notify_rx.try_recv().is_ok() {
53 had_events = true;
54 }
55 if had_events {
56 pending = Some(Instant::now());
57 continue;
58 }
59
60 pending = None;
61 let lib = Library::scan(&dir);
62 let _ = event_tx.send(Event::LibraryReady(Box::new(lib)));
63 }
64 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
65 }
66 }
67 });
68
69 Some(watcher)
70 }