parent
263c9092b5
commit
f73a1750c5
14
bin/gpo
14
bin/gpo
|
@ -172,9 +172,9 @@ class gPodderCli(object):
|
|||
self._prefixes, self._expansions = self._build_prefixes_expansions()
|
||||
self._prefixes.update({'?': 'help'})
|
||||
self._valid_commands = sorted(self._prefixes.values())
|
||||
gpodder.user_hooks.on_ui_initialized(self.client.core.model,
|
||||
self._hooks_podcast_update_cb,
|
||||
self._hooks_episode_download_cb)
|
||||
gpodder.user_extensions.on_ui_initialized(self.client.core.model,
|
||||
self._extensions_podcast_update_cb,
|
||||
self._extensions_episode_download_cb)
|
||||
|
||||
def _build_prefixes_expansions(self):
|
||||
prefixes = {}
|
||||
|
@ -204,12 +204,12 @@ class gPodderCli(object):
|
|||
|
||||
return prefixes, expansions
|
||||
|
||||
def _hooks_podcast_update_cb(self, podcast):
|
||||
self._info(_('Podcast update requested by hooks.'))
|
||||
def _extensions_podcast_update_cb(self, podcast):
|
||||
self._info(_('Podcast update requested by extensions.'))
|
||||
self._update_podcast(podcast)
|
||||
|
||||
def _hooks_episode_download_cb(self, episode):
|
||||
self._info(_('Episode download requested by hooks.'))
|
||||
def _extensions_episode_download_cb(self, episode):
|
||||
self._info(_('Episode download requested by extensions.'))
|
||||
self._download_episode(episode)
|
||||
|
||||
def _start_action(self, msg, *args):
|
||||
|
|
|
@ -60,6 +60,13 @@
|
|||
</object>
|
||||
<accelerator key="P" modifiers="GDK_CONTROL_MASK"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="itemExtensionSettings">
|
||||
<property name="name">itemExtensionSettings</property>
|
||||
<property name="label" translatable="yes">Extension Manager</property>
|
||||
<signal handler="on_itemExtensionSettings_activate" name="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAction" id="itemQuit">
|
||||
<property name="stock_id">gtk-quit</property>
|
||||
|
@ -330,6 +337,7 @@
|
|||
<menuitem action="itemRemoveOldEpisodes"/>
|
||||
<separator/>
|
||||
<menuitem action="itemPreferences"/>
|
||||
<menuitem action="itemExtensionSettings"/>
|
||||
<separator/>
|
||||
<menuitem action="itemQuit"/>
|
||||
</menu>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/python
|
||||
# Example script that can be used as post-play hook in media players
|
||||
# Example script that can be used as post-play extension in media players
|
||||
#
|
||||
# Set the configuration options "audio_played_dbus" and "video_played_dbus"
|
||||
# to True to let gPodder leave the played status untouched when playing
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Example hooks script for gPodder.
|
||||
# To use, copy it as a Python script into $GPODDER_HOME/Hooks/mySetOfHooks.py
|
||||
# (The default value of $GPODDER_HOME is ~/gPodder/ on Desktop installations)
|
||||
# See the module "gpodder.hooks" for a description of when each hook
|
||||
# gets called and what the parameters of each hook are.
|
||||
|
||||
import gpodder
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class gPodderHooks(object):
|
||||
def __init__(self):
|
||||
logger.info('Example extension is initializing.')
|
||||
|
||||
def on_podcast_updated(self, podcast):
|
||||
logger.info('on_podcast_updated(%s)', podcast.title)
|
||||
|
||||
def on_podcast_save(self, podcast):
|
||||
logger.info('on_podcast_save(%s)', podcast.title)
|
||||
|
||||
def on_episode_downloaded(self, episode):
|
||||
logger.info('on_episode_downloaded(%s)', episode.title)
|
||||
|
||||
def on_episode_save(self, episode):
|
||||
logger.info('on_episode_save(%s)', episode.title)
|
||||
|
||||
def on_episodes_context_menu(self, episodes):
|
||||
logger.info('on_episodes_context_menu(%d episodes)', len(episodes))
|
||||
|
|
@ -128,7 +128,8 @@ ui_folders = []
|
|||
credits_file = None
|
||||
icon_file = None
|
||||
images_folder = None
|
||||
user_hooks = None
|
||||
user_extensions = None
|
||||
notify = None
|
||||
|
||||
# Episode states used in the database
|
||||
STATE_NORMAL, STATE_DOWNLOADED, STATE_DELETED = range(3)
|
||||
|
|
|
@ -142,6 +142,10 @@ defaults = {
|
|||
'youtube': {
|
||||
'preferred_fmt_id': 18,
|
||||
},
|
||||
|
||||
'extensions': {
|
||||
'enabled': [],
|
||||
},
|
||||
}
|
||||
|
||||
# The sooner this goes away, the better
|
||||
|
|
|
@ -26,8 +26,9 @@ import gpodder
|
|||
from gpodder import util
|
||||
from gpodder import config
|
||||
from gpodder import dbsqlite
|
||||
from gpodder import hooks
|
||||
from gpodder import extensions
|
||||
from gpodder import model
|
||||
from gpodder import notification
|
||||
|
||||
|
||||
class Core(object):
|
||||
|
@ -38,17 +39,20 @@ class Core(object):
|
|||
# Initialize the gPodder home directory
|
||||
util.make_directory(gpodder.home)
|
||||
|
||||
# Load hook modules and install the hook manager
|
||||
gpodder.user_hooks = hooks.HookManager()
|
||||
|
||||
# Load installed/configured plugins
|
||||
gpodder.load_plugins()
|
||||
|
||||
# Open the database and configuration file
|
||||
self.db = database_class(gpodder.database_file)
|
||||
self.model = model_class(self.db)
|
||||
self.config = config_class(gpodder.config_file)
|
||||
|
||||
# Initialize the notification system
|
||||
gpodder.notify = notification.init_notify(self.config)
|
||||
|
||||
# Load extension modules and install the extension manager
|
||||
gpodder.user_extensions = extensions.ExtensionManager(self.config)
|
||||
|
||||
# Load installed/configured plugins
|
||||
gpodder.load_plugins()
|
||||
|
||||
# Update the current device in the configuration
|
||||
self.config.mygpo.device.type = util.detect_device_type()
|
||||
|
||||
|
|
|
@ -860,7 +860,7 @@ class DownloadTask(object):
|
|||
self.total_size = util.calculate_size(self.filename)
|
||||
logger.info('Total size updated to %d', self.total_size)
|
||||
self.progress = 1.0
|
||||
gpodder.user_hooks.on_episode_downloaded(self.__episode)
|
||||
gpodder.user_extensions.on_episode_downloaded(self.__episode)
|
||||
return True
|
||||
|
||||
self.speed = 0.0
|
||||
|
|
|
@ -31,12 +31,6 @@ from gpodder.gtkui.base import GtkBuilderWidget
|
|||
|
||||
from gpodder.gtkui.widgets import NotificationWindow
|
||||
|
||||
try:
|
||||
import pynotify
|
||||
if not pynotify.init('gPodder'):
|
||||
pynotify = None
|
||||
except ImportError:
|
||||
pynotify = None
|
||||
|
||||
class BuilderWidget(GtkBuilderWidget):
|
||||
def __init__(self, parent, **kwargs):
|
||||
|
@ -137,16 +131,10 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
dlg.run()
|
||||
dlg.destroy()
|
||||
elif config is not None and config.enable_notifications:
|
||||
if pynotify is not None:
|
||||
if gpodder.notify.is_initted():
|
||||
if title is None:
|
||||
title = 'gPodder'
|
||||
notification = pynotify.Notification(title, message,\
|
||||
gpodder.icon_file)
|
||||
try:
|
||||
notification.show()
|
||||
except:
|
||||
# See http://gpodder.org/bug/966
|
||||
pass
|
||||
notification = gpodder.notify.message(title, message)
|
||||
elif widget and isinstance(widget, gtk.Widget):
|
||||
if not widget.window:
|
||||
widget = self.main_window
|
||||
|
|
|
@ -77,13 +77,14 @@ from gpodder.gtkui.download import DownloadStatusModel
|
|||
from gpodder.gtkui.desktop.welcome import gPodderWelcome
|
||||
from gpodder.gtkui.desktop.channel import gPodderChannel
|
||||
from gpodder.gtkui.desktop.preferences import gPodderPreferences
|
||||
from gpodder.gtkui.desktop.extensions import gPodderExtensionManager
|
||||
from gpodder.gtkui.desktop.shownotes import gPodderShownotes
|
||||
from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
|
||||
from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
|
||||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||
|
||||
from gpodder.dbusproxy import DBusPodcastsProxy
|
||||
from gpodder import hooks
|
||||
from gpodder import extensions
|
||||
|
||||
class gPodder(BuilderWidget, dbus.service.Object):
|
||||
# Delay until live search is started after typing stop
|
||||
|
@ -203,9 +204,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.active_channel = None
|
||||
self.channels = self.model.get_podcasts()
|
||||
|
||||
gpodder.user_hooks.on_ui_initialized(self.model,
|
||||
self.hooks_podcast_update_cb,
|
||||
self.hooks_episode_download_cb)
|
||||
gpodder.user_extensions.on_ui_initialized(self.model,
|
||||
self.extensions_podcast_update_cb,
|
||||
self.extensions_episode_download_cb)
|
||||
|
||||
# load list of user applications for audio playback
|
||||
self.user_apps_reader = UserAppsReader(['audio', 'video'])
|
||||
|
@ -1130,6 +1131,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
|
||||
else:
|
||||
self.downloads_finished(self.download_tasks_seen)
|
||||
gpodder.user_extensions.on_all_episodes_downloaded()
|
||||
logger.info('All downloads have finished.')
|
||||
|
||||
# Remove finished episodes
|
||||
|
@ -1639,13 +1641,13 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
item.connect('activate', self.on_btnDownloadedDelete_clicked)
|
||||
menu.append(item)
|
||||
|
||||
result = gpodder.user_hooks.on_episodes_context_menu(episodes)
|
||||
result = gpodder.user_extensions.on_episodes_context_menu(episodes)
|
||||
if result:
|
||||
menu.append(gtk.SeparatorMenuItem())
|
||||
for label, callback in result:
|
||||
item = gtk.MenuItem(label)
|
||||
item.connect('activate', lambda item, callback:
|
||||
callback(episodes), callback)
|
||||
item.connect('activate', self.on_menuItem_activated,
|
||||
callback, episodes)
|
||||
menu.append(item)
|
||||
|
||||
# Ok, this probably makes sense to only display for downloaded files
|
||||
|
@ -1702,6 +1704,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
menu.popup(None, None, None, event.button, event.time)
|
||||
|
||||
return True
|
||||
|
||||
def on_menuItem_activated(self, menuitem, callback, episodes):
|
||||
threading.Thread(target=callback, args=(episodes,)).start()
|
||||
|
||||
def set_title(self, new_title):
|
||||
self.default_title = new_title
|
||||
|
@ -2813,6 +2818,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
mygpo_client=self.mygpo_client, \
|
||||
on_send_full_subscriptions=self.on_send_full_subscriptions, \
|
||||
on_itemExportChannels_activate=self.on_itemExportChannels_activate)
|
||||
|
||||
def on_itemExtensionSettings_activate(self, widget, *args):
|
||||
gPodderExtensionManager(self.main_window, \
|
||||
_config=self.config, \
|
||||
parent_window=self.main_window)
|
||||
|
||||
def on_goto_mygpo(self, widget):
|
||||
self.mygpo_client.open_website()
|
||||
|
@ -3314,13 +3324,13 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
return False
|
||||
|
||||
def hooks_podcast_update_cb(self, podcast):
|
||||
logger.debug('hooks_podcast_update_cb(%s)', podcast)
|
||||
def extensions_podcast_update_cb(self, podcast):
|
||||
logger.debug('extensions_podcast_update_cb(%s)', podcast)
|
||||
self.update_feed_cache(channels=[podcast],
|
||||
show_new_episodes_dialog=False)
|
||||
|
||||
def hooks_episode_download_cb(self, episode):
|
||||
logger.debug('hooks_episode_download_cb(%s)', episode)
|
||||
def extensions_episode_download_cb(self, episode):
|
||||
logger.debug('extension_episode_download_cb(%s)', episode)
|
||||
self.download_episode_list(episodes=[episode])
|
||||
|
||||
def main(options=None):
|
||||
|
|
|
@ -1,236 +0,0 @@
|
|||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Loads and executes user hooks.
|
||||
|
||||
Hooks are python scripts in the "Hooks" folder of $GPODDER_HOME. Each script
|
||||
must define a class named "gPodderHooks", otherwise it will be ignored.
|
||||
|
||||
The hooks class defines several callbacks that will be called by the
|
||||
gPodder application at certain points. See the methods defined below
|
||||
for a list on what these callbacks are and the parameters they take.
|
||||
|
||||
For an example extension see examples/hooks.py
|
||||
"""
|
||||
|
||||
import glob
|
||||
import imp
|
||||
import os
|
||||
import functools
|
||||
|
||||
import gpodder
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def call_hooks(func):
|
||||
"""Decorator to create handler functions in HookManager
|
||||
|
||||
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
|
||||
for filename, module in self.modules:
|
||||
try:
|
||||
callback = getattr(module, method_name, None)
|
||||
if callback is not None:
|
||||
# If the results are lists, concatenate them to show all
|
||||
# possible items that are generated by all hooks 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
|
||||
except Exception, e:
|
||||
logger.error('Error in %s, function %s: %s', filename,
|
||||
method_name, e, exc_info=True)
|
||||
func(self, *args, **kwargs)
|
||||
return result
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
class HookManager(object):
|
||||
# The class name that has to appear in a hook module
|
||||
HOOK_CLASS = 'gPodderHooks'
|
||||
|
||||
def __init__(self):
|
||||
"""Create a new hook manager"""
|
||||
self.modules = []
|
||||
|
||||
for filename in glob.glob(os.path.join(gpodder.home, 'Hooks', '*.py')):
|
||||
try:
|
||||
module = self._load_module(filename)
|
||||
if module is not None:
|
||||
self.modules.append((filename, module))
|
||||
logger.info('Module loaded: %s', filename)
|
||||
except Exception, e:
|
||||
logger.error('Cannot load %s: %s', filename, e, exc_info=True)
|
||||
|
||||
def register_hooks(self, obj):
|
||||
"""
|
||||
Register an object that implements some hooks.
|
||||
"""
|
||||
self.modules.append((None, obj))
|
||||
|
||||
def unregister_hooks(self, obj):
|
||||
"""
|
||||
Unregister a previously registered object.
|
||||
"""
|
||||
if (None, obj) in self.modules:
|
||||
self.modules.remove((None, obj))
|
||||
else:
|
||||
logger.warn('Unregistered hook which was not registered.')
|
||||
|
||||
def _load_module(self, filepath):
|
||||
"""Load a Python module by filename
|
||||
|
||||
Returns an instance of the HOOK_CLASS class defined
|
||||
in the module, or None if the module does not contain
|
||||
such a class.
|
||||
"""
|
||||
basename, extension = os.path.splitext(os.path.basename(filepath))
|
||||
module = imp.load_module(basename, file(filepath, 'r'), filepath, (extension, 'r', imp.PY_SOURCE))
|
||||
hook_class = getattr(module, HookManager.HOOK_CLASS, None)
|
||||
|
||||
if hook_class is None:
|
||||
return None
|
||||
else:
|
||||
return hook_class()
|
||||
|
||||
# Define all known handler functions here, decorate them with the
|
||||
# "call_hooks" decorator to forward all calls to hook scripts that have
|
||||
# the same function defined in them. If the handler functions here contain
|
||||
# any code, it will be called after all the hooks have been called.
|
||||
|
||||
@call_hooks
|
||||
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
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
def on_podcast_subscribe(self, podcast):
|
||||
"""Called when the user subscribes to a new podcast feed.
|
||||
|
||||
@param podcast: A gpodder.model.PodcastChannel instance
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
def on_podcast_updated(self, podcast):
|
||||
"""Called when a podcast feed was updated
|
||||
|
||||
This hook will be called even if there were no new episodes.
|
||||
|
||||
@param podcast: A gpodder.model.PodcastChannel instance
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
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.
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
def on_podcast_save(self, podcast):
|
||||
"""Called when a podcast is saved to the database
|
||||
|
||||
This hooks 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
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
def on_podcast_delete(self, podcast):
|
||||
"""Called when a podcast is deleted from the database
|
||||
|
||||
@param podcast: A gpodder.model.PodcastChannel instance
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
def on_episode_save(self, episode):
|
||||
"""Called when an episode is saved to the database
|
||||
|
||||
This hook 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
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
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
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
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 episode: A list of gpodder.model.PodcastEpisode instances
|
||||
"""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
def on_episode_delete(self, episode, filename):
|
||||
"""Called just before the episode's disk file is about to be
|
||||
deleted."""
|
||||
pass
|
||||
|
||||
@call_hooks
|
||||
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
|
||||
"""
|
||||
pass
|
|
@ -402,7 +402,7 @@ class PodcastEpisode(PodcastModelObject):
|
|||
not self.downloading)
|
||||
|
||||
def save(self):
|
||||
gpodder.user_hooks.on_episode_save(self)
|
||||
gpodder.user_extensions.on_episode_save(self)
|
||||
|
||||
self._clear_changes()
|
||||
|
||||
|
@ -470,7 +470,7 @@ class PodcastEpisode(PodcastModelObject):
|
|||
def delete_from_disk(self):
|
||||
filename = self.local_filename(create=False, check_only=True)
|
||||
if filename is not None:
|
||||
gpodder.user_hooks.on_episode_delete(self, filename)
|
||||
gpodder.user_extensions.on_episode_delete(self, filename)
|
||||
util.delete_file(filename)
|
||||
|
||||
self.set_state(gpodder.STATE_DELETED)
|
||||
|
@ -923,7 +923,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
|
||||
tmp.save()
|
||||
|
||||
gpodder.user_hooks.on_podcast_subscribe(tmp)
|
||||
gpodder.user_extensions.on_podcast_subscribe(tmp)
|
||||
|
||||
return tmp
|
||||
|
||||
|
@ -1091,7 +1091,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
for episode in episodes_to_purge:
|
||||
logger.debug('Episode removed from feed: %s (%s)',
|
||||
episode.title, episode.guid)
|
||||
gpodder.user_hooks.on_episode_removed_from_podcast(episode)
|
||||
gpodder.user_extensions.on_episode_removed_from_podcast(episode)
|
||||
self.db.delete_episode_by_guid(episode.guid, self.id)
|
||||
|
||||
# Remove the episode from the "children" episodes list
|
||||
|
@ -1149,10 +1149,10 @@ class PodcastChannel(PodcastModelObject):
|
|||
#feedcore.NotFound
|
||||
#feedcore.InvalidFeed
|
||||
#feedcore.UnknownStatusCode
|
||||
gpodder.user_hooks.on_podcast_update_failed(self, e)
|
||||
gpodder.user_extensions.on_podcast_update_failed(self, e)
|
||||
raise
|
||||
|
||||
gpodder.user_hooks.on_podcast_updated(self)
|
||||
gpodder.user_extensions.on_podcast_updated(self)
|
||||
|
||||
# Re-determine the common prefix for all episodes
|
||||
self._determine_common_prefix()
|
||||
|
@ -1167,7 +1167,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
if self.download_folder is None:
|
||||
self.get_save_dir()
|
||||
|
||||
gpodder.user_hooks.on_podcast_save(self)
|
||||
gpodder.user_extensions.on_podcast_save(self)
|
||||
|
||||
self._clear_changes()
|
||||
|
||||
|
@ -1317,7 +1317,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
for episode in self.get_downloaded_episodes():
|
||||
filename = episode.local_filename(create=False, check_only=True)
|
||||
if filename is not None:
|
||||
gpodder.user_hooks.on_episode_delete(episode, filename)
|
||||
gpodder.user_extensions.on_episode_delete(episode, filename)
|
||||
|
||||
shutil.rmtree(self.save_dir, True)
|
||||
|
||||
|
@ -1339,7 +1339,7 @@ class Model(object):
|
|||
|
||||
def _remove_podcast(self, podcast):
|
||||
self.children.remove(podcast)
|
||||
gpodder.user_hooks.on_podcast_delete(self)
|
||||
gpodder.user_extensions.on_podcast_delete(self)
|
||||
|
||||
def get_podcasts(self):
|
||||
def podcast_factory(dct, db):
|
||||
|
|
|
@ -488,7 +488,7 @@ class WoodchuckLoader():
|
|||
logger.info('Got on_ui_initialized. Setting up woodchuck..')
|
||||
|
||||
global woodchuck_loader
|
||||
gpodder.user_hooks.unregister_hooks(woodchuck_loader)
|
||||
gpodder.user_extensions.unregister_extensions(woodchuck_loader)
|
||||
woodchuck_loader = None
|
||||
|
||||
if not woodchuck_imported:
|
||||
|
@ -508,12 +508,12 @@ class WoodchuckLoader():
|
|||
|
||||
logger.info('Connected to Woodchuck server.')
|
||||
|
||||
gpodder.user_hooks.register_hooks(woodchuck_instance)
|
||||
gpodder.user_extensions.register_extensions(woodchuck_instance)
|
||||
|
||||
idle_add(check_subscriptions)
|
||||
|
||||
woodchuck_loader = WoodchuckLoader()
|
||||
woodchuck_instance = None
|
||||
|
||||
gpodder.user_hooks.register_hooks(woodchuck_loader)
|
||||
gpodder.user_extensions.register_extensions(woodchuck_loader)
|
||||
|
||||
|
|
|
@ -642,9 +642,9 @@ class qtPodder(QObject):
|
|||
# Initialize the gpodder.net client
|
||||
self.mygpo_client = my.MygPoClient(self.config)
|
||||
|
||||
gpodder.user_hooks.on_ui_initialized(self.model,
|
||||
self.hooks_podcast_update_cb,
|
||||
self.hooks_episode_download_cb)
|
||||
gpodder.user_extensions.on_ui_initialized(self.model,
|
||||
self.extensions_podcast_update_cb,
|
||||
self.extensions_episode_download_cb)
|
||||
|
||||
self.view = DeclarativeView()
|
||||
self.view.closing.connect(self.on_quit)
|
||||
|
@ -846,24 +846,24 @@ class qtPodder(QObject):
|
|||
return podcasts[0]
|
||||
return None
|
||||
|
||||
def hooks_podcast_update_cb(self, podcast):
|
||||
logger.debug('hooks_podcast_update_cb(%s)', podcast)
|
||||
def extensions_podcast_update_cb(self, podcast):
|
||||
logger.debug('extensions_podcast_update_cb(%s)', podcast)
|
||||
try:
|
||||
qpodcast = self.podcast_to_qpodcast(podcast)
|
||||
if qpodcast is not None:
|
||||
qpodcast.qupdate(
|
||||
finished_callback=self.controller.update_subset_stats)
|
||||
except Exception, e:
|
||||
logger.exception('hooks_podcast_update_cb(%s): %s', podcast, e)
|
||||
logger.exception('extensions_podcast_update_cb(%s): %s', podcast, e)
|
||||
|
||||
def hooks_episode_download_cb(self, episode):
|
||||
logger.debug('hooks_episode_download_cb(%s)', episode)
|
||||
def extensions_episode_download_cb(self, episode):
|
||||
logger.debug('extensions_episode_download_cb(%s)', episode)
|
||||
try:
|
||||
qpodcast = self.podcast_to_qpodcast(episode.channel)
|
||||
qepisode = self.wrap_episode(qpodcast, episode)
|
||||
self.controller.downloadEpisode(qepisode)
|
||||
except Exception, e:
|
||||
logger.exception('hooks_episode_download_cb(%s): %s', episode, e)
|
||||
logger.exception('extensions_episode_download_cb(%s): %s', episode, e)
|
||||
|
||||
def main(args):
|
||||
gui = qtPodder(args, core.Core())
|
||||
|
|
|
@ -25,11 +25,10 @@
|
|||
|
||||
"""Miscellaneous helper functions for gPodder
|
||||
|
||||
This module provides helper and utility functions for gPodder that
|
||||
This module provides helper and utility functions for gPodder that
|
||||
are not tied to any specific part of gPodder.
|
||||
|
||||
"""
|
||||
|
||||
import gpodder
|
||||
|
||||
import logging
|
||||
|
@ -111,14 +110,37 @@ def _sanitize_char(c):
|
|||
SANITIZATION_TABLE = ''.join(map(_sanitize_char, map(chr, range(256))))
|
||||
del _sanitize_char
|
||||
|
||||
# Used by file_type_by_extension()
|
||||
_BUILTIN_FILE_TYPES = None
|
||||
_MIME_TYPE_LIST = [
|
||||
('.aac', 'audio/aac'),
|
||||
('.axa', 'audio/annodex'),
|
||||
('.flac', 'audio/flac'),
|
||||
('.m4b', 'audio/m4b'),
|
||||
('.m4a', 'audio/mp4'),
|
||||
('.mp3', 'audio/mpeg'),
|
||||
('.spx', 'audio/ogg'),
|
||||
('.oga', 'audio/ogg'),
|
||||
('.ogg', 'audio/ogg'),
|
||||
('.wma', 'audio/x-ms-wma'),
|
||||
('.3gp', 'video/3gpp'),
|
||||
('.axv', 'video/annodex'),
|
||||
('.divx', 'video/divx'),
|
||||
('.m4v', 'video/m4v'),
|
||||
('.mp4', 'video/mp4'),
|
||||
('.ogv', 'video/ogg'),
|
||||
('.mov', 'video/quicktime'),
|
||||
('.flv', 'video/x-flv'),
|
||||
('.mkv', 'video/x-matroska'),
|
||||
('.wmv', 'video/x-ms-wmv'),
|
||||
]
|
||||
|
||||
_MIME_TYPES = dict((k, v) for v, k in _MIME_TYPE_LIST)
|
||||
_MIME_TYPES_EXT = dict(_MIME_TYPE_LIST)
|
||||
|
||||
|
||||
def make_directory( path):
|
||||
"""
|
||||
Tries to create a directory if it does not exist already.
|
||||
Returns True if the directory exists after the function
|
||||
Returns True if the directory exists after the function
|
||||
call, False otherwise.
|
||||
"""
|
||||
if os.path.isdir( path):
|
||||
|
@ -135,7 +157,7 @@ def make_directory( path):
|
|||
|
||||
def normalize_feed_url(url):
|
||||
"""
|
||||
Converts any URL to http:// or ftp:// so that it can be
|
||||
Converts any URL to http:// or ftp:// so that it can be
|
||||
used with "wget". If the URL cannot be converted (invalid
|
||||
or unknown scheme), "None" is returned.
|
||||
|
||||
|
@ -285,9 +307,9 @@ def username_password_from_url(url):
|
|||
|
||||
def calculate_size( path):
|
||||
"""
|
||||
Tries to calculate the size of a directory, including any
|
||||
subdirectories found. The returned value might not be
|
||||
correct if the user doesn't have appropriate permissions
|
||||
Tries to calculate the size of a directory, including any
|
||||
subdirectories found. The returned value might not be
|
||||
correct if the user doesn't have appropriate permissions
|
||||
to list all subdirectories of the given path.
|
||||
"""
|
||||
if path is None:
|
||||
|
@ -433,12 +455,12 @@ def format_date(timestamp):
|
|||
except ValueError, ve:
|
||||
logger.warn('Cannot convert timestamp', exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
if timestamp_date == today:
|
||||
return _('Today')
|
||||
elif timestamp_date == yesterday:
|
||||
return _('Yesterday')
|
||||
|
||||
|
||||
try:
|
||||
diff = int( (time.time() - timestamp)/seconds_in_a_day )
|
||||
except:
|
||||
|
@ -460,7 +482,7 @@ def format_date(timestamp):
|
|||
|
||||
def format_filesize(bytesize, use_si_units=False, digits=2):
|
||||
"""
|
||||
Formats the given size in bytes to be human-readable,
|
||||
Formats the given size in bytes to be human-readable,
|
||||
|
||||
Returns a localized "(unknown)" string when the bytesize
|
||||
has a negative value.
|
||||
|
@ -515,7 +537,7 @@ def delete_file(filename):
|
|||
def remove_html_tags(html):
|
||||
"""
|
||||
Remove HTML tags from a string and replace numeric and
|
||||
named entities with the corresponding character, so the
|
||||
named entities with the corresponding character, so the
|
||||
HTML text can be displayed in a simple text view.
|
||||
"""
|
||||
if html is None:
|
||||
|
@ -529,7 +551,7 @@ def remove_html_tags(html):
|
|||
re_listing_tags = re.compile('<li[^>]*>', re.I)
|
||||
|
||||
result = html
|
||||
|
||||
|
||||
# Convert common HTML elements to their text equivalent
|
||||
result = re_newline_tags.sub('\n', result)
|
||||
result = re_listing_tags.sub('\n * ', result)
|
||||
|
@ -543,7 +565,7 @@ def remove_html_tags(html):
|
|||
|
||||
# Convert named HTML entities to their unicode character
|
||||
result = re_html_entities.sub(lambda x: unicode(entitydefs.get(x.group(1),''), 'iso-8859-1'), result)
|
||||
|
||||
|
||||
# Convert more than two newlines to two newlines
|
||||
result = re.sub('([\r\n]{2})([\r\n])+', '\\1', result)
|
||||
|
||||
|
@ -599,17 +621,47 @@ def wrong_extension(extension):
|
|||
def extension_from_mimetype(mimetype):
|
||||
"""
|
||||
Simply guesses what the file extension should be from the mimetype
|
||||
|
||||
>>> extension_from_mimetype('audio/mp4')
|
||||
'.m4a'
|
||||
>>> extension_from_mimetype('audio/ogg')
|
||||
'.ogg'
|
||||
>>> extension_from_mimetype('audio/mpeg')
|
||||
'.mp3'
|
||||
>>> extension_from_mimetype('video/x-matroska')
|
||||
'.mkv'
|
||||
>>> extension_from_mimetype('wrong-mimetype')
|
||||
''
|
||||
"""
|
||||
MIMETYPE_EXTENSIONS = {
|
||||
# This is required for YouTube downloads on Maemo 5
|
||||
'video/x-flv': '.flv',
|
||||
'video/mp4': '.mp4',
|
||||
}
|
||||
if mimetype in MIMETYPE_EXTENSIONS:
|
||||
return MIMETYPE_EXTENSIONS[mimetype]
|
||||
if mimetype in _MIME_TYPES:
|
||||
return _MIME_TYPES[mimetype]
|
||||
return mimetypes.guess_extension(mimetype) or ''
|
||||
|
||||
|
||||
def mimetype_from_extension(extension):
|
||||
"""
|
||||
Simply guesses what the mimetype should be from the file extension
|
||||
|
||||
>>> mimetype_from_extension('.m4a')
|
||||
'audio/mp4'
|
||||
>>> mimetype_from_extension('.ogg')
|
||||
'audio/ogg'
|
||||
>>> mimetype_from_extension('.mp3')
|
||||
'audio/mpeg'
|
||||
>>> mimetype_from_extension('.mkv')
|
||||
'video/x-matroska'
|
||||
>>> mimetype_from_extension('.abc')
|
||||
''
|
||||
"""
|
||||
if extension in _MIME_TYPES_EXT:
|
||||
return _MIME_TYPES_EXT[extension]
|
||||
|
||||
# Need to prepend something to the extension, so guess_type works
|
||||
type, encoding = mimetypes.guess_type('file'+extension)
|
||||
|
||||
return type or ''
|
||||
|
||||
|
||||
def extension_correct_for_mimetype(extension, mimetype):
|
||||
"""
|
||||
Check if the given filename extension (e.g. ".ogg") is a possible
|
||||
|
@ -622,6 +674,8 @@ def extension_correct_for_mimetype(extension, mimetype):
|
|||
True
|
||||
>>> extension_correct_for_mimetype('.ogg', 'audio/mpeg')
|
||||
False
|
||||
>>> extension_correct_for_mimetype('.m4a', 'audio/mp4')
|
||||
True
|
||||
>>> extension_correct_for_mimetype('mp3', 'audio/mpeg')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
|
@ -636,10 +690,12 @@ def extension_correct_for_mimetype(extension, mimetype):
|
|||
if not extension.startswith('.'):
|
||||
raise ValueError('"%s" is not an extension (missing .)' % extension)
|
||||
|
||||
if (extension, mimetype) in _MIME_TYPE_LIST:
|
||||
return True
|
||||
|
||||
# Create a "default" extension from the mimetype, e.g. "application/ogg"
|
||||
# becomes ".ogg", "audio/mpeg" becomes ".mpeg", etc...
|
||||
default = ['.'+mimetype.split('/')[-1]]
|
||||
|
||||
return extension in default+mimetypes.guess_all_extensions(mimetype)
|
||||
|
||||
|
||||
|
@ -649,10 +705,10 @@ def filename_from_url(url):
|
|||
from a URL, e.g. http://server.com/file.MP3?download=yes
|
||||
will result in the string ("file", ".mp3") being returned.
|
||||
|
||||
This function will also try to best-guess the "real"
|
||||
This function will also try to best-guess the "real"
|
||||
extension for a media file (audio, video) by
|
||||
trying to match an extension to these types and recurse
|
||||
into the query string to find better matches, if the
|
||||
into the query string to find better matches, if the
|
||||
original extension does not resolve to a known type.
|
||||
|
||||
http://my.net/redirect.php?my.net/file.ogg => ("file", ".ogg")
|
||||
|
@ -682,14 +738,16 @@ def filename_from_url(url):
|
|||
|
||||
def file_type_by_extension(extension):
|
||||
"""
|
||||
Tries to guess the file type by looking up the filename
|
||||
extension from a table of known file types. Will return
|
||||
Tries to guess the file type by looking up the filename
|
||||
extension from a table of known file types. Will return
|
||||
"audio", "video" or None.
|
||||
|
||||
>>> file_type_by_extension('.aif')
|
||||
'audio'
|
||||
>>> file_type_by_extension('.3GP')
|
||||
'video'
|
||||
>>> file_type_by_extension('.m4a')
|
||||
'audio'
|
||||
>>> file_type_by_extension('.txt') is None
|
||||
True
|
||||
>>> file_type_by_extension(None) is None
|
||||
|
@ -705,23 +763,10 @@ def file_type_by_extension(extension):
|
|||
if not extension.startswith('.'):
|
||||
raise ValueError('Extension does not start with a dot: %s' % extension)
|
||||
|
||||
global _BUILTIN_FILE_TYPES
|
||||
if _BUILTIN_FILE_TYPES is None:
|
||||
# List all types that are not in the default mimetypes.types_map
|
||||
# (even if they might be detected by mimetypes.guess_type)
|
||||
# For OGG, see http://wiki.xiph.org/MIME_Types_and_File_Extensions
|
||||
audio_types = ('.ogg', '.oga', '.spx', '.flac', '.axa', \
|
||||
'.aac', '.m4a', '.m4b', '.wma')
|
||||
video_types = ('.ogv', '.axv', '.mp4', \
|
||||
'.mkv', '.m4v', '.divx', '.flv', '.wmv', '.3gp')
|
||||
_BUILTIN_FILE_TYPES = {}
|
||||
_BUILTIN_FILE_TYPES.update((ext, 'audio') for ext in audio_types)
|
||||
_BUILTIN_FILE_TYPES.update((ext, 'video') for ext in video_types)
|
||||
|
||||
extension = extension.lower()
|
||||
|
||||
if extension in _BUILTIN_FILE_TYPES:
|
||||
return _BUILTIN_FILE_TYPES[extension]
|
||||
if extension in _MIME_TYPES_EXT:
|
||||
return _MIME_TYPES_EXT[extension].split('/')[0]
|
||||
|
||||
# Need to prepend something to the extension, so guess_type works
|
||||
type, encoding = mimetypes.guess_type('file'+extension)
|
||||
|
@ -730,7 +775,7 @@ def file_type_by_extension(extension):
|
|||
filetype, rest = type.split('/', 1)
|
||||
if filetype in ('audio', 'video', 'image'):
|
||||
return filetype
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
@ -744,10 +789,10 @@ def get_first_line( s):
|
|||
|
||||
def object_string_formatter( s, **kwargs):
|
||||
"""
|
||||
Makes attributes of object passed in as keyword
|
||||
arguments available as {OBJECTNAME.ATTRNAME} in
|
||||
the passed-in string and returns a string with
|
||||
the above arguments replaced with the attribute
|
||||
Makes attributes of object passed in as keyword
|
||||
arguments available as {OBJECTNAME.ATTRNAME} in
|
||||
the passed-in string and returns a string with
|
||||
the above arguments replaced with the attribute
|
||||
values of the corresponding object.
|
||||
|
||||
Example:
|
||||
|
@ -755,7 +800,7 @@ def object_string_formatter( s, **kwargs):
|
|||
e = Episode()
|
||||
e.title = 'Hello'
|
||||
s = '{episode.title} World'
|
||||
|
||||
|
||||
print object_string_formatter( s, episode = e)
|
||||
=> 'Hello World'
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue