JavDB Watched Marker

自动标记看过的影片

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
  1. // ==UserScript==
  2. // @name JavDB Watched Marker
  3. // @version 1.1
  4. // @namespace https://gist.github.com/sqzw-x
  5. // @description 自动标记看过的影片
  6. // @match https://javdb.com/*
  7. // @grant GM_registerMenuCommand
  8. // @grant GM_addElement
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. "use strict";
  13.  
  14. const get_localStorage = (key) => JSON.parse(localStorage.getItem(key));
  15. const set_localStorage = (key, value) =>
  16. localStorage.setItem(key, JSON.stringify(value));
  17.  
  18. const URL_SET_KEY = "javdb-watched-url-set";
  19. const NUM_SET_KEY = "javdb-watched-num-set";
  20.  
  21. let watchedURLSet = new Set(get_localStorage(URL_SET_KEY));
  22. let watchedNumSet = new Set(get_localStorage(NUM_SET_KEY));
  23.  
  24. // 创建隐藏的文件输入框
  25. const fileInput = GM_addElement(document.body, "input", {
  26. type: "file",
  27. accept: ".json",
  28. style: "display: none",
  29. });
  30.  
  31. fileInput.addEventListener("change", handleFileSelect);
  32.  
  33. // 注册油猴菜单
  34. GM_registerMenuCommand("导入看过列表", () => fileInput.click());
  35. GM_registerMenuCommand("手动添加看过", showManualInputDialog);
  36.  
  37. // 处理文件导入
  38. function handleFileSelect(event) {
  39. const file = event.target.files[0];
  40. if (!file) return;
  41.  
  42. const reader = new FileReader();
  43. reader.onload = (e) => {
  44. try {
  45. const r = JSON.parse(e.target.result);
  46. // r is an array of objects
  47. // each object has a url property and a number property
  48. // add each url to watchedURLSet and each num to watchedNumSet
  49. r.forEach((item) => {
  50. watchedURLSet.add(item.url);
  51. watchedNumSet.add(item.number);
  52. });
  53. set_localStorage(URL_SET_KEY, Array.from(watchedURLSet));
  54. set_localStorage(NUM_SET_KEY, Array.from(watchedNumSet));
  55. } catch (error) {
  56. console.error("解析文件失败:", error);
  57. alert("无法解析文件,请确保是有效的JSON格式");
  58. }
  59. };
  60. reader.readAsText(file);
  61. }
  62.  
  63. // 显示手动输入对话框
  64. function showManualInputDialog() {
  65. // 创建对话框容器
  66. const dialogContainer = document.createElement("div");
  67. dialogContainer.style.position = "fixed";
  68. dialogContainer.style.top = "0";
  69. dialogContainer.style.left = "0";
  70. dialogContainer.style.width = "100%";
  71. dialogContainer.style.height = "100%";
  72. dialogContainer.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
  73. dialogContainer.style.display = "flex";
  74. dialogContainer.style.justifyContent = "center";
  75. dialogContainer.style.alignItems = "center";
  76. dialogContainer.style.zIndex = "10000";
  77.  
  78. // 创建对话框内容
  79. const dialog = document.createElement("div");
  80. dialog.style.backgroundColor = "white";
  81. dialog.style.padding = "20px";
  82. dialog.style.borderRadius = "5px";
  83. dialog.style.width = "300px";
  84. dialog.style.maxWidth = "90%";
  85.  
  86. dialog.innerHTML = `
  87. <h3 style="margin-top: 0">添加已观看视频</h3>
  88. <div style="margin-bottom: 10px;">
  89. <label for="url-input">URLID:</label>
  90. <input id="url-input" type="text" style="width: 100%; box-sizing: border-box;" placeholder="例如: abc123">
  91. </div>
  92. <div style="margin-bottom: 15px;">
  93. <label for="number-input">番号:</label>
  94. <input id="number-input" type="text" style="width: 100%; box-sizing: border-box;" placeholder="例如: ABC-123">
  95. </div>
  96. <div style="text-align: right;">
  97. <button id="cancel-btn" style="margin-right: 10px; padding: 5px 10px;">取消</button>
  98. <button id="save-btn" style="padding: 5px 10px;">保存</button>
  99. </div>
  100. `;
  101.  
  102. dialogContainer.appendChild(dialog);
  103. document.body.appendChild(dialogContainer);
  104.  
  105. // 添加事件监听
  106. document.getElementById("cancel-btn").addEventListener("click", () => {
  107. document.body.removeChild(dialogContainer);
  108. });
  109.  
  110. document.getElementById("save-btn").addEventListener("click", () => {
  111. const urlInput = document.getElementById("url-input").value.trim();
  112. const numberInput = document.getElementById("number-input").value.trim();
  113.  
  114. if (urlInput) {
  115. // 从URL中提取ID部分
  116. const urlId = urlInput.includes("/v/")
  117. ? urlInput.split("/v/")[1]
  118. : urlInput;
  119. watchedURLSet.add(urlId);
  120. set_localStorage(URL_SET_KEY, Array.from(watchedURLSet));
  121. }
  122.  
  123. if (numberInput) {
  124. watchedNumSet.add(numberInput);
  125. set_localStorage(NUM_SET_KEY, Array.from(watchedNumSet));
  126. }
  127.  
  128. if (urlInput || numberInput) {
  129. alert("已添加到观看列表");
  130. markWatchedVideos(); // 重新标记视频
  131. } else {
  132. alert("请至少输入一项");
  133. return;
  134. }
  135.  
  136. document.body.removeChild(dialogContainer);
  137. });
  138. }
  139.  
  140. // 标记已看过的视频
  141. function markWatchedVideos() {
  142. const videos = document.querySelectorAll(".item");
  143.  
  144. const watched = (v) => {
  145. const link = v.querySelector("a");
  146. if (link) {
  147. const url = link.getAttribute("href")?.replace("/v/", "");
  148. return url && watchedURLSet.has(url) ? "看过" : null;
  149. }
  150. const num = v.querySelector(".video-title strong").textContent;
  151. return num && watchedNumSet.has(num) ? "可能看过" : null;
  152. };
  153.  
  154. videos.forEach((v) => {
  155. const t = watched(v);
  156. if (t) {
  157. const marker = document.createElement("span");
  158. marker.className = "review";
  159. marker.textContent = t;
  160. v.querySelector(".cover").appendChild(marker);
  161. }
  162. });
  163. }
  164.  
  165. // 初始化
  166. function init() {
  167. markWatchedVideos();
  168. }
  169.  
  170. init();