Javdb_Emby助手

Javdb_Emby助手_weiShao

// ==UserScript==
// @name         Javdb_Emby助手
// @version      1.0.1
// @author       weiShao
// @description  Javdb_Emby助手_weiShao
// @license      MIT
// @icon         https://www.javdb.com/favicon.ico
// @match        https://*.javdb.com/*
// @match        *://*.javdb.com/*
// @connect      jable.tv
// @connect      missav.com
// @connect      javhhh.com
// @connect      netflav.com
// @connect      avgle.com
// @connect      bestjavporn.com
// @connect      jav.guru
// @connect      javmost.cx
// @connect      hpjav.tv
// @connect      av01.tv
// @connect      javbus.com
// @connect      javmenu.com
// @connect      javfc2.net
// @connect      paipancon.com
// @connect      ggjav.com
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @namespace https://greasyfork.org/users/434740
// ==/UserScript==

;(function () {
  'use strict'
  /* globals jQuery, $, waitForKeyElements */


  /**
   *   加载动画
   */
  const LoadingGif = {
    /**
     *    加载动画的元素
     */
    element: null,

    /**
     *  启动加载动画
     */
    start: function () {
      if (!this.element) {
        this.element = document.createElement('img')
        this.element.src =
          'https://upload.wikimedia.org/wikipedia/commons/3/3a/Gray_circles_rotate.gif'
        this.element.alt = '读取文件夹中...'
        Object.assign(this.element.style, {
          position: 'fixed',
          bottom: '0',
          left: '50px',
          zIndex: '1000',
          width: '40px',
          height: '40px',
          padding: '5px'
        })
        document.body.appendChild(this.element)
      }
    },

    /**
     *   停止加载动画
     */
    stop: function () {
      if (this.element) {
        document.body.removeChild(this.element)
        this.element = null
      }
    }
  }

  /**
   *   本地文件夹处理函数
   */
  const LocalFolderHandler = (function () {
    class LocalFolderHandlerClass {
      constructor() {
        this.nfoFileNamesSet = new Set()
        this.initButton()
      }

      /**
       * 创建一个按钮元素并添加到页面中
       */
      initButton() {
        const button = this.createButtonElement()
        button.addEventListener('click', this.handleButtonClick.bind(this))
        document.body.appendChild(button)
      }

      /**
       * 创建一个按钮元素
       * @returns {HTMLButtonElement}
       */
      createButtonElement() {
        const button = document.createElement('button')
        button.innerHTML = '仓'

        Object.assign(button.style, {
          color: '#fff',
          backgroundColor: '#FF8400',
          borderColor: '#FF8400',
          borderRadius: '5px',
          position: 'fixed',
          bottom: '2px',
          left: '2px',
          zIndex: '1000',
          padding: '5px 10px',
          cursor: 'pointer',
          fontSize: '16px',
          fontWeight: 'bold'
        })

        return button
      }

      /**
       * 按钮点击事件处理函数
       */
      async handleButtonClick() {
        this.nfoFileNamesSet.clear()

        const directoryHandle = await window.showDirectoryPicker()
        console.log(
          '%c Line:90 🍖 directoryHandle',
          'color:#42b983',
          directoryHandle.name
        )

        if (!directoryHandle) {
          alert('获取本地信息失败')
          return
        }

        const startTime = Date.now()
        LoadingGif.start()

        for await (const fileData of this.getFiles(directoryHandle, [
          directoryHandle.name
        ])) {
          const file = await fileData.fileHandle.getFile()
          const videoFullName = await this.findVideoFileName(
            fileData.parentDirectoryHandle
          )

          const item = {
            originalFileName: file.name.substring(
              0,
              file.name.length - '.nfo'.length
            ),
            transformedName: this.processFileName(file.name),
            videoFullName: videoFullName,
            hierarchicalStructure: [...fileData.folderNames, videoFullName]
          }

          this.nfoFileNamesSet.add(item)
        }

        const str = JSON.stringify(Array.from(this.nfoFileNamesSet))
        localStorage.setItem('nfoFiles', str)

        LoadingGif.stop()

        const endTime = Date.now()
        const time = ((endTime - startTime) / 1000).toFixed(2)

        alert(
          `读取文件夹: '${directoryHandle.name}' 成功,耗时 ${time} 秒, 共读取 ${this.nfoFileNamesSet.size} 个视频。`
        )

        onBeforeMount()
      }

      /**
       * 递归获取目录下的所有文件
       * @param {FileSystemDirectoryHandle} directoryHandle - 当前目录句柄
       * @param {string[]} folderNames - 目录名数组
       * @returns {AsyncGenerator}
       */
      async *getFiles(directoryHandle, folderNames = []) {
        for await (const entry of directoryHandle.values()) {
          try {
            if (entry.kind === 'file' && entry.name.endsWith('.nfo')) {
              yield {
                fileHandle: entry,
                folderNames: [...folderNames],
                parentDirectoryHandle: directoryHandle
              }
            } else if (entry.kind === 'directory') {
              yield* this.getFiles(entry, [...folderNames, entry.name])
            }
          } catch (e) {
            console.error(e)
          }
        }
      }

      /**
       * 查找视频文件名
       * @param {FileSystemDirectoryHandle} directoryHandle - 当前目录句柄
       * @returns {Promise<string>} 找到的视频文件名或空字符串
       */
      async findVideoFileName(directoryHandle) {
        for await (const entry of directoryHandle.values()) {
          if (
            entry.kind === 'file' &&
            (entry.name.endsWith('.mp4') ||
              entry.name.endsWith('.mkv') ||
              entry.name.endsWith('.avi') ||
              entry.name.endsWith('.flv') ||
              entry.name.endsWith('.wmv') ||
              entry.name.endsWith('.mov') ||
              entry.name.endsWith('.rmvb'))
          ) {
            return entry.name
          }
        }
        return ''
      }

      /**
       * 处理文件名
       * 去掉 '.nfo'、'-c'、'-C' 和 '-破解' 后缀,并转换为小写
       * @param {string} fileName - 原始文件名
       * @returns {string} 处理后的文件名
       */
      processFileName(fileName) {
        let processedName = fileName.substring(
          0,
          fileName.length - '.nfo'.length
        )
        processedName = processedName.replace(/-c$/i, '')
        processedName = processedName.replace(/-破解$/i, '')
        return processedName.toLowerCase()
      }
    }

    return function () {
      new LocalFolderHandlerClass()
    }
  })()

  /**
   * 列表页处理函数
   */
  const ListPageHandler = (function () {
    /**
     * @type {string} btsow 搜索 URL 基础路径
     */
    const btsowUrl = 'https://btsow.com/search/'

    /**
     * 获取本地存储的 nfo 文件名的 JSON 字符串
     * @returns {string[]|null} nfo 文件名数组或 null
     */
    function getNfoFiles() {
      const nfoFilesJson = localStorage.getItem('nfoFiles')
      return nfoFilesJson ? JSON.parse(nfoFilesJson) : null
    }

    /**
     * 创建本地打开视频所在文件夹按钮
     * @param {HTMLElement} ele 要添加的所在的元素
     */
    function createOpenLocalFolderBtn(ele) {
      if (ele.querySelector('.open_local_folder')) {
        return
      }

      const openLocalFolderBtnElement = document.createElement('div')
      openLocalFolderBtnElement.className = 'tag open_local_folder'
      openLocalFolderBtnElement.textContent = '本地打开'

      Object.assign(openLocalFolderBtnElement.style, {
        marginLeft: '10px',
        color: '#fff',
        backgroundColor: '#F8D714'
      })

      openLocalFolderBtnElement.addEventListener('click', function (event) {
        event.preventDefault()
        const localFolderPath = 'Z:\\日本'
        // 打开本地文件夹逻辑
      })

      ele.querySelector('.tags').appendChild(openLocalFolderBtnElement)
    }

    /**
     * 创建 btsow 搜索视频按钮
     * @param {HTMLElement} ele 要添加的所在的元素
     * @param {string} videoTitle 视频标题
     */
    function createBtsowBtn(ele, videoTitle) {
      if (ele.querySelector('.btsow')) {
        return
      }

      const btsowBtnElement = document.createElement('div')
      btsowBtnElement.className = 'tag btsow'
      btsowBtnElement.textContent = 'Btsow'

      Object.assign(btsowBtnElement.style, {
        marginLeft: '10px',
        color: '#fff',
        backgroundColor: '#FF8400'
      })

      btsowBtnElement.addEventListener('click', function (event) {
        event.preventDefault()
        window.open(`${btsowUrl}${videoTitle}`, '_blank')
      })

      ele.querySelector('.tags').appendChild(btsowBtnElement)
    }

    /**
     * 显示本地下载的文件名并改写样式
     * @param {HTMLElement} ele 元素
     * @param {Object} item 影片项
     */
    function displayOperationOfTheItemInQuestion(ele, item) {
      const imgElement = ele.querySelector('.cover img')
      imgElement.style.padding = '10px'
      imgElement.style.backgroundColor = '#FF0000'

      const videoTitleElement = document.createElement('div')
      videoTitleElement.textContent = item.originalFileName

      Object.assign(videoTitleElement.style, {
        margin: '1rem',
        backgroundColor: 'rgba(0, 0, 0, 0.5)',
        color: '#fff',
        fontSize: '.75rem',
        height: '2rem',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        borderRadius: '5px'
      })

      ele.querySelector('.box').appendChild(videoTitleElement)

      videoTitleElement.addEventListener('click', function () {
        navigator.clipboard.writeText(item.originalFileName)
        videoTitleElement.textContent = item.originalFileName + ' 复制成功'
      })
    }

    /**
     * 处理列表页逻辑
     */
    function handler() {
      const nfoFilesArray = getNfoFiles()
      if (!nfoFilesArray) {
        return
      }

      LoadingGif.start()

      $('.movie-list .item').each(function (index, ele) {
        const videoTitle = ele.querySelector('strong').innerText.toLowerCase()
        createBtsowBtn(ele, videoTitle)

        nfoFilesArray.forEach(function (item) {
          if (item.transformedName.includes(videoTitle)) {
            createOpenLocalFolderBtn(ele)
            displayOperationOfTheItemInQuestion(ele, item)
          }
        })
      })

      LoadingGif.stop()
    }

    return handler
  })()

  /**
   * 详情页处理函数
   */
  const DetailPageHandler = (function () {
    /**
     * 获取页面视频标题
     * @returns {string} 视频标题文本
     */
    function getVideoTitle() {
      return $('.video-detail strong').first().text().trim().toLowerCase()
    }

    /**
     * 从 localStorage 获取 nfoFiles
     * @returns {Array} nfoFiles 数组
     */
    function getNfoFiles() {
      const nfoFilesJson = localStorage.getItem('nfoFiles')
      return nfoFilesJson ? JSON.parse(nfoFilesJson) : null
    }

    /**
     * 设置 .video-meta-panel 背景色
     */
    function highlightVideoPanel() {
      $('.video-meta-panel').css({ backgroundColor: '#FFC0CB' })
    }

    /**
     * 创建或获取影片存在提示元素
     * @returns {HTMLElement} localFolderTitleListElement 元素
     */
    function createOrGetLocalFolderTitleListElement() {
      let localFolderTitleListElement = document.querySelector(
        '.localFolderTitleListElement'
      )
      if (!localFolderTitleListElement) {
        localFolderTitleListElement = document.createElement('div')
        localFolderTitleListElement.className = 'localFolderTitleListElement'
        localFolderTitleListElement.textContent = 'Emby已存在影片'

        Object.assign(localFolderTitleListElement.style, {
          color: '#fff',
          backgroundColor: '#FF8400',
          padding: '5px 10px',
          borderRadius: '5px',
          fontSize: '16px',
          fontWeight: 'bold',
          position: 'fixed',
          left: '20px',
          top: '200px',
          width: '240px'
        })

        document.body.appendChild(localFolderTitleListElement)
      }
      return localFolderTitleListElement
    }

    /**
     * 添加影片列表项
     * @param {Object} item 影片项
     */
    function addLocalFolderTitleListItem(item) {
      const localFolderTitleListElement =
        createOrGetLocalFolderTitleListElement()

      const localFolderTitleListItem = document.createElement('div')
      localFolderTitleListItem.className = 'localFolderTitleListItem'
      localFolderTitleListItem.textContent = item.originalFileName

      Object.assign(localFolderTitleListItem.style, {
        color: '#fff',
        padding: '5px 10px',
        borderRadius: '5px',
        fontSize: '16px',
        fontWeight: 'bold',
        marginTop: '10px'
      })

      localFolderTitleListElement.appendChild(localFolderTitleListItem)

      localFolderTitleListItem.addEventListener('click', function () {
        navigator.clipboard.writeText(item.transformedName)
        localFolderTitleListItem.textContent =
          item.originalFileName + ' 复制成功'
      })
    }

    /**
     * 排序种子列表
     */
    function sortBtList() {
      const magnetsContent = document.getElementById('magnets-content')
      if (!magnetsContent?.children.length) return

      const items = Array.from(magnetsContent.querySelectorAll('.item'))

      items.forEach(function (item) {
        const metaSpan = item.querySelector('.meta')
        if (metaSpan) {
          const metaText = metaSpan.textContent.trim()
          const match = metaText.match(/(\d+(\.\d+)?)GB/)
          const size = match ? parseFloat(match[1]) : 0
          item.dataset.size = size
        }
      })

      items.sort(function (a, b) {
        return b.dataset.size - a.dataset.size
      })

      const priority = {
        high: [],
        medium: [],
        low: []
      }

      items.forEach(function (item) {
        const nameSpan = item.querySelector('.name')
        if (nameSpan) {
          const nameText = nameSpan.textContent.trim()

          if (/(-c| -C)/i.test(nameText)) {
            priority.high.push(item)
            item.style.backgroundColor = '#FFCCFF'
          } else if (!/[A-Z]/.test(nameText)) {
            priority.medium.push(item)
            item.style.backgroundColor = '#FFFFCC'
          } else {
            priority.low.push(item)
          }
        }
      })

      magnetsContent.innerHTML = ''

      priority.high.forEach(function (item) {
        magnetsContent.appendChild(item)
      })
      priority.medium.forEach(function (item) {
        magnetsContent.appendChild(item)
      })
      priority.low.forEach(function (item) {
        magnetsContent.appendChild(item)
      })
    }

    /**
     * 主函数,处理详情页逻辑
     */
    function handler() {
      const videoTitle = getVideoTitle()
      if (!videoTitle) return

      const nfoFiles = getNfoFiles()
      if (!nfoFiles) return

      LoadingGif.start()

      nfoFiles.forEach(function (item) {
        if (item.transformedName.includes(videoTitle)) {
          highlightVideoPanel()
          addLocalFolderTitleListItem(item)
        }
      })

      sortBtList()

      LoadingGif.stop()
    }

    return handler
  })()

  /**
   *  页面加载前执行
   */
  async function onBeforeMount() {
    // 立即调用以初始化按钮和事件处理程序
    LocalFolderHandler()

    // 调用列表页处理函数
    ListPageHandler()

    // 调用详情页处理函数
    DetailPageHandler()
  }

  onBeforeMount()
})()