Browse Source

[Untested] Merge branch 'feat-black-posts' into 中出し

Anonymous 2 years ago
parent
commit
f81987ed8a
  1. 3
      build.js
  2. 2
      main.d.ts
  3. 4
      main.meta.js
  4. 1111
      main.user.js
  5. 21
      src/App.svelte
  6. 2
      src/Embedding.svelte
  7. 51
      src/EyeButton.svelte
  8. BIN
      src/assets/hasembed.png
  9. 6
      src/gif.ts
  10. 39
      src/main.ts
  11. 14
      src/png.ts
  12. 152
      src/pngv3.ts
  13. 76
      src/pomf.ts
  14. 36
      src/requests.ts
  15. 5
      src/stores.ts
  16. 63
      src/thirdeye.ts
  17. 5
      src/utils.ts
  18. 9
      src/webm.ts

3
build.js

@ -30,7 +30,8 @@ let rev = +res.stdout;
})
],
loader: {
'.css': 'text'
'.css': 'text',
'.png': 'binary'
},
metafile: true
})

2
main.d.ts

@ -3,5 +3,5 @@ declare module '*.css' {
}
declare module '*.png' {
export default Uint8Array;
export default new Uint8Array;
}

4
main.meta.js

@ -1,7 +1,11 @@
// ==UserScript==
// @name PNGExtraEmbed2
// @namespace https://coom.tech/
<<<<<<< HEAD
// @version 0.127
=======
// @version 0.120
>>>>>>> feat-black-posts
// @description uhh
// @author You
// @match https://boards.4channel.org/*

1111
main.user.js

File diff suppressed because it is too large

21
src/App.svelte

@ -1,10 +1,10 @@
<script lang="ts">
import { hasContext, onDestroy } from 'svelte'
import Dialog from './Dialog.svelte';
import Dialog from './Dialog.svelte';
import { settings } from './stores'
import Tag from './Tag.svelte'
import type { Booru } from './thirdeye';
import type { Booru } from './thirdeye';
let newbooru: Partial<Omit<Booru, 'quirks'> & {view: string}> = {};
let dial: Dialog;
@ -65,7 +65,7 @@ import type { Booru } from './thirdeye';
</label>
<label>
<input type="checkbox" bind:checked={$settings.dh} />
Turn off hover preview.
Disable hover preview.
</label>
<label>
<input type="checkbox" bind:checked={$settings.eye} />
@ -96,15 +96,22 @@ import type { Booru } from './thirdeye';
<label>
<input type="checkbox" bind:checked={$settings.ep} />
<!-- svelte-ignore a11y-missing-attribute -->
Turn off embedded file preloading<a
Disable embedded file preloading<a
title="You might still want to enable 'preload external files'">?</a
>
</label>
<label>
<input type="checkbox" bind:checked={$settings.te} />
Turn off third-eye.
Disable third-eye.
</label>
{#if !$settings.te}
<label>
<input type="checkbox" bind:checked={$settings.expte} />
<!-- svelte-ignore a11y-missing-attribute -->
Use Experimental Query API<a title="Can be up to 30% faster, reduces strain on boorus, may break">?</a
>
</label>
<h3>Booru sources</h3>
<div class="tagcont">
{#each $settings.rsources as source, i}
@ -193,10 +200,6 @@ import type { Booru } from './thirdeye';
flex-wrap: wrap;
}
/* select {
font-size: 1.2em;
} */
.enabled {
display: block;
}

2
src/Embedding.svelte

@ -216,6 +216,8 @@
if (!contracted) return
const [sw, sh] = [visualViewport.width, visualViewport.height]
// shamelessly stolen from 4chanX
if (dims[0] == 0 && dims[1] == 0)
recompute();
let width = dims[0]
let height = dims[1] + 25
let { clientX, clientY } = (ev || lastev!)

51
src/EyeButton.svelte

@ -24,6 +24,44 @@ import { EmbeddedFile, EMBED_TYPES } from './main';
}
const isNotChrome = !navigator.userAgent.includes("Chrome/");
const opContainer = document.querySelector('.opContainer') as HTMLDivElement;
const fullId = opContainer?.dataset.fullID ?? "";
const boardID = fullId?.substring(0, fullId.indexOf('.')) ?? "";
const opID = fullId?.substring(fullId.indexOf('.') + '.'.length) ?? "";
const opSubject = opContainer?.querySelector('.postInfo.desktop .subject')?.textContent ?? "";
const opComment = opContainer?.querySelector('.postMessage')?.textContent ?? "";
const opInfo = (opSubject + opComment).substring(0, 46);
const opTotal = `[${opInfo}]_oID[${opID}]`;
const MAX_PATH = 237;
function downloadName(filename: string) {
//! TODO add user flag or check for Windows or not, if not remove .substring() calls.
//MAX Length=(260-12-1) = 247. The trailing minus 1 is for the invisible NUL terminator. (Windows)
//opera browser max length 243?
//firefox browser max length 237?
//style -> '-[]_oID[]_pID[]_[].' = 19 char length
//style example -> '$boardID-[$name]_opID.[$opID]_postID.[$postID]_[$fileName]'
const downloadExt = filename.substring(filename.lastIndexOf("."));
const postWrapper = document.querySelector('[alt="'+filename+'"]')?.parentElement?.parentElement?.parentElement?.parentElement;
const pID = postWrapper?.id.substring(1) ?? "";
// const pSubject = postWrapper?.querySelector('.subject')?.textContent ?? "";
// const pComment = postWrapper?.querySelector('.postMessage')?.textContent ?? "";
// const pInfo = (opSubject + opComment).substring(0, 46);
// const pTotal = `[${pInfo}]_ID[${pID}]`;
// return `${boardID}-${opTotal}_pID[${postID}]_[{3e}${fileName}]`;
const suffix = ']' + downloadExt;
const max = MAX_PATH - suffix.length;
const substringed = `${boardID}-${opTotal}_pID[${pID}]_[{PEE}${filename}`.substring(0, max);
return substringed + suffix;
}
async function downloadFile(file: EmbeddedFile) {
const a = document.createElement("a") as HTMLAnchorElement;
document.body.appendChild(a);
@ -32,7 +70,8 @@ import { EmbeddedFile, EMBED_TYPES } from './main';
const type = await fileTypeFromBuffer(thumb);
const url = URL.createObjectURL(new Blob([thumb], { type: type?.mime }))
a.href = url;
a.download = file.filename;
a.download = downloadName(file.filename);
a.click();
window.URL.revokeObjectURL(url);
}
@ -161,6 +200,7 @@ import { EmbeddedFile, EMBED_TYPES } from './main';
margin-right: 2px;
}
details.tags > summary {
user-select: none;
cursor: pointer;
}
details.tags > ul {
@ -168,13 +208,14 @@ import { EmbeddedFile, EMBED_TYPES } from './main';
min-width: 35px;
list-style: none;
margin: 0;
margin-top: 2px;
padding: 0;
background-color: #00000044;
background-color: #191a1c;
z-index: 10;
}
details.tags > ul > li {
padding: 2px 2px;
margin-bottom: 2px;
padding: 2px 4px;
/* margin-bottom: 2px; */
}
</style>

BIN
src/assets/hasembed.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

6
src/gif.ts

@ -1,5 +1,5 @@
import { Buffer } from "buffer";
import { EmbeddedFile, EMBED_STATUS, ImageProcessor } from "./main";
import { EmbeddedFile, EMBED_STATUS, EMBED_TYPES, ImageProcessor } from "./main";
import { BufferWriteStream } from "./png";
const netscape = Buffer.from("!\xFF\x0BNETSCAPE2.0", 'ascii');
@ -24,7 +24,7 @@ const read_section = (gif: Buffer, pos: number) => {
};
};
const extractBuff = (gif: Buffer) => {
const extractBuff = (gif: Buffer) : EmbeddedFile[] => {
const field = gif.readUInt8(10);
const gcte = !!(field & (1 << 7));
let end = 13;
@ -43,7 +43,7 @@ const extractBuff = (gif: Buffer) => {
ptr += sec.data.byteLength;
end = sec.end;
} while (sec.appname == "COOMTECH" && gif[end] == '!'.charCodeAt(0));
return { data: ret, filename: 'embedded' } as EmbeddedFile;
return [{ embed_type: EMBED_TYPES.MEDIA_EMBED, data: ret, filename: 'embedded' }] as EmbeddedFile[];
}
end = sec.end;
}

39
src/main.ts

@ -6,6 +6,7 @@ import png from "./png";
import webm from "./webm";
import gif from "./gif";
import thirdeye from "./thirdeye";
import pomf from "./pomf";
import { GM_fetch, GM_head, headerStringToObject } from "./requests";
@ -20,18 +21,18 @@ export interface ImageProcessor {
skip?: true;
match(fn: string): boolean;
has_embed(b: Buffer, fn?: string): EMBED_STATUS | Promise<EMBED_STATUS>;
extract(b: Buffer, fn?: string): EmbeddedFile | Promise<EmbeddedFile>;
extract(b: Buffer, fn?: string): EmbeddedFile[] | Promise<EmbeddedFile[] | undefined>;
inject?(b: File, c: File): Buffer | Promise<Buffer>;
}
export let csettings: Parameters<typeof settings['set']>[0];
let processors: ImageProcessor[] =
[thirdeye, png, webm, gif];
[thirdeye, pomf, png, webm, gif];
let cappState: Parameters<typeof appState['set']>[0];
settings.subscribe(b => {
csettings = b;
processors = [...(!csettings.te ? [thirdeye] : []), png, webm, gif
processors = [...(!csettings.te ? [thirdeye] : []), png, pomf, webm, gif
];
});
@ -102,7 +103,7 @@ type EmbeddedFileWithoutPreview = {
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview;
const processImage = async (src: string, fn: string, hex: string): Promise<([EmbeddedFile, EMBED_TYPES] | undefined)[]> => {
const processImage = async (src: string, fn: string, hex: string): Promise<([EmbeddedFile[], EMBED_TYPES] | undefined)[]> => {
return Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
if (proc.skip) { /* third-eye */
// skip file downloading, file is referenced from the filename
@ -111,11 +112,12 @@ const processImage = async (src: string, fn: string, hex: string): Promise<([Emb
const embed_status = await proc.has_embed(md5, fn)
if (embed_status === EMBED_STATUS.SUCCESS || embed_status === EMBED_STATUS.TE_BLACKLISTED){
const file = await proc.extract(md5, fn);
file.embed_type = EMBED_TYPES.THIRD_EYE;
if(embed_status === EMBED_STATUS.TE_BLACKLISTED)
file.isBlacklisted = true
return [file, EMBED_TYPES.THIRD_EYE] as [EmbeddedFile, EMBED_TYPES];
file?.forEach(e=>{e!.isBlacklisted = true;})
return [file, EMBED_TYPES.THIRD_EYE] as [EmbeddedFile[], EMBED_TYPES];
}
// if (await proc.has_embed(md5, fn) === true)
// return [await proc.extract(md5, fn), true] as [EmbeddedFile[], boolean];
return;
}
const iter = streamRemote(src); /* media-embed */
@ -142,9 +144,8 @@ const processImage = async (src: string, fn: string, hex: string): Promise<([Emb
return;
}
const file = await proc.extract(cumul);
file.embed_type = EMBED_TYPES.MEDIA_EMBED;
return [file, EMBED_TYPES.MEDIA_EMBED] as [EmbeddedFile, EMBED_TYPES];
// return [await proc.extract(cumul), EMBED_TYPES.HAS_PEE] as [EmbeddedFile, EMBED_TYPES];
return [file, EMBED_TYPES.MEDIA_EMBED] as [EmbeddedFile[], EMBED_TYPES];
// return [await proc.extract(cumul), false] as [EmbeddedFile[], boolean];
}));
};
@ -162,15 +163,17 @@ const processPost = async (post: HTMLDivElement) => {
res2 = res2?.filter(e => e);
if (!res2 || res2.length == 0)
return;
// processAttachments(post, res2?.filter(e => e) as [EmbeddedFile, boolean][]);
processAttachments(post, res2?.filter(e => {
if(!e) return;
if(e[0]?.isBlacklisted === true){
post.querySelector('.reply')?.classList.add('hasblack');
}
return e;
}) as [EmbeddedFile, EMBED_TYPES][]);
let test = res2?.flatMap(e => e![0].map(k => [k, e![1]] as [EmbeddedFile, EMBED_TYPES]));
// processAttachments(post, res2?.flatMap(e => e![0].map(k => [k, e![1]] as [EmbeddedFile, boolean])));
processAttachments(post, res2?.flatMap(e =>
e![0].map(k => {
if(k?.isBlacklisted === true){
post.querySelector('.reply')?.classList.add('hasblack');
}
return [k, e![1]] as [EmbeddedFile, EMBED_TYPES]
})
) as [EmbeddedFile, EMBED_TYPES][]);
};
const startup = async () => {

14
src/png.ts

@ -1,10 +1,10 @@
import { buf } from "crc-32";
import { Buffer } from "buffer";
import { EMBED_STATUS, ImageProcessor } from "./main";
import { EmbeddedFile, EMBED_STATUS, EMBED_TYPES, ImageProcessor } from "./main";
type PNGChunk = [string, Buffer, number, number];
export type PNGChunk = [string, Buffer, number, number];
class PNGDecoder {
export class PNGDecoder {
repr: Buffer;
req = 8;
@ -47,7 +47,7 @@ class PNGDecoder {
}
}
class PNGEncoder {
export class PNGEncoder {
writer: WritableStreamDefaultWriter<Buffer>;
constructor(bytes: WritableStream<Buffer>) {
@ -83,7 +83,7 @@ const BufferReadStream = (b: Buffer) => {
return ret;
};
const extract = async (png: Buffer) => {
const extract = async (png: Buffer) : Promise<EmbeddedFile[] | undefined> => {
let magic = false;
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader);
@ -118,7 +118,7 @@ const extract = async (png: Buffer) => {
const fn = data.slice(4, 4 + fnsize).toString();
// Todo: xor the buffer to prevent scanning for file signatures (4chan embedded file detection)?
data = data.slice(4 + fnsize);
return { filename: fn, data };
return [{ embed_type: EMBED_TYPES.MEDIA_EMBED, filename: fn, data } as EmbeddedFile];
}
} catch (e) {
console.error(e);
@ -129,7 +129,7 @@ const extract = async (png: Buffer) => {
const buildChunk = (tag: string, data: Buffer) => {
const ret = Buffer.alloc(data.byteLength + 4);
ret.write(tag.substr(0, 4), 0);
ret.write(tag.slice(0, 4), 0);
data.copy(ret, 4);
return ret;
};

152
src/pngv3.ts

@ -0,0 +1,152 @@
import { buf } from "crc-32";
import { Buffer } from "buffer";
import { EMBED_TYPES, ImageProcessor, EmbeddedFile, EMBED_STATUS } from './main';
import { PNGDecoder, PNGEncoder } from "./png";
import { decodeCoom3Payload } from "./utils";
const CUM0 = Buffer.from("CUM\0" + "0");
const CUM3 = Buffer.from("CUM\0" + "3");
const BufferReadStream = (b: Buffer) => {
const ret = new ReadableStream<Buffer>({
pull(cont) {
cont.enqueue(b);
cont.close();
}
});
return ret;
};
const extract = async (png: Buffer): Promise<EmbeddedFile[] | undefined> => {
let magic = false;
let coom3 = false;
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader);
try {
let lastIDAT: Buffer | null = null;
for await (const [name, chunk, crc, offset] of sneed.chunks()) {
let buff: Buffer;
switch (name) {
// should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
case 'tEXt':
buff = chunk;
if (buff.slice(4, 4 + CUM0.length).equals(CUM0))
magic = true;
if (buff.slice(4, 4 + CUM0.length).equals(CUM3)) {
coom3 = true;
magic = true;
}
break;
case 'IDAT':
if (magic) {
lastIDAT = chunk;
break;
}
// eslint-disable-next-line no-fallthrough
case 'IEND':
if (!magic)
return; // Didn't find tExt Chunk;
// eslint-disable-next-line no-fallthrough
default:
break;
}
}
if (lastIDAT) {
let data = (lastIDAT as Buffer).slice(4);
if (coom3) {
let file = decodeCoom3Payload(data)
return;
}
const fnsize = data.readUInt32LE(0);
const fn = data.slice(4, 4 + fnsize).toString();
// Todo: xor the buffer to prevent scanning for file signatures (4chan embedded file detection)?
data = data.slice(4 + fnsize);
return [{ embed_type: EMBED_TYPES.MEDIA_EMBED, filename: fn, data } as EmbeddedFile];
}
} catch (e) {
console.error(e);
} finally {
reader.releaseLock();
}
};
const buildChunk = (tag: string, data: Buffer) => {
const ret = Buffer.alloc(data.byteLength + 4);
ret.write(tag.slice(0, 4), 0);
data.copy(ret, 4);
return ret;
};
export const BufferWriteStream = () => {
let b = Buffer.from([]);
const ret = new WritableStream<Buffer>({
write(chunk) {
b = Buffer.concat([b, chunk]);
}
});
return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
};
const inject = async (container: File, inj: File) => {
const [writestream, extract] = BufferWriteStream();
const encoder = new PNGEncoder(writestream);
const decoder = new PNGDecoder(container.stream().getReader());
let magic = false;
for await (const [name, chunk, crc, offset] of decoder.chunks()) {
if (magic && name != "IDAT")
break;
if (!magic && name == "IDAT") {
await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]);
magic = true;
}
await encoder.insertchunk([name, chunk, crc, offset]);
}
const injb = Buffer.alloc(4 + inj.name.length + inj.size);
injb.writeInt32LE(inj.name.length, 0);
injb.write(inj.name, 4);
Buffer.from(await inj.arrayBuffer()).copy(injb, 4 + inj.name.length);
await encoder.insertchunk(["IDAT", buildChunk("IDAT", injb), 0, 0]);
await encoder.insertchunk(["IEND", buildChunk("IEND", Buffer.from([])), 0, 0]);
return extract();
};
const has_embed = async (png: Buffer): Promise<EMBED_STATUS> => {
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader);
try {
for await (const [name, chunk, crc, offset] of sneed.chunks()) {
let buff: Buffer;
switch (name) {
// should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
case 'tEXt':
buff = chunk;
if (buff.slice(4, 4 + CUM0.length).equals(CUM0))
return EMBED_STATUS.SUCCESS;
if (buff.slice(4, 4 + CUM0.length).equals(CUM3))
return EMBED_STATUS.SUCCESS;
break;
case 'IDAT':
// eslint-disable-next-line no-fallthrough
case 'IEND':
return EMBED_STATUS.NONE; // Didn't find tExt Chunk; Definite no
// eslint-disable-next-line no-fallthrough
default:
break;
}
}
// stream ended on chunk boundary, so no unexpected EOF was fired, need more data anyway
} catch (e) {
return EMBED_STATUS.PEE_UNDEFINED; // possibly unexpected EOF, need more data to decide
} finally {
reader.releaseLock();
}
return EMBED_STATUS.PEE_UNDEFINED; // possibly unexpected EOF, need more data to decide
};
export default {
extract,
has_embed,
inject,
match: fn => !!fn.match(/\.png$/)
} as ImageProcessor;

76
src/pomf.ts

@ -0,0 +1,76 @@
import { EmbeddedFile, EMBED_STATUS, EMBED_TYPES, ImageProcessor } from "./main";
import { GM_fetch, GM_head } from "./requests";
import type { Buffer } from "buffer";
import thumbnail from "./assets/hasembed.png";
const sources = [
{ host: 'Catbox', prefix: 'https://files.catbox.moe/' },
{ host: 'Litter', prefix: 'https://litter.catbox.moe/' },
{ host: 'Pomf', prefix: 'https://a.pomf.cat/' },
];
const getExt = (fn: string) => {
const isDum = fn!.match(/^([a-z0-9]{6}\.(?:jpe?g|png|webm|gif))/gi);
const isB64 = fn!.match(/^((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))?\.(gif|jpe?g|png|webm)/);
const isExt = fn!.match(/\[.*=(.*)\]/);
let ext;
if (isDum) {
ext = isDum[0];
} else if (isB64) {
ext = atob(isB64[1]);
} else if (isExt) {
ext = isExt[1];
}
return ext;
};
const extract = async (b: Buffer, fn?: string) : Promise<EmbeddedFile[]> => {
const ext = getExt(fn!);
let rsource: string;
for (const source of sources) {
try {
await GM_head(source.prefix + ext);
rsource = source.prefix + ext;
break;
} catch {
// 404
}
}
return [{
embed_type: EMBED_TYPES.MEDIA_EMBED,
filename: ext,
data: async (lsn) => {
try {
return (await GM_fetch(rsource, undefined, lsn)).arrayBuffer();
} catch (e) {
//404
}
},
// thumbnail
} as EmbeddedFile];
};
const has_embed = async (b: Buffer, fn?: string) : Promise<EMBED_STATUS> => {
const ext = getExt(fn!);
if (!ext)
return EMBED_STATUS.NONE;
for (const source of sources) {
try {
const e = await GM_head(source.prefix + ext);
return EMBED_STATUS.SUCCESS;
} catch {
// 404
}
}
return EMBED_STATUS.NONE;
};
export default {
skip: true,
extract,
has_embed,
match: fn => !!getExt(fn)
} as ImageProcessor;

36
src/requests.ts

@ -1,3 +1,5 @@
import { localLoad, settings } from "./stores";
const xmlhttprequest = typeof GM_xmlhttpRequest != 'undefined' ?
GM_xmlhttpRequest :
(typeof GM != "undefined" ?
@ -18,7 +20,10 @@ export function GM_head(...[url, opt]: Parameters<typeof fetch>) {
data: opt?.body?.toString(),
method: "HEAD",
onload: (resp) => {
resolve(resp.responseHeaders);
if ((resp.status / 100) >= 4)
reject("response error");
else
resolve(resp.responseHeaders);
},
ontimeout: () => reject("fetch timeout"),
onerror: () => reject("fetch error"),
@ -76,4 +81,31 @@ export function GM_fetch(...[url, opt, lisn]: [...Parameters<typeof fetch>, Even
};
xmlhttprequest(gmopt);
});
}
}
const makePoolable = <T extends any[], U>(fun: (...args: T) => Promise<U>, getPoolSize: () => number) => {
const pool = [];
let pending = 0;
const poolFree: (() => void)[] = [];
return async (...args: T) => {
while (pending >= getPoolSize())
await new Promise<void>(_ => poolFree.push(_));
pending++;
const prom = fun(...args);
prom.then(() => {
pending--;
poolFree.forEach(_ => _());
poolFree.length = 0;
});
return prom;
};
};
let csettings: Parameters<typeof settings['set']>[0] = localLoad('settingsv2', {} as any);
settings.subscribe(s => {
csettings = s;
});
const poolFetch = makePoolable(GM_fetch, () => csettings.conc);

5
src/stores.ts

@ -22,6 +22,8 @@ export const settings = writable(localLoad('settingsv2', {
prev: false,
sh: false,
ep: false,
expte: false,
conc: 8,
ho: false,
blacklist: ['guro', 'scat', 'ryona', 'gore'],
rsources: [{
@ -66,7 +68,8 @@ export const settings = writable(localLoad('settingsv2', {
domain: "booru.allthefallen.moe",
endpoint: "/posts.json?tags=md5:",
view: 'https://booru.allthefallen.moe/posts/'
}] as (Omit<Booru, 'quirks'> & {view: string, disabled?: boolean})[]
}] as (Omit<Booru, 'quirks'> & {view: string, disabled?: boolean})[],
...localLoad('settingsv2', {}),
}));
export const appState = writable({

63
src/thirdeye.ts

@ -1,6 +1,7 @@
import { EmbeddedFile, EMBED_STATUS, ImageProcessor } from "./main";
import { EmbeddedFile, EMBED_STATUS, EMBED_TYPES, ImageProcessor } from "./main";
import { GM_fetch } from "./requests";
import { localLoad, settings } from "./stores";
import { Buffer } from "buffer";
export type Booru = {
disabled?: boolean;
@ -41,7 +42,9 @@ const gelquirk: (s: string) => tran = prefix => (a =>
tags: (e.tag_string || e.tags || '').split(' ')
} as BooruMatch)) || []);
let experimentalApi = false;
settings.subscribe(s => {
experimentalApi = s.expte;
boorus = s.rsources.map(e => ({
...e,
quirks: gelquirk(e.view)
@ -60,10 +63,60 @@ settings.subscribe(s => {
black = new Set(s.blacklist);
});
const bufferingTime = 2000;
let expired: NodeJS.Timeout | undefined = undefined;
type ApiResult = { [md5 in string]: { [domain in string]: BooruMatch[] } };
let reqQueue: [string, (a: ApiResult) => void][] = [];
let unlockQueue = Promise.resolve();
const queryCache: ApiResult = {};
const processQueries = async () => {
let unlock!: () => void;
unlockQueue = new Promise<void>(_ => unlock = _);
const md5 = reqQueue.map(e => e[0]).filter(e => !(e in queryCache));
expired = undefined;
if (md5.length > 0) {
const res = await fetch("https://shoujo.coom.tech/api", {
method: "POST",
body: JSON.stringify({ md5 }),
headers: {
'content-type': 'application/json'
}
});
const results: ApiResult = await res.json();
Object.entries(results).forEach(e => queryCache[e[0]] = e[1]);
}
reqQueue.forEach(e => e[1]({ [e[0]]: queryCache[e[0]] }));
reqQueue = [];
unlock();
};
const queueForProcessing = async (hex: string, cb: (a: ApiResult) => void) => {
console.log("putting", hex, 'in queue');
await unlockQueue;
console.log("put", hex, 'in queue');
reqQueue.push([hex, cb]);
if (!expired) {
expired = setTimeout(processQueries, bufferingTime);
}
};
const cache: any = {};
const shoujoFind = async (hex: string): Promise<ApiResult> => {
return new Promise(res => {
queueForProcessing(hex, res);
});
};
const findFileFrom = async (b: Booru, hex: string, abort?: EventTarget) => {
try {
if (experimentalApi) {
const res = await shoujoFind(hex);
if (!res)
debugger;
return hex in res ? (res[hex][b.domain] || []) : [];
}
if (b.domain in cache && hex in cache[b.domain])
return cache[b.domain][hex] as BooruMatch[];
const res = await GM_fetch(`https://${b.domain}${b.endpoint}${hex}`);
@ -79,7 +132,7 @@ const findFileFrom = async (b: Booru, hex: string, abort?: EventTarget) => {
}
};
const extract = async (b: Buffer, fn?: string) => {
const extract = async (b: Buffer, fn?: string) : Promise<EmbeddedFile[]> => {
let result!: BooruMatch[];
let booru!: string;
let tags: string[] = [];
@ -87,6 +140,7 @@ const extract = async (b: Buffer, fn?: string) => {
if (e.disabled)
continue;
result = await findFileFrom(e, fn!.substring(0, 32));
if (result.length) {
booru = e.name;
tags = result.flatMap(e2 => e2.tags)
@ -98,7 +152,8 @@ const extract = async (b: Buffer, fn?: string) => {
let cachedFull: ArrayBuffer;
const prev = result[0].preview_url;
const full = result[0].full_url;
return {
return [{
embed_type: EMBED_TYPES.THIRD_EYE,
tags: tags,
source: result[0].source,
page: { title: booru, url: result[0].page },
@ -113,7 +168,7 @@ const extract = async (b: Buffer, fn?: string) => {
cachedFull = (await (await GM_fetch(full || prev, undefined, lsn)).arrayBuffer()); // prefer full
return cachedFull;
}
} as EmbeddedFile;
} as EmbeddedFile];
};
const has_embed = async (b: Buffer, fn?: string) : Promise<EMBED_STATUS> => {

5
src/utils.ts

@ -0,0 +1,5 @@
import type { Buffer } from "buffer";
export const decodeCoom3Payload = (buff: Buffer) => {
//
};

9
src/webm.ts

@ -1,9 +1,9 @@
import { Buffer } from "buffer";
import * as ebml from "ts-ebml";
import { EMBED_STATUS, ImageProcessor } from "./main";
import { EmbeddedFile, EMBED_STATUS, EMBED_TYPES, ImageProcessor } from "./main";
// unused, but will in case 4chan does file sig checks
//const password = Buffer.from("NOA");
const password = Buffer.from("NOA");
const xor = (a: Buffer, p: Buffer) => {
let n = 0;
@ -108,7 +108,7 @@ const embed = (webm: Buffer, data: Buffer) => {
return Buffer.from(enc.encode(chunks.filter(e => e.name != "unknown")));
};
const extract = (webm: Buffer) => {
const extract = (webm: Buffer) : EmbeddedFile[] | undefined => {
const dec = new ebml.Decoder();
const chunks = dec.decode(webm);
@ -120,7 +120,7 @@ const extract = (webm: Buffer) => {
return;
const chk = chunks[embed + 1];
if (chk.type == "b" && chk.name == "TagBinary")
return { filename: 'string', data: chk.data };
return [{ embed_type: EMBED_TYPES.MEDIA_EMBED, filename: 'string', data: chk.data } as EmbeddedFile];
};
const inject = async (container: File, inj: File): Promise<Buffer> =>
@ -142,5 +142,6 @@ const has_embed = (webm: Buffer) : EMBED_STATUS => {
export default {
extract,
has_embed,
inject,
match: fn => !!fn.match(/\.webm$/)
} as ImageProcessor;

Loading…
Cancel
Save