class AbstractEmbedPlayer(AbstractPlayer): """ Abstract Embed Player for third-party services like YouTube Typically embed players will play only their own content, and that is the only way such content can be played. Therefore each embed type has been given its own :attr:`~mediadrop.lib.uri.StorageURI.scheme` which uniquely identifies it. For example, :meth:`mediadrop.lib.storage.YoutubeStorage.get_uris` returns URIs with a scheme of `'youtube'`, and the special :class:`YoutubePlayer` would overload :attr:`scheme` to also be `'youtube'`. This would allow the Youtube player to play only those URIs. """ scheme = abstractproperty() """The `StorageURI.scheme` which uniquely identifies this embed type.""" @classmethod def can_play(cls, uris): """Test all the given URIs to see if they can be played by this player. This is a class method, not an instance or static method. :type uris: list :param uris: A collection of StorageURI tuples to test. :rtype: tuple :returns: Boolean result for each of the given URIs. """ return tuple(uri.scheme == cls.scheme for uri in uris)
class FileSupportMixin(object): """ Mixin that provides a can_play test on a number of common parameters. """ supported_containers = abstractproperty() supported_schemes = set([HTTP]) supported_types = set([AUDIO, VIDEO]) @classmethod def can_play(cls, uris): """Test all the given URIs to see if they can be played by this player. This is a class method, not an instance or static method. :type uris: list :param uris: A collection of StorageURI tuples to test. :rtype: tuple :returns: Boolean result for each of the given URIs. """ playable = [] for uri in uris: is_type_ok = (uri.file.type in cls.supported_types) is_scheme_ok = (uri.scheme in cls.supported_schemes) is_container_ok = (not uri.file.container) or (uri.file.container in cls.supported_containers) is_playable = is_type_ok and is_scheme_ok and is_container_ok playable.append(is_playable) return tuple(playable)
class EmbedStorageEngine(StorageEngine): """ A specialized URL storage engine for URLs that match a certain pattern. """ is_singleton = True try_after = [FileStorageEngine] url_pattern = abstractproperty() """A compiled pattern object that uses named groupings for matches.""" def parse(self, file=None, url=None): """Return metadata for the given URL or raise an error. If the given URL matches :attr:`url_pattern` then :meth:`_parse` is called with the named matches as kwargs and the result returned. :type file: :class:`cgi.FieldStorage` or None :param file: A freshly uploaded file object. :type url: unicode or None :param url: A remote URL string. :rtype: dict :returns: Any extracted metadata. :raises UnsuitableEngineError: If file information cannot be parsed. """ if url is None: raise UnsuitableEngineError match = self.url_pattern.match(url) if match is None: raise UnsuitableEngineError return self._parse(url, **match.groupdict()) @abstractmethod def _parse(self, url, **kwargs): """Return metadata for the given URL that matches :attr:`url_pattern`.
class StorageEngine(AbstractClass): """ Base class for all Storage Engine implementations. """ engine_type = abstractproperty() """A unique identifying unicode string for the StorageEngine.""" default_name = abstractproperty() """A user-friendly display name that identifies this StorageEngine.""" is_singleton = abstractproperty() """A flag that indicates whether this engine should be added only once.""" settings_form_class = None """Your :class:`mediadrop.forms.Form` class for changing :attr:`_data`.""" _default_data = {} """The default data dictionary to create from the start. If you plan to store something in :attr:`_data`, declare it in this dict for documentation purposes, if nothing else. Down the road, we may validate data against this dict to ensure that only known keys are used. """ try_before = [] """Storage Engines that should :meth:`parse` after this class has. This is a list of StorageEngine class objects which is used to perform a topological sort of engines. See :func:`sort_engines` and :func:`add_new_media_file`. """ try_after = [] """Storage Engines that should :meth:`parse` before this class has. This is a list of StorageEngine class objects which is used to perform a topological sort of engines. See :func:`sort_engines` and :func:`add_new_media_file`. """ def __init__(self, display_name=None, data=None): """Initialize with the given data, or the class defaults. :type display_name: unicode :param display_name: Name, defaults to :attr:`default_name`. :type data: dict :param data: The unique parameters of this engine instance. """ self.display_name = display_name or self.default_name self._data = data or self._default_data def engine_params(self): """Return the unique parameters of this engine instance. :rtype: dict :returns: All the data necessary to create a functionally equivalent instance of this engine. """ return self._data @property @memoize def settings_form(self): """Return an instance of :attr:`settings_form_class` if defined. :rtype: :class:`mediadrop.forms.Form` or None :returns: A memoized form instance, since instantiation is expensive. """ if self.settings_form_class is None: return None return self.settings_form_class() @abstractmethod def parse(self, file=None, url=None): """Return metadata for the given file or URL, or raise an error. It is expected that different storage engines will be able to extract different metadata. **Required metadata keys**: * type (generally 'audio' or 'video') **Optional metadata keys**: * unique_id * container * display_name * title * size * width * height * bitrate * thumbnail_file * thumbnail_url :type file: :class:`cgi.FieldStorage` or None :param file: A freshly uploaded file object. :type url: unicode or None :param url: A remote URL string. :rtype: dict :returns: Any extracted metadata. :raises UnsuitableEngineError: If file information cannot be parsed. """ def store(self, media_file, file=None, url=None, meta=None): """Store the given file or URL and return a unique identifier for it. This method is called with a newly persisted instance of :class:`~mediadrop.model.media.MediaFile`. The instance has been flushed and therefore has its primary key, but it has not yet been committed. An exception here will trigger a rollback. This method need not necessarily return anything. If :meth:`parse` returned a `unique_id` key, this can return None. It is only when this method generates the unique ID, or if it must override the unique ID from :meth:`parse`, that it should be returned here. This method SHOULD NOT modify the `media_file`. It is provided for informational purposes only, so that a unique ID may be generated with the primary key from the database. :type media_file: :class:`~mediadrop.model.media.MediaFile` :param media_file: The associated media file object. :type file: :class:`cgi.FieldStorage` or None :param file: A freshly uploaded file object. :type url: unicode or None :param url: A remote URL string. :type meta: dict :param meta: The metadata returned by :meth:`parse`. :rtype: unicode or None :returns: The unique ID string. Return None if not generating it here. """ def postprocess(self, media_file): """Perform additional post-processing after the save is complete. This is called after :meth:`parse`, :meth:`store`, thumbnails have been saved and the changes to database flushed. :type media_file: :class:`~mediadrop.model.media.MediaFile` :param media_file: The associated media file object. :returns: None """ def delete(self, unique_id): """Delete the stored file represented by the given unique ID. :type unique_id: unicode :param unique_id: The identifying string for this file. :rtype: boolean :returns: True if successful, False if an error occurred. """ def transcode(self, media_file): """Transcode an existing MediaFile. The MediaFile may be stored already by another storage engine. New MediaFiles will be created for each transcoding generated by this method. :type media_file: :class:`~mediadrop.model.media.MediaFile` :param media_file: The MediaFile object to transcode. :raises CannotTranscode: If this storage engine can't or won't transcode the file. :rtype: NoneType :returns: Nothing """ raise CannotTranscode( 'This StorageEngine does not support transcoding.') @abstractmethod def get_uris(self, media_file): """Return a list of URIs from which the stored file can be accessed.
class AbstractPlayer(AbstractClass): """ Player Base Class that all players must implement. """ name = abstractproperty() """A unicode string identifier for this class.""" display_name = abstractproperty() """A unicode display name for the class, to be used in the settings UI.""" settings_form_class = None """An optional :class:`mediadrop.forms.admin.players.PlayerPrefsForm`.""" default_data = {} """An optional default data dictionary for user preferences.""" supports_resizing = True """A flag that allows us to mark the few players that can't be resized. Setting this to False ensures that the resize (expand/shrink) controls will not be shown in our player control bar. """ @abstractmethod def can_play(cls, uris): """Test all the given URIs to see if they can be played by this player. This is a class method, not an instance or static method. :type uris: list :param uris: A collection of StorageURI tuples to test. :rtype: tuple :returns: Boolean result for each of the given URIs. """ def render_markup(self, error_text=None): """Render the XHTML markup for this player instance. :param error_text: Optional error text that should be included in the final markup if appropriate for the player. :rtype: ``unicode`` or :class:`genshi.core.Markup` :returns: XHTML that will not be escaped by Genshi. """ return error_text or u'' @abstractmethod def render_js_player(self): """Render a javascript string to instantiate a javascript player. Each player has a client-side component to provide a consistent way of initializing and interacting with the player. For more information see :file:`mediadrop/public/scripts/mcore/players/`. :rtype: ``unicode`` :returns: A javascript string which will evaluate to an instance of a JS player class. For example: ``new mcore.Html5Player()``. """ def __init__(self, media, uris, data=None, width=None, height=None, autoplay=False, autobuffer=False, qualified=False, **kwargs): """Initialize the player with the media that it will be playing. :type media: :class:`mediadrop.model.media.Media` instance :param media: The media object that will be rendered. :type uris: list :param uris: The StorageURIs this player has said it :meth:`can_play`. :type data: dict or None :param data: Optional player preferences from the database. :type elem_id: unicode, None, Default :param elem_id: The element ID to use when rendering. If left undefined, a sane default value is provided. Use None to disable. """ self.media = media self.uris = uris self.data = data or {} self.width = width or 400 self.height = height or 225 self.autoplay = autoplay self.autobuffer = autobuffer self.qualified = qualified self.elem_id = kwargs.pop('elem_id', '%s-player' % media.slug) _width_diff = 0 _height_diff = 0 @property def adjusted_width(self): """Return the desired viewable width + any extra for the player.""" return self.width + self._width_diff @property def adjusted_height(self): """Return the desired viewable height + the height of the controls.""" return self.height + self._height_diff def get_uris(self, **kwargs): """Return a subset of the :attr:`uris` for this player. This allows for easy filtering of URIs by feeding any number of kwargs to this function. See :func:`mediadrop.lib.uri.pick_uris`. """ return pick_uris(self.uris, **kwargs) @classmethod def inject_in_db(cls, enable_player=False): from mediadrop.model import DBSession from mediadrop.model.players import players as players_table, PlayerPrefs prefs = PlayerPrefs() prefs.name = cls.name prefs.enabled = enable_player # MySQL does not allow referencing the same table in a subquery # (i.e. insert, max): http://stackoverflow.com/a/14302701/138526 # Therefore we need to alias the table in max current_max_query = sql.select([sql.func.max(players_table.alias().c.priority)]) # sql.func.coalesce == "set default value if func.max does " # In case there are no players in the database the current max is NULL. With # coalesce we can set a default value. new_priority_query = sql.func.coalesce( current_max_query.as_scalar()+1, 1 ) prefs.priority = new_priority_query prefs.created_on = datetime.now() prefs.modified_on = datetime.now() prefs.data = cls.default_data DBSession.add(prefs) DBSession.commit()