Compare commits

..

5 commits

7 changed files with 555 additions and 560 deletions

994
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "photojawn" name = "photojawn"
version = "0.2.0" version = "0.2.0-pre.1"
description = "A static site generator for photo albums" description = "A static site generator for photo albums"
authors = ["Nick Pegg <nick@nickpegg.com>"] authors = ["Nick Pegg <nick@nickpegg.com>"]
license = "MIT" license = "MIT"
@ -8,21 +8,21 @@ repository = "https://github.com/nickpegg/photojawn"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "^1.0"
clap = { version = "4.5", features = ["derive"] } clap = { version = "^4.5", features = ["derive"] }
env_logger = "0.11.8" env_logger = "^0.11.8"
fs_extra = "1.3" fs_extra = "^1.3.0"
image = "0.25.6" image = "^0.25.6"
indicatif = "0.17.11" indicatif = "^0.17.11"
kamadak-exif = "0.6.1" kamadak-exif = "^0.6.1"
log = "0.4.27" log = "^0.4.27"
pulldown-cmark = "0.13.0" pulldown-cmark = "^0.13.0"
rayon = "1.10" rayon = "^1.10.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "^1.0", features = ["derive"] }
serde_yaml_ng = "0.10.0" serde_yml = "^0.0.12"
tera = { version = "1.20", default-features = false } tera = { version = "^1.20", default-features = false }
thiserror = "2.0" thiserror = "^2.0"
time = { version = "0.3.41", features = ["formatting", "macros", "parsing"] } time = { version = "^0.3.41", features = ["formatting", "macros", "parsing"] }
[dev-dependencies] [dev-dependencies]
mktemp = "0.5.1" mktemp = "^0.5.1"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View file

@ -23,7 +23,7 @@ impl Config {
config_path.display(), config_path.display(),
) )
})?; })?;
let cfg = serde_yaml_ng::from_slice(&content) let cfg = serde_yml::from_slice(&content)
.with_context(|| format!("Failed to parse config from {}", config_path.display()))?; .with_context(|| format!("Failed to parse config from {}", config_path.display()))?;
Ok(cfg) Ok(cfg)
} }
@ -56,11 +56,11 @@ mod test {
fn from_yaml() { fn from_yaml() {
// Empty YAML gives full default values // Empty YAML gives full default values
let default_cfg = Config::default(); let default_cfg = Config::default();
let cfg: Config = serde_yaml_ng::from_str("").unwrap(); let cfg: Config = serde_yml::from_str("").unwrap();
assert_eq!(cfg, default_cfg); assert_eq!(cfg, default_cfg);
// Default values for any unspecified fields // Default values for any unspecified fields
let cfg: Config = serde_yaml_ng::from_str("thumbnail_size: [1, 1]").unwrap(); let cfg: Config = serde_yml::from_str("thumbnail_size: [1, 1]").unwrap();
assert_ne!(cfg, default_cfg); assert_ne!(cfg, default_cfg);
assert_eq!(cfg.thumbnail_size, (1, 1)); assert_eq!(cfg.thumbnail_size, (1, 1));
assert_eq!(cfg.view_size, default_cfg.view_size); assert_eq!(cfg.view_size, default_cfg.view_size);

View file

@ -19,7 +19,7 @@ pub struct AlbumDir {
impl AlbumDir { impl AlbumDir {
/// Returns an iterator over all images in the album and subalbums /// Returns an iterator over all images in the album and subalbums
pub fn iter_all_images(&self) -> AlbumImageIter<'_> { pub fn iter_all_images(&self) -> AlbumImageIter {
AlbumImageIter::new(self) AlbumImageIter::new(self)
} }
@ -79,9 +79,8 @@ impl AlbumDir {
} }
} }
} }
} else if entry_path.is_dir() } else if entry_path.is_dir() {
&& let Some(dirname) = entry_path.file_name().and_then(|n| n.to_str()) if let Some(dirname) = entry_path.file_name().and_then(|n| n.to_str()) {
{
if dirname.starts_with("_") { if dirname.starts_with("_") {
// Likely a templates or static dir // Likely a templates or static dir
continue; continue;
@ -95,6 +94,7 @@ impl AlbumDir {
children.push(AlbumDir::from_path(&entry_path, root)?); children.push(AlbumDir::from_path(&entry_path, root)?);
} }
} }
}
children.sort_by_key(|c| c.path.clone()); children.sort_by_key(|c| c.path.clone());
images.sort_by_key(|i| i.path.clone()); images.sort_by_key(|i| i.path.clone());

View file

@ -55,7 +55,8 @@ impl Image {
/// return "blah.thumb" /// return "blah.thumb"
fn slide_filename(path: &Path, ext: &str, keep_ext: bool) -> anyhow::Result<String> { fn slide_filename(path: &Path, ext: &str, keep_ext: bool) -> anyhow::Result<String> {
let mut new_ext: OsString = ext.into(); let mut new_ext: OsString = ext.into();
if keep_ext && let Some(e) = path.extension() { if keep_ext {
if let Some(e) = path.extension() {
new_ext = OsString::from( new_ext = OsString::from(
ext.to_string() ext.to_string()
+ "." + "."
@ -65,6 +66,7 @@ impl Image {
))?, ))?,
) )
} }
}
let new_path = path.with_extension(new_ext); let new_path = path.with_extension(new_ext);
let new_name = new_path let new_name = new_path

View file

@ -1,7 +1,7 @@
use anyhow::{Context, anyhow}; use anyhow::{anyhow, Context};
use image::ImageReader; use image::ImageReader;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs::{File, rename}; use std::fs::{rename, File};
use std::io::BufReader; use std::io::BufReader;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::from_utf8; use std::str::from_utf8;
@ -51,10 +51,6 @@ fn get_renames(dir: &Path) -> anyhow::Result<Vec<(PathBuf, PathBuf)>> {
for entry in dir.read_dir()? { for entry in dir.read_dir()? {
let entry = entry?; let entry = entry?;
if !entry.path().is_file() {
continue;
}
// Only bother with image files, because those are the only hope for EXIF // Only bother with image files, because those are the only hope for EXIF
let is_image: bool = ImageReader::open(entry.path())? let is_image: bool = ImageReader::open(entry.path())?
.with_guessed_format()? .with_guessed_format()?
@ -138,6 +134,7 @@ fn get_exif_datetime(path: PathBuf) -> anyhow::Result<UtcDateTime> {
let file = File::open(&path).with_context(|| format!("Couldn't open {}", path.display()))?; let file = File::open(&path).with_context(|| format!("Couldn't open {}", path.display()))?;
let mut bufreader = BufReader::new(file); let mut bufreader = BufReader::new(file);
// TODO: Return a better error if EXIF is not supported
let exif = exif::Reader::new() let exif = exif::Reader::new()
.read_from_container(&mut bufreader) .read_from_container(&mut bufreader)
.with_context(|| format!("Couldn't read EXIF data from {}", path.display()))?; .with_context(|| format!("Couldn't read EXIF data from {}", path.display()))?;
@ -155,7 +152,8 @@ fn get_exif_datetime(path: PathBuf) -> anyhow::Result<UtcDateTime> {
Err(_) => PrimitiveDateTime::parse(s, format_without_offset)?.as_utc(), Err(_) => PrimitiveDateTime::parse(s, format_without_offset)?.as_utc(),
} }
} }
_ => return Err(OrganizeError::ExifNoDateTime(path).into()), // TODO: return some error
_ => todo!(),
}; };
Ok(dt) Ok(dt)
@ -191,7 +189,7 @@ mod tests {
} }
#[test] #[test]
fn test_basic_renames() { fn basic_renames() {
init(); init();
let tmp_album_dir = make_test_album(); let tmp_album_dir = make_test_album();
let dir = tmp_album_dir.join("with_description"); let dir = tmp_album_dir.join("with_description");
@ -212,27 +210,10 @@ mod tests {
); );
} }
#[test]
/// get_renames() should ignore other stuff in the directory
fn test_other_junk() {
init();
let tmp_album_dir = make_test_album();
let renames = get_renames(&tmp_album_dir).unwrap();
// No mountain.jpg since it doesn't have EXIF data
assert_eq!(
renames,
vec![(
tmp_album_dir.join("moon.jpg"),
tmp_album_dir.join("19700101_133700_moon.jpg")
)]
);
}
#[test] #[test]
/// The rename function will prepend date and time to the original filenames. If we do it a /// The rename function will prepend date and time to the original filenames. If we do it a
/// second time, it should be a no-op instead of continuing to prepend date and time. /// second time, it should be a no-op instead of continuing to prepend date and time.
fn test_rerename() { fn rerename() {
let tmp_album_dir = make_test_album(); let tmp_album_dir = make_test_album();
let dir = tmp_album_dir.join("with_description"); let dir = tmp_album_dir.join("with_description");
reorganize(&dir, false).unwrap(); reorganize(&dir, false).unwrap();