Disney+ Subtitles Downloader

Download subtitles from Disney+

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           Disney+ Subtitles Downloader
// @name:fr        Disney+ Subtitles Downloader
// @namespace      https://greasyfork.org/users/572942-stegner
// @homepage       https://greasyfork.org/scripts/404223-disney-subtitles-downloader
// @description    Download subtitles from Disney+
// @description:fr Télécharger les sous-titres de Disney+
// @version        2.15
// @author         stegner
// @license        MIT; https://opensource.org/licenses/MIT
// @match          https://www.disneyplus.com/*
// @grant          none
// @require        https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// @require        https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
// @run-at         document-start
// ==/UserScript==

(function(open, send) {
    'use strict';
    var debug = (location.hash=="#debug");
    debuglog("Script loaded : Disney+ Subtitles Downloader");

    function init(){
        debuglog("Document state : "+document.readyState);
        if (document.readyState == "complete" || document.readyState == "loaded"){
            start();
            debuglog("Already loaded");
        }
        else {
            if (window.addEventListener) {
                window.addEventListener("load", start, false);
                debuglog("Onload method : addEventListener");
            } else if (window.attachEvent) {
                window.attachEvent("onload", start);
                debuglog("Onload method : attachEvent");
            } else {
                window.onload = start;
                debuglog("Onload method : onload");
            }
        }
        document.listen=true;
    }

    function start(){
        debuglog("start");
        if (typeof document.initaudio !== "undefined") {
            document.initaudio();
        }
        if (typeof document.initsub !== "undefined") {
            document.initsub();
        }
        listensend();
        document.handleinterval = setInterval(buttonhandle,100);
    }

    if(!document.listen){
        init();
    }

    document.initsub = function(){
        debuglog("initsub");
        document.langs = [];
        document.segments = "";
        document.wait=false;
        document.m3u8found=false;
        document.url=null;
        document.oldlocation=null;
        document.filename="";
        document.episode="";
        document.downloadall=false;
        document.downloadid=0;
        document.waitsub=false;
        document.segid=0;
        document.vttlist=[];

        // Add download icon
        document.styleSheets[0].addRule('#subtitleTrackPicker > div:before','content:"";color:#fff;padding-right:25px;padding-top:2px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAIGNIUk0AAHonAACAgwAA+mQAAIDSAAB2hgAA7OkAADmeAAAV/sZ+0zoAAAE4SURBVHja1JS7LkRRFIa/M6aYRCEuCUEUgihFBolGVGqiFY1ConfpNB7CiygUGm8hOiMukwiCCMl8mj2xc5yZM8M0/mTlrLP2v75zydo7UclRL3AGlIAl4L6ZuUC+5oEZYBoo55lbAdai/LPTwFongG3pfwI3gZ3ovhjlXVG+BWz/6FbjKPuto1CbjWoLobYf1RZjRho4pt5F5g11QK2F6FFXo/UXdbwZEHVQvY2aztWPECdR/TkNawREHUpB03pSJ7J6Cf9gL3xOvDiiXmfAHtSplLek7qorqI/BeJjxxFG1kgNDPQjrn4VoLPozRqgCzAGXwFXILzJ8w+H6XgRegW7grcGs3gCTOfP8UgfGg139wwapxrugDl0H+oCkTZjAcsiTxBaO7HZUBI6BtfCmv4Un4aw8/RoA7wq6AO4uOhAAAAAASUVORK5CYII=) no-repeat right;width:20px;height:20px;position:absolute;top:6px;right:10px;opacity:0.6;cursor:pointer;');
        document.styleSheets[0].addRule('#subtitleTrackPicker > div:hover:before','opacity:1;');
        document.styleSheets[0].addRule('#subtitleTrackPicker > div:first-child:before','content:"All";');
    };

    // Catch M3U8 files
    function listensend(){
        debuglog("listensend");

        var newOpen = function(...args) {
            if(!document.m3u8found && args.length>=2){
                if(args[1].indexOf(".m3u8")>0 && document.url!=args[1]) {
                    // m3u8 url
                    debuglog("m3u8 found : "+args[1]);
                    document.url = args[1];
                    document.langs = [];
                    document.baseurl=document.url.substring(0,document.url.lastIndexOf('/')+1);
                    document.m3u8found=true;
                    getpagecontent(m3u8loaded,document.url);
                }
            }

            open.call(this,...args);
        }

        var newSend = function(...args) {
            if(args[0] && args[0].match && args[0].match(/globalization/)){
                this.addEventListener('readystatechange', function(e) {
                    try {
                        document.globalization = JSON.parse(e.target.response).data.globalization;
                    } catch(e) {}
                }, false);
            }
            send.call(this,...args);
        }

        if(typeof unsafeWindow !== "undefined"){
            debuglog("Window state : unsafe");
            var define = Object.defineProperty;
            define(unsafeWindow.XMLHttpRequest.prototype, "open", {value: exportFunction(newOpen, window)});
            define(unsafeWindow.XMLHttpRequest.prototype, "send", {value: exportFunction(newSend, window)});
        }
        else {
            debuglog("Window state : safe");
            XMLHttpRequest.prototype.open = newOpen;
            XMLHttpRequest.prototype.send = newSend;
        }
    }

    function m3u8loaded(response) {
        debuglog("m3u8loaded");
        if (typeof document.m3u8sub !== "undefined") {
            document.m3u8sub(response);
        }
        if (typeof document.m3u8audio !== "undefined") {
            document.m3u8audio(response);
        }
    }

    document.m3u8sub = function(response){
        var regexpm3u8 =/^#.{0,}GROUP-ID="sub-main".{0,}\.m3u8"$/gm;
        var regexpvtt = /^[\w-_\/]{0,}MAIN[\w-_\/]{0,}.vtt$/gm;
        var regexpvtt2 = /^[\w-_\/]{0,}.vtt$/gm;

        if(response.indexOf('#EXT-X-INDEPENDENT-SEGMENTS')>0){
            // sub infos
            var lines = response.match(regexpm3u8);
            lines.forEach(function(line) {
                var lang = linetoarray(line);
                lang.LOCALIZED = document.globalization.timedText.find(t => t.language == lang.LANGUAGE);
                document.langs.push(lang);
                debuglog("Sub found : "+lang.NAME);
            });
        }
        else if(response.indexOf('.vtt')>0) {
            // vtt urls
            debuglog("vtt found");
            var lines = response.match(regexpvtt);
            if(!lines){
                lines = response.match(regexpvtt2);
            }
            if(lines){
                lines.forEach(function(line) {
                    var url = document.baseurl;
                    var uri = document.langs[document.langid].URI;
                    url+=uri.substring(0,2);
                    if(line.indexOf("/")<0){
                       url+= uri.substring(2,uri.lastIndexOf("/")+1);
                    }
                    url+=line;
                    document.vttlist.push(url);
                });
            }
            else {
                alert("Unable to parse the m3u8 file, please report a bug for this video.");
            }
            
            if(document.vttlist.length>0){
                getsegment();
            }
            else {
                alert("Unknown error, please report a bug for this video.");
            }
        }
    }

    function vttloaded(response) {
        debuglog("vttloaded");
        // save segment
        document.segments+=response.substring(response.indexOf("-->")-13);
        document.segid++;
        if(document.segid<document.vttlist.length){
            getsegment();
        }
        else if(document.segments.length>0) {
            // export segments
            exportfile(vtttosrt(document.segments));
            document.segments="";
            document.vttlist=[];
            document.segid=0;
        }
        else {
            alert("Unknown error, please report a bug for this video.");
        }
    }

    function vtttosrt(vtt) {
        var lines = vtt.split(/\r\n|\r|\n/);
        var result = [];
        var subcount = 0;

        lines.forEach(function (line) {
            if(line.indexOf("-->") == 13) {
                subcount++;
                result.push(subcount);
                result.push(line.substring(0,29).replace(/[.]/g,','));
            }
            else if(subcount>0) {
                result.push(line.replace(/<\/?c(\.\w{1,})?>/g,'').replace(/&amp;/g,'&'));
            }
        });

        return result.join('\r\n');
    }

    function linetoarray(line) {
        var result = [];
        var values = line.split(',');
        values.forEach(function(value) {
            var data = value.replace(/\r\n|\r|\n/g,'').split('=');
            if(data.length>1) {
                var key = data[0];
                var content = data[1].replace(/"/g,'');
                result[key]=content;
            }
        });
        return result;
    }

    function buttonhandle() {
        var buttons = document.getElementsByClassName("control-icon-btn");
        if(buttons.length>0) {
            if (typeof document.clickhandlesub !== "undefined") {
                document.clickhandlesub();
            }
            if (typeof document.clickhandleaudio !== "undefined") {
                document.clickhandleaudio();
            }

            document.filename = document.getElementsByClassName("title-field")[0]?.innerText;
            if(document.getElementsByClassName("subtitle-field").length>0) {
                document.episode = document.getElementsByClassName("subtitle-field")[0]?.innerText
            }
        }

        if(document.oldlocation!=window.location.href&&document.oldlocation!=null) {
            // location changed
            document.m3u8found=false;
            document.langs = [];
            document.audios = [];
        }

        document.oldlocation=window.location.href;
    }

    document.clickhandlesub = function() {
        var picker = document.getElementsByClassName("options-picker subtitle-track-picker");
        if(picker && picker[0]) {
            picker[0].childNodes.forEach(function(child) {
                var element = child.childNodes[0];
                if(child.onclick==null) {
                    child.onclick = selectsub;
                }
            });
        }
    }

    function selectsub(e) {
        debuglog("selectsub");
        var width = this.offsetWidth;
        // Check click position
        if(e.layerX>=width-30&&e.layerX<=width-10&&e.layerY>=5&&e.layerY<=25){
            var lang = this.childNodes[0].childNodes[1].innerHTML;
            if(lang=="Off"){
                // Download all subs
                debuglog("Download all subs");
                document.zip = new JSZip();
                document.downloadall=true;
                document.downloadid=-1;
                downloadnext();
            }
            else {
                // Download sub
                document.downloadall=false;
                download(lang);
            }
            // Cancel selection
            return false;
        }
    }

    function downloadnext(){
        document.downloadid++;

        if(document.downloadid<document.langs.length){
            document.styleSheets[0].addRule('#subtitleTrackPicker > div:first-child:before','padding-right:35px;content:"'+Math.round((document.downloadid/document.langs.length)*100)+'%";');
            download(document.langs[document.downloadid].NAME,false,false);
        }
        else {
            debuglog("Subs downloaded");
            clearInterval(document.downloadinterval);
            document.styleSheets[0].addRule('#subtitleTrackPicker > div:first-child:before','padding-right:25px;content:"All";');

            debuglog("Save zip");
            document.zip.generateAsync({type:"blob"}).then(function(content) {
                var output = document.filename;
                if(document.episode!="") {
                    output+= " - "+document.episode.replace(':','');
                }
                saveAs(content, output+".zip");
            });
        }
    }

    function download(langname,withForced=true,localized=true) {
        if(!document.wait){
            debuglog("Download sub : "+langname);
            var language;
            var count=0;
            document.forced=false;
            document.langs.forEach(function(lang) {
                if(lang.NAME == langname || (localized && lang.LOCALIZED && Object.values(lang.LOCALIZED.renditions).includes(langname) && lang.FORCED=="NO")) {
                    language=lang.LANGUAGE;
                    document.langid=count;
                    getpagecontent(m3u8loaded,document.baseurl+lang.URI);
                    document.wait=true;
                }
                count++;
            });
            if(withForced)
            {
                count=0;
                var subid;
                document.langs.forEach(function(lang) {
                    if(lang.LANGUAGE==language && lang.NAME!=langname && lang.FORCED=="YES") {
                        subid=count;
                        document.waitsub=true;
                        document.waitInterval = setInterval(function () {
                            if(!document.wait) {
                                debuglog("Download forced : "+langname);
                                clearInterval(document.waitInterval);
                                document.langid=subid;
                                getpagecontent(m3u8loaded,document.baseurl+lang.URI);
                                document.wait=true;
                            }
                        },10);
                    }
                    count++;
                });
            }
            
            if(count==0){
                alert("An error has occurred, please reload the page.");
            }
        }
        
    }

    function getsegment() {
        debuglog("getsegment "+document.segid);
        getpagecontent(vttloaded,document.vttlist[document.segid]);
    }

    function exportfile(text) {
        debuglog("exportfile");
        var output = document.filename;
        if(document.episode!="") {
            output+= " - "+document.episode.replace(':','');
        }
        output += "."+document.langs[document.langid].LANGUAGE;
        if(document.langs[document.langid].FORCED=="YES") {
            output += ".forced";
            document.waitsub=false;
        }
        output += ".srt";

        if(document.downloadall){
            debuglog("Add to zip");
            document.zip.file(output, text);
            document.downloadinterval = setTimeout(function () { 
                document.wait = false;
                if(!document.waitsub){
                    downloadnext();
                }
            },20);
        }
        else {
            debuglog("Save sub");
            var hiddenElement = document.createElement('a');

            hiddenElement.href = 'data:attachment/text,' + encodeURI(text).replace(/#/g, '%23');
            hiddenElement.target = '_blank';
            hiddenElement.download = output;
            hiddenElement.click();
            setTimeout(function () { document.wait = false; },50);
        }
    }

    function getpagecontent(callback,url) {
        debuglog("Downloading : "+url);
        var http=new XMLHttpRequest();
        http.open("GET", url, true);
        http.onloadend = function() {
            if(http.readyState == 4 && http.status == 200) {
                callback(http.responseText);
            }
            else if (http.status === 404) {
                debuglog("Not found");
                callback("");
            }
            else {
                debuglog("Unknown error, retrying");
                setTimeout(function () { getpagecontent(callback,url); },100);
            }
        }
        http.send();
    }

    String.prototype.lpad = function(padString, length) {
        var str = this;
        while (str.length < length) {
            str = padString + str;
        }
        return str;
    }

    function debuglog(message){
        if(debug){
            console.log("%c [debug] "+message, 'background: #222; color: #bada55');
        }
    }
})(XMLHttpRequest.prototype.open, XMLHttpRequest.prototype.send);