Skip to content

Instantly share code, notes, and snippets.

@syareez
Created January 6, 2018 11:54
Show Gist options
  • Select an option

  • Save syareez/a3ccfd9ce25f60442c8814d6db4f1280 to your computer and use it in GitHub Desktop.

Select an option

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.
@wesleyesaf

wesleyesaf commented Jul 30, 2026 •

Copy link
Copy Markdown

Atualizado e funcionando

(async () => {
"use strict";

/*

if (window.redditSavedCleanupRunning) {
console.warn("⚠️ O script já está em execução.");
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.");
}

}
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment