JavDB Filter

JavDB 过滤插件,用于过滤不感兴趣的标签、演员等

  1. // ==UserScript==
  2. // @name JavDB Filter
  3. // @name:zh-CN JavDB 过滤插件
  4. // @description JavDB 过滤插件,用于过滤不感兴趣的标签、演员等
  5. // @namespace https://sleazyfork.org/users/263018
  6. // @version 1.1.1
  7. // @author snyssss
  8. // @license MIT
  9. // @match https://javdb.com/*
  10. // @include /^https:\/\/(\w*\.)?javdb(\d)*\.com.*$/
  11. // @icon https://javdb.com/favicon-32x32.png
  12. // @grant none
  13.  
  14. // ==/UserScript==
  15.  
  16. // https://cdn.jsdelivr.net/npm/idb@8/build/umd.js
  17. !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).idb={})}(this,(function(e){"use strict";const t=(e,t)=>t.some((t=>e instanceof t));let n,r;const o=new WeakMap,s=new WeakMap,i=new WeakMap;let a={get(e,t,n){if(e instanceof IDBTransaction){if("done"===t)return o.get(e);if("store"===t)return n.objectStoreNames[1]?void 0:n.objectStore(n.objectStoreNames[0])}return f(e[t])},set:(e,t,n)=>(e[t]=n,!0),has:(e,t)=>e instanceof IDBTransaction&&("done"===t||"store"===t)||t in e};function c(e){a=e(a)}function u(e){return(r||(r=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])).includes(e)?function(...t){return e.apply(l(this),t),f(this.request)}:function(...t){return f(e.apply(l(this),t))}}function d(e){return"function"==typeof e?u(e):(e instanceof IDBTransaction&&function(e){if(o.has(e))return;const t=new Promise(((t,n)=>{const r=()=>{e.removeEventListener("complete",o),e.removeEventListener("error",s),e.removeEventListener("abort",s)},o=()=>{t(),r()},s=()=>{n(e.error||new DOMException("AbortError","AbortError")),r()};e.addEventListener("complete",o),e.addEventListener("error",s),e.addEventListener("abort",s)}));o.set(e,t)}(e),t(e,n||(n=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction]))?new Proxy(e,a):e)}function f(e){if(e instanceof IDBRequest)return function(e){const t=new Promise(((t,n)=>{const r=()=>{e.removeEventListener("success",o),e.removeEventListener("error",s)},o=()=>{t(f(e.result)),r()},s=()=>{n(e.error),r()};e.addEventListener("success",o),e.addEventListener("error",s)}));return i.set(t,e),t}(e);if(s.has(e))return s.get(e);const t=d(e);return t!==e&&(s.set(e,t),i.set(t,e)),t}const l=e=>i.get(e);const p=["get","getKey","getAll","getAllKeys","count"],D=["put","add","delete","clear"],I=new Map;function y(e,t){if(!(e instanceof IDBDatabase)||t in e||"string"!=typeof t)return;if(I.get(t))return I.get(t);const n=t.replace(/FromIndex$/,""),r=t!==n,o=D.includes(n);if(!(n in(r?IDBIndex:IDBObjectStore).prototype)||!o&&!p.includes(n))return;const s=async function(e,...t){const s=this.transaction(e,o?"readwrite":"readonly");let i=s.store;return r&&(i=i.index(t.shift())),(await Promise.all([i[n](...t),o&&s.done]))[0]};return I.set(t,s),s}c((e=>({...e,get:(t,n,r)=>y(t,n)||e.get(t,n,r),has:(t,n)=>!!y(t,n)||e.has(t,n)})));const B=["continue","continuePrimaryKey","advance"],b={},g=new WeakMap,v=new WeakMap,h={get(e,t){if(!B.includes(t))return e[t];let n=b[t];return n||(n=b[t]=function(...e){g.set(this,v.get(this)[t](...e))}),n}};async function*m(...e){let t=this;if(t instanceof IDBCursor||(t=await t.openCursor(...e)),!t)return;const n=new Proxy(t,h);for(v.set(n,t),i.set(n,l(t));t;)yield n,t=await(g.get(n)||t.continue()),g.delete(n)}function w(e,n){return n===Symbol.asyncIterator&&t(e,[IDBIndex,IDBObjectStore,IDBCursor])||"iterate"===n&&t(e,[IDBIndex,IDBObjectStore])}c((e=>({...e,get:(t,n,r)=>w(t,n)?m:e.get(t,n,r),has:(t,n)=>w(t,n)||e.has(t,n)}))),e.deleteDB=function(e,{blocked:t}={}){const n=indexedDB.deleteDatabase(e);return t&&n.addEventListener("blocked",(e=>t(e.oldVersion,e))),f(n).then((()=>{}))},e.openDB=function(e,t,{blocked:n,upgrade:r,blocking:o,terminated:s}={}){const i=indexedDB.open(e,t),a=f(i);return r&&i.addEventListener("upgradeneeded",(e=>{r(f(i.result),e.oldVersion,e.newVersion,f(i.transaction),e)})),n&&i.addEventListener("blocked",(e=>n(e.oldVersion,e.newVersion,e))),a.then((e=>{s&&e.addEventListener("close",(()=>s())),o&&e.addEventListener("versionchange",(e=>o(e.oldVersion,e.newVersion,e)))})).catch((()=>{})),a},e.unwrap=l,e.wrap=f}));
  18.  
  19. (async () => {
  20. // 初始化 idb
  21. const db = await idb.openDB("JavDB-Filter", 1, {
  22. upgrade(db) {
  23. db.createObjectStore("videos", {
  24. keyPath: "id",
  25. });
  26. db.createObjectStore("idPrefixs", {
  27. keyPath: "value",
  28. });
  29. db.createObjectStore("directors", {
  30. keyPath: "value",
  31. });
  32. db.createObjectStore("makers", {
  33. keyPath: "value",
  34. });
  35. db.createObjectStore("series", {
  36. keyPath: "value",
  37. });
  38. db.createObjectStore("tags", {
  39. keyPath: "value",
  40. });
  41. db.createObjectStore("actors", {
  42. keyPath: "value",
  43. });
  44. },
  45. }); // 超时时间
  46.  
  47. const expiry = 24 * 60 * 60 * 1000; // 获取当前页面的 URL
  48.  
  49. const url = window.location.href; // 检查 URL 是否为详情页
  50.  
  51. const isDetailPage = url.startsWith("https://javdb.com/v/"); // 检查是否包含屏蔽项
  52.  
  53. const isBlocked = async (type, value) => {
  54. if (value) {
  55. const data = await db.get(type, value);
  56.  
  57. return data !== undefined;
  58. }
  59.  
  60. return false;
  61. }; // 提取当前页面数据
  62.  
  63. const getInfo = (doc) => {
  64. const info = doc.querySelector(".movie-panel-info");
  65.  
  66. if (info === null) {
  67. return null;
  68. }
  69.  
  70. const links = [...info.querySelectorAll("A")];
  71.  
  72. const idPrefix = links
  73. .filter((link) => link.href.includes("/video_codes"))
  74. .map((link) => link.href.match(/(\w+)(\?f=\w+)?$/)[1]);
  75.  
  76. const director = links
  77. .filter((link) => link.href.includes("/directors"))
  78. .map((link) => link.href.match(/(\w+)(\?f=\w+)?$/)[1]);
  79.  
  80. const maker = links
  81. .filter((link) => link.href.includes("/makers"))
  82. .map((link) => link.href.match(/(\w+)(\?f=\w+)?$/)[1]);
  83.  
  84. const series = links
  85. .filter((link) => link.href.includes("/series"))
  86. .map((link) => link.href.match(/(\w+)(\?f=\w+)?$/)[1]);
  87.  
  88. const tags = links
  89. .filter((link) => link.href.includes("/tags"))
  90. .map((link) => link.href.match(/c\d=\d+/)[0]);
  91.  
  92. const actors = links
  93. .filter((link) => link.href.includes("/actors"))
  94. .map((link) => link.href.match(/\w+$/)[0]);
  95.  
  96. return {
  97. idPrefix: idPrefix.length > 0 ? idPrefix[0] : null,
  98. director: director.length > 0 ? director[0] : null,
  99. maker: maker.length > 0 ? maker[0] : null,
  100. series: series.length > 0 ? series[0] : null,
  101. tags,
  102. actors,
  103. };
  104. }; // 绑定事件
  105.  
  106. const bindEvents = (doc) => {
  107. const info = doc.querySelector(".movie-panel-info");
  108.  
  109. if (info === null) {
  110. return null;
  111. }
  112.  
  113. const links = [...info.querySelectorAll("A")];
  114.  
  115. const bindEvent = (element, type, value) => {
  116. let timer = null;
  117.  
  118. const check = async () => {
  119. const result = await isBlocked(type, value);
  120.  
  121. if (result) {
  122. element.style.color = "red";
  123. } else {
  124. element.style.removeProperty("color");
  125. }
  126.  
  127. return result;
  128. };
  129.  
  130. element.addEventListener("mousedown", () => {
  131. timer = setTimeout(async () => {
  132. const isBlocked = await check();
  133.  
  134. if (isBlocked) {
  135. await db.delete(type, value);
  136. } else {
  137. await db.put(type, { value });
  138. }
  139.  
  140. await check();
  141.  
  142. timer = null;
  143. }, 1000);
  144. });
  145.  
  146. element.addEventListener("mouseup", () => {
  147. if (timer === null) {
  148. return;
  149. }
  150.  
  151. clearTimeout(timer);
  152.  
  153. window.location.href = element.href;
  154. });
  155.  
  156. element.addEventListener("click", (e) => {
  157. e.preventDefault();
  158. });
  159.  
  160. check();
  161. };
  162.  
  163. links
  164. .filter((link) => link.href.includes("/video_codes"))
  165. .forEach((link) => {
  166. const value = link.href.match(/(\w+)(\?f=\w+)?$/)[1];
  167.  
  168. bindEvent(link, "idPrefixs", value);
  169. });
  170.  
  171. links
  172. .filter((link) => link.href.includes("/directors"))
  173. .forEach((link) => {
  174. const value = link.href.match(/(\w+)(\?f=\w+)?$/)[1];
  175.  
  176. bindEvent(link, "directors", value);
  177. });
  178.  
  179. links
  180. .filter((link) => link.href.includes("/makers"))
  181. .forEach((link) => {
  182. const value = link.href.match(/(\w+)(\?f=\w+)?$/)[1];
  183.  
  184. bindEvent(link, "makers", value);
  185. });
  186.  
  187. links
  188. .filter((link) => link.href.includes("/series"))
  189. .forEach((link) => {
  190. const value = link.href.match(/(\w+)(\?f=\w+)?$/)[1];
  191.  
  192. bindEvent(link, "series", value);
  193. });
  194.  
  195. links
  196. .filter((link) => link.href.includes("/tags"))
  197. .forEach((link) => {
  198. const value = link.href.match(/c\d=\d+/)[0];
  199.  
  200. bindEvent(link, "tags", value);
  201. });
  202.  
  203. links
  204. .filter((link) => link.href.includes("/actors"))
  205. .forEach((link) => {
  206. const value = link.href.match(/\w+$/)[0];
  207.  
  208. bindEvent(link, "actors", value);
  209. });
  210. }; // 非详情页的情况下,尝试过滤列表
  211.  
  212. if (isDetailPage === false) {
  213. // 列表容器
  214. const list = document.querySelector(".movie-list"); // 过滤函数
  215.  
  216. const filter = (() => {
  217. let isRunning = false;
  218.  
  219. const queue = [];
  220.  
  221. const getIsBlocked = async (data) => {
  222. const items = [
  223. () => isBlocked("idPrefixs", data.idPrefix),
  224. () => isBlocked("directors", data.directors),
  225. () => isBlocked("makers", data.maker),
  226. () => isBlocked("series", data.series),
  227. ...data.tags.map((tag) => () => isBlocked("tags", tag)),
  228. ...data.actors.map((actor) => () => isBlocked("actors", actor)),
  229. ];
  230.  
  231. for (const fn of items) {
  232. if (await fn()) {
  233. return true;
  234. }
  235. }
  236.  
  237. return false;
  238. };
  239.  
  240. const dequeue = async () => {
  241. if (isRunning) {
  242. return;
  243. }
  244.  
  245. if (queue.length === 0) {
  246. return;
  247. }
  248.  
  249. isRunning = true;
  250.  
  251. await queue.shift()();
  252.  
  253. isRunning = false;
  254.  
  255. dequeue();
  256. };
  257.  
  258. const run = async () => {
  259. // 查找列表下所有视频
  260. const items = list.querySelectorAll(".item"); // 过滤视频
  261.  
  262. for (const item of items) {
  263. // 获取链接、标题
  264. const { href, title } = item.querySelector("A"); // 获取番号
  265.  
  266. const id = item.querySelector("STRONG").textContent; // 过滤
  267.  
  268. const check = async (data) => {
  269. const items = [
  270. () => isBlocked("idPrefixs", data.idPrefix),
  271. () => isBlocked("directors", data.directors),
  272. () => isBlocked("makers", data.maker),
  273. () => isBlocked("series", data.series),
  274. ...data.tags.map((tag) => () => isBlocked("tags", tag)),
  275. ...data.actors.map((actor) => () => isBlocked("actors", actor)),
  276. ];
  277.  
  278. for (const fn of items) {
  279. if (await fn()) {
  280. item.remove();
  281. break;
  282. }
  283. }
  284. }; // 从缓存中查找
  285.  
  286. const data = await db.get("videos", id); // 有缓存,直接过滤
  287.  
  288. if (data) {
  289. await check(data); // 缓存没有过期,无需更新
  290.  
  291. if (Date.now() - data.timestamp < expiry) {
  292. continue;
  293. }
  294. }
  295.  
  296. // 请求后过滤
  297. const task = () =>
  298. new Promise(async (resolve) => {
  299. // 从缓存中查找
  300. const cache = await db.get("videos", id);
  301.  
  302. // 有缓存且没有过期,返回结果
  303. if (cache && Date.now() - cache.timestamp < expiry) {
  304. resolve(cache);
  305. return;
  306. }
  307.  
  308. // 请求数据
  309. const response = await fetch(href);
  310.  
  311. const parser = new DOMParser();
  312.  
  313. const responseText = await response.text();
  314.  
  315. const doc = parser.parseFromString(responseText, "text/html");
  316.  
  317. const info = getInfo(doc);
  318.  
  319. if (info === null) {
  320. // 防止爬虫检测
  321. await new Promise((resolve) => setTimeout(resolve, 1600));
  322.  
  323. resolve();
  324. return;
  325. }
  326.  
  327. const result = {
  328. id,
  329. title: title.trim(),
  330. timestamp: Date.now(),
  331. ...info,
  332. };
  333.  
  334. await db.put("videos", result);
  335. await new Promise((resolve) => setTimeout(resolve, 160));
  336.  
  337. resolve(result);
  338. }).then((data) => {
  339. if (data) {
  340. return check(data);
  341. }
  342. }); // 加入任务队列
  343.  
  344. queue.push(task);
  345. } // 执行队列
  346.  
  347. dequeue();
  348. };
  349.  
  350. return run;
  351. })(); // 有列表的情况下,开始过滤
  352.  
  353. if (list) {
  354. const observer = new MutationObserver(filter);
  355.  
  356. observer.observe(list, {
  357. childList: true,
  358. });
  359.  
  360. filter();
  361. }
  362. return;
  363. } // 详情页的情况下,获取数据
  364.  
  365. const info = getInfo(document);
  366.  
  367. if (info === null) {
  368. return;
  369. } // 获取番号
  370.  
  371. const id = document.querySelector(".title > STRONG").textContent.trim();
  372. const title = document.querySelector(".title > .current-title").textContent; // 更新缓存
  373.  
  374. await db.put("videos", {
  375. id,
  376. title: title.trim(),
  377. timestamp: Date.now(),
  378. ...info,
  379. }); // 绑定事件
  380.  
  381. bindEvents(document);
  382. })();