Compare commits

...
Sign in to create a new pull request.

6 commits
dev ... main

15 changed files with 110 additions and 416 deletions

View file

@ -2,11 +2,11 @@ PORT=6600
HOST=0.0.0.0 HOST=0.0.0.0
#NODE_ENV=development #NODE_ENV=development
#REDIS_HOST=127.0.0.1
#REDIS_PORT=6379
REDIS_PASSWORD=pasw0rd
#REQUIRED if you want to use the e621 API #REQUIRED if you want to use the e621 API
E621_USER_AGENT=YourApplication/1.0 (by username on e621) E621_USER_AGENT=YourApplication/1.0 (by username on e621)
E621_USERNAME=your-username E621_USERNAME=your-username
E621_API_KEY=your-apikey E621_API_KEY=your-apikey
#REQUIRED if you want to use the Gelbooru API
GELBOORU_API_KEY=your-apikey
GELBOORU_USER_ID=your-user-id

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ bun.lockb
/config/secrets.ts /config/secrets.ts
.env .env
/logs /logs
bun.lock

View file

@ -6,7 +6,7 @@
1. Clone the repository: 1. Clone the repository:
```bash ```bash
git clone <repository-url> git clone https://git.creations.works/creations/booru-api
cd booru-api cd booru-api
``` ```
@ -48,13 +48,21 @@
--- ---
> **Note** > **Note**
> To use the e621 API, you must update the following environment variables in your `.env` file: > To use the **e621 API**, you must update the following environment variables in your `.env` file:
> >
> ```env > ```env
> # REQUIRED if you want to use the e621 API > E621_USER_AGENT=YourApplication/1.0 (by username on e621)
> E621_USER_AGENT=YourApplicationName/1.0 (by username on e621)
> E621_USERNAME=your-username > E621_USERNAME=your-username
> E621_API_KEY=your-apikey > E621_API_KEY=your-apikey
> ``` > ```
>
> Replace `your-username` and `your-apikey` with your e621 account credentials. Update the `User-Agent` string to include your application name, version, and a contact method (e.g., your e621 username) to comply with e621's API guidelines. > Replace `your-username` and `your-apikey` with your e621 account credentials. Update the `User-Agent` string to include your application name, version, and a contact method (e.g., your e621 username) to comply with e621's API guidelines.
>
>
> To use the **Gelbooru API**, you must also update the following:
>
> ```env
> GELBOORU_API_KEY=your-apikey
> GELBOORU_USER_ID=your-user-id
> ```
> You can find these credentials in your [Gelbooru account settings](https://gelbooru.com/index.php?page=account&s=options).
> These are required for authenticated API requests and higher rate limits.

View file

@ -10,18 +10,6 @@ services:
- "${PORT:-6600}:${PORT:-6600}" - "${PORT:-6600}:${PORT:-6600}"
env_file: env_file:
- .env - .env
depends_on:
- dragonfly-redis
networks:
- booru-network
dragonfly-redis:
container_name: dragonfly-redis
image: docker.dragonflydb.io/dragonflydb/dragonfly
restart: unless-stopped
environment:
REDIS_PASSWORD: ${redis_password:-pasw0rd}
command: ["--requirepass", "${redis_password:-pasw0rd}"]
networks: networks:
- booru-network - booru-network

View file

@ -1,6 +1,6 @@
// cSpell:disable // cSpell:disable
import { getE621Auth } from "./environment"; import { gelBooruAUTH, getE621Auth } from "@config/environment";
const booruDefaults: IBooruDefaults = { const booruDefaults: IBooruDefaults = {
search: "index.php?page=dapi&s=post&q=index&json=1", search: "index.php?page=dapi&s=post&q=index&json=1",
@ -18,7 +18,7 @@ export const booruConfig: IBooruConfigMap = {
autocomplete: "ac.rule34.xxx/autocomplete.php?q=", autocomplete: "ac.rule34.xxx/autocomplete.php?q=",
}, },
"realbooru.com": { "realbooru.com": {
enabled: true, enabled: false,
name: "realbooru.com", name: "realbooru.com",
aliases: ["realbooru", "rb", "real34", "realb"], aliases: ["realbooru", "rb", "real34", "realb"],
endpoint: "realbooru.com", endpoint: "realbooru.com",
@ -70,4 +70,13 @@ export const booruConfig: IBooruConfigMap = {
}, },
auth: getE621Auth(), auth: getE621Auth(),
}, },
"gelbooru.com": {
enabled: true,
name: "gelbooru.com",
aliases: ["gelbooru", "gb", "gelboorucom"],
endpoint: "gelbooru.com",
autocomplete: "gelbooru.com/index.php?page=autocomplete&term=",
functions: booruDefaults,
auth: gelBooruAUTH(),
},
}; };

View file

@ -8,12 +8,6 @@ export const environment: Environment = {
process.argv.includes("--dev"), process.argv.includes("--dev"),
}; };
export const redisConfig: RedisConfig = {
host: process.env.REDIS_HOST || "dragonfly-redis",
port: parseInt(process.env.REDIS_PORT || "6379", 10),
password: process.env.REDIS_PASSWORD || undefined,
};
if ( if (
!process.env.E621_USER_AGENT || !process.env.E621_USER_AGENT ||
!process.env.E621_USERNAME || !process.env.E621_USERNAME ||
@ -40,3 +34,14 @@ export function getE621Auth(): Record<string, string> {
"Basic " + btoa(`${e621Username || ""}:${e621ApiKey || ""}`), "Basic " + btoa(`${e621Username || ""}:${e621ApiKey || ""}`),
}; };
} }
if (!process.env.GELBOORU_API_KEY || !process.env.GELBOORU_USER_ID) {
logger.error("Missing Gelbooru credentials in .env file");
}
export function gelBooruAUTH(): Record<string, string> {
return {
apiKey: process.env.GELBOORU_API_KEY || "",
userId: process.env.GELBOORU_USER_ID || "",
};
}

View file

@ -23,11 +23,7 @@
"dev": "bun run --watch src/index.ts --dev", "dev": "bun run --watch src/index.ts --dev",
"lint": "eslint", "lint": "eslint",
"lint:fix": "bun lint --fix", "lint:fix": "bun lint --fix",
"cleanup": "rm -rf logs node_modules bun.lockdb" "cleanup": "rm -rf logs node_modules bun.lockb"
}, },
"type": "module", "type": "module"
"dependencies": {
"dotenv": "^16.4.7",
"redis": "^4.7.0"
}
} }

View file

@ -1,182 +0,0 @@
import { redisConfig } from "@config/environment";
import { logger } from "@helpers/logger";
import { createClient, type RedisClientType } from "redis";
class RedisJson {
private static instance: RedisJson | null = null;
private client: RedisClientType | null = null;
private constructor() {}
public static async initialize(): Promise<RedisJson> {
if (!RedisJson.instance) {
RedisJson.instance = new RedisJson();
RedisJson.instance.client = createClient({
socket: {
host: redisConfig.host,
port: redisConfig.port,
},
username: redisConfig.username || undefined,
password: redisConfig.password || undefined,
});
RedisJson.instance.client.on("error", (err: Error) => {
logger.error("Redis connection error:");
logger.error((err as Error) || "Unknown error");
logger.error(redisConfig.host);
process.exit(1);
});
RedisJson.instance.client.on("connect", () => {
logger.info([
"Connected to Redis on",
`${redisConfig.host}:${redisConfig.port}`,
]);
});
await RedisJson.instance.client.connect();
}
return RedisJson.instance;
}
public static getInstance(): RedisJson {
if (!RedisJson.instance || !RedisJson.instance.client) {
throw new Error(
"Redis instance not initialized. Call initialize() first.",
);
}
return RedisJson.instance;
}
public async disconnect(): Promise<void> {
if (!this.client) {
logger.error("Redis client is not initialized.");
process.exit(1);
}
try {
await this.client.disconnect();
this.client = null;
logger.info("Redis disconnected successfully.");
} catch (error) {
logger.error("Error disconnecting Redis client:");
logger.error(error as Error);
throw error;
}
}
public async get(
type: "JSON" | "STRING",
key: string,
path?: string,
): Promise<
string | number | boolean | Record<string, unknown> | null | unknown
> {
if (!this.client) {
logger.error("Redis client is not initialized.");
throw new Error("Redis client is not initialized.");
}
try {
if (type === "JSON") {
const value: unknown = await this.client.json.get(key, {
path,
});
if (value instanceof Date) {
return value.toISOString();
}
return value;
} else if (type === "STRING") {
const value: string | null = await this.client.get(key);
return value;
} else {
throw new Error(`Invalid type: ${type}`);
}
} catch (error) {
logger.error(`Error getting value from Redis for key: ${key}`);
logger.error(error as Error);
throw error;
}
}
public async set(
type: "JSON" | "STRING",
key: string,
value: unknown,
expiresInSeconds?: number,
path?: string,
): Promise<void> {
if (!this.client) {
logger.error("Redis client is not initialized.");
throw new Error("Redis client is not initialized.");
}
try {
if (type === "JSON") {
await this.client.json.set(key, path || "$", value as string);
if (expiresInSeconds) {
await this.client.expire(key, expiresInSeconds);
}
} else if (type === "STRING") {
if (expiresInSeconds) {
await this.client.set(key, value as string, {
EX: expiresInSeconds,
});
} else {
await this.client.set(key, value as string);
}
} else {
throw new Error(`Invalid type: ${type}`);
}
} catch (error) {
logger.error(`Error setting value in Redis for key: ${key}`);
logger.error(error as Error);
throw error;
}
}
public async delete(type: "JSON" | "STRING", key: string): Promise<void> {
if (!this.client) {
logger.error("Redis client is not initialized.");
throw new Error("Redis client is not initialized.");
}
try {
if (type === "JSON") {
await this.client.json.del(key);
} else if (type === "STRING") {
await this.client.del(key);
} else {
throw new Error(`Invalid type: ${type}`);
}
} catch (error) {
logger.error(`Error deleting value from Redis for key: ${key}`);
logger.error(error as Error);
throw error;
}
}
public async expire(key: string, seconds: number): Promise<void> {
if (!this.client) {
logger.error("Redis client is not initialized.");
throw new Error("Redis client is not initialized.");
}
try {
await this.client.expire(key, seconds);
} catch (error) {
logger.error(`Error expiring key in Redis: ${key}`);
logger.error(error as Error);
throw error;
}
}
}
export const redis: {
initialize: () => Promise<RedisJson>;
getInstance: () => RedisJson;
} = {
initialize: RedisJson.initialize,
getInstance: RedisJson.getInstance,
};
export { RedisJson };

View file

@ -1,3 +1,5 @@
/* eslint-disable prettier/prettier */
import { booruConfig } from "@config/booru"; import { booruConfig } from "@config/booru";
export function timestampToReadable(timestamp?: number): string { export function timestampToReadable(timestamp?: number): string {
@ -74,6 +76,7 @@ export function determineBooru(
export function postExpectedFormat( export function postExpectedFormat(
booru: IBooruConfig, booru: IBooruConfig,
posts: BooruPost[] | BooruPost, posts: BooruPost[] | BooruPost,
tag_format: string = "string",
): { posts: BooruPost[] } | null { ): { posts: BooruPost[] } | null {
if (!posts) return null; if (!posts) return null;
@ -89,9 +92,12 @@ export function postExpectedFormat(
post_url: post_url:
post.post_url ?? post.post_url ??
`https://${booru.endpoint}/posts/${post.id}`, `https://${booru.endpoint}/posts/${post.id}`,
tags: Object.values(post.tags || {}) tags:
.flat() tag_format === "unformatted"
.join(" "), ? post.tags
: Object.values(post.tags || {})
.flat()
.join(" "),
}; };
}), }),
}; };

View file

@ -1,11 +1,9 @@
import { redis } from "@database/redis";
import { logger } from "@helpers/logger"; import { logger } from "@helpers/logger";
import { serverHandler } from "./server"; import { serverHandler } from "./server";
async function main(): Promise<void> { async function main(): Promise<void> {
try { try {
await redis.initialize();
serverHandler.initialize(); serverHandler.initialize();
} catch (error) { } catch (error) {
throw error; throw error;

View file

@ -1,7 +1,6 @@
import { determineBooru } from "@helpers/char"; import { determineBooru } from "@helpers/char";
import { fetch } from "bun"; import { fetch } from "bun";
import { redis } from "@/database/redis";
import { logger } from "@/helpers/logger"; import { logger } from "@/helpers/logger";
const routeDef: RouteDef = { const routeDef: RouteDef = {
@ -17,7 +16,6 @@ async function handler(
query: Query, query: Query,
params: Params, params: Params,
): Promise<Response> { ): Promise<Response> {
const { force } = query as { force: string };
const { booru, tag } = params as { booru: string; tag: string }; const { booru, tag } = params as { booru: string; tag: string };
if (!booru) { if (!booru) {
@ -47,6 +45,8 @@ async function handler(
} }
const booruConfig: IBooruConfig | null = determineBooru(booru); const booruConfig: IBooruConfig | null = determineBooru(booru);
const isE621: boolean = booruConfig?.name === "e621.net";
const isGelbooru: boolean = booruConfig?.name === "gelbooru.com";
if (!booruConfig) { if (!booruConfig) {
return Response.json( return Response.json(
@ -74,8 +74,6 @@ async function handler(
); );
} }
const isE621: boolean = booruConfig.name === "e621.net";
if (isE621 && tag.length < 3) { if (isE621 && tag.length < 3) {
return Response.json( return Response.json(
{ {
@ -95,47 +93,6 @@ async function handler(
} }
editedTag = editedTag.replace(/\s/g, "_"); editedTag = editedTag.replace(/\s/g, "_");
const cacheKey: string = `nsfw:${booru}:autocomplete:${editedTag}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", cacheKey);
if (cacheData) {
const dataAsType: { count: number; data: unknown } = cacheData as {
count: number;
data: unknown;
};
if (dataAsType.count === 0) {
return Response.json(
{
success: false,
code: 404,
error: "No results found",
},
{
status: 404,
},
);
}
return Response.json(
{
success: true,
code: 200,
cache: true,
count: dataAsType.count,
data: dataAsType.data,
},
{
status: 200,
},
);
}
}
if (!booruConfig.autocomplete) { if (!booruConfig.autocomplete) {
return Response.json( return Response.json(
{ {
@ -149,12 +106,15 @@ async function handler(
); );
} }
const url: string = `https://${booruConfig.autocomplete}${editedTag}`; let url: string = `https://${booruConfig.autocomplete}${editedTag}`;
if (isGelbooru) {
url += `&api_key=${booruConfig.auth?.api_key}&user_id=${booruConfig.auth?.user_id}`;
}
try { try {
const headers: IBooruConfig["auth"] | undefined = booruConfig.auth const headers: IBooruConfig["auth"] | undefined =
? booruConfig.auth booruConfig.auth && isE621 ? booruConfig.auth : undefined;
: undefined;
const response: Response = await fetch(url, { const response: Response = await fetch(url, {
headers, headers,
@ -203,10 +163,6 @@ async function handler(
const resultCount: number = (data as unknown[]).length; const resultCount: number = (data as unknown[]).length;
if (resultCount === 0) { if (resultCount === 0) {
await redis
.getInstance()
.set("JSON", cacheKey, { count: 0, data }, 60 * 60 * 2); // 2 hours
return Response.json( return Response.json(
{ {
success: false, success: false,
@ -219,15 +175,10 @@ async function handler(
); );
} }
await redis
.getInstance()
.set("JSON", cacheKey, { count: resultCount, data }, 60 * 60 * 24); // 24 hours
return Response.json( return Response.json(
{ {
success: true, success: true,
code: 200, code: 200,
cache: false,
count: resultCount, count: resultCount,
data, data,
}, },

View file

@ -1,7 +1,6 @@
import { determineBooru, postExpectedFormat } from "@helpers/char"; import { determineBooru, postExpectedFormat } from "@helpers/char";
import { fetch } from "bun"; import { fetch } from "bun";
import { redis } from "@/database/redis";
import { logger } from "@/helpers/logger"; import { logger } from "@/helpers/logger";
const routeDef: RouteDef = { const routeDef: RouteDef = {
@ -17,7 +16,9 @@ async function handler(
query: Query, query: Query,
params: Params, params: Params,
): Promise<Response> { ): Promise<Response> {
const { force } = query as { force: string }; const { tag_format } = query as {
tag_format: string;
};
const { booru, id } = params as { booru: string; id: string }; const { booru, id } = params as { booru: string; id: string };
if (!booru || !id) { if (!booru || !id) {
@ -34,6 +35,8 @@ async function handler(
} }
const booruConfig: IBooruConfig | null = determineBooru(booru); const booruConfig: IBooruConfig | null = determineBooru(booru);
const isE621: boolean = booruConfig?.name === "e621.net";
const isGelbooru: boolean = booruConfig?.name === "gelbooru.com";
if (!booruConfig) { if (!booruConfig) {
return Response.json( return Response.json(
@ -64,38 +67,19 @@ async function handler(
const funcString: string | [string, string] = booruConfig.functions.id; const funcString: string | [string, string] = booruConfig.functions.id;
let url: string = `https://${booruConfig.endpoint}/${booruConfig.functions.id}${id}`; let url: string = `https://${booruConfig.endpoint}/${booruConfig.functions.id}${id}`;
if (isGelbooru) {
url += `&api_key=${booruConfig.auth?.api_key}&user_id=${booruConfig.auth?.user_id}`;
}
if (Array.isArray(funcString)) { if (Array.isArray(funcString)) {
const [start, end] = funcString; const [start, end] = funcString;
url = `https://${booruConfig.endpoint}/${start}${id}${end}`; url = `https://${booruConfig.endpoint}/${start}${id}${end}`;
} }
const cacheKey: string = `nsfw:${booru}:${id}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", cacheKey);
if (cacheData) {
return Response.json(
{
success: true,
code: 200,
cache: true,
post:
(cacheData as { posts: BooruPost[] }).posts[0] || null,
},
{
status: 200,
},
);
}
}
try { try {
const headers: IBooruConfig["auth"] | undefined = booruConfig.auth const headers: IBooruConfig["auth"] | undefined =
? booruConfig.auth booruConfig.auth && isE621 ? booruConfig.auth : undefined;
: undefined;
const response: Response = await fetch(url, { const response: Response = await fetch(url, {
headers, headers,
@ -166,6 +150,7 @@ async function handler(
const expectedData: { posts: BooruPost[] } | null = postExpectedFormat( const expectedData: { posts: BooruPost[] } | null = postExpectedFormat(
booruConfig, booruConfig,
posts, posts,
tag_format,
); );
if (!expectedData) { if (!expectedData) {
@ -187,13 +172,10 @@ async function handler(
); );
} }
await redis.getInstance().set("JSON", cacheKey, expectedData, 60 * 30); // 30 minutes
return Response.json( return Response.json(
{ {
success: true, success: true,
code: 200, code: 200,
cache: false,
post: expectedData?.posts[0] || null, post: expectedData?.posts[0] || null,
}, },
{ {

View file

@ -7,7 +7,6 @@ import {
} from "@helpers/char"; } from "@helpers/char";
import { fetch } from "bun"; import { fetch } from "bun";
import { redis } from "@/database/redis";
import { logger } from "@/helpers/logger"; import { logger } from "@/helpers/logger";
const routeDef: RouteDef = { const routeDef: RouteDef = {
@ -24,16 +23,17 @@ async function handler(
query: Query, query: Query,
params: Params, params: Params,
): Promise<Response> { ): Promise<Response> {
const { force } = query as { force: string };
const { booru } = params as { booru: string }; const { booru } = params as { booru: string };
const { const {
tags, tags,
results = 5, results = 5,
excludeTags, excludeTags,
tag_format = "formatted",
} = requestBody as { } = requestBody as {
tags: string[]; tags: string[];
results: number; results: number;
excludeTags: string[]; excludeTags: string[];
tag_format: string;
}; };
if (!booru) { if (!booru) {
@ -120,6 +120,7 @@ async function handler(
} }
const isE621: boolean = booruConfig.name === "e621.net"; const isE621: boolean = booruConfig.name === "e621.net";
const isGelbooru: boolean = booruConfig.name === "gelbooru.com";
const formattedTags: string = tags ? tagsToExpectedFormat(tags) : ""; const formattedTags: string = tags ? tagsToExpectedFormat(tags) : "";
const formattedExcludeTags: string = excludeTags const formattedExcludeTags: string = excludeTags
@ -163,36 +164,24 @@ async function handler(
parts.push("&"); parts.push("&");
} }
if (isGelbooru) {
parts.push("api_key");
parts.push(booruConfig.auth?.apiKey || "");
parts.push("&");
parts.push("user_id");
parts.push(booruConfig.auth?.userId || "");
parts.push("&");
}
const queryParams: string = [tagsString(), page, resultsString] const queryParams: string = [tagsString(), page, resultsString]
.filter(Boolean) .filter(Boolean)
.join("&"); .join("&");
parts.push(queryParams); parts.push(queryParams);
console.log("URL", parts.join(""));
return parts.join(""); return parts.join("");
}; };
const noResultsCacheKey: string = `nsfw:${booru}:random:noResults:${tagsString()}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", noResultsCacheKey);
if (cacheData) {
return Response.json(
{
success: false,
code: 404,
cache: true,
error: "No posts found with the given tags",
},
{
status: 404,
},
);
}
}
const config: { maxPage: number; maxTries: number } = { const config: { maxPage: number; maxTries: number } = {
maxPage: 12, maxPage: 12,
maxTries: 6, maxTries: 6,
@ -200,44 +189,6 @@ async function handler(
let state: { tries: number; page: number } = { tries: 0, page: 16 }; let state: { tries: number; page: number } = { tries: 0, page: 16 };
while (state.tries < config.maxTries) { while (state.tries < config.maxTries) {
if (state.tries === config.maxTries) {
state.page = 0;
} else {
const oldPage: number = state.page;
do {
state.page = Math.floor(Math.random() * config.maxPage);
} while (state.page === oldPage);
}
const cacheKey: string = `nsfw:${booru}:random:${tagsString()}:${results}:${state.page}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", cacheKey);
if (cacheData) {
const minimizedPosts: BooruPost[] = minPosts(
(cacheData as { posts: BooruPost[] }).posts,
results,
);
const shuffledPosts: BooruPost[] = shufflePosts(minimizedPosts);
return Response.json(
{
success: true,
code: 200,
cache: true,
posts: shuffledPosts,
},
{
status: 200,
},
);
}
}
const url: string = getUrl(pageString(state.page), resultsString); const url: string = getUrl(pageString(state.page), resultsString);
try { try {
@ -281,7 +232,7 @@ async function handler(
const parsedData: Data = data as Data; const parsedData: Data = data as Data;
let posts: BooruPost[] = []; let posts: BooruPost[] = [];
if (booruConfig.name === "realbooru.com") { if (booruConfig.name === "realbooru.com" || isGelbooru) {
posts = parsedData.post || []; posts = parsedData.post || [];
} else { } else {
if (parsedData.post) { if (parsedData.post) {
@ -296,22 +247,17 @@ async function handler(
if (posts.length === 0) continue; if (posts.length === 0) continue;
let expectedData: { posts: BooruPost[] } | null = let expectedData: { posts: BooruPost[] } | null =
postExpectedFormat(booruConfig, posts); postExpectedFormat(booruConfig, posts, tag_format);
if (!expectedData) continue; if (!expectedData) continue;
expectedData.posts = minPosts(expectedData.posts, results);
expectedData.posts = shufflePosts(expectedData.posts); expectedData.posts = shufflePosts(expectedData.posts);
expectedData.posts = minPosts(expectedData.posts, results);
await redis
.getInstance()
.set("JSON", cacheKey, expectedData, 60 * 60 * 1); // 1 hours
return Response.json( return Response.json(
{ {
success: true, success: true,
code: 200, code: 200,
cache: false,
posts: expectedData.posts, posts: expectedData.posts,
}, },
{ {
@ -322,11 +268,18 @@ async function handler(
continue; continue;
} finally { } finally {
state.tries++; state.tries++;
if (state.tries >= config.maxTries - 1) {
state.page = 0;
} else {
const oldPage: number = state.page;
do {
state.page = Math.floor(Math.random() * config.maxPage);
} while (state.page === oldPage);
}
} }
} }
await redis.getInstance().set("JSON", noResultsCacheKey, true, 60 * 30); // 30 minutes
logger.error([ logger.error([
"No posts found", "No posts found",
`Booru: ${booru}`, `Booru: ${booru}`,

View file

@ -5,8 +5,6 @@ import {
} from "@helpers/char"; } from "@helpers/char";
import { fetch } from "bun"; import { fetch } from "bun";
import { redis } from "@/database/redis";
const routeDef: RouteDef = { const routeDef: RouteDef = {
method: "POST", method: "POST",
accepts: "*/*", accepts: "*/*",
@ -21,18 +19,19 @@ async function handler(
query: Query, query: Query,
params: Params, params: Params,
): Promise<Response> { ): Promise<Response> {
const { force } = query as { force: string };
const { booru } = params as { booru: string }; const { booru } = params as { booru: string };
const { const {
page = 0, page = 0,
tags, tags,
results = 5, results = 5,
excludeTags, excludeTags,
tag_format = "formatted",
} = requestBody as { } = requestBody as {
page: 0; page: 0;
tags: string[] | string; tags: string[] | string;
results: number; results: number;
excludeTags: string[] | string; excludeTags: string[] | string;
tag_format: string;
}; };
if (!booru) { if (!booru) {
@ -119,6 +118,7 @@ async function handler(
} }
const isE621: boolean = booruConfig.name === "e621.net"; const isE621: boolean = booruConfig.name === "e621.net";
const isGelbooru: boolean = booruConfig.name === "gelbooru.com";
const formattedTags: string = tags ? tagsToExpectedFormat(tags) : ""; const formattedTags: string = tags ? tagsToExpectedFormat(tags) : "";
const formattedExcludeTags: string = excludeTags const formattedExcludeTags: string = excludeTags
@ -162,6 +162,15 @@ async function handler(
parts.push("&"); parts.push("&");
} }
if (isGelbooru) {
parts.push("api_key");
parts.push(booruConfig.auth?.apiKey || "");
parts.push("&");
parts.push("user_id");
parts.push(booruConfig.auth?.userId || "");
parts.push("&");
}
const queryParams: string = [tagsString(), pageString(), resultsString] const queryParams: string = [tagsString(), pageString(), resultsString]
.filter(Boolean) .filter(Boolean)
.join("&"); .join("&");
@ -170,27 +179,6 @@ async function handler(
return parts.join(""); return parts.join("");
}; };
const cacheKey: string = `nsfw:${booru}:${formattedTags}:${formattedExcludeTags}:${safePage}:${safeResults}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", cacheKey);
if (cacheData) {
return Response.json(
{
success: true,
code: 200,
cache: true,
posts: (cacheData as { posts: BooruPost[] }).posts,
},
{
status: 200,
},
);
}
}
try { try {
const headers: IBooruConfig["auth"] | undefined = booruConfig.auth const headers: IBooruConfig["auth"] | undefined = booruConfig.auth
? booruConfig.auth ? booruConfig.auth
@ -256,6 +244,7 @@ async function handler(
const expectedData: { posts: BooruPost[] } | null = postExpectedFormat( const expectedData: { posts: BooruPost[] } | null = postExpectedFormat(
booruConfig, booruConfig,
posts, posts,
tag_format,
); );
if (!expectedData) { if (!expectedData) {
@ -271,13 +260,10 @@ async function handler(
); );
} }
await redis.getInstance().set("JSON", cacheKey, expectedData, 60 * 30); // 30 minutes
return Response.json( return Response.json(
{ {
success: true, success: true,
code: 200, code: 200,
cache: false,
posts: expectedData.posts, posts: expectedData.posts,
}, },
{ {

7
types/config.d.ts vendored
View file

@ -4,13 +4,6 @@ type Environment = {
development: boolean; development: boolean;
}; };
type RedisConfig = {
host: string;
port: number;
username?: string;
password?: string;
};
type IBooruDefaults = { type IBooruDefaults = {
search: string; search: string;
random: string; random: string;