Properties3
TypeProject
Note createdMar 16, 2026
Last updatedMar 18, 2026

I like to track the number of pages I read per day, even though it is a wildly inconsistent metric that depends on the format: it’s not the same in a coffee table book or in my Kobo Libra Color. KOReader shows different metrics in the toolbar, but the closest to this one is the total number of pages read vs the total number of pages in the book. I can get the number I want by substracting, but I would prefer to get a number for that.

This note contains the code that is loaded via KOReader user patches to achieve that goal. The patch:

  • Adds a new footer item that shows ✓ N where N is unique pages read today
  • A page counts as “read” when you turn forward past it — opening a book shows 0, going backwards doesn’t count
  • On first render, queries the statistics SQLite database directly to seed the count with pages already read earlier today
  • Wraps ReaderFooter.onPageUpdate() and onPosUpdate() to track the previous page in an in-memory set when advancing, updating the counter instantly
  • Uses a shared set (pages_today_set) to deduplicate: re-visiting a page you already read today doesn’t increment the counter
  • Respects the calendar_day_start_hour/calendar_day_start_minute settings from the Statistics plugin (configurable in Reading Statistics > Calendar > “Daily timeline starts at”), so reading at 2 AM counts as the previous day if set to e.g. 05:00
  • Resets automatically when a new day starts (based on the configured day boundary)
  • Monkey-patches ReaderFooter.init() to inject the mode at position 1 in the footer’s mode list (the MODE table is local and inaccessible from patches), making it the default visible item
  • Force-enables itself on init because saved settings can persist a false value across reboots

Full Source

local ReaderFooter = require("apps/reader/modules/readerfooter")
local SQ3 = require("lua-ljsqlite3/init")
local DataStorage = require("datastorage")
 
local MODE_NAME = "session_pages"
local MODE_PREFIX = "✓"
local pages_today_set = {}
local pages_today_count = 0
local db_loaded = false
local loaded_day = nil
 
local function getStartOfDay()
    local day_start_hour = 0
    local day_start_minute = 0
    local stats_settings = G_reader_settings:readSetting("statistics")
    if stats_settings then
        day_start_hour = stats_settings.calendar_day_start_hour or 0
        day_start_minute = stats_settings.calendar_day_start_minute or 0
    end
    local now = os.time()
    local now_t = os.date("*t")
    local seconds_since_midnight = now_t.hour * 3600 + now_t.min * 60 + now_t.sec
    local day_start_offset = day_start_hour * 3600 + day_start_minute * 60
    local start_of_day = now - seconds_since_midnight + day_start_offset
    if seconds_since_midnight < day_start_offset then
        start_of_day = start_of_day - 86400
    end
    return start_of_day
end
 
local function getCurrentDay()
    local start = getStartOfDay()
    return os.date("%Y-%m-%d", start)
end
 
local function resetState()
    pages_today_set = {}
    pages_today_count = 0
    db_loaded = false
    loaded_day = nil
end
 
local function loadTodayPagesFromDB()
    local db_path = DataStorage:getSettingsDir() .. "/statistics.sqlite3"
    local ok, conn = pcall(SQ3.open, db_path)
    if not ok or not conn then return end
 
    local start_of_day = getStartOfDay()
    local sql = string.format([[
        SELECT id_book, page
        FROM page_stat
        WHERE start_time >= %d
        GROUP BY id_book, page
    ]], start_of_day)
 
    local rows = conn:exec(sql)
    if rows then
        local id_books = rows[1] or {}
        local pages = rows[2] or {}
        for i = 1, #id_books do
            local key = tostring(id_books[i]) .. ":" .. tostring(pages[i])
            if not pages_today_set[key] then
                pages_today_set[key] = true
                pages_today_count = pages_today_count + 1
            end
        end
    end
    conn:close()
end
 
local function ensureLoaded(footer)
    local today = getCurrentDay()
    if loaded_day and loaded_day ~= today then
        resetState()
    end
    if not db_loaded then
        local stats = footer.ui and footer.ui.statistics
        if stats then stats:insertDB() end
        loadTodayPagesFromDB()
        db_loaded = true
        loaded_day = today
    end
end
 
local function trackPage(self, pageno)
    if not pageno then return end
    local prev = self.pageno
    if not prev or pageno <= prev then return end
    local stats = self.ui and self.ui.statistics
    if not stats or not stats.id_curr_book then return end
    local key = tostring(stats.id_curr_book) .. ":" .. tostring(prev)
    if not pages_today_set[key] then
        pages_today_set[key] = true
        pages_today_count = pages_today_count + 1
    end
end
 
ReaderFooter.textGeneratorMap[MODE_NAME] = function(footer)
    ensureLoaded(footer)
    if footer.settings.all_at_once
            and footer.settings.hide_empty_generators
            and pages_today_count == 0 then
        return ""
    end
    return MODE_PREFIX .. " " .. tostring(pages_today_count)
end
 
ReaderFooter.default_settings[MODE_NAME] = true
 
local orig_onPageUpdate = ReaderFooter.onPageUpdate
ReaderFooter.onPageUpdate = function(self, pageno, ...)
    trackPage(self, pageno)
    return orig_onPageUpdate(self, pageno, ...)
end
 
local orig_onPosUpdate = ReaderFooter.onPosUpdate
ReaderFooter.onPosUpdate = function(self, pos, pageno, ...)
    trackPage(self, pageno)
    return orig_onPosUpdate(self, pos, pageno, ...)
end
 
local orig_init = ReaderFooter.init
ReaderFooter.init = function(self, ...)
    orig_init(self, ...)
    local already_present = false
    for _, name in pairs(self.mode_index) do
        if name == MODE_NAME then
            already_present = true
            break
        end
    end
    if not already_present then
        for i = self.mode_nb, 1, -1 do
            self.mode_index[i] = self.mode_index[i - 1]
        end
        self.mode_index[1] = MODE_NAME
        self.mode_nb = self.mode_nb + 1
        self.mode_list = {}
        for i = 0, self.mode_nb - 1 do
            self.mode_list[self.mode_index[i]] = i
        end
    end
    self.settings[MODE_NAME] = true
    self:updateFooterTextGenerator()
end
 
local orig_textOptionTitles = ReaderFooter.textOptionTitles
ReaderFooter.textOptionTitles = function(self, option, ...)
    if option == MODE_NAME then
        return "Pages read today (" .. MODE_PREFIX .. ")"
    end
    return orig_textOptionTitles(self, option, ...)
end