import { Elysia } from 'elysia'; import DOMPurify from 'isomorphic-dompurify' import { db } from '@/utils/database' import { getChannel, getChannelVideos } from '@/utils/metadata'; import { convertRelativeToDate } from '@/utils/common'; import { m, eta } from '@/utils/html' import redis from '@/utils/redis'; const app = new Elysia() interface processedVideo { id: string; title: string; thumbnail: string; published: string; deleted?: undefined; } app.get('/watch', async ({ query: { v }, set, redirect, error }) => { if (!v) return error(404) const cached = await redis.get(`watch:${v}:html`) if (cached) { set.headers['Content-Type'] = 'text/html; charset=utf-8' return cached } if (!v.match(/[\w\-_]{11}/)) return error(404) const json = await db.selectFrom('videos') .selectAll() .where('id', '=', v) .executeTakeFirst() if (!json) { const html = await m(eta.render('./watch', { isMissing: true, id: v, title: 'Video Not Found | PreserveTube', manualAnalytics: true })) set.headers['cache-control'] = 'public, no-cache' set.headers['content-type'] = 'text/html; charset=utf-8' return error(404, html) } if (json.disabled) return redirect(`/transparency/${v}`) let transparency: any[] = [] if (json.hasBeenReported) { transparency = await db.selectFrom('reports') .selectAll() .where('target', '=', v) .execute() } const html = await m(eta.render('./watch', { transparency, ...json, description: DOMPurify.sanitize(json.description), title: `${json.title} | PreserveTube`, v_title: json.title, keywords: `${json.title} video archive, ${json.title} ${json.channel} archive`, manualAnalytics: true })) await redis.set(`watch:${v}:html`, html, 'EX', 3600) set.headers['Content-Type'] = 'text/html; charset=utf-8' return html }) app.get('/video/:id', async ({ params: { id }, error }) => { const cached = await redis.get(`video:${id}`) if (cached) return JSON.parse(cached) const json = await db.selectFrom('videos') .selectAll() .where('id', '=', id) .executeTakeFirst() if (!json) return error(404, { error: '404' }) await redis.set(`video:${id}`, JSON.stringify(json), 'EX', 3600) return { ...json, description: DOMPurify.sanitize(json.description), } }) app.get('/channel/:id', async ({ params: { id }, set }) => { const cached = await redis.get(`channel:${id}:html`) if (cached) { set.headers['Content-Type'] = 'text/html; charset=utf-8' return cached } const [videos, channel] = await Promise.all([ getChannelVideos(id), getChannel(id) ]) if (!videos || !channel || videos.error || channel.error) { const html = await m(eta.render('./channel', { failedToFetch: true, id })) set.headers['Content-Type'] = 'text/html; charset=utf-8' return html } const archived = await db.selectFrom('videos') .select(['id', 'title', 'thumbnail', 'published', 'archived']) .where('channelId', '=', id) .execute() const processedVideos: processedVideo[] = videos.map((video: any) => ({ // it would be impossible to set types for youtube output... they change it every day. id: video.video_id, title: video.title.text, thumbnail: video.thumbnails[0].url, published: video.upcoming?.slice(0, 10) || (video.published.text.endsWith('ago') ? convertRelativeToDate(video.published.text) : new Date(video.published.text)).toISOString().slice(0, 10) })) archived.forEach(v => { const existingVideoIndex = processedVideos.findIndex(video => video.id === v.id); if (existingVideoIndex !== -1) { processedVideos[existingVideoIndex] = v; } else { processedVideos.push({ ...v, deleted: undefined }); } }); processedVideos.sort((a: any, b: any) => new Date(b.published).getTime() - new Date(a.published).getTime()); const html = await m(eta.render('./channel', { name: channel.metadata.title, avatar: channel.metadata.avatar[0].url, verified: channel.header.author?.is_verified, videos: processedVideos, title: `${channel.metadata.title} | PreserveTube`, keywords: `${channel.metadata.title} archive, ${channel.metadata.title} channel archive, ${channel.metadata.title} deleted video, ${channel.metadata.title} video deleted` })) await redis.set(`channel:${id}:html`, html, 'EX', 3600) set.headers['Content-Type'] = 'text/html; charset=utf-8' return html }) app.get('/channel/:id/videos', async ({ params: { id }, set }) => { const cached = await redis.get(`channelVideos:${id}:html`) if (cached) { set.headers['Content-Type'] = 'text/html; charset=utf-8' return cached } const archived = await db.selectFrom('videos') .select(['id', 'title', 'thumbnail', 'published', 'archived']) .where('channelId', '=', id) .orderBy('published desc') .execute() const html = await m(eta.render('./channel-videos', { videos: archived, title: `${id} videos | PreserveTube`, keywords: `${id} archive, ${id} channel archive, ${id} deleted video, ${id} video deleted` })) await redis.set(`channelVideos:${id}:html`, html, 'EX', 3600) set.headers['Content-Type'] = 'text/html; charset=utf-8' return html }) export default app