Selection and folder-structure flag redesign. --exclude-album becomes --album '!NAME'. New --smart-folder and --unfiled per-category flags. --library now repeatable. Per-category --folder-structure-{albums,smart-folders} templates with their own defaults. Multi-library scope with {library} in any template. State DB schema migrates to v9 (per-zone primary key on assets, asset_albums, and asset_people) on first sync. Old shapes keep working with deprecation warnings until v0.20. Full migration guide: docs/v0.13-migration.md. ([#215], #288)
Added
--smart-folder NAMEflag for selecting Apple smart folders. Repeatable. Accepts a literal name (Favorites,Hidden), bare sentinels (all,all-with-sensitive,none), or!literalexclusions. Default isnoneso existing setups don't gain extra passes.allexcludes Hidden and Recently Deleted (mirroring iOS Photos UX);all-with-sensitiveincludes them. The TOML key is[filters].smart_folders. ([#215])--unfiledboolean flag for the unfiled-pass toggle.--unfiled(or--unfiled true) opts in;--unfiled falsedisables. Default istrue, so unfiled photos land at--folder-structureeven when album passes also run. To restrict to album passes only, pass--unfiled falseexplicitly. The TOML key is[filters].unfiled. The{album}-in-template-implies-unfiled smart default is gone; layout no longer drives selection. ([#215])--library NAMEis now repeatable and accepts new sentinels. Pass multiple--libraryflags; bare sentinelsprimary(PrimarySync),shared(every SharedSync zone), andall(every library) compose with named zones and!nameexclusions in one grammar. The TOML key[filters].librariestakes the array form. The truncated 8-char form (SharedSync-A1B2C3D4) round-trips between paths and the flag. Every combination executes end-to-end against a real account: e.g.--library primary --library SharedSync-AB12CD34,--library all '!SharedSync-Photos', or--library shared(every shared zone, primary excluded). A--libraryvalue that doesn't match any live zone bails at startup with the available zone list. ([#215])--folder-structure-albumsand--folder-structure-smart-foldersper-category templates. Album passes default to{album}(flat), smart-folder passes to{smart-folder}(flat). The base--folder-structure(default%Y/%m/%d) now drives only the unfiled pass. Add date hierarchy inside album folders explicitly with--folder-structure-albums '{album}/%Y/%m/%d'. The auto-migration of legacy{album}in--folder-structureproduces exactly that shape when the legacy template carried a date suffix. Both new flags map to TOML keys[download].folder_structure_albumsand[download].folder_structure_smart_folders. ([#215]){library}token in folder-structure templates. Renders asPrimarySyncor a truncated raw zone name (SharedSync-A1B2C3D4) so users running multi-library syncs can keep paths separate. Allowed in any template; must be the first path segment; single occurrence. Combined with{album}or{smart-folder}, the order is always{library}/{album}/.... The truncated form is what the flag accepts back, so a path segment can be copied directly into--library. ([#215])- Per-zone primary key in the state DB (schema v9). The
assets,asset_albums, andasset_peopletables all key on(library, ...)so an asset present in both PrimarySync and a shared library tracks distinct rows -- including its album and person memberships -- instead of silently sharing them. Existing rows backfill tolibrary = "PrimarySync"on first sync after upgrade. Single-library users see no behavior change. ([#215]) - Multi-library commingle warning at startup. When more than one library is in scope and none of the active templates includes
{library}, kei warns that paths share a namespace and the state DB will dedup cross-library matches by ID. Informational; the run continues. Users who want intentional merge see the warning and ignore it; users who wanted separation get pointed at{library}. ([#215])
Changed
kei syncwith no flags now enumerates each user album and runs an unfiled pass. Previously it ran one library-wide stream. The internal change is real (more API calls, per-album knowledge); the visible change is the on-disk tree shape only when--folder-structure-albumsis non-default -- the default{album}template gives flat per-album folders, while unfiled photos still land at%Y/%m/%d/. Aligns the no-flags experience with woutervanwijk's #215 ask without requiring opt-in configuration. ([#215])--folder-structurenow drives only the unfiled pass. Album passes consume--folder-structure-albums; smart-folder passes consume--folder-structure-smart-folders. The legacy{album}-in-base auto-migrates with a warning (split across the two new templates) so existing configs keep working through v0.19. Removed in v0.20. ([#215])--librarynow matches album names across every library in scope.--album Familyresolves to an album named "Family" in every selected library. To get distinct paths per library, add{library}to the relevant template; otherwise paths commingle and the state DB dedups by asset ID (same rule the multi-library warning surfaces at startup). ([#215])- First sync after upgrade triggers a full enumeration. The sync-token invalidation hash now folds in the new library selector shape (and the per-zone state DB key), so existing sync tokens hash differently and kei refetches the full asset list once before resuming incremental syncs. Subsequent runs return to incremental performance. No data loss; idempotent. ([#215])
kei import-existingaccepts repeatable--libraryand the deprecated TOML album keys.--librarywas single-value before, so a second--libraryflag silently overrode the first. It now takes the same repeatable v0.13 grammar askei sync --library(primary/shared/all/ raw zone names /!nameexclusions). Deprecated TOML keys[filters].album(singular) and[filters].exclude_albumsare now read on the import path with the same deprecation warning sync emits, so a config mid-migration produces the same scope for both commands instead of letting import fall back to every album. ([#215])
Deprecated
--exclude-album NAMECLI flag andKEI_EXCLUDE_ALBUMenv var. Use--album '!NAME'(the new inline-exclusion grammar). Bare!Foooperates on the category default (allfor albums), so--album '!Family'means "every album except Family". Both forms still work and log a deprecation warning; removed in v0.20.0. ([#215]){album}in--folder-structure(CLI / TOML /KEI_FOLDER_STRUCTUREenv). Use--folder-structure-albums "{album}/..."and keep--folder-structurefor the unfiled pass. kei auto-migrates at startup:{album}/%Y/%m/%dlifts intofolder_structure_albumsand the base template strips the prefix. A one-line warning prints with the equivalent new-flag pair so the suggestion is paste-ready. Removed in v0.20.0. ([#215])[filters].album(singular string) TOML key. Use[filters].albums = ["name"](array). Round-trips through the same selector grammar so--albumand the array form are interchangeable. Removed in v0.20.0. ([#215])[filters].exclude_albumsTOML key. Merge into[filters].albumsas"!name"entries. The new selector grammar validates contradictions ("Foo"and"!Foo"together) in one place. Removed in v0.20.0. ([#215])[filters].library(single-string) TOML key. Use[filters].libraries = ["name"](array). Removed in v0.20.0. ([#215])- Implicit
--album allfrom{album}token in template. v0.12 promoted the legacy template-driven shape to "every album" when--albumwas unset; v0.13's new--album alldefault makes the promotion redundant. The compat path still fires but logs a deprecation warning. Removed in v0.20.0. ([#215])
Full changelog: CHANGELOG.md