django-embed-video/embed_video/backends.py

315 lines
8.5 KiB
Python
Raw Normal View History

2013-06-01 12:04:55 +00:00
import re
2013-08-22 13:44:27 +00:00
import requests
import json
2013-08-23 11:37:44 +00:00
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
2013-08-22 12:29:08 +00:00
from django.conf import settings
2014-02-27 08:48:47 +00:00
from django.template.loader import render_to_string
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
2014-05-07 20:53:46 +00:00
from django.utils.datastructures import SortedDict
2013-06-01 12:04:55 +00:00
2013-08-22 14:19:07 +00:00
from .utils import import_by_path
2014-02-21 22:21:00 +00:00
from .settings import EMBED_VIDEO_BACKENDS, EMBED_VIDEO_TIMEOUT
2013-08-23 12:16:01 +00:00
2014-03-13 08:18:00 +00:00
class EmbedVideoException(Exception):
2014-03-13 08:39:53 +00:00
""" Parental class for all embed_video exceptions """
2013-08-23 10:52:10 +00:00
pass
2014-03-13 08:18:00 +00:00
class VideoDoesntExistException(EmbedVideoException):
2014-03-13 08:39:53 +00:00
""" Exception thrown if video doesn't exist """
2013-06-01 12:04:55 +00:00
pass
2014-03-13 08:18:00 +00:00
class UnknownBackendException(EmbedVideoException):
2014-03-13 08:39:53 +00:00
""" Exception thrown if video backend is not recognized. """
2014-03-13 08:18:00 +00:00
pass
class UnknownIdException(EmbedVideoException):
2014-03-13 08:39:53 +00:00
"""
Exception thrown if backend is detected, but video ID cannot be parsed.
"""
2013-06-18 22:30:53 +00:00
pass
2013-06-01 12:04:55 +00:00
2013-06-01 12:04:55 +00:00
def detect_backend(url):
2013-08-22 08:17:59 +00:00
"""
Detect the right backend for given URL.
2014-03-13 08:39:53 +00:00
Goes over backends in ``settings.EMBED_VIDEO_BACKENDS``,
calls :py:func:`~VideoBackend.is_valid` and returns backend instance.
2013-08-22 08:17:59 +00:00
"""
2013-08-22 13:44:27 +00:00
for backend_name in EMBED_VIDEO_BACKENDS:
backend = import_by_path(backend_name)
if backend.is_valid(url):
2013-08-22 13:44:27 +00:00
return backend(url)
raise UnknownBackendException
2013-06-01 12:04:55 +00:00
class VideoBackend(object):
2013-08-22 08:17:59 +00:00
"""
2014-03-13 08:39:53 +00:00
Base class used as parental class for backends.
.. code-block:: python
class MyBackend(VideoBackend):
...
2013-08-22 08:17:59 +00:00
"""
re_code = None
"""
Compiled regex (:py:func:`re.compile`) to search code in URL.
Example: ``re.compile(r'myvideo\.com/\?code=(?P<code>\w+)')``
"""
2013-08-22 13:44:27 +00:00
re_detect = None
"""
Compilede regec (:py:func:`re.compile`) to detect, if input URL is valid
for current backend.
Example: ``re.compile(r'^http://myvideo\.com/.*')``
"""
2013-08-22 08:17:59 +00:00
pattern_url = None
"""
Pattern in which the code is inserted.
Example: ``http://myvideo.com?code=%s``
"""
pattern_thumbnail_url = None
"""
Pattern in which the code is inserted to get thumbnail url.
Example: ``http://static.myvideo.com/thumbs/%s``
"""
allow_https = True
"""
Sets if HTTPS version allowed for specific backend.
"""
2014-02-27 08:29:54 +00:00
template_name = 'embed_video/embed_code.html'
"""
Name of embed code template used by :py:meth:`get_embed_code`.
Passed template variables: ``{{ backend }}`` (instance of VideoBackend),
``{{ width }}``, ``{{ height }}``
2014-02-27 08:29:54 +00:00
"""
def __init__(self, url, is_secure=False, query=None):
2013-08-23 12:16:01 +00:00
"""
First it tries to load data from cache and if it don't succeed, run
:py:meth:`init` and then save it to cache.
"""
2013-12-23 08:50:03 +00:00
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 = SortedDict(self.get_default_query())
if query is not None:
self._query.update(query)
2013-08-23 12:16:01 +00:00
@cached_property
def code(self):
return self.get_code()
2013-06-01 12:04:55 +00:00
@cached_property
def url(self):
return self.get_url()
2013-08-23 12:16:01 +00:00
@cached_property
def protocol(self):
return 'https' if self.allow_https and self.is_secure else 'http'
@cached_property
def thumbnail(self):
return self.get_thumbnail_url()
@cached_property
def info(self):
return self.get_info()
2013-06-01 12:04:55 +00:00
@classmethod
2013-12-23 08:50:03 +00:00
def is_valid(cls, url):
"""
Class method to control if passed url is valid for current backend. By
default it is done by :py:data:`re_detect` regex.
"""
2013-12-23 08:50:03 +00:00
return True if cls.re_detect.match(url) else False
2013-06-01 12:04:55 +00:00
def get_code(self):
2014-03-13 08:39:53 +00:00
"""
Returns video code matched from given url by :py:data:`re_code`.
"""
2013-06-01 12:04:55 +00:00
match = self.re_code.search(self._url)
if match:
return match.group('code')
def get_url(self):
2013-08-22 08:17:59 +00:00
"""
Returns URL folded from :py:data:`pattern_url` and parsed code.
"""
url = self.pattern_url.format(code=self.code, protocol=self.protocol)
if self._query:
url += '?' + urlencode(self._query, doseq=True)
return mark_safe(url)
2013-06-01 12:04:55 +00:00
def get_thumbnail_url(self):
"""
Returns thumbnail URL folded from :py:data:`pattern_thumbnail_url` and
parsed code.
"""
return self.pattern_thumbnail_url.format(code=self.code,
protocol=self.protocol)
2013-06-01 12:04:55 +00:00
def get_embed_code(self, width, height):
"""
Returns embed code rendered from template :py:data:`template_name`.
"""
2014-02-27 08:29:54 +00:00
return render_to_string(self.template_name, {
'backend': self,
'width': width,
'height': height,
})
2013-12-23 08:50:03 +00:00
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()
2013-06-01 12:04:55 +00:00
class YoutubeBackend(VideoBackend):
2013-08-22 08:17:59 +00:00
"""
Backend for YouTube URLs.
"""
2013-08-22 13:44:27 +00:00
re_detect = re.compile(
r'^(http(s)?://)?(www\.)?youtu(\.?)be(\.com)?/.*', re.I
)
re_code = re.compile(
r'''youtu(\.?)be(\.com)?/ # match youtube's domains
(embed/)? # match the embed url syntax
(v/)?
(watch\?v=)? # match the youtube page url
(ytscreeningroom\?v=)?
(feeds/api/videos/)?
(user\S*[^\w\-\s])?
(?P<code>[\w\-]{11})[a-z0-9;:@?&%=+/\$_.-]* # match and extract
''',
re.I | re.X
)
2013-08-22 13:44:27 +00:00
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()
if not code:
parse_data = urlparse.urlparse(self._url)
try:
code = urlparse.parse_qs(parse_data.query)['v'][0]
except KeyError:
raise UnknownIdException
return code
class VimeoBackend(VideoBackend):
2013-08-22 08:17:59 +00:00
"""
Backend for Vimeo URLs.
"""
2013-08-22 13:44:27 +00:00
re_detect = re.compile(
r'^((http(s)?:)?//)?(www\.)?(player\.)?vimeo\.com/.*', re.I
2013-08-22 13:44:27 +00:00
)
re_code = re.compile(r'''vimeo\.com/(video/)?(?P<code>[0-9]+)''', re.I)
pattern_url = '{protocol}://player.vimeo.com/video/{code}'
pattern_info = '{protocol}://vimeo.com/api/v2/video/{code}.json'
2013-08-23 10:52:10 +00:00
def get_info(self):
try:
response = requests.get(
2014-02-21 22:21:00 +00:00
self.pattern_info.format(code=self.code, protocol=self.protocol),
timeout=EMBED_VIDEO_TIMEOUT
)
2013-08-23 10:52:10 +00:00
return json.loads(response.text)[0]
except ValueError:
raise VideoDoesntExistException()
def get_thumbnail_url(self):
2013-10-04 08:53:16 +00:00
return self.info.get('thumbnail_large')
class SoundCloudBackend(VideoBackend):
2013-08-22 08:17:59 +00:00
"""
Backend for SoundCloud URLs.
"""
base_url = 'http://soundcloud.com/oembed'
2013-08-22 13:44:27 +00:00
re_detect = re.compile(r'^(http(s)?://(www\.)?)?soundcloud\.com/.*', re.I)
re_code = re.compile(r'src=".*%2F(?P<code>\d+)&show_artwork.*"', re.I)
re_url = re.compile(r'src="(?P<url>.*?)"', re.I)
@cached_property
def width(self):
return self.info.get('width')
@cached_property
def height(self):
return self.info.get('height')
def get_info(self):
2013-06-25 00:03:35 +00:00
params = {
'format': 'json',
'url': self._url,
2013-06-25 00:03:35 +00:00
}
2014-02-21 22:21:00 +00:00
r = requests.get(self.base_url, data=params,
timeout=EMBED_VIDEO_TIMEOUT)
2014-02-22 09:04:41 +00:00
if r.status_code != 200:
raise VideoDoesntExistException(
'SoundCloud returned status code `{0}`.'.format(r.status_code)
)
return json.loads(r.text)
2013-06-25 00:03:35 +00:00
def get_thumbnail_url(self):
return self.info.get('thumbnail_url')
2013-06-25 00:03:35 +00:00
def get_url(self):
match = self.re_url.search(self.info.get('html'))
return match.group('url')
2013-06-25 00:03:35 +00:00
def get_code(self):
match = self.re_code.search(self.info.get('html'))
return match.group('code')
def get_embed_code(self, width, height):
2013-10-04 07:45:01 +00:00
return super(SoundCloudBackend, self). \
2013-12-23 08:50:03 +00:00
get_embed_code(width=width, height=self.height)