ankit_engine/export.rs
1//! Deck and review history export operations.
2//!
3//! This module provides high-level export workflows for extracting
4//! deck contents and review history.
5
6use crate::Result;
7use ankit::AnkiClient;
8use serde::Serialize;
9
10/// Exported note with all fields and metadata.
11#[derive(Debug, Clone, Serialize)]
12pub struct ExportedNote {
13 /// The note ID.
14 pub note_id: i64,
15 /// The model (note type) name.
16 pub model_name: String,
17 /// The deck name.
18 pub deck_name: String,
19 /// Field values keyed by field name.
20 pub fields: std::collections::HashMap<String, String>,
21 /// Tags on the note.
22 pub tags: Vec<String>,
23}
24
25/// Exported card with scheduling information.
26#[derive(Debug, Clone, Serialize)]
27pub struct ExportedCard {
28 /// The card ID.
29 pub card_id: i64,
30 /// The note ID this card belongs to.
31 pub note_id: i64,
32 /// The deck name.
33 pub deck_name: String,
34 /// Number of reviews.
35 pub reps: i64,
36 /// Number of lapses.
37 pub lapses: i64,
38 /// Current interval in days.
39 pub interval: i64,
40 /// Due date (days since collection creation, or negative for learning).
41 pub due: i64,
42 /// Ease factor (as integer, e.g., 2500 = 250%).
43 pub ease_factor: i64,
44 /// Card type (0 = new, 1 = learning, 2 = review, 3 = relearning).
45 pub card_type: i32,
46 /// Queue (-1 = suspended, -2 = sibling buried, -3 = manually buried,
47 /// 0 = new, 1 = learning, 2 = review, 3 = day learn, 4 = preview).
48 pub queue: i32,
49 /// Last modification timestamp (seconds since epoch).
50 pub mod_time: i64,
51}
52
53/// Export of deck contents.
54#[derive(Debug, Clone, Serialize)]
55pub struct DeckExport {
56 /// Deck name.
57 pub deck_name: String,
58 /// All notes in the deck.
59 pub notes: Vec<ExportedNote>,
60 /// All cards in the deck.
61 pub cards: Vec<ExportedCard>,
62}
63
64/// Export workflow engine.
65#[derive(Debug)]
66pub struct ExportEngine<'a> {
67 client: &'a AnkiClient,
68}
69
70impl<'a> ExportEngine<'a> {
71 pub(crate) fn new(client: &'a AnkiClient) -> Self {
72 Self { client }
73 }
74
75 /// Export all notes and cards from a deck.
76 ///
77 /// # Arguments
78 ///
79 /// * `deck_name` - Name of the deck to export
80 ///
81 /// # Example
82 ///
83 /// ```no_run
84 /// # use ankit_engine::Engine;
85 /// # async fn example() -> ankit_engine::Result<()> {
86 /// let engine = Engine::new();
87 /// let export = engine.export().deck("Japanese").await?;
88 /// println!("Exported {} notes", export.notes.len());
89 /// # Ok(())
90 /// # }
91 /// ```
92 pub async fn deck(&self, deck_name: &str) -> Result<DeckExport> {
93 // Find all notes in deck
94 let query = format!("deck:\"{}\"", deck_name);
95 let note_ids = self.client.notes().find(&query).await?;
96 let note_infos = self.client.notes().info(¬e_ids).await?;
97
98 // Find all cards in deck
99 let card_ids = self.client.cards().find(&query).await?;
100 let card_infos = self.client.cards().info(&card_ids).await?;
101
102 // Convert to export format
103 let notes = note_infos
104 .into_iter()
105 .map(|info| ExportedNote {
106 note_id: info.note_id,
107 model_name: info.model_name,
108 deck_name: deck_name.to_string(),
109 fields: info.fields.into_iter().map(|(k, v)| (k, v.value)).collect(),
110 tags: info.tags,
111 })
112 .collect();
113
114 let cards = card_infos
115 .into_iter()
116 .map(|info| ExportedCard {
117 card_id: info.card_id,
118 note_id: info.note_id,
119 deck_name: info.deck_name,
120 reps: info.reps,
121 lapses: info.lapses,
122 interval: info.interval,
123 due: info.due,
124 ease_factor: info.ease_factor,
125 card_type: info.card_type,
126 queue: info.queue,
127 mod_time: info.mod_time,
128 })
129 .collect();
130
131 Ok(DeckExport {
132 deck_name: deck_name.to_string(),
133 notes,
134 cards,
135 })
136 }
137
138 /// Export review history for cards.
139 ///
140 /// # Arguments
141 ///
142 /// * `query` - Anki search query to select cards
143 ///
144 /// # Example
145 ///
146 /// ```no_run
147 /// # use ankit_engine::Engine;
148 /// # async fn example() -> ankit_engine::Result<()> {
149 /// let engine = Engine::new();
150 /// let reviews = engine.export().reviews("deck:Japanese").await?;
151 /// # Ok(())
152 /// # }
153 /// ```
154 pub async fn reviews(&self, query: &str) -> Result<Vec<CardReviewHistory>> {
155 let card_ids = self.client.cards().find(query).await?;
156
157 if card_ids.is_empty() {
158 return Ok(Vec::new());
159 }
160
161 let reviews = self
162 .client
163 .statistics()
164 .reviews_for_cards(&card_ids)
165 .await?;
166
167 // Convert HashMap<String, Vec<ReviewEntry>> to Vec<CardReviewHistory>
168 let mut result = Vec::new();
169
170 for (card_id_str, card_reviews) in reviews {
171 let card_id: i64 = card_id_str.parse().unwrap_or(0);
172 let entries: Vec<ExportedReviewEntry> = card_reviews
173 .iter()
174 .map(|r| ExportedReviewEntry {
175 timestamp: r.review_id,
176 ease: r.ease,
177 interval: r.interval,
178 last_interval: r.last_interval,
179 time_ms: r.time,
180 })
181 .collect();
182 result.push(CardReviewHistory {
183 card_id,
184 reviews: entries,
185 });
186 }
187
188 Ok(result)
189 }
190}
191
192/// Review history for a single card.
193#[derive(Debug, Clone, Serialize)]
194pub struct CardReviewHistory {
195 /// The card ID.
196 pub card_id: i64,
197 /// Review entries in chronological order.
198 pub reviews: Vec<ExportedReviewEntry>,
199}
200
201/// A single review entry.
202#[derive(Debug, Clone, Serialize)]
203pub struct ExportedReviewEntry {
204 /// Review timestamp (milliseconds since epoch).
205 pub timestamp: i64,
206 /// Ease button pressed (1-4).
207 pub ease: i32,
208 /// Resulting interval.
209 pub interval: i64,
210 /// Previous interval.
211 pub last_interval: i64,
212 /// Time spent on review in milliseconds.
213 pub time_ms: i64,
214}