def test_no_video_thumbnail_downloaded( self, image_content, image_content_type, error_message, mock_download_youtube_thumbnail, mock_logger ): """ Test that when no thumbnail is downloaded, video image is not updated. """ mock_download_youtube_thumbnail.return_value = image_content, image_content_type course_id = unicode(self.course.id) video1_edx_video_id = 'test-youtube-video-1' # Verify that video1 has no image attached. video1_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video1_edx_video_id) self.assertIsNone(video1_image_url) # Scrape video thumbnail. scrape_youtube_thumbnail(course_id, video1_edx_video_id, 'test-yt-id') mock_logger.info.assert_called_with( u'VIDEOS: Scraping youtube video thumbnail failed for edx_video_id [%s] in course [%s] with error: %s', video1_edx_video_id, course_id, error_message ) # Verify that no image is attached to video1. video1_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video1_edx_video_id) self.assertIsNone(video1_image_url)
def test_scrape_youtube_thumbnail(self, mocked_request): """ Test that youtube thumbnails are correctly scrapped. """ course_id = unicode(self.course.id) video1_edx_video_id = 'test-youtube-video-1' video2_edx_video_id = 'test-youtube-video-2' # Mock get youtube thumbnail responses. mocked_request.side_effect = [self.mocked_youtube_thumbnail_response()] # Verify that video1 has no image attached. video1_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video1_edx_video_id) self.assertIsNone(video1_image_url) # Verify that video2 has already image attached. video2_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video2_edx_video_id) self.assertIsNotNone(video2_image_url) # Scrape video thumbnails. scrape_youtube_thumbnail(course_id, video1_edx_video_id, 'test-yt-id') scrape_youtube_thumbnail(course_id, video2_edx_video_id, 'test-yt-id2') # Verify that now video1 image is attached. video1_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video1_edx_video_id) self.assertIsNotNone(video1_image_url) # Also verify that video2's image is not updated. video2_image_url_latest = get_course_video_image_url(course_id=course_id, edx_video_id=video2_edx_video_id) self.assertEqual(video2_image_url, video2_image_url_latest)
def test_no_video_thumbnail_downloaded(self, image_content, image_content_type, error_message, mock_download_youtube_thumbnail, mock_logger): """ Test that when no thumbnail is downloaded, video image is not updated. """ mock_download_youtube_thumbnail.return_value = image_content, image_content_type course_id = six.text_type(self.course.id) video1_edx_video_id = 'test-youtube-video-1' # Verify that video1 has no image attached. video1_image_url = get_course_video_image_url( course_id=course_id, edx_video_id=video1_edx_video_id) self.assertIsNone(video1_image_url) # Scrape video thumbnail. scrape_youtube_thumbnail(course_id, video1_edx_video_id, 'test-yt-id') mock_logger.info.assert_called_with( u'VIDEOS: Scraping youtube video thumbnail failed for edx_video_id [%s] in course [%s] with error: %s', video1_edx_video_id, course_id, error_message) # Verify that no image is attached to video1. video1_image_url = get_course_video_image_url( course_id=course_id, edx_video_id=video1_edx_video_id) self.assertIsNone(video1_image_url)
def test_scrape_youtube_thumbnail(self, mocked_request): """ Test that youtube thumbnails are correctly scrapped. """ course_id = six.text_type(self.course.id) video1_edx_video_id = 'test-youtube-video-1' video2_edx_video_id = 'test-youtube-video-2' # Mock get youtube thumbnail responses. mocked_request.side_effect = [self.mocked_youtube_thumbnail_response()] # Verify that video1 has no image attached. video1_image_url = get_course_video_image_url( course_id=course_id, edx_video_id=video1_edx_video_id) self.assertIsNone(video1_image_url) # Verify that video2 has already image attached. video2_image_url = get_course_video_image_url( course_id=course_id, edx_video_id=video2_edx_video_id) self.assertIsNotNone(video2_image_url) # Scrape video thumbnails. scrape_youtube_thumbnail(course_id, video1_edx_video_id, 'test-yt-id') scrape_youtube_thumbnail(course_id, video2_edx_video_id, 'test-yt-id2') # Verify that now video1 image is attached. video1_image_url = get_course_video_image_url( course_id=course_id, edx_video_id=video1_edx_video_id) self.assertIsNotNone(video1_image_url) # Also verify that video2's image is not updated. video2_image_url_latest = get_course_video_image_url( course_id=course_id, edx_video_id=video2_edx_video_id) self.assertEqual(video2_image_url, video2_image_url_latest)
def test_no_video_image(self): """ Test image url is set to None if no video image. """ edx_video_id = 'test1' get_videos_url = reverse_course_url('videos_handler', self.course.id) video_image_upload_url = self.get_url_for_course_key( self.course.id, {'edx_video_id': edx_video_id}) with make_image_file( dimensions=(settings.VIDEO_IMAGE_MIN_WIDTH, settings.VIDEO_IMAGE_MIN_HEIGHT), ) as image_file: self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') val_image_url = get_course_video_image_url(course_id=self.course.id, edx_video_id=edx_video_id) response = self.client.get_json(get_videos_url) self.assertEqual(response.status_code, 200) response_videos = json.loads(response.content)["videos"] for response_video in response_videos: if response_video['edx_video_id'] == edx_video_id: self.assertEqual(response_video['course_video_image_url'], val_image_url) else: self.assertEqual(response_video['course_video_image_url'], None)
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.iteritems()} 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)
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)
def verify_image_upload_reponse(self, course_id, edx_video_id, upload_response): """ Verify that image is uploaded successfully. Arguments: course_id: ID of course edx_video_id: ID of video upload_response: Upload response object Returns: uploaded image url """ self.assertEqual(upload_response.status_code, 200) response = json.loads(upload_response.content) val_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=edx_video_id) self.assertEqual(response['image_url'], val_image_url) return val_image_url
def verify_image_upload_reponse(self, course_id, edx_video_id, upload_response): """ Verify that image is uploaded successfully. Arguments: course_id: ID of course edx_video_id: ID of video upload_response: Upload response object Returns: uploaded image url """ self.assertEqual(upload_response.status_code, 200) response = json.loads(upload_response.content) val_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=edx_video_id) self.assertEqual(response['image_url'], val_image_url) return val_image_url
def test_no_video_image(self): """ Test image url is set to None if no video image. """ edx_video_id = 'test1' get_videos_url = reverse_course_url('videos_handler', self.course.id) video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id}) with make_image_file( dimensions=(settings.VIDEO_IMAGE_MIN_WIDTH, settings.VIDEO_IMAGE_MIN_HEIGHT), ) as image_file: self.client.post(video_image_upload_url, {'file': image_file}, format='multipart') val_image_url = get_course_video_image_url(course_id=self.course.id, edx_video_id=edx_video_id) response = self.client.get_json(get_videos_url) self.assertEqual(response.status_code, 200) response_videos = json.loads(response.content)["videos"] for response_video in response_videos: if response_video['edx_video_id'] == edx_video_id: self.assertEqual(response_video['course_video_image_url'], val_image_url) else: self.assertEqual(response_video['course_video_image_url'], None)
def get_html(self): track_status = (self.download_track and self.track) transcript_download_format = self.transcript_download_format if not track_status else None sources = filter(None, self.html5_sources) download_video_link = None branding_info = None youtube_streams = "" # Determine if there is an alternative source for this video # based on user locale. This exists to support cases where # we leverage a geography specific CDN, like China. cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(self.system.user_location) # If we have an edx_video_id, we prefer its values over what we store # internally for download links (source, html5_sources) and the youtube # stream. if self.edx_video_id and edxval_api: try: val_profiles = ["youtube", "desktop_webm", "desktop_mp4"] if HLSPlaybackEnabledFlag.feature_enabled(self.course_id): val_profiles.append('hls') # strip edx_video_id to prevent ValVideoNotFoundError error if unwanted spaces are there. TNL-5769 val_video_urls = edxval_api.get_urls_for_profiles(self.edx_video_id.strip(), val_profiles) # VAL will always give us the keys for the profiles we asked for, but # if it doesn't have an encoded video entry for that Video + Profile, the # value will map to `None` # add the non-youtube urls to the list of alternative sources # use the last non-None non-youtube non-hls url as the link to download the video for url in [val_video_urls[p] for p in val_profiles if p != "youtube"]: if url: if url not in sources: sources.append(url) # don't include hls urls for download if self.download_video and not url.endswith('.m3u8'): # function returns None when the url cannot be re-written rewritten_link = rewrite_video_url(cdn_url, url) if rewritten_link: download_video_link = rewritten_link else: download_video_link = url # set the youtube url if val_video_urls["youtube"]: youtube_streams = "1.00:{}".format(val_video_urls["youtube"]) except edxval_api.ValInternalError: # VAL raises this exception if it can't find data for the edx video ID. This can happen if the # course data is ported to a machine that does not have the VAL data. So for now, pass on this # exception and fallback to whatever we find in the VideoDescriptor. log.warning("Could not retrieve information from VAL for edx Video ID: %s.", self.edx_video_id) # If the user comes from China use China CDN for html5 videos. # 'CN' is China ISO 3166-1 country code. # Video caching is disabled for Studio. User_location is always None in Studio. # CountryMiddleware disabled for Studio. if getattr(self, 'video_speed_optimizations', True) and cdn_url: branding_info = BrandingInfoConfig.get_config().get(self.system.user_location) for index, source_url in enumerate(sources): new_url = rewrite_video_url(cdn_url, source_url) if new_url: sources[index] = new_url # If there was no edx_video_id, or if there was no download specified # for it, we fall back on whatever we find in the VideoDescriptor if not download_video_link and self.download_video: if self.source: download_video_link = self.source elif self.html5_sources: download_video_link = self.html5_sources[0] # don't give the option to download HLS video urls if download_video_link and download_video_link.endswith('.m3u8'): download_video_link = None track_url, transcript_language, sorted_languages = self.get_transcripts_for_student(self.get_transcripts_info()) # CDN_VIDEO_URLS is only to be used here and will be deleted # TODO([email protected]): Delete this after the CDN experiment has completed. html_id = self.location.html_id() if self.system.user_location == 'CN' and \ settings.FEATURES.get('ENABLE_VIDEO_BEACON', False) and \ html_id in getattr(settings, 'CDN_VIDEO_URLS', {}).keys(): cdn_urls = getattr(settings, 'CDN_VIDEO_URLS', {})[html_id] cdn_exp_group, new_source = random.choice(zip(range(len(cdn_urls)), cdn_urls)) if cdn_exp_group > 0: sources[0] = new_source cdn_eval = True else: cdn_eval = False cdn_exp_group = None self.youtube_streams = youtube_streams or create_youtube_string(self) # pylint: disable=W0201 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( course_id=self.runtime.course_id.for_branch(None), edx_video_id=self.edx_video_id.strip() ) metadata = { 'saveStateUrl': self.system.ajax_url + '/save_user_state', 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), 'streams': self.youtube_streams, 'sub': self.sub, 'sources': sources, 'poster': poster, # This won't work when we move to data that # isn't on the filesystem 'captionDataDir': getattr(self, 'data_dir', None), 'showCaptions': json.dumps(self.show_captions), 'generalSpeed': self.global_speed, 'speed': self.speed, 'savedVideoPosition': self.saved_video_position.total_seconds(), 'start': self.start_time.total_seconds(), 'end': self.end_time.total_seconds(), 'transcriptLanguage': transcript_language, 'transcriptLanguages': sorted_languages, 'ytTestTimeout': settings.YOUTUBE['TEST_TIMEOUT'], 'ytApiUrl': settings.YOUTUBE['API'], 'ytMetadataUrl': settings.YOUTUBE['METADATA_URL'], 'ytKey': yt_api_key, 'transcriptTranslationUrl': self.runtime.handler_url( self, 'transcript', 'translation/__lang__' ).rstrip('/?'), 'transcriptAvailableTranslationsUrl': self.runtime.handler_url( self, 'transcript', 'available_translations' ).rstrip('/?'), ## For now, the option "data-autohide-html5" is hard coded. This option ## either enables or disables autohiding of controls and captions on mouse ## inactivity. If set to true, controls and captions will autohide for ## HTML5 sources (non-YouTube) after a period of mouse inactivity over the ## whole video. When the mouse moves (or a key is pressed while any part of ## the video player is focused), the captions and controls will be shown ## once again. ## ## There is no option in the "Advanced Editor" to set this option. However, ## this option will have an effect if changed to "True". The code on ## front-end exists. 'autohideHtml5': False, # This is the server's guess at whether youtube is available for # this user, based on what was recorded the last time we saw the # user, and defaulting to True. 'recordedYoutubeIsAvailable': self.youtube_is_available, } bumperize(self) context = { 'bumper_metadata': json.dumps(self.bumper['metadata']), # pylint: disable=E1101 'metadata': json.dumps(OrderedDict(metadata)), 'poster': json.dumps(get_poster(self)), 'branding_info': branding_info, 'cdn_eval': cdn_eval, 'cdn_exp_group': cdn_exp_group, 'id': self.location.html_id(), 'display_name': self.display_name_with_default, 'handout': self.handout, 'download_video_link': download_video_link, 'track': track_url, 'transcript_download_format': transcript_download_format, 'transcript_download_formats_list': self.descriptor.fields['transcript_download_format'].values, 'license': getattr(self, "license", None), } return self.system.render_template('video.html', context)