kobo_db_tools/parser/
parser.rs

1use crate::{
2    get_bookmarks, Bookmark, Brightness, BrightnessEvent, BrightnessHistory, DictionaryWord,
3    NaturalLightHistory, ReadingSession, ReadingSessions,
4};
5use chrono::{DateTime, Utc};
6use rusqlite::Connection;
7use std::collections::HashMap;
8use std::str::FromStr;
9use thiserror::Error;
10use std::path::Path;
11
12#[derive(Debug, Error)]
13pub enum ParseError {
14    #[error("Event is not valid")]
15    InvalidEventType,
16    #[error("Error during session completation")]
17    SessionCompletionFailed,
18    #[error("Error during deserialize")]
19    DeserializationError,
20}
21
22#[derive(serde::Deserialize, Clone)]
23struct ReadingSessionAttributes {
24    progress: String,
25    volumeid: Option<String>,
26    title: Option<String>,
27}
28
29#[derive(serde::Deserialize)]
30struct LeaveContentMetrics {
31    #[serde(rename = "ButtonPressCount")]
32    button_press_count: usize,
33    #[serde(rename = "SecondsRead")]
34    seconds_read: usize,
35    #[serde(rename = "PagesTurned")]
36    pages_turned: usize,
37}
38
39#[derive(serde::Deserialize)]
40struct LightAttributes {
41    #[serde(rename = "Method")]
42    method: String,
43}
44
45#[derive(serde::Deserialize)]
46struct LightMetrics {
47    #[serde(alias = "NewNaturalLight")]
48    #[serde(alias = "NewBrightness")]
49    new_light: u8,
50}
51
52#[derive(serde::Deserialize)]
53struct DicitonaryAttributes {
54    #[serde(rename = "Dictionary")]
55    lang: String,
56    #[serde(rename = "Word")]
57    word: String,
58}
59
60#[derive(Debug)]
61pub struct EventAnalysis {
62    pub sessions: ReadingSessions,
63    pub terms: HashMap<DictionaryWord, usize>,
64    pub brightness_history: BrightnessHistory,
65    pub natural_light_history: NaturalLightHistory,
66    pub bookmarks: Vec<Bookmark>,
67}
68
69pub struct Parser;
70
71impl Parser {
72    pub fn parse_events(db: &Connection) -> rusqlite::Result<EventAnalysis> {
73        let q = "SELECT Id, Type, Timestamp, Attributes, Metrics\n                 FROM AnalyticsEvents\n                 WHERE Type IN\n                 (  'OpenContent', 'LeaveContent',\n                    'DictionaryLookup',\n                    'BrightnessAdjusted','NaturalLightAdjusted'\n                 )\n                 ORDER BY Timestamp ASC;";
74
75        let mut stmt = db.prepare(q)?;
76        let mut rows = stmt.query([])?;
77
78        let mut analysis = EventAnalysis {
79            sessions: ReadingSessions::new(),
80            terms: HashMap::new(),
81            brightness_history: BrightnessHistory::new(),
82            natural_light_history: NaturalLightHistory::new(),
83            bookmarks: get_bookmarks(db)?,
84        };
85
86        let mut current_session: Option<ReadingSession> = None;
87
88        while let Some(row) = rows.next()? {
89            let event_id: String = row.get("Id")?;
90            let event_type: String = row.get("Type")?;
91            let ts_str: String = row.get("Timestamp")?;
92            let ts = DateTime::<Utc>::from_str(&ts_str).map_err(|e| {
93                rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(e))
94            })?;
95
96            match event_type.as_str() {
97                "OpenContent" | "LeaveContent" => {
98                    let attr_json: String = row.get("Attributes")?;
99                    let attr: ReadingSessionAttributes =
100                        serde_json::from_str(&attr_json).map_err(|e| {
101                            rusqlite::Error::FromSqlConversionFailure(
102                                1,
103                                rusqlite::types::Type::Text,
104                                Box::new(e),
105                            )
106                        })?;
107                    let progress = attr.progress.parse::<u8>().unwrap_or(0);
108
109                    let metrics = if event_type == "LeaveContent" {
110                        let metr_json: String = row.get("Metrics")?;
111                        Some(
112                            serde_json::from_str::<LeaveContentMetrics>(&metr_json).map_err(|e| {
113                                rusqlite::Error::FromSqlConversionFailure(
114                                    2,
115                                    rusqlite::types::Type::Text,
116                                    Box::new(e),
117                                )
118                            })?,
119                        )
120                    } else {
121                        None
122                    };
123
124                    match handle_reading_session_event(
125                        &event_type,
126                        &event_id,
127                        &mut current_session,
128                        ts,
129                        progress,
130                        &attr,
131                        metrics,
132                    ) {
133                        Ok(Some(session)) => analysis.sessions.add_session(session),
134                        Ok(None) => {}
135                        Err(e) => eprintln!("Errore evento {}: {:?}", &event_id, e),
136                    }
137                }
138                "DictionaryLookup" => {
139                    let attr_json: String = row.get("Attributes")?;
140                    *analysis
141                        .terms
142                        .entry(on_dictionary_lookup(attr_json)?)
143                        .or_insert(0) += 1;
144                }
145                "BrightnessAdjusted" => {
146                    let attr_json: String = row.get("Attributes")?;
147                    let metr_json: String = row.get("Metrics")?;
148                    let event = on_light_adjusted(attr_json, metr_json, ts)?;
149                    analysis.brightness_history.insert(event);
150                }
151                "NaturalLightAdjusted" => {
152                    let attr_json: String = row.get("Attributes")?;
153                    let metr_json: String = row.get("Metrics")?;
154                    let event = on_light_adjusted(attr_json, metr_json, ts)?;
155                    analysis.natural_light_history.insert(event);
156                }
157                _ => {
158                    eprintln!("Unknown event: {}", event_type);
159                }
160            }
161        }
162        Ok(analysis)
163    }
164    pub fn parse_from_str<P: AsRef<Path>>(path:P)->rusqlite::Result<EventAnalysis>{
165        let conn = Connection::open(path)?;
166        Self::parse_events(&conn)
167    }
168}
169
170fn handle_reading_session_event(
171    event_type: &str,
172    event_id: &str,
173    current_session: &mut Option<ReadingSession>,
174    ts: DateTime<Utc>,
175    progress: u8,
176    attr: &ReadingSessionAttributes,
177    metrics: Option<LeaveContentMetrics>,
178) -> Result<Option<ReadingSession>, ParseError> {
179    match event_type {
180        "OpenContent" => {
181            *current_session = Some(ReadingSession::new(
182                ts,
183                progress,
184                attr.title.clone(),
185                attr.volumeid.clone(),
186                event_id.to_string(),
187            ));
188            Ok(None)
189        }
190        "LeaveContent" => {
191            if let Some(ref mut session) = current_session {
192                let _open_content_id = session.open_content_id.clone();
193                let m = metrics.ok_or(ParseError::SessionCompletionFailed)?;
194                session
195                    .complete_session(
196                        ts,
197                        progress,
198                        m.button_press_count as u64,
199                        m.seconds_read as u64,
200                        m.pages_turned as u64,
201                        event_id.to_string(),
202                    )
203                    .map_err(|_| ParseError::SessionCompletionFailed)?;
204
205                let completed = std::mem::take(session);
206                *current_session = None;
207                Ok(Some(completed))
208            } else {
209                Err(ParseError::SessionCompletionFailed)
210            }
211        }
212        _ => Err(ParseError::InvalidEventType),
213    }
214}
215
216fn on_dictionary_lookup(attr_json: String) -> rusqlite::Result<DictionaryWord> {
217    let attr: DicitonaryAttributes = serde_json::from_str(&attr_json).map_err(|e| {
218        rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, Box::new(e))
219    })?;
220    Ok(DictionaryWord::new(attr.word, attr.lang))
221}
222
223fn on_light_adjusted(
224    attr_json: String,
225    metr_json: String,
226    ts: DateTime<Utc>,
227) -> rusqlite::Result<BrightnessEvent> {
228    let attributes: LightAttributes = serde_json::from_str(&attr_json).map_err(|e| {
229        rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, Box::new(e))
230    })?;
231    let metrics: LightMetrics = serde_json::from_str(&metr_json).map_err(|e| {
232        rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, Box::new(e))
233    })?;
234    let brightness = Brightness::new(attributes.method, metrics.new_light);
235    Ok(BrightnessEvent::new(brightness, ts))
236}