Smart Ad & Tracker Cleaner (EasyList + Peter Lowe)

Consumer-friendly ad & tracker removal using EasyList (cosmetic + network) + Peter Lowe hosts. Caches lists in localStorage (24h). Silent, standalone, preserves useful embeds (YouTube/Maps/Vimeo/etc.). Safe selectors only; avoids removing all scripts/iframes blindly.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Smart Ad & Tracker Cleaner (EasyList + Peter Lowe)
// @namespace    https://example.com/tm/smart-ad-cleaner
// @version      1.0.1
// @description  Consumer-friendly ad & tracker removal using EasyList (cosmetic + network) + Peter Lowe hosts. Caches lists in localStorage (24h). Silent, standalone, preserves useful embeds (YouTube/Maps/Vimeo/etc.). Safe selectors only; avoids removing all scripts/iframes blindly.
// @author       Iamnobody
// @license      MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @connect      easylist.to
// @connect      pgl.yoyo.org
// @run-at       document-end
// ==/UserScript==

(function () {
  "use strict";

  // -------- CONFIG --------
  const EASYLIST_URL = "https://easylist.to/easylist/easylist.txt"; // cosmetic + network rules
  const PETER_LOWE_URL = "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&mimetype=plaintext";
  const CACHE_KEY = "sm_ac_cache_v1"; // stores { ts, easylistText, peterText, selectors[], domains[] }
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
  const POLL_INTERVAL_MS = 3000; // periodically apply rules in first seconds
  const OBSERVE_DURATION_MS = 20000; // observe DOM for this many ms after load
  const MAX_SELECTOR_TRIES = 500; // limit to avoid heavy operations

  // Domains we MUST NOT remove (useful embeds)
  const ALLOW_DOMAINS = [
    "youtube.com", "youtu.be", "google.com/maps", "maps.google.com", "googleusercontent.com",
    "gstatic.com", "vimeo.com", "player.vimeo.com", "openstreetmap.org", "docs.google.com",
    "drive.google.com", "twitter.com", "t.co", "facebook.com", "instagram.com", "maps.googleapis.com"
  ];

  // Extra safe attribute selectors to remove if present (lightweight)
  const SAFE_ATTR_SELECTORS = [
    "[data-ad]", "[data-ads]", "[data-ad-client]", "[data-ad-slot]", "[data-qa='ad']"
  ];

  // -------- Storage helpers --------
  function lsGet(key, fallback = null) {
    try {
      const raw = localStorage.getItem(key);
      return raw ? JSON.parse(raw) : fallback;
    } catch (e) {
      return fallback;
    }
  }
  function lsSet(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      /* ignore quota errors silently */
    }
  }

  // -------- Fetch lists (GM_xmlhttpRequest) --------
  function fetchText(url) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url,
        responseType: "text",
        onload(resp) {
          if (resp.status >= 200 && resp.status < 300) resolve(resp.responseText);
          else reject(new Error("HTTP " + resp.status));
        },
        onerror(err) { reject(err); }
      });
    });
  }

  // -------- Parse rules --------
  function parseEasyList(text) {
    // extract cosmetic selectors: lines starting with "##" or "#@#"
    const selectors = new Set();
    const networkDomains = new Set();

    const lines = text.split(/\r?\n/);
    for (const raw of lines) {
      const line = raw.trim();
      if (!line || line.startsWith("!")) continue; // comment

      // cosmetic rule
      if (line.startsWith("##")) {
        const sel = line.slice(2).trim();
        if (sel) selectors.add(sel);
        continue;
      }
      // cosmetic exception with #@# (rare) - skip
      if (line.startsWith("#@#")) {
        // could be exceptions, ignore for blocking
        continue;
      }

      // network rule like "||example.com^" or "||example.com^$third-party"
      // try to extract domain
      if (line.startsWith("||")) {
        const rest = line.slice(2);
        const match = rest.match(/^([^\^\/\:\$]+)/);
        if (match && match[1]) {
          networkDomains.add(match[1]);
        }
        continue;
      }

      // domain anchored rules like "example.com##.ad" - skip
      const anchorMatch = line.match(/^([^\#\/\:\s]+)\#\#/);
      if (anchorMatch && anchorMatch[1]) {
        networkDomains.add(anchorMatch[1]);
      }
    }

    return { selectors: Array.from(selectors), domains: Array.from(networkDomains) };
  }

  function parsePeterLowe(text) {
    // hostfile lines: "0.0.0.0 domain.com" or "127.0.0.1 domain.com"
    const domains = new Set();
    const lines = text.split(/\r?\n/);
    for (const raw of lines) {
      const line = raw.trim();
      if (!line || line.startsWith("#")) continue;
      const parts = line.split(/\s+/);
      if (parts.length >= 2) {
        const d = parts[1].replace(/^\.*|\.*$/g, "");
        if (d && d.indexOf(":") === -1) domains.add(d);
      } else {
        // fallback: if line looks like a domain only
        if (line.indexOf(".") > -1 && !line.includes(" ")) domains.add(line);
      }
    }
    return Array.from(domains);
  }

  // -------- Utility --------
  function domainMatchInUrl(url, domain) {
    if (!url) return false;
    try {
      const u = url.toLowerCase();
      return u.includes(domain.toLowerCase());
    } catch (e) {
      return url.toLowerCase().indexOf(domain.toLowerCase()) !== -1;
    }
  }

  function isAllowedDomainInUrl(url) {
    if (!url) return false;
    for (const ad of ALLOW_DOMAINS) {
      if (url.indexOf(ad) !== -1) return true;
    }
    return false;
  }

  // -------- Apply rules safely --------
  function applySelectors(selectors) {
    if (!selectors || selectors.length === 0) return;
    let tries = 0;
    for (const sel of selectors) {
      if (tries++ > MAX_SELECTOR_TRIES) break;
      try {
        // Some selectors from lists can be invalid in the page context; guard with try/catch
        const nodes = document.querySelectorAll(sel);
        if (!nodes || nodes.length === 0) continue;
        nodes.forEach(n => {
          // protect critical site areas: don't remove if it is a top-level embed from allowed domains
          let src = "";
          if (n instanceof HTMLIFrameElement) src = n.src || "";
          else if (n instanceof HTMLImageElement) src = n.src || "";
          else src = (n.getAttribute && (n.getAttribute("src") || n.getAttribute("href"))) || "";
          if (src && isAllowedDomainInUrl(src)) return;
          try { n.remove(); } catch (e) { /* ignore */ }
        });
      } catch (e) {
        // ignore invalid selectors
      }
    }
  }

  function applyDomainBlocks(domains) {
    if (!domains || domains.length === 0) return;
    // Check scripts, images, iframes, link[href], source[src], audio/video, embed, object
    const tagAttrs = [
      { tag: "script", attr: "src" },
      { tag: "iframe", attr: "src" },
      { tag: "img", attr: "src" },
      { tag: "audio", attr: "src" },
      { tag: "video", attr: "src" },
      { tag: "source", attr: "src" },
      { tag: "link", attr: "href" },
      { tag: "embed", attr: "src" },
      { tag: "object", attr: "data" },
      { tag: "iframe", attr: "data-src" },
      { tag: "*", attr: "data-ad-src" } // custom attribute common in some ad frameworks
    ];

    // For performance, create a combined regex of domains (escaped), but cap length
    // We'll instead iterate over elements and check against domain list to avoid huge regex
    tagAttrs.forEach(({ tag, attr }) => {
      try {
        const nodes = tag === "*" ? document.querySelectorAll("[data-ad-src]") : document.getElementsByTagName(tag);
        if (!nodes || nodes.length === 0) return;
        Array.from(nodes).forEach(node => {
          try {
            const val = (node.getAttribute && node.getAttribute(attr)) || (node[attr] || "");
            if (!val) return;
            const v = val.toLowerCase();
            // allow if from allowed domains (preserve embeds)
            if (isAllowedDomainInUrl(v)) return;
            // Check any domain substring match
            for (const d of domains) {
              if (d.length < 3) continue;
              if (v.indexOf(d.toLowerCase()) !== -1) {
                // remove node (but avoid removing top-level <html> etc.)
                node.remove();
                break;
              }
            }
          } catch (e) { /* ignore per node */ }
        });
      } catch (e) { /* ignore per tag */ }
    });

    // Additionally, remove elements that have obvious ad-related attributes (safe ones)
    SAFE_ATTR_SELECTORS.forEach(sel => {
      try {
        document.querySelectorAll(sel).forEach(n => n.remove());
      } catch (e) { /* ignore invalid */ }
    });
  }

  // -------- Core: fetch + cache + apply --------
  async function prepareAndApply() {
    try {
      // Load cached
      let cache = lsGet(CACHE_KEY, null);
      const now = Date.now();
      let needFetch = true;
      if (cache && cache.ts && (now - cache.ts) < CACHE_TTL_MS && cache.selectors && cache.domains) {
        needFetch = false;
      }

      if (needFetch) {
        // fetch both lists concurrently
        const [easyText, peterText] = await Promise.allSettled([fetchText(EASYLIST_URL), fetchText(PETER_LOWE_URL)]);
        let easy = "";
        let peter = "";
        if (easyText.status === "fulfilled") easy = easyText.value;
        if (peterText.status === "fulfilled") peter = peterText.value;

        const parsed = parseEasyList(easy);
        const peterDomains = parsePeterLowe(peter);

        // merge domain lists (unique)
        const domainsSet = new Set([...(parsed.domains || []), ...(peterDomains || [])]);

        cache = {
          ts: now,
          easyText: easy,
          peterText: peter,
          selectors: parsed.selectors || [],
          domains: Array.from(domainsSet)
        };
        lsSet(CACHE_KEY, cache);
      }

      // apply rules now
      if (cache && cache.selectors) applySelectors(cache.selectors);
      if (cache && cache.domains) applyDomainBlocks(cache.domains);

    } catch (e) {
      // silent fail (per user choice)
      // console.error("Smart Ad Cleaner error:", e);
    }
  }

  // Run apply repeatedly for the first OBSERVE_DURATION_MS ms to catch late-load ads
  async function runCleaner() {
    await prepareAndApply();
    const start = Date.now();
    const interval = setInterval(async () => {
      await prepareAndApply();
      if (Date.now() - start > OBSERVE_DURATION_MS) clearInterval(interval);
    }, POLL_INTERVAL_MS);

    // MutationObserver to catch dynamic insertions quickly
    const mo = new MutationObserver(muts => {
      // quick run, but keep it light — call prepareAndApply() which uses cached parsed selectors/domains
      prepareAndApply();
    });
    try {
      mo.observe(document.documentElement || document.body, { childList: true, subtree: true });
      // stop observer after OBSERVE_DURATION_MS
      setTimeout(() => mo.disconnect(), OBSERVE_DURATION_MS + 2000);
    } catch (e) {
      // ignore observe errors
    }
  }

  // Kick off (don't wait for load; run at document-end)
  runCleaner();

  // Also schedule a daily background refresh of lists (non-blocking)
  (async function refreshCacheDaily() {
    const cache = lsGet(CACHE_KEY, null);
    const now = Date.now();
    if (!cache || !cache.ts || (now - cache.ts) > CACHE_TTL_MS) {
      try { await prepareAndApply(); } catch (e) { /* silent */ }
    }
  })();

})();