Greasy Fork is available in English.

edX Downloader

Download edX course mp4 and srt in one click, and save them as same file name (except the file suffix). <https://github.com/jks-liu/edx-downloader.monkey.js>

// ==UserScript==
// @name         edX Downloader
// @name:zh-CN   edX网课下载器
// @name:zh-TW	 edX網課下載器
// @name:ja      edXダウンローダー
// @namespace    https://github.com/jks-liu/edx-downloader.monkey.js
// @supportURL   https://github.com/jks-liu/edx-downloader.monkey.js
// @version      2.0.1
// @description  Download edX course mp4 and srt in one click, and save them as same file name (except the file suffix). <https://github.com/jks-liu/edx-downloader.monkey.js>
// @description:zh-CN  一键下载edX网课视频和字幕,并保存为相同的文件名(除了文件后缀)。<https://github.com/jks-liu/edx-downloader.monkey.js>
// @description:zh-TW  一鍵下載edX網課視頻和字幕,並保存爲相同的文件名(除了文件後綴)。<https://github.com/jks-liu/edx-downloader.monkey.js>
// @description:ja     edXオンラインコースのビデオと字幕をワンクリックでダウンロードし、同じファイル名で保存します(ファイル拡張子を除く)。<https://github.com/jks-liu/edx-downloader.monkey.js>
// @author       Jks Liu (https://github.com/jks-liu)
// @license      MIT
// @match        https://edx.org/*
// @match        https://www.edx.org/*
// @match        https://learning.edx.org/*
// @match        https://courses.edx.org/*
// @icon         https://www.google.com/s2/favicons?domain=edx.com
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

// https://stackoverflow.com/questions/14308588/simple-jquery-selector-only-selects-first-element-in-chrome
// If jQuery isn't present on the webpage, and of course no other code assigns something to $, Chrome's JS console assigns $ a shortcut to document.querySelector().
// You can achieve what you want with $$(), which is assigned by the console a shortcut to document.querySelectorAll().

// TODO: Add real title
// TODO: Download in floder
// TODO: label of already download
// TODO: Process bar
// TODO: Count of current course
// TODO: Download html
// TODO: Auto goto nextpage and download

// jks_ is the namespace

// The structure is as below
// learning.edx.org -> Containing title
//      ifram, courses.edx.org -> Containing video address


function jks_is_host(site) {
    return window.location.href.indexOf(site) != -1
}


function jks_learning_edu_org() {
    'use strict';

    // Original page has no jQuery
    // $$ can be used in broswer, but not here
    // below line from https://github.com/akhodakivskiy/VimFx/issues/841#issuecomment-263197162
    const $$ = (...args) => Array.from(document.querySelectorAll(...args));

    var checkExist = setInterval(function() {
        let all_titles = $$("ol.list-unstyled a");
        let all_buttons = $$("div.sequence-navigation-tabs > button");
        if (all_titles.length && all_buttons.length) {
            clearInterval(checkExist);

            /// Get course meta data
            let course_name = all_titles[1].text;
            let paragraph_name = all_titles[2].text;
        
            let all_sections = all_buttons.map(button=>button.title);
            let active_section_index = all_buttons.findIndex(button=>button.classList.contains("active"));
            let active_section = all_sections[active_section_index];
        
            console.log("jks edx names", course_name, paragraph_name, active_section);
            GM_setValue("names", [course_name, paragraph_name, active_section, active_section_index]);

            /// Register action to detect meta data change
            let class_context = $$("div.sequence-navigation-tabs")[0];
            // https://stackoverflow.com/questions/38861601/how-to-only-trigger-parent-click-event-when-a-child-is-clicked/38861760
            class_context.addEventListener('click', function(event) {
                let button = event.target;
                while (button.nodeName != "BUTTON") {
                    button = button.parentElement;
                }

                let new_section = button.title;
                let new_section_index = all_buttons.findIndex(b => b===button);
                GM_setValue("names", [course_name, paragraph_name, new_section, new_section_index]);
            }, true);
        }
     }, 100); // check every 100ms
}

function jks_courses_edx_org() {
    'use strict';

    // Original page has jQuery

    // The content is under an iframe
    // So @match is https://courses.edx.org, not https://learning.edx.org
    // Can not use `window.$` as below ajax, don't know why
    let video = $('.video-download-button')[0];
    let video_url = video.href;

    // Create a button
    let download_all_button = document.createElement("div");
    download_all_button.innerHTML = `
        <label>
            <input id="jks_checkbox_mp4" type="checkbox", checked=true>
            <span>*.mp4</span>
            <input id="jks_checkbox_srt" type="checkbox", checked=true>
            <span>*.srt</span>
            <input id="jks_checkbox_txt" type="checkbox">
            <span>*.txt</span>
        </label>
        <button id="jks_button">Download All</button>
        <form>
            <label for="jks_input_folder">Folder</label>
            <input type="text" id="jks_input_folder" name="jks_input_folder" size="72">
            <label for="jks_input_mp4">*.mp4 location:</label>
            <input type="text" id="jks_input_mp4" name="jks_input_mp4" size="72">
            <label for="jks_input_srt">*.srt location:</label>
            <input type="text" id="jks_input_srt" name="jks_input_srt" size="72">
            <label for="jks_input_txt">*.txt location:</label>
            <input type="text" id="jks_input_txt" name="jks_input_txt" size="72">
        </form> 
        `;
    // Video download event is at parent element 
    // So add button after parent, or it's button click will be hide by parent
    // Add 2 other parentElement to widen input box
    video.parentElement.parentElement.parentElement.append(download_all_button)

    // Cannot use $, don't know why
    let srt_url = document.querySelector("li.transcript-option a").href;

    // Use $ instead of window.$, previously window.$ is ok, don't know why
    $.ajax({
        url: document.querySelector("li.transcript-option a").href,
        type: "HEAD",
        success: function(res, status, xhr) {
            let srt_header = xhr.getResponseHeader("content-disposition");
            // attachment; filename="02_TinyML_C02_03-01-01-en.srt" => 02_TinyML_C02_03-01-01-en.srt
            // let srt_file_name = srt_header.slice(22, -1);
            // let mp4_file_name = srt_file_name.slice(0, -4)+".mp4";
            // let txt_file_name = srt_file_name.slice(0, -4)+".txt";

            let names = GM_getValue("names", ["edx-unknown-course", "edx-unknown-paragraph", "edx-unknown-section"]);
            let folder_name = names[0]+"/"+names[1]
            let base_name = String(names[3]).padStart(2, "0")+"-"+names[2];
            let mp4_file_name = base_name + ".mp4";
            let srt_file_name = base_name + ".srt";
            let txt_file_name = base_name + ".txt";
            $("#jks_input_folder")[0].value = folder_name;
            $("#jks_input_mp4")[0].value = mp4_file_name;
            $("#jks_input_srt")[0].value = srt_file_name;
            $("#jks_input_txt")[0].value = txt_file_name;

            $("#jks_button").click(function() {
                if ($("#jks_checkbox_mp4")[0].checked) {
                    // video is CORS (Cross-origin resource)
                    // Set tampermonkey `Download Mode` option to `Browser API`
                    // https://www.tampermonkey.net/faq.php?ext=dhdg#Q302
                    GM_download(video_url, $("#jks_input_folder")[0].value+"/"+$("#jks_input_mp4")[0].value);
                }

                if ($("#jks_checkbox_srt")[0].checked) {
                    GM_download(srt_url, $("#jks_input_folder")[0].value+"/"+$("#jks_input_srt")[0].value);
                }

                if ($("#jks_checkbox_txt")[0].checked) {
                // Txt and srt and the same file
                    GM_download(srt_url, $("#jks_input_folder")[0].value+"/"+$("#jks_input_txt")[0].value);
                }
            })
        }
    })
}

(function() {
    'use strict';

    if (jks_is_host("courses.edx.org")) {
        jks_courses_edx_org();
    }

    if (jks_is_host("learning.edx.org")) {
        jks_learning_edu_org();
    }
})();