// ==UserScript== // @name Arte+7 Downloader // @description Download videos or get stream link of ARTE programs in the selected language. // @version 3.6 // @license GPL // @include https://*.arte.tv/* // @icon https://www.arte.tv/favicon.ico // @homepageURL https://github.com/GuGuss/ARTE-7-Downloader // @supportURL https://github.com/GuGuss/ARTE-7-Downloader/issues // @downloadURL https://raw.githubusercontent.com/GuGuss/ARTE-7-Downloader/master/src/arte-downloader.js // @updateURL https://raw.githubusercontent.com/GuGuss/ARTE-7-Downloader/master/src/arte-downloader.js // ==/UserScript== /* --- GLOBAL VARIABLES --- */ const scriptVersion = 3.6; let playerJson; let nbVideos; let nbHTTP; let nbHLS; let languages; let qualities; /* --- FUNCTIONS: utilities --- */ function getURLParameter(url, name) { return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(url) || [, ""])[1].replace(/\+/g, '%20')) || null; } function insertAfter(newNode, referenceNode) { if (referenceNode.parentNode == null) { referenceNode.insertBefore(newNode, referenceNode.nextSibling); } else { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } } function stringStartsWith(string, prefix) { return string.slice(0, prefix.length) === prefix; } function hasClass(element, cls) { return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1; } // Get a parent node of the chosen type and class function getParent(nodeReference, nodeName, classString) { let parent = nodeReference; let nbNodeIteration = 0; let nbNodeIterationMax = 10; // any node if (nodeName === '') { while (parent.nodeName !== "BODY" && nbNodeIteration < nbNodeIterationMax && hasClass(parent, classString) === false) { nbNodeIteration++; parent = parent.parentNode; } } // with defined node type else { console.log("> Looking for a <" + nodeName + " class='" + classString + "'> parent node"); while (parent.nodeName !== "BODY" && parent.parentNode !== null && nbNodeIteration < nbNodeIterationMax && (parent.nodeName !== nodeName.toUpperCase() || hasClass(parent, classString) === false)) { nbNodeIteration++; parent = parent.parentNode; } } return parent; } /* --- FUNCTIONS: analysis --- */ function addLanguage(videoElementIndex, language, wording) { if (!languages[videoElementIndex].hasOwnProperty(language)) { languages[videoElementIndex][language] = wording; } } function addQuality(videoElementIndex, quality, wording) { if (!qualities[videoElementIndex].hasOwnProperty(quality)) { qualities[videoElementIndex][quality] = wording; } } function preParsePlayerJson(videoElementIndex) { if (playerJson[videoElementIndex]) { let videos = Object.keys(playerJson[videoElementIndex].videoJsonPlayer.VSR); nbVideos[videoElementIndex] = videos.length; if (nbVideos[videoElementIndex] > 0) { // Loop through all videos URLs. for (let key in videos) { let video = playerJson[videoElementIndex].videoJsonPlayer.VSR[videos[key]]; // Check if video format or media type if (video.videoFormat === "HBBTV" || video.mediaType === "mp4") { nbHTTP[videoElementIndex]++; } else if (video.videoFormat === "M3U8" || video.mediaType === "hls") { nbHLS[videoElementIndex]++; } addLanguage(videoElementIndex, video.versionCode, video.versionLibelle); addQuality(videoElementIndex, ( video.VQU !== undefined ? video.VQU : video.quality), video.height ? video.height + "p@" + Math.round(video.bitrate /1000*10) /10 + "Mbps (" // convert kbps > Mbps + Math.round(video.bitrate *1000/8/1024) + "kB/s)" // convert kbps > kB/s : video.quality); } // Remove Apple HLS if HTTP available if (nbHTTP[videoElementIndex] > 0) { delete qualities[videoElementIndex].XS; delete qualities[videoElementIndex].XQ; } // Reorder qualities let sortedKeys = Object.keys(qualities[videoElementIndex]).sort( function(a, b) { // array of sorted keys return qualities[videoElementIndex][b].split('@')[1].split('M')[0] * 1 - qualities[videoElementIndex][a].split('@')[1].split('M')[0] * 1; } ); // Create new object to rearrange qualities according to new key order let temp = new Object; for (let i = 0; i < sortedKeys.length; i++) { temp[sortedKeys[i]] = qualities[videoElementIndex][sortedKeys[i]]; } qualities[videoElementIndex] = temp; // replace with new ordered object // Display preparse info console.log("\n====== player #" + videoElementIndex+1 + " ======\n> " + nbVideos[videoElementIndex] + " formats: " + nbHTTP[videoElementIndex] + " MP4 videos | " + nbHLS[videoElementIndex] + " streams."); let languagesFound = ""; for (let l in languages[videoElementIndex]) { languagesFound += "\n - " + languages[videoElementIndex][l]; } console.log("> Languages:" + languagesFound); } else { console.warn("> No video found for player"); } } } function getVideoName(videoElementIndex) { let name = playerJson[videoElementIndex].videoJsonPlayer.VTI; if (name === null) { name = playerJson[videoElementIndex].videoJsonPlayer.VST.VNA; if (name === null) { return "undefined"; } } name = name.split('_').join(' '); return name.charAt(0).toUpperCase() + name.slice(1); } function getVideoUrl(videoElementIndex, quality, language) { // Get videos object let videos = Object.keys(playerJson[videoElementIndex].videoJsonPlayer.VSR); // Check if there are HTTP videos if (nbHTTP[videoElementIndex] > 0) { // Loop through all videos URLs. for (let key in videos) { let video = playerJson[videoElementIndex].videoJsonPlayer.VSR[videos[key]]; // Check language, format, quality if (video.versionCode === language && (video.videoFormat === "HBBTV" || video.mediaType === "mp4") && (video.VQU === quality || video.quality === quality)) { console.log("> " + quality + " MP4 in " + language + ": " + video.url); return video.url; } } } // Search HLS streams if (nbHLS[videoElementIndex] > 0) { for(let key in videos) { let video = playerJson[videoElementIndex].videoJsonPlayer.VSR[videos[key]]; if ( (video.videoFormat === "M3U8" || video.mediaType === "hls") && (video.VQU === quality || video.quality === quality) && video.versionCode === language ) { console.log("> HLS stream: " + video.url); return video.url; } } } console.log("> No video found in " + language + " [" + quality + "] for #" + videoElementIndex ) return ''; } function initParsingSystem(nbVideoPlayers) { if (nbVideoPlayers > 0) { console.log("> Found " + nbVideoPlayers + " video player" + (nbVideoPlayers > 1 ? 's':'')); playerJson = [nbVideoPlayers]; nbVideos = [nbVideoPlayers]; nbHTTP = [nbVideoPlayers]; nbHLS = [nbVideoPlayers]; languages = [nbVideoPlayers]; qualities = [nbVideoPlayers]; for (let i = 0; i < nbVideoPlayers; i++) { playerJson[i] = 0; nbVideos[i] = 0; nbHTTP[i] = 0; nbHLS[i] = 0; languages[i] = new Object; qualities[i] = new Object; } } else { console.log("> No video players found."); } } /* =========== ENTRY POINT =========== */ (function findPlayers() { console.log('\n===== ARTE DOWNLOADER v' + scriptVersion + ' started ====='); // Observe href change => rerun script. var oldHref = document.location.href; window.addEventListener("load",function(event) { var bodyList = document.querySelector("body") ,observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (oldHref != document.location.href) { console.log("> URL changed from " + oldHref + " => " + document.location.href ); oldHref = document.location.href; // dirty hack avoiding vars reset. // @TODO: reset global vars console.log("> Reloading page to rerun the script.."); window.location.reload(); } }); }); var config = { childList: true, subtree: true }; observer.observe(bodyList, config); }, false); console.log("> Querying Arte API"); const _win_loc = window.location.pathname.split('/'); const _lang = _win_loc[1]; const _api_base = "https://api.arte.tv/api/player/v1/config/" + _lang + "/"; const _video_id = _win_loc[3]; const _video_name = _win_loc[4]; initParsingSystem(1); let _cb = (json) => { playerJson[0] = json; preParsePlayerJson(0); if (!document.getElementById("cbLanguage0")) { let anchor = document.querySelector('main[role="main"]').firstChild.nextSibling.firstChild; if (anchor != null && nbVideos[0] > 0) { console.log(anchor); insertAfter(buildContainer(0), anchor); console.log("> Success: exiting Arte Downloader."); } } }; window.fetch(_api_base + _video_id).then((resp) => resp.json()).then(_cb).catch((err) => console.error(err)); })(); /* --- FUNCTIONS: decorating --- */ function createButtonDownload(videoElementIndex, language) { let button = document.createElement('a'); let videoUrl; for (let q in qualities[videoElementIndex]) { videoUrl = getVideoUrl(videoElementIndex, q, language); if (videoUrl !== '') { break; } } // Check if video exists if (videoUrl === null) { console.log("Could not find video feed"); return null; } // Check HTTP if (nbHTTP[videoElementIndex] > 0 && videoUrl.substring(videoUrl.length - 4, videoUrl.length) === ".mp4") { button.innerHTML = "📥 video "; } // Check HTTP Live Stream else if (nbHLS[videoElementIndex] > 0 && videoUrl.substring(videoUrl.length - 5, videoUrl.length === ".m3u8")) { button.innerHTML = "Copy this link > Open VLC > CTRL+R > Network > CTRL+V > Convert/Save video. "; } // Unknown URL format : should not happen else { console.error('Unknown URL format'); return null; } button.setAttribute('id', 'btnDownload' + videoElementIndex); // to refer later in select changes button.setAttribute('href', videoUrl); button.setAttribute('target', '_blank'); button.setAttribute('download', getVideoName(videoElementIndex) + ".mp4"); button.setAttribute('class', 'btn btn-default'); button.setAttribute('style', 'line-height: 17px; margin-left:10px; text-align: center; padding: 10px; color:rgb(40, 40, 40); background-color: rgb(230, 230, 230); font-family: ProximaNova,Arial,Helvetica,sans-serif; font-size: 13px; font-weight: 400;'); return button; } function createButtonMetadata(videoElementIndex) { let title = getVideoName(videoElementIndex); let subtitle = playerJson[videoElementIndex].videoJsonPlayer.VSU; let description_short = playerJson[videoElementIndex].videoJsonPlayer.V7T; let description = playerJson[videoElementIndex].videoJsonPlayer.VDE; let tags = playerJson[videoElementIndex].videoJsonPlayer.VTA; // Continue if at least one field is filled if (title !== undefined || description_short !== undefined || subtitle !== undefined || description !== undefined || tags !== undefined) { let button = document.createElement('a'); button.setAttribute('class', 'btn btn-default'); button.setAttribute('style', 'line-height: 17px; margin-left:10px; text-align: center; padding: 10px; color:rgb(40, 40, 40); background-color: rgb(230, 230, 230); font-family: ProximaNova,Arial,Helvetica,sans-serif; font-size: 13px;'); button.innerHTML = "📥 description "; let metadata = (title !== undefined ? "[Title]\n" + title:'') + (subtitle !== undefined ? "\n\n[Subtitle]\n" + subtitle:'') + (description_short !== undefined ? "\n\n[Description-short]\n" + description_short:'') + (description !== undefined ? "\n\n[Description]\n" + description:'') + (tags !== undefined ? "\n\n[Tags]\n" + tags:''); let encodedData = window.btoa(unescape(encodeURIComponent(metadata))); button.setAttribute('href', 'data:application/octet-stream;charset=utf-8;base64,' + encodedData); button.setAttribute('download', getVideoName(videoElementIndex) + '.txt'); return button; } else { return null; } } function getComboboxSelectedValue(combobox) { let cb = document.getElementById(combobox); if (cb == null) { cb = parent.document.getElementById(combobox); } return cb[cb.selectedIndex].value; } function getDownloadButton(index) { let btn = document.getElementById('btnDownload' + index); if (btn == null) { btn = parent.document.getElementById('btnDownload' + index); } return btn; } function createLanguageComboBox(videoElementIndex) { let languageComboBox = document.createElement('select'); languageComboBox.setAttribute('id', 'cbLanguage' + videoElementIndex); // Associate onchange event with function (bypass for GM) languageComboBox.onchange = () => { let selectedLanguage = languageComboBox.options[languageComboBox.selectedIndex].value; console.log("\n> Language changed to " + selectedLanguage); let btn = getDownloadButton(videoElementIndex); let selectedQuality = getComboboxSelectedValue('cbQuality' + videoElementIndex); let url = getVideoUrl(videoElementIndex, selectedQuality, selectedLanguage); if (url !== '') { btn.style.visibility = "visible"; btn.setAttribute('href', url); } else { btn.style.visibility = "hidden"; } }; // Fill with available languages for (let l in languages[videoElementIndex]) { if (languages[videoElementIndex][l] !== 0) { languageComboBox.innerHTML += ""; } } languageComboBox.setAttribute('class', 'btn btn-default'); languageComboBox.setAttribute('style', (languageComboBox.innerHTML === "" ? "visibility:hidden;" : "max-width: 100px; padding: 10px; color:rgb(40, 40, 40); background-color: rgb(230, 230, 230); font-family: ProximaNova,Arial,Helvetica,sans-serif; font-size: 13px; font-weight: 400;")); return languageComboBox; } function createQualityComboBox(videoElementIndex) { let qualityComboBox = document.createElement('select'); qualityComboBox.setAttribute('id', 'cbQuality' + videoElementIndex); // Associate onchange event with function (bypass for GM) qualityComboBox.onchange = () => { let selectedQuality = qualityComboBox.options[qualityComboBox.selectedIndex].value; console.log("\n> Quality changed to " + selectedQuality); let btn = document.getElementById('btnDownload' + videoElementIndex); if (btn == null) { btn = parent.document.getElementById('btnDownload' + videoElementIndex); } let selectedLanguage = getComboboxSelectedValue('cbLanguage' + videoElementIndex); console.log(selectedLanguage); let url = getVideoUrl(videoElementIndex, selectedQuality, selectedLanguage); if (url !== '') { btn.style.visibility = "visible"; btn.setAttribute('href', url); } else { console.log("Video not found for these settings!") btn.style.visibility = "hidden"; } }; // Fill with available qualities for (let q in qualities[videoElementIndex]) { if (qualities[videoElementIndex][q] !== 0) { qualityComboBox.innerHTML += ""; } } qualityComboBox.setAttribute('class', 'btn btn-default'); qualityComboBox.setAttribute('style', 'width:200px; padding: 6px; margin-left:10px; color:rgb(40, 40, 40); background-color: rgb(230, 230, 230); font-family: ProximaNova,Arial,Helvetica,sans-serif; font-size: 13px; font-weight: 400;'); return qualityComboBox; } function createCreditsElement() { let credits = document.createElement('div'); credits.setAttribute('style', 'text-align: center; line-height: 20px; font-size: 11.2px; color: rgb(255, 255, 255); font-family: ProximaNova, Arial, Helvetica, sans-serif; padding: 5px; background:#262626'); credits.innerHTML = 'Arte Downloader v. ' + scriptVersion + ''; return credits; } function buildContainer(videoElementIndex) { let container = document.createElement('div'); container.setAttribute('id', 'video_' + videoElementIndex); container.setAttribute('class', 'ArteDownloader-v' + scriptVersion) container.setAttribute('style', 'background:#262626; padding: 10px;'); // Create video name span let videoNameSpan = document.createElement('span'); let subtitle = playerJson[videoElementIndex].videoJsonPlayer.subtitle; videoNameSpan.innerHTML = "" + getVideoName(videoElementIndex) + (subtitle !== undefined ? " - " + subtitle : "") + "
"; videoNameSpan.setAttribute('style', 'margin-top:10px; text-align: center; color:rgb(255, 255, 255); font-family: ProximaNova,Arial,Helvetica,sans-serif; font-size: 16px;'); container.appendChild(videoNameSpan); // Create language combobox let languageComboBox = createLanguageComboBox(videoElementIndex) container.appendChild(languageComboBox); // Check if there are languages available to select let selectedLanguage; if (languageComboBox.options.length > 0) { selectedLanguage = languageComboBox.options[languageComboBox.selectedIndex].value; } // Create quality combobox container.appendChild(createQualityComboBox(videoElementIndex)); // Create download button let btnDownload = createButtonDownload(videoElementIndex, selectedLanguage); if (btnDownload !== null) { container.appendChild(btnDownload); } // Create metadata button let btnMetadata = createButtonMetadata(videoElementIndex); if (btnMetadata !== null) { container.appendChild(btnMetadata); } // Create credits ribbon let credits = createCreditsElement(); container.appendChild(credits); return container; }