From 4dfc7af7b8c7d06fa512223efc4aeba58e840a91 Mon Sep 17 00:00:00 2001 From: Florian Bach Date: Sun, 23 Oct 2022 10:19:55 +0200 Subject: [PATCH] Add setting to choose between parallel or sequencial fulfillment --- calibre-plugin/__init__.py | 281 +++++++++++++++++++++++-------------- calibre-plugin/config.py | 7 + calibre-plugin/prefs.py | 4 + 3 files changed, 188 insertions(+), 104 deletions(-) diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 78c6ae8..c6bcc9c 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -47,10 +47,13 @@ # rename plugin from "DeACSM" to "ACSM Input". BETA build, not a normal release!! # # v0.1.0: Continue work on renaming from "DeACSM" to "ACSM Input". -# The big version number jump is to make that name change clearer. +# The big version number jump is to make that name change clearer, +# and to support the "migration plugin" to rename the plugin. # 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, +# add setting to choose between simultaneous (faster) or sequencial (more ADE-like) +# import of multiple ACSM files @@ -63,7 +66,6 @@ __version__ = PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE]) from calibre.utils.config import config_dir # type: ignore -from calibre.constants import islinux # type: ignore import os, shutil, traceback, sys, time, io, random import zipfile @@ -406,146 +408,217 @@ class ACSMInput(FileTypePlugin): print("{0} v{1}: Error: Unsupported file type ...".format(PLUGIN_NAME, PLUGIN_VERSION)) return None + 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 + + + + def run(self, path_to_ebook): # type: (str) -> str + 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 - # This code gets called by Calibre with a path to the new book file. - # We need to check if it's an ACSM file + import calibre_plugins.deacsm.prefs as prefs # type: ignore + deacsmprefs = prefs.ACSMInput_Prefs() - print("{0} v{1}: Trying to parse file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) + if deacsmprefs['allow_parallel_fulfillment'] == False: + self.wait_and_block() - ext = os.path.splitext(path_to_ebook)[1].lower() - if (ext != ".acsm"): - print("{0} v{1}: That's not an ACSM, returning (is {2} instead)... ".format(PLUGIN_NAME, PLUGIN_VERSION, ext)) - return path_to_ebook + print("{0} v{1}: Trying to parse file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) - # That's an ACSM. - # We would fulfill this now, but first perform some sanity checks ... + ext = os.path.splitext(path_to_ebook)[1].lower() - if not self.ADE_sanity_check(): - print("{0} v{1}: ADE auth is missing or broken ".format(PLUGIN_NAME, PLUGIN_VERSION)) - return path_to_ebook + 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 + # That's an ACSM. + # We would fulfill this now, but first perform some sanity checks ... - from libadobe import are_ade_version_lists_valid - from libadobeFulfill import fulfill + 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 - 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)) - return path_to_ebook - print("{0} v{1}: Try to fulfill ...".format(PLUGIN_NAME, PLUGIN_VERSION)) + from libadobe import are_ade_version_lists_valid + from libadobeFulfill import fulfill - success, replyData = fulfill(path_to_ebook, deacsmprefs["notify_fulfillment"]) + 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 - 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 + print("{0} v{1}: Try to fulfill ...".format(PLUGIN_NAME, PLUGIN_VERSION)) - # 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. + success, replyData = fulfill(path_to_ebook, deacsmprefs["notify_fulfillment"]) - # 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"] + 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 - try: - from calibre.customize.ui import _initialized_plugins, is_disabled - from calibre.customize import FileTypePlugin + # 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. - original_file_for_plugins = rpl + # 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"] - oo, oe = sys.stdout, sys.stderr + try: + from calibre.customize.ui import _initialized_plugins, is_disabled + from calibre.customize import FileTypePlugin - for plugin in _initialized_plugins: + original_file_for_plugins = rpl - #print("{0} v{1}: Plugin '{2}' has prio {3}".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name, plugin.priority)) + oo, oe = sys.stdout, sys.stderr - # 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 + for plugin in _initialized_plugins: - # 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 + #print("{0} v{1}: Plugin '{2}' has prio {3}".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name, plugin.priority)) - if plugin.name == self.name: - #print("{0} v{1}: Plugin '{2}' is me - skipping".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name)) - continue + # 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 - # 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 + # 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 - # 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 + if plugin.name == self.name: + #print("{0} v{1}: Plugin '{2}' is me - skipping".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name)) + continue - 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 + # 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 - print("{0} v{1}: Executing plugin {2} ...".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name)) + # 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 - plugin.original_path_to_file = original_file_for_plugins + 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 - 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) + print("{0} v{1}: Executing plugin {2} ...".format(PLUGIN_NAME, PLUGIN_VERSION, plugin.name)) - # Restore stdout and stderr, in case a plugin broke them. - sys.stdout, sys.stderr = oo, oe + plugin.original_path_to_file = original_file_for_plugins + 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) - 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)) + # Restore stdout and stderr, in case a plugin broke them. + sys.stdout, sys.stderr = oo, oe - - except: - delete_src_file = False - print("{0} v{1}: Error while executing other plugins".format(PLUGIN_NAME, PLUGIN_VERSION)) - traceback.print_exc() - pass + 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)) + + - # 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. - return rpl + delete_src_file = False + print("{0} v{1}: Error while executing other plugins".format(PLUGIN_NAME, PLUGIN_VERSION)) + traceback.print_exc() + pass + # 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 - return path_to_ebook + self.unblock() + return path_to_ebook + except: + self.unblock() + traceback.print_exc() + return path_to_ebook diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index 69f6ac0..d8df061 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -55,6 +55,7 @@ class ConfigWidget(QWidget): self.tempdeacsmprefs['notify_fulfillment'] = self.deacsmprefs['notify_fulfillment'] self.tempdeacsmprefs['detailed_logging'] = self.deacsmprefs['detailed_logging'] self.tempdeacsmprefs['delete_acsm_after_fulfill'] = self.deacsmprefs['delete_acsm_after_fulfill'] + self.tempdeacsmprefs['allow_parallel_fulfillment'] = self.deacsmprefs['allow_parallel_fulfillment'] self.tempdeacsmprefs['list_of_rented_books'] = self.deacsmprefs['list_of_rented_books'] @@ -176,6 +177,11 @@ class ConfigWidget(QWidget): self.chkDeleteAfterFulfill.toggled.connect(self.toggle_acsm_delete) layout.addWidget(self.chkDeleteAfterFulfill) + self.chkParallelFulfill = QtGui.QCheckBox("Allow parallel fulfillment") + self.chkParallelFulfill.setToolTip("Default: True\n\nIf this is enabled (which was the default in previous versions), \nthe plugin will import multiple ACSM files simultaneously when you add more than one.\n\nIf this is disabled, it will add them one after another like ADE.") + self.chkParallelFulfill.setChecked(self.tempdeacsmprefs["allow_parallel_fulfillment"]) + layout.addWidget(self.chkParallelFulfill) + # Key shortcut Ctrl+Shift+D / Cmd+Shift+D to remove authorization, just like in ADE. self.deauthShortcut = QShortcut(QKeySequence("Ctrl+Shift+D"), self) self.deauthShortcut.activated.connect(self.delete_ade_auth) @@ -1269,6 +1275,7 @@ class ConfigWidget(QWidget): self.deacsmprefs.set('notify_fulfillment', self.chkNotifyFulfillment.isChecked()) self.deacsmprefs.set('detailed_logging', self.chkDetailedLogging.isChecked()) self.deacsmprefs.set('delete_acsm_after_fulfill', self.chkDeleteAfterFulfill.isChecked()) + self.deacsmprefs.set('allow_parallel_fulfillment', self.chkParallelFulfill.isChecked()) self.deacsmprefs.writeprefs() def load_resource(self, name): diff --git a/calibre-plugin/prefs.py b/calibre-plugin/prefs.py index 3bf73fc..a3070af 100644 --- a/calibre-plugin/prefs.py +++ b/calibre-plugin/prefs.py @@ -28,8 +28,12 @@ class ACSMInput_Prefs(): self.deacsmprefs.defaults['notify_fulfillment'] = True self.deacsmprefs.defaults['detailed_logging'] = False self.deacsmprefs.defaults['delete_acsm_after_fulfill'] = False + self.deacsmprefs.defaults['allow_parallel_fulfillment'] = True self.deacsmprefs.defaults['loan_identifier_token'] = 0 + self.deacsmprefs.defaults['fulfillment_block_token'] = 0 + self.deacsmprefs.defaults['fulfillment_block_time'] = 0 + self.deacsmprefs.defaults['list_of_rented_books'] = []