derpibooru-api
Version:
Modern TypeScript implementation of Derpibooru API with Zod validation
211 lines (210 loc) • 7.82 kB
JavaScript
import { z, ZodError } from "zod";
import { safe, ImageResponseSchema, TagSchema, FilterResponseSchema, UserResponseSchema, OembedResponseSchema, SearchImagesResponseSchema, SearchTagsResponseSchema, CommentResponseSchema, PostResponseSchema, SearchCommentsResponseSchema, SearchGalleriesResponseSchema, SearchPostsResponseSchema, } from "./index.js";
class DerpibooruClient {
#baseUrl;
#apiKey;
constructor(config = {}) {
this.#baseUrl = config.baseUrl ?? "https://derpibooru.org";
this.#apiKey = config.apiKey;
}
#createUrl(path, searchParams) {
const url = new URL(path, this.#baseUrl);
if (this.#apiKey) {
url.searchParams.set("key", this.#apiKey);
}
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
return url;
}
async #makeRequest(path, options = {}, schema) {
const url = this.#createUrl(path, options.searchParams);
const fetchResult = await safe(fetch(url, {
method: options.method ?? "GET",
headers: {
"Content-Type": "application/json",
},
body: options.body ? JSON.stringify(options.body) : undefined,
}));
if (!fetchResult.success) {
return { success: false, error: fetchResult.error };
}
if (!fetchResult.data.ok) {
return {
success: false,
error: `HTTP error! status: ${fetchResult.data.status}`,
};
}
const jsonResult = await safe(fetchResult.data.json());
if (!jsonResult.success) {
return { success: false, error: jsonResult.error };
}
const parseResult = safe(() => schema.parse(jsonResult.data), {
processError: (error) => JSON.stringify(error.issues, null, 2),
});
if (!parseResult.success) {
return {
success: false,
error: `Response validation failed: ${parseResult.error}`,
};
}
return { success: true, data: parseResult.data };
}
async searchImages(query, page = 1, perPage = 10) {
return this.#makeRequest("/api/v1/json/search/images", {
searchParams: {
q: query,
page: String(page),
per_page: String(perPage),
},
}, SearchImagesResponseSchema);
}
async getImage(id) {
const result = await this.#makeRequest(`/api/v1/json/images/${id}`, {}, z.object({ image: ImageResponseSchema }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.image };
}
async getFeaturedImage() {
const result = await this.#makeRequest("/api/v1/json/images/featured", {}, z.object({ image: ImageResponseSchema }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.image };
}
async searchTags(query, page = 1) {
return this.#makeRequest("/api/v1/json/search/tags", {
searchParams: {
q: query,
page: String(page),
},
}, SearchTagsResponseSchema);
}
async getTag(tagId) {
const result = await this.#makeRequest(`/api/v1/json/tags/${tagId}`, {}, z.object({ tag: TagSchema }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.tag };
}
async getFilter(id) {
const result = await this.#makeRequest(`/api/v1/json/filters/${id}`, {}, z.object({ filter: FilterResponseSchema }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.filter };
}
async getSystemFilters(page = 1) {
const result = await this.#makeRequest("/api/v1/json/filters/system", {
searchParams: { page: String(page) },
}, z.object({ filters: z.array(FilterResponseSchema) }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.filters };
}
async getUser(id) {
if (!this.#apiKey) {
return {
success: false,
error: "API key is required for user retrieval",
};
}
const result = await this.#makeRequest(`/api/v1/json/profiles/${id}`, {}, z.object({ user: UserResponseSchema }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.user };
}
async getOembed(url) {
return this.#makeRequest("/api/v1/json/oembed", {
searchParams: { url },
}, OembedResponseSchema);
}
async uploadImage(params) {
if (!this.#apiKey) {
return { success: false, error: "API key is required for image upload" };
}
const result = await this.#makeRequest("/api/v1/json/images", {
method: "POST",
body: {
url: params.url,
image: {
description: params.description,
tags: params.tags?.join(", "),
source_url: params.source_url,
},
},
}, z.object({ image: ImageResponseSchema }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.image };
}
async reverseImageSearch(url, distance = 0.25) {
return this.#makeRequest("/api/v1/json/search/reverse", {
method: "POST",
searchParams: {
url,
distance: String(distance),
},
}, SearchImagesResponseSchema);
}
async getUserFilters(page = 1) {
if (!this.#apiKey) {
return {
success: false,
error: "API key is required for user filters retrieval",
};
}
const result = await this.#makeRequest("/api/v1/json/filters/user", {
searchParams: { page: String(page) },
}, z.object({ filters: z.array(FilterResponseSchema) }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.filters };
}
async getComment(id) {
const result = await this.#makeRequest(`/api/v1/json/comments/${id}`, {}, z.object({ comment: CommentResponseSchema }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.comment };
}
async searchComments(query, page = 1) {
return this.#makeRequest("/api/v1/json/search/comments", {
searchParams: {
q: query,
page: String(page),
},
}, SearchCommentsResponseSchema);
}
async searchGalleries(query, page = 1) {
return this.#makeRequest("/api/v1/json/search/galleries", {
searchParams: {
q: query,
page: String(page),
},
}, SearchGalleriesResponseSchema);
}
async getPost(id) {
const result = await this.#makeRequest(`/api/v1/json/posts/${id}`, {}, z.object({ post: PostResponseSchema }));
if (!result.success) {
return result;
}
return { success: true, data: result.data.post };
}
async searchPosts(query, page = 1) {
return this.#makeRequest("/api/v1/json/search/posts", {
searchParams: {
q: query,
page: String(page),
},
}, SearchPostsResponseSchema);
}
}
export { DerpibooruClient };