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.4.2
// @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';

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

    const VIDEO_EXTENSIONS = ['mp4', 'm4v', 'mov', 'webm'];
    const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif'];
    const POSTS_PER_PAGE = 50;
    const API_DELAY = 1000; // Delay between API page requests
    const SUBSTRING_TITLE_LENGTH = 100;
    const LS_COLLAPSE_KEY = 'videoFilterPanelCollapsed_v2';
    const VIDEO_DURATION_CHECK_TIMEOUT = 5000;
    const MAX_CONCURRENT_METADATA_REQUESTS = 25;

    // --- GLOBAL STATE VARIABLES ---
    let currentDomain = window.location.hostname;
    let allFoundVideoUrls = [];
    let videoIntersectionObserver = null;
    let isPanelCollapsed = false;

    // --- UI ELEMENT CREATION AND STYLING ---
    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);

    // --- UI HELPER FUNCTIONS ---

    /**
     * Toggles the visibility of the filter panel and saves its state to localStorage.
     */
    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());
    }

    /**
     * Initializes or resets the IntersectionObserver for lazy-loading videos.
     */
    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);
    }

    /**
     * Displays a message in the status area of the UI panel.
     * @param {string} message - The message to display.
     * @param {string} type - The type of message ('info', 'success', 'error').
     */
    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);
        }
    }

    /**
     * Parses the user-provided page range string (e.g., "1, 3-5") into an array of page numbers.
     * @param {string} inputStr - The input string from the page range text field.
     * @returns {number[]|null} An array of page numbers or null if parsing fails.
     */
    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);
    }

    /**
     * Parses the user-provided duration range string (e.g., "10-30", "60-") into a min/max object.
     * @param {string} inputStr - The input string from the duration range text field.
     * @returns {object|null} An object with min/max properties or an error object.
     */
    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
        };
    }

    // --- VIDEO DURATION FETCHING LOGIC ---

    /**
     * Fetches the duration of a single video by loading its metadata. Includes a timeout mechanism.
     * @param {string} videoUrl - The URL of the video to check.
     * @returns {Promise<number>} A promise that resolves with the video duration in seconds.
     */
    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();
        });
    }

    /**
     * Manages a pool of concurrent requests to get video durations,
     * preventing too many simultaneous requests.
     */
    class DurationCheckerPool {
        constructor(maxConcurrent) {
            this.maxConcurrent = maxConcurrent;
            this.queue = [];
            this.activeCount = 0;
        }

        /**
         * Adds a video URL to the processing queue and returns a promise that resolves with the duration.
         * @param {string} videoUrl - The video URL to process.
         * @returns {Promise<number>} A promise for the video's duration.
         */
        add(videoUrl) {
            return new Promise((resolve, reject) => {
                this.queue.push({
                    videoUrl,
                    resolve,
                    reject
                });
                this._processQueue();
            });
        }

        /**
         * Internal method to process the next item in the queue if the concurrent limit is not reached.
         */
        _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();
                });
        }
    }

    // --- PAGE CONTEXT AND API HANDLING ---

    /**
     * Analyzes the current URL to determine the page context (e.g., user profile, search results, popular posts).
     * @returns {object|null} A context object or null if the context is not recognized.
     */
    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') {
            return {
                type: 'popular_posts',
                date: searchParams.get('date') || 'none',
                period: searchParams.get('period') || 'recent'
            };
        };
        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;
    }

    /**
     * Constructs the appropriate API URL based on the determined page context and offset.
     * @param {object} context - The context object from determinePageContext.
     * @param {number} offset - The post offset for pagination.
     * @returns {string|null} The constructed API URL or 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':
                if(context.date !='none') queryParams.push(`date=${encodeURIComponent(context.date)}`);
                queryParams.push(`period=${encodeURIComponent(context.period)}`);
                return `${baseApiUrl}/posts/popular?${queryParams.join('&')}`;
            default:
                return null;
        }
    }

    /**
     * Fetches data from a given API URL using GM_xmlhttpRequest and returns a promise with the parsed JSON.
     * @param {string} apiUrl - The URL to fetch data from.
     * @returns {Promise<object>} A promise that resolves with the parsed JSON response.
     */
    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'}`)
            });
        });
    }

    // --- GENERAL HELPER FUNCTIONS ---
    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;
    }

    /**
     * Generates the HTML string for a single post card, including data-attributes for sorting.
     * @param {object} postData - The post object and its metadata.
     * @param {string} previewUrl - The URL for the post's preview image.
     * @param {number|null} videoDurationToDisplay - The duration of the video to display on the card.
     * @returns {string} The HTML string for the post card.
     */
    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}`;
        const sortDate = postDate.getTime();
        const sortDuration = videoDurationToDisplay !== null ? Math.round(videoDurationToDisplay) : -1;
        return `<article class="post-card post-card--preview" data-sort-date="${sortDate}" data-sort-duration="${sortDuration}"><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>`;
    }


    // --- CORE LOGIC: MAIN FILTERING AND DISPLAY FUNCTION ---

    /**
     * The main function that orchestrates the entire filtering process.
     * It fetches data from multiple pages, processes each post asynchronously to find videos
     * and their durations, renders the results as they become available, and then sorts the
     * final DOM elements.
     */
    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 postsProcessedCounter = 0;
        const allProcessingPromises = [];

        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 = `Fetching Page ${i + 1}/${pagesToFetch.length}...`;
            try {
                const apiResponse = await fetchData(apiUrl);
                const posts = Array.isArray(apiResponse) ? apiResponse : (apiResponse.results || apiResponse.posts || []);
                const resultPreviews = apiResponse.result_previews;

                for (const post of posts) {
                    postsProcessedCounter++;
                    const postProcessingPromise = (async () => {
                        const postVideoUrls = getAllVideoUrlsFromPost(post);
                        if (postVideoUrls.length === 0) return false;

                        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 (!parsedDurationFilter && postDuration !== null) {
                                matchesDurationFilter = true;
                            }
                        }

                        if (matchesDurationFilter) {
                            allFoundVideoUrls.push(...postVideoUrls);
                            const apiPreviewEntry = resultPreviews ? (resultPreviews[post.id] || (Array.isArray(resultPreviews) ? resultPreviews.find(p => p.id === post.id) : null)) : null;
                            const postData = {
                                post,
                                postDate: new Date(post.published || post.added)
                            };

                            const cardHtml = createPostCardHtml(postData, getPostPreviewUrl(post, apiPreviewEntry), postDuration);
                            postListContainer.insertAdjacentHTML('beforeend', cardHtml);

                            const newCard = postListContainer.lastElementChild;
                            const newVideo = newCard.querySelector('video.lazy-load-video');
                            if (newVideo) videoIntersectionObserver.observe(newVideo);
                            return true;
                        }

                        return false;
                    })();

                    allProcessingPromises.push(postProcessingPromise);
                }
            } 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('Processing remaining videos...', 'info');

        const results = await Promise.all(allProcessingPromises);
        const matchedPostsCounter = results.filter(Boolean).length;

        showStatus('Sorting results...', 'info');

        const cards = Array.from(postListContainer.children);
        cards.sort((a, b) => {
            const aDate = Number(a.dataset.sortDate);
            const bDate = Number(b.dataset.sortDate);
            const aDuration = Number(a.dataset.sortDuration);
            const bDuration = Number(b.dataset.sortDuration);

            switch (sortOption) {
                case 'date_asc':
                    return aDate - bDate;
                case 'duration_desc':
                    return bDuration - aDuration;
                case 'duration_asc':
                    return aDuration - bDuration;
                case 'date_desc':
                default:
                    return bDate - aDate;
            }
        });

        cards.forEach(card => postListContainer.appendChild(card));

        if (paginatorInfo) paginatorInfo.textContent = `Showing ${matchedPostsCounter} video posts. Processed ${postsProcessedCounter} posts.`;
        filterButton.textContent = 'Filter Videos';
        styleButton(filterButton, false);
        filterButton.disabled = false;

        if (matchedPostsCounter > 0) {
            showStatus(`Filter complete. Found ${matchedPostsCounter} video posts.`, 'success');
            styleButton(copyUrlsButton, false);
            copyUrlsButton.disabled = false;
        } else {
            showStatus(`Filter complete. No matching video posts found.`, 'info');
        }
    }

    // --- EVENT HANDLERS AND SCRIPT INITIALIZATION ---

    /**
     * Handles the 'Copy Video URLs' button click event.
     */
    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);
    }

    /**
     * Resets the script's state when the URL changes (for Single Page Application support).
     */
    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);
    }

    // Attaching event listeners to UI elements and browser navigation events.
    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);

    // Initial setup on script load.
    const initiallyCollapsed = localStorage.getItem(LS_COLLAPSE_KEY) === 'true';
    if (initiallyCollapsed) togglePanelCollapse();
    handleUrlChangeAndSetStatus();

})();