JavDB Exporter plus

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

  1. // ==UserScript==
  2. // @name JavDB Exporter plus
  3. // @version 1.3.1
  4. // @namespace https://gist.github.com/sqzw-x
  5. // @description 导出 想看、看过、清单 | Export Want, watched, list
  6. // @match https://javdb.com/users/want_watch_videos*
  7. // @match https://javdb.com/users/watched_videos*
  8. // @match https://javdb.com/users/list_detail*
  9. // @match https://javdb.com/lists*
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_listValues
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. const INTERVAL = 500; // 获取评论的请求间隔, 单位毫秒
  16.  
  17. const get_localStorage = (key) => JSON.parse(localStorage.getItem(key));
  18. const set_localStorage = (key, value) =>
  19. localStorage.setItem(key, JSON.stringify(value));
  20. const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  21.  
  22. let current_action = get_localStorage("current_action") || "";
  23. let actions = [];
  24. let current_result = get_localStorage("current_result") || [];
  25. let url = window.location.href;
  26. let root = window.location.origin;
  27.  
  28. // 执行前的准备工作
  29. function preExecute() {
  30. actions.map((b) => {
  31. b.button.disabled = true;
  32. });
  33. const allImages = document.querySelectorAll("img"); //移除图像增加速度
  34. allImages.forEach((image) => {
  35. image.remove();
  36. });
  37. }
  38.  
  39. // 重置状态
  40. function reset() {
  41. localStorage.removeItem("current_result");
  42. localStorage.removeItem("current_action");
  43. current_result = [];
  44. current_action = "";
  45. actions.map((b) => {
  46. b.button.disabled = false;
  47. b.button.textContent = b.button.textContent.replace("(运行中...)", "");
  48. });
  49. }
  50.  
  51. async function fetchWithRetry(url, maxAttempts = 5) {
  52. let response;
  53. let html;
  54. let attempts = 0;
  55.  
  56. while (attempts < maxAttempts) {
  57. try {
  58. response = await fetch(url);
  59. if (response.ok) {
  60. html = await response.text();
  61. return html;
  62. }
  63. if (response.status === 429) {
  64. attempts++;
  65. console.warn(
  66. `429 Too Many Requests. Retrying... (${attempts}/${maxAttempts - 1})`
  67. );
  68. await delay(1000 * attempts); // Exponential backoff
  69. }
  70. } catch (error) {
  71. console.error(`Fetch error: ${error.message}`);
  72. attempts++;
  73. await delay(1000 * attempts); // Exponential backoff
  74. }
  75. }
  76. if (!html) {
  77. throw new Error("Failed to fetch the page after multiple attempts");
  78. }
  79. return html;
  80. }
  81.  
  82. // 获取当前列表页中所有视频的信息
  83. async function getVideosInfo(with_comment) {
  84. const videoElements = document.querySelectorAll(".item");
  85. const parser = new DOMParser();
  86. const fetchPromises = Array.from(videoElements).map(
  87. async (element, index) => {
  88. const title = element.querySelector(".video-title").textContent.trim();
  89. const [number, ...titleWords] = title.split(" ");
  90. const formattedTitle = titleWords.join(" ");
  91. const [score, scoreNumber] = element
  92. .querySelector(".value")
  93. .textContent.replace(/[^0-9-.,]/g, "")
  94. .split(",");
  95. const premiered = element
  96. .querySelector(".meta")
  97. .textContent.replace(/[^0-9-]/g, "");
  98. const url = element.id.replace("video-", "");
  99. const full_url = root + "/v/" + url;
  100.  
  101. let comment = "";
  102. if (with_comment) {
  103. await delay(index * INTERVAL);
  104. console.info(`fetch ${full_url}`);
  105. const html = await fetchWithRetry(full_url);
  106. const doc = parser.parseFromString(html, "text/html");
  107. comment = doc.querySelector(".textarea").textContent;
  108. }
  109. return {
  110. number,
  111. title: formattedTitle,
  112. score: Number(score),
  113. scoreNumber: Number(scoreNumber),
  114. premiered: premiered,
  115. url: url,
  116. comment,
  117. };
  118. }
  119. );
  120. return Promise.all(fetchPromises).catch((e) => {
  121. console.error(e);
  122. return [];
  123. });
  124. }
  125.  
  126. // 导出视频信息
  127. async function doExport(with_comment = false) {
  128. preExecute();
  129. const res = await getVideosInfo(with_comment);
  130. current_result = current_result.concat(res);
  131. const nextPageButton = document.querySelector(".pagination-next");
  132. if (nextPageButton) {
  133. // 前往下一页
  134. set_localStorage("current_result", current_result);
  135. nextPageButton.click();
  136. return;
  137. }
  138. // 没有下一页, 导出结果
  139. downloadResult(current_result);
  140. // 重置状态
  141. reset();
  142. }
  143. function downloadResult(res) {
  144. const json = JSON.stringify(res, null, 2);
  145. const jsonUrl = URL.createObjectURL(
  146. new Blob([json], { type: "application/json" })
  147. );
  148. const downloadLink = document.createElement("a");
  149. const dateTime = new Date().toISOString().replace("T", " ").split(".")[0];
  150. let fileName = "";
  151. if (url.includes("/watched_videos")) {
  152. fileName = "watched-videos";
  153. } else if (url.includes("/want_watch_videos")) {
  154. fileName = "want-watch-videos";
  155. } else if (url.includes("/list_detail")) {
  156. const breadcrumb = document.getElementsByClassName("breadcrumb")[0];
  157. const li = breadcrumb.parentNode.querySelectorAll("li");
  158. fileName = li[1].innerText;
  159. } else if (url.includes("/lists")) {
  160. fileName = document.querySelector(".actor-section-name").innerText;
  161. }
  162. downloadLink.href = jsonUrl;
  163. downloadLink.download = `${fileName} ${dateTime}.json`;
  164. document.body.appendChild(downloadLink);
  165. downloadLink.click();
  166. }
  167.  
  168. async function tryUntilExist(fn, interval = 100) {
  169. return new Promise((resolve) => {
  170. const timer = setInterval(() => {
  171. console.debug("tryUntilExist", fn);
  172. const result = fn();
  173. if (result) {
  174. clearInterval(timer);
  175. resolve(result);
  176. }
  177. }, interval);
  178. });
  179. }
  180.  
  181. // 将当前列表页中的所有视频标记为看过
  182. async function markWatched() {
  183. const videoElements = document.querySelectorAll(".item");
  184. if (videoElements.length === 0) {
  185. return;
  186. }
  187. for (const element of videoElements) {
  188. const url = element.id.replace("video-", "");
  189. const full_url = root + "/v/" + url;
  190. console.info(`open ${full_url}`);
  191. const newWindow = window.open(
  192. full_url,
  193. "window-for-mark",
  194. "popup,left=100,top=1000,width=100,height=100"
  195. );
  196. if (!newWindow) {
  197. console.error("Failed to open new window");
  198. return;
  199. }
  200. // newWindow.location.href = full_url;
  201. while (true) {
  202. // 检查 div.review-title 内的文本是否包含 "看過"
  203. const e = await tryUntilExist(() =>
  204. newWindow.document.querySelector("div.review-title")
  205. );
  206. if (e.textContent.includes("看過")) {
  207. console.info("success");
  208. break;
  209. }
  210. // 点击看过按钮
  211. const watchedButton = await tryUntilExist(() =>
  212. newWindow.document.querySelector(
  213. "input[value='watched']#video_review_status_watched"
  214. )
  215. );
  216. watchedButton.click();
  217. console.debug(`click watched button`);
  218. // 点击保存按钮
  219. const saveButton = await tryUntilExist(() =>
  220. newWindow.document.querySelector(
  221. 'input[value="保存"].button.is-success'
  222. )
  223. );
  224. saveButton.click();
  225. console.debug(`click save button`);
  226. }
  227. await delay(1000);
  228. }
  229. }
  230.  
  231. async function doMarkWatched() {
  232. preExecute();
  233. await markWatched();
  234. const nextPageButton = document.querySelector(".pagination-next");
  235. if (nextPageButton) {
  236. nextPageButton.click();
  237. return;
  238. }
  239. // 关闭窗口
  240. const w = window.open("", "window-for-mark");
  241. w.close();
  242. reset();
  243. }
  244.  
  245. function runAction(action) {
  246. switch (action) {
  247. case "export-json":
  248. doExport();
  249. break;
  250. case "export-json-comment":
  251. doExport(true);
  252. break;
  253. case "mark-watched":
  254. doMarkWatched();
  255. break;
  256. }
  257. }
  258.  
  259. function init() {
  260. const handler = (action) => (e) => {
  261. const button = e.target;
  262. button.textContent += "(运行中...)";
  263. set_localStorage("current_action", action);
  264. runAction(action);
  265. };
  266. b1 = document.createElement("button");
  267. b1.textContent = "导出 json";
  268. b1.className = "button is-small";
  269. b1.addEventListener("click", handler("export-json"));
  270. actions.push({ button: b1 });
  271.  
  272. b2 = document.createElement("button");
  273. b2.textContent = "导出(包括评论)";
  274. b2.className = "button is-small";
  275. b2.addEventListener("click", handler("export-json-comment"));
  276. actions.push({ button: b2 });
  277.  
  278. b3 = document.createElement("button");
  279. b3.textContent = "标记为看过";
  280. b3.className = "button is-small";
  281. b3.addEventListener("click", handler("mark-watched"));
  282. actions.push({ button: b3 });
  283.  
  284. b4 = document.createElement("button");
  285. b4.textContent = "停止";
  286. b4.className = "button is-small";
  287. b4.addEventListener("click", () => {
  288. if (current_result.length > 0) downloadResult(current_result);
  289. reset();
  290. location.reload();
  291. });
  292.  
  293. [b1, b2, b3, b4].map((b) => {
  294. if (url.includes("/list_detail")) {
  295. document.querySelector(".breadcrumb").querySelector("ul").appendChild(b);
  296. } else {
  297. document.querySelector(".toolbar").appendChild(b);
  298. }
  299. });
  300. // 继续上页任务
  301. runAction(current_action);
  302. }
  303.  
  304. if (
  305. url.includes("/watched_videos") ||
  306. url.includes("/want_watch_videos") ||
  307. url.includes("/list_detail") ||
  308. url.includes("/lists")
  309. ) {
  310. init();
  311. }