Skip to content

Commit

Permalink
feat: migrate from custom DirectoryScan implementation to fs-more, …
Browse files Browse the repository at this point in the history
…make euphony subcrates inherit from the workspace
  • Loading branch information
simongoricar committed Mar 17, 2024
1 parent a962658 commit b9bdf59
Show file tree
Hide file tree
Showing 12 changed files with 615 additions and 618 deletions.
942 changes: 468 additions & 474 deletions Cargo.lock

Large diffs are not rendered by default.

43 changes: 40 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ rust-version = "1.70.0"
[workspace]
members = ["euphony_library", "euphony_configuration"]

[dependencies]
# Workspace dependencies
[workspace.dependencies]
# Workspace crates
euphony_library = { path = "./euphony_library" }
euphony_configuration = { path = "./euphony_configuration" }

# Error handling
miette = { version = "5.10.0", features = ["fancy"]}
miette = { version = "5.10.0", features = ["fancy"] }
thiserror = "1.0.51"

# Serialization and deserialization
Expand All @@ -46,3 +46,40 @@ linked-hash-map = "0.5.6"
parking_lot = "0.12.1"
textwrap = "0.16.0"
chrono = "0.4.26"

fs-more = { git = "https://github.com/simongoricar/fs-more.git", rev = "c3c2e421ca5e6dc09ea7a1af8e34c809dd0e6910", features = ["fs-err", "miette"] }


[dependencies]
# Workspace dependencies
euphony_library = { workspace = true }
euphony_configuration = { workspace = true }

# Error handling
miette = { workspace = true }
thiserror = { workspace = true }

# Serialization and deserialization
serde = { workspace = true }
serde_json = { workspace = true }
toml = { workspace = true }

# Other dependencies
clap = { workspace = true }
pathdiff = { workspace = true }
dunce = { workspace = true }
state = { workspace = true }
ratatui = { workspace = true }
crossterm = { workspace = true }
tokio = { workspace = true }
rand = { workspace = true }
ansi-to-tui = { workspace = true }
ctrlc = { workspace = true }
crossbeam = { workspace = true }
strip-ansi-escapes = { workspace = true }
oneshot = { workspace = true }
closure = { workspace = true }
linked-hash-map = { workspace = true }
parking_lot = { workspace = true }
textwrap = { workspace = true }
chrono = { workspace = true }
11 changes: 6 additions & 5 deletions euphony_configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4.26"
miette = "5.10.0"
serde = { version = "1.0.136", features = ["derive"] }
toml = "0.8.8"
dunce = "1.0.4"
chrono = { workspace = true }
miette = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }
dunce = { workspace = true }
thiserror = { workspace = true }
50 changes: 34 additions & 16 deletions euphony_configuration/src/album.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
use std::fs;
use std::path::PathBuf;

use miette::{miette, Context, IntoDiagnostic, Result};
use serde::Deserialize;

// This file is not required to exist in each album directory, but the user may create it
// to influence various configuration values per-album.
use crate::error::ConfigurationError;


/// The file name for the album overrides (see [`AlbumConfiguration`]).
///
/// This file is not required to exist in each album directory,
/// but the user may create it to influence
/// various configuration values per-album.
pub const ALBUM_OVERRIDE_FILE_NAME: &str = ".album.override.euphony";

/// Per-album options for euphony.

/// Album-specific options for `euphony`.
///
/// Usage: create a `.album.override.euphony` file in an album directory.
/// You can look at the structure below or copy a template from
/// `data/.album.override.TEMPLATE.euphony`.
#[derive(Deserialize, Clone, Debug, Default)]
pub struct AlbumConfiguration {
/// Scanning options.
Expand All @@ -17,35 +27,43 @@ pub struct AlbumConfiguration {
}

impl AlbumConfiguration {
/// Given a directory path, load its `.album.override.euphony` file (if it exists).
/// Given a `directory_path`, load its `.album.override.euphony` file (if it exists).
///
/// NOTE: Any optional values will be filled with defaults.
/// NOTE: Any optional values will be filled with defaults
/// (e.g. `scan.depth` will default to `0`).
pub fn load<P: Into<PathBuf>>(
directory_path: P,
) -> Result<AlbumConfiguration> {
let file_path = directory_path.into().join(ALBUM_OVERRIDE_FILE_NAME);
) -> Result<AlbumConfiguration, ConfigurationError> {
let file_path: PathBuf =
directory_path.into().join(ALBUM_OVERRIDE_FILE_NAME);

// If no override exists, just return the defaults.
if !file_path.is_file() {
return Ok(AlbumConfiguration::default());
}

// It it exists, load the configuration and fill any empty optional fields with defaults.
let album_override_string = fs::read_to_string(&file_path)
.into_diagnostic()
.wrap_err_with(|| miette!("Could not read file into string."))?;
let album_override_string =
fs::read_to_string(&file_path).map_err(|error| {
ConfigurationError::FileLoadError {
file_path: file_path.clone(),
error,
}
})?;

let album_override: AlbumConfiguration =
toml::from_str(&album_override_string)
.into_diagnostic()
.wrap_err_with(|| {
miette!("Could not load TOML contents of {:?}.", file_path)
})?;
toml::from_str(&album_override_string).map_err(|error| {
ConfigurationError::FileFormatError {
file_path,
error: Box::new(error),
}
})?;

Ok(album_override)
}
}


#[derive(Deserialize, Clone, Debug, Default)]
pub struct AlbumScanConfiguration {
/// Maximum album scanning depth. Zero (the default) means no subdirectories are scanned.
Expand Down
23 changes: 23 additions & 0 deletions euphony_configuration/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::{io, path::PathBuf};

use miette::Diagnostic;
use thiserror::Error;

#[derive(Error, Debug, Diagnostic)]
pub enum ConfigurationError {
#[error("Failed to load configuration file.")]
FileLoadError {
file_path: PathBuf,
error: io::Error,
},

#[error(
"Failed to parse configuration file \
\"{file_path}\" as TOML: {error}."
)]
FileFormatError {
file_path: PathBuf,
error: Box<toml::de::Error>,
},
// TODO
}
77 changes: 2 additions & 75 deletions euphony_configuration/src/filesystem.rs
Original file line number Diff line number Diff line change
@@ -1,79 +1,6 @@
use std::fs;
use std::fs::DirEntry;
use std::path::{Path, PathBuf};
use std::path::Path;

use miette::{miette, Context, IntoDiagnostic, Result};

/// A directory scan containing `files` and `directories`.
///
/// Depending on the initialization, the scan can contain just direct children (`scan_depth == 0`)
/// or files and directories deeper in the tree (`scan_depth >= 1`).
pub struct DirectoryScan {
pub files: Vec<DirEntry>,
pub directories: Vec<DirEntry>,
}

impl DirectoryScan {
/// Scan the given directory.
///
/// If the `scan_depth` parameter equals `0`, only the immediate files and directories will be listed.
/// Any non-zero number will scan up to that subdirectory depth (e.g. `1` will result in the scan
/// containing direct files and all files directly in the directories one level down).
pub fn from_directory_path<P: AsRef<Path>>(
directory_path: P,
directory_scan_depth: u16,
) -> Result<Self> {
let directory_path = directory_path.as_ref();

let mut file_list: Vec<DirEntry> = Vec::new();
let mut directory_list: Vec<DirEntry> = Vec::new();

// The scanning works by maintaining a queue of directories to search

// Meaning: Vec<(directory_to_search, directory's depth)>
let mut search_queue: Vec<(PathBuf, u16)> = Vec::new();
search_queue.push((directory_path.to_path_buf(), 0));

while let Some((directory_to_scan, directory_depth)) = search_queue.pop()
{
let directory_iterator = fs::read_dir(directory_to_scan)
.into_diagnostic()
.wrap_err_with(|| miette!("Could not read directory."))?;

// Split the directory iterator elements into files and directories.
for entry in directory_iterator {
let entry = entry.into_diagnostic().wrap_err_with(|| {
miette!("Could not get directory entry.")
})?;

let entry_type = entry
.file_type()
.into_diagnostic()
.wrap_err_with(|| miette!("Could not get file type."))?;

if entry_type.is_file() {
file_list.push(entry);
} else if entry_type.is_dir() {
// If we can go deeper, queue the directory we found for further search.
if directory_depth < directory_scan_depth {
search_queue.push((entry.path(), directory_depth + 1));
}

// But always store the directories we have found so far.
directory_list.push(entry);
} else {
// FIXME: Implement a solution for symlinks (which are currently simply ignored).
continue;
}
}
}

Ok(Self {
files: file_list,
directories: directory_list,
})
}
}
use miette::{miette, Result};

/// Get a file's extension (or an empty string if none).
/// Returns `Err` if the extension is not valid UTF-8.
Expand Down
1 change: 1 addition & 0 deletions euphony_configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use filesystem::*;
pub use structure::*;

mod album;
pub mod error;
mod filesystem;
mod structure;
mod traits;
Expand Down
13 changes: 7 additions & 6 deletions euphony_library/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ edition = "2021"
[dependencies]
euphony_configuration = { path = "../euphony_configuration" }

miette = "5.10.0"
thiserror = "1.0.51"
parking_lot = "0.12.1"
pathdiff = "0.2.1"
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.81"
miette = { workspace = true }
thiserror = { workspace = true }
parking_lot = { workspace = true }
pathdiff = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
fs-more = { workspace = true }
File renamed without changes.
29 changes: 13 additions & 16 deletions euphony_library/src/view/album.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use std::path::PathBuf;
use std::sync::Arc;

use euphony_configuration::library::LibraryConfiguration;
use euphony_configuration::{AlbumConfiguration, Configuration, DirectoryScan};
use euphony_configuration::{AlbumConfiguration, Configuration};
use fs_more::directory::DirectoryScan;
use miette::{miette, Context, Result};
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};

Expand Down Expand Up @@ -119,19 +120,16 @@ impl<'config> AlbumView<'config> {
pub fn album_validation_files(&self) -> Result<Vec<PathBuf>> {
let album_scan = self.scan_album_directory()?;

Ok(album_scan
.files
.into_iter()
.map(|item| item.path())
.collect())
Ok(album_scan.files.into_iter().collect())
}

/// Perform a directory scan of the album directory, respecting the depth configuration
/// for the particular album.
fn scan_album_directory(&self) -> Result<DirectoryScan> {
DirectoryScan::from_directory_path(
DirectoryScan::scan_with_options(
self.album_directory_in_source_library(),
self.configuration.scan.depth,
Some(self.configuration.scan.depth as usize),
false,
)
.wrap_err_with(|| {
miette!(
Expand Down Expand Up @@ -267,22 +265,21 @@ impl<'config> AlbumSourceFileList<'config> {
let album_directory =
locked_album_view.album_directory_in_source_library();

let album_scan = DirectoryScan::from_directory_path(
let album_scan = DirectoryScan::scan_with_options(
&album_directory,
locked_album_view.configuration.scan.depth,
Some(locked_album_view.configuration.scan.depth as usize),
true,
)?;

let mut audio_files: Vec<PathBuf> = Vec::new();
let mut data_files: Vec<PathBuf> = Vec::new();

for file in album_scan.files {
let file_absolute_path = file.path();
for file_path in album_scan.files {
// (relative to album source directory)
let file_relative_path =
pathdiff::diff_paths(file_absolute_path, &album_directory)
.ok_or_else(|| {
miette!("Could not generate relative path.")
})?;
pathdiff::diff_paths(file_path, &album_directory).ok_or_else(
|| miette!("Could not generate relative path."),
)?;

if transcoding_configuration
.is_path_audio_file_by_extension(&file_relative_path)?
Expand Down
15 changes: 6 additions & 9 deletions euphony_library/src/view/artist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

use euphony_configuration::DirectoryScan;
use fs_more::directory::DirectoryScan;
use miette::{miette, Context, Result};
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};

Expand Down Expand Up @@ -124,8 +124,8 @@ impl<'config> ArtistView<'config> {
for directory in artist_directory_scan.directories {
let album_directory_name = directory
.file_name()
.to_str()
.ok_or_else(|| miette!("Could not parse directory file name."))?
.to_string_lossy()
.to_string();

album_map.insert(
Expand Down Expand Up @@ -177,11 +177,7 @@ impl<'config> ArtistView<'config> {
pub fn artist_directory_validation_files(&self) -> Result<Vec<PathBuf>> {
let artist_directory_scan = self.scan_artist_directory()?;

Ok(artist_directory_scan
.files
.into_iter()
.map(|item| item.path())
.collect())
Ok(artist_directory_scan.files.into_iter().collect())
}

/*
Expand All @@ -190,9 +186,10 @@ impl<'config> ArtistView<'config> {

/// Perform a zero-depth directory scan of the artist directory.
fn scan_artist_directory(&self) -> Result<DirectoryScan> {
DirectoryScan::from_directory_path(
DirectoryScan::scan_with_options(
self.artist_directory_in_source_library(),
0,
Some(0),
true,
)
.wrap_err_with(|| {
miette!(
Expand Down
Loading

0 comments on commit b9bdf59

Please sign in to comment.