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.
This commit is contained in:
Kovid Goyal 2025-02-23 20:01:43 +05:30
parent 249c2d11a1
commit 9199cb6905
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 47 additions and 13 deletions

View File

@ -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(

View File

@ -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'<?xml' not in raw and b'<html' not in raw and KOBO_SPAN_CLASS.encode() not in raw:
raise DRMError()
break
def unkepubify_path(path, outpath='', max_workers=0, allow_overwrite=False):
container = get_container(path, tweak_mode=True, ebook_cls=EpubContainer)
check_for_kobo_drm(container)
unkepubify_container(container, max_workers)
base, ext = os.path.splitext(path)
outpath = outpath or base + '.epub'