ommp.git

ommp

~ Oh My Music Player — a standalone terminal music player built with Rust and ratatui
git clone https://github.com/debuman/ommp.git

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 }