您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filters posts; duration check is skipped if duration field is empty.
// ==UserScript== // @name Tampermonkey Video Filter v4 // @namespace http://tampermonkey.net/ // @version 1.2 // @description Filters posts; duration check is skipped if duration field is empty. // @author harryangstrom, xdegeneratex, remuru // @match https://*.coomer.party/*/user/* // @match https://*.kemono.party/*/user/* // @match https://*.coomer.su/*/user/* // @match https://*.kemono.su/*/user/* // @match https://*.coomer.party/posts* // @match https://*.kemono.party/posts* // @match https://*.coomer.su/posts* // @match https://*.kemono.su/posts* // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; const VIDEO_EXTENSIONS = ['mp4', 'm4v', 'mov', 'webm']; const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif']; const POSTS_PER_PAGE = 50; const API_DELAY = 1000; const SUBSTRING_TITLE_LENGTH = 100; const LS_COLLAPSE_KEY = 'videoFilterPanelCollapsed_v1'; const VIDEO_DURATION_CHECK_TIMEOUT = 15000; const MAX_CONCURRENT_METADATA_REQUESTS = 3; let currentDomain = window.location.hostname; let allFoundVideoUrls = []; let videoIntersectionObserver = null; let isPanelCollapsed = false; // --- UI Elements (без изменений, опущено для краткости) --- const uiContainer = document.createElement('div'); uiContainer.id = 'video-filter-ui'; uiContainer.style.position = 'fixed'; uiContainer.style.bottom = '10px'; uiContainer.style.right = '10px'; uiContainer.style.backgroundColor = '#2c2c2e'; uiContainer.style.color = '#e0e0e0'; uiContainer.style.border = '1px solid #444444'; const initialUiContainerPadding = '12px'; uiContainer.style.padding = initialUiContainerPadding; uiContainer.style.zIndex = '9999'; uiContainer.style.fontFamily = 'Arial, sans-serif'; uiContainer.style.fontSize = '14px'; uiContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.5)'; uiContainer.style.borderRadius = '4px'; uiContainer.style.transition = 'width 0.2s ease-in-out, height 0.2s ease-in-out, padding 0.2s ease-in-out'; const collapseButton = document.createElement('button'); collapseButton.id = 'video-filter-collapse-button'; collapseButton.innerHTML = '»'; collapseButton.title = 'Collapse/Expand Panel'; collapseButton.style.position = 'absolute'; collapseButton.style.bottom = '8px'; collapseButton.style.left = '8px'; collapseButton.style.width = '25px'; collapseButton.style.height = '60px'; collapseButton.style.display = 'flex'; collapseButton.style.alignItems = 'center'; collapseButton.style.justifyContent = 'center'; collapseButton.style.padding = '0'; collapseButton.style.fontSize = '16px'; collapseButton.style.backgroundColor = '#4a4a4c'; collapseButton.style.color = '#f0f0f0'; collapseButton.style.border = '1px solid #555555'; collapseButton.style.borderRadius = '3px'; collapseButton.style.cursor = 'pointer'; collapseButton.style.zIndex = '1'; const panelMainContent = document.createElement('div'); panelMainContent.id = 'video-filter-main-content'; panelMainContent.style.marginLeft = '30px'; const pageRangeInput = document.createElement('input'); pageRangeInput.type = 'text'; pageRangeInput.id = 'video-filter-page-range'; pageRangeInput.value = '1'; pageRangeInput.placeholder = 'e.g., 1, 2-5, 7'; pageRangeInput.style.width = '100px'; pageRangeInput.style.marginRight = '8px'; pageRangeInput.style.padding = '6px 8px'; pageRangeInput.style.backgroundColor = '#1e1e1e'; pageRangeInput.style.color = '#e0e0e0'; pageRangeInput.style.border = '1px solid #555555'; pageRangeInput.style.borderRadius = '3px'; const durationLabel = document.createElement('label'); durationLabel.htmlFor = 'video-filter-duration-range'; durationLabel.textContent = 'Duration (s): '; durationLabel.style.marginLeft = '10px'; const durationRangeInput = document.createElement('input'); durationRangeInput.type = 'text'; durationRangeInput.id = 'video-filter-duration-range'; durationRangeInput.placeholder = 'e.g., 10-30, 60-, -120'; durationRangeInput.title = 'Filter by video duration in seconds. Examples:\n"10-30": 10 to 30 seconds\n"60-": 60 seconds or more\n"-120": up to 120 seconds\nLeave empty for no duration filter.'; durationRangeInput.style.width = '100px'; durationRangeInput.style.marginRight = '8px'; durationRangeInput.style.padding = '6px 8px'; durationRangeInput.style.backgroundColor = '#1e1e1e'; durationRangeInput.style.color = '#e0e0e0'; durationRangeInput.style.border = '1px solid #555555'; durationRangeInput.style.borderRadius = '3px'; const filterButton = document.createElement('button'); filterButton.id = 'video-filter-button'; filterButton.textContent = 'Filter Videos'; const copyUrlsButton = document.createElement('button'); copyUrlsButton.id = 'video-copy-urls-button'; copyUrlsButton.textContent = 'Copy Video URLs'; copyUrlsButton.disabled = true; const baseButtonBg = '#3a3a3c'; const hoverButtonBg = '#4a4a4c'; const disabledButtonBg = '#303030'; const disabledButtonColor = '#777777'; function styleButton(button, disabled = false) { if (disabled) { button.style.backgroundColor = disabledButtonBg; button.style.color = disabledButtonColor; button.style.cursor = 'default'; } else { button.style.backgroundColor = baseButtonBg; button.style.color = '#f0f0f0'; button.style.cursor = 'pointer'; } button.style.marginRight = '8px'; button.style.padding = '6px 12px'; button.style.border = '1px solid #555555'; button.style.borderRadius = '3px'; } [filterButton, copyUrlsButton].forEach(btn => { styleButton(btn, btn.disabled); btn.onmouseenter = () => { if (!btn.disabled) btn.style.backgroundColor = hoverButtonBg; }; btn.onmouseleave = () => { if (!btn.disabled) btn.style.backgroundColor = baseButtonBg; }; }); collapseButton.onmouseenter = () => { if (collapseButton.style.backgroundColor !== disabledButtonBg) collapseButton.style.backgroundColor = hoverButtonBg; }; collapseButton.onmouseleave = () => { if (collapseButton.style.backgroundColor !== disabledButtonBg) collapseButton.style.backgroundColor = '#4a4a4c'; }; const statusMessage = document.createElement('div'); statusMessage.id = 'video-filter-status'; statusMessage.style.marginTop = '8px'; statusMessage.style.fontSize = '12px'; statusMessage.style.minHeight = '15px'; statusMessage.style.color = '#cccccc'; const topControlsContainer = document.createElement('div'); topControlsContainer.style.marginBottom = '8px'; topControlsContainer.appendChild(document.createTextNode('Pages: ')); topControlsContainer.appendChild(pageRangeInput); topControlsContainer.appendChild(durationLabel); topControlsContainer.appendChild(durationRangeInput); const bottomControlsContainer = document.createElement('div'); bottomControlsContainer.appendChild(filterButton); bottomControlsContainer.appendChild(copyUrlsButton); panelMainContent.appendChild(topControlsContainer); panelMainContent.appendChild(bottomControlsContainer); panelMainContent.appendChild(statusMessage); uiContainer.appendChild(collapseButton); uiContainer.appendChild(panelMainContent); document.body.appendChild(uiContainer); // --- End of UI Elements --- function togglePanelCollapse() { isPanelCollapsed = !isPanelCollapsed; if (isPanelCollapsed) { panelMainContent.style.display = 'none'; collapseButton.innerHTML = '«'; uiContainer.style.width = '41px'; uiContainer.style.height = '80px'; uiContainer.style.padding = '0'; } else { panelMainContent.style.display = 'block'; collapseButton.innerHTML = '»'; uiContainer.style.width = ''; uiContainer.style.height = ''; uiContainer.style.padding = initialUiContainerPadding; } localStorage.setItem(LS_COLLAPSE_KEY, isPanelCollapsed.toString()); } collapseButton.addEventListener('click', togglePanelCollapse); const initiallyCollapsed = localStorage.getItem(LS_COLLAPSE_KEY) === 'true'; if (initiallyCollapsed) { togglePanelCollapse(); } function setupVideoIntersectionObserver() { if (videoIntersectionObserver) { videoIntersectionObserver.disconnect(); } const options = { root: null, rootMargin: '200px 0px', threshold: 0.01 }; videoIntersectionObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const videoElement = entry.target; const sourceElement = videoElement.querySelector('source[data-src]'); if (sourceElement) { const videoUrl = sourceElement.getAttribute('data-src'); sourceElement.setAttribute('src', videoUrl); videoElement.load(); sourceElement.removeAttribute('data-src'); observer.unobserve(videoElement); } } }); }, options); } function showStatus(message, type = 'info') { statusMessage.textContent = message; switch (type) { case 'error': statusMessage.style.color = '#ff6b6b'; break; case 'success': statusMessage.style.color = '#76c7c0'; break; case 'info': default: statusMessage.style.color = '#cccccc'; break; } if (type === 'success' && message.includes("Copied")) { setTimeout(() => { if (statusMessage.textContent === message) { statusMessage.textContent = ''; statusMessage.style.color = '#cccccc'; } }, 3000); } } function parsePageRange(inputStr) { const pages = new Set(); if (!inputStr || inputStr.trim() === '') { showStatus('Error: Page range cannot be empty.', 'error'); return null; } const parts = inputStr.split(','); for (const part of parts) { if (part.includes('-')) { const [startStr, endStr] = part.split('-'); const start = parseInt(startStr, 10); const end = parseInt(endStr, 10); if (isNaN(start) || isNaN(end) || start <= 0 || end < start) { showStatus(`Error: Invalid range "${part}". Start must be > 0 and end >= start.`, 'error'); return null; } for (let i = start; i <= end; i++) pages.add(i); } else { const page = parseInt(part, 10); if (isNaN(page) || page <= 0) { showStatus(`Error: Invalid page number "${part}". Must be > 0.`, 'error'); return null; } pages.add(page); } } if (pages.size === 0) { showStatus('Error: No valid pages specified.', 'error'); return null; } return Array.from(pages).sort((a, b) => a - b); } function parseDurationRange(inputStr) { if (!inputStr || inputStr.trim() === '') { return null; // No filter, so no duration check } const trimmedInput = inputStr.trim(); let match = trimmedInput.match(/^(\d+)-(\d+)$/); // "min-max" if (match) { const min = parseInt(match[1], 10); const max = parseInt(match[2], 10); if (!isNaN(min) && !isNaN(max) && min <= max) { return { min, max }; } else { showStatus(`Error: Invalid duration range "${trimmedInput}". Min must be a number <= Max.`, 'error'); return { error: true }; } } match = trimmedInput.match(/^(\d+)-$/); // "min-" if (match) { const min = parseInt(match[1], 10); if (!isNaN(min)) { return { min, max: Infinity }; } else { showStatus(`Error: Invalid duration start "${trimmedInput}". Must be a number.`, 'error'); return { error: true }; } } match = trimmedInput.match(/^-(\d+)$/); // "-max" if (match) { const max = parseInt(match[1], 10); if (!isNaN(max)) { return { min: 0, max }; } else { showStatus(`Error: Invalid duration end "${trimmedInput}". Must be a number.`, 'error'); return { error: true }; } } showStatus(`Error: Invalid duration format "${trimmedInput}". Use e.g. "10-30", "60-", or "-120".`, 'error'); return { error: true }; } // --- Video Duration Logic --- function _getVideoDurationInternal(videoUrl) { return new Promise((resolve, reject) => { const video = document.createElement('video'); video.preload = 'metadata'; video.style.display = 'none'; document.body.appendChild(video); let resolved = false; let timeoutId = null; const cleanup = () => { if (timeoutId) clearTimeout(timeoutId); video.onloadedmetadata = null; video.onerror = null; video.onabort = null; try { video.src = ''; video.removeAttribute('src'); while (video.firstChild) { video.removeChild(video.firstChild); } } catch (e) { /* ignore */ } if (video.parentNode) { video.parentNode.removeChild(video); } }; timeoutId = setTimeout(() => { if (resolved) return; resolved = true; reject(new Error(`Timeout loading metadata for ${videoUrl.split('/').pop()} after ${VIDEO_DURATION_CHECK_TIMEOUT / 1000}s.`)); cleanup(); }, VIDEO_DURATION_CHECK_TIMEOUT); video.onloadedmetadata = () => { if (resolved) return; resolved = true; const duration = video.duration; if (typeof duration === 'number' && !isNaN(duration) && isFinite(duration)) { resolve(duration); } else { reject(new Error(`Invalid or infinite duration for ${videoUrl.split('/').pop()}: ${duration}`)); } cleanup(); }; video.onerror = (e) => { if (resolved) return; resolved = true; let errorMsg = `Error loading metadata for ${videoUrl.split('/').pop()}`; if (video.error) { switch (video.error.code) { case 1: errorMsg += ': Aborted.'; break; case 2: errorMsg += ': Network error.'; break; case 3: errorMsg += ': Decode error.'; break; case 4: errorMsg += ': Source not supported/found.'; break; default: errorMsg += `: Unknown error code ${video.error.code}.`; } } else if (e && e.type === 'error') { errorMsg += ': General error event.'; } reject(new Error(errorMsg)); cleanup(); }; video.onabort = () => { if (resolved) return; resolved = true; reject(new Error(`Metadata loading aborted for ${videoUrl.split('/').pop()}.`)); cleanup(); }; const sourceElement = document.createElement('source'); sourceElement.src = videoUrl; video.appendChild(sourceElement); video.load(); }); } class DurationCheckerPool { constructor(maxConcurrent) { this.maxConcurrent = maxConcurrent; this.queue = []; this.activeCount = 0; } add(videoUrl) { return new Promise((resolve, reject) => { this.queue.push({ videoUrl, resolve, reject }); this._processQueue(); }); } async _processQueue() { if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) { return; } this.activeCount++; const task = this.queue.shift(); const { videoUrl, resolve, reject } = task; const videoFileNameForStatus = videoUrl.split('/').pop(); const statusMsg = `Dur. check (${this.activeCount}/${this.maxConcurrent}, Q:${this.queue.length}): ${videoFileNameForStatus.substring(0,15)}${videoFileNameForStatus.length > 15 ? '...' : ''}`; showStatus(statusMsg, 'info'); _getVideoDurationInternal(videoUrl) .then(duration => resolve(duration)) .catch(error => reject(error)) .finally(() => { this.activeCount--; this._processQueue(); }); } } // --- End Video Duration Logic --- function determinePageContext() { const pathname = window.location.pathname; const searchParams = new URLSearchParams(window.location.search); const query = searchParams.get('q'); const userProfileMatch = pathname.match(/^\/([^/]+)\/user\/([^/]+)$/); if (userProfileMatch && !query) return { type: 'profile', service: userProfileMatch[1], userId: userProfileMatch[2] }; if (userProfileMatch && query) return { type: 'user_search', service: userProfileMatch[1], userId: userProfileMatch[2], query }; if (pathname === '/posts') return { type: 'global_search', query: query || null }; showStatus('Error: Could not determine page context.', 'error'); console.error('Unknown page structure:', pathname, window.location.search); return null; } function buildApiUrl(context, offset) { let baseApiUrl = `https://${currentDomain}/api/v1`; let queryParams = `o=${offset}`; switch (context.type) { case 'profile': return `${baseApiUrl}/${context.service}/user/${context.userId}?${queryParams}`; case 'user_search': queryParams += `&q=${encodeURIComponent(context.query)}`; return `${baseApiUrl}/${context.service}/user/${context.userId}/posts-legacy?${queryParams}`; case 'global_search': if (context.query) queryParams += `&q=${encodeURIComponent(context.query)}`; return `${baseApiUrl}/posts?${queryParams}`; default: return null; } } function fetchData(apiUrl) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, onload: resp => { if (resp.status >= 200 && resp.status < 300) { try { resolve(JSON.parse(resp.responseText)); } catch (e) { reject(`Error parsing JSON from ${apiUrl}: ${e.message}`); } } else { reject(`API request failed for ${apiUrl}: ${resp.status} ${resp.statusText}`); } }, onerror: resp => reject(`API request error for ${apiUrl}: ${resp.statusText || 'Network error'}`) }); }); } function isVideoFile(filenameOrPath) { if (!filenameOrPath) return false; const lowerName = filenameOrPath.toLowerCase(); return VIDEO_EXTENSIONS.some(ext => lowerName.endsWith('.' + ext)); } function isImageFile(filenameOrPath) { if (!filenameOrPath) return false; const lowerName = filenameOrPath.toLowerCase(); return IMAGE_EXTENSIONS.some(ext => lowerName.endsWith('.' + ext)); } function getPostPreviewUrl(post, apiPreviewsEntry) { if (apiPreviewsEntry && apiPreviewsEntry.length > 0 && apiPreviewsEntry[0]) { const previewData = apiPreviewsEntry[0]; if (previewData.server && previewData.path) return `${previewData.server}${previewData.path}`; } if (post.file && post.file.path && isImageFile(post.file.path)) return `https://${currentDomain}/data${post.file.path}`; if (post.file && post.file.name && isImageFile(post.file.name) && post.file.name.startsWith('/')) return `https://${currentDomain}/data${post.file.name}`; if (post.attachments) { for (const attachment of post.attachments) { if (attachment.path && isImageFile(attachment.path)) return `https://${currentDomain}/data${attachment.path}`; if (attachment.name && isImageFile(attachment.name) && attachment.name.startsWith('/')) return `https://${currentDomain}/data${attachment.name}`; } } return null; } function getAllVideoUrlsFromPost(post) { const domain = `https://${currentDomain}/data`; const videoUrls = []; if (post.file && (isVideoFile(post.file.name) || isVideoFile(post.file.path))) { if (post.file.path) videoUrls.push(domain + post.file.path); else if (post.file.name && post.file.name.startsWith('/')) videoUrls.push(domain + post.file.name); } if (post.attachments) { for (const att of post.attachments) { if (isVideoFile(att.name) || isVideoFile(att.path)) { if (att.path) videoUrls.push(domain + att.path); else if (att.name && att.name.startsWith('/')) videoUrls.push(domain + att.name); } } } return [...new Set(videoUrls)]; // Return unique URLs } function getFirstVideoUrlForDisplay(post) { const allVideos = getAllVideoUrlsFromPost(post); return allVideos.length > 0 ? allVideos[0] : null; } function createPostCardHtml(post, previewUrl, videoDurationToDisplay = null) { const postDate = new Date(post.published || post.added); const formattedDate = postDate.toLocaleString(); const dateTimeAttr = postDate.toISOString(); const attachmentCount = post.attachments ? post.attachments.length : 0; const attachmentText = attachmentCount === 1 ? "1 Attachment" : `${attachmentCount} Attachments`; let displayTitle = (post.title && post.title.trim()) ? post.title.trim() : ''; let potentialContent = post.content || post.substring || ''; if (!displayTitle && potentialContent) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = potentialContent; let contentForTitle = (tempDiv.textContent || tempDiv.innerText || "").trim(); if (contentForTitle) { displayTitle = contentForTitle.substring(0, SUBSTRING_TITLE_LENGTH) + (contentForTitle.length > SUBSTRING_TITLE_LENGTH ? '...' : ''); } } displayTitle = displayTitle || 'No Title'; let mediaHtml = ''; const firstVideoUrlForCard = getFirstVideoUrlForDisplay(post); const durationDisplay = videoDurationToDisplay !== null ? `<div class="video-duration-overlay" style="position: absolute; bottom: 5px; right: 5px; background-color: rgba(0,0,0,0.7); color: white; padding: 2px 5px; font-size: 0.8em; border-radius: 3px;">${Math.round(videoDurationToDisplay)}s</div>` : ''; if (firstVideoUrlForCard) { const posterAttribute = previewUrl ? `poster="${previewUrl}"` : ''; mediaHtml = ` <div class="post-card__image-container" style="position: relative; text-align: center; margin-bottom: 5px; background-color: #000;"> <video class="lazy-load-video" controls preload="none" width="100%" style="max-height: 265px; display: block;" ${posterAttribute}> <source data-src="${firstVideoUrlForCard}" type="video/mp4"> Your browser does not support the video tag. </video> ${durationDisplay} </div>`; } else if (previewUrl) { mediaHtml = ` <div class="post-card__image-container" style="text-align: center; margin-bottom: 5px;"> <img class="post-card__image" src="${previewUrl}" alt="Preview for post ${post.id}" style="max-width: 100%; height: auto; max-height: 200px; object-fit: contain; border: 1px solid #444;"> </div>`; } else { mediaHtml = ` <div class="post-card__image-container" style="text-align: center; margin-bottom: 5px; height: 100px; display: flex; align-items: center; justify-content: center; background-color: #333; color: #aaa; font-size:0.9em; border: 1px solid #444;"> No Preview Available </div>`; } const postLink = `/${post.service}/user/${post.user}/post/${post.id}`; return ` <article class="post-card post-card--preview" data-id="${post.id}" data-service="${post.service}" data-user="${post.user}"> <a class="fancy-link fancy-link--kemono" href="${postLink}" target="_blank" rel="noopener noreferrer"> <header class="post-card__header" title="${displayTitle.replace(/"/g, '"')}">${displayTitle}</header> ${mediaHtml} <footer class="post-card__footer"> <div> <div> <time class="timestamp" datetime="${dateTimeAttr}">${formattedDate}</time> <div>${attachmentCount > 0 ? attachmentText : 'No Attachments'}</div> </div> </div> </footer> </a> </article>`; } async function handleFilter() { showStatus(''); filterButton.textContent = 'Filter Videos'; styleButton(filterButton, true); filterButton.disabled = true; styleButton(copyUrlsButton, true); copyUrlsButton.disabled = true; allFoundVideoUrls = []; setupVideoIntersectionObserver(); const pageRangeStr = pageRangeInput.value; const pagesToFetch = parsePageRange(pageRangeStr); if (!pagesToFetch) { styleButton(filterButton, false); filterButton.disabled = false; return; } const durationRangeStr = durationRangeInput.value; const parsedDurationFilter = parseDurationRange(durationRangeStr); if (parsedDurationFilter && parsedDurationFilter.error) { // Error in parsing styleButton(filterButton, false); filterButton.disabled = false; return; } // parsedDurationFilter will be null if input is empty, which is fine. const context = determinePageContext(); if (!context) { styleButton(filterButton, false); filterButton.disabled = false; return; } const postListContainer = document.querySelector('.card-list__items'); if (!postListContainer) { showStatus('Error: Post container (.card-list__items) not found.', 'error'); styleButton(filterButton, false); filterButton.disabled = false; return; } postListContainer.style.setProperty('--card-size', '350px'); postListContainer.innerHTML = ''; document.querySelectorAll('.paginator menu, .content > menu.Paginator').forEach(menu => menu.style.display = 'none'); const paginatorInfo = document.querySelector('.paginator > small, .content > div > small.subtle-text'); if (paginatorInfo) paginatorInfo.textContent = `Filtering posts...`; let totalVideoPostsFound = 0; let postsProcessedCounter = 0; const durationCheckerPool = new DurationCheckerPool(MAX_CONCURRENT_METADATA_REQUESTS); for (let i = 0; i < pagesToFetch.length; i++) { const pageNum = pagesToFetch[i]; const offset = (pageNum - 1) * POSTS_PER_PAGE; const apiUrl = buildApiUrl(context, offset); if (!apiUrl) { showStatus('Error: Could not build API URL.', 'error'); break; } const originalFilterButtonText = filterButton.textContent; filterButton.textContent = `Page ${i + 1}/${pagesToFetch.length}...`; const pageStatus = `Fetching page ${pageNum} (offset ${offset})...`; showStatus(pageStatus, 'info'); try { const apiResponse = await fetchData(apiUrl); showStatus(pageStatus, 'info'); let posts = [], resultPreviews = null; if (Array.isArray(apiResponse)) posts = apiResponse; else if (apiResponse.results && Array.isArray(apiResponse.results)) { posts = apiResponse.results; resultPreviews = apiResponse.result_previews; } else if (apiResponse.posts && Array.isArray(apiResponse.posts)) { posts = apiResponse.posts; resultPreviews = apiResponse.result_previews; } else { showStatus(`Warning: Unexpected API response for page ${pageNum}.`, 'error'); console.warn("Unexpected API response:", apiResponse); continue; } for (let postIndex = 0; postIndex < posts.length; postIndex++) { postsProcessedCounter++; filterButton.textContent = `Page ${i + 1}/${pagesToFetch.length} (Post ${postsProcessedCounter})...`; const post = posts[postIndex]; const postVideoUrlsToCheck = getAllVideoUrlsFromPost(post); let isVideoPostOverall = postVideoUrlsToCheck.length > 0; let durationToDisplayOnCard = null; let postMatchesDurationFilter = true; // Assume true if no duration filter or if it's a non-video post if (isVideoPostOverall) { if (parsedDurationFilter) { // Only check duration if a filter is set postMatchesDurationFilter = false; // Must prove it matches for (const videoUrl of postVideoUrlsToCheck) { try { const currentVideoDuration = await durationCheckerPool.add(videoUrl); if (durationToDisplayOnCard === null) { durationToDisplayOnCard = currentVideoDuration; } if (currentVideoDuration >= parsedDurationFilter.min && currentVideoDuration <= parsedDurationFilter.max) { postMatchesDurationFilter = true; durationToDisplayOnCard = currentVideoDuration; break; } } catch (err) { const videoFileNameForError = videoUrl.split('/').pop(); console.warn(`Could not get duration for ${videoFileNameForError}:`, err.message); } } } // If parsedDurationFilter is null, postMatchesDurationFilter remains true, // and durationToDisplayOnCard remains null. No duration check is performed. if (postMatchesDurationFilter) { totalVideoPostsFound++; allFoundVideoUrls.push(...postVideoUrlsToCheck); const apiPreviewEntry = resultPreviews ? (Array.isArray(resultPreviews) ? resultPreviews[postIndex] : resultPreviews[post.id]) : null; const previewUrl = getPostPreviewUrl(post, apiPreviewEntry); const cardHtml = createPostCardHtml(post, previewUrl, durationToDisplayOnCard); postListContainer.insertAdjacentHTML('beforeend', cardHtml); const newlyAddedCard = postListContainer.lastElementChild; if (newlyAddedCard) { const videoEl = newlyAddedCard.querySelector('video.lazy-load-video'); if (videoEl && videoEl.querySelector('source[data-src]') && videoIntersectionObserver) { videoIntersectionObserver.observe(videoEl); } } } } } if (paginatorInfo) paginatorInfo.textContent = `Showing ${totalVideoPostsFound} video posts. Processed ${postsProcessedCounter} posts total.`; } catch (error) { showStatus(`Error on page ${pageNum}: ${error}`, 'error'); console.error("Filter error:", error); } finally { filterButton.textContent = originalFilterButtonText; } if (i < pagesToFetch.length - 1) await new Promise(resolve => setTimeout(resolve, API_DELAY)); } filterButton.textContent = 'Filter Videos'; styleButton(filterButton, false); filterButton.disabled = false; if (totalVideoPostsFound > 0) { showStatus(`Filter complete. Found ${totalVideoPostsFound} video posts. Processed ${postsProcessedCounter} posts.`, 'success'); styleButton(copyUrlsButton, false); copyUrlsButton.disabled = false; } else { const durationFilterActive = parsedDurationFilter ? " with current duration filter" : ""; showStatus(`Filter complete. No matching video posts found${durationFilterActive}. Processed ${postsProcessedCounter} posts.`, 'info'); styleButton(copyUrlsButton, true); copyUrlsButton.disabled = true; } } function handleCopyUrls() { if (allFoundVideoUrls.length === 0) { showStatus("No video URLs to copy.", 'error'); return; } const uniqueUrls = [...new Set(allFoundVideoUrls)]; GM_setClipboard(uniqueUrls.join('\n')); const originalText = copyUrlsButton.textContent; copyUrlsButton.textContent = `Copied ${uniqueUrls.length} URLs!`; showStatus(`Copied ${uniqueUrls.length} unique video URLs!`, 'success'); setTimeout(() => { copyUrlsButton.textContent = originalText; }, 3000); } filterButton.addEventListener('click', handleFilter); copyUrlsButton.addEventListener('click', handleCopyUrls); if (determinePageContext()) { showStatus("Video filter ready. Enter page & duration, then filter.", 'info'); } else { showStatus("Page not recognized. Filter may not work as expected.", 'error'); } })();