Browse Source

Add hotlinking option

pull/46/head
coomdev 2 years ago
parent
commit
25c27b72e1
  1. 4
      src/App.svelte
  2. 236
      src/Embedding.svelte
  3. 4
      src/main.ts
  4. 23
      src/pngv3.ts
  5. 8
      src/pomf.ts
  6. 1
      src/stores.ts
  7. 9
      src/thirdeye.ts

4
src/App.svelte

@ -85,6 +85,10 @@
<input type="checkbox" bind:checked={$settings.prev} /> <input type="checkbox" bind:checked={$settings.prev} />
Preload external files when they are in view. Preload external files when they are in view.
</label> </label>
<label>
<input type="checkbox" bind:checked={$settings.hotlink} />
Hotlink content.
</label>
<label> <label>
<input type="checkbox" bind:checked={$settings.ca} /> <input type="checkbox" bind:checked={$settings.ca} />
Control audio on videos with mouse wheel. Control audio on videos with mouse wheel.

236
src/Embedding.svelte

@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { fileTypeFromBuffer } from 'file-type' import { fileTypeFromBuffer, FileTypeResult } from 'file-type'
import { settings, appState } from './stores' import { settings, appState } from './stores'
import { beforeUpdate, tick } from 'svelte' import { beforeUpdate, tick } from 'svelte'
import type {EmbeddedFile} from './main'; import type { EmbeddedFile } from './main'
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte'
import { GM_head, headerStringToObject } from '../dist/requests'
export const dispatch = createEventDispatcher(); export const dispatch = createEventDispatcher()
export let file: EmbeddedFile export let file: EmbeddedFile
let isVideo = false let isVideo = false
@ -13,9 +14,9 @@
let isAudio = false let isAudio = false
let url = '' let url = ''
let settled = false let settled = false
let contracted = true; let contracted = true
let hovering = false let hovering = false
let ftype = ''; let ftype = ''
let place: HTMLDivElement let place: HTMLDivElement
let hoverElem: HTMLDivElement let hoverElem: HTMLDivElement
@ -23,102 +24,115 @@
let videoElem: HTMLVideoElement let videoElem: HTMLVideoElement
let hoverVideo: HTMLVideoElement let hoverVideo: HTMLVideoElement
let dims: [number, number] = [0, 0] let dims: [number, number] = [0, 0]
let furl: string | undefined = undefined; let furl: string | undefined = undefined
let visible = false; let visible = false
export const isNotChrome = !navigator.userAgent.includes("Chrome/"); export const isNotChrome = !navigator.userAgent.includes('Chrome/')
export let id = ''; export let id = ''
document.addEventListener("reveal", (e: any) => { document.addEventListener('reveal', (e: any) => {
if (e.detail.id == id) if (e.detail.id == id) visible = !visible
visible = !visible; })
});
export function isContracted() { export function isContracted() {
return contracted; return contracted
} }
beforeUpdate(async () => { beforeUpdate(async () => {
if (settled) return if (settled) return
settled = true settled = true
const thumb = file.thumbnail || file.data; const thumb = file.thumbnail || file.data
const type = await fileTypeFromBuffer(thumb); let type: FileTypeResult | undefined
url = URL.createObjectURL(new Blob([thumb], { type: type?.mime })) if (typeof thumb != 'string') {
if (!type) type = await fileTypeFromBuffer(thumb)
return; url = URL.createObjectURL(new Blob([thumb], { type: type?.mime }))
ftype = type.mime; if (!type) return
} else {
let head = headerStringToObject(await GM_head(thumb, undefined))
type = { ext: '' as any, mime: head['content-type'].split(';')[0].trim() }
}
ftype = type.mime
isVideo = type.mime.startsWith('video/') isVideo = type.mime.startsWith('video/')
isAudio = type.mime.startsWith('audio/') isAudio = type.mime.startsWith('audio/')
isImage = type.mime.startsWith('image/') isImage = type.mime.startsWith('image/')
dispatch("fileinfo", {type}) dispatch('fileinfo', { type })
if (isImage) { if (isImage) {
contracted = !$settings.xpi; contracted = !$settings.xpi
} }
if (isVideo) { if (isVideo) {
contracted = !$settings.xpv && !$appState.isCatalog contracted = !$settings.xpv && !$appState.isCatalog
} }
if ($appState.isCatalog) if ($appState.isCatalog) contracted = true
contracted = true;
if ($settings.pre) { if ($settings.pre) {
unzip(); // not awaiting on purpose unzip() // not awaiting on purpose
} }
if ($settings.prev) { if ($settings.prev) {
let obs = new IntersectionObserver((entries, obs) => { let obs = new IntersectionObserver(
for(const item of entries) { (entries, obs) => {
if(!item.isIntersecting) continue for (const item of entries) {
unzip(); if (!item.isIntersecting) continue
obs.unobserve(place); unzip()
} obs.unobserve(place)
}, {root:null, rootMargin: '0px', threshold: 0.01}); }
obs.observe(place); },
{ root: null, rootMargin: '0px', threshold: 0.01 },
)
obs.observe(place)
} }
}); })
let unzipping = false; let unzipping = false
let progress = [0, 0] let progress = [0, 0]
async function unzip() { async function unzip() {
if (!file.thumbnail) if (!file.thumbnail) return
return; if (unzipping) return
if (unzipping)
return; let type: FileTypeResult | undefined
unzipping = true; if (typeof file.data != 'string') {
let lisn = new EventTarget(); unzipping = true
lisn.addEventListener("progress", (e: any) => { let lisn = new EventTarget()
progress = e.detail lisn.addEventListener('progress', (e: any) => {
}); progress = e.detail
let full = await file.data(lisn); })
const type = await fileTypeFromBuffer(full); let full = await file.data(lisn)
furl = URL.createObjectURL(new Blob([full], { type: type?.mime })); type = await fileTypeFromBuffer(full)
unzipping = false; furl = URL.createObjectURL(new Blob([full], { type: type?.mime }))
if (!type) } else {
return; url = file.data
furl = file.data
let head = headerStringToObject(await GM_head(file.data, undefined))
type = { ext: '' as any, mime: head['content-type'].split(';')[0].trim() }
}
if (!type) return
isVideo = type.mime.startsWith('video/') isVideo = type.mime.startsWith('video/')
isAudio = type.mime.startsWith('audio/') isAudio = type.mime.startsWith('audio/')
isImage = type.mime.startsWith('image/') isImage = type.mime.startsWith('image/')
dispatch("fileinfo", {type}) unzipping = false
dispatch('fileinfo', { type })
if (hovering) { if (hovering) {
// reset hovering to recompute proper image coordinates // reset hovering to recompute proper image coordinates
setTimeout(() => { setTimeout(() => {
recompute(); recompute()
hoverUpdate(); hoverUpdate()
}, 20); }, 20)
} }
} }
function hasAudio(video: any) { function hasAudio(video: any) {
return ( return (
video.mozHasAudio || video.mozHasAudio ||
!!(video.webkitAudioDecodedByteCount) || !!video.webkitAudioDecodedByteCount ||
!!(video.audioTracks && video.audioTracks.length) !!(video.audioTracks && video.audioTracks.length)
) )
} }
export async function bepis(ev: MouseEvent) { export async function bepis(ev: MouseEvent) {
if ($appState.isCatalog) return; if ($appState.isCatalog) return
if (ev.button == 0) { if (ev.button == 0) {
contracted = !contracted contracted = !contracted
@ -131,7 +145,7 @@
videoElem.controls = true videoElem.controls = true
// has to be delayed // has to be delayed
setTimeout(async () => { setTimeout(async () => {
videoElem.currentTime = hoverVideo.currentTime || 0; videoElem.currentTime = hoverVideo.currentTime || 0
await videoElem.play() await videoElem.play()
}, 10) }, 10)
} }
@ -139,27 +153,33 @@
// don't know how you managed to click before hovering but oh well // don't know how you managed to click before hovering but oh well
unzip() unzip()
} }
ev.preventDefault(); ev.preventDefault()
} else if (ev.button == 1) { // middle click } else if (ev.button == 1) {
let src = furl || url; // middle click
let src = furl || url
if (ev.altKey && file.source) { if (ev.altKey && file.source) {
src = file.source; src = file.source
} }
if (ev.shiftKey && file.page) { if (ev.shiftKey && file.page) {
src = file.page.url; src = file.page.url
} }
ev.preventDefault(); ev.preventDefault()
if (isNotChrome) { if (isNotChrome) {
window.open(src, '_blank'); window.open(src, '_blank')
} else } else await GM.openInTab(src, { active: false, insert: true })
await GM.openInTab(src, {active: false, insert: true});
} }
} }
const getViewport = () => (typeof visualViewport != "undefined" ? () => [visualViewport.width, visualViewport.height] : () => [document.documentElement.clientWidth, document.documentElement.clientHeight])(); const getViewport = () =>
(typeof visualViewport != 'undefined'
? () => [visualViewport.width, visualViewport.height]
: () => [
document.documentElement.clientWidth,
document.documentElement.clientHeight,
])()
function recompute() { function recompute() {
const [sw, sh] = getViewport(); const [sw, sh] = getViewport()
let [iw, ih] = [0, 0] let [iw, ih] = [0, 0]
if (isImage) { if (isImage) {
@ -175,47 +195,46 @@
} }
async function hoverStart(ev?: MouseEvent) { async function hoverStart(ev?: MouseEvent) {
if ($settings.dh)return; if ($settings.dh) return
if (file.thumbnail && !furl) { if (file.thumbnail && !furl) {
unzip(); unzip()
} }
if (!isImage && !isVideo) return if (!isImage && !isVideo) return
if (!contracted) return if (!contracted) return
recompute(); recompute()
hovering = true hovering = true
if (isVideo){ if (isVideo) {
try { try {
await hoverVideo.play() await hoverVideo.play()
} catch (e) { } catch (e) {
// probably didn't interact with document error, mute the video and try again? // probably didn't interact with document error, mute the video and try again?
hoverVideo.muted = true; hoverVideo.muted = true
hoverVideo.volume = 0; hoverVideo.volume = 0
await hoverVideo.play() await hoverVideo.play()
}
} }
} }
}
function hoverStop(ev?: MouseEvent) { function hoverStop(ev?: MouseEvent) {
if ($settings.dh) return; if ($settings.dh) return
hovering = false hovering = false
if (isVideo) hoverVideo.pause() if (isVideo) hoverVideo.pause()
} }
let lastev: MouseEvent | undefined; let lastev: MouseEvent | undefined
function hoverUpdate(ev?: MouseEvent) { function hoverUpdate(ev?: MouseEvent) {
lastev = lastev || ev; lastev = lastev || ev
if ($settings.dh) return; if ($settings.dh) return
if (!contracted) return if (!contracted) return
const [sw, sh] = [visualViewport.width, visualViewport.height] const [sw, sh] = [visualViewport.width, visualViewport.height]
// shamelessly stolen from 4chanX // shamelessly stolen from 4chanX
if (dims[0] == 0 && dims[1] == 0) if (dims[0] == 0 && dims[1] == 0) recompute()
recompute();
let width = dims[0] let width = dims[0]
let height = dims[1] + 25 let height = dims[1] + 25
let { clientX, clientY } = (ev || lastev!) let { clientX, clientY } = ev || lastev!
let top = Math.max(0, (clientY * (sh - height)) / sh) let top = Math.max(0, (clientY * (sh - height)) / sh)
let threshold = sw / 2 let threshold = sw / 2
let marginX: number | string = let marginX: number | string =
@ -233,13 +252,12 @@
if (!$settings.ca) return if (!$settings.ca) return
if (!isVideo) return if (!isVideo) return
if ($settings.dh && contracted) return if ($settings.dh && contracted) return
if (!hasAudio(videoElem)) if (!hasAudio(videoElem)) return
return; let vol = videoElem.volume * (ev.deltaY > 0 ? 0.9 : 1.1)
let vol = videoElem.volume * (ev.deltaY > 0 ? 0.9 : 1.1); vol = Math.max(0, Math.min(1, vol))
vol = Math.max(0, Math.min(1, vol)); videoElem.volume = vol
videoElem.volume = vol; hoverVideo.volume = videoElem.volume
hoverVideo.volume = videoElem.volume; hoverVideo.muted = vol < 0
hoverVideo.muted = vol < 0;
ev.preventDefault() ev.preventDefault()
} }
</script> </script>
@ -249,8 +267,8 @@
<div <div
class:contract={contracted} class:contract={contracted}
class="place" class="place"
on:click={e => e.preventDefault()} on:click={(e) => e.preventDefault()}
on:auxclick={e => e.preventDefault()} on:auxclick={(e) => e.preventDefault()}
on:mousedown={bepis} on:mousedown={bepis}
on:mouseover={hoverStart} on:mouseover={hoverStart}
on:mouseout={hoverStop} on:mouseout={hoverStop}
@ -260,10 +278,16 @@
> >
{#if isImage} {#if isImage}
<!-- svelte-ignore a11y-missing-attribute --> <!-- svelte-ignore a11y-missing-attribute -->
<img bind:this={imgElem} alt={file.filename} src={furl || url} /> <img
referrerpolicy="no-referrer"
bind:this={imgElem}
alt={file.filename}
src={furl || url}
/>
{/if} {/if}
{#if isAudio} {#if isAudio}
<audio <audio
referrerpolicy="no-referrer"
controls controls
src={furl || url} src={furl || url}
loop={$settings.loop} loop={$settings.loop}
@ -275,7 +299,12 @@
{#if isVideo} {#if isVideo}
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<!-- svelte-ignore a11y-missing-attribute --> <!-- svelte-ignore a11y-missing-attribute -->
<video loop={$settings.loop} bind:this={videoElem} src={furl || url} /> <video
referrerpolicy="no-referrer"
loop={$settings.loop}
bind:this={videoElem}
src={furl || url}
/>
<!-- assoom videos will never be loaded from thumbnails --> <!-- assoom videos will never be loaded from thumbnails -->
{/if} {/if}
</div> </div>
@ -289,11 +318,16 @@
>{/if} >{/if}
{#if isImage} {#if isImage}
<img alt={file.filename} src={furl || url} /> <img referrerpolicy="no-referrer" alt={file.filename} src={furl || url} />
{/if} {/if}
{#if isVideo} {#if isVideo}
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<video loop={$settings.loop} bind:this={hoverVideo} src={furl || url} /> <video
referrerpolicy="no-referrer"
loop={$settings.loop}
bind:this={hoverVideo}
src={furl || url}
/>
<!-- assoom videos will never be loaded from thumbnails --> <!-- assoom videos will never be loaded from thumbnails -->
{/if} {/if}
</div> </div>

4
src/main.ts

@ -73,7 +73,7 @@ type EmbeddedFileWithPreview = {
source?: string; // can be like a twitter post this was posted in originally source?: string; // can be like a twitter post this was posted in originally
thumbnail: Buffer; thumbnail: Buffer;
filename: string; filename: string;
data: (lisn?: EventTarget) => Promise<Buffer>; data: string | ((lisn?: EventTarget) => Promise<Buffer>);
}; };
type EmbeddedFileWithoutPreview = { type EmbeddedFileWithoutPreview = {
@ -81,7 +81,7 @@ type EmbeddedFileWithoutPreview = {
source: undefined; source: undefined;
thumbnail: undefined; thumbnail: undefined;
filename: string; filename: string;
data: Buffer; data: string | Buffer;
}; };
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview; export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview;

23
src/pngv3.ts

@ -4,7 +4,6 @@ import type { ImageProcessor } from "./main";
import { PNGDecoder, PNGEncoder } from "./png"; import { PNGDecoder, PNGEncoder } from "./png";
import { decodeCoom3Payload } from "./utils"; import { decodeCoom3Payload } from "./utils";
const CUM0 = Buffer.from("CUM\0" + "0");
const CUM3 = Buffer.from("CUM\0" + "3"); const CUM3 = Buffer.from("CUM\0" + "3");
const BufferReadStream = (b: Buffer) => { const BufferReadStream = (b: Buffer) => {
@ -19,7 +18,6 @@ const BufferReadStream = (b: Buffer) => {
const extract = async (png: Buffer) => { const extract = async (png: Buffer) => {
let magic = false; let magic = false;
let coom3 = false;
const reader = BufferReadStream(png).getReader(); const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader); const sneed = new PNGDecoder(reader);
try { try {
@ -30,10 +28,7 @@ const extract = async (png: Buffer) => {
// should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk // should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
case 'tEXt': case 'tEXt':
buff = chunk; buff = chunk;
if (buff.slice(4, 4 + CUM0.length).equals(CUM0)) if (buff.slice(4, 4 + CUM3.length).equals(CUM3)) {
magic = true;
if (buff.slice(4, 4 + CUM0.length).equals(CUM3)) {
coom3 = true;
magic = true; magic = true;
} }
break; break;
@ -52,14 +47,8 @@ const extract = async (png: Buffer) => {
} }
} }
if (lastIDAT) { if (lastIDAT) {
let data = (lastIDAT as Buffer).slice(4); const data = (lastIDAT as Buffer).slice(4);
if (coom3) return await decodeCoom3Payload(data);
return decodeCoom3Payload(data);
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 [{ filename: fn, data }];
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -95,7 +84,7 @@ const inject = async (container: File, inj: File) => {
if (magic && name != "IDAT") if (magic && name != "IDAT")
break; break;
if (!magic && name == "IDAT") { if (!magic && name == "IDAT") {
await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]); await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM3), 0, 0]);
magic = true; magic = true;
} }
await encoder.insertchunk([name, chunk, crc, offset]); await encoder.insertchunk([name, chunk, crc, offset]);
@ -119,9 +108,7 @@ const has_embed = async (png: Buffer) => {
// should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk // should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
case 'tEXt': case 'tEXt':
buff = chunk; buff = chunk;
if (buff.slice(4, 4 + CUM0.length).equals(CUM0)) if (buff.slice(4, 4 + CUM3.length).equals(CUM3))
return true;
if (buff.slice(4, 4 + CUM0.length).equals(CUM3))
return true; return true;
break; break;
case 'IDAT': case 'IDAT':

8
src/pomf.ts

@ -2,6 +2,7 @@ import type { EmbeddedFile, ImageProcessor } from "./main";
import { GM_fetch, GM_head } from "./requests"; import { GM_fetch, GM_head } from "./requests";
import type { Buffer } from "buffer"; import type { Buffer } from "buffer";
import thumbnail from "./assets/hasembed.png"; import thumbnail from "./assets/hasembed.png";
import { settings } from "./stores";
const sources = [ const sources = [
{ host: 'Catbox', prefix: 'https://files.catbox.moe/' }, { host: 'Catbox', prefix: 'https://files.catbox.moe/' },
@ -9,6 +10,11 @@ const sources = [
{ host: 'Pomf', prefix: 'https://a.pomf.cat/' }, { host: 'Pomf', prefix: 'https://a.pomf.cat/' },
]; ];
export let csettings: Parameters<typeof settings['set']>[0];
settings.subscribe(b => {
csettings = b;
});
const getExt = (fn: string) => { const getExt = (fn: string) => {
const isDum = fn!.match(/^([a-z0-9]{6}\.(?:jpe?g|png|webm|gif))/gi); 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 isB64 = fn!.match(/^((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))?\.(gif|jpe?g|png|webm)/);
@ -40,7 +46,7 @@ const extract = async (b: Buffer, fn?: string) => {
return [{ return [{
filename: ext, filename: ext,
data: async (lsn) => { data: csettings.hotlink ? rsource! : async (lsn) => {
try { try {
return (await GM_fetch(rsource, undefined, lsn)).arrayBuffer(); return (await GM_fetch(rsource, undefined, lsn)).arrayBuffer();
} catch (e) { } catch (e) {

1
src/stores.ts

@ -23,6 +23,7 @@ export const settings = writable(localLoad('settingsv2', {
sh: false, sh: false,
ep: false, ep: false,
expte: false, expte: false,
hotlink: false,
conc: 8, conc: 8,
ho: false, ho: false,
blacklist: ['guro', 'scat', 'ryona', 'gore'], blacklist: ['guro', 'scat', 'ryona', 'gore'],

9
src/thirdeye.ts

@ -3,6 +3,11 @@ import { GM_fetch } from "./requests";
import { localLoad, settings } from "./stores"; import { localLoad, settings } from "./stores";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
export let csettings: Parameters<typeof settings['set']>[0];
settings.subscribe(b => {
csettings = b;
});
export type Booru = { export type Booru = {
disabled?: boolean; disabled?: boolean;
name: string; name: string;
@ -139,7 +144,7 @@ const extract = async (b: Buffer, fn?: string) => {
if (e.disabled) if (e.disabled)
continue; continue;
result = await findFileFrom(e, fn!.substring(0, 32)); result = await findFileFrom(e, fn!.substring(0, 32));
if (result.length) { if (result.length) {
booru = e.name; booru = e.name;
break; break;
@ -153,7 +158,7 @@ const extract = async (b: Buffer, fn?: string) => {
page: { title: booru, url: result[0].page }, page: { title: booru, url: result[0].page },
filename: fn!.substring(0, 33) + result[0].ext, filename: fn!.substring(0, 33) + result[0].ext,
thumbnail: (await (await GM_fetch(prev || full)).arrayBuffer()), // prefer preview thumbnail: (await (await GM_fetch(prev || full)).arrayBuffer()), // prefer preview
data: async (lsn) => { data: csettings.hotlink ? (full || prev) : async (lsn) => {
if (!cachedFile) if (!cachedFile)
cachedFile = (await (await GM_fetch(full || prev, undefined, lsn)).arrayBuffer()); // prefer full cachedFile = (await (await GM_fetch(full || prev, undefined, lsn)).arrayBuffer()); // prefer full
return cachedFile; return cachedFile;

Loading…
Cancel
Save