Merge pull request #2786 from edx/alex/fix_download_html5_subs
Fix donwload subs for non youtube videos and non-en language.
This commit is contained in:
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Blades: Fix download subs for non youtube videos and non-en language. BLD-897.
|
||||
|
||||
Blades: Fix issues related to videos that have separate YouTube IDs for the
|
||||
different video speeds. BLD-915, BLD-901.
|
||||
|
||||
|
||||
@@ -369,31 +369,32 @@ Feature: CMS Transcripts
|
||||
And I click transcript button "choose" number 2
|
||||
And I see value "test_transcripts|t_not_exist" in the field "Transcript (primary)"
|
||||
|
||||
# Flaky test fails occasionally in master. https://edx-wiki.atlassian.net/browse/BLD-927
|
||||
#21
|
||||
Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o transcripts - click on use existing - > change it to another one HTML5 source w/o transcripts - click on use existing
|
||||
Given I have created a Video component with subtitles "t_not_exist"
|
||||
And I edit the component
|
||||
|
||||
And I enter a "t_not_exist.mp4" source to field number 1
|
||||
Then I see status message "found"
|
||||
And I see button "download_to_edit"
|
||||
And I see button "upload_new_timed_transcripts"
|
||||
And I see value "t_not_exist" in the field "Transcript (primary)"
|
||||
|
||||
And I save changes
|
||||
And I edit the component
|
||||
|
||||
And I enter a "video_name_2.mp4" source to field number 1
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_2" in the field "Transcript (primary)"
|
||||
|
||||
And I enter a "video_name_3.mp4" source to field number 1
|
||||
Then I see status message "use existing"
|
||||
And I see button "use_existing"
|
||||
And I click transcript button "use_existing"
|
||||
And I see value "video_name_3" in the field "Transcript (primary)"
|
||||
#Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o #transcripts - click on use existing - > change it to another one HTML5 source w/o transcripts - click on use existing
|
||||
# Given I have created a Video component with subtitles "t_not_exist"
|
||||
# And I edit the component
|
||||
#
|
||||
# And I enter a "t_not_exist.mp4" source to field number 1
|
||||
# Then I see status message "found"
|
||||
# And I see button "download_to_edit"
|
||||
# And I see button "upload_new_timed_transcripts"
|
||||
# And I see value "t_not_exist" in the field "Transcript (primary)"
|
||||
#
|
||||
# And I save changes
|
||||
# And I edit the component
|
||||
#
|
||||
# And I enter a "video_name_2.mp4" source to field number 1
|
||||
# Then I see status message "use existing"
|
||||
# And I see button "use_existing"
|
||||
# And I click transcript button "use_existing"
|
||||
# And I see value "video_name_2" in the field "Transcript (primary)"
|
||||
#
|
||||
# And I enter a "video_name_3.mp4" source to field number 1
|
||||
# Then I see status message "use existing"
|
||||
# And I see button "use_existing"
|
||||
# And I click transcript button "use_existing"
|
||||
# And I see value "video_name_3" in the field "Transcript (primary)"
|
||||
|
||||
#22
|
||||
Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - click on use existing -> change it to another one HTML5 source w/o transcripts - do not click on use existing -> change it to another one HTML5 source w/o transcripts - click on use existing
|
||||
|
||||
@@ -487,3 +487,64 @@ class TestYoutubeTranscripts(unittest.TestCase):
|
||||
transcripts = transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
|
||||
self.assertEqual(transcripts, expected_transcripts)
|
||||
mock_get.assert_called_with('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_youtube_id'})
|
||||
|
||||
class TestTranscript(unittest.TestCase):
|
||||
"""
|
||||
Tests for Transcript class e.g. different transcript conversions.
|
||||
"""
|
||||
def setUp(self):
|
||||
|
||||
self.srt_transcript = textwrap.dedent("""\
|
||||
0
|
||||
00:00:10,500 --> 00:00:13,000
|
||||
Elephant's Dream
|
||||
|
||||
1
|
||||
00:00:15,000 --> 00:00:18,000
|
||||
At the left we can see...
|
||||
|
||||
""")
|
||||
|
||||
|
||||
self.sjson_transcript = textwrap.dedent("""\
|
||||
{
|
||||
"start": [
|
||||
10500,
|
||||
15000
|
||||
],
|
||||
"end": [
|
||||
13000,
|
||||
18000
|
||||
],
|
||||
"text": [
|
||||
"Elephant's Dream",
|
||||
"At the left we can see..."
|
||||
]
|
||||
}
|
||||
""")
|
||||
|
||||
self.txt_transcript = u"Elephant's Dream\nAt the left we can see..."
|
||||
|
||||
def test_convert_srt_to_txt(self):
|
||||
expected = self.txt_transcript
|
||||
actual = transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'txt')
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_convert_srt_to_srt(self):
|
||||
expected = self.srt_transcript
|
||||
actual = transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'srt')
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_convert_sjson_to_txt(self):
|
||||
expected = self.txt_transcript
|
||||
actual = transcripts_utils.Transcript.convert(self.sjson_transcript, 'sjson', 'txt')
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_convert_sjson_to_srt(self):
|
||||
expected = self.srt_transcript
|
||||
actual = transcripts_utils.Transcript.convert(self.sjson_transcript, 'sjson', 'srt')
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_convert_srt_to_sjson(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
transcripts_utils.Transcript.convert(self.srt_transcript, 'srt', 'sjson')
|
||||
|
||||
@@ -233,8 +233,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
end_time="00:01:00">
|
||||
<source src="http://www.example.com/source.mp4"/>
|
||||
<track src="http://www.example.com/track"/>
|
||||
<transcript language="ua" src="ukrainian_translation.srt" />
|
||||
<transcript language="ge" src="german_translation.srt" />
|
||||
<transcript language="uk" src="ukrainian_translation.srt" />
|
||||
<transcript language="de" src="german_translation.srt" />
|
||||
</video>
|
||||
'''
|
||||
output = VideoDescriptor.from_xml(xml_data, module_system, Mock())
|
||||
@@ -251,7 +251,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
'download_video': False,
|
||||
'html5_sources': ['http://www.example.com/source.mp4'],
|
||||
'data': '',
|
||||
'transcripts': {'ua': 'ukrainian_translation.srt', 'ge': 'german_translation.srt'},
|
||||
'transcripts': {'uk': 'ukrainian_translation.srt', 'de': 'german_translation.srt'},
|
||||
})
|
||||
|
||||
def test_from_xml_missing_attributes(self):
|
||||
|
||||
@@ -9,6 +9,7 @@ import requests
|
||||
import logging
|
||||
from pysrt import SubRipTime, SubRipItem, SubRipFile
|
||||
from lxml import etree
|
||||
from HTMLParser import HTMLParser
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
@@ -77,7 +78,7 @@ def save_subs_to_store(subs, subs_id, item, language='en'):
|
||||
filedata = json.dumps(subs, indent=2)
|
||||
mime_type = 'application/json'
|
||||
filename = subs_filename(subs_id, language)
|
||||
content_location = asset_location(item.location, filename)
|
||||
content_location = Transcript.asset_location(item.location, filename)
|
||||
content = StaticContent(content_location, filename, mime_type, filedata)
|
||||
contentstore().save(content)
|
||||
return content_location
|
||||
@@ -193,7 +194,7 @@ def remove_subs_from_store(subs_id, item, lang='en'):
|
||||
Remove from store, if transcripts content exists.
|
||||
"""
|
||||
try:
|
||||
content = asset(item.location, subs_id, lang)
|
||||
content = Transcript.asset(item.location, subs_id, lang)
|
||||
contentstore().delete(content.get_id())
|
||||
log.info("Removed subs %s from store", subs_id)
|
||||
except NotFoundError:
|
||||
@@ -412,30 +413,6 @@ def subs_filename(subs_id, lang='en'):
|
||||
return '{0}_subs_{1}.srt.sjson'.format(lang, subs_id)
|
||||
|
||||
|
||||
def asset_location(location, filename):
|
||||
"""
|
||||
Return asset location.
|
||||
|
||||
`location` is module location.
|
||||
"""
|
||||
return StaticContent.compute_location(
|
||||
location.org, location.course, filename
|
||||
)
|
||||
|
||||
|
||||
def asset(location, subs_id, lang='en', filename=None):
|
||||
"""
|
||||
Get asset from contentstore, asset location is built from subs_id and lang.
|
||||
|
||||
`location` is module location.
|
||||
"""
|
||||
return contentstore().find(
|
||||
asset_location(
|
||||
location,
|
||||
subs_filename(subs_id, lang) if not filename else filename
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
|
||||
"""
|
||||
@@ -444,7 +421,7 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
|
||||
`item` is module object.
|
||||
"""
|
||||
try:
|
||||
srt_transcripts = contentstore().find(asset_location(item.location, user_filename))
|
||||
srt_transcript = contentstore().find(Transcript.asset_location(item.location, user_filename))
|
||||
except NotFoundError as ex:
|
||||
raise TranscriptException("{}: Can't find uploaded transcripts: {}".format(ex.message, user_filename))
|
||||
|
||||
@@ -454,7 +431,7 @@ def generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, lang):
|
||||
generate_subs_from_source(
|
||||
result_subs_dict,
|
||||
os.path.splitext(user_filename)[1][1:],
|
||||
srt_transcripts.data.decode('utf8'),
|
||||
srt_transcript.data.decode('utf8'),
|
||||
item,
|
||||
lang
|
||||
)
|
||||
@@ -477,8 +454,72 @@ def get_or_create_sjson(item):
|
||||
user_subs_id = os.path.splitext(user_filename)[0]
|
||||
source_subs_id, result_subs_dict = user_subs_id, {1.0: user_subs_id}
|
||||
try:
|
||||
sjson_transcript = asset(item.location, source_subs_id, item.transcript_language).data
|
||||
sjson_transcript = Transcript.asset(item.location, source_subs_id, item.transcript_language).data
|
||||
except (NotFoundError): # generating sjson from srt
|
||||
generate_sjson_for_all_speeds(item, user_filename, result_subs_dict, item.transcript_language)
|
||||
sjson_transcript = asset(item.location, source_subs_id, item.transcript_language).data
|
||||
sjson_transcript = Transcript.asset(item.location, source_subs_id, item.transcript_language).data
|
||||
return sjson_transcript
|
||||
|
||||
class Transcript(object):
|
||||
"""
|
||||
Container for transcript methods.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def convert(content, input_format, output_format):
|
||||
"""
|
||||
Convert transcript `content` from `input_format` to `output_format`.
|
||||
|
||||
Accepted input formats: sjson, srt.
|
||||
Accepted output format: srt, txt.
|
||||
"""
|
||||
assert input_format in ('srt', 'sjson')
|
||||
assert output_format in ('txt', 'srt', 'sjson')
|
||||
|
||||
if input_format == output_format:
|
||||
return content
|
||||
|
||||
if input_format == 'srt':
|
||||
|
||||
if output_format == 'txt':
|
||||
text = SubRipFile.from_string(content.decode('utf8')).text
|
||||
return HTMLParser().unescape(text)
|
||||
|
||||
elif output_format == 'sjson':
|
||||
raise NotImplementedError
|
||||
|
||||
if input_format == 'sjson':
|
||||
|
||||
if output_format == 'txt':
|
||||
text = json.loads(content)['text']
|
||||
return HTMLParser().unescape("\n".join(text))
|
||||
|
||||
elif output_format == 'srt':
|
||||
return generate_srt_from_sjson(json.loads(content), speed=1.0)
|
||||
|
||||
@staticmethod
|
||||
def asset(location, subs_id, lang='en', filename=None):
|
||||
"""
|
||||
Get asset from contentstore, asset location is built from subs_id and lang.
|
||||
|
||||
`location` is module location.
|
||||
"""
|
||||
return contentstore().find(
|
||||
Transcript.asset_location(
|
||||
location,
|
||||
subs_filename(subs_id, lang) if not filename else filename
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def asset_location(location, filename):
|
||||
"""
|
||||
Return asset location.
|
||||
|
||||
`location` is module location.
|
||||
"""
|
||||
return StaticContent.compute_location(
|
||||
location.org, location.course, filename
|
||||
)
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ in-browser HTML5 video method (when in HTML5 mode).
|
||||
in XML.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from HTMLParser import HTMLParser
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
@@ -33,12 +33,11 @@ from xblock.core import XBlock
|
||||
from xblock.fields import Scope, String, Float, Boolean, List, Dict, ScopeIds
|
||||
from xmodule.fields import RelativeTime
|
||||
from .transcripts_utils import (
|
||||
generate_srt_from_sjson,
|
||||
asset,
|
||||
get_or_create_sjson,
|
||||
TranscriptException,
|
||||
generate_sjson_for_all_speeds,
|
||||
youtube_speed_dict
|
||||
youtube_speed_dict,
|
||||
Transcript,
|
||||
)
|
||||
from .video_utils import create_youtube_string
|
||||
|
||||
@@ -271,13 +270,13 @@ class VideoModule(VideoFields, XModule):
|
||||
track_url = self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/download'
|
||||
|
||||
if not self.transcripts:
|
||||
transcript_language = 'en'
|
||||
transcript_language = u'en'
|
||||
languages = {'en': 'English'}
|
||||
else:
|
||||
if self.transcript_language in self.transcripts:
|
||||
transcript_language = self.transcript_language
|
||||
elif self.sub:
|
||||
transcript_language = 'en'
|
||||
transcript_language = u'en'
|
||||
else:
|
||||
transcript_language = sorted(self.transcripts.keys())[0]
|
||||
|
||||
@@ -323,30 +322,47 @@ class VideoModule(VideoFields, XModule):
|
||||
'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/available_translations',
|
||||
})
|
||||
|
||||
def get_transcript(self, format='srt'):
|
||||
def get_transcript(self, transcript_format='srt'):
|
||||
"""
|
||||
Returns transcript in *.srt format.
|
||||
Returns transcript, filename and MIME type.
|
||||
|
||||
Raises:
|
||||
- NotFoundError if cannot find transcript file in storage.
|
||||
- ValueError if transcript file is empty or incorrect JSON.
|
||||
- KeyError if transcript file has incorrect format.
|
||||
|
||||
If language is 'en', self.sub should be correct subtitles name.
|
||||
If language is 'en', but if self.sub is not defined, this means that we
|
||||
should search for video name in order to get proper transcript (old style courses).
|
||||
If language is not 'en', give back transcript in proper language and format.
|
||||
"""
|
||||
lang = self.transcript_language
|
||||
subs_id = self.sub if lang == 'en' else self.youtube_id_1_0
|
||||
data = asset(self.location, subs_id, lang).data
|
||||
if format == 'txt':
|
||||
text = json.loads(data)['text']
|
||||
str_subs = HTMLParser().unescape("\n".join(text))
|
||||
mime_type = 'text/plain'
|
||||
|
||||
if lang == 'en':
|
||||
if self.sub: # HTML5 case and (Youtube case for new style videos)
|
||||
transcript_name = self.sub
|
||||
elif self.youtube_id_1_0: # old courses
|
||||
transcript_name = self.youtube_id_1_0
|
||||
else:
|
||||
log.debug("No subtitles for 'en' language")
|
||||
raise ValueError
|
||||
|
||||
data = Transcript.asset(self.location, transcript_name, lang).data
|
||||
filename = '{}.{}'.format(transcript_name, transcript_format)
|
||||
content = Transcript.convert(data, 'sjson', transcript_format)
|
||||
else:
|
||||
str_subs = generate_srt_from_sjson(json.loads(data), speed=1.0)
|
||||
mime_type = 'application/x-subrip'
|
||||
if not str_subs:
|
||||
log.debug('generate_srt_from_sjson produces no subtitles')
|
||||
data = Transcript.asset(self.location, None, None, self.transcripts[lang]).data
|
||||
filename = '{}.{}'.format(os.path.splitext(self.transcripts[lang])[0], transcript_format)
|
||||
content = Transcript.convert(data, 'srt', transcript_format)
|
||||
|
||||
if not content:
|
||||
log.debug('no subtitles produced in get_transcript')
|
||||
raise ValueError
|
||||
|
||||
return str_subs, format, mime_type
|
||||
mime_type = 'text/plain' if transcript_format == 'txt' else 'application/x-subrip'
|
||||
|
||||
return content, filename, mime_type
|
||||
|
||||
|
||||
@XBlock.handler
|
||||
def transcript(self, request, dispatch):
|
||||
@@ -384,34 +400,31 @@ class VideoModule(VideoFields, XModule):
|
||||
|
||||
elif dispatch == 'download':
|
||||
try:
|
||||
subs, format, mime_type = self.get_transcript(format=self.transcript_download_format)
|
||||
transcript_content, transcript_filename, transcript_mime_type = self.get_transcript(self.transcript_download_format)
|
||||
except (NotFoundError, ValueError, KeyError):
|
||||
log.debug("Video@download exception")
|
||||
response = Response(status=404)
|
||||
else:
|
||||
response = Response(
|
||||
subs,
|
||||
transcript_content,
|
||||
headerlist=[
|
||||
('Content-Disposition', 'attachment; filename="{filename}.{format}"'.format(
|
||||
filename=self.transcript_language,
|
||||
format=format,
|
||||
)),
|
||||
('Content-Disposition', 'attachment; filename="{}"'.format(transcript_filename)),
|
||||
]
|
||||
)
|
||||
response.content_type = mime_type
|
||||
response.content_type = transcript_mime_type
|
||||
|
||||
elif dispatch == 'available_translations':
|
||||
available_translations = []
|
||||
if self.sub: # check if sjson exists for 'en'.
|
||||
try:
|
||||
asset(self.location, self.sub, 'en')
|
||||
Transcript.asset(self.location, self.sub, 'en')
|
||||
except NotFoundError:
|
||||
pass
|
||||
else:
|
||||
available_translations = ['en']
|
||||
for lang in self.transcripts:
|
||||
try:
|
||||
asset(self.location, None, None, self.transcripts[lang])
|
||||
Transcript.asset(self.location, None, None, self.transcripts[lang])
|
||||
except NotFoundError:
|
||||
continue
|
||||
available_translations.append(lang)
|
||||
@@ -462,13 +475,13 @@ class VideoModule(VideoFields, XModule):
|
||||
if youtube_id:
|
||||
# Youtube case:
|
||||
if self.transcript_language == 'en':
|
||||
return asset(self.location, youtube_id).data
|
||||
return Transcript.asset(self.location, youtube_id).data
|
||||
|
||||
youtube_ids = youtube_speed_dict(self)
|
||||
assert youtube_id in youtube_ids
|
||||
|
||||
try:
|
||||
sjson_transcript = asset(self.location, youtube_id, self.transcript_language).data
|
||||
sjson_transcript = Transcript.asset(self.location, youtube_id, self.transcript_language).data
|
||||
except (NotFoundError):
|
||||
log.info("Can't find content in storage for %s transcript: generating.", youtube_id)
|
||||
generate_sjson_for_all_speeds(
|
||||
@@ -477,19 +490,17 @@ class VideoModule(VideoFields, XModule):
|
||||
{speed: youtube_id for youtube_id, speed in youtube_ids.iteritems()},
|
||||
self.transcript_language
|
||||
)
|
||||
sjson_transcript = asset(self.location, youtube_id, self.transcript_language).data
|
||||
sjson_transcript = Transcript.asset(self.location, youtube_id, self.transcript_language).data
|
||||
|
||||
return sjson_transcript
|
||||
else:
|
||||
# HTML5 case
|
||||
if self.transcript_language == 'en':
|
||||
return asset(self.location, self.sub).data
|
||||
return Transcript.asset(self.location, self.sub).data
|
||||
else:
|
||||
return get_or_create_sjson(self)
|
||||
|
||||
|
||||
|
||||
|
||||
class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor):
|
||||
"""Descriptor for `VideoModule`."""
|
||||
module_class = VideoModule
|
||||
|
||||
@@ -82,9 +82,9 @@ Feature: LMS Video component
|
||||
# 10
|
||||
Scenario: Language menu works correctly in Video component
|
||||
Given I am registered for the course "test_course"
|
||||
And I have a "chinese_transcripts.srt" transcript file in assets
|
||||
And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets
|
||||
And it has a video in "Youtube" mode:
|
||||
And I have a "chinese_transcripts.srt" transcript file in assets
|
||||
And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets
|
||||
And it has a video in "Youtube" mode:
|
||||
| transcripts | sub |
|
||||
| {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM |
|
||||
And I make sure captions are closed
|
||||
@@ -97,8 +97,8 @@ Feature: LMS Video component
|
||||
# 11
|
||||
Scenario: CC button works correctly w/o english transcript in HTML5 mode of Video component
|
||||
Given I am registered for the course "test_course"
|
||||
And I have a "chinese_transcripts.srt" transcript file in assets
|
||||
And it has a video in "HTML5" mode:
|
||||
And I have a "chinese_transcripts.srt" transcript file in assets
|
||||
And it has a video in "HTML5" mode:
|
||||
| transcripts |
|
||||
| {"zh": "chinese_transcripts.srt"} |
|
||||
And I make sure captions are opened
|
||||
@@ -181,11 +181,11 @@ Feature: LMS Video component
|
||||
| track | download_track |
|
||||
| http://example.org/ | true |
|
||||
And I open the section with videos
|
||||
And I can download transcript in "srt" format
|
||||
Then I can download transcript in "srt" format and has text "00:00:00,270"
|
||||
And I select the transcript format "txt"
|
||||
And I can download transcript in "txt" format
|
||||
Then I can download transcript in "txt" format and has text "Hi, welcome to Edx."
|
||||
When I open video "B"
|
||||
Then I can download transcript in "txt" format
|
||||
Then I can download transcript in "txt" format and has text "Hi, welcome to Edx."
|
||||
When I open video "C"
|
||||
Then menu "download_transcript" doesn't exist
|
||||
|
||||
@@ -203,3 +203,47 @@ Feature: LMS Video component
|
||||
And I reload the page
|
||||
Then I see "Hi, welcome to Edx." text in the captions
|
||||
And I see duration "1:00"
|
||||
|
||||
# 21
|
||||
Scenario: Download button works correctly for non-english transcript in Youtube mode of Video component
|
||||
Given I am registered for the course "test_course"
|
||||
And I have a "chinese_transcripts.srt" transcript file in assets
|
||||
And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets
|
||||
And it has a video in "Youtube" mode:
|
||||
| transcripts | sub | download_track |
|
||||
| {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | true |
|
||||
And I select language with code "zh"
|
||||
And I see "好 各位同学" text in the captions
|
||||
Then I can download transcript in "srt" format and has text "好 各位同学"
|
||||
|
||||
# 22
|
||||
Scenario: Download button works correctly for non-english transcript in HTML5 mode of Video component
|
||||
Given I am registered for the course "test_course"
|
||||
And I have a "chinese_transcripts.srt" transcript file in assets
|
||||
And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets
|
||||
And it has a video in "HTML5" mode:
|
||||
| transcripts | sub | download_track |
|
||||
| {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | true |
|
||||
And I select language with code "zh"
|
||||
And I see "好 各位同学" text in the captions
|
||||
Then I can download transcript in "srt" format and has text "好 各位同学"
|
||||
|
||||
# 23
|
||||
Scenario: Download button works correctly w/o english transcript in HTML5 mode of Video component
|
||||
Given I am registered for the course "test_course"
|
||||
And I have a "chinese_transcripts.srt" transcript file in assets
|
||||
And it has a video in "HTML5" mode:
|
||||
| transcripts | download_track |
|
||||
| {"zh": "chinese_transcripts.srt"} | true |
|
||||
And I see "好 各位同学" text in the captions
|
||||
Then I can download transcript in "srt" format and has text "好 各位同学"
|
||||
|
||||
# 24
|
||||
Scenario: Download button works correctly w/o english transcript in Youtube mode of Video component
|
||||
Given I am registered for the course "test_course"
|
||||
And I have a "chinese_transcripts.srt" transcript file in assets
|
||||
And it has a video in "Youtube" mode:
|
||||
| transcripts | download_track |
|
||||
| {"zh": "chinese_transcripts.srt"} | true |
|
||||
And I see "好 各位同学" text in the captions
|
||||
Then I can download transcript in "srt" format and has text "好 各位同学"
|
||||
@@ -2,16 +2,17 @@
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
from common import i_am_registered_for_the_course, section_location, visit_scenario_item
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
LANGUAGES = settings.ALL_LANGUAGES
|
||||
|
||||
@@ -54,7 +55,7 @@ class ReuqestHandlerWithSessionId(object):
|
||||
"""
|
||||
kwargs = dict()
|
||||
|
||||
session_id = [{i['name']:i['value']} for i in world.browser.cookies.all() if i['name']==u'sessionid']
|
||||
session_id = [{i['name']:i['value']} for i in world.browser.cookies.all() if i['name'] == u'sessionid']
|
||||
if session_id:
|
||||
kwargs.update({
|
||||
'cookies': session_id[0]
|
||||
@@ -118,7 +119,7 @@ def add_video_to_course(course, player_mode, hashes, display_name='Video'):
|
||||
})
|
||||
if player_mode == 'youtube_html5':
|
||||
kwargs['metadata'].update({
|
||||
'html5_sources': HTML5_SOURCES
|
||||
'html5_sources': HTML5_SOURCES,
|
||||
})
|
||||
if player_mode == 'youtube_html5_unsupported_video':
|
||||
kwargs['metadata'].update({
|
||||
@@ -166,7 +167,6 @@ def _change_video_speed(speed):
|
||||
speed_css = 'li[data-speed="{0}"] a'.format(speed)
|
||||
world.css_click(speed_css)
|
||||
|
||||
|
||||
def _open_menu(menu):
|
||||
world.browser.execute_script("$('{selector}').parent().addClass('open')".format(
|
||||
selector=VIDEO_MENUS[menu]
|
||||
@@ -333,11 +333,11 @@ def set_captions_visibility_state(_step, captions_state):
|
||||
def i_see_menu(_step, menu):
|
||||
_open_menu(menu)
|
||||
menu_items = world.css_find(VIDEO_MENUS[menu] + ' li')
|
||||
Video = world.scenario_dict['VIDEO']
|
||||
transcripts = dict(Video.transcripts)
|
||||
if Video.sub:
|
||||
video = world.scenario_dict['VIDEO']
|
||||
transcripts = dict(video.transcripts)
|
||||
if video.sub:
|
||||
transcripts.update({
|
||||
'en': Video.sub
|
||||
'en': video.sub
|
||||
})
|
||||
|
||||
languages = {i[0]: i[1] for i in LANGUAGES}
|
||||
@@ -359,9 +359,8 @@ def select_language(_step, code):
|
||||
selector = VIDEO_MENUS["language"] + ' li[data-lang-code={code}]'.format(
|
||||
code=code
|
||||
)
|
||||
item = world.css_find(selector)
|
||||
|
||||
item.click()
|
||||
world.css_click(selector)
|
||||
|
||||
assert world.css_has_class(selector, 'active')
|
||||
assert len(world.css_find(VIDEO_MENUS["language"] + ' li.active')) == 1
|
||||
@@ -406,6 +405,7 @@ def upload_to_assets(_step, filename):
|
||||
def is_hidden_button(_step, button):
|
||||
assert not world.css_visible(VIDEO_BUTTONS[button])
|
||||
|
||||
|
||||
@step('menu "([^"]*)" doesn\'t exist$')
|
||||
def is_hidden_menu(_step, menu):
|
||||
assert world.is_css_not_present(VIDEO_MENUS[menu])
|
||||
@@ -435,27 +435,21 @@ def video_alignment(_step, transcript_visibility):
|
||||
assert all([width, height])
|
||||
|
||||
|
||||
@step('I can download transcript in "([^"]*)" format$')
|
||||
def i_can_download_transcript(_step, format):
|
||||
@step('I can download transcript in "([^"]*)" format and has text "([^"]*)"$')
|
||||
def i_can_download_transcript(_step, format, text):
|
||||
button = world.css_find('.video-tracks .a11y-menu-button').first
|
||||
assert button.text.strip() == '.' + format
|
||||
|
||||
formats = {
|
||||
'srt': {
|
||||
'content': '0\n00:00:00,270',
|
||||
'mime_type': 'application/x-subrip'
|
||||
},
|
||||
'txt': {
|
||||
'content': 'Hi, welcome to Edx.',
|
||||
'mime_type': 'text/plain'
|
||||
},
|
||||
'srt': 'application/x-subrip',
|
||||
'txt': 'text/plain',
|
||||
}
|
||||
|
||||
url = world.css_find(VIDEO_BUTTONS['download_transcript'])[0]['href']
|
||||
request = ReuqestHandlerWithSessionId()
|
||||
assert request.get(url).is_success()
|
||||
assert request.check_header('content-type', formats[format]['mime_type'])
|
||||
assert request.content.startswith(formats[format]['content'])
|
||||
assert request.check_header('content-type', formats[format])
|
||||
assert (text.encode('utf-8') in request.content)
|
||||
|
||||
|
||||
@step('I select the transcript format "([^"]*)"$')
|
||||
|
||||
@@ -135,10 +135,69 @@ class TestVideo(BaseTestXmodule):
|
||||
def tearDown(self):
|
||||
_clear_assets(self.item_descriptor.location)
|
||||
|
||||
|
||||
class TestVideoTranscriptTranslation(TestVideo):
|
||||
class TestTranscriptAvailableTranslationsDispatch(TestVideo):
|
||||
"""
|
||||
Test video handlers that provide translation transcripts.
|
||||
Test video handler that provide available translations info.
|
||||
|
||||
Tests for `available_translations` dispatch.
|
||||
"""
|
||||
non_en_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1])
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestTranscriptAvailableTranslationsDispatch, self).setUp()
|
||||
self.item_descriptor.render('student_view')
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
self.subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
|
||||
|
||||
def test_available_translation_en(self):
|
||||
good_sjson = _create_file(json.dumps(self.subs))
|
||||
_upload_sjson_file(good_sjson, self.item_descriptor.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
request = Request.blank('/translation')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertEqual(json.loads(response.body), ['en'])
|
||||
|
||||
def test_available_translation_non_en(self):
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
|
||||
request = Request.blank('/translation')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertEqual(json.loads(response.body), ['uk'])
|
||||
|
||||
def test_multiple_available_translations(self):
|
||||
good_sjson = _create_file(json.dumps(self.subs))
|
||||
|
||||
# Upload english transcript.
|
||||
_upload_sjson_file(good_sjson, self.item_descriptor.location)
|
||||
|
||||
# Upload non-english transcript.
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
|
||||
request = Request.blank('/translation')
|
||||
response = self.item.transcript(request=request, dispatch='available_translations')
|
||||
self.assertEqual(json.loads(response.body), ['en', 'uk'])
|
||||
|
||||
class TestTranscriptDownloadDispatch(TestVideo):
|
||||
"""
|
||||
Test video handler that provide translation transcripts.
|
||||
|
||||
Tests for `download` dispatch.
|
||||
"""
|
||||
|
||||
non_en_file = _create_srt_file()
|
||||
@@ -157,11 +216,10 @@ class TestVideoTranscriptTranslation(TestVideo):
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestVideoTranscriptTranslation, self).setUp()
|
||||
super(TestTranscriptDownloadDispatch, self).setUp()
|
||||
self.item_descriptor.render('student_view')
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
# Tests for `download` dispatch:
|
||||
|
||||
def test_language_is_not_supported(self):
|
||||
request = Request.blank('/download?language=ru')
|
||||
@@ -173,7 +231,7 @@ class TestVideoTranscriptTranslation(TestVideo):
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
self.assertEqual(response.status, '404 Not Found')
|
||||
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'srt', 'application/x-subrip'))
|
||||
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'test_filename.srt', 'application/x-subrip'))
|
||||
def test_download_srt_exist(self, __):
|
||||
request = Request.blank('/download?language=en')
|
||||
response = self.item.transcript(request=request, dispatch='download')
|
||||
@@ -195,7 +253,32 @@ class TestVideoTranscriptTranslation(TestVideo):
|
||||
with self.assertRaises(NotFoundError):
|
||||
self.item.get_transcript()
|
||||
|
||||
# Tests for `translation` dispatch:
|
||||
class TestTranscriptTranslationDispatch(TestVideo):
|
||||
"""
|
||||
Test video handler that provide translation transcripts.
|
||||
|
||||
Tests for `translation` dispatch.
|
||||
"""
|
||||
|
||||
non_en_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
</video>
|
||||
""".format(os.path.split(non_en_file.name)[1])
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestTranscriptTranslationDispatch, self).setUp()
|
||||
self.item_descriptor.render('student_view')
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
def test_translation_fails(self):
|
||||
# No language
|
||||
@@ -295,30 +378,35 @@ class TestVideoTranscriptTranslation(TestVideo):
|
||||
self.assertDictEqual(json.loads(response.body), subs)
|
||||
|
||||
|
||||
class TestVideoTranscriptsDownload(TestVideo):
|
||||
class TestGetTranscript(TestVideo):
|
||||
"""
|
||||
Make sure that `get_transcript` method works correctly
|
||||
"""
|
||||
|
||||
non_en_file = _create_srt_file()
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<transcript language="uk" src="{}"/>
|
||||
</video>
|
||||
"""
|
||||
""".format(os.path.split(non_en_file.name)[1])
|
||||
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
METADATA = {}
|
||||
|
||||
def setUp(self):
|
||||
super(TestVideoTranscriptsDownload, self).setUp()
|
||||
super(TestGetTranscript, self).setUp()
|
||||
self.item_descriptor.render('student_view')
|
||||
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
def test_good_srt_transcript(self):
|
||||
def test_good_transcript(self):
|
||||
"""
|
||||
Test for download 'en' sub with html5 video and self.sub has correct non-empty value.
|
||||
"""
|
||||
good_sjson = _create_file(content=textwrap.dedent("""\
|
||||
{
|
||||
"start": [
|
||||
@@ -338,7 +426,9 @@ class TestVideoTranscriptsDownload(TestVideo):
|
||||
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
text, format, download = self.item.get_transcript()
|
||||
|
||||
text, filename, mime_type = self.item.get_transcript()
|
||||
|
||||
expected_text = textwrap.dedent("""\
|
||||
0
|
||||
00:00:00,270 --> 00:00:02,720
|
||||
@@ -351,6 +441,8 @@ class TestVideoTranscriptsDownload(TestVideo):
|
||||
""")
|
||||
|
||||
self.assertEqual(text, expected_text)
|
||||
self.assertEqual(filename[:-4], self.item.sub)
|
||||
self.assertEqual(mime_type, 'application/x-subrip')
|
||||
|
||||
def test_good_txt_transcript(self):
|
||||
good_sjson = _create_file(content=textwrap.dedent("""\
|
||||
@@ -372,17 +464,77 @@ class TestVideoTranscriptsDownload(TestVideo):
|
||||
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.sub = _get_subs_id(good_sjson.name)
|
||||
text, format, mime_type = self.item.get_transcript(format="txt")
|
||||
text, filename, mime_type = self.item.get_transcript("txt")
|
||||
expected_text = textwrap.dedent("""\
|
||||
Hi, welcome to Edx.
|
||||
Let's start with what is on your screen right now.""")
|
||||
|
||||
self.assertEqual(text, expected_text)
|
||||
self.assertEqual(filename, self.item.sub + '.txt')
|
||||
self.assertEqual(mime_type, 'text/plain')
|
||||
|
||||
def test_not_found_error(self):
|
||||
def test_en_with_empty_sub(self):
|
||||
|
||||
# no self.sub, self.youttube_1_0 exist, but no file in assets
|
||||
with self.assertRaises(NotFoundError):
|
||||
self.item.get_transcript()
|
||||
|
||||
# no self.sub and no self.youtube_1_0
|
||||
self.item.youtube_id_1_0 = None
|
||||
with self.assertRaises(ValueError):
|
||||
self.item.get_transcript()
|
||||
|
||||
# no self.sub but youtube_1_0 exists with file in assets
|
||||
good_sjson = _create_file(content=textwrap.dedent("""\
|
||||
{
|
||||
"start": [
|
||||
270,
|
||||
2720
|
||||
],
|
||||
"end": [
|
||||
2720,
|
||||
5430
|
||||
],
|
||||
"text": [
|
||||
"Hi, welcome to Edx.",
|
||||
"Let's start with what is on your screen right now."
|
||||
]
|
||||
}
|
||||
"""))
|
||||
_upload_sjson_file(good_sjson, self.item.location)
|
||||
self.item.youtube_id_1_0 = _get_subs_id(good_sjson.name)
|
||||
|
||||
text, filename, mime_type = self.item.get_transcript()
|
||||
expected_text = textwrap.dedent("""\
|
||||
0
|
||||
00:00:00,270 --> 00:00:02,720
|
||||
Hi, welcome to Edx.
|
||||
|
||||
1
|
||||
00:00:02,720 --> 00:00:05,430
|
||||
Let's start with what is on your screen right now.
|
||||
|
||||
""")
|
||||
|
||||
self.assertEqual(text, expected_text)
|
||||
self.assertEqual(filename, self.item.youtube_id_1_0 + '.srt')
|
||||
self.assertEqual(mime_type, 'application/x-subrip')
|
||||
|
||||
def test_non_en(self):
|
||||
self.item.transcript_language = 'uk'
|
||||
self.non_en_file.seek(0)
|
||||
_upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
|
||||
|
||||
text, filename, mime_type = self.item.get_transcript()
|
||||
expected_text = textwrap.dedent("""
|
||||
0
|
||||
00:00:00,12 --> 00:00:00,100
|
||||
Привіт, edX вітає вас.
|
||||
""")
|
||||
self.assertEqual(text, expected_text)
|
||||
self.assertEqual(filename, os.path.split(self.non_en_file.name)[1])
|
||||
self.assertEqual(mime_type, 'application/x-subrip')
|
||||
|
||||
def test_value_error(self):
|
||||
good_sjson = _create_file(content='bad content')
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class TestVideoYouTube(TestVideo):
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': 'en',
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English", "uk": "Ukrainian"}',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
@@ -51,6 +51,7 @@ class TestVideoYouTube(TestVideo):
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/available_translations',
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
|
||||
@@ -106,7 +107,7 @@ class TestVideoNonYouTube(TestVideo):
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': 'en',
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
@@ -143,6 +144,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
{track}
|
||||
{transcripts}
|
||||
</video>
|
||||
"""
|
||||
|
||||
@@ -152,24 +154,35 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'track': u'<track src="http://www.example.com/track"/>',
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'expected_track_url': u'http://www.example.com/track',
|
||||
'transcripts': '',
|
||||
},
|
||||
{
|
||||
'download_track': u'true',
|
||||
'track': u'',
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'expected_track_url': u'a_sub_file.srt.sjson',
|
||||
'transcripts': '',
|
||||
},
|
||||
{
|
||||
'download_track': u'true',
|
||||
'track': u'',
|
||||
'sub': u'',
|
||||
'expected_track_url': None
|
||||
'expected_track_url': None,
|
||||
'transcripts': '',
|
||||
},
|
||||
{
|
||||
'download_track': u'false',
|
||||
'track': u'<track src="http://www.example.com/track"/>',
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'expected_track_url': None,
|
||||
'transcripts': '',
|
||||
},
|
||||
{
|
||||
'download_track': u'true',
|
||||
'track': u'',
|
||||
'sub': u'',
|
||||
'expected_track_url': u'a_sub_file.srt.sjson',
|
||||
'transcripts': '<transcript language="uk" src="ukrainian.srt" />',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -201,7 +214,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
DATA = SOURCE_XML.format(
|
||||
download_track=data['download_track'],
|
||||
track=data['track'],
|
||||
sub=data['sub']
|
||||
sub=data['sub'],
|
||||
transcripts=data['transcripts'],
|
||||
)
|
||||
|
||||
self.initialize_module(data=DATA)
|
||||
@@ -213,8 +227,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
|
||||
expected_context.update({
|
||||
'transcript_download_format': None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
'transcript_language': 'en',
|
||||
'transcript_languages': '{"en": "English"}' if not data['transcripts'] else '{"uk": "Ukrainian"}',
|
||||
'transcript_language': u'en' if not data['transcripts'] or data.get('sub') else u'uk',
|
||||
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
|
||||
self.item_descriptor, 'transcript'
|
||||
).rstrip('/?') + '/translation',
|
||||
@@ -312,7 +326,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
|
||||
'transcript_download_format': 'srt',
|
||||
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
|
||||
'transcript_language': 'en',
|
||||
'transcript_language': u'en',
|
||||
'transcript_languages': '{"en": "English"}',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user