Skip to main content

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(&note_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}