抖音视频提取器(简洁版)

提取抖音用户视频链接,支持正常提取和排序提取功能

// ==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,
    })
  }
})()