From 9b89ea803b72503bebb01019cadce7f9dfc5128e Mon Sep 17 00:00:00 2001 From: skybldev Date: Fri, 23 Oct 2026 04:11:31 -0400 Subject: [PATCH] feat: add basic types and scanning --- .gitignore | 1 + src/library_scan.rs | 78 +++++++++++++++++++++++++++++++++ src/main.rs | 63 ++++++++++++++++++++++++++- src/types.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 src/library_scan.rs create mode 100644 src/types.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..e17032c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/test/fixtures/ \ No newline at end of file diff --git a/src/library_scan.rs b/src/library_scan.rs new file mode 100644 index 0000000..c614b9d --- /dev/null +++ b/src/library_scan.rs @@ -0,0 +1,78 @@ +use std::error::Error; +use std::fmt; +use std::fs; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; + +use lofty::file; +use lofty::prelude::*; +use lofty::read_from; + +use crate::types::errors::{ScanError, TrackScanError}; +use crate::types::library::ScannedTrack; +use crate::types::library::ScannedTrackMetadata; + +pub fn scan(path: &Path) -> Result, ScanError> { + println!("Scanning library..."); + + if !path.is_dir() { + return Err(ScanError::NotDirectory(path.to_path_buf())); + } + + let entries = fs::read_dir(path).map_err(|source| ScanError::ReadDir { + path: path.to_path_buf(), + source, + })?; + + let mut tracks: Vec = Vec::new(); + + // iterate through entries in directory (depth of one) + for entry in entries { + // validate if entry is even valid (this annoys me) + if let Ok(entry) = entry { + let entry_path = entry.path(); + + // try to read file and come up with metadata + let metadata = || -> Result { + let mut file = + File::open(&entry_path).map_err(|source| TrackScanError::FileOpen { + path: entry_path.clone(), + source, + })?; + + let tagged_file = + read_from(&mut file).map_err(|source| TrackScanError::FileParse { + path: entry_path.clone(), + source, + })?; + + if let Some(tags) = tagged_file.primary_tag() { + Ok(ScannedTrackMetadata { + artist: tags.artist().as_deref().map(|tag| String::from(tag)), + collection: tags.album().as_deref().map(|tag| String::from(tag)), + title: tags.title().as_deref().map(|tag| String::from(tag)), + }) + } else { + Ok(ScannedTrackMetadata { + artist: None, + collection: None, + title: None, + }) + } + }(); + + tracks.push(ScannedTrack { + path: entry_path, + metadata, + }); + } else { + eprintln!( + "there was an error reading some file in {}", + path.to_str().unwrap() + ); + } + } + + Ok(tracks) +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..0bb7e39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,64 @@ -fn main() { +use std::{fs::metadata, path::Path}; + +use lofty::ogg::tag; +use rusqlite::{Connection, Result}; + +mod library_scan; +mod types; + +fn main() -> Result<()> { println!("Hello, world!"); + + let conn = Connection::open_in_memory()?; + + println!("Initializing database..."); + + conn.execute( + "CREATE TABLE artist ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )", + (), + )?; + + conn.execute( + "CREATE TABLE collection ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + )", + (), + )?; + + conn.execute( + "CREATE TABLE track ( + id INTEGER PRIMARY KEY, + artist INTEGER, + collection INTEGER, + title TEXT + )", + (), + )?; + + println!("Database initialized."); + + for track in library_scan::scan(Path::new("./test/fixtures")).expect("oh no") { + println!("-- {}", track.path.into_string().unwrap()); + + match track.metadata { + Ok(metadata) => { + println!("artist: {}", metadata.artist.unwrap_or("unknown".into())); + println!( + "collection: {}", + metadata.collection.unwrap_or("unknown".into()) + ); + println!("title: {}", metadata.title.unwrap_or("unknown".into())); + } + Err(e) => { + eprint!("{e}"); + } + } + println!(""); + } + + Ok(()) } diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..162fa56 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,102 @@ +pub mod library { + use std::path::PathBuf; + + use crate::types::errors; + + #[derive(Debug)] + struct Artist { + id: u32, + name: String, + } + + #[derive(Debug)] + struct Collection { + id: u32, + name: String, + } + + #[derive(Debug)] + struct Track { + id: u32, + artist: Option, + collection: Option, + title: Option, + } + + #[derive(Debug)] + pub struct ScannedTrackMetadata { + pub artist: Option, + pub collection: Option, + pub title: Option, + } + + #[derive(Debug)] + pub struct ScannedTrack { + pub path: PathBuf, + pub metadata: Result, + } +} + +pub mod errors { + use std::{error::Error, fmt, io, path::PathBuf}; + + #[derive(Debug)] + pub enum ScanError { + NotDirectory(PathBuf), + ReadDir { path: PathBuf, source: io::Error }, + } + + impl fmt::Display for ScanError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotDirectory(path) => write!(f, "{} is not a directory", path.display()), + Self::ReadDir { path, source } => { + write!(f, "could not read directory {}: {source}", path.display()) + } + } + } + } + + impl Error for ScanError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::ReadDir { source, .. } => Some(source), + Self::NotDirectory(_) => None, + } + } + } + + #[derive(Debug)] + pub enum TrackScanError { + FileOpen { + path: PathBuf, + source: io::Error, + }, + FileParse { + path: PathBuf, + source: lofty::error::FileParseError, + }, + } + + impl fmt::Display for TrackScanError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::FileOpen { path, source } => { + write!(f, "could not open file {}: {source}", path.display()) + } + Self::FileParse { path, source } => { + write!(f, "could not parse file {}: {source}", path.display()) + } + } + } + } + + impl Error for TrackScanError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::FileOpen { path, source } => Some(source), + Self::FileParse { path, source } => Some(source), + } + } + } +}