173 lines
5.2 KiB
TypeScript
173 lines
5.2 KiB
TypeScript
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 |