JavDB Filter

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

От 05.01.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==

// @name              JavDB Filter
// @name:zh-CN        JavDB 过滤插件

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

// @namespace         https://sleazyfork.org/users/263018
// @version           1.0.0
// @author            snyssss
// @license           MIT

// @match             https://javdb.com/*
// @include           /^https:\/\/(\w*\.)?javdb(\d)*\.com.*$/
// @icon              https://javdb.com/favicon-32x32.png
// @grant             none

// ==/UserScript==

// https://cdn.jsdelivr.net/npm/idb@8/build/umd.js
!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}));

(async () => {
  // 初始化 idb
  const db = await idb.openDB("JavDB-Filter", 1, {
    upgrade(db) {
      db.createObjectStore("videos", {
        keyPath: "id",
      });
      db.createObjectStore("idPrefixs", {
        keyPath: "value",
      });
      db.createObjectStore("directors", {
        keyPath: "value",
      });
      db.createObjectStore("makers", {
        keyPath: "value",
      });
      db.createObjectStore("series", {
        keyPath: "value",
      });
      db.createObjectStore("tags", {
        keyPath: "value",
      });
      db.createObjectStore("actors", {
        keyPath: "value",
      });
    },
  });

  // 超时时间
  const expiry = 24 * 60 * 60 * 1000;

  // 获取当前页面的 URL
  const url = window.location.href;

  // 检查 URL 是否为详情页
  const isDetailPage = url.startsWith("https://javdb.com/v/");

  // 检查是否包含屏蔽项
  const isBlocked = async (type, value) => {
    if (value) {
      const data = await db.get(type, value);

      return data !== undefined;
    }

    return false;
  };

  // 提取当前页面数据
  const getInfo = (doc) => {
    const info = doc.querySelector(".movie-panel-info");

    if (info === null) {
      return null;
    }

    const links = [...info.querySelectorAll("A")];

    const idPrefix = links
      .filter((link) => link.href.includes("/video_codes"))
      .map((link) => link.href.match(/(\w+)(\?f=\w+)?$/)[1]);

    const director = links
      .filter((link) => link.href.includes("/directors"))
      .map((link) => link.href.match(/(\w+)(\?f=\w+)?$/)[1]);

    const maker = links
      .filter((link) => link.href.includes("/makers"))
      .map((link) => link.href.match(/(\w+)(\?f=\w+)?$/)[1]);

    const series = links
      .filter((link) => link.href.includes("/series"))
      .map((link) => link.href.match(/(\w+)(\?f=\w+)?$/)[1]);

    const tags = links
      .filter((link) => link.href.includes("/tags"))
      .map((link) => link.href.match(/c\d=\d+/)[0]);

    const actors = links
      .filter((link) => link.href.includes("/actors"))
      .map((link) => link.href.match(/\w+$/)[0]);

    return {
      idPrefix: idPrefix.length > 0 ? idPrefix[0] : null,
      director: director.length > 0 ? director[0] : null,
      maker: maker.length > 0 ? maker[0] : null,
      series: series.length > 0 ? series[0] : null,
      tags,
      actors,
    };
  };

  // 绑定事件
  const bindEvents = (doc) => {
    const info = doc.querySelector(".movie-panel-info");

    if (info === null) {
      return null;
    }

    const links = [...info.querySelectorAll("A")];

    const bindEvent = (element, type, value) => {
      let timer = null;

      const check = async () => {
        const result = await isBlocked(type, value);

        if (result) {
          element.style.color = "red";
        } else {
          element.style.removeProperty("color");
        }

        return result;
      };

      element.addEventListener("mousedown", () => {
        timer = setTimeout(async () => {
          const isBlocked = await check();

          if (isBlocked) {
            await db.delete(type, value);
          } else {
            await db.put(type, { value });
          }

          await check();

          timer = null;
        }, 1000);
      });

      element.addEventListener("mouseup", () => {
        if (timer === null) {
          return;
        }

        clearTimeout(timer);

        window.location.href = element.href;
      });

      element.addEventListener("click", (e) => {
        e.preventDefault();
      });

      check();
    };

    links
      .filter((link) => link.href.includes("/video_codes"))
      .forEach((link) => {
        const value = link.href.match(/(\w+)(\?f=\w+)?$/)[1];

        bindEvent(link, "idPrefixs", value);
      });

    links
      .filter((link) => link.href.includes("/directors"))
      .forEach((link) => {
        const value = link.href.match(/(\w+)(\?f=\w+)?$/)[1];

        bindEvent(link, "directors", value);
      });

    links
      .filter((link) => link.href.includes("/makers"))
      .forEach((link) => {
        const value = link.href.match(/(\w+)(\?f=\w+)?$/)[1];

        bindEvent(link, "makers", value);
      });

    links
      .filter((link) => link.href.includes("/series"))
      .forEach((link) => {
        const value = link.href.match(/(\w+)(\?f=\w+)?$/)[1];

        bindEvent(link, "series", value);
      });

    links
      .filter((link) => link.href.includes("/tags"))
      .forEach((link) => {
        const value = link.href.match(/c\d=\d+/)[0];

        bindEvent(link, "tags", value);
      });

    links
      .filter((link) => link.href.includes("/actors"))
      .forEach((link) => {
        const value = link.href.match(/\w+$/)[0];

        bindEvent(link, "actors", value);
      });
  };

  // 非详情页的情况下,尝试过滤列表
  if (isDetailPage === false) {
    // 列表容器
    const list = document.querySelector(".movie-list");

    // 过滤函数
    const filter = (() => {
      let isRunning = false;
      let shouldRun = false;

      const run = async () => {
        // 防止重复执行
        if (isRunning) {
          shouldRun = true;
          return;
        }

        // 标识正在执行
        isRunning = true;
        shouldRun = false;

        // 查找列表下所有视频
        const items = list.querySelectorAll(".item");

        // 过滤视频
        for (const item of items) {
          // 获取链接、标题
          const { href, title } = item.querySelector("A");

          // 获取番号
          const id = item.querySelector("STRONG").textContent;

          // 优先从缓存中查找
          let data = await db.get("videos", id);

          // 有缓存的情况下,判断是否过期
          if (data && Date.now() - data.timestamp > expiry) {
            data = undefined;
          }

          // 没有缓存的情况下,请求数据
          if (data === undefined) {
            const response = await fetch(href);

            const parser = new DOMParser();

            const responseText = await response.text();

            const doc = parser.parseFromString(responseText, "text/html");

            const info = getInfo(doc);

            if (info === null) {
              // 防止爬虫检测
              await new Promise((resolve) => setTimeout(resolve, 1600));
              continue;
            }

            data = {
              id,
              title: title.trim(),
              timestamp: Date.now(),
              ...info,
            };

            await db.put("videos", data);
            await new Promise((resolve) => setTimeout(resolve, 160));
          }

          // 过滤
          const queue = [
            () => isBlocked("idPrefixs", data.idPrefix),
            () => isBlocked("directors", data.directors),
            () => isBlocked("makers", data.makers),
            () => isBlocked("series", data.series),
            ...data.tags.map((tag) => () => isBlocked("tags", tag)),
            ...data.actors.map((actor) => () => isBlocked("actors", actor)),
          ];

          for (const fn of queue) {
            if (await fn()) {
              item.remove();
              break;
            }
          }
        }

        // 执行完毕,判断是否需要继续执行
        isRunning = false;

        if (shouldRun) {
          run();
        }
      };

      return run;
    })();

    // 有列表的情况下,开始过滤
    if (list) {
      const observer = new MutationObserver(filter);

      observer.observe(list, {
        childList: true,
      });

      filter();
    }
    return;
  }

  // 详情页的情况下,获取数据
  const info = getInfo(document);

  if (info === null) {
    return;
  }

  // 获取番号
  const id = document.querySelector(".title > STRONG").textContent.trim();
  const title = document.querySelector(".title > .current-title").textContent;

  // 更新缓存
  await db.put("videos", {
    id,
    title: title.trim(),
    timestamp: Date.now(),
    ...info,
  });

  // 绑定事件
  bindEvents(document);
})();