-
-
Save syareez/a3ccfd9ce25f60442c8814d6db4f1280 to your computer and use it in GitHub Desktop.
| Open up browser console, for Chrome, hit F12 and copy-paste and enter this | |
| $("a").filter(function(index){return $(this).text()==="unsave"}).click();setTimeout(function(){location.reload();},500); | |
| Repeat until all items are unsaved. |
this is working
(async () => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const querySelectorAllDeep = (selector, root = document) => {
let nodes = Array.from(root.querySelectorAll(selector));
for (const child of root.querySelectorAll('*')) {
if (child.shadowRoot) {
nodes = nodes.concat(querySelectorAllDeep(selector, child.shadowRoot));
}
}
return nodes;
};
const doScrollBatch = async () => {
console.log("⏬ Scrolling 5x to load more content...");
for (let i = 0; i < 5; i++) {
window.scrollBy(0, 2000);
await sleep(2000);
}
console.log("✅ Scrolling done, resuming deletions.");
};
let totalDeleted = parseInt(localStorage.getItem("unsave_total") || "0");
let batchCount = 0;
console.log(`🚀 Starting script. Total deleted so far: ${totalDeleted}`);
await doScrollBatch();
while (true) {
const allSpans = querySelectorAllDeep('span');
const unsaveBtn = allSpans.find(el => el.innerText?.trim() === 'Remove from saved');
if (unsaveBtn) {
unsaveBtn.click();
totalDeleted++;
batchCount++;
localStorage.setItem("unsave_total", totalDeleted);
console.log(`🗑️ Removed: ${totalDeleted} total (Batch: ${batchCount}/15)`);
await sleep(2500 + Math.random() * 500);
if (batchCount >= 15) {
batchCount = 0;
await doScrollBatch();
}
continue;
}
const menuTrackers = querySelectorAllDeep('faceplate-tracker[noun="overflow_menu"]');
if (menuTrackers.length > 0) {
menuTrackers[0].click();
await sleep(1000);
continue;
}
console.log("⬇️ Nothing found, scrolling...");
window.scrollBy(0, 1500);
await sleep(3000);
}
})();
this is working
(async () => { const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const querySelectorAllDeep = (selector, root = document) => { let nodes = Array.from(root.querySelectorAll(selector)); for (const child of root.querySelectorAll('*')) { if (child.shadowRoot) { nodes = nodes.concat(querySelectorAllDeep(selector, child.shadowRoot)); } } return nodes; }; const doScrollBatch = async () => { console.log("⏬ Scrolling 5x to load more content..."); for (let i = 0; i < 5; i++) { window.scrollBy(0, 2000); await sleep(2000); } console.log("✅ Scrolling done, resuming deletions."); }; let totalDeleted = parseInt(localStorage.getItem("unsave_total") || "0"); let batchCount = 0; console.log(`🚀 Starting script. Total deleted so far: ${totalDeleted}`); await doScrollBatch(); while (true) { const allSpans = querySelectorAllDeep('span'); const unsaveBtn = allSpans.find(el => el.innerText?.trim() === 'Remove from saved'); if (unsaveBtn) { unsaveBtn.click(); totalDeleted++; batchCount++; localStorage.setItem("unsave_total", totalDeleted); console.log(`🗑️ Removed: ${totalDeleted} total (Batch: ${batchCount}/15)`); await sleep(2500 + Math.random() * 500); if (batchCount >= 15) { batchCount = 0; await doScrollBatch(); } continue; } const menuTrackers = querySelectorAllDeep('faceplate-tracker[noun="overflow_menu"]'); if (menuTrackers.length > 0) { menuTrackers[0].click(); await sleep(1000); continue; } console.log("⬇️ Nothing found, scrolling..."); window.scrollBy(0, 1500); await sleep(3000); } })();
thanks man 👍🏻
Atualizado e funcionando
(async () => {
"use strict";
/*
- Reddit Saved Posts Cleaner
- Use somente na página de itens salvos:
- https://www.reddit.com/user/SEU_USUARIO/saved/
- Para interromper:
- window.stopRedditSavedCleanup = true;
*/
if (window.redditSavedCleanupRunning) {
console.warn("
return;
}
window.redditSavedCleanupRunning = true;
window.stopRedditSavedCleanup = false;
const CONFIG = {
delayBetweenPostsMin: 1200,
delayBetweenPostsMax: 2500,
delayAfterMenuOpen: 500,
delayAfterScroll: 3000,
maximumEmptyScrolls: 5,
maximumErrorsInSequence: 15,
};
const stats = {
found: 0,
processed: 0,
removed: 0,
skipped: 0,
errors: 0,
batches: 0,
};
const processedPosts = new WeakSet();
function randomDelay(min, max) {
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
function normalizeText(value) {
return String(value || "")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
function isRemoveFromSavedText(text) {
const normalized = normalizeText(text);
const acceptedTexts = [
"remove from saved",
"remove from saved posts",
"unsave",
"remover dos salvos",
"remover de salvos",
"retirar dos salvos",
"remover dos itens salvos",
"remover dos posts salvos",
"deixar de salvar",
];
return acceptedTexts.some((accepted) =>
normalized.includes(normalizeText(accepted))
);
}
function isSaveText(text) {
const normalized = normalizeText(text);
const acceptedTexts = [
"save",
"save post",
"salvar",
"salvar post",
"adicionar aos salvos",
];
return acceptedTexts.some(
(accepted) => normalized === normalizeText(accepted)
);
}
/*
- Pesquisa recursivamente no DOM normal, Shadow DOM e slots.
*/
function deepQuerySelector(root, selector, visited = new Set()) {
if (!root || visited.has(root)) {
return null;
}
visited.add(root);
try {
if (typeof root.querySelector === "function") {
const directResult = root.querySelector(selector);
if (directResult) {
return directResult;
}
}
} catch {
// Ignora raízes que não permitem querySelector.
}
let children = [];
try {
if (root instanceof HTMLSlotElement) {
children.push(
...root.assignedElements({
flatten: true,
})
);
}
if (root.shadowRoot) {
children.push(root.shadowRoot);
}
if (root.children) {
children.push(...Array.from(root.children));
}
} catch {
// Ignora elementos inacessíveis.
}
for (const child of children) {
const result = deepQuerySelector(child, selector, visited);
if (result) {
return result;
}
}
return null;
}
function deepQuerySelectorAll(root, selector, visited = new Set()) {
if (!root || visited.has(root)) {
return [];
}
visited.add(root);
const results = [];
try {
if (typeof root.matches === "function" && root.matches(selector)) {
results.push(root);
}
if (typeof root.querySelectorAll === "function") {
results.push(...Array.from(root.querySelectorAll(selector)));
}
} catch {
// Ignora seletores incompatíveis com alguma raiz.
}
let children = [];
try {
if (root instanceof HTMLSlotElement) {
children.push(
...root.assignedElements({
flatten: true,
})
);
}
if (root.shadowRoot) {
children.push(root.shadowRoot);
}
if (root.children) {
children.push(...Array.from(root.children));
}
} catch {
// Ignora elementos inacessíveis.
}
for (const child of children) {
results.push(...deepQuerySelectorAll(child, selector, visited));
}
return [...new Set(results)];
}
function getPostIdentifier(article, index) {
const shredditPost =
article.matches?.("shreddit-post")
? article
: article.querySelector?.("shreddit-post");
return (
shredditPost?.getAttribute("id") ||
shredditPost?.getAttribute("post-id") ||
shredditPost?.getAttribute("permalink") ||
article.getAttribute?.("id") ||
`post-${index + 1}`
);
}
function getLoadedPosts() {
const selectors = [
"article",
"shreddit-post",
"faceplate-batch article",
];
let posts = [];
for (const selector of selectors) {
posts.push(...deepQuerySelectorAll(document, selector));
}
posts = [...new Set(posts)];
/*
* Se o resultado tiver article e shreddit-post do mesmo item,
* prefere o elemento article.
*/
const articles = posts.filter(
(element) => element.tagName?.toLowerCase() === "article"
);
if (articles.length > 0) {
return articles;
}
return posts.filter(
(element) => element.tagName?.toLowerCase() === "shreddit-post"
);
}
function findOverflowMenu(postElement) {
return (
deepQuerySelector(postElement, "shreddit-post-overflow-menu") ||
deepQuerySelector(postElement, '[slot="post-overflow-menu"]') ||
deepQuerySelector(postElement, '[aria-label*="more" i]') ||
deepQuerySelector(postElement, '[aria-label*="mais" i]')
);
}
function findOverflowButton(postElement, overflowMenu) {
const menuRoot = overflowMenu?.shadowRoot || overflowMenu;
return (
deepQuerySelector(menuRoot, "button") ||
deepQuerySelector(menuRoot, '[role="button"]') ||
deepQuerySelector(postElement, 'button[aria-label*="more" i]') ||
deepQuerySelector(postElement, 'button[aria-label*="mais" i]') ||
deepQuerySelector(postElement, '[aria-haspopup="menu"]')
);
}
function findSaveMenuItem(postElement) {
const exactSelectors = [
"li#post-overflow-save",
'[id="post-overflow-save"]',
'[data-testid="post-overflow-save"]',
'[action="save"]',
'[data-action="save"]',
];
for (const selector of exactSelectors) {
const result =
deepQuerySelector(postElement, selector) ||
deepQuerySelector(document, selector);
if (result) {
return result;
}
}
/*
* Busca alternativa pelo texto do item de menu.
*/
const candidates = deepQuerySelectorAll(
document,
[
'[role="menuitem"]',
"li",
"button",
"faceplate-tracker",
].join(",")
);
return (
candidates.find((candidate) => {
const text = candidate.textContent || "";
return isRemoveFromSavedText(text);
}) || null
);
}
function getClickableElement(element) {
if (!element) {
return null;
}
return (
deepQuerySelector(element, '[role="menuitem"]') ||
deepQuerySelector(element, "button") ||
deepQuerySelector(element, "a") ||
element
);
}
async function openPostMenu(postElement) {
const overflowMenu = findOverflowMenu(postElement);
if (!overflowMenu) {
throw new Error("Menu de opções não encontrado");
}
const overflowButton = findOverflowButton(postElement, overflowMenu);
if (!overflowButton) {
throw new Error("Botão do menu de opções não encontrado");
}
overflowButton.scrollIntoView({
block: "center",
behavior: "auto",
});
overflowButton.click();
await randomDelay(
CONFIG.delayAfterMenuOpen,
CONFIG.delayAfterMenuOpen + 400
);
}
async function closeOpenMenu() {
try {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "Escape",
code: "Escape",
keyCode: 27,
which: 27,
bubbles: true,
})
);
await randomDelay(150, 300);
} catch {
// Não é essencial.
}
}
async function removePostFromSaved(postElement, index, total) {
const postId = getPostIdentifier(postElement, index);
console.log(`🔎 Verificando ${index + 1}/${total}: ${postId}`);
await openPostMenu(postElement);
const saveMenuItem = findSaveMenuItem(postElement);
if (!saveMenuItem) {
await closeOpenMenu();
throw new Error("Opção de salvar/remover dos salvos não encontrada");
}
const menuText = saveMenuItem.textContent || "";
const normalizedMenuText = normalizeText(menuText);
if (isSaveText(menuText) && !isRemoveFromSavedText(menuText)) {
stats.skipped++;
console.log(
`⚪ O item aparentemente já não está salvo: ${postId}`,
normalizedMenuText
);
await closeOpenMenu();
return false;
}
/*
* Alguns idiomas ou versões do Reddit podem não ter texto detectável.
* Como estamos na página /saved/, o item post-overflow-save normalmente
* representa "remover dos salvos".
*/
const hasKnownRemoveText = isRemoveFromSavedText(menuText);
const hasExpectedId =
saveMenuItem.id === "post-overflow-save" ||
saveMenuItem.closest?.("#post-overflow-save");
if (!hasKnownRemoveText && !hasExpectedId) {
await closeOpenMenu();
throw new Error(
`Texto inesperado no botão de salvar: "${menuText.trim()}"`
);
}
const clickable = getClickableElement(saveMenuItem);
if (!clickable) {
await closeOpenMenu();
throw new Error("Elemento clicável não encontrado");
}
clickable.click();
stats.removed++;
stats.processed++;
console.log(
`✅ Removido ${stats.removed}: ${postId}`
);
await randomDelay(
CONFIG.delayBetweenPostsMin,
CONFIG.delayBetweenPostsMax
);
return true;
}
async function processCurrentBatch() {
const loadedPosts = getLoadedPosts();
const unprocessedPosts = loadedPosts.filter(
(post) => !processedPosts.has(post)
);
stats.found += unprocessedPosts.length;
if (unprocessedPosts.length === 0) {
return 0;
}
stats.batches++;
console.log(
`\n📦 Lote ${stats.batches}: ${unprocessedPosts.length} itens novos encontrados`
);
let errorsInSequence = 0;
for (let index = 0; index < unprocessedPosts.length; index++) {
if (window.stopRedditSavedCleanup) {
console.warn("⛔ Processo interrompido pelo usuário.");
return -1;
}
const postElement = unprocessedPosts[index];
processedPosts.add(postElement);
try {
await removePostFromSaved(
postElement,
index,
unprocessedPosts.length
);
errorsInSequence = 0;
} catch (error) {
stats.errors++;
errorsInSequence++;
console.warn(
`❌ Falha no item ${index + 1}/${unprocessedPosts.length}:`,
error?.message || error
);
await closeOpenMenu();
if (errorsInSequence >= CONFIG.maximumErrorsInSequence) {
throw new Error(
`${errorsInSequence} erros consecutivos. O Reddit provavelmente alterou a estrutura da página.`
);
}
}
}
return unprocessedPosts.length;
}
async function scrollToLoadMore() {
const previousHeight = document.documentElement.scrollHeight;
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior: "smooth",
});
await randomDelay(
CONFIG.delayAfterScroll,
CONFIG.delayAfterScroll + 1500
);
const currentHeight = document.documentElement.scrollHeight;
return currentHeight > previousHeight;
}
async function main() {
console.log("🚀 Iniciando remoção dos itens salvos...");
console.log(
"⛔ Para interromper, execute: window.stopRedditSavedCleanup = true"
);
if (!location.pathname.includes("/saved")) {
console.warn(
"⚠️ Você não parece estar na página de itens salvos."
);
console.warn(
"Abra: https://www.reddit.com/user/SEU_USUARIO/saved/"
);
}
let emptyScrolls = 0;
while (!window.stopRedditSavedCleanup) {
const batchResult = await processCurrentBatch();
if (batchResult === -1) {
break;
}
console.log("⬇️ Rolando para carregar mais itens...");
const pageHeightIncreased = await scrollToLoadMore();
const newlyLoadedPosts = getLoadedPosts().filter(
(post) => !processedPosts.has(post)
);
if (batchResult === 0 && newlyLoadedPosts.length === 0) {
emptyScrolls++;
} else if (!pageHeightIncreased && newlyLoadedPosts.length === 0) {
emptyScrolls++;
} else {
emptyScrolls = 0;
}
console.log(
`📊 Removidos: ${stats.removed} | Erros: ${stats.errors} | Tentativas vazias: ${emptyScrolls}/${CONFIG.maximumEmptyScrolls}`
);
if (emptyScrolls >= CONFIG.maximumEmptyScrolls) {
console.log(
"🏁 Nenhum outro item foi encontrado. Processo finalizado."
);
break;
}
}
}
try {
await main();
} catch (error) {
console.error(
"💥 O processo foi interrompido devido a um erro:",
error
);
} finally {
window.redditSavedCleanupRunning = false;
console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.log("📊 RESULTADO");
console.log(`Lotes: ${stats.batches}`);
console.log(`Itens encontrados: ${stats.found}`);
console.log(`Removidos com sucesso: ${stats.removed}`);
console.log(`Ignorados: ${stats.skipped}`);
console.log(`Erros: ${stats.errors}`);
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
if (window.stopRedditSavedCleanup) {
console.log("⛔ Processo interrompido.");
} else {
console.log("✅ Processo concluído.");
}
}
})();
Hey, everything here was outdated or required some manual stuff for me, so I made a quick userscript that works for old.reddit.com. Just copy-paste it into Tampermonkey/Greasemonkey and it handles unsaving posts automatically, it refreshes the page etc so you can just run it in the background.
Check it out here: https://github.com/Zakaboy26/auto-unsave-reddit