diff --git a/src/router/admin.ts b/src/router/admin.ts index cd4d658..ffd0fab 100644 --- a/src/router/admin.ts +++ b/src/router/admin.ts @@ -10,9 +10,11 @@ import { clearSessionCookie } from '@/utils/adminAuth' import { startRestore, RestoreError } from '@/utils/restore' -import { approveRequest, rejectRequest, dismissRequest } from '@/utils/archiveRequests' +import { approveRequest, rejectRequest, dismissRequest, archiveWithRetry } from '@/utils/archiveRequests' import { approveColdRequest, rejectColdRequest, dismissColdRequest } from '@/utils/coldRequests' import { ingestInboxMail, listInboxMail } from '@/utils/inbox' +import { extractVideoId } from '@/utils/archive' +import redis from '@/utils/redis' const app = new Elysia({ prefix: '/admin' }) @@ -123,6 +125,56 @@ app.get('/inbox', async ({ set }) => { })) }) +app.get('/archive', async ({ set }) => { + set.headers['Content-Type'] = 'text/html; charset=utf-8' + return await m(eta.render('./admin/archive', { + title: 'Manual Archive | PreserveTube Admin' + })) +}) + +app.post('/archive', async ({ body, set, redirect }) => { + const videoId = extractVideoId(body.video) + if (!videoId) { + set.headers['Content-Type'] = 'text/html; charset=utf-8' + set.status = 400 + return await m(eta.render('./admin/archive', { + title: 'Manual Archive | PreserveTube Admin', + archiveError: 'Enter a valid YouTube video URL or ID.' + })) + } + + const jobId = crypto.randomUUID() + await redis.hset(`admin:manual-archive:${jobId}`, 'videoId', videoId, 'status', 'archiving') + await redis.expire(`admin:manual-archive:${jobId}`, 24 * 3600) + + archiveWithRetry(videoId).then(async result => { + await redis.hset(`admin:manual-archive:${jobId}`, 'status', result.success ? 'done' : 'failed', 'message', result.message) + }).catch(async (error: unknown) => { + console.log(`[manual-archive] ${videoId} failed: ${(error as Error).message}`) + await redis.hset(`admin:manual-archive:${jobId}`, 'status', 'failed', 'message', (error as Error).message) + }) + + return redirect(`/admin/archive/${jobId}`) +}, { + body: t.Object({ + video: t.String() + }) +}) + +app.get('/archive/:jobId', async ({ params, set }) => { + const job = /^[0-9a-f-]{36}$/.test(params.jobId) + ? await redis.hgetall(`admin:manual-archive:${params.jobId}`) + : {} + set.headers['Content-Type'] = 'text/html; charset=utf-8' + if (job.status === 'archiving') set.headers['Refresh'] = '5' + if (!job.status) set.status = 404 + return await m(eta.render('./admin/archive', { + title: 'Manual Archive | PreserveTube Admin', + job, + archiveError: job.status ? null : 'Archive job not found or expired.' + })) +}) + app.post('/inbox/ingest', async ({ body, redirect }) => { await ingestInboxMail(body.uid) return redirect('/admin') diff --git a/src/templates/admin/archive.eta b/src/templates/admin/archive.eta new file mode 100644 index 0000000..1172e40 --- /dev/null +++ b/src/templates/admin/archive.eta @@ -0,0 +1,44 @@ +<% layout('../layout') %> + +
Archive a YouTube video directly. Longer videos are added to the size whitelist before archiving.
+ + + + <% if (it.archiveError) { %><%= it.archiveError %>
<% } %> + + <% if (it.job?.status) { %> +Video: <%= it.job.videoId %>
+ <% if (it.job.status === 'archiving') { %> +Archiving in progress. This page refreshes automatically.
+ <% } else if (it.job.status === 'done') { %> +<%= it.job.message %>
+ + <% } else { %> +<%= it.job.message || 'Archiving failed.' %>
+ <% } %> +Requests found in the admin inbox. Approve to whitelist + archive and email the requester the links. Reject asks the requester for more context (optionally with your note) instead of refusing.
@@ -262,6 +265,12 @@ margin: 0; } + .section-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .section-head button { padding: 8px 16px; border: 1px solid #1b1c1f; diff --git a/src/utils/archiveRequests.ts b/src/utils/archiveRequests.ts index 6b7e0f0..9401d24 100644 --- a/src/utils/archiveRequests.ts +++ b/src/utils/archiveRequests.ts @@ -281,4 +281,4 @@ async function dismissRequest(uuid: string): Promise