Skip to content

Instantly share code, notes, and snippets.

@Chooks22
Last active July 14, 2025 08:01
Show Gist options
  • Save Chooks22/6f28904bebe7f7cdc0a2a2b4cf6a6df1 to your computer and use it in GitHub Desktop.
Save Chooks22/6f28904bebe7f7cdc0a2a2b4cf6a6df1 to your computer and use it in GitHub Desktop.
Bulk Downloader for Kemono.Party
// ==UserScript==
// @name Kemono Toolkit
// @namespace http://kemono.su/
// @version 5.0.2-beta
// @author Chooks22 <[email protected]> (https://github.com/Choooks22)
// @description Adds features and QoL improvements to kemono.su.
// @icon https://www.google.com/s2/favicons?sz=64&domain=kemono.su
// @downloadURL https://gist.githubusercontent.com/Chooks22/6f28904bebe7f7cdc0a2a2b4cf6a6df1/raw
// @updateURL https://gist.githubusercontent.com/Chooks22/6f28904bebe7f7cdc0a2a2b4cf6a6df1/raw
// @match https://kemono.su/*
// @connect kemono.su
// @grant GM.xmlHttpRequest
// @grant GM_notification
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const e$1 = ({ base: e2 = "", routes: t2 = [], ...r2 } = {}) => ({ __proto__: new Proxy({}, { get: (r3, o2, a2, s2) => (r4, ...c2) => t2.push([o2.toUpperCase(), RegExp(`^${(s2 = (e2 + r4).replace(/\/+(\/|$)/g, "$1")).replace(/(\/?\.?):(\w+)\+/g, "($1(?<$2>*))").replace(/(\/?\.?):(\w+)/g, "($1(?<$2>[^$1/]+?))").replace(/\./g, "\\.").replace(/(\/?)\*/g, "($1.*)?")}/*$`), c2, s2]) && a2 }), routes: t2, ...r2, async fetch(e3, ...r3) {
let o2, a2, s2 = new URL(e3.url), c2 = e3.query = { __proto__: null };
for (let [e4, t3] of s2.searchParams) c2[e4] = c2[e4] ? [].concat(c2[e4], t3) : t3;
for (let [c3, n2, l2, i2] of t2) if ((c3 == e3.method || "ALL" == c3) && (a2 = s2.pathname.match(n2))) {
e3.params = a2.groups || {}, e3.route = i2;
for (let t3 of l2) if (null != (o2 = await t3(e3.proxy ?? e3, ...r3))) return o2;
}
} }), c = (e2) => {
e2.proxy = new Proxy(e2.proxy ?? e2, { get: (t2, r2) => {
var _a2, _b, _c;
return ((_b = (_a2 = t2[r2]) == null ? void 0 : _a2.bind) == null ? void 0 : _b.call(_a2, e2)) ?? t2[r2] ?? ((_c = t2 == null ? void 0 : t2.params) == null ? void 0 : _c[r2]);
} });
};
let push_state = history.pushState;
history.pushState = function pushState(...args) {
let res = push_state.apply(this, args);
dispatchEvent(new Event("pushstate"));
dispatchEvent(new Event("locationchange"));
return res;
};
let replace_state = history.replaceState;
history.replaceState = function replaceState(...args) {
let res = replace_state.apply(this, args);
dispatchEvent(new Event("replacestate"));
dispatchEvent(new Event("locationchange"));
return res;
};
addEventListener("popstate", () => {
dispatchEvent(new Event("locationchange"));
});
function onlocationchange(handler) {
handler();
addEventListener("locationchange", handler);
}
async function get(path) {
let res = await fetch(`/api/v1${path}`);
return res.json();
}
async function get_current_post() {
let body = await get(location.pathname);
let post = body.post;
let attachments = get_post_attachments(post);
return {
id: post.id,
user: post.user,
service: post.service,
title: post.title,
attachments
};
}
function get_post_attachments(post) {
let attachments = [];
if (post.file) {
attachments.push({
name: `thumbnail-${post.file.name}`,
path: post.file.path,
type: "thumbnail"
});
}
for (let i2 = 0; i2 < post.attachments.length; i2++) {
let attachment = post.attachments[i2];
attachments.push({
name: `${i2}-${attachment.name}`,
path: attachment.path,
type: "attachment"
});
}
return attachments;
}
"stream" in Blob.prototype || Object.defineProperty(Blob.prototype, "stream", { value() {
return new Response(this).body;
} }), "setBigUint64" in DataView.prototype || Object.defineProperty(DataView.prototype, "setBigUint64", { value(e2, n2, t2) {
const i2 = Number(0xffffffffn & n2), r2 = Number(n2 >> 32n);
this.setUint32(e2 + (t2 ? 0 : 4), i2, t2), this.setUint32(e2 + (t2 ? 4 : 0), r2, t2);
} });
var e = (e2) => new DataView(new ArrayBuffer(e2)), n = (e2) => new Uint8Array(e2.buffer || e2), t = (e2) => new TextEncoder().encode(String(e2)), i = (e2) => Math.min(4294967295, Number(e2)), r = (e2) => Math.min(65535, Number(e2));
function o(e2, i2, r2) {
void 0 === i2 || i2 instanceof Date || (i2 = new Date(i2));
const o2 = void 0 !== e2;
if (r2 || (r2 = o2 ? 436 : 509), e2 instanceof File) return { isFile: o2, t: i2 || new Date(e2.lastModified), bytes: e2.stream(), mode: r2 };
if (e2 instanceof Response) return { isFile: o2, t: i2 || new Date(e2.headers.get("Last-Modified") || Date.now()), bytes: e2.body, mode: r2 };
if (void 0 === i2) i2 = /* @__PURE__ */ new Date();
else if (isNaN(i2)) throw new Error("Invalid modification date.");
if (!o2) return { isFile: o2, t: i2, mode: r2 };
if ("string" == typeof e2) return { isFile: o2, t: i2, bytes: t(e2), mode: r2 };
if (e2 instanceof Blob) return { isFile: o2, t: i2, bytes: e2.stream(), mode: r2 };
if (e2 instanceof Uint8Array || e2 instanceof ReadableStream) return { isFile: o2, t: i2, bytes: e2, mode: r2 };
if (e2 instanceof ArrayBuffer || ArrayBuffer.isView(e2)) return { isFile: o2, t: i2, bytes: n(e2), mode: r2 };
if (Symbol.asyncIterator in e2) return { isFile: o2, t: i2, bytes: f(e2[Symbol.asyncIterator]()), mode: r2 };
throw new TypeError("Unsupported input format.");
}
function f(e2, n2 = e2) {
return new ReadableStream({ async pull(n3) {
let t2 = 0;
for (; n3.desiredSize > t2; ) {
const i2 = await e2.next();
if (!i2.value) {
n3.close();
break;
}
{
const e3 = a(i2.value);
n3.enqueue(e3), t2 += e3.byteLength;
}
}
}, cancel(e3) {
var _a2;
(_a2 = n2.throw) == null ? void 0 : _a2.call(n2, e3);
} });
}
function a(e2) {
return "string" == typeof e2 ? t(e2) : e2 instanceof Uint8Array ? e2 : n(e2);
}
function s(e2, i2, r2) {
let [o2, f2] = function(e3) {
return e3 ? e3 instanceof Uint8Array ? [e3, 1] : ArrayBuffer.isView(e3) || e3 instanceof ArrayBuffer ? [n(e3), 1] : [t(e3), 0] : [void 0, 0];
}(i2);
if (e2 instanceof File) return { i: d(o2 || t(e2.name)), o: BigInt(e2.size), u: f2 };
if (e2 instanceof Response) {
const n2 = e2.headers.get("content-disposition"), i3 = n2 && n2.match(/;\s*filename\*?\s*=\s*(?:UTF-\d+''|)["']?([^;"'\r\n]*)["']?(?:;|$)/i), a2 = i3 && i3[1] || e2.url && new URL(e2.url).pathname.split("/").findLast(Boolean), s2 = a2 && decodeURIComponent(a2), u2 = r2 || +e2.headers.get("content-length");
return { i: d(o2 || t(s2)), o: BigInt(u2), u: f2 };
}
return o2 = d(o2, void 0 !== e2 || void 0 !== r2), "string" == typeof e2 ? { i: o2, o: BigInt(t(e2).length), u: f2 } : e2 instanceof Blob ? { i: o2, o: BigInt(e2.size), u: f2 } : e2 instanceof ArrayBuffer || ArrayBuffer.isView(e2) ? { i: o2, o: BigInt(e2.byteLength), u: f2 } : { i: o2, o: u(e2, r2), u: f2 };
}
function u(e2, n2) {
return n2 > -1 ? BigInt(n2) : e2 ? void 0 : 0n;
}
function d(e2, n2 = 1) {
if (!e2 || e2.every((c2) => 47 === c2)) throw new Error("The file must have a name.");
if (n2) for (; 47 === e2[e2.length - 1]; ) e2 = e2.subarray(0, -1);
else 47 !== e2[e2.length - 1] && (e2 = new Uint8Array([...e2, 47]));
return e2;
}
var l = new Uint32Array(256);
for (let e2 = 0; e2 < 256; ++e2) {
let n2 = e2;
for (let e3 = 0; e3 < 8; ++e3) n2 = n2 >>> 1 ^ (1 & n2 && 3988292384);
l[e2] = n2;
}
function y(e2, n2 = 0) {
n2 = ~n2;
for (var t2 = 0, i2 = e2.length; t2 < i2; t2++) n2 = n2 >>> 8 ^ l[255 & n2 ^ e2[t2]];
return ~n2 >>> 0;
}
function w(e2, n2, t2 = 0) {
const i2 = e2.getSeconds() >> 1 | e2.getMinutes() << 5 | e2.getHours() << 11, r2 = e2.getDate() | e2.getMonth() + 1 << 5 | e2.getFullYear() - 1980 << 9;
n2.setUint16(t2, i2, 1), n2.setUint16(t2 + 2, r2, 1);
}
function B({ i: e2, u: n2 }, t2) {
return 8 * (!n2 || (t2 ?? function(e3) {
try {
b.decode(e3);
} catch {
return 0;
}
return 1;
}(e2)));
}
var b = new TextDecoder("utf8", { fatal: 1 });
function p(t2, i2 = 0) {
const r2 = e(30);
return r2.setUint32(0, 1347093252), r2.setUint32(4, 754976768 | i2), w(t2.t, r2, 10), r2.setUint16(26, t2.i.length, 1), n(r2);
}
async function* g(e2) {
let { bytes: n2 } = e2;
if ("then" in n2 && (n2 = await n2), n2 instanceof Uint8Array) yield n2, e2.l = y(n2, 0), e2.o = BigInt(n2.length);
else {
e2.o = 0n;
const t2 = n2.getReader();
for (; ; ) {
const { value: n3, done: i2 } = await t2.read();
if (i2) break;
e2.l = y(n3, e2.l), e2.o += BigInt(n3.length), yield n3;
}
}
}
function I(t2, r2) {
const o2 = e(16 + (r2 ? 8 : 0));
return o2.setUint32(0, 1347094280), o2.setUint32(4, t2.isFile ? t2.l : 0, 1), r2 ? (o2.setBigUint64(8, t2.o, 1), o2.setBigUint64(16, t2.o, 1)) : (o2.setUint32(8, i(t2.o), 1), o2.setUint32(12, i(t2.o), 1)), n(o2);
}
function v(t2, r2, o2 = 0, f2 = 0) {
const a2 = e(46);
return a2.setUint32(0, 1347092738), a2.setUint32(4, 755182848), a2.setUint16(8, 2048 | o2), w(t2.t, a2, 12), a2.setUint32(16, t2.isFile ? t2.l : 0, 1), a2.setUint32(20, i(t2.o), 1), a2.setUint32(24, i(t2.o), 1), a2.setUint16(28, t2.i.length, 1), a2.setUint16(30, f2, 1), a2.setUint16(40, t2.mode | (t2.isFile ? 32768 : 16384), 1), a2.setUint32(42, i(r2), 1), n(a2);
}
function h(t2, i2, r2) {
const o2 = e(r2);
return o2.setUint16(0, 1, 1), o2.setUint16(2, r2 - 4, 1), 16 & r2 && (o2.setBigUint64(4, t2.o, 1), o2.setBigUint64(12, t2.o, 1)), o2.setBigUint64(r2 - 8, i2, 1), n(o2);
}
function D(e2) {
return e2 instanceof File || e2 instanceof Response ? [[e2], [e2]] : [[e2.input, e2.name, e2.size], [e2.input, e2.lastModified, e2.mode]];
}
var S = (e2) => function(e3) {
let n2 = BigInt(22), t2 = 0n, i2 = 0;
for (const r2 of e3) {
if (!r2.i) throw new Error("Every file must have a non-empty name.");
if (void 0 === r2.o) throw new Error(`Missing size for file "${new TextDecoder().decode(r2.i)}".`);
const e4 = r2.o >= 0xffffffffn, o2 = t2 >= 0xffffffffn;
t2 += BigInt(46 + r2.i.length + (e4 && 8)) + r2.o, n2 += BigInt(r2.i.length + 46 + (12 * o2 | 28 * e4)), i2 || (i2 = e4);
}
return (i2 || t2 >= 0xffffffffn) && (n2 += BigInt(76)), n2 + t2;
}(function* (e3) {
for (const n2 of e3) yield s(...D(n2)[0]);
}(e2));
function A(e2, n2 = {}) {
const t2 = { "Content-Type": "application/zip", "Content-Disposition": "attachment" };
return ("bigint" == typeof n2.length || Number.isInteger(n2.length)) && n2.length > 0 && (t2["Content-Length"] = String(n2.length)), n2.metadata && (t2["Content-Length"] = String(S(n2.metadata))), new Response(N(e2, n2), { headers: t2 });
}
function N(t2, a2 = {}) {
const u2 = function(e2) {
var _a2;
const n2 = e2[Symbol.iterator in e2 ? Symbol.iterator : Symbol.asyncIterator]();
return { async next() {
const e3 = await n2.next();
if (e3.done) return e3;
const [t3, i2] = D(e3.value);
return { done: 0, value: Object.assign(o(...i2), s(...t3)) };
}, throw: (_a2 = n2.throw) == null ? void 0 : _a2.bind(n2), [Symbol.asyncIterator]() {
return this;
} };
}(t2);
return f(async function* (t3, o2) {
const f2 = [];
let a3 = 0n, s2 = 0n, u3 = 0;
for await (const e2 of t3) {
const n2 = B(e2, o2.buffersAreUTF8);
yield p(e2, n2), yield new Uint8Array(e2.i), e2.isFile && (yield* g(e2));
const t4 = e2.o >= 0xffffffffn, i2 = 12 * (a3 >= 0xffffffffn) | 28 * t4;
yield I(e2, t4), f2.push(v(e2, a3, n2, i2)), f2.push(e2.i), i2 && f2.push(h(e2, a3, i2)), t4 && (a3 += 8n), s2++, a3 += BigInt(46 + e2.i.length) + e2.o, u3 || (u3 = t4);
}
let d2 = 0n;
for (const e2 of f2) yield e2, d2 += BigInt(e2.length);
if (u3 || a3 >= 0xffffffffn) {
const t4 = e(76);
t4.setUint32(0, 1347094022), t4.setBigUint64(4, BigInt(44), 1), t4.setUint32(12, 755182848), t4.setBigUint64(24, s2, 1), t4.setBigUint64(32, s2, 1), t4.setBigUint64(40, d2, 1), t4.setBigUint64(48, a3, 1), t4.setUint32(56, 1347094023), t4.setBigUint64(64, a3 + d2, 1), t4.setUint32(72, 1, 1), yield n(t4);
}
const l2 = e(22);
l2.setUint32(0, 1347093766), l2.setUint16(8, r(s2), 1), l2.setUint16(10, r(s2), 1), l2.setUint32(12, i(d2), 1), l2.setUint32(16, i(a3), 1), yield n(l2);
}(u2, a2), u2);
}
var _a;
if (typeof Promise.withResolvers !== "function") {
(_a = Promise.withResolvers) != null ? _a : Promise.withResolvers = function() {
let p2 = {};
p2.promise = new Promise((res, rej) => {
p2.resolve = res;
p2.reject = rej;
});
return p2;
};
}
function create_observable() {
let is_closed = false;
let cursor = 0;
let queue = [];
let p2 = null;
return {
push(...items) {
let did_push = !is_closed;
if (did_push) {
cursor = queue.length;
queue.push(...items);
p2 == null ? void 0 : p2.resolve();
}
return did_push;
},
end() {
is_closed = true;
p2 == null ? void 0 : p2.resolve();
},
async *[Symbol.asyncIterator]() {
if (queue.length > 0) {
yield* queue;
}
while (!is_closed) {
if (p2 === null) {
p2 = Promise.withResolvers();
await p2.promise;
p2 = null;
} else {
await p2.promise;
}
for (let i2 = cursor; i2 < queue.length; i2++) {
yield queue[i2];
}
}
}
};
}
const instanceOfAny = (object, constructors) => constructors.some((c2) => object instanceof c2);
let idbProxyableTypes;
let cursorAdvanceMethods;
function getIdbProxyableTypes() {
return idbProxyableTypes || (idbProxyableTypes = [
IDBDatabase,
IDBObjectStore,
IDBIndex,
IDBCursor,
IDBTransaction
]);
}
function getCursorAdvanceMethods() {
return cursorAdvanceMethods || (cursorAdvanceMethods = [
IDBCursor.prototype.advance,
IDBCursor.prototype.continue,
IDBCursor.prototype.continuePrimaryKey
]);
}
const transactionDoneMap = /* @__PURE__ */ new WeakMap();
const transformCache = /* @__PURE__ */ new WeakMap();
const reverseTransformCache = /* @__PURE__ */ new WeakMap();
function promisifyRequest(request) {
const promise = new Promise((resolve, reject) => {
const unlisten = () => {
request.removeEventListener("success", success);
request.removeEventListener("error", error);
};
const success = () => {
resolve(wrap(request.result));
unlisten();
};
const error = () => {
reject(request.error);
unlisten();
};
request.addEventListener("success", success);
request.addEventListener("error", error);
});
reverseTransformCache.set(promise, request);
return promise;
}
function cacheDonePromiseForTransaction(tx) {
if (transactionDoneMap.has(tx))
return;
const done = new Promise((resolve, reject) => {
const unlisten = () => {
tx.removeEventListener("complete", complete);
tx.removeEventListener("error", error);
tx.removeEventListener("abort", error);
};
const complete = () => {
resolve();
unlisten();
};
const error = () => {
reject(tx.error || new DOMException("AbortError", "AbortError"));
unlisten();
};
tx.addEventListener("complete", complete);
tx.addEventListener("error", error);
tx.addEventListener("abort", error);
});
transactionDoneMap.set(tx, done);
}
let idbProxyTraps = {
get(target, prop, receiver) {
if (target instanceof IDBTransaction) {
if (prop === "done")
return transactionDoneMap.get(target);
if (prop === "store") {
return receiver.objectStoreNames[1] ? void 0 : receiver.objectStore(receiver.objectStoreNames[0]);
}
}
return wrap(target[prop]);
},
set(target, prop, value) {
target[prop] = value;
return true;
},
has(target, prop) {
if (target instanceof IDBTransaction && (prop === "done" || prop === "store")) {
return true;
}
return prop in target;
}
};
function replaceTraps(callback) {
idbProxyTraps = callback(idbProxyTraps);
}
function wrapFunction(func) {
if (getCursorAdvanceMethods().includes(func)) {
return function(...args) {
func.apply(unwrap(this), args);
return wrap(this.request);
};
}
return function(...args) {
return wrap(func.apply(unwrap(this), args));
};
}
function transformCachableValue(value) {
if (typeof value === "function")
return wrapFunction(value);
if (value instanceof IDBTransaction)
cacheDonePromiseForTransaction(value);
if (instanceOfAny(value, getIdbProxyableTypes()))
return new Proxy(value, idbProxyTraps);
return value;
}
function wrap(value) {
if (value instanceof IDBRequest)
return promisifyRequest(value);
if (transformCache.has(value))
return transformCache.get(value);
const newValue = transformCachableValue(value);
if (newValue !== value) {
transformCache.set(value, newValue);
reverseTransformCache.set(newValue, value);
}
return newValue;
}
const unwrap = (value) => reverseTransformCache.get(value);
function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
const request = indexedDB.open(name, version);
const openPromise = wrap(request);
if (upgrade) {
request.addEventListener("upgradeneeded", (event) => {
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
});
}
if (blocked) {
request.addEventListener("blocked", (event) => blocked(
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
event.oldVersion,
event.newVersion,
event
));
}
openPromise.then((db) => {
if (terminated)
db.addEventListener("close", () => terminated());
if (blocking) {
db.addEventListener("versionchange", (event) => blocking(event.oldVersion, event.newVersion, event));
}
}).catch(() => {
});
return openPromise;
}
const readMethods = ["get", "getKey", "getAll", "getAllKeys", "count"];
const writeMethods = ["put", "add", "delete", "clear"];
const cachedMethods = /* @__PURE__ */ new Map();
function getMethod(target, prop) {
if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === "string")) {
return;
}
if (cachedMethods.get(prop))
return cachedMethods.get(prop);
const targetFuncName = prop.replace(/FromIndex$/, "");
const useIndex = prop !== targetFuncName;
const isWrite = writeMethods.includes(targetFuncName);
if (
// Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))
) {
return;
}
const method = async function(storeName, ...args) {
const tx = this.transaction(storeName, isWrite ? "readwrite" : "readonly");
let target2 = tx.store;
if (useIndex)
target2 = target2.index(args.shift());
return (await Promise.all([
target2[targetFuncName](...args),
isWrite && tx.done
]))[0];
};
cachedMethods.set(prop, method);
return method;
}
replaceTraps((oldTraps) => ({
...oldTraps,
get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop)
}));
const advanceMethodProps = ["continue", "continuePrimaryKey", "advance"];
const methodMap = {};
const advanceResults = /* @__PURE__ */ new WeakMap();
const ittrProxiedCursorToOriginalProxy = /* @__PURE__ */ new WeakMap();
const cursorIteratorTraps = {
get(target, prop) {
if (!advanceMethodProps.includes(prop))
return target[prop];
let cachedFunc = methodMap[prop];
if (!cachedFunc) {
cachedFunc = methodMap[prop] = function(...args) {
advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
};
}
return cachedFunc;
}
};
async function* iterate(...args) {
let cursor = this;
if (!(cursor instanceof IDBCursor)) {
cursor = await cursor.openCursor(...args);
}
if (!cursor)
return;
cursor = cursor;
const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
reverseTransformCache.set(proxiedCursor, unwrap(cursor));
while (cursor) {
yield proxiedCursor;
cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
advanceResults.delete(proxiedCursor);
}
}
function isIteratorProp(target, prop) {
return prop === Symbol.asyncIterator && instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]);
}
replaceTraps((oldTraps) => ({
...oldTraps,
get(target, prop, receiver) {
if (isIteratorProp(target, prop))
return iterate;
return oldTraps.get(target, prop, receiver);
},
has(target, prop) {
return isIteratorProp(target, prop) || oldTraps.has(target, prop);
}
}));
async function select_until(selector) {
let el;
while (!el) {
await new Promise(requestAnimationFrame);
el = document.querySelector(selector);
}
return el;
}
async function create_button(text) {
let button = document.createElement("button");
button.textContent = text;
button.className = "post__fav";
let container = await select_until(".post__actions");
container.append(button);
return button;
}
function use_settings(db) {
return {
async get(key, get_default) {
let res = await db.get("settings", key);
if (res !== void 0) {
return res.value;
}
let value = await get_default();
await db.add("settings", { key, value });
return value;
},
set(key, value) {
return db.add("settings", { key, value });
}
};
}
function open_db() {
return openDB("kemono-toolkit", 1, {
upgrade(db) {
db.createObjectStore("settings", {
keyPath: "key"
});
}
});
}
function fs_archiver(root) {
return {
type: "fs",
async file(name, data) {
let handle = await root.getFileHandle(name, { create: true });
let writer = await handle.createWritable({ keepExistingData: false });
await writer.write(data);
await writer.close();
},
async done() {
}
};
}
function zip_archiver(title) {
let files = create_observable();
let zip = A(files);
return {
type: "zip",
async file(name, data) {
files.push({ name, input: data });
},
async done() {
files.end();
let zip_blob = await zip.blob();
save_as(zip_blob, `${title}.zip`);
}
};
}
function save_as(blob, title) {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `${title}.zip`;
link.click();
link.remove();
URL.revokeObjectURL(link.href);
}
async function create_archiver(title) {
let uses_new_archiver = typeof window.showDirectoryPicker === "function";
let settings = use_settings(await open_db());
if (uses_new_archiver) {
try {
let root = await settings.get("root", () => showDirectoryPicker({ startIn: "downloads", mode: "readwrite" }));
let dir = await root.getDirectoryHandle(title, { create: true });
return fs_archiver(dir);
} catch (e2) {
console.error("could not use file system api.");
console.error(e2);
return zip_archiver(title);
}
} else {
return zip_archiver(title);
}
}
async function download_attachment(path) {
console.log("downloading (%s)...", path);
let res = await GM.xmlHttpRequest({
url: path,
responseType: "arraybuffer"
});
console.log("downloaded (%s)");
return res.response;
}
function create_notification(id) {
let title = `Downloading Post #${id}...`;
return function(text, done = false) {
return new Promise((res) => GM_notification({
tag: id,
title,
text,
timeout: done ? 3e3 : 0,
ondone: res
}));
};
}
let router = e$1();
router.get("/:service/user/:user_id/post/:post_id", c, async (c2) => {
let btn = await create_button("Download");
btn.onclick = async () => {
let notify = create_notification(c2.post_id);
void notify("Downloading post data...");
let post = await get_current_post();
let title;
{
title = post.title;
}
let saver = await create_archiver(title);
for (let i2 = 0, n2 = post.attachments.length; i2 < n2; i2++) {
let attachment = post.attachments[i2];
void notify(`Downloading ${i2 + 1} of ${n2} attachments...`);
let contents = await download_attachment(attachment.path);
await saver.file(attachment.name, contents);
}
if (saver.type === "zip") {
void notify("Saving files...");
await saver.done();
}
await notify("Post downloaded!", true);
};
});
onlocationchange(() => {
console.log("update():", location.href);
router.fetch(new Request(location.pathname));
});
})();
// @todo: source code
// will be moving it to a dedicated repo, since it now uses multiple files
@Chooks22
Copy link
Author

Chooks22 commented Apr 1, 2025

Idk if the old update link will still detect a new update, since I changed my name and github now returns a 404 for the old link

Just uninstall the old one and reinstall this one, since I also changed the name to reflect my plans for this script, bunch of plans lined up

@AshenTrickstar
Copy link

How do i change the folder to where the downloads go after putting one?

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