Compare commits

..

No commits in common. "e0e10fd9268ece438d666e938549e072f6a843dc" and "f238c13739ed851d6e2b596ac757b01f0590c7d5" have entirely different histories.

281 changed files with 309335 additions and 2063 deletions

1
.nvmrc
View File

@ -1 +0,0 @@
lts/jod

View File

@ -7,6 +7,5 @@
"https://json.schemastore.org/kustomization.json": "file:///home/cj/Documents/futureporn-monorepo/clusters/production/infrastructure.yaml" "https://json.schemastore.org/kustomization.json": "file:///home/cj/Documents/futureporn-monorepo/clusters/production/infrastructure.yaml"
}, },
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.formatOnSave": true, "editor.formatOnSave": true
"git.ignoreLimitWarning": true
} }

View File

@ -3,7 +3,6 @@
echo(" * Remux the .ts to .mp4 using `ffmpeg -i 2025-08-25T00-12-24Z.ts -c copy projektmelody-chaturbate-2025-08-24.mp4") echo(" * Remux the .ts to .mp4 using `ffmpeg -i 2025-08-25T00-12-24Z.ts -c copy projektmelody-chaturbate-2025-08-24.mp4")
echo(" If multiple files, use `ffmpeg -f concat -safe 0 -i ./files.txt -c copy ./projektmelody-chaturbate-2025-08-24.mp4`")
echo(" [Press Enter When Complete...]") echo(" [Press Enter When Complete...]")
_ = stdin() _ = stdin()
@ -20,8 +19,7 @@ echo(" * Remote pin the IPFS CID")
echo(" [Press Enter When Complete...]") echo(" [Press Enter When Complete...]")
_ = stdin() _ = stdin()
echo(" * Create magnet link. ex: `mktorrent --web-seed https://futureporn-b2.b-cdn.net/projektmelody-fansly-2025-08-27.mp4 ~/Documents/voddo/projektmelody-fansly-2025-08-27.mp4`") echo(" * Create magnet link. ex: `torf ~/Downloads/projektmelody-chaturbate-2025-08-25.mp4 --notorrent --notracker --webseed https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2025-08-25.mp4'")
echo(" * Create magnet link. ex: `torf ~/Downloads/projektmelody-chaturbate-2025-08-27.mp4 --notorrent --notracker`")
echo(" [Press Enter When Complete...]") echo(" [Press Enter When Complete...]")
_ = stdin() _ = stdin()

View File

@ -57,7 +57,7 @@ RUN npm run build
RUN python3 -m venv /app/venv RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH" ENV PATH="/app/venv/bin:$PATH"
# Install python deps # Install torrentfile & other python deps
RUN ./venv/bin/pip install --no-cache-dir -r requirements.txt RUN ./venv/bin/pip install --no-cache-dir -r requirements.txt
# Expose the port # Expose the port

View File

@ -1,30 +0,0 @@
// build.mjs
import esbuild from "esbuild";
import copyStaticFiles from "esbuild-copy-static-files";
const build = async () => {
console.log("🏗️ Building...");
await esbuild.build({
entryPoints: ["./src/client/index.js", './src/client/vod.js'],
outdir: "./dist/client",
bundle: true,
minify: true,
sourcemap: true,
platform: "browser",
target: "es2020",
logLevel: "info",
color: true,
plugins: [
copyStaticFiles({
src: "./src/assets",
dest: "./dist/client",
recursive: true,
})
],
});
};
build().catch((err) => {
console.error(err);
process.exit(1);
});

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,14 @@
{ {
"name": "futureporn", "name": "futureporn",
"private": true, "private": true,
"version": "2.5.0", "version": "2.4.14",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently npm:dev:serve npm:dev:build:server npm:dev:build:client npm:dev:worker npm:dev:compose npm:dev:sftp", "dev": "concurrently npm:dev:serve npm:dev:build npm:dev:worker npm:dev:compose npm:dev:sftp",
"dev:serve": "npx @dotenvx/dotenvx run -f ../../.env.development.local -- tsx watch ./src/index.ts", "dev:serve": "npx @dotenvx/dotenvx run -f ../../.env.development.local -- tsx watch ./src/index.ts",
"dev:compose": "docker compose -f compose.development.yaml up", "dev:compose": "docker compose -f compose.development.yaml up",
"dev:worker": "npx @dotenvx/dotenvx run -e GRAPHILE_LOGGER_DEBUG=1 -f ../../.env.development.local -- tsx watch ./src/worker.ts", "dev:worker": "npx @dotenvx/dotenvx run -e GRAPHILE_LOGGER_DEBUG=1 -f ../../.env.development.local -- tsx watch ./src/worker.ts",
"dev:build": "echo please use either dev:build:server or dev:build:client", "dev:build": "chokidar 'src/**/*.{js,ts}' -c tsup --clean",
"dev:build:server": "chokidar 'src/**/*.{js,ts}' --ignore 'src/client/**' -c tsup --clean",
"dev:build:client": "chokidar 'src/client/**/*.{js,css}' -c 'node build.mjs'",
"dev:sftp": "docker run -p 2222:22 --rm atmoz/sftp user:pass:::watch", "dev:sftp": "docker run -p 2222:22 --rm atmoz/sftp user:pass:::watch",
"start": "echo please use either start:server or start:worker; exit 1", "start": "echo please use either start:server or start:worker; exit 1",
"start:server": "tsx ./src/index.ts", "start:server": "tsx ./src/index.ts",
@ -26,9 +24,6 @@
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.3.1", "@eslint/compat": "^1.3.1",
"@eslint/js": "^9.31.0", "@eslint/js": "^9.31.0",
"chokidar": "^4.0.3",
"esbuild": "^0.25.9",
"esbuild-copy-static-files": "^0.1.0",
"eslint": "^9.31.0", "eslint": "^9.31.0",
"eslint-plugin-svelte": "^3.10.1", "eslint-plugin-svelte": "^3.10.1",
"globals": "^16.3.0", "globals": "^16.3.0",
@ -36,11 +31,7 @@
"prisma": "6.8.2", "prisma": "6.8.2",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.36.0", "typescript-eslint": "^8.36.0",
"video.js": "^8.23.4", "vitest": "^3.2.4"
"videojs-contrib-quality-levels": "^4.1.0",
"videojs-hls-quality-selector": "^2.0.0",
"vitest": "^3.2.4",
"yargs": "^18.0.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@ -67,7 +58,6 @@
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "6.8.2", "@prisma/client": "6.8.2",
"@prisma/extension-accelerate": "^1.3.0", "@prisma/extension-accelerate": "^1.3.0",
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.16.3", "@types/node": "^22.16.3",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"cache-manager": "^7.0.1", "cache-manager": "^7.0.1",
@ -77,11 +67,10 @@
"create-torrent": "^6.1.0", "create-torrent": "^6.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"fastify": "^5.4.0", "fastify": "^5.4.0",
"fastify-favicon": "^5.0.0",
"fastify-plugin": "^5.0.1", "fastify-plugin": "^5.0.1",
"fastify-sse-v2": "^4.2.1", "fastify-sse-v2": "^4.2.1",
"form-data": "^4.0.3", "form-data": "^4.0.3",
"fs-extra": "^11.3.1", "fs-extra": "^11.3.0",
"graphile-worker": "^0.16.6", "graphile-worker": "^0.16.6",
"handlebars": "4.7.8", "handlebars": "4.7.8",
"jdenticon": "^3.3.0", "jdenticon": "^3.3.0",
@ -95,6 +84,8 @@
"onnxruntime-node": "1.22.0-rev", "onnxruntime-node": "1.22.0-rev",
"pino": "^9.7.0", "pino": "^9.7.0",
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
"pnpm": "^10.14.0",
"protobufjs": "^7.5.3",
"rate-limiter-flexible": "^7.1.1", "rate-limiter-flexible": "^7.1.1",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"sharp": "^0.34.3", "sharp": "^0.34.3",
@ -110,4 +101,4 @@
"prisma": { "prisma": {
"seed": "tsx prisma/seed.ts" "seed": "tsx prisma/seed.ts"
} }
} }

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Vod" ADD COLUMN "sourceVideoDuration" INTEGER;

View File

@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "Vod" ADD COLUMN "sourceAudioCodec" TEXT,
ADD COLUMN "sourceVideoCodec" TEXT,
ADD COLUMN "sourceVideoFps" DOUBLE PRECISION;

View File

@ -1,10 +0,0 @@
/*
Warnings:
- You are about to drop the column `funscript` on the `Vod` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Vod" DROP COLUMN "funscript",
ADD COLUMN "funscriptThrust" TEXT,
ADD COLUMN "funscriptVibrate" TEXT;

View File

@ -71,26 +71,21 @@ model Vod {
uploaderId String // previously in Upload uploaderId String // previously in Upload
uploader User @relation(fields: [uploaderId], references: [id]) uploader User @relation(fields: [uploaderId], references: [id])
streamDate DateTime streamDate DateTime
notes String? notes String?
segmentKeys Json? segmentKeys Json?
sourceVideo String? sourceVideo String?
sourceVideoDuration Int? // milliseconds hlsPlaylist String?
sourceVideoCodec String? thumbnail String?
sourceAudioCodec String? asrVttKey String?
sourceVideoFps Float? slvttSheetKeys Json?
hlsPlaylist String? slvttVTTKey String?
thumbnail String? magnetLink String?
asrVttKey String?
slvttSheetKeys Json?
slvttVTTKey String?
magnetLink String?
status VodStatus @default(pending) status VodStatus @default(pending)
sha256sum String? sha256sum String?
cidv1 String? cidv1 String?
funscriptVibrate String? funscript String?
funscriptThrust String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@ -10,7 +10,7 @@ import adminRoutes from './plugins/admin'
import hls from './plugins/hls.ts' import hls from './plugins/hls.ts'
import fastifyStatic from '@fastify/static' import fastifyStatic from '@fastify/static'
import fastifySecureSession from '@fastify/secure-session' import fastifySecureSession from '@fastify/secure-session'
import path, { basename, resolve } from 'node:path' import path, { basename } from 'node:path'
import fastifyFormbody from '@fastify/formbody' import fastifyFormbody from '@fastify/formbody'
import fastifyView from "@fastify/view" import fastifyView from "@fastify/view"
import { env } from './config/env' import { env } from './config/env'
@ -21,19 +21,102 @@ import graphileWorker from './plugins/graphileWorker'
import fastifySwagger from "@fastify/swagger" import fastifySwagger from "@fastify/swagger"
import fastifySwaggerUi from "@fastify/swagger-ui" import fastifySwaggerUi from "@fastify/swagger-ui"
import { join } from 'node:path' import { join } from 'node:path'
import { format } from 'date-fns'
import * as jdenticon from 'jdenticon'
import { Role } from '../generated/prisma'
import fastifyFlash from '@fastify/flash' import fastifyFlash from '@fastify/flash'
import { registerHbsHelpers } from './utils/hbsHelpers.ts' import { isModerator, hasRole } from './utils/privs'
import fastifyFavicon from 'fastify-favicon' import { signUrl } from './utils/cdn'
import { extractBasePath } from './utils/filesystem'
import { truncate } from './utils/formatters.ts'
import { icons } from './utils/icons.ts'
import logger from './utils/logger.ts'
import fastifyCaching from '@fastify/caching'
const __dirname = import.meta.dirname; export function buildApp() {
export async function buildApp() {
const app = Fastify() const app = Fastify()
registerHbsHelpers(Handlebars) Handlebars.registerHelper('formatDate', function (dateString) {
if (!dateString) return ''
return format(new Date(dateString), 'yyyy-MM-dd')
})
Handlebars.registerHelper('identicon', function (str, size = 48) {
return jdenticon.toSvg(str, size)
})
Handlebars.registerHelper('safeJson', function (context) {
return new Handlebars.SafeString(JSON.stringify(context));
});
Handlebars.registerHelper('json', function (context) {
return JSON.stringify(context)
})
Handlebars.registerHelper('patron', function (user) {
if (!user.roles) {
throw new Error(
'patron hbs helper was called without roles. This usually means you forgot to include roles relationship in the query.'
);
}
return user.roles.some((r: Role) => r.name.startsWith('supporter'));
});
Handlebars.registerHelper('notEqual', function (a, b) {
return a !== b;
});
Handlebars.registerHelper('isEqual', function (a, b) {
logger.trace(`isEqual a=${a} b=${b}`)
return a == b
});
Handlebars.registerHelper('isModerator', function (user) {
return isModerator(user)
})
Handlebars.registerHelper('hasRole', hasRole)
Handlebars.registerHelper('breaklines', function (text) {
text = Handlebars.Utils.escapeExpression(text);
text = text.replace(/(\r\n|\n|\r)/gm, '<br>');
return new Handlebars.SafeString(text);
});
Handlebars.registerHelper('getCdnUrl', function (s3Key) {
// Before you remove this log, find a way to memoize this function!
logger.info(`getCdnUrl called with CDN_ORIGIN=${env.CDN_ORIGIN} and CDN_TOKEN_SECRET=${env.CDN_TOKEN_SECRET}`)
return signUrl(`${env.CDN_ORIGIN}/${s3Key}`, {
securityKey: env.CDN_TOKEN_SECRET,
expirationTime: constants.timeUnits.sevenDaysInSeconds,
})
})
/**
* @see https://github.com/video-dev/hls.js/issues/2152
*/
Handlebars.registerHelper('signedHlsUrl', function (s3Key) {
if (!s3Key) throw new Error(`signedHlsUrl called with falsy s3Key=${s3Key}`);
const pathAllowed = extractBasePath(s3Key)
const url = signUrl(`${env.CDN_ORIGIN}/${s3Key}`, {
securityKey: env.CDN_TOKEN_SECRET,
pathAllowed,
isDirectory: true,
expirationTime: constants.timeUnits.sevenDaysInSeconds,
})
logger.debug(`pathAllowed=${pathAllowed} url=${url}`)
return url
})
Handlebars.registerHelper('basename', function (url: string) {
return basename(url)
})
Handlebars.registerHelper('trunc', function (str, length = 6) {
return truncate(str, length)
});
Handlebars.registerHelper('icon', function (name: string, size = 20) {
const svg = icons[name];
if (!svg) {
return new Handlebars.SafeString(`<!-- icon "${name}" not found -->`);
}
// Inject width/height if not already set
const sizedSvg = svg
.replace(/<svg([^>]*)>/, `<svg$1 width="${size}" height="${size}">`);
return new Handlebars.SafeString(sizedSvg);
});
const __dirname = import.meta.dirname;
const swaggerOptions = { const swaggerOptions = {
swagger: { swagger: {
info: { info: {
@ -53,18 +136,12 @@ export async function buildApp() {
exposeRoute: true, exposeRoute: true,
}; };
app.register(fastifySwagger, swaggerOptions); app.register(fastifySwagger, swaggerOptions);
app.register(fastifySwaggerUi, swaggerUiOptions); app.register(fastifySwaggerUi, swaggerUiOptions);
app.register(fastifyFavicon, { path: path.join(__dirname, '..', 'dist', 'client'), name: 'favicon.ico', maxAge: 3600 })
app.register(fastifyStatic, { app.register(fastifyStatic, {
root: path.join(__dirname, '..', 'dist', 'client'), root: path.join(__dirname, 'assets'),
prefix: '/assets', // optional: default '/' prefix: '/', // optional: default '/'
constraints: {} // optional: default {} constraints: {} // optional: default {}
}) })
app.register(fastifyFormbody) app.register(fastifyFormbody)
@ -115,8 +192,6 @@ export async function buildApp() {
app.register(hls) app.register(hls)
app.register(vodsRoutes) app.register(vodsRoutes)
app.register(streamsRoutes) app.register(streamsRoutes)
app.register(vtubersRoutes) app.register(vtubersRoutes)
app.register(uploadsRoutes) app.register(uploadsRoutes)
@ -125,6 +200,5 @@ export async function buildApp() {
app.register(adminRoutes) app.register(adminRoutes)
app.register(authRoutes) app.register(authRoutes)
return app return app
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More