Annict dアニメストア ニコニコ支店

Annictの作品詳細ページにdアニメストア ニコニコ支店のリンクを追加する

目前為 2022-09-25 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Annict dアニメストア ニコニコ支店
// @namespace    https://midra.me
// @version      1.0.3
// @description  Annictの作品詳細ページにdアニメストア ニコニコ支店のリンクを追加する
// @author       Midra
// @match        https://annict.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=annict.com
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      site.nicovideo.jp
// ==/UserScript==

(async function() {
  const ANNICT_EXT = {
    request: {
      config: {
        targetUrl: 'https://site.nicovideo.jp/danime/static/data/list.json',
      },
      async getDanimeList() {
        return new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: 'GET',
            url: this.config.targetUrl,
            responseType: 'json',
            onload: e => resolve(e.response),
            onerror: e => reject(e),
          })
        })
      },
    },

    cache: {
      _cache_key: '_mid_danimeList_cache',
      _lastupdated_key: '_mid_danimeList_lastUpdated',
      get() {
        try {
          const cache = JSON.parse(GM_getValue(this._cache_key))
          return Object.keys(cache).length !== 0 ? cache : undefined
        } catch(e) {
          console.error(e)
        }
      },
      getLastUpdated() {
        return new Date(GM_getValue(this._lastupdated_key)).getTime()
      },
      set(data) {
        if (Array.isArray(data) && data.length !== 0) {
          data.forEach(v => delete v['col_key'])
          data.sort((a, b) => a.title < b.title ? -1 : b.title < a.title ? 1 : 0)
          GM_setValue(this._cache_key, JSON.stringify(data))
          GM_setValue(this._lastupdated_key, new Date().getTime().toString())
          return true
        }
        return false
      },
      reset() {
        GM_deleteValue('_mid_danimeList_cache')
        GM_deleteValue('_mid_danimeList_lastUpdated')
      },
      async update() {
        const data = await ANNICT_EXT.request.getDanimeList()
        if (this.set(data)) {
          console.log('「dアニメストア ニコニコ支店」の作品リストを更新しました。')
        } else {
          console.error('「dアニメストア ニコニコ支店」の作品リストの更新に失敗しました。')
        }
      },
      isOld(period_h = 24) {
        const now = new Date().getTime()
        const lastUpdated = this.getLastUpdated()
        return (now - lastUpdated) >= (period_h * 216000)
      },
    },

    async getList() {
      let data = this.cache.get()
      if (data === undefined || this.cache.isOld()) {
        await this.cache.update()
        data = this.cache.get() || data
      }
      return data
    },

    async getMatchLink(title) {
      const list = await this.getList()
      if (list === undefined) return
      const annictTitle = this.normalizeTitle(title)
      if (annictTitle === '') return
      const result = list.reduce((result, item) => {
        const itemTitle = this.normalizeTitle(item.title)
        if (itemTitle === annictTitle) {
          result.item = item
        } else if (Math.abs(itemTitle.length - annictTitle.length) <= 15) {
          const idxA = itemTitle.indexOf(annictTitle)
          const idxB = annictTitle.indexOf(itemTitle)
          const idx = idxA !== -1 ? idxA : idxB !== -1 ? idxB : -1
          if (idx !== -1) {
            result.items.push({ idx, ...item })
          }
        }
        return result
      }, { item: null, items: [] })
      console.log({ item: result.item, items: [...result.items] })
      if (result.item !== null) {
        return result.item.url
      } else if (result.items.length !== 0) {
        result.items.sort((a, b) => a.idx < b.idx ? -1 : b.idx < a.idx ? 1 : 0)
        return result.items.pop().url
        // return result.items[0]?.url
      }
    },

    normalizeTitle(title = '') {
      return title.toLowerCase()
        .replace(/[\s-\(\)()「」\[\]]/g, '')
        .replace(/[A-Za-z0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0))
        .replace(/./g, s => ({
          '〜': '~',
          '?': '?',
          '!': '!',
          '”': '"',
          '’': "'",
          '´': "'",
          '`': '`',
          ':': ':',
          ',': ',',
          '.': '.',
          '・': '・',
          '/': '/',
          '#': '#',
          '$': '$',
          '%': '%',
          '&': '&',
          'ー': '-',
          '=': '=',
          '@': '@',
        }[s] || s))
    },

    async init() {
      if (this.cache.isOld()) {
        await this.cache.update()
      }
    },
  }

  ANNICT_EXT.init()

  let timeout = null
  const addLink = () => {
    if (timeout !== null) {
      clearTimeout(timeout)
    }
    if (!location.href.startsWith('https://annict.com/works/')) return
    timeout = setTimeout(async () => {
      const title = document.querySelector('.c-work-header h1.fw-bold.h2.mt-1 > a.text-body')
      const linkContainer = document.querySelector('.c-work-header ul.list-inline.mb-0')
      let streamingLinkContainer = document.querySelector('.c-work-header ul.list-inline.mt-2')
      const hasDanimeLink = Array.from(streamingLinkContainer.children).find(v => v.textContent.indexOf('dアニメストア ニコニコ支店') !== -1) !== undefined
      if (hasDanimeLink || title === null || streamingLinkContainer === null && linkContainer === null) return
      const url = await ANNICT_EXT.getMatchLink(title.textContent)
      if (url) {
        if (streamingLinkContainer === null) {
          linkContainer.insertAdjacentHTML('beforebegin',
            `<ul class="list-inline mt-2"></ul>`
          )
          streamingLinkContainer = document.querySelector('.c-work-header ul.list-inline.mt-2')
        }
        streamingLinkContainer.insertAdjacentHTML('beforeend',
          `<li class="list-inline-item mt-2"><a class="btn btn-outline-primary btn-sm rounded-pill" href="${url}" target="_blank" rel="noopener">dアニメストア (ニコニコ支店)<svg class="svg-inline--fa fa-external-link-alt fa-w-16 ms-1 small" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="external-link-alt" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="currentColor" d="M432,320H400a16,16,0,0,0-16,16V448H64V128H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V336A16,16,0,0,0,432,320ZM488,0h-128c-21.37,0-32.05,25.91-17,41l35.73,35.73L135,320.37a24,24,0,0,0,0,34L157.67,377a24,24,0,0,0,34,0L435.28,133.32,471,169c15,15,41,4.5,41-17V24A24,24,0,0,0,488,0Z"></path></svg></a></li>`
        )
      }
      timeout = null
    }, 200)
  }

  const obs = new MutationObserver(mutationList => {
    Array.from(mutationList).forEach(mutation => {
      Array.from(mutation.addedNodes).forEach(added => {
        if (added.nodeName === 'TITLE') {
          addLink()
        }
      })
    })
  })
  obs.observe(document.documentElement, {
    childList: true,
    subtree: true
  })

  addLink()
})()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址