// ==UserScript==
// @name 抖音视频提取器(简洁版)
// @namespace http://tampermonkey.net/
// @version 1.1.5
// @description 提取抖音用户视频链接,支持正常提取和排序提取功能
// @author qqlcx5
// @match *://www.douyin.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=douyin.com
// @grant GM_setClipboard
// @run-at document-end
// @license MIT
// ==/UserScript==
;(function () {
'use strict'
/**
* 存储提取到的视频链接和点赞数
* Stores the extracted video links and like counts
*/
let videoLinks = []
/**
* 提取用户主页中的所有视频链接和点赞数
* Extracts all video links and like counts from the user's profile page
*/
function extractVideoLinks() {
// 修改为动态选择器组合
const selectors = [
'div[data-e2e="user-post-list"], div[data-e2e="user-like-list"], ul[data-e2e="scroll-list"]',
]
// 修复querySelector参数错误(原数组改为字符串)
const videoListContainer = document.querySelector(selectors.join(', '))
if (!videoListContainer) {
console.warn('未找到视频列表元素 (Video list container not found)')
return
}
// 选择所有以 "/video/" 开头的链接
// Select all links starting with "/video/"
const videoAnchorElements = videoListContainer.querySelectorAll('a[href*="/video/"]')
console.log(videoAnchorElements, 'videoAnchorElements')
videoLinks = Array.from(videoAnchorElements).map((anchor) => {
// 使用更稳定的data-e2e属性选择器
const videoElement = anchor.closest('li[data-e2e="user-post-item"]')
const likeCountElement = videoElement?.querySelector('span[data-e2e="like-count"]')
// 增加空值保护
const likeCount = likeCountElement ? parseLikeCount(likeCountElement.textContent) : 0
// 将点赞数拼接到 URL 上
const url = new URL(anchor.href)
url.searchParams.set('likeCount', likeCount)
return {
href: url.toString(),
likeCount: likeCount,
}
})
console.info(`提取到 ${videoLinks.length} 个视频链接。`)
}
/**
* 将点赞数文本转换为数字
* Converts like count text to a number
* @param {string} text - 点赞数文本 (Like count text)
* @returns {number} - 转换后的点赞数 (Converted like count)
*/
function parseLikeCount(text) {
if (text.includes('万')) {
return parseFloat(text) * 10000
}
return parseInt(text, 10)
}
/**
* 按点赞数排序视频链接
* Sorts video links by like count
* @param {boolean} ascending - 是否升序排序 (Whether to sort in ascending order)
*/
function sortVideoLinksByLikes(ascending = false) {
videoLinks.sort((a, b) => {
return ascending ? a.likeCount - b.likeCount : b.likeCount - a.likeCount
})
}
/**
* 复制所有视频链接到剪贴板
* Copies all video links to the clipboard
* @param {boolean} shouldSort - 是否按点赞数排序 (Whether to sort by like count)
*/
function copyAllVideoLinks(shouldSort = false) {
// 先尝试重新提取
extractVideoLinks()
// 增加重试机制
if (videoLinks.length === 0) {
setTimeout(() => {
extractVideoLinks()
if (videoLinks.length === 0) {
alert('请刷新页面确保视频加载完成后再试')
}
}, 1000)
return
}
if (shouldSort) {
sortVideoLinksByLikes()
notifyUser('已按点赞数降序排序。')
}
const linksText = videoLinks.map((video) => video.href).join('\n')
GM_setClipboard(linksText)
notifyUser(`已复制 ${videoLinks.length} 个视频链接到剪贴板。`)
}
/**
* 创建并添加悬浮按钮组到页面
* Creates and adds a floating button group to the page
*/
function createFloatingButtonGroup() {
const buttonGroup = document.createElement('div')
buttonGroup.id = 'floating-button-group'
// 样式设计 (Styling)
Object.assign(buttonGroup.style, {
position: 'fixed',
right: '20px',
bottom: '20px',
display: 'flex',
flexDirection: 'column',
gap: '10px',
zIndex: '10000',
})
// 正常提取按钮
const normalButton = createButton('顺序提取', '#1890FF', () => {
copyAllVideoLinks(false) // 不排序
})
// 排序提取按钮
const sortButton = createButton('点赞数提取', '#FF4D4F', () => {
copyAllVideoLinks(true) // 排序
})
// 添加按钮到按钮组
buttonGroup.appendChild(normalButton)
buttonGroup.appendChild(sortButton)
// 添加按钮组到页面主体
document.body.appendChild(buttonGroup)
}
/**
* 创建按钮
* Creates a button
* @param {string} text - 按钮文字 (Button text)
* @param {string} color - 按钮背景色 (Button background color)
* @param {function} onClick - 点击事件 (Click event)
* @returns {HTMLElement} - 按钮元素 (Button element)
*/
function createButton(text, color, onClick) {
const button = document.createElement('button')
button.textContent = text
// 样式设计 (Styling)
Object.assign(button.style, {
padding: '12px 20px',
backgroundColor: color,
color: '#fff',
border: 'none',
borderRadius: '16px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
cursor: 'pointer',
fontSize: '14px',
transition: 'background-color 0.3s, transform 0.3s',
})
// 鼠标悬停效果 (Hover Effects)
button.addEventListener('mouseenter', () => {
button.style.transform = 'scale(1.05)'
})
button.addEventListener('mouseleave', () => {
button.style.transform = 'scale(1)'
})
// 点击事件 (Click Event)
button.addEventListener('click', onClick)
return button
}
/**
* 显示提示通知给用户
* Displays a notification to the user
* @param {string} message - 要显示的消息 (Message to display)
*/
function notifyUser(message) {
// 创建通知元素
const notification = document.createElement('div')
notification.textContent = message
// 样式设计 (Styling)
Object.assign(notification.style, {
position: 'fixed',
bottom: '80px',
right: '20px',
backgroundColor: '#333',
color: '#fff',
padding: '10px 15px',
borderRadius: '5px',
opacity: '0',
transition: 'opacity 0.5s',
zIndex: '10000',
fontSize: '13px',
})
document.body.appendChild(notification)
// 触发动画
setTimeout(() => {
notification.style.opacity = '1'
}, 100) // Slight delay to allow transition
// 自动淡出和移除
setTimeout(() => {
notification.style.opacity = '0'
setTimeout(() => {
document.body.removeChild(notification)
}, 500) // 等待淡出完成
}, 3000) // 显示3秒
}
/**
* 初始化脚本
* Initializes the userscript
*/
function initializeScript() {
createFloatingButtonGroup()
observeDOMChanges() // 添加DOM变化监听
setTimeout(extractVideoLinks, 2000) // 添加延迟加载处理
console.info('抖音视频链接提取器已启用。')
}
// 等待页面内容加载完毕后初始化脚本
// Wait for the DOM to be fully loaded before initializing
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initializeScript()
} else {
document.addEventListener('DOMContentLoaded', initializeScript)
}
// 新增DOM变化监听
function observeDOMChanges() {
const observer = new MutationObserver((mutations) => {
if (document.querySelector('[data-e2e="user-post-item"]')) {
extractVideoLinks()
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false,
})
}
})()