BLD-844: Add possibility to download transcripts in different formats.

This commit is contained in:
jmclaus
2014-02-25 09:38:24 +01:00
parent 2c9585ea5d
commit 3a740c0479
15 changed files with 1037 additions and 31 deletions

View File

@@ -0,0 +1,133 @@
$gray: rgb(127, 127, 127);
$blue: rgb(0, 159, 230);
$gray-d1: shade($gray,20%);
$gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%);
$blue-s1: saturate($blue,15%);
%use-font-awesome {
font-family: FontAwesome;
-webkit-font-smoothing: antialiased;
display: inline-block;
speak: none;
}
.a11y-menu-container {
position: relative;
&.open {
.a11y-menu-list {
display: block;
}
}
.a11y-menu-list {
top: 100%;
margin: 0;
padding: 0;
display: none;
position: absolute;
z-index: 10;
list-style: none;
background-color: $white;
border: 1px solid #eee;
li {
margin: 0;
padding: 0;
border-bottom: 1px solid #eee;
color: $white;
cursor: pointer;
a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: $gray-l2;
font-size: 14px;
line-height: 23px;
&:hover {
color: $gray-d1;
}
}
&.active{
a {
color: $blue;
}
}
&:last-child {
box-shadow: none;
border-bottom: 0;
margin-top: 0;
}
}
}
}
// Video track button specific styles
.video-tracks {
.a11y-menu-container {
display: inline-block;
vertical-align: top;
border-left: 1px solid #eee;
&.open {
> a {
background-color: $action-primary-active-bg;
color: $very-light-text;
&:after {
color: $very-light-text;
}
}
}
> a {
@include transition(all 0.25s ease-in-out 0s);
@include font-size(12);
display: block;
border-radius: 0 3px 3px 0;
background-color: $very-light-text;
padding: ($baseline*.75 $baseline*1.25 $baseline*.75 $baseline*.75);
color: $gray-l2;
min-width: 1.5em;
line-height: 14px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
&:after {
@extend %use-font-awesome;
content: "\f0d7";
position: absolute;
right: ($baseline*.5);
top: 33%;
color: $lighter-base-font-color;
}
}
.a11y-menu-list {
right: 0;
li {
font-size: em(14);
a {
border: 0;
display: block;
padding: lh(.5);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}

View File

@@ -46,13 +46,15 @@ div.video {
.video-sources,
.video-tracks {
display: inline-block;
vertical-align: top;
margin: ($baseline*.75) ($baseline/2) 0 0;
a {
> a {
@include transition(all 0.25s ease-in-out 0s);
@include font-size(14);
display: inline-block;
border-radius: 3px 3px 3px 3px;
line-height : 14px;
float: left;
border-radius: 3px;
background-color: $very-light-text;
padding: ($baseline*.75);
color: $lighter-base-font-color;
@@ -62,7 +64,14 @@ div.video {
color: $very-light-text;
}
}
}
.video-tracks {
> a {
border-radius: 3px 0 0 3px;
}
> a.external-track {
border-radius: 3px;
}
}
}

View File

@@ -69,6 +69,23 @@
</div>
<div class="focus_grabber last"></div>
<ul class="wrapper-downloads">
<li class="video-tracks">
<div class="a11y-menu-container">
<a class="a11y-menu-button" href="#" title=".srt">.srt</a>
<ol class="a11y-menu-list">
<li class="a11y-menu-item">
<a class="a11y-menu-item-link" href="#txt" title="Text (.txt) file" data-value="txt">Text (.txt) file</a>
</li>
<li class="a11y-menu-item active">
<a class="a11y-menu-item-link" href="#srt" title="SubRip (.srt) file" data-value="srt">SubRip (.srt) file</a>
</li>
</ol>
</div>
</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,307 @@
(function (undefined) {
describe('Video Accessible Menu', function () {
var state;
afterEach(function () {
$('source').remove();
state.storage.clear();
});
describe('constructor', function () {
describe('always', function () {
var videoTracks, container, button, menu, menuItems,
menuItemsLinks;
beforeEach(function () {
state = jasmine.initializePlayer();
videoTracks = $('li.video-tracks');
container = videoTracks.children('div.a11y-menu-container');
button = container.children('a.a11y-menu-button');
menuList = container.children('ol.a11y-menu-list');
menuItems = menuList.children('li.a11y-menu-item');
menuItemsLinks = menuItems.children('a.a11y-menu-item-link');
});
it('add the accessible menu', function () {
var activeMenuItem;
// Make sure we have the expected HTML structure:
// Menu container exists
expect(container.length).toBe(1);
// Only one button and one menu list per menu container.
expect(button.length).toBe(1);
expect(menuList.length).toBe(1);
// At least one menu item and one menu link per menu
// container. Exact length test?
expect(menuItems.length).toBeGreaterThan(0);
expect(menuItemsLinks.length).toBeGreaterThan(0);
expect(menuItems.length).toBe(menuItemsLinks.length);
// And one menu item is active
activeMenuItem = menuItems.filter('.active');
expect(activeMenuItem.length).toBe(1);
expect(activeMenuItem.children('a.a11y-menu-item-link'))
.toHaveData('value', 'srt');
expect(activeMenuItem.children('a.a11y-menu-item-link'))
.toHaveHtml('SubRip (.srt) file');
/* TO DO: Check that all the anchors contain correct text.
$.each(li.toArray().reverse(), function (index, link) {
expect($(link)).toHaveData(
'speed', state.videoSpeedControl.speeds[index]
);
expect($(link).find('a').text()).toBe(
state.videoSpeedControl.speeds[index] + 'x'
);
});
*/
});
it('add ARIA attributes to button, menu, and menu items links',
function () {
expect(button).toHaveAttrs({
'role': 'button',
'title': '.srt',
'aria-disabled': 'false'
});
expect(menuList).toHaveAttr('role', 'menu');
menuItemsLinks.each(function(){
expect($(this)).toHaveAttrs({
'role': 'menuitem',
'aria-disabled': 'false'
});
});
});
});
describe('when running', function () {
var videoTracks, container, button, menu, menuItems,
menuItemsLinks, KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', {keyCode: key});
},
tabBackPressEvent = function() {
return $.Event('keydown',
{keyCode: KEY.TAB, shiftKey: true});
},
tabForwardPressEvent = function() {
return $.Event('keydown',
{keyCode: KEY.TAB, shiftKey: false});
},
// Get previous element in array or cyles back to the last
// if it is the first.
previousSpeed = function(index) {
return speedEntries.eq(index < 1 ?
speedEntries.length - 1 :
index - 1);
},
// Get next element in array or cyles back to the first if
// it is the last.
nextSpeed = function(index) {
return speedEntries.eq(index >= speedEntries.length-1 ?
0 :
index + 1);
};
beforeEach(function () {
state = jasmine.initializePlayer();
videoTracks = $('li.video-tracks');
container = videoTracks.children('div.a11y-menu-container');
button = container.children('a.a11y-menu-button');
menuList = container.children('ol.a11y-menu-list');
menuItems = menuList.children('li.a11y-menu-item');
menuItemsLinks = menuItems.children('a.a11y-menu-item-link');
spyOn($.fn, 'focus').andCallThrough();
});
it('open/close the menu on mouseenter/mouseleave', function () {
container.mouseenter();
expect(container).toHaveClass('open');
container.mouseleave();
expect(container).not.toHaveClass('open');
});
it('do not close the menu on mouseleave if a menu item has ' +
'focus', function () {
// Open menu. Focus is on last menu item.
container.trigger(keyPressEvent(KEY.ENTER));
container.mouseenter().mouseleave();
expect(container).toHaveClass('open');
});
it('close the menu on click', function () {
container.mouseenter().click();
expect(container).not.toHaveClass('open');
});
it('close the menu on outside click', function () {
container.trigger(keyPressEvent(KEY.ENTER));
$(window).click();
expect(container).not.toHaveClass('open');
});
it('open the menu on ENTER keydown', function () {
container.trigger(keyPressEvent(KEY.ENTER));
expect(container).toHaveClass('open');
expect(menuItemsLinks.last().focus).toHaveBeenCalled();
});
it('open the menu on SPACE keydown', function () {
container.trigger(keyPressEvent(KEY.SPACE));
expect(container).toHaveClass('open');
expect(menuItemsLinks.last().focus).toHaveBeenCalled();
});
it('open the menu on UP keydown', function () {
container.trigger(keyPressEvent(KEY.UP));
expect(container).toHaveClass('open');
expect(menuItemsLinks.last().focus).toHaveBeenCalled();
});
it('close the menu on ESCAPE keydown', function () {
container.trigger(keyPressEvent(KEY.ESCAPE));
expect(container).not.toHaveClass('open');
});
it('UP and DOWN keydown function as expected on menu items',
function () {
// Iterate through list in both directions and check if
// things wrap up correctly.
var lastEntry = menuItemsLinks.length-1, i;
// First open menu
container.trigger(keyPressEvent(KEY.UP));
// Iterate with UP key until we have looped.
for (i = lastEntry; i >= 0; i--) {
menuItemsLinks.eq(i).trigger(keyPressEvent(KEY.UP));
}
// Iterate with DOWN key until we have looped.
for (i = 0; i <= lastEntry; i++) {
menuItemsLinks.eq(i).trigger(keyPressEvent(KEY.DOWN));
}
// Test if each element has been called twice.
expect($.fn.focus.calls.length)
.toEqual(2*menuItemsLinks.length+1);
});
it('ESC keydown on menu item closes menu', function () {
// First open menu. Focus is on last speed entry.
container.trigger(keyPressEvent(KEY.UP));
menuItemsLinks.last().trigger(keyPressEvent(KEY.ESCAPE));
// Menu is closed and focus has been returned to speed
// control.
expect(container).not.toHaveClass('open');
expect(container.focus).toHaveBeenCalled();
});
it('ENTER keydown on menu item selects its data and closes menu',
function () {
// First open menu.
container.trigger(keyPressEvent(KEY.UP));
// Focus on '.txt'
menuItemsLinks.eq(0).focus();
menuItemsLinks.eq(0).trigger(keyPressEvent(KEY.ENTER));
// Menu is closed, focus has been returned to container
// and file format is '.txt'.
/* TO DO
expect(container.focus).toHaveBeenCalled();
expect($('.video_speeds li[data-speed="1.50"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('1.50x');
*/
});
it('SPACE keydown on menu item selects its data and closes menu',
function () {
// First open menu.
container.trigger(keyPressEvent(KEY.UP));
// Focus on '.txt'
menuItemsLinks.eq(0).focus();
menuItemsLinks.eq(0).trigger(keyPressEvent(KEY.SPACE));
// Menu is closed, focus has been returned to container
// and file format is '.txt'.
/* TO DO
expect(speedControl.focus).toHaveBeenCalled();
expect($('.video_speeds li[data-speed="1.50"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('1.50x');
*/
});
// TO DO? No such behavior implemented.
xit('TAB + SHIFT keydown on speed entry closes menu and gives ' +
'focus to Play/Pause control', function () {
// First open menu. Focus is on last speed entry.
speedControl.trigger(keyPressEvent(KEY.UP));
speedEntries.last().trigger(tabBackPressEvent());
// Menu is closed and focus has been given to Play/Pause
// control.
expect(state.videoControl.playPauseEl.focus)
.toHaveBeenCalled();
});
// TO DO? No such behavior implemented.
xit('TAB keydown on speed entry closes menu and gives focus ' +
'to Volume control', function () {
// First open menu. Focus is on last speed entry.
speedControl.trigger(keyPressEvent(KEY.UP));
speedEntries.last().trigger(tabForwardPressEvent());
// Menu is closed and focus has been given to Volume
// control.
expect(state.videoVolumeControl.buttonEl.focus)
.toHaveBeenCalled();
});
});
});
// TODO
xdescribe('change file format', function () {
describe('when new file format is not the same', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
state.videoSpeedControl.setSpeed(1.0);
spyOn(state.videoPlayer, 'onSpeedChange').andCallThrough();
$('li[data-speed="0.75"] a').click();
});
it('trigger speedChange event', function () {
expect(state.videoPlayer.onSpeedChange).toHaveBeenCalled();
expect(state.videoSpeedControl.currentSpeed).toEqual(0.75);
});
});
});
// TODO
xdescribe('onSpeedChange', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
$('li[data-speed="1.0"] a').addClass('active');
state.videoSpeedControl.setSpeed(0.75);
});
it('set the new speed as active', function () {
expect($('.video_speeds li[data-speed="1.0"]'))
.not.toHaveClass('active');
expect($('.video_speeds li[data-speed="0.75"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('0.75x');
});
});
});
}).call(this);

View File

@@ -0,0 +1,308 @@
(function (requirejs, require, define) {
// VideoAccessibleMenu module.
define(
'video/035_video_accessible_menu.js',
[],
function () {
// VideoAccessibleMenu() function - what this module "exports".
return function (state) {
var dfd = $.Deferred();
if (state.el.find('li.video-tracks') === 0) {
dfd.resolve();
return dfd.promise();
}
state.videoAccessibleMenu = {
value: state.storage.getItem('transcript_download_format')
};
_initialize(state);
dfd.resolve();
return dfd.promise();
};
// ***************************************************************
// Private functions start here.
// ***************************************************************
function _initialize(state) {
_makeFunctionsPublic(state);
_renderElements(state);
_addAriaAttributes(state);
_bindHandlers(state);
}
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called,
// these functions will get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
changeFileType: changeFileType,
setValue: setValue
};
state.bindTo(methodsDict, state.videoAccessibleMenu, state);
}
// function _renderElements(state)
//
// Create any necessary DOM elements, attach them, and set their
// initial configuration. Also make the created DOM elements available
// via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects.
function _renderElements(state) {
// For the time being, we assume that the menu structure is present in
// the template HTML. In the future accessible menu plugin, everything
// inside <div class='menu-container'></div> will be generated in this
// file.
var container = state.el.find('li.video-tracks>div.a11y-menu-container'),
button = container.children('a.a11y-menu-button'),
menuList = container.children('ol.a11y-menu-list'),
menuItems = menuList.children('li.a11y-menu-item'),
menuItemsLinks = menuItems.children('a.a11y-menu-item-link'),
value = (function (val, activeElement) {
return val || activeElement.find('a').data('value') || 'srt';
}(state.videoAccessibleMenu.value, menuItems.filter('.active'))),
msg = '.' + value;
$.extend(state.videoAccessibleMenu, {
container: container,
button: button,
menuList: menuList,
menuItems: menuItems,
menuItemsLinks: menuItemsLinks
});
if (value) {
state.videoAccessibleMenu.setValue(value);
button.text(gettext(msg));
}
}
function _addAriaAttributes(state) {
var menu = state.videoAccessibleMenu;
menu.button.attr({
'role': 'button',
'aria-disabled': 'false'
});
menu.menuList.attr('role', 'menu');
menu.menuItemsLinks.each(function(){
$(this).attr({
'role': 'menuitem',
'aria-disabled': 'false'
});
});
}
// Get previous element in array or cyles back to the last if it is the
// first.
function _previousMenuItemLink(links, index) {
return $(links.eq(index < 1 ? links.length - 1 : index - 1));
}
// Get next element in array or cyles back to the first if it is the last.
function _nextMenuItemLink(links, index) {
return $(links.eq(index >= links.length - 1 ? 0 : index + 1));
}
function _menuItemsLinksFocused(menu) {
return menu.menuItemsLinks.is(':focus');
}
function _openMenu(menu, without_handler) {
// When menu items have focus, the menu stays open on
// mouseleave. A _closeMenuHandler is added to the window
// element to have clicks close the menu when they happen
// outside of it. We namespace the click event to easily remove it (and
// only it) in _closeMenu.
menu.container.addClass('open');
menu.button.text('...');
if (!without_handler) {
$(window).on('click.currentMenu', _closeMenuHandler.bind(menu));
}
// @TODO: onOpen callback
}
function _closeMenu(menu, without_handler) {
// Remove the previously added clickHandler from window element.
var msg = '.' + menu.value;
menu.container.removeClass('open');
menu.button.text(gettext(msg));
if (!without_handler) {
$(window).off('click.currentMenu');
}
// @TODO: onClose callback
}
function _openMenuHandler(event) {
_openMenu(this, true);
return false;
}
function _closeMenuHandler(event) {
// Only close the menu if no menu item link has focus or `click` event.
if (!_menuItemsLinksFocused(this) || event.type == 'click') {
_closeMenu(this, true);
}
return false;
}
function _toggleMenuHandler(event) {
if (this.container.hasClass('open')) {
_closeMenu(this, true);
} else {
_openMenu(this, true);
}
return false;
}
// Various event handlers. They all return false to stop propagation and
// prevent default behavior.
function _clickHandler(event) {
var target = $(event.currentTarget);
this.changeFileType.call(this, event);
_closeMenu(this, true);
return false;
}
function _keyDownHandler(event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode,
target = $(event.currentTarget),
index;
if (target.is('a.a11y-menu-item-link')) {
index = target.parent().index();
switch (keyCode) {
// Scroll up menu, wrapping at the top. Keep menu open.
case KEY.UP:
_previousMenuItemLink(this.menuItemsLinks, index).focus();
break;
// Scroll down menu, wrapping at the bottom. Keep menu
// open.
case KEY.DOWN:
_nextMenuItemLink(this.menuItemsLinks, index).focus();
break;
// Close menu.
case KEY.TAB:
_closeMenu(this);
// TODO
// What has to happen here? In speed menu, tabbing backward
// will give focus to Play/Pause button and tabbing
// forward to Volume button.
break;
// Close menu, give focus to button and change
// file type.
case KEY.ENTER:
case KEY.SPACE:
this.button.focus();
this.changeFileType.call(this, event);
_closeMenu(this);
break;
// Close menu and give focus to speed control.
case KEY.ESCAPE:
_closeMenu(this);
this.button.focus();
break;
}
return false;
}
else {
switch(keyCode) {
// Open menu and focus on last element of list above it.
case KEY.ENTER:
case KEY.SPACE:
case KEY.UP:
_openMenu(this);
this.menuItemsLinks.last().focus();
break;
// Close menu.
case KEY.ESCAPE:
_closeMenu(this);
break;
}
// We do not stop propagation and default behavior on a TAB
// keypress.
return event.keyCode === KEY.TAB;
}
}
/**
* @desc Bind any necessary function callbacks to DOM events (click,
* mousemove, etc.).
*
* @type {function}
* @access private
*
* @param {object} state The object containg the state of the video player.
* All other modules, their parameters, public variables, etc. are
* available via this object.
*
* @this {object} The global window object.
*
* @returns {undefined}
*/
function _bindHandlers(state) {
var menu = state.videoAccessibleMenu;
// Attach various events handlers to menu container.
menu.container.on({
'mouseenter': _openMenuHandler.bind(menu),
'mouseleave': _closeMenuHandler.bind(menu),
'click': _toggleMenuHandler.bind(menu),
'keydown': _keyDownHandler.bind(menu)
});
// Attach click and keydown event handlers to individual menu items.
menu.menuItems
.on('click', 'a.a11y-menu-item-link', _clickHandler.bind(menu))
.on('keydown', 'a.a11y-menu-item-link', _keyDownHandler.bind(menu));
}
function setValue(value) {
var menu = this.videoAccessibleMenu;
menu.value = value;
menu.menuItems
.removeClass('active')
.find("a[data-value='" + value + "']")
.parent()
.addClass('active');
}
// ***************************************************************
// Public functions start here.
// These are available via the 'state' object. Their context ('this'
// keyword) is the 'state' object. The magic private function that makes
// them available and sets up their context is makeFunctionsPublic().
// ***************************************************************
function changeFileType(event) {
var fileType = $(event.currentTarget).data('value');
this.videoAccessibleMenu.setValue(fileType);
this.saveState(true, {'transcript_download_format': fileType});
this.storage.setItem('transcript_download_format', fileType);
}
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));

View File

@@ -42,6 +42,7 @@ require(
[
'video/01_initialize.js',
'video/025_focus_grabber.js',
'video/035_video_accessible_menu.js',
'video/04_video_control.js',
'video/05_video_quality_control.js',
'video/06_video_progress_slider.js',
@@ -52,6 +53,7 @@ require(
function (
Initialize,
FocusGrabber,
VideoAccessibleMenu,
VideoControl,
VideoQualityControl,
VideoProgressSlider,
@@ -87,6 +89,7 @@ function (
state.modules = [
FocusGrabber,
VideoAccessibleMenu,
VideoControl,
VideoQualityControl,
VideoProgressSlider,

View File

@@ -13,6 +13,7 @@ in XML.
import json
import logging
from operator import itemgetter
from HTMLParser import HTMLParser
from lxml import etree
from pkg_resources import resource_string
@@ -155,6 +156,15 @@ class VideoFields(object):
scope=Scope.preferences,
default="en"
)
transcript_download_format = String(
help="Transcript file format to download by user.",
scope=Scope.preferences,
values=[
{"display_name": "SubRip (.srt) file", "value": "srt"},
{"display_name": "Text (.txt) file", "value": "txt"}
],
default='srt',
)
speed = Float(
help="The last speed that was explicitly set by user for the video.",
scope=Scope.user_state,
@@ -193,6 +203,7 @@ class VideoModule(VideoFields, XModule):
resource_string(module, 'js/src/video/025_focus_grabber.js'),
resource_string(module, 'js/src/video/02_html5_video.js'),
resource_string(module, 'js/src/video/03_video_player.js'),
resource_string(module, 'js/src/video/035_video_accessible_menu.js'),
resource_string(module, 'js/src/video/04_video_control.js'),
resource_string(module, 'js/src/video/05_video_quality_control.js'),
resource_string(module, 'js/src/video/06_video_progress_slider.js'),
@@ -202,20 +213,33 @@ class VideoModule(VideoFields, XModule):
resource_string(module, 'js/src/video/10_main.js')
]
}
css = {'scss': [resource_string(module, 'css/video/display.scss')]}
css = {'scss': [
resource_string(module, 'css/video/display.scss'),
resource_string(module, 'css/video/accessible_menu.scss'),
]}
js_module_name = "Video"
def handle_ajax(self, dispatch, data):
accepted_keys = ['speed', 'saved_video_position', 'transcript_language']
if dispatch == 'save_user_state':
accepted_keys = [
'speed', 'saved_video_position', 'transcript_language',
'transcript_download_format',
]
conversions = {
'speed': json.loads,
'saved_video_position': lambda v: RelativeTime.isotime_to_timedelta(v),
}
if dispatch == 'save_user_state':
for key in data:
if hasattr(self, key) and key in accepted_keys:
if key == 'saved_video_position':
relative_position = RelativeTime.isotime_to_timedelta(data[key])
self.saved_video_position = relative_position
if key in conversions:
value = conversions[key](data[key])
else:
setattr(self, key, json.loads(data[key]))
value = data[key]
setattr(self, key, value)
if key == 'speed':
self.global_speed = self.speed
@@ -228,6 +252,7 @@ class VideoModule(VideoFields, XModule):
def get_html(self):
track_url = None
transcript_download_format = self.transcript_download_format
get_ext = lambda filename: filename.rpartition('.')[-1]
sources = {get_ext(src): src for src in self.html5_sources}
@@ -241,7 +266,8 @@ class VideoModule(VideoFields, XModule):
if self.download_track:
if self.track:
track_url = self.track
elif self.sub:
transcript_download_format = None
elif self.sub or self.transcripts:
track_url = self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/download'
if not self.transcripts:
@@ -289,13 +315,15 @@ class VideoModule(VideoFields, XModule):
# configuration setting field.
'yt_test_timeout': 1500,
'yt_test_url': settings.YOUTUBE_TEST_URL,
'transcript_download_format': transcript_download_format,
'transcript_download_formats_list': self.descriptor.fields['transcript_download_format'].values,
'transcript_language': transcript_language,
'transcript_languages': json.dumps(sorted_languages),
'transcript_translation_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/translation',
'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/available_translations',
})
def get_transcript(self):
def get_transcript(self, format='srt'):
"""
Returns transcript in *.srt format.
@@ -307,12 +335,18 @@ class VideoModule(VideoFields, XModule):
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
str_subs = generate_srt_from_sjson(json.loads(data), speed=1.0)
if format == 'txt':
text = json.loads(data)['text']
str_subs = HTMLParser().unescape("\n".join(text))
mime_type = 'text/plain'
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')
raise ValueError
return str_subs
return str_subs, format, mime_type
@XBlock.handler
def transcript(self, request, dispatch):
@@ -350,7 +384,7 @@ class VideoModule(VideoFields, XModule):
elif dispatch == 'download':
try:
subs = self.get_transcript()
subs, format, mime_type = self.get_transcript(format=self.transcript_download_format)
except (NotFoundError, ValueError, KeyError):
log.debug("Video@download exception")
response = Response(status=404)
@@ -358,10 +392,13 @@ class VideoModule(VideoFields, XModule):
response = Response(
subs,
headerlist=[
('Content-Disposition', 'attachment; filename="{0}.srt"'.format(self.transcript_language)),
('Content-Disposition', 'attachment; filename="{filename}.{format}"'.format(
filename=self.transcript_language,
format=format,
)),
]
)
response.content_type = "application/x-subrip"
response.content_type = mime_type
elif dispatch == 'available_translations':
available_translations = []