smriti/services/
library_health.rs1use std::path::Path;
15
16use rusqlite::{Connection, Result as SqliteResult};
17
18use crate::services::path_util::safe_join_relative;
19
20#[derive(Debug, Clone, Default)]
21pub struct LibraryHealth {
22 pub total_photos: i64,
23 pub missing_thumbnails: i64,
26 pub inaccurate_dates: i64,
29 pub missing_dates: i64,
31 pub heic_count: i64,
33 pub heic_decoder_available: bool,
37 pub face_processed_no_faces: i64,
41}
42
43pub struct LibraryHealthSnapshot {
44 health: LibraryHealth,
45 thumbnail_paths: Vec<String>,
46}
47
48pub fn compute_snapshot(conn: &Connection) -> SqliteResult<LibraryHealthSnapshot> {
49 let total_photos: i64 = conn.query_row(
50 "SELECT COUNT(*) FROM photos WHERE is_trashed = FALSE",
51 [],
52 |r| r.get(0),
53 )?;
54
55 let inaccurate_dates: i64 = conn.query_row(
56 "SELECT COUNT(*) FROM photos
57 WHERE is_trashed = FALSE
58 AND date_taken_source IN ('mtime', 'file_meta')",
59 [],
60 |r| r.get(0),
61 )?;
62
63 let missing_dates: i64 = conn.query_row(
64 "SELECT COUNT(*) FROM photos WHERE is_trashed = FALSE AND date_taken IS NULL",
65 [],
66 |r| r.get(0),
67 )?;
68
69 let heic_count: i64 = conn.query_row(
70 "SELECT COUNT(*) FROM photos
71 WHERE is_trashed = FALSE
72 AND (LOWER(file_path) LIKE '%.heic' OR LOWER(file_path) LIKE '%.heif')",
73 [],
74 |r| r.get(0),
75 )?;
76
77 let face_processed_no_faces: i64 = conn.query_row(
78 "SELECT COUNT(*) FROM photos p
79 WHERE is_trashed = FALSE
80 AND faces_processed = TRUE
81 AND NOT EXISTS (SELECT 1 FROM faces f WHERE f.photo_id = p.id)",
82 [],
83 |r| r.get(0),
84 )?;
85
86 let null_thumbs: i64 = conn.query_row(
87 "SELECT COUNT(*) FROM photos
88 WHERE is_trashed = FALSE AND thumbnail_path IS NULL",
89 [],
90 |r| r.get(0),
91 )?;
92
93 let mut thumbnail_paths = Vec::new();
94 let mut stmt = conn.prepare(
95 "SELECT thumbnail_path FROM photos
96 WHERE is_trashed = FALSE AND thumbnail_path IS NOT NULL",
97 )?;
98 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
99 for row in rows {
100 thumbnail_paths.push(row?);
101 }
102
103 Ok(LibraryHealthSnapshot {
104 health: LibraryHealth {
105 total_photos,
106 missing_thumbnails: null_thumbs,
107 inaccurate_dates,
108 missing_dates,
109 heic_count,
110 heic_decoder_available: cfg!(feature = "heic"),
111 face_processed_no_faces,
112 },
113 thumbnail_paths,
114 })
115}
116
117pub fn finish_compute(mut snapshot: LibraryHealthSnapshot, drive_root: &Path) -> LibraryHealth {
118 for row in snapshot.thumbnail_paths {
119 if safe_join_relative(drive_root, &row)
120 .map(|p| !p.exists())
121 .unwrap_or(true)
122 {
123 snapshot.health.missing_thumbnails += 1;
124 }
125 }
126 snapshot.health
127}
128
129pub fn compute(conn: &Connection, drive_root: &Path) -> SqliteResult<LibraryHealth> {
132 let total_photos: i64 = conn.query_row(
133 "SELECT COUNT(*) FROM photos WHERE is_trashed = FALSE",
134 [],
135 |r| r.get(0),
136 )?;
137
138 let inaccurate_dates: i64 = conn.query_row(
139 "SELECT COUNT(*) FROM photos
140 WHERE is_trashed = FALSE
141 AND date_taken_source IN ('mtime', 'file_meta')",
142 [],
143 |r| r.get(0),
144 )?;
145
146 let missing_dates: i64 = conn.query_row(
147 "SELECT COUNT(*) FROM photos WHERE is_trashed = FALSE AND date_taken IS NULL",
148 [],
149 |r| r.get(0),
150 )?;
151
152 let heic_count: i64 = conn.query_row(
153 "SELECT COUNT(*) FROM photos
154 WHERE is_trashed = FALSE
155 AND (LOWER(file_path) LIKE '%.heic' OR LOWER(file_path) LIKE '%.heif')",
156 [],
157 |r| r.get(0),
158 )?;
159
160 let face_processed_no_faces: i64 = conn.query_row(
161 "SELECT COUNT(*) FROM photos p
162 WHERE is_trashed = FALSE
163 AND faces_processed = TRUE
164 AND NOT EXISTS (SELECT 1 FROM faces f WHERE f.photo_id = p.id)",
165 [],
166 |r| r.get(0),
167 )?;
168
169 let null_thumbs: i64 = conn.query_row(
175 "SELECT COUNT(*) FROM photos
176 WHERE is_trashed = FALSE AND thumbnail_path IS NULL",
177 [],
178 |r| r.get(0),
179 )?;
180 let mut stat_misses = 0i64;
181 {
182 let mut stmt = conn.prepare(
183 "SELECT thumbnail_path FROM photos
184 WHERE is_trashed = FALSE AND thumbnail_path IS NOT NULL",
185 )?;
186 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
187 for row in rows.flatten() {
188 if safe_join_relative(drive_root, &row)
189 .map(|p| !p.exists())
190 .unwrap_or(true)
191 {
192 stat_misses += 1;
193 }
194 }
195 }
196
197 Ok(LibraryHealth {
198 total_photos,
199 missing_thumbnails: null_thumbs + stat_misses,
200 inaccurate_dates,
201 missing_dates,
202 heic_count,
203 heic_decoder_available: cfg!(feature = "heic"),
204 face_processed_no_faces,
205 })
206}