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

View file

@ -1,6 +1,6 @@
[package]
name = "photojawn"
version = "0.2.0-pre.1"
version = "0.2.0"
description = "A static site generator for photo albums"
authors = ["Nick Pegg <nick@nickpegg.com>"]
license = "MIT"
@ -8,21 +8,21 @@ repository = "https://github.com/nickpegg/photojawn"
edition = "2024"
[dependencies]
anyhow = "^1.0"
clap = { version = "^4.5", features = ["derive"] }
env_logger = "^0.11.8"
fs_extra = "^1.3.0"
image = "^0.25.6"
indicatif = "^0.17.11"
kamadak-exif = "^0.6.1"
log = "^0.4.27"
pulldown-cmark = "^0.13.0"
rayon = "^1.10.0"
serde = { version = "^1.0", features = ["derive"] }
serde_yml = "^0.0.12"
tera = { version = "^1.20", default-features = false }
thiserror = "^2.0"
time = { version = "^0.3.41", features = ["formatting", "macros", "parsing"] }
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11.8"
fs_extra = "1.3"
image = "0.25.6"
indicatif = "0.17.11"
kamadak-exif = "0.6.1"
log = "0.4.27"
pulldown-cmark = "0.13.0"
rayon = "1.10"
serde = { version = "1.0", features = ["derive"] }
serde_yaml_ng = "0.10.0"
tera = { version = "1.20", default-features = false }
thiserror = "2.0"
time = { version = "0.3.41", features = ["formatting", "macros", "parsing"] }
[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(),
)
})?;
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()))?;
Ok(cfg)
}
@ -56,11 +56,11 @@ mod test {
fn from_yaml() {
// Empty YAML gives full default values
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);
// 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_eq!(cfg.thumbnail_size, (1, 1));
assert_eq!(cfg.view_size, default_cfg.view_size);

View file

@ -19,7 +19,7 @@ pub struct AlbumDir {
impl AlbumDir {
/// 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)
}
@ -79,20 +79,20 @@ impl AlbumDir {
}
}
}
} else if entry_path.is_dir() {
if let Some(dirname) = entry_path.file_name().and_then(|n| n.to_str()) {
if dirname.starts_with("_") {
// Likely a templates or static dir
continue;
} else if dirname == "site" {
// Is a generated site dir, don't descend into it
continue;
} else if dirname == "slides" {
continue;
}
children.push(AlbumDir::from_path(&entry_path, root)?);
} else if entry_path.is_dir()
&& let Some(dirname) = entry_path.file_name().and_then(|n| n.to_str())
{
if dirname.starts_with("_") {
// Likely a templates or static dir
continue;
} else if dirname == "site" {
// Is a generated site dir, don't descend into it
continue;
} else if dirname == "slides" {
continue;
}
children.push(AlbumDir::from_path(&entry_path, root)?);
}
}

View file

@ -55,17 +55,15 @@ impl Image {
/// return "blah.thumb"
fn slide_filename(path: &Path, ext: &str, keep_ext: bool) -> anyhow::Result<String> {
let mut new_ext: OsString = ext.into();
if keep_ext {
if let Some(e) = path.extension() {
new_ext = OsString::from(
ext.to_string()
+ "."
+ e.to_str().ok_or(anyhow!(
"Image {} extension is not valid UTF-8",
path.display()
))?,
)
}
if keep_ext && let Some(e) = path.extension() {
new_ext = OsString::from(
ext.to_string()
+ "."
+ e.to_str().ok_or(anyhow!(
"Image {} extension is not valid UTF-8",
path.display()
))?,
)
}
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 std::ffi::OsStr;
use std::fs::{rename, File};
use std::fs::{File, rename};
use std::io::BufReader;
use std::path::{Path, PathBuf};
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()? {
let entry = entry?;
if !entry.path().is_file() {
continue;
}
// Only bother with image files, because those are the only hope for EXIF
let is_image: bool = ImageReader::open(entry.path())?
.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 mut bufreader = BufReader::new(file);
// TODO: Return a better error if EXIF is not supported
let exif = exif::Reader::new()
.read_from_container(&mut bufreader)
.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(),
}
}
// TODO: return some error
_ => todo!(),
_ => return Err(OrganizeError::ExifNoDateTime(path).into()),
};
Ok(dt)
@ -189,7 +191,7 @@ mod tests {
}
#[test]
fn basic_renames() {
fn test_basic_renames() {
init();
let tmp_album_dir = make_test_album();
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]
/// 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.
fn rerename() {
fn test_rerename() {
let tmp_album_dir = make_test_album();
let dir = tmp_album_dir.join("with_description");
reorganize(&dir, false).unwrap();