UNPKG

derpibooru-api

Version:

Modern TypeScript implementation of Derpibooru API with Zod validation

211 lines (210 loc) 7.82 kB
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 };