BitChute | Download Button for Videos

Add a Download button to every video on BitChute

目前為 2018-12-01 提交的版本,檢視 最新版本

// ==UserScript==
// @name            BitChute | Download Button for Videos
// @namespace       de.sidneys.userscripts
// @homepage        https://gist.githubusercontent.com/sidneys/b4783b0450e07e12942aa22b3a11bc00/raw/
// @version         14.5.0
// @description     Add a Download button to every video on BitChute
// @author          sidneys
// @icon            https://www.bitchute.com/static/v76/images/android-icon-192x192.png
// @noframes
// @include         *://www.bitchute.com/video/*
// @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 */
Debug = false


/**
 * Video File Extension
 * @constant
 */
const fileExtension = 'mp4'


/**
 * Inject Stylesheet
 */
let injectStylesheet = () => {
    console.debug('injectStylesheet')

    GM.addStyle(`
        /* ==========================================================================
           ELEMENTS
           ========================================================================== */

        /* #download-button
           ========================================================================== */

        #download-button,
        #download-button:hover
        {
            color: rgb(41, 113, 237);
            display: inline-block;
            animation: fade-in 0.3s;
            pointer-events: all;
            filter: none;
            cursor: pointer;
            white-space: nowrap;
            transition: all 500ms ease-in-out;
            opacity: 0;
        }

        #download-button.ready
        {
            opacity: 1;
    	    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;
        }

        #download-button-label
        {
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
            transition: all 500ms ease-in-out;
        }

        /* .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 sanitizeFilename = (filename) => {
    console.debug('sanitizeFilename')

    return filename.trim().replace(/[^a-z0-9]/gi, '_')
}


/**
 * Generate Video Filename
 * @return {String} Filename
 */
let generateVideoFilename = () => {
    console.debug('generateVideoFilename')

    // Parse Page for File Name Components
    const videoAuthor = document.querySelector('p.video-author > a').textContent
    const pageTitle = document.querySelector('h1.page-title').textContent

    // Generate Safe File Title
    const fileTitle = `${sanitizeFilename(videoAuthor)} - ${sanitizeFilename(pageTitle)}`

    return `${fileTitle}.${fileExtension}`
}


/**
 * Render download button
 * @param {String} url - Target URL
 */
let renderDownloadButton = (url) => {
    console.debug('renderDownloadButton')

    // Label Text
    const labelText = 'Download'

    // Parse URL
    const urlObject = new URL(url)
    const urlHref = urlObject.href

    // Create filename
    const filename = generateVideoFilename() || urlObject.pathname.url.split('/').pop()

    // Parent
    const parentElement = document.querySelector('.video-information .video-statistics')

    // Button Element
    const buttonElement = document.createElement('a')
    buttonElement.innerHTML = `
        <i class="action-icon far fa-cloud-download-alt fa-fw"></i>
        <span id="download-button-label">${labelText}</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')

    // Label Element
    const labelElement = document.querySelector('#download-button-label')

    // Button Events
    buttonElement.onclick = (event) => {
        // Cancel regular download
        event.preventDefault()

        // Reset label, style
        labelElement.innerText = labelText
        buttonElement.classList.remove('error')
        buttonElement.classList.add('busy')

        // Start download
        saveAs(urlHref, filename, (error) => {
            // Error
            if (error) {
                labelElement.innerText = `${error}. Retry?`
                buttonElement.classList.remove('busy')
                buttonElement.classList.add('error')

                return
            }

            // Success
            buttonElement.classList.remove('busy')
        })
    }

    /**
     * Filename
     * @type {String}
     */
    console.debug('Added download button:', urlHref)
}


/**
 * Init
 */
let init = () => {
    console.info('init')

    // Add Stylesheet
    injectStylesheet()

    // Detect Bitchute Version / Browser (test for "window.client")
    if (unsafeWindow.client) {
        // Chromium
        onElementReady('video', false, () => {
            renderDownloadButton(unsafeWindow.client.torrents[0].urlList[0])
        })
    } else {
        // Firefox
        onElementReady('source', false, (element) => {
            renderDownloadButton(element.src)
        })
    }
}


/** @listens window:Event#load */
window.addEventListener('load', init())

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址