1# -*- coding: utf-8 -*- 2# 3# Picard, the next-generation MusicBrainz tagger 4# 5# Copyright (C) 2007-2008 Lukáš Lalinský 6# Copyright (C) 2009 Carlin Mangar 7# Copyright (C) 2009, 2014, 2017-2020 Philipp Wolfer 8# Copyright (C) 2011 johnny64 9# Copyright (C) 2011-2013 Michael Wiencek 10# Copyright (C) 2013 Sebastian Ramacher 11# Copyright (C) 2013 Wieland Hoffmann 12# Copyright (C) 2013 brainz34 13# Copyright (C) 2013-2014 Sophist-UK 14# Copyright (C) 2014 Johannes Dewender 15# Copyright (C) 2014 Shadab Zafar 16# Copyright (C) 2014-2015, 2018-2019 Laurent Monin 17# Copyright (C) 2016-2018 Sambhav Kothari 18# Copyright (C) 2017 Frederik “Freso” S. Olesen 19# Copyright (C) 2018 Vishal Choudhary 20# 21# This program is free software; you can redistribute it and/or 22# modify it under the terms of the GNU General Public License 23# as published by the Free Software Foundation; either version 2 24# of the License, or (at your option) any later version. 25# 26# This program is distributed in the hope that it will be useful, 27# but WITHOUT ANY WARRANTY; without even the implied warranty of 28# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 29# GNU General Public License for more details. 30# 31# You should have received a copy of the GNU General Public License 32# along with this program; if not, write to the Free Software 33# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 34 35 36from collections import defaultdict 37import os.path 38 39from picard import log 40from picard.config import get_config 41from picard.const import USER_PLUGIN_DIR 42from picard.version import ( 43 Version, 44 VersionError, 45) 46 47 48_PLUGIN_MODULE_PREFIX = "picard.plugins." 49_PLUGIN_MODULE_PREFIX_LEN = len(_PLUGIN_MODULE_PREFIX) 50 51_extension_points = [] 52 53 54def _unregister_module_extensions(module): 55 for ep in _extension_points: 56 ep.unregister_module(module) 57 58 59class ExtensionPoint(object): 60 61 def __init__(self, label=None): 62 if label is None: 63 import uuid 64 label = uuid.uuid4() 65 self.label = label 66 self.__dict = defaultdict(list) 67 _extension_points.append(self) 68 69 def register(self, module, item): 70 if module.startswith(_PLUGIN_MODULE_PREFIX): 71 name = module[_PLUGIN_MODULE_PREFIX_LEN:] 72 log.debug("ExtensionPoint: %s register <- plugin=%r item=%r" % (self.label, name, item)) 73 else: 74 name = None 75 # uncomment to debug internal extensions loaded at startup 76 # print("ExtensionPoint: %s register <- item=%r" % (self.label, item)) 77 self.__dict[name].append(item) 78 79 def unregister_module(self, name): 80 try: 81 del self.__dict[name] 82 except KeyError: 83 # NOTE: needed due to defaultdict behaviour: 84 # >>> d = defaultdict(list) 85 # >>> del d['a'] 86 # KeyError: 'a' 87 # >>> d['a'] 88 # [] 89 # >>> del d['a'] 90 # >>> #^^ no exception, after first read 91 pass 92 93 def __iter__(self): 94 config = get_config() 95 enabled_plugins = config.setting["enabled_plugins"] if config else [] 96 for name in self.__dict: 97 if name is None or name in enabled_plugins: 98 yield from self.__dict[name] 99 100 101class PluginShared(object): 102 103 def __init__(self): 104 super().__init__() 105 106 107class PluginWrapper(PluginShared): 108 109 def __init__(self, module, plugindir, file=None, manifest_data=None): 110 super().__init__() 111 self.module = module 112 self.compatible = False 113 self.dir = os.path.normpath(plugindir) 114 self._file = file 115 self.data = manifest_data or self.module.__dict__ 116 117 @property 118 def name(self): 119 try: 120 return self.data['PLUGIN_NAME'] 121 except KeyError: 122 return self.module_name 123 124 @property 125 def module_name(self): 126 name = self.module.__name__ 127 if name.startswith(_PLUGIN_MODULE_PREFIX): 128 name = name[_PLUGIN_MODULE_PREFIX_LEN:] 129 return name 130 131 @property 132 def author(self): 133 try: 134 return self.data['PLUGIN_AUTHOR'] 135 except KeyError: 136 return "" 137 138 @property 139 def description(self): 140 try: 141 return self.data['PLUGIN_DESCRIPTION'] 142 except KeyError: 143 return "" 144 145 @property 146 def version(self): 147 try: 148 return Version.from_string(self.data['PLUGIN_VERSION']) 149 except (KeyError, VersionError): 150 return Version(0, 0, 0) 151 152 @property 153 def api_versions(self): 154 try: 155 return self.data['PLUGIN_API_VERSIONS'] 156 except KeyError: 157 return [] 158 159 @property 160 def file(self): 161 if not self._file: 162 return self.module.__file__ 163 else: 164 return self._file 165 166 @property 167 def license(self): 168 try: 169 return self.data['PLUGIN_LICENSE'] 170 except KeyError: 171 return "" 172 173 @property 174 def license_url(self): 175 try: 176 return self.data['PLUGIN_LICENSE_URL'] 177 except KeyError: 178 return "" 179 180 @property 181 def files_list(self): 182 return self.file[len(self.dir)+1:] 183 184 @property 185 def is_user_installed(self): 186 return self.dir == USER_PLUGIN_DIR 187 188 189class PluginData(PluginShared): 190 191 """Used to store plugin data from JSON API""" 192 193 def __init__(self, d, module_name): 194 self.__dict__ = d 195 super().__init__() 196 self.module_name = module_name 197 198 def __getattribute__(self, name): 199 try: 200 return super().__getattribute__(name) 201 except AttributeError: 202 log.debug('Attribute %r not found for plugin %r', name, self.module_name) 203 return None 204 205 @property 206 def version(self): 207 try: 208 return Version.from_string(self.__dict__['version']) 209 except (KeyError, VersionError): 210 return Version(0, 0, 0) 211 212 @property 213 def files_list(self): 214 return ", ".join(self.files.keys()) 215 216 217class PluginPriority: 218 219 """ 220 Define few priority values for plugin functions execution order 221 Those with higher values are executed first 222 Default priority is PluginPriority.NORMAL 223 """ 224 HIGH = 100 225 NORMAL = 0 226 LOW = -100 227 228 229class PluginFunctions: 230 231 """ 232 Store ExtensionPoint in a defaultdict with priority as key 233 run() method will execute entries with higher priority value first 234 """ 235 236 def __init__(self, label=None): 237 self.functions = defaultdict(lambda: ExtensionPoint(label=label)) 238 239 def register(self, module, item, priority=PluginPriority.NORMAL): 240 self.functions[priority].register(module, item) 241 242 def run(self, *args, **kwargs): 243 """Execute registered functions with passed parameters honouring priority""" 244 for priority, functions in sorted(self.functions.items(), 245 key=lambda i: i[0], 246 reverse=True): 247 for function in functions: 248 function(*args, **kwargs) 249