mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-16 02:02:58 -07:00
Update cloudinary to 1.20.0
This commit is contained in:
parent
1c56d9c513
commit
2984629b39
27 changed files with 2865 additions and 923 deletions
|
@ -1,29 +1,43 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from copy import deepcopy
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import numbers
|
||||
import certifi
|
||||
from math import ceil
|
||||
from six import python_2_unicode_compatible
|
||||
|
||||
logger = logging.getLogger("Cloudinary")
|
||||
ch = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from six import python_2_unicode_compatible
|
||||
|
||||
from cloudinary import utils
|
||||
from cloudinary.exceptions import GeneralError
|
||||
from cloudinary.cache import responsive_breakpoints_cache
|
||||
from cloudinary.http_client import HttpClient
|
||||
from cloudinary.compat import urlparse, parse_qs
|
||||
from cloudinary.search import Search
|
||||
|
||||
from platform import python_version
|
||||
|
||||
CERT_KWARGS = {
|
||||
'cert_reqs': 'CERT_REQUIRED',
|
||||
'ca_certs': certifi.where(),
|
||||
}
|
||||
|
||||
CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
|
||||
OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"
|
||||
AKAMAI_SHARED_CDN = "res.cloudinary.com"
|
||||
SHARED_CDN = AKAMAI_SHARED_CDN
|
||||
CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
URI_SCHEME = "cloudinary"
|
||||
|
||||
VERSION = "1.11.0"
|
||||
USER_AGENT = "CloudinaryPython/" + VERSION
|
||||
VERSION = "1.20.0"
|
||||
|
||||
USER_AGENT = "CloudinaryPython/{} (Python {})".format(VERSION, python_version())
|
||||
""" :const: USER_AGENT """
|
||||
|
||||
USER_PLATFORM = ""
|
||||
|
@ -39,7 +53,8 @@ The format of the value should be <ProductName>/Version[ (comment)].
|
|||
|
||||
|
||||
def get_user_agent():
|
||||
"""Provides the `USER_AGENT` string that is passed to the Cloudinary servers.
|
||||
"""
|
||||
Provides the `USER_AGENT` string that is passed to the Cloudinary servers.
|
||||
Prepends `USER_PLATFORM` if it is defined.
|
||||
|
||||
:returns: the user agent
|
||||
|
@ -54,15 +69,27 @@ def get_user_agent():
|
|||
|
||||
def import_django_settings():
|
||||
try:
|
||||
import django.conf
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
try:
|
||||
if 'CLOUDINARY' in dir(django.conf.settings):
|
||||
return django.conf.settings.CLOUDINARY
|
||||
from django.conf import settings as _django_settings
|
||||
|
||||
# We can get a situation when Django module is installed in the system, but not initialized,
|
||||
# which means we are running not in a Django process.
|
||||
# In this case the following line throws ImproperlyConfigured exception
|
||||
if 'cloudinary' in _django_settings.INSTALLED_APPS:
|
||||
from django import get_version as _get_django_version
|
||||
global USER_PLATFORM
|
||||
USER_PLATFORM = "Django/{django_version}".format(django_version=_get_django_version())
|
||||
|
||||
if 'CLOUDINARY' in dir(_django_settings):
|
||||
return _django_settings.CLOUDINARY
|
||||
else:
|
||||
return None
|
||||
|
||||
except ImproperlyConfigured:
|
||||
return None
|
||||
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
@ -78,14 +105,18 @@ class Config(object):
|
|||
api_key=os.environ.get("CLOUDINARY_API_KEY"),
|
||||
api_secret=os.environ.get("CLOUDINARY_API_SECRET"),
|
||||
secure_distribution=os.environ.get("CLOUDINARY_SECURE_DISTRIBUTION"),
|
||||
private_cdn=os.environ.get("CLOUDINARY_PRIVATE_CDN") == 'true'
|
||||
private_cdn=os.environ.get("CLOUDINARY_PRIVATE_CDN") == 'true',
|
||||
api_proxy=os.environ.get("CLOUDINARY_API_PROXY"),
|
||||
)
|
||||
elif os.environ.get("CLOUDINARY_URL"):
|
||||
cloudinary_url = os.environ.get("CLOUDINARY_URL")
|
||||
self._parse_cloudinary_url(cloudinary_url)
|
||||
|
||||
def _parse_cloudinary_url(self, cloudinary_url):
|
||||
uri = urlparse(cloudinary_url.replace("cloudinary://", "http://"))
|
||||
uri = urlparse(cloudinary_url)
|
||||
if not self._is_url_scheme_valid(uri):
|
||||
raise ValueError("Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'")
|
||||
|
||||
for k, v in parse_qs(uri.query).items():
|
||||
if self._is_nested_key(k):
|
||||
self._put_nested_key(k, v)
|
||||
|
@ -115,7 +146,7 @@ class Config(object):
|
|||
|
||||
def _put_nested_key(self, key, value):
|
||||
chain = re.split(r'[\[\]]+', key)
|
||||
chain = [key for key in chain if key]
|
||||
chain = [k for k in chain if k]
|
||||
outer = self.__dict__
|
||||
last_key = chain.pop()
|
||||
for inner_key in chain:
|
||||
|
@ -128,7 +159,21 @@ class Config(object):
|
|||
if isinstance(value, list):
|
||||
value = value[0]
|
||||
outer[last_key] = value
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _is_url_scheme_valid(url):
|
||||
"""
|
||||
Helper function. Validates url scheme
|
||||
|
||||
:param url: A named tuple containing URL components
|
||||
|
||||
:return: bool True on success or False on failure
|
||||
"""
|
||||
if not url.scheme or url.scheme.lower() != URI_SCHEME:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
_config = Config()
|
||||
|
||||
|
||||
|
@ -143,8 +188,35 @@ def reset_config():
|
|||
_config = Config()
|
||||
|
||||
|
||||
_http_client = HttpClient()
|
||||
|
||||
# FIXME: circular import issue
|
||||
from cloudinary.search import Search
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CloudinaryResource(object):
|
||||
"""
|
||||
Recommended sources for video tag
|
||||
"""
|
||||
default_video_sources = [
|
||||
{
|
||||
"type": "mp4",
|
||||
"codecs": "hev1",
|
||||
"transformations": {"video_codec": "h265"}
|
||||
}, {
|
||||
"type": "webm",
|
||||
"codecs": "vp9",
|
||||
"transformations": {"video_codec": "vp9"}
|
||||
}, {
|
||||
"type": "mp4",
|
||||
"transformations": {"video_codec": "auto"}
|
||||
}, {
|
||||
"type": "webm",
|
||||
"transformations": {"video_codec": "auto"}
|
||||
},
|
||||
]
|
||||
|
||||
def __init__(self, public_id=None, format=None, version=None,
|
||||
signature=None, url_options=None, metadata=None, type=None, resource_type=None,
|
||||
default_resource_type=None):
|
||||
|
@ -174,9 +246,11 @@ class CloudinaryResource(object):
|
|||
return None
|
||||
prep = ''
|
||||
prep = prep + self.resource_type + '/' + self.type + '/'
|
||||
if self.version: prep = prep + 'v' + str(self.version) + '/'
|
||||
if self.version:
|
||||
prep = prep + 'v' + str(self.version) + '/'
|
||||
prep = prep + self.public_id
|
||||
if self.format: prep = prep + '.' + self.format
|
||||
if self.format:
|
||||
prep = prep + '.' + self.format
|
||||
return prep
|
||||
|
||||
def get_presigned(self):
|
||||
|
@ -199,28 +273,283 @@ class CloudinaryResource(object):
|
|||
def build_url(self, **options):
|
||||
return self.__build_url(**options)[0]
|
||||
|
||||
def default_poster_options(self, options):
|
||||
@staticmethod
|
||||
def default_poster_options(options):
|
||||
options["format"] = options.get("format", "jpg")
|
||||
|
||||
def default_source_types(self):
|
||||
@staticmethod
|
||||
def default_source_types():
|
||||
return ['webm', 'mp4', 'ogv']
|
||||
|
||||
@staticmethod
|
||||
def _validate_srcset_data(srcset_data):
|
||||
"""
|
||||
Helper function. Validates srcset_data parameters
|
||||
|
||||
:param srcset_data: A dictionary containing the following keys:
|
||||
breakpoints A list of breakpoints.
|
||||
min_width Minimal width of the srcset images
|
||||
max_width Maximal width of the srcset images.
|
||||
max_images Number of srcset images to generate.
|
||||
|
||||
:return: bool True on success or False on failure
|
||||
"""
|
||||
if not all(k in srcset_data and isinstance(srcset_data[k], numbers.Number) for k in ("min_width", "max_width",
|
||||
"max_images")):
|
||||
logger.warning("Either valid (min_width, max_width, max_images)" +
|
||||
"or breakpoints must be provided to the image srcset attribute")
|
||||
return False
|
||||
|
||||
if srcset_data["min_width"] > srcset_data["max_width"]:
|
||||
logger.warning("min_width must be less than max_width")
|
||||
return False
|
||||
|
||||
if srcset_data["max_images"] <= 0:
|
||||
logger.warning("max_images must be a positive integer")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _generate_breakpoints(self, srcset_data):
|
||||
"""
|
||||
Helper function. Calculates static responsive breakpoints using provided parameters.
|
||||
|
||||
Either the breakpoints or min_width, max_width, max_images must be provided.
|
||||
|
||||
:param srcset_data: A dictionary containing the following keys:
|
||||
breakpoints A list of breakpoints.
|
||||
min_width Minimal width of the srcset images
|
||||
max_width Maximal width of the srcset images.
|
||||
max_images Number of srcset images to generate.
|
||||
|
||||
:return: A list of breakpoints
|
||||
|
||||
:raises ValueError: In case of invalid or missing parameters
|
||||
"""
|
||||
breakpoints = srcset_data.get("breakpoints", list())
|
||||
|
||||
if breakpoints:
|
||||
return breakpoints
|
||||
|
||||
if not self._validate_srcset_data(srcset_data):
|
||||
return None
|
||||
|
||||
min_width, max_width, max_images = srcset_data["min_width"], srcset_data["max_width"], srcset_data["max_images"]
|
||||
|
||||
if max_images == 1:
|
||||
# if user requested only 1 image in srcset, we return max_width one
|
||||
min_width = max_width
|
||||
|
||||
step_size = int(ceil(float(max_width - min_width) / (max_images - 1 if max_images > 1 else 1)))
|
||||
|
||||
curr_breakpoint = min_width
|
||||
|
||||
while curr_breakpoint < max_width:
|
||||
breakpoints.append(curr_breakpoint)
|
||||
curr_breakpoint += step_size
|
||||
|
||||
breakpoints.append(max_width)
|
||||
|
||||
return breakpoints
|
||||
|
||||
def _fetch_breakpoints(self, srcset_data=None, **options):
|
||||
"""
|
||||
Helper function. Retrieves responsive breakpoints list from cloudinary server
|
||||
|
||||
When passing special string to transformation `width` parameter of form `auto:breakpoints{parameters}:json`,
|
||||
the response contains JSON with data of the responsive breakpoints
|
||||
|
||||
:param srcset_data: A dictionary containing the following keys:
|
||||
min_width Minimal width of the srcset images
|
||||
max_width Maximal width of the srcset images
|
||||
bytes_step Minimal bytes step between images
|
||||
max_images Number of srcset images to generate
|
||||
:param options: Additional options
|
||||
|
||||
:return: Resulting breakpoints
|
||||
"""
|
||||
if srcset_data is None:
|
||||
srcset_data = dict()
|
||||
|
||||
min_width = srcset_data.get("min_width", 50)
|
||||
max_width = srcset_data.get("max_width", 1000)
|
||||
bytes_step = srcset_data.get("bytes_step", 20000)
|
||||
max_images = srcset_data.get("max_images", 20)
|
||||
transformation = srcset_data.get("transformation")
|
||||
|
||||
kbytes_step = int(ceil(float(bytes_step)/1024))
|
||||
|
||||
breakpoints_width_param = "auto:breakpoints_{min_width}_{max_width}_{kbytes_step}_{max_images}:json".format(
|
||||
min_width=min_width, max_width=max_width, kbytes_step=kbytes_step, max_images=max_images)
|
||||
breakpoints_url = utils.cloudinary_scaled_url(self.public_id, breakpoints_width_param, transformation, options)
|
||||
|
||||
return _http_client.get_json(breakpoints_url).get("breakpoints", None)
|
||||
|
||||
def _get_or_generate_breakpoints(self, srcset_data, **options):
|
||||
"""
|
||||
Helper function. Gets from cache or calculates srcset breakpoints using provided parameters
|
||||
|
||||
:param srcset_data: A dictionary containing the following keys:
|
||||
breakpoints A list of breakpoints.
|
||||
min_width Minimal width of the srcset images
|
||||
max_width Maximal width of the srcset images
|
||||
max_images Number of srcset images to generate
|
||||
:param options: Additional options
|
||||
|
||||
:return: Resulting breakpoints
|
||||
"""
|
||||
|
||||
breakpoints = srcset_data.get("breakpoints")
|
||||
|
||||
if breakpoints:
|
||||
return breakpoints
|
||||
|
||||
if srcset_data.get("use_cache"):
|
||||
breakpoints = responsive_breakpoints_cache.instance.get(self.public_id, **options)
|
||||
if not breakpoints:
|
||||
try:
|
||||
breakpoints = self._fetch_breakpoints(srcset_data, **options)
|
||||
except GeneralError as e:
|
||||
logger.warning("Failed getting responsive breakpoints: {error}".format(error=e.message))
|
||||
|
||||
if breakpoints:
|
||||
responsive_breakpoints_cache.instance.set(self.public_id, breakpoints, **options)
|
||||
|
||||
if not breakpoints:
|
||||
# Static calculation if cache is not enabled or we failed to fetch breakpoints
|
||||
breakpoints = self._generate_breakpoints(srcset_data)
|
||||
|
||||
return breakpoints
|
||||
|
||||
def _generate_srcset_attribute(self, breakpoints, transformation=None, **options):
|
||||
"""
|
||||
Helper function. Generates srcset attribute value of the HTML img tag.
|
||||
|
||||
:param breakpoints: A list of breakpoints.
|
||||
:param transformation: Custom transformation
|
||||
:param options: Additional options
|
||||
|
||||
:return: Resulting srcset attribute value
|
||||
|
||||
:raises ValueError: In case of invalid or missing parameters
|
||||
"""
|
||||
if not breakpoints:
|
||||
return None
|
||||
|
||||
if transformation is None:
|
||||
transformation = dict()
|
||||
|
||||
return ", ".join(["{0} {1}w".format(utils.cloudinary_scaled_url(
|
||||
self.public_id, w, transformation, options), w) for w in breakpoints])
|
||||
|
||||
@staticmethod
|
||||
def _generate_sizes_attribute(breakpoints):
|
||||
"""
|
||||
Helper function. Generates sizes attribute value of the HTML img tag.
|
||||
|
||||
:param breakpoints: A list of breakpoints.
|
||||
|
||||
:return: Resulting 'sizes' attribute value
|
||||
"""
|
||||
if not breakpoints:
|
||||
return None
|
||||
|
||||
return ", ".join("(max-width: {bp}px) {bp}px".format(bp=bp) for bp in breakpoints)
|
||||
|
||||
def _generate_image_responsive_attributes(self, attributes, srcset_data, **options):
|
||||
"""
|
||||
Helper function. Generates srcset and sizes attributes of the image tag
|
||||
|
||||
Create both srcset and sizes here to avoid fetching breakpoints twice
|
||||
|
||||
:param attributes: Existing attributes
|
||||
:param srcset_data: A dictionary containing the following keys:
|
||||
breakpoints A list of breakpoints.
|
||||
min_width Minimal width of the srcset images
|
||||
max_width Maximal width of the srcset images.
|
||||
max_images Number of srcset images to generate.
|
||||
:param options: Additional options
|
||||
|
||||
:return: The responsive attributes
|
||||
"""
|
||||
responsive_attributes = dict()
|
||||
|
||||
if not srcset_data:
|
||||
return responsive_attributes
|
||||
|
||||
breakpoints = None
|
||||
|
||||
if "srcset" not in attributes:
|
||||
breakpoints = self._get_or_generate_breakpoints(srcset_data, **options)
|
||||
transformation = srcset_data.get("transformation")
|
||||
srcset_attr = self._generate_srcset_attribute(breakpoints, transformation, **options)
|
||||
if srcset_attr:
|
||||
responsive_attributes["srcset"] = srcset_attr
|
||||
|
||||
if "sizes" not in attributes and srcset_data.get("sizes") is True:
|
||||
if not breakpoints:
|
||||
breakpoints = self._get_or_generate_breakpoints(srcset_data, **options)
|
||||
sizes_attr = self._generate_sizes_attribute(breakpoints)
|
||||
if sizes_attr:
|
||||
responsive_attributes["sizes"] = sizes_attr
|
||||
|
||||
return responsive_attributes
|
||||
|
||||
def image(self, **options):
|
||||
"""
|
||||
Generates HTML img tag
|
||||
|
||||
:param options: Additional options
|
||||
|
||||
:return: Resulting img tag
|
||||
"""
|
||||
if options.get("resource_type", self.resource_type) == "video":
|
||||
self.default_poster_options(options)
|
||||
|
||||
custom_attributes = options.pop("attributes", dict())
|
||||
|
||||
srcset_option = options.pop("srcset", dict())
|
||||
srcset_data = dict()
|
||||
|
||||
if isinstance(srcset_option, dict):
|
||||
srcset_data = config().srcset or dict()
|
||||
srcset_data = srcset_data.copy()
|
||||
srcset_data.update(srcset_option)
|
||||
else:
|
||||
if "srcset" not in custom_attributes:
|
||||
custom_attributes["srcset"] = srcset_option
|
||||
|
||||
src, attrs = self.__build_url(**options)
|
||||
|
||||
client_hints = attrs.pop("client_hints", config().client_hints)
|
||||
responsive = attrs.pop("responsive", False)
|
||||
hidpi = attrs.pop("hidpi", False)
|
||||
|
||||
if (responsive or hidpi) and not client_hints:
|
||||
attrs["data-src"] = src
|
||||
classes = "cld-responsive" if responsive else "cld-hidpi"
|
||||
if "class" in attrs: classes += " " + attrs["class"]
|
||||
attrs["class"] = classes
|
||||
src = attrs.pop("responsive_placeholder", config().responsive_placeholder)
|
||||
if src == "blank": src = CL_BLANK
|
||||
|
||||
if src: attrs["src"] = src
|
||||
classes = "cld-responsive" if responsive else "cld-hidpi"
|
||||
if "class" in attrs:
|
||||
classes += " " + attrs["class"]
|
||||
attrs["class"] = classes
|
||||
|
||||
src = attrs.pop("responsive_placeholder", config().responsive_placeholder)
|
||||
if src == "blank":
|
||||
src = CL_BLANK
|
||||
|
||||
responsive_attrs = self._generate_image_responsive_attributes(custom_attributes, srcset_data, **options)
|
||||
|
||||
if responsive_attrs:
|
||||
# width and height attributes override srcset behavior, they should be removed from html attributes.
|
||||
for key in {"width", "height"}:
|
||||
attrs.pop(key, None)
|
||||
|
||||
attrs.update(responsive_attrs)
|
||||
# Explicitly provided attributes override options
|
||||
attrs.update(custom_attributes)
|
||||
|
||||
if src:
|
||||
attrs["src"] = src
|
||||
|
||||
return u"<img {0}/>".format(utils.html_attrs(attrs))
|
||||
|
||||
|
@ -228,69 +557,231 @@ class CloudinaryResource(object):
|
|||
self.default_poster_options(options)
|
||||
return self.build_url(**options)
|
||||
|
||||
# Creates an HTML video tag for the provided +source+
|
||||
#
|
||||
# ==== Options
|
||||
# * <tt>source_types</tt> - Specify which source type the tag should include. defaults to webm, mp4 and ogv.
|
||||
# * <tt>source_transformation</tt> - specific transformations to use for a specific source type.
|
||||
# * <tt>poster</tt> - override default thumbnail:
|
||||
# * url: provide an ad hoc url
|
||||
# * options: with specific poster transformations and/or Cloudinary +:public_id+
|
||||
#
|
||||
# ==== Examples
|
||||
# CloudinaryResource("mymovie.mp4").video()
|
||||
# CloudinaryResource("mymovie.mp4").video(source_types = 'webm')
|
||||
# CloudinaryResource("mymovie.ogv").video(poster = "myspecialplaceholder.jpg")
|
||||
# CloudinaryResource("mymovie.webm").video(source_types = ['webm', 'mp4'], poster = {'effect': 'sepia'})
|
||||
def video(self, **options):
|
||||
public_id = options.get('public_id', self.public_id)
|
||||
source = re.sub("\.({0})$".format("|".join(self.default_source_types())), '', public_id)
|
||||
@staticmethod
|
||||
def _video_mime_type(video_type, codecs=None):
|
||||
"""
|
||||
Helper function for video(), generates video MIME type string from video_type and codecs.
|
||||
Example: video/mp4; codecs=mp4a.40.2
|
||||
|
||||
:param video_type: mp4, webm, ogg etc.
|
||||
:param codecs: List or string of codecs. E.g.: "avc1.42E01E" or "avc1.42E01E, mp4a.40.2" or
|
||||
["avc1.42E01E", "mp4a.40.2"]
|
||||
|
||||
:return: Resulting mime type
|
||||
"""
|
||||
|
||||
video_type = 'ogg' if video_type == 'ogv' else video_type
|
||||
|
||||
if not video_type:
|
||||
return ""
|
||||
|
||||
codecs_str = ", ".join(codecs) if isinstance(codecs, (list, tuple)) else codecs
|
||||
codecs_attr = "; codecs={codecs_str}".format(codecs_str=codecs_str) if codecs_str else ""
|
||||
|
||||
return "video/{}{}".format(video_type, codecs_attr)
|
||||
|
||||
@staticmethod
|
||||
def _collect_video_tag_attributes(video_options):
|
||||
"""
|
||||
Helper function for video tag, collects remaining options and returns them as attributes
|
||||
|
||||
:param video_options: Remaining options
|
||||
|
||||
:return: Resulting attributes
|
||||
"""
|
||||
attributes = video_options.copy()
|
||||
|
||||
if 'html_width' in attributes:
|
||||
attributes['width'] = attributes.pop('html_width')
|
||||
if 'html_height' in attributes:
|
||||
attributes['height'] = attributes.pop('html_height')
|
||||
|
||||
if "poster" in attributes and not attributes["poster"]:
|
||||
attributes.pop("poster", None)
|
||||
|
||||
return attributes
|
||||
|
||||
def _generate_video_poster_attr(self, source, video_options):
|
||||
"""
|
||||
Helper function for video tag, generates video poster URL
|
||||
|
||||
:param source: The public ID of the resource
|
||||
:param video_options: Additional options
|
||||
|
||||
:return: Resulting video poster URL
|
||||
"""
|
||||
if 'poster' not in video_options:
|
||||
return self.video_thumbnail(public_id=source, **video_options)
|
||||
|
||||
poster_options = video_options['poster']
|
||||
|
||||
if not isinstance(poster_options, dict):
|
||||
return poster_options
|
||||
|
||||
if 'public_id' not in poster_options:
|
||||
return self.video_thumbnail(public_id=source, **poster_options)
|
||||
|
||||
return utils.cloudinary_url(poster_options['public_id'], **poster_options)[0]
|
||||
|
||||
def _populate_video_source_tags(self, source, options):
|
||||
"""
|
||||
Helper function for video tag, populates source tags from provided options.
|
||||
|
||||
source_types and sources are mutually exclusive, only one of them can be used.
|
||||
If both are not provided, source types are used (for backwards compatibility)
|
||||
|
||||
:param source: The public ID of the video
|
||||
:param options: Additional options
|
||||
|
||||
:return: Resulting source tags (may be empty)
|
||||
"""
|
||||
source_tags = []
|
||||
|
||||
# Consume all relevant options, otherwise they are left and passed as attributes
|
||||
video_sources = options.pop('sources', [])
|
||||
source_types = options.pop('source_types', [])
|
||||
source_transformation = options.pop('source_transformation', {})
|
||||
|
||||
if video_sources and isinstance(video_sources, list):
|
||||
# processing new source structure with codecs
|
||||
for source_data in video_sources:
|
||||
transformation = options.copy()
|
||||
transformation.update(source_data.get("transformations", {}))
|
||||
source_type = source_data.get("type", '')
|
||||
src = utils.cloudinary_url(source, format=source_type, **transformation)[0]
|
||||
codecs = source_data.get("codecs", [])
|
||||
source_tags.append("<source {attributes}>".format(
|
||||
attributes=utils.html_attrs({'src': src, 'type': self._video_mime_type(source_type, codecs)})))
|
||||
|
||||
return source_tags
|
||||
|
||||
# processing old source_types structure with out codecs
|
||||
if not source_types:
|
||||
source_types = self.default_source_types()
|
||||
|
||||
if not isinstance(source_types, (list, tuple)):
|
||||
return source_tags
|
||||
|
||||
for source_type in source_types:
|
||||
transformation = options.copy()
|
||||
transformation.update(source_transformation.get(source_type, {}))
|
||||
src = utils.cloudinary_url(source, format=source_type, **transformation)[0]
|
||||
source_tags.append("<source {attributes}>".format(
|
||||
attributes=utils.html_attrs({'src': src, 'type': self._video_mime_type(source_type)})))
|
||||
|
||||
return source_tags
|
||||
|
||||
def video(self, **options):
|
||||
"""
|
||||
Creates an HTML video tag for the provided +source+
|
||||
|
||||
Examples:
|
||||
CloudinaryResource("mymovie.mp4").video()
|
||||
CloudinaryResource("mymovie.mp4").video(source_types = 'webm')
|
||||
CloudinaryResource("mymovie.ogv").video(poster = "myspecialplaceholder.jpg")
|
||||
CloudinaryResource("mymovie.webm").video(source_types = ['webm', 'mp4'], poster = {'effect': 'sepia'})
|
||||
|
||||
:param options:
|
||||
* <tt>source_types</tt> - Specify which source type the tag should include.
|
||||
defaults to webm, mp4 and ogv.
|
||||
* <tt>sources</tt> - Similar to source_types, but may contain codecs list.
|
||||
source_types and sources are mutually exclusive, only one of
|
||||
them can be used. If both are not provided, default source types
|
||||
are used.
|
||||
* <tt>source_transformation</tt> - specific transformations to use
|
||||
for a specific source type.
|
||||
* <tt>poster</tt> - override default thumbnail:
|
||||
* url: provide an ad hoc url
|
||||
* options: with specific poster transformations and/or Cloudinary +:public_id+
|
||||
|
||||
:return: Video tag
|
||||
"""
|
||||
public_id = options.get('public_id', self.public_id)
|
||||
source = re.sub(r"\.({0})$".format("|".join(self.default_source_types())), '', public_id)
|
||||
|
||||
custom_attributes = options.pop("attributes", dict())
|
||||
|
||||
fallback = options.pop('fallback_content', '')
|
||||
options['resource_type'] = options.pop('resource_type', self.resource_type or 'video')
|
||||
|
||||
if not source_types: source_types = self.default_source_types()
|
||||
video_options = options.copy()
|
||||
# Save source types for a single video source handling (it can be a single type)
|
||||
source_types = options.get('source_types', "")
|
||||
|
||||
if 'poster' in video_options:
|
||||
poster_options = video_options['poster']
|
||||
if isinstance(poster_options, dict):
|
||||
if 'public_id' in poster_options:
|
||||
video_options['poster'] = utils.cloudinary_url(poster_options['public_id'], **poster_options)[0]
|
||||
else:
|
||||
video_options['poster'] = self.video_thumbnail(public_id=source, **poster_options)
|
||||
else:
|
||||
video_options['poster'] = self.video_thumbnail(public_id=source, **options)
|
||||
poster_options = options.copy()
|
||||
if "poster" not in custom_attributes:
|
||||
options["poster"] = self._generate_video_poster_attr(source, poster_options)
|
||||
|
||||
if not video_options['poster']: del video_options['poster']
|
||||
if "resource_type" not in options:
|
||||
options["resource_type"] = self.resource_type or "video"
|
||||
|
||||
nested_source_types = isinstance(source_types, list) and len(source_types) > 1
|
||||
if not nested_source_types:
|
||||
# populate video source tags
|
||||
source_tags = self._populate_video_source_tags(source, options)
|
||||
|
||||
if not source_tags:
|
||||
source = source + '.' + utils.build_array(source_types)[0]
|
||||
|
||||
video_url = utils.cloudinary_url(source, **video_options)
|
||||
video_options = video_url[1]
|
||||
if not nested_source_types:
|
||||
video_options['src'] = video_url[0]
|
||||
if 'html_width' in video_options: video_options['width'] = video_options.pop('html_width')
|
||||
if 'html_height' in video_options: video_options['height'] = video_options.pop('html_height')
|
||||
video_url, video_options = utils.cloudinary_url(source, **options)
|
||||
|
||||
sources = ""
|
||||
if nested_source_types:
|
||||
for source_type in source_types:
|
||||
transformation = options.copy()
|
||||
transformation.update(source_transformation.get(source_type, {}))
|
||||
src = utils.cloudinary_url(source, format=source_type, **transformation)[0]
|
||||
video_type = "ogg" if source_type == 'ogv' else source_type
|
||||
mime_type = "video/" + video_type
|
||||
sources += "<source {attributes}>".format(attributes=utils.html_attrs({'src': src, 'type': mime_type}))
|
||||
if not source_tags:
|
||||
custom_attributes['src'] = video_url
|
||||
|
||||
attributes = self._collect_video_tag_attributes(video_options)
|
||||
attributes.update(custom_attributes)
|
||||
|
||||
sources_str = ''.join(str(x) for x in source_tags)
|
||||
html = "<video {attributes}>{sources}{fallback}</video>".format(
|
||||
attributes=utils.html_attrs(video_options), sources=sources, fallback=fallback)
|
||||
attributes=utils.html_attrs(attributes), sources=sources_str, fallback=fallback)
|
||||
|
||||
return html
|
||||
|
||||
@staticmethod
|
||||
def __generate_media_attr(**media_options):
|
||||
media_query_conditions = []
|
||||
if "min_width" in media_options:
|
||||
media_query_conditions.append("(min-width: {}px)".format(media_options["min_width"]))
|
||||
if "max_width" in media_options:
|
||||
media_query_conditions.append("(max-width: {}px)".format(media_options["max_width"]))
|
||||
|
||||
return " and ".join(media_query_conditions)
|
||||
|
||||
def source(self, **options):
|
||||
attrs = options.get("attributes") or {}
|
||||
|
||||
srcset_data = config().srcset or dict()
|
||||
srcset_data = srcset_data.copy()
|
||||
srcset_data.update(options.pop("srcset", dict()))
|
||||
|
||||
responsive_attrs = self._generate_image_responsive_attributes(attrs, srcset_data, **options)
|
||||
|
||||
attrs.update(responsive_attrs)
|
||||
|
||||
# `source` tag under `picture` tag uses `srcset` attribute for both `srcset` and `src` urls
|
||||
if "srcset" not in attrs:
|
||||
attrs["srcset"], _ = self.__build_url(**options)
|
||||
|
||||
if "media" not in attrs:
|
||||
media_attr = self.__generate_media_attr(**(options.get("media", {})))
|
||||
if media_attr:
|
||||
attrs["media"] = media_attr
|
||||
|
||||
return u"<source {0}>".format(utils.html_attrs(attrs))
|
||||
|
||||
def picture(self, **options):
|
||||
sub_tags = []
|
||||
sources = options.pop("sources") or list()
|
||||
for source in sources:
|
||||
curr_options = deepcopy(options)
|
||||
|
||||
if "transformation" in source:
|
||||
curr_options = utils.chain_transformations(curr_options, source["transformation"])
|
||||
|
||||
curr_options["media"] = dict((k, source[k]) for k in ['min_width', 'max_width'] if k in source)
|
||||
|
||||
sub_tags.append(self.source(**curr_options))
|
||||
|
||||
sub_tags.append(self.image(**options))
|
||||
|
||||
return u"<picture>{}</picture>".format("".join(sub_tags))
|
||||
|
||||
|
||||
class CloudinaryImage(CloudinaryResource):
|
||||
def __init__(self, public_id=None, **kwargs):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue