// ==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/jszip@3.5.0/dist/jszip.min.js
// @require https://cdn.jsdelivr.net/npm/file-saver@2.0.2/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(/&/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);