Skip to main content

immich_lib/models/
execution.rs

1//! Execution types for duplicate processing pipeline.
2//!
3//! These types capture configuration, results, and outcomes for
4//! the duplicate execution workflow.
5
6use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9
10/// Configuration for the execution pipeline.
11#[derive(Debug, Clone)]
12pub struct ExecutionConfig {
13    /// Maximum requests per second to the Immich API
14    pub requests_per_sec: u32,
15
16    /// Maximum concurrent operations
17    pub max_concurrent: usize,
18
19    /// Directory to save backup downloads before deletion
20    pub backup_dir: PathBuf,
21
22    /// If true, permanently delete assets; if false, move to trash
23    pub force_delete: bool,
24}
25
26impl Default for ExecutionConfig {
27    fn default() -> Self {
28        Self {
29            requests_per_sec: 10,
30            max_concurrent: 5,
31            backup_dir: PathBuf::from("./backups"),
32            force_delete: false,
33        }
34    }
35}
36
37/// Result of a single operation (download or delete).
38#[derive(Debug, Clone, Serialize)]
39#[serde(tag = "status", rename_all = "snake_case")]
40pub enum OperationResult {
41    /// Operation completed successfully
42    Success {
43        /// Asset ID that was processed
44        id: String,
45        /// Path where file was saved (for downloads)
46        #[serde(skip_serializing_if = "Option::is_none")]
47        path: Option<PathBuf>,
48    },
49
50    /// Operation failed with an error
51    Failed {
52        /// Asset ID that failed
53        id: String,
54        /// Error message describing the failure
55        error: String,
56    },
57
58    /// Operation was skipped
59    Skipped {
60        /// Asset ID that was skipped
61        id: String,
62        /// Reason for skipping
63        reason: String,
64    },
65}
66
67/// Result of metadata consolidation from loser assets to winner.
68///
69/// Tracks which metadata fields were transferred and from which asset.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ConsolidationResult {
72    /// Whether GPS coordinates were transferred
73    pub gps_transferred: bool,
74
75    /// Whether date/time was transferred
76    pub datetime_transferred: bool,
77
78    /// Whether description was transferred
79    pub description_transferred: bool,
80
81    /// Asset ID that provided the consolidated metadata
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub source_asset_id: Option<String>,
84}
85
86impl ConsolidationResult {
87    /// Check if any consolidation was performed.
88    pub fn any_transferred(&self) -> bool {
89        self.gps_transferred || self.datetime_transferred || self.description_transferred
90    }
91}
92
93/// Result of processing a single duplicate group.
94#[derive(Debug, Clone, Serialize)]
95pub struct GroupResult {
96    /// The duplicate group identifier
97    pub duplicate_id: String,
98
99    /// The winner asset ID
100    pub winner_id: String,
101
102    /// Result of metadata consolidation (if attempted)
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub consolidation_result: Option<ConsolidationResult>,
105
106    /// Results of downloading each loser asset
107    pub download_results: Vec<OperationResult>,
108
109    /// Result of deleting assets (if downloads succeeded)
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub delete_result: Option<OperationResult>,
112}
113
114/// Summary report of the entire execution.
115#[derive(Debug, Clone, Serialize)]
116pub struct ExecutionReport {
117    /// Total number of duplicate groups processed
118    pub total_groups: usize,
119
120    /// Number of assets successfully downloaded
121    pub downloaded: usize,
122
123    /// Number of assets deleted
124    pub deleted: usize,
125
126    /// Number of operations that failed
127    pub failed: usize,
128
129    /// Number of operations that were skipped
130    pub skipped: usize,
131
132    /// Detailed results for each group
133    pub results: Vec<GroupResult>,
134}
135
136impl ExecutionReport {
137    /// Create an empty execution report.
138    pub fn new() -> Self {
139        Self {
140            total_groups: 0,
141            downloaded: 0,
142            deleted: 0,
143            failed: 0,
144            skipped: 0,
145            results: Vec::new(),
146        }
147    }
148
149    /// Add a group result and update counters.
150    pub fn add_group_result(&mut self, result: GroupResult) {
151        self.total_groups += 1;
152
153        // Count download outcomes
154        for download in &result.download_results {
155            match download {
156                OperationResult::Success { .. } => self.downloaded += 1,
157                OperationResult::Failed { .. } => self.failed += 1,
158                OperationResult::Skipped { .. } => self.skipped += 1,
159            }
160        }
161
162        // Count delete outcomes
163        if let Some(ref delete) = result.delete_result {
164            match delete {
165                OperationResult::Success { .. } => {
166                    // Count deleted losers (download successes that were deleted)
167                    self.deleted += result
168                        .download_results
169                        .iter()
170                        .filter(|r| matches!(r, OperationResult::Success { .. }))
171                        .count();
172                }
173                OperationResult::Failed { .. } => self.failed += 1,
174                OperationResult::Skipped { .. } => self.skipped += 1,
175            }
176        }
177
178        self.results.push(result);
179    }
180}
181
182impl Default for ExecutionReport {
183    fn default() -> Self {
184        Self::new()
185    }
186}