2020-01-04 00:18:51 +00:00
local BD = require ( " ui/bidi " )
2017-09-14 20:29:09 +00:00
local BookStatusWidget = require ( " ui/widget/bookstatuswidget " )
2017-08-29 15:34:49 +00:00
local ConfirmBox = require ( " ui/widget/confirmbox " )
local DataStorage = require ( " datastorage " )
2019-08-01 17:10:46 +00:00
local Device = require ( " device " )
2021-09-01 01:24:03 +00:00
local Dispatcher = require ( " dispatcher " )
2017-08-29 15:34:49 +00:00
local DocSettings = require ( " docsettings " )
2020-09-15 18:39:32 +00:00
local FFIUtil = require ( " ffi/util " )
2017-08-29 15:34:49 +00:00
local InfoMessage = require ( " ui/widget/infomessage " )
2016-02-14 21:47:36 +00:00
local KeyValuePage = require ( " ui/widget/keyvaluepage " )
2020-05-29 12:22:27 +00:00
local Math = require ( " optmath " )
2017-09-16 11:36:54 +00:00
local ReaderFooter = require ( " apps/reader/modules/readerfooter " )
2017-08-29 15:34:49 +00:00
local ReaderProgress = require ( " readerprogress " )
local ReadHistory = require ( " readhistory " )
2017-12-17 17:27:24 +00:00
local Screensaver = require ( " ui/screensaver " )
2017-08-29 15:34:49 +00:00
local SQ3 = require ( " lua-ljsqlite3/init " )
local UIManager = require ( " ui/uimanager " )
local Widget = require ( " ui/widget/widget " )
2015-09-07 17:06:17 +00:00
local lfs = require ( " libs/libkoreader-lfs " )
2017-08-29 15:34:49 +00:00
local logger = require ( " logger " )
2015-11-27 15:13:01 +00:00
local util = require ( " util " )
2017-08-29 15:34:49 +00:00
local _ = require ( " gettext " )
2019-08-25 13:32:41 +00:00
local N_ = _.ngettext
2020-09-15 18:39:32 +00:00
local T = FFIUtil.template
2016-11-04 21:35:20 +00:00
2017-08-29 15:34:49 +00:00
local statistics_dir = DataStorage : getDataDir ( ) .. " /statistics/ "
2017-08-29 18:42:17 +00:00
local db_location = DataStorage : getSettingsDir ( ) .. " /statistics.sqlite3 "
2020-10-15 03:31:21 +00:00
local MAX_PAGETURNS_BEFORE_FLUSH = 50
2017-08-29 15:34:49 +00:00
local DEFAULT_MIN_READ_SEC = 5
local DEFAULT_MAX_READ_SEC = 120
2020-02-16 00:03:12 +00:00
local DEFAULT_CALENDAR_START_DAY_OF_WEEK = 2 -- Monday
local DEFAULT_CALENDAR_NB_BOOK_SPANS = 3
2015-09-07 17:06:17 +00:00
2020-10-15 03:31:21 +00:00
-- Current DB schema version
2020-10-22 18:15:35 +00:00
local DB_SCHEMA_VERSION = 20201022
2020-10-15 03:31:21 +00:00
-- This is the query used to compute the total time spent reading distinct pages of the book,
2021-03-06 21:44:18 +00:00
-- capped at self.settings.max_sec per distinct page.
2020-10-15 03:31:21 +00:00
-- c.f., comments in insertDB
local STATISTICS_SQL_BOOK_CAPPED_TOTALS_QUERY = [ [
SELECT count ( * ) ,
sum ( durations )
FROM (
SELECT min ( sum ( duration ) , % d ) AS durations
FROM page_stat
WHERE id_book = % d
GROUP BY page
) ;
] ]
-- As opposed to the uncapped version
local STATISTICS_SQL_BOOK_TOTALS_QUERY = [ [
SELECT count ( DISTINCT page ) ,
sum ( duration )
FROM page_stat
WHERE id_book = % d ;
] ]
2016-12-25 20:13:30 +00:00
local ReaderStatistics = Widget : extend {
2018-08-17 18:54:11 +00:00
name = " statistics " ,
2017-08-29 15:34:49 +00:00
start_current_period = 0 ,
curr_page = 0 ,
id_curr_book = nil ,
2015-09-07 17:06:17 +00:00
is_enabled = nil ,
2020-02-16 00:03:12 +00:00
convert_to_db = nil , -- true when migration to DB has been done
2020-10-15 03:31:21 +00:00
pageturn_count = 0 ,
mem_read_time = 0 ,
mem_read_pages = 0 ,
book_read_pages = 0 ,
book_read_time = 0 ,
2017-08-29 15:34:49 +00:00
avg_time = nil ,
2020-10-15 03:31:21 +00:00
page_stat = { } , -- Dictionary, indexed by page (hash), contains a list (array) of { timestamp, duration } tuples.
2015-09-07 17:06:17 +00:00
data = {
title = " " ,
2021-06-18 17:16:02 +00:00
authors = " N/A " ,
language = " N/A " ,
series = " N/A " ,
2015-11-23 19:37:36 +00:00
performance_in_pages = { } ,
total_time_in_sec = 0 ,
2015-09-07 17:06:17 +00:00
highlights = 0 ,
notes = 0 ,
pages = 0 ,
2017-08-29 15:34:49 +00:00
md5 = nil ,
2015-09-07 17:06:17 +00:00
} ,
}
2020-02-16 00:03:12 +00:00
local weekDays = { " Sun " , " Mon " , " Tue " , " Wed " , " Thu " , " Fri " , " Sat " } -- in Lua wday order
2017-02-19 21:12:45 +00:00
local shortDayOfWeekTranslation = {
[ " Mon " ] = _ ( " Mon " ) ,
[ " Tue " ] = _ ( " Tue " ) ,
[ " Wed " ] = _ ( " Wed " ) ,
[ " Thu " ] = _ ( " Thu " ) ,
[ " Fri " ] = _ ( " Fri " ) ,
[ " Sat " ] = _ ( " Sat " ) ,
[ " Sun " ] = _ ( " Sun " ) ,
}
2020-02-16 00:03:12 +00:00
local longDayOfWeekTranslation = {
[ " Mon " ] = _ ( " Monday " ) ,
[ " Tue " ] = _ ( " Tuesday " ) ,
[ " Wed " ] = _ ( " Wednesday " ) ,
[ " Thu " ] = _ ( " Thursday " ) ,
[ " Fri " ] = _ ( " Friday " ) ,
[ " Sat " ] = _ ( " Saturday " ) ,
[ " Sun " ] = _ ( " Sunday " ) ,
}
2017-02-19 21:12:45 +00:00
local monthTranslation = {
[ " January " ] = _ ( " January " ) ,
[ " February " ] = _ ( " February " ) ,
[ " March " ] = _ ( " March " ) ,
[ " April " ] = _ ( " April " ) ,
[ " May " ] = _ ( " May " ) ,
[ " June " ] = _ ( " June " ) ,
[ " July " ] = _ ( " July " ) ,
[ " August " ] = _ ( " August " ) ,
[ " September " ] = _ ( " September " ) ,
[ " October " ] = _ ( " October " ) ,
[ " November " ] = _ ( " November " ) ,
[ " December " ] = _ ( " December " ) ,
}
2017-01-06 09:33:57 +00:00
function ReaderStatistics : isDocless ( )
2021-05-20 17:09:54 +00:00
return self.ui == nil or self.ui . document == nil or self.ui . document.is_pic == true
2017-01-06 09:33:57 +00:00
end
2021-04-13 15:54:11 +00:00
-- NOTE: This is used in a migration script by ui/data/onetime_migration,
-- which is why it's public.
ReaderStatistics.default_settings = {
min_sec = DEFAULT_MIN_READ_SEC ,
max_sec = DEFAULT_MAX_READ_SEC ,
is_enabled = true ,
convert_to_db = nil ,
calendar_start_day_of_week = DEFAULT_CALENDAR_START_DAY_OF_WEEK ,
calendar_nb_book_spans = DEFAULT_CALENDAR_NB_BOOK_SPANS ,
calendar_show_histogram = true ,
calendar_browse_future_months = false ,
}
2021-09-01 01:24:03 +00:00
function ReaderStatistics : onDispatcherRegisterActions ( )
2021-09-10 20:11:24 +00:00
Dispatcher : registerAction ( " stats_calendar_view " , { category = " none " , event = " ShowCalendarView " , title = _ ( " Statistics calendar view " ) , general = true , separator = true } )
Dispatcher : registerAction ( " book_statistics " , { category = " none " , event = " ShowBookStats " , title = _ ( " Book statistics " ) , reader = true , separator = true } )
2021-09-01 01:24:03 +00:00
end
2015-09-13 19:34:20 +00:00
function ReaderStatistics : init ( )
2021-06-14 14:15:23 +00:00
-- Disable in PIC documents (but not the FM, as we want to be registered to the FM's menu).
if self.ui and self.ui . document and self.ui . document.is_pic then
2015-09-07 17:06:17 +00:00
return
end
2021-05-20 17:09:54 +00:00
2020-10-15 03:31:21 +00:00
self.start_current_period = os.time ( )
self : resetVolatileStats ( )
2021-03-30 23:08:05 +00:00
2021-04-13 15:54:11 +00:00
self.settings = G_reader_settings : readSetting ( " statistics " , self.default_settings )
2021-03-30 23:08:05 +00:00
2017-08-29 15:34:49 +00:00
self.ui . menu : registerToMainMenu ( self )
2021-09-01 01:24:03 +00:00
self : onDispatcherRegisterActions ( )
2017-08-29 15:34:49 +00:00
self : checkInitDatabase ( )
2017-09-14 20:29:09 +00:00
BookStatusWidget.getStats = function ( )
2021-03-06 21:44:18 +00:00
return self : getStatsBookStatus ( self.id_curr_book , self.settings . is_enabled )
2017-09-14 20:29:09 +00:00
end
2017-09-16 11:36:54 +00:00
ReaderFooter.getAvgTimePerPage = function ( )
2021-03-06 21:44:18 +00:00
if self.settings . is_enabled then
2017-09-16 11:36:54 +00:00
return self.avg_time
end
end
2021-06-29 23:45:34 +00:00
Screensaver.getAvgTimePerPage = function ( )
if self.settings . is_enabled then
return self.avg_time
end
end
2017-12-17 17:27:24 +00:00
Screensaver.getReaderProgress = function ( )
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2020-10-15 03:31:21 +00:00
local current_duration , current_pages = self : getCurrentBookStats ( )
local today_duration , today_pages = self : getTodayBookStats ( )
2017-12-17 17:27:24 +00:00
local dates_stats = self : getReadingProgressStats ( 7 )
2020-10-15 03:31:21 +00:00
local readingprogress
2017-12-17 17:27:24 +00:00
if dates_stats then
readingprogress = ReaderProgress : new {
dates = dates_stats ,
2020-10-15 03:31:21 +00:00
current_duration = current_duration ,
2017-12-17 17:27:24 +00:00
current_pages = current_pages ,
2020-10-15 03:31:21 +00:00
today_duration = today_duration ,
2017-12-17 17:27:24 +00:00
today_pages = today_pages ,
readonly = true ,
}
end
return readingprogress
end
2016-02-14 21:47:36 +00:00
end
2017-08-29 15:34:49 +00:00
function ReaderStatistics : initData ( )
2021-03-06 21:44:18 +00:00
if self : isDocless ( ) or not self.settings . is_enabled then
2017-01-06 09:33:57 +00:00
return
end
2016-02-14 21:47:36 +00:00
-- first execution
2017-01-06 09:33:57 +00:00
if not self.data then
self.data = { performance_in_pages = { } }
end
local book_properties = self : getBookProperties ( )
self.data . title = book_properties.title
2017-08-29 15:34:49 +00:00
if self.data . title == nil or self.data . title == " " then
self.data . title = self.document . file : match ( " ^.+/(.+)$ " )
end
2017-01-06 09:33:57 +00:00
self.data . authors = book_properties.authors
2021-06-18 17:16:02 +00:00
if self.data . authors == nil or self.data . authors == " " then
self.data . authors = " N/A "
end
2017-01-06 09:33:57 +00:00
self.data . language = book_properties.language
2021-06-18 17:16:02 +00:00
if self.data . language == nil or self.data . language == " " then
self.data . language = " N/A "
end
2017-01-06 09:33:57 +00:00
self.data . series = book_properties.series
2021-06-18 17:16:02 +00:00
if self.data . series == nil or self.data . series == " " then
self.data . series = " N/A "
end
2016-02-14 21:47:36 +00:00
2017-01-06 09:33:57 +00:00
self.data . pages = self.view . document : getPageCount ( )
2019-10-26 07:08:57 +00:00
if not self.data . md5 then
self.data . md5 = self : partialMd5 ( self.document . file )
end
2020-05-20 19:40:49 +00:00
-- Update these numbers to what's actually stored in the settings
-- (not that "notes" is invalid and does not represent edited highlights)
self.data . highlights , self.data . notes = self.ui . bookmark : getNumberOfHighlightsAndNotes ( )
2017-08-29 15:34:49 +00:00
self.id_curr_book = self : getIdBookDB ( )
2020-10-15 03:31:21 +00:00
self.book_read_pages , self.book_read_time = self : getPageTimeTotalStats ( self.id_curr_book )
if self.book_read_pages > 0 then
self.avg_time = self.book_read_time / self.book_read_pages
2020-03-20 11:01:38 +00:00
else
2020-10-15 03:31:21 +00:00
-- NOTE: Possibly less weird-looking than initializing this to 0?
2021-03-06 21:44:18 +00:00
self.avg_time = math.floor ( 0.50 * self.settings . max_sec )
2021-01-09 20:59:25 +00:00
logger.dbg ( " ReaderStatistics: Initializing average time per page at 50% of the max value, i.e., " , self.avg_time )
2020-10-15 03:31:21 +00:00
end
end
2022-01-04 20:58:56 +00:00
function ReaderStatistics : isEnabled ( )
return self.settings . is_enabled
end
2020-10-15 03:31:21 +00:00
-- Reset the (volatile) stats on page count changes (e.g., after a font size update)
function ReaderStatistics : onUpdateToc ( )
2022-01-31 18:18:33 +00:00
-- Note: this is called *after* onPageUpdate(new current page in new page count), which
-- has updated the duration for (previous current page in old page count) and created
-- a tuple for (new current page) with a 0-duration.
-- The insertDB() call below will save the previous page stat correctly with the old
-- page count, and will drop the new current page stat.
-- Only after this insertDB(), self.data.pages is updated with the new page count.
--
-- To make this clearer, here's what happens with an example:
-- - We were reading page 127/200 with latest self.page_stat[127]={..., {now-35s, 0}}
-- - Increasing font size, re-rendering... going to page 153/254
-- - OnPageUpdate(153) is called:
-- - it updates duration in self.page_stat[127]={..., {now-35s, 35}}
-- - it adds/creates self.page_stat[153]={..., {now, 0}}
-- - it sets self.curr_page=153
-- - (at this point, we don't know the new page count is 254)
-- - OnUpdateToc() is called:
-- - insertDB() is called, which will still use the previous self.data.pages=200 as the
-- page count, and will go at inserting or not in the DB:
-- - (127, now-35s, 35, 200) inserted
-- - (153, now, 0, 200) not inserted as 0-duration (and using 200 for its associated
-- page count would be erroneous)
-- and will restore self.page_stat[153]={{now, 0}}
-- - we only then update self.data.pages=254 as the new page count
-- - 5 minutes later, on the next insertDB(), (153, now-5mn, 42, 254) will be inserted in DB
2020-10-15 03:31:21 +00:00
local new_pagecount = self.view . document : getPageCount ( )
if new_pagecount ~= self.data . pages then
2021-01-09 20:59:25 +00:00
logger.dbg ( " ReaderStatistics: Pagecount change, flushing volatile book statistics " )
2020-10-15 03:31:21 +00:00
-- Flush volatile stats to DB for current book, and update pagecount and average time per page stats
2022-01-31 18:18:31 +00:00
self : insertDB ( new_pagecount )
2020-10-15 03:31:21 +00:00
end
-- Update our copy of the page count
self.data . pages = new_pagecount
end
function ReaderStatistics : resetVolatileStats ( now_ts )
-- Computed by onPageUpdate
self.pageturn_count = 0
self.mem_read_time = 0
self.mem_read_pages = 0
-- Volatile storage pending flush to db
self.page_stat = { }
-- Re-seed the volatile stats with minimal data about the current page.
-- If a timestamp is passed, it's the caller's responsibility to ensure that self.curr_page is accurate.
if now_ts then
self.page_stat [ self.curr_page ] = { { now_ts , 0 } }
2017-08-29 15:34:49 +00:00
end
2015-09-07 17:06:17 +00:00
end
2017-09-14 20:29:09 +00:00
function ReaderStatistics : getStatsBookStatus ( id_curr_book , stat_enable )
if not stat_enable or id_curr_book == nil then
return { }
end
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2017-09-14 20:29:09 +00:00
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT count ( * )
FROM (
SELECT strftime ( ' %%Y-%%m-%%d ' , start_time , ' unixepoch ' , ' localtime ' ) AS dates
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE id_book = % d
2017-09-14 20:29:09 +00:00
GROUP BY dates
2020-10-15 03:31:21 +00:00
) ;
2017-09-14 20:29:09 +00:00
] ]
local total_days = conn : rowexec ( string.format ( sql_stmt , id_curr_book ) )
2020-10-15 03:31:21 +00:00
local total_read_pages , total_time_book = conn : rowexec ( string.format ( STATISTICS_SQL_BOOK_TOTALS_QUERY , id_curr_book ) )
2017-09-14 20:29:09 +00:00
conn : close ( )
if total_time_book == nil then
total_time_book = 0
end
if total_read_pages == nil then
total_read_pages = 0
end
return {
days = tonumber ( total_days ) ,
time = tonumber ( total_time_book ) ,
pages = tonumber ( total_read_pages ) ,
}
end
2017-08-29 15:34:49 +00:00
function ReaderStatistics : checkInitDatabase ( )
local conn = SQ3.open ( db_location )
2021-03-06 21:44:18 +00:00
if self.settings . convert_to_db then -- if conversion to sqlite DB has already been done
2020-10-15 03:31:21 +00:00
if not conn : exec ( " PRAGMA table_info('book'); " ) then
2017-08-29 15:34:49 +00:00
UIManager : show ( ConfirmBox : new {
2019-08-22 15:11:47 +00:00
text = T ( _ ( [ [
Cannot open database in % 1.
2017-08-31 04:53:13 +00:00
The database may have been moved or deleted .
Do you want to create an empty database ?
] ] ) ,
2020-01-04 00:18:51 +00:00
BD.filepath ( db_location ) ) ,
2017-08-29 15:34:49 +00:00
cancel_text = _ ( " Close " ) ,
cancel_callback = function ( )
return
end ,
ok_text = _ ( " Create " ) ,
ok_callback = function ( )
local conn_new = SQ3.open ( db_location )
self : createDB ( conn_new )
conn_new : close ( )
UIManager : show ( InfoMessage : new { text = _ ( " A new empty database has been created. " ) , timeout = 3 } )
self : initData ( )
end ,
} )
end
2020-10-15 03:31:21 +00:00
-- Check if we need to migrate to a newer schema
local db_version = tonumber ( conn : rowexec ( " PRAGMA user_version; " ) )
if db_version < DB_SCHEMA_VERSION then
logger.info ( " ReaderStatistics: Migrating DB from schema " , db_version , " to schema " , DB_SCHEMA_VERSION , " ... " )
-- Backup the existing DB first
conn : close ( )
local bkp_db_location = db_location .. " .bkp. " .. db_version .. " -to- " .. DB_SCHEMA_VERSION
2020-10-19 23:14:24 +00:00
-- Don't overwrite an existing backup
if lfs.attributes ( bkp_db_location , " mode " ) == " file " then
logger.warn ( " ReaderStatistics: A DB backup from schema " , db_version , " to schema " , DB_SCHEMA_VERSION , " already exists! " )
else
FFIUtil.copyFile ( db_location , bkp_db_location )
logger.info ( " ReaderStatistics: Old DB backed up as " , bkp_db_location )
end
2020-10-15 03:31:21 +00:00
conn = SQ3.open ( db_location )
2020-10-22 18:15:35 +00:00
if db_version < 20201010 then
self : upgradeDBto20201010 ( conn )
end
if db_version < 20201022 then
self : upgradeDBto20201022 ( conn )
end
-- Get back the space taken by the deleted page_stat table
conn : exec ( " PRAGMA temp_store = 2; " ) -- use memory for temp files
local ok , errmsg = pcall ( conn.exec , conn , " VACUUM; " ) -- this may take some time
if not ok then
logger.warn ( " Failed compacting statistics database: " , errmsg )
end
2020-10-15 03:31:21 +00:00
logger.info ( " ReaderStatistics: DB migration complete " )
2020-12-27 02:23:08 +00:00
UIManager : show ( InfoMessage : new { text = _ ( " Statistics database updated. " ) , timeout = 3 } )
2020-10-15 03:31:21 +00:00
elseif db_version > DB_SCHEMA_VERSION then
logger.warn ( " ReaderStatistics: You appear to be using a database with an unknown schema version: " , db_version , " instead of " , DB_SCHEMA_VERSION )
logger.warn ( " ReaderStatistics: Expect things to break in fun and interesting ways! " )
-- We can't know what might happen, so, back the DB up...
conn : close ( )
local bkp_db_location = db_location .. " .bkp. " .. db_version .. " -to- " .. DB_SCHEMA_VERSION
2020-10-19 23:14:24 +00:00
-- Don't overwrite an existing backup
if lfs.attributes ( bkp_db_location , " mode " ) == " file " then
logger.warn ( " ReaderStatistics: A DB backup from schema " , db_version , " to schema " , DB_SCHEMA_VERSION , " already exists! " )
else
FFIUtil.copyFile ( db_location , bkp_db_location )
logger.info ( " ReaderStatistics: Old DB backed up as " , bkp_db_location )
end
2020-10-15 03:31:21 +00:00
conn = SQ3.open ( db_location )
end
2020-02-16 00:03:12 +00:00
else -- Migrate stats for books in history from metadata.lua to sqlite database
2021-03-06 21:44:18 +00:00
self.settings . convert_to_db = true
2020-10-15 03:31:21 +00:00
if not conn : exec ( " PRAGMA table_info('book'); " ) then
2019-07-17 13:15:21 +00:00
local filename_first_history , quickstart_filename , __
if # ReadHistory.hist == 1 then
filename_first_history = ReadHistory.hist [ 1 ] [ " text " ]
local quickstart_path = require ( " ui/quickstart " ) . quickstart_filename
__ , quickstart_filename = util.splitFilePathName ( quickstart_path )
end
if # ReadHistory.hist > 1 or ( # ReadHistory.hist == 1 and filename_first_history ~= quickstart_filename ) then
2017-08-29 15:34:49 +00:00
local info = InfoMessage : new {
2020-10-15 03:31:21 +00:00
text = _ ( [ [
2019-08-22 15:11:47 +00:00
New version of statistics plugin detected .
2017-08-31 04:53:13 +00:00
Statistics data needs to be converted into the new database format .
2017-09-23 20:03:28 +00:00
This may take a few minutes .
2017-08-31 04:53:13 +00:00
Please wait …
] ] ) }
2017-08-29 15:34:49 +00:00
UIManager : show ( info )
UIManager : forceRePaint ( )
local nr_book = self : migrateToDB ( conn )
UIManager : close ( info )
UIManager : forceRePaint ( )
UIManager : show ( InfoMessage : new {
2020-07-26 05:31:20 +00:00
text = T ( N_ ( " Conversion complete. \n Imported one book to the database. \n Tap to continue. " , " Conversion complete. \n Imported %1 books to the database. \n Tap to continue. " ) , nr_book ) } )
2017-08-29 15:34:49 +00:00
else
self : createDB ( conn )
end
end
end
conn : close ( )
end
function ReaderStatistics : partialMd5 ( file )
if file == nil then
return nil
end
2019-03-27 21:50:44 +00:00
local bit = require ( " bit " )
2020-07-21 21:25:46 +00:00
local md5 = require ( " ffi/sha2 " ) . md5
2017-08-29 15:34:49 +00:00
local lshift = bit.lshift
local step , size = 1024 , 1024
2020-07-21 21:25:46 +00:00
local update = md5 ( )
2017-08-29 15:34:49 +00:00
local file_handle = io.open ( file , ' rb ' )
for i = - 1 , 10 do
file_handle : seek ( " set " , lshift ( step , 2 * i ) )
local sample = file_handle : read ( size )
if sample then
2020-07-21 21:25:46 +00:00
update ( sample )
2017-08-29 15:34:49 +00:00
else
break
end
2016-11-04 21:35:20 +00:00
end
2020-07-21 21:25:46 +00:00
return update ( )
2017-08-29 15:34:49 +00:00
end
2020-10-15 03:31:21 +00:00
-- Mainly so we don't duplicate the schema twice between the creation/upgrade codepaths
local STATISTICS_DB_PAGE_STAT_DATA_SCHEMA = [ [
CREATE TABLE IF NOT EXISTS page_stat_data
(
id_book integer ,
page integer NOT NULL DEFAULT 0 ,
start_time integer NOT NULL DEFAULT 0 ,
duration integer NOT NULL DEFAULT 0 ,
total_pages integer NOT NULL DEFAULT 0 ,
UNIQUE ( id_book , page , start_time ) ,
FOREIGN KEY ( id_book ) REFERENCES book ( id )
) ;
2020-10-22 18:15:35 +00:00
] ]
local STATISTICS_DB_PAGE_STAT_DATA_INDEX = [ [
2020-10-21 16:49:08 +00:00
CREATE INDEX IF NOT EXISTS page_stat_data_start_time ON page_stat_data ( start_time ) ;
2020-10-15 03:31:21 +00:00
] ]
local STATISTICS_DB_PAGE_STAT_VIEW_SCHEMA = [ [
-- Create the numbers table, used as a source of extra rows when scaling pages in the page_stat view
CREATE TABLE IF NOT EXISTS numbers
(
number INTEGER PRIMARY KEY
) ;
WITH RECURSIVE counter AS
(
SELECT 1 as N UNION ALL
SELECT N + 1 FROM counter WHERE N < 1000
)
INSERT INTO numbers SELECT N AS number FROM counter ;
-- Create the page_stat view
-- This view rescales data from the page_stat_data table to the current number of book pages
-- c.f., https://github.com/koreader/koreader/pull/6761#issuecomment-705660154
2020-10-22 18:15:35 +00:00
CREATE VIEW IF NOT EXISTS page_stat AS
2020-10-15 03:31:21 +00:00
SELECT id_book , first_page + idx - 1 AS page , start_time , duration / ( last_page - first_page + 1 ) AS duration
FROM (
SELECT id_book , page , total_pages , pages , start_time , duration ,
-- First page_number for this page after rescaling single row
( ( page - 1 ) * pages ) / total_pages + 1 AS first_page ,
-- Last page_number for this page after rescaling single row
max ( ( ( page - 1 ) * pages ) / total_pages + 1 , ( page * pages ) / total_pages ) AS last_page ,
idx
FROM page_stat_data
JOIN book ON book.id = id_book
-- Duplicate rows for multiple pages as needed (as a result of rescaling)
JOIN ( SELECT number as idx FROM numbers ) AS N ON idx <= ( last_page - first_page + 1 )
) ;
] ]
2017-08-29 15:34:49 +00:00
function ReaderStatistics : createDB ( conn )
2019-08-01 17:10:46 +00:00
-- Make it WAL, if possible
if Device : canUseWAL ( ) then
conn : exec ( " PRAGMA journal_mode=WAL; " )
else
conn : exec ( " PRAGMA journal_mode=TRUNCATE; " )
end
2020-10-15 03:31:21 +00:00
2017-08-29 15:34:49 +00:00
local sql_stmt = [ [
2020-10-15 03:31:21 +00:00
-- book
2017-08-29 15:34:49 +00:00
CREATE TABLE IF NOT EXISTS book
(
id integer PRIMARY KEY autoincrement ,
title text ,
authors text ,
notes integer ,
last_open integer ,
highlights integer ,
pages integer ,
series text ,
language text ,
md5 text ,
total_read_time integer ,
total_read_pages integer
) ;
2020-10-15 03:31:21 +00:00
] ]
conn : exec ( sql_stmt )
2020-10-22 18:15:35 +00:00
-- Index
2020-10-15 03:31:21 +00:00
sql_stmt = [ [
2017-09-01 20:05:57 +00:00
CREATE INDEX IF NOT EXISTS book_title_authors_md5 ON book ( title , authors , md5 ) ;
2017-08-29 15:34:49 +00:00
] ]
conn : exec ( sql_stmt )
2020-10-15 03:31:21 +00:00
2020-10-22 18:15:35 +00:00
-- page_stat_data
conn : exec ( STATISTICS_DB_PAGE_STAT_DATA_SCHEMA )
conn : exec ( STATISTICS_DB_PAGE_STAT_DATA_INDEX )
2020-10-15 03:31:21 +00:00
-- page_stat view
conn : exec ( STATISTICS_DB_PAGE_STAT_VIEW_SCHEMA )
-- DB schema version
conn : exec ( string.format ( " PRAGMA user_version=%d; " , DB_SCHEMA_VERSION ) )
end
2020-10-22 18:15:35 +00:00
function ReaderStatistics : upgradeDBto20201010 ( conn )
2020-10-15 03:31:21 +00:00
local sql_stmt = [ [
-- Start by updating the layout of the old page_stat table
ALTER TABLE page_stat RENAME COLUMN period TO duration ;
-- We're now using the user_version PRAGMA to keep track of schema version
2020-10-22 18:15:35 +00:00
DROP TABLE IF EXISTS info ;
2020-10-15 03:31:21 +00:00
] ]
conn : exec ( sql_stmt )
-- Migrate page_stat content to page_stat_data, which we'll have to create first ;).
conn : exec ( STATISTICS_DB_PAGE_STAT_DATA_SCHEMA )
sql_stmt = [ [
-- Migrate page_stat content to page_stat_data, and populate total_pages from book's pages while we're at it.
-- NOTE: While doing a per-book migration could ensure a potentially more accurate page count,
-- we need to populate total_pages *now*, or queries against unopened books would return completely bogus values...
-- We'll just have to hope the current value of the column pages in the book table is not too horribly out of date,
-- and not too horribly out of phase with the actual page count at the time the data was originally collected...
INSERT INTO page_stat_data
SELECT id_book , page , start_time , duration , pages as total_pages FROM page_stat
2020-10-19 23:14:24 +00:00
JOIN book on book.id = id_book ;
2020-10-15 03:31:21 +00:00
-- Drop old page_stat table
DROP INDEX IF EXISTS page_stat_id_book ;
DROP TABLE IF EXISTS page_stat ;
] ]
conn : exec ( sql_stmt )
-- Create the new page_stat view stuff
conn : exec ( STATISTICS_DB_PAGE_STAT_VIEW_SCHEMA )
-- Update DB schema version
2020-10-22 18:15:35 +00:00
conn : exec ( " PRAGMA user_version=20201010; " )
end
function ReaderStatistics : upgradeDBto20201022 ( conn )
conn : exec ( STATISTICS_DB_PAGE_STAT_DATA_INDEX )
-- Update DB schema version
conn : exec ( " PRAGMA user_version=20201022; " )
2017-08-29 15:34:49 +00:00
end
function ReaderStatistics : addBookStatToDB ( book_stats , conn )
local id_book
local last_open_book = 0
local total_read_pages = 0
local total_read_time = 0
local sql_stmt
if book_stats.total_time_in_sec and book_stats.total_time_in_sec > 0
and util.tableSize ( book_stats.performance_in_pages ) > 0 then
local read_pages = util.tableSize ( book_stats.performance_in_pages )
logger.dbg ( " Insert to database: " .. book_stats.title )
sql_stmt = [ [
SELECT count ( id )
FROM book
WHERE title = ?
AND authors = ?
2020-10-15 03:31:21 +00:00
AND md5 = ? ;
2017-08-29 15:34:49 +00:00
] ]
local stmt = conn : prepare ( sql_stmt )
local result = stmt : reset ( ) : bind ( self.data . title , self.data . authors , self.data . md5 ) : step ( )
local nr_id = tonumber ( result [ 1 ] )
if nr_id == 0 then
2020-10-15 03:31:21 +00:00
stmt = conn : prepare ( " INSERT INTO book VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); " )
2017-08-29 15:34:49 +00:00
stmt : reset ( ) : bind ( book_stats.title , book_stats.authors , book_stats.notes ,
last_open_book , book_stats.highlights , book_stats.pages ,
book_stats.series , book_stats.language , self : partialMd5 ( book_stats.file ) , total_read_time , total_read_pages ) : step ( )
sql_stmt = [ [
SELECT last_insert_rowid ( ) AS num ;
] ]
id_book = conn : rowexec ( sql_stmt )
else
sql_stmt = [ [
SELECT id
FROM book
WHERE title = ?
AND authors = ?
2020-10-15 03:31:21 +00:00
AND md5 = ? ;
2017-08-29 15:34:49 +00:00
] ]
stmt = conn : prepare ( sql_stmt )
result = stmt : reset ( ) : bind ( self.data . title , self.data . authors , self.data . md5 ) : step ( )
id_book = result [ 1 ]
end
local sorted_performance = { }
for k , _ in pairs ( book_stats.performance_in_pages ) do
table.insert ( sorted_performance , k )
end
table.sort ( sorted_performance )
2020-10-15 03:31:21 +00:00
conn : exec ( ' BEGIN; ' )
stmt = conn : prepare ( " INSERT OR IGNORE INTO page_stat VALUES(?, ?, ?, ?); " )
2017-08-29 15:34:49 +00:00
local avg_time = math.ceil ( book_stats.total_time_in_sec / read_pages )
2021-03-06 21:44:18 +00:00
if avg_time > self.settings . max_sec then
avg_time = self.settings . max_sec
2017-09-09 18:18:16 +00:00
end
2017-08-29 15:34:49 +00:00
local first_read_page = book_stats.performance_in_pages [ sorted_performance [ 1 ] ]
if first_read_page > 1 then
first_read_page = first_read_page - 1
end
2020-10-15 03:31:21 +00:00
local start_open_page = sorted_performance [ 1 ]
2017-08-29 15:34:49 +00:00
--first page
stmt : reset ( ) : bind ( id_book , first_read_page , start_open_page - avg_time , avg_time ) : step ( )
for i = 2 , # sorted_performance do
start_open_page = sorted_performance [ i - 1 ]
2020-10-15 03:31:21 +00:00
local diff_time = sorted_performance [ i ] - sorted_performance [ i - 1 ]
2021-03-06 21:44:18 +00:00
if diff_time <= self.settings . max_sec then
2017-08-29 15:34:49 +00:00
stmt : reset ( ) : bind ( id_book , book_stats.performance_in_pages [ sorted_performance [ i - 1 ] ] ,
start_open_page , diff_time ) : step ( )
2021-03-06 21:44:18 +00:00
elseif diff_time > self.settings . max_sec then --and diff_time <= 2 * avg_time then
2017-08-29 15:34:49 +00:00
stmt : reset ( ) : bind ( id_book , book_stats.performance_in_pages [ sorted_performance [ i - 1 ] ] ,
start_open_page , avg_time ) : step ( )
end
end
--last page
stmt : reset ( ) : bind ( id_book , book_stats.performance_in_pages [ sorted_performance [ # sorted_performance ] ] ,
sorted_performance [ # sorted_performance ] , avg_time ) : step ( )
--last open book
last_open_book = sorted_performance [ # sorted_performance ] + avg_time
2020-10-15 03:31:21 +00:00
conn : exec ( ' COMMIT; ' )
total_read_pages , total_read_time = conn : rowexec ( string.format ( STATISTICS_SQL_BOOK_TOTALS_QUERY , tonumber ( id_book ) ) )
2017-08-29 15:34:49 +00:00
sql_stmt = [ [
UPDATE book
SET last_open = ? ,
total_read_time = ? ,
total_read_pages = ?
2020-10-15 03:31:21 +00:00
WHERE id = ? ;
2017-08-29 15:34:49 +00:00
] ]
stmt = conn : prepare ( sql_stmt )
stmt : reset ( ) : bind ( last_open_book , total_read_time , total_read_pages , id_book ) : step ( )
stmt : close ( )
return true
end
end
function ReaderStatistics : migrateToDB ( conn )
self : createDB ( conn )
local nr_of_conv_books = 0
local exclude_titles = { }
for _ , v in pairs ( ReadHistory.hist ) do
2021-03-06 21:44:18 +00:00
local book_stats = DocSettings : open ( v.file ) : readSetting ( " stats " )
2017-08-29 15:34:49 +00:00
if book_stats and book_stats.title == " " then
book_stats.title = v.file : match ( " ^.+/(.+)$ " )
end
if book_stats then
book_stats.file = v.file
if self : addBookStatToDB ( book_stats , conn ) then
nr_of_conv_books = nr_of_conv_books + 1
exclude_titles [ book_stats.title ] = true
else
logger.dbg ( " Book not converted: " .. book_stats.title )
end
else
logger.dbg ( " Empty stats for file: " , v.file )
end
end
-- import from stats files (for backward compatibility)
if lfs.attributes ( statistics_dir , " mode " ) == " directory " then
for curr_file in lfs.dir ( statistics_dir ) do
local path = statistics_dir .. curr_file
if lfs.attributes ( path , " mode " ) == " file " then
local old_data = self : importFromFile ( statistics_dir , curr_file )
if old_data and old_data.total_time > 0 and not exclude_titles [ old_data.title ] then
2017-09-09 18:18:16 +00:00
local book_stats = { performance_in_pages = { } }
2017-08-29 15:34:49 +00:00
for _ , v in pairs ( old_data.details ) do
book_stats.performance_in_pages [ v.time ] = v.page
end
book_stats.title = old_data.title
book_stats.authors = old_data.authors
book_stats.notes = old_data.notes
book_stats.highlights = old_data.highlights
book_stats.pages = old_data.pages
book_stats.series = old_data.series
book_stats.language = old_data.language
2017-09-09 18:18:16 +00:00
book_stats.total_time_in_sec = old_data.total_time
2017-08-29 15:34:49 +00:00
book_stats.file = nil
if self : addBookStatToDB ( book_stats , conn ) then
nr_of_conv_books = nr_of_conv_books + 1
else
logger.dbg ( " Book not converted (old stats): " .. book_stats.title )
end
end
end
end
end
return nr_of_conv_books
end
function ReaderStatistics : getIdBookDB ( )
local conn = SQ3.open ( db_location )
local id_book
local sql_stmt = [ [
SELECT count ( id )
FROM book
WHERE title = ?
AND authors = ?
2020-10-15 03:31:21 +00:00
AND md5 = ? ;
2017-08-29 15:34:49 +00:00
] ]
local stmt = conn : prepare ( sql_stmt )
local result = stmt : reset ( ) : bind ( self.data . title , self.data . authors , self.data . md5 ) : step ( )
local nr_id = tonumber ( result [ 1 ] )
if nr_id == 0 then
2020-10-15 03:31:21 +00:00
-- Not in the DB yet, initialize it
stmt = conn : prepare ( " INSERT INTO book VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); " )
2017-08-29 15:34:49 +00:00
stmt : reset ( ) : bind ( self.data . title , self.data . authors , self.data . notes ,
2020-10-15 03:31:21 +00:00
os.time ( ) , self.data . highlights , self.data . pages ,
self.data . series , self.data . language , self.data . md5 , 0 , 0 ) : step ( )
2017-08-29 15:34:49 +00:00
sql_stmt = [ [
SELECT last_insert_rowid ( ) AS num ;
] ]
id_book = conn : rowexec ( sql_stmt )
else
sql_stmt = [ [
SELECT id
FROM book
WHERE title = ?
AND authors = ?
2020-10-15 03:31:21 +00:00
AND md5 = ? ;
2017-08-29 15:34:49 +00:00
] ]
stmt = conn : prepare ( sql_stmt )
result = stmt : reset ( ) : bind ( self.data . title , self.data . authors , self.data . md5 ) : step ( )
id_book = result [ 1 ]
end
2020-10-15 03:31:21 +00:00
stmt : close ( )
2017-08-29 15:34:49 +00:00
conn : close ( )
2020-10-15 03:31:21 +00:00
2017-08-29 15:34:49 +00:00
return tonumber ( id_book )
end
2022-01-31 18:18:31 +00:00
function ReaderStatistics : insertDB ( updated_pagecount )
if not self.id_curr_book then
2017-08-29 15:34:49 +00:00
return
end
2022-01-31 18:18:31 +00:00
local id_book = self.id_curr_book
2020-10-15 03:31:21 +00:00
local now_ts = os.time ( )
2022-01-31 18:18:33 +00:00
-- The current page stat, having yet no duration, will be ignored
-- in the insertion, and its start ts would be lost. We'll give it
-- to resetVolatileStats() so it can restore it
local cur_page_start_ts = now_ts
local cur_page_data = self.page_stat [ self.curr_page ]
local cur_page_data_tuple = cur_page_data and cur_page_data [ # cur_page_data ]
if cur_page_data_tuple and cur_page_data_tuple [ 2 ] == 0 then -- should always be true
cur_page_start_ts = cur_page_data_tuple [ 1 ]
end
2017-08-29 15:34:49 +00:00
local conn = SQ3.open ( db_location )
2020-10-15 03:31:21 +00:00
conn : exec ( ' BEGIN; ' )
local stmt = conn : prepare ( " INSERT OR IGNORE INTO page_stat_data VALUES(?, ?, ?, ?, ?); " )
for page , data_list in pairs ( self.page_stat ) do
for _ , data_tuple in ipairs ( data_list ) do
-- See self.page_stat declaration above about the tuple's layout
local ts = data_tuple [ 1 ]
local duration = data_tuple [ 2 ]
-- Skip placeholder durations
if duration > 0 then
-- NOTE: The fact that we update self.data.pages *after* this call on layout changes
-- should ensure that it matches the layout in which said data was collected.
-- Said data is used to re-scale page numbers, regardless of the document layout,
-- at query time, via a fancy SQL view.
-- This allows the progress tracking to be accurate even in the face of wild
-- document layout changes (e.g., after font size changes).
stmt : reset ( ) : bind ( id_book , page , ts , duration , self.data . pages ) : step ( )
end
2017-08-29 15:34:49 +00:00
end
end
2020-10-15 03:31:21 +00:00
conn : exec ( ' COMMIT; ' )
-- Update the new pagecount now, so that subsequent queries against the view are accurate
2017-08-29 15:34:49 +00:00
local sql_stmt = [ [
2020-10-15 03:31:21 +00:00
UPDATE book
SET pages = ?
WHERE id = ? ;
2017-08-29 15:34:49 +00:00
] ]
2020-10-15 03:31:21 +00:00
stmt = conn : prepare ( sql_stmt )
stmt : reset ( ) : bind ( updated_pagecount and updated_pagecount or self.data . pages , id_book ) : step ( )
-- NOTE: See the tail end of the discussions in #6761 for more context on the choice of this heuristic.
-- Basically, we're counting distinct pages,
2021-03-06 21:44:18 +00:00
-- while making sure the sum of durations per distinct page is clamped to self.settings.max_sec
2020-10-15 03:31:21 +00:00
-- This is expressly tailored to a fairer computation of self.avg_time ;).
2021-03-06 21:44:18 +00:00
local book_read_pages , book_read_time = conn : rowexec ( string.format ( STATISTICS_SQL_BOOK_CAPPED_TOTALS_QUERY , self.settings . max_sec , id_book ) )
2020-10-15 03:31:21 +00:00
-- NOTE: What we cache in the book table is the plain uncapped sum (mainly for deleteBooksByTotalDuration's benefit)...
local total_read_pages , total_read_time = conn : rowexec ( string.format ( STATISTICS_SQL_BOOK_TOTALS_QUERY , id_book ) )
-- And now update the rest of the book table...
2017-08-29 15:34:49 +00:00
sql_stmt = [ [
UPDATE book
SET last_open = ? ,
notes = ? ,
highlights = ? ,
total_read_time = ? ,
2020-10-15 03:31:21 +00:00
total_read_pages = ?
WHERE id = ? ;
2017-08-29 15:34:49 +00:00
] ]
stmt = conn : prepare ( sql_stmt )
2020-10-15 03:31:21 +00:00
stmt : reset ( ) : bind ( now_ts , self.data . notes , self.data . highlights , total_read_time , total_read_pages , id_book ) : step ( )
stmt : close ( )
conn : close ( )
-- NOTE: On the other hand, this is used for the average time estimate, so we use the capped variants here!
if book_read_pages then
self.book_read_pages = tonumber ( book_read_pages )
2017-08-29 15:34:49 +00:00
else
2020-10-15 03:31:21 +00:00
self.book_read_pages = 0
2017-08-29 15:34:49 +00:00
end
2020-10-15 03:31:21 +00:00
if book_read_time then
self.book_read_time = tonumber ( book_read_time )
2017-08-29 15:34:49 +00:00
else
2020-10-15 03:31:21 +00:00
self.book_read_time = 0
2017-08-29 15:34:49 +00:00
end
2020-10-15 03:31:21 +00:00
self.avg_time = self.book_read_time / self.book_read_pages
2022-01-31 18:18:33 +00:00
self : resetVolatileStats ( cur_page_start_ts )
2017-08-29 15:34:49 +00:00
end
function ReaderStatistics : getPageTimeTotalStats ( id_book )
if id_book == nil then
return
end
local conn = SQ3.open ( db_location )
2020-10-15 03:31:21 +00:00
-- NOTE: Similarly, this one is used for time-based estimates and averages, so, use the capped variant
2021-03-06 21:44:18 +00:00
local total_pages , total_time = conn : rowexec ( string.format ( STATISTICS_SQL_BOOK_CAPPED_TOTALS_QUERY , self.settings . max_sec , id_book ) )
2020-10-15 03:31:21 +00:00
conn : close ( )
2017-08-29 15:34:49 +00:00
if total_pages then
total_pages = tonumber ( total_pages )
else
total_pages = 0
end
if total_time then
total_time = tonumber ( total_time )
else
total_time = 0
end
return total_pages , total_time
end
function ReaderStatistics : getBookProperties ( )
local props = self.view . document : getProps ( )
if props.title == " No document " or props.title == " " then
2019-08-23 17:53:53 +00:00
--- @fixme Sometimes crengine returns "No document", try one more time.
2017-08-29 15:34:49 +00:00
props = self.view . document : getProps ( )
end
return props
2016-11-04 21:35:20 +00:00
end
2016-02-14 21:47:36 +00:00
function ReaderStatistics : getStatisticEnabledMenuItem ( )
2015-09-07 17:06:17 +00:00
return {
2016-02-14 21:47:36 +00:00
text = _ ( " Enabled " ) ,
2021-03-06 21:44:18 +00:00
checked_func = function ( ) return self.settings . is_enabled end ,
2015-09-07 17:06:17 +00:00
callback = function ( )
-- if was enabled, have to save data to file
2021-03-06 21:44:18 +00:00
if self.settings . is_enabled and not self : isDocless ( ) then
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2015-11-23 19:37:36 +00:00
self.ui . doc_settings : saveSetting ( " stats " , self.data )
2015-09-07 17:06:17 +00:00
end
2021-03-06 21:44:18 +00:00
self.settings . is_enabled = not self.settings . is_enabled
2017-08-29 15:34:49 +00:00
-- if was disabled have to get data from db
2021-03-06 21:44:18 +00:00
if self.settings . is_enabled and not self : isDocless ( ) then
2017-08-29 15:34:49 +00:00
self : initData ( )
2020-10-15 03:31:21 +00:00
self.start_current_period = os.time ( )
2020-03-16 15:52:09 +00:00
self.curr_page = self.ui : getCurrentPage ( )
2020-10-15 03:31:21 +00:00
self : resetVolatileStats ( self.start_current_period )
2015-09-07 17:06:17 +00:00
end
2017-09-16 11:36:54 +00:00
if not self : isDocless ( ) then
2020-07-12 18:47:49 +00:00
self.view . footer : onUpdateFooter ( )
2017-09-16 11:36:54 +00:00
end
2015-09-07 17:06:17 +00:00
end ,
}
end
2017-03-04 13:46:38 +00:00
function ReaderStatistics : addToMainMenu ( menu_items )
menu_items.statistics = {
2017-04-09 20:16:56 +00:00
text = _ ( " Reading statistics " ) ,
2016-02-14 21:47:36 +00:00
sub_item_table = {
self : getStatisticEnabledMenuItem ( ) ,
{
text = _ ( " Settings " ) ,
2020-02-16 00:03:12 +00:00
sub_item_table = {
{
text_func = function ( )
return T ( _ ( " Read page duration limits: %1 s / %2 s " ) ,
2021-03-06 21:44:18 +00:00
self.settings . min_sec , self.settings . max_sec )
2020-02-16 00:03:12 +00:00
end ,
callback = function ( touchmenu_instance )
local DoubleSpinWidget = require ( " /ui/widget/doublespinwidget " )
local durations_widget
durations_widget = DoubleSpinWidget : new {
left_text = _ ( " Min " ) ,
2021-03-06 21:44:18 +00:00
left_value = self.settings . min_sec ,
2020-02-16 00:03:12 +00:00
left_default = DEFAULT_MIN_READ_SEC ,
2021-09-28 12:00:36 +00:00
left_min = 0 ,
2020-02-16 00:03:12 +00:00
left_max = 120 ,
left_step = 1 ,
left_hold_step = 10 ,
right_text = _ ( " Max " ) ,
2021-03-06 21:44:18 +00:00
right_value = self.settings . max_sec ,
2020-02-16 00:03:12 +00:00
right_default = DEFAULT_MAX_READ_SEC ,
right_min = 10 ,
right_max = 7200 ,
right_step = 10 ,
right_hold_step = 60 ,
default_values = true ,
title_text = _ ( " Read page duration limits " ) ,
info_text = _ ( [ [
Set min and max time spent ( in seconds ) on a page for it to be counted as read in statistics .
The min value ensures pages you quickly browse and skip are not included .
The max value ensures a page you stay on for a long time ( because you fell asleep or went away ) will be included , but with a duration capped to this specified max value . ] ] ) ,
callback = function ( min , max )
2021-12-01 11:42:54 +00:00
if not min then -- "Default" button pressed
min = DEFAULT_MIN_READ_SEC
max = DEFAULT_MAX_READ_SEC
2020-02-16 00:03:12 +00:00
end
2021-03-06 21:44:18 +00:00
self.settings . min_sec = min
self.settings . max_sec = max
2020-02-16 00:03:12 +00:00
UIManager : close ( durations_widget )
touchmenu_instance : updateItems ( )
end ,
}
UIManager : show ( durations_widget )
end ,
keep_menu_open = true ,
separator = true ,
} ,
{
text_func = function ( )
return T ( _ ( " Calendar weeks start on %1 " ) ,
2021-03-06 21:44:18 +00:00
longDayOfWeekTranslation [ weekDays [ self.settings . calendar_start_day_of_week ] ] )
2020-02-16 00:03:12 +00:00
end ,
sub_item_table = {
{ -- Friday (Bangladesh and Maldives)
text = longDayOfWeekTranslation [ weekDays [ 6 ] ] ,
2021-03-06 21:44:18 +00:00
checked_func = function ( ) return self.settings . calendar_start_day_of_week == 6 end ,
callback = function ( ) self.settings . calendar_start_day_of_week = 6 end
2020-02-16 00:03:12 +00:00
} ,
{ -- Saturday (some Middle East countries)
text = longDayOfWeekTranslation [ weekDays [ 7 ] ] ,
2021-03-06 21:44:18 +00:00
checked_func = function ( ) return self.settings . calendar_start_day_of_week == 7 end ,
callback = function ( ) self.settings . calendar_start_day_of_week = 7 end
2020-02-16 00:03:12 +00:00
} ,
{ -- Sunday
text = longDayOfWeekTranslation [ weekDays [ 1 ] ] ,
2021-03-06 21:44:18 +00:00
checked_func = function ( ) return self.settings . calendar_start_day_of_week == 1 end ,
callback = function ( ) self.settings . calendar_start_day_of_week = 1 end
2020-02-16 00:03:12 +00:00
} ,
{ -- Monday
text = longDayOfWeekTranslation [ weekDays [ 2 ] ] ,
2021-03-06 21:44:18 +00:00
checked_func = function ( ) return self.settings . calendar_start_day_of_week == 2 end ,
callback = function ( ) self.settings . calendar_start_day_of_week = 2 end
2020-02-16 00:03:12 +00:00
} ,
} ,
} ,
{
text_func = function ( )
2021-03-06 21:44:18 +00:00
return T ( _ ( " Books per calendar day: %1 " ) , self.settings . calendar_nb_book_spans )
2020-02-16 00:03:12 +00:00
end ,
callback = function ( touchmenu_instance )
local SpinWidget = require ( " ui/widget/spinwidget " )
UIManager : show ( SpinWidget : new {
2021-03-06 21:44:18 +00:00
value = self.settings . calendar_nb_book_spans ,
2020-02-16 00:03:12 +00:00
value_min = 1 ,
value_max = 5 ,
ok_text = _ ( " Set " ) ,
title_text = _ ( " Books per calendar day " ) ,
2020-05-09 11:16:09 +00:00
info_text = _ ( " Set the max number of book spans to show for a day " ) ,
2020-02-16 00:03:12 +00:00
callback = function ( spin )
2021-03-06 21:44:18 +00:00
self.settings . calendar_nb_book_spans = spin.value
2020-02-16 00:03:12 +00:00
touchmenu_instance : updateItems ( )
end ,
extra_text = _ ( " Use default " ) ,
extra_callback = function ( )
2021-03-06 21:44:18 +00:00
self.settings . calendar_nb_book_spans = DEFAULT_CALENDAR_NB_BOOK_SPANS
2020-02-16 00:03:12 +00:00
touchmenu_instance : updateItems ( )
end
} )
end ,
keep_menu_open = true ,
} ,
{
text = _ ( " Show hourly histogram in calendar days " ) ,
2021-03-06 21:44:18 +00:00
checked_func = function ( ) return self.settings . calendar_show_histogram end ,
2020-02-16 00:03:12 +00:00
callback = function ( )
2021-03-06 21:44:18 +00:00
self.settings . calendar_show_histogram = not self.settings . calendar_show_histogram
2020-02-16 00:03:12 +00:00
end ,
} ,
{
text = _ ( " Allow browsing coming months " ) ,
2021-03-06 21:44:18 +00:00
checked_func = function ( ) return self.settings . calendar_browse_future_months end ,
2020-02-16 00:03:12 +00:00
callback = function ( )
2021-03-06 21:44:18 +00:00
self.settings . calendar_browse_future_months = not self.settings . calendar_browse_future_months
2020-02-16 00:03:12 +00:00
end ,
} ,
} ,
2016-02-14 21:47:36 +00:00
} ,
2017-08-29 15:34:49 +00:00
{
2020-02-16 00:03:12 +00:00
text = _ ( " Reset statistics " ) ,
sub_item_table = self : genResetBookSubItemTable ( ) ,
separator = true ,
2017-08-29 15:34:49 +00:00
} ,
2016-02-14 21:47:36 +00:00
{
text = _ ( " Current book " ) ,
2018-09-04 21:55:58 +00:00
keep_menu_open = true ,
2016-02-14 21:47:36 +00:00
callback = function ( )
UIManager : show ( KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = _ ( " Current statistics " ) ,
2022-01-31 18:18:31 +00:00
kv_pairs = self : getCurrentStat ( )
2016-02-14 21:47:36 +00:00
} )
2017-01-06 09:33:57 +00:00
end ,
2021-03-06 21:44:18 +00:00
enabled_func = function ( ) return not self : isDocless ( ) and self.settings . is_enabled end ,
2016-02-14 21:47:36 +00:00
} ,
2016-11-04 21:35:20 +00:00
{
text = _ ( " Reading progress " ) ,
2018-09-04 21:55:58 +00:00
keep_menu_open = true ,
2016-11-04 21:35:20 +00:00
callback = function ( )
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2020-10-15 03:31:21 +00:00
local current_duration , current_pages = self : getCurrentBookStats ( )
local today_duration , today_pages = self : getTodayBookStats ( )
2017-09-01 20:05:57 +00:00
local dates_stats = self : getReadingProgressStats ( 7 )
if dates_stats then
UIManager : show ( ReaderProgress : new {
dates = dates_stats ,
2020-10-15 03:31:21 +00:00
current_duration = current_duration ,
2017-09-01 20:05:57 +00:00
current_pages = current_pages ,
2020-10-15 03:31:21 +00:00
today_duration = today_duration ,
2017-09-01 20:05:57 +00:00
today_pages = today_pages ,
} )
else
UIManager : show ( InfoMessage : new {
2021-01-16 20:40:00 +00:00
text = _ ( " Reading progress is not available. \n There is no data for the last week. " ) ,
} )
2017-09-01 20:05:57 +00:00
end
2016-11-04 21:35:20 +00:00
end
} ,
2016-10-24 17:15:56 +00:00
{
text = _ ( " Time range " ) ,
2018-09-04 21:55:58 +00:00
keep_menu_open = true ,
2017-09-28 13:35:25 +00:00
callback = function ( )
self : statMenu ( )
end
2016-10-24 17:15:56 +00:00
} ,
2020-02-12 22:05:18 +00:00
{
text = _ ( " Calendar view " ) ,
keep_menu_open = true ,
callback = function ( )
2022-01-31 18:18:31 +00:00
self : onShowCalendarView ( )
2020-02-12 22:05:18 +00:00
end ,
} ,
2016-02-14 21:47:36 +00:00
} ,
2017-02-28 21:46:32 +00:00
}
2015-09-07 17:06:17 +00:00
end
2017-09-28 13:35:25 +00:00
function ReaderStatistics : statMenu ( )
self.kv = KeyValuePage : new {
title = _ ( " Time range statistics " ) ,
return_button = true ,
kv_pairs = {
{ _ ( " All books " ) , " " ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
local total_msg , kv_pairs = self : getTotalStats ( )
self.kv = KeyValuePage : new {
title = total_msg ,
2017-12-17 21:08:13 +00:00
value_align = " right " ,
2017-09-28 13:35:25 +00:00
kv_pairs = kv_pairs ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
2020-05-29 12:22:27 +00:00
{ _ ( " Books by week " ) , " " ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = _ ( " Books by week " ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2020-05-29 12:22:27 +00:00
kv_pairs = self : getDatesFromAll ( 0 , " weekly " , true ) ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
2020-05-29 12:22:27 +00:00
{ _ ( " Books by month " ) , " " ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = _ ( " Books by month " ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2020-05-29 12:22:27 +00:00
kv_pairs = self : getDatesFromAll ( 0 , " monthly " , true ) ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
2021-02-20 19:15:43 +00:00
separator = true ,
2017-09-28 13:35:25 +00:00
} ,
2020-05-29 12:22:27 +00:00
{ _ ( " Last week " ) , " " ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = _ ( " Last week " ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2020-05-29 12:22:27 +00:00
kv_pairs = self : getDatesFromAll ( 7 , " daily_weekday " ) ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
2020-05-29 12:22:27 +00:00
{ _ ( " Last month by day " ) , " " ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = _ ( " Last month by day " ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2020-05-29 12:22:27 +00:00
kv_pairs = self : getDatesFromAll ( 30 , " daily_weekday " ) ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
2020-05-29 12:22:27 +00:00
{ _ ( " Last year by day " ) , " " ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = _ ( " Last year by day " ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2020-05-29 12:22:27 +00:00
kv_pairs = self : getDatesFromAll ( 365 , " daily " ) ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
2020-05-29 12:22:27 +00:00
{ _ ( " Last year by week " ) , " " ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = _ ( " Last year by week " ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2020-05-29 12:22:27 +00:00
kv_pairs = self : getDatesFromAll ( 365 , " weekly " ) ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} ,
2020-05-29 12:22:27 +00:00
{ _ ( " All stats by month " ) , " " ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = _ ( " All stats by month " ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2020-05-29 12:22:27 +00:00
kv_pairs = self : getDatesFromAll ( 0 , " monthly " ) ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
2020-05-29 12:22:27 +00:00
} ,
2017-09-28 13:35:25 +00:00
}
}
UIManager : show ( self.kv )
end
2017-08-29 15:34:49 +00:00
function ReaderStatistics : getTodayBookStats ( )
2016-10-31 18:47:57 +00:00
local now_stamp = os.time ( )
local now_t = os.date ( " *t " )
local from_begin_day = now_t.hour * 3600 + now_t.min * 60 + now_t.sec
local start_today_time = now_stamp - from_begin_day
2017-08-29 15:34:49 +00:00
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT count ( * ) ,
2020-10-15 03:31:21 +00:00
sum ( sum_duration )
2017-08-29 15:34:49 +00:00
FROM (
2020-10-15 03:31:21 +00:00
SELECT sum ( duration ) AS sum_duration
2017-08-29 15:34:49 +00:00
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE start_time >= % d
2017-08-29 15:34:49 +00:00
GROUP BY id_book , page
2020-10-15 03:31:21 +00:00
) ;
2017-08-29 15:34:49 +00:00
] ]
2020-10-15 03:31:21 +00:00
local today_pages , today_duration = conn : rowexec ( string.format ( sql_stmt , start_today_time ) )
conn : close ( )
2017-08-29 15:34:49 +00:00
if today_pages == nil then
today_pages = 0
2015-09-07 17:06:17 +00:00
end
2020-10-15 03:31:21 +00:00
if today_duration == nil then
today_duration = 0
2017-08-29 15:34:49 +00:00
end
2020-10-15 03:31:21 +00:00
today_duration = tonumber ( today_duration )
2017-08-29 15:34:49 +00:00
today_pages = tonumber ( today_pages )
2020-10-15 03:31:21 +00:00
return today_duration , today_pages
2017-08-29 15:34:49 +00:00
end
2016-10-31 18:47:57 +00:00
2017-08-29 15:34:49 +00:00
function ReaderStatistics : getCurrentBookStats ( )
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT count ( * ) ,
2020-10-15 03:31:21 +00:00
sum ( sum_duration )
2017-08-29 15:34:49 +00:00
FROM (
2020-10-15 03:31:21 +00:00
SELECT sum ( duration ) AS sum_duration
2017-08-29 15:34:49 +00:00
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE start_time >= % d
2017-08-29 15:34:49 +00:00
GROUP BY id_book , page
2020-10-15 03:31:21 +00:00
) ;
2017-08-29 15:34:49 +00:00
] ]
2020-10-15 03:31:21 +00:00
local current_pages , current_duration = conn : rowexec ( string.format ( sql_stmt , self.start_current_period ) )
conn : close ( )
2017-08-29 15:34:49 +00:00
if current_pages == nil then
current_pages = 0
2016-10-31 18:47:57 +00:00
end
2020-10-15 03:31:21 +00:00
if current_duration == nil then
current_duration = 0
2017-08-29 15:34:49 +00:00
end
2020-10-15 03:31:21 +00:00
current_duration = tonumber ( current_duration )
2017-08-29 15:34:49 +00:00
current_pages = tonumber ( current_pages )
2020-10-15 03:31:21 +00:00
return current_duration , current_pages
2015-09-07 17:06:17 +00:00
end
2022-01-31 18:18:31 +00:00
function ReaderStatistics : getCurrentStat ( )
self : insertDB ( )
local id_book = self.id_curr_book
2020-10-15 03:31:21 +00:00
local today_duration , today_pages = self : getTodayBookStats ( )
local current_duration , current_pages = self : getCurrentBookStats ( )
2015-09-07 17:06:17 +00:00
2017-08-29 15:34:49 +00:00
local conn = SQ3.open ( db_location )
2020-10-15 03:31:21 +00:00
local highlights , notes = conn : rowexec ( string.format ( " SELECT highlights, notes FROM book WHERE id = %d; " , id_book ) ) -- luacheck: no unused
2017-08-29 15:34:49 +00:00
local sql_stmt = [ [
SELECT count ( * )
FROM (
SELECT strftime ( ' %%Y-%%m-%%d ' , start_time , ' unixepoch ' , ' localtime ' ) AS dates
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE id_book = % d
2017-08-29 15:34:49 +00:00
GROUP BY dates
2020-10-15 03:31:21 +00:00
) ;
2017-08-29 15:34:49 +00:00
] ]
local total_days = conn : rowexec ( string.format ( sql_stmt , id_book ) )
2020-05-29 12:22:27 +00:00
2020-10-15 03:31:21 +00:00
-- NOTE: Here, we generally want to account for the *full* amount of time spent reading this book.
2017-08-29 15:34:49 +00:00
sql_stmt = [ [
2020-10-15 03:31:21 +00:00
SELECT sum ( duration ) ,
2020-05-29 12:22:27 +00:00
count ( DISTINCT page ) ,
min ( start_time )
2017-08-29 15:34:49 +00:00
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE id_book = % d ;
2017-08-29 15:34:49 +00:00
] ]
2020-05-29 12:22:27 +00:00
local total_time_book , total_read_pages , first_open = conn : rowexec ( string.format ( sql_stmt , id_book ) )
2017-08-29 15:34:49 +00:00
conn : close ( )
2020-10-15 03:31:21 +00:00
-- NOTE: But, as the "Average time per page" entry is already re-using self.avg_time,
-- which is computed slightly differently (c.f., insertDB), we'll be using this tweaked book read time
-- to compute the other time-based statistics...
2020-10-17 21:22:23 +00:00
local __ , book_read_time = self : getPageTimeTotalStats ( id_book )
2020-10-18 18:38:17 +00:00
local now_ts = os.time ( )
2020-10-15 03:31:21 +00:00
2017-08-29 15:34:49 +00:00
if total_time_book == nil then
total_time_book = 0
end
if total_read_pages == nil then
total_read_pages = 0
end
2020-05-29 12:22:27 +00:00
if first_open == nil then
2020-10-18 18:38:17 +00:00
first_open = now_ts
2020-05-29 12:22:27 +00:00
end
2017-08-29 15:34:49 +00:00
self.data . pages = self.view . document : getPageCount ( )
total_time_book = tonumber ( total_time_book )
total_read_pages = tonumber ( total_read_pages )
2021-09-11 18:17:18 +00:00
local time_to_read = self.view . state.page and ( ( self.data . pages - self.view . state.page ) * self.avg_time ) or 0
2020-10-15 03:31:21 +00:00
local estimate_days_to_read = math.ceil ( time_to_read / ( book_read_time / tonumber ( total_days ) ) )
2020-10-18 18:38:17 +00:00
local estimate_end_of_read_date = os.date ( " %Y-%m-%d " , tonumber ( now_ts + estimate_days_to_read * 86400 ) )
2021-02-20 19:15:45 +00:00
local estimates_valid = time_to_read > 0 -- above values could be 'nan' and 'nil'
2021-06-29 23:45:34 +00:00
local user_duration_format = G_reader_settings : readSetting ( " duration_format " , " classic " )
2017-08-29 15:34:49 +00:00
return {
2020-05-29 12:22:27 +00:00
-- Global statistics (may consider other books than current book)
-- since last resume
2021-06-29 23:45:34 +00:00
{ _ ( " Time spent reading this session " ) , util.secondsToClockDuration ( user_duration_format , current_duration , false ) } ,
2019-09-10 19:32:02 +00:00
{ _ ( " Pages read this session " ) , tonumber ( current_pages ) } ,
2020-05-29 12:22:27 +00:00
-- today
2021-06-29 23:45:34 +00:00
{ _ ( " Time spent reading today " ) , util.secondsToClockDuration ( user_duration_format , today_duration , false ) } ,
2021-02-20 19:15:43 +00:00
{ _ ( " Pages read today " ) , tonumber ( today_pages ) , separator = true } ,
2020-05-29 12:22:27 +00:00
-- Current book statistics
2020-10-15 03:31:21 +00:00
-- Includes re-reads
2021-06-29 23:45:34 +00:00
{ _ ( " Total time spent on this book " ) , util.secondsToClockDuration ( user_duration_format , total_time_book , false ) } ,
2021-03-06 21:44:18 +00:00
-- Capped to self.settings.max_sec per distinct page
2021-06-29 23:45:34 +00:00
{ _ ( " Time spent reading this book " ) , util.secondsToClockDuration ( user_duration_format , book_read_time , false ) } ,
2020-05-29 12:22:27 +00:00
-- per days
{ _ ( " Reading started " ) , os.date ( " %Y-%m-%d (%H:%M) " , tonumber ( first_open ) ) } ,
{ _ ( " Days reading this book " ) , tonumber ( total_days ) } ,
2021-06-29 23:45:34 +00:00
{ _ ( " Average time per day " ) , util.secondsToClockDuration ( user_duration_format , book_read_time / tonumber ( total_days ) , false ) } ,
2021-02-20 19:15:45 +00:00
-- per page (% read)
2021-06-29 23:45:34 +00:00
{ _ ( " Average time per page " ) , util.secondsToClockDuration ( user_duration_format , self.avg_time , false ) } ,
2021-02-20 19:15:45 +00:00
{ _ ( " Pages read " ) , string.format ( " %d (%d%%) " , total_read_pages , Math.round ( 100 * total_read_pages / self.data . pages ) ) } ,
-- current page (% completed)
{ _ ( " Current page/Total pages " ) , string.format ( " %d/%d (%d%%) " , self.curr_page , self.data . pages , Math.round ( 100 * self.curr_page / self.data . pages ) ) } ,
2020-05-29 12:22:27 +00:00
-- estimation, from current page to end of book
2021-06-29 23:45:34 +00:00
{ _ ( " Estimated time to read " ) , estimates_valid and util.secondsToClockDuration ( user_duration_format , time_to_read , false ) or _ ( " N/A " ) } ,
2021-02-20 19:15:45 +00:00
{ _ ( " Estimated reading finished " ) , estimates_valid and
T ( N_ ( " %1 (1 day) " , " %1 (%2 days) " , estimate_days_to_read ) , estimate_end_of_read_date , estimate_days_to_read )
or _ ( " N/A " ) } ,
-- highlights
2021-02-20 19:15:43 +00:00
{ _ ( " Highlights " ) , tonumber ( highlights ) , separator = true } ,
2020-05-29 12:22:27 +00:00
-- { _("Total notes"), tonumber(notes) }, -- not accurate, don't show it
2021-02-20 19:15:45 +00:00
{ _ ( " Show days " ) , _ ( " Tap to display " ) ,
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
title = T ( _ ( " Days reading %1 " ) , self.data . title ) ,
value_overflow_align = " right " ,
kv_pairs = self : getDatesForBook ( id_book ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
}
2017-08-29 15:34:49 +00:00
}
2015-11-23 19:37:36 +00:00
end
2017-09-23 17:51:58 +00:00
function ReaderStatistics : getBookStat ( id_book )
if id_book == nil then
return
end
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
2020-05-29 12:22:27 +00:00
SELECT title , authors , pages , last_open , highlights , notes
2017-09-23 17:51:58 +00:00
FROM book
2020-10-15 03:31:21 +00:00
WHERE id = % d ;
2017-09-23 17:51:58 +00:00
] ]
2020-05-29 12:22:27 +00:00
local title , authors , pages , last_open , highlights , notes = conn : rowexec ( string.format ( sql_stmt , id_book ) )
-- Due to some bug, some books opened around April 2020 might
-- have notes and highlight NULL in the DB.
-- See: https://github.com/koreader/koreader/issues/6190#issuecomment-633693940
-- (We made these last in the SQL so NULL/nil doesn't prevent
-- fetching the other fields.)
-- Show "?" when these values are not known (they will be
-- fixed next time this book is opened).
highlights = highlights and tonumber ( highlights ) or " ? "
notes = notes and tonumber ( notes ) or " ? " -- luacheck: no unused
2017-09-23 17:51:58 +00:00
sql_stmt = [ [
SELECT count ( * )
FROM (
SELECT strftime ( ' %%Y-%%m-%%d ' , start_time , ' unixepoch ' , ' localtime ' ) AS dates
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE id_book = % d
2017-09-23 17:51:58 +00:00
GROUP BY dates
2020-10-15 03:31:21 +00:00
) ;
2017-09-23 17:51:58 +00:00
] ]
local total_days = conn : rowexec ( string.format ( sql_stmt , id_book ) )
2020-10-15 03:31:21 +00:00
-- NOTE: Same general principle as getCurrentStat
2017-09-23 17:51:58 +00:00
sql_stmt = [ [
2020-10-15 03:31:21 +00:00
SELECT sum ( duration ) ,
2020-05-29 12:22:27 +00:00
count ( DISTINCT page ) ,
2021-02-20 19:15:45 +00:00
min ( start_time ) ,
( select max ( ps2.page ) from page_stat as ps2 where ps2.start_time = max ( page_stat.start_time ) )
2017-09-23 17:51:58 +00:00
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE id_book = % d ;
2017-09-23 17:51:58 +00:00
] ]
2021-02-20 19:15:45 +00:00
local total_time_book , total_read_pages , first_open , last_page = conn : rowexec ( string.format ( sql_stmt , id_book ) )
2017-09-23 17:51:58 +00:00
conn : close ( )
2020-10-15 03:31:21 +00:00
local book_read_pages , book_read_time = self : getPageTimeTotalStats ( id_book )
2017-09-23 17:51:58 +00:00
if total_time_book == nil then
total_time_book = 0
end
if total_read_pages == nil then
total_read_pages = 0
end
2020-05-29 12:22:27 +00:00
if first_open == nil then
2020-10-15 03:31:21 +00:00
first_open = os.time ( )
2020-05-29 12:22:27 +00:00
end
2017-09-23 17:51:58 +00:00
total_time_book = tonumber ( total_time_book )
total_read_pages = tonumber ( total_read_pages )
2021-02-20 19:15:45 +00:00
last_page = tonumber ( last_page )
if last_page == nil then
last_page = 0
end
2017-09-23 17:51:58 +00:00
pages = tonumber ( pages )
if pages == nil or pages == 0 then
pages = 1
end
2020-10-15 03:31:21 +00:00
local avg_time_per_page = book_read_time / book_read_pages
2021-06-29 23:45:34 +00:00
local user_duration_format = G_reader_settings : readSetting ( " duration_format " )
2017-09-23 17:51:58 +00:00
return {
{ _ ( " Title " ) , title } ,
{ _ ( " Authors " ) , authors } ,
2020-05-29 12:22:27 +00:00
{ _ ( " Reading started " ) , os.date ( " %Y-%m-%d (%H:%M) " , tonumber ( first_open ) ) } ,
{ _ ( " Last read " ) , os.date ( " %Y-%m-%d (%H:%M) " , tonumber ( last_open ) ) } ,
{ _ ( " Days reading this book " ) , tonumber ( total_days ) } ,
2021-06-29 23:45:34 +00:00
{ _ ( " Total time spent on this book " ) , util.secondsToClockDuration ( user_duration_format , total_time_book , false ) } ,
{ _ ( " Time spent reading this book " ) , util.secondsToClockDuration ( user_duration_format , book_read_time , false ) } ,
{ _ ( " Average time per day " ) , util.secondsToClockDuration ( user_duration_format , book_read_time / tonumber ( total_days ) , false ) } ,
{ _ ( " Average time per page " ) , util.secondsToClockDuration ( user_duration_format , avg_time_per_page , false ) } ,
2021-02-20 19:15:45 +00:00
{ _ ( " Pages read " ) , string.format ( " %d (%d%%) " , total_read_pages , Math.round ( 100 * total_read_pages / pages ) ) } ,
{ _ ( " Last read page/Total pages " ) , string.format ( " %d/%d (%d%%) " , last_page , pages , Math.round ( 100 * last_page / pages ) ) } ,
2021-02-20 19:15:43 +00:00
{ _ ( " Highlights " ) , highlights , separator = true } ,
2020-05-29 12:22:27 +00:00
-- { _("Total notes"), notes }, -- not accurate, don't show it
2017-09-23 17:51:58 +00:00
{ _ ( " Show days " ) , _ ( " Tap to display " ) ,
callback = function ( )
2017-09-28 13:35:25 +00:00
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = T ( _ ( " Days reading %1 " ) , title ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2017-09-23 17:51:58 +00:00
kv_pairs = self : getDatesForBook ( id_book ) ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
2017-09-23 17:51:58 +00:00
end ,
}
}
end
2017-09-28 13:35:25 +00:00
local function sqlDaily ( )
return
[ [
2017-08-29 15:34:49 +00:00
SELECT dates ,
count ( * ) AS pages ,
2020-10-15 03:31:21 +00:00
sum ( sum_duration ) AS durations ,
2017-08-29 15:34:49 +00:00
start_time
FROM (
SELECT strftime ( ' %%Y-%%m-%%d ' , start_time , ' unixepoch ' , ' localtime ' ) AS dates ,
2020-10-15 03:31:21 +00:00
sum ( duration ) AS sum_duration ,
2017-08-29 15:34:49 +00:00
start_time
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE start_time >= % d
2017-08-29 15:34:49 +00:00
GROUP BY id_book , page , dates
)
GROUP BY dates
2020-10-15 03:31:21 +00:00
ORDER BY dates DESC ;
2017-09-28 13:35:25 +00:00
] ]
end
local function sqlWeekly ( )
return
[ [
2017-08-29 15:34:49 +00:00
SELECT dates ,
count ( * ) AS pages ,
2020-10-15 03:31:21 +00:00
sum ( sum_duration ) AS durations ,
2017-08-29 15:34:49 +00:00
start_time
FROM (
SELECT strftime ( ' %%Y-%%W ' , start_time , ' unixepoch ' , ' localtime ' ) AS dates ,
2020-10-15 03:31:21 +00:00
sum ( duration ) AS sum_duration ,
2017-08-29 15:34:49 +00:00
start_time
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE start_time >= % d
2017-08-29 15:34:49 +00:00
GROUP BY id_book , page , dates
)
GROUP BY dates
2020-10-15 03:31:21 +00:00
ORDER BY dates DESC ;
2017-09-28 13:35:25 +00:00
] ]
end
local function sqlMonthly ( )
return
[ [
2017-08-29 15:34:49 +00:00
SELECT dates ,
count ( * ) AS pages ,
2020-10-15 03:31:21 +00:00
sum ( sum_duration ) AS durations ,
2017-08-29 15:34:49 +00:00
start_time
FROM (
SELECT strftime ( ' %%Y-%%m ' , start_time , ' unixepoch ' , ' localtime ' ) AS dates ,
2020-10-15 03:31:21 +00:00
sum ( duration ) AS sum_duration ,
2017-08-29 15:34:49 +00:00
start_time
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE start_time >= % d
2017-08-29 15:34:49 +00:00
GROUP BY id_book , page , dates
)
GROUP BY dates
2020-10-15 03:31:21 +00:00
ORDER BY dates DESC ;
2017-09-28 13:35:25 +00:00
] ]
end
function ReaderStatistics : callbackMonthly ( begin , finish , date_text , book_mode )
local kv = self.kv
UIManager : close ( kv )
if book_mode then
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = T ( _ ( " Books read in %1 " ) , date_text ) ,
2017-12-17 21:08:13 +00:00
value_align = " right " ,
2017-09-28 13:35:25 +00:00
kv_pairs = self : getBooksFromPeriod ( begin , finish ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
else
self.kv = KeyValuePage : new {
title = date_text ,
2017-12-17 21:08:13 +00:00
value_align = " right " ,
2017-09-28 13:35:25 +00:00
kv_pairs = self : getDaysFromPeriod ( begin , finish ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
end
UIManager : show ( self.kv )
end
function ReaderStatistics : callbackWeekly ( begin , finish , date_text , book_mode )
local kv = self.kv
UIManager : close ( kv )
if book_mode then
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = T ( _ ( " Books read in %1 " ) , date_text ) ,
2017-12-17 21:08:13 +00:00
value_align = " right " ,
2017-09-28 13:35:25 +00:00
kv_pairs = self : getBooksFromPeriod ( begin , finish ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
else
self.kv = KeyValuePage : new {
title = date_text ,
2017-12-17 21:08:13 +00:00
value_align = " right " ,
2017-09-28 13:35:25 +00:00
kv_pairs = self : getDaysFromPeriod ( begin , finish ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
end
UIManager : show ( self.kv )
end
function ReaderStatistics : callbackDaily ( begin , finish , date_text )
local kv = self.kv
UIManager : close ( kv )
self.kv = KeyValuePage : new {
title = date_text ,
2017-12-17 21:08:13 +00:00
value_align = " right " ,
2017-09-28 13:35:25 +00:00
kv_pairs = self : getBooksFromPeriod ( begin , finish ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end
-- sdays -> number of days to show
-- ptype -> daily - show daily without weekday name
-- daily_weekday - show daily with weekday name
-- weekly - show weekly
-- monthly - show monthly
-- book_mode = if true than show book in this period
function ReaderStatistics : getDatesFromAll ( sdays , ptype , book_mode )
local results = { }
local now_t = os.date ( " *t " )
local from_begin_day = now_t.hour * 3600 + now_t.min * 60 + now_t.sec
local now_stamp = os.time ( )
local one_day = 86400 -- one day in seconds
local period_begin = 0
2021-06-29 23:45:34 +00:00
local user_duration_format = G_reader_settings : readSetting ( " duration_format " )
2017-09-28 13:35:25 +00:00
if sdays > 0 then
period_begin = now_stamp - ( ( sdays - 1 ) * one_day ) - from_begin_day
end
2020-10-15 03:31:21 +00:00
local sql_stmt_res_book
2017-09-28 13:35:25 +00:00
if ptype == " daily " or ptype == " daily_weekday " then
sql_stmt_res_book = sqlDaily ( )
elseif ptype == " weekly " then
sql_stmt_res_book = sqlWeekly ( )
elseif ptype == " monthly " then
sql_stmt_res_book = sqlMonthly ( )
2017-08-29 15:34:49 +00:00
end
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2017-08-29 15:34:49 +00:00
local conn = SQ3.open ( db_location )
local result_book = conn : exec ( string.format ( sql_stmt_res_book , period_begin ) )
conn : close ( )
2020-10-15 03:31:21 +00:00
2017-08-29 15:34:49 +00:00
if result_book == nil then
return { }
end
for i = 1 , # result_book.dates do
2020-10-15 03:31:21 +00:00
local timestamp = tonumber ( result_book [ 4 ] [ i ] )
2017-08-29 15:34:49 +00:00
local date_text
if ptype == " daily_weekday " then
date_text = string.format ( " %s (%s) " ,
2017-09-28 13:35:25 +00:00
os.date ( " %Y-%m-%d " , timestamp ) ,
shortDayOfWeekTranslation [ os.date ( " %a " , timestamp ) ] )
2017-08-29 15:34:49 +00:00
elseif ptype == " daily " then
date_text = result_book [ 1 ] [ i ]
elseif ptype == " weekly " then
2017-09-28 13:35:25 +00:00
date_text = T ( _ ( " %1 Week %2 " ) , os.date ( " %Y " , timestamp ) , os.date ( " %W " , timestamp ) )
2017-08-29 15:34:49 +00:00
elseif ptype == " monthly " then
2017-09-28 13:35:25 +00:00
date_text = monthTranslation [ os.date ( " %B " , timestamp ) ] .. os.date ( " %Y " , timestamp )
2015-11-23 19:37:36 +00:00
else
2017-08-29 15:34:49 +00:00
date_text = result_book [ 1 ] [ i ]
2015-11-23 19:37:36 +00:00
end
2017-09-09 18:19:05 +00:00
if ptype == " monthly " then
2020-10-15 03:31:21 +00:00
local year_begin = tonumber ( os.date ( " %Y " , timestamp ) )
local year_end
local month_begin = tonumber ( os.date ( " %m " , timestamp ) )
local month_end
2017-09-09 18:19:05 +00:00
if month_begin == 12 then
year_end = year_begin + 1
month_end = 1
else
year_end = year_begin
month_end = month_begin + 1
end
local start_month = os.time { year = year_begin , month = month_begin , day = 1 , hour = 0 , min = 0 }
local stop_month = os.time { year = year_end , month = month_end , day = 1 , hour = 0 , min = 0 }
table.insert ( results , {
date_text ,
2021-06-29 23:45:34 +00:00
T ( _ ( " Pages: (%1) Time: %2 " ) , tonumber ( result_book [ 2 ] [ i ] ) , util.secondsToClockDuration ( user_duration_format , tonumber ( result_book [ 3 ] [ i ] ) , false ) ) ,
2017-09-09 18:19:05 +00:00
callback = function ( )
2017-09-28 13:35:25 +00:00
self : callbackMonthly ( start_month , stop_month , date_text , book_mode )
end ,
} )
elseif ptype == " weekly " then
local time_book = os.date ( " %H%M%S%w " , timestamp )
local begin_week = tonumber ( result_book [ 4 ] [ i ] ) - 3600 * tonumber ( string.sub ( time_book , 1 , 2 ) )
- 60 * tonumber ( string.sub ( time_book , 3 , 4 ) ) - tonumber ( string.sub ( time_book , 5 , 6 ) )
local weekday = tonumber ( string.sub ( time_book , 7 , 8 ) )
if weekday == 0 then weekday = 6 else weekday = weekday - 1 end
begin_week = begin_week - weekday * 86400
table.insert ( results , {
date_text ,
2021-06-29 23:45:34 +00:00
T ( _ ( " Pages: (%1) Time: %2 " ) , tonumber ( result_book [ 2 ] [ i ] ) , util.secondsToClockDuration ( user_duration_format , tonumber ( result_book [ 3 ] [ i ] ) , false ) ) ,
2017-09-28 13:35:25 +00:00
callback = function ( )
self : callbackWeekly ( begin_week , begin_week + 7 * 86400 , date_text , book_mode )
2017-09-09 18:19:05 +00:00
end ,
} )
else
2017-09-28 13:35:25 +00:00
local time_book = os.date ( " %H%M%S " , timestamp )
local begin_day = tonumber ( result_book [ 4 ] [ i ] ) - 3600 * tonumber ( string.sub ( time_book , 1 , 2 ) )
- 60 * tonumber ( string.sub ( time_book , 3 , 4 ) ) - tonumber ( string.sub ( time_book , 5 , 6 ) )
2017-09-09 18:19:05 +00:00
table.insert ( results , {
date_text ,
2021-06-29 23:45:34 +00:00
T ( _ ( " Pages: (%1) Time: %2 " ) , tonumber ( result_book [ 2 ] [ i ] ) , util.secondsToClockDuration ( user_duration_format , tonumber ( result_book [ 3 ] [ i ] ) , false ) ) ,
2017-09-28 13:35:25 +00:00
callback = function ( )
self : callbackDaily ( begin_day , begin_day + 86400 , date_text )
end ,
2017-09-09 18:19:05 +00:00
} )
end
end
return results
end
function ReaderStatistics : getDaysFromPeriod ( period_begin , period_end )
local results = { }
local sql_stmt_res_book = [ [
SELECT dates ,
count ( * ) AS pages ,
2020-10-15 03:31:21 +00:00
sum ( sum_duration ) AS durations ,
2017-09-09 18:19:05 +00:00
start_time
FROM (
SELECT strftime ( ' %%Y-%%m-%%d ' , start_time , ' unixepoch ' , ' localtime ' ) AS dates ,
2020-10-15 03:31:21 +00:00
sum ( duration ) AS sum_duration ,
2017-09-09 18:19:05 +00:00
start_time
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE start_time BETWEEN % d AND % d
2017-09-09 18:19:05 +00:00
GROUP BY id_book , page , dates
)
GROUP BY dates
2020-10-15 03:31:21 +00:00
ORDER BY dates DESC ;
2017-09-09 18:19:05 +00:00
] ]
local conn = SQ3.open ( db_location )
2020-10-15 03:31:21 +00:00
local result_book = conn : exec ( string.format ( sql_stmt_res_book , period_begin , period_end - 1 ) )
2017-09-09 18:19:05 +00:00
conn : close ( )
2020-10-15 03:31:21 +00:00
2017-09-09 18:19:05 +00:00
if result_book == nil then
return { }
end
2021-06-29 23:45:34 +00:00
local user_duration_format = G_reader_settings : readSetting ( " duration_format " )
2017-09-09 18:19:05 +00:00
for i = 1 , # result_book.dates do
2017-09-28 13:35:25 +00:00
local time_begin = os.time { year = string.sub ( result_book [ 1 ] [ i ] , 1 , 4 ) , month = string.sub ( result_book [ 1 ] [ i ] , 6 , 7 ) ,
day = string.sub ( result_book [ 1 ] [ i ] , 9 , 10 ) , hour = 0 , min = 0 , sec = 0 }
2017-08-29 15:34:49 +00:00
table.insert ( results , {
2017-09-09 18:19:05 +00:00
result_book [ 1 ] [ i ] ,
2021-06-29 23:45:34 +00:00
T ( _ ( " Pages: (%1) Time: %2 " ) , tonumber ( result_book [ 2 ] [ i ] ) , util.secondsToClockDuration ( user_duration_format , tonumber ( result_book [ 3 ] [ i ] ) , false ) ) ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( kv )
self.kv = KeyValuePage : new {
2020-05-29 12:22:27 +00:00
title = T ( _ ( " Books read %1 " ) , result_book [ 1 ] [ i ] ) ,
2017-11-20 17:08:55 +00:00
value_overflow_align = " right " ,
2017-09-28 13:35:25 +00:00
kv_pairs = self : getBooksFromPeriod ( time_begin , time_begin + 86400 ) ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
end ,
} )
end
return results
end
2020-05-29 12:22:27 +00:00
function ReaderStatistics : getBooksFromPeriod ( period_begin , period_end , callback_shows_days )
2017-09-28 13:35:25 +00:00
local results = { }
local sql_stmt_res_book = [ [
SELECT book_tbl.title AS title ,
2020-10-15 03:31:21 +00:00
sum ( page_stat_tbl.duration ) ,
2017-09-28 13:35:25 +00:00
count ( distinct page_stat_tbl.page ) ,
book_tbl.id
FROM page_stat AS page_stat_tbl , book AS book_tbl
2020-10-15 03:31:21 +00:00
WHERE page_stat_tbl.id_book = book_tbl.id AND page_stat_tbl.start_time BETWEEN % d AND % d
2017-09-28 13:35:25 +00:00
GROUP BY book_tbl.id
2020-10-15 03:31:21 +00:00
ORDER BY book_tbl.last_open DESC ;
2017-09-28 13:35:25 +00:00
] ]
local conn = SQ3.open ( db_location )
2020-10-15 03:31:21 +00:00
local result_book = conn : exec ( string.format ( sql_stmt_res_book , period_begin + 1 , period_end ) )
2017-09-28 13:35:25 +00:00
conn : close ( )
2020-10-15 03:31:21 +00:00
2017-09-28 13:35:25 +00:00
if result_book == nil then
return { }
end
2021-06-29 23:45:34 +00:00
local user_duration_format = G_reader_settings : readSetting ( " duration_format " )
2017-09-28 13:35:25 +00:00
for i = 1 , # result_book.title do
table.insert ( results , {
result_book [ 1 ] [ i ] ,
2021-06-29 23:45:34 +00:00
T ( _ ( " %1 (%2) " ) , util.secondsToClockDuration ( user_duration_format , tonumber ( result_book [ 2 ] [ i ] ) , false ) , tonumber ( result_book [ 3 ] [ i ] ) ) ,
2017-09-28 13:35:25 +00:00
callback = function ( )
local kv = self.kv
UIManager : close ( self.kv )
2020-05-29 12:22:27 +00:00
if callback_shows_days then -- not used currently by any code
self.kv = KeyValuePage : new {
title = T ( _ ( " Days reading %1 " ) , result_book [ 1 ] [ i ] ) ,
kv_pairs = self : getDatesForBook ( tonumber ( result_book [ 4 ] [ i ] ) ) ,
value_overflow_align = " right " ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
else
self.kv = KeyValuePage : new {
title = result_book [ 1 ] [ i ] ,
kv_pairs = self : getBookStat ( tonumber ( result_book [ 4 ] [ i ] ) ) ,
value_overflow_align = " right " ,
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
end
2017-09-28 13:35:25 +00:00
UIManager : show ( self.kv )
end ,
2022-01-31 18:18:37 +00:00
hold_callback = function ( kv_page , kv_item )
self : resetStatsForBookForPeriod ( result_book [ 4 ] [ i ] , period_begin , period_end , false , function ( )
kv_page : removeKeyValueItem ( kv_item ) -- Reset, refresh what's displayed
end )
end ,
2017-08-29 15:34:49 +00:00
} )
2015-11-23 19:37:36 +00:00
end
2017-08-29 15:34:49 +00:00
return results
end
function ReaderStatistics : getReadingProgressStats ( sdays )
local results = { }
local now_t = os.date ( " *t " )
local from_begin_day = now_t.hour * 3600 + now_t.min * 60 + now_t.sec
local now_stamp = os.time ( )
local one_day = 86400 -- one day in seconds
local period_begin = now_stamp - ( ( sdays - 1 ) * one_day ) - from_begin_day
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT dates ,
count ( * ) AS pages ,
2020-10-15 03:31:21 +00:00
sum ( sum_duration ) AS durations ,
2017-08-29 15:34:49 +00:00
start_time
FROM (
SELECT strftime ( ' %%Y-%%m-%%d ' , start_time , ' unixepoch ' , ' localtime ' ) AS dates ,
2020-10-15 03:31:21 +00:00
sum ( duration ) AS sum_duration ,
2017-08-29 15:34:49 +00:00
start_time
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE start_time >= % d
2017-08-29 15:34:49 +00:00
GROUP BY id_book , page , dates
)
GROUP BY dates
2020-10-15 03:31:21 +00:00
ORDER BY dates DESC ;
2017-08-29 15:34:49 +00:00
] ]
local result_book = conn : exec ( string.format ( sql_stmt , period_begin ) )
2020-10-15 03:31:21 +00:00
conn : close ( )
2017-09-01 20:05:57 +00:00
if not result_book then return end
2017-08-29 15:34:49 +00:00
for i = 1 , sdays do
2020-10-15 03:31:21 +00:00
local pages = tonumber ( result_book [ 2 ] [ i ] )
local duration = tonumber ( result_book [ 3 ] [ i ] )
local date_read = result_book [ 1 ] [ i ]
2017-08-29 15:34:49 +00:00
if pages == nil then pages = 0 end
2020-10-15 03:31:21 +00:00
if duration == nil then duration = 0 end
2017-08-29 15:34:49 +00:00
table.insert ( results , {
pages ,
2020-10-15 03:31:21 +00:00
duration ,
2017-08-29 15:34:49 +00:00
date_read
} )
end
return results
end
2017-09-23 17:51:58 +00:00
function ReaderStatistics : getDatesForBook ( id_book )
2017-08-29 15:34:49 +00:00
local results = { }
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT date ( start_time , ' unixepoch ' , ' localtime ' ) AS dates ,
count ( DISTINCT page ) AS pages ,
2022-01-31 18:18:37 +00:00
sum ( duration ) AS durations ,
min ( start_time ) AS min_start_time ,
max ( start_time ) AS max_start_time
2017-08-29 15:34:49 +00:00
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE id_book = % d
2017-08-29 15:34:49 +00:00
GROUP BY Date ( start_time , ' unixepoch ' , ' localtime ' )
2020-10-15 03:31:21 +00:00
ORDER BY dates DESC ;
2017-08-29 15:34:49 +00:00
] ]
local result_book = conn : exec ( string.format ( sql_stmt , id_book ) )
conn : close ( )
2020-10-15 03:31:21 +00:00
2017-08-29 15:34:49 +00:00
if result_book == nil then
return { }
end
2021-06-29 23:45:34 +00:00
local user_duration_format = G_reader_settings : readSetting ( " duration_format " )
2017-08-29 15:34:49 +00:00
for i = 1 , # result_book.dates do
table.insert ( results , {
result_book [ 1 ] [ i ] ,
2022-01-31 18:18:37 +00:00
T ( _ ( " Pages: (%1) Time: %2 " ) , tonumber ( result_book [ 2 ] [ i ] ) , util.secondsToClockDuration ( user_duration_format , tonumber ( result_book [ 3 ] [ i ] ) , false ) ) ,
hold_callback = function ( kv_page , kv_item )
self : resetStatsForBookForPeriod ( id_book , result_book [ 4 ] [ i ] , result_book [ 5 ] [ i ] , result_book [ 1 ] [ i ] , function ( )
kv_page : removeKeyValueItem ( kv_item ) -- Reset, refresh what's displayed
end )
end ,
2017-08-29 15:34:49 +00:00
} )
end
return results
2015-09-07 17:06:17 +00:00
end
2022-01-31 18:18:37 +00:00
function ReaderStatistics : resetStatsForBookForPeriod ( id_book , min_start_time , max_start_time , day_str , on_reset_confirmed_callback )
local confirm_text
if day_str then
-- From getDatesForBook(): we are showing a list of days, with book title at top title:
-- show the day string to confirm the long-press was on the right day
confirm_text = T ( _ ( " Do you want to reset statistics for day %1 for this book? " ) , day_str )
else
-- From getBooksFromPeriod(): we are showing a list of books, with the period as top title:
-- show the book title to confirm the long-press was on the right book
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT title
FROM book
WHERE id = % d ;
] ]
local book_title = conn : rowexec ( string.format ( sql_stmt , id_book ) )
conn : close ( )
confirm_text = T ( _ ( " Do you want to reset statistics for this period for book: \n %1 " ) , book_title )
end
UIManager : show ( ConfirmBox : new {
text = confirm_text ,
cancel_text = _ ( " Cancel " ) ,
cancel_callback = function ( )
return
end ,
ok_text = _ ( " Reset " ) ,
ok_callback = function ( )
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
DELETE FROM page_stat_data
WHERE id_book = ?
AND start_time between ? and ?
] ]
local stmt = conn : prepare ( sql_stmt )
stmt : reset ( ) : bind ( id_book , min_start_time , max_start_time ) : step ( )
stmt : close ( )
conn : close ( )
if on_reset_confirmed_callback then
on_reset_confirmed_callback ( )
end
end ,
} )
end
2016-02-15 10:18:10 +00:00
function ReaderStatistics : getTotalStats ( )
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2017-08-29 15:34:49 +00:00
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
2020-10-15 03:31:21 +00:00
SELECT sum ( duration )
FROM page_stat ;
2017-08-29 15:34:49 +00:00
] ]
local total_books_time = conn : rowexec ( sql_stmt )
if total_books_time == nil then
total_books_time = 0
end
2017-01-06 09:33:57 +00:00
local total_stats = { }
2017-08-29 15:34:49 +00:00
sql_stmt = [ [
SELECT id
FROM book
2020-10-15 03:31:21 +00:00
ORDER BY last_open DESC ;
2017-08-29 15:34:49 +00:00
] ]
local id_book_tbl = conn : exec ( sql_stmt )
local nr_books
if id_book_tbl ~= nil then
nr_books = # id_book_tbl.id
else
nr_books = 0
2017-01-06 09:33:57 +00:00
end
2021-06-29 23:45:34 +00:00
local user_duration_format = G_reader_settings : readSetting ( " duration_format " )
2017-08-29 15:34:49 +00:00
for i = 1 , nr_books do
local id_book = tonumber ( id_book_tbl [ 1 ] [ i ] )
sql_stmt = [ [
SELECT title
FROM book
2020-10-15 03:31:21 +00:00
WHERE id = % d ;
2017-08-29 15:34:49 +00:00
] ]
local book_title = conn : rowexec ( string.format ( sql_stmt , id_book ) )
sql_stmt = [ [
2020-10-15 03:31:21 +00:00
SELECT sum ( duration )
2017-08-29 15:34:49 +00:00
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE id_book = % d ;
2017-08-29 15:34:49 +00:00
] ]
2020-10-15 03:31:21 +00:00
local total_time_book = conn : rowexec ( string.format ( sql_stmt , id_book ) )
2017-08-29 15:34:49 +00:00
if total_time_book == nil then
total_time_book = 0
end
table.insert ( total_stats , {
book_title ,
2021-06-29 23:45:34 +00:00
util.secondsToClockDuration ( user_duration_format , total_time_book , false ) ,
2017-08-29 15:34:49 +00:00
callback = function ( )
2017-09-28 13:35:25 +00:00
local kv = self.kv
UIManager : close ( self.kv )
self.kv = KeyValuePage : new {
2017-08-29 15:34:49 +00:00
title = book_title ,
2017-09-23 17:51:58 +00:00
kv_pairs = self : getBookStat ( id_book ) ,
2020-05-29 12:22:27 +00:00
value_overflow_align = " right " ,
2017-09-28 13:35:25 +00:00
callback_return = function ( )
UIManager : show ( kv )
self.kv = kv
end
}
UIManager : show ( self.kv )
2017-08-29 15:34:49 +00:00
end ,
} )
end
conn : close ( )
2020-10-15 03:31:21 +00:00
2021-06-29 23:45:34 +00:00
return T ( _ ( " Total time spent reading: %1 " ) , util.secondsToClockDuration ( user_duration_format , total_books_time , false ) ) , total_stats
2015-09-07 17:06:17 +00:00
end
2020-02-16 00:03:12 +00:00
function ReaderStatistics : genResetBookSubItemTable ( )
local sub_item_table = { }
table.insert ( sub_item_table , {
2021-01-06 20:49:19 +00:00
text = _ ( " Reset statistics for the current book " ) ,
2020-02-16 00:03:12 +00:00
keep_menu_open = true ,
callback = function ( )
2021-01-06 20:49:19 +00:00
self : resetCurrentBook ( )
2020-02-16 00:03:12 +00:00
end ,
2021-03-06 21:44:18 +00:00
enabled_func = function ( ) return not self : isDocless ( ) and self.settings . is_enabled and self.id_curr_book end ,
2020-02-16 00:03:12 +00:00
separator = true ,
} )
2020-10-15 03:31:21 +00:00
table.insert ( sub_item_table , {
2021-01-06 20:49:19 +00:00
text = _ ( " Reset statistics per book " ) ,
2020-10-15 03:31:21 +00:00
keep_menu_open = true ,
callback = function ( )
2022-01-31 18:18:31 +00:00
self : resetPerBook ( )
2020-10-15 03:31:21 +00:00
end ,
separator = true ,
} )
2020-02-16 00:03:12 +00:00
local reset_minutes = { 1 , 5 , 15 , 30 , 60 }
for _ , minutes in ipairs ( reset_minutes ) do
local text = T ( N_ ( " Reset stats for books read for < 1 m " ,
" Reset stats for books read for < %1 m " ,
minutes ) , minutes )
table.insert ( sub_item_table , {
text = text ,
keep_menu_open = true ,
callback = function ( )
self : deleteBooksByTotalDuration ( minutes )
end ,
} )
end
return sub_item_table
end
2022-01-31 18:18:31 +00:00
function ReaderStatistics : resetPerBook ( )
2017-08-29 15:34:49 +00:00
local total_stats = { }
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2017-08-29 15:34:49 +00:00
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT id
FROM book
2020-10-15 03:31:21 +00:00
ORDER BY last_open DESC ;
2017-08-29 15:34:49 +00:00
] ]
local id_book_tbl = conn : exec ( sql_stmt )
local nr_books
if id_book_tbl ~= nil then
nr_books = # id_book_tbl.id
else
nr_books = 0
end
2021-06-29 23:45:34 +00:00
local user_duration_format = G_reader_settings : readSetting ( " duration_format " )
2017-08-29 15:34:49 +00:00
local total_time_book
2020-10-15 03:31:21 +00:00
local kv_reset_book
2017-08-29 15:34:49 +00:00
for i = 1 , nr_books do
local id_book = tonumber ( id_book_tbl [ 1 ] [ i ] )
sql_stmt = [ [
SELECT title
FROM book
2020-10-15 03:31:21 +00:00
WHERE id = % d ;
2017-08-29 15:34:49 +00:00
] ]
local book_title = conn : rowexec ( string.format ( sql_stmt , id_book ) )
sql_stmt = [ [
2020-10-15 03:31:21 +00:00
SELECT sum ( duration )
2017-08-29 15:34:49 +00:00
FROM page_stat
2020-10-15 03:31:21 +00:00
WHERE id_book = % d ;
2017-08-29 15:34:49 +00:00
] ]
total_time_book = conn : rowexec ( string.format ( sql_stmt , id_book ) )
if total_time_book == nil then
total_time_book = 0
2017-01-24 17:51:42 +00:00
end
2017-08-29 15:34:49 +00:00
if id_book ~= self.id_curr_book then
2016-10-24 17:15:56 +00:00
table.insert ( total_stats , {
2017-08-29 15:34:49 +00:00
book_title ,
2021-06-29 23:45:34 +00:00
util.secondsToClockDuration ( user_duration_format , total_time_book , false ) ,
2017-08-29 15:34:49 +00:00
id_book ,
2022-01-31 18:18:35 +00:00
callback = function ( kv_page , kv_item )
2017-08-29 15:34:49 +00:00
UIManager : show ( ConfirmBox : new {
text = T ( _ ( " Do you want to reset statistics for book: \n %1 " ) , book_title ) ,
cancel_text = _ ( " Cancel " ) ,
cancel_callback = function ( )
return
end ,
ok_text = _ ( " Reset " ) ,
ok_callback = function ( )
2022-01-31 18:18:35 +00:00
self : deleteBook ( id_book )
kv_page : removeKeyValueItem ( kv_item ) -- Reset, refresh what's displayed
2017-08-29 15:34:49 +00:00
end ,
2016-10-24 17:15:56 +00:00
} )
end ,
} )
2015-11-23 19:37:36 +00:00
end
end
2020-10-15 03:31:21 +00:00
conn : close ( )
2017-08-29 15:34:49 +00:00
kv_reset_book = KeyValuePage : new {
title = _ ( " Reset book statistics " ) ,
2017-12-17 21:08:13 +00:00
value_align = " right " ,
2017-08-29 15:34:49 +00:00
kv_pairs = total_stats ,
}
UIManager : show ( kv_reset_book )
2020-10-15 03:31:21 +00:00
end
function ReaderStatistics : resetCurrentBook ( )
-- Flush to db first, so we get a resetVolatileStats
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2020-10-15 03:31:21 +00:00
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
SELECT title
FROM book
WHERE id = % d ;
] ]
local book_title = conn : rowexec ( string.format ( sql_stmt , self.id_curr_book ) )
2017-08-29 15:34:49 +00:00
conn : close ( )
2020-10-15 03:31:21 +00:00
UIManager : show ( ConfirmBox : new {
text = T ( _ ( " Do you want to reset statistics for book: \n %1 " ) , book_title ) ,
cancel_text = _ ( " Cancel " ) ,
cancel_callback = function ( )
return
end ,
ok_text = _ ( " Reset " ) ,
ok_callback = function ( )
self : deleteBook ( self.id_curr_book )
-- We also need to reset the time/page/avg tracking
self.book_read_pages = 0
self.book_read_time = 0
2021-03-06 21:44:18 +00:00
self.avg_time = math.floor ( 0.50 * self.settings . max_sec )
2021-01-09 20:59:25 +00:00
logger.dbg ( " ReaderStatistics: Initializing average time per page at 50% of the max value, i.e., " , self.avg_time )
2020-10-15 03:31:21 +00:00
-- And the current volatile stats
self : resetVolatileStats ( os.time ( ) )
-- And re-create the Book's data in the book table and get its new ID...
self.id_curr_book = self : getIdBookDB ( )
end ,
} )
2017-08-29 15:34:49 +00:00
end
function ReaderStatistics : deleteBook ( id_book )
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
2020-10-15 03:31:21 +00:00
DELETE FROM book
WHERE id = ? ;
2017-08-29 15:34:49 +00:00
] ]
local stmt = conn : prepare ( sql_stmt )
stmt : reset ( ) : bind ( id_book ) : step ( )
sql_stmt = [ [
2020-10-15 03:31:21 +00:00
DELETE FROM page_stat_data
WHERE id_book = ? ;
2017-08-29 15:34:49 +00:00
] ]
stmt = conn : prepare ( sql_stmt )
stmt : reset ( ) : bind ( id_book ) : step ( )
stmt : close ( )
conn : close ( )
2015-09-07 17:06:17 +00:00
end
2020-02-16 00:03:12 +00:00
function ReaderStatistics : deleteBooksByTotalDuration ( max_total_duration_mn )
local max_total_duration_sec = max_total_duration_mn * 60
UIManager : show ( ConfirmBox : new {
text = T ( N_ ( " Permanently remove statistics for books read for less than 1 minute? " ,
" Permanently remove statistics for books read for less than %1 minutes? " ,
max_total_duration_mn ) , max_total_duration_mn ) ,
ok_text = _ ( " Remove " ) ,
ok_callback = function ( )
2020-02-17 15:53:09 +00:00
-- Allow following SQL statements to work even when doc less by
-- using -1 as the book id, as real book ids are positive.
local id_curr_book = self.id_curr_book or - 1
2020-02-16 00:03:12 +00:00
local conn = SQ3.open ( db_location )
local sql_stmt = [ [
2020-10-15 03:31:21 +00:00
DELETE FROM page_stat_data
WHERE id_book IN (
SELECT id FROM book WHERE id ! = ? AND ( total_read_time IS NULL OR total_read_time < ? )
) ;
2020-02-16 00:03:12 +00:00
] ]
local stmt = conn : prepare ( sql_stmt )
2020-02-17 15:53:09 +00:00
stmt : reset ( ) : bind ( id_curr_book , max_total_duration_sec ) : step ( )
2020-02-16 00:03:12 +00:00
sql_stmt = [ [
2020-10-15 03:31:21 +00:00
DELETE FROM book
WHERE id ! = ? AND ( total_read_time IS NULL OR total_read_time < ? ) ;
2020-02-16 00:03:12 +00:00
] ]
stmt = conn : prepare ( sql_stmt )
2020-02-17 15:53:09 +00:00
stmt : reset ( ) : bind ( id_curr_book , max_total_duration_sec ) : step ( )
2020-02-16 00:03:12 +00:00
stmt : close ( )
-- Get nb of deleted books
sql_stmt = [ [
2020-10-15 03:31:21 +00:00
SELECT changes ( ) ;
2020-02-16 00:03:12 +00:00
] ]
local nb_deleted = conn : rowexec ( sql_stmt )
nb_deleted = nb_deleted and tonumber ( nb_deleted ) or 0
if max_total_duration_mn >= 30 and nb_deleted >= 10 then
-- Do a VACUUM to reduce db size (but not worth doing if not much was removed)
2020-10-15 03:31:21 +00:00
conn : exec ( " PRAGMA temp_store = 2; " ) -- use memory for temp files
local ok , errmsg = pcall ( conn.exec , conn , " VACUUM; " ) -- this may take some time
2020-02-16 00:03:12 +00:00
if not ok then
logger.warn ( " Failed compacting statistics database: " , errmsg )
end
end
conn : close ( )
UIManager : show ( InfoMessage : new {
text = nb_deleted > 0 and T ( N_ ( " Statistics for 1 book removed. " ,
" Statistics for %1 books removed. " ,
nb_deleted ) , nb_deleted )
or T ( _ ( " No statistics removed. " ) )
} )
end ,
} )
end
2017-10-09 18:22:34 +00:00
function ReaderStatistics : onPosUpdate ( pos , pageno )
2017-10-10 06:49:13 +00:00
if self.curr_page ~= pageno then
self : onPageUpdate ( pageno )
end
2017-10-05 19:49:59 +00:00
end
2015-09-13 19:34:20 +00:00
function ReaderStatistics : onPageUpdate ( pageno )
2021-03-06 21:44:18 +00:00
if self : isDocless ( ) or not self.settings . is_enabled then
2017-01-06 09:33:57 +00:00
return
end
2020-10-15 03:31:21 +00:00
-- We only care about *actual* page turns ;)
if self.curr_page == pageno then
return
end
2022-01-31 18:18:33 +00:00
local closing = false
if pageno == false then -- from onCloseDocument()
closing = true
pageno = self.curr_page -- avoid issues in following code
end
2020-10-15 03:31:21 +00:00
self.pageturn_count = self.pageturn_count + 1
local now_ts = os.time ( )
-- Get the previous page's last timestamp (if there is one)
local page_data = self.page_stat [ self.curr_page ]
-- This is a list of tuples, in insertion order, we want the last one
local data_tuple = page_data and page_data [ # page_data ]
-- Tuple layout is { timestamp, duration }
local then_ts = data_tuple and data_tuple [ 1 ]
-- If we don't have a previous timestamp to compare to, abort early
if not then_ts then
logger.dbg ( " ReaderStatistics: No timestamp for previous page " , self.curr_page )
self.page_stat [ pageno ] = { { now_ts , 0 } }
self.curr_page = pageno
return
end
-- By now, we're sure that we actually have a tuple (and the rest of the code ensures they're sane, i.e., zero-initialized)
local curr_duration = data_tuple [ 2 ]
-- NOTE: If all goes well, given the earlier curr_page != pageno check, curr_duration should always be 0 here.
-- Compute the difference between now and the previous page's last timestamp
local diff_time = now_ts - then_ts
2021-03-06 21:44:18 +00:00
if diff_time >= self.settings . min_sec and diff_time <= self.settings . max_sec then
2020-10-15 03:31:21 +00:00
self.mem_read_time = self.mem_read_time + diff_time
-- If it's the first time we're computing a duration for this page, count it as read
if # page_data == 1 and curr_duration == 0 then
self.mem_read_pages = self.mem_read_pages + 1
2017-08-29 15:34:49 +00:00
end
2020-10-15 03:31:21 +00:00
-- Update the tuple with the computed duration
data_tuple [ 2 ] = curr_duration + diff_time
2021-03-06 21:44:18 +00:00
elseif diff_time > self.settings . max_sec then
self.mem_read_time = self.mem_read_time + self.settings . max_sec
2020-10-15 03:31:21 +00:00
if # page_data == 1 and curr_duration == 0 then
self.mem_read_pages = self.mem_read_pages + 1
2017-08-29 15:34:49 +00:00
end
2020-10-15 03:31:21 +00:00
-- Update the tuple with the computed duration
2021-03-06 21:44:18 +00:00
data_tuple [ 2 ] = curr_duration + self.settings . max_sec
2017-01-06 09:33:57 +00:00
end
2020-10-15 03:31:21 +00:00
2022-01-31 18:18:33 +00:00
if closing then
return -- current page data updated, nothing more needed
end
2020-10-15 03:31:21 +00:00
-- We want a flush to db every 50 page turns
if self.pageturn_count >= MAX_PAGETURNS_BEFORE_FLUSH then
2020-11-28 21:59:27 +00:00
-- I/O, delay until after the pageturn, but reset the count now, to avoid potentially scheduling multiple inserts...
self.pageturn_count = 0
UIManager : tickAfterNext ( function ( )
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2020-11-28 21:59:27 +00:00
-- insertDB will call resetVolatileStats for us ;)
end )
2015-09-07 17:06:17 +00:00
end
2020-10-15 03:31:21 +00:00
-- Update average time per page (if need be, insertDB will have updated the totals and cleared the volatiles)
-- NOTE: Until insertDB runs, while book_read_pages only counts *distinct* pages,
-- and while mem_read_pages does the same, there may actually be an overlap between the two!
-- (i.e., the same page may be counted as read both in total and in mem, inflating the pagecount).
-- Only insertDB will actually check that the count (and as such average time) is actually accurate.
if self.book_read_pages > 0 or self.mem_read_pages > 0 then
self.avg_time = ( self.book_read_time + self.mem_read_time ) / ( self.book_read_pages + self.mem_read_pages )
end
-- We're done, update the current page tracker
self.curr_page = pageno
-- And, in the new page's list, append a new tuple with the current timestamp and a placeholder duration
-- (duration will be computed on next pageturn)
local new_page_data = self.page_stat [ pageno ]
if new_page_data then
table.insert ( new_page_data , { now_ts , 0 } )
else
self.page_stat [ pageno ] = { { now_ts , 0 } }
2015-11-23 19:37:36 +00:00
end
end
-- For backward compatibility
function ReaderStatistics : importFromFile ( base_path , item )
2020-09-30 17:56:56 +00:00
item = util.trim ( item )
2016-02-15 13:58:46 +00:00
if item ~= " .stat " then
2020-09-15 18:39:32 +00:00
local statistic_file = FFIUtil.joinPath ( base_path , item )
2016-02-15 13:58:46 +00:00
if lfs.attributes ( statistic_file , " mode " ) == " directory " then
return
end
local ok , stored = pcall ( dofile , statistic_file )
if ok then
return stored
end
2015-09-07 17:06:17 +00:00
end
end
2015-09-13 19:34:20 +00:00
function ReaderStatistics : onCloseDocument ( )
2021-03-06 21:44:18 +00:00
if not self : isDocless ( ) and self.settings . is_enabled then
2015-11-23 19:37:36 +00:00
self.ui . doc_settings : saveSetting ( " stats " , self.data )
2022-01-31 18:18:33 +00:00
self : onPageUpdate ( false ) -- update current page duration
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2015-09-07 17:06:17 +00:00
end
end
2015-09-13 19:34:20 +00:00
function ReaderStatistics : onAddHighlight ( )
2021-03-06 21:44:18 +00:00
if self.settings . is_enabled then
2020-10-26 11:27:05 +00:00
self.data . highlights = self.data . highlights + 1
end
2017-08-29 15:34:49 +00:00
end
function ReaderStatistics : onDelHighlight ( )
2021-03-06 21:44:18 +00:00
if self.settings . is_enabled then
2020-10-26 11:27:05 +00:00
if self.data . highlights > 0 then
self.data . highlights = self.data . highlights - 1
end
2017-08-29 15:34:49 +00:00
end
2015-09-07 17:06:17 +00:00
end
2015-09-13 19:34:20 +00:00
function ReaderStatistics : onAddNote ( )
2021-03-06 21:44:18 +00:00
if self.settings . is_enabled then
2020-10-26 11:27:05 +00:00
self.data . notes = self.data . notes + 1
end
2015-09-07 17:06:17 +00:00
end
2020-10-15 03:31:21 +00:00
-- Triggered by auto_save_settings_interval_minutes
2015-10-26 14:06:52 +00:00
function ReaderStatistics : onSaveSettings ( )
2017-01-06 09:33:57 +00:00
if not self : isDocless ( ) then
self.ui . doc_settings : saveSetting ( " stats " , self.data )
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2017-08-29 15:34:49 +00:00
end
end
-- in case when screensaver starts
function ReaderStatistics : onSuspend ( )
if not self : isDocless ( ) then
self.ui . doc_settings : saveSetting ( " stats " , self.data )
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2022-01-31 18:18:33 +00:00
self : onReadingPaused ( )
2017-01-06 09:33:57 +00:00
end
2015-09-07 17:06:17 +00:00
end
-- screensaver off
2015-09-13 19:34:20 +00:00
function ReaderStatistics : onResume ( )
2020-10-15 03:31:21 +00:00
self.start_current_period = os.time ( )
2022-01-31 18:18:33 +00:00
self : onReadingResumed ( )
end
function ReaderStatistics : onReadingPaused ( )
if self : isDocless ( ) or not self.settings . is_enabled then
return
end
if not self._reading_paused_ts then
self._reading_paused_ts = os.time ( )
end
end
function ReaderStatistics : onReadingResumed ( )
if self : isDocless ( ) or not self.settings . is_enabled then
self._reading_paused_ts = nil
return
end
if self._reading_paused_ts then
-- Just add the pause duration to the current page start_time
local pause_duration = os.time ( ) - self._reading_paused_ts
local page_data = self.page_stat [ self.curr_page ]
local data_tuple = page_data and page_data [ # page_data ]
if data_tuple then
data_tuple [ 1 ] = data_tuple [ 1 ] + pause_duration
end
self._reading_paused_ts = nil
end
2015-09-07 17:06:17 +00:00
end
2015-11-23 19:37:36 +00:00
function ReaderStatistics : onReadSettings ( config )
2020-04-15 06:52:16 +00:00
self.data = config.data . stats or { }
2015-11-23 19:37:36 +00:00
end
2015-09-07 17:06:17 +00:00
2016-02-12 14:55:02 +00:00
function ReaderStatistics : onReaderReady ( )
2016-02-07 22:03:53 +00:00
-- we have correct page count now, do the actual initialization work
self : initData ( )
2020-07-12 18:47:49 +00:00
self.view . footer : onUpdateFooter ( )
2016-02-07 22:03:53 +00:00
end
2015-09-07 17:06:17 +00:00
2022-01-31 18:18:31 +00:00
function ReaderStatistics : onShowCalendarView ( )
self : insertDB ( )
2020-04-10 17:14:04 +00:00
local CalendarView = require ( " calendarview " )
2022-01-31 18:18:31 +00:00
UIManager : show ( CalendarView : new {
2020-04-10 17:14:04 +00:00
reader_statistics = self ,
monthTranslation = monthTranslation ,
shortDayOfWeekTranslation = shortDayOfWeekTranslation ,
longDayOfWeekTranslation = longDayOfWeekTranslation ,
2021-03-06 21:44:18 +00:00
start_day_of_week = self.settings . calendar_start_day_of_week ,
nb_book_spans = self.settings . calendar_nb_book_spans ,
show_hourly_histogram = self.settings . calendar_show_histogram ,
browse_future_months = self.settings . calendar_browse_future_months ,
2022-01-31 18:18:31 +00:00
} )
2020-04-10 17:14:04 +00:00
end
2020-02-12 22:05:18 +00:00
-- Used by calendarview.lua CalendarView
function ReaderStatistics : getFirstTimestamp ( )
local sql_stmt = [ [
SELECT min ( start_time )
2020-10-15 03:31:21 +00:00
FROM page_stat ;
2020-02-12 22:05:18 +00:00
] ]
local conn = SQ3.open ( db_location )
local first_ts = conn : rowexec ( sql_stmt )
conn : close ( )
return first_ts and tonumber ( first_ts ) or nil
end
function ReaderStatistics : getReadingRatioPerHourByDay ( month )
2020-10-21 16:49:08 +00:00
-- We used to have in the SQL statement (with ? = 'YYYY-MM'):
-- WHERE strftime('%Y-%m', start_time, 'unixepoch', 'localtime') = ?
-- but strftime()ing all start_time is slow.
-- Comverting the month into timestamp boundaries, and just comparing
-- integers, can be 5 times faster.
-- We let SQLite compute these timestamp boundaries from the provided
-- month; we need the start of the month to be a real date:
month = month .. " -01 "
2020-02-12 22:05:18 +00:00
local sql_stmt = [ [
SELECT
strftime ( ' %Y-%m-%d ' , start_time , ' unixepoch ' , ' localtime ' ) day ,
strftime ( ' %H ' , start_time , ' unixepoch ' , ' localtime ' ) hour ,
2020-10-15 03:31:21 +00:00
sum ( duration ) / 3600.0 ratio
2020-02-12 22:05:18 +00:00
FROM page_stat
2020-10-21 16:49:08 +00:00
WHERE start_time BETWEEN strftime ( ' %s ' , ? , ' utc ' )
AND strftime ( ' %s ' , ? , ' utc ' , ' +33 days ' , ' start of month ' , ' -1 second ' )
2020-02-12 22:05:18 +00:00
GROUP BY
strftime ( ' %Y-%m-%d ' , start_time , ' unixepoch ' , ' localtime ' ) ,
strftime ( ' %H ' , start_time , ' unixepoch ' , ' localtime ' )
2020-10-15 03:31:21 +00:00
ORDER BY day , hour ;
2020-02-12 22:05:18 +00:00
] ]
local conn = SQ3.open ( db_location )
local stmt = conn : prepare ( sql_stmt )
2020-10-21 16:49:08 +00:00
local res , nb = stmt : reset ( ) : bind ( month , month ) : resultset ( " i " )
2020-02-12 22:05:18 +00:00
stmt : close ( )
conn : close ( )
local per_day = { }
for i = 1 , nb do
local day , hour , ratio = res [ 1 ] [ i ] , res [ 2 ] [ i ] , res [ 3 ] [ i ]
if not per_day [ day ] then
per_day [ day ] = { }
end
-- +1 as histogram starts counting at 1
per_day [ day ] [ tonumber ( hour ) + 1 ] = ratio
end
return per_day
end
function ReaderStatistics : getReadBookByDay ( month )
2020-10-21 16:49:08 +00:00
month = month .. " -01 "
2020-02-12 22:05:18 +00:00
local sql_stmt = [ [
SELECT
strftime ( ' %Y-%m-%d ' , start_time , ' unixepoch ' , ' localtime ' ) day ,
2020-10-15 03:31:21 +00:00
sum ( duration ) durations ,
2020-02-12 22:05:18 +00:00
id_book book_id ,
book.title book_title
FROM page_stat
2020-10-15 03:31:21 +00:00
JOIN book ON book.id = page_stat.id_book
2020-10-21 16:49:08 +00:00
WHERE start_time BETWEEN strftime ( ' %s ' , ? , ' utc ' )
AND strftime ( ' %s ' , ? , ' utc ' , ' +33 days ' , ' start of month ' , ' -1 second ' )
2020-02-12 22:05:18 +00:00
GROUP BY
strftime ( ' %Y-%m-%d ' , start_time , ' unixepoch ' , ' localtime ' ) ,
id_book ,
title
2020-10-15 03:31:21 +00:00
ORDER BY day , durations desc , book_id , book_title ;
2020-02-12 22:05:18 +00:00
] ]
local conn = SQ3.open ( db_location )
local stmt = conn : prepare ( sql_stmt )
2020-10-21 16:49:08 +00:00
local res , nb = stmt : reset ( ) : bind ( month , month ) : resultset ( " i " )
2020-02-12 22:05:18 +00:00
stmt : close ( )
conn : close ( )
local per_day = { }
for i = 1 , nb do
-- (We don't care about the duration, we just needed it
-- to have the books in decreasing duration order)
local day , duration , book_id , book_title = res [ 1 ] [ i ] , res [ 2 ] [ i ] , res [ 3 ] [ i ] , res [ 4 ] [ i ] -- luacheck: no unused
if not per_day [ day ] then
per_day [ day ] = { }
end
table.insert ( per_day [ day ] , { id = tonumber ( book_id ) , title = tostring ( book_title ) } )
end
return per_day
end
2020-07-12 18:47:49 +00:00
function ReaderStatistics : onShowReaderProgress ( )
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2020-10-15 03:31:21 +00:00
local current_duration , current_pages = self : getCurrentBookStats ( )
local today_duration , today_pages = self : getTodayBookStats ( )
2020-07-12 18:47:49 +00:00
local dates_stats = self : getReadingProgressStats ( 7 )
2020-10-15 03:31:21 +00:00
local readingprogress
2020-07-12 18:47:49 +00:00
if dates_stats then
readingprogress = ReaderProgress : new {
dates = dates_stats ,
2020-10-15 03:31:21 +00:00
current_duration = current_duration ,
2020-07-12 18:47:49 +00:00
current_pages = current_pages ,
2020-10-15 03:31:21 +00:00
today_duration = today_duration ,
2020-07-12 18:47:49 +00:00
today_pages = today_pages ,
--readonly = true,
}
end
UIManager : show ( readingprogress )
end
function ReaderStatistics : onShowBookStats ( )
2021-03-06 21:44:18 +00:00
if self : isDocless ( ) or not self.settings . is_enabled then return end
2020-07-12 18:47:49 +00:00
local stats = KeyValuePage : new {
title = _ ( " Current statistics " ) ,
2022-01-31 18:18:31 +00:00
kv_pairs = self : getCurrentStat ( )
2020-07-12 18:47:49 +00:00
}
UIManager : show ( stats )
end
2022-01-04 20:58:56 +00:00
function ReaderStatistics : getCurrentBookReadPages ( )
if self : isDocless ( ) or not self.settings . is_enabled then return end
2022-01-31 18:18:31 +00:00
self : insertDB ( )
2022-01-04 20:58:56 +00:00
local sql_stmt = [ [
SELECT
page ,
min ( sum ( duration ) , ? ) AS durations ,
strftime ( " %s " , " now " ) - max ( start_time ) AS delay
FROM page_stat
WHERE id_book = ?
GROUP BY page
ORDER BY page ;
] ]
local conn = SQ3.open ( db_location )
local stmt = conn : prepare ( sql_stmt )
local res , nb = stmt : reset ( ) : bind ( self.settings . max_sec , self.id_curr_book ) : resultset ( " i " )
stmt : close ( )
conn : close ( )
local read_pages = { }
local max_duration = 0
for i = 1 , nb do
local page , duration , delay = res [ 1 ] [ i ] , res [ 2 ] [ i ] , res [ 3 ] [ i ]
page = tonumber ( page )
duration = tonumber ( duration )
delay = tonumber ( delay )
read_pages [ page ] = { duration , delay }
if duration > max_duration then
max_duration = duration
end
end
for page , info in pairs ( read_pages ) do
-- Make the value a duration ratio (vs capped or max duration)
read_pages [ page ] [ 1 ] = info [ 1 ] / max_duration
end
return read_pages
end
2016-02-07 22:03:53 +00:00
return ReaderStatistics