Skip to content

Instantly share code, notes, and snippets.

@xdegeneratex
Last active February 21, 2026 05:10
Show Gist options
  • Select an option

  • Save xdegeneratex/6633074876935306eedacfe9932363a8 to your computer and use it in GitHub Desktop.

Select an option

Save xdegeneratex/6633074876935306eedacfe9932363a8 to your computer and use it in GitHub Desktop.
Coomer / Kemono Video Filter v3
// ==UserScript==
// @name Tampermonkey Video Filter v3
// @namespace Simpcity Scripts
// @match https://*.coomer.party/*/user/*
// @match https://*.kemono.party/*/user/*
// @match https://*.coomer.st/*/user/*
// @match https://*.coomer.su/*/user/*
// @match https://*.kemono.su/*/user/*
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @version 1.3
// @author harryangstrom, xdegeneratex
// @description Filters posts with videos using dynamic content detection and improved performance.
// ==/UserScript==
let error = null;
const videoExtensions = ['mp4', 'm4v', 'mov'];
const extensionsPattern = `${videoExtensions.join('|')}`;
const isVideoPath = (path, extensionsPattern) => new RegExp(`\.(${extensionsPattern})$`, 'i').test(path);
const filterVideos = async (updateCB, service, user, extensions) => {
const apiURL = `https://coomer.st/api/v1/${service}/user/${user}/posts`;
const posts = [];
let offset = 0;
let page = 1;
try {
while (true) {
const requestURL = `${apiURL}?o=${offset}`;
const response = await new Promise((resolve) => {
GM_xmlhttpRequest({
url: requestURL,
headers: { Accept: 'text/css' },
method: 'GET',
responseType: 'json',
onload: (response) => {
resolve(response.response);
},
onerror: (response) => {
resolve(null);
}
});
});
if (response) {
const postData = response;
if (!postData || !postData.length) {
break;
}
updateCB(page);
const isVideo = (path) => isVideoPath(path, extensionsPattern);
posts.push(
...postData.map(post => {
const hasVideo = post.file && (isVideo(post.file.name) || isVideo(post.file.path)) || post.attachments.some(attachment => isVideo(attachment.name) || isVideo(attachment.path));
return {
...post,
hasVideo,
};
}).filter(post => post.hasVideo)
);
} else if (response.status === 503) {
error = '[WARN] 503 Service unavailable.';
break;
} else {
error = '[WARN] Call to API failed. API or network might be unvailable.';
break;
}
page++;
offset +=50;
// Wait 2 seconds before making the next API call.
await new Promise((resolve) => setTimeout(() => resolve(true), 2000));
}
} catch (e) {
error = '[WARN] Call to API failed. API or network might be unvailable.';
}
return posts;
}
const getFilterButton = () => {
const filterBtn = document.createElement("button");
filterBtn.textContent = "Filter Videos";
filterBtn.style.margin = "10px";
filterBtn.style.padding = "5px 10px";
filterBtn.style.background = "#007bff";
filterBtn.style.color = "#fff";
filterBtn.style.border = "none";
filterBtn.style.cursor = "pointer";
filterBtn.style.position = "fixed"; // Ensure it's always visible
filterBtn.style.top = "10px";
filterBtn.style.right = "10px";
filterBtn.style.zIndex = "1000";
return filterBtn;
}
(async () => {
const posts = [];
const btnFilter = getFilterButton();
document.body.appendChild(btnFilter);
btnFilter.addEventListener('click', async () => {
if (btnFilter.textContent.includes('Copied')) {
return;
}
if (btnFilter.textContent.includes('Copy')) {
const content = posts.flatMap(post => {
const paths = [];
paths.push((post.file && (isVideoPath(post.file.name, extensionsPattern) || isVideoPath(post.file.path, extensionsPattern))) ? post.file.path : null);
paths.push(...(post.attachments || []).map(attachment => (isVideoPath(attachment.name, extensionsPattern) || isVideoPath(attachment.path, extensionsPattern)) ? attachment.path : null));
return paths.filter(path => path !== null);
})
.filter(path => path !== null)
.map(path => `https://coomer.st/data${path}`)
.join('\n');
console.log('[INFO] Copying the following URLs to clipboard:');
console.log(content);
GM_setClipboard(content);
const textContent = btnFilter.textContent;
btnFilter.textContent = `Copied urls for ${posts.length} video posts to clipboard`;
setTimeout(() => {
btnFilter.textContent = textContent;
}, 3000);
return;
}
btnFilter.textContent = 'Filtering...';
const segments = document.location.pathname.replace(/\?.*/, '').split('/').filter(segment => segment.trim() !== '');
if (segments.length < 3) {
btnFilter.textContent = 'Unable to Filter';
return;
}
const service = segments[0];
const username = segments[2];
let totalPages = 1;
const lastPageNavEl = document.querySelector('menu > a[aria-current="page"]:last-child');
if (lastPageNavEl) {
const matches = /(?<=\?o=)\d+$/.exec(lastPageNavEl.href);
if (matches && matches.length) {
totalPages = (Number(matches[0]) + 50) / 50;
}
}
const filteredPosts = await filterVideos((page) => {
btnFilter.textContent = `Filtering Page ${page} / ${totalPages}...`;
}, service, username, videoExtensions);
console.log(error ? `[INFO] Filter complete but with an error: ${error}` : '[INFO] Filter completed successfully.');
const totalFiltered = filteredPosts.length;
if (totalPages > 1) {
document.querySelectorAll('.paginator > small').forEach(el => {
el.innerHTML = `Showing ${totalFiltered} video posts`;
});
document.querySelectorAll('menu').forEach(menu => menu.remove());
}
btnFilter.textContent = `Copy URLs for ${totalFiltered} videos`;
const postsContainerEl = document.querySelector('.card-list__items');
if (!postsContainerEl) {
return;
}
if (posts.length) {
postsContainerEl.innerHTML = '';
}
for (const post of filteredPosts) {
const isImage = (path) => /\.(jpg|jpeg|png)$/i.test(path);
const hasAttachments = post.attachments && post.attachments.length;
const attachementsDiv = hasAttachments ? `<div>${post.attachments.length} Attachments</div>` : '<div>No Attachments</div>';
const postHTML = `
<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="/${post.service}/user/${post.user}/post/${post.id}"
>
<header class="post-card__header">${post.title}</header>
<footer class="post-card__footer">
<div>
<div>
<time class="timestamp" datetime="${post.published}">${post.published}</time>
${attachementsDiv}
</div>
</div>
</footer>
</a>
</article>
`;
posts.push({ ...post, html: postHTML })
}
postsContainerEl.innerHTML = posts.map(post => post.html).join('');
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment