diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 31659a9131..6612b72d60 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -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): diff --git a/src/calibre/db/tests/reading.py b/src/calibre/db/tests/reading.py index 367474c55f..d4eb723585 100644 --- a/src/calibre/db/tests/reading.py +++ b/src/calibre/db/tests/reading.py @@ -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') diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 4e4a2c22a8..19b18bc0d7 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -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