diff --git a/douyin/douyinVideoDownload.user.js b/douyin/douyinVideoDownload.user.js new file mode 100644 index 0000000..6b38d08 --- /dev/null +++ b/douyin/douyinVideoDownload.user.js @@ -0,0 +1,430 @@ +// ==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 = ` +
+ + + + 下载 +
+ `; + + // 添加样式 + 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 = ''; + } else { + downloadBtn.classList.add('disabled'); + downloadBtn.title = '等待视频加载...'; + text.textContent = '等待'; + icon.innerHTML = ''; + } + break; + + case 'downloading': + downloadBtn.classList.add('downloading'); + downloadBtn.title = '正在下载...'; + text.textContent = '下载中'; + icon.classList.add('spinning'); + icon.innerHTML = ''; + 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); + } +})(); \ No newline at end of file