Rai Play video download

This script allows you to download videos on Rai Play

当前为 2023-04-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Rai Play video download
  3. // @namespace http://andrealazzarotto.com
  4. // @version 11.3.2
  5. // @description This script allows you to download videos on Rai Play
  6. // @description:it Questo script ti permette di scaricare i video su Rai Play
  7. // @author Andrea Lazzarotto
  8. // @match https://www.raiplay.it/*
  9. // @match https://www.rainews.it/*
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/foundation-essential/6.2.2/js/foundation.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
  13. // @require https://unpkg.com/@ungap/from-entries@0.1.2/min.js
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM.xmlHttpRequest
  16. // @connect rai.it
  17. // @connect akamaized.net
  18. // @connect akamaihd.net
  19. // @connect msvdn.net
  20. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  21. // ==/UserScript==
  22.  
  23. var instance;
  24.  
  25. /* Greasemonkey 4 wrapper */
  26. if (typeof GM !== "undefined" && !!GM.xmlHttpRequest) {
  27. GM_xmlhttpRequest = GM.xmlHttpRequest;
  28. }
  29.  
  30. function fetch(params) {
  31. return new Promise(function(resolve, reject) {
  32. params.onload = resolve;
  33. params.onerror = reject;
  34. GM_xmlhttpRequest(params);
  35. });
  36. }
  37.  
  38. (function() {
  39. 'use strict';
  40.  
  41. var Foundation = window.Foundation;
  42. var download_icon = '<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="M42 40v4H6v-4h36zM20 24v-8h8v8h7L24 37 13 24h7zm8-14v4h-8v-4h8zm0-6v4h-8V4h8z" /></svg>';
  43.  
  44. var showModal = (title, content) => {
  45. if (instance) {
  46. instance.close();
  47. }
  48. var modal = $(`
  49. <div id="video-download-modal" class="small reveal" data-reveal aria-labelledby="Download video">
  50. <h2 id="modal-title">${title}</h2>
  51. <div id="modal-content"></div>
  52. <button class="close-button" data-close aria-label="Chiudi finestrella" type="button">
  53. <span aria-hidden="true">&times;</span>
  54. </button>
  55. </div>
  56. `);
  57. modal.css({
  58. 'padding': '2rem',
  59. 'background-color': '#001623',
  60. });
  61. modal.find('#modal-content').append(content);
  62. instance = new Foundation.Reveal(modal);
  63. instance.open();
  64. modal.find('.close-button').css({
  65. 'color': 'white',
  66. }).click(() => instance.close());
  67. // Prevent fullscreen issues
  68. $(".vjs-fullscreen-control:contains('Exit')").click();
  69. };
  70.  
  71. var checkQuality = (url, rate) => {
  72. return fetch({
  73. method: 'GET',
  74. url: url,
  75. headers: {
  76. 'User-Agent': 'raiweb',
  77. 'Range': 'bytes=0-255',
  78. },
  79. }).then(
  80. (response) => {
  81. let headers = fromEntries(response.responseHeaders.split("\n").map(element => element.trim().toLowerCase().split(":")));
  82. let range = headers['content-range'] || '/0';
  83. let size = +(range.split('/').slice(1,)[0] || 0);
  84. let megabytes = Math.round(size / 1024 / 1024);
  85. if (size > 102400) {
  86. return { quality: rate, url: url, megabytes: megabytes };
  87. } else {
  88. return null;
  89. }
  90. },
  91. () => null
  92. );
  93. }
  94.  
  95. var qualities = async (url) => {
  96. let bases = [];
  97. let rates = [5000, 3200, 2401, 2400, 1800, 1500, 1200, 800, 600, 400];
  98. if (url.indexOf('.m3u8') > 0) {
  99. let parts = url.replace(/\?.*/, '').split(',');
  100. // Verify all the rates
  101. const new_rates = parts.slice(1).map(value => value.replace(/\/.*/, '')).reverse().filter(value => !isNaN(value));
  102. // Handle single rate case
  103. if (new_rates.length) {
  104. rates = new_rates;
  105. } else {
  106. let rate = url.split('.mp4')[0].split('_').slice(-1)[0];
  107. rates = [rate];
  108. }
  109. const path = parts[0];
  110. let servers = [
  111. 'creativemedia1-rai-it.akamaized.net',
  112. 'creativemedia2-rai-it.akamaized.net',
  113. 'creativemedia3-rai-it.akamaized.net',
  114. 'creativemedia4-rai-it.akamaized.net',
  115. 'creativemedia6-rai-it.akamaized.net',
  116. 'creativemedia7-rai-it.akamaized.net',
  117. 'creativemedia8-rai-it.akamaized.net',
  118. 'creativemedia9-rai-it.akamaized.net',
  119. 'download2.rai.it',
  120. 'download2-geo.rai.it',
  121. 'creativemediax1.rai.it',
  122. 'creativemediax2.rai.it',
  123. ];
  124. let file_path;
  125. if (path.indexOf('akamaized.net') > 0 || path.indexOf('akamaihd.net') > 0) {
  126. const path_parts = path.split('.net/');
  127. file_path = '/' + path_parts[1];
  128. } else {
  129. const path_parts = path.split('msvdn.net/');
  130. const first = path_parts[1].replace(/^[^0-9]+/, '');
  131. file_path = first.slice(1);
  132. }
  133. // Fix the "/i/" prefix
  134. if (file_path.slice(0, 3) === '/i/') {
  135. file_path = file_path.slice(2);
  136. }
  137. // Fix the "/VOD/" prefix
  138. if (file_path.slice(0, 5) === '/VOD/') {
  139. file_path = '/podcastcdn' + file_path.slice(4);
  140. }
  141. console.log(file_path);
  142. file_path = file_path.replace(/_[1-9]+0+[01]\.mp4.*/, '_');
  143. if (file_path.indexOf('.mp4') > 0) {
  144. file_path = file_path.replace(/\.mp4.*/, '');
  145. rates = [''];
  146. }
  147. bases = servers.map(server => {
  148. return `http://${server}${file_path}${rates[0]}.mp4`;
  149. });
  150. console.log(bases);
  151. } else {
  152. bases.push(url);
  153.  
  154. var ending = url.match(/_[1-9]+0+[01]\.mp4/);
  155. if (!ending || !ending.length) {
  156. let result = await checkQuality(url, '');
  157. return [result].filter(value => (value !== null));
  158. }
  159. }
  160.  
  161. let promises = [];
  162. bases.forEach(url => {
  163. var promise = Promise.all(rates.map(rate => {
  164. var quality_url = url.replace(/_[1-9]+0+[01]\.mp4/, `_${rate}.mp4`);
  165. return checkQuality(quality_url, rate);
  166. }));
  167. promises.push(promise);
  168. });
  169. const groups = await Promise.all(promises);
  170. for (let i = 0; i < groups.length; i++) {
  171. const filtered = groups[i].filter(value => (value !== null));
  172. if (filtered.length) {
  173. return filtered;
  174. }
  175. }
  176.  
  177. return [];
  178. };
  179.  
  180. var DRMError = () => {
  181. showModal('Niente da fare...', "<p>Non è stato possibile trovare un link del video in formato MP4. Il video sembra essere protetto da DRM.</p>");
  182. };
  183.  
  184. var resolveRelinker = (relinker) => {
  185. return fetch({
  186. method: 'HEAD',
  187. url: relinker,
  188. headers: {
  189. 'User-Agent': 'raiweb',
  190. },
  191. }).then(
  192. (response) => {
  193. let final = response.finalUrl;
  194. let valid = (final.indexOf('mp4') > 0 || final.indexOf('.m3u8') > 0) && final.indexOf('DRM_') < 0;
  195. if (!valid) {
  196. DRMError();
  197. } else {
  198. qualities(response.finalUrl).then(results => {
  199. if (!results.length) {
  200. showModal('Errore sconosciuto', "<p>Non è stato possibile trovare un link del video in formato MP4.</p>");
  201. return;
  202. }
  203.  
  204. var buttons = '';
  205. results.forEach(video => {
  206. buttons += `<a href="${video.url}" class="button" target="_blank">MP4 ${video.quality} (${video.megabytes} MB)</a>`;
  207. });
  208.  
  209. showModal('Link diretti', `
  210. <p>Clicca su una opzione per aprire il video in formato MP4. Usa il tasto destro del mouse per salvarlo, oppure copiare il link.</p>
  211. <p><strong>Per evitare interruzioni è raccomandato l'uso di un download manager.</strong></p>
  212. <p>${buttons}</p>`);
  213. });
  214. }
  215. },
  216. (response) => {
  217. var drm = response.finalUrl.indexOf('DRM_') > 0 || response.status === 0;
  218. if (drm) {
  219. DRMError();
  220. } else {
  221. showModal('Errore di rete', "<p>Si è verificato un errore di rete. Riprova più tardi.</p>");
  222. }
  223. }
  224. );
  225. }
  226.  
  227. var getVideo = () => {
  228. showModal('Attendere', '<p>Sto elaborando...</p>');
  229.  
  230. var path = location.href.replace(/\.html(\?.*)?$/, '.json');
  231. $.getJSON(path).then((data) => {
  232. var secure = data.video.content_url.replace('http://', 'https://');
  233. return resolveRelinker(secure);
  234. });
  235. };
  236.  
  237. var getRaiNewsVideo = (relinker) => {
  238. showModal('Attendere', '<p>Sto elaborando...</p>');
  239. return resolveRelinker(relinker);
  240. };
  241.  
  242. var downloadButton = (container, action) => {
  243. if (container.find('.video-download-button').length) {
  244. return;
  245. }
  246.  
  247. container.find('.vjs-custom-control-spacer').after(`
  248. <button class="video-download-button vjs-control vjs-button" aria-disabled="false">
  249. <span aria-hidden="true" class="vjs-icon-placeholder">${download_icon}</span>
  250. <span class="vjs-control-text" aria-live="polite">Download</span>
  251. </button>
  252. `);
  253. container.find('.video-download-button').css({
  254. 'order': 110,
  255. }).click(action).find('svg').css({
  256. 'fill': '#039cf9',
  257. 'height': '1.5em',
  258. });
  259. };
  260.  
  261. $(document).arrive('rai-player .vjs-custom-control-spacer', (element) => {
  262. var container = $(element).parent();
  263. downloadButton(container, getVideo);
  264. });
  265.  
  266. $(document).arrive('rainews-player .vjs-custom-control-spacer', (element) => {
  267. let container = $(element).parent();
  268. let player = container.closest('rainews-player');
  269. let relinker = JSON.parse(player.attr("data")).mediapolis;
  270. downloadButton(container, () => {
  271. getRaiNewsVideo(relinker);
  272. });
  273. });
  274.  
  275. var isAnon = function() {
  276. return !!$('#accountPanelLoginPanel').is(':visible');
  277. };
  278.  
  279. $(document).ready(() => {
  280. if (location.pathname.startsWith('/video')) {
  281. $('rai-sharing').after(`
  282. <a id="inline-download-button" class="cell small-4 medium-shrink highlight__share" aria-label="Download">
  283. <div class="leaf__share__button button button--light-ghost button--circle float-center">${download_icon}</div>
  284. <span class="button-label">Download</span>
  285. </a>
  286. `);
  287. $('#inline-download-button').click(getVideo);
  288. }
  289.  
  290. $('body').on('touchstart mousedown', 'a.card-item__link', (event) => {
  291. if (isAnon() && event.which !== 3) {
  292. location.href = $(event.currentTarget).attr('href');
  293. }
  294. });
  295.  
  296. $('body').on('touchstart mousedown', 'button[data-video-json]', (event) => {
  297. if (isAnon() && event.which !== 3) {
  298. location.href = $(event.currentTarget).data('video-json').replace(/\.json/, '.html');
  299. }
  300. });
  301. });
  302. })();