admin: add manual video archive action
This commit is contained in:
parent
8592467bfb
commit
c1dc678629
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<% layout('../layout') %>
|
||||
|
||||
<div class="page">
|
||||
<div class="admin-nav">
|
||||
<h2>Manual Archive</h2>
|
||||
<a class="a" href="/admin">Back to requests</a>
|
||||
</div>
|
||||
|
||||
<p class="stage-desc">Archive a YouTube video directly. Longer videos are added to the size whitelist before archiving.</p>
|
||||
|
||||
<form method="POST" action="/admin/archive" class="archive-form">
|
||||
<input type="text" name="video" placeholder="YouTube video URL or ID" aria-label="YouTube video URL or ID" required />
|
||||
<button type="submit">Archive video</button>
|
||||
</form>
|
||||
|
||||
<% if (it.archiveError) { %><p class="error"><%= it.archiveError %></p><% } %>
|
||||
|
||||
<% if (it.job?.status) { %>
|
||||
<div class="result">
|
||||
<p><strong>Video:</strong> <a class="a" href="https://youtube.com/watch?v=<%= it.job.videoId %>" target="_blank" rel="noopener"><%= it.job.videoId %></a></p>
|
||||
<% if (it.job.status === 'archiving') { %>
|
||||
<p>Archiving in progress. This page refreshes automatically.</p>
|
||||
<% } else if (it.job.status === 'done') { %>
|
||||
<p><%= it.job.message %></p>
|
||||
<p><a class="a" href="/watch?v=<%= it.job.videoId %>">Watch archived video</a></p>
|
||||
<% } else { %>
|
||||
<p class="error"><%= it.job.message || 'Archiving failed.' %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page { margin: 0 auto; padding: 5px 10px 10px; width: 65%; }
|
||||
.admin-nav { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }
|
||||
.stage-desc { color: #aaa; font-size: 0.9rem; }
|
||||
.archive-form { display: flex; gap: 8px; margin: 20px 0; }
|
||||
.archive-form input { flex: 1; min-width: 0; padding: 8px; border: 1px solid #ccc; font-family: inherit; }
|
||||
.archive-form button { padding: 8px 16px; border: 1px solid #1b1c1f; background-color: #1b1c1f; color: white; cursor: pointer; font-family: inherit; }
|
||||
.result { border: 1px solid #eee; padding: 10px; }
|
||||
.error { color: #a33; }
|
||||
.a { text-decoration-line: underline; text-decoration-style: dotted; }
|
||||
@media (max-width: 900px) { .page { width: auto; } }
|
||||
</style>
|
||||
|
|
@ -10,7 +10,10 @@
|
|||
|
||||
<div class="section-head">
|
||||
<h3>Archive Requests</h3>
|
||||
<form method="GET" action="/admin/inbox"><button type="submit">manually ingest</button></form>
|
||||
<div class="section-actions">
|
||||
<form method="GET" action="/admin/inbox"><button type="submit">manually ingest</button></form>
|
||||
<form method="GET" action="/admin/archive"><button type="submit">manually archive</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="stage-desc">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.</p>
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -281,4 +281,4 @@ async function dismissRequest(uuid: string): Promise<void> {
|
|||
.execute()
|
||||
}
|
||||
|
||||
export { createRequest, addContext, approveRequest, rejectRequest, dismissRequest, classifyEmail }
|
||||
export { createRequest, addContext, approveRequest, rejectRequest, dismissRequest, classifyEmail, archiveWithRetry }
|
||||
|
|
|
|||
Loading…
Reference in New Issue