A new template function reading_progress() to get read progress for the specified book

This commit is contained in:
Kovid Goyal
2026-03-11 19:42:57 +05:30
parent d91b7645bd
commit 56baffee50
3 changed files with 107 additions and 15 deletions
+15 -8
View File
@@ -3300,14 +3300,21 @@ class Cache:
report_progress(i+1, len(book_ids), mi)
@read_api
def get_last_read_positions(self, book_id, fmt, user):
fmt = fmt.upper()
ans = []
for device, cfi, epoch, pos_frac in self.backend.execute(
'SELECT device,cfi,epoch,pos_frac FROM last_read_positions WHERE book=? AND format=? AND user=?',
(book_id, fmt, user)):
ans.append({'device':device, 'cfi': cfi, 'epoch':epoch, 'pos_frac':pos_frac})
return ans
def get_last_read_positions(self, book_id, fmt='', user='', order_by='', limit=0):
q = 'SELECT device,cfi,epoch,pos_frac,format,user FROM last_read_positions WHERE book=?'
bindings = [book_id]
if fmt:
q += ' AND format=?'
bindings.append(fmt.upper())
if user:
q += ' AND user=?'
bindings.append(user)
if order_by in ('pos_frac', 'epoch'):
q += f' ORDER BY {order_by} DESC'
if limit:
q += f' LIMIT {int(limit)}'
return tuple({'device':device, 'cfi': cfi, 'epoch':epoch, 'pos_frac':pos_frac, 'format': format, 'user': user}
for device, cfi, epoch, pos_frac, format, user in self.backend.execute(q, tuple(bindings)))
@write_api
def set_last_read_position(self, book_id, fmt, user='_', device='_', cfi=None, epoch=None, pos_frac=0):
+18 -7
View File
@@ -813,7 +813,8 @@ class ReadingTest(BaseTest):
epoch = time()
cache.set_last_read_position(1, 'EPUB', 'user', 'device', 'cFi', epoch, 0.3)
self.assertFalse(cache.get_last_read_positions(1, 'x', 'u'))
self.assertEqual(cache.get_last_read_positions(1, 'ePuB', 'user'), [{'epoch':epoch, 'device':'device', 'cfi':'cFi', 'pos_frac':0.3}])
self.assertEqual(cache.get_last_read_positions(1, 'ePuB', 'user'), ({
'epoch':epoch, 'device':'device', 'cfi':'cFi', 'pos_frac':0.3, 'format': 'EPUB', 'user': 'user'},))
cache.set_last_read_position(1, 'EPUB', 'user', 'device')
self.assertFalse(cache.get_last_read_positions(1, 'ePuB', 'user'))
# }}}
@@ -836,12 +837,22 @@ class ReadingTest(BaseTest):
db = self.init_cache(self.library_path)
db.create_custom_column('mult', 'CC1', 'composite', True, display={'composite_template': 'b,a,c'})
db.create_custom_column('pages', 'Pages', 'int', False)
# need an empty metadata object to pass to the formatter
db = self.init_legacy(self.library_path)
db.new_api.set_field('#pages', {1: '11', 2: '22', 3: '33'})
mi = db.get_metadata(1)
db.set_pages(2, 100, format='FMT1')
db.set_pages(2, 100, format='FMT2')
db.set_last_read_position(2, 'FMT1', pos_frac=0.25, cfi='epubcfi(/2)', epoch=2)
db.set_last_read_position(2, 'FMT2', pos_frac=0.35, cfi='epubcfi(/2)', epoch=1)
db.close()
db = self.init_cache(self.library_path)
db.set_field('#pages', {1: '11', 2: '22', 3: '33'})
mi = db.get_metadata(2)
# test reading_progress
def trp(expected, args=''):
self.assertEqual(expected, formatter.safe_format(f'{{id:reading_progress({args})}}', {}, 'TEMPLATE ERROR', mi))
trp('25 / 100')
trp('25%', ',percent')
trp('25%', '_,percent')
trp('0.35', ',pos_frac,furthest')
trp('0.25', ',pos_frac,furthest,fmt1')
# test width_from_pages
v = formatter.safe_format('{#pages:width_from_pages(2,2,0.5)}', {}, 'TEMPLATE ERROR', mi)
self.assertEqual(v, '1.0')
+74
View File
@@ -3267,6 +3267,80 @@ This function works only in the GUI and the content server.
raise ValueError(str(e))
class BuiltinReadingProgress(BuiltinFormatterFunction):
name = 'reading_progress'
arg_count = -1
category = DB_FUNCS
def __doc__getter__(self): return translate_ffml(
r'''
``reading_progress(book_id, [user, output_fmt, which, fmt])`` -- returns the reading progress, in the specified ''
output format.[/]The ``user`` parameter defaults to match any user. Use the value ``local`` to match reading progress in
the calibre e-book viewer. Use ``_`` to match reading progress for anonymous users of the Content server viewer.
Any other value matches the correcsponding username as used in the Content server.
The ``output_fmt`` paramter controls the format of the text returned by this function. It takes three values:
[LIST]
[*] ``page_count`` - the default outputs ``pages read / total pages``. If page counting is not enabled outputs percent read.
[*] ``percent`` - outputs percent read
[*] ``pos_frac`` - outputs a fraction between zero and one.
[/LIST]
The ``which`` parameter controls how the specific reading progress record for the specified ``user`` is selected.
There can be more than one record if the no user is specified or if the book has been read in multiple formats.
It accepts two values:
[LIST]
[*] ``most_recent`` - the progress of the most recent reader of the book (the default value)
[*] ``furthest`` - the furthest progress of all matching records
[/LIST]
The ``fmt`` parameter controls which book format is used. The default is to return records for all formats, the specific
record is then selected by the ``which`` parameter.
Some examples:
[CODE]
{id:reading_progress()} -- the reading progress as pages read / total pages
for the most recent reading session of this book
{id:reading_progress(,percent)} -- same as above, but as a percentage
{id:reading_progress(,pos_frac,furthest)} -- same as above, but as a fraction and using the
furthest progress on this book.
{id:reading_progress(bob,pos_frac,furthest,EPUB)} -- for the user "bob" and the "EPUB format
[/CODE]
''')
def evaluate(self, formatter, kwargs, mi, locals, book_id, *args):
if len(args) > 4:
raise ValueError(_('Incorrect number of arguments for function {0}').format('reading_progress'))
book_id = int(book_id)
for_user, output_fmt, which, fmt = '', 'page_count', 'most_recent', ''
match len(args):
case 4:
for_user, output_fmt, which, fmt = args
case 3:
for_user, output_fmt, which = args
case 2:
for_user, output_fmt = args
case 1:
for_user = args[0]
order_by = 'pos_frac' if which in ('furthest', 'farthest') else 'epoch'
pos_frac = 0
with (db := self.get_database(mi, formatter=formatter).new_api).safe_read_lock:
if records := db._get_last_read_positions(book_id, fmt=fmt, user=for_user, order_by=order_by, limit=1):
pos_frac = records[0]['pos_frac']
fmt = records[0]['format']
match output_fmt:
case 'percent':
return f'{pos_frac:.0%}'
case 'page_count':
page_count = 0
if pages := db._get_pages(book_id):
page_count = pages.pages
if page_count > 0:
return f'{int(pos_frac * page_count)} / {page_count}'
return f'{pos_frac:.0%}'
case _:
return str(pos_frac)
class BuiltinIsDarkMode(BuiltinFormatterFunction):
name = 'is_dark_mode'
arg_count = 0