您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a download button to Panopto videos.
当前为
// ==UserScript== // @name Downopto - Panopto Video Downloader // @namespace http://github.com/jaytohe/ // @version 1.2.3 // @description Adds a download button to Panopto videos. // @author jaytohe // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js // @match https://*.panopto.eu/Panopto/Pages/Viewer.aspx?id=* // @match https://*.panopto.eu/Panopto/Pages/Sessions/List.aspx // @grant GM_addStyle // ==/UserScript== //Global Vars //Coordinates for download, progress bar elements. const dl_container_pos = {left: 5, bottom: 20}; const downopto_bar_container_pos = {bottom: 60}; const downopto_bar_pos = {width: 97}; function downloadVideo(url, savename, mimetype) { console.log(`Init video dl : ${url}`); setProgressBarVisibility(true); return axios.get(url, { responseType: 'blob', onDownloadProgress: function(progressEvent) { const percentage = Math.round(progressEvent.loaded / progressEvent.total * 100); setProgressBarValue(percentage); setProgressText(`Fetching: ${percentage}%`,'downopto-bar'); } }) .then(function(response) { const blob = response.data; const blob_url = URL.createObjectURL(blob, { type: mimetype }); //Create hidden anchor and click it to download blob vid. const tmp = document.createElement('a'); tmp.href = blob_url; tmp.download = sanitize(savename,'_') || ''; //set the filename. document.body.appendChild(tmp); tmp.click(); tmp.remove(); return Promise.resolve(); }) .catch(function(error) { return Promise.reject(new Error(`DL failed with msg : ${error.message}`)); }); } function constructDownloadURL(institution_prefix, delivery_id) { //Extract dl link from Embed.aspx page return new Promise(function (resolve, reject) { const embedURL = `https://${institution_prefix}.panopto.eu/Panopto/Pages/Embed.aspx?id=${delivery_id}&v=1&ignoreflash=true`; console.log(embedURL); const xhr = new XMLHttpRequest(); xhr.responseType = "document"; xhr.withCredentials = true; //use authentication cookies with GET request to simulate logged-in user. xhr.open("GET", embedURL); xhr.onload = function() { if (xhr.status == 200) { const embed_dom = xhr.response; const xpath_res = embed_dom.evaluate( "//script[contains(text(), 'Panopto.Embed.instance')]", embed_dom, null, XPathResult.FIRST_ORDERED_NODE_TYPE, //makes sure we always grab the first matching script tag null ); const embed_src_node = xpath_res.singleNodeValue.textContent; if (embed_src_node != null) { const lower_offset = embed_src_node.search(/"VideoUrl":/); if (lower_offset !== -1) { const upper_offset = embed_src_node.indexOf(',', lower_offset); const matches = embed_src_node.substring(lower_offset, upper_offset).match(/"(.+)":"(.+)"/); //extract VideoUrl key-value pair if (matches !== null) resolve(matches[2].replaceAll(/\\/g, "")); //clean up videoUrl value and return reject(new Error("Regex failed to find VideoUrl value")); } reject(new Error("Unable to find VideoUrl key")); } reject(new Error("Unable to find Panopto.Embed.Instance object")); } else reject(new Error(`Got ${xhr.status} status code when trying to fetch Embed.aspx page.`)); } xhr.send(); }); } function LecturesListHandler(institution_prefix) { //handles scriptMode =1 //Fix coordinates of download button and progress bar. dl_container_pos.left = 17; dl_container_pos.bottom = 40; downopto_bar_container_pos.bottom = 110; downopto_bar_pos.width = 125; //Register event listener of dl button. createDownloadButton("Download All").addEventListener("click", function() { onLecturesListBtnClick(institution_prefix); }, false); //Create the two progress bars. // One for the individual video download progress. // Second for how many videos have been downloaded so far. createDownloadProgressBar(); createDownloadProgressBar('downopto-batch-bar'); } function onLecturesListBtnClick(institution_prefix) { //handles the dl btn click when scriptMode = 1. console.log("called lecturesList"); const LecturesList = document.querySelectorAll("table[id^=detailsTable] tr[draggable='false']"); //get tr rows of all visible videos. const videoIDS = []; //holds the id and the name for each video in LecturesList. for (const row of LecturesList) { const filename = row.querySelector("span[class='detail-title']").innerText; //extract the video's name from the row. videoIDS.push({"id": row.id, "name": filename}); } let videos_dled = 0; //keep track of how many vids have been downloaded. videoIDS.reduce(function(previousVideoPromise, video) { //sequentially service promises. const batch_progress_bar = document.getElementById("downopto-batch-bar"); return previousVideoPromise .catch(function(err) { console.log(err.message); //log dl error in case of Promise.reject videos_dled = (videos_dled < 0) ? 0 : videos_dled - 1; //if error in download, decrement videos_dled. }).then(function() { return constructDownloadURL(institution_prefix, video.id).then(function(source) { return downloadVideo(source, video.name, 'video/mp4').then(function() { //mimetype hardcoded to make my life easier. //After successful download: videos_dled += 1; batch_progress_bar.setAttribute( 'value', Math.round(videos_dled / videoIDS.length * 100) //Update second "videos downloaded thus far" progress bar. ); setProgressText(`${videos_dled} out of ${videoIDS.length}`,'downopto-batch-bar'); }) }); }); }, Promise.resolve()); //initial accumulator value to set Promise type. } function ViewerPageHandler(institution_prefix, delivery_id) { //handles scriptMode = 0 const button = createDownloadButton("Download"); createDownloadProgressBar(); button.addEventListener("click", function() { constructDownloadURL(institution_prefix, delivery_id).then(function(link) { const name = document.querySelector("meta[property^='og:title']").content; //get video's name downloadVideo(link, name, "video/mp4"); }) }, false); } function scriptMode() { //re-checks if we are on single lecture page or not. //Unfortunately, there's no way to find which specific match pattern called the script. //So we need to re-check. const viewRegex = /^https:\/\/([\w.]+)\.panopto\.eu\/Panopto\/Pages\/Viewer\.aspx\?id=(.+)$/; const listRegex = /^https:\/\/([\w.]+)\.panopto\.eu\/Panopto\/Pages\/Sessions\/List\.aspx$/; let url = window.location.href; url = url.substring(0, url.indexOf('#')) || url; const viewMatches = url.match(viewRegex); //grab institution_prefix and delivery_id if (viewMatches !== null) { return [0, viewMatches[1], viewMatches[2]]; // 0 indicates signle video download mode (Viewer.aspx page) } const listMatches = url.match(listRegex); //grab institution_prefix only; delivery_id for each video in list is extracted from the DOM. if (listMatches !== null) { // 1 indicates Bulk Download Mode (List.aspx page) return [1, listMatches[1]]; } return null; } //MAIN FUNCTION. (function () { const params = scriptMode(); if (params === null) return; if (params[0] === 0) ViewerPageHandler(params[1], params[2]); else if (params[0] === 1) LecturesListHandler(params[1]); })(); //HTML, CSS functions function createDownloadButton(btn_txt) { const btnNode = document.createElement('div'); const dl_btn = document.createElement('button'); btnNode.setAttribute('id', 'pdl-container'); dl_btn.id = 'pdl-btn'; dl_btn.innerHTML = btn_txt; btnNode.appendChild(dl_btn); document.body.appendChild(btnNode); return dl_btn; } function createDownloadProgressBar(bar_id = 'downopto-bar') { const container = document.createElement("div"); const bar = document.createElement("progress"); container.setAttribute('id', `${bar_id}-container`); bar.setAttribute("max", "100"); bar.setAttribute("id", bar_id); container.appendChild(bar); document.body.appendChild(container); createPercentageText(bar_id); setProgressBarVisibility(true, bar_id); setProgressBarValue(0, bar_id); } function createPercentageText(bar_id = 'downopto-bar') { const container = document.createElement("div"); container.setAttribute('id', `${bar_id}-percentage-container`); const s = document.createElement("span"); s.setAttribute("id", `${bar_id}-percentage`); s.innerHTML = ""; container.appendChild(s); document.body.appendChild(container); } function setProgressBarVisibility(k, bar_id = 'downopto-bar') { const t = k ? 'block' : 'none'; document.getElementById(bar_id).style.display = t; } function setProgressBarValue(val, bar_id = 'downopto-bar') { document.getElementById(bar_id).setAttribute('value', val); } function setProgressText(val, bar_id) { document.getElementById(bar_id+'-percentage').innerHTML = val; } //UTILITY FUNCTIONS. //Adapted from https://github.com/parshap/node-sanitize-filename/blob/master/index.js function sanitize(input, replacement='') { const illegalRe = /[\/\?<>\\:\*\|"]/g; const controlRe = /[\x00-\x1f\x80-\x9f]/g; const reservedRe = /\.+/; const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; const windowsTrailingRe = /[\. ]+$/; if (typeof input !== 'string') { throw new Error('Input must be string'); } const sanitized = input .replace(illegalRe, replacement) .replace(controlRe, replacement) .replace(reservedRe, replacement) .replace(windowsReservedRe, replacement) .replace(windowsTrailingRe, replacement); return sanitized; } GM_addStyle(` #pdl-container { position: absolute; bottom: ${dl_container_pos.bottom}px; left: ${dl_container_pos.left}px; opacity: 0.8; z-index: 1100; } #downopto-bar-container { position: absolute; bottom: ${downopto_bar_container_pos.bottom}px; left: ${dl_container_pos.left}px; opacity: 0.8; z-index: 1100; } #downopto-bar-percentage-container { position: absolute; bottom: ${downopto_bar_container_pos.bottom}px; left: ${dl_container_pos.left}px; opacity: 0.8; z-index: 1100; } #downopto-batch-bar-percentage-container { position: absolute; bottom: 90px; left: 17px; opacity: 0.8; z-index: 1100; } #downopto-batch-bar-container { position: absolute; bottom: 90px; left: 17px; opacity: 0.8; z-index: 1100; } #pdl-btn { cursor: pointer; border: none; background: #008080; font-size: 20px; color: white; padding: 5px 5px; text-align: center; } #downopto-bar { height: 20px; width: ${downopto_bar_pos.width}px; } #downopto-batch-bar { height: 12px; width: 125px; } `);