430 lines
14 KiB
JavaScript
430 lines
14 KiB
JavaScript
// ==UserScript==
|
||
// @name 抖音视频下载助手
|
||
// @namespace https://github.com/scriptCat/
|
||
// @version 1.0.0
|
||
// @description 抖音视频下载助手,监听视频详情API并提供下载功能
|
||
// @author wangxi
|
||
// @match https://www.douyin.com/*
|
||
// @grant GM_addStyle
|
||
// @grant GM_download
|
||
// @connect douyin.com
|
||
// @connect *
|
||
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
|
||
// @run-at document-start
|
||
// @license MIT
|
||
// ==/UserScript==
|
||
|
||
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// 配置
|
||
const CONFIG = {
|
||
enabled: true
|
||
};
|
||
|
||
// 当前视频信息
|
||
let currentVideoInfo = null;
|
||
|
||
// 简化工具函数
|
||
function showToast(message, type = 'info') {
|
||
const Toast = Swal.mixin({
|
||
toast: true,
|
||
position: 'top-end',
|
||
showConfirmButton: false,
|
||
timer: 2000
|
||
});
|
||
Toast.fire({ icon: type, title: message });
|
||
}
|
||
|
||
|
||
|
||
// 下载视频
|
||
function downloadVideo() {
|
||
if (!currentVideoInfo) {
|
||
showToast('没有可下载的视频', 'error');
|
||
return;
|
||
}
|
||
|
||
const filename = `${currentVideoInfo.videoId}.mp4`;
|
||
console.log('[抖音下载] 下载链接:', currentVideoInfo.downloadUrl);
|
||
console.log('[抖音下载] 文件名:', filename);
|
||
|
||
updateFloatingButton('downloading');
|
||
|
||
// 使用fetch下载并创建blob链接
|
||
fetch(currentVideoInfo.downloadUrl, {
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
'Referer': 'https://www.douyin.com/',
|
||
'Accept': '*/*'
|
||
}
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
return response.blob();
|
||
})
|
||
.then(blob => {
|
||
// 创建下载链接
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
showToast('下载成功', 'success');
|
||
updateFloatingButton('ready');
|
||
})
|
||
.catch(error => {
|
||
console.error('[抖音下载] 下载失败:', error);
|
||
showToast('下载失败', 'error');
|
||
updateFloatingButton('ready');
|
||
});
|
||
}
|
||
|
||
// 创建悬浮按钮
|
||
function createFloatingButton() {
|
||
// 检查是否已存在
|
||
if (document.querySelector('.douyin-download-float')) {
|
||
return;
|
||
}
|
||
|
||
const floatingButton = document.createElement('div');
|
||
floatingButton.className = 'douyin-download-float';
|
||
floatingButton.innerHTML = `
|
||
<div class="douyin-download-btn" title="下载视频">
|
||
<svg class="douyin-download-icon" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||
</svg>
|
||
<span class="douyin-download-text">下载</span>
|
||
</div>
|
||
`;
|
||
|
||
// 添加样式
|
||
GM_addStyle(`
|
||
.douyin-download-float {
|
||
position: fixed;
|
||
bottom: 100px;
|
||
right: 30px;
|
||
z-index: 9999;
|
||
user-select: none;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.douyin-download-float:hover {
|
||
opacity: 1;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.douyin-download-float.dragging {
|
||
transition: none !important;
|
||
opacity: 1;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.douyin-download-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px 16px;
|
||
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 100%);
|
||
color: white;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
|
||
transition: all 0.2s ease;
|
||
min-width: 80px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.douyin-download-btn:hover {
|
||
background: linear-gradient(135deg, #ff5252 0%, #ff7979 100%);
|
||
box-shadow: 0 6px 16px rgba(255, 107, 107, 0.4);
|
||
}
|
||
|
||
.douyin-download-btn:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.douyin-download-btn.disabled {
|
||
background: linear-gradient(135deg, #d1d5db 0%, #9ca3af 100%);
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.douyin-download-btn.downloading {
|
||
background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
|
||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||
}
|
||
|
||
.douyin-download-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.douyin-download-icon.spinning {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.douyin-download-text {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 吸附效果 */
|
||
.douyin-download-float.snap-right {
|
||
right: 0;
|
||
border-radius: 25px 0 0 25px;
|
||
}
|
||
|
||
.douyin-download-float.snap-left {
|
||
left: 0;
|
||
border-radius: 0 25px 25px 0;
|
||
}
|
||
`);
|
||
|
||
// 添加到页面
|
||
document.body.appendChild(floatingButton);
|
||
|
||
// 设置拖拽功能
|
||
setupDragAndSnap(floatingButton);
|
||
|
||
// 设置点击事件
|
||
const downloadBtn = floatingButton.querySelector('.douyin-download-btn');
|
||
downloadBtn.addEventListener('click', () => {
|
||
if (downloadBtn.classList.contains('disabled')) return;
|
||
|
||
// 点击反馈
|
||
downloadBtn.style.transform = 'scale(0.9)';
|
||
setTimeout(() => {
|
||
downloadBtn.style.transform = '';
|
||
}, 150);
|
||
|
||
downloadVideo();
|
||
});
|
||
|
||
|
||
}
|
||
|
||
// 设置拖拽和吸附功能
|
||
function setupDragAndSnap(floatingButton) {
|
||
let isDragging = false;
|
||
let startX, startY, startLeft, startTop;
|
||
|
||
floatingButton.addEventListener('mousedown', (e) => {
|
||
isDragging = true;
|
||
startX = e.clientX;
|
||
startY = e.clientY;
|
||
|
||
const rect = floatingButton.getBoundingClientRect();
|
||
startLeft = rect.left;
|
||
startTop = rect.top;
|
||
|
||
floatingButton.classList.add('dragging');
|
||
e.preventDefault();
|
||
});
|
||
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (!isDragging) return;
|
||
|
||
const deltaX = e.clientX - startX;
|
||
const deltaY = e.clientY - startY;
|
||
|
||
let newLeft = startLeft + deltaX;
|
||
let newTop = startTop + deltaY;
|
||
|
||
// 限制在视窗内
|
||
const maxX = window.innerWidth - floatingButton.offsetWidth;
|
||
const maxY = window.innerHeight - floatingButton.offsetHeight;
|
||
|
||
newLeft = Math.max(0, Math.min(newLeft, maxX));
|
||
newTop = Math.max(0, Math.min(newTop, maxY));
|
||
|
||
// 设置位置
|
||
floatingButton.style.left = newLeft + 'px';
|
||
floatingButton.style.top = newTop + 'px';
|
||
floatingButton.style.right = 'auto';
|
||
floatingButton.style.bottom = 'auto';
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
if (!isDragging) return;
|
||
|
||
isDragging = false;
|
||
floatingButton.classList.remove('dragging');
|
||
|
||
// 吸附效果
|
||
const rect = floatingButton.getBoundingClientRect();
|
||
const windowWidth = window.innerWidth;
|
||
const centerX = rect.left + rect.width / 2;
|
||
|
||
// 移除吸附类
|
||
floatingButton.classList.remove('snap-left', 'snap-right');
|
||
|
||
if (centerX < windowWidth / 2) {
|
||
// 吸附到左边
|
||
floatingButton.style.left = '0px';
|
||
floatingButton.style.right = 'auto';
|
||
floatingButton.classList.add('snap-left');
|
||
} else {
|
||
// 吸附到右边
|
||
floatingButton.style.right = '0px';
|
||
floatingButton.style.left = 'auto';
|
||
floatingButton.classList.add('snap-right');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新悬浮按钮状态
|
||
function updateFloatingButton(status = 'ready') {
|
||
const floatingButton = document.querySelector('.douyin-download-float');
|
||
if (!floatingButton) return;
|
||
|
||
const downloadBtn = floatingButton.querySelector('.douyin-download-btn');
|
||
const icon = floatingButton.querySelector('.douyin-download-icon');
|
||
const text = floatingButton.querySelector('.douyin-download-text');
|
||
|
||
if (!downloadBtn || !icon || !text) return;
|
||
|
||
// 移除所有状态类
|
||
downloadBtn.classList.remove('disabled', 'downloading');
|
||
icon.classList.remove('spinning');
|
||
|
||
switch (status) {
|
||
case 'ready':
|
||
if (currentVideoInfo) {
|
||
downloadBtn.title = `下载视频: ${currentVideoInfo.title}`;
|
||
text.textContent = '下载';
|
||
icon.innerHTML = '<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>';
|
||
} else {
|
||
downloadBtn.classList.add('disabled');
|
||
downloadBtn.title = '等待视频加载...';
|
||
text.textContent = '等待';
|
||
icon.innerHTML = '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>';
|
||
}
|
||
break;
|
||
|
||
case 'downloading':
|
||
downloadBtn.classList.add('downloading');
|
||
downloadBtn.title = '正在下载...';
|
||
text.textContent = '下载中';
|
||
icon.classList.add('spinning');
|
||
icon.innerHTML = '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>';
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 早期初始化
|
||
function earlyInit() {
|
||
// 简化的早期初始化
|
||
}
|
||
|
||
// 从页面提取视频信息
|
||
function extractVideoInfoFromPage() {
|
||
// 直接从HTML video/source标签提取
|
||
extractFromVideoTags();
|
||
}
|
||
|
||
|
||
|
||
// 从HTML video/source标签提取下载链接
|
||
function extractFromVideoTags() {
|
||
// 直接使用完整选择器获取第一个source
|
||
const firstSource = document.querySelector("xg-video-container video source:first-child");
|
||
|
||
if (firstSource) {
|
||
const src = firstSource.src || firstSource.getAttribute('src');
|
||
|
||
if (src) {
|
||
const videoId = extractVideoIdFromPage();
|
||
|
||
currentVideoInfo = {
|
||
videoId: videoId || 'unknown',
|
||
vid: 'vid',
|
||
downloadUrl: src,
|
||
title: '抖音视频'
|
||
};
|
||
|
||
updateFloatingButton();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从页面提取视频ID
|
||
function extractVideoIdFromPage() {
|
||
// 方法1:从URL提取
|
||
const urlMatch = window.location.href.match(/\/video\/(\d+)/);
|
||
if (urlMatch) return urlMatch[1];
|
||
|
||
// 方法2:从指定CSS选择器的链接提取
|
||
const linkElement = document.querySelector('#user_detail_element > div > div._1PChJilq > div > div > div._akC5KCF.W6aLyAOC > div > div.UTSD8keU.bVgnIvTC > ul > li:nth-child(4) > div > a');
|
||
if (linkElement && linkElement.href) {
|
||
const linkMatch = linkElement.href.match(/\/video\/(\d+)/);
|
||
if (linkMatch) return linkMatch[1];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 界面初始化
|
||
function uiInit() {
|
||
// 创建悬浮按钮
|
||
createFloatingButton();
|
||
|
||
// 提取视频信息(带重试机制)
|
||
function tryExtractVideo(retries = 5) {
|
||
extractVideoInfoFromPage();
|
||
if (!currentVideoInfo && retries > 0) {
|
||
setTimeout(() => tryExtractVideo(retries - 1), 1000);
|
||
}
|
||
}
|
||
tryExtractVideo();
|
||
|
||
// 监听页面变化
|
||
let currentUrl = location.href;
|
||
new MutationObserver(() => {
|
||
// 检查URL变化
|
||
if (location.href !== currentUrl) {
|
||
currentUrl = location.href;
|
||
currentVideoInfo = null;
|
||
updateFloatingButton();
|
||
setTimeout(() => tryExtractVideo(3), 500);
|
||
}
|
||
|
||
// 检查新video标签
|
||
if (!currentVideoInfo) {
|
||
setTimeout(extractFromVideoTags, 500);
|
||
}
|
||
}).observe(document, {
|
||
subtree: true,
|
||
childList: true
|
||
});
|
||
}
|
||
|
||
// 立即执行早期初始化
|
||
earlyInit();
|
||
|
||
// 等待页面准备好后初始化界面
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
setTimeout(uiInit, 500);
|
||
});
|
||
} else {
|
||
setTimeout(uiInit, 500);
|
||
}
|
||
})();
|