JavDB Custom Blocker

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
})();