Greasy Fork

Greasy Fork is available in English.

抖音/快手主页视频下载

在抖音/快手主页右小角显示视频下载按钮

目前为 2024-07-25 提交的版本。查看 最新版本

// ==UserScript==
// @name         抖音/快手主页视频下载
// @namespace    shortvideo_homepage_downloader
// @version      0.0.2
// @description  在抖音/快手主页右小角显示视频下载按钮
// @author       hunmer
// @match        https://www.douyin.com/user/*
// @match        https://www.kuaishou.com/profile/*
// @icon         https://lf1-cdn-tos.bytegoofy.com/goofy/ies/douyin_web/public/favicon.ico
// @grant        GM_download
// @license      MIT
// ==/UserScript==

(function() {

    const HOSTS = {
        'www.kuaishou.com': {
            url: 'https://www.kuaishou.com/graphql',
            type: 'json',
            list: json => json?.data?.visionProfilePhotoList?.feeds,
            item: ({photo}) => {
                console.log(photo)
                return {
                    video_url: photo.videoResource.h264.adaptationSet[0].representation[0].url,
                    title: photo.originCaption,
                }
            }
        },
        'www.douyin.com': {
            url: 'https://www.douyin.com/aweme/v1/web/aweme/post/',
            type: 'network',
            list: json => json?.aweme_list,
            item: ({video, desc}) => {
                if(video.format == 'mp4') return {
                    video_url: video.play_addr.url_list[0],
                    title: desc,
                }
            }
        },
    }
    const DETAIL = HOSTS[location.host]
    console.log({DETAIL, host: location.host})
    if(!DETAIL) return

    var originalSend = XMLHttpRequest.prototype.send;
    var resources = [], fv
    var callback = json => {
        let cnt = resources.push(...DETAIL.list(json) || [])
        if(!cnt > 0) return
        if(!fv){
            fv = document.createElement('div')
            fv.style.cssText = `position: fixed;bottom: 50px;right: 50px;border-radius: 20px;background-color: #fe2c55;color: white;z-index: 999;cursor: pointer;`
            fv.onclick = () => {
                console.log(resources)
                if(confirm(`确定要下载${resources.length}个视频吗?`)){
                    let done = 0
                    const next = () => {
                        let item = resources.splice(0, 1)
                        fv.innerHTML = `${++done}/${resources.length}`
                        if(!item.length) return alert("下载完成")

                        let {video_url, title} = DETAIL.item(item[0]) || {}
                        if(!video_url) return
                        let detail = {
                            url: video_url,
                            name: safeFileName(title) + '.mp4',
                            conflictAction: 'overwrite',
                            onload: () => endOutput(`下载完成...`),
                            onerror: () => endOutput(`下载失败...`),
                            ontimeout: () => endOutput(`下载失败...`),
                            // onprogress: console.log,
                            headers: {
                                'User-Agent': 'Mozilla/5.0 (Linux; U; Android 4.0.2; en-us; Galaxy Nexus Build/ICL53F) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
                                'Referer': video_url
                            }
                        }
                        console.log(detail)
                        GM_download(detail)
                    }
                    const endOutput = (...args) => console.log(args) & next()
                    endOutput('开始下载')
                }
            }
            document.body.append(fv)
        }
        fv.innerHTML = `下载 ${cnt} 个视频`
    }
    switch(DETAIL.type){
        case 'json':
            var parse = JSON.parse;
            JSON.parse = function(raw) {
                let json = parse(raw)
                callback(json)
                return json;
            }
            return

        case 'network':
             XMLHttpRequest.prototype.send = function() {
                 this.addEventListener('load', function() {
                     if (this.responseURL.startsWith(DETAIL.url)) {
                         callback(JSON.parse(this.responseText))
                     }
                 });
                 originalSend.apply(this, arguments);
             };
            const originalFetch = window.fetch;
            window.fetch = function() {
                return originalFetch.apply(this, arguments).then(response => {
                    if (response.url.startsWith(DETAIL.url)) {
                        response.clone().json().then(callback);
                    }
                    return response;
                });
            }
            return
    }
   
    function safeFileName(str) {
        return str
            .replaceAll('(', '(')
            .replaceAll(')', ')')
            .replaceAll(':', ':')
            .replaceAll('*', '*')
            .replaceAll('?', '?')
            .replaceAll('"', '"')
            .replaceAll('<', '<')
            .replaceAll('>', '>')
            .replaceAll("|", "|")
            .replaceAll('\\', '\')
            .replaceAll('/', '/')
    }
})();