diff --git a/crates/engine/src/lightroom_catalog.rs b/crates/engine/src/lightroom_catalog.rs index f5a4abbf8..201286c50 100644 --- a/crates/engine/src/lightroom_catalog.rs +++ b/crates/engine/src/lightroom_catalog.rs @@ -524,9 +524,9 @@ pub fn read_with_progress(path: &Path, cancel: &AtomicBool, total: &AtomicUsize, warnings, }; // images whose file, folder or root record is gone, or whose path is empty (Lightroom leaves - // these behind and ignores them); they and their virtual copies are skipped with one warning - // per reason instead of failing the import - let mut orphans: BTreeMap<&str, Vec> = BTreeMap::new(); + // these behind and ignores them); they and their virtual copies are skipped with a warning + // instead of failing the import + let mut orphans = HashSet::new(); for image in table("Adobe_images", true)? { ensure_read_active(cancel)?; let id = number(&image, "id_local"); @@ -538,7 +538,8 @@ pub fn read_with_progress(path: &Path, cancel: &AtomicBool, total: &AtomicUsize, let (file, folder, root) = match record { Ok(found) => found, Err(why) => { - orphans.entry(why).or_default().push(id); + out.warnings.push(format!("image {id}: not imported; {why}")); + orphans.insert(id); continue; } }; @@ -547,7 +548,8 @@ pub fn read_with_progress(path: &Path, cancel: &AtomicBool, total: &AtomicUsize, let folder_path = text(folder, "pathFromRoot"); let name = text(file, "idx_filename"); if root_path.is_empty() || name.is_empty() { - orphans.entry("incomplete original path").or_default().push(id); + out.warnings.push(format!("image {id}: not imported; incomplete original path")); + orphans.insert(id); continue; } let (photo_path, original) = original_path(root_path, folder_path, name, path); @@ -576,46 +578,29 @@ pub fn read_with_progress(path: &Path, cancel: &AtomicBool, total: &AtomicUsize, original, }); } - for (why, ids) in &orphans { - out.warnings.push(skipped_warning(ids, why)); - } - drop_copies_of(&mut out, &orphans.into_values().flatten().collect()); + drop_copies_of(&mut out, &orphans); refuse_non_media(&mut out); ensure_read_active(cancel)?; Ok(out) } -/// One warning for a group of images skipped for the same reason, listing the first few ids so a -/// catalog with many broken records doesn't bury the other warnings. -fn skipped_warning(ids: &[i64], why: &str) -> String { - const SHOWN: usize = 10; - let mut list = ids.iter().take(SHOWN).map(i64::to_string).collect::>().join(", "); - if ids.len() > SHOWN { - list.push_str(&format!(", … ({} more)", ids.len() - SHOWN)); - } - let n = ids.len(); - format!("{n} image{} not imported ({why}): {list}", if n == 1 { "" } else { "s" }) -} - /// Drop the virtual copies whose master was not imported, saying so in the import's warnings: a /// copy has no original of its own. fn drop_copies_of(data: &mut CatalogImport, masters: &HashSet) { if masters.is_empty() { return; } - let mut dropped = Vec::new(); + let mut warnings = Vec::new(); data.photos.retain(|p| { let master = number(&p.image, "masterImage"); // 0 marks a master, never a copy if master == 0 || !masters.contains(&master) { return true; } - dropped.push(p.source_id); + warnings.push(format!("image {}: not imported; its original was not imported", p.source_id)); false }); - if !dropped.is_empty() { - data.warnings.push(skipped_warning(&dropped, "virtual copy of an image not imported")); - } + data.warnings.extend(warnings); } /// Drop the catalog's images whose original is not a file type LightCraft reads, saying so in the @@ -1641,7 +1626,7 @@ mod tests { let catalog = read(&path).unwrap(); std::fs::remove_file(path).unwrap(); assert!(catalog.photos.is_empty()); - assert!(catalog.warnings.iter().any(|w| w == "1 image not imported (missing file record): 1"), "{:?}", catalog.warnings); + assert!(catalog.warnings.iter().any(|w| w == "image 1: not imported; missing file record"), "{:?}", catalog.warnings); } #[test] @@ -1653,16 +1638,7 @@ mod tests { let catalog = read(&path).unwrap(); std::fs::remove_file(path).unwrap(); assert!(catalog.photos.is_empty()); - assert!(catalog.warnings.iter().any(|w| w == "1 image not imported (incomplete original path): 1"), "{:?}", catalog.warnings); - } - - #[test] - fn skipped_images_are_summarised_in_one_warning() { - let ids: Vec = (1..=12).collect(); - assert_eq!( - skipped_warning(&ids, "missing file record"), - "12 images not imported (missing file record): 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, … (2 more)" - ); + assert!(catalog.warnings.iter().any(|w| w == "image 1: not imported; incomplete original path"), "{:?}", catalog.warnings); } #[test] @@ -1671,7 +1647,7 @@ mod tests { data.photos.retain(|p| p.source_id != 1); drop_copies_of(&mut data, &HashSet::from([1])); assert!(data.photos.is_empty()); - assert_eq!(data.warnings, ["1 image not imported (virtual copy of an image not imported): 2"]); + assert_eq!(data.warnings, ["image 2: not imported; its original was not imported"]); validate(&data).unwrap(); } diff --git a/docs/lightroom-catalog-import.md b/docs/lightroom-catalog-import.md index 4e31b5edc..10bde01a7 100644 --- a/docs/lightroom-catalog-import.md +++ b/docs/lightroom-catalog-import.md @@ -30,7 +30,7 @@ again. Previously browsed Local records join All Photos even when their original their promotion is counted once and undone together with the catalog import. An image whose file, folder or root folder record is gone from the catalog, or whose original path is empty (Lightroom leaves these behind and ignores them), is skipped together with its virtual -copies instead of failing the whole import; one warning per reason lists the skipped images. +copies instead of failing the whole import, with a warning for each. Virtual copies count as added records too; reimport reports zero additions when all records already exist. Existing LightCraft edits are preserved by default; `updateExisting=true` explicitly replaces them. Persistent import diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index de7ba697f..b3f7c06b7 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -698,7 +698,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06- | LRC-LIB-SURVEY | Survey view | P2 | ✅ | `cmd:view.survey` | N | | LRC-LIB-REFVIEW | Reference view | P2 | ✅ | `cmd:view.reference`, `cmd:photo.setReference`, `crates/ui-egui/src/panels/compare.rs` (`show_reference`) | View ▸ Reference View (⇧R): the reference photo beside the active one (edited with the Edit panel); photo menu ▸ Set as Reference Photo; each pane says “Rendering…” (widget `compare:rendering:`) or “This photo can't be opened” (`compare:renderFailed:`) as the loupe does | | LRC-LIB-CATALOG-PANEL | Catalog sets | P1 | ✅ | `cmd:library.source` (`missing`) | all, recently added, picks, recently deleted, missing photos (when any) | -| LRC-LIB-CATALOG-IMPORT | Native Lightroom Classic catalog import | P1 | 🟡 | `cmd:library.inspectLightroom`, `cmd:library.importLightroom`, `crates/engine/src/lightroom_catalog.rs`, `docs/lightroom-catalog-import.md` | read-only `.lrcat` plus committed WAL pages (also WAL-only tables with main schema format 0), in-tree read-only Rust SQLite B-tree reader; background inspection/import with progress/cancellation, per-open library identity checks; persistent result with destination library, offline-original counts and Show in Library, dismissed when switching libraries; offline Local records are promoted without probing missing files; originals in place, ratings/flags/labels, XMP, hierarchical keywords, collection sets/regular collections, virtual copies, mapped develop settings; stable reimport identities, undo, missing-file placeholders, a root folder not found at its recorded path looked up relative to the catalog (Lightroom's `relativePathFromCatalog`, so a project folder copied from another computer stays linked; reported as a warning), existing edits preserved. Best-effort deduplicated recovery archives retain settings/history/snapshots/collection rules (32 MiB each, eight archives/128 MiB aggregate); archive limits/write failures and optional-table limits warn without blocking photo import; unrelated unsupported tables and opaque SQL BLOBs skipped. Custom white balance on relative-WB raws (`RAW` in Lightroom's terms) stays As Shot and is reported, since the catalog keeps no as-shot white to shift from (issue #510). Adobe rendering, AI data and native history replay remain incomplete; no writes to Lightroom catalogs. Open Library given a Lightroom catalog folder, an `.lrdata` / `.lrcat-data` folder or the `.lrcat` makes no library there and offers this import with the catalog found (`cmd:app.openLibrary`, `crates/engine/src/lightroom_folder.rs`; issue #891) | +| LRC-LIB-CATALOG-IMPORT | Native Lightroom Classic catalog import | P1 | 🟡 | `cmd:library.inspectLightroom`, `cmd:library.importLightroom`, `crates/engine/src/lightroom_catalog.rs`, `docs/lightroom-catalog-import.md` | read-only `.lrcat` plus committed WAL pages (also WAL-only tables with main schema format 0), in-tree read-only Rust SQLite B-tree reader; background inspection/import with progress/cancellation, per-open library identity checks; persistent result with destination library, offline-original counts and Show in Library, dismissed when switching libraries; offline Local records are promoted without probing missing files; originals in place, ratings/flags/labels, XMP, hierarchical keywords, collection sets/regular collections, virtual copies, mapped develop settings; stable reimport identities, undo, missing-file placeholders, a root folder not found at its recorded path looked up relative to the catalog (Lightroom's `relativePathFromCatalog`, so a project folder copied from another computer stays linked; reported as a warning), existing edits preserved. Best-effort deduplicated recovery archives retain settings/history/snapshots/collection rules (32 MiB each, eight archives/128 MiB aggregate); archive limits/write failures and optional-table limits warn without blocking photo import; unrelated unsupported tables and opaque SQL BLOBs skipped. Custom white balance on relative-WB raws (`RAW` in Lightroom's terms) stays As Shot and is reported, since the catalog keeps no as-shot white to shift from (issue #510). An image whose file, folder or root folder record is missing from the catalog, or whose original path is empty, is skipped with its virtual copies, each with a warning, instead of failing the whole import (PR #2230). Adobe rendering, AI data and native history replay remain incomplete; no writes to Lightroom catalogs. Open Library given a Lightroom catalog folder, an `.lrdata` / `.lrcat-data` folder or the `.lrcat` makes no library there and offers this import with the catalog found (`cmd:app.openLibrary`, `crates/engine/src/lightroom_folder.rs`; issue #891) | | LRC-LIB-CATALOG-LOCK | Catalog open in one program at a time | P1 | ✅ | `cmd:app.openLibrary`, `crates/catalog/src/lock.rs`, `crates/engine/src/library.rs` (`open_library`) | exclusive OS lock on `catalog.lock` for the session (app and `lightcraft-cli --library`); a second opener is refused with who has it (`catalog.lock.owner`: program, process, computer); released by the OS on a crash, so never stale; file systems without locks open unprotected with a warning (the app's notices, `library.info` → `settingsWarnings`, and the CLI's stderr; issue #171); no read-only mode; the browser build (OPFS) has no guard across tabs yet. A library that can't be opened at launch (locked, unreadable, newer format, drive missing) shows a window — Try Again, Choose Another Library…, Continue Without Saving (then a banner; nothing written), Quit — instead of a silent demo session (`crates/ui-egui/src/panels/library_problem.rs`); the lock is released explicitly when the library closes, not by letting its file handle close, so a program another thread is starting at that moment (which holds a copy of every file handle until it is launched) cannot keep the library "already open" for a moment (issue #680) | | LRC-LIB-CATALOG-SWITCH | New / Open Recent catalog, catalog name shown | P1 | ✅ | `cmd:app.newLibrary`, `cmd:app.openLibrary`, `cmd:app.recentLibraries`, `cmd:app.clearRecentLibraries`, `crates/ui-egui/src/panels/settings.rs`, `crates/ui-egui/src/panels/left.rs` | File ▸ New Library… (name, then location), File ▸ Open Recent Library (last 8; missing ones greyed out, dropped when opened; Clear), the open library's name under My Photos and in the window title (issue #902); a library that can't be opened leaves the current one open; ⌘O (Ctrl+O on Windows and Linux) runs Open Library… and asks for a folder once (`crates/ui-egui/src/tests_keymap.rs`: `cmd_o_opens_the_library_picker_once`); a dialog or a text field with the focus keeps the key | | LRC-LIB-FOLDERS | Disk folder tree | P1 | ✅ | `cmd:library.browse`, `cmd:folder.rename`, `cmd:folder.move`, `crates/ui-egui/src/panels/left.rs` (`local_section`, `folder_tree`) | standard places + the browsed folder + Browse Folder…, each an expandable tree of subfolders, breadcrumb navigation; context menu: Rename Folder…, Move Folder To…, Show in Finder (on disk, photos relinked; one undo step each: undo / redo move the folder back and forth, refused when the destination is taken) |