Sleazy Fork is available in English.

Video Filter with Diagnostics

Filters posts on Coomer/Kemono sites by videos, blacklist, and minimum duration

// ==UserScript==
// @name        Video Filter with Diagnostics
// @namespace   LuckyFemboyScripts
// @match       https://*.coomer.party/*
// @match       https://*.kemono.party/*
// @match       https://*.coomer.su/*
// @match       https://*.kemono.su/*
// @match       https://*.coomer.st/*
// @match       https://*.kemono.cr/*
// @grant        none
// @version     2.18
// @description Filters posts on Coomer/Kemono sites by videos, blacklist, and minimum duration
// @author       LuckyFemboy
// @grant        GM_xmlhttpRequest
// @license     MIT
// ==/UserScript==

// Configuración principal

// Configuración de idioma - 1: English, 2: Español
let language = 1;














const interfaceTexts = {
    title: {
        1: "Video Filter",
        2: "Filtro de Videos"
    },
    modes: {
        1: [
            { value: "onlyVideos", text: "Videos only" },
            { value: "videosAndBlacklist", text: "Videos + Blacklist" },
            { value: "blacklistOnly", text: "Blacklist only" },
            { value: "blacklistAndDuration", text: "Blacklist + Duration" },
            { value: "durationOnly", text: "Duration only" }
        ],
        2: [
            { value: "onlyVideos", text: "Solo videos" },
            { value: "videosAndBlacklist", text: "Videos y Blacklist" },
            { value: "blacklistOnly", text: "Solo Blacklist" },
            { value: "blacklistAndDuration", text: "Blacklist y duración mínima" },
            { value: "durationOnly", text: "Solo duración mínima" }
        ]
    },
    durationLabel: {
        1: "Minimum Duration (seconds):",
        2: "Duración Mínima (segundos):"
    },
    applyButton: {
        1: "Apply Filter",
        2: "Aplicar Filtro"
    },
    applyingText: {
        1: "Applying...",
        2: "Aplicando..."
    },
    completedText: {
        1: "Filter Completed",
        2: "Filtro Completado"
    },
    bannerText: {
        1: "⏳ Applying filters...",
        2: "⏳ Aplicando filtros..."
    }
};






// Blacklist
const authorBlacklist = ["", "", "", "", "", "", "",
        "", "", "", "", "", "", "", "", "", "", "", "", "",

             // Favorites




        "", "", "", "", "", "", "", "", "", "", "",
         "", "", "", "", "", "", "", "", "", "", "", "",
         "", "", "", "", "", "", "", "", "", "", "", "",
         "", "", "", "", "", "", "", "", "", "", "", "",
         "", "", "", "", "", "", "", "", "", "", "", "",
         "", "", "", "", "", "", "", "", "", "", "", "",
         "", "", "", "", "", "", "", "", "", "", "", "",]; // Fat or ugly

// 🔍 Verifica si el usuario está en la blacklist

let applyFilterButton = null; //

function isUserBlacklisted(userId) {
    return authorBlacklist.includes(userId);
}

const minimumVideoLength = 150; // Longitud mínima de video en segundos
const validURLs = [
    'https://coomer.su/posts',
    'https://coomer.su/posts?',
    'https://coomer.su/posts/popular',
    'https://coomer.su/onlyfans/user/',
    'https://coomer.su/fansly/user/',

    'https://coomer.st/posts',
    'https://coomer.st/posts?',
    'https://coomer.st/posts/popular',
    'https://coomer.st/onlyfans/user/',
    'https://coomer.st/fansly/user/'
];


(function () {
    'use strict';

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", () => {
            initializeScript();
        });
    } else {
        initializeScript();
    }

    function initializeScript() {
        console.log("Inicializando script...");
        createFilterUI();
        observeURLChanges();
        observePosts();
    }
})();

function createFilterUI() {
    document.querySelector('#filterContainer')?.remove();

    if (!isURLValid()) {
        console.log("URL no válida, no se mostrarán controles.");
        return;
    }

    console.log("Creando UI de filtros...");

    const container = document.createElement("div");
    container.id = "filterContainer";
    container.style.position = "fixed";
    container.style.top = "10px";
    container.style.right = "10px";
    container.style.backgroundColor = "#1e1e2e";
    container.style.border = "1px solid #313244";
    container.style.padding = "7px";
    container.style.borderRadius = "8px";
    container.style.zIndex = "9999";
    container.style.width = "224px";
    container.style.boxShadow = "0 2px 6px rgba(0,0,0,0.3)";
    container.style.fontFamily = "Arial, sans-serif";
    container.style.color = "#cdd6f4";
    container.style.fontSize = "12px";


  // Language switcher
const langContainer = document.createElement("div");
langContainer.style.marginBottom = "10px";
langContainer.style.textAlign = "center";

const langLabel = document.createElement("span");
langLabel.textContent = "Language: ";
langLabel.style.color = "#cdd6f4";
langLabel.style.fontSize = "11px";

const langSelect = document.createElement("select");
langSelect.style.backgroundColor = "#313244";
langSelect.style.color = "#cdd6f4";
langSelect.style.border = "1px solid #45475a";
langSelect.style.borderRadius = "3px";

const langOption1 = document.createElement("option");
langOption1.value = "1";
langOption1.text = "English";
langOption1.selected = language === 1;

const langOption2 = document.createElement("option");
langOption2.value = "2";
langOption2.text = "Español";
langOption2.selected = language === 2;

langSelect.appendChild(langOption1);
langSelect.appendChild(langOption2);

langSelect.onchange = function() {
    language = parseInt(this.value);
    createFilterUI(); // Recrear la UI con el nuevo idioma
};

langContainer.appendChild(langLabel);
langContainer.appendChild(langSelect);
container.appendChild(langContainer);


    const title = document.createElement("h3");
    title.textContent = interfaceTexts.title[language];
    title.style.marginTop = "0";
    title.style.marginBottom = "8px";
    title.style.color = "#f5c2e7";
    title.style.fontSize = "14px";
    title.style.borderBottom = "1px solid #45475a";
    title.style.paddingBottom = "5px";
    container.appendChild(title);

    const modeContainer = document.createElement("div");
    modeContainer.style.marginBottom = "10px";


    const select = document.createElement("select");
    select.id = "filterMode";
    select.style.width = "100%";
    select.style.padding = "5px";
    select.style.borderRadius = "5px";
    select.style.backgroundColor = "#313244";
    select.style.color = "#cdd6f4";
    select.style.border = "1px solid #45475a";
    select.style.fontSize = "12px";
interfaceTexts.modes[language].forEach(opt => {
    const option = document.createElement("option");
    option.value = opt.value;
    option.text = opt.text;
    if (opt.value === "durationOnly") option.selected = true;
    select.appendChild(option);
});

    modeContainer.appendChild(select);
    container.appendChild(modeContainer);

    const durationContainer = document.createElement("div");
    durationContainer.id = "durationContainer";
    durationContainer.style.marginBottom = "10px";
    durationContainer.style.display = "none";

    const durationLabel = document.createElement("label");
    durationLabel.textContent = interfaceTexts.durationLabel[language];
    durationLabel.style.display = "block";
    durationLabel.style.marginBottom = "4px";
    durationLabel.style.fontWeight = "bold";
    durationLabel.style.color = "#a6e3a1";
    durationLabel.style.fontSize = "12px";
    durationContainer.appendChild(durationLabel);

    const durationInput = document.createElement("input");
    durationInput.id = "minDurationInput";
    durationInput.type = "number";
    durationInput.min = "0";
    durationInput.value = minimumVideoLength;
    durationInput.style.width = "100%";
    durationInput.style.padding = "5px";
    durationInput.style.borderRadius = "5px";
    durationInput.style.backgroundColor = "#313244";
    durationInput.style.color = "#cdd6f4";
    durationInput.style.border = "1px solid #45475a";
    durationInput.style.fontSize = "12px";
    durationContainer.appendChild(durationInput);

    container.appendChild(durationContainer);

    const button = document.createElement("button");
    button.textContent = interfaceTexts.applyButton[language];
    button.style.width = "100%";
    button.style.padding = "6px";
    button.style.borderRadius = "5px";
    button.style.backgroundColor = "#f38ba8";
    button.style.color = "#1e1e1e";
    button.style.border = "none";
    button.style.fontWeight = "bold";
    button.style.cursor = "pointer";
    button.style.fontSize = "12px";
    button.style.transition = "background-color 0.2s";

button.onmouseover = () => {
    if (button.dataset.status === "completed") {
        button.style.backgroundColor = "#c3f9c1";
    } else {
        button.style.backgroundColor = "#f5c2e7";
    }
};

button.onmouseout = () => {
    if (button.dataset.status === "completed") {
        button.style.backgroundColor = "#a6e3a1";
    } else {
        button.style.backgroundColor = "#f38ba8";
    }
};


    button.onclick = function () {
        const mode = select.value;
        const minDuration = parseInt(durationInput.value || minimumVideoLength, 10);
        applySelectedFilter(mode, minDuration);
    };

    applyFilterButton = button;

    container.appendChild(button);

    select.onchange = () => {
        const showDuration = select.value === "durationOnly" || select.value === "blacklistAndDuration";
        durationContainer.style.display = showDuration ? "block" : "none";


    if (applyFilterButton && applyFilterButton.dataset.status === "completed") {
        applyFilterButton.textContent = "Aplicar Filtro";
        applyFilterButton.style.backgroundColor = "#f38ba8";
        applyFilterButton.dataset.status = "";
    }
    };
    select.onchange();


    document.body.appendChild(container);
    select.dispatchEvent(new Event('change'));

}






function observeURLChanges() {
    let lastURL = location.href;

    new MutationObserver(() => {
        const currentURL = location.href;
        if (currentURL !== lastURL) {
            lastURL = currentURL;
            console.log("Cambio de URL detectado. Actualizando UI...");
            createFilterUI();
        }
    }).observe(document, { subtree: true, childList: true });
}



function observePosts() {
let attempts = 0;
let maxAttempts = 10
    function tryObserve() {
        const postsContainer = document.querySelector('.card-list__items');
        if (!postsContainer) {
            attempts++;
            if (attempts >= maxAttempts) {
                console.warn(`No se encontró el contenedor de posts tras ${attempts} intentos. Se detiene la búsqueda.`);
                return;
            }
            console.log(`Intento ${attempts}: Contenedor de posts no encontrado. Reintentando...`);
            setTimeout(tryObserve, 1000);
            return;
        }

        const observer = new MutationObserver(() => {
            console.log("Cambios detectados en los posts");
        });

        observer.observe(postsContainer, { childList: true, subtree: true });
        console.log("Observador activado.");
    }

    tryObserve();
}





// Verificar URL
function isURLValid() {
    const currentURL = window.location.href;
    return validURLs.some(url => currentURL.startsWith(url) && !currentURL.includes('/post/'));
}
let filterMode = ""; // Se setea desde applySelectedFilter()

function applySelectedFilter(mode, minDuration) {
    console.log(`Aplicando filtro: ${mode} | Duración mínima: ${minDuration}s`);

    filterMode = mode;

    if (applyFilterButton) {
        applyFilterButton.disabled = true;
        applyFilterButton.textContent = interfaceTexts.applyingText[language];
        applyFilterButton.style.backgroundColor = "#f9e2af";
        applyFilterButton.dataset.status = "applying";
    }

    const banner = document.createElement("div");
    banner.id = "filter-status-banner";
    banner.style = "position:fixed;top:0;left:0;right:0;padding:15px;background:#11111b;color:#a6e3a1;font-weight:bold;text-align:center;z-index:9998;font-size:16px;";
    banner.innerText = interfaceTexts.bannerText[language];
    document.body.appendChild(banner);

    setTimeout(() => {
        if (mode === "onlyVideos") filterVideos();
        else if (mode === "videosAndBlacklist") filterVideos();
        else if (mode === "blacklistOnly") filterVideos();
        else if (mode === "durationOnly") filterByDuration(minDuration);
        else if (mode === "blacklistAndDuration") filterByBlacklistAndDuration(minDuration);

        setTimeout(() => {
            banner.remove();
        }, 2000);
    }, 500);
}






async function filterVideos() {
    console.log("[INFO] Iniciando filtro de publicaciones sin video...");

    const cardList = await new Promise(resolve => {
        const existing = document.querySelector(".card-list__items");
        if (existing) return resolve(existing);

        const observer = new MutationObserver(() => {
            const list = document.querySelector(".card-list__items");
            if (list) {
                observer.disconnect();
                resolve(list);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    });

    const posts = [...cardList.children];
    let totalEliminados = 0;

    const eliminados = [];
    const aprobados = [];

    for (const post of posts) {
        const postLink = post.querySelector("a")?.href;
        if (!postLink) continue;


        const urlParts = postLink.match(/https?:\/\/[^/]+\/([^/]+)\/user\/([^/]+)\/post\/(\d+)/);
        if (!urlParts) continue;
        const [, service, creator_id, post_id] = urlParts;

        try {
            // Llamar a la API
            const urlApi = `https://coomer.su/api/v1/${service}/user/${creator_id}/post/${post_id}`;
            let data;
try {
    data = await gmFetchJson(urlApi);
} catch (err) {
    console.warn(`[WARN] No se pudo obtener info del post ${post_id}`, err);
    return;
}



            const postData = data.post || {};

            const title = postData.title || "[Sin título]";
            const content = postData.content || "[Sin contenido]";
            const user = creator_id;

            const resultado = await postHasVideoAndCheckBlacklist(service, creator_id, post_id, filterMode, user);

            const videoUrls = resultado?.videoUrls || [];

            const info = {
                title,
                content,
                user,
                link: postLink,
                video: videoUrls.length ? videoUrls[0] : ""
            };

            if (!resultado?.passes) {
                let razon = "Desconocida";

                if (filterMode === "onlyVideos") {
                    razon = "No Video";
                } else if (filterMode === "blacklistOnly") {
                    razon = "blacklist";
                } else if (filterMode === "videosAndBlacklist") {
                    const noVideo = resultado?.videoUrls?.length === 0;
                    const enListaNegra = isUserBlacklisted(info.user);

                    if (noVideo && enListaNegra) {
                        razon = "No Video y Blacklist";
                    } else if (noVideo) {
                        razon = "No Video";
                    } else {
                        razon = "Blacklist";
                    }
                }

                eliminados.push({
                    ...info,
                    razon
                });

                post.style.display = "none";
                totalEliminados++;
            } else {
                aprobados.push(info);
            }

        } catch (e) {
            console.warn(`[ERROR] Excepción procesando post ${post_id}`, e);
        }
    }

    console.log(`[RESULTADO] Total de publicaciones eliminadas: ${totalEliminados}`);

    mostrarResultadosEnConsola(eliminados, aprobados);
}

async function probarDuracionVideo(service, creator_id, post_id) {
    const url = `https://coomer.su/api/v1/${service}/user/${creator_id}/post/${post_id}`;
    try {
let data;
try {
    data = await gmFetchJson(url);
} catch (e) {
    console.log(`[ERROR] API no respondió para duración: ${url}`, e);
    return 0;
}


        const videoUrls = extractVideoUrls(data).filter(url => esUrlVideo(url));

        if (videoUrls.length === 0) {
            console.log("[INFO] No se encontró ningún link real de video en la respuesta.");
            return 0;
        }

        let totalDuracion = 0;
        for (const videoUrl of videoUrls) {
            const duracion = await obtenerDuracionConVideoElement(videoUrl);
            totalDuracion += duracion || 0;
        }

        console.log(`[INFO] Duración total combinada del post ${post_id}: ${totalDuracion.toFixed(2)}s`);
        return totalDuracion;

    } catch (e) {
        console.log("[ERROR] Falló consulta para duración de video:", e);
        return 0;
    }
}




function esUrlVideo(url) {
    const videoExts = ['.mp4', '.m4v', '.webm', '.mov', '.avi', '.mkv', '.flv', '.ogg'];
    url = url.toLowerCase();
    return videoExts.some(ext => url.endsWith(ext));
}

async function filterByDuration(minDuration, batchSize = 17) {
    console.log(`[INFO] Filtrando por duración mínima: ${minDuration}s (solo posts con videos)`);

    const cardList = await new Promise(resolve => {
        const existing = document.querySelector(".card-list__items");
        if (existing) return resolve(existing);

        const observer = new MutationObserver(() => {
            const list = document.querySelector(".card-list__items");
            if (list) {
                observer.disconnect();
                resolve(list);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    });

    const posts = [...cardList.children];
    let totalEliminados = 0;

    const eliminados = [];
    const aprobados = [];

    async function procesarPost(post) {
        const postLink = post.querySelector("a")?.href;
        if (!postLink) return;

        const urlParts = postLink.match(/https?:\/\/[^/]+\/([^/]+)\/user\/([^/]+)\/post\/(\d+)/);
        if (!urlParts) return;
        const [, service, creator_id, post_id] = urlParts;

        try {
            const urlApi = `https://coomer.su/api/v1/${service}/user/${creator_id}/post/${post_id}`;
let data;
try {
    data = await gmFetchJson(urlApi);
} catch (err) {
    console.warn(`[WARN] No se pudo obtener info del post ${post_id}`, err);
    return;
}


            const videoUrlsRaw = extractVideoUrls(data);
            const videoUrls = videoUrlsRaw.filter(url => esUrlVideo(url));

            if (videoUrls.length === 0) {
                eliminados.push({
                    title: data.post?.title || "[Sin título]",
                    content: data.post?.content || "[Sin contenido]",
                    user: creator_id,
                    link: postLink,
                    video: "",
                    razon: "No Video"
                });
                post.style.display = "none";
                totalEliminados++;
                return;
            }

let duracionReal = await probarDuracionVideo(service, creator_id, post_id);

if (typeof duracionReal !== "number" || isNaN(duracionReal)) {
    duracionReal = 0;
}
            if (duracionReal < minDuration) {
                eliminados.push({
                    title: data.post?.title || "[Sin título]",
                    content: data.post?.content || "[Sin contenido]",
                    user: creator_id,
                    link: postLink,
                    video: videoUrls[0],
                    razon: `Duración total menor a ${minDuration}s (${duracionReal.toFixed(2)}s)`,
                    duracionTotal: duracionReal.toFixed(2) + "s"
                });
                post.style.display = "none";
                totalEliminados++;
            } else {
                aprobados.push({
                    title: data.post?.title || "[Sin título]",
                    content: data.post?.content || "[Sin contenido]",
                    user: creator_id,
                    link: postLink,
                    video: videoUrls[0],
                    duracionTotal: duracionReal.toFixed(2) + "s"
                });
            }

        } catch (e) {
            console.warn(`[ERROR] Excepción procesando post ${post_id}`, e);
        }
    }

    // Función para procesar en batches paralelos
    for (let i = 0; i < posts.length; i += batchSize) {
        const batch = posts.slice(i, i + batchSize);
        await Promise.all(batch.map(procesarPost));
    }

    console.log(`[RESULTADO] Total de publicaciones eliminadas: ${totalEliminados}`);
    mostrarResultadosEnConsola(eliminados, aprobados);
}



async function filterByBlacklistAndDuration(minDuration, batchSize = 17) {
    console.log(`[INFO] Filtrando por blacklist y duración mínima: ${minDuration}s`);

    const cardList = await new Promise(resolve => {
        const existing = document.querySelector(".card-list__items");
        if (existing) return resolve(existing);

        const observer = new MutationObserver(() => {
            const list = document.querySelector(".card-list__items");
            if (list) {
                observer.disconnect();
                resolve(list);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    });

    const posts = [...cardList.children];
    let totalEliminados = 0;

    const eliminados = [];
    const aprobados = [];

    // Procesa un solo post
    async function procesarPost(post) {
        const postLink = post.querySelector("a")?.href;
        if (!postLink) return;

        const urlParts = postLink.match(/https?:\/\/[^/]+\/([^/]+)\/user\/([^/]+)\/post\/(\d+)/);
        if (!urlParts) return;
        const [, service, creator_id, post_id] = urlParts;

        try {
            const urlApi = `https://coomer.su/api/v1/${service}/user/${creator_id}/post/${post_id}`;
let data;
try {
    data = await gmFetchJson(urlApi);
} catch (err) {
    console.warn(`[WARN] No se pudo obtener info del post ${post_id}`, err);
    return;
}

            const username = creator_id;

            // Rechazar por blacklist
            if (isUserBlacklisted(username)) {
                eliminados.push({
                    title: data.post?.title || "[Sin título]",
                    content: data.post?.content || "[Sin contenido]",
                    user: username,
                    link: postLink,
                    video: "",
                    razon: "Blacklist",
                    duracionTotal: "0s"
                });
                post.style.display = "none";
                totalEliminados++;
                return;
            }

            const videoUrlsRaw = extractVideoUrls(data);
            const videoUrls = videoUrlsRaw.filter(url => esUrlVideo(url));

            if (videoUrls.length === 0) {
                eliminados.push({
                    title: data.post?.title || "[Sin título]",
                    content: data.post?.content || "[Sin contenido]",
                    user: username,
                    link: postLink,
                    video: "",
                    razon: "No Videos",
                    duracionTotal: "0s"
                });
                post.style.display = "none";
                totalEliminados++;
                return;
            }

            let duracionReal = await probarDuracionVideo(service, creator_id, post_id);
            if (typeof duracionReal !== "number" || isNaN(duracionReal)) {
                duracionReal = 0;
            }

            if (duracionReal < minDuration) {
                eliminados.push({
                    title: data.post?.title || "[Sin título]",
                    content: data.post?.content || "[Sin contenido]",
                    user: username,
                    link: postLink,
                    video: videoUrls[0],
                    razon: `Duración total menor a ${minDuration}s (${duracionReal.toFixed(2)}s)`,
                    duracionTotal: duracionReal.toFixed(2) + "s"
                });
                post.style.display = "none";
                totalEliminados++;
            } else {
                aprobados.push({
                    title: data.post?.title || "[Sin título]",
                    content: data.post?.content || "[Sin contenido]",
                    user: username,
                    link: postLink,
                    video: videoUrls[0],
                    duracionTotal: duracionReal.toFixed(2) + "s"
                });
            }

        } catch (e) {
            console.warn(`[ERROR] Excepción procesando post ${post_id}`, e);
        }
    }

    // Ejecuta los posts por lotes en paralelo
    for (let i = 0; i < posts.length; i += batchSize) {
        const batch = posts.slice(i, i + batchSize);
        await Promise.all(batch.map(procesarPost));
    }

    console.log(`[RESULTADO] Total de publicaciones eliminadas: ${totalEliminados}`);
    mostrarResultadosEnConsola(eliminados, aprobados);
}


function extractVideoUrls(data) {
    if (!data) return [];

    // Función auxiliar para insertar "/data/" después del dominio en una URL base + path
    function insertDataSegment(url) {
        try {
            const urlObj = new URL(url);
            // Insertar "/data" justo después del hostname
            urlObj.pathname = '/data' + urlObj.pathname;
            return urlObj.toString();
        } catch {
            return url;
        }
    }

    // Priorizar data.videos
    if (Array.isArray(data.videos)) {
        const urls = data.videos
            .map(v => {
                if (v.server && v.path) {
                    const fullUrl = v.server + v.path;
                    return insertDataSegment(fullUrl);
                }
                return null;
            })
            .filter(url => url !== null);
        if (urls.length) return urls;
    }

    // Si no, revisar attachments en data.post
    if (Array.isArray(data.post?.attachments)) {
        const urls = data.post.attachments
            .map(att => {
                if (att.server && att.path) {
                    const fullUrl = att.server + att.path;
                    return insertDataSegment(fullUrl);
                }
                if (att.path) {
                    // Fallback raro: solo path, sin server
                    return att.path;
                }
                return null;
            })
            .filter(url => url !== null);
        if (urls.length) return urls;
    }

    return [];
}


function obtenerDuracionConVideoElement(url) {
    return new Promise((resolve) => {
        console.log(`[INFO] Intentando obtener duración real con video element para URL: ${url}`);

        const video = document.createElement("video");
        video.preload = "metadata";
        video.src = url;

        const onLoadedMetadata = () => {
            const duracion = video.duration;
            console.log(`[DURACIÓN REAL] URL: ${url}, duración: ${duracion.toFixed(2)} segundos`);
            cleanup();
            resolve(duracion);
        };

        const onError = () => {
            console.log(`[WARN] No se pudo cargar metadata para URL: ${url}`);
            cleanup();
            resolve(0);
        };

        function cleanup() {
            video.removeEventListener("loadedmetadata", onLoadedMetadata);
            video.removeEventListener("error", onError);
            video.src = "";
        }

        video.addEventListener("loadedmetadata", onLoadedMetadata);
        video.addEventListener("error", onError);
    });
}

async function postHasVideoAndCheckBlacklist(service, creator_id, post_id, mode, username) {
    const url = `https://coomer.su/api/v1/${service}/user/${creator_id}/post/${post_id}`;
    try {
let data;
try {
    data = await gmFetchJson(url);
} catch (e) {
    return { passes: false };
}

        const hasVideo = Array.isArray(data.videos) && data.videos.length > 0;
        const isBlacklisted = isUserBlacklisted(username);

        let passes = true;
        if (mode === "onlyVideos") {
            passes = hasVideo;
        } else if (mode === "blacklistOnly") {
            passes = !isBlacklisted;
        } else if (mode === "videosAndBlacklist") {
            passes = hasVideo && !isBlacklisted;
        }

        return {
            passes,
            videoUrls: extractVideoUrls(data)
        };
    } catch (e) {
        console.warn(`[ERROR] Fallo al obtener info de post ${post_id}`, e);
        return { passes: false };
    }
}





function mostrarResultadosEnConsola(eliminados, aprobados) {
    const incluirDuracion = (arr) => arr.some(p => p.duracionTotal !== undefined);

    console.log("✅ POSTS APROBADOS:");
    console.table(
        aprobados.map(p => {
            const base = {
                Título: p.title,
                Contenido: p.content,
                Usuario: p.user,
                Video: p.video ? "Sí" : "No",
                Link: p.video || "-"
            };
            if (incluirDuracion(aprobados)) {
                base["Duración total"] = p.duracionTotal || "N/A";
            }
            return base;
        })
    );

    console.log("\n❌ POSTS ELIMINADOS:");
    console.table(
        eliminados.map(p => {
            const base = {
                Título: p.title,
                Contenido: p.content,
                Usuario: p.user,
                Video: p.video ? "Sí" : "No",
                "Razón de filtrado": p.razon || "Desconocida",
                Link: p.video || "-"
            };
            if (incluirDuracion(eliminados)) {
                base["Duración total"] = p.duracionTotal || "N/A";
            }
            return base;
        })
    );
    if (applyFilterButton) {
        applyFilterButton.disabled = false;
        applyFilterButton.textContent = interfaceTexts.completedText[language];
        applyFilterButton.style.backgroundColor = "#a6e3a1";
        applyFilterButton.dataset.status = "completed";
    }
}

function gmFetchJson(url) {
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: "GET",
            url,
            headers: {
                // pedir CSS en lugar de JSON
                "Accept": "text/css"
            },
            onload: response => {
                try {
                    const text = response.responseText;
                    // en algunos casos ya viene JSON válido aunque el header diga "css"
                    if (text.startsWith("{") || text.startsWith("[")) {
                        resolve(JSON.parse(text));
                    } else {
                        console.warn("[WARN] Respuesta no es JSON. Fragmento:", text.slice(0, 120));
                        resolve(null);
                    }
                } catch (e) {
                    console.error("[ERROR] al parsear JSON desde", url, e);
                    reject(e);
                }
            },
            onerror: reject
        });
    });
}