Amazon Video - subtitle downloader

Allows you to download subtitles from Amazon Video

  1. // ==UserScript==
  2. // @name Amazon Video - subtitle downloader
  3. // @description Allows you to download subtitles from Amazon Video
  4. // @license MIT
  5. // @version 1.9.15
  6. // @namespace tithen-firion.github.io
  7. // @match https://*.amazon.com/*
  8. // @match https://*.amazon.de/*
  9. // @match https://*.amazon.co.uk/*
  10. // @match https://*.amazon.co.jp/*
  11. // @match https://*.primevideo.com/*
  12. // @grant unsafeWindow
  13. // @grant GM.xmlHttpRequest
  14. // @grant GM_xmlhttpRequest
  15. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  16. // @require https://cdn.jsdelivr.net/gh/Tithen-Firion/UserScripts@7bd6406c0d264d60428cfea16248ecfb4753e5e3/libraries/xhrHijacker.js?version=1.0
  17. // @require https://cdn.jsdelivr.net/gh/Stuk/jszip@579beb1d45c8d586d8be4411d5b2e48dea018c06/dist/jszip.min.js?version=3.1.5
  18. // @require https://cdn.jsdelivr.net/gh/eligrey/FileSaver.js@283f438c31776b622670be002caf1986c40ce90c/dist/FileSaver.min.js?version=2018-12-29
  19. // ==/UserScript==
  20.  
  21. class ProgressBar {
  22. constructor() {
  23. let container = document.querySelector('#userscript_progress_bars');
  24. if(container === null) {
  25. container = document.createElement('div');
  26. container.id = 'userscript_progress_bars'
  27. document.body.appendChild(container)
  28. container.style
  29. container.style.position = 'fixed';
  30. container.style.top = 0;
  31. container.style.left = 0;
  32. container.style.width = '100%';
  33. container.style.background = 'red';
  34. container.style.zIndex = '99999999';
  35. }
  36. self.container = container;
  37. }
  38.  
  39. init() {
  40. this.current = 0;
  41. this.max = 0;
  42.  
  43. this.progressElement = document.createElement('div');
  44. this.progressElement.style.width = 0;
  45. this.progressElement.style.height = '10px';
  46. this.progressElement.style.background = 'green';
  47.  
  48. self.container.appendChild(this.progressElement);
  49. }
  50.  
  51. increment() {
  52. this.current += 1;
  53. if(this.current <= this.max)
  54. this.progressElement.style.width = this.current / this.max * 100 + '%';
  55. }
  56.  
  57. incrementMax() {
  58. this.max += 1;
  59. if(this.current <= this.max)
  60. this.progressElement.style.width = this.current / this.max * 100 + '%';
  61. }
  62.  
  63. destroy() {
  64. this.progressElement.remove();
  65. }
  66. }
  67.  
  68. var progressBar = new ProgressBar();
  69.  
  70. // add CSS style
  71. var s = document.createElement('style');
  72. s.innerHTML = `
  73. p.download {
  74. text-align: center;
  75. grid-column: 1/-1;
  76. }
  77. p.download:hover {
  78. cursor: pointer;
  79. }
  80. `;
  81. document.head.appendChild(s);
  82.  
  83. // XML to SRT
  84. function parseTTMLLine(line, parentStyle, styles) {
  85. const topStyle = line.getAttribute('style') || parentStyle;
  86. let prefix = '';
  87. let suffix = '';
  88. let italic = line.getAttribute('tts:fontStyle') === 'italic';
  89. let bold = line.getAttribute('tts:fontWeight') === 'bold';
  90. let ruby = line.getAttribute('tts:ruby') === 'text';
  91. if(topStyle !== null) {
  92. italic = italic || styles[topStyle][0];
  93. bold = bold || styles[topStyle][1];
  94. ruby = ruby || styles[topStyle][2];
  95. }
  96.  
  97. if(italic) {
  98. prefix = '<i>';
  99. suffix = '</i>';
  100. }
  101. if(bold) {
  102. prefix += '<b>';
  103. suffix = '</b>' + suffix;
  104. }
  105. if(ruby) {
  106. prefix += '(';
  107. suffix = ')' + suffix;
  108. }
  109.  
  110. let result = '';
  111.  
  112. for(const node of line.childNodes) {
  113. if(node.nodeType === Node.ELEMENT_NODE) {
  114. const tagName = node.tagName.split(':').pop().toUpperCase();
  115. if(tagName === 'BR') {
  116. result += '\n';
  117. }
  118. else if(tagName === 'SPAN') {
  119. result += parseTTMLLine(node, topStyle, styles);
  120. }
  121. else {
  122. console.log('unknown node:', node);
  123. throw 'unknown node';
  124. }
  125. }
  126. else if(node.nodeType === Node.TEXT_NODE) {
  127. result += prefix + node.textContent + suffix;
  128. }
  129. }
  130.  
  131. return result;
  132. }
  133. function xmlToSrt(xmlString, lang) {
  134. try {
  135. let parser = new DOMParser();
  136. var xmlDoc = parser.parseFromString(xmlString, 'text/xml');
  137.  
  138. const styles = {};
  139. for(const style of xmlDoc.querySelectorAll('head styling style')) {
  140. const id = style.getAttribute('xml:id');
  141. if(id === null) throw "style ID not found";
  142. const italic = style.getAttribute('tts:fontStyle') === 'italic';
  143. const bold = style.getAttribute('tts:fontWeight') === 'bold';
  144. const ruby = style.getAttribute('tts:ruby') === 'text';
  145. styles[id] = [italic, bold, ruby];
  146. }
  147.  
  148. const regionsTop = {};
  149. for(const style of xmlDoc.querySelectorAll('head layout region')) {
  150. const id = style.getAttribute('xml:id');
  151. if(id === null) throw "style ID not found";
  152. const origin = style.getAttribute('tts:origin') || "0% 80%";
  153. const position = parseInt(origin.match(/\s(\d+)%/)[1]);
  154. regionsTop[id] = position < 50;
  155. }
  156.  
  157. const topStyle = xmlDoc.querySelector('body').getAttribute('style');
  158.  
  159. console.log(topStyle, styles, regionsTop);
  160.  
  161. const lines = [];
  162. const textarea = document.createElement('textarea');
  163.  
  164. let i = 0;
  165. for(const line of xmlDoc.querySelectorAll('body p')) {
  166. let parsedLine = parseTTMLLine(line, topStyle, styles);
  167. if(parsedLine != '') {
  168. if(lang.indexOf('ar') == 0)
  169. parsedLine = parsedLine.replace(/^(?!\u202B|\u200F)/gm, '\u202B');
  170.  
  171. textarea.innerHTML = parsedLine;
  172. parsedLine = textarea.value;
  173. parsedLine = parsedLine.replace(/\n{2,}/g, '\n');
  174.  
  175. const region = line.getAttribute('region');
  176. if(regionsTop[region] === true) {
  177. parsedLine = '{\\an8}' + parsedLine;
  178. }
  179.  
  180. lines.push(++i);
  181. lines.push((line.getAttribute('begin') + ' --> ' + line.getAttribute('end')).replace(/\./g,','));
  182. lines.push(parsedLine);
  183. lines.push('');
  184. }
  185. }
  186. return lines.join('\n');
  187. }
  188. catch(e) {
  189. console.error(e);
  190. alert('Failed to parse XML subtitle file, see browser console for more details');
  191. return null;
  192. }
  193. }
  194.  
  195. function sanitizeTitle(title) {
  196. return title.replace(/[:*?"<>|\\\/]+/g, '_').replace(/ /g, '.');
  197. }
  198.  
  199. // download subs and save them
  200. function downloadSubs(url, title, downloadVars, lang) {
  201. GM.xmlHttpRequest({
  202. url: url,
  203. method: 'get',
  204. onload: function(resp) {
  205.  
  206. progressBar.increment();
  207. var srt = xmlToSrt(resp.responseText, lang);
  208. if(srt === null) {
  209. srt = resp.responseText;
  210. title = title.replace(/\.[^\.]+$/, '.ttml2');
  211. }
  212. if(downloadVars) {
  213. downloadVars.zip.file(title, srt);
  214. --downloadVars.subCounter;
  215. if((downloadVars.subCounter|downloadVars.infoCounter) === 0)
  216. downloadVars.zip.generateAsync({type:"blob"})
  217. .then(function(content) {
  218. saveAs(content, sanitizeTitle(downloadVars.title) + '.zip');
  219. progressBar.destroy();
  220. });
  221. }
  222. else {
  223. var blob = new Blob([srt], {type: 'text/plain;charset=utf-8'});
  224. saveAs(blob, title, true);
  225. progressBar.destroy();
  226. }
  227.  
  228. }
  229. });
  230. }
  231.  
  232. // download episodes/movie info and start downloading subs
  233. function downloadInfo(url, downloadVars) {
  234. var req = new XMLHttpRequest();
  235. req.open('get', url);
  236. req.withCredentials = true;
  237. req.onload = function() {
  238. var info = JSON.parse(req.response);
  239. try {
  240. var catalogMetadata = info.catalogMetadata;
  241. if(typeof catalogMetadata === 'undefined')
  242. catalogMetadata = {catalog:{type: 'MOVIE', title: info.returnedTitleRendition.asin}};
  243. var epInfo = catalogMetadata.catalog;
  244. var ep = epInfo.episodeNumber;
  245. var title, season;
  246. if(epInfo.type == 'MOVIE' || ep === 0) {
  247. title = epInfo.title;
  248. downloadVars.title = title;
  249. }
  250. else {
  251. info.catalogMetadata.family.tvAncestors.forEach(function(tvAncestor) {
  252. switch(tvAncestor.catalog.type) {
  253. case 'SEASON':
  254. season = tvAncestor.catalog.seasonNumber;
  255. break;
  256. case 'SHOW':
  257. title = tvAncestor.catalog.title;
  258. break;
  259. }
  260. });
  261. title += '.S' + season.toString().padStart(2, '0');
  262. if(downloadVars.type === 'all')
  263. downloadVars.title = title;
  264. title += 'E' + ep.toString().padStart(2, '0');
  265. if(downloadVars.type === 'one')
  266. downloadVars.title = title;
  267. title += '.' + epInfo.title;
  268. }
  269. title = sanitizeTitle(title);
  270. title += '.WEBRip.Amazon.';
  271. var languages = new Set();
  272.  
  273. var forced = info.forcedNarratives || [];
  274. forced.forEach(function(forcedInfo) {
  275. forcedInfo.languageCode += '-forced';
  276. });
  277.  
  278. var subs = (info.subtitleUrls || []).concat(forced);
  279.  
  280. subs.forEach(function(subInfo) {
  281. let lang = subInfo.languageCode;
  282. if(subInfo.type === 'subtitle' || subInfo.type === 'subtitle') {}
  283. else if(subInfo.type === 'shd')
  284. lang += '[cc]';
  285. else
  286. lang += `[${subInfo.type}]`;
  287. if(languages.has(lang)) {
  288. let index = 0;
  289. let newLang;
  290. do {
  291. newLang = `${lang}_${++index}`;
  292. } while(languages.has(newLang));
  293. lang = newLang;
  294. }
  295. languages.add(lang);
  296. ++downloadVars.subCounter;
  297. progressBar.incrementMax();
  298. downloadSubs(subInfo.url, title + lang + '.srt', downloadVars, lang);
  299. });
  300. }
  301. catch(e) {
  302. console.log(info);
  303. alert(e);
  304. }
  305. if(--downloadVars.infoCounter === 0 && downloadVars.subCounter === 0) {
  306. alert("No subs found, make sure you're logged in and you have access to watch this video!");
  307. progressBar.destroy();
  308. }
  309. };
  310. req.send(null);
  311. }
  312.  
  313. function downloadThis(e) {
  314. progressBar.init();
  315. var id = e.target.getAttribute('data-id');
  316. var downloadVars = {
  317. type: 'one',
  318. subCounter: 0,
  319. infoCounter: 1,
  320. zip: new JSZip()
  321. };
  322. downloadInfo(gUrl + id, downloadVars);
  323. }
  324. function downloadAll(e) {
  325. progressBar.init();
  326. var IDs = e.target.getAttribute('data-id').split(';');
  327. var downloadVars = {
  328. type: 'all',
  329. subCounter: 0,
  330. infoCounter: IDs.length,
  331. zip: new JSZip()
  332. };
  333. IDs.forEach(function(id) {
  334. downloadInfo(gUrl + id, downloadVars);
  335. });
  336. }
  337.  
  338. // remove unnecessary parameters from URL
  339. function parseURL(url) {
  340. var filter = ['consumptionType', 'deviceID', 'deviceTypeID', 'firmware', 'gascEnabled', 'marketplaceID', 'userWatchSessionId', 'videoMaterialType', 'clientId', 'operatingSystemName', 'operatingSystemVersion', 'customerID', 'token'];
  341. var urlParts = url.split('?');
  342. var params = ['desiredResources=CatalogMetadata%2CSubtitleUrls%2CForcedNarratives'];
  343. urlParts[1].split('&').forEach(function(param) {
  344. var p = param.split('=');
  345. if(filter.indexOf(p[0]) > -1)
  346. params.push(param);
  347. });
  348. params.push('resourceUsage=CacheResources');
  349. params.push('titleDecorationScheme=primary-content');
  350. params.push('subtitleFormat=TTMLv2');
  351. params.push('asin=');
  352. urlParts[1] = params.join('&');
  353. return urlParts.join('?');
  354. }
  355.  
  356. function createDownloadButton(id, type) {
  357. var p = document.createElement('p');
  358. p.classList.add('download');
  359. p.setAttribute('data-id', id);
  360. p.innerHTML = 'Download subs for this ' + type;
  361. p.addEventListener('click', (type == 'season' ? downloadAll : downloadThis));
  362. return p;
  363. }
  364.  
  365. function getArgs(a) {
  366. return a.initArgs || a.args;
  367. }
  368.  
  369. function findMovieID() {
  370. let movieId;
  371.  
  372. for(const templateElement of document.querySelectorAll('script[type="text/template"]')) {
  373. let data;
  374. try {
  375. data = JSON.parse(templateElement.innerHTML);
  376. }
  377. catch(ignore) {
  378. continue;
  379. }
  380.  
  381. for(let i = 0; i < 3; ++i) {
  382. try {
  383. if(i === 0) {
  384. movieId = getArgs(getArgs(data).apexes[0]).titleID;
  385. }
  386. else if(i === 1) {
  387. movieId = getArgs(data).titleID;
  388. }
  389. else if(i === 2) {
  390. movieId = getArgs(data.props.body[0]).titleID;
  391. }
  392.  
  393. if(typeof movieId !== "undefined") {
  394. return movieId;
  395. }
  396. }
  397. catch(ignore) {}
  398. }
  399. }
  400.  
  401. for(const name of ["titleId", "titleID"]) {
  402. try {
  403. movieId = document.querySelector(`input[name="${name}"]`).value;
  404. if(typeof movieId !== "undefined" && movieId !== "") {
  405. return movieId;
  406. }
  407. }
  408. catch(ignore) {}
  409. }
  410.  
  411. throw Error("Couldn't find movie ID");
  412. }
  413.  
  414. function allLoaded(resolve, epCount) {
  415. if(epCount !== document.querySelectorAll('.js-node-episode-container, li[id^=av-ep-episodes-], li[id^=av-ep-episode-]').length)
  416. resolve();
  417. else
  418. window.setTimeout(allLoaded, 200, resolve, epCount);
  419. }
  420.  
  421. function manualShowAll(resolve) {
  422. alert(
  423. "Some episodes are not loaded yet! Scroll to the bottom of the page to load them."
  424. + "\n\n"
  425. + "Once all episodes are loaded - click on the button at the bottom of your screen."
  426. );
  427. const btn = document.createElement("div");
  428. btn.innerHTML = "Click here after all episodes load";
  429. btn.style.position = "fixed";
  430. btn.style.bottom = "0";
  431. btn.style.left = "0";
  432. btn.style.padding = "10px";
  433. btn.style.zIndex = "999999";
  434. btn.style.background = "white";
  435. btn.addEventListener("click", () => {
  436. btn.remove();
  437. resolve();
  438. });
  439. document.body.append(btn);
  440. }
  441.  
  442. function showAll() {
  443. return new Promise(resolve => {
  444. for(const templateElement of document.querySelectorAll('script[type="text/template"]')) {
  445. let data;
  446. if(templateElement.innerHTML.includes("NextPage")) {
  447. manualShowAll(resolve);
  448. return;
  449. }
  450. }
  451.  
  452. let btn = document.querySelector('[data-automation-id="ep-expander"]');
  453. if(btn === null)
  454. resolve();
  455.  
  456. let epCount = document.querySelectorAll('.js-node-episode-container, li[id^=av-ep-episodes-], li[id^=av-ep-episode-]').length;
  457. btn.click();
  458. allLoaded(resolve, epCount);
  459. });
  460. }
  461.  
  462. // add download buttons
  463. async function init(url) {
  464. initialied = true;
  465. gUrl = parseURL(url);
  466. console.log(gUrl);
  467.  
  468. await showAll();
  469.  
  470. let button;
  471. let epElems = document.querySelectorAll('.dv-episode-container, .avu-context-card, .js-node-episode-container, li[id^=av-ep-episodes-], li[id^=av-ep-episode-]');
  472. if(epElems.length > 0) {
  473. let IDs = [];
  474. for(let i=epElems.length; i--; ) {
  475. let selector, id, el;
  476. if((el = epElems[i].querySelector('input[name="highlight-list-selector"]')) !== null) {
  477. id = el.id.replace('selector-', '');
  478. selector = '.js-episode-offers';
  479. }
  480. else if((el = epElems[i].querySelector('input[name="ep-list-selector"]')) !== null) {
  481. id = el.value;
  482. selector = '.av-episode-meta-info';
  483. }
  484. else if(id = epElems[i].getAttribute('data-aliases'))
  485. selector = '.dv-el-title';
  486. else
  487. continue;
  488. id = id.split(',')[0];
  489. epElems[i].querySelector(selector).parentNode.appendChild(createDownloadButton(id, 'episode'));
  490. IDs.push(id);
  491. }
  492. button = createDownloadButton(IDs.join(';'), 'season');
  493. }
  494. else {
  495. let id = findMovieID();
  496. id = id.split(',')[0];
  497. button = createDownloadButton(id, 'movie');
  498. }
  499. document.querySelector('.dv-node-dp-badges, .av-badges').appendChild(button);
  500. }
  501.  
  502. var initialied = false, gUrl;
  503. // hijack xhr, we need to find out tokens and other parameters needed for subtitle info
  504. xhrHijacker(function(xhr, id, origin, args) {
  505. if(!initialied && origin === 'open')
  506. if(args[1].indexOf('/GetPlaybackResources') > -1) {
  507. init(args[1])
  508. .catch(error => {
  509. console.log(error);
  510. alert(`subtitle downloader error: ${error.message}`);
  511. });
  512. }
  513. });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址