Pixiv Media Downloader

Simple media downloader for pixiv.net

目前為 2021-07-21 提交的版本,檢視 最新版本

// ==UserScript==
// @name Pixiv Media Downloader
// @description Simple media downloader for pixiv.net
// @version 0.3
// @icon https://pixiv.net/favicon.ico
// @homepageURL https://github.com/mkm5/pixiv-media-downloader
// @namespace https://github.com/mkm5
// @match https://www.pixiv.net/*
// @run-at document-idle
// @noframes
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js
// @require https://gf.qytechs.cn/scripts/2963-gif-js/code/gifjs.js
// @grant GM_xmlhttpRequest
// ==/UserScript==

const ARTWORK_URL = /https:\/\/www\.pixiv\.net\/([a-z]+\/)?artworks\/[0-9]+/

async function waitFor(f_condition) {
  return new Promise(resolve => {
    new MutationObserver((mutation, me) => {
      let result = f_condition(mutation)
      if (result) {
        resolve(result)
        me.disconnect()
        return
      }
    }).observe(document, {
      childList: true,
      subtree: true
    })
  })
}

function createButton(text, onclick) {
  const button = document.createElement("button")
  button.type = "button"
  button.innerText = text
  button.onclick = onclick
  button.style.marginRight = "10px"
  button.style.display = "inline-block"
  button.style.height = "32px"
  button.style.lineHeight = "32px"
  button.style.border = "none"
  button.style.background = "none"
  button.style.color = "inherit"
  button.style.fontWeight = "700"
  button.style.cursor = "pointer"
  button._setup = function () { this._ot = this.innerText; this.disabled = true; return this }
  button._reset = function () { this.innerText = this._ot; this.disabled = false }
  button._update = function (text) { this.innerText = this._ot + text }
  return button
}

function saveFile(filename, data) {
  let link = document.createElement("a")
  link.href = URL.createObjectURL(data)
  link.download = filename
  link.click()
  URL.revokeObjectURL(link.href)
  link.remove()
}

async function requestImage(url) {
  return new Promise(resolve => {
    GM_xmlhttpRequest({
      method: "GET",
      url: url,
      responseType: "blob",
      headers: { "Referer": "https://www.pixiv.net/" },
      onload: (req) => {
        console.log(`[${req.statusText}:${req.status}] ${req.finalUrl}`)
        if (req.status == 200) {
          resolve(req.response)
        }
      }
    })
  })
}

async function loadImage(src) {
  return new Promise(resolve => {
    const img = new Image()
    img.onload = () => resolve(img)
    img.src = src
  })
}

async function fetchImages(url_f, n, _call_on_fetch) {
  return Promise.all(
    [...Array(n).keys()].map(idx => {
      return new Promise(resolve => {
        const url = url_f(idx)
        requestImage(url)
        .then(data => {
          _call_on_fetch(idx, data)
          resolve([idx, data])
        })
      })
    })
  )
}

history.pushState = (function (_super) {
  return function () {
    const funcResult = _super.apply(this, arguments)
    if (window.location.href.match(ARTWORK_URL))
      scriptInit()
    return funcResult
  }
})(history.pushState);

(async function scriptInit() {
  if (!window.location.href.match(ARTWORK_URL))
    return;

  if (typeof GIF === "undefined") {
    const gif_script = document.createElement("script")
    gif_script.src = "https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"
    document.head.appendChild(gif_script)
  }

  const image_id = document.URL.split("/").pop()

  const illust_data_response = await fetch("https://www.pixiv.net/ajax/illust/" + image_id)
  const illust_data = (await illust_data_response.json()).body
  console.log("Fetched data:", illust_data)

  const filename = `${illust_data.illustTitle},${illust_data.illustId}-[${illust_data.userName}]-(${illust_data.createDate.split("T")[0]})`

  const button_section = await waitFor(() => {
    let sections = document.querySelectorAll("section")
    if (sections.length >= 2 && sections[1].childElementCount >= 3 /* NOTE: 3 for guests, 4 for logged users */ ) {
      return sections[1]
    }
  })


  if (illust_data.illustType == 0 || illust_data.illustType == 1) /* Picture & Manga */ {
    const url = illust_data.urls.original
    const extension = url.split(".").pop()

    if (illust_data.pageCount == 1) /* Single image mode */ {
      button_section.appendChild(createButton("Download original", async function () {
        const btn = this._setup()

        requestImage(url).then(data => {
          saveFile(filename + '.' + extension, data)
          btn._reset()
        })
      }))
      return;
    }

    const next_url = n => url.replace(/p\d+/, `p${n}`)

    button_section.appendChild(createButton("Download separately", async function () {
      const btn = this._setup()
      let i = 0
      await fetchImages(next_url, illust_data.pageCount, (idx, data) => {
        const percents = Math.round((++i / illust_data.pageCount) * 100)
        btn._update(` [${percents}%]`)
        saveFile(filename + `.p${idx}` + "." + extension, data)
      })
      btn._reset()
    }))

    button_section.appendChild(createButton("Download zip", async function () {
      const btn = this._setup()
      const zip = new JSZip()

      let i = 0
      await fetchImages(next_url, illust_data.pageCount, (idx, data) => {
        const percents = Math.round((++i / illust_data.pageCount) * 100)
        btn._update(` [${percents}%]`)
        zip.file(filename + `.p${idx}` + "." + extension, data, { binary: true })
      })

      zip.generateAsync({ type: "blob" }).then(content => {
        saveFile(filename + ".zip", content)
        btn._reset()
      })
    }))

    button_section.appendChild(createButton("Download continuous", async function () {
      const btn = this._setup()
      const canvas = document.createElement("canvas")
      canvas.width = 0
      canvas.height = 0
      const context = canvas.getContext("2d")

      let i = 0;
      const fetched_images = await fetchImages(next_url, illust_data.pageCount, () => {
        const percents = Math.round((++i / illust_data.pageCount) * 33)
        btn._update(` [${percents}%]`)
      })

      let j = 0;
      const images = await Promise.all(
        Object.values(fetched_images).map(([_, data]) => {
          const object_url = URL.createObjectURL(data)
          return new Promise(resolve => {
            loadImage(object_url).then(image => {
              if (canvas.width < image.width)
                canvas.width = image.width
              canvas.height += image.height

              const percents = Math.round(33 + (++j / illust_data.pageCount) * 33)
              btn._update(` [${percents}%]`)
              resolve(image)
              URL.revokeObjectURL(object_url)
            })
          })
        })
      )

      // TODO: Break image loading process when error occures
      if (canvas.height > 32757 || canvas.width > 32757) {
        btn._rest()
        alert("[Error] Image height would exceed the limit. Aborting.")
        return;
      }

      let k = 0
      let current_position = 0
      for (const image of images) {
        const percents = Math.round(66 + (++k / illust_data.pageCount) * 34)
        btn._update(` [${percents}%]`)
        context.drawImage(image, Math.round((canvas.width - image.width) / 2), current_position)
        current_position += image.height
      }

      canvas.toBlob(blob => {
        saveFile(filename + "." + extension, blob)
        btn._reset()
      })
    }))
  }
  else if (illust_data.illustType == 2) /* Ugoira */ {
    const ugoira_meta_response = await fetch("https://www.pixiv.net/ajax/illust/" + image_id + "/ugoira_meta")
    const ugoira_meta_data = (await ugoira_meta_response.json()).body

    button_section.appendChild(createButton("Download GIF", async function () {
      const btn = this._setup()

      btn._update(` [0%]`)
      const zip_file_response = await fetch(ugoira_meta_data.originalSrc)
      btn._update(` [25%]`)
      const zip_blob = await zip_file_response.blob()
      btn._update(` [50%]`)

      new JSZip().loadAsync(zip_blob)
        .then(async zip => {
          const gif = new GIF({ workers: 6, quality: 10, workerScript: GIF_worker_URL })
          gif.on("finished", blob => {
            saveFile(filename + ".gif", blob)
            btn._reset()
          })

          gif.on("progress", x => {
            btn._update(` [${Math.round(50 + x * 50)}%]`)
          })

          await Promise.allSettled(
            ugoira_meta_data.frames.map(async frame => {
              const data = await zip.file(frame.file).async("blob")
              const url = URL.createObjectURL(data)
              const image = await loadImage(url)
              gif.addFrame(image, { delay: frame.delay })
              URL.revokeObjectURL(url)
            })
          )
          gif.render()
        })
    }))
  }
})()

QingJ © 2025

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