From e810fd3322aea6d9ce1449ee1f130ec2adc718ba Mon Sep 17 00:00:00 2001
From: CJ_Clippy
Date: Wed, 13 Aug 2025 04:31:07 -0800
Subject: [PATCH] add b2-cli
---
services/our/Dockerfile | 3 +
services/our/src/app.ts | 3 +-
services/our/src/plugins/admin.ts | 162 ++++++++++++++----
services/our/src/plugins/vods.ts | 10 +-
services/our/src/tasks/copyV1S3ToV2.ts | 133 ++++++++++++++
services/our/src/tasks/findWork.ts | 5 +-
.../our/src/tasks/scheduleVodProcessing.ts | 1 +
services/our/src/views/admin.hbs | 79 ++++++++-
services/our/src/views/partials/navbar.hbs | 5 +
services/our/src/views/profile.hbs | 10 ++
services/our/src/views/stream.hbs | 15 +-
services/our/src/views/uploads/show.hbs | 116 ++++++++-----
services/our/src/views/vod.hbs | 2 +-
services/our/src/views/vods.hbs | 2 +-
14 files changed, 457 insertions(+), 89 deletions(-)
create mode 100644 services/our/src/tasks/copyV1S3ToV2.ts
diff --git a/services/our/Dockerfile b/services/our/Dockerfile
index 733a569..36e2705 100644
--- a/services/our/Dockerfile
+++ b/services/our/Dockerfile
@@ -36,6 +36,9 @@ COPY --from=ghcr.io/ggml-org/whisper.cpp:main-e7bf0294ec9099b5fc21f5ba969805dfb2
ENV PATH="$PATH:/app/whisper.cpp/build/bin"
ENV LD_LIBRARY_PATH="/app/whisper.cpp/build/src:/app/whisper.cpp/build/ggml/src:/usr/local/lib:/usr/lib"
+# Install b2-cli
+RUN wget https://github.com/Backblaze/B2_Command_Line_Tool/releases/download/v4.4.1/b2-linux -o /usr/local/bin/b2 && chmod +x /usr/local/bin/b2
+
# Copy and install dependencies
COPY package.json package-lock.json ./
RUN npm install --ignore-scripts=false --foreground-scripts --verbose
diff --git a/services/our/src/app.ts b/services/our/src/app.ts
index d4cb06a..43675d9 100644
--- a/services/our/src/app.ts
+++ b/services/our/src/app.ts
@@ -6,6 +6,7 @@ import vtubersRoutes from './plugins/vtubers'
import usersRoutes from './plugins/users'
import indexRoutes from './plugins/index'
import streamsRoutes from './plugins/streams'
+import adminRoutes from './plugins/admin'
import hls from './plugins/hls.ts'
import fastifyStatic from '@fastify/static'
import fastifySecureSession from '@fastify/secure-session'
@@ -32,7 +33,6 @@ import { icons } from './utils/icons.ts'
import logger from './utils/logger.ts'
import fastifyCaching from '@fastify/caching'
-
export function buildApp() {
const app = Fastify()
@@ -197,6 +197,7 @@ export function buildApp() {
app.register(uploadsRoutes)
app.register(usersRoutes)
app.register(indexRoutes)
+ app.register(adminRoutes)
app.register(authRoutes)
return app
diff --git a/services/our/src/plugins/admin.ts b/services/our/src/plugins/admin.ts
index 66a55cf..973a8ba 100644
--- a/services/our/src/plugins/admin.ts
+++ b/services/our/src/plugins/admin.ts
@@ -1,41 +1,141 @@
-import { PrismaClient } from '../../generated/prisma'
+import { PrismaClient, Prisma } from '../../generated/prisma'
import { withAccelerate } from "@prisma/extension-accelerate"
import { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify'
import { env } from '../config/env'
import { constants } from '../config/constants'
-import { isModerator, isAdmin } from '../utils/privs'
-import type { pool }
+import { isAdmin } from '../utils/privs'
+import { makeWorkerUtils, WorkerUtils } from 'graphile-worker'
+import logger from '../utils/logger'
+import { } from '../../generated/prisma'
-// const prisma = new PrismaClient().$extends(withAccelerate())
+const prisma = new PrismaClient().$extends(withAccelerate())
+
+interface JobRow {
+ id: number;
+ queue_name: string | null;
+ task_identifier: string;
+ priority: number;
+ run_at: Date;
+ attempts: number;
+ max_attempts: number;
+ last_error: string | null;
+ created_at: Date;
+ updated_at: Date;
+ key: string | null;
+ locked_at: Date | null;
+ locked_by: string | null;
+ revision: number;
+ flags: string | null;
+}
+
+async function getJobs() {
+ const jobs = await prisma.$queryRaw(
+ Prisma.sql`
+ SELECT
+ id,
+ queue_name,
+ task_identifier,
+ priority,
+ run_at,
+ attempts,
+ max_attempts,
+ last_error,
+ created_at,
+ updated_at,
+ key,
+ locked_at,
+ locked_by,
+ revision,
+ flags
+ FROM graphile_worker.jobs
+ ORDER BY id DESC
+ LIMIT 2500
+ `
+ );
+
+ return jobs;
+}
-async function indexRoutes(fastify: FastifyInstance): Promise {
- fastify.delete('/admin/queue', async function (request, reply) {
- // const userId = request.session.get('userId')
- // const user = await prisma.user.findUnique({
- // where: { id: userId },
- // include: {
- // roles: true
- // }
- // })
- // if (isAdmin(user)) {
- // // @todo delete the queue
+export default async function adminRoutes(fastify: FastifyInstance): Promise {
+
+ fastify.get('/admin', async function (request, reply) {
+ const userId = request.session.get('userId');
+ if (!userId) {
+ throw new Error('No user is logged in.');
+ }
+
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ include: { roles: true }
+ });
+ if (!user) throw new Error('User not found.');
+ if (!isAdmin(user)) {
+ return reply.status(403).send("you are not a jedi, yet");
+ } else {
+
+ const jobs = await getJobs();
+ logger.debug(jobs[0])
+
+ return reply.viewAsync("admin.hbs", {
+ jobs,
+ user,
+ site: constants.site,
+ }, { layout: 'layouts/main.hbs' });
+ }
+ })
+
+ fastify.delete('/admin/jobs', async function (request, reply) {
- // // const pool = new Pool({
- // // connectionString: process.env.DATABASE_URL,
- // // });
-
- // // const workerUtils = await WorkerUtils.make({ pgPool: pool });
-
- // // await permanentlyFailAllJobs();
- // // await workerUtils.release();
- // // await pool.end();
-
- // }
- // return reply.viewAsync("admin.hbs", {
- // user,
- // site: constants.site
- // });
+ const userId = request.session.get('userId')
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ include: {
+ roles: true
+ }
})
- }
+ if (!user) throw new Error('User not found.');
+
+ if (isAdmin(user)) {
+
+ let workerUtils: WorkerUtils | undefined;
+ try {
+
+ const workerUtils = await makeWorkerUtils({ connectionString: env.DATABASE_URL });
+
+
+
+ const jobIdsResult = await prisma.$queryRaw<{ id: number }[]>(
+ Prisma.sql`SELECT id FROM graphile_worker.jobs`
+ );
+
+
+
+
+ const jobIds: string[] = jobIdsResult.map(j => '' + j.id);
+ logger.warn(`Permanently failing all graphile-worker jobs on behalf of ${user.id}.`);
+
+ if (jobIds.length > 0) {
+ await workerUtils.permanentlyFailJobs(jobIds, 'Job deleted by admin');
+ }
+
+
+
+ }
+ finally {
+ if (workerUtils) {
+ await workerUtils.release();
+ }
+ }
+
+
+ }
+ return reply.viewAsync("admin.hbs", {
+ user,
+ site: constants.site,
+ notification: 'Jobs beleted!',
+
+ });
+ })
+}
diff --git a/services/our/src/plugins/vods.ts b/services/our/src/plugins/vods.ts
index 7dc8d9b..cfff124 100644
--- a/services/our/src/plugins/vods.ts
+++ b/services/our/src/plugins/vods.ts
@@ -57,9 +57,13 @@ export default async function vodsRoutes(
const vods = await prisma.vod.findMany({
where: {
- status: {
- in: ['approved', 'processed', 'processing'],
- },
+ AND: [
+ {
+ status: {
+ in: ['approved', 'processed', 'processing'],
+ }
+ }
+ ]
},
orderBy: { createdAt: 'desc' },
include: {
diff --git a/services/our/src/tasks/copyV1S3ToV2.ts b/services/our/src/tasks/copyV1S3ToV2.ts
new file mode 100644
index 0000000..8d2707a
--- /dev/null
+++ b/services/our/src/tasks/copyV1S3ToV2.ts
@@ -0,0 +1,133 @@
+// Copy futureporn.net s3 asset to future.porn s3 bucket.
+// ex: https://futureporn-b2.b-cdn.net/(...) -> https://fp-usc.b-cdn.net/(...)
+
+import logger from "../utils/logger";
+import { Task } from "graphile-worker";
+import { PrismaClient } from "../../generated/prisma";
+import { withAccelerate } from "@prisma/extension-accelerate";
+import { generateS3Path } from '../utils/formatters';
+import { getOrDownloadAsset } from "../utils/cache";
+import { getNanoSpawn } from "../utils/nanoSpawn";
+import { env } from "../config/env";
+import type { default as NanoSpawn } from 'nano-spawn';
+
+const prisma = new PrismaClient().$extends(withAccelerate());
+
+interface Payload {
+ vodId: string;
+}
+
+function assertPayload(payload: any): asserts payload is Payload {
+ if (typeof payload !== "object" || !payload) throw new Error("invalid payload-- was not an object.");
+ if (typeof payload.vodId !== "string") throw new Error("invalid payload-- was missing vodId");
+}
+
+function isV1Url(url: string): Boolean {
+ return url.includes('futureporn-b2.b-cdn.net');
+}
+
+
+/**
+ * Copy a file from V1 bucket to V2 bucket
+ * @param v1Url The V1 file URL (b2://futureporn/...)
+ * @param v2Key Desired key in V2 bucket
+ * @param contentType Optional override for content type
+ */
+async function copyFromBucketToBucket(spawn: typeof NanoSpawn, v1Url: string, v2Key: string, contentType?: string) {
+ // Get file info from V1
+ logger.info(`v1Url=${v1Url}`);
+ const infoResult = await spawn('b2', ['file', 'info', v1Url]);
+
+ const infoJson = JSON.parse(infoResult.stdout);
+
+ const v2Url = `b2://${process.env.S3_BUCKET}/${v2Key}`;
+
+ logger.info(`Copying ${v1Url} to ${v2Url}...`);
+
+ // Copy file by ID
+ const copyResult = await spawn('b2', [
+ 'file', 'copy-by-id',
+ infoJson.fileId,
+ process.env.S3_BUCKET,
+ v2Key,
+ ]);
+
+ logger.info('Copying complete.');
+
+
+ return v2Url;
+}
+
+// example v1 https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2023-01-01.mp4
+// example v2 https://fp-usc.b-cdn.net/projektmelody-chaturbate-2023-01-01.mp4
+const copyV1S3ToV2: Task = async (payload: any) => {
+
+ const spawn = await getNanoSpawn();
+
+ assertPayload(payload)
+ const { vodId } = payload
+ const vod = await prisma.vod.findFirstOrThrow({
+ where: {
+ id: vodId,
+ AND: [
+ { thumbnail: { not: '' } },
+ { thumbnail: { not: null } }
+ ]
+ },
+ select: {
+ thumbnail: true,
+ sourceVideo: true,
+ streamDate: true,
+ vtubers: true,
+ }
+ })
+
+
+
+ // find what we need to copy over.
+ // potentially vod.thumbnail and vod.sourceVideo
+
+
+
+
+ const thumbnail = vod.thumbnail;
+ const sourceVideo = vod.sourceVideo;
+
+
+ if (!thumbnail && !sourceVideo) {
+ logger.info(`thumbnail and sourceVideo for ${vodId} are missing. nothing to do.`);
+ return;
+ }
+
+ const slug = vod.vtubers[0].slug
+ if (!slug) throw new Error(`vtuber ${vod.vtubers[0].id} is missing a slug`);
+
+
+ let v2ThumbnailKey: string | undefined = undefined;
+ let v2SourceVideoKey: string | undefined = undefined;
+
+ if (thumbnail && isV1Url(thumbnail)) {
+ v2ThumbnailKey = generateS3Path(slug, vod.streamDate, vodId, 'thumbnail.png');
+ await copyFromBucketToBucket(spawn, thumbnail.replace('https://futureporn-b2.b-cdn.net', 'b2://futureporn'), v2ThumbnailKey, 'application/png');
+ }
+
+ if (sourceVideo && isV1Url(sourceVideo)) {
+ v2SourceVideoKey = generateS3Path(slug, vod.streamDate, vodId, 'source.mp4');
+ await copyFromBucketToBucket(spawn, sourceVideo.replace('https://futureporn-b2.b-cdn.net', 'b2://futureporn'), v2SourceVideoKey, 'video/mp4');
+ }
+
+
+
+ logger.debug(`updating vod record`);
+ await prisma.vod.update({
+ where: { id: vodId },
+ data: {
+ ...(v2ThumbnailKey ? { thumbnail: v2ThumbnailKey } : {}), // Variable 'v2ThumbnailKey' is used before being assigned.ts(2454)
+
+ ...(v2SourceVideoKey ? { sourceVideo: v2SourceVideoKey } : {}),
+ }
+ });
+}
+
+
+export default copyV1S3ToV2;
diff --git a/services/our/src/tasks/findWork.ts b/services/our/src/tasks/findWork.ts
index e6b176d..bf24c9e 100644
--- a/services/our/src/tasks/findWork.ts
+++ b/services/our/src/tasks/findWork.ts
@@ -9,7 +9,10 @@ const findWork: Task = async (_payload, helpers: Helpers) => {
const approvedUploads = await prisma.vod.findMany({
where: {
- status: "approved",
+ OR: [
+ { status: "approved" },
+ { status: "pending" },
+ ]
},
});
diff --git a/services/our/src/tasks/scheduleVodProcessing.ts b/services/our/src/tasks/scheduleVodProcessing.ts
index b65b29e..6aebb4c 100644
--- a/services/our/src/tasks/scheduleVodProcessing.ts
+++ b/services/our/src/tasks/scheduleVodProcessing.ts
@@ -47,6 +47,7 @@ const scheduleVodProcessing: Task = async (payload: unknown, helpers) => {
if (!vod.asrVttKey) jobs.push(helpers.addJob("createTranscription", { vodId }));
if (!vod.slvttVTTKey) jobs.push(helpers.addJob("createStoryboard", { vodId }));
if (!vod.magnetLink) jobs.push(helpers.addJob("createTorrent", { vodId }));
+ if (vod.thumbnail && vod.sourceVideo) jobs.push(helpers.addJob("copyV1S3ToV2", { vodId }));
const changes = jobs.length;
if (changes > 0) {
diff --git a/services/our/src/views/admin.hbs b/services/our/src/views/admin.hbs
index d8df832..b6751aa 100644
--- a/services/our/src/views/admin.hbs
+++ b/services/our/src/views/admin.hbs
@@ -1,7 +1,78 @@
-
- Admin page
+
+
+
+ Admin page
-
+
+ {{#if notification}}
+ {{notification}}
+ {{/if}}
+
-
\ No newline at end of file
+
+
+
Graphile Worker Jobs
+
+ {{#if jobs.length}}
+
+
+
+ Id |
+ Queue |
+ Task |
+ Priority |
+ Run At |
+ Attempts |
+ Max Attempts |
+ Last Error |
+ Created |
+ Updated |
+ Key |
+ Locked At |
+ Locked By |
+ Revision |
+ Flags |
+
+
+
+ {{#each jobs}}
+
+ {{this.id}} |
+ {{this.queue_name}} |
+ {{this.task_identifier}} |
+ {{this.priority}} |
+ {{this.run_at}} |
+ {{this.attempts}} |
+ {{this.max_attempts}} |
+ {{this.last_error}} |
+ {{this.created_at}} |
+ {{this.updated_at}} |
+ {{this.key}} |
+ {{this.locked_at}} |
+ {{this.locked_by}} |
+ {{this.revision}} |
+ {{this.flags}} |
+
+ {{/each}}
+
+
+ {{else}}
+
No jobs found.
+ {{/if}}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/services/our/src/views/partials/navbar.hbs b/services/our/src/views/partials/navbar.hbs
index d3c4a70..a57d2ad 100644
--- a/services/our/src/views/partials/navbar.hbs
+++ b/services/our/src/views/partials/navbar.hbs
@@ -50,6 +50,11 @@
{{else}}
Log in via Patreon
{{/if}}
+
+
+ {{#if (hasRole "admin" user)}}
+ Admin
+ {{/if}}
\ No newline at end of file
diff --git a/services/our/src/views/profile.hbs b/services/our/src/views/profile.hbs
index 5ee0644..5c7c228 100644
--- a/services/our/src/views/profile.hbs
+++ b/services/our/src/views/profile.hbs
@@ -20,6 +20,16 @@
--}}
+
+ {{#if (hasRole "admin" user)}}
+
+ {{/if}}
+
Logout
{{>footer}}
diff --git a/services/our/src/views/stream.hbs b/services/our/src/views/stream.hbs
index 1f8d839..7bc3438 100644
--- a/services/our/src/views/stream.hbs
+++ b/services/our/src/views/stream.hbs
@@ -1,22 +1,21 @@
{{#> main}}
-
-
+
+
-
-
+
-
+
{{#each vod.vtubers}}
{{this.displayName}}{{#unless @last}}, {{/unless}}
{{/each}}
– {{formatDate vod.stream.date}}
- Details
+ Details
{{!-- - vtuber
- datetime (formatted)
@@ -24,11 +23,11 @@
- announcementUrl
- platforms
--}}
- Vods
+ Vods
-
+
VOD ID |
diff --git a/services/our/src/views/uploads/show.hbs b/services/our/src/views/uploads/show.hbs
index 2bea886..3bd31b1 100644
--- a/services/our/src/views/uploads/show.hbs
+++ b/services/our/src/views/uploads/show.hbs
@@ -2,7 +2,7 @@
-