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}