Javdb_Emby助手

Javdb_Emby助手_weiShao

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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