|  06-03-2023, 05:56 AM | #1111 | |
| Wizard            Posts: 1,216 Karma: 1995558 Join Date: Aug 2015 Device: Kindle | Quote: 
 | |
|   |   | 
|  06-12-2023, 01:14 PM | #1112 | 
| Zealot  Posts: 124 Karma: 10 Join Date: Dec 2008 Location: France Device: None | 
			
			Windows 11 / calibre 6.20 / Action Chains 1.18.7 I have the same problem as Philantrop. I can't add a calibre action. Here is the json file from export. Spoiler: 
 Thanks | 
|   |   | 
| Advert | |
|  | 
|  06-13-2023, 06:38 AM | #1113 | |
| Wizard            Posts: 1,216 Karma: 1995558 Join Date: Aug 2015 Device: Kindle | Quote: 
 Anyway, here is modified version of the plugin that has print some debug statements. Please run this modified version in debug mode, and post your debug log. Last edited by capink; 06-14-2023 at 01:43 PM. Reason: remove debug version | |
|   |   | 
|  06-13-2023, 07:17 AM | #1114 | 
| Zealot  Posts: 124 Karma: 10 Join Date: Dec 2008 Location: France Device: None | 
			
			Thank you. Here is the log file (not sure if it is what you want) and a new export of the action chain, just in case. To be clear, I get the error as soon as I click the icon settings - If I want to modify a calibre action (for example, Extract ISBN in the screenshot) - If I want to add a calibre action (I also tried on a portable version of calibre, and I get the same thing) | 
|   |   | 
|  06-13-2023, 07:44 AM | #1115 | 
| Wizard            Posts: 1,216 Karma: 1995558 Join Date: Aug 2015 Device: Kindle | 
			
			From what I can see from the log the problem is one of the multiple SortAction is returning an int value of 0 instead of returning a text value (when calling QAction.text() method). I don't why this is happening and why it is not replicated on my system. Anyway try this new version, see whether it helps with the problem. Last edited by capink; 06-14-2023 at 01:42 PM. Reason: zip removed. New version with modifications releases | 
|   |   | 
| Advert | |
|  | 
|  06-13-2023, 08:50 AM | #1116 | 
| Zealot  Posts: 124 Karma: 10 Join Date: Dec 2008 Location: France Device: None | 
			
			It works.  Thanks a lot ! | 
|   |   | 
|  06-14-2023, 01:12 PM | #1117 | |
| Addict            Posts: 296 Karma: 32153 Join Date: Dec 2008 Device: Kindles (e-ink) | Quote: 
 | |
|   |   | 
|  07-05-2023, 01:07 AM | #1118 | 
| want to learn what I want            Posts: 1,679 Karma: 7908443 Join Date: Sep 2020 Device: none | 
			
			Hi, do we have a chain for Prince PDF plugin? I could only set up the first step, that runs the existing Calibre Action and brings up the plugin dialog. Then we would need some script to send alt+P, then maybe wait a second, then send alt + V.
		 | 
|   |   | 
|  07-05-2023, 04:38 AM | #1119 | 
| Member  Posts: 15 Karma: 10 Join Date: Apr 2013 Device: lots of, frequently changing | 
				
				Persistant vars?
			 
			
			Trying to use an old chain (which has worked AFAIK), I get "EXCEPTION: Formatter: Unknown function set_persistent_vars"?  Problem in front of the screen? | 
|   |   | 
|  07-05-2023, 07:21 AM | #1120 | |
| Wizard            Posts: 1,216 Karma: 1995558 Join Date: Aug 2015 Device: Kindle | Quote: 
 I thought no one used them. But if you want to get them back, copy the code below into your module editor: Code: from calibre.utils.config import JSONConfig
from calibre_plugins.action_chains.templates.functions.base import TemplateFunction
###########################################
# Persistent Vars
###########################################
persistent_storage = JSONConfig('plugins/action_chains/action-chains-persistent-storage')
# initialize root namespace if not present
if not persistent_storage.get('root_namespace'):
    persistent_storage['root_namespace'] = {}
class VarStorage(object):
    def __init__(self):
        self.root_namespace = persistent_storage['root_namespace']
        # prefixes are used to avoid vars overwriting namespaces
        self.var_type = 'VAR'
        self.ns_type = 'NS'
    # Naming rules : {{{{
    def mangle_name(self, name, typ):
        if not self.validate_name(name):
            raise ValueError('Invalid {} name: {}'.format(typ, name))
        prefix = '_{}_'.format(typ)
        name = name if name.startswith(prefix) else prefix + name
        return name
    def unmangle_name(self, name, typ):
        prefix = '_{}_'.format(typ)
        if name.startswith(prefix):
            name = name.replace(prefix, '')
        return name
    def is_var(self, name):
        prefix = '_{}_'.format(self.var_type)
        if name.startswith(prefix):
            return True
        return False
    def is_namespace(self, name):
        prefix = '_{}_'.format(self.ns_type)
        if name.startswith(prefix):
            return True
        return False
    def validate_namespace_path(self, namespace_path):
        for node in namespace_path:
            if not self.validate_name(node):
                return False
        return True
    def validate_name(self, name):
        if not name.replace('_', '').isalnum():
            return False
        return True
    def mangle_namespace_path(self, namespace_path):
        if not self.validate_namespace_path(namespace_path):
            raise ValueError('Invalid namespace path: {}'.format(namespace_path))
        mangled_path = []
        for node in namespace_path:
            mangled_node = self.mangle_name(node, self.ns_type)
            mangled_path.append(mangled_node)
        return mangled_path
    # }}}}
    def commit(self):
        persistent_storage.commit()
    def get_namespace(self, namespace_path=[]):
        mangled_path = self.mangle_namespace_path(namespace_path)
        ns = self.root_namespace
        for node in mangled_path:
            try:
                ns = ns[node]
            except KeyError as e:
                raise ValueError('Namespace cannot be found: {}'.format(namespace_path))
        return ns
    def create_namespace_if_not_present(self, namespace_path=[]):
        mangled_path = self.mangle_namespace_path(namespace_path)
        ns = self.root_namespace
        for node in mangled_path:
            if not ns.get(node):
                ns[node] = {}
            ns = ns[node]
    def get_var(self, var_name, namespace_path=[]):
        try:
            namespace = self.get_namespace(namespace_path)
        except:
            return ''
        name = self.mangle_name(var_name, self.var_type)
        return namespace.get(name, '')
    def set_var(self, var_name, var_value, namespace_path=[]):
        if not isinstance(var_value, str):
            raise ValueError('Persistent variable values can only of string type')
        self.create_namespace_if_not_present(namespace_path)
        namespace = self.get_namespace(namespace_path)
        name = self.mangle_name(var_name, self.var_type)
        namespace[name] = var_value
        self.commit()
    def list_vars(self, namespace_path=[]):
        namespace = self.get_namespace(namespace_path)
        all_vars = []
        for name in namespace.keys():
            if self.is_var(name):
                var = self.unmangle_name(name, self.var_type)
                all_vars.append(var)
        return all_vars
    def list_namespaces(self, namespace_path=[]):
        if not namespace_path:
            namespace = self.root_namespace
        else:
            namespace = self.get_namespace(namespace_path)
        all_ns = []
        for name in namespace.keys():
            if self.is_namespace(name):
                ns = self.unmangle_name(name, self.ns_type)
                all_ns.append(ns)
        return all_ns
    def delete_namespace(self, namespace_path=[]):
        if not namespace_path:
            self.root_namespace.clear()
        else:
            if len(namespace_path) == 1:
                parent = self.root_namespace
            elif len(namespace_path) > 1:
                parent_path = namespace_path[:-1]
                parent = self.get_namespace(parent_path)
            mangled_name = self.mangle_name(namespace_path[-1], self.ns_type)
            del parent[mangled_name]
        self.commit()
    def delete_var(self, var_name, namespace_path=[]):
        namespace = self.get_namespace(namespace_path)
        mangled_name = self.mangle_name(var_name, self.var_type)
        del namespace[mangled_name]
        self.commit()
persistent_vars = VarStorage()
###########################################
# Persistent Vars Template Functions
###########################################
class PersistentVars(TemplateFunction):
    doc = _('Retrieve value for variable stored in persistent memory using set_persistent_vars()\n'
            'Usage: persistent_vars(var_name, [namespace])\n'
            '`var_name`: name of the variable\n'
            '`namespace`: (optional) namespace where the variable is stored. Nested namespaces '
            'are possible and are represented by dot notation e.g. namespace1.namspace2 '
            'If no namespace is provided, the default namespace will be used.\n'
            'This function is part of the action chain plugin and will not work elsewhere.')
    name = 'persistent_vars'
    arg_count = -1
    def evaluate(self, formatter, kwargs, mi, locals, *args):
        if len(args) not in [1, 2]:
            raise ValueError('Incorrect number of arguments')
        var_name = args[0]
        if len(args) == 2:
            namespace_path = args[1].split('.')
        else:
            namespace_path = []
        if not persistent_vars.validate_namespace_path(namespace_path):
            raise ValueError('Namespaces must contain only letter, numbers and underscores')
        try:
            return persistent_vars.get_var(var_name, namespace_path)
        except:
            # if namespace cannot be found
            return ''
class SetPersistentVars(TemplateFunction):
    doc = _('Set variables to presistent memory\n'
            'Usage: set_persistent_vars(var_name, var_value, [namespace])\n'
            '`var_name`: name of the variable\n'
            '`var_value`: value of the variable\n'
            '`namespace`: (optional) namespace where the variable is stored. Nested namespaces '
            'are possible and are represented by dot notation e.g. namespace1.namspace2 '
            'If no namespace is provided, the default namespace will be used.\n'
            'This function is part of the action chain plugin and will not work elsewhere.')
    name = 'set_persistent_vars'
    arg_count = -1
    def evaluate(self, formatter, kwargs, mi, locals, *args):
        if len(args) not in [2, 3]:
            raise ValueError('Incorrect number of arguments')
        var_name = args[0]
        var_value = args[1]
        if not persistent_vars.validate_name(var_name):
            raise ValueError('Invalid variable name: {}'.format(var_name))
        if len(args) == 3:
            namespace_path = args[2].split('.')
        else:
            namespace_path = []
        if not persistent_vars.validate_namespace_path(namespace_path):
            raise ValueError('Namespaces must contain only letter, numbers and underscores')
        # We don't want to write vars when the user is typing in the template
        if not self.context == 'template_dialog_mode':
            persistent_vars.set_var(var_name, var_value, namespace_path)
        return ''
class DeletePersistentVars(TemplateFunction):
    doc = _('Delete variable from presistent memory\n'
            'Usage: set_persistent_vars(var_name, var_value, [namespace])\n'
            '`var_name`: name of the variable\n'
            '`namespace`: (optional) namespace where the variable is stored. Nested namespaces '
            'are possible and are represented by dot notation e.g. namespace1.namspace2 '
            'If no namespace is provided, the default namespace will be used.\n'
            'This function is part of the action chain plugin and will not work elsewhere.')
    name = 'delete_persistent_vars'
    arg_count = -1
    def evaluate(self, formatter, kwargs, mi, locals, *args):
        if len(args) not in [1, 2]:
            raise ValueError('Incorrect number of arguments')
        var_name = args[0]
        if not persistent_vars.validate_name(var_name):
            raise ValueError('Invalid variable name: {}'.format(var_name))
        if len(args) == 2:
            namespace_path = args[1].split('.')
        else:
            namespace_path = []
        if not persistent_vars.validate_namespace_path(namespace_path):
            raise ValueError('Namespaces must contain only letter, numbers and underscores')
        # We don't want to delete vars when the user is typing in the template
        if not self.context == 'template_dialog_mode':
            try:
                persistent_vars.delete_var(var_name, namespace_path)
            except Exception as e:
                import traceback
                print(traceback.format_exc())
        return ''
class ListPersistentVars(TemplateFunction):
    doc = _('List variables stored in presistent memory using set_persistent_vars()\n'
            'Usage: list_persistent_vars([namespace])\n'
            '`namespace`: (optional) namespace where the variable is stored. Nested namespaces '
            'are possible and are represented by dot notation e.g. namespace1.namspace2 '
            'If no namespace is provided, the default namespace will be used.\n'
            'This function is part of the action chain plugin and will not work elsewhere.')
    name = 'list_persistent_vars'
    arg_count = -1
    def evaluate(self, formatter, kwargs, mi, locals, *args):
        if len(args) not in [0, 1]:
            raise ValueError('Incorrect number of arguments')
        if len(args) == 1:
            namespace_path = args[0].split('.')
        else:
            namespace_path = []
        if not persistent_vars.validate_namespace_path(namespace_path):
            raise ValueError('Namespaces must contain only letter, numbers and underscores')
        all_vars = persistent_vars.list_vars(namespace_path)
        return ','.join(all_vars)
class ListPersistentNamespaces(TemplateFunction):
    doc = _('List namespaces in presistent memory used by set_persistent_vars()\n'
            'Usage: list_persistent_namespaces([namespace])\n'
            '`namespace`: (optional) namespace where the variable is stored. Nested namespaces '
            'are possible and are represented by dot notation e.g. namespace1.namspace2 '
            'If no namespace is provided, all top level namespaces will be listed.\n'
            'This function is part of the action chain plugin and will not work elsewhere.')
    name = 'list_persistent_namespaces'
    arg_count = -1
    def evaluate(self, formatter, kwargs, mi, locals, *args):
        if len(args) not in [0, 1]:
            raise ValueError('Incorrect number of arguments')
        if len(args) == 1:
            namespace_path = args[0].split('.')
        else:
            namespace_path = []
        if not persistent_vars.validate_namespace_path(namespace_path):
            raise ValueError('Namespaces must contain only letter, numbers and underscores')
        all_ns = persistent_vars.list_namespaces(namespace_path)
        return ','.join(all_ns)
class DeletePersistentNamespaces(TemplateFunction):
    doc = _('Delete a namespace in presistent memory used by set_persistent_vars()\n'
            'Usage: delete_persistent_namespaces([namespace])\n'
            '`namespace`: (optional) namespace where the variable is stored. Nested namespaces '
            'are possible and are represented by dot notation e.g. namespace1.namspace2 '
            'If no namespace is provided, the top level namespace including all namespaces and '
            'vars will be deleted\n'
            'This function is part of the action chain plugin and will not work elsewhere.')
    name = 'delete_persistent_namespaces'
    arg_count = -1
    def evaluate(self, formatter, kwargs, mi, locals, *args):
        if len(args) not in [0, 1]:
            raise ValueError('Incorrect number of arguments')
        if len(args) == 1:
            namespace_path = args[0].split('.')
        else:
            namespace_path = []
        if not persistent_vars.validate_namespace_path(namespace_path):
            raise ValueError('Namespaces must contain only letter, numbers and underscores')
        # We don't want to delete namespaces when the user is typing in the template
        if not self.context == 'template_dialog_mode':
            try:
                persistent_vars.delete_namespace(namespace_path)
            except Exception as e:
                import traceback
                print(traceback.format_exc())
        return '' | |
|   |   | 
|  07-05-2023, 07:24 AM | #1121 | 
| Wizard            Posts: 1,216 Karma: 1995558 Join Date: Aug 2015 Device: Kindle | 
			
			No. It it is not working through Calibre Actions, the only way is for someone to write a custom action that calls the plugin's code directly.
		 | 
|   |   | 
|  07-05-2023, 07:36 AM | #1122 | 
| want to learn what I want            Posts: 1,679 Karma: 7908443 Join Date: Sep 2020 Device: none | |
|   |   | 
|  07-05-2023, 10:28 AM | #1123 | 
| Member  Posts: 15 Karma: 10 Join Date: Apr 2013 Device: lots of, frequently changing | 
			
			Thank you, code works, I'll switch then to python templates.
		 | 
|   |   | 
|  07-12-2023, 10:45 AM | #1124 | 
| Custom User Title            Posts: 11,347 Karma: 79528341 Join Date: Oct 2018 Location: Canada Device: Kobo Libra H2O, formerly Aura HD | 
			
			Question: Is there a method to add a file to the extra data folder similar to SFE on 'formats'? Context: I have a chain that copies the cover path to clipboard then follows it with the 'add data file' Calibre action where I paste it in. It's a little clunky though. | 
|   |   | 
|  07-12-2023, 02:16 PM | #1125 | |
| Grand Sorcerer            Posts: 12,525 Karma: 8065948 Join Date: Jan 2010 Location: Notts, England Device: Kobo Libra 2 | Quote: 
 Code: python:
def evaluate(book, context):
	import os
	from shutil import copy
	from calibre.db.constants import DATA_DIR_NAME
	db = context.db
	cache = db.new_api
	# Get the normalized library path
	library_path = cache.backend.library_path
	
    # Construct the full path to the book folder
	path = os.path.join(library_path, cache.field_for('path', book.id).replace('/', os.sep))
	# Ensure the data directory exists
	data_dir = os.path.join(path, DATA_DIR_NAME)
	if not os.path.exists(data_dir):
		os.mkdir(data_dir)
	cover_file = os.path.join(path, 'cover.jpg')
	if os.path.exists(cover_file):
		# It does. Copy it to the data directory. The 'copy' method takes a
		# directory as a target.
		copy(cover_file, data_dir)
		return 'copied'
	return 'not copied' | |
|   |   | 
|  | 
| 
 | 
|  Similar Threads | ||||
| Thread | Thread Starter | Forum | Replies | Last Post | 
| Action Chains Resources | capink | Plugins | 80 | 09-18-2025 04:45 AM | 
| [Editor Plugin] Editor Chains | capink | Plugins | 106 | 06-17-2025 05:36 PM | 
| [GUI Plugin] Noosfere_util, a companion plugin to noosfere DB | lrpirlet | Plugins | 2 | 08-18-2022 03:15 PM | 
| [GUI Plugin] Save Virtual Libraries To Column (GUI) | chaley | Plugins | 14 | 04-04-2021 05:25 AM |