diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index e2e8ad5638ab2d34fd3b93a1dfc4fc67fb3f5786..4165a1b04de7657d870315245e4485d018bef4c5 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -726,23 +726,17 @@ function(VideoPlayer, i18n, moment, _) { if (!(_.isString(url))) { url = this.videos['1.0'] || ''; } - // Will hit the API URL iF YT key is defined in settings. - if (this.config.ytKey) { - return $.ajax({ - url: [this.config.ytMetadataUrl, '?id=', url, '&part=contentDetails&key=', this.config.ytKey].join(''), - timeout: this.config.ytTestTimeout, - success: _.isFunction(callback) ? callback : null, - error: function() { - console.warn( - 'YouTube API request failed - usually this means the YouTube API key is invalid. ' + - 'Some video metadata may be unavailable.' - ); - }, - notifyOnError: false - }); - } else { - return $.Deferred().reject().promise(); - } + // Will hit the API URL to get the youtube video metadata. + return $.ajax({ + url: [this.config.lmsRootURL, '/courses/yt_video_metadata', '?id=', url].join(''), + success: _.isFunction(callback) ? callback : null, + error: function() { + console.warn( + 'Unable to get youtube video metadata. Some video metadata may be unavailable.' + ); + }, + notifyOnError: false + }); } function youtubeId(speed) { diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 8385410bab7ace8566189a677a9b7c4bf01da36d..c6617d1a99ee2a8505a6349471e7adfae91e6d94 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -344,12 +344,6 @@ class VideoBlock( settings_service = self.runtime.service(self, 'settings') - yt_api_key = None - if settings_service: - xblock_settings = settings_service.get_settings_bucket(self) - if xblock_settings and 'YOUTUBE_API_KEY' in xblock_settings: - yt_api_key = xblock_settings['YOUTUBE_API_KEY'] - poster = None if edxval_api and self.edx_video_id: poster = edxval_api.get_course_video_image_url( @@ -401,8 +395,7 @@ class VideoBlock( 'transcriptLanguages': sorted_languages, 'ytTestTimeout': settings.YOUTUBE['TEST_TIMEOUT'], 'ytApiUrl': settings.YOUTUBE['API'], - 'ytMetadataUrl': settings.YOUTUBE['METADATA_URL'], - 'ytKey': yt_api_key, + 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.runtime.handler_url( self, 'transcript', 'translation/__lang__' diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index 70fb649e9e9506bbab3b56037fa69390617b9755..8f23b9c5003c07aacec4f98b74961d42ec536178 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -112,8 +112,7 @@ class TestVideoYouTube(TestVideo): 'transcriptLanguages': OrderedDict({'en': 'English', 'uk': u'УкраїнÑька'}), 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', - 'ytMetadataUrl': 'https://www.googleapis.com/youtube/v3/videos/', - 'ytKey': None, + 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), 'autohideHtml5': False, @@ -194,8 +193,7 @@ class TestVideoNonYouTube(TestVideo): 'transcriptLanguages': OrderedDict({'en': 'English'}), 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', - 'ytMetadataUrl': 'https://www.googleapis.com/youtube/v3/videos/', - 'ytKey': None, + 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), 'autohideHtml5': False, @@ -253,8 +251,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): 'transcriptLanguages': OrderedDict({'en': 'English'}), 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', - 'ytMetadataUrl': 'https://www.googleapis.com/youtube/v3/videos/', - 'ytKey': None, + 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), 'autohideHtml5': False, @@ -2212,8 +2209,7 @@ class TestVideoWithBumper(TestVideo): 'transcriptLanguages': OrderedDict({'en': 'English', 'uk': u'УкраїнÑька'}), 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', - 'ytMetadataUrl': 'https://www.googleapis.com/youtube/v3/videos/', - 'ytKey': None, + 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), 'autohideHtml5': False, @@ -2286,8 +2282,7 @@ class TestAutoAdvanceVideo(TestVideo): 'transcriptLanguages': OrderedDict({'en': 'English', 'uk': u'УкраїнÑька'}), 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', - 'ytMetadataUrl': 'https://www.googleapis.com/youtube/v3/videos/', - 'ytKey': None, + 'lmsRootURL': settings.LMS_ROOT_URL, 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index f9be6c3c15ff507862021b5a0e78a04ea6765214..f65a39d9f03f26e4ec7a5849f6165f5d7e708854 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -2,9 +2,12 @@ Courseware views functions """ from __future__ import absolute_import +from __future__ import division import json import logging +import requests +from requests.exceptions import Timeout, ConnectionError from collections import OrderedDict, namedtuple from datetime import datetime @@ -39,6 +42,9 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey from pytz import UTC from rest_framework import status +from rest_framework.decorators import api_view, throttle_classes +from rest_framework.response import Response +from rest_framework.throttling import UserRateThrottle from six import text_type from web_fragments.fragment import Fragment @@ -256,6 +262,55 @@ def courses(request): ) +class PerUserVideoMetadataThrottle(UserRateThrottle): + """ + setting rate limit for yt_video_metadata API + """ + rate = settings.RATE_LIMIT_FOR_VIDEO_METADATA_API + + +@ensure_csrf_cookie +@login_required +@api_view(['GET']) +@throttle_classes([PerUserVideoMetadataThrottle]) +def yt_video_metadata(request): + """ + Will hit the youtube API if the key is available in settings + :return: youtube video metadata + """ + response = {} + status_code = 500 + video_id = request.GET.get('id', None) + if settings.YOUTUBE_API_KEY and video_id: + yt_api_key = settings.YOUTUBE_API_KEY + yt_metadata_url = settings.YOUTUBE['METADATA_URL'] + yt_timeout = settings.YOUTUBE.get('TEST_TIMEOUT', 1500) / 1000 # converting milli seconds to seconds + payload = {'id': video_id, 'part': 'contentDetails', 'key': yt_api_key} + try: + res = requests.get(yt_metadata_url, params=payload, timeout=yt_timeout) + status_code = res.status_code + if res.status_code == 200: + try: + res = res.json() + if res.get('items', []): + response = res + else: + logging.warning(u'Unable to find the items in response. Following response ' + u'was received: {res}'.format(res=res.text)) + except ValueError: + logging.warning(u'Unable to decode response to json. Following response ' + u'was received: {res}'.format(res=res.text)) + else: + logging.warning(u'YouTube API request failed with status code={status} - ' + u'Error message is={message}'.format(status=status_code, message=res.text)) + except (Timeout, ConnectionError): + logging.warning(u'YouTube API request failed because of connection time out or connection error') + else: + logging.warning(u'YouTube API key or video id is None. Please make sure API key and video id is not None') + + return Response(response, status=status_code, content_type='application/json') + + @ensure_csrf_cookie @ensure_valid_course_key def jump_to_id(request, course_id, module_id): diff --git a/lms/envs/common.py b/lms/envs/common.py index 49f46fe3636917525d0a35c73f74015f7801edb4..6b5daca23f88860578694ffa3a9b78442dca7bfe 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3826,3 +3826,7 @@ SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = "" SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = "" SOCIAL_AUTH_SAML_SP_PRIVATE_KEY_DICT = {} SOCIAL_AUTH_SAML_SP_PUBLIC_CERT_DICT = {} + +######################### rate limit for yt_video_metadata api ############ + +RATE_LIMIT_FOR_VIDEO_METADATA_API = '10/minute' diff --git a/lms/envs/production.py b/lms/envs/production.py index ec09dc0f0696f5b3b30c291bbbce8295af2cb812..ac424609d11738ca6bc6a0c9a72b6a22fadfe33e 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -814,6 +814,7 @@ FACEBOOK_APP_ID = AUTH_TOKENS.get("FACEBOOK_APP_ID") XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) XBLOCK_SETTINGS.setdefault("VideoBlock", {})["licensing_enabled"] = FEATURES.get("LICENSING", False) XBLOCK_SETTINGS.setdefault("VideoBlock", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) +YOUTUBE_API_KEY = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) ##### VIDEO IMAGE STORAGE ##### VIDEO_IMAGE_SETTINGS = ENV_TOKENS.get('VIDEO_IMAGE_SETTINGS', VIDEO_IMAGE_SETTINGS) diff --git a/lms/urls.py b/lms/urls.py index 12d8e879224be0e30f2c7543dbd8ca3ae6e0fa5b..0dcbaa9e01576774013b385311340d722229e5a6 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -291,7 +291,11 @@ urlpatterns += [ courseware_views.course_about, name='about_course', ), - + url( + r'^courses/yt_video_metadata$', + courseware_views.yt_video_metadata, + name='yt_video_metadata', + ), url( r'^courses/{}/enroll_staff$'.format( settings.COURSE_ID_PATTERN,