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