Merge branch 'cache-get_book_path' of https://github.com/un-pogaz/calibre

This commit is contained in:
Kovid Goyal 2025-10-05 09:39:24 +05:30
commit 86945ccc46
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 53 additions and 41 deletions

View File

@ -983,6 +983,19 @@ class Cache:
return {v:k for k, v in self.fields[field].table.id_map.items()}
return {normalize_func(v):k for k, v in self.fields[field].table.id_map.items()}
@read_api
def get_book_path(self, book_id, sep=os.sep, unsafe=False):
'''
Return the relative book path for the given id.
Prefer this because you can choose the directory separator, default use the os one.
If unsafe is True, allow to return None if the book_id is not in the library.
'''
rslt = self._field_for('path', book_id)
if unsafe and not rslt:
return rslt
else:
return rslt.replace('/', sep)
@read_api
def author_data(self, author_ids=None):
'''
@ -1002,7 +1015,7 @@ class Cache:
kind of hash is backend dependent, but is usually SHA-256. '''
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
raise NoSuchFormat(f'Record {book_id} has no fmt: {fmt}')
return self.backend.format_hash(book_id, fmt, name, path)
@ -1032,7 +1045,7 @@ class Cache:
with self.safe_read_lock:
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
return {}
@ -1171,7 +1184,7 @@ class Cache:
@read_api
def cover_or_cache(self, book_id, timestamp, as_what='bytes'):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except AttributeError:
return False, None, None
return self.backend.cover_or_cache(path, timestamp, as_what)
@ -1179,7 +1192,7 @@ class Cache:
@read_api
def cover_last_modified(self, book_id):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except AttributeError:
return
return self.backend.cover_last_modified(path)
@ -1194,7 +1207,7 @@ class Cache:
case sensitivity into account).
'''
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except AttributeError:
return False
@ -1215,7 +1228,7 @@ class Cache:
path_map = {}
for book_id in book_ids:
try:
path_map[book_id] = self._field_for('path', book_id).replace('/', os.sep)
path_map[book_id] = self.get_book_path(book_id)
except AttributeError:
continue
self.backend.compress_covers(path_map, jpeg_quality, progress_callback)
@ -1232,7 +1245,7 @@ class Cache:
fmt = (fmt or '').upper()
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except (KeyError, AttributeError):
raise NoSuchFormat(f'Record {book_id} has no {fmt} file')
@ -1255,7 +1268,7 @@ class Cache:
'''
fmt = (fmt or '').upper()
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
return None
if path:
@ -1275,7 +1288,7 @@ class Cache:
fmt = (fmt or '').upper()
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
return False
return self.backend.has_format(book_id, fmt, name, path)
@ -1302,7 +1315,7 @@ class Cache:
fmt = original_fmt.partition('_')[2]
try:
ofmt_name = self.fields['formats'].format_fname(book_id, original_fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
return False
if self.backend.is_format_accessible(book_id, original_fmt, ofmt_name, path):
@ -1323,7 +1336,7 @@ class Cache:
ans = self.field_for('formats', book_id)
if verify_formats and ans:
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
return ()
@ -1627,7 +1640,7 @@ class Cache:
is_series = f.metadata['datatype'] == 'series'
update_path = name in {'title', 'authors'}
if update_path and iswindows:
paths = (x for x in (self._field_for('path', book_id) for book_id in book_id_to_val_map) if x)
paths = (x for x in (self.get_book_path(book_id, sep='/', unsafe=True) for book_id in book_id_to_val_map) if x)
self.backend.windows_check_if_files_in_use(paths)
if is_series:
@ -1701,7 +1714,7 @@ class Cache:
try:
# While a book is being created, the path is empty. Don't bother to
# try to write the opf, because it will go to the wrong folder.
if self._field_for('path', book_id):
if self.get_book_path(book_id, unsafe=True):
mi = self._metadata_as_object_for_dump(book_id)
except Exception:
# This almost certainly means that the book has been deleted while
@ -1722,7 +1735,7 @@ class Cache:
@write_api
def write_backup(self, book_id, raw):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
return
@ -1737,7 +1750,7 @@ class Cache:
''' Return the OPF metadata backup for the book as a bytestring or None
if no such backup exists. '''
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
return
@ -1760,7 +1773,7 @@ class Cache:
callback(len(book_ids), True, False)
for book_id in book_ids:
if self._field_for('path', book_id) is None:
if self.get_book_path(book_id, unsafe=True) is None:
if callback is not None:
callback(book_id, None, False)
continue
@ -1787,10 +1800,10 @@ class Cache:
for book_id, data in iteritems(book_id_data_map):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except AttributeError:
self._update_path((book_id,))
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
self.backend.set_cover(book_id, path, data)
for cc in self.cover_caches:
@ -1934,14 +1947,13 @@ class Cache:
return dirtied
def _do_add_format(self, book_id, fmt, stream, name=None, mtime=None):
path = self._field_for('path', book_id)
path = self.get_book_path(book_id, unsafe=True)
if path is None:
# Theoretically, this should never happen, but apparently it
# does: https://www.mobileread.com/forums/showthread.php?t=233353
self._update_path({book_id}, mark_as_dirtied=False)
path = self._field_for('path', book_id)
path = self.get_book_path(book_id)
path = path.replace('/', os.sep)
title = self._field_for('title', book_id, default_value=_('Unknown'))
try:
author = self._field_for('authors', book_id, default_value=(_('Unknown'),))[0]
@ -2034,7 +2046,7 @@ class Cache:
metadata_map = {}
for book_id, fmts in iteritems(formats_map):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
continue
for fmt in fmts:
@ -2222,7 +2234,7 @@ class Cache:
path_map = {}
for book_id in book_ids:
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
path = None
path_map[book_id] = path
@ -2751,7 +2763,7 @@ class Cache:
@read_api
def get_top_level_move_items(self):
all_paths = {self._field_for('path', book_id).partition('/')[0] for book_id in self._all_book_ids()}
all_paths = {self.get_book_path(book_id, sep='/').partition('/')[0] for book_id in self._all_book_ids()}
return self.backend.get_top_level_move_items(all_paths)
@write_api
@ -2763,7 +2775,7 @@ class Cache:
except Exception:
traceback.print_exc()
all_paths = {self._field_for('path', book_id).partition('/')[0] for book_id in self._all_book_ids()}
all_paths = {self.get_book_path(book_id, sep='/').partition('/')[0] for book_id in self._all_book_ids()}
self.backend.move_library_to(all_paths, newloc, progress=progress_callback, abort=abort)
@read_api
@ -2913,7 +2925,7 @@ class Cache:
mi.cover = None
self._create_book_entry(mi, add_duplicates=True,
force_id=book_id, apply_import_tags=False, preserve_uuid=True)
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
self.backend.move_book_from_trash(book_id, path)
self.format_metadata_cache.pop(book_id, None)
f = self.fields['formats'].table
@ -3060,7 +3072,7 @@ class Cache:
if cdata:
mi.cover_data = ('jpeg', cdata)
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
continue
for fmt in fmts:
@ -3189,7 +3201,7 @@ class Cache:
dest.discard()
else:
fm['.cover'] = cover_key
bp = self.field_for('path', book_id)
bp = self.get_book_path(book_id, sep='/', unsafe=True)
extra_files[book_id] = ef = {}
if bp:
for (relpath, fobj, stat_result) in self.backend.iter_extra_files(book_id, bp, self.fields['formats']):
@ -3332,7 +3344,7 @@ class Cache:
@read_api
def are_paths_inside_book_dir(self, book_id, paths, sub_path=''):
try:
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
except Exception:
return set()
return {x for x in paths if self.backend.is_path_inside_book_dir(x, path, sub_path)}
@ -3340,7 +3352,7 @@ class Cache:
@write_api
def add_extra_files(self, book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False):
' Add extra data files '
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
added = {}
for relpath, stream_or_path in map_of_relpath_to_stream_or_path.items():
added[relpath] = bool(self.backend.add_extra_file(relpath, stream_or_path, path, replace, auto_rename))
@ -3350,7 +3362,7 @@ class Cache:
@write_api
def rename_extra_files(self, book_id, map_of_relpath_to_new_relpath, replace=False):
' Rename extra data files '
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
renamed = set()
for relpath, newrelpath in map_of_relpath_to_new_relpath.items():
if self.backend.rename_extra_file(relpath, newrelpath, path, replace):
@ -3380,7 +3392,7 @@ class Cache:
'''
Delete the specified extra files, either to Recycle Bin or permanently.
'''
path = self._field_for('path', book_id)
path = self.get_book_path(book_id, sep='/', unsafe=True)
if path:
self._clear_extra_files_cache(book_id)
return self.backend.remove_extra_files(path, relpaths, permanent)
@ -3403,7 +3415,7 @@ class Cache:
ans = self.extra_files_cache.setdefault(book_id, {}).get(pattern)
if ans is None or not use_cache:
ans = []
path = self._field_for('path', book_id)
path = self.get_book_path(book_id, sep='/', unsafe=True)
if path:
for (relpath, file_path, stat_result) in self.backend.iter_extra_files(
book_id, path, self.fields['formats'], yield_paths=True, pattern=pattern
@ -3414,7 +3426,7 @@ class Cache:
@read_api
def copy_extra_file_to(self, book_id, relpath, stream_or_path):
path = self._field_for('path', book_id).replace('/', os.sep)
path = self.get_book_path(book_id)
self.backend.copy_extra_file_to(book_id, path, relpath, stream_or_path)
@write_api
@ -3590,7 +3602,7 @@ def import_library(library_key, importer, library_path, progress=None, abort=Non
for fmt, fmtkey in fmt_key_map.items():
if fmt == '.cover':
with importer.start_file(fmtkey, _('Cover for %s') % title) as stream:
path = cache._field_for('path', book_id).replace('/', os.sep)
path = cache.get_book_path(book_id)
cache.backend.set_cover(book_id, path, stream, no_processing=True)
else:
with importer.start_file(fmtkey, _('{0} format for {1}').format(fmt.upper(), title)) as stream:
@ -3598,7 +3610,7 @@ def import_library(library_key, importer, library_path, progress=None, abort=Non
cache.fields['formats'].table.update_fmt(book_id, fmt, fname, size, cache.backend)
for relpath, efkey in extra_files.get(book_id, {}).items():
with importer.start_file(efkey, _('Extra file {0} for book {1}').format(relpath, title)) as stream:
path = cache._field_for('path', book_id).replace('/', os.sep)
path = cache.get_book_path(book_id)
cache.backend.add_extra_file(relpath, stream, path)
cache.dump_metadata({book_id})
if importer.corrupted_files:

View File

@ -112,7 +112,7 @@ def copy_one_book(
new_book_id = newdb.add_books(
[(mi, format_map)], add_duplicates=True, apply_import_tags=tweaks['add_new_book_tags_when_importing_books'],
preserve_uuid=preserve_uuid, run_hooks=False)[0][0]
bp = db.field_for('path', book_id)
bp = db.get_book_path(book_id, sep='/', unsafe=True)
if bp:
for (relpath, src_path, stat_result) in db.backend.iter_extra_files(book_id, bp, db.fields['formats'], yield_paths=True):
nbp = newdb.field_for('path', new_book_id)

View File

@ -325,7 +325,7 @@ class LibraryDatabase:
def path(self, index, index_is_id=False):
'Return the relative path to the directory containing this books files as a unicode string.'
book_id = index if index_is_id else self.id(index)
return self.new_api.field_for('path', book_id).replace('/', os.sep)
return self.new_api.get_book_path(book_id)
def abspath(self, index, index_is_id=False, create_dirs=True):
'Return the absolute path to the directory containing this books files as a unicode string.'

View File

@ -104,7 +104,7 @@ class FilesystemTest(BaseTest):
expected_contents.add(fname + '.' + fmt.lower())
ae(expected_contents, bookdir_contents)
fs_path = bookdir.split(os.sep)[-2:]
db_path = cache.field_for('path', book_id).split('/')
db_path = cache.get_book_path(book_id, sep='/').split('/')
ae(db_path, fs_path)
ae(initial_side_data, side_data(book_id))

View File

@ -379,7 +379,7 @@ class WritingTest(BaseTest):
def read_all_extra_files(book_id=1):
ans = {}
bp = cache.field_for('path', book_id)
bp = cache.get_book_path(book_id, sep='/')
for (relpath, fobj, stat_result) in cache.backend.iter_extra_files(book_id, bp, cache.fields['formats']):
ans[relpath] = fobj.read()
return ans

View File

@ -306,7 +306,7 @@ class BookInfo(QDialog, DropMixin):
dbn = db.new_api
mi = dbn.get_metadata(book_id, get_cover=False)
mi.cover_data = [None, dbn.cover(book_id, as_image=True)]
mi.path = dbn._field_for('path', book_id)
mi.path = dbn.get_book_path(book_id, sep='/')
mi.format_files = dbn.format_files(book_id)
mi.marked = db.data.get_marked(book_id)
mi.field_metadata = db.field_metadata