Skip to main content

Deterministic, verifiable naming for photo and video archives

Project description

ChronoCatalog

CI

Deterministic, verifiable naming for photo and video archives.

The name is ChronoCatalog in prose; the command, package, and all identifiers are chronocatalog, and it is never two words.

chronocatalog names every file in an archive after what it is and when it happened:

20260703_150727_9b677b64.nef
└──────┬──────┘ └──┬───┘
  capture time    hash

The name encodes the capture time (local wall clock, from camera metadata via ExifTool) and a slice of the file's content hash. Because the name is derived purely from the file itself, it is reproducible: the archive can be re-verified at any time, corruption can be told apart from legitimate edits, and duplicates identify themselves.

RAW files travel with their families — XMP/PP3 sidecars, editor derivatives, even sidecars kept in separate subdirectories — which always share the master's name prefix and are renamed together, atomically.

Usage

import

Copy a memory card into the archive, named on arrival:

$ chronocatalog import /Volumes/CARD --config archive.toml
  /Volumes/CARD/DSC_0001.NEF  ->  /archive/Photos/2026/2026-07/20260701_100000_9b677b64.nef
  /Volumes/CARD/DSC_0001.xmp  ->  /archive/Photos/2026/2026-07/20260701_100000_9b677b64.xmp

dry run: 1 group(s) would be imported; pass --apply to copy

Masters travel with their sidecars and labeled derivatives as one group. Import copies — files on the card are never modified or removed, so the card stays a backup until you format it in the camera. Every copied file is re-hashed at its destination and compared with the digest read from the card, so a transfer error cannot slip through. Nothing changes without --apply, and every apply is journaled and revertable with chronocatalog undo.

After --apply, exit code 0 means the card is fully accounted for — every file was either copied and verified, already sits in the archive with byte-identical content, or is explicitly listed as ignored (hidden paths, your [import] ignore globs, skipped JPEG twins — review that list once; anything you ignore by policy exists only on the card). Anything else (unresolvable capture time, a same-name file in the archive with different content, a family only partially present) exits 1 and blocks the "safe to format" verdict. Re-running import after an import is therefore the pre-format check. With --json the verdict is structural, not just an exit code: the envelope carries verdict.safe_to_format with the imported / already-imported / ignored counts.

Directories after the card path narrow the import to just those batches (chronocatalog import /Volumes/CARD /Volumes/CARD/keepers) — made for importing triaged groups out of organize. A selective run never clears the card for formatting: the verdict speaks for the whole card, and only a full import can issue it.

verify

Recompute every name from metadata and content, and report what disagrees:

$ chronocatalog verify --config archive.toml
scanned 147717 files in 73140 families: 73088 ok, 76 findings

date-mismatch (24):
  Wideo/2025/2025-06/20250615_140910_388ec696.mov  name says 20250615_140910, metadata says 20250615_160910 (MakerNotes:DateTimeOriginal)
  ...

Findings are classified by what they mean, not just what differs: a content change in a format that is edited in place (DNG, TIFF, sidecars) is expected drift, while the same change in a write-once camera format is a corruption alarm. Other buckets cover date mismatches, unresolvable capture times, name collisions (duplicate content), malformed and unnamed files, orphaned sidecars and masters that exist in several formats.

--skip-hash checks capture times only — fast enough for a whole archive between full runs. --json emits the report machine-readably, and --json-stream emits NDJSON — progress events while the command runs, the result as the last line. The exit code is 0 when clean, 1 with findings, 2 on errors.

Hashes and resolved capture times are cached in a per-machine manifest (.chronocatalog/manifest-<machine>.tsv under the archive root), so repeat runs only re-examine new and touched files — verify, rename and inject all use it. A cached entry is trusted only while the file's size and mtime are unchanged (any metadata write invalidates it), and cached dates are additionally keyed to the date chain and timezone, so editing either re-resolves everything. --full re-reads and re-hashes everything regardless — run it periodically, since silent corruption that preserves size and mtime is exactly what the cache cannot see. --no-manifest disables the cache entirely. If you sync the archive between machines, exclude .chronocatalog/ from the sync.

inject

A DAM (e.g. Adobe Lightroom Classic) must rename the masters it manages itself, or its catalog loses track of them — but it cannot compute content hashes. chronocatalog inject bridges the two: it finds masters whose names have gone stale, writes each one's freshly computed name into a metadata field the DAM exposes as a filename template token, and the DAM does the renaming.

$ chronocatalog inject --config archive.toml --apply
...
3 token(s) written. In the DAM: Read Metadata from Files on the affected
folders, then rename with the token template (Lightroom Classic: the
{Job Identifier} filename token).

The token lands in the master's .xmp sidecar for RAW files, or inside the file itself for embedded-metadata formats (JPEG, DNG, TIFF). A RAW without a sidecar is reported as needs-sidecarchronocatalog never fabricates sidecars, because a DAM reading a minimal script-made file could wipe catalog-side metadata. Save metadata from the DAM first, then rerun.

rename

Direct renames, for what no DAM manages: whole families in unmanaged trees (fixing a wrong capture date renames the master and every sidecar atomically), and — in DAM-managed trees — only the members the DAM does not know about, while inject handles the master. Also fixes names that differ from canonical only by extension case (.FP2.fp2).

$ chronocatalog rename --config archive.toml
  /archive/Video/2025/2025-06/20250615_140910_388ec696.mov  would become 20250615_160910_388ec696.mov

dry run: 1 rename(s) planned; pass --apply to execute

Every apply is validated as a whole first, journaled before the first change, applied atomically per family, and revertable with chronocatalog undo.

organize

Triage for the messy tree every archive drags along. Runs the import planning over it and reports, without ever renaming: what each group would be named and where it would go, what is already in the archive, which groups duplicate each other, what could only be dated from file modification time (proposed, but flagged — mtime is hearsay), and what is unresolvable. There is no --apply; import confirmed batches with chronocatalog import.

Status

Early development; the command surface is complete. Installable from PyPI as chronocatalog. See CHANGELOG.md.

Safety first: every command is a dry run unless explicitly applied, and a file whose capture time cannot be resolved is reported, never renamed. Long runs show a live progress line on the terminal, and interrupting one (Ctrl-C) is safe: planning stops cleanly, and an interrupted apply is the journal's own case — finish it with resume or revert it with undo. Applies are validated as a whole before anything is touched, guarded by a per-archive lock, journaled to ~/.chronocatalog/journals/ before the first change, applied atomically per file family, finishable after interruption with chronocatalog resume, and revertable with chronocatalog undo — which, for imported copies, re-verifies each file's digest and refuses to delete anything edited since. chronocatalog history lists every recorded run with its originating command and status (pending, partial, complete, undone), optionally narrowed to one archive with --config/--root. Nothing is ever overwritten. Exit codes everywhere: 0 clean, 1 findings needing attention, 2 errors.

Configuration

An archive is described by a TOML file; every setting has a sensible default. See docs/examples.md for six complete setups — from a single folder of photos to a Lightroom workflow and a naming-scheme migration — and examples/config.toml for every option annotated:

section what it configures
[[trees]] archive subtrees, their media kind and directory layout
[pattern] the naming pattern: datetime format, digest, slice length — per-format image-data hashing, plus additional patterns recognized during a migration
[dates] metadata fields tried in order to resolve capture time, and the timezone for UTC-only sources
[extensions] which extensions are RAW masters, and which formats are edited in place
[[sidecar_dirs]] sidecars kept in subdirectories beside their masters
excludes glob patterns never to touch
[import] card files to ignore (camera housekeeping, *.jpg for RAW-only shooters) and the RAW+JPEG twin policy
[dam] hand off renaming of DAM-managed masters via a metadata token

Requirements

No Python package dependencies (on Windows, tzdata is pulled in for the timezone database).

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

chronocatalog-0.1.0.tar.gz (98.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

chronocatalog-0.1.0-py3-none-any.whl (67.1 kB view details)

Uploaded Python 3

File details

Details for the file chronocatalog-0.1.0.tar.gz.

File metadata

  • Download URL: chronocatalog-0.1.0.tar.gz
  • Upload date:
  • Size: 98.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for chronocatalog-0.1.0.tar.gz
Algorithm Hash digest
SHA256 783bea897712f88d3dac9f35a338e62cadd4e88f6219d414b9368313e4ce1e61
MD5 961d99ccea7036cadb7eda2c000d430f
BLAKE2b-256 a4ae45105b60283243906228b7e55626d328bdc8acf6a818683b453cce1141a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for chronocatalog-0.1.0.tar.gz:

Publisher: release.yml on chronocatalog/chronocatalog

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file chronocatalog-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: chronocatalog-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 67.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for chronocatalog-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5675d16401fa116b27b2e11f5bed65197cd97d7faf735d297488d3b051c45bf2
MD5 aedf5ed872152bfa801f01ba5c574bf4
BLAKE2b-256 105a9a0e736bcb81b079ef2a96a18635f73a8d0cf98c1ab5d4835943af4643cc

See more details on using hashes here.

Provenance

The following attestation bundles were made for chronocatalog-0.1.0-py3-none-any.whl:

Publisher: release.yml on chronocatalog/chronocatalog

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page