JavDB Exporter plus

导出 想看、看过、清单 | Export Want, watched, list

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         JavDB Exporter plus
// @version      1.3.1
// @namespace    https://gist.github.com/sqzw-x
// @description  导出 想看、看过、清单 | Export Want, watched, list
// @match        https://javdb.com/users/want_watch_videos*
// @match        https://javdb.com/users/watched_videos*
// @match        https://javdb.com/users/list_detail*
// @match        https://javdb.com/lists*
// @grant        GM_xmlhttpRequest
// @grant        GM_listValues
// @license MIT
// ==/UserScript==

const INTERVAL = 500; // 获取评论的请求间隔, 单位毫秒

const get_localStorage = (key) => JSON.parse(localStorage.getItem(key));
const set_localStorage = (key, value) =>
  localStorage.setItem(key, JSON.stringify(value));
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

let current_action = get_localStorage("current_action") || "";
let actions = [];
let current_result = get_localStorage("current_result") || [];
let url = window.location.href;
let root = window.location.origin;

// 执行前的准备工作
function preExecute() {
  actions.map((b) => {
    b.button.disabled = true;
  });
  const allImages = document.querySelectorAll("img"); //移除图像增加速度
  allImages.forEach((image) => {
    image.remove();
  });
}

// 重置状态
function reset() {
  localStorage.removeItem("current_result");
  localStorage.removeItem("current_action");
  current_result = [];
  current_action = "";
  actions.map((b) => {
    b.button.disabled = false;
    b.button.textContent = b.button.textContent.replace("(运行中...)", "");
  });
}

async function fetchWithRetry(url, maxAttempts = 5) {
  let response;
  let html;
  let attempts = 0;

  while (attempts < maxAttempts) {
    try {
      response = await fetch(url);
      if (response.ok) {
        html = await response.text();
        return html;
      }
      if (response.status === 429) {
        attempts++;
        console.warn(
          `429 Too Many Requests. Retrying... (${attempts}/${maxAttempts - 1})`
        );
        await delay(1000 * attempts); // Exponential backoff
      }
    } catch (error) {
      console.error(`Fetch error: ${error.message}`);
      attempts++;
      await delay(1000 * attempts); // Exponential backoff
    }
  }
  if (!html) {
    throw new Error("Failed to fetch the page after multiple attempts");
  }
  return html;
}

// 获取当前列表页中所有视频的信息
async function getVideosInfo(with_comment) {
  const videoElements = document.querySelectorAll(".item");
  const parser = new DOMParser();
  const fetchPromises = Array.from(videoElements).map(
    async (element, index) => {
      const title = element.querySelector(".video-title").textContent.trim();
      const [number, ...titleWords] = title.split(" ");
      const formattedTitle = titleWords.join(" ");
      const [score, scoreNumber] = element
        .querySelector(".value")
        .textContent.replace(/[^0-9-.,]/g, "")
        .split(",");
      const premiered = element
        .querySelector(".meta")
        .textContent.replace(/[^0-9-]/g, "");
      const url = element.id.replace("video-", "");
      const full_url = root + "/v/" + url;

      let comment = "";
      if (with_comment) {
        await delay(index * INTERVAL);
        console.info(`fetch ${full_url}`);
        const html = await fetchWithRetry(full_url);
        const doc = parser.parseFromString(html, "text/html");
        comment = doc.querySelector(".textarea").textContent;
      }
      return {
        number,
        title: formattedTitle,
        score: Number(score),
        scoreNumber: Number(scoreNumber),
        premiered: premiered,
        url: url,
        comment,
      };
    }
  );
  return Promise.all(fetchPromises).catch((e) => {
    console.error(e);
    return [];
  });
}

// 导出视频信息
async function doExport(with_comment = false) {
  preExecute();
  const res = await getVideosInfo(with_comment);
  current_result = current_result.concat(res);
  const nextPageButton = document.querySelector(".pagination-next");
  if (nextPageButton) {
    // 前往下一页
    set_localStorage("current_result", current_result);
    nextPageButton.click();
    return;
  }
  // 没有下一页, 导出结果
  downloadResult(current_result);
  // 重置状态
  reset();
}
function downloadResult(res) {
  const json = JSON.stringify(res, null, 2);
  const jsonUrl = URL.createObjectURL(
    new Blob([json], { type: "application/json" })
  );
  const downloadLink = document.createElement("a");
  const dateTime = new Date().toISOString().replace("T", " ").split(".")[0];
  let fileName = "";
  if (url.includes("/watched_videos")) {
    fileName = "watched-videos";
  } else if (url.includes("/want_watch_videos")) {
    fileName = "want-watch-videos";
  } else if (url.includes("/list_detail")) {
    const breadcrumb = document.getElementsByClassName("breadcrumb")[0];
    const li = breadcrumb.parentNode.querySelectorAll("li");
    fileName = li[1].innerText;
  } else if (url.includes("/lists")) {
    fileName = document.querySelector(".actor-section-name").innerText;
  }
  downloadLink.href = jsonUrl;
  downloadLink.download = `${fileName} ${dateTime}.json`;
  document.body.appendChild(downloadLink);
  downloadLink.click();
}

async function tryUntilExist(fn, interval = 100) {
  return new Promise((resolve) => {
    const timer = setInterval(() => {
      console.debug("tryUntilExist", fn);
      const result = fn();
      if (result) {
        clearInterval(timer);
        resolve(result);
      }
    }, interval);
  });
}

// 将当前列表页中的所有视频标记为看过
async function markWatched() {
  const videoElements = document.querySelectorAll(".item");
  if (videoElements.length === 0) {
    return;
  }
  for (const element of videoElements) {
    const url = element.id.replace("video-", "");
    const full_url = root + "/v/" + url;
    console.info(`open ${full_url}`);
    const newWindow = window.open(
      full_url,
      "window-for-mark",
      "popup,left=100,top=1000,width=100,height=100"
    );
    if (!newWindow) {
      console.error("Failed to open new window");
      return;
    }
    // newWindow.location.href = full_url;
    while (true) {
      // 检查 div.review-title 内的文本是否包含 "看過"
      const e = await tryUntilExist(() =>
        newWindow.document.querySelector("div.review-title")
      );
      if (e.textContent.includes("看過")) {
        console.info("success");
        break;
      }
      // 点击看过按钮
      const watchedButton = await tryUntilExist(() =>
        newWindow.document.querySelector(
          "input[value='watched']#video_review_status_watched"
        )
      );
      watchedButton.click();
      console.debug(`click watched button`);
      // 点击保存按钮
      const saveButton = await tryUntilExist(() =>
        newWindow.document.querySelector(
          'input[value="保存"].button.is-success'
        )
      );
      saveButton.click();
      console.debug(`click save button`);
    }
    await delay(1000);
  }
}

async function doMarkWatched() {
  preExecute();
  await markWatched();
  const nextPageButton = document.querySelector(".pagination-next");
  if (nextPageButton) {
    nextPageButton.click();
    return;
  }
  // 关闭窗口
  const w = window.open("", "window-for-mark");
  w.close();
  reset();
}

function runAction(action) {
  switch (action) {
    case "export-json":
      doExport();
      break;
    case "export-json-comment":
      doExport(true);
      break;
    case "mark-watched":
      doMarkWatched();
      break;
  }
}

function init() {
  const handler = (action) => (e) => {
    const button = e.target;
    button.textContent += "(运行中...)";
    set_localStorage("current_action", action);
    runAction(action);
  };
  b1 = document.createElement("button");
  b1.textContent = "导出 json";
  b1.className = "button is-small";
  b1.addEventListener("click", handler("export-json"));
  actions.push({ button: b1 });

  b2 = document.createElement("button");
  b2.textContent = "导出(包括评论)";
  b2.className = "button is-small";
  b2.addEventListener("click", handler("export-json-comment"));
  actions.push({ button: b2 });

  b3 = document.createElement("button");
  b3.textContent = "标记为看过";
  b3.className = "button is-small";
  b3.addEventListener("click", handler("mark-watched"));
  actions.push({ button: b3 });

  b4 = document.createElement("button");
  b4.textContent = "停止";
  b4.className = "button is-small";
  b4.addEventListener("click", () => {
    if (current_result.length > 0) downloadResult(current_result);
    reset();
    location.reload();
  });

  [b1, b2, b3, b4].map((b) => {
    if (url.includes("/list_detail")) {
      document.querySelector(".breadcrumb").querySelector("ul").appendChild(b);
    } else {
      document.querySelector(".toolbar").appendChild(b);
    }
  });
  // 继续上页任务
  runAction(current_action);
}

if (
  url.includes("/watched_videos") ||
  url.includes("/want_watch_videos") ||
  url.includes("/list_detail") ||
  url.includes("/lists")
) {
  init();
}