Sleazy Fork is available in English.
Supports custom code prefix、Title Keywords、Release Time、Minimum Rating Filter、Western content filter, etc.。
// ==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">×</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();
})();