Obscura Trace ships a command-line tool, obscura, which relies on the same engines and the same catalog as the app. It is used to automate, to verify, and to access two functions that the app does not expose yet: batch renaming and path templates during a transfer.
This chapter assumes you are comfortable with the Terminal. None of the commands in this chapter is needed to use the app.
Where the tool is
In a packaged version, the tool is inside the app bundle:
"/Applications/Obscura Trace.app/Contents/Helpers/obscura" versionTo keep the examples short, define two variables:
OBSCURA="/Applications/Obscura Trace.app/Contents/Helpers/obscura"
CATALOG="$HOME/Library/Application Support/Obscura Trace/catalog.sqlite""$OBSCURA" with no argument (or with an unknown argument) prints the full help and exits with code 2.
"$OBSCURA" version prints the version of the app that contains the tool (for example 0.1.1); it is also the version it writes into manifests. A tool copied outside the app prints 0.0.0-dev.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Error: the operation did not succeed (unreadable catalog, unknown identifier, malformed JSON command), or a review decision or a setting was refused. |
| 2 | Usage: unknown command, option or value. Nothing is opened. |
| 3 | Refusal or interruption of an operation on files: plan refused (REPLACE without consent, insufficient capacity, missing destination), rename preview rejected, disk full or unmounted during execution. What was done is recorded in the journal; resuming takes care of it. |
| 75 | The catalog is open in another process (see below). |
An error is always printed on standard error, preceded by “erreur :” (error:) or “refusé :” (refused:). An unknown option value is named, along with the accepted values: --strategy skip is refused (“valeur inconnue pour –strategy : « skip » (valeurs admises : SKIP, RENAME, COMPARE, REPLACE)” (unknown value for –strategy: “skip” (accepted values: SKIP, RENAME, COMPARE, REPLACE))); it is not taken to mean COMPARE.
Precautions
Quit the app before any command that opens its catalog. Only one process writes to a catalog: as long as the app has it open, every
obscuracommand on that catalog — reading included (settings get,review counts,job list) — is refused, without opening or modifying anything, with exit code 75:“refusé : CATALOG_LOCKED : le catalogue « … » est déjà ouvert par le processus N (l’application ou la CLI). Un seul processus écrit dans un catalogue : fermez l’autre, puis recommencez. Rien n’a été ouvert ni modifié.” (meaning: refused: CATALOG_LOCKED: the catalog “…” is already open in process N (the app or the CLI). Only one process writes to a catalog: close the other one, then try again. Nothing was opened or modified.)
The same refusal applies between two
obscuracommands run at the same time on the same catalog, and to a second window of the app (open -n), which shows “Le catalogue est déjà ouvert” (The catalog is already open). This is what prevents a command from taking the app’s work in progress for interrupted work.Make a backup copy of the catalog before experimenting (see chapter 11).
Every command takes the catalog explicitly through
--catalog. Nothing is read from anywhere else.Measurement commands, not to be pointed at your catalog:
tree,memory,hash,similarity,scanandjob runare used for performance measurements and tests. In particular,scan --rootdeclares a fictitious measurement source in the given catalog. Use them, if needed, with a throwaway catalog (--catalog /tmp/essai.sqlite). To analyze your collections, use the app’s Assistant de scan (Scan Assistant).The production CLI contains no test tool: any option reserved for the fault harness (
--crash,--trash-dir,crash…) is refused before anything is opened: “refusé : … appartient au harnais de pannes (obscura-harness), absent de la CLI de production … Rien n’a été ouvert ni modifié.” (refused: … belongs to the fault harness (obscura-harness), absent from the production CLI … Nothing was opened or modified.)
Settings: obscura settings
The same settings as the Réglages (Settings) window, in the same catalog.
"$OBSCURA" settings get --catalog "$CATALOG"
# permanentDeletionEnabled=falseDisable permanent deletion:
"$OBSCURA" settings set --catalog "$CATALOG" --permanent-deletion offEnabling it requires an explicit confirmation, the --confirm-irreversible flag (the equivalent of the app’s dialog):
"$OBSCURA" settings set --catalog "$CATALOG" --permanent-deletion on
# refusé : une suppression définitive ne passe pas par la Corbeille et ne se récupère pas.
# Relancez avec --confirm-irreversible pour l'autoriser ; chaque job devra encore y consentir.
# permanentDeletionEnabled=false (exit code 1)
"$OBSCURA" settings set --catalog "$CATALOG" --permanent-deletion on --confirm-irreversible
# permanentDeletionEnabled=true(The refusal reads: permanent deletion does not go through the Trash and cannot be recovered; rerun with --confirm-irreversible to allow it; each job will still have to consent.)
The printed value is always reread from the catalog, never the requested one. The change appears in the Activité (Activity) screen and in the Réglages window the next time it opens.
Review: obscura review
"$OBSCURA" review counts --catalog "$CATALOG"
# EXACT_DUPLICATES 20000
# PROBABLE_DUPLICATES 312
# …
"$OBSCURA" review list --catalog "$CATALOG" --filter UNRESOLVED --offset 0 --limit 20
# GROUP <groupId> EXACT_DUPLICATES UNDECIDED members=3 subject=<assetId>,<assetId>,<assetId>
# FILE <id> <chemin relatif>Filters: EXACT_DUPLICATES, PROBABLE_DUPLICATES, VISUAL_MATCHES, UNIQUE_ASSETS, CORRUPT_FILES, UNRESOLVED (the default), NEEDS_REVIEW — the filters of the Centre de revue (Review Center).
Record a decision, with the same JSON command as the one the app emits:
"$OBSCURA" review decide --catalog "$CATALOG" \
--command '{"action":"CHOOSE_MASTER","subjectKey":"<a>,<b>,<c>","assetId":"<b>"}'
# {"decisionId":"…","status":"IN_PROGRESS"}subjectKey: thesubject=value fromreview list(the member identifiers, separated by commas).action:KEEP,MARK_DUPLICATE,MARK_VERSION,CHOOSE_MASTER(withassetId, one of the members), orIGNORE,REOPEN(withoutassetId).- A refusal is printed on standard error (“refusé : …” (refused: …)) with code 1; nothing is recorded.
No decision touches a file.
Proposed moves (T-29) — list them, then decide them:
"$OBSCURA" review proposals --catalog "$CATALOG"
# PROPOSAL <proposalId> PENDING lost=<assetId> new=<assetId> 2019/a.jpg -> Archives 2019/a.jpg
"$OBSCURA" review settle --catalog "$CATALOG" \
--command '{"proposalId":"<proposalId>","decision":"ACCEPT"}'
# {"assetId":"…","proposalId":"…","status":"ACCEPTED"}decision: ACCEPT (attach, final) or REJECT (keep separate). A proposal already decided or out of date is refused (“refusé : …” (refused: …), code 1), without writing anything.
A source’s “full fingerprint” option:
"$OBSCURA" settings fingerprint --catalog "$CATALOG" --source <sourceId> --full on
# fullFingerprint=trueTransfers: plan, execute, recover-transfers
You need the source identifier (source-…). The CLI has no command to list sources; you can read it, read-only, with the macOS sqlite3 tool:
sqlite3 -readonly "$CATALOG" "SELECT sourceId, displayName FROM sources;"Planning (transfers nothing)
"$OBSCURA" plan --catalog "$CATALOG" --source-id <sourceId> --destination /Volumes/Archive \
[--move] [--strategy COMPARE|SKIP|RENAME] [--template '<modèle de chemin>'] [--dry-run]
# plan=<planId> fingerprint=<empreinte> operations=<n> bytes=<octets>- The source root is found through its bookmark, never through a typed path.
--strategydefaults toCOMPARE. The CLI does not pass on any consent to replacement:REPLACEis refused there.--templatefiles the files according to a path template, for example'{YEAR}/{MONTH}/{ORIGINAL_NAME}'(variables below). Without it, the source’s folder tree is kept.--moveplans a move; see the caveat about the Trash below.- Permanent deletion is not available through
plan.
Executing a real plan
"$OBSCURA" execute --catalog "$CATALOG" --plan <planId> --source-id <sourceId> \
--executor copy-first [--manifest]
# performed=… alreadyVerified=… failed=… trashed=0 …
# manifest=/Volumes/Archive/.obscura-manifests/<planId>/manifest.json--executor copy-firstis mandatory in the production CLI: it is the product’s executor (copy, verification, promotion).--manifestwrites the manifest before the job ends.- The CLI never touches the Trash. For a move executed by the CLI, each source stays in place, as on a disk without a Trash: it is a verified copy, and removing the sources is up to you.
- No confirmation screen in the CLI: reading the plan before executing it is your responsibility. Running
executeagain on the same plan does not recopy what is already verified.
Resuming after an interruption
"$OBSCURA" recover-transfers --catalog "$CATALOG" --executor copy-first
# dangling=<n> verifiedInPlace=<n> rolledBack=<n>Resuming reads only the journal: for each operation left open, it verifies the copy in place or undoes it. Then run execute again on the same plan to finish the rest.
Batch renaming: rename, recover-renames
Batch renaming is not available in the app; it is available in the CLI. It renames files in place, in their folder, according to a name template.
Preview (renames nothing):
"$OBSCURA" rename --catalog "$CATALOG" --source-id <sourceId> \ --source-root "/Volumes/Photos" --template '{YYYY}-{MM}-{DD}_{ORIGINAL_NAME}' # PREVIEW 2004/IMG_0042.CR2 -> 2004/2004-07-14_IMG_0042.CR2 # ISSUE … # pairs=… issues=… skipped=… fingerprint=<empreinte>Apply, requiring the preview to be exactly the one you read:
"$OBSCURA" rename … --apply --fingerprint <empreinte> # job=<jobId> renamed=… failed=…If the preview has changed in the meantime: “REFUSED preview différente de celle annoncée” (REFUSED preview different from the one announced) (code 3), nothing is renamed.
Undo a batch:
--undo <jobId>in place of--templateproduces the preview of the compensation; add--applyto apply it.Resume after an interruption:
"$OBSCURA" recover-renames --catalog "$CATALOG".
Rules:
- the template is a name, without “/”; the original extension is kept as is (
.TIFstays.TIF); - a missing value is replaced by a fixed token, for example
unknown-camera; --source-rootmust be the current root of the source (the one shown in the Assistant de scan);- companions follow their main file;
- no renaming on an exFAT or FAT32 disk: these formats do not guarantee renaming without overwriting; the preview announces it with a
REFUSED_VOLUMEline, the operation fails and no name changes.
Template variables
{DATE}, {YEAR} (or {YYYY}), {MONTH} (or {MM}), {DAY} (or {DD}), {CAMERA}, {LENS}, {ISO}, {LOCATION}, {CATEGORY}, {ORIGINAL_NAME}, {SEQUENCE}, {HASH8}. The app has neither geolocation nor classification: {LOCATION} and {CATEGORY} receive their absence token.
Jobs: obscura job
"$OBSCURA" job list --catalog "$CATALOG"
# <jobId> COMPLETED 1284234/1284234
# <jobId> RUNNING 5120/-
"$OBSCURA" job recover --catalog "$CATALOG"
# recovered=<n> ids=…job list shows the state of each job (analyses, plans, transfers, renamings). job recover replays the startup recovery: jobs left “en cours” (running) after a process stopped abruptly are marked interrupted.