Tampermonkey Video Filter v4

Filters posts; duration check is skipped if duration field is empty.

// ==UserScript==
// @name          Tampermonkey Video Filter v4
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Filters posts; duration check is skipped if duration field is empty.
// @author       harryangstrom, xdegeneratex, remuru
// @match        https://*.coomer.party/*/user/*
// @match        https://*.kemono.party/*/user/*
// @match        https://*.coomer.su/*/user/*
// @match        https://*.kemono.su/*/user/*
// @match        https://*.coomer.party/posts*
// @match        https://*.kemono.party/posts*
// @match        https://*.coomer.su/posts*
// @match        https://*.kemono.su/posts*
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const VIDEO_EXTENSIONS = ['mp4', 'm4v', 'mov', 'webm'];
    const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif'];
    const POSTS_PER_PAGE = 50;
    const API_DELAY = 1000;
    const SUBSTRING_TITLE_LENGTH = 100;
    const LS_COLLAPSE_KEY = 'videoFilterPanelCollapsed_v1';
    const VIDEO_DURATION_CHECK_TIMEOUT = 15000;
    const MAX_CONCURRENT_METADATA_REQUESTS = 3;

    let currentDomain = window.location.hostname;
    let allFoundVideoUrls = [];
    let videoIntersectionObserver = null;
    let isPanelCollapsed = false;

    // --- UI Elements (без изменений, опущено для краткости) ---
    const uiContainer = document.createElement('div');
    uiContainer.id = 'video-filter-ui';
    uiContainer.style.position = 'fixed';
    uiContainer.style.bottom = '10px';
    uiContainer.style.right = '10px';
    uiContainer.style.backgroundColor = '#2c2c2e';
    uiContainer.style.color = '#e0e0e0';
    uiContainer.style.border = '1px solid #444444';
    const initialUiContainerPadding = '12px';
    uiContainer.style.padding = initialUiContainerPadding;
    uiContainer.style.zIndex = '9999';
    uiContainer.style.fontFamily = 'Arial, sans-serif';
    uiContainer.style.fontSize = '14px';
    uiContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.5)';
    uiContainer.style.borderRadius = '4px';
    uiContainer.style.transition = 'width 0.2s ease-in-out, height 0.2s ease-in-out, padding 0.2s ease-in-out';

    const collapseButton = document.createElement('button');
    collapseButton.id = 'video-filter-collapse-button';
    collapseButton.innerHTML = '»';
    collapseButton.title = 'Collapse/Expand Panel';
    collapseButton.style.position = 'absolute';
    collapseButton.style.bottom = '8px';
    collapseButton.style.left = '8px';
    collapseButton.style.width = '25px';
    collapseButton.style.height = '60px';
    collapseButton.style.display = 'flex';
    collapseButton.style.alignItems = 'center';
    collapseButton.style.justifyContent = 'center';
    collapseButton.style.padding = '0';
    collapseButton.style.fontSize = '16px';
    collapseButton.style.backgroundColor = '#4a4a4c';
    collapseButton.style.color = '#f0f0f0';
    collapseButton.style.border = '1px solid #555555';
    collapseButton.style.borderRadius = '3px';
    collapseButton.style.cursor = 'pointer';
    collapseButton.style.zIndex = '1';

    const panelMainContent = document.createElement('div');
    panelMainContent.id = 'video-filter-main-content';
    panelMainContent.style.marginLeft = '30px';

    const pageRangeInput = document.createElement('input');
    pageRangeInput.type = 'text';
    pageRangeInput.id = 'video-filter-page-range';
    pageRangeInput.value = '1';
    pageRangeInput.placeholder = 'e.g., 1, 2-5, 7';
    pageRangeInput.style.width = '100px';
    pageRangeInput.style.marginRight = '8px';
    pageRangeInput.style.padding = '6px 8px';
    pageRangeInput.style.backgroundColor = '#1e1e1e';
    pageRangeInput.style.color = '#e0e0e0';
    pageRangeInput.style.border = '1px solid #555555';
    pageRangeInput.style.borderRadius = '3px';

    const durationLabel = document.createElement('label');
    durationLabel.htmlFor = 'video-filter-duration-range';
    durationLabel.textContent = 'Duration (s): ';
    durationLabel.style.marginLeft = '10px';

    const durationRangeInput = document.createElement('input');
    durationRangeInput.type = 'text';
    durationRangeInput.id = 'video-filter-duration-range';
    durationRangeInput.placeholder = 'e.g., 10-30, 60-, -120';
    durationRangeInput.title = 'Filter by video duration in seconds. Examples:\n"10-30": 10 to 30 seconds\n"60-": 60 seconds or more\n"-120": up to 120 seconds\nLeave empty for no duration filter.';
    durationRangeInput.style.width = '100px';
    durationRangeInput.style.marginRight = '8px';
    durationRangeInput.style.padding = '6px 8px';
    durationRangeInput.style.backgroundColor = '#1e1e1e';
    durationRangeInput.style.color = '#e0e0e0';
    durationRangeInput.style.border = '1px solid #555555';
    durationRangeInput.style.borderRadius = '3px';

    const filterButton = document.createElement('button');
    filterButton.id = 'video-filter-button';
    filterButton.textContent = 'Filter Videos';

    const copyUrlsButton = document.createElement('button');
    copyUrlsButton.id = 'video-copy-urls-button';
    copyUrlsButton.textContent = 'Copy Video URLs';
    copyUrlsButton.disabled = true;

    const baseButtonBg = '#3a3a3c';
    const hoverButtonBg = '#4a4a4c';
    const disabledButtonBg = '#303030';
    const disabledButtonColor = '#777777';

    function styleButton(button, disabled = false) {
        if (disabled) {
            button.style.backgroundColor = disabledButtonBg;
            button.style.color = disabledButtonColor;
            button.style.cursor = 'default';
        } else {
            button.style.backgroundColor = baseButtonBg;
            button.style.color = '#f0f0f0';
            button.style.cursor = 'pointer';
        }
        button.style.marginRight = '8px';
        button.style.padding = '6px 12px';
        button.style.border = '1px solid #555555';
        button.style.borderRadius = '3px';
    }

    [filterButton, copyUrlsButton].forEach(btn => {
        styleButton(btn, btn.disabled);
        btn.onmouseenter = () => { if (!btn.disabled) btn.style.backgroundColor = hoverButtonBg; };
        btn.onmouseleave = () => { if (!btn.disabled) btn.style.backgroundColor = baseButtonBg; };
    });
    collapseButton.onmouseenter = () => { if (collapseButton.style.backgroundColor !== disabledButtonBg) collapseButton.style.backgroundColor = hoverButtonBg; };
    collapseButton.onmouseleave = () => { if (collapseButton.style.backgroundColor !== disabledButtonBg) collapseButton.style.backgroundColor = '#4a4a4c'; };

    const statusMessage = document.createElement('div');
    statusMessage.id = 'video-filter-status';
    statusMessage.style.marginTop = '8px';
    statusMessage.style.fontSize = '12px';
    statusMessage.style.minHeight = '15px';
    statusMessage.style.color = '#cccccc';

    const topControlsContainer = document.createElement('div');
    topControlsContainer.style.marginBottom = '8px';
    topControlsContainer.appendChild(document.createTextNode('Pages: '));
    topControlsContainer.appendChild(pageRangeInput);
    topControlsContainer.appendChild(durationLabel);
    topControlsContainer.appendChild(durationRangeInput);

    const bottomControlsContainer = document.createElement('div');
    bottomControlsContainer.appendChild(filterButton);
    bottomControlsContainer.appendChild(copyUrlsButton);

    panelMainContent.appendChild(topControlsContainer);
    panelMainContent.appendChild(bottomControlsContainer);
    panelMainContent.appendChild(statusMessage);

    uiContainer.appendChild(collapseButton);
    uiContainer.appendChild(panelMainContent);
    document.body.appendChild(uiContainer);
    // --- End of UI Elements ---

    function togglePanelCollapse() {
        isPanelCollapsed = !isPanelCollapsed;
        if (isPanelCollapsed) {
            panelMainContent.style.display = 'none';
            collapseButton.innerHTML = '«';
            uiContainer.style.width = '41px';
            uiContainer.style.height = '80px';
            uiContainer.style.padding = '0';
        } else {
            panelMainContent.style.display = 'block';
            collapseButton.innerHTML = '»';
            uiContainer.style.width = '';
            uiContainer.style.height = '';
            uiContainer.style.padding = initialUiContainerPadding;
        }
        localStorage.setItem(LS_COLLAPSE_KEY, isPanelCollapsed.toString());
    }
    collapseButton.addEventListener('click', togglePanelCollapse);
    const initiallyCollapsed = localStorage.getItem(LS_COLLAPSE_KEY) === 'true';
    if (initiallyCollapsed) {
        togglePanelCollapse();
    }

    function setupVideoIntersectionObserver() {
        if (videoIntersectionObserver) {
            videoIntersectionObserver.disconnect();
        }
        const options = { root: null, rootMargin: '200px 0px', threshold: 0.01 };
        videoIntersectionObserver = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const videoElement = entry.target;
                    const sourceElement = videoElement.querySelector('source[data-src]');
                    if (sourceElement) {
                        const videoUrl = sourceElement.getAttribute('data-src');
                        sourceElement.setAttribute('src', videoUrl);
                        videoElement.load();
                        sourceElement.removeAttribute('data-src');
                        observer.unobserve(videoElement);
                    }
                }
            });
        }, options);
    }

    function showStatus(message, type = 'info') {
        statusMessage.textContent = message;
        switch (type) {
            case 'error': statusMessage.style.color = '#ff6b6b'; break;
            case 'success': statusMessage.style.color = '#76c7c0'; break;
            case 'info': default: statusMessage.style.color = '#cccccc'; break;
        }
        if (type === 'success' && message.includes("Copied")) {
            setTimeout(() => {
                if (statusMessage.textContent === message) {
                    statusMessage.textContent = '';
                    statusMessage.style.color = '#cccccc';
                }
            }, 3000);
        }
    }

    function parsePageRange(inputStr) {
        const pages = new Set();
        if (!inputStr || inputStr.trim() === '') {
            showStatus('Error: Page range cannot be empty.', 'error');
            return null;
        }
        const parts = inputStr.split(',');
        for (const part of parts) {
            if (part.includes('-')) {
                const [startStr, endStr] = part.split('-');
                const start = parseInt(startStr, 10);
                const end = parseInt(endStr, 10);
                if (isNaN(start) || isNaN(end) || start <= 0 || end < start) {
                    showStatus(`Error: Invalid range "${part}". Start must be > 0 and end >= start.`, 'error');
                    return null;
                }
                for (let i = start; i <= end; i++) pages.add(i);
            } else {
                const page = parseInt(part, 10);
                if (isNaN(page) || page <= 0) {
                    showStatus(`Error: Invalid page number "${part}". Must be > 0.`, 'error');
                    return null;
                }
                pages.add(page);
            }
        }
        if (pages.size === 0) {
            showStatus('Error: No valid pages specified.', 'error');
            return null;
        }
        return Array.from(pages).sort((a, b) => a - b);
    }

    function parseDurationRange(inputStr) {
        if (!inputStr || inputStr.trim() === '') {
            return null; // No filter, so no duration check
        }
        const trimmedInput = inputStr.trim();

        let match = trimmedInput.match(/^(\d+)-(\d+)$/); // "min-max"
        if (match) {
            const min = parseInt(match[1], 10);
            const max = parseInt(match[2], 10);
            if (!isNaN(min) && !isNaN(max) && min <= max) {
                return { min, max };
            } else {
                showStatus(`Error: Invalid duration range "${trimmedInput}". Min must be a number <= Max.`, 'error');
                return { error: true };
            }
        }

        match = trimmedInput.match(/^(\d+)-$/); // "min-"
        if (match) {
            const min = parseInt(match[1], 10);
            if (!isNaN(min)) {
                return { min, max: Infinity };
            } else {
                 showStatus(`Error: Invalid duration start "${trimmedInput}". Must be a number.`, 'error');
                 return { error: true };
            }
        }

        match = trimmedInput.match(/^-(\d+)$/); // "-max"
        if (match) {
            const max = parseInt(match[1], 10);
             if (!isNaN(max)) {
                return { min: 0, max };
            } else {
                 showStatus(`Error: Invalid duration end "${trimmedInput}". Must be a number.`, 'error');
                 return { error: true };
            }
        }

        showStatus(`Error: Invalid duration format "${trimmedInput}". Use e.g. "10-30", "60-", or "-120".`, 'error');
        return { error: true };
    }

    // --- Video Duration Logic ---
    function _getVideoDurationInternal(videoUrl) {
        return new Promise((resolve, reject) => {
            const video = document.createElement('video');
            video.preload = 'metadata';
            video.style.display = 'none';
            document.body.appendChild(video);

            let resolved = false;
            let timeoutId = null;

            const cleanup = () => {
                if (timeoutId) clearTimeout(timeoutId);
                video.onloadedmetadata = null;
                video.onerror = null;
                video.onabort = null;
                try {
                    video.src = '';
                    video.removeAttribute('src');
                    while (video.firstChild) { video.removeChild(video.firstChild); }
                } catch (e) { /* ignore */ }
                if (video.parentNode) {
                    video.parentNode.removeChild(video);
                }
            };

            timeoutId = setTimeout(() => {
                if (resolved) return;
                resolved = true;
                reject(new Error(`Timeout loading metadata for ${videoUrl.split('/').pop()} after ${VIDEO_DURATION_CHECK_TIMEOUT / 1000}s.`));
                cleanup();
            }, VIDEO_DURATION_CHECK_TIMEOUT);

            video.onloadedmetadata = () => {
                if (resolved) return;
                resolved = true;
                const duration = video.duration;
                if (typeof duration === 'number' && !isNaN(duration) && isFinite(duration)) {
                    resolve(duration);
                } else {
                    reject(new Error(`Invalid or infinite duration for ${videoUrl.split('/').pop()}: ${duration}`));
                }
                cleanup();
            };

            video.onerror = (e) => {
                if (resolved) return;
                resolved = true;
                let errorMsg = `Error loading metadata for ${videoUrl.split('/').pop()}`;
                if (video.error) {
                    switch (video.error.code) {
                        case 1: errorMsg += ': Aborted.'; break;
                        case 2: errorMsg += ': Network error.'; break;
                        case 3: errorMsg += ': Decode error.'; break;
                        case 4: errorMsg += ': Source not supported/found.'; break;
                        default: errorMsg += `: Unknown error code ${video.error.code}.`;
                    }
                } else if (e && e.type === 'error') { errorMsg += ': General error event.'; }
                reject(new Error(errorMsg));
                cleanup();
            };

             video.onabort = () => {
                if (resolved) return;
                resolved = true;
                reject(new Error(`Metadata loading aborted for ${videoUrl.split('/').pop()}.`));
                cleanup();
            };

            const sourceElement = document.createElement('source');
            sourceElement.src = videoUrl;
            video.appendChild(sourceElement);
            video.load();
        });
    }

    class DurationCheckerPool {
        constructor(maxConcurrent) {
            this.maxConcurrent = maxConcurrent;
            this.queue = [];
            this.activeCount = 0;
        }

        add(videoUrl) {
            return new Promise((resolve, reject) => {
                this.queue.push({ videoUrl, resolve, reject });
                this._processQueue();
            });
        }

        async _processQueue() {
            if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) {
                return;
            }

            this.activeCount++;
            const task = this.queue.shift();
            const { videoUrl, resolve, reject } = task;

            const videoFileNameForStatus = videoUrl.split('/').pop();
            const statusMsg = `Dur. check (${this.activeCount}/${this.maxConcurrent}, Q:${this.queue.length}): ${videoFileNameForStatus.substring(0,15)}${videoFileNameForStatus.length > 15 ? '...' : ''}`;
            showStatus(statusMsg, 'info');

            _getVideoDurationInternal(videoUrl)
                .then(duration => resolve(duration))
                .catch(error => reject(error))
                .finally(() => {
                    this.activeCount--;
                    this._processQueue();
                });
        }
    }
    // --- End Video Duration Logic ---


    function determinePageContext() {
        const pathname = window.location.pathname;
        const searchParams = new URLSearchParams(window.location.search);
        const query = searchParams.get('q');
        const userProfileMatch = pathname.match(/^\/([^/]+)\/user\/([^/]+)$/);

        if (userProfileMatch && !query) return { type: 'profile', service: userProfileMatch[1], userId: userProfileMatch[2] };
        if (userProfileMatch && query) return { type: 'user_search', service: userProfileMatch[1], userId: userProfileMatch[2], query };
        if (pathname === '/posts') return { type: 'global_search', query: query || null };

        showStatus('Error: Could not determine page context.', 'error');
        console.error('Unknown page structure:', pathname, window.location.search);
        return null;
    }

    function buildApiUrl(context, offset) {
        let baseApiUrl = `https://${currentDomain}/api/v1`;
        let queryParams = `o=${offset}`;
        switch (context.type) {
            case 'profile': return `${baseApiUrl}/${context.service}/user/${context.userId}?${queryParams}`;
            case 'user_search':
                queryParams += `&q=${encodeURIComponent(context.query)}`;
                return `${baseApiUrl}/${context.service}/user/${context.userId}/posts-legacy?${queryParams}`;
            case 'global_search':
                if (context.query) queryParams += `&q=${encodeURIComponent(context.query)}`;
                return `${baseApiUrl}/posts?${queryParams}`;
            default: return null;
        }
    }

    function fetchData(apiUrl) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET", url: apiUrl,
                onload: resp => {
                    if (resp.status >= 200 && resp.status < 300) {
                        try { resolve(JSON.parse(resp.responseText)); }
                        catch (e) { reject(`Error parsing JSON from ${apiUrl}: ${e.message}`); }
                    } else { reject(`API request failed for ${apiUrl}: ${resp.status} ${resp.statusText}`); }
                },
                onerror: resp => reject(`API request error for ${apiUrl}: ${resp.statusText || 'Network error'}`)
            });
        });
    }

    function isVideoFile(filenameOrPath) {
        if (!filenameOrPath) return false;
        const lowerName = filenameOrPath.toLowerCase();
        return VIDEO_EXTENSIONS.some(ext => lowerName.endsWith('.' + ext));
    }

    function isImageFile(filenameOrPath) {
        if (!filenameOrPath) return false;
        const lowerName = filenameOrPath.toLowerCase();
        return IMAGE_EXTENSIONS.some(ext => lowerName.endsWith('.' + ext));
    }

    function getPostPreviewUrl(post, apiPreviewsEntry) {
        if (apiPreviewsEntry && apiPreviewsEntry.length > 0 && apiPreviewsEntry[0]) {
            const previewData = apiPreviewsEntry[0];
            if (previewData.server && previewData.path) return `${previewData.server}${previewData.path}`;
        }
        if (post.file && post.file.path && isImageFile(post.file.path)) return `https://${currentDomain}/data${post.file.path}`;
        if (post.file && post.file.name && isImageFile(post.file.name) && post.file.name.startsWith('/')) return `https://${currentDomain}/data${post.file.name}`;
        if (post.attachments) {
            for (const attachment of post.attachments) {
                if (attachment.path && isImageFile(attachment.path)) return `https://${currentDomain}/data${attachment.path}`;
                if (attachment.name && isImageFile(attachment.name) && attachment.name.startsWith('/')) return `https://${currentDomain}/data${attachment.name}`;
            }
        }
        return null;
    }

    function getAllVideoUrlsFromPost(post) {
        const domain = `https://${currentDomain}/data`;
        const videoUrls = [];

        if (post.file && (isVideoFile(post.file.name) || isVideoFile(post.file.path))) {
            if (post.file.path) videoUrls.push(domain + post.file.path);
            else if (post.file.name && post.file.name.startsWith('/')) videoUrls.push(domain + post.file.name);
        }
        if (post.attachments) {
            for (const att of post.attachments) {
                if (isVideoFile(att.name) || isVideoFile(att.path)) {
                    if (att.path) videoUrls.push(domain + att.path);
                    else if (att.name && att.name.startsWith('/')) videoUrls.push(domain + att.name);
                }
            }
        }
        return [...new Set(videoUrls)]; // Return unique URLs
    }

    function getFirstVideoUrlForDisplay(post) {
        const allVideos = getAllVideoUrlsFromPost(post);
        return allVideos.length > 0 ? allVideos[0] : null;
    }


    function createPostCardHtml(post, previewUrl, videoDurationToDisplay = null) {
        const postDate = new Date(post.published || post.added);
        const formattedDate = postDate.toLocaleString();
        const dateTimeAttr = postDate.toISOString();
        const attachmentCount = post.attachments ? post.attachments.length : 0;
        const attachmentText = attachmentCount === 1 ? "1 Attachment" : `${attachmentCount} Attachments`;

        let displayTitle = (post.title && post.title.trim()) ? post.title.trim() : '';
        let potentialContent = post.content || post.substring || '';
        if (!displayTitle && potentialContent) {
            const tempDiv = document.createElement('div');
            tempDiv.innerHTML = potentialContent;
            let contentForTitle = (tempDiv.textContent || tempDiv.innerText || "").trim();
            if (contentForTitle) {
                displayTitle = contentForTitle.substring(0, SUBSTRING_TITLE_LENGTH) + (contentForTitle.length > SUBSTRING_TITLE_LENGTH ? '...' : '');
            }
        }
        displayTitle = displayTitle || 'No Title';

        let mediaHtml = '';
        const firstVideoUrlForCard = getFirstVideoUrlForDisplay(post);
        const durationDisplay = videoDurationToDisplay !== null ? `<div class="video-duration-overlay" style="position: absolute; bottom: 5px; right: 5px; background-color: rgba(0,0,0,0.7); color: white; padding: 2px 5px; font-size: 0.8em; border-radius: 3px;">${Math.round(videoDurationToDisplay)}s</div>` : '';

        if (firstVideoUrlForCard) {
            const posterAttribute = previewUrl ? `poster="${previewUrl}"` : '';
            mediaHtml = `
            <div class="post-card__image-container" style="position: relative; text-align: center; margin-bottom: 5px; background-color: #000;">
                <video class="lazy-load-video" controls preload="none" width="100%" style="max-height: 265px; display: block;" ${posterAttribute}>
                    <source data-src="${firstVideoUrlForCard}" type="video/mp4">
                    Your browser does not support the video tag.
                </video>
                ${durationDisplay}
            </div>`;
        } else if (previewUrl) {
            mediaHtml = `
            <div class="post-card__image-container" style="text-align: center; margin-bottom: 5px;">
                <img class="post-card__image" src="${previewUrl}" alt="Preview for post ${post.id}" style="max-width: 100%; height: auto; max-height: 200px; object-fit: contain; border: 1px solid #444;">
            </div>`;
        } else {
            mediaHtml = `
            <div class="post-card__image-container" style="text-align: center; margin-bottom: 5px; height: 100px; display: flex; align-items: center; justify-content: center; background-color: #333; color: #aaa; font-size:0.9em; border: 1px solid #444;">
                No Preview Available
            </div>`;
        }

        const postLink = `/${post.service}/user/${post.user}/post/${post.id}`;
        return `
        <article class="post-card post-card--preview" data-id="${post.id}" data-service="${post.service}" data-user="${post.user}">
          <a class="fancy-link fancy-link--kemono" href="${postLink}" target="_blank" rel="noopener noreferrer">
           <header class="post-card__header" title="${displayTitle.replace(/"/g, '"')}">${displayTitle}</header>
           ${mediaHtml}
           <footer class="post-card__footer">
             <div>
                <div>
                  <time class="timestamp" datetime="${dateTimeAttr}">${formattedDate}</time>
                  <div>${attachmentCount > 0 ? attachmentText : 'No Attachments'}</div>
                </div>
              </div>
            </footer>
          </a>
        </article>`;
    }

    async function handleFilter() {
        showStatus('');
        filterButton.textContent = 'Filter Videos';
        styleButton(filterButton, true); filterButton.disabled = true;
        styleButton(copyUrlsButton, true); copyUrlsButton.disabled = true;
        allFoundVideoUrls = [];

        setupVideoIntersectionObserver();

        const pageRangeStr = pageRangeInput.value;
        const pagesToFetch = parsePageRange(pageRangeStr);
        if (!pagesToFetch) {
            styleButton(filterButton, false); filterButton.disabled = false;
            return;
        }

        const durationRangeStr = durationRangeInput.value;
        const parsedDurationFilter = parseDurationRange(durationRangeStr);
        if (parsedDurationFilter && parsedDurationFilter.error) { // Error in parsing
            styleButton(filterButton, false); filterButton.disabled = false;
            return;
        }
        // parsedDurationFilter will be null if input is empty, which is fine.

        const context = determinePageContext();
        if (!context) {
            styleButton(filterButton, false); filterButton.disabled = false;
            return;
        }

        const postListContainer = document.querySelector('.card-list__items');
        if (!postListContainer) {
            showStatus('Error: Post container (.card-list__items) not found.', 'error');
            styleButton(filterButton, false); filterButton.disabled = false;
            return;
        }
        postListContainer.style.setProperty('--card-size', '350px');
        postListContainer.innerHTML = '';

        document.querySelectorAll('.paginator menu, .content > menu.Paginator').forEach(menu => menu.style.display = 'none');
        const paginatorInfo = document.querySelector('.paginator > small, .content > div > small.subtle-text');
        if (paginatorInfo) paginatorInfo.textContent = `Filtering posts...`;

        let totalVideoPostsFound = 0;
        let postsProcessedCounter = 0;
        const durationCheckerPool = new DurationCheckerPool(MAX_CONCURRENT_METADATA_REQUESTS);


        for (let i = 0; i < pagesToFetch.length; i++) {
            const pageNum = pagesToFetch[i];
            const offset = (pageNum - 1) * POSTS_PER_PAGE;
            const apiUrl = buildApiUrl(context, offset);
            if (!apiUrl) {
                showStatus('Error: Could not build API URL.', 'error');
                break;
            }

            const originalFilterButtonText = filterButton.textContent;
            filterButton.textContent = `Page ${i + 1}/${pagesToFetch.length}...`;
            const pageStatus = `Fetching page ${pageNum} (offset ${offset})...`;
            showStatus(pageStatus, 'info');

            try {
                const apiResponse = await fetchData(apiUrl);
                showStatus(pageStatus, 'info');

                let posts = [], resultPreviews = null;

                if (Array.isArray(apiResponse)) posts = apiResponse;
                else if (apiResponse.results && Array.isArray(apiResponse.results)) { posts = apiResponse.results; resultPreviews = apiResponse.result_previews; }
                else if (apiResponse.posts && Array.isArray(apiResponse.posts)) { posts = apiResponse.posts; resultPreviews = apiResponse.result_previews; }
                else {
                    showStatus(`Warning: Unexpected API response for page ${pageNum}.`, 'error');
                    console.warn("Unexpected API response:", apiResponse);
                    continue;
                }

                for (let postIndex = 0; postIndex < posts.length; postIndex++) {
                    postsProcessedCounter++;
                    filterButton.textContent = `Page ${i + 1}/${pagesToFetch.length} (Post ${postsProcessedCounter})...`;
                    const post = posts[postIndex];

                    const postVideoUrlsToCheck = getAllVideoUrlsFromPost(post);
                    let isVideoPostOverall = postVideoUrlsToCheck.length > 0;
                    let durationToDisplayOnCard = null;
                    let postMatchesDurationFilter = true; // Assume true if no duration filter or if it's a non-video post

                    if (isVideoPostOverall) {
                        if (parsedDurationFilter) { // Only check duration if a filter is set
                            postMatchesDurationFilter = false; // Must prove it matches
                            for (const videoUrl of postVideoUrlsToCheck) {
                                try {
                                    const currentVideoDuration = await durationCheckerPool.add(videoUrl);
                                    if (durationToDisplayOnCard === null) {
                                        durationToDisplayOnCard = currentVideoDuration;
                                    }
                                    if (currentVideoDuration >= parsedDurationFilter.min && currentVideoDuration <= parsedDurationFilter.max) {
                                        postMatchesDurationFilter = true;
                                        durationToDisplayOnCard = currentVideoDuration;
                                        break;
                                    }
                                } catch (err) {
                                    const videoFileNameForError = videoUrl.split('/').pop();
                                    console.warn(`Could not get duration for ${videoFileNameForError}:`, err.message);
                                }
                            }
                        }
                        // If parsedDurationFilter is null, postMatchesDurationFilter remains true,
                        // and durationToDisplayOnCard remains null. No duration check is performed.

                        if (postMatchesDurationFilter) {
                            totalVideoPostsFound++;
                            allFoundVideoUrls.push(...postVideoUrlsToCheck);
                            const apiPreviewEntry = resultPreviews ? (Array.isArray(resultPreviews) ? resultPreviews[postIndex] : resultPreviews[post.id]) : null;
                            const previewUrl = getPostPreviewUrl(post, apiPreviewEntry);
                            const cardHtml = createPostCardHtml(post, previewUrl, durationToDisplayOnCard);
                            postListContainer.insertAdjacentHTML('beforeend', cardHtml);

                            const newlyAddedCard = postListContainer.lastElementChild;
                            if (newlyAddedCard) {
                                const videoEl = newlyAddedCard.querySelector('video.lazy-load-video');
                                if (videoEl && videoEl.querySelector('source[data-src]') && videoIntersectionObserver) {
                                    videoIntersectionObserver.observe(videoEl);
                                }
                            }
                        }
                    }
                }
                if (paginatorInfo) paginatorInfo.textContent = `Showing ${totalVideoPostsFound} video posts. Processed ${postsProcessedCounter} posts total.`;

            } catch (error) {
                showStatus(`Error on page ${pageNum}: ${error}`, 'error');
                console.error("Filter error:", error);
            } finally {
                 filterButton.textContent = originalFilterButtonText;
            }
            if (i < pagesToFetch.length - 1) await new Promise(resolve => setTimeout(resolve, API_DELAY));
        }

        filterButton.textContent = 'Filter Videos';
        styleButton(filterButton, false); filterButton.disabled = false;

        if (totalVideoPostsFound > 0) {
            showStatus(`Filter complete. Found ${totalVideoPostsFound} video posts. Processed ${postsProcessedCounter} posts.`, 'success');
            styleButton(copyUrlsButton, false); copyUrlsButton.disabled = false;
        } else {
            const durationFilterActive = parsedDurationFilter ? " with current duration filter" : "";
            showStatus(`Filter complete. No matching video posts found${durationFilterActive}. Processed ${postsProcessedCounter} posts.`, 'info');
            styleButton(copyUrlsButton, true); copyUrlsButton.disabled = true;
        }
    }

    function handleCopyUrls() {
        if (allFoundVideoUrls.length === 0) {
            showStatus("No video URLs to copy.", 'error');
            return;
        }
        const uniqueUrls = [...new Set(allFoundVideoUrls)];
        GM_setClipboard(uniqueUrls.join('\n'));
        const originalText = copyUrlsButton.textContent;
        copyUrlsButton.textContent = `Copied ${uniqueUrls.length} URLs!`;
        showStatus(`Copied ${uniqueUrls.length} unique video URLs!`, 'success');
        setTimeout(() => { copyUrlsButton.textContent = originalText; }, 3000);
    }

    filterButton.addEventListener('click', handleFilter);
    copyUrlsButton.addEventListener('click', handleCopyUrls);

    if (determinePageContext()) {
        showStatus("Video filter ready. Enter page & duration, then filter.", 'info');
    } else {
        showStatus("Page not recognized. Filter may not work as expected.", 'error');
    }

})();