mirror of
https://github.com/Leseratte10/acsm-calibre-plugin.git
synced 2024-12-23 01:34:36 +06:00
v0.0.6: PDF support & importing activation backup support
This commit is contained in:
parent
b927a549c6
commit
ac655fdd9d
@ -1,6 +1,6 @@
|
|||||||
# Calibre ACSM plugin
|
# Calibre ACSM plugin
|
||||||
|
|
||||||
This is a Calibre plugin that allows you to turn ACSM files into EPUBs without the need for ADE.
|
This is a Calibre plugin that allows you to turn ACSM files into EPUB or PDF files without the need for ADE.
|
||||||
It is a full Python reimplementation of libgourou by Grégory Soutadé (http://indefero.soutade.fr/p/libgourou/).
|
It is a full Python reimplementation of libgourou by Grégory Soutadé (http://indefero.soutade.fr/p/libgourou/).
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
@ -12,15 +12,16 @@ It is a full Python reimplementation of libgourou by Grégory Soutadé (http://i
|
|||||||
5. The settings window should now say "Authorized with ADE ID X on device Y".
|
5. The settings window should now say "Authorized with ADE ID X on device Y".
|
||||||
6. Click the "Export account activation data" and "Export account encryption key" buttons to export / backup your keys. Do not skip this step. The first file (ZIP) can be used to re-authorize Calibre after a reset / reinstall without using up one of your Adobe authorizations. The second file (DER) can be imported into DeDRM.
|
6. Click the "Export account activation data" and "Export account encryption key" buttons to export / backup your keys. Do not skip this step. The first file (ZIP) can be used to re-authorize Calibre after a reset / reinstall without using up one of your Adobe authorizations. The second file (DER) can be imported into DeDRM.
|
||||||
7. If needed (new AdobeID), import the DER file into the DeDRM plugin.
|
7. If needed (new AdobeID), import the DER file into the DeDRM plugin.
|
||||||
8. Download an EPUB ACSM file from Adobe's test library and see if you can import it into Calibre: https://www.adobe.com/de/solutions/ebook/digital-editions/sample-ebook-library.html
|
8. Download an ACSM file from Adobe's test library and see if you can import it into Calibre: https://www.adobe.com/de/solutions/ebook/digital-editions/sample-ebook-library.html
|
||||||
|
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
|
|
||||||
- I would suggest creating a new dummy AdobeID to use for Calibre so just in case Adobe detects this and bans you, you don't lose your main AdobeID.
|
- I would suggest creating a new dummy AdobeID to use for Calibre so just in case Adobe detects this and bans you, you don't lose your main AdobeID.
|
||||||
- Combined with that I suggest importing the DER file into the DeDRM plugin to make sure that losing your AdobeID doesn't also mean you'll lose access to all your eBooks.
|
- Combined with that I suggest importing the DER file into the DeDRM plugin to make sure that losing your AdobeID doesn't also mean you'll lose access to all your eBooks.
|
||||||
- This plugin doesn't yet work with PDFs. Importing an ACSM file for a PDF book will just result in the ACSM file being imported, it won't be converted into a PDF.
|
- Support for PDFs might be unreliable. You will need to apply pull request #1689 (including my additional bugfix in the comments of that PR) to the DeDRM plugin in order to remove the DRM from PDF files. If you still encounter an issue with a PDF file created by this tool even with these bugfixes, please report a bug and attach the corrupted PDF (in this repository, not in the DeDRM one).
|
||||||
- This software is not approved by Adobe. I am not responsible if Adobe detects that you're using nonstandard software and bans your account. Do not complain to me if Adobe bans your main ADE account - you have been warned.
|
- This software is not approved by Adobe. I am not responsible if Adobe detects that you're using nonstandard software and bans your account. Do not complain to me if Adobe bans your main ADE account - you have been warned.
|
||||||
|
|
||||||
|
|
||||||
## Standalone version
|
## Standalone version
|
||||||
|
|
||||||
In the folder "calibre-plugin" in this repo (or inside the Calibre plugin ZIP file) there's some scripts that can also be used standalone without Calibre. If you want to use these, you need to extract the whole ZIP file.
|
In the folder "calibre-plugin" in this repo (or inside the Calibre plugin ZIP file) there's some scripts that can also be used standalone without Calibre. If you want to use these, you need to extract the whole ZIP file.
|
||||||
@ -37,7 +38,5 @@ There's a bunch of features that could still be added, but most of them aren't i
|
|||||||
|
|
||||||
- Support for anonymous Adobe IDs
|
- Support for anonymous Adobe IDs
|
||||||
- Support for un-authorizing a machine
|
- Support for un-authorizing a machine
|
||||||
- Support to re-import a backed-up authorization after a reinstallation (right now you can only do that manually)
|
|
||||||
- Support for PDFs
|
|
||||||
- Support for returning loan books
|
- Support for returning loan books
|
||||||
- ...
|
- ...
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
# v0.0.3: Standalone Calibre plugin for Linux, Windows, MacOS without the need for libgourou.
|
# v0.0.3: Standalone Calibre plugin for Linux, Windows, MacOS without the need for libgourou.
|
||||||
# v0.0.4: Manually execute DeDRM (if installed) after converting ACSM to EPUB.
|
# v0.0.4: Manually execute DeDRM (if installed) after converting ACSM to EPUB.
|
||||||
# v0.0.5: Bugfix: DeDRM plugin was also executed if it's installed but disabled.
|
# v0.0.5: Bugfix: DeDRM plugin was also executed if it's installed but disabled.
|
||||||
|
# v0.0.6: First PDF support, allow importing previously exported activation data.
|
||||||
|
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin # type: ignore
|
from calibre.customize import FileTypePlugin # type: ignore
|
||||||
__version__ = '0.0.5'
|
__version__ = '0.0.6'
|
||||||
|
|
||||||
PLUGIN_NAME = "DeACSM"
|
PLUGIN_NAME = "DeACSM"
|
||||||
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
|
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
|
||||||
@ -175,13 +176,22 @@ class DeACSM(FileTypePlugin):
|
|||||||
print("{0} v{1}: Error while importing Fulfillment stuff".format(PLUGIN_NAME, PLUGIN_VERSION))
|
print("{0} v{1}: Error while importing Fulfillment stuff".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from calibre_plugins.deacsm.libpdf import patch_drm_into_pdf, prepare_string_from_xml
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
from libpdf import patch_drm_into_pdf, prepare_string_from_xml
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Error while importing PDF patch".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
adobe_fulfill_response = etree.fromstring(replyData)
|
adobe_fulfill_response = etree.fromstring(replyData)
|
||||||
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
adDC = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
adDC = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
||||||
|
|
||||||
|
metadata_node = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("metadata")))
|
||||||
download_url = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("src"))).text
|
download_url = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("src"))).text
|
||||||
license_token_node = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken")))
|
license_token_node = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken")))
|
||||||
|
|
||||||
@ -205,6 +215,19 @@ class DeACSM(FileTypePlugin):
|
|||||||
|
|
||||||
filename = self.temporary_file(filetype).name
|
filename = self.temporary_file(filetype).name
|
||||||
|
|
||||||
|
author = "None"
|
||||||
|
title = "None"
|
||||||
|
|
||||||
|
try:
|
||||||
|
title = metadata_node.find("./%s" % (adDC("title"))).text
|
||||||
|
author = metadata_node.find("./%s" % (adDC("creator"))).text
|
||||||
|
|
||||||
|
title = title.replace("(", "").replace(")", "").replace("/", "")
|
||||||
|
author = author.replace("(", "").replace(")", "").replace("/", "")
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# Store book:
|
# Store book:
|
||||||
f = open(filename, "wb")
|
f = open(filename, "wb")
|
||||||
f.write(book_content)
|
f.write(book_content)
|
||||||
@ -219,13 +242,13 @@ class DeACSM(FileTypePlugin):
|
|||||||
return filename
|
return filename
|
||||||
|
|
||||||
elif filetype == ".pdf":
|
elif filetype == ".pdf":
|
||||||
print("Successfully downloaded PDF, but PDF encryption is not yet supported")
|
print("{0} v{1}: Downloaded PDF, adding encryption config ...".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
print("You will not be able to use the downloaded PDF file")
|
pdf_tmp_file = self.temporary_file(filetype).name
|
||||||
print("Here's the raw string:")
|
patch_drm_into_pdf(filename, prepare_string_from_xml(rights_xml_str, author, title), pdf_tmp_file)
|
||||||
print(rights_xml_str)
|
print("{0} v{1}: File successfully fulfilled ...".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
return None
|
return pdf_tmp_file
|
||||||
else:
|
else:
|
||||||
print("Error: Weird filetype")
|
print("{0} v{1}: Error: Unsupported file type ...".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def run(self, path_to_ebook: str):
|
def run(self, path_to_ebook: str):
|
||||||
|
@ -16,7 +16,7 @@ from PyQt5 import Qt as QtGui
|
|||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
# calibre modules and constants.
|
# calibre modules and constants.
|
||||||
from calibre.gui2 import (question_dialog, error_dialog, info_dialog, choose_save_file) # type: ignore
|
from calibre.gui2 import (question_dialog, error_dialog, info_dialog, choose_save_file, choose_files) # type: ignore
|
||||||
# modules from this plugin's zipfile.
|
# modules from this plugin's zipfile.
|
||||||
from calibre_plugins.deacsm.__init__ import PLUGIN_NAME, PLUGIN_VERSION # type: ignore
|
from calibre_plugins.deacsm.__init__ import PLUGIN_NAME, PLUGIN_VERSION # type: ignore
|
||||||
import calibre_plugins.deacsm.prefs as prefs # type: ignore
|
import calibre_plugins.deacsm.prefs as prefs # type: ignore
|
||||||
@ -47,7 +47,7 @@ class ConfigWidget(QWidget):
|
|||||||
ua_group_box_layout = QVBoxLayout()
|
ua_group_box_layout = QVBoxLayout()
|
||||||
ua_group_box.setLayout(ua_group_box_layout)
|
ua_group_box.setLayout(ua_group_box_layout)
|
||||||
|
|
||||||
info_string, activated = self.get_account_info()
|
info_string, activated, mail = self.get_account_info()
|
||||||
|
|
||||||
self.lblAccInfo = QtGui.QLabel(self)
|
self.lblAccInfo = QtGui.QLabel(self)
|
||||||
self.lblAccInfo.setText(info_string)
|
self.lblAccInfo.setText(info_string)
|
||||||
@ -59,6 +59,11 @@ class ConfigWidget(QWidget):
|
|||||||
self.button_link_account.clicked.connect(self.link_account)
|
self.button_link_account.clicked.connect(self.link_account)
|
||||||
ua_group_box_layout.addWidget(self.button_link_account)
|
ua_group_box_layout.addWidget(self.button_link_account)
|
||||||
|
|
||||||
|
self.button_import_activation = QtGui.QPushButton(self)
|
||||||
|
self.button_import_activation.setText(_("Import existing activation data (ZIP)"))
|
||||||
|
self.button_import_activation.clicked.connect(self.import_activation)
|
||||||
|
ua_group_box_layout.addWidget(self.button_import_activation)
|
||||||
|
|
||||||
self.button_export_key = QtGui.QPushButton(self)
|
self.button_export_key = QtGui.QPushButton(self)
|
||||||
self.button_export_key.setText(_("Export account encryption key"))
|
self.button_export_key.setText(_("Export account encryption key"))
|
||||||
self.button_export_key.clicked.connect(self.export_key)
|
self.button_export_key.clicked.connect(self.export_key)
|
||||||
@ -71,6 +76,7 @@ class ConfigWidget(QWidget):
|
|||||||
self.button_export_activation.setEnabled(activated)
|
self.button_export_activation.setEnabled(activated)
|
||||||
ua_group_box_layout.addWidget(self.button_export_activation)
|
ua_group_box_layout.addWidget(self.button_export_activation)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from calibre_plugins.deacsm.libadobe import VAR_HOBBES_VERSION, createDeviceKeyFile, update_account_path
|
from calibre_plugins.deacsm.libadobe import VAR_HOBBES_VERSION, createDeviceKeyFile, update_account_path
|
||||||
from calibre_plugins.deacsm.libadobeAccount import createDeviceFile, createUser, signIn, activateDevice
|
from calibre_plugins.deacsm.libadobeAccount import createDeviceFile, createUser, signIn, activateDevice
|
||||||
@ -97,7 +103,7 @@ class ConfigWidget(QWidget):
|
|||||||
container = etree.parse(activation_xml_path)
|
container = etree.parse(activation_xml_path)
|
||||||
containerdev = etree.parse(device_xml_path)
|
containerdev = etree.parse(device_xml_path)
|
||||||
except (FileNotFoundError, OSError) as e:
|
except (FileNotFoundError, OSError) as e:
|
||||||
return "Not authorized for any ADE ID", False
|
return "Not authorized for any ADE ID", False, None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
adeptNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
adeptNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
@ -108,20 +114,21 @@ class ConfigWidget(QWidget):
|
|||||||
ade_device_name = devicenameXML.text
|
ade_device_name = devicenameXML.text
|
||||||
|
|
||||||
if container.find(adeptNS("activationToken")) == None:
|
if container.find(adeptNS("activationToken")) == None:
|
||||||
return "ADE authorization seems to be corrupted (activationToken missing)", False
|
return "ADE authorization seems to be corrupted (activationToken missing)", False, None
|
||||||
|
|
||||||
if container.find(adeptNS("credentials")).find(adeptNS("pkcs12")) == None:
|
if container.find(adeptNS("credentials")).find(adeptNS("pkcs12")) == None:
|
||||||
return "ADE authorization seems to be corrupted (pkcs12 missing)", False
|
return "ADE authorization seems to be corrupted (pkcs12 missing)", False, None
|
||||||
|
|
||||||
return "Authorized with ADE ID ("+ade_type+") " + ade_mail + "\non device " + ade_device_name, True
|
return "Authorized with ADE ID ("+ade_type+") " + ade_mail + "\non device " + ade_device_name, True, ade_mail
|
||||||
except:
|
except:
|
||||||
return "ADE authorization seems to be corrupted", False
|
return "ADE authorization seems to be corrupted", False, None
|
||||||
|
|
||||||
|
|
||||||
def export_activation(self):
|
def export_activation(self):
|
||||||
|
|
||||||
filters = [("ZIP", ["zip"])]
|
filters = [("ZIP", ["zip"])]
|
||||||
filename = choose_save_file(self, "Export ADE activation files", _("Export ADE activation files"), filters, all_files=False)
|
filename = choose_save_file(self, "Export ADE activation files", _("Export ADE activation files"),
|
||||||
|
filters, all_files=False, initial_filename="adobe_account_backup.zip")
|
||||||
|
|
||||||
if (filename is None):
|
if (filename is None):
|
||||||
return
|
return
|
||||||
@ -136,6 +143,57 @@ class ConfigWidget(QWidget):
|
|||||||
except:
|
except:
|
||||||
return error_dialog(None, "Export failed", "Export failed.", show=True, show_copy_button=False)
|
return error_dialog(None, "Export failed", "Export failed.", show=True, show_copy_button=False)
|
||||||
|
|
||||||
|
def import_activation(self):
|
||||||
|
|
||||||
|
filters = [("ZIP", ["zip"])]
|
||||||
|
filenames = choose_files(self, "Import ADE activation file (ZIP)", _("Import ADE activation file (ZIP)"),
|
||||||
|
filters, all_files=False, select_only_single_file=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
filename = filenames[0]
|
||||||
|
if (filename is None):
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
print("{0} v{1}: Importing activation data from {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename))
|
||||||
|
|
||||||
|
with ZipFile(filename, 'r') as zipfile:
|
||||||
|
try:
|
||||||
|
device = zipfile.read("device.xml")
|
||||||
|
activation = zipfile.read("activation.xml")
|
||||||
|
salt = zipfile.read("devicesalt")
|
||||||
|
except:
|
||||||
|
return error_dialog(None, "Import failed", "Can't find required files in this ZIP")
|
||||||
|
|
||||||
|
try:
|
||||||
|
output_device = open(os.path.join(self.deacsmprefs["path_to_account_data"], "device.xml"), "w")
|
||||||
|
output_device.write(device.decode("utf-8"))
|
||||||
|
output_device.close()
|
||||||
|
|
||||||
|
output_activation = open(os.path.join(self.deacsmprefs["path_to_account_data"], "activation.xml"), "w")
|
||||||
|
output_activation.write(activation.decode("utf-8"))
|
||||||
|
output_activation.close()
|
||||||
|
|
||||||
|
output_salt = open(os.path.join(self.deacsmprefs["path_to_account_data"], "devicesalt"), "wb")
|
||||||
|
output_salt.write(salt)
|
||||||
|
output_salt.close()
|
||||||
|
|
||||||
|
except:
|
||||||
|
err = traceback.print_exc()
|
||||||
|
return error_dialog(None, "Import failed", "Can't write file", show=True, det_msg=err, show_copy_button=False)
|
||||||
|
|
||||||
|
# update display
|
||||||
|
info_string, activated, ade_mail = self.get_account_info()
|
||||||
|
self.lblAccInfo.setText(info_string)
|
||||||
|
|
||||||
|
self.button_link_account.setEnabled(not activated)
|
||||||
|
self.button_import_activation.setEnabled(not activated)
|
||||||
|
self.button_export_key.setEnabled(activated)
|
||||||
|
self.button_export_activation.setEnabled(activated)
|
||||||
|
|
||||||
|
info_dialog(None, "Done", "Successfully imported authorization for " + ade_mail, show=True, show_copy_button=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def link_account(self):
|
def link_account(self):
|
||||||
@ -181,10 +239,11 @@ class ConfigWidget(QWidget):
|
|||||||
|
|
||||||
|
|
||||||
# update display
|
# update display
|
||||||
info_string, activated = self.get_account_info()
|
info_string, activated, mail = self.get_account_info()
|
||||||
self.lblAccInfo.setText(info_string)
|
self.lblAccInfo.setText(info_string)
|
||||||
|
|
||||||
self.button_link_account.setEnabled(False)
|
self.button_link_account.setEnabled(False)
|
||||||
|
self.button_import_activation.setEnabled(False)
|
||||||
self.button_export_key.setEnabled(True)
|
self.button_export_key.setEnabled(True)
|
||||||
self.button_export_activation.setEnabled(True)
|
self.button_export_activation.setEnabled(True)
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ This is an experimental Python version of libgourou.
|
|||||||
|
|
||||||
# pyright: reportUndefinedVariable=false
|
# pyright: reportUndefinedVariable=false
|
||||||
|
|
||||||
import sys
|
import sys, os
|
||||||
if sys.version_info[0] < 3:
|
if sys.version_info[0] < 3:
|
||||||
print("This script requires Python 3.")
|
print("This script requires Python 3.")
|
||||||
exit(1)
|
exit(1)
|
||||||
@ -17,6 +17,7 @@ from lxml import etree
|
|||||||
|
|
||||||
from libadobe import sendHTTPRequest
|
from libadobe import sendHTTPRequest
|
||||||
from libadobeFulfill import buildRights, fulfill
|
from libadobeFulfill import buildRights, fulfill
|
||||||
|
from libpdf import patch_drm_into_pdf, prepare_string_from_xml
|
||||||
|
|
||||||
FILE_DEVICEKEY = "devicesalt"
|
FILE_DEVICEKEY = "devicesalt"
|
||||||
FILE_DEVICEXML = "device.xml"
|
FILE_DEVICEXML = "device.xml"
|
||||||
@ -32,6 +33,8 @@ def download(replyData):
|
|||||||
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
adDC = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
adDC = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
||||||
|
|
||||||
|
print (replyData)
|
||||||
|
|
||||||
|
|
||||||
metadata_node = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("metadata")))
|
metadata_node = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("metadata")))
|
||||||
download_url = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("src"))).text
|
download_url = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("src"))).text
|
||||||
@ -45,13 +48,27 @@ def download(replyData):
|
|||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
book_name = None
|
book_name = None
|
||||||
|
author = "None"
|
||||||
|
title = "None"
|
||||||
try:
|
try:
|
||||||
book_name = metadata_node.find("./%s" % (adDC("title"))).text
|
book_name = metadata_node.find("./%s" % (adDC("title"))).text
|
||||||
except:
|
except:
|
||||||
book_name = "Book"
|
book_name = "Book"
|
||||||
|
|
||||||
|
try:
|
||||||
|
title = metadata_node.find("./%s" % (adDC("title"))).text
|
||||||
|
author = metadata_node.find("./%s" % (adDC("creator"))).text
|
||||||
|
|
||||||
|
title = title.replace("(", "").replace(")", "").replace("/", "")
|
||||||
|
author = author.replace("(", "").replace(")", "").replace("/", "")
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# Download eBook:
|
# Download eBook:
|
||||||
|
|
||||||
|
print(download_url)
|
||||||
|
|
||||||
book_content = sendHTTPRequest(download_url)
|
book_content = sendHTTPRequest(download_url)
|
||||||
filetype = ".bin"
|
filetype = ".bin"
|
||||||
|
|
||||||
@ -75,14 +92,17 @@ def download(replyData):
|
|||||||
zf.writestr("META-INF/rights.xml", rights_xml_str)
|
zf.writestr("META-INF/rights.xml", rights_xml_str)
|
||||||
zf.close()
|
zf.close()
|
||||||
|
|
||||||
print("File successfully fulfilled!")
|
print("File successfully fulfilled to " + filename)
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
elif filetype == ".pdf":
|
elif filetype == ".pdf":
|
||||||
print("Successfully downloaded PDF, but PDF encryption is not yet supported")
|
print("Successfully downloaded PDF, patching encryption ...")
|
||||||
print("You will not be able to use the downloaded PDF file")
|
|
||||||
print("Here's the raw string:")
|
os.rename(filename, "tmp_" + filename)
|
||||||
print(rights_xml_str)
|
patch_drm_into_pdf("tmp_" + filename, prepare_string_from_xml(rights_xml_str, author, title), filename)
|
||||||
exit(1)
|
os.remove("tmp_" + filename)
|
||||||
|
print("File successfully fulfilled to " + filename)
|
||||||
|
exit(0)
|
||||||
else:
|
else:
|
||||||
print("Error: Weird filetype")
|
print("Error: Weird filetype")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
@ -276,11 +276,8 @@ def fulfill(acsm_file):
|
|||||||
mimetype = acsmxml.find("./%s/%s/%s" % (adNS("resourceItemInfo"), adNS("metadata"), dcNS("format"))).text
|
mimetype = acsmxml.find("./%s/%s/%s" % (adNS("resourceItemInfo"), adNS("metadata"), dcNS("format"))).text
|
||||||
|
|
||||||
if (mimetype == "application/pdf"):
|
if (mimetype == "application/pdf"):
|
||||||
print("You're trying to fulfill a PDF file.")
|
#print("You're trying to fulfill a PDF file.")
|
||||||
print("While that's technically possible with this script, the script doesn't yet support embedding the DRM information into PDF files.")
|
pass
|
||||||
print("This means the PDF would be unusable.")
|
|
||||||
print("Thus, PDF fulfillment is disabled for now.")
|
|
||||||
return False, "PDF not supported"
|
|
||||||
elif (mimetype == "application/epub+zip"):
|
elif (mimetype == "application/epub+zip"):
|
||||||
#print("Trying to fulfill an EPUB file ...")
|
#print("Trying to fulfill an EPUB file ...")
|
||||||
pass
|
pass
|
||||||
|
147
calibre-plugin/libpdf.py
Normal file
147
calibre-plugin/libpdf.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import os, zlib, base64
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
|
def read_reverse_order(file_name):
|
||||||
|
# Open file for reading in binary mode
|
||||||
|
with open(file_name, 'rb') as read_obj:
|
||||||
|
# Move the cursor to the end of the file
|
||||||
|
read_obj.seek(0, os.SEEK_END)
|
||||||
|
# Get the current position of pointer i.e eof
|
||||||
|
pointer_location = read_obj.tell()
|
||||||
|
# Create a buffer to keep the last read line
|
||||||
|
buffer = bytearray()
|
||||||
|
# Loop till pointer reaches the top of the file
|
||||||
|
while pointer_location >= 0:
|
||||||
|
# Move the file pointer to the location pointed by pointer_location
|
||||||
|
read_obj.seek(pointer_location)
|
||||||
|
# Shift pointer location by -1
|
||||||
|
pointer_location = pointer_location -1
|
||||||
|
# read that byte / character
|
||||||
|
new_byte = read_obj.read(1)
|
||||||
|
# If the read byte is new line character then it means one line is read
|
||||||
|
if new_byte == b'\n':
|
||||||
|
# Fetch the line from buffer and yield it
|
||||||
|
yield buffer.decode()[::-1]
|
||||||
|
# Reinitialize the byte array to save next line
|
||||||
|
buffer = bytearray()
|
||||||
|
else:
|
||||||
|
# If last read character is not eol then add it in buffer
|
||||||
|
buffer.extend(new_byte)
|
||||||
|
# As file is read completely, if there is still data in buffer, then its the first line.
|
||||||
|
if len(buffer) > 0:
|
||||||
|
# Yield the first line too
|
||||||
|
yield buffer.decode()[::-1]
|
||||||
|
|
||||||
|
def deflate_and_base64_encode( string_val ):
|
||||||
|
zlibbed_str = zlib.compress( string_val )
|
||||||
|
compressed_string = zlibbed_str[2:-4]
|
||||||
|
return base64.b64encode( compressed_string )
|
||||||
|
|
||||||
|
def prepare_string_from_xml(xmlstring, title, author):
|
||||||
|
b64data = deflate_and_base64_encode(xmlstring.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
adobe_fulfill_response = etree.fromstring(xmlstring)
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
resource = adobe_fulfill_response.find("./%s/%s" % (adNS("licenseToken"), adNS("resource"))).text
|
||||||
|
|
||||||
|
return "<</Length 128/EBX_TITLE(%s)/Filter/EBX_HANDLER/EBX_AUTHOR(%s)/V 4/ADEPT_ID(%s)/EBX_BOOKID(%s)/ADEPT_LICENSE(%s)>>" % (title, author, resource, resource, b64data)
|
||||||
|
|
||||||
|
def patch_drm_into_pdf(filename_in, drm_string, filename_out):
|
||||||
|
|
||||||
|
ORIG_FILE = filename_in
|
||||||
|
|
||||||
|
trailer = ""
|
||||||
|
|
||||||
|
trailer_idx = 0
|
||||||
|
|
||||||
|
for line in read_reverse_order(ORIG_FILE):
|
||||||
|
trailer_idx += 1
|
||||||
|
trailer = line + "\n" + trailer
|
||||||
|
if (line == "trailer"):
|
||||||
|
if (trailer_idx > 20):
|
||||||
|
print("trailer_idx is very large (%d). Usually it's 10 or less. File might be corrupted." % trailer_idx)
|
||||||
|
break
|
||||||
|
|
||||||
|
r_encrypt_offs1 = 0
|
||||||
|
r_encrypt_offs2 = 0
|
||||||
|
root_str = None
|
||||||
|
next_startxref = False
|
||||||
|
startxref = None
|
||||||
|
|
||||||
|
for line in trailer.split('\n'):
|
||||||
|
#print(line)
|
||||||
|
if ("R/Encrypt" in line):
|
||||||
|
root_str = line
|
||||||
|
line_split = line.split(' ')
|
||||||
|
next = 0
|
||||||
|
for element in line_split:
|
||||||
|
if element == "R/Encrypt":
|
||||||
|
next = 2
|
||||||
|
continue
|
||||||
|
if next == 2:
|
||||||
|
r_encrypt_offs1 = element
|
||||||
|
next = 1
|
||||||
|
continue
|
||||||
|
if next == 1:
|
||||||
|
r_encrypt_offs2 = element
|
||||||
|
next = 0
|
||||||
|
continue
|
||||||
|
if "startxref" in line:
|
||||||
|
next_startxref = True
|
||||||
|
continue
|
||||||
|
if next_startxref:
|
||||||
|
startxref = line
|
||||||
|
next_startxref = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
filesize_str = str(os.path.getsize(ORIG_FILE))
|
||||||
|
filesize_pad = filesize_str.zfill(10)
|
||||||
|
|
||||||
|
|
||||||
|
additional_data = "\r"
|
||||||
|
additional_data += r_encrypt_offs1 + " " + r_encrypt_offs2 + " " + "obj" + "\r"
|
||||||
|
additional_data += drm_string
|
||||||
|
additional_data += "\r"
|
||||||
|
additional_data += "endobj"
|
||||||
|
|
||||||
|
ptr = int(filesize_str) + len(additional_data)
|
||||||
|
|
||||||
|
additional_data += "\rxref\r" + r_encrypt_offs1 + " " + str((int(r_encrypt_offs2) + 1)) + "\r"
|
||||||
|
additional_data += filesize_pad + " 00000 n" + "\r\n"
|
||||||
|
additional_data += "trailer"
|
||||||
|
additional_data += "\r"
|
||||||
|
|
||||||
|
arr_root_str = root_str.split('/')
|
||||||
|
did_prev = False
|
||||||
|
for elem in arr_root_str:
|
||||||
|
if elem.startswith("Prev"):
|
||||||
|
did_prev = True
|
||||||
|
additional_data += "Prev " + startxref
|
||||||
|
#print("Replacing prev from '%s' to '%s'" % (elem, "Prev " + startxref))
|
||||||
|
elif elem.startswith("ID[<"):
|
||||||
|
additional_data += elem.replace("><", "> <")
|
||||||
|
else:
|
||||||
|
additional_data += elem
|
||||||
|
additional_data += "/"
|
||||||
|
|
||||||
|
if not did_prev:
|
||||||
|
# remove two >> at end
|
||||||
|
additional_data = additional_data[:-3]
|
||||||
|
additional_data += "/Prev " + startxref + ">>" + "/"
|
||||||
|
#print("Faking Prev %s" % startxref)
|
||||||
|
|
||||||
|
additional_data = additional_data[:-1]
|
||||||
|
|
||||||
|
additional_data += "\r" + "startxref\r" + str(ptr) + "\r" + "%%EOF"
|
||||||
|
|
||||||
|
|
||||||
|
inp = open(ORIG_FILE, "rb")
|
||||||
|
|
||||||
|
out = open(filename_out, "wb")
|
||||||
|
out.write(inp.read())
|
||||||
|
out.write(additional_data.encode("latin-1"))
|
||||||
|
inp.close()
|
||||||
|
out.close()
|
Loading…
Reference in New Issue
Block a user