Rai Play video download

This script allows you to download videos on Rai Play

目前为 2021-11-24 提交的版本,查看 最新版本

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