JavDB Custom Blocker

Supports custom code prefix、Title Keywords、Release Time、Minimum Rating Filter、Western content filter, etc.。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         JavDB Custom Blocker
// @namespace    JavDB Custom Blocker
// @version      5.0
// @description  Supports custom code prefix、Title Keywords、Release Time、Minimum Rating Filter、Western content filter, etc.。
// @author       CNOS
// @match        https://javdb.com/*
// @match        https://javdb570.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=javdb.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // --- CSS Style ---
  const styles = `
        :root {
            --jd-primary: #e53935;
            --jd-primary-hover: #b71c1c;
            --jd-bg: #1a1a1a;
            --jd-card-bg: #2d2d2d;
            --jd-text: #e0e0e0;
            --jd-text-sub: #9e9e9e;
            --jd-border: #424242;
            --jd-shadow: 0 8px 24px rgba(0,0,0,0.6);
        }

        #javdb-filter-btn {
            position: fixed;
            bottom: 25px;
            left: 25px;
            z-index: 9999;
            background-color: rgba(229, 57, 53, 0.9);
            color: white;
            border: none;
            border-radius: 50px;
            padding: 12px 24px;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(0,0,0,0.4);
            transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
            display: flex;
            align-items: center;
            gap: 8px;
            backdrop-filter: blur(4px);
        }
        #javdb-filter-btn:hover {
            background-color: var(--jd-primary);
            transform: translateY(-2px) scale(1.02);
            box-shadow: 0 6px 16px rgba(229, 57, 53, 0.4);
        }
        .filter-badge {
            background: white;
            color: var(--jd-primary);
            border-radius: 12px;
            padding: 2px 8px;
            font-size: 12px;
            font-weight: 800;
            min-width: 18px;
            text-align: center;
        }

        #javdb-filter-modal {
            display: none;
            position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            background-color: rgba(0, 0, 0, 0.85);
            backdrop-filter: blur(5px);
            z-index: 10000;
            opacity: 0;
            transition: opacity 0.2s ease;
            justify-content: center;
            align-items: center;
        }
        #javdb-filter-modal.show { opacity: 1; }

        .filter-modal-content {
            background-color: var(--jd-bg);
            color: var(--jd-text);
            width: 680px;
            max-width: 95%;
            height: 620px;
            border-radius: 16px;
            box-shadow: var(--jd-shadow);
            display: flex;
            flex-direction: column;
            border: 1px solid var(--jd-border);
            overflow: hidden;
            animation: modalSlideIn 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
        }

        @keyframes modalSlideIn {
            from { transform: translateY(20px) scale(0.95); opacity: 0; }
            to { transform: translateY(0) scale(1); opacity: 1; }
        }

        .filter-header {
            padding: 20px 24px;
            border-bottom: 1px solid var(--jd-border);
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: linear-gradient(to bottom, #252525, #1f1f1f);
        }
        .filter-title { font-size: 18px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
        .filter-close { background: none; border: none; color: var(--jd-text-sub); font-size: 24px; cursor: pointer; }

        .filter-tabs { display: flex; background-color: #222; padding: 0 10px; border-bottom: 1px solid var(--jd-border); overflow-x: auto; }
        .filter-tab {
            flex: 0 0 auto;
            padding: 16px 15px;
            background: none; border: none; color: var(--jd-text-sub);
            cursor: pointer; font-size: 13px; font-weight: 500;
            transition: all 0.2s; position: relative; white-space: nowrap;
        }
        .filter-tab.active { color: var(--jd-primary); font-weight: bold; }
        .filter-tab.active::after {
            content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 3px; background-color: var(--jd-primary);
        }

        .filter-body { flex: 1; overflow-y: auto; padding: 24px; position: relative; }
        .tab-pane { display: none; animation: fadeIn 0.2s ease; }
        .tab-pane.active { display: block; }
        @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }

        .input-group { margin-bottom: 15px; }
        .input-label { display: block; margin-bottom: 10px; color: var(--jd-text-sub); font-size: 13px; }
        .filter-textarea {
            width: 100%; height: 300px; background-color: #151515; color: #ddd;
            border: 1px solid var(--jd-border); padding: 15px; border-radius: 8px;
            resize: none; font-family: monospace; font-size: 13px; box-sizing: border-box; outline: none;
        }
        .filter-textarea:focus { border-color: var(--jd-primary); }

        .radio-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
        .option-card {
            background-color: var(--jd-card-bg); border: 2px solid transparent;
            border-radius: 8px; padding: 14px; cursor: pointer;
            transition: all 0.2s; display: flex; align-items: center; gap: 12px;
        }
        .option-card:hover { background-color: #383838; }
        .option-card:has(input:checked) { background-color: rgba(229, 57, 53, 0.1); border-color: var(--jd-primary); }
        .option-card input[type="radio"], .option-card input[type="checkbox"] {
            appearance: none; width: 18px; height: 18px; border: 2px solid #666; border-radius: 50%;
            margin: 0; position: relative; cursor: pointer;
        }
        .option-card input:checked { border-color: var(--jd-primary); background-color: var(--jd-primary); box-shadow: inset 0 0 0 3px #2d2d2d; }
        .option-card input[type="checkbox"] { border-radius: 4px; }

        .opt-title { font-weight: bold; font-size: 14px; display: block; }
        .opt-desc { font-size: 12px; color: var(--jd-text-sub); margin-top: 4px; display: block; }

        .custom-day-input {
            background: #111; border: 1px solid #444; color: white; padding: 4px 8px;
            border-radius: 4px; width: 60px; text-align: center; margin: 0 5px;
        }

        .log-item {
            display: flex; justify-content: space-between; align-items: center;
            padding: 10px 12px; margin-bottom: 6px; background-color: #252525; border-radius: 6px;
        }
        .log-main { display: flex; flex-direction: column; width: 70%; }
        .log-id { color: #64b5f6; font-weight: bold; font-size: 13px; }
        /* Modify: Adapt log title style to hyperlink format */
        .log-title { color: #bbb; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-decoration: none; transition: color 0.2s;}
        .log-title:hover { color: var(--jd-primary); text-decoration: underline; }
        .log-tag { padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; color: white; min-width: 65px; text-align: center; }

        .type-id { background-color: #d32f2f; }
        .type-title { background-color: #f57c00; }
        .type-date { background-color: #7b1fa2; }
        .type-score { background-color: #0288d1; }
        .type-western { background-color: #388e3c; }

        .filter-footer {
            padding: 16px 24px; border-top: 1px solid var(--jd-border);
            display: flex; justify-content: space-between; background-color: #252525;
        }
        .btn { padding: 8px 16px; border-radius: 6px; border: none; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
        .btn-secondary { background-color: transparent; border: 1px solid var(--jd-border); color: var(--jd-text-sub); }
        .btn-primary { background-color: var(--jd-primary); color: white; }
    `;

  GM_addStyle(styles);

  // --- Data Management ---
  let currentLogs = [];

  function getData() {
    return {
      ids: GM_getValue('blockedPrefixes', []),
      titles: GM_getValue('blockedTitles', []),
      dateLimit: GM_getValue('blockedDateLimit', 0),
      scoreLimit: GM_getValue('blockedScoreLimit', 0),
      filterWestern: GM_getValue('filterWestern', false),
      keepZeroScore: GM_getValue('keepZeroScore', true),
    };
  }

  function saveData(
    idList,
    titleList,
    dateLimitVal,
    scoreLimitVal,
    westernVal,
    keepZeroVal,
  ) {
    GM_setValue('blockedPrefixes', [
      ...new Set(
        idList.map((item) => item.trim().toUpperCase()).filter((item) => item),
      ),
    ]);
    GM_setValue('blockedTitles', [
      ...new Set(titleList.map((item) => item.trim()).filter((item) => item)),
    ]);
    GM_setValue('blockedDateLimit', parseInt(dateLimitVal) || 0);
    GM_setValue('blockedScoreLimit', parseFloat(scoreLimitVal) || 0);
    GM_setValue('filterWestern', !!westernVal);
    GM_setValue('keepZeroScore', !!keepZeroVal);
  }

  // --- Core logic ---
  function isAsianContent(text) {
    // Match Kanji/Chinese characters、Hiragana、Katakana
    return /[\u4e00-\u9fa5\u3040-\u30ff\u31f0-\u31ff]/.test(text);
  }

  function executeFilter() {
    const config = getData();
    currentLogs = [];
    let hiddenCount = 0;

    document.querySelectorAll('.movie-list .item').forEach((item) => {
      const titleStrong = item.querySelector('.video-title strong');
      const titleFullEl = item.querySelector('.video-title');
      const dateEl = item.querySelector('.meta');
      const scoreEl = item.querySelector('.score .value');
      // Modify: Extract <a> tag content to get link
      const linkEl = item.querySelector('a.box');

      let videoID = '',
        videoTitle = '',
        videoDate = '',
        videoScore = null,
        videoLink = '#';

      if (titleFullEl) videoTitle = titleFullEl.textContent.trim();
      if (titleStrong) videoID = titleStrong.textContent.trim().toUpperCase();
      else if (videoTitle) videoID = videoTitle.split(' ')[0].toUpperCase();
      if (dateEl) videoDate = dateEl.textContent.trim().split(' ')[0];
      // Modify: Get target link
      if (linkEl) videoLink = linkEl.getAttribute('href') || '#';

      if (scoreEl) {
        videoScore = parseFloat(scoreEl.textContent.trim());
        if (isNaN(videoScore)) videoScore = 0;
      } else {
        videoScore = 0; // No rating treated as0
      }

      let shouldHide = false,
        type = '',
        detail = '';

      // 1. Code Block
      if (!shouldHide && videoID && config.ids.length) {
        const match = config.ids.find((p) => videoID.startsWith(p));
        if (match) {
          shouldHide = true;
          type = 'type-id';
          detail = `Code: ${match}`;
        }
      }
      // 2. Keyword Block
      if (!shouldHide && videoTitle && config.titles.length) {
        const match = config.titles.find((k) => videoTitle.includes(k));
        if (match) {
          shouldHide = true;
          type = 'type-title';
          detail = `Keywords: ${match}`;
        }
      }
      // 3. Western Filter (Check for Asian characters)
      if (!shouldHide && config.filterWestern && videoTitle) {
        if (!isAsianContent(videoTitle)) {
          shouldHide = true;
          type = 'type-western';
          detail = 'Western/Uncensored Content';
        }
      }
      // 4. Date Filter
      if (!shouldHide && config.dateLimit > 0 && videoDate) {
        const itemDate = new Date(videoDate);
        const days = Math.ceil(
          Math.abs(new Date() - itemDate) / (1000 * 60 * 60 * 24),
        );
        if (days > config.dateLimit) {
          shouldHide = true;
          type = 'type-date';
          detail = `${days}days ago`;
        }
      }
      // 5. Rating Filter
      if (!shouldHide && config.scoreLimit > 0) {
        const isZero = videoScore === 0;
        // If it is0points and set to keep0points,then skip blocking;otherwise block if below threshold
        if (isZero && config.keepZeroScore) {
          shouldHide = false;
        } else if (videoScore < config.scoreLimit) {
          shouldHide = true;
          type = 'type-score';
          detail = `Rating: ${videoScore}`;
        }
      }

      if (shouldHide) {
        item.style.display = 'none';
        hiddenCount++;
        // Modify: will videoLink together push into log data structure
        currentLogs.push({
          id: videoID,
          title: videoTitle,
          link: videoLink,
          type,
          detail,
        });
      } else {
        item.style.display = '';
      }
    });

    updateButtonCount(hiddenCount);
  }

  function updateButtonCount(count) {
    const btn = document.getElementById('javdb-filter-btn');
    if (btn)
      btn.innerHTML =
        count > 0
          ? `<span>🛡️ Filter</span><span class="filter-badge">${count}</span>`
          : `<span>🛡️ Filter</span>`;
  }

  // --- UI Build ---
  function createUI() {
    const btn = document.createElement('button');
    btn.id = 'javdb-filter-btn';
    btn.innerHTML = `<span>🛡️ Filter</span>`;
    btn.onclick = openModal;
    document.body.appendChild(btn);

    const modal = document.createElement('div');
    modal.id = 'javdb-filter-modal';
    modal.innerHTML = `
            <div class="filter-modal-content">
                <div class="filter-header">
                    <div class="filter-title">🛡️ JavDB Purification Settings</div>
                    <button class="filter-close" id="f-close-btn">&times;</button>
                </div>
                <div class="filter-tabs">
                    <button class="filter-tab active" data-tab="ids">🆔 Code Rules</button>
                    <button class="filter-tab" data-tab="titles">🔤 Title Keywords</button>
                    <button class="filter-tab" data-tab="date">📅 Date Range</button>
                    <button class="filter-tab" data-tab="score">⭐ Rating Filter</button>
                    <button class="filter-tab" data-tab="western">🌍 Filter Western</button>
                    <button class="filter-tab" data-tab="log">📜 Block Log</button>
                </div>
                <div class="filter-body">
                    <div id="tab-content-ids" class="tab-pane active">
                        <div class="input-group">
                            <span class="input-label">Enter code prefix,One per line。For example:FC2, MID</span>
                            <textarea id="filter-input-ids" class="filter-textarea" placeholder="FC2\nMID..."></textarea>
                        </div>
                    </div>
                    <div id="tab-content-titles" class="tab-pane">
                        <div class="input-group">
                            <span class="input-label">Enter title keywords,One per line。For example:VR, Uncensored</span>
                            <textarea id="filter-input-titles" class="filter-textarea" placeholder="VR\nUncensored..."></textarea>
                        </div>
                    </div>
                    <div id="tab-content-date" class="tab-pane">
                        <span class="input-label">Block videos released older than these days:</span>
                        <div class="radio-grid">
                            <label class="option-card"><input type="radio" name="date-filter" value="0" checked><div><span class="opt-title">🚫 No limit</span><span class="opt-desc">Show All</span></div></label>
                            <label class="option-card"><input type="radio" name="date-filter" value="7"><div><span class="opt-title">📅 7 days ago</span><span class="opt-desc">Block old videos from a week ago</span></div></label>
                            <label class="option-card"><input type="radio" name="date-filter" value="31"><div><span class="opt-title">📅 1 months ago</span><span class="opt-desc">Only show this months new releases</span></div></label>
                            <label class="option-card"><input type="radio" name="date-filter" value="custom"><div><span class="opt-title">🔧 Custom</span><span class="opt-desc">Block <input type="number" id="date-custom-input" class="custom-day-input" min="1" disabled> days ago</span></div></label>
                        </div>
                    </div>
                    <div id="tab-content-score" class="tab-pane">
                        <span class="input-label">Block videos rated lower than this score:</span>
                        <div class="radio-grid">
                            <label class="option-card"><input type="radio" name="score-filter" value="0" checked><div><span class="opt-title">🚫 No limit</span></div></label>
                            <label class="option-card"><input type="radio" name="score-filter" value="4"><div><span class="opt-title">⭐ 4.0 points below</span></div></label>
                            <label class="option-card"><input type="radio" name="score-filter" value="3"><div><span class="opt-title">⭐ 3.0 points below</span></div></label>
                            <label class="option-card"><input type="radio" name="score-filter" value="2"><div><span class="opt-title">⭐ 2.0 points below</span></div></label>
                        </div>
                        <div style="margin-top: 20px;">
                            <span class="input-label">Additional Settings:</span>
                            <label class="option-card" style="grid-column: span 2;">
                                <input type="checkbox" id="keep-zero-score-chk">
                                <div>
                                    <span class="opt-title">Keep 0 points(No rating yet)videos</span>
                                    <span class="opt-desc">When enabled,Newly released unrated content will not be filtered(Recommend)</span>
                                </div>
                            </label>
                        </div>
                    </div>
                    <div id="tab-content-western" class="tab-pane">
                        <span class="input-label">Western/Uncensored Content Filter Settings:</span>
                        <div class="radio-grid">
                            <label class="option-card">
                                <input type="radio" name="western-filter" value="off" checked>
                                <div>
                                    <span class="opt-title">🚫 Close</span>
                                    <span class="opt-desc">Display content from all regions normally</span>
                                </div>
                            </label>
                            <label class="option-card">
                                <input type="radio" name="western-filter" value="on">
                                <div>
                                    <span class="opt-title">🌍 Enable Filtering</span>
                                    <span class="opt-desc">Auto-hide titles without Japanese/Kanji Western uncensored videos</span>
                                </div>
                            </label>
                        </div>
                        <div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.05); border-radius: 8px; color: var(--jd-text-sub); font-size: 12px; line-height: 1.6;">
                            💡 <b>Principle:</b> This function identifies by title language。If the title contains no Japanese kana or Chinese characters(Common in Western sources and English titles),The system will block it。
                        </div>
                    </div>
                    <div id="tab-content-log" class="tab-pane">
                        <div style="display:flex; justify-content:space-between; margin-bottom:10px; font-size:13px; color:var(--jd-text-sub);">
                            <span id="log-stats">Counting...</span>
                        </div>
                        <ul id="filter-log-list" style="list-style:none; padding:0; margin:0;"></ul>
                    </div>
                </div>
                <div class="filter-footer">
                    <div class="btn-group"><button class="btn btn-secondary" id="filter-export-btn">Export Config</button></div>
                    <div class="btn-group" style="display:flex; gap:10px;">
                        <button class="btn btn-secondary" id="filter-cancel-btn">Cancel</button>
                        <button class="btn btn-primary" id="filter-save-btn">Save and Apply</button>
                    </div>
                </div>
            </div>
        `;
    document.body.appendChild(modal);

    // Event binding
    document.getElementById('f-close-btn').onclick = closeModal;
    document.getElementById('filter-cancel-btn').onclick = closeModal;
    document.getElementById('filter-save-btn').onclick = saveAndClose;
    document.getElementById('filter-export-btn').onclick = exportConfig;

    const tabs = modal.querySelectorAll('.filter-tab');
    tabs.forEach((tab) => {
      tab.addEventListener('click', () => {
        tabs.forEach((t) => t.classList.remove('active'));
        modal
          .querySelectorAll('.tab-pane')
          .forEach((c) => c.classList.remove('active'));
        tab.classList.add('active');
        document
          .getElementById(`tab-content-${tab.dataset.tab}`)
          .classList.add('active');
        if (tab.dataset.tab === 'log') renderLogs();
      });
    });

    document.getElementsByName('date-filter').forEach((radio) => {
      radio.addEventListener('change', () => {
        const custom = document.getElementById('date-custom-input');
        custom.disabled = radio.value !== 'custom';
        if (radio.value === 'custom') custom.focus();
      });
    });
  }

  function renderLogs() {
    const listEl = document.getElementById('filter-log-list');
    listEl.innerHTML = '';
    document.getElementById('log-stats').innerHTML =
      `Blocked on current page: <strong style="color:var(--jd-primary)">${currentLogs.length}</strong> items`;
    if (currentLogs.length === 0) {
      listEl.innerHTML =
        '<div style="text-align:center; padding:40px; color:#666;">🎉 Clean!No matching content found。</div>';
      return;
    }
    currentLogs.forEach((log) => {
      const li = document.createElement('li');
      li.className = 'log-item';
      // Modify: take <span class="log-title"> changed to <a class="log-title" href="..." target="_blank">
      li.innerHTML = `
                <div class="log-main"><span class="log-id">${log.id || 'Unknown'}</span><a href="${log.link}" target="_blank" class="log-title">${log.title}</a></div>
                <span class="log-tag ${log.type}">${log.detail}</span>
            `;
      listEl.appendChild(li);
    });
  }

  function openModal() {
    const modal = document.getElementById('javdb-filter-modal');
    const config = getData();

    document.getElementById('filter-input-ids').value = config.ids.join('\n');
    document.getElementById('filter-input-titles').value =
      config.titles.join('\n');
    document.getElementById('keep-zero-score-chk').checked =
      config.keepZeroScore;

    // Date backfill
    let dateMatched = false;
    document.getElementsByName('date-filter').forEach((r) => {
      if (r.value !== 'custom' && parseInt(r.value) === config.dateLimit) {
        r.checked = true;
        dateMatched = true;
      }
    });
    if (!dateMatched && config.dateLimit > 0) {
      document.querySelector('input[value="custom"]').checked = true;
      document.getElementById('date-custom-input').value = config.dateLimit;
      document.getElementById('date-custom-input').disabled = false;
    }

    // Rating backfill
    document.getElementsByName('score-filter').forEach((r) => {
      if (parseInt(r.value) === config.scoreLimit) r.checked = true;
    });

    // Western filter backfill
    document.querySelector(
      `input[name="western-filter"][value="${config.filterWestern ? 'on' : 'off'}"]`,
    ).checked = true;

    modal.style.display = 'flex';
    setTimeout(() => modal.classList.add('show'), 10);
  }

  function closeModal() {
    const modal = document.getElementById('javdb-filter-modal');
    modal.classList.remove('show');
    setTimeout(() => (modal.style.display = 'none'), 200);
  }

  function saveAndClose() {
    const ids = document.getElementById('filter-input-ids').value.split('\n');
    const titles = document
      .getElementById('filter-input-titles')
      .value.split('\n');
    const keepZero = document.getElementById('keep-zero-score-chk').checked;

    let dateLimit = 0;
    document.getElementsByName('date-filter').forEach((r) => {
      if (r.checked)
        dateLimit =
          r.value === 'custom'
            ? document.getElementById('date-custom-input').value || 0
            : r.value;
    });

    let scoreLimit = 0;
    document.getElementsByName('score-filter').forEach((r) => {
      if (r.checked) scoreLimit = r.value;
    });

    let westernFilter = false;
    document.getElementsByName('western-filter').forEach((r) => {
      if (r.checked) westernFilter = r.value === 'on';
    });

    saveData(ids, titles, dateLimit, scoreLimit, westernFilter, keepZero);
    closeModal();
    executeFilter();
  }

  function exportConfig() {
    const blob = new Blob([JSON.stringify(getData(), null, 2)], {
      type: 'application/json',
    });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'javdb_filter_config.json';
    a.click();
  }

  function init() {
    createUI();
    executeFilter();
    const observer = new MutationObserver(() => executeFilter());
    const target = document.querySelector('.movie-list');
    if (target) observer.observe(target, { childList: true, subtree: true });
  }

  if (document.readyState === 'loading')
    document.addEventListener('DOMContentLoaded', init);
  else init();
})();