2012-02-04 21:40:47 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
#
|
|
|
|
# gPodder - A media aggregator and podcast client
|
|
|
|
# Copyright (c) 2005-2009 Thomas Perl and the gPodder Team
|
|
|
|
#
|
|
|
|
# gPodder is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# gPodder is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
"""
|
2012-02-05 15:46:35 +01:00
|
|
|
Loads and executes user extensions
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-02-05 15:46:35 +01:00
|
|
|
Extensions are Python scripts in "$GPODDER_HOME/Extensions". Each script must
|
2012-02-20 23:55:36 +01:00
|
|
|
define a class named "gPodderExtension", otherwise it will be ignored.
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-02-05 15:46:35 +01:00
|
|
|
The extensions class defines several callbacks that will be called by gPodder
|
|
|
|
at certain points. See the methods defined below for a list of callbacks and
|
|
|
|
their parameters.
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-02-23 20:38:55 +01:00
|
|
|
For an example extension see share/gpodder/examples/extensions.py
|
2012-02-04 21:40:47 +01:00
|
|
|
"""
|
|
|
|
|
2018-07-24 11:08:10 +02:00
|
|
|
import functools
|
2012-02-04 21:40:47 +01:00
|
|
|
import glob
|
|
|
|
import imp
|
2018-07-24 11:08:10 +02:00
|
|
|
import logging
|
2012-02-04 21:40:47 +01:00
|
|
|
import os
|
2018-07-24 11:08:10 +02:00
|
|
|
import re
|
2012-02-04 21:40:47 +01:00
|
|
|
|
|
|
|
import gpodder
|
2018-07-24 11:08:10 +02:00
|
|
|
from gpodder import util
|
2012-02-20 23:55:36 +01:00
|
|
|
|
2012-02-21 11:28:18 +01:00
|
|
|
_ = gpodder.gettext
|
|
|
|
|
2012-02-04 21:40:47 +01:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
CATEGORY_DICT = {
|
|
|
|
'desktop-integration': _('Desktop Integration'),
|
|
|
|
'interface': _('Interface'),
|
|
|
|
'post-download': _('Post download'),
|
|
|
|
}
|
|
|
|
DEFAULT_CATEGORY = _('Other')
|
|
|
|
|
|
|
|
|
2012-02-04 21:40:47 +01:00
|
|
|
def call_extensions(func):
|
|
|
|
"""Decorator to create handler functions in ExtensionManager
|
|
|
|
|
|
|
|
Calls the specified function in all user extensions that define it.
|
|
|
|
"""
|
|
|
|
method_name = func.__name__
|
|
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
def handler(self, *args, **kwargs):
|
|
|
|
result = None
|
2012-02-21 13:50:33 +01:00
|
|
|
for container in self.containers:
|
|
|
|
if not container.enabled or container.module is None:
|
2012-02-20 23:55:36 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
callback = getattr(container.module, method_name, None)
|
|
|
|
if callback is None:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# If the results are lists, concatenate them to show all
|
|
|
|
# possible items that are generated by all extension together
|
|
|
|
cb_res = callback(*args, **kwargs)
|
|
|
|
if isinstance(result, list) and isinstance(cb_res, list):
|
|
|
|
result.extend(cb_res)
|
|
|
|
elif cb_res is not None:
|
|
|
|
result = cb_res
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as exception:
|
2012-02-20 23:55:36 +01:00
|
|
|
logger.error('Error in %s in %s: %s', container.filename,
|
|
|
|
method_name, exception, exc_info=True)
|
2012-02-04 21:40:47 +01:00
|
|
|
func(self, *args, **kwargs)
|
|
|
|
return result
|
|
|
|
|
|
|
|
return handler
|
|
|
|
|
2012-02-23 00:18:02 +01:00
|
|
|
|
2012-02-20 23:55:36 +01:00
|
|
|
class ExtensionMetadata(object):
|
2012-02-21 11:28:18 +01:00
|
|
|
# Default fallback metadata in case metadata fields are missing
|
|
|
|
DEFAULTS = {
|
|
|
|
'description': _('No description for this extension.'),
|
2013-03-24 21:13:27 +01:00
|
|
|
'doc': None,
|
|
|
|
'payment': None,
|
2012-02-21 11:28:18 +01:00
|
|
|
}
|
2012-11-18 19:26:02 +01:00
|
|
|
SORTKEYS = {
|
|
|
|
'title': 1,
|
|
|
|
'description': 2,
|
|
|
|
'category': 3,
|
2013-03-24 21:13:27 +01:00
|
|
|
'authors': 4,
|
2012-11-18 19:26:02 +01:00
|
|
|
'only_for': 5,
|
|
|
|
'mandatory_in': 6,
|
|
|
|
'disable_in': 7,
|
|
|
|
}
|
2012-02-21 11:28:18 +01:00
|
|
|
|
|
|
|
def __init__(self, container, metadata):
|
|
|
|
if 'title' not in metadata:
|
|
|
|
metadata['title'] = container.name
|
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
category = metadata.get('category', 'other')
|
|
|
|
metadata['category'] = CATEGORY_DICT.get(category, DEFAULT_CATEGORY)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2012-02-20 23:55:36 +01:00
|
|
|
self.__dict__.update(metadata)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2012-02-21 11:28:18 +01:00
|
|
|
def __getattr__(self, name):
|
|
|
|
try:
|
|
|
|
return self.DEFAULTS[name]
|
2016-11-21 23:13:46 +01:00
|
|
|
except KeyError as e:
|
2012-11-18 19:26:02 +01:00
|
|
|
raise AttributeError(name, e)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
def get_sorted(self):
|
2018-05-06 22:26:54 +02:00
|
|
|
|
|
|
|
def kf(x):
|
|
|
|
return self.SORTKEYS.get(x[0], 99)
|
|
|
|
|
2016-11-21 23:13:46 +01:00
|
|
|
return sorted([(k, v) for k, v in list(self.__dict__.items())], key=kf)
|
2012-11-18 19:26:02 +01:00
|
|
|
|
|
|
|
def check_ui(self, target, default):
|
|
|
|
"""Checks metadata information like
|
|
|
|
__only_for__ = 'gtk'
|
|
|
|
__mandatory_in__ = 'gtk'
|
|
|
|
__disable_in__ = 'gtk'
|
|
|
|
|
|
|
|
The metadata fields in an extension can be a string with
|
|
|
|
comma-separated values for UIs. This will be checked against
|
|
|
|
boolean variables in the "gpodder.ui" object.
|
2012-02-21 10:14:23 +01:00
|
|
|
|
|
|
|
Example metadata field in an extension:
|
|
|
|
|
2016-02-03 19:54:33 +01:00
|
|
|
__only_for__ = 'gtk'
|
2012-11-18 19:26:02 +01:00
|
|
|
__only_for__ = 'unity'
|
2012-02-21 10:14:23 +01:00
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
In this case, this function will return the value of the default
|
|
|
|
if any of the following expressions will evaluate to True:
|
2012-02-21 10:14:23 +01:00
|
|
|
|
|
|
|
gpodder.ui.gtk
|
2012-11-18 19:26:02 +01:00
|
|
|
gpodder.ui.unity
|
|
|
|
gpodder.ui.cli
|
|
|
|
gpodder.ui.osx
|
|
|
|
gpodder.ui.win32
|
2012-02-21 10:14:23 +01:00
|
|
|
|
|
|
|
New, unknown UIs are silently ignored and will evaluate to False.
|
|
|
|
"""
|
2012-11-18 19:26:02 +01:00
|
|
|
if not hasattr(self, target):
|
|
|
|
return default
|
2012-02-21 10:14:23 +01:00
|
|
|
|
2016-11-21 23:13:46 +01:00
|
|
|
uis = [_f for _f in [x.strip() for x in getattr(self, target).split(',')] if _f]
|
2012-02-21 14:12:46 +01:00
|
|
|
return any(getattr(gpodder.ui, ui.lower(), False) for ui in uis)
|
2012-02-21 10:14:23 +01:00
|
|
|
|
2018-01-30 14:04:28 +01:00
|
|
|
@property
|
2012-11-18 19:26:02 +01:00
|
|
|
def available_for_current_ui(self):
|
|
|
|
return self.check_ui('only_for', True)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
@property
|
|
|
|
def mandatory_in_current_ui(self):
|
|
|
|
return self.check_ui('mandatory_in', False)
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
@property
|
|
|
|
def disable_in_current_ui(self):
|
|
|
|
return self.check_ui('disable_in', False)
|
|
|
|
|
2018-02-11 00:22:00 +01:00
|
|
|
|
2012-03-11 10:24:25 +01:00
|
|
|
class MissingDependency(Exception):
|
|
|
|
def __init__(self, message, dependency, cause=None):
|
|
|
|
Exception.__init__(self, message)
|
|
|
|
self.dependency = dependency
|
|
|
|
self.cause = cause
|
|
|
|
|
2018-02-11 00:22:00 +01:00
|
|
|
|
2012-03-11 10:24:25 +01:00
|
|
|
class MissingModule(MissingDependency): pass
|
2018-02-11 00:22:00 +01:00
|
|
|
|
|
|
|
|
2012-03-11 10:24:25 +01:00
|
|
|
class MissingCommand(MissingDependency): pass
|
2012-02-23 00:18:02 +01:00
|
|
|
|
2018-02-11 00:22:00 +01:00
|
|
|
|
2012-02-14 22:58:32 +01:00
|
|
|
class ExtensionContainer(object):
|
2012-02-20 23:55:36 +01:00
|
|
|
"""An extension container wraps one extension module"""
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-02-20 23:55:36 +01:00
|
|
|
def __init__(self, manager, name, config, filename=None, module=None):
|
|
|
|
self.manager = manager
|
|
|
|
|
|
|
|
self.name = name
|
|
|
|
self.config = config
|
|
|
|
self.filename = filename
|
2012-02-04 21:40:47 +01:00
|
|
|
self.module = module
|
2012-02-21 13:50:33 +01:00
|
|
|
self.enabled = False
|
2012-03-11 10:24:25 +01:00
|
|
|
self.error = None
|
2012-02-20 23:55:36 +01:00
|
|
|
|
|
|
|
self.default_config = None
|
|
|
|
self.parameters = None
|
2012-02-21 11:28:18 +01:00
|
|
|
self.metadata = ExtensionMetadata(self, self._load_metadata(filename))
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-03-11 09:37:49 +01:00
|
|
|
def require_command(self, command):
|
2013-02-16 15:29:51 +01:00
|
|
|
"""Checks if the given command is installed on the system
|
|
|
|
|
|
|
|
Returns the complete path of the command
|
|
|
|
|
|
|
|
@param command: String with the command name
|
|
|
|
"""
|
2012-03-11 09:37:49 +01:00
|
|
|
result = util.find_command(command)
|
|
|
|
if result is None:
|
2012-03-11 10:24:25 +01:00
|
|
|
msg = _('Command not found: %(command)s') % {'command': command}
|
|
|
|
raise MissingCommand(msg, command)
|
2012-03-11 09:37:49 +01:00
|
|
|
return result
|
|
|
|
|
2013-02-16 15:29:51 +01:00
|
|
|
def require_any_command(self, command_list):
|
|
|
|
"""Checks if any of the given commands is installed on the system
|
|
|
|
|
|
|
|
Returns the complete path of first found command in the list
|
|
|
|
|
|
|
|
@param command: List with the commands name
|
|
|
|
"""
|
|
|
|
for command in command_list:
|
|
|
|
result = util.find_command(command)
|
|
|
|
if result is not None:
|
|
|
|
return result
|
|
|
|
|
|
|
|
msg = _('Need at least one of the following commands: %(list_of_commands)s') % \
|
|
|
|
{'list_of_commands': ', '.join(command_list)}
|
|
|
|
raise MissingCommand(msg, ', '.join(command_list))
|
|
|
|
|
2012-02-08 22:03:51 +01:00
|
|
|
def _load_metadata(self, filename):
|
2012-02-20 23:55:36 +01:00
|
|
|
if not filename or not os.path.exists(filename):
|
|
|
|
return {}
|
|
|
|
|
2018-02-06 16:13:21 +01:00
|
|
|
encoding = util.guess_encoding(filename)
|
2022-04-17 11:35:28 +02:00
|
|
|
with open(filename, "r", encoding=encoding) as f:
|
|
|
|
extension_py = f.read()
|
2021-01-14 22:17:10 +01:00
|
|
|
metadata = dict(re.findall(r"__([a-z_]+)__ = '([^']+)'", extension_py))
|
2012-02-24 00:12:00 +01:00
|
|
|
|
|
|
|
# Support for using gpodder.gettext() as _ to localize text
|
2021-01-14 22:17:10 +01:00
|
|
|
localized_metadata = dict(re.findall(r"__([a-z_]+)__ = _\('([^']+)'\)",
|
2012-02-24 00:12:00 +01:00
|
|
|
extension_py))
|
|
|
|
|
|
|
|
for key in localized_metadata:
|
|
|
|
metadata[key] = gpodder.gettext(localized_metadata[key])
|
|
|
|
|
|
|
|
return metadata
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-02-21 13:50:33 +01:00
|
|
|
def set_enabled(self, enabled):
|
2012-02-21 15:13:18 +01:00
|
|
|
if enabled and not self.enabled:
|
2012-02-21 13:50:33 +01:00
|
|
|
try:
|
|
|
|
self.load_extension()
|
2012-03-11 10:24:25 +01:00
|
|
|
self.error = None
|
2012-02-21 13:50:33 +01:00
|
|
|
self.enabled = True
|
2012-02-21 15:13:18 +01:00
|
|
|
if hasattr(self.module, 'on_load'):
|
|
|
|
self.module.on_load()
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as exception:
|
2012-02-21 13:50:33 +01:00
|
|
|
logger.error('Cannot load %s from %s: %s', self.name,
|
|
|
|
self.filename, exception, exc_info=True)
|
2012-03-11 10:24:25 +01:00
|
|
|
if isinstance(exception, ImportError):
|
|
|
|
# Wrap ImportError in MissingCommand for user-friendly
|
|
|
|
# message (might be displayed in the GUI)
|
2016-11-26 15:25:00 +01:00
|
|
|
if exception.name:
|
|
|
|
module = exception.name
|
2012-03-11 10:24:25 +01:00
|
|
|
msg = _('Python module not found: %(module)s') % {
|
|
|
|
'module': module
|
|
|
|
}
|
|
|
|
exception = MissingCommand(msg, module, exception)
|
|
|
|
self.error = exception
|
2012-02-21 13:50:33 +01:00
|
|
|
self.enabled = False
|
2012-02-21 15:13:18 +01:00
|
|
|
elif not enabled and self.enabled:
|
|
|
|
try:
|
|
|
|
if hasattr(self.module, 'on_unload'):
|
|
|
|
self.module.on_unload()
|
2016-11-21 23:13:46 +01:00
|
|
|
except Exception as exception:
|
2012-02-21 15:13:18 +01:00
|
|
|
logger.error('Failed to on_unload %s: %s', self.name,
|
|
|
|
exception, exc_info=True)
|
2012-02-21 13:50:33 +01:00
|
|
|
self.enabled = False
|
|
|
|
|
2012-02-04 21:40:47 +01:00
|
|
|
def load_extension(self):
|
2012-02-20 23:55:36 +01:00
|
|
|
"""Load and initialize the gPodder extension module"""
|
|
|
|
if self.module is not None:
|
|
|
|
logger.info('Module already loaded.')
|
|
|
|
return
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
if not self.metadata.available_for_current_ui:
|
2012-02-21 10:14:23 +01:00
|
|
|
logger.info('Not loading "%s" (only_for = "%s")',
|
|
|
|
self.name, self.metadata.only_for)
|
|
|
|
return
|
|
|
|
|
2012-02-21 15:13:18 +01:00
|
|
|
basename, extension = os.path.splitext(os.path.basename(self.filename))
|
|
|
|
fp = open(self.filename, 'r')
|
2012-03-11 10:24:25 +01:00
|
|
|
try:
|
|
|
|
module_file = imp.load_module(basename, fp, self.filename,
|
|
|
|
(extension, 'r', imp.PY_SOURCE))
|
|
|
|
finally:
|
|
|
|
# Remove the .pyc file if it was created during import
|
|
|
|
util.delete_file(self.filename + 'c')
|
2012-02-21 15:13:18 +01:00
|
|
|
fp.close()
|
|
|
|
|
2012-02-21 13:50:33 +01:00
|
|
|
self.default_config = getattr(module_file, 'DefaultConfig', {})
|
2012-03-11 09:37:49 +01:00
|
|
|
if self.default_config:
|
|
|
|
self.manager.core.config.register_defaults({
|
|
|
|
'extensions': {
|
|
|
|
self.name: self.default_config,
|
|
|
|
}
|
|
|
|
})
|
2012-02-23 00:18:02 +01:00
|
|
|
self.config = getattr(self.manager.core.config.extensions, self.name)
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-02-21 13:50:33 +01:00
|
|
|
self.module = module_file.gPodderExtension(self)
|
|
|
|
logger.info('Module loaded: %s', self.filename)
|
2012-02-04 21:40:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
class ExtensionManager(object):
|
2012-02-20 23:55:36 +01:00
|
|
|
"""Loads extensions and manages self-registering plugins"""
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-03-11 08:12:49 +01:00
|
|
|
def __init__(self, core):
|
2012-02-21 14:12:46 +01:00
|
|
|
self.core = core
|
2012-03-11 08:12:49 +01:00
|
|
|
self.filenames = os.environ.get('GPODDER_EXTENSIONS', '').split()
|
2012-02-20 23:55:36 +01:00
|
|
|
self.containers = []
|
2012-02-21 14:12:46 +01:00
|
|
|
|
|
|
|
core.config.add_observer(self._config_value_changed)
|
|
|
|
enabled_extensions = core.config.extensions.enabled
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-03-11 08:12:49 +01:00
|
|
|
if os.environ.get('GPODDER_DISABLE_EXTENSIONS', '') != '':
|
|
|
|
logger.info('Disabling all extensions (from environment)')
|
|
|
|
return
|
|
|
|
|
2012-02-20 23:55:36 +01:00
|
|
|
for name, filename in self._find_extensions():
|
2012-02-21 11:28:18 +01:00
|
|
|
logger.debug('Found extension "%s" in %s', name, filename)
|
2012-02-21 14:12:46 +01:00
|
|
|
config = getattr(core.config.extensions, name)
|
2012-02-20 23:55:36 +01:00
|
|
|
container = ExtensionContainer(self, name, config, filename)
|
2022-09-06 22:28:39 +02:00
|
|
|
if (name in enabled_extensions
|
|
|
|
or container.metadata.mandatory_in_current_ui):
|
2012-02-21 13:50:33 +01:00
|
|
|
container.set_enabled(True)
|
2022-09-06 22:28:39 +02:00
|
|
|
if (name in enabled_extensions
|
|
|
|
and container.metadata.disable_in_current_ui):
|
2012-11-18 19:26:02 +01:00
|
|
|
container.set_enabled(False)
|
2012-02-21 13:50:33 +01:00
|
|
|
self.containers.append(container)
|
|
|
|
|
2012-02-21 15:13:18 +01:00
|
|
|
def shutdown(self):
|
|
|
|
for container in self.containers:
|
|
|
|
container.set_enabled(False)
|
|
|
|
|
2012-02-21 13:50:33 +01:00
|
|
|
def _config_value_changed(self, name, old_value, new_value):
|
2012-11-18 19:26:02 +01:00
|
|
|
if name != 'extensions.enabled':
|
|
|
|
return
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
for container in self.containers:
|
|
|
|
new_enabled = (container.name in new_value)
|
|
|
|
if new_enabled == container.enabled:
|
|
|
|
continue
|
2018-10-14 17:34:50 +02:00
|
|
|
if not new_enabled and container.metadata.mandatory_in_current_ui:
|
|
|
|
# forced extensions are never listed in extensions.enabled
|
|
|
|
continue
|
2018-01-30 14:04:28 +01:00
|
|
|
|
2012-11-18 19:26:02 +01:00
|
|
|
logger.info('Extension "%s" is now %s', container.name,
|
|
|
|
'enabled' if new_enabled else 'disabled')
|
|
|
|
container.set_enabled(new_enabled)
|
|
|
|
if new_enabled and not container.enabled:
|
2022-04-17 11:07:51 +02:00
|
|
|
logger.warning('Could not enable extension: %s',
|
2012-11-18 19:26:02 +01:00
|
|
|
container.error)
|
|
|
|
self.core.config.extensions.enabled = [x
|
|
|
|
for x in self.core.config.extensions.enabled
|
|
|
|
if x != container.name]
|
2012-02-04 21:40:47 +01:00
|
|
|
|
2012-02-20 23:55:36 +01:00
|
|
|
def _find_extensions(self):
|
2012-02-21 11:28:18 +01:00
|
|
|
extensions = {}
|
|
|
|
|
2012-03-11 08:12:49 +01:00
|
|
|
if not self.filenames:
|
2012-02-23 00:18:02 +01:00
|
|
|
builtins = os.path.join(gpodder.prefix, 'share', 'gpodder',
|
2012-02-23 20:38:55 +01:00
|
|
|
'extensions', '*.py')
|
2012-02-23 00:18:02 +01:00
|
|
|
user_extensions = os.path.join(gpodder.home, 'Extensions', '*.py')
|
2012-03-11 08:12:49 +01:00
|
|
|
self.filenames = glob.glob(builtins) + glob.glob(user_extensions)
|
2012-02-21 11:28:18 +01:00
|
|
|
|
|
|
|
# Let user extensions override built-in extensions of the same name
|
2012-03-11 08:12:49 +01:00
|
|
|
for filename in self.filenames:
|
|
|
|
if not filename or not os.path.exists(filename):
|
|
|
|
logger.info('Skipping non-existing file: %s', filename)
|
|
|
|
continue
|
|
|
|
|
2012-02-20 23:55:36 +01:00
|
|
|
name, _ = os.path.splitext(os.path.basename(filename))
|
2012-02-21 11:28:18 +01:00
|
|
|
extensions[name] = filename
|
|
|
|
|
|
|
|
return sorted(extensions.items())
|
2012-02-04 21:40:47 +01:00
|
|
|
|
|
|
|
def get_extensions(self):
|
2012-02-20 21:51:06 +01:00
|
|
|
"""Get a list of all loaded extensions and their enabled flag"""
|
2018-02-01 07:59:22 +01:00
|
|
|
return [c for c in self.containers
|
2022-09-06 22:28:39 +02:00
|
|
|
if c.metadata.available_for_current_ui
|
|
|
|
and not c.metadata.mandatory_in_current_ui
|
|
|
|
and not c.metadata.disable_in_current_ui]
|
2012-02-04 21:40:47 +01:00
|
|
|
|
|
|
|
# Define all known handler functions here, decorate them with the
|
|
|
|
# "call_extension" decorator to forward all calls to extension scripts that have
|
|
|
|
# the same function defined in them. If the handler functions here contain
|
|
|
|
# any code, it will be called after all the extensions have been called.
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_ui_initialized(self, model, update_podcast_callback,
|
|
|
|
download_episode_callback):
|
|
|
|
"""Called when the user interface is initialized.
|
|
|
|
|
|
|
|
@param model: A gpodder.model.Model instance
|
|
|
|
@param update_podcast_callback: Function to update a podcast feed
|
|
|
|
@param download_episode_callback: Function to download an episode
|
|
|
|
"""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_podcast_subscribe(self, podcast):
|
|
|
|
"""Called when the user subscribes to a new podcast feed.
|
|
|
|
|
|
|
|
@param podcast: A gpodder.model.PodcastChannel instance
|
|
|
|
"""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_podcast_updated(self, podcast):
|
|
|
|
"""Called when a podcast feed was updated
|
|
|
|
|
|
|
|
This extension will be called even if there were no new episodes.
|
|
|
|
|
|
|
|
@param podcast: A gpodder.model.PodcastChannel instance
|
|
|
|
"""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_podcast_update_failed(self, podcast, exception):
|
|
|
|
"""Called when a podcast update failed.
|
|
|
|
|
|
|
|
@param podcast: A gpodder.model.PodcastChannel instance
|
|
|
|
|
|
|
|
@param exception: The reason.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_podcast_save(self, podcast):
|
|
|
|
"""Called when a podcast is saved to the database
|
|
|
|
|
|
|
|
This extensions will be called when the user edits the metadata of
|
|
|
|
the podcast or when the feed was updated.
|
|
|
|
|
|
|
|
@param podcast: A gpodder.model.PodcastChannel instance
|
|
|
|
"""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_podcast_delete(self, podcast):
|
|
|
|
"""Called when a podcast is deleted from the database
|
|
|
|
|
|
|
|
@param podcast: A gpodder.model.PodcastChannel instance
|
|
|
|
"""
|
|
|
|
|
2013-01-19 17:07:00 +01:00
|
|
|
@call_extensions
|
|
|
|
def on_episode_playback(self, episode):
|
|
|
|
"""Called when an episode is played back
|
|
|
|
|
|
|
|
This function will be called when the user clicks on "Play" or
|
|
|
|
"Open" in the GUI to open an episode with the media player.
|
|
|
|
|
|
|
|
@param episode: A gpodder.model.PodcastEpisode instance
|
|
|
|
"""
|
|
|
|
|
2012-02-04 21:40:47 +01:00
|
|
|
@call_extensions
|
|
|
|
def on_episode_save(self, episode):
|
|
|
|
"""Called when an episode is saved to the database
|
|
|
|
|
|
|
|
This extension will be called when a new episode is added to the
|
|
|
|
database or when the state of an existing episode is changed.
|
|
|
|
|
|
|
|
@param episode: A gpodder.model.PodcastEpisode instance
|
|
|
|
"""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_episode_downloaded(self, episode):
|
|
|
|
"""Called when an episode has been downloaded
|
|
|
|
|
|
|
|
You can retrieve the filename via episode.local_filename(False)
|
|
|
|
|
|
|
|
@param episode: A gpodder.model.PodcastEpisode instance
|
|
|
|
"""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_all_episodes_downloaded(self):
|
|
|
|
"""Called when all episodes has been downloaded
|
|
|
|
"""
|
|
|
|
|
2014-04-10 16:45:49 +02:00
|
|
|
@call_extensions
|
|
|
|
def on_episode_synced(self, device, episode):
|
|
|
|
"""Called when an episode has been synced to device
|
|
|
|
|
|
|
|
You can retrieve the filename via episode.local_filename(False)
|
|
|
|
For MP3PlayerDevice:
|
|
|
|
You can retrieve the filename on device via
|
|
|
|
device.get_episode_file_on_device(episode)
|
|
|
|
You can retrieve the folder name on device via
|
|
|
|
device.get_episode_folder_on_device(episode)
|
|
|
|
|
|
|
|
@param device: A gpodder.sync.Device instance
|
|
|
|
@param episode: A gpodder.model.PodcastEpisode instance
|
|
|
|
"""
|
|
|
|
|
2017-06-25 09:25:35 +02:00
|
|
|
@call_extensions
|
|
|
|
def on_create_menu(self):
|
|
|
|
"""Called when the Extras menu is created
|
|
|
|
|
|
|
|
You can add additional Extras menu entries here. You have to return a
|
|
|
|
list of tuples, where the first item is a label and the second item is a
|
|
|
|
callable that will get no parameter.
|
|
|
|
|
|
|
|
Example return value:
|
|
|
|
|
|
|
|
[('Sync to Smartphone', lambda : ...)]
|
|
|
|
"""
|
|
|
|
|
2012-02-04 21:40:47 +01:00
|
|
|
@call_extensions
|
|
|
|
def on_episodes_context_menu(self, episodes):
|
|
|
|
"""Called when the episode list context menu is opened
|
|
|
|
|
|
|
|
You can add additional context menu entries here. You have to
|
|
|
|
return a list of tuples, where the first item is a label and
|
|
|
|
the second item is a callable that will get the episode as its
|
|
|
|
first and only parameter.
|
|
|
|
|
|
|
|
Example return value:
|
|
|
|
|
|
|
|
[('Mark as new', lambda episodes: ...)]
|
|
|
|
|
|
|
|
@param episodes: A list of gpodder.model.PodcastEpisode instances
|
|
|
|
"""
|
|
|
|
|
2012-06-25 16:07:17 +02:00
|
|
|
@call_extensions
|
|
|
|
def on_channel_context_menu(self, channel):
|
|
|
|
"""Called when the channel list context menu is opened
|
|
|
|
|
|
|
|
You can add additional context menu entries here. You have to return a
|
|
|
|
list of tuples, where the first item is a label and the second item is a
|
|
|
|
callable that will get the channel as its first and only parameter.
|
|
|
|
|
|
|
|
Example return value:
|
|
|
|
|
|
|
|
[('Update channel', lambda channel: ...)]
|
|
|
|
@param channel: A gpodder.model.PodcastChannel instance
|
|
|
|
"""
|
|
|
|
|
2012-02-04 21:40:47 +01:00
|
|
|
@call_extensions
|
|
|
|
def on_episode_delete(self, episode, filename):
|
|
|
|
"""Called just before the episode's disk file is about to be
|
|
|
|
deleted."""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_episode_removed_from_podcast(self, episode):
|
|
|
|
"""Called just before the episode is about to be removed from
|
|
|
|
the podcast channel, e.g., when the episode has not been
|
|
|
|
downloaded and it disappears from the feed.
|
|
|
|
|
|
|
|
@param podcast: A gpodder.model.PodcastChannel instance
|
|
|
|
"""
|
2012-02-14 22:58:32 +01:00
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_notification_show(self, title, message):
|
|
|
|
"""Called when a notification should be shown
|
|
|
|
|
|
|
|
@param title: title of the notification
|
|
|
|
@param message: message of the notification
|
|
|
|
"""
|
2012-02-20 23:55:36 +01:00
|
|
|
|
2012-02-24 00:12:00 +01:00
|
|
|
@call_extensions
|
|
|
|
def on_download_progress(self, progress):
|
|
|
|
"""Called when the overall download progress changes
|
|
|
|
|
|
|
|
@param progress: The current progress value (0..1)
|
|
|
|
"""
|
|
|
|
|
2012-02-24 00:44:16 +01:00
|
|
|
@call_extensions
|
|
|
|
def on_ui_object_available(self, name, ui_object):
|
|
|
|
"""Called when an UI-specific object becomes available
|
|
|
|
|
|
|
|
XXX: Experimental. This hook might go away without notice (and be
|
|
|
|
replaced with something better). Only use for in-tree extensions.
|
|
|
|
|
|
|
|
@param name: The name/ID of the object
|
|
|
|
@param ui_object: The object itself
|
|
|
|
"""
|
|
|
|
|
2017-09-13 20:32:23 +02:00
|
|
|
@call_extensions
|
|
|
|
def on_application_started(self):
|
|
|
|
"""Called when the application started.
|
|
|
|
|
|
|
|
This is for extensions doing stuff at startup that they don't
|
|
|
|
want to do if they have just been enabled.
|
|
|
|
e.g. minimize at startup should not minimize the application when
|
|
|
|
enabled but only on following startups.
|
|
|
|
|
|
|
|
It is called after on_ui_object_available and on_ui_initialized.
|
|
|
|
"""
|
2019-09-23 20:10:14 +02:00
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_find_partial_downloads_done(self):
|
|
|
|
"""Called when the application started and the lookout for resume is done
|
|
|
|
|
|
|
|
This is mainly for extensions scheduling refresh or downloads at startup,
|
|
|
|
to prevent race conditions with the find_partial_downloads method.
|
|
|
|
|
|
|
|
It is called after on_application_started.
|
|
|
|
"""
|
2020-01-31 06:25:37 +01:00
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_preferences(self):
|
|
|
|
"""Called when the preferences dialog is opened
|
|
|
|
|
|
|
|
You can add additional tabs to the preferences dialog here. You have to
|
|
|
|
return a list of tuples, where the first item is a label and the second
|
|
|
|
item is a callable with no parameters and returns a Gtk widget.
|
|
|
|
|
|
|
|
Example return value:
|
|
|
|
|
|
|
|
[('Tab name', lambda: ...)]
|
|
|
|
"""
|
|
|
|
|
|
|
|
@call_extensions
|
|
|
|
def on_channel_settings(self, channel):
|
|
|
|
"""Called when a channel settings dialog is opened
|
|
|
|
|
|
|
|
You can add additional tabs to the channel settings dialog here. You
|
|
|
|
have to return a list of tuples, where the first item is a label and the
|
|
|
|
second item is a callable that will get the channel as its first and
|
|
|
|
only parameter and returns a Gtk widget.
|
|
|
|
|
|
|
|
Example return value:
|
|
|
|
|
|
|
|
[('Tab name', lambda channel: ...)]
|
|
|
|
|
|
|
|
@param channel: A gpodder.model.PodcastChannel instance
|
|
|
|
"""
|