Skip to content

Instantly share code, notes, and snippets.

@Tarrgon
Last active November 30, 2023 04:09
Show Gist options
  • Select an option

  • Save Tarrgon/54bb87df7e502ada1a8bfef75a2a439b to your computer and use it in GitHub Desktop.

Select an option

Save Tarrgon/54bb87df7e502ada1a8bfef75a2a439b to your computer and use it in GitHub Desktop.
e621 Mass Unfavorite

This tool will allow you to mass unfavorite posts on e621. You can use it to unfavorite all posts, or ones that pass a filter.

The filter syntax is much like my advanced search syntax, however it does not have any metatag support. It does, however, have full tag grouping, OR and NOT capability. Read the above post for more details and examples.

The program must be ran with node.js. I recommend running it dry first if you are using the filter to make sure nothing looks wrong and that the filter worked properly. After that just let it run. It will do one unfavorite every 800ms if the post passes the filter.

To use this you will need to download node.js and then run the program.

const SHOW_SUCCESS_LOGS = false
const USER_AGENT = "Mass Unfavorite Script/1.0"
const fs = require("fs")
const readline = require("readline")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
let AUTH = ""
function prompt(prompt) {
return new Promise(resolve => {
rl.question(prompt, (response) => {
resolve(response)
})
})
}
async function getFavorites() {
try {
let res = await fetch("https://e621.net/favorites.json", {
headers: {
"User-Agent": USER_AGENT,
Authorization: AUTH
}
})
if (res.ok) {
return (await res.json()).posts.map(p => {
p.tags = Object.values(p.tags).flat()
return p
})
} else {
console.error(`Fetching favorites failed (${res.status})`)
console.error(await res.text())
return null
}
} catch (e) {
console.error(`Fetching favorites failed`)
console.error(e)
return null
}
}
async function unfavorite(postId) {
try {
let res = await fetch(`https://e621.net/favorites/${postId}.json`, {
method: "DELETE",
headers: {
"User-Agent": USER_AGENT,
Authorization: AUTH
}
})
if (!res.ok) {
console.error(`Failed to unfavorite ${postId} (https://e621.net/posts/${postId}) (${res.status})`)
console.error(await res.text())
} else if (SHOW_SUCCESS_LOGS) {
console.log(`Unfavorited ${postId} (https://e621.net/posts/${postId})`)
}
} catch (e) {
console.error(`Failed to unfavorite ${postId} (https://e621.net/posts/${postId})`)
console.error(e)
}
}
function wait(ms) {
return new Promise(r => setTimeout(r, ms))
}
async function run() {
let dry = (await prompt("DRY RUN (no operation, show all) (Y/N): ")).toLowerCase() == "y"
let username = await prompt("Enter e621 username (Case-Sensitive): ")
let apiKey = await prompt("Enter e621 api key: ")
let filter = await prompt("Filter (leave blank to unfavorite all, only those that pass the filter will be unfavorited. Supports advanced tag grouping, OR and NOT syntax. Does not support metatags): ")
let groups = getGroups(filter.trim())
if (!groups) {
console.error("Error parsing filter.")
return
}
let builtQuery = groups === true ? null : buildQueryFromGroup(groups)
AUTH = `Basic ${btoa(`${username}:${apiKey}`)}`
let favorites = await getFavorites()
let changes = { unfavorited: [], noChange: [] }
for (let favorite of favorites) {
if (builtQuery && !passesGroup(favorite.tags, builtQuery)) {
changes.noChange.push({ id: favorite.id, tags: favorite.tags, operation: "NO CHANGE" })
continue
}
if (!dry) {
await unfavorite(favorite.id)
await wait(800)
}
changes.unfavorited.push({ id: favorite.id, tags: favorite.tags, operation: "UNFAVORITE" })
}
if (dry) console.log("DRY RUN, NO OPERATIONS DONE.")
console.log(`Total unfavorited: ${changes.unfavorited.length}`)
console.log(`Total unchanged: ${changes.noChange.length}`)
let showAllChanges = dry || (await prompt("Show all? (Y/N): ")).toLowerCase() == "y"
if (showAllChanges) {
fs.writeFileSync("temp.json", JSON.stringify(changes, null, 4))
const start = (process.platform == "darwin" ? "open" : process.platform == "win32" ? "start" : "xdg-open")
require("child_process").exec(start + " " + `${__dirname}/temp.json`)
await wait(1000)
fs.unlinkSync("temp.json")
}
rl.close()
}
const TOKENS_TO_SKIP = ["~", "-"]
const MODIFIERS = {
NONE: 0,
OR: 1
}
function passesGroup(tags, curGroup) {
let { must, mustNot, should } = curGroup
for (let token of must) {
if (typeof (token) == "string") {
if (!tags.includes(token)) return false
} else {
if (!passesGroup(tags, token)) return false
}
}
let shouldPassed = should.length == 0
for (let token of should) {
if (typeof (token) == "string") {
if (tags.includes(token)) {
shouldPassed = true
break
}
} else {
if (passesGroup(tags, token)) {
shouldPassed = true
break
}
}
}
if (!shouldPassed) return false
for (let token of mustNot) {
if (typeof (token) == "string") {
if (tags.includes(token)) return false
} else {
if (passesGroup(tags, token)) return false
}
}
return true
}
function buildQueryFromGroup(group, curQuery = { must: [], should: [], mustNot: [] }) {
let modifier = MODIFIERS.NONE
for (let i = 0; i < group.tokens.length; i++) {
let token = group.tokens[i]
if (TOKENS_TO_SKIP.includes(token) || token == "") continue
let previousToken = i > 0 ? group.tokens[i - 1] : null
let previousNegate = previousToken == "-"
let nextToken = i < group.tokens.length - 1 ? group.tokens[i + 1] : null
if (nextToken == "~") modifier = MODIFIERS.OR
if (!token.startsWith("__")) {
if (modifier == MODIFIERS.NONE) {
if (!previousNegate) {
curQuery.must.push(token)
} else {
curQuery.mustNot.push(token)
}
} else if (modifier == MODIFIERS.OR) {
if (!previousNegate) {
curQuery.should.push(token)
} else {
curQuery.should.push({
mustNot: token
})
}
}
} else {
if (token.startsWith("__")) {
let nextGroup = group.groups[parseInt(token.slice(2))]
let query = { must: [], should: [], mustNot: [] }
buildQueryFromGroup(nextGroup, query)
if (modifier == MODIFIERS.NONE) {
if (!previousNegate) curQuery.must.push(query)
else curQuery.mustNot.push(query)
} else if (modifier == MODIFIERS.OR) {
if (!previousNegate) curQuery.should.push(query)
else {
curQuery.should.push({
mustNot: query
})
}
}
}
}
if (modifier == MODIFIERS.OR && nextToken != "~") modifier = MODIFIERS.NONE
}
return curQuery
}
function getGroups(tags) {
if (tags.length == 0) return true
let tokenizer = new Tokenizer(tags)
let currentGroupIndex = []
let group = { tokens: [], groups: [] }
for (let token of tokenizer) {
let curGroup = group
for (let group of currentGroupIndex) {
curGroup = curGroup.groups[group]
}
if (token == "(") {
currentGroupIndex.push(curGroup.groups.length)
curGroup.groups.push({ tokens: [], groups: [] })
curGroup.tokens.push(`__${curGroup.groups.length - 1}`)
} else if (token == ")") {
currentGroupIndex.splice(currentGroupIndex.length - 1, 1)
} else {
curGroup.tokens.push(token.toLowerCase())
}
}
if (currentGroupIndex.length != 0) {
return false
}
return group
}
class Tokenizer {
constructor(raw) {
this.raw = raw
this.split = raw.trim().replace(/\s+/g, " ").replace("\n", " ").split("")
this.done = false
this.index = 0
}
*[Symbol.iterator]() {
while (!this.done) {
yield this.consume()
}
}
peek() {
let token = ""
for (let i = this.index; i < this.split.length; i++) {
let t = this.split[i]
if (t == " ") {
return token
} else {
token += t
}
}
}
consume() {
let token = ""
for (let i = this.index; i < this.split.length; i++) {
let t = this.split[i]
if (t == " ") {
this.index = i + 1
this.done = this.index >= this.split.length
return token
} else if ((t == "-") && token.length == 0) {
this.index = i + 1
this.done = this.index >= this.split.length
return t
} else {
token += t
}
}
this.index = this.split.length
this.done = true
return token
}
}
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment