use std::path::{ Path, PathBuf };
use thiserror::Error;
const SUPPORTED_EXTENSIONS: &[&str] = &[
"mp3", "flac", "ogg", "wav", "m4a", "aac", "opus", "wma", "aiff", "alac",
];
#[derive( Debug, Error )]
pub enum LibraryError {
#[error( "IO error: {0}" )]
Io( #[from] std::io::Error ),
#[error( "Path not found: {0}" )]
NotFound( PathBuf ),
#[error( "Access denied: {0}" )]
AccessDenied( PathBuf ),
}
#[derive( Debug, Clone, Default )]
pub struct TrackMetadata {
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub track_number: Option<u32>,
pub duration_secs: Option<f64>,
pub year: Option<i32>,
pub genre: Option<String>,
}
#[derive( Debug, Clone )]
pub struct ScannedTrack {
pub path: PathBuf,
pub metadata: TrackMetadata,
}
pub struct LibraryScanner {
roots: Vec<PathBuf>,
}
impl LibraryScanner {
pub fn new() -> Self {
Self { roots: Vec::new() }
}
pub fn add_root( &mut self, path: PathBuf ) {
if !self.roots.contains( &path ) {
self.roots.push( path );
}
}
pub fn remove_root( &mut self, path: &Path ) -> bool {
if let Some( pos ) = self.roots.iter().position( |p| p == path ) {
self.roots.remove( pos );
true
} else {
false
}
}
pub fn roots( &self ) -> &[PathBuf] {
&self.roots
}
pub fn scan( &self ) -> Result<Vec<ScannedTrack>, LibraryError> {
let mut tracks = Vec::new();
for root in &self.roots {
tracing::info!( "Scanning: {:?}", root );
self.scan_directory( root, &mut tracks )?;
}
tracing::info!( "Found {} tracks", tracks.len() );
Ok( tracks )
}
pub fn scan_directory(
&self,
dir: &Path,
tracks: &mut Vec<ScannedTrack>,
) -> Result<(), LibraryError> {
self.scan_recursive( dir, tracks )
}
fn scan_recursive(
&self,
dir: &Path,
tracks: &mut Vec<ScannedTrack>,
) -> Result<(), LibraryError> {
let entries = match std::fs::read_dir( dir ) {
Ok( e ) => e,
Err( e ) if e.kind() == std::io::ErrorKind::PermissionDenied => {
tracing::warn!( "Access denied: {:?}", dir );
return Ok(()); }
Err( e ) if e.kind() == std::io::ErrorKind::NotFound => {
return Err( LibraryError::NotFound( dir.to_path_buf() ) );
}
Err( e ) => return Err( LibraryError::Io( e ) ),
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
self.scan_recursive( &path, tracks )?;
} else if Self::is_audio_file( &path ) {
tracks.push( ScannedTrack {
path,
metadata: TrackMetadata::default(), } );
}
}
Ok(())
}
fn is_audio_file( path: &Path ) -> bool {
path.extension()
.and_then( |e| e.to_str() )
.map( |e| SUPPORTED_EXTENSIONS.contains( &e.to_lowercase().as_str() ) )
.unwrap_or( false )
}
}
impl Default for LibraryScanner {
fn default() -> Self {
Self::new()
}
}
pub fn is_network_path( path: &Path ) -> bool {
path.to_str()
.map( |s| s.starts_with( r"\\" ) || s.starts_with( "//" ) )
.unwrap_or( false )
}