Tampermonkey Video Filter v4 (video duration)

Filters and sorts posts by video duration, supports popular pages, and includes SPA navigation handling. Skips duration check if field is empty or timeout is reached.

// ==UserScript==
// @name         Tampermonkey Video Filter v4 (video duration)
// @namespace    http://tampermonkey.net/
// @version      1.3.9
// @description  Filters and sorts posts by video duration, supports popular pages, and includes SPA navigation handling. Skips duration check if field is empty or timeout is reached.
// @author       harryangstrom, xdegeneratex, remuru, AI Assistant
// @match        https://*.coomer.party/*
// @match        https://*.coomer.su/*
// @match        https://*.coomer.st/*
// @match        https://*.kemono.su/*
// @match        https://*.kemono.party/*
// @match        https://*.kemono.cr/*
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const CUSTOM_STYLES = ` select option { color: var(--colour0-primary) !important; } `;
    GM_addStyle(CUSTOM_STYLES);

    // --- CONFIGURATION ---
    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_v2';
    const VIDEO_DURATION_CHECK_TIMEOUT = 5000;
    const MAX_CONCURRENT_METADATA_REQUESTS = 10;

    // --- GLOBAL STATE ---
    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.cssText = 'position:fixed; bottom:10px; right:10px; background-color:#2c2c2e; color:#e0e0e0; border:1px solid #444; padding:12px; z-index:9999; font-family:Arial,sans-serif; font-size:14px; box-shadow:0 2px 8px rgba(0,0,0,0.5); border-radius:4px; transition:all 0.2s ease-in-out;';
    const initialUiContainerPadding = '12px';
    const collapseButton = document.createElement('button');
    collapseButton.id = 'video-filter-collapse-button';
    collapseButton.innerHTML = '»';
    collapseButton.title = 'Collapse/Expand Panel';
    collapseButton.style.cssText = 'position:absolute; bottom:8px; left:8px; width:25px; height:60px; display:flex; align-items:center; justify-content:center; padding:0; font-size:16px; background-color:#4a4a4c; color:#f0f0f0; border:1px solid #555; border-radius:3px; cursor:pointer; z-index: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.cssText = 'width:100px; margin-right:8px; padding:6px 8px; background-color:#1e1e1e; color:#e0e0e0; border:1px solid #555; border-radius: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.cssText = 'width:100px; margin-right:8px; padding:6px 8px; background-color:#1e1e1e; color:#e0e0e0; border:1px solid #555; border-radius:3px;';
    const sortLabel = document.createElement('label');
    sortLabel.htmlFor = 'video-filter-sort-by';
    sortLabel.textContent = 'Sort by: ';
    sortLabel.style.marginLeft = '10px';
    const sortBySelect = document.createElement('select');
    sortBySelect.id = 'video-filter-sort-by';
    sortBySelect.style.cssText = 'padding:6px 8px; background-color:#1e1e1e; color:var(--colour0-primary, #e0e0e0); border:1px solid #555; border-radius:3px;';
    sortBySelect.innerHTML = `<option value="date_desc">Date (Newest First)</option><option value="date_asc">Date (Oldest First)</option><option value="duration_desc">Duration (Longest First)</option><option value="duration_asc">Duration (Shortest First)</option>`;
    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',
        hoverButtonBg = '#4a4a4c',
        disabledButtonBg = '#303030',
        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.cssText = 'margin-top:8px; font-size:12px; min-height:15px; 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);
    bottomControlsContainer.appendChild(sortLabel);
    bottomControlsContainer.appendChild(sortBySelect);
    panelMainContent.appendChild(topControlsContainer);
    panelMainContent.appendChild(bottomControlsContainer);
    panelMainContent.appendChild(statusMessage);
    uiContainer.appendChild(collapseButton);
    uiContainer.appendChild(panelMainContent);
    document.body.appendChild(uiContainer);

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

    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;
        const trimmedInput = inputStr.trim();
        let match;
        match = trimmedInput.match(/^(\d+)-(\d+)$/);
        if (match) return {
            min: parseInt(match[1], 10),
            max: parseInt(match[2], 10)
        };
        match = trimmedInput.match(/^(\d+)-$/);
        if (match) return {
            min: parseInt(match[1], 10),
            max: Infinity
        };
        match = trimmedInput.match(/^-(\d+)$/);
        if (match) return {
            min: 0,
            max: parseInt(match[1], 10)
        };
        showStatus(`Error: Invalid duration format "${trimmedInput}". Use e.g. "10-30", "60-", or "-120".`, 'error');
        return {
            error: true
        };
    }

    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 = video.onerror = 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;
                const errorMsg = `Timeout loading metadata for ${videoUrl.split('/').pop()} after ${VIDEO_DURATION_CHECK_TIMEOUT / 1000}s.`;
                console.warn(errorMsg);
                reject(new Error(errorMsg));
                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 = () => {
                if (resolved) return;
                resolved = true;
                reject(new Error(`Error loading metadata for ${videoUrl.split('/').pop()}`));
                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 {
                videoUrl,
                resolve,
                reject
            } = this.queue.shift();
            const videoFileNameForStatus = videoUrl.split('/').pop();
            showStatus(`Dur. check (${this.activeCount}/${this.maxConcurrent}, Q:${this.queue.length}): ${videoFileNameForStatus.substring(0,15)}...`, 'info');
            _getVideoDurationInternal(videoUrl).then(duration => resolve(duration)).catch(error => reject(error)).finally(() => {
                this.activeCount--;
                this._processQueue();
            });
        }
    }

    // ИЗМЕНЕНО: Возвращена полная версия функции
    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/popular') {
            let ctxDate = searchParams.get('date');
            let ctxPeriod = searchParams.get('period');
            let dateFound = !!ctxDate;
            let periodFound = !!ctxPeriod;

            if (!dateFound || !periodFound) {
                try {
                    const nextDataScript = document.getElementById('__NEXT_DATA__');
                    if (nextDataScript) {
                        const jsonData = JSON.parse(nextDataScript.textContent);
                        const pageProps = jsonData?.props?.pageProps;
                        if (pageProps) {
                            let tempDate = null,
                                tempPeriod = null;
                            if (pageProps.data?.info?.date && pageProps.data?.base?.period) {
                                tempDate = pageProps.data.info.date.substring(0, 10);
                                tempPeriod = pageProps.data.base.period;
                            } else if (pageProps.props?.today && pageProps.props?.currentPage === "popular_posts") {
                                tempDate = pageProps.props.today;
                                tempPeriod = 'week';
                            } else if (pageProps.initialState?.feed?.feed?.info?.date && pageProps.initialState?.feed?.feed?.base?.period) {
                                tempDate = pageProps.initialState.feed.feed.info.date.substring(0, 10);
                                tempPeriod = pageProps.initialState.feed.feed.base.period;
                            } else if (pageProps.initialProps?.pageProps?.data?.info?.date && pageProps.initialProps?.pageProps?.data?.base?.period) {
                                tempDate = pageProps.initialProps.pageProps.data.info.date.substring(0, 10);
                                tempPeriod = pageProps.initialProps.pageProps.data.base.period;
                            }
                            if (!dateFound && tempDate) {
                                ctxDate = tempDate;
                                dateFound = true;
                            }
                            if (!periodFound && tempPeriod) {
                                ctxPeriod = tempPeriod;
                                periodFound = true;
                            }
                        }
                    }
                } catch (e) {
                    console.warn("Video Filter: Could not parse __NEXT_DATA__.", e);
                }

                if (!dateFound) {
                    const popularDateSpan = document.querySelector('main#main.main section.site-section.site-section--popular-posts header.site-section__header h1.site-section__heading span');
                    if (popularDateSpan && popularDateSpan.title) {
                        const titleDateMatch = popularDateSpan.title.match(/^(\d{4}-\d{2}-\d{2})/);
                        if (titleDateMatch && titleDateMatch[1]) {
                            ctxDate = titleDateMatch[1];
                            dateFound = true;
                            if (!periodFound) {
                                ctxPeriod = 'day';
                                periodFound = true;
                            }
                        }
                    }
                }

                if (dateFound && !periodFound) {
                    ctxPeriod = 'week';
                    periodFound = true;
                }

                if (searchParams.toString() === '' && !dateFound) {
                    const today = new Date();
                    ctxDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
                    dateFound = true;
                    if (!periodFound) {
                        ctxPeriod = 'week';
                        periodFound = true;
                    }
                    console.warn("Video Filter: Used fallback to today's date for /posts/popular.");
                }
            }

            if (dateFound && periodFound) {
                return {
                    type: 'popular_posts',
                    date: ctxDate,
                    period: ctxPeriod,
                    query: null
                };
            } else {
                console.error('Video Filter: Missing date/period for /posts/popular. Final Date:', ctxDate, 'Final Period:', ctxPeriod);
                return null;
            }
        }

        if (pathname === '/posts') return {
            type: 'global_search',
            query: query || null
        };

        console.error('Video Filter: Unknown page structure for context.', 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}/posts?${queryParams.join('&')}`;
            case 'user_search':
                queryParams.push(`q=${encodeURIComponent(context.query)}`);
                return `${baseApiUrl}/${context.service}/user/${context.userId}/posts-legacy?${queryParams.join('&')}`;
            case 'global_search':
                if (context.query) queryParams.push(`q=${encodeURIComponent(context.query)}`);
                return `${baseApiUrl}/posts?${queryParams.join('&')}`;
            case 'popular_posts':
                queryParams.push(`date=${encodeURIComponent(context.date)}`);
                queryParams.push(`period=${encodeURIComponent(context.period)}`);
                return `${baseApiUrl}/posts/popular?${queryParams.join('&')}`;
            default:
                return null;
        }
    }

    function fetchData(apiUrl) {
        const headers = {
            "Accept": "text/css",
            "Referer": window.location.href,
            "User-Agent": navigator.userAgent,
            "X-Requested-With": "XMLHttpRequest"
        };
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: apiUrl,
                headers: headers,
                onload: resp => {
                    if (resp.status >= 200 && resp.status < 300) {
                        try {
                            resolve(JSON.parse(resp.responseText));
                        } catch (e) {
                            reject(`Error parsing JSON: ${e.message}`);
                        }
                    } else {
                        reject(`API request failed: ${resp.status} ${resp.statusText}`);
                    }
                },
                onerror: resp => reject(`Network error: ${resp.statusText || 'Unknown'}`)
            });
        });
    }

    function isVideoFile(p) {
        return p ? VIDEO_EXTENSIONS.some(e => p.toLowerCase().endsWith('.' + e)) : false;
    }

    function isImageFile(p) {
        return p ? IMAGE_EXTENSIONS.some(e => p.toLowerCase().endsWith('.' + e)) : false;
    }

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

    function getAllVideoUrlsFromPost(post) {
        const d = `https://${currentDomain}/data`,
            u = [];
        if (post.file?.path && isVideoFile(post.file.path)) u.push(d + post.file.path);
        if (post.attachments) {
            for (const a of post.attachments) {
                if (a.path && isVideoFile(a.path)) u.push(d + a.path);
            }
        }
        return [...new Set(u)];
    }

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

    function createPostCardHtml(postData, previewUrl, videoDurationToDisplay = null) {
        const {
            post,
            postDate
        } = postData;
        const formattedDate = postDate.toLocaleString();
        const attachmentCount = post.attachments?.length || 0;
        const attachmentText = attachmentCount === 1 ? "1 Attachment" : `${attachmentCount} Attachments`;
        let displayTitle = (post.title || '').trim();
        if (!displayTitle) {
            const div = document.createElement('div');
            div.innerHTML = post.content || '';
            displayTitle = (div.textContent || "").trim().substring(0, SUBSTRING_TITLE_LENGTH);
        }
        displayTitle = displayTitle || 'No Title';
        const firstVideoUrlForCard = getFirstVideoUrlForDisplay(post);
        const durationDisplay = videoDurationToDisplay !== null ? `<div style="position:absolute;bottom:5px;right:5px;background:rgba(0,0,0,0.7);color:white;padding:2px 5px;font-size:0.8em;border-radius:3px;">${Math.round(videoDurationToDisplay)}s</div>` : '';
        let mediaHtml = '';
        if (firstVideoUrlForCard) {
            const poster = previewUrl ? `poster="${previewUrl}"` : '';
            mediaHtml = `<div style="position:relative;background:#000;"><video class="lazy-load-video" controls preload="none" width="100%" style="max-height:265px;display:block;" ${poster}><source data-src="${firstVideoUrlForCard}" type="video/mp4"></video>${durationDisplay}</div>`;
        } else if (previewUrl) {
            mediaHtml = `<div><img src="${previewUrl}" style="max-width:100%;max-height:200px;object-fit:contain;"></div>`;
        } else {
            mediaHtml = `<div style="height:100px;display:flex;align-items:center;justify-content:center;background:#333;color:#aaa;">No Preview</div>`;
        }
        const postLink = `/${post.service}/user/${post.user}/post/${post.id}`;
        return `<article class="post-card post-card--preview"><a class="fancy-link" 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 datetime="${postDate.toISOString()}">${formattedDate}</time><div>${attachmentCount > 0 ? attachmentText : 'No Attachments'}</div></div></div></footer></a></article>`;
    }
    async function handleFilter() {
        showStatus('');
        filterButton.textContent = 'Filtering...';
        styleButton(filterButton, true);
        filterButton.disabled = true;
        styleButton(copyUrlsButton, true);
        copyUrlsButton.disabled = true;
        allFoundVideoUrls = [];
        setupVideoIntersectionObserver();
        const pagesToFetch = parsePageRange(pageRangeInput.value);
        if (!pagesToFetch) {
            styleButton(filterButton, false);
            filterButton.disabled = false;
            return;
        }
        const parsedDurationFilter = parseDurationRange(durationRangeInput.value);
        if (parsedDurationFilter?.error) {
            styleButton(filterButton, false);
            filterButton.disabled = false;
            return;
        }
        const context = determinePageContext();
        if (!context) {
            showStatus('Filter disabled, context not recognized.', 'error');
            styleButton(filterButton, false);
            filterButton.disabled = false;
            return;
        }
        const postListContainer = document.querySelector('.card-list__items');
        if (!postListContainer) {
            showStatus('Error: Post container 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(m => m.style.display = 'none');
        const paginatorInfo = document.querySelector('.paginator > small, .content > div > small.subtle-text');
        if (paginatorInfo) paginatorInfo.textContent = `Filtering posts...`;
        const sortOption = sortBySelect.value;
        const needsDurationCheck = !!parsedDurationFilter || sortOption.startsWith('duration_');
        const durationCheckerPool = new DurationCheckerPool(MAX_CONCURRENT_METADATA_REQUESTS);
        let postsToDisplay = [];
        let postsProcessedCounter = 0;
        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;
            }
            filterButton.textContent = `Page ${i + 1}/${pagesToFetch.length}...`;
            try {
                const apiResponse = await fetchData(apiUrl);
                let posts = Array.isArray(apiResponse) ? apiResponse : (apiResponse.results || apiResponse.posts || []);
                if (!Array.isArray(posts)) {
                    console.error("Could not extract a valid posts array from API response:", apiResponse);
                    continue;
                }
                let resultPreviews = apiResponse.result_previews;
                for (let postIndex = 0; postIndex < posts.length; postIndex++) {
                    postsProcessedCounter++;
                    filterButton.textContent = `Page ${i + 1}/${pagesToFetch.length} (Post ${postsProcessedCounter})...`;
                    const post = posts[postIndex];
                    const postVideoUrls = getAllVideoUrlsFromPost(post);
                    if (postVideoUrls.length === 0) continue;
                    let postDuration = null;
                    let matchesDurationFilter = !parsedDurationFilter;
                    if (needsDurationCheck) {
                        const durationPromises = postVideoUrls.map(url => durationCheckerPool.add(url));
                        const results = await Promise.allSettled(durationPromises);
                        for (const result of results) {
                            if (result.status === 'fulfilled') {
                                const duration = result.value;
                                if (postDuration === null) postDuration = duration;
                                if (parsedDurationFilter && duration >= parsedDurationFilter.min && duration <= parsedDurationFilter.max) {
                                    matchesDurationFilter = true;
                                    postDuration = duration;
                                    break;
                                }
                            } else {
                                console.warn(`Could not get duration for a video in post ${post.id}:`, result.reason.message);
                            }
                        }
                    }
                    if (matchesDurationFilter) {
                        allFoundVideoUrls.push(...postVideoUrls);
                        const apiPreviewEntry = resultPreviews ? (resultPreviews[postIndex] || resultPreviews[post.id]) : null;
                        postsToDisplay.push({
                            post,
                            previewUrl: getPostPreviewUrl(post, apiPreviewEntry),
                            videoDuration: postDuration,
                            postDate: new Date(post.published || post.added)
                        });
                    }
                }
            } catch (error) {
                showStatus(`Error on page ${pageNum}: ${error}`, 'error');
                console.error("Filter error:", error);
            }
            if (i < pagesToFetch.length - 1) await new Promise(r => setTimeout(r, API_DELAY));
        }
        showStatus('Sorting results...', 'info');
        postsToDisplay.sort((a, b) => {
            switch (sortOption) {
                case 'date_asc':
                    return a.postDate - b.postDate;
                case 'duration_desc':
                    if (a.videoDuration === null) return 1;
                    if (b.videoDuration === null) return -1;
                    return b.videoDuration - a.videoDuration;
                case 'duration_asc':
                    if (a.videoDuration === null) return 1;
                    if (b.videoDuration === null) return -1;
                    return a.videoDuration - b.videoDuration;
                case 'date_desc':
                default:
                    return b.postDate - a.postDate;
            }
        });
        showStatus('Rendering sorted posts...', 'info');
        postsToDisplay.forEach(postData => postListContainer.insertAdjacentHTML('beforeend', createPostCardHtml(postData, postData.previewUrl, postData.videoDuration)));
        postListContainer.querySelectorAll('video.lazy-load-video').forEach(videoEl => {
            if (videoEl.querySelector('source[data-src]') && videoIntersectionObserver) videoIntersectionObserver.observe(videoEl);
        });
        if (paginatorInfo) paginatorInfo.textContent = `Showing ${postsToDisplay.length} video posts. Processed ${postsProcessedCounter} posts.`;
        filterButton.textContent = 'Filter Videos';
        styleButton(filterButton, false);
        filterButton.disabled = false;
        if (postsToDisplay.length > 0) {
            showStatus(`Filter complete. Found ${postsToDisplay.length} video posts.`, 'success');
            styleButton(copyUrlsButton, false);
            copyUrlsButton.disabled = false;
        } else {
            showStatus(`Filter complete. No matching video posts found.`, 'info');
        }
    }

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

    function handleUrlChangeAndSetStatus() {
        setTimeout(() => {
            const currentContext = determinePageContext();
            allFoundVideoUrls = [];
            styleButton(copyUrlsButton, true);
            copyUrlsButton.disabled = true;
            if (videoIntersectionObserver) videoIntersectionObserver.disconnect();
            if (currentContext) {
                showStatus("Video filter ready. Set filters and click 'Filter Videos'.", 'info');
                styleButton(filterButton, false);
                filterButton.disabled = false;
            } else {
                showStatus("Page context not recognized. Filter disabled on this page.", 'error');
                styleButton(filterButton, true);
                filterButton.disabled = true;
            }
        }, 100);
    }
    filterButton.addEventListener('click', handleFilter);
    copyUrlsButton.addEventListener('click', handleCopyUrls);
    collapseButton.addEventListener('click', togglePanelCollapse);
    const originalPushState = history.pushState;
    history.pushState = function() {
        originalPushState.apply(this, arguments);
        window.dispatchEvent(new Event('custompushstate'));
    };
    const originalReplaceState = history.replaceState;
    history.replaceState = function() {
        originalReplaceState.apply(this, arguments);
        window.dispatchEvent(new Event('customreplacestate'));
    };
    window.addEventListener('popstate', handleUrlChangeAndSetStatus);
    window.addEventListener('custompushstate', handleUrlChangeAndSetStatus);
    window.addEventListener('customreplacestate', handleUrlChangeAndSetStatus);
    const initiallyCollapsed = localStorage.getItem(LS_COLLAPSE_KEY) === 'true';
    if (initiallyCollapsed) togglePanelCollapse();
    handleUrlChangeAndSetStatus();

})();