Greasy Fork

Greasy Fork is available in English.

B站表情包下载

批量下载B站表情包/收藏集图片

目前为 2024-09-12 提交的版本。查看 最新版本

// ==UserScript==
// @name         B站表情包下载
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  批量下载B站表情包/收藏集图片
// @author       jzh
// @match        https://*.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/2.6.1/jszip.min.js
// @license      MIT
// ==/UserScript==

;(function () {
  'use strict'
  const buttons = [
    {
      selector: '.emoji-list .emoji div',
      innerHTML: '下载选中系列表情包',
      alertMessage: '请先点击表情选项并选中需要下载的表情包'
      // <div title="[xxx]" style="background-image: url("https://xxx.png");"></div>
    },
    {
      selector: '.bili-im .right .dialog:not(.hide) .send-box .input-box .core-style img',
      innerHTML: '下载输入框表情包',
      alertMessage: '输入框没有表情包'
      // <img src="https://xxx.png" alt="[xxx]">
    },
    {
      selector: '.message-list-content .msg-item .emotion-items.emotion-items-big',
      innerHTML: '下载对话框表情包',
      alertMessage: '对话框没有表情包'
      // <a title="xxx">
      //  <div style="background-image:url(http://xxx.png);"></div>
      // </a>
    },
    {
      innerHTML: '下载评论区表情包',
      alertMessage: '评论区没有表情包'
      // <img src="//xxx.png@yyy" alt="[xxx]">
    },
    {
      innerHTML: '下载收藏集图片',
      alertMessage: '请参照教程打开收藏集卡池详情'
      // <div class="card-item">
      //   <div class="card-container">
      //     <div class="card">
      //       <div class="card-img">
      //         <img src="https://xxx.png@yyy" class="img" />
      //       </div>
      //     </div>
      //   </div>
      //   <div class="name">xxx</div>
      // </div>
    }
  ]
  // 是否私信
  const isMessage = location.href.startsWith('https://message.bilibili.com')
  // 是否通用格式的评论区
  const isCommonComment = /^https:\/\/www\.bilibili\.com\/(list|bangumi|read|opus)/.test(
    location.href
  )
  // 是否特殊格式的评论区
  const isSpecialComment = location.href.startsWith('https://www.bilibili.com/video')
  // 是否收藏集
  const isBlackboard = location.href.startsWith('https://www.bilibili.com/blackboard')
  const div = document.createElement('div')
  Object.assign(div.style, {
    position: 'fixed',
    top: '250px',
    right: 0,
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'center'
  })
  for (let i = 0; i < 5; i++) {
    if (isMessage) {
      if (i === 3 || i === 4) {
        continue
      }
    } else if (isCommonComment || isSpecialComment) {
      if (i !== 3) {
        continue
      }
    } else if (isBlackboard) {
      if (i !== 4) {
        continue
      }
    } else {
      return
    }
    const { selector, innerHTML, alertMessage } = buttons[i]
    const button = document.createElement('button')
    button.innerHTML = innerHTML
    Object.assign(button.style, {
      width: '125px',
      marginBottom: '25px',
      cursor: 'pointer',
      padding: '2px',
      border: '1px solid #767676',
      backgroundColor: '#efefef',
      fontSize: '12px'
    })
    button.onclick = () => {
      let emojis = []
      try {
        if (isMessage) {
          emojis = Array.from(document.querySelectorAll(selector))
        } else if (isCommonComment) {
          emojis = Array.from(
            // list
            // #app #mirror-vdcon .playlist-container--left .playlist-comment .comment
            // bangumi
            // #__next .home-container .main-container .plp-l .player-left-components #comment-module #comment-body
            // read
            // #app .article-detail #comment-wrapper.comment-wrapper .comment-m .article-comment
            // opus
            // #app .opus-detail .bili-tabs.opus-tabs .bili-tabs__content .bili-tab-pane .comment-wrap .bili-comment-container
            document.querySelectorAll(
              '.bili-comment .comment-container .reply-warp .reply-list .reply-item'
            )
          ).flatMap(item =>
            Array.from(
              item.querySelectorAll(
                '.root-reply-container .content-warp .root-reply .reply-content-container .reply-content .emoji-large'
              )
            ).concat(
              Array.from(item.querySelectorAll('.sub-reply-container .sub-reply-list')).flatMap(
                item2 =>
                  Array.from(
                    item2.querySelectorAll(
                      '.sub-reply-item .reply-content-container.sub-reply-content .reply-content .emoji-large'
                    )
                  )
              )
            )
          )
        } else if (isSpecialComment) {
          emojis = Array.from(
            document
              .querySelector(
                '#app #mirror-vdcon .left-container .left-container-under-player #commentapp bili-comments'
              )
              .shadowRoot.querySelectorAll('#contents #feed bili-comment-thread-renderer')
          ).flatMap(item =>
            Array.from(
              Array.from(
                item.shadowRoot
                  .querySelector('#comment')
                  .shadowRoot.querySelector('#body #main #content bili-rich-text')
                  .shadowRoot.querySelectorAll('#contents>img')
              ).concat(
                Array.from(
                  item.shadowRoot
                    .querySelector('#replies bili-comment-replies-renderer')
                    .shadowRoot.querySelectorAll(
                      '#expander #expander-contents bili-comment-reply-renderer'
                    )
                ).flatMap(item2 =>
                  Array.from(
                    item2.shadowRoot
                      .querySelector('#body #main bili-rich-text')
                      .shadowRoot.querySelectorAll('#contents>img')
                  )
                )
              )
            )
          )
        } else if (isBlackboard) {
          emojis = Array.from(
            document
              .querySelector('#mall-iframe')
              .contentDocument.querySelectorAll(
                '#app .digital-card .digital-card-content .drawer .content .v-switcher .v-switcher__content .v-switcher__content__wrap .v-switcher__content__item .dlc-detail .dlc-cards .scarcity-block'
              )
          ).flatMap(item => Array.from(item.querySelectorAll('.card-block .card-item')))
        }
      } catch (error) {
        console.log(error)
      }
      const seen = new Set()
      const filteredEmojis = emojis.filter(item => {
        const imgName = getImgName(item, i)
        if (i !== 0 && i !== 4 && !imgName.includes('_')) {
          // 排除普通表情
          return false
        }
        if (!seen.has(imgName)) {
          seen.add(imgName)
          return true
        }
        return false
      })
      if (filteredEmojis.length === 0) {
        alert(alertMessage)
        return
      }
      let zipName = ''
      // eslint-disable-next-line no-undef
      const zip = new JSZip()
      const promises = filteredEmojis.map(async (item, imgIndex) => {
        if (imgIndex === 0) {
          zipName = getZipName(item, i)
        }
        zip.file(
          getImgName(item, i) + '.png',
          await blobToBinary(await (await fetch(getUrl(item, i))).blob())
        )
      })
      Promise.all(promises).then(() => {
        const url = URL.createObjectURL(zip.generate({ type: 'blob' }))
        if (isCommonComment) {
          // a.click()会重定向
          window.open(url)
          URL.revokeObjectURL(url)
          return
        }
        const a = document.createElement('a')
        a.href = url
        a.download = zipName + '.zip'
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
        URL.revokeObjectURL(url)
      })
    }
    div.appendChild(button)
  }
  document.body.appendChild(div)
  function getUrl(item, type) {
    let url, index
    switch (type) {
      case 0:
        // url("https://xxx.png")
        // url("//xxx.png"
        url = item.style.backgroundImage
        // return item.style.backgroundImage.match(/url\((")(.*?)\1\)/)?.[2]
        return url.replace('url("', '').replace('")', '') || url
      case 1:
        return item.src
      case 2:
        // 'url("https://xxx.png")'
        // 'url("http://xxx.png")'
        url = item.querySelector('.img-emoji-big').style.backgroundImage
        // return backgroundImage.match(/url\((")(.*?)\1\)/)?.[2].replace('http:', 'https:')
        return url.replace('url("', '').replace('")', '').replace('http:', 'https:') || url
      case 3:
        // //xxx.png@yyy
        url = item.src
        index = url.indexOf('@')
        return index > 0 ? url.slice(0, index) : url
      case 4:
        url = item.querySelector('.card-container .card .card-img img').src
        index = url.indexOf('@')
        return index > 0 ? url.slice(0, index) : url
    }
  }
  function getImgName(item, type) {
    switch (type) {
      case 0:
        return item.title.slice(1, -1) || item.title
      case 1:
        return item.alt.slice(1, -1) || item.alt
      case 2:
        return item.title
      case 3:
        return item.alt.slice(1, -1) || item.alt
      case 4:
        return item.querySelector('.name').innerText
    }
  }
  function getZipName(item, type) {
    switch (type) {
      case 0:
        return item.title.slice(1, -1).match(/^(.*?)_/)?.[1] || item.title
      default:
        return getImgName(item, type) + '等'
    }
  }
  function blobToBinary(blob) {
    return new Promise((resolve, reject) => {
      let fileReader = new FileReader()
      fileReader.readAsArrayBuffer(blob)
      fileReader.onloadend = e => {
        resolve(new Uint8Array(e.target.result))
      }
      fileReader.onerror = () => {
        reject(new Error('blobToBinary failure'))
      }
    })
  }
})()