From 7ed08fe011d1ff38fe513324b5f1b858552b90f6 Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:09:15 +0200 Subject: [PATCH] add get_book_path() to Cache (DRY) --- src/calibre/db/cache.py | 84 +++++++++++++++------------ src/calibre/db/copy_to_library.py | 2 +- src/calibre/db/legacy.py | 2 +- src/calibre/db/tests/filesystem.py | 2 +- src/calibre/db/tests/writing.py | 2 +- src/calibre/gui2/dialogs/book_info.py | 2 +- 6 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 56d7bf13f3..3d62511afc 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -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: diff --git a/src/calibre/db/copy_to_library.py b/src/calibre/db/copy_to_library.py index f470a805be..4c82acb4cc 100644 --- a/src/calibre/db/copy_to_library.py +++ b/src/calibre/db/copy_to_library.py @@ -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) diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index 6771bebd47..76f5ec030b 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -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.' diff --git a/src/calibre/db/tests/filesystem.py b/src/calibre/db/tests/filesystem.py index fa7db6c358..79b88daf21 100644 --- a/src/calibre/db/tests/filesystem.py +++ b/src/calibre/db/tests/filesystem.py @@ -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)) diff --git a/src/calibre/db/tests/writing.py b/src/calibre/db/tests/writing.py index 908c37097f..293459f496 100644 --- a/src/calibre/db/tests/writing.py +++ b/src/calibre/db/tests/writing.py @@ -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 diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 02f2c830fc..fbbb7bb3ac 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -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