add vtuber rss
This commit is contained in:
parent
31efd1ff51
commit
afc9e2d1c8
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Vod" ADD COLUMN "magetLink" TEXT;
|
@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `magetLink` on the `Vod` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Vod" DROP COLUMN "magetLink",
|
||||
ADD COLUMN "magnetLink" TEXT;
|
@ -80,6 +80,7 @@ model Vod {
|
||||
asrVttKey String?
|
||||
slvttSheetKeys Json?
|
||||
slvttVTTKey String?
|
||||
magnetLink String?
|
||||
|
||||
status VodStatus @default(pending)
|
||||
sha256sum String?
|
||||
|
@ -168,7 +168,6 @@ export function buildApp() {
|
||||
handlebars: Handlebars,
|
||||
},
|
||||
templates: join(__dirname, '..', 'src', 'views'),
|
||||
layout: 'layouts/main',
|
||||
viewExt: 'hbs',
|
||||
options: {
|
||||
partials: {
|
||||
|
1
services/our/src/assets/svg/magnet.svg
Normal file
1
services/our/src/assets/svg/magnet.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none"><path fill="#d3d3d3" d="M11 23v6.06c0 .52-.42.94-.94.94H3.94c-.52 0-.94-.42-.94-.94V23l4.028-2.152zm18 0v6.06c0 .52-.42.94-.94.94h-6.12c-.52 0-.94-.42-.94-.94V23l3.99-2.152z"/><path fill="#f8312f" d="M11 23v-7.94c0-2.75 2.2-5.04 4.95-5.06c2.78-.03 5.05 2.23 5.05 5v8h8v-8c0-7.18-5.82-13-13-13S3 7.82 3 15v8z"/></g></svg>
|
After Width: | Height: | Size: 395 B |
1
services/our/src/assets/svg/patreon.svg
Normal file
1
services/our/src/assets/svg/patreon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482c-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46c3.765.047 4.326-4.804 6.068-7.141c1.24-1.662 2.836-2.132 4.801-2.618c3.376-.836 5.678-3.501 5.673-7.031"/></svg>
|
After Width: | Height: | Size: 379 B |
1
services/our/src/assets/svg/rss.svg
Normal file
1
services/our/src/assets/svg/rss.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="SVGKULYdcTK" x1="30.06" x2="225.94" y1="30.06" y2="225.94" gradientTransform="matrix(.11 0 0 .11 2 2)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e3702d"/><stop offset=".11" stop-color="#ea7d31"/><stop offset=".35" stop-color="#f69537"/><stop offset=".5" stop-color="#fb9e3a"/><stop offset=".7" stop-color="#ea7c31"/><stop offset=".89" stop-color="#de642b"/><stop offset="1" stop-color="#d95b29"/></linearGradient></defs><rect width="28" height="28" x="2" y="2" fill="#cc5d15" rx="6.01" ry="6.01"/><rect width="26.91" height="26.91" x="2.54" y="2.54" fill="#f49c52" rx="5.47" ry="5.47"/><rect width="25.82" height="25.82" x="3.1" y="3.1" fill="url(#SVGKULYdcTK)" rx="5.14" ry="5.14"/><path fill="#fff" d="M6.82 6.16v3.83a15.31 15.31 0 0 1 15.3 15.3h3.83A19.14 19.14 0 0 0 6.81 6.17zm0 6.45v3.72a8.97 8.97 0 0 1 8.96 8.97h3.72A12.69 12.69 0 0 0 6.81 12.6zm2.62 7.44a2.63 2.63 0 0 0-2.63 2.62a2.63 2.63 0 0 0 2.63 2.63a2.63 2.63 0 0 0 2.63-2.63a2.63 2.63 0 0 0-2.63-2.63z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -31,6 +31,9 @@ const EnvSchema = z.object({
|
||||
APP_DIR: z.string().default('/app'),
|
||||
WHISPER_DIR: z.string(),
|
||||
LOG_LEVEL: z.string().default('info'),
|
||||
SEEDBOX_SFTP_URL: z.string(),
|
||||
SEEDBOX_SFTP_USERNAME: z.string(),
|
||||
SEEDBOX_SFTP_PASSWORD: z.string(),
|
||||
});
|
||||
|
||||
const parsed = EnvSchema.safeParse(process.env);
|
||||
|
3
services/our/src/plugins/README.md
Normal file
3
services/our/src/plugins/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
Everything in fastify is a plugin. Routes are plugins too.
|
||||
|
||||
@see https://fastify.dev/docs/latest/Guides/Plugins-Guide/#register
|
@ -9,6 +9,7 @@ import { constants } from '../config/constants'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { extractBasePath } from '../utils/filesystem'
|
||||
import { dirname } from 'node:path'
|
||||
import logger from '../utils/logger'
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate())
|
||||
const s3 = getS3Client()
|
||||
@ -61,7 +62,7 @@ function rewriteMp4ReferencesWithSignedUrls(
|
||||
signFn: (path: string) => string
|
||||
): string {
|
||||
|
||||
console.log(`rewriteMp4ReferencesWithSignedUrls called with ${playlistContent} ${cdnBasePath} ${signFn}`)
|
||||
logger.debug(`rewriteMp4ReferencesWithSignedUrls called with ${playlistContent} ${cdnBasePath} ${signFn}`)
|
||||
|
||||
const cleanBase = cdnBasePath.replace(/^\/|\/$/g, '') // remove leading/trailing slash
|
||||
|
||||
@ -101,8 +102,8 @@ export default async function registerHlsRoute(app: FastifyInstance) {
|
||||
select: { hlsPlaylist: true },
|
||||
})
|
||||
|
||||
console.log(`vod as follows`)
|
||||
console.log(vod)
|
||||
logger.debug(`vod as follows`)
|
||||
logger.debug(vod)
|
||||
if (!vod.hlsPlaylist) return reply.status(404).send('')
|
||||
|
||||
const s3Key = `${dirname(vod.hlsPlaylist)}/${manifest}`
|
||||
@ -119,7 +120,7 @@ export default async function registerHlsRoute(app: FastifyInstance) {
|
||||
|
||||
// Otherwise, rewrite .mp4 references with signed URLs
|
||||
const tokenPath = `/${dirname(vod.hlsPlaylist)}/`
|
||||
console.log(`tokenPath=${tokenPath} hlsPlaylist=${vod.hlsPlaylist}`)
|
||||
logger.debug(`tokenPath=${tokenPath} hlsPlaylist=${vod.hlsPlaylist}`)
|
||||
|
||||
if (!tokenPath.startsWith('/')) {
|
||||
throw new Error('tokenPath did not start with a forward slash');
|
||||
|
@ -20,7 +20,7 @@ export default async function indexRoutes(fastify: FastifyInstance): Promise<voi
|
||||
return reply.viewAsync("profile.hbs", {
|
||||
user,
|
||||
site: constants.site
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
})
|
||||
fastify.get('/', async function (request, reply) {
|
||||
|
||||
@ -62,7 +62,7 @@ export default async function indexRoutes(fastify: FastifyInstance): Promise<voi
|
||||
patreonChannels: [],
|
||||
authPath,
|
||||
site: constants.site
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
}
|
||||
|
||||
// Safe to query database
|
||||
@ -94,7 +94,7 @@ export default async function indexRoutes(fastify: FastifyInstance): Promise<voi
|
||||
// streams,
|
||||
// tags,
|
||||
site: constants.site,
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
|
||||
fastify.get('/health', function (request, reply) {
|
||||
@ -114,6 +114,6 @@ export default async function indexRoutes(fastify: FastifyInstance): Promise<voi
|
||||
NODE_ENV,
|
||||
authPath,
|
||||
site: constants.site,
|
||||
})
|
||||
}, { layout: 'layouts/main.hbs' })
|
||||
})
|
||||
}
|
||||
|
@ -345,7 +345,7 @@ export default async function streamsRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
return reply.view("obs.ejs", { obsToken, cdnOrigin });
|
||||
return reply.view("obs.ejs", { obsToken, cdnOrigin }, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
|
||||
|
||||
|
@ -129,7 +129,7 @@ export default async function uploadsRoutes(
|
||||
user,
|
||||
info,
|
||||
site: constants.site,
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
|
||||
fastify.get('/uploads/:uploadId', async function (request, reply) {
|
||||
@ -165,7 +165,7 @@ export default async function uploadsRoutes(
|
||||
upload,
|
||||
user,
|
||||
site: constants.site,
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
|
||||
fastify.get('/upload', async function (request, reply) {
|
||||
@ -203,7 +203,7 @@ export default async function uploadsRoutes(
|
||||
vtubers,
|
||||
user,
|
||||
site: constants.site,
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
})
|
||||
|
||||
|
||||
@ -247,7 +247,7 @@ export default async function uploadsRoutes(
|
||||
vtubers,
|
||||
user,
|
||||
site: constants.site,
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
}
|
||||
|
||||
if (!body.streamDate) {
|
||||
@ -256,7 +256,7 @@ export default async function uploadsRoutes(
|
||||
vtubers,
|
||||
user,
|
||||
site
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
}
|
||||
const vtuberIds = [body.vtuberIds].flat()
|
||||
|
||||
@ -266,7 +266,7 @@ export default async function uploadsRoutes(
|
||||
vtubers,
|
||||
user,
|
||||
site
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
}
|
||||
|
||||
|
||||
@ -602,7 +602,7 @@ export default async function uploadsRoutes(
|
||||
user,
|
||||
site: constants.site,
|
||||
message: `Saved ${updatedUpload.updatedAt} ✅`,
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
|
||||
} catch (err) {
|
||||
request.log.error(err);
|
||||
|
@ -69,7 +69,7 @@ export default async function vodsRoutes(
|
||||
user,
|
||||
vods,
|
||||
site: constants.site,
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
|
||||
|
||||
@ -110,7 +110,7 @@ export default async function vodsRoutes(
|
||||
vod,
|
||||
site: constants.site,
|
||||
user,
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
|
||||
fastify.post('/vods/:id/process', async function (request, reply) {
|
||||
@ -147,7 +147,7 @@ export default async function vodsRoutes(
|
||||
site: constants.site,
|
||||
user,
|
||||
message: 'Successfully scheduled vod processing.'
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
})
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { withAccelerate } from "@prisma/extension-accelerate"
|
||||
import { isUnprivilegedUser } from "../utils/privs";
|
||||
import { slug } from "../utils/formatters";
|
||||
import type { UploadResult } from '../types/index'
|
||||
import { env } from "../config/env";
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate())
|
||||
const hexColorRegex = /^#([0-9a-fA-F]{6})$/;
|
||||
@ -37,7 +38,7 @@ export default async function vtubersRoutes(
|
||||
user,
|
||||
vtubers,
|
||||
site: constants.site
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
|
||||
|
||||
@ -60,7 +61,7 @@ export default async function vtubersRoutes(
|
||||
user,
|
||||
info: reply.flash('info'),
|
||||
error: reply.flash('error')
|
||||
})
|
||||
}, { layout: 'layouts/main.hbs' })
|
||||
})
|
||||
|
||||
fastify.post('/vtubers/create', async function (request, reply) {
|
||||
@ -169,7 +170,7 @@ export default async function vtubersRoutes(
|
||||
vtubers,
|
||||
user,
|
||||
site
|
||||
});
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
}
|
||||
|
||||
|
||||
@ -284,7 +285,61 @@ export default async function vtubersRoutes(
|
||||
vtuber,
|
||||
site: constants.site,
|
||||
user,
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
|
||||
|
||||
fastify.get('/vtubers/:idOrSlug/rss', async function (request, reply) {
|
||||
const { idOrSlug } = request.params as { idOrSlug: string };
|
||||
|
||||
|
||||
if (!idOrSlug) {
|
||||
return reply.status(400).send({ error: 'Invalid VTuber identifier' });
|
||||
}
|
||||
|
||||
// Determine if it's a CUID (starts with "c" and length of 24)
|
||||
const isCuid = /^c[a-z0-9]{23}$/i.test(idOrSlug);
|
||||
|
||||
const vtuber = await prisma.vtuber.findFirst({
|
||||
where: isCuid
|
||||
? { id: idOrSlug }
|
||||
: {
|
||||
OR: [
|
||||
{ slug: idOrSlug },
|
||||
{ id: idOrSlug }, // fallback if someone pastes a cuid as a slug
|
||||
],
|
||||
},
|
||||
include: {
|
||||
vods: {
|
||||
orderBy: {
|
||||
streamDate: 'desc',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!vtuber) {
|
||||
return reply.status(404).send({ error: 'VTuber not found' });
|
||||
}
|
||||
|
||||
const items = vtuber.vods.map(vod => ({
|
||||
title: `Stream on ${vod.streamDate.toDateString()}`,
|
||||
link: `${env.ORIGIN}/vod/${vod.id}`,
|
||||
guid: `${env.ORIGIN}/vod/${vod.id}`,
|
||||
pubDate: vod.streamDate.toUTCString(),
|
||||
description: vod.notes || 'No description available',
|
||||
}));
|
||||
|
||||
const title = 'future.porn - ' + vtuber.displayName || vtuber.slug
|
||||
|
||||
return reply
|
||||
.type('application/rss+xml')
|
||||
.view('/feed.hbs', {
|
||||
title,
|
||||
description: vtuber.description || title,
|
||||
link: `${env.ORIGIN}/vtuber/${vtuber.slug || vtuber.id}`,
|
||||
items,
|
||||
}, { layout: 'layouts/xml.hbs' });
|
||||
});
|
||||
|
||||
}
|
154
services/our/src/tasks/createTorrent.ts
Normal file
154
services/our/src/tasks/createTorrent.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import type { Helpers } from "graphile-worker";
|
||||
import { PrismaClient } from "../../generated/prisma";
|
||||
import { withAccelerate } from "@prisma/extension-accelerate";
|
||||
import { getOrDownloadAsset } from "../utils/cache";
|
||||
import { env } from "../config/env";
|
||||
import { getS3Client, uploadFile } from "../utils/s3";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
import { preparePython } from "../utils/python";
|
||||
import logger from "../utils/logger";
|
||||
import { basename, join } from "node:path";
|
||||
import SftpClient from 'ssh2-sftp-client';
|
||||
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
|
||||
|
||||
interface Payload {
|
||||
vodId: string;
|
||||
}
|
||||
|
||||
|
||||
// async function createTorrent(payload: any, helpers: Helpers) {
|
||||
// helpers.logger.debug(`createTorrent`)
|
||||
|
||||
|
||||
// if (!inputFilePath) {
|
||||
// throw new Error("inputFilePath is missing");
|
||||
// }
|
||||
|
||||
// await preparePython()
|
||||
// const outputFilePath = inputFilePath.replace(/\.[^/.]+$/, '') + '-thumb.png';
|
||||
// const spawn = await getNanoSpawn();
|
||||
|
||||
|
||||
// helpers.logger.debug('result as follows')
|
||||
// helpers.logger.debug(JSON.stringify(result, null, 2))
|
||||
|
||||
// helpers.logger.info(`✅ Thumbnail saved to: ${outputFilePath}`);
|
||||
// return outputFilePath
|
||||
|
||||
// }
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
export default async function createTorrent(payload: any, helpers: Helpers) {
|
||||
assertPayload(payload)
|
||||
const { vodId } = payload
|
||||
const vod = await prisma.vod.findFirstOrThrow({
|
||||
where: {
|
||||
id: vodId
|
||||
}
|
||||
})
|
||||
|
||||
const spawn = await getNanoSpawn();
|
||||
|
||||
// * [x] load vod
|
||||
|
||||
// * [x] exit if video.thumbnail already defined
|
||||
if (vod.magnetLink) {
|
||||
logger.info(`Doing nothing-- vod ${vodId} already has a magnet link.`)
|
||||
return; // Exit the function early
|
||||
}
|
||||
|
||||
if (!vod.sourceVideo) {
|
||||
throw new Error(`Failed to create magnet link-- vod ${vodId} is missing a sourceVideo.`);
|
||||
}
|
||||
|
||||
|
||||
logger.info('Creating magnet link.')
|
||||
const s3Client = getS3Client()
|
||||
|
||||
// * [x] download video segments from pull-thru cache
|
||||
const videoFilePath = await getOrDownloadAsset(s3Client, env.S3_BUCKET, vod.sourceVideo)
|
||||
logger.debug(`videoFilePath=${videoFilePath}`)
|
||||
|
||||
// * [x] run torrentfile
|
||||
|
||||
// torrentfile create
|
||||
// --magnet
|
||||
// --prog 0
|
||||
// --out ./test-fixture.torrent
|
||||
// --announce udp://tracker.futureporn.net/
|
||||
// --source https://futureporn.net/
|
||||
// --comment https://futureporn.net/
|
||||
// --web-seed https://futureporn-b2.b-cdn.net/test-fixture.ts
|
||||
// --meta-version 3
|
||||
// ~/Downloads/test-fixture.ts
|
||||
|
||||
const torrentOutputFile = join(env.CACHE_ROOT, `${nanoid()}.torrent`);
|
||||
|
||||
const result = await spawn('./venv/bin/torrentfile', [
|
||||
'create',
|
||||
'--magnet',
|
||||
'--prog', '0',
|
||||
'--meta-version', '2',
|
||||
'--comment', 'https://future.porn',
|
||||
'--source', `https://future.porn/vod/${vodId}`,
|
||||
'--out', torrentOutputFile,
|
||||
videoFilePath,
|
||||
], {
|
||||
cwd: env.APP_DIR,
|
||||
});
|
||||
|
||||
|
||||
|
||||
logger.trace(JSON.stringify(result));
|
||||
|
||||
|
||||
const match = result.stdout.match(/magnet:\?[^\s]+/);
|
||||
if (!match) {
|
||||
throw new Error('No magnet link found in torrentfile output:\n' + result.stdout);
|
||||
}
|
||||
|
||||
const magnetLink = match[0];
|
||||
logger.debug(`Magnet link=${magnetLink}`);
|
||||
|
||||
|
||||
// upload torrent file to seedbox sftp
|
||||
// Actually I don't think we need this, because our seedbox can use the RSS feed and get informed about torrents that way
|
||||
// let sftp = new SftpClient();
|
||||
// const torrentBasename = basename(torrentOutputFile);
|
||||
|
||||
// const parsed = new URL(env.SEEDBOX_SFTP_URL);
|
||||
// logger.debug(`url=${env.SEEDBOX_SFTP_URL} hostname=${parsed.hostname} port=${parsed.port} username=${env.SEEDBOX_SFTP_USERNAME} password=${env.SEEDBOX_SFTP_PASSWORD}`);
|
||||
// await sftp.connect({
|
||||
// host: parsed.hostname,
|
||||
// port: parsed.port,
|
||||
// username: env.SEEDBOX_SFTP_USERNAME,
|
||||
// password: env.SEEDBOX_SFTP_PASSWORD
|
||||
// })
|
||||
|
||||
// const remoteFilePath = join(parsed.pathname, torrentBasename)
|
||||
|
||||
// const data = await sftp.list(parsed.pathname);
|
||||
// logger.debug(`the data=${JSON.stringify(data)}`);
|
||||
|
||||
// logger.debug(`uploading ${torrentOutputFile} to ${remoteFilePath}`)
|
||||
// await sftp.put(torrentOutputFile, remoteFilePath);
|
||||
|
||||
|
||||
logger.debug(`updating vod record`);
|
||||
await prisma.vod.update({
|
||||
where: { id: vodId },
|
||||
data: { magnetLink }
|
||||
});
|
||||
|
||||
logger.debug(`all done.`)
|
||||
|
||||
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
import type { Task, Helpers } from "graphile-worker";
|
||||
import { PrismaClient } from "../../generated/prisma";
|
||||
import { access, stat, writeFile } from "node:fs/promises";
|
||||
import { createReadStream, createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { getOrDownloadAsset } from "../utils/cache";
|
||||
import { env } from "../config/env";
|
||||
@ -11,6 +9,8 @@ import { uploadFile } from "../utils/s3";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
import { VodSegment } from "../types";
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
import { parseISO, getYear, getMonth, getDate } from 'date-fns';
|
||||
import logger from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const client = new S3Client({
|
||||
@ -46,7 +46,7 @@ async function validateSegments(segments: VodSegment[], helpers: Helpers) {
|
||||
throw new Error("No VOD segments provided");
|
||||
}
|
||||
|
||||
helpers.logger.info(`Processing ${segments.length} video segments`);
|
||||
logger.info(`Processing ${segments.length} video segments`);
|
||||
}
|
||||
|
||||
async function downloadSegments(
|
||||
@ -57,14 +57,14 @@ async function downloadSegments(
|
||||
|
||||
for (const [index, segment] of segmentKeys.entries()) {
|
||||
try {
|
||||
helpers.logger.debug(`Downloading segment ${index + 1}/${segmentKeys.length}`);
|
||||
logger.debug(`Downloading segment ${index + 1}/${segmentKeys.length}`);
|
||||
const path = await getOrDownloadAsset(client, env.S3_BUCKET, segment.key);
|
||||
downloadedPaths.push(path);
|
||||
|
||||
// Verify the segment exists and is accessible
|
||||
await access(path);
|
||||
const size = await getFileSize(path);
|
||||
helpers.logger.debug(`Segment ${index + 1} size: ${(size / 1024 / 1024).toFixed(2)}MB`);
|
||||
logger.debug(`Segment ${index + 1} size: ${(size / 1024 / 1024).toFixed(2)}MB`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to download segment ${segment.key}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
@ -89,7 +89,7 @@ async function concatenateSegments(
|
||||
concatSpec: FFmpegConcatSpec,
|
||||
helpers: Helpers
|
||||
): Promise<string> {
|
||||
helpers.logger.info(`Concatenating ${concatSpec.files.length} segments`);
|
||||
logger.info(`Concatenating ${concatSpec.files.length} segments`);
|
||||
|
||||
try {
|
||||
const spawn = await getNanoSpawn();
|
||||
@ -105,13 +105,13 @@ async function concatenateSegments(
|
||||
cwd: env.CACHE_ROOT,
|
||||
});
|
||||
|
||||
helpers.logger.debug(`FFmpeg output: ${proc.stdout}`);
|
||||
helpers.logger.debug(`FFmpeg stderr: ${proc.stderr}`);
|
||||
logger.debug(`FFmpeg output: ${proc.stdout}`);
|
||||
logger.debug(`FFmpeg stderr: ${proc.stderr}`);
|
||||
|
||||
// Verify output file
|
||||
await access(concatSpec.outputPath);
|
||||
const outputSize = await getFileSize(concatSpec.outputPath);
|
||||
helpers.logger.info(`Concatenated file size: ${(outputSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
logger.info(`Concatenated file size: ${(outputSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
return concatSpec.outputPath;
|
||||
} catch (error) {
|
||||
@ -124,7 +124,7 @@ async function updateVodWithSourceVideo(
|
||||
sourcePath: string,
|
||||
helpers: Helpers
|
||||
): Promise<void> {
|
||||
helpers.logger.info(`Updating VOD ${vodId} with source video ${sourcePath}`);
|
||||
logger.info(`Updating VOD ${vodId} with source video ${sourcePath}`);
|
||||
|
||||
await prisma.vod.update({
|
||||
where: { id: vodId },
|
||||
@ -158,7 +158,7 @@ const getSourceVideo: Task = async (payload: unknown, helpers) => {
|
||||
assertPayload(payload);
|
||||
const { vodId } = payload;
|
||||
|
||||
helpers.logger.info(`Processing source video for VOD ${vodId}`);
|
||||
logger.info(`Processing source video for VOD ${vodId}`);
|
||||
|
||||
try {
|
||||
// Get VOD info from database
|
||||
@ -168,12 +168,14 @@ const getSourceVideo: Task = async (payload: unknown, helpers) => {
|
||||
sourceVideo: true,
|
||||
segmentKeys: true,
|
||||
status: true,
|
||||
vtubers: true,
|
||||
streamDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Skip if already processed
|
||||
if (vod.sourceVideo) {
|
||||
helpers.logger.debug(`VOD ${vodId} already has a source video`);
|
||||
logger.debug(`VOD ${vodId} already has a source video`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -196,7 +198,7 @@ const getSourceVideo: Task = async (payload: unknown, helpers) => {
|
||||
if (downloadedPaths.length === 1) {
|
||||
// Single segment - no concatenation needed
|
||||
sourceVideoPath = downloadedPaths[0];
|
||||
helpers.logger.info(`Using single segment as source video`);
|
||||
logger.info(`Using single segment as source video`);
|
||||
} else {
|
||||
// Multiple segments - concatenate
|
||||
const concatSpec: FFmpegConcatSpec = {
|
||||
@ -211,14 +213,17 @@ const getSourceVideo: Task = async (payload: unknown, helpers) => {
|
||||
|
||||
|
||||
// upload the concatenated video
|
||||
const key = await uploadFile(client, env.S3_BUCKET, `source/${nanoid()}.mp4`, sourceVideoPath, 'video/mp4');
|
||||
const year = getYear(vod.streamDate);
|
||||
const month = getMonth(vod.streamDate) + 1;
|
||||
const day = getDate(vod.streamDate);
|
||||
const key = await uploadFile(client, env.S3_BUCKET, `fp/${vod.vtubers[0].slug}/${year}/${month}/${day}/${nanoid()}/source.mp4`, sourceVideoPath, 'video/mp4');
|
||||
|
||||
// Update database with source video path
|
||||
await updateVodWithSourceVideo(vodId, key, helpers);
|
||||
|
||||
helpers.logger.info(`Successfully processed source video for VOD ${vodId}`);
|
||||
logger.info(`Successfully processed source video for VOD ${vodId}`);
|
||||
} catch (error) {
|
||||
helpers.logger.error(`Failed to process source video for VOD ${vodId}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(`Failed to process source video for VOD ${vodId}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
// await prisma.vod.update({
|
||||
// where: { id: vodId },
|
||||
// data: { status: "failed" },
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { Helpers, Task, Job } from "graphile-worker";
|
||||
import type { Task, Job } from "graphile-worker";
|
||||
import { PrismaClient } from "../../generated/prisma";
|
||||
import { withAccelerate } from "@prisma/extension-accelerate";
|
||||
import { addMinutes, addSeconds } from 'date-fns';
|
||||
|
||||
|
||||
interface Payload {
|
||||
@ -47,6 +46,7 @@ const scheduleVodProcessing: Task = async (payload: unknown, helpers) => {
|
||||
if (!vod.funscript) jobs.push(helpers.addJob("createFunscript", { vodId }));
|
||||
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 }));
|
||||
|
||||
const changes = jobs.length;
|
||||
if (changes > 0) {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import logger from "./logger";
|
||||
|
||||
|
||||
type UserWithRoles = { roles: { name: string }[] };
|
||||
|
||||
@ -20,9 +22,11 @@ export function isUnprivilegedUser(user: { roles: { name: string }[] }): boolean
|
||||
|
||||
|
||||
export function hasRole(...args: any[]) {
|
||||
const options = args.pop(); // handlebars injects this
|
||||
const roles: string[] = args.slice(0, -1);
|
||||
const user = args[args.length - 1];
|
||||
const options = args.pop(); // handlebars options object
|
||||
const user = args.pop(); // user is second-to-last arg
|
||||
const roles: string[] = args; // everything else is role names
|
||||
|
||||
logger.trace(`roles=${roles.join(',')} user=${JSON.stringify(user)}`);
|
||||
|
||||
if (!user?.roles) return false;
|
||||
return user.roles.some((r: any) => roles.includes(r.name));
|
||||
|
@ -23,7 +23,7 @@ export async function preparePython() {
|
||||
await spawn(pythonCmd, ["-m", "venv", venvPath], {
|
||||
cwd: env.APP_DIR,
|
||||
});
|
||||
console.log("✅ Python venv created.");
|
||||
console.log("Python venv created.");
|
||||
} else {
|
||||
console.log("Using existing Python venv.");
|
||||
}
|
||||
@ -34,15 +34,15 @@ export async function preparePython() {
|
||||
await spawn(pipCmd, ["install", "-r", "requirements.txt"], {
|
||||
cwd: env.APP_DIR,
|
||||
});
|
||||
console.log("✅ requirements.txt installed.");
|
||||
console.log("requirements.txt installed.");
|
||||
|
||||
// 4. Confirm vcsi CLI binary exists
|
||||
const vcsiBinary = join(venvBin, "vcsi");
|
||||
if (!existsSync(vcsiBinary)) {
|
||||
console.error("❌ vcsi binary not found in venv after installing requirements.");
|
||||
console.error("vcsi binary not found in venv after installing requirements.");
|
||||
console.error("Make sure 'vcsi' is listed in requirements.txt and that it installs a CLI.");
|
||||
throw new Error("vcsi installation failed or did not expose CLI.");
|
||||
}
|
||||
|
||||
console.log("✅ vcsi CLI is available at", vcsiBinary);
|
||||
console.log("vcsi CLI is available at", vcsiBinary);
|
||||
}
|
||||
|
16
services/our/src/views/feed.hbs
Normal file
16
services/our/src/views/feed.hbs
Normal file
@ -0,0 +1,16 @@
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>{{title}}</title>
|
||||
<link>{{link}}</link>
|
||||
<description>{{description}}</description>
|
||||
{{#each items}}
|
||||
<item>
|
||||
<title>{{this.title}}</title>
|
||||
<link>{{this.link}}</link>
|
||||
<guid isPermaLink="true">{{this.guid}}</guid>
|
||||
<pubDate>{{this.pubDate}}</pubDate>
|
||||
<description><![CDATA[{{this.description}}]]></description>
|
||||
</item>
|
||||
{{/each}}
|
||||
</channel>
|
||||
</rss>
|
2
services/our/src/views/layouts/xml.hbs
Normal file
2
services/our/src/views/layouts/xml.hbs
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
{{{body}}}
|
@ -8,9 +8,10 @@
|
||||
<section id="perks">
|
||||
<h2>Perks</h2>
|
||||
|
||||
<p>future.porn is free to use, but to keep the site running, we need your help! In return, we offer extra perks
|
||||
<p>future.porn is free to use, but to keep the site running we need your help! In return, we offer extra perks
|
||||
to supporters.</p>
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -63,6 +64,8 @@
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
{{!--
|
||||
@todo add these things
|
||||
<tr>
|
||||
<td><abbr title="Closed Captions">CC</abbr> Search</td>
|
||||
<td></td>
|
||||
@ -87,9 +90,15 @@
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
--}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<article>
|
||||
<p>Become a patron at <a target="_blank" href="https://patreon.com/CJ_Clippy">patreon.com/CJ_Clippy</a></p>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
|
||||
{{> footer}}
|
||||
|
@ -119,7 +119,7 @@
|
||||
|
||||
|
||||
<h2>Downloads</h2>
|
||||
<h3>Raw Recorded File Segments</h3>
|
||||
<h3>Raw Segments</h3>
|
||||
{{#if vod.segmentKeys}}
|
||||
<ul>
|
||||
{{#each vod.segmentKeys}}
|
||||
@ -135,13 +135,24 @@
|
||||
</article>
|
||||
{{/if}}
|
||||
|
||||
<h3>Concatenated Video</h3>
|
||||
<h3>VOD</h3>
|
||||
{{#if vod.sourceVideo}}
|
||||
|
||||
{{#if (hasRole "supporterTier1" user)}}
|
||||
<p><a data-source-video="{{getCdnUrl vod.sourceVideo}}" data-file-name="{{basename vod.sourceVideo}}"
|
||||
x-on:click.prevent="download($el.dataset.sourceVideo, $el.dataset.fileName)"
|
||||
href="{{getCdnUrl vod.sourceVideo}}" download="{{basename vod.sourceVideo}}"
|
||||
target="_blank">{{icon "download" 24}} Download</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<p>
|
||||
<a href="/perks">{{icon "patreon" 24}}</a>
|
||||
<del>
|
||||
CDN Download
|
||||
</del>
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<p>{{#if vod.sha256sum}}<span><b>sha256sum</b> {{vod.sha256sum}}</span>{{/if}}</p>
|
||||
{{#if vod.cidv1}}
|
||||
<p><b>IPFS cidv1</b> {{vod.cidv1}}</p>
|
||||
@ -150,6 +161,13 @@
|
||||
IPFS CID is processing.
|
||||
</article>
|
||||
{{/if}}
|
||||
{{#if vod.magnetLink}}
|
||||
<p><a href="{{vod.magnetLink}}">{{icon "magnet" 24}} Magnet Link</a></p>
|
||||
{{else}}
|
||||
<article>
|
||||
Magnet Link is processing.
|
||||
</article>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<article>
|
||||
Video Source is processing.
|
||||
|
@ -1,4 +1,5 @@
|
||||
{{#> main}}
|
||||
|
||||
<header class="container">
|
||||
{{> navbar}}
|
||||
</header>
|
||||
|
@ -1,4 +1,6 @@
|
||||
{{#> main}}
|
||||
<link rel="alternate" type="application/rss+xml" href="/vtubers/{{vtuber.slug}}/rss" />
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/video.js@8.22.0/dist/video-js.min.css">
|
||||
|
||||
<header class="container">
|
||||
@ -38,7 +40,8 @@
|
||||
</video> --}}
|
||||
|
||||
<h1>
|
||||
{{vtuber.displayName}}
|
||||
{{vtuber.displayName}} <a href="/vtubers/{{vtuber.slug}}/rss"
|
||||
alt="RSS feed for {{vtuber.displayName}}">{{icon "rss" 32}}</a>
|
||||
</h1>
|
||||
|
||||
<div class="grid">
|
||||
|
Loading…
x
Reference in New Issue
Block a user