2024-12-03 19:33:43 +00:00
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' ;
2025-11-01 21:42:00 +00:00
import { m , eta , error } from '@/utils/html'
2024-12-03 20:29:03 +00:00
import redis from '@/utils/redis' ;
2024-12-03 19:33:43 +00:00
const app = new Elysia ( )
interface processedVideo {
id : string ;
title : string ;
thumbnail : string ;
published : string ;
deleted? : undefined ;
}
2025-10-14 18:53:40 +00:00
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 ,
2025-11-01 21:13:41 +00:00
title : 'Video Not Found | PreserveTube' ,
manualAnalytics : true
2025-10-14 18:53:40 +00:00
} ) )
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
} )
2024-12-03 19:33:43 +00:00
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 ) ,
}
} )
2025-10-14 18:53:40 +00:00
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
}
2024-12-03 19:33:43 +00:00
const [ videos , channel ] = await Promise . all ( [
getChannelVideos ( id ) ,
getChannel ( id )
] )
2025-10-14 18:53:40 +00:00
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
}
2024-12-03 19:33:43 +00:00
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.
2025-03-21 14:04:45 +00:00
id : video.video_id ,
2024-12-03 19:33:43 +00:00
title : video.title.text ,
thumbnail : video.thumbnails [ 0 ] . url ,
2025-08-16 10:24:26 +00:00
published : video.upcoming?.slice ( 0 , 10 ) || ( video . published . text . endsWith ( 'ago' ) ? convertRelativeToDate ( video . published . text ) : new Date ( video . published . text ) ) . toISOString ( ) . slice ( 0 , 10 )
2024-12-03 19:33:43 +00:00
} ) )
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 ( ) ) ;
2025-10-14 18:53:40 +00:00
const html = await m ( eta . render ( './channel' , {
2024-12-03 19:33:43 +00:00
name : channel.metadata.title ,
avatar : channel.metadata.avatar [ 0 ] . url ,
verified : channel.header.author?.is_verified ,
2025-10-14 18:53:40 +00:00
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
2024-12-03 19:33:43 +00:00
} )
2025-10-14 18:53:40 +00:00
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
}
2024-12-03 19:33:43 +00:00
const archived = await db . selectFrom ( 'videos' )
. select ( [ 'id' , 'title' , 'thumbnail' , 'published' , 'archived' ] )
. where ( 'channelId' , '=' , id )
. orderBy ( 'published desc' )
. execute ( )
2025-10-14 18:53:40 +00:00
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
2024-12-03 19:33:43 +00:00
} )
2025-11-01 21:42:00 +00:00
app . onError ( error )
2024-12-03 19:33:43 +00:00
export default app