add work for the new extension system (bugs #1491, #1500, #1555)

This commit is contained in:
Bernd Schlapsi 2012-02-04 21:43:37 +01:00 committed by Thomas Perl
parent 263c9092b5
commit f73a1750c5
15 changed files with 171 additions and 378 deletions

14
bin/gpo
View File

@ -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):

View File

@ -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>

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -142,6 +142,10 @@ defaults = {
'youtube': {
'preferred_fmt_id': 18,
},
'extensions': {
'enabled': [],
},
}
# The sooner this goes away, the better

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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())

View File

@ -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'
"""