// ==UserScript==
// @name 抖音/快手主页视频下载
// @namespace shortvideo_homepage_downloader
// @version 0.0.5
// @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
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @downloadURL none
// ==/UserScript==
const $ = selector => document.querySelectorAll('#_dialog '+selector)
const DOWNLOADED = 2
const DOWNLOADING = 1
const WAITTING = 0
const ERROR = -1
const VERSION = '0.0.5'
const RELEASE_DATE = '2024/07/26'
// 样式
GM_addStyle(`
body:has(dialog[open]) {
overflow: hidden;
}
`);
({
resources: [], running: false, downloads: {},
options: GM_getValue('config', {
threads: 2,
douyin_host: 0, // 抖音默认第一个线路
}),
saveOptions(opts){
GM_setValue('config', Object.assign(this.options, opts))
},
init(){ // 初始化
this.HOSTS = { // 网站规则
'www.kuaishou.com': {
title: '快手', id: 'kuaishou',
url: 'https://www.kuaishou.com/graphql',
type: 'json',
parseList: json => json?.data?.visionProfilePhotoList?.feeds,
parseItem: data => {
let {photo, author} = data
return {
status: WAITTING, author_name: author.name, id: photo.id, url: 'https://www.kuaishou.com/short-video/'+photo.id,
cover: photo.coverUrl,
video_url: photo.photoUrl,
// video_url: photo.videoResource.h264.adaptationSet[0].representation[0].url,
title: photo.originCaption,
data
}
}
},
'www.douyin.com': {
title: '抖音', id: 'douyin',
url: 'https://www.douyin.com/aweme/v1/web/aweme/post/',
type: 'network',
hosts: [0, 1, 2], // 3个线路
parseList: json => json?.aweme_list,
parseItem: data => {
let {video, desc, author, aweme_id} = data
if(video.format == 'mp4') return {
status: WAITTING,
id: aweme_id,
url: 'https://www.douyin.com/video/'+aweme_id,
cover: video.cover.url_list[0],
author_name: author.nickname,
video_url: video.play_addr.url_list.at(this.options.douyin_host),
title: desc,
data
}
}
},
}
let DETAIL = this.DETAIL = this.HOSTS[location.host]
if(!DETAIL) return
let callback = (...args) => this.callback.apply(this, args)
switch(DETAIL.type){
case 'json':
let parse = JSON.parse;
JSON.parse = function(raw) {
let json = parse(raw)
callback(json)
return json;
}
return
case 'network':
let originalSend = XMLHttpRequest.prototype.send, resources = []
XMLHttpRequest.prototype.send = function() {
this.addEventListener('load', function() {
if (this.responseURL.startsWith(DETAIL.url)) {
callback(JSON.parse(this.responseText))
}
});
originalSend.apply(this, arguments);
};
let originalFetch = window.fetch;
window.fetch = function() {
return originalFetch.apply(this, arguments).then(response => {
if (response.url.startsWith(DETAIL.url)) {
response.clone().json().then(json => callback(json));
}
return response;
});
}
return
}
},
callback(json){ // 捕获数据回调
let {resources, DETAIL} = this
let {parseList, parseItem} = DETAIL
let cnt = resources.push(...(parseList(json) || []).map(parseItem))
if(!cnt > 0) return
let fv = document.querySelector('#_ftb')
if(!fv){
fv = document.createElement('div')
fv.id = '_ftb'
fv.style.cssText = `position: fixed;bottom: 50px;right: 50px;border-radius: 20px;background-color: #fe2c55;color: white;z-index: 999;cursor: pointer;`
fv.onclick = () => this.showList(),
document.body.append(fv)
}
fv.innerHTML = `下载 ${cnt} 个视频`
},
showList(){ // 展示主界面
let threads = this.options['threads']
this.showDialog({
id: '_dialog',
html: `
`
}) & this.bindEvents() & this.writeLog(`欢迎使用!当前版本: ${VERSION} 发布日期: ${RELEASE_DATE}`)
},
showDialog({html, id, callback}){ // 弹窗
document.body.insertAdjacentHTML('beforeEnd', `