您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
导出 想看、看过、清单 | Export Want, watched, list
// ==UserScript== // @name JavDB Exporter plus // @version 1.3.2 // @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']") ); 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); }; const b1 = document.createElement("button"); b1.textContent = "导出 json"; b1.className = "button is-small"; b1.addEventListener("click", handler("export-json")); actions.push({ button: b1 }); const b2 = document.createElement("button"); b2.textContent = "导出(包括评论)"; b2.className = "button is-small"; b2.addEventListener("click", handler("export-json-comment")); actions.push({ button: b2 }); const b3 = document.createElement("button"); b3.textContent = "标记为看过"; b3.className = "button is-small"; b3.addEventListener("click", handler("mark-watched")); actions.push({ button: b3 }); const 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(); }