Skip to content

Instantly share code, notes, and snippets.

@raisaputra
Last active April 3, 2026 18:47
Show Gist options
  • Select an option

  • Save raisaputra/6eee62d226af40160578a4775d5f43ed to your computer and use it in GitHub Desktop.

Select an option

Save raisaputra/6eee62d226af40160578a4775d5f43ed to your computer and use it in GitHub Desktop.
Delete All Twitter Media Image or Video

Reference Script : https://gist.github.com/aymericbeaumet/d1d6799a1b765c3c8bc0b675b1a1547d

Make sure check and support his work in link above!

Don't forget follow me on twitter : https://twitter.com/astin_rai

Thank You

Warning!

If the image or video media is no longer visible and the number of images and videos are still visible under your X(Twitter) name after using this script you must wait at least 1 week or more to continue using this script. I don't know why the rest of media disappear forever from Media List but the total of media still showing below X(Twitter) Username. So, Do With Your Own Risk! (Update: Weird the Media appear again in list after 4 Week/1 Month)

//Make sure open your twitter Profile and go to Media
//Only Work On Desktop Mode!
(() => {
  const scrollToTheBottom = () => window.scrollTo(0, document.body.scrollHeight);
  const retry = {
      count: 0,
      limit: 3,
  };
  const addNewRetry = () => retry.count++;
  const retryLimitReached = () => retry.count === retry.limit;
  const getTwitterXUsername = document.querySelectorAll('div h2[role="heading"]')[2].innerText;
  let getTotalMedia = parseInt(document.querySelectorAll('div h2[role="heading"]')[2].offsetParent.querySelectorAll('div')[4].innerText.split(" ")[0]);

  //Time For Sleep To Next Batch
  const minSleepToNextBatch = 20; //Minimal Sleep Shouldn't Less Than 20 To Avoid Delete Limit
  const maxSleepToNextBatch = 60; //Maximal Sleep Value More Greater More Better

  //Language
  //Change The Variable Value By Your Language And Must Be In Sentence Case
  //You Can Find It By Using Inspect Tool In Chrome/Firefox Or You Can Change Your X/Twitter Display Langauge To English
  const moreLanguage = 'More';
  const deleteLanguage = 'Delete';
  const viewMoreLanguage = 'View post';
  const closeLanguage = 'Close';

  //Example For Indonesian Language
  //const moreLanguage = 'Lainnya';
  //const deleteLanguage = 'Hapus';
  //const viewMoreLanguage = 'Lihat postingan';
  //const closeLanguage = 'Tutup';

  console.log(`Hello ${getTwitterXUsername}, Total Media In Your X/Twitter Is ${getTotalMedia} Photos & Videos!`);
  const sleep = ({ proggress,seconds }) =>
      new Promise((proceed) => {
        console.log(`${proggress}`);
        console.log(`WAITING FOR ${seconds} SECONDS...`);
        setTimeout(proceed, seconds * 1000);
  });
  
  const getRandomNumber = (min, max) => {
    const minCeiled = Math.ceil(min);
    const maxFloored = Math.floor(max);
    const resultRandom = Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled);
    return resultRandom;
  }

  const deleteImage = async () => {
    await sleep({ proggress: "Click Media" ,seconds: getRandomNumber(5,10) });
    await document.querySelectorAll("li[role='listitem']")[0].querySelector("a[role='link']").click();

    await sleep({ proggress: "Check If Sidebar Available" ,seconds: getRandomNumber(5,10) });
    if(document.querySelector(`button[aria-label='${viewMoreLanguage}'`)) {
      document.querySelector(`button[aria-label='${viewMoreLanguage}'`).click();
    }

    await sleep({ proggress: "Click Button More" ,seconds: getRandomNumber(5,10) });
    const moreMenu = document.querySelectorAll(`button[aria-label='${moreLanguage}']`);
    for (const menu of moreMenu) {
      menu.click();
    }

    await sleep({ proggress: "Click Delete" ,seconds: getRandomNumber(5,10) });
    Array.prototype.slice.call(document.querySelectorAll("div[role='menuitem']"))
      .filter(function (el) {
        return el.textContent === `${deleteLanguage}`
      })[0].click();
    
    await sleep({ proggress: "Confirm Delete" ,seconds: getRandomNumber(5,10) });
    await document.querySelectorAll('button[data-testid="confirmationSheetConfirm"]')[0].click();

    await sleep({ proggress: "Force Close Media Dialog" ,seconds: getRandomNumber(5,10) });
    if(document.querySelector("div[role='dialog'")) {
        await document.querySelector("div[role='dialog'").querySelector(`button[aria-label='${closeLanguage}']`).click();
    }

    await sleep({ proggress: "Remove Deleted Media In List Item" ,seconds: getRandomNumber(5,10) });
    document.querySelector("li[role='listitem']").remove();
  }

  const nextBatch = async () => {
    await sleep({ proggress: "Sleeping.....", seconds: getRandomNumber(minSleepToNextBatch,maxSleepToNextBatch) });
    await sleep({ proggress: "Checking Media...", seconds: getRandomNumber(5,10) });

    const itemImageList = document.querySelectorAll("section[role='region']")[0].querySelectorAll("li[role='listitem']");
    const itemImageFound = itemImageList.length > 0;

    if (itemImageFound) {
      if(!itemImageList[0].innerHTML || !document.querySelector("li[role='listitem']").querySelector("a[role='link']")) {
        await sleep({ proggress: "Found Media Item With Problem!, Deleting Item From List......", seconds: 1 });
        document.querySelector("li[role='listitem']").remove()
      }
      await sleep({ proggress: "Media Found!", seconds: 1 });
      await deleteImage();
      getTotalMedia = getTotalMedia-1;
      console.log(`Total Media Left In Your X/Twitter Is ${getTotalMedia} Photos & Videos!`);
      await sleep({ proggress: "Proggress to next batch", seconds: getRandomNumber(5,10) });
      return nextBatch();
    } else {
      scrollToTheBottom();
      addNewRetry();
    }

    if (retryLimitReached()) {
      console.log(`NO MEDIA FOUND, SO I THINK WE'RE DONE`);
      console.log(`RELOAD PAGE AND RE-RUN SCRIPT IF ANY WERE MISSED`);
    } else {
      await sleep({ proggress: "Proggress to next batch", seconds: getRandomNumber(5,10) });
      return nextBatch();
    }
  };
  
nextBatch();
})();

Thanks to ckane for the Update fix

@almutwerner
Copy link
Copy Markdown

almutwerner commented Jan 21, 2025

await sleep({ proggress: "Click Button More" ,seconds: 2 });
      const moreMenu = document.querySelectorAll("button[aria-label='More']");

Here, in line 22 or sth is a tiny error. You also have to replace [aria-label='More'] for the appropriate word in your language (whatever shows up when you hower over the three dots)

Edit:

const deleteImage = async () => { await sleep({ proggress: "Click Media" ,seconds: 2 }); await document.querySelectorAll("li[role='listitem']")[0].querySelector("a[role='link']").click();

Additionally the script might fail, if twitter is buggy and there is a random empty slot in the grid that becomes the first item. In that case, change the index in line 19? from [0] to [1]

@ckane
Copy link
Copy Markdown

ckane commented Jan 28, 2025

await sleep({ proggress: "Click Button More" ,seconds: 2 });
      const moreMenu = document.querySelectorAll("button[aria-label='More']");

Here, in line 22 or sth is a tiny error. You also have to replace [aria-label='More'] for the appropriate word in your language (whatever shows up when you hower over the three dots)

Edit:

const deleteImage = async () => { await sleep({ proggress: "Click Media" ,seconds: 2 }); await document.querySelectorAll("li[role='listitem']")[0].querySelector("a[role='link']").click();

Additionally the script might fail, if twitter is buggy and there is a random empty slot in the grid that becomes the first item. In that case, change the index in line 19? from [0] to [1]

Yeah, I encountered this too - I wonder if Twitter engineers did that to try to stymie this kind of scripting

@raisaputra
Copy link
Copy Markdown
Author

await sleep({ proggress: "Click Button More" ,seconds: 2 });
      const moreMenu = document.querySelectorAll("button[aria-label='More']");

Here, in line 22 or sth is a tiny error. You also have to replace [aria-label='More'] for the appropriate word in your language (whatever shows up when you hower over the three dots)

Edit:

const deleteImage = async () => { await sleep({ proggress: "Click Media" ,seconds: 2 }); await document.querySelectorAll("li[role='listitem']")[0].querySelector("a[role='link']").click();

Additionally the script might fail, if twitter is buggy and there is a random empty slot in the grid that becomes the first item. In that case, change the index in line 19? from [0] to [1]

Is the <li> element still present in the random empty slot in list media?

@ckane
Copy link
Copy Markdown

ckane commented Jan 30, 2025

Yeah, but it doesn't have the inner <a> element. I did a bit of digging and I think it has to do with tweets with external-video embeds & retweets of the same - I found that the "empty" item could be in any or multiple of the slots, but never all three.

Here's my attempt to make the logic dynamic:

(() => {
    const scrollToTheBottom = () => window.scrollTo(0, document.body.scrollHeight);
    const retry = {
        count: 0,
        limit: 3,
    };
    const addNewRetry = () => retry.count++;
    const retryLimitReached = () => retry.count === retry.limit;
    const sleepToNextBatch = 10; //Should not be less than 10 seconds to avoid shadowban
    const sleep = ({ proggress,seconds }) =>
        new Promise((proceed) => {
          console.log(`${proggress}`);
          console.log(`WAITING FOR ${seconds} SECONDS...`);
          setTimeout(proceed, seconds * 1000);
    });
    
    const deleteImage = async () => {
      await sleep({ proggress: "Click Media" ,seconds: 2 });
      for(const i of [0, 1, 2]) {
        const ele = document.querySelectorAll("li[role='listitem']")[i].querySelector("a[role='link']");
        if (ele != null) {
            await ele.click();
            break;
        }
      }
  
      await sleep({ proggress: "Click Button More" ,seconds: 2 });
      const moreMenu = document.querySelectorAll("button[aria-label='More']");
      for (const menu of moreMenu) {
        menu.click();
      }
  
      await sleep({ proggress: "Click Delete" ,seconds: 2 });
      Array.prototype.slice.call(document.querySelectorAll("div[role='menuitem']"))
        .filter(function (el) {
          return el.textContent === 'Delete' //Change 'Delete' by your language like Delete in Indonesian is 'Hapus' and must be in Sentence Case
        })[0].click();
      
      await sleep({ proggress: "Confirm Delete" ,seconds: 2 });
      await document.querySelectorAll('button[data-testid="confirmationSheetConfirm"]')[0].click();

      await sleep({ proggress: "Force Close Media Dialog" ,seconds: 2 });
      if(document.querySelector("div[role='dialog'")) {
          await document.querySelector("div[role='dialog'").querySelector("button[aria-label='Close']").click();
      }
  
      await sleep({ proggress: "Remove Media In List Item" ,seconds: 2 });
      await document.querySelector("li[role='listitem']").remove();
    }

    const nextBatch = async () => {
      await sleep({ proggress: "Sleeping.....", seconds: sleepToNextBatch });
      await sleep({ proggress: "Checking Media...", seconds: 1 });

      const itemImageList = Array.from(document.querySelectorAll("section[role='region']")[0].querySelectorAll("li[role='listitem']"));
      const itemImageFound = itemImageList.length > 0;
  
      if (itemImageFound) {
        await sleep({ proggress: "Media Found!", seconds: 1 });
        await deleteImage();
        await sleep({ proggress: "Proggress to next batch", seconds: 2 });
        return nextBatch();
      } else {
        scrollToTheBottom();
        addNewRetry();
      }
  
      if (retryLimitReached()) {
        console.log(`NO MEDIA FOUND, SO I THINK WE'RE DONE`);
        console.log(`RELOAD PAGE AND RE-RUN SCRIPT IF ANY WERE MISSED`);
      } else {
        await sleep({ proggress: "Proggress to next batch", seconds: 2 });
        return nextBatch();
      }
    };
    
  nextBatch();
})();

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