feat: add basic types and scanning
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
/test/fixtures/
|
||||||
78
src/library_scan.rs
Normal file
78
src/library_scan.rs
Normal file
@@ -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<Vec<ScannedTrack>, 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<ScannedTrack> = 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<ScannedTrackMetadata, TrackScanError> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
63
src/main.rs
63
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!");
|
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(())
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/types.rs
Normal file
102
src/types.rs
Normal file
@@ -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<Artist>,
|
||||||
|
collection: Option<Collection>,
|
||||||
|
title: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScannedTrackMetadata {
|
||||||
|
pub artist: Option<String>,
|
||||||
|
pub collection: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScannedTrack {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub metadata: Result<ScannedTrackMetadata, errors::TrackScanError>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user