Compare commits

..

7 commits

Author SHA1 Message Date
35cb7949fd fix warnings, fmt
Some checks failed
Rust / build (push) Has been cancelled
2025-09-25 15:13:16 -07:00
39d449889d serde_yml -> serde_yaml_ng
Some checks are pending
Rust / build (push) Waiting to run
2025-09-25 15:04:59 -07:00
9ae778bb79 bump version to 0.2.0 and update deps
Some checks failed
Rust / build (push) Has been cancelled
2025-05-19 10:22:24 -07:00
5ff3338b30 Fix dep spec, no need for caret
Some checks are pending
Rust / build (push) Waiting to run
2025-05-18 08:52:26 -07:00
b1d66d7e9f bump version
Some checks are pending
Rust / build (push) Waiting to run
2025-05-18 08:47:59 -07:00
aba9fa4025
Reorganize command (#4)
Some checks are pending
Rust / build (push) Waiting to run
Adds a command to reorganize a folder of photos, renaming them so that
they contain date and time so that they're sorted by that.

This also renames files associated with the photos, like the
descriptions, like IMG_1234.jpg with IMG_1234.md
2025-05-18 08:46:41 -07:00
37581ee6a0 fix .gitignore
Some checks failed
Rust / build (push) Has been cancelled
2025-05-11 14:44:51 -07:00
7 changed files with 103 additions and 92 deletions

74
Cargo.lock generated
View file

@ -145,9 +145,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.0" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]] [[package]]
name = "bitstream-io" name = "bitstream-io"
@ -200,9 +200,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.21" version = "1.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -227,9 +227,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.37" version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -237,9 +237,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.37" version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -481,9 +481,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@ -520,7 +520,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.1",
"ignore", "ignore",
"walkdir", "walkdir",
] ]
@ -537,9 +537,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]] [[package]]
name = "heck" name = "heck"
@ -659,9 +659,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.12" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd" checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
dependencies = [ dependencies = [
"jiff-static", "jiff-static",
"log", "log",
@ -672,9 +672,9 @@ dependencies = [
[[package]] [[package]]
name = "jiff-static" name = "jiff-static"
version = "0.2.12" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300" checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -687,7 +687,7 @@ version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.3",
"libc", "libc",
] ]
@ -744,16 +744,6 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "libyml"
version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980"
dependencies = [
"anyhow",
"version_check",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.27"
@ -959,7 +949,7 @@ dependencies = [
[[package]] [[package]]
name = "photojawn" name = "photojawn"
version = "0.2.0-pre.1" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -973,7 +963,7 @@ dependencies = [
"pulldown-cmark", "pulldown-cmark",
"rayon", "rayon",
"serde", "serde",
"serde_yml", "serde_yaml_ng",
"tera", "tera",
"thiserror 2.0.12", "thiserror 2.0.12",
"time", "time",
@ -1062,7 +1052,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.1",
"getopts", "getopts",
"memchr", "memchr",
"pulldown-cmark-escape", "pulldown-cmark-escape",
@ -1303,18 +1293,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "serde_yml" name = "serde_yaml_ng"
version = "0.0.12" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa",
"libyml",
"memchr",
"ryu", "ryu",
"serde", "serde",
"version_check", "unsafe-libyaml",
] ]
[[package]] [[package]]
@ -1609,6 +1597,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
@ -1830,9 +1824,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.9" version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -1843,7 +1837,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.1",
] ]
[[package]] [[package]]

View file

@ -1,6 +1,6 @@
[package] [package]
name = "photojawn" name = "photojawn"
version = "0.2.0-pre.1" version = "0.2.0"
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.0" fs_extra = "1.3"
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.0" rayon = "1.10"
serde = { version = "^1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_yml = "^0.0.12" serde_yaml_ng = "0.10.0"
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.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -23,7 +23,7 @@ impl Config {
config_path.display(), config_path.display(),
) )
})?; })?;
let cfg = serde_yml::from_slice(&content) let cfg = serde_yaml_ng::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_yml::from_str("").unwrap(); let cfg: Config = serde_yaml_ng::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_yml::from_str("thumbnail_size: [1, 1]").unwrap(); let cfg: Config = serde_yaml_ng::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,20 +79,20 @@ impl AlbumDir {
} }
} }
} }
} else if entry_path.is_dir() { } else if entry_path.is_dir()
if let Some(dirname) = entry_path.file_name().and_then(|n| n.to_str()) { && let Some(dirname) = entry_path.file_name().and_then(|n| n.to_str())
if dirname.starts_with("_") { {
// Likely a templates or static dir if dirname.starts_with("_") {
continue; // Likely a templates or static dir
} else if dirname == "site" { continue;
// Is a generated site dir, don't descend into it } else if dirname == "site" {
continue; // Is a generated site dir, don't descend into it
} else if dirname == "slides" { continue;
continue; } else if dirname == "slides" {
} continue;
children.push(AlbumDir::from_path(&entry_path, root)?);
} }
children.push(AlbumDir::from_path(&entry_path, root)?);
} }
} }

View file

@ -55,17 +55,15 @@ 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 { if keep_ext && let Some(e) = path.extension() {
if let Some(e) = path.extension() { new_ext = OsString::from(
new_ext = OsString::from( ext.to_string()
ext.to_string() + "."
+ "." + e.to_str().ok_or(anyhow!(
+ e.to_str().ok_or(anyhow!( "Image {} extension is not valid UTF-8",
"Image {} extension is not valid UTF-8", path.display()
path.display() ))?,
))?, )
)
}
} }
let new_path = path.with_extension(new_ext); let new_path = path.with_extension(new_ext);

View file

@ -1,7 +1,7 @@
use anyhow::{anyhow, Context}; use anyhow::{Context, anyhow};
use image::ImageReader; use image::ImageReader;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs::{rename, File}; use std::fs::{File, rename};
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,6 +51,10 @@ 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()?
@ -134,7 +138,6 @@ 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()))?;
@ -152,8 +155,7 @@ 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(),
} }
} }
// TODO: return some error _ => return Err(OrganizeError::ExifNoDateTime(path).into()),
_ => todo!(),
}; };
Ok(dt) Ok(dt)
@ -189,7 +191,7 @@ mod tests {
} }
#[test] #[test]
fn basic_renames() { fn test_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");
@ -210,10 +212,27 @@ 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 rerename() { fn test_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();