Tampermonkey Video Filter v4

Filters posts with videos using dynamic content detection and improved performance.

2025-05-29 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name          Tampermonkey Video Filter v4
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Filters posts with videos using dynamic content detection and improved performance.
// @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'];
    const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif'];
    const POSTS_PER_PAGE = 50;
    const API_DELAY = 2000; // Delay between API requests
    const SUBSTRING_TITLE_LENGTH = 100; // Max length for title from substring
    const LS_COLLAPSE_KEY = 'videoFilterPanelCollapsed_v1';

    let currentDomain = window.location.hostname;
    let allFoundVideoUrls = [];
    let videoIntersectionObserver = null;
    let isPanelCollapsed = false; // Panel is initially expanded

    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'; // Store for restore
    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 = '»'; // « (Collapse)
    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'; // Above panelMainContent

    collapseButton.onmouseenter = () => { if (collapseButton.style.backgroundColor !== disabledButtonBg) collapseButton.style.backgroundColor = hoverButtonBg; };
    collapseButton.onmouseleave = () => { if (collapseButton.style.backgroundColor !== disabledButtonBg) collapseButton.style.backgroundColor = '#4a4a4c'; };

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

    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 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';

    // Styles buttons based on disabled state
    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';
    }

    // Apply initial styles and hover effects
    [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; };
    });

    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';

    panelMainContent.appendChild(document.createTextNode('Pages: '));
    panelMainContent.appendChild(pageRangeInput);
    panelMainContent.appendChild(filterButton);
    panelMainContent.appendChild(copyUrlsButton);
    panelMainContent.appendChild(statusMessage);

    uiContainer.appendChild(collapseButton);
    uiContainer.appendChild(panelMainContent);
    document.body.appendChild(uiContainer);

    // Logic for collapsing and expanding the panel
    function togglePanelCollapse() {
        isPanelCollapsed = !isPanelCollapsed;
        if (isPanelCollapsed) {
            panelMainContent.style.display = 'none';
            collapseButton.innerHTML = '«'; // » (Expand)
            uiContainer.style.width = '41px';
            uiContainer.style.height = '80px';
            uiContainer.style.padding = '0';
        } else {
            panelMainContent.style.display = 'block';
            collapseButton.innerHTML = '»'; // « (Collapse)
            uiContainer.style.width = '';
            uiContainer.style.height = '';
            uiContainer.style.padding = initialUiContainerPadding;
        }
        localStorage.setItem(LS_COLLAPSE_KEY, isPanelCollapsed.toString());
    }
    collapseButton.addEventListener('click', togglePanelCollapse);

    // Load and apply saved collapsed state on script load
    const initiallyCollapsed = localStorage.getItem(LS_COLLAPSE_KEY) === 'true';
    if (initiallyCollapsed) {
        // isPanelCollapsed is currently false. togglePanelCollapse will invert it to true and apply styles.
        togglePanelCollapse();
    }

    // Set up Intersection Observer for lazy loading videos
    function setupVideoIntersectionObserver() {
        if (videoIntersectionObserver) {
            videoIntersectionObserver.disconnect();
        }

        const options = {
            root: null, // relative to document viewport
            rootMargin: '200px 0px', // start loading when video is 200px away from viewport
            threshold: 0.01 // minimal visibility to trigger
        };

        videoIntersectionObserver = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const videoElement = entry.target; // This is the <video> element
                    const sourceElement = videoElement.querySelector('source[data-src]');

                    if (sourceElement) {
                        const videoUrl = sourceElement.getAttribute('data-src');
                        // console.log('Lazy loading video:', videoUrl); // Debugging line, keep commented
                        sourceElement.setAttribute('src', videoUrl);
                        videoElement.load(); // Tell the video element to load the new source
                        sourceElement.removeAttribute('data-src'); // Remove data-src to prevent re-processing
                        observer.unobserve(videoElement); // Stop observing this video once loaded
                    }
                }
            });
        }, options);
    }

    // Display status messages in the UI
    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;
        }
        // Clear success message after a delay
        if (type === 'success' && message.includes("Copied")) {
            setTimeout(() => {
                if (statusMessage.textContent === message) {
                    statusMessage.textContent = '';
                    statusMessage.style.color = '#cccccc';
                }
            }, 3000);
        }
    }

    // Parses the page range input string (e.g., "1, 3-5, 7") into an array of numbers
    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);
    }

    // Determines the current page context (user profile or global search)
    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;
    }

    // Builds the API URL based on the determined context and offset
    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;
        }
    }

    // Fetches data from the given API URL using GM_xmlhttpRequest
    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'}`)
            });
        });
    }

    // Checks if a filename or path has a video extension
    function isVideoFile(filenameOrPath) {
        if (!filenameOrPath) return false;
        const lowerName = filenameOrPath.toLowerCase();
        return VIDEO_EXTENSIONS.some(ext => lowerName.endsWith('.' + ext));
    }

    // Checks if a filename or path has an image extension
    function isImageFile(filenameOrPath) {
        if (!filenameOrPath) return false;
        const lowerName = filenameOrPath.toLowerCase();
        return IMAGE_EXTENSIONS.some(ext => lowerName.endsWith('.' + ext));
    }

    // Gets the URL for the post preview image
    function getPostPreviewUrl(post, apiPreviewsEntry) {
        // Check API previews first
        if (apiPreviewsEntry && apiPreviewsEntry.length > 0 && apiPreviewsEntry[0]) {
            const previewData = apiPreviewsEntry[0];
            if (previewData.server && previewData.path) return `${previewData.server}${previewData.path}`;
        }
        // Check post file if it's an image
        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}`;
        // Check attachments for images
        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;
    }

    // Finds the first video URL associated with a post
    function getFirstVideoUrlFromPost(post) {
        const domain = `https://${currentDomain}/data`;
        // Check main post file
        if (post.file && (isVideoFile(post.file.name) || isVideoFile(post.file.path))) {
            if (post.file.path) return domain + post.file.path;
            if (post.file.name && post.file.name.startsWith('/')) return domain + post.file.name;
        }
        // Check attachments
        if (post.attachments) {
            for (const att of post.attachments) {
                if (isVideoFile(att.name) || isVideoFile(att.path)) {
                    if (att.path) return domain + att.path;
                    if (att.name && att.name.startsWith('/')) return domain + att.name;
                }
            }
        }
        return null;
    }

    // Creates the HTML structure for a post card preview
    function createPostCardHtml(post, previewUrl) {
        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 || '';
        // Use substring or content if no title is available
        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 firstVideoUrl = getFirstVideoUrlFromPost(post);

        // Generate media HTML based on available content (video preferred)
        if (firstVideoUrl) {
            const posterAttribute = previewUrl ? `poster="${previewUrl}"` : '';
            mediaHtml = `
        <div class="post-card__image-container" style="text-align: center; margin-bottom: 5px; background-color: #000;">
            <video class="lazy-load-video" controls preload="none" width="100%" style="max-height: 300px; display: block;" ${posterAttribute}>
                <source data-src="${firstVideoUrl}" type="video/mp4">
                Your browser does not support the video tag.
            </video>
        </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>`;
}

    // Main function to handle the filtering process
    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 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;
        }
        // Adjust container layout for new card size
        postListContainer.style.setProperty('--card-size', '350px');
        // Clear existing posts
        postListContainer.innerHTML = '';

        // Hide original paginator elements
        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;

        // Iterate through selected pages and fetch data
        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 = `Filtering Page ${i + 1} / ${pagesToFetch.length}...`;
            showStatus(`Fetching page ${pageNum} (offset ${offset})...`, 'info');

            try {
                const apiResponse = await fetchData(apiUrl);
                let posts = [], resultPreviews = null;

                // Parse response format (can vary slightly)
                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; // Skip to the next page
                }

                // Process each post in the response
                for (let postIndex = 0; postIndex < posts.length; postIndex++) {
                    const post = posts[postIndex];
                    let isVideo = false, postVideoUrls = [];

                    // Check main post file for video
                    if (post.file && (isVideoFile(post.file.name) || isVideoFile(post.file.path))) {
                        isVideo = true;
                        if (post.file.path) postVideoUrls.push(`https://${currentDomain}/data${post.file.path}`);
                        else if (post.file.name && post.file.name.startsWith('/')) postVideoUrls.push(`https://${currentDomain}/data${post.file.name}`);
                    }
                    // Check attachments for videos
                    if (post.attachments) {
                        post.attachments.forEach(att => {
                            if (isVideoFile(att.name) || isVideoFile(att.path)) {
                                isVideo = true;
                                if (att.path) postVideoUrls.push(`https://${currentDomain}/data${att.path}`);
                                else if (att.name && att.name.startsWith('/')) postVideoUrls.push(`https://${currentDomain}/data${att.name}`);
                            }
                        });
                    }

                    // If video found, add to list and display card
                    if (isVideo) {
                        totalVideoPostsFound++;
                        allFoundVideoUrls.push(...postVideoUrls);
                        const apiPreviewEntry = resultPreviews ? resultPreviews[postIndex] : null;
                        const previewUrl = getPostPreviewUrl(post, apiPreviewEntry);
                        const cardHtml = createPostCardHtml(post, previewUrl);
                        postListContainer.insertAdjacentHTML('beforeend', cardHtml);
                    }
                }
                // Update progress message
                if (paginatorInfo) paginatorInfo.textContent = `Showing ${totalVideoPostsFound} video posts from selected pages.`;

            } catch (error) {
                showStatus(`Error fetching page ${pageNum}: ${error}`, 'error');
                console.error("Filter error:", error);
            }
            // Wait before fetching the next page to avoid hitting rate limits
            if (i < pagesToFetch.length - 1) await new Promise(resolve => setTimeout(resolve, API_DELAY));
        }

        // Observe newly added video elements for lazy loading
        const videoElementsInContainer = postListContainer.querySelectorAll('video.lazy-load-video');
        videoElementsInContainer.forEach(videoEl => {
            // Only observe if it still has the data-src attribute
            if (videoEl.querySelector('source[data-src]') && videoIntersectionObserver) {
                videoIntersectionObserver.observe(videoEl);
            }
        });

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

        // Final status update based on results
        if (totalVideoPostsFound > 0) {
            showStatus(`Filter complete. Found ${totalVideoPostsFound} video posts.`, 'success');
            styleButton(copyUrlsButton, false); copyUrlsButton.disabled = false;
        } else {
            showStatus('Filter complete. No video posts found on selected pages.', 'info');
            styleButton(copyUrlsButton, true); copyUrlsButton.disabled = true;
        }
    }

    // Handles copying the found video URLs to the clipboard
    function handleCopyUrls() {
        if (allFoundVideoUrls.length === 0) {
            showStatus("No video URLs to copy.", 'error');
            return;
        }
        const uniqueUrls = [...new Set(allFoundVideoUrls)]; // Get unique URLs
        GM_setClipboard(uniqueUrls.join('\n')); // Copy to clipboard, one URL per line
        const originalText = copyUrlsButton.textContent;
        copyUrlsButton.textContent = `Copied ${uniqueUrls.length} URLs!`;
        showStatus(`Copied ${uniqueUrls.length} unique video URLs!`, 'success');
        // Restore button text after a delay
        setTimeout(() => { copyUrlsButton.textContent = originalText; }, 3000);
    }

    // Attach event listeners to buttons
    filterButton.addEventListener('click', handleFilter);
    copyUrlsButton.addEventListener('click', handleCopyUrls);

    // Initial status message based on page context
    if (determinePageContext()) {
        showStatus("Video filter ready. Enter page range and click 'Filter Videos'.", 'info');
    } else {
        showStatus("Page not recognized. Filter may not work as expected.", 'error');
    }

})();