Skip to content

Commit

Permalink
Merge pull request #20 from bburan/bburan/embed-with-kwargs
Browse files Browse the repository at this point in the history
Added `video_embed` tag that accepts kwargs
  • Loading branch information
yetty committed May 2, 2014
2 parents 8fc5022 + 5ebea38 commit 5857d80
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 24 deletions.
24 changes: 24 additions & 0 deletions docs/api/embed_video.settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,27 @@ EMBED_VIDEO_TIMEOUT
Sets timeout for ``GET`` requests to remote servers.

Default: ``10``


.. setting:: EMBED_VIDEO_{BACKEND}_QUERY

EMBED_VIDEO_{BACKEND}_QUERY
---------------------------

Set default query dictionary for including in the embedded URL. This is
backend-specific. Substitute the actual name of the backend for {BACKEND}
(i.e. to set the default query for the YouTube backend, the setting name would
be ``EMBED_VIDEO_YOUTUBE_QUERY`` and for SoundCloud the name would be
``EMBED_VIDEO_SOUNDCLOUD_QUERY``).

As an example, if you set the following::

EMBED_VIDEO_YOUTUBE_QUERY = {
'wmode': 'opaque',
'rel': '0'
}

All URLs for YouTube will include the query string ``?wmode=opaque&rel=0``.

The default value for EMBED_VIDEO_YOUTUBE_QUERY is ``{'wmode': 'opaque'}``.
None of the other backends have default values set.
28 changes: 28 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ Default sizes are ``tiny`` (420x315), ``small`` (480x360), ``medium`` (640x480),
{% video my_video '100% x 50%' %}


Some backends (e.g. YouTube) allow configuration of the embedding via passing
query parameters. To specify the parameters:

::

{% video item.video 'small' rel=0 %}

{% video item.video 'small' start=2 stop=5 repeat=1 %}

{% video item.video rel=0 as my_video %}
URL: {{ my_video.url }}
Thumbnail: {{ my_video.thumbnail }}
Backend: {{ my_video.backend }}
{% video my_video 'small' %}
{% endvideo %}

Parameters may also be template variables:

::

{% video item.video 'small' start=item.start stop=item.stop repeat=item.repeat %}


.. tip::

You can provide default values for the query string that's included in the
embedded URL by updating your Django settings file.


.. tip::

Expand Down
28 changes: 25 additions & 3 deletions embed_video/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import json

try:
# Python <= 2.7
import urlparse
from urllib import urlencode
except ImportError:
# support for py3
import urllib.parse as urlparse
from urllib.parse import urlencode

from django.conf import settings
from django.template.loader import render_to_string
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe

from .utils import import_by_path
from .settings import EMBED_VIDEO_BACKENDS, EMBED_VIDEO_TIMEOUT
Expand Down Expand Up @@ -106,14 +111,20 @@ class MyBackend(VideoBackend):
``{{ width }}``, ``{{ height }}``
"""

def __init__(self, url, is_secure=False):
def __init__(self, url, is_secure=False, query=None):
"""
First it tries to load data from cache and if it don't succeed, run
:py:meth:`init` and then save it to cache.
"""
self.is_secure = is_secure
self.backend = self.__class__.__name__
self._url = url
self.update_query(query)

def update_query(self, query=None):
self._query = self.get_default_query()
if query is not None:
self._query.update(query)

@cached_property
def code(self):
Expand Down Expand Up @@ -155,7 +166,10 @@ def get_url(self):
"""
Returns URL folded from :py:data:`pattern_url` and parsed code.
"""
return self.pattern_url.format(code=self.code, protocol=self.protocol)
url = self.pattern_url.format(code=self.code, protocol=self.protocol)
if self._query:
url += '?' + urlencode(self._query, doseq=True)
return mark_safe(url)

def get_thumbnail_url(self):
"""
Expand All @@ -178,6 +192,13 @@ def get_embed_code(self, width, height):
def get_info(self):
raise NotImplementedError

def get_default_query(self):
# Derive backend name from class name
backend_name = self.__class__.__name__[:-7].upper()
default = getattr(self, 'default_query', {})
settings_key = 'EMBED_VIDEO_{}_QUERY'.format(backend_name)
return getattr(settings, settings_key, default).copy()


class YoutubeBackend(VideoBackend):
"""
Expand All @@ -200,8 +221,9 @@ class YoutubeBackend(VideoBackend):
re.I | re.X
)

pattern_url = '{protocol}://www.youtube.com/embed/{code}?wmode=opaque'
pattern_url = '{protocol}://www.youtube.com/embed/{code}'
pattern_thumbnail_url = '{protocol}://img.youtube.com/vi/{code}/hqdefault.jpg'
default_query = {'wmode': 'opaque'}

def get_code(self):
code = super(YoutubeBackend, self).get_code()
Expand Down
74 changes: 55 additions & 19 deletions embed_video/templatetags/embed_video_tags.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.template import Library, Node, TemplateSyntaxError
from django.template import Library, Node, TemplateSyntaxError, Variable
from django.utils.safestring import mark_safe
from django.utils.encoding import smart_str
import re
import logging
import requests
from collections import defaultdict

from ..backends import detect_backend, VideoBackend, \
VideoDoesntExistException, UnknownBackendException
Expand All @@ -11,6 +13,9 @@

logger = logging.getLogger(__name__)

# Used for parsing keyword arguments passed in as key-value pairs
kw_pat = re.compile(r'^(?P<key>[\w]+)=(?P<value>.+)$')


@register.tag('video')
class VideoNode(Node):
Expand Down Expand Up @@ -47,37 +52,66 @@ class VideoNode(Node):
{% endvideo %}
"""
error_msg = 'Syntax error. Expected: ``{% video URL ... %}``'
error_msg = 'Syntax error. Expected: ``{% video URL ' \
'[size] [key1=val1 key2=val2 ...] [as var] %}``'
default_size = 'small'

re_size = re.compile('(?P<width>\d+%?) *x *(?P<height>\d+%?)')

def __init__(self, parser, token):
self.size = None
self.bits = token.split_contents()
self.query = None

try:
self.url = parser.compile_filter(self.bits[1])
except IndexError:
raise TemplateSyntaxError(self.error_msg)

# Determine if the tag is being used as a context variable
if self.bits[-2] == 'as':
option_bits = self.bits[2:-2]
self.nodelist_file = parser.parse(('endvideo',))
parser.delete_first_token()
else:
try:
self.size = parser.compile_filter(self.bits[2])
except IndexError:
self.size = self.default_size
option_bits = self.bits[2:]

# Size must be the first argument and is only accepted when this is
# used as a template tag (but not when used as a block tag)
if len(option_bits) != 0 and '=' not in option_bits[0]:
self.size = parser.compile_filter(option_bits[0])
option_bits = option_bits[1:]
else:
self.size= self.default_size

# Parse arguments passed in as KEY=VALUE pairs that will be added to
# the URL as a GET query string
if len(option_bits) != 0:
self.query = defaultdict(list)

for bit in option_bits:
match = kw_pat.match(bit)
key = smart_str(match.group('key'))
value = Variable(smart_str(match.group('value')))
self.query[key].append(value)

def render(self, context):
url = self.url.resolve(context)
# Attempt to resolve any parameters passed in.
if self.query is not None:
resolved_query = defaultdict(list)
for key, values in self.query.items():
for value in values:
resolved_value = value.resolve(context)
resolved_query[key].append(resolved_value)
else:
resolved_query = None

url = self.url.resolve(context)
try:
if self.size:
return self.__render_embed(url, context)
return self.__render_embed(url, context, resolved_query)
else:
return self.__render_block(url, context)
return self.__render_block(url, context, resolved_query)
except requests.Timeout:
logger.exception('Timeout reached during rendering embed video (`{0}`)'.format(url))
except UnknownBackendException:
Expand All @@ -87,44 +121,46 @@ def render(self, context):

return ''

def __render_embed(self, url, context):
def __render_embed(self, url, context, query):
size = self.size.resolve(context) \
if hasattr(self.size, 'resolve') else self.size
return self.embed(url, size, context=context)
return self.embed(url, size, context=context, query=query)

def __render_block(self, url, context):
def __render_block(self, url, context, query):
as_var = self.bits[-1]

context.push()
context[as_var] = self.get_backend(url, context=context)
context[as_var] = self.get_backend(url, context=context, query=query)
output = self.nodelist_file.render(context)
context.pop()

return output

@staticmethod
def get_backend(backend_or_url, context=None):
def get_backend(backend_or_url, context=None, query=None):
"""
Returns instance of VideoBackend. If context is passed to the method
and request is secure, than the is_secure mark is set to backend.
A string or VideoBackend instance can be passed to the method.
"""

backend = backend_or_url if isinstance(backend_or_url, VideoBackend) \
else detect_backend(backend_or_url)

if context and 'request' in context:
backend.is_secure = context['request'].is_secure()

backend.update_query(query)

return backend

@staticmethod
def embed(url, size, context=None):
def embed(url, size, GET=None, context=None, query=None):
"""
Direct render of embed video.
"""
backend = VideoNode.get_backend(url, context=context)
backend = VideoNode.get_backend(url, context=context, query=query)
width, height = VideoNode.get_size(size)
return mark_safe(backend.get_embed_code(width=width, height=height))

Expand Down

0 comments on commit 5857d80

Please sign in to comment.