wip
This commit is contained in:
parent
aa57c0d092
commit
16699d86a8
8 changed files with 180 additions and 3 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,6 +7,6 @@ dist
|
||||||
/target
|
/target
|
||||||
|
|
||||||
# Project specific files
|
# Project specific files
|
||||||
test_album*
|
/test_album*
|
||||||
DESIGN.md
|
DESIGN.md
|
||||||
TODO.md
|
TODO.md
|
||||||
|
|
69
Cargo.lock
generated
69
Cargo.lock
generated
|
@ -349,6 +349,15 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
@ -698,6 +707,15 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kamadak-exif"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837"
|
||||||
|
dependencies = [
|
||||||
|
"mutate_once",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
@ -792,6 +810,12 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mutate_once"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "new_debug_unreachable"
|
name = "new_debug_unreachable"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
@ -824,6 +848,12 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -937,6 +967,7 @@ dependencies = [
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
"image",
|
"image",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
|
"kamadak-exif",
|
||||||
"log",
|
"log",
|
||||||
"mktemp",
|
"mktemp",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
|
@ -945,6 +976,7 @@ dependencies = [
|
||||||
"serde_yml",
|
"serde_yml",
|
||||||
"tera",
|
"tera",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -981,6 +1013,12 @@ dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
|
@ -1420,6 +1458,37 @@ dependencies = [
|
||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.22"
|
version = "0.8.22"
|
||||||
|
|
|
@ -14,6 +14,7 @@ env_logger = "^0.11.8"
|
||||||
fs_extra = "^1.3.0"
|
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"
|
||||||
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.0"
|
||||||
|
@ -21,6 +22,7 @@ serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_yml = "^0.0.12"
|
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"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mktemp = "^0.5.1"
|
mktemp = "^0.5.1"
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
@ -4,7 +4,7 @@ mod image;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::generate::image::Image;
|
use crate::generate::image::Image;
|
||||||
use album_dir::AlbumDir;
|
use album_dir::AlbumDir;
|
||||||
use anyhow::{Context, anyhow};
|
use anyhow::{anyhow, Context};
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
@ -98,7 +98,8 @@ fn generate_images(config: &Config, album: &AlbumDir, full: bool) -> anyhow::Res
|
||||||
fs::hard_link(&img.path, &full_size_path)
|
fs::hard_link(&img.path, &full_size_path)
|
||||||
.with_context(|| format!("Error creating hard link at {}", full_size_path.display()))?;
|
.with_context(|| format!("Error creating hard link at {}", full_size_path.display()))?;
|
||||||
|
|
||||||
let orig_image = ::image::open(&img.path)?;
|
let orig_image = ::image::open(&img.path)
|
||||||
|
.with_context(|| format!("Failed to read image {}", &img.path.display()))?;
|
||||||
let thumb_path = output_path.join(&img.thumb_path);
|
let thumb_path = output_path.join(&img.thumb_path);
|
||||||
log::info!(
|
log::info!(
|
||||||
"Resizing {} -> {}",
|
"Resizing {} -> {}",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod generate;
|
pub mod generate;
|
||||||
|
pub mod reorganize;
|
||||||
pub mod skel;
|
pub mod skel;
|
||||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -1,5 +1,6 @@
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use photojawn::generate::generate;
|
use photojawn::generate::generate;
|
||||||
|
use photojawn::reorganize::reorganize;
|
||||||
use photojawn::skel::make_skeleton;
|
use photojawn::skel::make_skeleton;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
@ -18,6 +19,9 @@ fn main() -> anyhow::Result<()> {
|
||||||
let path = generate(&album_path.to_path_buf(), full)?;
|
let path = generate(&album_path.to_path_buf(), full)?;
|
||||||
println!("Album site generated in {}", path.display());
|
println!("Album site generated in {}", path.display());
|
||||||
}
|
}
|
||||||
|
Commands::Reorganize { path, dry_run } => {
|
||||||
|
reorganize(Path::new(&path), dry_run)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -44,4 +48,12 @@ enum Commands {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
full: bool,
|
full: bool,
|
||||||
},
|
},
|
||||||
|
/// Reorganize photos in an album by date
|
||||||
|
Reorganize {
|
||||||
|
#[arg()]
|
||||||
|
path: String,
|
||||||
|
/// Don't actually reorganize, just say what renames would happen
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
92
src/reorganize.rs
Normal file
92
src/reorganize.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::from_utf8;
|
||||||
|
use thiserror::Error;
|
||||||
|
use time::macros::format_description;
|
||||||
|
use time::{OffsetDateTime, PrimitiveDateTime};
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum OrganizeError {
|
||||||
|
#[error("These files are not supported, unable to parse EXIF data: {0:?}")]
|
||||||
|
ExifNotSupported(Vec<PathBuf>),
|
||||||
|
#[error("File {0} is missing an EXIF DateTimeOriginal field")]
|
||||||
|
ExifNoDateTime(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reorganize(dir: &Path, dry_run: bool) -> anyhow::Result<()> {
|
||||||
|
// Run through all the images and figure out new names for them
|
||||||
|
for entry in dir.read_dir()? {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.path().is_file() {
|
||||||
|
let dt = get_exif_datetime(entry.path())?;
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either do the renames, or if dry-run print what the names would be
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to figure out the datetime that t
|
||||||
|
fn get_exif_datetime(path: PathBuf) -> anyhow::Result<()> {
|
||||||
|
let DT_WITH_OFFSET = format_description!(
|
||||||
|
"[year]:[month]:[day] [hour]:[minute]:[second][offset_hour]:[offset_minute]"
|
||||||
|
);
|
||||||
|
let DT_WITHOUT_OFFSET =
|
||||||
|
format_description!(version = 2, "[year]:[month]:[day] [hour]:[minute]:[second]");
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
let field = exif
|
||||||
|
.get_field(exif::Tag::DateTimeOriginal, exif::In::PRIMARY)
|
||||||
|
.ok_or(OrganizeError::ExifNoDateTime(path.clone()))?;
|
||||||
|
|
||||||
|
let dt = match &field.value {
|
||||||
|
exif::Value::Ascii(v) => {
|
||||||
|
let s = from_utf8(&v[0])?;
|
||||||
|
log::debug!("Date string: {s}");
|
||||||
|
log::debug!("{DT_WITH_OFFSET:?}");
|
||||||
|
match OffsetDateTime::parse(&s, DT_WITH_OFFSET) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => {
|
||||||
|
log::debug!("Unable to parse {s} with offset");
|
||||||
|
PrimitiveDateTime::parse(&s, DT_WITHOUT_OFFSET)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
};
|
||||||
|
println!("{dt:?}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn init() {
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Make sure we can get the datetime from one of our test photos
|
||||||
|
fn basic_datetime_read() {
|
||||||
|
init();
|
||||||
|
let dt = get_exif_datetime("resources/test_album/moon.jpg".into()).unwrap();
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exif_datetime_missing() {
|
||||||
|
init();
|
||||||
|
let result = get_exif_datetime("resources/test_album/mountains.jpg".into());
|
||||||
|
assert!(result.is_err());
|
||||||
|
//result.unwrap();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue