beta testing the html serving via backend
This commit is contained in:
parent
ce7bb54f8c
commit
c673a4dbc9
|
|
@ -14,9 +14,13 @@
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@elysiajs/static": "^1.4.0",
|
||||||
|
"@types/html-minifier-next": "^2.1.0",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"elysia": "^1.1.25",
|
"elysia": "^1.1.25",
|
||||||
|
"eta": "^4.0.1",
|
||||||
|
"html-minifier-next": "^2.1.4",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
"isomorphic-dompurify": "^2.18.0",
|
"isomorphic-dompurify": "^2.18.0",
|
||||||
"kysely": "^0.27.4",
|
"kysely": "^0.27.4",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" d="M5 21q-.825 0-1.413-.588T3 19V6.5q0-.375.125-.675t.325-.575l1.4-1.7q.2-.275.5-.413T6 3h12q.35 0 .65.137t.5.413l1.4 1.7q.2.275.325.575T21 6.5V19q0 .825-.588 1.413T19 21H5Zm.4-15h13.2l-.85-1H6.25L5.4 6ZM12 17.575q.2 0 .375-.062t.325-.213l2.6-2.6q.275-.275.275-.7t-.275-.7q-.275-.275-.7-.275t-.7.275l-.9.9V11q0-.425-.288-.713T12 10q-.425 0-.713.288T11 11v3.2l-.9-.9q-.275-.275-.7-.275t-.7.275q-.275.275-.275.7t.275.7l2.6 2.6q.15.15.325.212t.375.063Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 560 B |
|
|
@ -0,0 +1,52 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
cw3z0oKcZ3Irwfu2LQ==
|
||||||
|
=5zdM
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /playlist
|
||||||
|
Disallow: /hashtag/*
|
||||||
|
Disallow: /live/*
|
||||||
|
Disallow: /user/*
|
||||||
|
Disallow: /shorts/*
|
||||||
|
Disallow: /c/*
|
||||||
|
Disallow: /@*
|
||||||
|
|
||||||
|
# seo backlink bots are a waste of bandwidth
|
||||||
|
User-agent: dotbot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: BLEXBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: SemrushBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: AhrefsBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: barkrowler
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: VelenPublicWebCrawler
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: AwarioRssBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: AwarioSmartBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: PetalBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-Agent: ImagesiftBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
Sitemap: https://api.preservetube.com/sitemap.xml
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
|
import { staticPlugin } from '@elysiajs/static'
|
||||||
|
|
||||||
import latest from '@/router/latest'
|
import latest from '@/router/latest'
|
||||||
import search from '@/router/search'
|
import search from '@/router/search'
|
||||||
import transparency from '@/router/transparency'
|
import transparency from '@/router/transparency'
|
||||||
import video from '@/router/video'
|
import video from '@/router/video'
|
||||||
import websocket from '@/router/websocket'
|
import websocket from '@/router/websocket'
|
||||||
|
import html from '@/router/html'
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
app.use(latest)
|
app.use(latest)
|
||||||
|
|
@ -12,11 +14,16 @@ app.use(search)
|
||||||
app.use(transparency)
|
app.use(transparency)
|
||||||
app.use(video)
|
app.use(video)
|
||||||
app.use(websocket)
|
app.use(websocket)
|
||||||
|
app.use(html)
|
||||||
|
app.onRequest(({ set, path }: any) => {
|
||||||
|
set.headers['Onion-Location'] = 'http://tubey5btlzxkcjpxpj2c7irrbhvgu3noouobndafuhbw4i5ndvn4v7qd.onion' + path
|
||||||
|
})
|
||||||
|
|
||||||
process.on('uncaughtException', err => {
|
process.on('uncaughtException', err => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.use(staticPlugin({ prefix: '/' }))
|
||||||
app.listen(1337);
|
app.listen(1337);
|
||||||
console.log(
|
console.log(
|
||||||
`api is running at ${app.server?.hostname}:${app.server?.port}`
|
`api is running at ${app.server?.hostname}:${app.server?.port}`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { Elysia } from 'elysia';
|
||||||
|
import { m, eta } from '@/utils/html'
|
||||||
|
const app = new Elysia()
|
||||||
|
|
||||||
|
app.get('/', async ({ set }) => {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return await m(eta.render('./index', {
|
||||||
|
title: 'Home | PreserveTube',
|
||||||
|
description: 'PreserveTube is a time capsule for YouTube videos! It allows you to preserve any YouTube video, creating a snapshot that will always be available even if the original video disappears or is taken down.',
|
||||||
|
keywords: 'youtube archive, youtube video history, youtube deleted, youtube deleted video, youtube downloader, youtube archiver'
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/save', async ({ query: { url }, set, error }) => {
|
||||||
|
if (!url) return error(400, 'No url provided.')
|
||||||
|
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return await m(eta.render('./save', {
|
||||||
|
title: 'Save Video | PreserveTube',
|
||||||
|
websocket: process.env.WEBSOCKET,
|
||||||
|
sitekey: process.env.SITEKEY,
|
||||||
|
url
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/savechannel', async ({ query: { url }, set, error }) => {
|
||||||
|
if (!url) return error(400, 'No url provided.')
|
||||||
|
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return await m(eta.render('./savechannel', {
|
||||||
|
title: 'Save Channel | PreserveTube',
|
||||||
|
websocket: process.env.WEBSOCKET,
|
||||||
|
sitekey: process.env.SITEKEY,
|
||||||
|
url
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/abuse', async ({ set }) => {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return await m(eta.render('./abuse', {
|
||||||
|
title: 'Abuse Report | PreserveTube',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/dmca', async ({ set }) => {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return await m(eta.render('./dmca', {
|
||||||
|
title: 'DMCA Takedown | PreserveTube',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/privacy', async ({ set }) => {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return await m(eta.render('./privacy', {
|
||||||
|
title: 'Privacy Policy | PreserveTube',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/donate', async ({ set }) => {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return await m(eta.render('./donate', {
|
||||||
|
title: 'Donations | PreserveTube',
|
||||||
|
description: "Support our mission through donations.",
|
||||||
|
keywords: "preservetube donations, crypto"
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
|
@ -1,24 +1,32 @@
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
|
|
||||||
import { db } from '@/utils/database'
|
import { db } from '@/utils/database'
|
||||||
import { createSitemapXML, createSitemapIndexXML } from '@/utils/sitemap'
|
import { createSitemapXML, createSitemapIndexXML } from '@/utils/sitemap'
|
||||||
|
import { m, eta } from '@/utils/html'
|
||||||
import redis from '@/utils/redis';
|
import redis from '@/utils/redis';
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
|
|
||||||
app.get('/latest', async () => {
|
app.get('/latest', async ({ set }) => {
|
||||||
const cached = await redis.get('latest')
|
const cached = await redis.get('latest:html')
|
||||||
if (cached) return JSON.parse(cached)
|
if (cached) {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
const json = await db.selectFrom('videos')
|
const json = await db.selectFrom('videos')
|
||||||
.select(['id', 'title', 'thumbnail', 'published', 'archived', 'channel', 'channelId', 'channelAvatar', 'channelVerified'])
|
.select(['id', 'title', 'thumbnail', 'published', 'archived', 'channel', 'channelId', 'channelAvatar', 'channelVerified'])
|
||||||
.orderBy('archived desc')
|
.orderBy('archived desc')
|
||||||
.limit(50)
|
.limit(51)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
|
const html = await m(eta.render('./latest', {
|
||||||
|
data: json,
|
||||||
|
title: 'Latest | PreserveTube',
|
||||||
|
}))
|
||||||
|
await redis.set('latest:html', html, 'EX', 3600)
|
||||||
|
|
||||||
await redis.set('latest', JSON.stringify(json), 'EX', 3600)
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return html
|
||||||
return json
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/sitemap-index.xml', async ({ set }) => {
|
app.get('/sitemap-index.xml', async ({ set }) => {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { RedisRateLimiter } from 'rolling-rate-limiter'
|
||||||
|
|
||||||
import { db } from '@/utils/database'
|
import { db } from '@/utils/database'
|
||||||
import { validateVideo, validateChannel } from '@/utils/regex'
|
import { validateVideo, validateChannel } from '@/utils/regex'
|
||||||
|
import { m, eta } from '@/utils/html'
|
||||||
import redis from '@/utils/redis';
|
import redis from '@/utils/redis';
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
|
|
@ -14,20 +15,33 @@ const limiter = new RedisRateLimiter({
|
||||||
maxInInterval: 15
|
maxInInterval: 15
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/search/video', async ({ headers, query: { search }, error }) => {
|
app.get('/search', async ({ headers, query: { search }, set, redirect, error }) => {
|
||||||
const hash = Bun.hash(headers['x-userip'] || headers['cf-connecting-ip'] || '0.0.0.0')
|
const hash = Bun.hash(headers['x-userip'] || headers['cf-connecting-ip'] || '0.0.0.0')
|
||||||
const isLimited = await limiter.limit(hash.toString())
|
const isLimited = await limiter.limit(hash.toString())
|
||||||
if (isLimited) return error(429, 'error-You have been ratelimited.')
|
if (isLimited) return error(429, 'You have been ratelimited.')
|
||||||
|
|
||||||
const videoId = validateVideo(search)
|
const videoId = validateVideo(search)
|
||||||
if (videoId) return `redirect-${process.env.FRONTEND}/watch?v=${videoId}`
|
if (videoId) return redirect(`/watch?v=${videoId}`)
|
||||||
|
|
||||||
|
const cached = await redis.get(`search:${Bun.hash(search).toString()}:html`)
|
||||||
|
if (cached) {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
const videos = await db.selectFrom('videos')
|
const videos = await db.selectFrom('videos')
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where('title', 'ilike', `%${search}%`)
|
.where('title', 'ilike', `%${search}%`)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
return videos
|
const html = await m(eta.render('./search', {
|
||||||
|
data: videos,
|
||||||
|
title: 'Search | PreserveTube',
|
||||||
|
}))
|
||||||
|
await redis.set(`search:${Bun.hash(search).toString()}:html`, html, 'EX', 3600)
|
||||||
|
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return html
|
||||||
}, {
|
}, {
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
search: t.String()
|
search: t.String()
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,55 @@
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
|
|
||||||
import { db } from '@/utils/database'
|
import { db } from '@/utils/database'
|
||||||
|
import { m, eta } from '@/utils/html'
|
||||||
import redis from '@/utils/redis';
|
import redis from '@/utils/redis';
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
|
|
||||||
app.get('/transparency/list', async () => {
|
app.get('/transparency', async ({ set }) => {
|
||||||
const cached = await redis.get('transparency')
|
const cached = await redis.get('transparency:html')
|
||||||
if (cached) return JSON.parse(cached)
|
if (cached) {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
const reports = await db.selectFrom('reports')
|
const reports = await db.selectFrom('reports')
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
const json = reports.map(r => {
|
const html = await m(eta.render('./transparency', {
|
||||||
return {
|
data: reports,
|
||||||
...r,
|
title: 'Transparency | PreserveTube',
|
||||||
details: (r.details).split('<').join('<').split('>').join('>'),
|
}))
|
||||||
date: (r.date).toISOString().slice(0, 10)
|
await redis.set('transparency:html', html, 'EX', 3600)
|
||||||
}
|
|
||||||
})
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return html
|
||||||
await redis.set('transparency', JSON.stringify(json), 'EX', 3600)
|
|
||||||
return json
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/transparency/:id', async ({ params: { id } }) => {
|
app.get('/transparency/:id', async ({ params: { id }, set, error }) => {
|
||||||
const cached = await redis.get(`transparency:${id}`)
|
const cached = await redis.get(`transparency:${id}:html`)
|
||||||
if (cached) return JSON.parse(cached)
|
if (cached) {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
const json = await db.selectFrom('reports')
|
const json = await db.selectFrom('reports')
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where('target', '=', id)
|
.where('target', '=', id)
|
||||||
.execute()
|
.executeTakeFirst()
|
||||||
|
if (!json) return error(404, 'Report not found.')
|
||||||
|
|
||||||
|
const html = await m(eta.render('./transparency-entry', {
|
||||||
|
title: `${json.title} | PreserveTube`,
|
||||||
|
t_title: json.title,
|
||||||
|
date: json.date.toISOString(),
|
||||||
|
details: json.details,
|
||||||
|
}))
|
||||||
|
await redis.set(`transparency:${id}:html`, html, 'EX', 3600)
|
||||||
|
|
||||||
await redis.set(`transparency:${id}`, JSON.stringify(json), 'EX', 3600)
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
return json
|
return html
|
||||||
})
|
})
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|
@ -4,6 +4,7 @@ import DOMPurify from 'isomorphic-dompurify'
|
||||||
import { db } from '@/utils/database'
|
import { db } from '@/utils/database'
|
||||||
import { getChannel, getChannelVideos } from '@/utils/metadata';
|
import { getChannel, getChannelVideos } from '@/utils/metadata';
|
||||||
import { convertRelativeToDate } from '@/utils/common';
|
import { convertRelativeToDate } from '@/utils/common';
|
||||||
|
import { m, eta } from '@/utils/html'
|
||||||
import redis from '@/utils/redis';
|
import redis from '@/utils/redis';
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
|
|
@ -16,6 +17,58 @@ interface processedVideo {
|
||||||
deleted?: undefined;
|
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'
|
||||||
|
}))
|
||||||
|
|
||||||
|
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 }) => {
|
app.get('/video/:id', async ({ params: { id }, error }) => {
|
||||||
const cached = await redis.get(`video:${id}`)
|
const cached = await redis.get(`video:${id}`)
|
||||||
if (cached) return JSON.parse(cached)
|
if (cached) return JSON.parse(cached)
|
||||||
|
|
@ -34,16 +87,26 @@ app.get('/video/:id', async ({ params: { id }, error }) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/channel/:id', async ({ params: { id }, error }) => {
|
app.get('/channel/:id', async ({ params: { id }, set }) => {
|
||||||
const cached = await redis.get(`channel:${id}`)
|
const cached = await redis.get(`channel:${id}:html`)
|
||||||
if (cached) return JSON.parse(cached)
|
if (cached) {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
const [videos, channel] = await Promise.all([
|
const [videos, channel] = await Promise.all([
|
||||||
getChannelVideos(id),
|
getChannelVideos(id),
|
||||||
getChannel(id)
|
getChannel(id)
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!videos || !channel || videos.error || channel.error) return error(404, { error: '404' })
|
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')
|
const archived = await db.selectFrom('videos')
|
||||||
.select(['id', 'title', 'thumbnail', 'published', 'archived'])
|
.select(['id', 'title', 'thumbnail', 'published', 'archived'])
|
||||||
|
|
@ -68,20 +131,26 @@ app.get('/channel/:id', async ({ params: { id }, error }) => {
|
||||||
|
|
||||||
processedVideos.sort((a: any, b: any) => new Date(b.published).getTime() - new Date(a.published).getTime());
|
processedVideos.sort((a: any, b: any) => new Date(b.published).getTime() - new Date(a.published).getTime());
|
||||||
|
|
||||||
const json = {
|
const html = await m(eta.render('./channel', {
|
||||||
name: channel.metadata.title,
|
name: channel.metadata.title,
|
||||||
avatar: channel.metadata.avatar[0].url,
|
avatar: channel.metadata.avatar[0].url,
|
||||||
verified: channel.header.author?.is_verified,
|
verified: channel.header.author?.is_verified,
|
||||||
videos: processedVideos
|
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}`, JSON.stringify(json), 'EX', 3600)
|
}))
|
||||||
return json
|
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 } }) => {
|
app.get('/channel/:id/videos', async ({ params: { id }, set }) => {
|
||||||
const cached = await redis.get(`channelVideos:${id}`)
|
const cached = await redis.get(`channelVideos:${id}:html`)
|
||||||
if (cached) return JSON.parse(cached)
|
if (cached) {
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
const archived = await db.selectFrom('videos')
|
const archived = await db.selectFrom('videos')
|
||||||
.select(['id', 'title', 'thumbnail', 'published', 'archived'])
|
.select(['id', 'title', 'thumbnail', 'published', 'archived'])
|
||||||
|
|
@ -89,11 +158,15 @@ app.get('/channel/:id/videos', async ({ params: { id } }) => {
|
||||||
.orderBy('published desc')
|
.orderBy('published desc')
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
const json = {
|
const html = await m(eta.render('./channel-videos', {
|
||||||
videos: archived
|
videos: archived,
|
||||||
}
|
title: `${id} videos | PreserveTube`,
|
||||||
await redis.set(`channelVideos:${id}`, JSON.stringify(json), 'EX', 3600)
|
keywords: `${id} archive, ${id} channel archive, ${id} deleted video, ${id} video deleted`
|
||||||
return json
|
}))
|
||||||
|
await redis.set(`channelVideos:${id}:html`, html, 'EX', 3600)
|
||||||
|
|
||||||
|
set.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
return html
|
||||||
})
|
})
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="text">
|
||||||
|
<div class="reports">
|
||||||
|
When it comes to reports of child abuse, PLEASE report it to <a href="https://report.cybertip.org/">NCMEC</a> or your local law enforcement agency, who will then contact us. PreserveTube is not legally equipped to handle such reports.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Our top priority at PreserveTube is to provide a safe and respectful environment for all users of our service. If you come across any content on our website that you believe is inappropriate or violates our community guidelines, we urge you to report it immediately so we can take appropriate action.<br><br>
|
||||||
|
|
||||||
|
To file an abuse report, please follow these steps:
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Identify the content that you believe is inappropriate or in violation of our community guidelines.</li>
|
||||||
|
<li>Take a screenshot or copy the URL of the content for reference.</li>
|
||||||
|
<li>Send an email to <a href="mailto:abuse@preservetube.com">abuse@preservetube.com</a> with the subject line "Abuse Report".</li>
|
||||||
|
<li>In the email, please provide the following information:
|
||||||
|
<ul>
|
||||||
|
<li>Your full name and contact information.</li>
|
||||||
|
<li>A detailed description of the content and why you believe it is inappropriate or in violation of our community guidelines.</li>
|
||||||
|
<li>The screenshot or URL of the content for reference.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
At Preservetube, we take a <i>Law of the Land</i> approach to content moderation. This means that we do not allow any content that is illegal or violates the laws of either The Netherlands or The United States of America.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pgp">
|
||||||
|
<a href="pgp.txt">Our PGP key</a> is available for secure communication.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-top: 5%;
|
||||||
|
margin-bottom: 5%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pgp {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports {
|
||||||
|
background-color: #ffd7cf;
|
||||||
|
border: 2px dashed #da7b5e;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<% it.videos.forEach(function(v){ %>
|
||||||
|
<div class="video">
|
||||||
|
<a href="/watch?v=<%= v.id %>">
|
||||||
|
<img class="thumbnail" src="<%= v.thumbnail %>" />
|
||||||
|
<div class="title"><%= v.title %></div>
|
||||||
|
<div class="date">
|
||||||
|
Published on <%= v.published %>
|
||||||
|
|
||||||
|
<% if (v.archived) { %>
|
||||||
|
| Archived on <%= v.archived %>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (v.deleted) { %>
|
||||||
|
<div class="deleted">(the original video has been deleted)</div>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.channel {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel img {
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel h1 {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified {
|
||||||
|
height: 20px;
|
||||||
|
margin-left: 5px;
|
||||||
|
content: url("https://api.iconify.design/ion/checkmark-circle.svg");
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-right: 10%;
|
||||||
|
margin-top: 1%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleted {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<% if (it.failedToFetch) { %>
|
||||||
|
<div class="channel">
|
||||||
|
<h2>
|
||||||
|
An error occurred while attempting to retrieve this channel from YouTube. <br/>
|
||||||
|
To access all videos without relying on YouTube data, please <a href="/channel/<%= it.id %>/videos">click here</a>.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="channel">
|
||||||
|
<img src="<%= it.avatar %>" />
|
||||||
|
<h1><%= it.name %></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<% it.videos.forEach(function(v){ %>
|
||||||
|
<div class="video">
|
||||||
|
<a href="/watch?v=<%= v.id %>">
|
||||||
|
<img class="thumbnail" src="<%= v.thumbnail %>" />
|
||||||
|
<div class="title"><%= v.title %></div>
|
||||||
|
<div class="date">
|
||||||
|
Published on <%= v.published %>
|
||||||
|
|
||||||
|
<% if (v.archived) { %>
|
||||||
|
| Archived on <%= v.archived %>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (v.deleted) { %>
|
||||||
|
<div class="deleted">(the original video has been deleted)</div>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.channel {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel img {
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel h1 {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified {
|
||||||
|
height: 20px;
|
||||||
|
margin-left: 5px;
|
||||||
|
content: url("https://api.iconify.design/ion/checkmark-circle.svg");
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-right: 10%;
|
||||||
|
margin-top: 1%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleted {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="text">
|
||||||
|
To submit a DMCA takedown notice, please send an email to <a href="mailto:legal@preservetube.com">legal@preservetube.com</a> that includes the following information:
|
||||||
|
<ul>
|
||||||
|
<li>Identification of the copyrighted work that you claim has been infringed, or if multiple copyrighted works are covered by a single notification, provide a representative list of such works.</li>
|
||||||
|
<li>Identification of the material that you claim is infringing and information reasonably sufficient to permit us to locate the material, including the URL(s) where the infringing material is located on PreserveTube.</li>
|
||||||
|
<li>Your contact information, including your full legal name, mailing address, telephone number, and email address.</li>
|
||||||
|
<li>A statement that you have a good faith belief that the use of the copyrighted materials described above and contained on the service is not authorized by the copyright owner, its agent, or by law.</li>
|
||||||
|
<li>A statement that you hereby declare, under penalty of perjury, that the information in your notification is accurate and that you are the owner of the copyright or other intellectual property interest, or authorized to act on behalf of the owner of the copyright or other intellectual property interest.</li>
|
||||||
|
<li>An electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
If we receive a valid DMCA takedown notice, we will add a notice to the video page indicating that the content has been stricken with a copyright claim. This notice will include the name of the person or entity that sent the copyright notice and the email logs associated with the notice. If the video is obviously copyrighted content, such as a movie or TV show, it will be taken down. Any other videos will remain online.<br><br>
|
||||||
|
Please note that submitting a false or misleading DMCA takedown notice may result in legal consequences. If you are unsure whether your copyrighted material has been infringed or if you need legal advice, we recommend that you consult with an attorney.<br><br>
|
||||||
|
P.S.: A DMCA takedown notice will not remove embarrassing videos that Preservetube has archived from the internet. We will ensure that the videos remain accessible to the public, even if that's not possible via our website.<br><br>
|
||||||
|
|
||||||
|
If you have any questions about this DMCA policy, please contact <a href="mailto:legal@preservetube.com">legal@preservetube.com</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pgp">
|
||||||
|
<a href="pgp.txt">Our PGP key</a> is available for secure communication.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-top: 5%;
|
||||||
|
margin-bottom: 5%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pgp {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="text">
|
||||||
|
Infrastructure costs money, and running PreserveTube costs $1,185 a year. If you love what we do, a little support would go a long way in keeping us safe and sound!
|
||||||
|
|
||||||
|
<h3>FIAT Donations</h3>
|
||||||
|
<p>
|
||||||
|
Unfortunately, PreserveTube only accepts cryptocurrency donations at this time.
|
||||||
|
Feeling extra generous? You can learn how to purchase Bitcoin here: <a style="font-weight: bold;" href="https://buybitcoinworldwide.com">buybitcoinworldwide.com</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Cryptocurrency Donations</h3>
|
||||||
|
<p>We accept the following cryptocurrencies to support our cause:</p>
|
||||||
|
<p>
|
||||||
|
<span class="label">BTC</span>: <code>bc1qfa6uqp72a2hgg3ac06f2du2cvug4hadufd7sql</code><br>
|
||||||
|
<span class="label">ETH</span>: <code>0xF6923d9e17B32efF1D82200E82E3b9f500c6fAED</code><br>
|
||||||
|
<span class="label">LTC</span>: <code>LW7qhYnAfjqieNdySNeURhrpCzcCjcuj7U</code> <br>
|
||||||
|
<span class="label">SOL</span>: <code>GK5WTtJeZRuJheSbm2ZFYWHQ3QfZPgLaZ1Vq5EXi3e91</code> <br>
|
||||||
|
<span class="label">XMR</span>: <code>8BqeuL5iRzXccqc8wy8U2RgJF7RdTX52Pgpdc6QYcrHfLnoK9jtQ3MrDv7V6T14hHjMHfE28wsVqhCGiCeXsRF4651SnGNC</code> <br><br>
|
||||||
|
<span class="label">Or anything else:</span> <br>
|
||||||
|
<iframe src="https://trocador.app/anonpay/?ticker_to=xmr&network_to=Mainnet&address=8BqeuL5iRzXccqc8wy8U2RgJF7RdTX52Pgpdc6QYcrHfLnoK9jtQ3MrDv7V6T14hHjMHfE28wsVqhCGiCeXsRF4651SnGNC&donation=True&simple_mode=True&ticker_from=doge&network_from=Mainnet&bgcolor=" width="310" height="230" style="border:0" scrolling="no"></iframe>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.text {
|
||||||
|
margin-top: 5%;
|
||||||
|
margin-bottom: 5%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.text {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
word-break: break-all;
|
||||||
|
user-select: all;
|
||||||
|
-webkit-user-select: all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<form class="archive" action="save">
|
||||||
|
<div class="title">Archive a Youtube Video</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="url"
|
||||||
|
name="url"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://www.youtube.com/watch?v=smVU37xAhY8"
|
||||||
|
tabindex="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input class="submit" type="submit" value="save" tabindex="-1" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- <form class="playlist" action="saveplaylist">
|
||||||
|
<div class="title">Archive a Youtube Playlist</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="url"
|
||||||
|
name="url"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://www.youtube.com/playlist?list=PLhixgUqwRTjwvBI-hmbZ2rpkAl4lutnJG"
|
||||||
|
tabindex="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input class="submit" type="submit" value="save" tabindex="-1" />
|
||||||
|
</form> -->
|
||||||
|
|
||||||
|
<form class="channel" action="savechannel">
|
||||||
|
<div class="title">Archive a Youtube Channel</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="url"
|
||||||
|
name="url"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://www.youtube.com/channel/UCyjKBSGMcxdGbSD86iq_SBw"
|
||||||
|
tabindex="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input class="submit" type="submit" value="save" tabindex="-1" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- <form class="channel" action="autodownload">
|
||||||
|
<div class="title">
|
||||||
|
Automatically download new videos from channel
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="url"
|
||||||
|
name="url"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://www.youtube.com/channel/UCyjKBSGMcxdGbSD86iq_SBw"
|
||||||
|
tabindex="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input class="submit" type="submit" value="save" tabindex="-1" />
|
||||||
|
</form> -->
|
||||||
|
|
||||||
|
<div class="about">
|
||||||
|
PreserveTube is a time capsule for YouTube videos! It allows you to
|
||||||
|
"preserve" any YouTube video, creating a snapshot that will always
|
||||||
|
be available even if the original video disappears or is taken down. <br
|
||||||
|
/><br />
|
||||||
|
It saves not only the original video, but also descriptions, titles and
|
||||||
|
other interesting metadata.<br /><br /><br />
|
||||||
|
|
||||||
|
PreserveTube is dedicated to preserving the legacy of YouTube, one
|
||||||
|
video at a time.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="search" action="/search">
|
||||||
|
<div class="title">Search for the archive of an Youtube Video</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="url"
|
||||||
|
name="search"
|
||||||
|
type="text"
|
||||||
|
placeholder="The Chris Chan Interview or https://www.youtube.com/watch?v=mA80W7qVcSM"
|
||||||
|
tabindex="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input class="submit" type="submit" value="search" tabindex="-1" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- <form class="search" action={`${backend}/search/playlist`}>
|
||||||
|
<div class="title">
|
||||||
|
Search for the archive of an Youtube Playlist
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="url"
|
||||||
|
name="url"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://www.youtube.com/playlist?list=PLhixgUqwRTjwvBI-hmbZ2rpkAl4lutnJG"
|
||||||
|
tabindex="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input class="submit" type="submit" value="search" tabindex="-1" />
|
||||||
|
</form> -->
|
||||||
|
|
||||||
|
<form class="search" action="/search/channel">
|
||||||
|
<div class="title">
|
||||||
|
Search for the archive of an Youtube Channel
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="url"
|
||||||
|
name="url"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://www.youtube.com/channel/UCyjKBSGMcxdGbSD86iq_SBw"
|
||||||
|
tabindex="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input class="submit" type="submit" value="search" tabindex="-1" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<a href="transparency">[TRANSPARENCY]</a><div class="space"></div>
|
||||||
|
<a href="dmca">[DMCA]</a><div class="space"></div>
|
||||||
|
<a href="abuse">[REPORT ABUSE]</a><div class="space"></div>
|
||||||
|
<a href="privacy">[PRIVACY POLICY]</a><div class="space"></div>
|
||||||
|
<a href="https://status.preservetube.com">[STATUS]</a><div class="space"></div>
|
||||||
|
<a href="https://github.com/PreserveTube">[GITHUB]</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
background-color: #515359;
|
||||||
|
color: white;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive {
|
||||||
|
margin-top: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist,
|
||||||
|
.channel {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url {
|
||||||
|
width: 85%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
width: 13%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about {
|
||||||
|
margin-top: 2%;
|
||||||
|
margin-bottom: 2%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
color: white;
|
||||||
|
background-color: #1b1c1f;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
bottom: 0px;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
form {
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about {
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
display: inline-block;
|
||||||
|
width: 3%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<% it.data.forEach(function(v){ %>
|
||||||
|
<div class="video">
|
||||||
|
<a href="/watch?v=<%= v.id %>">
|
||||||
|
<img class="thumbnail" src="<%= v.thumbnail %>" />
|
||||||
|
<div class="title"><%= v.title %></div>
|
||||||
|
<div class="date">Published on <%= v.published %> | Archived on <%= v.archived %></div>
|
||||||
|
</a>
|
||||||
|
<div class="channel-profile">
|
||||||
|
<img src="<%= v.channelAvatar %>" />
|
||||||
|
<span class="channel-name">
|
||||||
|
<a href="/channel/<%= v.channelId %>">
|
||||||
|
<%= v.channel %>
|
||||||
|
<% if (it.channelVerified) { %>
|
||||||
|
<div class="verified"></div>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-right: 10%;
|
||||||
|
margin-top: 1%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-profile {
|
||||||
|
margin-top: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-profile img {
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified {
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 2px;
|
||||||
|
content: url('https://api.iconify.design/ion/checkmark-circle.svg');
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<% if (it.keywords) { %>
|
||||||
|
<meta name="keywords" content="<%= it.keywords %>"/>
|
||||||
|
<% } %>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|
||||||
|
<meta property="og:title" content="<%= it.title %>"/>
|
||||||
|
<% if (it.description) { %>
|
||||||
|
<meta name="og:description" content="<%= it.description %>">
|
||||||
|
<% } else { %>
|
||||||
|
<meta name="og:description" content="PreserveTube is a time capsule for YouTube videos! It allows you to preserve any YouTube video, creating a snapshot that will always be available even if the original video disappears or is taken down.">
|
||||||
|
<% } %>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:url" content="https://preservetube.com"/>
|
||||||
|
<meta property="og:site_name" content="PreserveTube"/>
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="<%= it.title %>">
|
||||||
|
<% if (it.description) { %>
|
||||||
|
<meta name="twitter:description" content="<%= it.description %>">
|
||||||
|
<% } else { %>
|
||||||
|
<meta name="twitter:description" content="PreserveTube is a time capsule for YouTube videos! It allows you to preserve any YouTube video, creating a snapshot that will always be available even if the original video disappears or is taken down.">
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/gh/preservetube/fonts@main/fonts.css" rel="stylesheet" type="text/css">
|
||||||
|
|
||||||
|
<% if (it.manualAnalytics) { %>
|
||||||
|
<script defer data-domain="preservetube.com" src="https://a.gloe.net/js/script.manual.js"></script>
|
||||||
|
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
||||||
|
<script>
|
||||||
|
function prepareUrl(params) {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
return params.reduce((customUrl, param) => {
|
||||||
|
const value = url.searchParams.get(param);
|
||||||
|
return value ? `${customUrl}/${value}` : customUrl;
|
||||||
|
}, `${url.origin}${url.pathname.replace(/\/$/, '')}`);
|
||||||
|
}
|
||||||
|
plausible('pageview', { u: prepareUrl(["v"]) + window.location.search });
|
||||||
|
</script>
|
||||||
|
<% } else { %>
|
||||||
|
<script defer data-domain="preservetube.com" src="https://a.gloe.net/js/script.js"></script>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<title><%= it.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<a href="/">PreserveTube</a>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
<div class="buttons">
|
||||||
|
<a href="/latest">Latest</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="donate">
|
||||||
|
<span>PreserveTube is dedicated to preserving internet history, and we need your help! Every donation, big or small, makes a difference.</span>
|
||||||
|
<a href="/donate" style="font-weight: bold;">Donate now.</a>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<%~ it.body %>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
page rendered by <code><%= hostname %></code>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #fffeff;
|
||||||
|
font-family: "Proxima Nova", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: relative;
|
||||||
|
background-color: #1b1c1f;
|
||||||
|
color: white;
|
||||||
|
height: 30px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-left: 17.5%;
|
||||||
|
padding-right: 17.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate {
|
||||||
|
position: relative;
|
||||||
|
background-color: #fff2cf;
|
||||||
|
border-bottom: 2px dashed #dab75e;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.donate {
|
||||||
|
padding-left: 5%;
|
||||||
|
padding-right: 5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding-right: 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
padding-left: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 767px) {
|
||||||
|
.header {
|
||||||
|
padding-left: 5%;
|
||||||
|
padding-right: 5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer > code {
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="text">
|
||||||
|
<div class="part">
|
||||||
|
<strong>Last updated:</strong> 2025-08-16
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="part">
|
||||||
|
<div class="title">Anonymous Analytics</div>
|
||||||
|
PreserveTube uses <a href="https://plausible.io/">Plausible</a> to get an aproximate number of active users, and the most watched videos.
|
||||||
|
No identifiable information, such as IPs, is stored. This Plausible instance is selfhosted, and not proxied through Cloudflare.
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<div class="title">Reverse Proxying</div>
|
||||||
|
We use <a href="https://caddyserver.com/">Caddy</a> to reverse proxy our services. On certain ocassions, such as when errors occur in requests,
|
||||||
|
Caddy might log these. These logs include headers, client IPs, and paths. Otherwise, our Caddy setup does not log any requests. <br><br>
|
||||||
|
Logs are purged automatically once the file size reaches 50MB.
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<div class="title">DDoS Protection</div>
|
||||||
|
We use a combination of Cloudflare (preservetube.com), BasedFlare (preservetube.net), and selfhosted solutions (preservetube.org) for our DDoS protection. We additionally use Cloudflare Turnstile as a captcha
|
||||||
|
to avoid automated bot traffic on the /save page. <br><br>
|
||||||
|
Cloudflare's privacy policy can be found here: <a href="https://www.cloudflare.com/en-gb/privacypolicy/">https://www.cloudflare.com/en-gb/privacypolicy/</a>. <br>
|
||||||
|
BasedFlare has no privacy policy. It should be assumed everything is logged. Avoid visiting preservetube.net if you're uncomfortable with this.
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<div class="title">DNS</div>
|
||||||
|
Besides Cloudflare and BasedFlare, PreserveTube also relies on ClouDNS for DNS services. ClouDNS's privacy policy can be found here: <a href="https://www.cloudns.net/privacy-policy/">https://www.cloudns.net/privacy-policy/</a>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<div class="title">Video Saving</div>
|
||||||
|
Saving videos is completly anonymous. We do not log who saved a video.
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<div class="title">Questions?</div>
|
||||||
|
I'm reachable at <a href="mailto:admin@preservetube.com">admin@preservetube.com</a> (<a style="text-decoration: none;" href="/pgp.txt">🔑</a>).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-top: 2%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part {
|
||||||
|
margin-bottom: 3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: large;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div id="logs" class="logs">
|
||||||
|
<h1 id="title"></h1>
|
||||||
|
<div class="captcha" id="captcha"></div>
|
||||||
|
<h3 id="bottom">
|
||||||
|
<noscript>This feature doesen't function properly unless JavaScript is enabled.</noscript>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
<script>
|
||||||
|
const url = "<%= it.url %>"
|
||||||
|
const websocket = "<%= it.websocket %>"
|
||||||
|
const sitekey = "<%= it.sitekey %>"
|
||||||
|
const ws = new WebSocket(`${websocket}/save?url=${encodeURIComponent(url)}`)
|
||||||
|
const h1 = document.getElementById("title")
|
||||||
|
const h3 = document.getElementById("bottom")
|
||||||
|
const captcha = document.getElementById("captcha")
|
||||||
|
|
||||||
|
ws.onmessage = function (msg) {
|
||||||
|
const text = msg.data.split(' - ').slice(1).join(' - ')
|
||||||
|
|
||||||
|
if (msg.data == 'ERROR - Missing URL' || msg.data == 'ERROR - Whoops! What is that? That is not a Youtube url.') {
|
||||||
|
h1.innerHTML = text
|
||||||
|
return
|
||||||
|
} else if (msg.data.startsWith('CAPTCHA -')) {
|
||||||
|
captcha.style.display = 'block'
|
||||||
|
turnstile.render('#captcha', {
|
||||||
|
sitekey: sitekey,
|
||||||
|
callback: function(token) {
|
||||||
|
captcha.remove()
|
||||||
|
ws.send(token)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (msg.data.startsWith('DONE -')) {
|
||||||
|
window.location.href = text
|
||||||
|
}
|
||||||
|
|
||||||
|
h3.innerHTML = `${h1.innerHTML}<br>${h3.innerHTML}`
|
||||||
|
h1.innerHTML = text
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = function (event) {
|
||||||
|
h3.innerHTML = `Websocket connection was closed: ${event.reason} (${event.code})<br>${h3.innerHTML}`
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
ws.send('alive')
|
||||||
|
}, 2000)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.logs {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div id="logs" class="logs">
|
||||||
|
<h1 id="title"></h1>
|
||||||
|
<div class="captcha" id="captcha"></div>
|
||||||
|
<h3 id="bottom">
|
||||||
|
<noscript>This feature doesen't function properly unless JavaScript is enabled.</noscript>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
<script>
|
||||||
|
const url = "<%= it.url %>"
|
||||||
|
const websocket = "<%= it.websocket %>"
|
||||||
|
const sitekey = "<%= it.sitekey %>"
|
||||||
|
const ws = new WebSocket(`${websocket}/savechannel?url=${encodeURIComponent(url)}`)
|
||||||
|
const h1 = document.getElementById("title")
|
||||||
|
const h3 = document.getElementById("bottom")
|
||||||
|
const captcha = document.getElementById("captcha")
|
||||||
|
|
||||||
|
ws.onmessage = function (msg) {
|
||||||
|
const text = msg.data.split(' - ').slice(1).join(' - ')
|
||||||
|
|
||||||
|
if (msg.data == 'ERROR - Missing URL' || msg.data == 'ERROR - Whoops! What is that? That is not a Youtube url.') {
|
||||||
|
h1.innerHTML = text
|
||||||
|
return
|
||||||
|
} else if (msg.data.startsWith('CAPTCHA -')) {
|
||||||
|
captcha.style.display = 'block'
|
||||||
|
turnstile.render('#captcha', {
|
||||||
|
sitekey: sitekey,
|
||||||
|
callback: function(token) {
|
||||||
|
captcha.remove()
|
||||||
|
ws.send(token)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (msg.data.startsWith('DONE -')) {
|
||||||
|
window.location.href = text
|
||||||
|
}
|
||||||
|
|
||||||
|
h3.innerHTML = `${h1.innerHTML}<br>${h3.innerHTML}`
|
||||||
|
h1.innerHTML = text
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = function (event) {
|
||||||
|
h3.innerHTML = `Websocket connection was closed: ${event.reason} (${event.code})<br>${h3.innerHTML}`
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
ws.send('alive')
|
||||||
|
}, 2000)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.logs {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<% it.data.forEach(function(v){ %>
|
||||||
|
<div class="video">
|
||||||
|
<a href="/watch?v=<%= v.id %>">
|
||||||
|
<img class="thumbnail" src="<%= v.thumbnail %>" />
|
||||||
|
<div class="title"><%= v.title %></div>
|
||||||
|
<div class="date">Published on <%= v.published %> | Archived on <%= v.archived %></div>
|
||||||
|
</a>
|
||||||
|
<div class="channel-profile">
|
||||||
|
<img src="<%= v.channelAvatar %>" />
|
||||||
|
<span class="channel-name">
|
||||||
|
<a href="/channel/<%= v.channelId %>">
|
||||||
|
<%= v.channel %>
|
||||||
|
<% if (it.channelVerified) { %>
|
||||||
|
<div class="verified"></div>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-right: 10%;
|
||||||
|
margin-top: 1%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-profile {
|
||||||
|
margin-top: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-profile img {
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified {
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 2px;
|
||||||
|
content: url("https://api.iconify.design/ion/checkmark-circle.svg");
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="report">
|
||||||
|
<div class="t-header">
|
||||||
|
<div class="title"><%= it.t_title %></div>
|
||||||
|
<div class="date"><%= it.date %></div>
|
||||||
|
</div>
|
||||||
|
<pre class="details"><%= it.details %></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.report {
|
||||||
|
margin-top: 5%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 15px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<% it.data.forEach(function(r){ %>
|
||||||
|
<a class="title" href="/transparency/<%= r.target %>"><%= r.title %></a>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: larger;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
<% layout('./layout') %>
|
||||||
|
|
||||||
|
<% if (it.isMissing) { %>
|
||||||
|
<div class="error">
|
||||||
|
<h2>Archive not found</h2>
|
||||||
|
<button onclick="window.location.href='/save?url=${encodeURIComponent(`https://www.youtube.com/watch?v=<%= it.id %>`)}'">
|
||||||
|
Archive Me!
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="report">
|
||||||
|
<a href="abuse">[report abuse]</a>
|
||||||
|
<div class="space"></div>
|
||||||
|
<a href="dmca">[dmca]</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (it.transparency.length != 0) { %>
|
||||||
|
<div class="reports">
|
||||||
|
<span class="reports-title">Somebody has complained about this video...</span> <br/>
|
||||||
|
<% it.transparency.forEach(function(t){ %>
|
||||||
|
<a href="<%= t.details %>"><%= t.title %></a>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<div class="video-wrapper">
|
||||||
|
<div class="video-loading hidden" id="video-loading">Loading...</div>
|
||||||
|
<video id="video-player" src="<%= it.source %>" poster="<%= it.thumbnail %>" controls preload="metadata"></video>
|
||||||
|
</div>
|
||||||
|
<h1><%= it.v_title %></h1>
|
||||||
|
|
||||||
|
<div class="channel-profile">
|
||||||
|
<img src="<%= it.channelAvatar %>" />
|
||||||
|
<span class="channel-name">
|
||||||
|
<a href="/channel/<%= it.channelId %>">
|
||||||
|
<%= it.channel %>
|
||||||
|
<% if (it.channelVerified) { %>
|
||||||
|
<div class="verified"></div>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metadata">
|
||||||
|
<p class="date">
|
||||||
|
Published on <%= it.published %> | Archived on <%= it.archived %>
|
||||||
|
<a href="<%= it.source %>" target="_blank">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icon">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p class="description"><%~ it.description %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 65%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 720px;
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
position: relative;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error h2 {
|
||||||
|
font-size: 30px;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-profile img {
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified {
|
||||||
|
height: 15px;
|
||||||
|
content: url('https://api.iconify.design/ion/checkmark-circle.svg');
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports {
|
||||||
|
background-color: #fff2cf;
|
||||||
|
border: 2px dashed #dab75e;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-title {
|
||||||
|
font-size: large;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-wrapper video {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 720px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: white;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border-radius: 0;
|
||||||
|
z-index: 2;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
font-size: large;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-loading.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 1.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
font-size: 1em
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-profile img {
|
||||||
|
width: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
width: 90%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script is:inline>
|
||||||
|
function initVideoLoading() {
|
||||||
|
const video = document.getElementById('video-player');
|
||||||
|
const loading = document.getElementById('video-loading');
|
||||||
|
if (!video || !loading) return;
|
||||||
|
loading?.classList.remove("hidden");
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
if (!loading.classList.contains('hidden')) {
|
||||||
|
loading.classList.add('hidden');
|
||||||
|
loading.addEventListener('transitionend', () => loading.remove(), { once: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// bunch of events that show readyness
|
||||||
|
['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'play'].forEach(evt =>
|
||||||
|
video.addEventListener(evt, hide)
|
||||||
|
);
|
||||||
|
|
||||||
|
// weird firefox something
|
||||||
|
video.addEventListener('click', hide);
|
||||||
|
video.addEventListener('keydown', hide);
|
||||||
|
video.addEventListener('pointerdown', hide);
|
||||||
|
|
||||||
|
video.addEventListener('error', () => {
|
||||||
|
loading.innerHTML = 'Video failed to load.';
|
||||||
|
loading.innerHTML += '<p style="font-size: medium;">This is not supposed to happen. Please email me at admin@preservetube.com with as much debugging information.</p>' +
|
||||||
|
'<p style="font-size: medium;">Please include your browser console logs. <a style="font-style: italic;" href="https://support.happyfox.com/kb/article/882-accessing-the-browser-console-and-network-logs/">' +
|
||||||
|
'See here on how to access them.</a> Please note you might have to refresh the page for anything to show up.</p>'
|
||||||
|
});
|
||||||
|
|
||||||
|
// video already has data (i.e. cached)
|
||||||
|
if (video.readyState > 0) hide();
|
||||||
|
|
||||||
|
// add an extra message after 10s
|
||||||
|
const FALLBACK_MS = 10000;
|
||||||
|
const fallbackTimer = setTimeout(() => {
|
||||||
|
if (!loading.classList.contains('hidden')) {
|
||||||
|
loading.innerHTML = 'Taking longer than usual to load...';
|
||||||
|
loading.innerHTML += '<p style="font-size: medium;">This might be due one of multiple reasons:</p>'
|
||||||
|
+ '<ol style="font-size: medium;"><li>This video is large. Depending on your browser, it might try loading the full thing at one time, which will slow things down.</li>'
|
||||||
|
+ '<li>Preservetube servers are overloaded. This happens once in a while, please be patient.</li><li>You\'re in an unfavourable region, which has a bad connection to our servers.</li></ol>'
|
||||||
|
+ '<p style="font-size: medium;">If this takes an unusual amount of time, even after considering the above mentioned reasons, please email me at admin@preservetube.com. Don\'t forget to include your aproximate region, your browser and OS.</p>';
|
||||||
|
}
|
||||||
|
}, FALLBACK_MS);
|
||||||
|
|
||||||
|
// clear fallback once overlay removed
|
||||||
|
loading.addEventListener('transitionend', () => clearTimeout(fallbackTimer), { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
initVideoLoading();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { minify } from 'html-minifier-next';
|
||||||
|
import { Eta } from 'eta';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const m = async (html: string) => {
|
||||||
|
return await minify(html, {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeComments: true,
|
||||||
|
minifyCSS: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eta = new Eta({ views: path.join(__dirname, '../templates'), functionHeader: `const hostname = "${process.env.SERVER_NICKNAME}"` })
|
||||||
Loading…
Reference in New Issue