Pixiv Mio Tools

获取pixiv的排行数据,并新增到mio

目前為 2024-04-03 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Pixiv Mio Tools
// @namespace    http://tampermonkey.net/
// @version      1.0.7
// @license      MIT
// @description  获取pixiv的排行数据,并新增到mio
// @author       kasuie
// @match        https://www.pixiv.net/ranking.php*
// @match        https://www.pixiv.net/artworks/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
// @grant        GM.xmlHttpRequest
// @grant        GM.notification
// @grant        GM.addStyle
// @grant        GM.getValue
// @grant        GM.setValue
// @run-at       document-end
// ==/UserScript==

(function () {
  "use strict";

  let DEV = false;

  let DATE = "";

  let mioDates = "";

  let artwork = null;

  const isArtwork = () => {
    if (
      window.location.pathname &&
      window.location.pathname.includes("artworks")
    ) {
      const pathname = window.location.pathname.split("/");
      if (pathname?.length) {
        return +pathname[pathname.length - 1];
      } else {
        return null;
      }
    } else {
      return null;
    }
  };

  let pid = isArtwork();

  const createRadio = (id, name, value, labelText, div, field) => {
    const radioButton = document.createElement("input");
    radioButton.type = "radio";
    radioButton.id = id;
    radioButton.name = name;
    radioButton.value = value;
    radioButton.addEventListener("click", () => {
      artwork[field] = +radioButton.value;
    });
    if (+artwork[field] == +value) {
      radioButton.checked = true;
    }

    const label = document.createElement("label");
    label.htmlFor = id;
    label.textContent = labelText;

    div.className = "mio-options-item";
    div.appendChild(radioButton);
    div.appendChild(label);
  };

  const renderOptions = (root) => {
    const tips = document.createElement("p");
    tips.className = "mio-result-message";
    const r18 = document.createElement("div");
    createRadio("r18-option0", "r18options", 10, "默认", r18, "r18");
    createRadio("r18-option1", "r18options", 0, "r12", r18, "r18");
    createRadio("r18-option2", "r18options", 1, "r18", r18, "r18");
    createRadio("r18-option3", "r18options", 2, "全年龄", r18, "r18");

    const wall = document.createElement("div");
    createRadio("wall-option0", "walloptions", 0, "默认", wall, "wallpaper");
    createRadio(
      "wall-option1",
      "walloptions",
      1,
      "横屏壁纸",
      wall,
      "wallpaper"
    );
    createRadio(
      "wall-option2",
      "walloptions",
      2,
      "竖屏壁纸",
      wall,
      "wallpaper"
    );
    createRadio("wall-option3", "walloptions", 3, "头像", wall, "wallpaper");

    root.appendChild(r18);
    root.appendChild(wall);
    root.appendChild(tips);
  };

  const topError = (text) => {
    const error = document.querySelector(".mio-error");
    error.innerText = text;
  };

  const onTips = (text, error) => {
    let msg = document.querySelector(".mio-result-message");
    if (msg) {
      msg.style.color = error ? "red" : "#69f769";
      msg.innerHTML = text;
    }
  };

  const format = (v, date, mode, uid, uploadName) => {
    let tags = v?.tags || [];
    let pageCount = +v.illust_page_count;
    let pathDate = null,
      pixAvatar = null,
      exts = [];
    if (v.attr == "original" && !tags.includes("original")) {
      tags.push("原创");
      tags.push("original");
    }
    if (tags?.length) {
      tags = tags.filter((vv) => {
        return (
          vv &&
          !vv.includes("收藏") &&
          !vv.includes("users") &&
          !vv.includes("bookmarks") &&
          !vv.includes("Bookmarks") &&
          !vv.includes("R-18")
        );
      });
    }
    const matches = v.url.match(
      /\/(\d{4}\/\d{2}\/\d{2}\/\d{2}\/\d{2}\/\d{2})\//
    );
    if (matches && matches[1]) {
      pathDate = matches[1];
    }
    const extArr = v.url?.split(".");
    if (extArr?.length) {
      const ext = extArr[extArr.length - 1];
      for (let index = 0; index < pageCount; index++) {
        exts.push(ext);
      }
    }

    if (v.profile_img && !v.profile_img.includes("no_profile")) {
      pixAvatar = v.profile_img
        ?.replace("https://i.pximg.net/user-profile/img/", "")
        ?.replace("_50", "");
    }

    return {
      pid: v.illust_id,
      uid: v.user_id,
      author:
        v.user_name
          ?.replace(/@(.*)/, "")
          ?.replace(/@(.*)/, "")
          ?.replace(/❤(.*)/, "")
          ?.replace(/■(.*)/, "")
          ?.replace(/▶(.*)/, "") || v.user_name,
      rankType: mode,
      tags: tags?.join(","),
      exts: exts[0],
      pageCount: pageCount,
      title: v.title,
      datePath: pathDate,
      pixAvatar,
      width: v.width,
      height: v.height,
      aspectRatio: Math.round((v.width / v.height) * 1000) / 1000,
      createDate: new Date(
        new Date(v.illust_upload_timestamp * 1000).toLocaleString("chinese", {
          hour12: false,
        })
      ),
      viewCount: v.view_count,
      ratingCount: v.rating_count,
      illusType: +v.illust_type,
      uploadName: uploadName,
      uploadUid: uid,
      status: v?.is_bookmarked ? v.yes_rank - 101 : v.yes_rank,
      startDate: v.yes_rank == 0 ? `${date}_${v.yes_rank}:${v.rank}` : null,
      endDate: v.yes_rank > 0 ? `${date}_${v.yes_rank}:${v.rank}` : null,
    };
  };

  const formatIllust = (image) => {
    const {
      id,
      height,
      width,
      aiType,
      pageCount,
      urls,
      createDate,
      bookmarkCount,
      illustType,
      title,
      tags: { tags: tagsObj },
      userName: author,
      userId: uid,
      userAccount: account,
    } = image;

    let datePath = null,
      pixAvatar = null,
      ext = null,
      tag = [],
      r18 = 10;

    const divAvatar = document.querySelector(`a[href="/users/${uid}"]`);

    if (divAvatar) {
      const src = divAvatar.querySelector("img")?.src || null;
      if (src && !src.includes("no_profile")) {
        pixAvatar = src
          .replace("https://i.pximg.net/user-profile/img/", "")
          ?.replace("_170", "")
          ?.replace("_50", "");
      }
    }

    if (urls && urls.original) {
      let matches = urls.original.match(
        /\/(\d{4}\/\d{2}\/\d{2}\/\d{2}\/\d{2}\/\d{2})\//
      );
      if (matches && matches[1]) {
        datePath = matches[1];
      }
      const arrs = urls.original.split(".");
      ext = arrs[arrs.length - 1];
    }

    if (tagsObj?.length) {
      tagsObj.forEach((v) => {
        if (r18 != 1 && v.tag.includes("R-18")) {
          return (r18 = 1);
        }
        if (
          v.tag.includes("收藏") ||
          v.tag.includes("users") ||
          v.tag.includes("bookmarks") ||
          v.tag.includes("Bookmarks")
        ) {
          return;
        }
        if (
          aiType != 2 &&
          (v.tag.includes("AIイラスト") || v.tag.includes("ai绘图"))
        ) {
          aiType = 2;
        }
        if (v.tag) {
          tag.push(v.tag);
        }
        if (v.translation && v.translation.en) {
          tag.push(v.translation.en);
        }
      });
    }

    tag = [...new Set(tag)];

    const userDom = document.querySelector("div.sc-1asno00-0");
    const uploadUserName = userDom?.getAttribute("title");

    return {
      pid: +id,
      pixAccount: account,
      illustType,
      height,
      width,
      aiType,
      pageCount,
      createDate,
      status: bookmarkCount,
      pixAvatar,
      title,
      uid,
      ext: [ext],
      r18,
      wallpaper: 0,
      aspectRatio: Math.round((width / height) * 1000) / 1000,
      author:
        author
          ?.replace(/@(.*)/, "")
          ?.replace(/@(.*)/, "")
          ?.replace(/❤(.*)/, "")
          ?.replace(/■(.*)/, "")
          ?.replace(/▶(.*)/, "") || author,
      datePath,
      tag,
      tags: tag.join(","),
      uploadUid: -1,
      uploadUserName,
    };
  };

  const getDate = (prev, next, date) => {
    let currentDate = new Date();

    if (!prev && !next) {
      if (
        currentDate.getHours() > 12 ||
        (currentDate.getHours() === 12 && currentDate.getMinutes() > 0)
      ) {
        currentDate.setDate(currentDate.getDate() - 1);
      } else {
        currentDate.setDate(currentDate.getDate() - 2);
      }
    } else if (date) {
      const year = date.slice(0, 4);
      const month = date.slice(4, 6) - 1;
      const day = date.slice(6, 8);
      currentDate = new Date(year, month, day);
      if (prev) {
        currentDate.setDate(currentDate.getDate() - 1);
      } else {
        currentDate.setDate(currentDate.getDate() + 1);
      }
    }
    const year = currentDate.getFullYear();
    const month = String(currentDate.getMonth() + 1).padStart(2, "0");
    const day = String(currentDate.getDate()).padStart(2, "0");
    return `${year}${month}${day}`;
  };

  const request = (data) => {
    return new Promise((resolve, reject) => {
      if (!data.method) {
        data.method = "get";
      }
      if (!data.timeout) {
        data.timeout = 10000;
      }
      data.onload = function (res) {
        try {
          resolve(JSON.parse(res.responseText));
        } catch (error) {
          reject(false);
        }
      };
      data.onerror = function (e) {
        reject(false);
      };
      data.ontimeout = function () {
        reject(false);
      };
      GM.xmlHttpRequest(data);
    });
  };

  const getRankAndToMio = (_e) => {
    if (!onCheckDate()) return;
    const urlParams = new URLSearchParams(window.location.search);
    let mode = urlParams.get("mode"),
      date = DATE;
    let data = [],
      url = `/ranking.php?format=json`,
      uid = null;

    if (mode) url = `${url}&mode=${mode}`;
    if (date) url = `${url}&date=${date}`;

    const userDom = document.querySelector("div.sc-1asno00-0");
    const uploadName = userDom.getAttribute("title");

    onLoading(true);

    const page_1 = request({
      method: "GET",
      url: `${url}&p=1`,
      headers: {
        referer: "https://www.pixiv.net/",
        "Accept-Language:":
          "zh-CN,zh-CN;q=0.9,zh;q=0.8,en-US;q=0.7,en,en-CN;q=0.6",
      },
    });

    const page_2 = request({
      method: "GET",
      url: `${url}&p=2`,
      headers: {
        referer: "https://www.pixiv.net/",
        "Accept-Language:":
          "zh-CN,zh-CN;q=0.9,zh;q=0.8,en-US;q=0.7,en,en-CN;q=0.6",
      },
    });

    Promise.all([page_1, page_2])
      .then(([res_1, res_2]) => {
        if (res_1 && res_2) {
          if (DEV) {
            console.log("page1:", res_1, "page_2", res_2);
          }
          const { contents: page1, date: date1, mode: mode1 } = res_1;
          const {
            contents: page2,
            date: date2,
            mode: mode2,
            prev_date,
            next_date,
          } = res_2;
          if (date1 == date2 && mode1 == mode2) {
            [...page1, ...page2].forEach((ele) => {
              // if (+ele?.illust_type == 0) {
              data.push(format(ele, date1, mode1, uid, uploadName));
              // }
            });
            return {
              rankDate: date1,
              prevDate: prev_date,
              nextDate: next_date,
              rankType: mode1,
              uploadName,
              rankList: data,
            };
          }
        }
      })
      .then((params) => {
        if (DEV) {
          console.log("请求mio参数:", params);
          return null;
        }
        content.innerHTML =
          content.innerHTML +
          `
          <div class="mio-pro-msg" style="display: flex;flex-direction: column;gap: 10px;">
            <p>当前排行榜类型为:${params.rankType}</p>
            <p>过滤一些非插画类型,实际抓取数据量为:${params.rankList.length}条</p>
            <p>开始发送数据...</p>
            <p style="color: #69f769;" class="mio-result-message"></p>
          </div>
        `;
        request({
          method: "POST",
          url: "https://kasuie.cc/apis/prank/newDate",
          headers: { "Content-Type": "application/json" },
          data: JSON.stringify(params),
        })
          .then((res) => {
            console.log("请求mio结果:", res);
            let msg = document.querySelector(".mio-result-message");
            if (res.success) {
              msg.innerHTML = "🎉好耶!发送数据成功~";
              if (mioDates) {
                GM.setValue("mio-dates", `${mioDates},${date}`);
                mioDates = `${mioDates},${date}`;
              } else {
                GM.setValue("mio-dates", date);
                mioDates = data;
              }
            } else {
              msg.style.color = "red";
              msg.innerHTML = "💔发送失败惹";
            }
            GM.notification(res.message);
          })
          .finally(() => {
            onLoading(false);
          });
      })
      .finally(() => {
        onLoading(false);
      });
  };

  const getArtworkAndToMio = (_e) => {
    console.log("artwork", artwork);
    if (+artwork.pid == +pid && artwork.illustType == 0) {
      if (DEV) {
        return console.log("新增mio请求参数:", artwork);
      }
      request({
        method: "POST",
        url: "https://kasuie.cc/apis/img/save",
        headers: { "Content-Type": "application/json" },
        data: JSON.stringify(artwork),
      })
        .then((res) => {
          console.log("请求新增mio结果:", res);
          if (res.success) {
            onTips(`🎉好耶!${res.message}~`);
          } else {
            onTips(`💔新增失败惹。${res.message}~`, true);
          }
        })
        .catch((e) => onTips(`💔新增失败惹。${e}~`, true))
        .finally(() => {
          onLoading(false);
        });
    } else {
      topError(
        artwork.pid != pid
          ? "❗pid不一致,请检查~"
          : "💤该作品类型不支持添加mio~"
      );
    }
  };

  const getArtwork = () => {
    request({
      method: "GET",
      url: `/ajax/illust/${pid}`,
      headers: {
        referer: "https://www.pixiv.net/",
        "Accept-Language:":
          "zh-CN,zh-CN;q=0.9,zh;q=0.8,en-US;q=0.7,en,en-CN;q=0.6",
      },
    }).then((res) => {
      if (!res.error) {
        artwork = formatIllust(res.body);
        if (
          artwork.illustType != 0 ||
          artwork.tag.includes("漫画") ||
          artwork.tag.includes("manga")
        ) {
          topError("💤该作品类型不支持添加mio~");
          addMio.disabled = true;
        } else {
          topError("");
          addMio.disabled = false;
        }
        content.innerHTML =
          content.innerHTML +
          `
            <p>Pid:${artwork.pid} 画师:${artwork.author}</P>
            <p>标题:${artwork.title}</P>
            <p>标签:${artwork.tags}</P>
            `;
        renderOptions(content);
      }
    });
  };

  const NOW = getDate();

  /** 操作按钮组 */
  const actions = document.createElement("div");
  /** 弹框内容 */
  const content = document.createElement("div");
  /** 提交到mio按钮 */
  const addMio = document.createElement("button");
  /** 提交到mio按钮 */
  const prevBtn = document.createElement("button");
  /** 提交到mio按钮 */
  const nextBtn = document.createElement("button");
  /** 关闭弹框按钮 */
  const span = document.createElement("span");

  const html = document.querySelector("html");

  /** 弹框遮罩 */
  const div = document.createElement("div");
  div.id = "mio-tools";

  /** 弹框 */
  const main = document.createElement("div");
  main.className = "mio-tools-main";

  const onLoading = (loading) => {
    if (loading) {
      addMio.disabled = true;
    } else {
      addMio.disabled = false;
    }
  };

  const onModalChange = async () => {
    if (div.classList.contains("mio-tools-open")) {
      html.style.overflow = "unset";
      div.classList.remove("mio-tools-open");
      content.innerHTML = null;
    } else {
      mioDates = await GM.getValue("mio-dates", "");
      html.style.overflow = "hidden";
      div.classList.add("mio-tools-open");
      pid = isArtwork();
      if (pid) {
        content.innerHTML = `
          <p style="color: #f5765c;" class="mio-error"></p>
          <p>正在获取:<span style="color: #69f769;" class="mio-date">${pid}</span>数据</p>
          `;
        getArtwork();
      } else {
        if (!DATE) {
          DATE = getDate();
        }
        if (NOW == DATE) {
          nextBtn.disabled = true;
        }
        content.innerHTML = `
          <p style="color: #f5765c;" class="mio-error"></p>
          <p>将要获取排行榜数据日期为:<span style="color: #69f769;" class="mio-date">${DATE}</span></p>
          `;
        onCheckDate();
      }
    }
  };

  const onCheckDate = () => {
    if (mioDates && mioDates.includes(DATE)) {
      topError("💤当前日期已抓取过~");
      return false;
    } else {
      const error = document.querySelector(".mio-error");
      error.innerText = "";
      return true;
    }
  };

  span.innerHTML = `
  <svg width="24" height="24" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
  `;
  span.className = "mio-tools-main-close";
  span.addEventListener("click", (_e) => onModalChange());

  addMio.innerText = "抓取并提交Mio";
  addMio.className = "mio-btn-add";
  addMio.addEventListener("click", (_e) => {
    if (pid) {
      getArtworkAndToMio(_e);
    } else {
      getRankAndToMio(_e);
    }
  });
  prevBtn.innerText = "前一天";
  prevBtn.className = "mio-btn-prev";
  prevBtn.addEventListener("click", (_e) => {
    const mioDate = document.querySelector(".mio-date");
    const proMsg = content.querySelector(".mio-pro-msg");
    DATE = getDate(true, null, DATE);
    if (NOW != DATE && nextBtn.disabled) {
      nextBtn.disabled = false;
    }
    if (proMsg) {
      content.removeChild(proMsg);
    }
    mioDate.innerText = DATE;
    onCheckDate();
  });
  nextBtn.innerText = "后一天";
  nextBtn.className = "mio-btn-next";
  nextBtn.addEventListener("click", (_e) => {
    const mioDate = document.querySelector(".mio-date");
    if (NOW == DATE) {
      nextBtn.disabled = true;
    } else {
      DATE = getDate(null, true, DATE);
      const proMsg = content.querySelector(".mio-pro-msg");
      if (proMsg) {
        content.removeChild(proMsg);
      }
      mioDate.innerText = DATE;
      onCheckDate();
    }
  });

  actions.className = "mio-tools-main-btns";

  if (!pid) {
    actions.appendChild(prevBtn);
    actions.appendChild(nextBtn);
  }
  actions.appendChild(addMio);

  content.className = "mio-tools-main-content";
  main.appendChild(span);
  main.appendChild(content);
  main.appendChild(actions);

  div.appendChild(main);

  const btn = document.createElement("button");
  btn.id = "mio-tools-btn";
  btn.addEventListener("click", (_e) => onModalChange());
  btn.innerHTML = "Mio";

  document.querySelector("body").appendChild(btn);

  GM.addStyle(`
    html {
      &::-webkit-scrollbar {
        width: 4px;
        transition: all .3s ease-in-out;
      }
      &::-webkit-scrollbar-thumb {
        cursor: pointer;
        border-radius: 10px;
        transition: all .15s ease-in-out;
        background: rgba(255, 255, 255, 0.15);
        box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
      }
      &::-webkit-scrollbar-track {
        border-radius: 10px;
        box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
        background: rgba(255, 255, 255, 0.05);
      }
      &::-webkit-scrollbar-thumb:hover {
        @apply bg-[#64d1e2];
      }
    }
    #mio-tools-btn {
        position: fixed;
        right: 0px;
        top: 85%;
        border-radius: 16px;
        width: 36px;
        height: 36px;
        outline: none;
        border: none;
        padding: 6px 10px;
        z-index: 10;
        background: #0097fac7;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        color: #fff;
    }
    #mio-tools {
        position: fixed;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 0;
        height: 0;
        overflow: hidden;
        top: 0;
        right: 0;
        background: #00000080;
        backdrop-filter: blur(2px);
        z-index: 99;
     }
    .mio-tools-open {
        width: 100% !important;
        height: 100vh !important;

        > .mio-tools-main {
            position: relative;
            background: #010101;
            width: 600px;
            height: 300px;
            border-radius: 16px;
            padding: 32px;
            transition: all .1s ease-in-out;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            gap: 16px;

            .mio-tools-main-close {
                position: absolute;
                right: 10px;
                top: 10px;
                color: #ffffff;
                cursor: pointer;
                rotate: 0deg;
                transition: all .3s ease-in-out;

                &:hover {
                    transform: scale(1.1);
                    color: #0097fa;
                    rotate: 180deg;
                }
            }

            .mio-tools-main-content {
                flex: 1;
                color: #ffffff;
                display: flex;
                flex-direction: column;
                gap: 10px;

                .mio-options-item {
                  display: flex;
                  align-items: center;
                  gap: 5px;
                  user-select: none;

                  > input, label {
                    cursor: pointer;
                  }

                  > label {
                    margin-right: 20px;
                  }
                }
            }

            .mio-tools-main-btns {
                display: flex;
                justify-content: flex-end;
                gap: 16px;

                .mio-btn-add, .mio-btn-prev, .mio-btn-next {
                    outline: none;
                    border: none;
                    padding: 6px 10px;
                    border-radius: 10px;
                    cursor: pointer;
                    background: #0097fa;
                    color: #ebebeb;
                }

                button:disabled {
                  opacity: 0.7;
                  cursor: not-allowed;
                }
            }
        }
    }
  `);

  document.querySelector("body").appendChild(div);
})();