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