Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(route): add route Gelbooru #18320

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,10 @@ export type Config = {
zsxq: {
accessToken?: string;
};
gelbooru: {
apiKey?: string;
userId?: string;
};
};

const value: Config | Record<string, any> = {};
Expand Down Expand Up @@ -827,6 +831,10 @@ const calculateValue = () => {
zsxq: {
accessToken: envs.ZSXQ_ACCESS_TOKEN,
},
gelbooru: {
apiKey: envs.GELBOORU_API_KEY,
userId: envs.GELBOORU_USER_ID,
},
};

for (const name in _value) {
Expand Down
7 changes: 7 additions & 0 deletions lib/routes/gelbooru/namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Namespace } from '@/types';

export const namespace: Namespace = {
name: 'Gelbooru',
url: 'gelbooru.com',
description: 'gelbooru posts',
};
113 changes: 113 additions & 0 deletions lib/routes/gelbooru/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Route, ViewType } from '@/types';
import { Context } from 'hono';
import { parseDate } from '@/utils/parse-date';
import { renderDesc, getAPIKeys } from './utils';

import got from '@/utils/got';
import queryString from 'query-string';

export const route: Route = {
path: '/post/:tags?/:quality?',
categories: ['picture'],
view: ViewType.Pictures,
example: '/gelbooru/post/1girl rating:general',
parameters: {
tags: '要搜索的标签,多个标签用 ` `(空格)隔开',
quality: {
description: '图片质量,可选值为 `sample`(压缩后的图片,推荐值) 或 `orig`(原图),默认为 `sample`',
default: 'sample',
},
},
features: {
requireConfig: [
{
name: 'GELBOORU_API_KEY',
description: 'Gelbooru 偶尔会开启 API 认证,需配合 `GELBOORU_USER_ID`,从 `https://gelbooru.com/index.php?page=account&s=options` 获取',
optional: true,
},
{
name: 'GELBOORU_USER_ID',
description: '参见 `GELBOORU_API_KEY`',
optional: true,
},
],
requirePuppeteer: false,
antiCrawler: false,
supportRadar: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['gelbooru.com/index.php'],
},
],
name: 'Gelbooru标签查询',
maintainers: ['magicFeirl'],
description: `
- 默认查询: \`/gelbooru/post\` 功能等同查询 Gelbooru 网站最新的投稿
- 单标签查询: \`/gelbooru/post/1girl\` 查询 \`1girl\` 的最新投稿
- 多标签查询: \`/gelbooru/post/1girl school_uniform rating:general\`
- 指定为原图: \`/gelbooru/post/1girl school_uniform rating:general/orig\`
- 更多例子: 请参考 Gelbooru 官方 wiki https://gelbooru.com/index.php?page=wiki&s=&s=view&id=25921

**可选的 URL 参数**
- limit 页面返回数据量,默认 40,可选 1 ~ 100

e.g.: \`/gelbooru/post?limit=20&\`
`,
handler,
};

async function handler(ctx: Context) {
const { tags: _tags = '', quality = 'sample' }: { tags?: string; quality?: 'sample' | 'orig' } = ctx.req.param();

const tags = decodeURIComponent(_tags).trim();

const { limit = 40 }: { limit?: number } = ctx.req.query();
const { apiKey, useId } = getAPIKeys();

const response = await got({
url: 'https://gelbooru.com/index.php',
searchParams: queryString.stringify({
page: 'dapi',
s: 'post',
q: 'index',
tags,
api_key: apiKey,
user_id: useId,
limit: limit <= 0 || limit > 100 ? 40 : limit,
json: 1,
}),
});

const posts = response.data.post;

return {
title: tags ? `${tags} - gelbooru.com` : 'gelbooru.com post list',
link: `https://gelbooru.com/index.php?page=post&s=list&tags=${tags}`,
icon: 'https://gelbooru.com/favicon.png',
logo: 'https://gelbooru.com/favicon.png',
description: 'Gelbooru post list',
item: posts.map((post) => ({
title: post.id,
id: post.id,
link: `https://gelbooru.com/index.php?page=post&s=view&id=${post.id}`,
author: post.owner,
pubDate: parseDate(post.created_at),
description: renderDesc(post, `https://gelbooru.com/index.php?page=post&s=view&id=${post.id}`, quality),
upvotes: post.score,
updated: parseDate(post.change),
media: {
content: {
url: post.file_url,
},
thumbnail: {
url: post.preview_url,
},
},
category: post.tags.split(/\s+/g),
})),
};
}
19 changes: 19 additions & 0 deletions lib/routes/gelbooru/templates/description.art
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div>
{{if isVideo}}
<video controls="true" muted src="{{contentURL}}"></video>
{{else}}
<img src="{{contentURL}}" />
{{/if}}
</div>
<h2>Info of <a href="{{link}}">#{{id}}</a>: </h2>
<p>
{{if isHttp}}
<a href="{{source}}">Source</a>
{{else}}
<span>Source: {{source}}</span>
{{/if}}
({{sourceHost}})
</p>
<p>Upload by: <a href="https://gelbooru.com/index.php?page=post&s=list&tags=user:{{owner}}">{{owner}}</a></p>
<p>Score: {{score}}</p>
<p>Tags: <p>{{tags}}</p></p>
43 changes: 43 additions & 0 deletions lib/routes/gelbooru/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);

import path from 'node:path';
import { art } from '@/utils/render';
import { config } from '@/config';

export function renderDesc(post, link, quality: 'sample' | 'orig') {
const { id, source, owner, file_url: fileUrl, tags, score } = post;
const isHttp = /^https?:\/\//.test(source);
const sourceHost = isHttp ? new URL(source).host : source || 'unknown';
const imgQualityMap = { sample: 'sample_url', orig: 'file_url' };

// 判断是否是视频链接
const videoExtList = ['mp4', 'webm'];
const fileExt = fileUrl.slice(fileUrl.lastIndexOf('.') + 1);
const isVideo = videoExtList.includes(fileExt);
// 如果是视频则始终使用 fileUrl(原文件)
let contentURL = post[imgQualityMap[quality]] || fileUrl;
if (isVideo) {
contentURL = fileUrl;
}

return art(path.join(__dirname, 'templates/description.art'), {
id,
source,
owner,
tags,
link,
isHttp,
sourceHost,
contentURL,
isVideo,
score: score || 0,
});
}

export function getAPIKeys() {
return {
apiKey: config.gelbooru.apiKey || '',
userId: config.gelbooru.userId || '',
};
}