138 lines
6.4 KiB
Python
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)
|