191 lines
4.6 KiB
TypeScript
191 lines
4.6 KiB
TypeScript
export function json(content: unknown) {
|
|
const headers = new Headers();
|
|
headers.append("Content-Type", "application/json");
|
|
return new Response(JSON.stringify(content), {
|
|
headers,
|
|
});
|
|
}
|
|
|
|
export const isValidUrl = (urlString: string) => {
|
|
try {
|
|
return Boolean(new URL(urlString));
|
|
} catch (_e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const fixRenderedMarkdown = (content: string) => {
|
|
return content.replace("***\n", "---")
|
|
.replace("----------------", "---")
|
|
.replace("\n---", "---")
|
|
.replace(/^(date:[^'\n]*)'|'/gm, (match, p1, p2) => {
|
|
if (p1) {
|
|
// This is a line starting with date: followed by single quotes
|
|
return p1.replace(/'/gm, "");
|
|
} else if (p2) {
|
|
return "";
|
|
} else {
|
|
// This is a line with single quotes, but not starting with date:
|
|
return match;
|
|
}
|
|
});
|
|
};
|
|
|
|
type StreamMessage = {
|
|
type: "info";
|
|
message: string;
|
|
} | {
|
|
type: "error";
|
|
message: string;
|
|
} | {
|
|
type: "warning";
|
|
message: string;
|
|
} | {
|
|
type: "finished";
|
|
url: string;
|
|
};
|
|
|
|
export async function fetchStream(
|
|
url: string,
|
|
cb: (chunk: StreamMessage) => void,
|
|
init?: RequestInit,
|
|
) {
|
|
const res = await fetch(url, init);
|
|
if (!res.body) return;
|
|
|
|
let buffer = "";
|
|
const reader = res.body
|
|
.pipeThrough(new TextDecoderStream())
|
|
.pipeThrough(
|
|
new TransformStream<string, string>({
|
|
transform(chunk, controller) {
|
|
buffer += chunk;
|
|
let idx;
|
|
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
const line = buffer.slice(0, idx).trim();
|
|
buffer = buffer.slice(idx + 1);
|
|
if (line) controller.enqueue(line);
|
|
}
|
|
},
|
|
flush(controller) {
|
|
const line = buffer.trim();
|
|
if (line) controller.enqueue(line);
|
|
},
|
|
}),
|
|
)
|
|
.getReader();
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
cb(JSON.parse(value));
|
|
}
|
|
}
|
|
|
|
export function hashString(message: string) {
|
|
let hash = 0;
|
|
for (let i = 0; i < message.length; i++) {
|
|
const char = message.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + char;
|
|
hash = hash & hash; // Convert to 32bit integer
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
export const createStreamResponse = () => {
|
|
const encoder = new TextEncoder();
|
|
let controller: ReadableStreamDefaultController<Uint8Array>;
|
|
|
|
const body = new ReadableStream<Uint8Array>({
|
|
start(c) {
|
|
controller = c;
|
|
},
|
|
});
|
|
|
|
const response = new Response(body, {
|
|
headers: {
|
|
// newline-delimited JSON
|
|
"content-type": "application/x-ndjson; charset=utf-8",
|
|
// prevent intermediaries from buffering/transforming
|
|
"cache-control": "no-cache, no-transform",
|
|
"x-content-type-options": "nosniff",
|
|
// nginx hint to disable proxy buffering
|
|
"x-accel-buffering": "no",
|
|
// if you control compression, keep it off for streams
|
|
// "content-encoding": "identity",
|
|
},
|
|
});
|
|
|
|
const send = (obj: unknown) => {
|
|
controller.enqueue(encoder.encode(JSON.stringify(obj) + "\n")); // ← delimiter
|
|
};
|
|
const cancel = () => controller.close();
|
|
|
|
function info(message: string) {
|
|
return send({ type: "info", message });
|
|
}
|
|
|
|
function error(message: string) {
|
|
return send({ type: "error", message });
|
|
}
|
|
|
|
function warning(message: string) {
|
|
return send({ type: "warning", message });
|
|
}
|
|
|
|
return {
|
|
response,
|
|
cancel,
|
|
send,
|
|
info,
|
|
error,
|
|
warning,
|
|
};
|
|
};
|
|
|
|
export type StreamResponse = ReturnType<typeof createStreamResponse>;
|
|
|
|
export function debounce<T extends (...args: Parameters<T>) => void>(
|
|
this: ThisParameterType<T>,
|
|
fn: T,
|
|
delay = 300,
|
|
) {
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
return (...args: Parameters<T>) => {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => fn.apply(this, args), delay);
|
|
};
|
|
}
|
|
|
|
export function parseRating(rating: string | number) {
|
|
if (typeof rating == "number") return rating;
|
|
try {
|
|
const res = parseInt(rating);
|
|
if (!Number.isNaN(res)) return res;
|
|
} catch (_e) {
|
|
// This is okay
|
|
}
|
|
return rating.length / 2;
|
|
}
|
|
|
|
export async function convertOggToMp3(
|
|
oggData: ArrayBuffer,
|
|
): Promise<Uint8Array> {
|
|
const ffmpeg = new Deno.Command("ffmpeg", {
|
|
args: ["-f", "ogg", "-i", "pipe:0", "-f", "mp3", "pipe:1"],
|
|
stdin: "piped",
|
|
stdout: "piped",
|
|
stderr: "null",
|
|
});
|
|
|
|
const process = ffmpeg.spawn();
|
|
const writer = process.stdin.getWriter();
|
|
await writer.write(new Uint8Array(oggData));
|
|
await writer.close();
|
|
|
|
const output = await process.output();
|
|
const { code } = await process.status;
|
|
if (code !== 0) throw new Error(`FFmpeg exited with code ${code}`);
|
|
return output.stdout;
|
|
}
|