From 9199cb69052714ffede5e76ef11e2670ed04a9e9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 23 Feb 2025 20:01:43 +0530 Subject: [PATCH] Kobo driver: Automatically unkepubify books when exporting from the device Also, allow adding the virtual books since nowadays some of them are DRM free. DRMed books will still give an error when attempting to add them to the library. --- src/calibre/devices/kobo/driver.py | 43 ++++++++++++++++------- src/calibre/ebooks/oeb/polish/kepubify.py | 17 +++++++++ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 1e839940eb..3aba2d049c 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -26,11 +26,12 @@ from calibre.devices.kobo.books import Book, ImageWrapper, KTCollectionsBookList from calibre.devices.mime import mime_type_ext from calibre.devices.usbms.books import BookList, CollectionsBookList from calibre.devices.usbms.driver import USBMS +from calibre.ebooks import DRMError from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.utils import normalize_languages from calibre.prints import debug_print -from calibre.ptempfile import PersistentTemporaryFile, better_mktemp +from calibre.ptempfile import PersistentTemporaryFile, TemporaryDirectory, better_mktemp from calibre.utils.config_base import prefs from calibre.utils.date import parse_date from polyglot.builtins import iteritems, itervalues, string_or_bytes @@ -115,8 +116,7 @@ class KOBO(USBMS): SUPPORTS_SUB_DIRS = True SUPPORTS_ANNOTATIONS = True - # "kepubs" do not have an extension. The name looks like a GUID. Using an empty string seems to work. - VIRTUAL_BOOK_EXTENSIONS = frozenset(('kobo', '')) + VIRTUAL_BOOK_EXTENSIONS = frozenset(('kobo',)) EXTRA_CUSTOMIZATION_MESSAGE = [ _('The Kobo supports several collections including ')+ 'Read, Closed, Im_Reading. ' + _( @@ -773,7 +773,7 @@ class KOBO(USBMS): # Supported database version return True - def get_file(self, path, *args, **kwargs): + def get_file(self, path, outfile, end_session=True): tpath = self.munge_path(path) extension = os.path.splitext(tpath)[1] if extension == '.kobo': @@ -783,8 +783,20 @@ class KOBO(USBMS): 'instead they are rows in the sqlite database. ' 'Currently they cannot be exported or viewed.'), UserFeedback.WARN) + if tpath.lower().endswith(KEPUB_EXT + EPUB_EXT): + with TemporaryDirectory() as tdir: + outpath = os.path.join(tdir, 'file.epub') + from calibre.ebooks.oeb.polish.kepubify import unkepubify_path + try: + unkepubify_path(path, outpath, allow_overwrite=True) + except DRMError: + pass + else: + with open(outpath, 'rb') as src: + shutil.copyfile(src, outfile) + return - return USBMS.get_file(self, path, *args, **kwargs) + return USBMS.get_file(self, path, outfile, end_session=end_session) @classmethod def book_from_path(cls, prefix, lpath, title, authors, mime, date, ContentType, ImageID): @@ -1125,17 +1137,23 @@ class KOBO(USBMS): with no file extension. I just hope that decision causes them as much grief as it does me :-) - This has to make a temporary copy of the book files with a + This has to make a temporary copy of the book files with an epub extension to allow calibre's normal processing to deal with the file appropriately ''' for idx, path in enumerate(paths): - if path.find('kepub') >= 0: - with closing(open(path, 'rb')) as r: - tf = PersistentTemporaryFile(suffix='.epub') - shutil.copyfileobj(r, tf) - # tf.write(r.read()) - paths[idx] = tf.name + parts = path.replace(os.sep, '/').split('/') + if path.lower().endswith(KEPUB_EXT + EPUB_EXT) or ('kepub' in parts and '.' not in parts[-1]): + with PersistentTemporaryFile(suffix=EPUB_EXT) as dest: + pass + from calibre.ebooks.oeb.polish.kepubify import unkepubify_path + try: + unkepubify_path(path, dest.name, allow_overwrite=True) + except DRMError as e: + import traceback + paths[idx] = (path, e, traceback.format_exc()) + else: + paths[idx] = dest.name return paths @classmethod @@ -2332,7 +2350,6 @@ class KOBOTOUCH(KOBO): return result def _kepubify(self, path, name, mi, extra_css) -> None: - from calibre.ebooks.oeb.polish.errors import DRMError from calibre.ebooks.oeb.polish.kepubify import kepubify_path, make_options debug_print(f'Starting conversion of {mi.title} ({name}) to kepub') opts = make_options( diff --git a/src/calibre/ebooks/oeb/polish/kepubify.py b/src/calibre/ebooks/oeb/polish/kepubify.py index acc0569640..76b2a35a4b 100644 --- a/src/calibre/ebooks/oeb/polish/kepubify.py +++ b/src/calibre/ebooks/oeb/polish/kepubify.py @@ -28,6 +28,7 @@ from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, XHTML, XPath, escape_c from calibre.ebooks.oeb.parse_utils import barename, merge_multiple_html_heads_and_bodies from calibre.ebooks.oeb.polish.container import Container, EpubContainer, get_container from calibre.ebooks.oeb.polish.cover import find_cover_image, find_cover_image3, find_cover_page +from calibre.ebooks.oeb.polish.errors import DRMError from calibre.ebooks.oeb.polish.parsing import parse from calibre.ebooks.oeb.polish.tts import lang_for_elem from calibre.ebooks.oeb.polish.utils import extract, insert_self_closing @@ -516,8 +517,24 @@ def kepubify_path(path, outpath='', max_workers=0, allow_overwrite=False, opts: return outpath +def check_for_kobo_drm(container: Container) -> None: + # sadly rights.xml is not definitive as various dedrm tools leave it behind + has_rights_xml = container.has_name_and_is_not_empty('rights.xml') + if not has_rights_xml: + return + for name, is_linear in container.spine_names: + mt = container.mime_map[name] + if mt in OEB_DOCS: + with container.open(name, 'rb') as f: + raw = f.read(8192) + if b'