Add GUI, rename to "ACSM Input"

This commit is contained in:
Florian Bach 2022-09-05 18:34:40 +02:00
parent 9be0d5e55d
commit 2904e187c4
12 changed files with 308 additions and 58 deletions

View File

@ -35,16 +35,16 @@ body:
id: calibre-version
attributes:
label: Which version of Calibre are you running?
description: "Example: 6.2.1"
placeholder: "6.2.1"
description: "Example: 6.4"
placeholder: "6.4"
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Which version of the DeACSM plugin are you running?
description: "Example: v0.0.16"
placeholder: "v0.0.16"
description: "Example: v0.0.17"
placeholder: "v0.0.17"
validations:
required: true
- type: textarea

View File

@ -43,10 +43,11 @@
# update python-oscrypto to unofficial fork to fix OpenSSL 3 support.
# In Progress:
# Fix bug that would sometimes return the wrong book (or none at all) if you had
# multiple active loans from the same distributor.
# multiple active loans from the same distributor, add experimental GUI button,
# rename plugin from "DeACSM" to "ACSM Input"
PLUGIN_NAME = "DeACSM"
PLUGIN_NAME = "ACSM Input"
PLUGIN_VERSION_TUPLE = (0, 0, 16)
from calibre.customize import FileTypePlugin # type: ignore
@ -60,10 +61,11 @@ from calibre.constants import isosx, iswindows, islinux # type:
import os, shutil, traceback, sys, time, io, random
import zipfile
from lxml import etree
from calibre.gui2 import error_dialog
#@@CALIBRE_COMPAT_CODE@@
class DeACSM(FileTypePlugin):
class ACSMInput(FileTypePlugin):
name = PLUGIN_NAME
description = "ACSM Input Plugin - Takes an Adobe ACSM file and converts that into a useable EPUB or PDF file. Python reimplementation of libgourou by Grégory Soutadé"
supported_platforms = ['linux', 'osx', 'windows']
@ -75,7 +77,35 @@ class DeACSM(FileTypePlugin):
on_preprocess = True
priority = 2000
def init_embedded_plugins(self):
"""
A Calibre plugin can normally only contain one Plugin class.
In our case, this would be the DeACSM class.
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
from calibre_plugins.deacsm.gui_main_wrapper import DeACSMGUIExtension
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
init_plg(DeACSMGUIExtension)
def initialize(self):
"""
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
@ -98,7 +128,49 @@ class DeACSM(FileTypePlugin):
self.pluginsdir = os.path.join(config_dir,"plugins")
if not os.path.exists(self.pluginsdir):
os.mkdir(self.pluginsdir)
self.maindir = os.path.join(self.pluginsdir,"DeACSM")
# Okay, "I" am now the new version. If I'm running under the old name,
# move "me" to the new one.
if os.path.exists(os.path.join(self.pluginsdir, "DeACSM.zip")):
from calibre.customize.ui import _config
shutil.copyfile(os.path.join(self.pluginsdir, "DeACSM.zip"), os.path.join(self.pluginsdir, "ACSM Input.zip"))
# Delete the old plugin.
os.remove(os.path.join(self.pluginsdir, "DeACSM.zip"))
# Forcibly add the new plugin, circumventing the Calibre code.
ui_plg_config = _config()
plugins = ui_plg_config['plugins']
plugins["ACSM Input"] = os.path.join(self.pluginsdir, "ACSM Input.zip")
ui_plg_config['plugins'] = plugins
return
# Make sure the GUI extension is loaded:
self.init_embedded_plugins()
self.maindir_old = os.path.join(self.pluginsdir,"DeACSM")
self.maindir = os.path.join(self.pluginsdir,"ACSMInput")
if os.path.exists(self.maindir_old) and not os.path.exists(self.maindir):
# Migrate config to new folder
os.rename(self.maindir_old, self.maindir)
if not iswindows:
# Linux and Mac support symlinks, so create one so the old paths
# still work and people can downgrade the plugin again.
# Windows ... doesn't, so downgrading will be tricky.
try:
os.symlink(self.maindir_old, self.maindir)
except:
pass
if not os.path.exists(self.maindir):
os.mkdir(self.maindir)
@ -222,7 +294,7 @@ class DeACSM(FileTypePlugin):
import calibre_plugins.deacsm.prefs as prefs # type: ignore
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
update_account_path(deacsmprefs["path_to_account_data"])
except Exception as e:
@ -242,7 +314,7 @@ class DeACSM(FileTypePlugin):
def ADE_sanity_check(self):
import calibre_plugins.deacsm.prefs as prefs # type: ignore
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
from libadobe import get_activation_xml_path
@ -403,7 +475,7 @@ class DeACSM(FileTypePlugin):
print("{0} v{1}: Try to fulfill ...".format(PLUGIN_NAME, PLUGIN_VERSION))
import calibre_plugins.deacsm.prefs as prefs # type: ignore
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
success, replyData = fulfill(path_to_ebook, deacsmprefs["notify_fulfillment"])

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -46,7 +46,7 @@ class ConfigWidget(QWidget):
self.plugin_path = plugin_path
# get the prefs
self.deacsmprefs = prefs.DeACSM_Prefs()
self.deacsmprefs = prefs.ACSMInput_Prefs()
# make a local copy
self.tempdeacsmprefs = {}
@ -58,7 +58,6 @@ class ConfigWidget(QWidget):
self.tempdeacsmprefs['list_of_rented_books'] = self.deacsmprefs['list_of_rented_books']
# Start Qt Gui dialog layout
layout = QVBoxLayout(self)
self.setLayout(layout)
@ -150,7 +149,13 @@ class ConfigWidget(QWidget):
self.button_rented_books.setEnabled(activated)
ua_group_box_layout.addWidget(self.button_rented_books)
if (len(self.deacsmprefs["list_of_rented_books"]) == 0):
# First remove all overdue books from the loan list,
# to determine if we should enable the button.
self.delete_overdue_books_from_loan_list()
if (len(self.tempdeacsmprefs["list_of_rented_books"]) == 0):
self.button_rented_books.setText(_("No loaned books available"))
self.button_rented_books.setEnabled(False)
@ -1275,17 +1280,47 @@ class ConfigWidget(QWidget):
def show_rented_books(self):
d = RentedBooksDialog(self, self.deacsmprefs["list_of_rented_books"])
self.delete_overdue_books_from_loan_list()
d = RentedBooksDialog(self)
d.exec_()
def delete_overdue_books_from_loan_list(self):
overdue_books = []
for book in self.deacsmprefs["list_of_rented_books"]:
try:
book_time_stamp = book["validUntil"]
timestamp = datetime.datetime.strptime(book_time_stamp, "%Y-%m-%dT%H:%M:%SZ")
currenttime = datetime.datetime.utcnow()
except:
# Invalid book timestano
continue
if (timestamp <= currenttime):
# Book is overdue, no need to return. Delete from list.
overdue_books.append(book)
continue
templist = self.deacsmprefs["list_of_rented_books"]
for book in overdue_books:
templist.remove(book)
self.deacsmprefs.set("list_of_rented_books", templist)
self.deacsmprefs.writeprefs()
class RentedBooksDialog(QDialog):
def __init__(self, parent, booklist):
def __init__(self, parent):
QDialog.__init__(self,parent)
self.parent = parent
self.setWindowTitle("DeACSM: Manage loaned Books")
self.deacsmprefs = prefs.ACSMInput_Prefs()
# Start Qt Gui dialog layout
layout = QVBoxLayout(self)
self.setLayout(layout)
@ -1353,37 +1388,22 @@ class RentedBooksDialog(QDialog):
def populate_list(self):
self.listy.clear()
overdue_books = []
for book in self.parent.deacsmprefs["list_of_rented_books"]:
for book in self.deacsmprefs["list_of_rented_books"]:
try:
book_time_stamp = book["validUntil"]
timestamp = datetime.datetime.strptime(book_time_stamp, "%Y-%m-%dT%H:%M:%SZ")
currenttime = datetime.datetime.utcnow()
except:
# Invalid book timestano
# Invalid book timestamp
continue
if (timestamp <= currenttime):
# Book is overdue, no need to return. Delete from list.
overdue_books.append(book)
continue
else:
info = "(" + self.td_format(timestamp - currenttime)
info += " remaining)"
info = "(" + self.td_format(timestamp - currenttime) + " remaining)"
item = QListWidgetItem(book["book_name"] + " " + info)
item.setData(QtCore.Qt.UserRole, book["loanID"])
self.listy.addItem(item)
for book in overdue_books:
self.parent.deacsmprefs["list_of_rented_books"].remove(book)
self.parent.deacsmprefs.writeprefs()
def return_book(self):
if not self.listy.currentItem():
@ -1400,7 +1420,7 @@ class RentedBooksDialog(QDialog):
traceback.print_exc()
Ret_book = None
for book in self.parent.deacsmprefs["list_of_rented_books"]:
for book in self.deacsmprefs["list_of_rented_books"]:
if book["loanID"] == userdata:
Ret_book = book
break
@ -1430,14 +1450,16 @@ class RentedBooksDialog(QDialog):
success = False
done = False
templist = self.deacsmprefs["list_of_rented_books"]
while not done:
done = True
for book in self.parent.deacsmprefs["list_of_rented_books"]:
for book in templist:
if book["loanID"] == userdata:
done = False
self.parent.deacsmprefs["list_of_rented_books"].remove(book)
templist.remove(book)
success = True
break
self.deacsmprefs.set("list_of_rented_books", templist)
self.populate_list()

View File

@ -101,7 +101,7 @@ def GetMasterKey():
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass

View File

@ -11,7 +11,7 @@ def GetMasterKey(wineprefix):
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass

View File

@ -115,7 +115,7 @@ def GetMasterKey():
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass

116
calibre-plugin/gui_main.py Normal file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# GUI for the ACSM plugin.
#
# "create_menu_action_unique" taken from the Quality Check plugin:
# GPLv3, Copyright 2011, Grant Drake <grant.drake@gmail.com>
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.actions import menu_action_unique_name
from PyQt5.QtGui import QMenu, QToolButton
#@@CALIBRE_COMPAT_CODE@@
def create_menu_action_unique(ia, parent_menu, menu_text, image=None, tooltip=None,
shortcut=None, triggered=None, is_checked=None, shortcut_name=None,
unique_name=None, favourites_menu_unique_name=None):
'''
Create a menu action with the specified criteria and action, using the new
InterfaceAction.create_menu_action() function which ensures that regardless of
whether a shortcut is specified it will appear in Preferences->Keyboard
'''
orig_shortcut = shortcut
kb = ia.gui.keyboard
if unique_name is None:
unique_name = menu_text
if not shortcut == False:
full_unique_name = menu_action_unique_name(ia, unique_name)
if full_unique_name in kb.shortcuts:
shortcut = False
else:
if shortcut is not None and not shortcut == False:
if len(shortcut) == 0:
shortcut = None
else:
shortcut = _(shortcut)
if shortcut_name is None:
shortcut_name = menu_text.replace('&','')
ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut,
description=tooltip, triggered=triggered, shortcut_name=shortcut_name)
if shortcut == False and not orig_shortcut == False:
if ac.calibre_shortcut_unique_name in ia.gui.keyboard.shortcuts:
kb.replace_action(ac.calibre_shortcut_unique_name, ac)
#if image:
#ac.setIcon(get_icons(image, "ACSM Input"))
return ac
class ActualDeACSMGUIExtension(InterfaceAction):
name = "ACSM Input Plugin GUI Extension"
popup_type = QToolButton.ToolButtonPopupMode.InstantPopup
action_type = 'global'
action_spec = ("ACSM Input", None, "ACSM Input Plugin by Leseratte10", None)
# Text, icon, tooltip, keyboard shortcut
def genesis(self):
print("Genesis!")
self.menu = QMenu(self.gui)
self.rebuild_menus()
self.qaction.setMenu(self.menu)
icon = get_icons('acsm_logo_2.png', "ACSM Input Plugin")
self.qaction.setIcon(icon)
#self.qaction.triggered.connect(self.trigger_config_dialog)
def rebuild_menus(self):
m = self.menu
m.clear()
create_menu_action_unique(self, m, "ACSM Input configuration", None, shortcut=None, shortcut_name="Open ACSM Input plugin settings dialog", triggered=self.trigger_config_dialog)
create_menu_action_unique(self, m, "Show loaned books", None, shortcut=None, shortcut_name="ACSM: Open list of loaned books", triggered=self.trigger_loan_dialog)
def trigger_loan_dialog(self):
import calibre_plugins.deacsm.prefs as prefs
from calibre.gui2 import info_dialog
deacsmprefs = prefs.ACSMInput_Prefs()
if (len(deacsmprefs["list_of_rented_books"]) == 0):
return info_dialog(None, "No loaned books", "You currently have no loaned books.", show=True, show_copy_button=False)
from calibre_plugins.deacsm.config import RentedBooksDialog # type: ignore
d = RentedBooksDialog(self.gui)
d.exec_()
def trigger_config_dialog(self):
from calibre.customize.ui import _initialized_plugins
from calibre_plugins.deacsm.__init__ import PLUGIN_NAME
from calibre.gui2 import error_dialog
plg = None
for plugin in _initialized_plugins:
if plugin.name == PLUGIN_NAME:
plg = plugin
break
if plg is None:
msg = "Tried to open the ACSM Input plugin (DeACSM) settings, but I couldn't find the ACSM Input plugin. "
msg += "This is most likely a bug in the plugin. Try restarting Calibre, and if you still get this error, "
msg += "please open a bug report. "
return error_dialog(None, "Plugin not found", msg, show=True)
plg.do_user_config(self.gui)

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# GUI for the ACSM plugin.
#
from calibre.customize import InterfaceActionBase # type: ignore
from calibre.customize import PluginInstallationType
#@@CALIBRE_COMPAT_CODE@@
class DeACSMGUIExtension(InterfaceActionBase):
name = "ACSM Input Plugin GUI Extension"
description = "GUI code for ACSM Input Plugin (DeACSM). This is automatically installed and updated with the ACSM plugin."
supported_platforms = ['linux', 'osx', 'windows']
author = "Leseratte10"
minimum_calibre_version = (4, 0, 0)
can_be_disabled = False
# This plugin will be auto-loaded from the ACSM Input plugin. It doesn't make sense for the user
# to disable it. If necessary, the menu bar button can be removed through the Calibre settings.
type = "File type"
# Just so that the GUI extension shows up at the same place as the actual ACSM Input plugin.
installation_type = PluginInstallationType.EXTERNAL
# Mark this as user-installed so it shows up in the plugin list by default.
actual_plugin = "calibre_plugins.deacsm.gui_main:ActualDeACSMGUIExtension"
def is_customizable(self):
return False

View File

@ -787,7 +787,7 @@ def activateDevice(useVersionIndex = 0, proxyData = None):
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass

View File

@ -321,7 +321,7 @@ def fulfill(acsm_file, do_notify = False):
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass
@ -549,7 +549,7 @@ def updateLoanReturnData(fulfillmentResultToken, forceTestBehaviour=False):
try:
import calibre_plugins.deacsm.prefs as prefs # type: ignore
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
except:
print("Exception while reading config file")
return False
@ -583,7 +583,7 @@ def tryReturnBook(bookData):
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass
@ -620,7 +620,7 @@ def tryReturnBook(bookData):
etree.SubElement(full_text_xml, etree.QName(NSMAP["adept"], "signature")).text = signature
print("Would notify server %s:" % (operatorURL + "/LoanReturn"))
print("Notifying loan return server %s" % (operatorURL + "/LoanReturn"))
doc_send = "<?xml version=\"1.0\"?>\n" + etree.tostring(full_text_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
if verbose_logging:
print(doc_send)
@ -650,7 +650,7 @@ def performFulfillmentNotification(fulfillmentResultToken, forceOptional = False
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
deacsmprefs = prefs.ACSMInput_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass

View File

@ -8,12 +8,20 @@ import os
import traceback
from calibre.utils.config import JSONConfig, config_dir # type: ignore
from calibre_plugins.deacsm.__init__ import PLUGIN_NAME # type: ignore
from calibre.constants import iswindows # type: ignore
class DeACSM_Prefs():
class ACSMInput_Prefs():
def __init__(self):
JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
JSON_PATH_OLD = os.path.join("plugins", "deacsm.json")
JSON_PATH = os.path.join("plugins", "ACSMInput", "ACSMInput.json")
if os.path.exists(JSON_PATH_OLD) and not os.path.exists(JSON_PATH):
os.rename(JSON_PATH_OLD, JSON_PATH)
if not iswindows:
os.symlink(JSON_PATH_OLD, JSON_PATH)
self.deacsmprefs = JSONConfig(JSON_PATH)
self.deacsmprefs.defaults['configured'] = False
@ -30,16 +38,12 @@ class DeACSM_Prefs():
self.pluginsdir = os.path.join(config_dir,"plugins")
if not os.path.exists(self.pluginsdir):
os.mkdir(self.pluginsdir)
self.maindir = os.path.join(self.pluginsdir,"DeACSM")
if not os.path.exists(self.maindir):
os.mkdir(self.maindir)
self.maindir = os.path.join(self.pluginsdir,"ACSMInput")
self.accountdir = os.path.join(self.maindir,"account")
if not os.path.exists(self.accountdir):
os.mkdir(self.accountdir)
raise Exception("Why does the account folder not exist?")
# Default to the builtin UA
# Default to the builtin account path
self.deacsmprefs.defaults['path_to_account_data'] = self.accountdir