Greasy Fork

Greasy Fork is available in English.

Khan Academy timestamps

Adds time duration labels near each lesson title

当前为 2016-06-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Khan Academy timestamps
// @namespace    https://www.khanacademy.org/
// @version      1.1
// @description  Adds time duration labels near each lesson title
// @author       nazikus
// @require      http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js
// @match        https://www.khanacademy.org/*/*/*
// @run-at       document-end
// ==/UserScript==
/* jshint -W097 */


(function() {
'use strict';

console.log = function() {};  // disable debugging logs

var logicSelector = function(){
  // CASE 1 - ToC page
  // dirty check if current path depth corresponds to page with a ToC
  // valid url example: https://www.khanacademy.org/math/linear-algebra/vectors-and-spaces
  if (window.location.href.split('/').length === 6){
    processTocPage();
  }
  // CASE 2 - if video lesson page
  else if ( (/\/v\/[\w\-]+$/.test(window.location.href)) )  {
    processLessonPage();
  }
  // Skip the rest
  else {
    console.log('No lessons to label here, skipping...');
  }
};

////////////////////////////////////////////////////////////////////////////////
// e.g. https://www.khanacademy.org/math/linear-algebra/vectors-and-spaces/vectors/v/linear-algebra-vector-examples
var processLessonPage = function() {
  console.log('Lesson page processing started...');
  var hrefs = $('div[class^="tutorialNavOnSide"] a');

  // create placeholder for Time Duration label of each lesson (master for cloning)
  var labelClass  = hrefs.find('div[class^="title"]').attr('class');
  var labelMaster = $('<span>', {class: labelClass}).css({'float':'left'}).text('[--:--]');

  // select module header (where module time label to be appended)
  var moduleLabelTarget = $('div[class^="navHeaderOnSide"]').eq(0);
  // create placholder for Module Time Duration label with cumulated time (master for cloning)
  var moduleLabelMaster = $('<span>').css({'float':'right'});

  var moduleCounter = moduleCounterFactory(hrefs.length, {
    targetEl: moduleLabelTarget,
    labelEl: moduleLabelMaster.clone(),
    title: moduleLabelTarget.find('a').text(),
  });

  hrefs.each( (function(labelToClone, modCounter) {
    return function(idx, lessonHref){
      var href = $(lessonHref);
      var labelTarget = href.find('div[class^="info"]');
      var lessonUrl = href.attr('href');

      // time duration label object
      var labelObject = {
        targetEl: labelTarget,
        labelEl: labelToClone.clone(),
        title: href.text()
      };

      // FETCH URL
      appendVideoDurationLabel(lessonUrl, labelObject, modCounter);
    };
  })(labelMaster, moduleCounter) );
};

////////////////////////////////////////////////////////////////////////////////
// e.g. https://www.khanacademy.org/math/linear-algebra/vectors-and-spaces
var processTocPage = function() {
  console.log('ToC processing starged...');

  // cleanup (while debugging)
  // $('span[class^="nodeTitle"]').remove();
  // $('span[style^="float: right"]').remove();

  // select topic modules, skip (slice) first div which is ToC
  var topicModules = $('div[class^="moduleList"] > div[class^="module"]').slice(1);

  // create placeholder for Time Duration label of each lesson (master for cloning)
  var labelClass = topicModules.find('> div > a div[class^="nodeTitle"]').attr('class');
  var labelColor = $('div[class^="header"][style^="background"').css('background-color');
  var labelMaster = $('<span>', {class: labelClass}).css({'float':'right', 'color': labelColor}).text('[--:--]');

  // create placholder for Module Time Duration label with cumulated time (master for cloning)
  var moduleLabelMaster = $('<span>').css({'float':'right'});

  // iterate over each topic module in a separate worker
  topicModules.each(function(){
    window.setTimeout(function(that, lMaster, mlMaster){
      var hrefs = $(that).find('> div > a');
      // get all hrefs links in current module
      console.log('hrefs: %d', hrefs.length);
      // get urls as string array (for debugging)
      // var urls = hrefs.map(function(){return this.href;}).get();

      // change dipslay alignment of divs containing time label
      hrefs.find('div[class^="nodeTitle"]').css('display', 'inline-block');

      // select module header (where module time label to be appended)
      var moduleLabelTarget = $(that).find('> h2 > div[style^="color:"]');

      // module time counter & label object
      var moduleCounter = moduleCounterFactory(hrefs.length, {
        targetEl: moduleLabelTarget,
        labelEl: mlMaster.clone(),
        title: moduleLabelTarget.find('a').text(),
      });

      // Info: extra closure here is to pass params into $.each()'s lambda
      hrefs.each( (function(labelToClone, modCounter){
        return function(idx, lessonHref){
          var href = $(lessonHref);
          var labelTarget = href.find('> div[class^="nodeInfo"]');
          var lessonUrl = href.attr('href');

          // time duration label object
          var labelObject = {
            targetEl: labelTarget,
            labelEl: labelToClone.clone(),
            title: href.text()
          };

          // FETCH URL
          appendVideoDurationLabel(lessonUrl, labelObject, modCounter);

        };})(lMaster, moduleCounter) ); // return hrefs.each()

    }, 0, this, labelMaster, moduleLabelMaster); // return window.setTimeout()

  }); // return topicModules.each();
};

////////////////////////////////////////////////////////////////////////////////
// starts requests (one for video page, and one for YouTube API),
// parses them out, caches, and appends corresponding time labels to DOM in async
var appendVideoDurationLabel = function(lessonUrl, labelObject, moduleCounter){

  // if url is not a video lesson (eg, exercise or read material)
  if ( !(/\/v\//.test(lessonUrl)) ) {
    // just append empty time duration and continue to the next link
    // labelObject.labelEl.text('[--:--]');
    labelObject.targetEl.append( labelObject.labelEl );
    moduleCounter.decSize(); // non-video lessons do not contribute to module time
    return ; // true - continue, false - break from $.each()
  }

  // check if lesson time duration is cached yet
  var cachedTd = localStorage.getItem(lessonUrl);
  if (cachedTd) {
      labelObject.labelEl.text( cachedTd.split('|')[1] );
      labelObject.targetEl.append( labelObject.labelEl );
      moduleCounter.addTime( ~~cachedTd.split('|')[0] );
      console.log('Cached: (%s) %s', labelObject.title, cachedTd.split('|')[1]);
      return ;
  }

  var api = 'https://www.googleapis.com/youtube/v3/videos?id={id}&part=contentDetails&key={yek}';
  var h = 'cmQyAhWMshjc2Go8HAnmOWhzauSnIkBfBySazIA';

  // get lesson page html in async request (inside a worker)
  var lessonHtml = $.ajax({
    url: lessonUrl,
    datatype: 'html',
    labObj: labelObject,
    modCounter: moduleCounter
  })
  .done(function(htmlData){
    // get youtube video id
    var videoId = $($.parseHTML(htmlData))
      .filter('meta[property="og:video"]').attr('content').split('/').pop();

    // perform async YouTube API call to get video duration
    $.ajax({
      url: api.replace('\x7b\x69\x64\x7d', videoId)
          .replace('\x7b\x79\x65\x6b\x7d', h.split('').reverse().join('')),
      datatype: 'json',
      lObj: this.labObj,
      mCounter: this.modCounter,
      vLessonUrl: this.url,
      success: function(jsonResponse){

        var duration = jsonResponse.items[0]
          .contentDetails.duration.match(/PT(\d+H)?(\d+M)?(\d+S)?/);
        var hours = (parseInt(duration[1]) || 0),
            minutes = (parseInt(duration[2]) || 0),
            seconds = (parseInt(duration[3]) || 0),
            totalSec = hours * 3600 + minutes * 60 + seconds,
            stamp = '[' + (duration[1] ? duration[1].slice(0,-1)+':' : '') +
              ('0'  + minutes).slice(-2)+':'+('0' + seconds).slice(-2) + ']';

        // attach cloned label to the DOM
        this.lObj.labelEl.text( stamp );
        this.lObj.targetEl.append( this.lObj.labelEl );

        // cache lesson time duration
        localStorage.setItem(this.vLessonUrl, totalSec+'|'+this.lObj.labelEl.text());
        console.log('(%s) %s. %s %s', this.mCounter.getLabel().title,
            this.mCounter.getCount(), this.lObj.title, stamp);

        // MODULE COUNTER
        this.mCounter.addTime( totalSec );
      },
      error: function(data) { console.error('YouTube API error:\n%s', data); }
    }); // YouTube $.ajax():success
  }) // lesson $.ajax().done()
  .fail(function(){ console.error('Could not retrieve URL: %s', this.url); });
};  // appendVideoDurationLabel()


// factory (closure) for counting processed lessons (hrefs), cummulating module
// total time and attaching corresponding time label to DOM.
// Invoked for each topic module separately
var moduleCounterFactory = function(moduleSize, moduleLabelObj){
  var totalSeconds = 0,
      count = 0,
      size = moduleSize,
      mlObj = moduleLabelObj;

  var getTimeStr = function() {
    var mHours = Math.floor(totalSeconds/60/60),
        mMinutes = Math.floor(totalSeconds/60) - mHours*60;
        return '[' + ('0'+mHours).slice(-2) + ':' +
          ('0'+mMinutes).slice(-2) + ':' +
          ('0'+totalSeconds%60).slice(-2) + ']';
  };

  var checkAndAttachToDom = function(){
    // if its the last lesson link to process in the module, then
    // insert module (total) time label near module title (target)
    if (count >= size){
      mlObj.labelEl.text( getTimeStr() );
      mlObj.targetEl.append( mlObj.labelEl );
    }
  };

  return {
    // some lessons are not video lessons, so skip those and decrease size
    getCount: function(){ return count; },
    getLabel: function(){ return moduleLabelObj; },
    decSize: function(){ size--; checkAndAttachToDom(); },
    addTime: function(seconds) {
      totalSeconds += seconds;
      count++;
      checkAndAttachToDom();
    },
  };
};

// START THE WHOLE THING
logicSelector();

})();

// alternative to YouTube API:
// http://stackoverflow.com/questions/30084140/youtube-video-title-with-api-v3-without-api-key