lrcat/
catalog.rs

1/*
2 This Source Code Form is subject to the terms of the Mozilla Public
3 License, v. 2.0. If a copy of the MPL was not distributed with this
4 file, You can obtain one at http://mozilla.org/MPL/2.0/.
5*/
6
7use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9
10use rusqlite::{params, Connection};
11
12use crate::collections::Collection;
13use crate::folders::{Folder, Folders, RootFolder};
14use crate::fromdb::FromDb;
15use crate::images::Image;
16use crate::keywords::Keyword;
17use crate::keywordtree::KeywordTree;
18use crate::libraryfiles::LibraryFile;
19use crate::lrobject::{LrId, LrObject};
20
21const LR2_VERSION: i32 = 2;
22const LR3_VERSION: i32 = 3;
23const LR4_VERSION: i32 = 4;
24const LR6_VERSION: i32 = 6;
25
26/// Catalog version.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum CatalogVersion {
29    /// Unknown version
30    Unknown,
31    /// Lightroom 2.x catalog.
32    Lr2,
33    /// Lightroom 3.x catalog. Unhandled.
34    Lr3,
35    /// Lightroom 4.x catalog.
36    Lr4,
37    /// Lightroom 6.x catalog.
38    Lr6,
39}
40
41impl CatalogVersion {
42    /// Return if we support this catalog version
43    pub fn is_supported(&self) -> bool {
44        (*self == Self::Lr2) || (*self == Self::Lr3) || (*self == Self::Lr4) || (*self == Self::Lr6)
45    }
46}
47
48/// Catalog is the main container for Lightroom. It represents
49/// the .lrcat database.
50pub struct Catalog {
51    /// Catalog path
52    path: PathBuf,
53    /// The version string
54    pub version: String,
55    /// The catalog version
56    pub catalog_version: CatalogVersion,
57    /// Id for the root (top level) keyword
58    pub root_keyword_id: LrId,
59
60    /// The keywords, mapped in the local `LrId`
61    keywords: BTreeMap<LrId, Keyword>,
62    /// The `Folders` container.
63    folders: Folders,
64    /// The `Image` container
65    images: Vec<Image>,
66    /// The `LibraryFile` container
67    libfiles: Vec<LibraryFile>,
68    /// The `Collection` container
69    collections: Vec<Collection>,
70
71    /// The sqlite connectio to the catalog
72    dbconn: Option<Connection>,
73}
74
75impl Catalog {
76    /// Create a new catalog.
77    pub fn new<P>(path: P) -> Catalog
78    where
79        P: AsRef<Path>,
80    {
81        Catalog {
82            path: path.as_ref().to_path_buf(),
83            version: "".to_string(),
84            catalog_version: CatalogVersion::Unknown,
85            root_keyword_id: 0,
86            keywords: BTreeMap::new(),
87            folders: Folders::new(),
88            images: vec![],
89            libfiles: vec![],
90            collections: vec![],
91            dbconn: None,
92        }
93    }
94
95    /// Open catalog. Return false in failure.
96    /// This doesn't check if the content is valid beyond the backing sqlite3.
97    pub fn open(&mut self) -> crate::Result<()> {
98        let conn = Connection::open(&self.path)?;
99
100        self.dbconn = Some(conn);
101
102        Ok(())
103    }
104
105    /// Get a variable from the table.
106    fn get_variable<T>(&self, name: &str) -> Option<T>
107    where
108        T: rusqlite::types::FromSql,
109    {
110        let conn = self.dbconn.as_ref()?;
111        if let Ok(mut stmt) = conn.prepare("SELECT value FROM Adobe_variablesTable WHERE name=?1") {
112            let mut rows = stmt.query([&name]).unwrap();
113            if let Ok(Some(row)) = rows.next() {
114                return row.get(0).ok();
115            }
116        }
117        None
118    }
119
120    /// Parse the version string from the database.
121    fn parse_version(mut v: String) -> i32 {
122        v.truncate(2);
123        v.parse::<i32>().unwrap_or_default()
124    }
125
126    /// Load version info for the catalog.
127    pub fn load_version(&mut self) {
128        if let Some(version) = self.get_variable::<String>("Adobe_DBVersion") {
129            self.version = version;
130            let v = Catalog::parse_version(self.version.clone());
131            self.catalog_version = match v {
132                LR6_VERSION => CatalogVersion::Lr6,
133                LR4_VERSION => CatalogVersion::Lr4,
134                LR3_VERSION => CatalogVersion::Lr3,
135                LR2_VERSION => CatalogVersion::Lr2,
136                _ => CatalogVersion::Unknown,
137            };
138        }
139
140        if let Some(root_keyword_id) = self.get_variable::<f64>("AgLibraryKeyword_rootTagID") {
141            self.root_keyword_id = root_keyword_id.round() as LrId;
142        }
143    }
144
145    /// Generic object loader leveraging the FromDb protocol
146    fn load_objects<T: FromDb>(conn: &Connection, catalog_version: CatalogVersion) -> Vec<T> {
147        let mut query = format!(
148            "SELECT {} FROM {}",
149            T::read_db_columns(catalog_version),
150            T::read_db_tables(catalog_version)
151        );
152        let where_join = T::read_join_where(catalog_version);
153        if !where_join.is_empty() {
154            query += &format!(" WHERE {where_join}");
155        }
156        if let Ok(mut stmt) = conn.prepare(&query) {
157            if let Ok(rows) =
158                stmt.query_and_then(params![], |row| T::read_from(catalog_version, row))
159            {
160                return rows.into_iter().filter_map(|obj| obj.ok()).collect();
161            }
162        }
163        vec![]
164    }
165
166    /// Load a keyword tree
167    pub fn load_keywords_tree(&mut self) -> KeywordTree {
168        let keywords = self.load_keywords();
169
170        let mut tree = KeywordTree::new();
171        tree.add_children(keywords);
172
173        tree
174    }
175
176    /// Load keywords.
177    pub fn load_keywords(&mut self) -> &BTreeMap<LrId, Keyword> {
178        if self.keywords.is_empty() {
179            if let Some(ref conn) = self.dbconn {
180                let result = Catalog::load_objects::<Keyword>(conn, self.catalog_version);
181                for keyword in result {
182                    self.keywords.insert(keyword.id(), keyword);
183                }
184            }
185        }
186        &self.keywords
187    }
188
189    /// Get the keywords. This assume the keywords have been loaded first.
190    /// This allow non-mutable borrowing that would be caused by `load_keywords()`.
191    pub fn keywords(&self) -> &BTreeMap<LrId, Keyword> {
192        &self.keywords
193    }
194
195    /// Load folders.
196    pub fn load_folders(&mut self) -> &Folders {
197        if self.folders.is_empty() {
198            if let Some(ref conn) = self.dbconn {
199                let folders = Catalog::load_objects::<RootFolder>(conn, self.catalog_version);
200                self.folders.append_root_folders(folders);
201                let mut folders = Catalog::load_objects::<Folder>(conn, self.catalog_version);
202                for folder in &mut folders {
203                    folder.content = Some(folder.read_content(conn));
204                }
205                self.folders.append_folders(folders);
206            }
207        }
208        &self.folders
209    }
210
211    /// Get the folders. This assume the folders have been loaded first.
212    /// This allow non-mutable borrowing that would be caused by `load_folders()`.
213    pub fn folders(&self) -> &Folders {
214        &self.folders
215    }
216
217    /// Load library files (that back images)
218    pub fn load_library_files(&mut self) -> &Vec<LibraryFile> {
219        if self.libfiles.is_empty() {
220            if let Some(ref conn) = self.dbconn {
221                let mut result = Catalog::load_objects::<LibraryFile>(conn, self.catalog_version);
222                self.libfiles.append(&mut result);
223            }
224        }
225        &self.libfiles
226    }
227
228    /// Get the libfiles. This assume the libfiles have been loaded first.
229    /// This allow non-mutable borrowing that would be caused by `load_libfiles()`.
230    pub fn libfiles(&self) -> &Vec<LibraryFile> {
231        &self.libfiles
232    }
233
234    /// Load images.
235    pub fn load_images(&mut self) -> &Vec<Image> {
236        if self.images.is_empty() {
237            if let Some(ref conn) = self.dbconn {
238                let mut result = Catalog::load_objects::<Image>(conn, self.catalog_version);
239                self.images.append(&mut result);
240            }
241        }
242        &self.images
243    }
244
245    /// Get the images. This assume the images have been loaded first.
246    /// This allow non-mutable borrowing that would be caused by `load_images()`.
247    pub fn images(&self) -> &Vec<Image> {
248        &self.images
249    }
250
251    /// Load collectons.
252    pub fn load_collections(&mut self) -> &Vec<Collection> {
253        if self.collections.is_empty() {
254            if let Some(ref conn) = self.dbconn {
255                let mut collections =
256                    Catalog::load_objects::<Collection>(conn, self.catalog_version);
257                for collection in &mut collections {
258                    collection.content = Some(collection.read_content(conn));
259                }
260                self.collections.append(&mut collections);
261            }
262        }
263        &self.collections
264    }
265
266    /// Get the collections. This assume the collections have been loaded first.
267    /// This allow non-mutable borrowing that would be caused by `load_collections()`.
268    pub fn collections(&self) -> &Vec<Collection> {
269        &self.collections
270    }
271
272    /// Lr2 use "Tags".
273    const LR2_QUERY: &'static str =
274        "SELECT image FROM AgLibraryTagImage WHERE tag = ?1 AND tagKind = \"AgCollectionTagKind\"";
275    /// Lr3, Lr4 and Lr6 store the relation in `AgLibraryCollectionImage`
276    const LR4_QUERY: &'static str =
277        "SELECT image FROM AgLibraryCollectionImage WHERE collection = ?1";
278
279    /// Collect images from collections using a specific query.
280    fn images_for_collection_with_query(
281        &self,
282        query: &str,
283        collection_id: LrId,
284    ) -> super::Result<Vec<LrId>> {
285        let conn = self.dbconn.as_ref().unwrap();
286        let mut stmt = conn.prepare(query)?;
287        let rows = stmt.query_map([&collection_id], |row| row.get::<usize, i64>(0))?;
288        let mut ids = Vec::new();
289        for id in rows {
290            ids.push(id?);
291        }
292        Ok(ids)
293    }
294
295    /// Return the of images in the given collection.
296    /// Not to be confused with Content.
297    pub fn images_for_collection(&self, collection_id: LrId) -> super::Result<Vec<LrId>> {
298        match self.catalog_version {
299            CatalogVersion::Lr2 => {
300                self.images_for_collection_with_query(Self::LR2_QUERY, collection_id)
301            }
302            CatalogVersion::Lr3 | CatalogVersion::Lr4 | CatalogVersion::Lr6 => {
303                self.images_for_collection_with_query(Self::LR4_QUERY, collection_id)
304            }
305            _ => Err(super::Error::UnsupportedVersion),
306        }
307    }
308}