返回首頁 

Greasy Fork is available in English.

Amazon Video - subtitle downloader

Allows you to download subtitles from Amazon Video


安装此脚本?
// ==UserScript==// @name        Amazon Video - subtitle downloader// @description Allows you to download subtitles from Amazon Video// @license     MIT// @version     1.9.15// @namespace   tithen-firion.github.io// @match       https://*.amazon.com/*// @match       https://*.amazon.de/*// @match       https://*.amazon.co.uk/*// @match       https://*.amazon.co.jp/*// @match       https://*.primevideo.com/*// @grant       unsafeWindow// @grant       GM.xmlHttpRequest// @grant       GM_xmlhttpRequest// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js// @require     https://cdn.jsdelivr.net/gh/Tithen-Firion/UserScripts@7bd6406c0d264d60428cfea16248ecfb4753e5e3/libraries/xhrHijacker.js?version=1.0// @require     https://cdn.jsdelivr.net/gh/Stuk/jszip@579beb1d45c8d586d8be4411d5b2e48dea018c06/dist/jszip.min.js?version=3.1.5// @require     https://cdn.jsdelivr.net/gh/eligrey/FileSaver.js@283f438c31776b622670be002caf1986c40ce90c/dist/FileSaver.min.js?version=2018-12-29// ==/UserScript==class ProgressBar {constructor() {let container = document.querySelector('#userscript_progress_bars');if(container === null) {container = document.createElement('div');container.id = 'userscript_progress_bars'document.body.appendChild(container)container.stylecontainer.style.position = 'fixed';container.style.top = 0;container.style.left = 0;container.style.width = '100%';container.style.background = 'red';container.style.zIndex = '99999999';}self.container = container;}init() {this.current = 0;this.max = 0;this.progressElement = document.createElement('div');this.progressElement.style.width = 0;this.progressElement.style.height = '10px';this.progressElement.style.background = 'green';self.container.appendChild(this.progressElement);}increment() {this.current += 1;if(this.current <= this.max)this.progressElement.style.width = this.current / this.max * 100 + '%';}incrementMax() {this.max += 1;if(this.current <= this.max)this.progressElement.style.width = this.current / this.max * 100 + '%';}destroy() {this.progressElement.remove();}}var progressBar = new ProgressBar();// add CSS stylevar s = document.createElement('style');s.innerHTML = `p.download {text-align: center;grid-column: 1/-1;}p.download:hover {cursor: pointer;}`;document.head.appendChild(s);// XML to SRTfunction parseTTMLLine(line, parentStyle, styles) {const topStyle = line.getAttribute('style') || parentStyle;let prefix = '';let suffix = '';let italic = line.getAttribute('tts:fontStyle') === 'italic';let bold = line.getAttribute('tts:fontWeight') === 'bold';let ruby = line.getAttribute('tts:ruby') === 'text';if(topStyle !== null) {italic = italic || styles[topStyle][0];bold = bold || styles[topStyle][1];ruby = ruby || styles[topStyle][2];}if(italic) {prefix = '<i>';suffix = '</i>';}if(bold) {prefix += '<b>';suffix = '</b>' + suffix;}if(ruby) {prefix += '(';suffix = ')' + suffix;}let r###lt = '';for(const node of line.childNodes) {if(node.nodeType === Node.ELEMENT_NODE) {const tagName = node.tagName.split(':').pop().toUpperCase();if(tagName === 'BR') {r###lt += '\n';}else if(tagName === 'SPAN') {r###lt += parseTTMLLine(node, topStyle, styles);}else {console.log('unknown node:', node);throw 'unknown node';}}else if(node.nodeType === Node.TEXT_NODE) {r###lt += prefix + node.textContent + suffix;}}return r###lt;}function xmlToSrt(xmlString, lang) {try {let parser = new DOMParser();var xmlDoc = parser.parseFromString(xmlString, 'text/xml');const styles = {};for(const style of xmlDoc.querySelectorAll('head styling style')) {const id = style.getAttribute('xml:id');if(id === null) throw "style ID not found";const italic = style.getAttribute('tts:fontStyle') === 'italic';const bold = style.getAttribute('tts:fontWeight') === 'bold';const ruby = style.getAttribute('tts:ruby') === 'text';styles[id] = [italic, bold, ruby];}const regionsTop = {};for(const style of xmlDoc.querySelectorAll('head layout region')) {const id = style.getAttribute('xml:id');if(id === null) throw "style ID not found";const origin = style.getAttribute('tts:origin') || "0% 80%";const position = parseInt(origin.match(/\s(\d+)%/)[1]);regionsTop[id] = position < 50;}const topStyle = xmlDoc.querySelector('body').getAttribute('style');console.log(topStyle, styles, regionsTop);const lines = [];const textarea = document.createElement('textarea');let i = 0;for(const line of xmlDoc.querySelectorAll('body p')) {let parsedLine = parseTTMLLine(line, topStyle, styles);if(parsedLine != '') {if(lang.indexOf('ar') == 0)parsedLine = parsedLine.replace(/^(?!\u202B|\u200F)/gm, '\u202B');textarea.innerHTML = parsedLine;parsedLine = textarea.value;parsedLine = parsedLine.replace(/\n{2,}/g, '\n');const region = line.getAttribute('region');if(regionsTop[region] === true) {parsedLine = '{\\an8}' + parsedLine;}lines.push(++i);lines.push((line.getAttribute('begin') + ' --> ' + line.getAttribute('end')).replace(/\./g,','));lines.push(parsedLine);lines.push('');}}return lines.join('\n');}catch(e) {console.error(e);alert('Failed to parse XML subtitle file, see browser console for more details');return null;}}function sanitizeTitle(title) {return title.replace(/[:*?"<>|\\\/]+/g, '_').replace(/ /g, '.');}// download subs and save themfunction downloadSubs(url, title, downloadVars, lang) {GM.xmlHttpRequest({url: url,method: 'get',onload: function(resp) {progressBar.increment();var srt = xmlToSrt(resp.responseText, lang);if(srt === null) {srt = resp.responseText;title = title.replace(/\.[^\.]+$/, '.ttml2');}if(downloadVars) {downloadVars.zip.file(title, srt);--downloadVars.subCounter;if((downloadVars.subCounter|downloadVars.infoCounter) === 0)downloadVars.zip.generateAsync({type:"blob"}).then(function(content) {saveAs(content, sanitizeTitle(downloadVars.title) + '.zip');progressBar.destroy();});}else {var blob = new Blob([srt], {type: 'text/plain;charset=utf-8'});saveAs(blob, title, true);progressBar.destroy();}}});}// download episodes/movie info and start downloading subsfunction downloadInfo(url, downloadVars) {var req = new XMLHttpRequest();req.open('get', url);req.withCredentials = true;req.onload = function() {var info = JSON.parse(req.response);try {var catalogMetadata = info.catalogMetadata;if(typeof catalogMetadata === 'undefined')catalogMetadata = {catalog:{type: 'MOVIE', title: info.returnedTitleRendition.asin}};var epInfo = catalogMetadata.catalog;var ep = epInfo.episodeNumber;var title, season;if(epInfo.type == 'MOVIE' || ep === 0) {title = epInfo.title;downloadVars.title = title;}else {info.catalogMetadata.family.tvAncestors.forEach(function(tvAncestor) {switch(tvAncestor.catalog.type) {case 'SEASON':season = tvAncestor.catalog.seasonNumber;break;case 'SHOW':title = tvAncestor.catalog.title;break;}});title += '.S' + season.toString().padStart(2, '0');if(downloadVars.type === 'all')downloadVars.title = title;title += 'E' + ep.toString().padStart(2, '0');if(downloadVars.type === 'one')downloadVars.title = title;title += '.' + epInfo.title;}title = sanitizeTitle(title);title += '.WEBRip.Amazon.';var languages = new Set();var forced = info.forcedNarratives || [];forced.forEach(function(forcedInfo) {forcedInfo.languageCode += '-forced';});var subs = (info.subtitleUrls || []).concat(forced);subs.forEach(function(subInfo) {let lang = subInfo.languageCode;if(subInfo.type === 'subtitle' || subInfo.type === 'subtitle') {}else if(subInfo.type === 'shd')lang += '[cc]';elselang += `[${subInfo.type}]`;if(languages.has(lang)) {let index = 0;let newLang;do {newLang = `${lang}_${++index}`;} while(languages.has(newLang));lang = newLang;}languages.add(lang);++downloadVars.subCounter;progressBar.incrementMax();downloadSubs(subInfo.url, title + lang + '.srt', downloadVars, lang);});}catch(e) {console.log(info);alert(e);}if(--downloadVars.infoCounter === 0 && downloadVars.subCounter === 0) {alert("No subs found, make sure you're logged in and you have access to watch this video!");progressBar.destroy();}};req.send(null);}function downloadThis(e) {progressBar.init();var id = e.target.getAttribute('data-id');var downloadVars = {type: 'one',subCounter: 0,infoCounter: 1,zip: new JSZip()};downloadInfo(gUrl + id, downloadVars);}function downloadAll(e) {progressBar.init();var IDs = e.target.getAttribute('data-id').split(';');var downloadVars = {type: 'all',subCounter: 0,infoCounter: IDs.length,zip: new JSZip()};IDs.forEach(function(id) {downloadInfo(gUrl + id, downloadVars);});}// remove unnecessary parameters from URLfunction parseURL(url) {var filter = ['consumptionType', 'deviceID', 'deviceTypeID', 'firmware', 'gascEnabled', 'marketplaceID', 'userWatchSessionId', 'videoMaterialType', 'clientId', 'operatingSystemName', 'operatingSystemVersion', 'customerID', 'token'];var urlParts = url.split('?');var params = ['desiredResources=CatalogMetadata%2CSubtitleUrls%2CForcedNarratives'];urlParts[1].split('&').forEach(function(param) {var p = param.split('=');if(filter.indexOf(p[0]) > -1)params.push(param);});params.push('resourceUsage=CacheResources');params.push('titleDecorationScheme=primary-content');params.push('subtitleFormat=TTMLv2');params.push('asin=');urlParts[1] = params.join('&');return urlParts.join('?');}function createDownloadButton(id, type) {var p = document.createElement('p');p.classList.add('download');p.setAttribute('data-id', id);p.innerHTML = 'Download subs for this ' + type;p.addEventListener('click', (type == 'season' ? downloadAll : downloadThis));return p;}function getArgs(a) {return a.initArgs || a.args;}function findMovieID() {let movieId;for(const templateElement of document.querySelectorAll('script[type="text/template"]')) {let data;try {data = JSON.parse(templateElement.innerHTML);}catch(ignore) {continue;}for(let i = 0; i < 3; ++i) {try {if(i === 0) {movieId = getArgs(getArgs(data).apexes[0]).titleID;}else if(i === 1) {movieId = getArgs(data).titleID;}else if(i === 2) {movieId = getArgs(data.props.body[0]).titleID;}if(typeof movieId !== "undefined") {return movieId;}}catch(ignore) {}}}for(const name of ["titleId", "titleID"]) {try {movieId = document.querySelector(`input[name="${name}"]`).value;if(typeof movieId !== "undefined" && movieId !== "") {return movieId;}}catch(ignore) {}}throw Error("Couldn't find movie ID");}function allLoaded(resolve, epCount) {if(epCount !== document.querySelectorAll('.js-node-episode-container, li[id^=av-ep-episodes-], li[id^=av-ep-episode-]').length)resolve();elsewindow.setTimeout(allLoaded, 200, resolve, epCount);}function manualShowAll(resolve) {alert("Some episodes are not loaded yet! Scroll to the bottom of the page to load them."+ "\n\n"+ "Once all episodes are loaded - click on the button at the bottom of your screen.");const btn = document.createElement("div");btn.innerHTML = "Click here after all episodes load";btn.style.position = "fixed";btn.style.bottom = "0";btn.style.left = "0";btn.style.padding = "10px";btn.style.zIndex = "999999";btn.style.background = "white";btn.addEventListener("click", () => {btn.remove();resolve();});document.body.append(btn);}function showAll() {return new Promise(resolve => {for(const templateElement of document.querySelectorAll('script[type="text/template"]')) {let data;if(templateElement.innerHTML.includes("NextPage")) {manualShowAll(resolve);return;}}let btn = document.querySelector('[data-automation-id="ep-expander"]');if(btn === null)resolve();let epCount = document.querySelectorAll('.js-node-episode-container, li[id^=av-ep-episodes-], li[id^=av-ep-episode-]').length;btn.click();allLoaded(resolve, epCount);});}// add download buttonsasync function init(url) {initialied = true;gUrl = parseURL(url);console.log(gUrl);await showAll();let button;let epElems = document.querySelectorAll('.dv-episode-container, .avu-context-card, .js-node-episode-container, li[id^=av-ep-episodes-], li[id^=av-ep-episode-]');if(epElems.length > 0) {let IDs = [];for(let i=epElems.length; i--; ) {let selector, id, el;if((el = epElems[i].querySelector('input[name="highlight-list-selector"]')) !== null) {id = el.id.replace('selector-', '');selector = '.js-episode-offers';}else if((el = epElems[i].querySelector('input[name="ep-list-selector"]')) !== null) {id = el.value;selector = '.av-episode-meta-info';}else if(id = epElems[i].getAttribute('data-aliases'))selector = '.dv-el-title';elsecontinue;id = id.split(',')[0];epElems[i].querySelector(selector).parentNode.appendChild(createDownloadButton(id, 'episode'));IDs.push(id);}button = createDownloadButton(IDs.join(';'), 'season');}else {let id = findMovieID();id = id.split(',')[0];button = createDownloadButton(id, 'movie');}document.querySelector('.dv-node-dp-badges, .av-badges').appendChild(button);}var initialied = false, gUrl;// hijack xhr, we need to find out tokens and other parameters needed for subtitle infoxhrHijacker(function(xhr, id, origin, args) {if(!initialied && origin === 'open')if(args[1].indexOf('/GetPlaybackResources') > -1) {init(args[1]).catch(error => {console.log(error);alert(`subtitle downloader error: ${error.message}`);});}});