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

Copy link
Copy Markdown
Array.from(document.querySelectorAll("button")).forEach(
  (el) => {
    if (el.innerText === "unsave") el.click()
  })

Without jquery and reload, just to delete whatever you see on the "saved" page.

This still works as of 4/2023; only edit is the button text should be capitalized.

if (el.innerText === "Unsave") el.click()

I just had to refresh the page and rerun the function a couple times since Reddit does dynamic loading as you scroll

@alwei1

alwei1 commented Jun 17, 2023

Copy link
Copy Markdown

You should change that to "unsave", since that is the word it's used now for deleting saved posts. I pasted the command with the change: $("a").filter(function(index){return $(this).text()==="unsave"}).click();setTimeout(function(){location.reload();},500);

This still works as of 5/2023 on old reddit!

@gitatmax

Copy link
Copy Markdown

@alwei1 Indeed it does—thanks for confirming!

@brijazz

brijazz commented Jun 17, 2024 •

Copy link
Copy Markdown

Would there be any way to omit saves from a particular sub? I'd love to be able to clear out all of my saves except /r/dogpictures

@TasseDeCafe

Copy link
Copy Markdown

If you're using old reddit, you can paste this in your console once you're in the saved tab:

// Function to unsave Reddit items with randomized delays and batch pauses
function unsaveRedditItems() {
    // Find all unsave links
    const unsaveLinks = $("a").filter(function(index) {
        return $(this).text() === "unsave";
    });
    
    // Counter for current item
    let currentItem = 0;
    
    // Batch size configuration
    const BATCH_SIZE = 50;
    const BATCH_BREAK_TIME = 60000; // 60 second break between batches
    
    // Function to get random delay between 2-5 seconds (2000-5000ms)
    function getRandomDelay() {
        return Math.floor(Math.random() * (5000 - 2000 + 1)) + 2000;
    }
    
    // Function to process one item
    function processNextItem() {
        if (currentItem < unsaveLinks.length) {
            // Click the current unsave link
            $(unsaveLinks[currentItem]).click();
            
            // Get random delay for next action
            const nextDelay = getRandomDelay();
            
            // Check if we need a batch break
            const isEndOfBatch = (currentItem + 1) % BATCH_SIZE === 0;
            const actualDelay = isEndOfBatch ? BATCH_BREAK_TIME : nextDelay;
            
            // Log progress with appropriate message
            if (isEndOfBatch && currentItem + 1 < unsaveLinks.length) {
                console.log(
                    `Completed batch of ${BATCH_SIZE}. ` +
                    `Taking a ${BATCH_BREAK_TIME/1000} second break...`
                );
            } else {
                console.log(
                    `Unsaved item ${currentItem + 1} of ${unsaveLinks.length}. ` +
                    `Next action in ${(actualDelay/1000).toFixed(1)}s`
                );
            }
            
            // Move to next item after delay
            currentItem++;
            setTimeout(processNextItem, actualDelay);
        } else {
            // Reload page when done
            console.log("Finished unsaving items on this page");
            setTimeout(() => location.reload(), 3000);
        }
    }
    
    // Start processing if there are items to unsave
    if (unsaveLinks.length > 0) {
        console.log(`Found ${unsaveLinks.length} items to unsave`);
        // Add initial random delay before starting
        const initialDelay = getRandomDelay();
        console.log(`Starting in ${(initialDelay/1000).toFixed(1)}s...`);
        setTimeout(processNextItem, initialDelay);
    } else {
        console.log("No items found to unsave");
    }
}

// Run the function
unsaveRedditItems();

You might have to make the breaks between batches longer to avoid triggering the rate limiter. The random range between each action might also not be necessary. It did the job for me.

@rlewkowicz

Copy link
Copy Markdown

https://github.com/j0be/PowerDeleteSuite

So this still has lots of comments. This repo above is what you want. It's great. You'll see.

@iamxeeshankhan

iamxeeshankhan commented Dec 24, 2025 •

Copy link
Copy Markdown

I tested many existing methods. Most do not work, likely because they are outdated. Some rely on old.reddit.com, which is throttled and frequently returns 429 Too Many Requests errors. Because of this, I wrote my own script that is fast and reliable.

I tested it on over 900 posts and it performed very well. At the moment, it does not support comments or text-only posts and cannot unsave them.

(Not self-promotion. Just a community service.)

The code is available in this repository: https://github.com/iamxeeshankhan/reddit-posts-actions

@Zakaboy26

Zakaboy26 commented Feb 9, 2026 •

Copy link
Copy Markdown

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

@pavlansky

Copy link
Copy Markdown

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);
    }
})();

@Siddheshkr

Copy link
Copy Markdown

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 👍🏻

@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