import Fastify, { FastifyPluginCallback, FastifyReply, FastifyRequest, FastifyReplyContext, FastifyPluginAsync, FastifyServerOptions } from 'fastify' import fastifySwagger from '@fastify/swagger' import fastifySwaggerUi from '@fastify/swagger-ui' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'url' import { dirname, join } from 'node:path' import { Config } from './config' import { VtuberRecord, VtuberResponse, VtuberDataScrape } from './schemas.ts' import scrapeVtuberData from './scrapeVtuberData.ts' import { getPlaylistUrl } from './ytdlp.ts' import { getRandomRoom } from './cb.ts' import { getPackageVersion } from '@futureporn/utils/file.ts' type VtuberDataRequest = FastifyRequest<{ Querystring: { url: string } }> const __dirname = dirname(fileURLToPath(import.meta.url)); const swaggerDarkCss = readFileSync(join(__dirname, './css/SwaggerDark.css'), { encoding: 'utf-8' }) const version = getPackageVersion(join(__dirname, '../package.json')) async function fastifySetup(configs: Config) { const fastify = Fastify({ logger: true }) await fastify.register(fastifySwagger, { openapi: { info: { title: '@futureporn/scout', description: 'Vtuber data acquisition API', version }, } }) await fastify.register(fastifySwaggerUi, { theme: { title: '@fp/scout', css: [ { filename: 'SwaggerDark.css', content: swaggerDarkCss } ] }, routePrefix: '/', uiConfig: { docExpansion: 'list', deepLinking: true }, uiHooks: { onRequest: function (request: FastifyRequest, reply: FastifyReply, next: any) { next() }, preHandler: function (request: FastifyRequest, reply: FastifyReply, next: any) { next() } }, staticCSP: true, transformStaticCSP: (header: any) => header, transformSpecification: (swaggerObject: any, request: FastifyRequest, reply: FastifyReply) => { return swaggerObject }, transformSpecificationClone: true }) fastify.addSchema(VtuberResponse) fastify.addSchema(VtuberRecord) fastify.addSchema(VtuberDataScrape) fastify.get('/chaturbate/random-room', { schema: { response: { '2xx': { type: 'object' } }, tags: ['chaturbate'] } }, async (req, reply) => { const room = await getRandomRoom() console.log(room) reply.type('application/json').send(JSON.stringify(room)) }) fastify.get('/ytdlp/playlist-url', { schema: { querystring: { type: 'object', properties: { url: { type: 'string' } } }, response: { '2xx': { error: { type: 'boolean' }, message: { type: 'string' }, data: { type: 'object', properties: { url: { type: 'string' } }} } }, tags: ['yt-dlp'] } }, async (req: VtuberDataRequest, reply) => { try { const playlistUrl = await getPlaylistUrl(req.query.url) console.log(`playlistUrl=${playlistUrl}`) reply.type('application/json').send(JSON.stringify({ data: { url: playlistUrl } })) } catch (e) { reply.type('application/json').send(JSON.stringify({ data: null, error: e })) } }) fastify.get('/vtuber/data', { schema: { querystring: { type: 'object', properties: { url: { type: 'string', description: 'URL of a vtuber profile on Chaturbate or Fansly. ex: https://chaturbate.com/projektmelody' } } }, response: { '2xx': { $ref: 'VtuberDataScrape' } }, tags: [ 'vtuber' ] } }, async (req: VtuberDataRequest, reply) => { console.log(`we received a request with url=${req.query.url}`) const data = await scrapeVtuberData(req.query.url) reply.type('application/json').send(data) }) fastify.listen({ host: '0.0.0.0', port: configs.port }, function (err, address) { console.log(`@futureporn/scout listening on ${address}`) if (err) { fastify.log.error(err) process.exit(1) } }) } export default fastifySetup