//
var express = require('@runkit/runkit/express-endpoint/1.0.0')
var app = express(exports)
const assert = require('assert')
const bodyParser = require('body-parser')
const statuses = require('statuses')
const cors = require('cors')
const Promise = require('bluebird')
const cheerio = require('cheerio')
const _debug = require('debug')
_debug.enable('*')
const debug = _debug('gelbooru')
const superagent = require("superagent");
//
_debug.log = console.log.bind(console)
Promise.config({
longStackTraces: true
})
const BASE_URL = 'http://gelbooru.com/index.php'
function TooMutchResultError() {}
TooMutchResultError.prototype = Object.create(Error.prototype);
function NotFoundError() {}
TooMutchResultError.prototype = Object.create(Error.prototype);
//
app.use(cors())
// http://gelbooru.com/index.php?page=dapi&s=post&q=index
app.get('/', (req, res) => res.send('up'))
// http://gelbooru.com/index.php?page=dapi&s=post&q=index
app.get('/posts', getLatestPosts)
app.get('/posts/:id', getPostById)
// http://gelbooru.com/index.php?page=tags&s=list&tags=nami_*&sort=desc&order_by=index_count
app.get('/tags/:name', getTagsStats)
app.get('/macro/characters/:name', getMacroOfCharacter)
app.use(errorHandler)
//
function getLatestPosts (req, res, next) {
debug('getLatestPosts')
const page = Math.max(0, Number(req.query.page) || 0)
const limit = Math.max(0, Number(req.query.limit) || 100)
const tags = false ||
typeof req.query.tags === 'string' ? [req.query.tags] :
req.query.tags || []
return Promise.resolve()
.then(() => queryGelbooruPosts({
limit,
page,
tags
}))
.then((result) => res.send(result))
.catch(next)
}
function getPostById (req, res, next) {
debug('getPostById')
const id = Math.max(0, Number(req.params.id) || 0)
return Promise.resolve()
.then(() => asyncRequest(`${BASE_URL}?page=dapi&s=post&q=index&id=${id}`))
.then((xml) => cheerio.load(xml, {
normalizeWhitespace: true,
xmlMode: true
}))
.then(format$ToPostData)
.then((result) => res.send(result))
.catch(next)
}
function getTagsStats (req, res, next) {
debug('getTagsStats')
const name = req.params.name
const {order, sort} = req.query
return Promise.resolve()
.then(() => queryGelbooruTags(
name,
{
order,
sort
}
))
.catch(TooMutchResultError, () => {
return Promise.reject({
status: 413
})
})
.then((result) => res.send(result))
.catch(next)
}
function getMacroOfCharacter (req, res, next) {
debug('getMacroOfCharacter')
const name = req.params.name
return Promise.resolve()
.then(() => macroCharacterQuery(name))
.catch(() => {
return Promise.reject({
status: 404
})
})
.then((result) => res.send(result))
.catch(next)
}
//
// BUISNESS
//
function queryGelbooruPosts (options) {
debug('queryGelbooruPosts options=%j', options)
const {
limit,
page,
tags
} = options
return Promise.resolve()
.then(() => asyncRequest(`${BASE_URL}?page=dapi&s=post&q=index&pid=${page}&tags=${tags.join('+')}&limit=${limit}`))
.then((xml) => cheerio.load(xml, {
normalizeWhitespace: true,
xmlMode: true
}))
.then(format$ToPostList)
.then((result) => Object.assign(result, {page: page}))
}
function queryGelbooruTags (name, options) {
debug('queryGelbooruTags name=%s options=%j', name, options)
const {order, sort} = options
return Promise.resolve()
.then(() => asyncRequest(`${BASE_URL}?page=tags&s=list&tags=${name}&sort=${sort}&order_by=${order}`))
.then((rawHtml) => cheerio.load(rawHtml, {}))
.then(format$ToTagList)
.then((resultTaglList) => {
let hasError = resultTaglList.length === 1
hasError &= !resultTaglList[0].type
hasError &= !resultTaglList[0].posts
if (hasError) {
const msg = resultTaglList[0].name
if (/No results found/i.test(msg)) {
return []
}
throw new TooMutchResultError()
}
return resultTaglList
})
}
function macroCharacterQuery (name) {
debug('macroCharacterQuery name=%s', name)
return Promise.resolve()
.then(() => queryGelbooruTags(
name,
{order: 'asc', sort: 'tag'}
))
.then((resultTagList) =>
resultTagList.filter((tag) => tag.type === 'character')
)
.then((characterTagList) => {
if (characterTagList.length != 1) {
throw new NotFoundError()
}
return characterTagList[0]
})
.then(({name, posts}) =>
Promise.props({
name,
posts,
ratings: Promise.props({
safe: queryGelbooruPosts({tags: [name, 'rating:safe']}),
questionable: queryGelbooruPosts({tags: [name, 'rating:questionable']}),
explicit: queryGelbooruPosts({tags: [name, 'rating:explicit']})
})
})
)
}
//
// SERVICES
//
function asyncRequest (uri) {
debug('asyncRequest uri=%s', uri)
return new Promise ((resolve, reject) => {
superagent
.get(uri)
.end(function (err, response) {
if (err) {
return reject(err)
}
return resolve(response.text)
})
})
}
//
// FORMATTERS
//
function format$ToPostList ($) {
debug('format$ToPostList')
return {
total: +$('posts').attr('count'),
count: $('posts').children().length,
posts: $('posts').children().map(function () {
const attrs = $(this).attr();
// https://simg3.gelbooru.com/ urls have to be replaced by https://simg3.gelbooru.com//
// so gelbooru doesn't 403 us.
const override = Object.entries(attrs)
.reduce((memo, [key, value]) => Object.assign({}, memo, {
[key]: value.replace('https://simg3.gelbooru.com/', 'https://simg3.gelbooru.com//')
}), {})
return Object.assign({}, attrs, override)
}).toArray()
};
}
function format$ToPostData ($) {
debug('format$ToPostData')
return $('posts').children().first().attr()
}
function format$ToTagList ($) {
debug('format$ToTagList')
const $tagTable = $('table.highlightable', '#content')
return $tagTable.children('tr')
.slice(1) // Remove first header line
.map((i, el) => {
const $td = $(el).children('td')
let posts = $td.eq(0).text()
let name = $td.eq(1).text()
let type = $td.eq(2).text()
type = (type || '').replace(' (edit)', '')
posts = Number(posts)
return {name, posts, type}
})
.toArray()
}
//
function errorHandler (err, req, res, next) {
debug('errorHandler')
const status = err.status || err.statusCode || 500
const body = {
status: status,
message: statuses[status]
}
if (err.stack) body.stack = err.stack
if (err.code) body.code = err.code
if (err.name) body.name = err.name
if (err.type) body.type = err.type
res.status(status).json(body)
}
debug('listening')
'started'