您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a Download Button to the BitChute Video Player.
当前为
// ==UserScript== // @name BitChute | Video Download Button // @namespace de.sidneys.userscripts // @homepage https://gist.githubusercontent.com/sidneys/b4783b0450e07e12942aa22b3a11bc00/raw/ // @version 18.0.0 // @description Adds a Download Button to the BitChute Video Player. // @author sidneys // @icon https://i.imgur.com/4GUWzW5.png // @noframes // @include *://www.bitchute.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/moment.min.js // @require https://gf.qytechs.cn/scripts/38888-greasemonkey-color-log/code/Greasemonkey%20%7C%20Color%20Log.js // @require https://gf.qytechs.cn/scripts/374849-library-onelementready-es6/code/Library%20%7C%20onElementReady%20ES6.js // @connect bitchute.com // @grant GM.addStyle // @grant GM.download // @grant unsafeWindow // @run-at document-start // ==/UserScript== /** * ESLint * @global */ /* global onElementReady, moment */ Debug = false /** * Video File Extension * @constant */ const videoExtension = 'mp4' /** * Inject Stylesheet */ let injectStylesheet = () => { console.debug('injectStylesheet') GM.addStyle(` /* ========================================================================== ELEMENTS ========================================================================== */ /* #download-button ========================================================================== */ #download-button, #download-button:hover { color: rgb(255, 255, 255); display: inline-block; animation: fade-in 0.3s; pointer-events: all; filter: none; cursor: pointer; white-space: nowrap; transition: all 500ms ease-in-out; } #download-button:not(.ready) { opacity: 0; width: 0; padding: 0; } #download-button.ready { opacity: 1; width: unset; padding: unset; animation: swing-in-bottom-fwd 2000ms cubic-bezier(0.175, 0.885, 0.320, 1.275) both, flash 500ms ease-in-out; animation-delay: 0s, 2000ms; } #download-button.error { color: rgb(255, 93, 12); } #download-button.busy { pointer-events: none; cursor: default; animation: pulsating-opacity 1000ms cubic-bezier(0.455, 0.03, 0.515, 0.955) 0s infinite alternate; } /* .video-statistics ========================================================================== */ .video-information .video-statistics { white-space: nowrap; } /* ========================================================================== ANIMATIONS ========================================================================== */ @keyframes pulsating-opacity { 0% { filter: opacity(0.95); } 25% { filter: opacity(0.95); } 50% { filter: opacity(0.50); } 75% { filter: opacity(0.95); } 100% { filter: opacity(0.95); } } @keyframes flash { 0% { color: rgb(41, 113, 237); } 50% { color: white; } 100% { color: rgb(41, 113, 237); } } @keyframes swing-in-bottom-fwd { 0% { transform: rotateX(100deg); transform-origin: bottom; filter: opacity(0); } 100% { transform: rotateX(0); transform-origin: bottom; filter: opacity(1); } } `) } /** * @callback saveAsCallback * @param {Error} error - Error * @param {Number} progress - Progress fraction * @param {Boolean} complete - Completion Yes/No */ /** * Download File via Greasemonkey * @param {String} url - Target URL * @param {String} filename - Target Filename * @param {saveAsCallback} callback - Callback */ let saveAs = (url, filename, callback = () => {}) => { console.debug('saveAs') // Parse URL const urlObject = new URL(url) const urlHref = urlObject.href // Trigger Download Request GM.download({ url: urlHref, name: filename, saveAs: true, onerror: (download) => { console.debug('saveAs', 'onerror') callback(new Error(download.error ? download.error.toUpperCase() : 'Unknown')) }, onload: (download) => { console.debug('saveAs', 'onload') console.dir(download) callback(null) }, ontimeout: (download) => { console.debug('saveAs', 'ontimeout') console.dir(download) callback(new Error('Network timeout')) } }) } /** * Sanitize Filename String * @param {String=} filename - Filename * @return {String} - Sanitized Filename */ let sanitize = (filename = '') => filename.trim().replace(/[^a-z0-9]/gi, '_') /** * Parse Video Timestamp * @return {Object|void} - Moment.js Object */ let parseTimestamp = () => { console.debug('parseTimestamp') // Parse const dateElement = document.querySelector('.video-publish-date') if (!dateElement) { return void 0 } // Format const dateText = dateElement.textContent.split('at').pop() const dateMoment = moment.utc(dateText, 'HH:mm UTC on MMMM Do, YYYY') const timestamp = dateMoment.format('YYYY-MM-DD') // Return return timestamp } /** * Parse Video Author * @return {String|void} - Video Author */ let parseAuthor = () => { console.debug('parseAuthor') // Parse const authorElement = document.querySelector('p.video-author > a') if (!authorElement) { return } // Format const authorText = authorElement.textContent // Return return authorText } /** * Parse Video Title * @return {String|void} - Video Title */ let parseTitle = () => { console.debug('parseTitle') // Parse const titleElement = document.querySelector('h1.page-title') || document.querySelector('title') if (!titleElement) { return } // Format const titleText = titleElement.textContent // Return return titleText } /** * Generate Filename * @return {String} Filename */ let generateFilename = () => { console.debug('generateFilename') let fileNameComponents = [] // Add timestamp, author, title fileNameComponents.push(parseTimestamp()) fileNameComponents.push(sanitize(parseAuthor())) fileNameComponents.push(sanitize(parseTitle())) // Sanitize fileNameComponents = fileNameComponents.filter(Boolean) // Join const fileName = fileNameComponents.join(' ') // Return return `${fileName}.${videoExtension}` } /** * Render download button * @param {String} url - Target URL */ let renderDownloadButton = (url) => { console.debug('renderDownloadButton') // Parse URL const urlObject = new URL(url) const urlHref = urlObject.href // Set filename const filename = generateFilename() || urlObject.pathname.url.split('/').pop() // Parent const parentElement = document.querySelector('.plyr__controls') // Button Element const buttonElement = document.createElement('button') buttonElement.className = 'plyr__control' buttonElement.innerHTML = ` <svg class="svg-inline--fa fa-download fa-w-16 action-icon" aria-hidden="true" data-prefix="fas" data-icon="download" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""> <path fill="currentColor" d="M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z"></path> </svg> <span class="plyr__tooltip">Download</span> ` buttonElement.id = 'download-button' buttonElement.href = urlHref buttonElement.download = filename buttonElement.target = '_blank' buttonElement.rel = 'noopener noreferrer' buttonElement.type = 'video/mp4' parentElement.appendChild(buttonElement) buttonElement.classList.add('ready') // Button Events buttonElement.onclick = (event) => { // Cancel regular download event.preventDefault() // Reset label, style buttonElement.classList.remove('error') buttonElement.classList.add('busy') // Start download saveAs(urlHref, filename, (error) => { // Error if (error) { buttonElement.classList.remove('busy') buttonElement.classList.add('error') return } // Success buttonElement.classList.remove('busy') }) } /** * Filename * @type {String} */ console.debug('Video Download button added for URL:', urlHref) } /** * Init */ let init = () => { console.info('init') // Add Stylesheet injectStylesheet() // Wait for .plyr onElementReady('.plyr', false, () => { // Detect Bitchute Version / Browser (test for "window.client") if (unsafeWindow.client) { // Wait for <video> (Chromium) onElementReady('video', false, () => { renderDownloadButton(unsafeWindow.client.torrents[0].urlList[0]) }) } else { // Wait for <source> (Firefox) onElementReady('source', false, (element) => { renderDownloadButton(element.src) }) } }) } /** @listens window:Event#load */ window.addEventListener('load', init())
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址