2021-09-19 16:20:56 +02:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Calibre plugin for ACSM files.
2023-02-24 14:11:15 +01:00
'''
Copyright ( c ) 2021 - 2023 Leseratte10
This file is part of the ACSM Input Plugin by Leseratte10
https : / / github . com / Leseratte10 / acsm - calibre - plugin
ACSM Input Plugin for Calibre / acsm - calibre - plugin
Formerly known as " DeACSM "
This software is based on a Python reimplementation of the C + + library
" libgourou " by Grégory Soutadé which is under the LGPLv3 or later
license ( http : / / indefero . soutade . fr / p / libgourou / ) .
I have no idea whether a reimplementation in another language counts
as " derivative use " , so just in case it does , I ' m putting this project
under the GPLv3 ( which is allowed in the LGPLv3 license ) to prevent any
licensing issues .
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with this program . If not , see < http : / / www . gnu . org / licenses / > .
See the " LICENSE " file for a full copy of the GNU GPL v3 .
'''
2021-09-19 16:20:56 +02:00
2021-09-21 17:42:51 +02:00
# Revision history:
# v0.0.1: First version.
2021-09-25 16:24:03 +02:00
# v0.0.2: Allow key extraction without extra binary call (unreleased test version)
# v0.0.3: Standalone Calibre plugin for Linux, Windows, MacOS without the need for libgourou.
2021-09-26 12:56:53 +02:00
# v0.0.4: Manually execute DeDRM (if installed) after converting ACSM to EPUB.
2021-09-27 17:07:17 +02:00
# v0.0.5: Bugfix: DeDRM plugin was also executed if it's installed but disabled.
2021-09-28 18:43:14 +02:00
# v0.0.6: First PDF support, allow importing previously exported activation data.
2021-10-01 14:32:46 +02:00
# v0.0.7: More PDF logging, PDF reading in latin-1, MacOS locale bugfix
2021-10-03 10:30:45 +02:00
# v0.0.8: More PDF bugfixes, support unlimited PDF file sizes, tell Calibre ACSMs are books.
2021-10-04 15:36:25 +02:00
# v0.0.9: Add FulfillmentNotification support, add LoanReturn support.
2021-10-23 21:45:43 +02:00
# v0.0.10: Fix nonce calculation, merge PRs #3 and #4 (PyCryptodome stuff)
2021-11-12 19:15:30 +01:00
# v0.0.11: Ignore SSL errors during ACS notify, improve element hashing code,
# improve PassHash support, include UUID in key export filename,
# fix bug that would block other FileTypePlugins
2021-11-15 06:46:48 +01:00
# v0.0.12: Fix Calibre Plugin index / updater
2021-12-11 11:32:37 +01:00
# v0.0.13: v0.0.13 was a development / beta version with lots of different published test
# versions. To make support easier there's no "final" v0.0.13 version. Instead,
# all the changes from the various v0.0.13 beta versions are released with v0.0.14.
# v0.0.14: Add support for emulating multiple ADE versions (1.7.2, 2.0.1, 3.0.1, 4.0.3, 4.5.11),
2021-11-25 09:15:37 +01:00
# add code to import existing activation from ADE (Windows, MacOS or Linux/Wine),
# add code to remove an existing activation from the plugin (Ctrl+Shift+D),
2021-11-20 06:53:51 +01:00
# fix race condition when importing multiple ACSMs simultaneously,
2021-11-25 09:15:37 +01:00
# fix authorization failing with certain non-ASCII characters in username,
2021-12-11 11:32:37 +01:00
# add detailed logging toggle setting, add auto-delete ACSM setting,
# add useful error message for ACSMs with nonstandard download type.
2021-12-15 13:48:34 +01:00
# v0.0.15: Add support for anonymous authorizations, add support for other ID providers,
2021-12-14 19:35:07 +01:00
# fix ACSM files from Google Play books (no metadata node),
2021-12-15 13:48:34 +01:00
# allow converting an anonymous auth to an AdobeID auth,
# update python-cryptography from 3.4.8 to 36.0.1, update python-rsa from 4.7.2 to 4.8.
2022-07-31 11:06:21 +02:00
# v0.0.16: Ignore fatal HTTP errors and/or a missing or broken server during optional
2022-01-16 10:12:55 +01:00
# fulfillment notifications, allow authorizing an eReader through USB (experimental),
2021-12-19 22:13:19 +01:00
# drop dependencies python-cryptography, python-rsa and python-pyasn1.
2022-01-16 19:30:53 +01:00
# add a ton of testing code, try to prevent AV false-positives,
2022-05-13 18:45:21 +02:00
# experimental support for Python2 / Calibre < 5,
2022-07-11 18:16:36 +02:00
# fix broken URLs with missing protocol, fix loan data for loans without device ID,
2022-07-31 11:06:21 +02:00
# fix nonce calculation yet again, merge #26 to make importing a WINE auth more reliable,
2022-07-28 18:07:52 +02:00
# update python-oscrypto to unofficial fork to fix OpenSSL 3 support.
2022-09-05 19:08:42 +02:00
# v0.0.17:
2022-09-04 11:13:53 +02:00
# Fix bug that would sometimes return the wrong book (or none at all) if you had
2022-09-05 18:34:40 +02:00
# multiple active loans from the same distributor, add experimental GUI button,
2022-09-05 19:08:42 +02:00
# rename plugin from "DeACSM" to "ACSM Input". BETA build, not a normal release!!
2022-10-08 17:51:15 +02:00
#
# v0.1.0: Continue work on renaming from "DeACSM" to "ACSM Input".
2022-10-23 10:19:55 +02:00
# The big version number jump is to make that name change clearer,
# and to support the "migration plugin" to rename the plugin.
2022-10-23 10:06:54 +02:00
# Print useful warning if LicenseServiceCertificate download fails,
# fix error with the loan list not being updated when importing multiple ACSMs at once,
# fix bug with the GUI extension in non-English environments,
2022-10-23 10:19:55 +02:00
# add setting to choose between simultaneous (faster) or sequencial (more ADE-like)
# import of multiple ACSM files
2022-10-08 17:51:15 +02:00
2021-09-21 17:42:51 +02:00
2022-07-31 11:06:21 +02:00
2022-09-05 18:34:40 +02:00
PLUGIN_NAME = " ACSM Input "
2022-10-08 17:51:15 +02:00
PLUGIN_VERSION_TUPLE = ( 0 , 1 , 0 )
2021-09-21 17:42:51 +02:00
2021-09-19 16:20:56 +02:00
from calibre . customize import FileTypePlugin # type: ignore
2021-11-15 06:46:48 +01:00
__version__ = PLUGIN_VERSION = " . " . join ( [ str ( x ) for x in PLUGIN_VERSION_TUPLE ] )
2021-09-19 16:20:56 +02:00
from calibre . utils . config import config_dir # type: ignore
2021-12-15 10:09:52 +01:00
import os , shutil , traceback , sys , time , io , random
2021-09-25 16:24:03 +02:00
import zipfile
2021-09-24 16:10:03 +02:00
from lxml import etree
2022-01-16 17:43:29 +01:00
#@@CALIBRE_COMPAT_CODE@@
2022-09-05 18:34:40 +02:00
class ACSMInput ( FileTypePlugin ) :
2021-09-19 16:20:56 +02:00
name = PLUGIN_NAME
2022-10-23 10:04:26 +02:00
description = " Takes an Adobe ACSM file and converts that into a useable EPUB or PDF file. Formerly known as DeACSM. Based on the libgourou library by Grégory Soutadé "
2021-09-25 16:24:03 +02:00
supported_platforms = [ ' linux ' , ' osx ' , ' windows ' ]
2021-09-24 16:10:03 +02:00
author = " Leseratte10 "
2021-09-19 16:20:56 +02:00
version = PLUGIN_VERSION_TUPLE
2022-01-16 17:43:29 +01:00
minimum_calibre_version = ( 4 , 0 , 0 )
2021-09-19 16:20:56 +02:00
file_types = set ( [ ' acsm ' ] )
on_import = True
on_preprocess = True
priority = 2000
2022-09-05 18:34:40 +02:00
def init_embedded_plugins ( self ) :
"""
A Calibre plugin can normally only contain one Plugin class .
2022-10-08 17:51:15 +02:00
In our case , this would be the file type class .
2022-09-05 18:34:40 +02:00
However , we want to load the GUI plugin , too , so we have to trick
Calibre into believing that there ' s actually a 2nd plugin.
"""
from calibre . customize . ui import _initialized_plugins
2022-10-08 17:51:15 +02:00
from calibre_plugins . deacsm . gui_main_wrapper import ACSMInputGUIExtension
2022-09-05 18:34:40 +02:00
def init_plg ( plg_type ) :
for plugin in _initialized_plugins :
if isinstance ( plugin , plg_type ) :
return plugin
plg_type . version = self . version
plg_type . minimum_calibre_version = self . minimum_calibre_version
plugin = plg_type ( self . plugin_path )
_initialized_plugins . append ( plugin )
plugin . initialize ( )
return plugin
2022-10-08 17:51:15 +02:00
init_plg ( ACSMInputGUIExtension )
2022-09-05 18:34:40 +02:00
2021-09-19 16:20:56 +02:00
def initialize ( self ) :
2022-09-05 18:34:40 +02:00
2021-09-19 16:20:56 +02:00
"""
2022-01-16 19:30:53 +01:00
On initialization , make sure we have all the libraries ( oscrypto and its dependency
asn1crypto ) that the plugin needs . Unfortunately the Adobe encryption is kinda weird
and nonstandard and doesn ' t work with just the python modules included with Calibre.
2021-09-19 16:20:56 +02:00
"""
2021-09-24 16:10:03 +02:00
2021-09-19 16:20:56 +02:00
try :
2021-10-03 10:30:45 +02:00
# Patch Calibre to consider "ACSM" a book. This makes ACSM files show up
# in the "Add Book" file selection, and it also makes the auto-add feature useable.
try :
from calibre . ebooks import BOOK_EXTENSIONS
if ( " acsm " not in BOOK_EXTENSIONS ) :
BOOK_EXTENSIONS . append ( " acsm " )
except :
print ( " {0} v {1} : Couldn ' t add ACSM to book extension list: " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( )
2021-09-19 16:20:56 +02:00
self . pluginsdir = os . path . join ( config_dir , " plugins " )
if not os . path . exists ( self . pluginsdir ) :
os . mkdir ( self . pluginsdir )
2022-09-05 18:34:40 +02:00
2022-10-08 17:51:15 +02:00
# If the old DeACSM plugin still exists, rename it to BAK or something so it doesn't load.
2022-10-08 18:32:29 +02:00
if os . path . exists ( os . path . join ( self . pluginsdir , " DeACSM.zip " ) ) :
os . rename ( os . path . join ( self . pluginsdir , " DeACSM.zip " ) , os . path . join ( self . pluginsdir , " DeACSM.BAK " ) )
2022-09-05 18:34:40 +02:00
2022-10-23 10:06:54 +02:00
try :
# Make sure the GUI extension is loaded:
self . init_embedded_plugins ( )
except :
# Apparently this can fail - if it does, ignore errors so the rest of the plugin still works.
print ( " {0} v {1} : Couldn ' t initialize GUI plugin: " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( )
pass
2022-09-05 18:34:40 +02:00
self . maindir_old = os . path . join ( self . pluginsdir , " DeACSM " )
self . maindir = os . path . join ( self . pluginsdir , " ACSMInput " )
2022-10-08 17:51:15 +02:00
# Do NOT try to migrate data, that just screws everything up.
# If this is a fresh install of the plugin and there's no old data,
# use the new path. Otherwise, if there's already data at the old location,
# continue to use that.
if os . path . exists ( self . maindir_old ) :
# We have the old folder, continue to use that
self . maindir = self . maindir_old
2022-09-05 18:34:40 +02:00
2021-09-19 16:20:56 +02:00
if not os . path . exists ( self . maindir ) :
os . mkdir ( self . maindir )
2021-11-15 12:09:11 +01:00
# Extract new modules
2021-09-24 16:10:03 +02:00
self . moddir = os . path . join ( self . maindir , " modules " )
2021-11-15 12:09:11 +01:00
if not os . path . exists ( self . moddir ) :
os . mkdir ( self . moddir )
2021-09-24 16:10:03 +02:00
2021-12-15 10:09:52 +01:00
# Check if we have a module id:
# Modules will only be extracted if this has changed.
# This A) saves time because we don't extract every time,
# and B) prevents a race condition.
# The compiling scripts need to be adapted to modify
# the module_id.txt in the plugin ZIP every time the
# modules change
try :
ts_file = os . path . join ( self . moddir , " module_id.txt " )
f = open ( ts_file , " r " )
id = f . readline ( ) . strip ( )
f . close ( )
except :
# No timestamp found, probably upgrading from an older version.
id = None
# Check ID file in the plugin ZIP
try :
ts_dict = self . load_resources ( [ " module_id.txt " ] )
id_plugin = ts_dict [ " module_id.txt " ] . decode ( " latin-1 " ) . split ( ' \n ' ) [ 0 ] . strip ( )
except :
# No timestamp found in the plugin ZIP?
# Assume that I made a mistake bundling the plugin, extract anyways.
id_plugin = None
if id is None or id_plugin is None or id != id_plugin :
print ( " Module update from \" {0} \" to \" {1} \" , extracting ... " . format ( id , id_plugin ) )
# Something changed, extract modules.
if os . path . exists ( self . moddir ) :
shutil . rmtree ( self . moddir , ignore_errors = True )
rand_path = self . moddir + str ( random . randint ( 0 , 1000000000 ) )
2022-09-03 19:06:12 +02:00
ctr = 0
while os . path . exists ( rand_path ) :
# None of this code should be necessary since a random number between 0 and a billion should be unique
# enough, but apparently not. Make new ones until we find one that's not in use.
# Should be using Calibre's TemporaryFile class but then I can't be certain it's on the same drive...
ctr + = 1
if ( ctr > 1000 ) :
print ( " {0} v {1} : Tried a thousand times to get a temp dir ... " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
raise Exception ( " Hey! " )
rand_path = self . moddir + str ( random . randint ( 0 , 1000000000 ) )
2021-12-15 10:09:52 +01:00
os . mkdir ( rand_path )
2021-12-19 11:24:48 +01:00
names = [ " oscrypto.zip " , " asn1crypto.zip " ]
# oscrypto is needed to parse the pkcs12 data from Adobe.
# asn1crypto is a dependency of oscrypto.
2021-12-15 10:09:52 +01:00
2021-11-25 09:15:37 +01:00
lib_dict = self . load_resources ( names )
2021-12-15 10:09:52 +01:00
2021-11-25 09:15:37 +01:00
for entry , data in lib_dict . items ( ) :
2021-12-15 10:09:52 +01:00
try :
with zipfile . ZipFile ( io . BytesIO ( data ) , ' r ' ) as ref :
ref . extractall ( rand_path )
except :
print ( " {0} v {1} : Exception when copying needed library files " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( )
2022-07-28 18:07:52 +02:00
pass
2022-05-14 18:56:33 +02:00
2022-06-15 19:29:55 +02:00
2021-12-15 10:09:52 +01:00
# Write module ID
if id_plugin is not None :
mod_file = os . path . join ( rand_path , " module_id.txt " )
f = open ( mod_file , " w " )
f . write ( id_plugin )
2021-11-25 09:15:37 +01:00
f . close ( )
2021-12-15 10:09:52 +01:00
# Rename temporary path to actual module path so this will be used next time.
os . rename ( rand_path , self . moddir )
2021-09-25 16:24:03 +02:00
sys . path . insert ( 0 , os . path . join ( self . moddir , " oscrypto " ) )
sys . path . insert ( 0 , os . path . join ( self . moddir , " asn1crypto " ) )
# Okay, now all the modules are available, import the Adobe modules.
2022-01-16 17:43:29 +01:00
from libadobe import createDeviceKeyFile , update_account_path , sendHTTPRequest
from libadobeAccount import createDeviceFile , createUser , signIn , activateDevice
2022-05-14 18:56:33 +02:00
from libadobeFulfill import buildRights , fulfill
2021-09-25 16:24:03 +02:00
2021-11-20 06:53:51 +01:00
2021-09-25 16:24:03 +02:00
import calibre_plugins . deacsm . prefs as prefs # type: ignore
2022-09-05 18:34:40 +02:00
deacsmprefs = prefs . ACSMInput_Prefs ( )
2021-09-25 16:24:03 +02:00
update_account_path ( deacsmprefs [ " path_to_account_data " ] )
2021-09-19 16:20:56 +02:00
except Exception as e :
traceback . print_exc ( )
raise
2021-09-24 16:10:03 +02:00
2021-09-19 16:20:56 +02:00
def is_customizable ( self ) :
return True
def config_widget ( self ) :
import calibre_plugins . deacsm . config as config # type: ignore
return config . ConfigWidget ( self . plugin_path )
def save_settings ( self , config_widget ) :
config_widget . save_settings ( )
2021-09-25 16:24:03 +02:00
def ADE_sanity_check ( self ) :
2022-05-14 10:26:11 +02:00
from libadobe import get_activation_xml_path
2021-09-25 16:24:03 +02:00
container = None
try :
2022-05-14 10:26:11 +02:00
container = etree . parse ( get_activation_xml_path ( ) )
2021-09-27 17:07:17 +02:00
except :
2022-05-14 10:26:11 +02:00
print ( " ADE sanity check: Can ' t parse activation container " )
2021-09-25 16:24:03 +02:00
return False
try :
adeptNS = lambda tag : ' { %s } %s ' % ( ' http://ns.adobe.com/adept ' , tag )
if container . find ( adeptNS ( " activationToken " ) ) == None :
2022-05-14 10:26:11 +02:00
print ( " ADE sanity check: activationToken missing " )
2021-09-25 16:24:03 +02:00
return False
if container . find ( adeptNS ( " credentials " ) ) . find ( adeptNS ( " pkcs12 " ) ) == None :
2022-05-14 10:26:11 +02:00
print ( " ADE sanity check: pkcs12 missing " )
return False
2022-05-14 18:56:33 +02:00
try :
from libadobeFulfill import getDecryptedCert
if getDecryptedCert ( ) is None :
print ( " ADE sanity check: Can ' t decrypt pkcs12 " )
return False
except :
print ( " Skipping decryption check " )
2021-09-25 16:24:03 +02:00
return True
except :
2022-05-14 10:26:11 +02:00
print ( " ADE sanity check: Exception " )
traceback . print_exc ( )
2021-09-25 16:24:03 +02:00
return False
2022-01-16 17:43:29 +01:00
def download ( self , replyData ) :
# type: (str) -> str
2021-09-25 16:24:03 +02:00
2022-01-16 17:43:29 +01:00
from libadobe import sendHTTPRequest_DL2FILE
from libadobeFulfill import buildRights , fulfill
from libpdf import patch_drm_into_pdf
2021-09-28 18:43:14 +02:00
2021-09-25 16:24:03 +02:00
adobe_fulfill_response = etree . fromstring ( replyData )
adNS = lambda tag : ' { %s } %s ' % ( ' http://ns.adobe.com/adept ' , tag )
2021-12-11 11:32:37 +01:00
try :
download_url = adobe_fulfill_response . find ( " ./ %s / %s / %s " % ( adNS ( " fulfillmentResult " ) , adNS ( " resourceItemInfo " ) , adNS ( " src " ) ) ) . text
except :
print ( " {0} v {1} : FulfillmentResult does not contain the <src> tag. This may be an ACSM with download type ' auth ' ? " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
2022-07-31 10:20:16 +02:00
print ( " {0} v {1} : Please download the book through ADE (so the ACSM file is ' used ' ). " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
print ( " {0} v {1} : After that, please open a bug report and attach the ACSM file. " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
2021-12-11 11:32:37 +01:00
return None
2021-09-25 16:24:03 +02:00
license_token_node = adobe_fulfill_response . find ( " ./ %s / %s / %s " % ( adNS ( " fulfillmentResult " ) , adNS ( " resourceItemInfo " ) , adNS ( " licenseToken " ) ) )
rights_xml_str = buildRights ( license_token_node )
if ( rights_xml_str is None ) :
print ( " {0} v {1} : Building rights.xml failed! " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
return None
# Download eBook:
2021-10-01 14:32:46 +02:00
print ( " {0} v {1} : Loading book from {2} " . format ( PLUGIN_NAME , PLUGIN_VERSION , download_url ) )
2021-09-25 16:24:03 +02:00
2021-10-03 10:30:45 +02:00
filename_tmp = self . temporary_file ( " .blob " ) . name
dl_start_time = int ( time . time ( ) * 1000 )
ret = sendHTTPRequest_DL2FILE ( download_url , filename_tmp )
dl_end_time = int ( time . time ( ) * 1000 )
print ( " Download took %d ms (HTTP %d ) " % ( dl_end_time - dl_start_time , ret ) )
if ( ret != 200 ) :
print ( " {0} v {1} : Download failed with error {2} " . format ( PLUGIN_NAME , PLUGIN_VERSION , ret ) )
return None
2021-09-25 16:24:03 +02:00
filetype = " .bin "
2021-10-03 10:30:45 +02:00
book_content = None
with open ( filename_tmp , " rb " ) as f :
book_content = f . read ( 10 )
2021-09-25 16:24:03 +02:00
if ( book_content . startswith ( b " PK " ) ) :
print ( " That ' s a ZIP file -> EPUB " )
filetype = " .epub "
elif ( book_content . startswith ( b " % PDF " ) ) :
print ( " That ' s a PDF file " )
filetype = " .pdf "
filename = self . temporary_file ( filetype ) . name
2021-10-03 10:30:45 +02:00
# Move to file name with matching extension
shutil . move ( filename_tmp , filename )
2021-09-28 18:43:14 +02:00
2021-09-25 16:24:03 +02:00
if filetype == " .epub " :
# Store EPUB rights / encryption stuff
zf = zipfile . ZipFile ( filename , " a " )
zf . writestr ( " META-INF/rights.xml " , rights_xml_str )
zf . close ( )
print ( " {0} v {1} : File successfully fulfilled ... " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
return filename
elif filetype == " .pdf " :
2021-10-03 10:30:45 +02:00
adobe_fulfill_response = etree . fromstring ( rights_xml_str )
2021-11-20 06:53:51 +01:00
2021-10-03 10:30:45 +02:00
adNS = lambda tag : ' { %s } %s ' % ( ' http://ns.adobe.com/adept ' , tag )
resource = adobe_fulfill_response . find ( " ./ %s / %s " % ( adNS ( " licenseToken " ) , adNS ( " resource " ) ) ) . text
2021-09-28 18:43:14 +02:00
print ( " {0} v {1} : Downloaded PDF, adding encryption config ... " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
pdf_tmp_file = self . temporary_file ( filetype ) . name
2021-10-03 10:30:45 +02:00
ret = patch_drm_into_pdf ( filename , rights_xml_str , pdf_tmp_file , resource )
if ( ret ) :
print ( " {0} v {1} : File successfully fulfilled ... " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
else :
print ( " {0} v {1} : There was an error patching the PDF file. " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
2021-09-28 18:43:14 +02:00
return pdf_tmp_file
2021-09-25 16:24:03 +02:00
else :
2021-09-28 18:43:14 +02:00
print ( " {0} v {1} : Error: Unsupported file type ... " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
2021-09-25 16:24:03 +02:00
return None
2022-10-23 10:19:55 +02:00
def is_blocked ( self ) :
import calibre_plugins . deacsm . prefs as prefs # type: ignore
deacsmprefs = prefs . ACSMInput_Prefs ( )
return deacsmprefs [ ' fulfillment_block_token ' ] != 0
def unblock ( self ) :
import calibre_plugins . deacsm . prefs as prefs # type: ignore
deacsmprefs = prefs . ACSMInput_Prefs ( )
my_token = deacsmprefs [ " fulfillment_block_token " ]
deacsmprefs . refresh ( )
if ( my_token == deacsmprefs [ " fulfillment_block_token " ] ) :
# Only unlock if this is my own lock
deacsmprefs . set ( " fulfillment_block_token " , 0 )
deacsmprefs . set ( " fulfillment_block_time " , 0 )
deacsmprefs . commit ( )
def wait_and_block ( self ) :
random_identifier = None
import calibre_plugins . deacsm . prefs as prefs # type: ignore
deacsmprefs = prefs . ACSMInput_Prefs ( )
while True :
deacsmprefs . refresh ( )
if deacsmprefs [ " fulfillment_block_token " ] == 0 :
random_identifier = random . getrandbits ( 64 )
#print("setting block token to %s" % (str(random_identifier)))
deacsmprefs . set ( " fulfillment_block_token " , random_identifier )
deacsmprefs . commit ( )
deacsmprefs . refresh ( )
if random_identifier != deacsmprefs [ " fulfillment_block_token " ] :
# print("we broke another thread's global token")
continue
deacsmprefs . set ( " fulfillment_block_time " , int ( time . time ( ) * 1000 ) )
#print("Obtained lock!")
return True
else :
# Token already exists, wait for it to finish ...
current_time = int ( time . time ( ) * 1000 )
saved_time = deacsmprefs [ " fulfillment_block_time " ]
if saved_time + 60000 < current_time :
# Already locked since 60s, assume error
print ( " {0} v {1} : Looks like the lock was stuck, removing lock {2} ... " . format ( PLUGIN_NAME , PLUGIN_VERSION , deacsmprefs [ " fulfillment_block_token " ] ) )
self . unblock ( )
time . sleep ( 0.02 )
continue
2022-01-16 17:43:29 +01:00
def run ( self , path_to_ebook ) :
# type: (str) -> str
2022-10-23 10:19:55 +02:00
try :
# This code gets called by Calibre with a path to the new book file.
# We need to check if it's an ACSM file
2022-01-16 17:43:29 +01:00
2022-10-23 10:19:55 +02:00
import calibre_plugins . deacsm . prefs as prefs # type: ignore
deacsmprefs = prefs . ACSMInput_Prefs ( )
2021-09-19 16:20:56 +02:00
2022-10-23 10:19:55 +02:00
if deacsmprefs [ ' allow_parallel_fulfillment ' ] == False :
self . wait_and_block ( )
2021-09-19 16:20:56 +02:00
2021-09-20 11:24:11 +02:00
2022-10-23 10:19:55 +02:00
print ( " {0} v {1} : Trying to parse file {2} " . format ( PLUGIN_NAME , PLUGIN_VERSION , os . path . basename ( path_to_ebook ) ) )
2021-09-20 11:24:11 +02:00
2022-10-23 10:19:55 +02:00
ext = os . path . splitext ( path_to_ebook ) [ 1 ] . lower ( )
2021-09-20 11:24:11 +02:00
2022-10-23 10:19:55 +02:00
if ( ext != " .acsm " ) :
print ( " {0} v {1} : That ' s not an ACSM, returning (is {2} instead)... " . format ( PLUGIN_NAME , PLUGIN_VERSION , ext ) )
self . unblock ( )
return path_to_ebook
2021-09-20 11:24:11 +02:00
2022-10-23 10:19:55 +02:00
# We would fulfill this now, but first perform some sanity checks ...
2022-01-16 17:43:29 +01:00
2022-10-23 10:19:55 +02:00
if not self . ADE_sanity_check ( ) :
print ( " {0} v {1} : ADE auth is missing or broken " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
self . unblock ( )
return path_to_ebook
2021-11-20 06:53:51 +01:00
2022-10-23 10:19:55 +02:00
from libadobe import are_ade_version_lists_valid
from libadobeFulfill import fulfill
2021-09-21 17:42:51 +02:00
2022-10-23 10:19:55 +02:00
if not are_ade_version_lists_valid ( ) :
print ( " {0} v {1} : ADE version list mismatch, please open a bug report. " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
self . unblock ( )
return path_to_ebook
2021-10-04 15:36:25 +02:00
2022-10-23 10:19:55 +02:00
print ( " {0} v {1} : Try to fulfill ... " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
2021-09-26 12:56:53 +02:00
2022-10-23 10:19:55 +02:00
success , replyData = fulfill ( path_to_ebook , deacsmprefs [ " notify_fulfillment " ] )
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
if ( success is False ) :
print ( " {0} v {1} : Hey, that didn ' t work: \n " . format ( PLUGIN_NAME , PLUGIN_VERSION ) + replyData )
else :
print ( " {0} v {1} : Downloading book ... " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
rpl = self . download ( replyData )
if ( rpl is not None ) :
# Got a file
2021-12-11 11:32:37 +01:00
2022-10-23 10:19:55 +02:00
# Because Calibre still thinks this is an ACSM file (not an EPUB)
# it will not run other FileTypePlugins that handle EPUB (or PDF) files.
# Loop through all plugins (the list is already sorted by priority),
# then execute all of them that can handle EPUB / PDF.
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
# if the source file is supposed to be deleted after successful fulfillment,
# this is set to True
# If there's any errors whatsoever during export / plugin execution,
# this will be set back to False to prevent deletion.
delete_src_file = deacsmprefs [ " delete_acsm_after_fulfill " ]
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
try :
from calibre . customize . ui import _initialized_plugins , is_disabled
from calibre . customize import FileTypePlugin
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
original_file_for_plugins = rpl
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
oo , oe = sys . stdout , sys . stderr
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
for plugin in _initialized_plugins :
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
#print("{0} v{1}: Plugin '{2}' has prio {3}".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name, plugin.priority))
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
# Check if this is a FileTypePlugin
if not isinstance ( plugin , FileTypePlugin ) :
#print("{0} v{1}: Plugin '{2}' is no FileTypePlugin, skipping ...".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name))
continue
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
# Check if it's disabled
if is_disabled ( plugin ) :
#print("{0} v{1}: Plugin '{2}' is disabled, skipping ...".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name))
continue
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
if plugin . name == self . name :
#print("{0} v{1}: Plugin '{2}' is me - skipping".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name))
continue
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
# Check if it's supposed to run on import:
if not plugin . on_import :
#print("{0} v{1}: Plugin '{2}' isn't supposed to run during import, skipping ...".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name))
continue
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
# Check filetype
# If neither the book file extension nor "*" is in the plugin,
# don't execute it.
my_file_type = os . path . splitext ( rpl ) [ - 1 ] . lower ( ) . replace ( ' . ' , ' ' )
if ( not my_file_type in plugin . file_types ) :
#print("{0} v{1}: Plugin '{2}' doesn't support {3} files, skipping ...".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name, my_file_type))
continue
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
if ( " acsm " in plugin . file_types or " * " in plugin . file_types ) :
#print("{0} v{1}: Plugin '{2}' would run anyways, skipping ...".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name, my_file_type))
continue
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
print ( " {0} v {1} : Executing plugin {2} ... " . format ( PLUGIN_NAME , PLUGIN_VERSION , plugin . name ) )
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
plugin . original_path_to_file = original_file_for_plugins
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
try :
plugin_ret = None
plugin_ret = plugin . run ( rpl )
except :
delete_src_file = False
print ( " {0} v {1} : Running file type plugin failed with traceback: " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( file = oe )
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
# Restore stdout and stderr, in case a plugin broke them.
sys . stdout , sys . stderr = oo , oe
2021-11-12 19:15:30 +01:00
2022-10-23 10:19:55 +02:00
if plugin_ret is not None :
# If the plugin returned a new path, update that.
print ( " {0} v {1} : Plugin returned path ' {2} ' , updating. " . format ( PLUGIN_NAME , PLUGIN_VERSION , plugin_ret ) )
rpl = plugin_ret
else :
print ( " {0} v {1} : Plugin returned nothing - skipping " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
2021-09-26 12:56:53 +02:00
2021-12-11 11:32:37 +01:00
except :
2022-10-23 10:19:55 +02:00
delete_src_file = False
print ( " {0} v {1} : Error while executing other plugins " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( )
pass
2021-09-21 17:42:51 +02:00
2022-10-23 10:19:55 +02:00
# If enabled, and if we didn't encounter any errors, delete the source ACSM file.
if delete_src_file :
try :
if os . path . exists ( path_to_ebook ) :
print ( " {0} v {1} : Deleting existing ACSM file {2} ... " . format ( PLUGIN_NAME , PLUGIN_VERSION , path_to_ebook ) )
os . remove ( path_to_ebook )
except :
print ( " {0} v {1} : Failed to delete source ACSM after fulfillment. " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
# Return path - either the original one or the one modified by the other plugins.
self . unblock ( )
return rpl
2021-09-20 15:05:07 +02:00
2022-10-23 10:19:55 +02:00
self . unblock ( )
return path_to_ebook
except :
self . unblock ( )
traceback . print_exc ( )
return path_to_ebook
2021-09-20 11:24:11 +02:00