Files
edx-platform/cms/djangoapps/contentstore/video_utils.py
2021-10-25 12:59:54 +05:00

138 lines
6.4 KiB
Python

"""
Utils related to the videos.
"""
import logging
from urllib.parse import urljoin
import requests
from django.conf import settings
from django.core.files.images import get_image_dimensions
from django.core.files.uploadedfile import SimpleUploadedFile
from django.utils.translation import gettext as _
from edxval.api import get_course_video_image_url, update_video_image
# Youtube thumbnail sizes.
# https://img.youtube.com/vi/{youtube_id}/{thumbnail_quality}.jpg
# High Quality Thumbnail - hqdefault (480x360 pixels)
# Medium Quality Thumbnail - mqdefault (320x180 pixels)
# Normal Quality Thumbnail - default (120x90 pixels)
# And additionally, the next two thumbnails may or may not exist. For HQ videos they exist.
# Standard Definition Thumbnail - sddefault (640x480 pixels)
# Maximum Resolution Thumbnail - maxresdefault (1920x1080 pixels)
YOUTUBE_THUMBNAIL_SIZES = ['maxresdefault', 'sddefault', 'hqdefault', 'mqdefault', 'default']
LOGGER = logging.getLogger(__name__)
def validate_video_image(image_file, skip_aspect_ratio=False):
"""
Validates video image file.
Arguments:
image_file: The selected image file.
Returns:
error (String or None): If there is error returns error message otherwise None.
"""
error = None
if not all(hasattr(image_file, attr) for attr in ['name', 'content_type', 'size']):
error = _('The image must have name, content type, and size information.')
elif image_file.content_type not in list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.values()):
error = _('This image file type is not supported. Supported file types are {supported_file_formats}.').format(
supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys())
)
elif image_file.size > settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES']:
error = _('This image file must be smaller than {image_max_size}.').format(
image_max_size=settings.VIDEO_IMAGE_MAX_FILE_SIZE_MB
)
elif image_file.size < settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES']:
error = _('This image file must be larger than {image_min_size}.').format(
image_min_size=settings.VIDEO_IMAGE_MIN_FILE_SIZE_KB
)
else:
try:
image_file_width, image_file_height = get_image_dimensions(image_file)
except TypeError:
return _('There is a problem with this image file. Try to upload a different file.')
if image_file_width is None or image_file_height is None:
return _('There is a problem with this image file. Try to upload a different file.')
image_file_aspect_ratio = abs(image_file_width / float(image_file_height) - settings.VIDEO_IMAGE_ASPECT_RATIO)
if image_file_width < settings.VIDEO_IMAGE_MIN_WIDTH or image_file_height < settings.VIDEO_IMAGE_MIN_HEIGHT:
error = _('Recommended image resolution is {image_file_max_width}x{image_file_max_height}. '
'The minimum resolution is {image_file_min_width}x{image_file_min_height}.').format(
image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH,
image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT,
image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH,
image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT
)
elif not skip_aspect_ratio and image_file_aspect_ratio > settings.VIDEO_IMAGE_ASPECT_RATIO_ERROR_MARGIN:
error = _('This image file must have an aspect ratio of {video_image_aspect_ratio_text}.').format(
video_image_aspect_ratio_text=settings.VIDEO_IMAGE_ASPECT_RATIO_TEXT
)
else:
try:
image_file.name.encode('ascii')
except UnicodeEncodeError:
error = _('The image file name can only contain letters, numbers, hyphens (-), and underscores (_).')
return error
def download_youtube_video_thumbnail(youtube_id):
"""
Download highest resoultion video thumbnail available from youtube.
"""
thumbnail_content = thumbnail_content_type = None
# Download highest resolution thumbnail available.
for thumbnail_quality in YOUTUBE_THUMBNAIL_SIZES:
thumbnail_url = urljoin('https://img.youtube.com', '/vi/{youtube_id}/{thumbnail_quality}.jpg'.format(
youtube_id=youtube_id, thumbnail_quality=thumbnail_quality
))
response = requests.get(thumbnail_url)
if response.status_code == requests.codes.ok: # pylint: disable=no-member
thumbnail_content = response.content
thumbnail_content_type = response.headers['content-type']
# If best available resolution is found, don't look for lower resolutions.
break
return thumbnail_content, thumbnail_content_type
def validate_and_update_video_image(course_key_string, edx_video_id, image_file, image_filename):
"""
Validates image content and updates video image.
"""
error = validate_video_image(image_file, skip_aspect_ratio=True)
if error:
LOGGER.info(
'VIDEOS: Scraping youtube video thumbnail failed for edx_video_id [%s] in course [%s] with error: %s',
edx_video_id,
course_key_string,
error
)
return
update_video_image(edx_video_id, course_key_string, image_file, image_filename)
LOGGER.info(
'VIDEOS: Scraping youtube video thumbnail for edx_video_id [%s] in course [%s]', edx_video_id, course_key_string # lint-amnesty, pylint: disable=line-too-long
)
def scrape_youtube_thumbnail(course_id, edx_video_id, youtube_id):
"""
Scrapes youtube thumbnail for a given video.
"""
# Scrape when course video image does not exist for edx_video_id.
if not get_course_video_image_url(course_id, edx_video_id):
thumbnail_content, thumbnail_content_type = download_youtube_video_thumbnail(youtube_id)
supported_content_types = {v: k for k, v in settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.items()}
image_filename = '{youtube_id}{image_extention}'.format(
youtube_id=youtube_id,
image_extention=supported_content_types.get(
thumbnail_content_type, supported_content_types['image/jpeg']
)
)
image_file = SimpleUploadedFile(image_filename, thumbnail_content, thumbnail_content_type)
validate_and_update_video_image(course_id, edx_video_id, image_file, image_filename)