progress
This commit is contained in:
parent
0b5ce37af8
commit
b7a64d1fd3
@ -1,21 +1,23 @@
|
||||
devbox for shareable development environment tooling
|
||||
nodejs for everything
|
||||
|
||||
Node version manager (nvm) to install node
|
||||
|
||||
git monorepo for housing separate packages within a single repository (see ./services and ./packages)
|
||||
|
||||
pnpm for package management and workspaces (separate node packages.)
|
||||
|
||||
Phoenix framework
|
||||
|
||||
docker-compose for containerized development
|
||||
|
||||
Kamal for deployments
|
||||
Komodo for deployments
|
||||
|
||||
ggshield for preventing git commits containing secrets
|
||||
ggshield for preventing accidental git commits containing secrets
|
||||
|
||||
direnv for loading .envrc
|
||||
|
||||
Graphile Worker for work queue, cron
|
||||
|
||||
nano-spawn or execa to run any non-node programs like yolo or
|
||||
|
||||
Postgres for data storage
|
||||
|
||||
S3 for media storage
|
||||
|
27
README.md
27
README.md
@ -2,47 +2,30 @@
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://www.jetify.com/devbox/docs/contributor-quickstart/)
|
||||
|
||||
Source Code for https://futureporn.net
|
||||
|
||||
See ./ARCHITECTURE.md for an overview of the infrastructure components.
|
||||
|
||||
## Getting Started
|
||||
## Dev notes
|
||||
|
||||
The main gist is as follows.
|
||||
|
||||
1. install [docker](https://docs.docker.com/engine/install) `wget -O- get.docker.com | bash`
|
||||
1. Install [devbox](https://www.jetify.com/devbox/docs/installing_devbox/) `curl -fsSL https://get.jetify.com/devbox | bash`
|
||||
2. Install development environment & packages using devbox.
|
||||
|
||||
devbox install
|
||||
|
||||
|
||||
3. Run database and other accessories with `docker compose up --watch`
|
||||
4. In another terminal, run the phoenix "bright" app with `devbox run bright:dev`
|
||||
4. Visit http://localhost:4000
|
||||
|
||||
If all went well, editing source code will automatically affect the website running in your browser.
|
||||
|
||||
## backup/restore dev database
|
||||
### backup/restore dev database
|
||||
|
||||
@see https://stackoverflow.com/a/29913462/1004931
|
||||
|
||||
### backup
|
||||
#### backup
|
||||
|
||||
Use devbox helper script
|
||||
|
||||
devbox run backup
|
||||
|
||||
### restore
|
||||
#### restore
|
||||
|
||||
cat ./backups/your-backup.sql | docker exec -i postgres_db psql -U postgres
|
||||
|
||||
|
||||
## testing
|
||||
### testing
|
||||
|
||||
there is some undesirable behavior when running tests because nektos/act mimicks github actions.
|
||||
we are banned from github so we aren't using that. instead, we use gitea act_runner.
|
||||
|
@ -26,12 +26,15 @@ RUN wget -q https://github.com/shaka-project/shaka-packager/releases/download/v3
|
||||
COPY --from=ipfs/kubo:v0.36.0 /usr/local/bin/ipfs /usr/local/bin/ipfs
|
||||
RUN ipfs init
|
||||
|
||||
|
||||
# Bundle the vibeui pytorch model
|
||||
RUN mkdir -p /app/vibeui \
|
||||
&& wget -q https://gitea.futureporn.net/futureporn/fp/raw/branch/main/apps/vibeui/public/vibeui.pt -O /app/vibeui/vibeui.pt \
|
||||
&& wget -q https://gitea.futureporn.net/futureporn/fp/raw/branch/main/apps/vibeui/public/data.yaml -O /app/vibeui/data.yaml
|
||||
|
||||
# Install openwhisper
|
||||
COPY --from=ghcr.io/ggml-org/whisper.cpp:main-e7bf0294ec9099b5fc21f5ba969805dfb2108cea /app /app/whisper.cpp
|
||||
ENV PATH="$PATH:/app/whisper.cpp/build/bin"
|
||||
|
||||
# Copy and install dependencies
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install --ignore-scripts=false --foreground-scripts --verbose
|
||||
|
@ -1,5 +1,34 @@
|
||||
# futureporn
|
||||
|
||||
https://future.porn
|
||||
|
||||
|
||||
## Software Dependencies
|
||||
|
||||
* node
|
||||
* pnpm
|
||||
* ffmpeg
|
||||
* [whisper-cli](https://github.com/ggml-org/whisper.cpp)
|
||||
* [shaka-packager](https://github.com/shaka-project/shaka-packager/releases)
|
||||
|
||||
|
||||
## Getting started (developers only)
|
||||
|
||||
Ensure you have all the above software dependencies available on system PATH
|
||||
|
||||
Install node packages
|
||||
|
||||
pnpm install
|
||||
|
||||
Start docker containers
|
||||
|
||||
docker compose -f ./compose.development.yaml up
|
||||
|
||||
Start node app in dev mode. Env vars must be available to the app-- We're using dotenvx to load them.
|
||||
|
||||
dotenvx run -f ../../.env.development.local -- pnpm run dev
|
||||
|
||||
|
||||
## projekt requirements
|
||||
|
||||
* [x] NO BUNDLER (esbuild/vite/webpack/parcel/swc/etc.). IF YOU REACH FOR A BUNDLER, YOU'RE OVERCOMPLICATING IT!
|
||||
@ -21,8 +50,8 @@
|
||||
## Tiers & Privs
|
||||
|
||||
* user - view, torrent, download
|
||||
* supporterTier1 - view, torrent, download, adfree, upload
|
||||
* supporterTier6 - view, torrent, download, adfree, upload, csv, sql
|
||||
* supporterTier1 - view, torrent, download, adfree, upload, vibeui, closed captions, search
|
||||
* supporterTier6 - view, torrent, download, adfree, upload, vibeui, closed captions, search, csv, sql, pytorch
|
||||
|
||||
## troubleshooting
|
||||
|
||||
@ -84,7 +113,7 @@ sharp is often a pain in the ass to install.
|
||||
```
|
||||
|
||||
|
||||
If you have trouble installing sharp, try ignoring the system's installed libvips.
|
||||
If you have trouble installing sharp, try ignoring the system's installed libvips. This usually needs to be done after every time npm installs a new package.
|
||||
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --ignore-scripts=false --foreground-scripts --verbose --platform=linux --arch=x64 sharp
|
||||
|
||||
@ -92,7 +121,6 @@ If you have trouble installing sharp, try ignoring the system's installed libvip
|
||||
|
||||
Actually, better advice is to probably **remove libvips from the system**. This way, a compatible libvips is always pulled during `npm install`.
|
||||
|
||||
Annoyingly, it might be necessary to re-install sharp after every new npm package, even if said package is unrelated to sharp.
|
||||
|
||||
#### edgesOut??
|
||||
|
||||
@ -153,13 +181,18 @@ npm verbose code 1
|
||||
npm error A complete log of this run can be found in: /home/cj/.npm/_logs/2025-07-14T12_49_09_670Z-debug-0.log
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Apply migrations
|
||||
|
||||
dotenvx run -f ../../.env.development.local -- npx prisma migrate dev --name "rename_asrvtt"
|
||||
|
||||
## Deployments
|
||||
|
||||
### Apply migrations
|
||||
|
||||
cd /opt/futureporn/services/our
|
||||
npx @dotenvx/dotenvx run -f /usr/local/etc/futureporn/our/env -- npx prisma migrate deploy
|
||||
npx @dotenvx/dotenvx run -f /usr/local/etc/futureporn/our/.env -- npx prisma migrate deploy
|
||||
|
||||
### pgweb
|
||||
|
||||
|
@ -5,7 +5,7 @@ services:
|
||||
container_name: our-postgres
|
||||
image: postgres:17
|
||||
restart: unless-stopped
|
||||
env_file: ./../../.env.development
|
||||
env_file: ./../../.env.development.local
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
|
2136
services/our/package-lock.json
generated
2136
services/our/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,14 +4,15 @@
|
||||
"version": "2.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently npm:dev:serve npm:dev:build npm:dev:worker",
|
||||
"dev:serve": "tsx watch ./src/index.ts",
|
||||
"dev": "concurrently npm:dev:serve npm:dev:build npm:dev:worker npm:dev:compose",
|
||||
"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:worker": "npx @dotenvx/dotenvx run -e GRAPHILE_LOGGER_DEBUG=1 -f ../../.env.development.local -- tsx watch ./src/worker.ts",
|
||||
"dev:build": "chokidar 'src/**/*.{js,ts}' -c tsup --clean",
|
||||
"start": "echo please use either start:server or start:worker; exit 1",
|
||||
"start:server": "tsx ./src/index.ts",
|
||||
"start:worker": "tsx ./src/worker.ts",
|
||||
"preview": "vite preview",
|
||||
"dev:worker": "GRAPHILE_LOGGER_DEBUG=1 tsx watch ./src/worker.ts",
|
||||
"dev:build": "chokidar 'src/**/*.{js,ts}' -c tsup --clean",
|
||||
"build": "tsup --clean",
|
||||
"lint": "eslint .",
|
||||
"clean": "rm -rf node_modules && rm -rf pnpm-lock.yaml",
|
||||
@ -78,11 +79,15 @@
|
||||
"nanoid": "^5.1.5",
|
||||
"node-fetch": "^3.3.2",
|
||||
"onnxruntime-node": "1.22.0-rev",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"pnpm": "^10.14.0",
|
||||
"protobufjs": "^7.5.3",
|
||||
"rate-limiter-flexible": "^7.1.1",
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "^0.34.3",
|
||||
"slugify": "^1.6.6",
|
||||
"slvtt": "^0.3.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.5.0",
|
||||
"tsx": "^4.20.3",
|
||||
@ -92,4 +97,4 @@
|
||||
"prisma": {
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
56
services/our/pnpm-lock.yaml
generated
56
services/our/pnpm-lock.yaml
generated
@ -131,6 +131,12 @@ importers:
|
||||
onnxruntime-node:
|
||||
specifier: 1.22.0-rev
|
||||
version: 1.22.0-rev
|
||||
pino:
|
||||
specifier: ^9.7.0
|
||||
version: 9.7.0
|
||||
pino-pretty:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
protobufjs:
|
||||
specifier: ^7.5.3
|
||||
version: 7.5.3
|
||||
@ -1743,6 +1749,9 @@ packages:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -1806,6 +1815,9 @@ packages:
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@ -2010,6 +2022,9 @@ packages:
|
||||
resolution: {integrity: sha512-i6FbQ0ZUPV6yhFSRI2SQBEqJzoWDiN4cnulTT2jm0f0lUIXg8/iPebACCrOY80rggd9LaSU65GFOI/xnJBdzyA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
fast-copy@3.0.2:
|
||||
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
|
||||
|
||||
fast-decode-uri-component@1.0.1:
|
||||
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
|
||||
|
||||
@ -2039,6 +2054,9 @@ packages:
|
||||
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fast-safe-stringify@2.1.1:
|
||||
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
|
||||
|
||||
fast-uri@3.0.6:
|
||||
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
||||
|
||||
@ -2241,6 +2259,9 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
|
||||
http-errors@2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -2809,6 +2830,10 @@ packages:
|
||||
pino-abstract-transport@2.0.0:
|
||||
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
||||
|
||||
pino-pretty@13.0.0:
|
||||
resolution: {integrity: sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==}
|
||||
hasBin: true
|
||||
|
||||
pino-std-serializers@7.0.0:
|
||||
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
|
||||
|
||||
@ -3024,6 +3049,9 @@ packages:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
|
||||
secure-json-parse@3.0.2:
|
||||
resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==}
|
||||
|
||||
@ -5579,6 +5607,8 @@ snapshots:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
@ -5632,6 +5662,8 @@ snapshots:
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
|
||||
debug@4.4.1(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@ -5879,6 +5911,8 @@ snapshots:
|
||||
node-addon-api: 8.5.0
|
||||
prebuild-install: 7.1.3
|
||||
|
||||
fast-copy@3.0.2: {}
|
||||
|
||||
fast-decode-uri-component@1.0.1: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
@ -5912,6 +5946,8 @@ snapshots:
|
||||
|
||||
fast-redact@3.5.0: {}
|
||||
|
||||
fast-safe-stringify@2.1.1: {}
|
||||
|
||||
fast-uri@3.0.6: {}
|
||||
|
||||
fast-xml-parser@4.4.1:
|
||||
@ -6167,6 +6203,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
http-errors@2.0.0:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@ -6670,6 +6708,22 @@ snapshots:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
pino-pretty@13.0.0:
|
||||
dependencies:
|
||||
colorette: 2.0.20
|
||||
dateformat: 4.6.3
|
||||
fast-copy: 3.0.2
|
||||
fast-safe-stringify: 2.1.1
|
||||
help-me: 5.0.0
|
||||
joycon: 3.1.1
|
||||
minimist: 1.2.8
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 2.0.0
|
||||
pump: 3.0.3
|
||||
secure-json-parse: 2.7.0
|
||||
sonic-boom: 4.2.0
|
||||
strip-json-comments: 3.1.1
|
||||
|
||||
pino-std-serializers@7.0.0: {}
|
||||
|
||||
pino@9.7.0:
|
||||
@ -6898,6 +6952,8 @@ snapshots:
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
secure-json-parse@2.7.0: {}
|
||||
|
||||
secure-json-parse@3.0.2: {}
|
||||
|
||||
secure-json-parse@4.0.0: {}
|
||||
|
@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `asrVtt` on the `Vod` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Vod" DROP COLUMN "asrVtt",
|
||||
ADD COLUMN "asrVttKey" TEXT;
|
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Vod" ADD COLUMN "slvttSheetKeys" JSONB,
|
||||
ADD COLUMN "vttKey" TEXT;
|
@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `vttKey` on the `Vod` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Vod" DROP COLUMN "vttKey",
|
||||
ADD COLUMN "slvttVTTKey" TEXT;
|
@ -71,17 +71,20 @@ model Vod {
|
||||
uploaderId String // previously in Upload
|
||||
uploader User @relation(fields: [uploaderId], references: [id])
|
||||
|
||||
streamDate DateTime
|
||||
notes String?
|
||||
segmentKeys Json?
|
||||
sourceVideo String?
|
||||
hlsPlaylist String?
|
||||
thumbnail String?
|
||||
asrVtt String?
|
||||
status VodStatus @default(pending)
|
||||
sha256sum String?
|
||||
cidv1 String?
|
||||
funscript String?
|
||||
streamDate DateTime
|
||||
notes String?
|
||||
segmentKeys Json?
|
||||
sourceVideo String?
|
||||
hlsPlaylist String?
|
||||
thumbnail String?
|
||||
asrVttKey String?
|
||||
slvttSheetKeys Json?
|
||||
slvttVTTKey String?
|
||||
|
||||
status VodStatus @default(pending)
|
||||
sha256sum String?
|
||||
cidv1 String?
|
||||
funscript String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
@ -72,6 +72,7 @@ export function buildApp() {
|
||||
return new Handlebars.SafeString(text);
|
||||
});
|
||||
Handlebars.registerHelper('getCdnUrl', function (s3Key) {
|
||||
// Before you remove this console.log, find a way to memoize this function!
|
||||
console.log(`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,
|
||||
|
@ -1,8 +1,8 @@
|
||||
// ./config/env.ts
|
||||
import { z } from 'zod';
|
||||
import dotenvx from '@dotenvx/dotenvx'
|
||||
// import dotenvx from '@dotenvx/dotenvx'
|
||||
|
||||
dotenvx.config({ path: ['../../.env.development'] })
|
||||
// dotenvx.config({ path: ['../../.env.development'] })
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// }
|
||||
|
||||
@ -28,7 +28,9 @@ const EnvSchema = z.object({
|
||||
CDN_TOKEN_SECRET: z.string(),
|
||||
CACHE_ROOT: z.string().default('/tmp/our'),
|
||||
VIBEUI_DIR: z.string().default('/opt/futureporn/apps/vibeui'),
|
||||
APP_DIR: z.string().default('/app')
|
||||
APP_DIR: z.string().default('/app'),
|
||||
WHISPER_DIR: z.string(),
|
||||
LOG_LEVEL: z.string().default('info'),
|
||||
});
|
||||
|
||||
const parsed = EnvSchema.safeParse(process.env);
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { env } from './config/env'
|
||||
import { buildApp } from "./app.js";
|
||||
import logger from './utils/logger.js'
|
||||
|
||||
const app = buildApp()
|
||||
|
||||
console.log(`app listening on port ${env.PORT}`)
|
||||
app.listen({ port: env.PORT, host: '0.0.0.0' })
|
||||
logger.info(`app listening on port ${env.PORT}`)
|
||||
app.listen({ port: env.PORT, host: '0.0.0.0' })
|
||||
|
@ -3,6 +3,7 @@ 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 { getTargetUser } from '../utils/authorization'
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate())
|
||||
|
||||
@ -102,4 +103,17 @@ export default async function indexRoutes(fastify: FastifyInstance): Promise<voi
|
||||
fastify.get('/version', function (request, reply) {
|
||||
return reply.send(constants.site.version)
|
||||
})
|
||||
fastify.get('/pricing', async function (request, reply) {
|
||||
const user = await getTargetUser(request, reply)
|
||||
const cdnOrigin = env.CDN_ORIGIN;
|
||||
const NODE_ENV = env.NODE_ENV;
|
||||
const authPath = env.PATREON_AUTHORIZE_PATH
|
||||
return reply.view('pricing.hbs', {
|
||||
user,
|
||||
cdnOrigin,
|
||||
NODE_ENV,
|
||||
authPath,
|
||||
site: constants.site,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
114
services/our/src/tasks/createStoryboard.ts
Normal file
114
services/our/src/tasks/createStoryboard.ts
Normal file
@ -0,0 +1,114 @@
|
||||
|
||||
import type { Task, 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 { S3Client } from "@aws-sdk/client-s3";
|
||||
import { getS3Client, uploadFile } from "../utils/s3";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
import { basename, join } from "node:path";
|
||||
import { mkdir } from "fs-extra";
|
||||
import { generateClosedCaptions } from "../utils/transcription";
|
||||
import logger from "../utils/logger";
|
||||
import { create, type SLVTTOptions } from 'slvtt';
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { concurrency } from "sharp";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
export default async function createStoryboard(payload: any, helpers: Helpers) {
|
||||
assertPayload(payload)
|
||||
const { vodId } = payload
|
||||
const vod = await prisma.vod.findFirstOrThrow({
|
||||
where: {
|
||||
id: vodId
|
||||
},
|
||||
select: {
|
||||
sourceVideo: true,
|
||||
slvttSheetKeys: true,
|
||||
slvttVTTKey: true,
|
||||
},
|
||||
})
|
||||
const taskId = nanoid()
|
||||
|
||||
if (vod.slvttVTTKey) {
|
||||
logger.info(`Doing nothing-- vod ${vodId} already has a slvttVTTKey.`)
|
||||
return; // Exit the function early
|
||||
}
|
||||
|
||||
if (!vod.sourceVideo) {
|
||||
throw new Error(`Failed to create vtt-- vod ${vodId} is missing a sourceVideo.`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const s3Client = getS3Client()
|
||||
|
||||
logger.debug(`downloading ${vod.sourceVideo}. CACHE_ROOT=${env.CACHE_ROOT}`)
|
||||
const videoFilePath = await getOrDownloadAsset(s3Client, env.S3_BUCKET, vod.sourceVideo)
|
||||
|
||||
logger.debug(`running slvtt on VOD vodId=${vodId}, videoFilePath=${videoFilePath}`)
|
||||
|
||||
const outputDirectory = join(env.CACHE_ROOT, nanoid())
|
||||
|
||||
const options: SLVTTOptions = {
|
||||
videoFilePath,
|
||||
outputDirectory,
|
||||
cols: 9,
|
||||
rows: 6,
|
||||
frameHeight: 78,
|
||||
frameWidth: 140,
|
||||
concurrencyLimit: 15,
|
||||
numSamples: 696,
|
||||
}
|
||||
|
||||
await create(options);
|
||||
|
||||
|
||||
logger.debug(`Uploading slvtt videoFrameSheets for vodId=${vodId}`)
|
||||
const files = await readdir(outputDirectory)
|
||||
const sheets = files.filter((f) => f.endsWith('.webp'));
|
||||
const vtts = files.filter((f) => f.endsWith('.vtt'))
|
||||
if (vtts.length === 0) {
|
||||
throw new Error('No .vtt found in the slvtt output. This should never happen.');
|
||||
}
|
||||
logger.debug(`slvtt created the following files.`)
|
||||
logger.debug(files)
|
||||
|
||||
let slvttSheetKeys: string[] = [];
|
||||
for (const sheet of sheets) {
|
||||
logger.debug(`Uploading sheet=${JSON.stringify(sheet)}`);
|
||||
let sheetKey = await uploadFile(s3Client, env.S3_BUCKET, `slvtt/${taskId}/${sheet}`, join(outputDirectory, sheet), 'image/webp')
|
||||
slvttSheetKeys.push(sheetKey);
|
||||
}
|
||||
|
||||
logger.debug(`Uploading slvtt vtt for vodId=${vodId}`)
|
||||
const vtt = vtts[0]
|
||||
const slvttVTTKey = await uploadFile(s3Client, env.S3_BUCKET, `slvtt/${taskId}/${vtt}`, join(outputDirectory, vtt), 'text/vtt')
|
||||
|
||||
logger.debug(`updating vod ${vodId} record. slvttSheetKeys=${JSON.stringify(slvttSheetKeys)}, slvttVTTKey=${slvttVTTKey}`);
|
||||
|
||||
await prisma.vod.update({
|
||||
where: { id: vodId },
|
||||
data: {
|
||||
slvttSheetKeys,
|
||||
slvttVTTKey,
|
||||
}
|
||||
});
|
||||
|
||||
}
|
@ -1,43 +1,86 @@
|
||||
/** ideas
|
||||
* - https://github.com/ggml-org/whisper.cpp/tree/master/examples/cli
|
||||
* - https://whisperapi.com
|
||||
* - https://elevenlabs.io/pricing
|
||||
* - https://easy-peasy.ai/#pricing
|
||||
* - https://www.clipto.com/pricing
|
||||
* - https://github.com/m-bain/whisperX
|
||||
* - https://github.com/kaldi-asr/kaldi
|
||||
* - https://github.com/usefulsensors/moonshine
|
||||
* - https://docs.bunny.net/reference/video_transcribevideo
|
||||
*/
|
||||
|
||||
|
||||
// const transcribe = async (helpers: Helpers) => {
|
||||
// /** ideas
|
||||
// * - https://whisperapi.com
|
||||
// * - https://elevenlabs.io/pricing
|
||||
// * - https://easy-peasy.ai/#pricing
|
||||
// * - https://www.clipto.com/pricing
|
||||
// * - https://github.com/m-bain/whisperX
|
||||
// * - https://github.com/kaldi-asr/kaldi
|
||||
// * - https://github.com/usefulsensors/moonshine
|
||||
// * - https://docs.bunny.net/reference/video_transcribevideo
|
||||
// */
|
||||
// helpers.logger.warn('@todo transcribe')
|
||||
// }
|
||||
import type { Task, 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 { S3Client } from "@aws-sdk/client-s3";
|
||||
import { getS3Client, uploadFile } from "../utils/s3";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
import { basename, join } from "node:path";
|
||||
import { mkdir } from "fs-extra";
|
||||
import { generateClosedCaptions } from "../utils/transcription";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
// const createHlsPlaylist = async (helpers: Helpers) => {
|
||||
// helpers.logger.warn('@todo createHlsPlaylist')
|
||||
// }
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
|
||||
|
||||
// /**
|
||||
// * Identify the tasks we need to do
|
||||
// */
|
||||
// function identify() {
|
||||
|
||||
// }
|
||||
import { Helpers } from "graphile-worker";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
export default async function createTranscription(payload: any, helpers: Helpers) {
|
||||
export default async function transcribeVideo(payload: any, helpers: Helpers) {
|
||||
assertPayload(payload)
|
||||
helpers.logger.warn(`@TODO createTranscription`)
|
||||
const { vodId } = payload
|
||||
const vod = await prisma.vod.findFirstOrThrow({
|
||||
where: {
|
||||
id: vodId
|
||||
},
|
||||
select: {
|
||||
sourceVideo: true,
|
||||
asrVttKey: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (vod.asrVttKey) {
|
||||
logger.info(`Doing nothing-- vod ${vodId} already has a vtt.`)
|
||||
return; // Exit the function early
|
||||
}
|
||||
|
||||
if (!vod.sourceVideo) {
|
||||
throw new Error(`Failed to create vtt-- vod ${vodId} is missing a sourceVideo.`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const s3Client = getS3Client()
|
||||
|
||||
logger.debug(`download video segments from pull-thru cache`)
|
||||
const videoFilePath = await getOrDownloadAsset(s3Client, env.S3_BUCKET, vod.sourceVideo)
|
||||
|
||||
logger.debug(`Transcribing VOD vodId=${vodId}, videoFilePath=${videoFilePath}`)
|
||||
const captionsFilePath = await generateClosedCaptions(videoFilePath)
|
||||
const captionsFileBasename = basename(captionsFilePath)
|
||||
|
||||
logger.debug(`Uploading closed captions for vodId=${vodId}`)
|
||||
const asrVttKey = await uploadFile(s3Client, env.S3_BUCKET, captionsFileBasename, captionsFilePath, 'text/vtt')
|
||||
|
||||
logger.debug(`updating vod ${vodId} record. asrVttKey=${asrVttKey}`);
|
||||
|
||||
await prisma.vod.update({
|
||||
where: { id: vodId },
|
||||
data: { asrVttKey }
|
||||
});
|
||||
|
||||
}
|
@ -48,11 +48,6 @@ async function createThumbnail(helpers: Helpers, inputFilePath: string) {
|
||||
cwd: env.APP_DIR,
|
||||
});
|
||||
|
||||
// const exitCode = await subprocess;
|
||||
// if (exitCode !== 0) {
|
||||
// console.error(`vcsi failed with exit code ${exitCode}`);
|
||||
// process.exit(exitCode);
|
||||
// }
|
||||
helpers.logger.debug('result as follows')
|
||||
helpers.logger.debug(JSON.stringify(result, null, 2))
|
||||
|
||||
|
@ -43,9 +43,10 @@ const scheduleVodProcessing: Task = async (payload: unknown, helpers) => {
|
||||
if (!vod.sha256sum) jobs.push(helpers.addJob("generateVideoChecksum", { vodId }));
|
||||
if (!vod.thumbnail) jobs.push(helpers.addJob("createVideoThumbnail", { vodId }));
|
||||
if (!vod.hlsPlaylist) jobs.push(helpers.addJob("createHlsPlaylist", { vodId }));
|
||||
if (!vod.asrVtt) jobs.push(helpers.addJob("createAsrVtt", { vodId }));
|
||||
if (!vod.cidv1) jobs.push(helpers.addJob("createIpfsCid", { vodId }));
|
||||
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 }));
|
||||
|
||||
const changes = jobs.length;
|
||||
if (changes > 0) {
|
||||
|
BIN
services/our/src/tests/fixtures/sample-projektmelody-chaturbate-2025-07-21.mp4
vendored
Normal file
BIN
services/our/src/tests/fixtures/sample-projektmelody-chaturbate-2025-07-21.mp4
vendored
Normal file
Binary file not shown.
51
services/our/src/tests/spriteVideo.integration.test.ts
Normal file
51
services/our/src/tests/spriteVideo.integration.test.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { generateStoryboards } from "../utils/spriteVideo";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { env } from "../config/env";
|
||||
import logger from "../utils/logger";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
logger.info(`NODE_ENV=${env.NODE_ENV}`);
|
||||
|
||||
const TEST_VIDEO = path.resolve(__dirname, "fixtures/sample-projektmelody-chaturbate-2025-07-21.mp4");
|
||||
// const TEST_VIDEO = path.resolve(`/home/cj/Documents/futureporn-meta/recordings/projektmelody-chaturbate-2025-07-21.mp4`)
|
||||
const BASE_TEMP_DIR = path.join(env.CACHE_ROOT, "sprites_test_output");
|
||||
|
||||
function getTempPrefix(testName: string): string {
|
||||
return path.join(BASE_TEMP_DIR, testName);
|
||||
}
|
||||
|
||||
function cleanDir(dir: string) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("generateSprites", () => {
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(TEST_VIDEO)) {
|
||||
throw new Error(`Test video not found at ${TEST_VIDEO}`);
|
||||
}
|
||||
});
|
||||
|
||||
// afterEach(() => {
|
||||
// cleanDir(BASE_TEMP_DIR);
|
||||
// });
|
||||
|
||||
it("generates multiple sprite files correctly", { timeout: 30_000 }, async () => {
|
||||
const prefix = getTempPrefix(nanoid());
|
||||
|
||||
const outputPath = await generateStoryboards(
|
||||
TEST_VIDEO,
|
||||
prefix,
|
||||
2,
|
||||
);
|
||||
|
||||
const spriteFiles = fs.readdirSync(outputPath).filter(f => f.endsWith(".webp"));
|
||||
|
||||
expect(spriteFiles.length).greaterThan(1);
|
||||
});
|
||||
|
||||
});
|
42
services/our/src/tests/storyboard.integration.test.ts
Normal file
42
services/our/src/tests/storyboard.integration.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { existsSync, readFile, remove } from "fs-extra";
|
||||
import { join } from "node:path";
|
||||
import { generateStoryboard } from "../utils/storyboard";
|
||||
import { env } from "../config/env";
|
||||
import logger from "../utils/logger";
|
||||
import { create } from 'slvtt';
|
||||
|
||||
// Sample video for test (you should keep a very small test MP4 under `__fixtures__`)
|
||||
const testVideoPath = join(__dirname, "fixtures", "sample.mp4");
|
||||
|
||||
let imagePath: string;
|
||||
let vttPath: string;
|
||||
|
||||
describe("generateStoryboard", () => {
|
||||
it("should generate a storyboard image and a VTT file", async () => {
|
||||
const result = await generateStoryboard(testVideoPath);
|
||||
|
||||
imagePath = result.imagePath;
|
||||
vttPath = result.vttPath;
|
||||
|
||||
// Check that both files exist
|
||||
expect(existsSync(imagePath)).toBe(true);
|
||||
expect(existsSync(vttPath)).toBe(true);
|
||||
|
||||
logger.info(`imagePath=${imagePath}, vttPath=${vttPath}`)
|
||||
|
||||
// Check contents of VTT file
|
||||
const vttContent = await readFile(vttPath, { encoding: "utf-8" }) as string;
|
||||
logger.info(`vttContent=${vttContent}`)
|
||||
|
||||
expect(vttContent.startsWith("WEBVTT")).toBe(true);
|
||||
expect(vttContent).toContain("-->"); // should have timestamps
|
||||
expect(vttContent).toMatch(/#xywh=\d+,\d+,\d+,\d+/); // should have sprite metadata
|
||||
}, 35_000);
|
||||
|
||||
// afterAll(async () => {
|
||||
// // Clean up generated files after test
|
||||
// if (imagePath) await remove(imagePath);
|
||||
// if (vttPath) await remove(vttPath);
|
||||
// });
|
||||
});
|
61
services/our/src/tests/transcription.integration.test.ts
Normal file
61
services/our/src/tests/transcription.integration.test.ts
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { join, extname } from 'node:path';
|
||||
import { stat, readFile } from 'node:fs/promises';
|
||||
import { extractAudio, generateClosedCaptions } from '../utils/transcription';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const sampleVideoPath = join(__dirname, 'fixtures', 'sample-projektmelody-chaturbate-2025-07-21.mp4');
|
||||
const expectedPhrases = [
|
||||
'WEBVTT',
|
||||
`I didn't see that, I didn't know that.`,
|
||||
`Oh, I don't have to use olives, fantastic.`,
|
||||
`Good, I like that I have choices here.`,
|
||||
`Same comes to this.`,
|
||||
`In anything in the world, life is so scary.`,
|
||||
`You are flying through space on this little blue ball`,
|
||||
`in the universe, there's so much in this world`,
|
||||
`that you don't have control of.`,
|
||||
`But when it comes to the world,`
|
||||
]
|
||||
|
||||
|
||||
describe('generateClosedCaptions', () => {
|
||||
|
||||
let vttOutputPath: string
|
||||
let outputText: string
|
||||
|
||||
beforeAll(async () => {
|
||||
vttOutputPath = await generateClosedCaptions(sampleVideoPath)
|
||||
logger.info(`generateClosedCaptions finished with vttOutputPath=${vttOutputPath}`)
|
||||
outputText = await readFile(vttOutputPath, { encoding: 'utf-8' })
|
||||
}, 20_000)
|
||||
|
||||
it('should contain expected phrases', async () => {
|
||||
for (const phrase of expectedPhrases) {
|
||||
expect(outputText).toContain(phrase)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
describe('extractAudio', () => {
|
||||
let audioFilePath: string
|
||||
|
||||
beforeAll(async () => {
|
||||
audioFilePath = await extractAudio(sampleVideoPath)
|
||||
}, 20_000)
|
||||
|
||||
it('should be a wav file', () => {
|
||||
expect(extname(audioFilePath)).toBe('.wav')
|
||||
})
|
||||
|
||||
it('should exist on disk', async () => {
|
||||
const stats = await stat(audioFilePath)
|
||||
expect(stats.isFile()).toBe(true)
|
||||
expect(stats.size).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
@ -7,6 +7,7 @@ import { readdir, unlink } from 'fs/promises';
|
||||
import { env } from '../config/env';
|
||||
import Keyv from 'keyv';
|
||||
import KeyvPostgres from "@keyv/postgres"
|
||||
import logger from './logger';
|
||||
|
||||
const keyv = new Keyv(new KeyvPostgres({ uri: env.DATABASE_URL, schema: 'keyv' }));
|
||||
keyv.on('error', (err) => {
|
||||
@ -26,7 +27,7 @@ const MAX_RETRIES = 10; // Max retries to acquire lock
|
||||
|
||||
|
||||
export async function getOrDownloadAsset(client: S3Client, bucket: string, key: string): Promise<string> {
|
||||
console.log(`getOrDownloadAsset with bucket=${bucket} key=${key}`)
|
||||
logger.debug(`getOrDownloadAsset with bucket=${bucket} key=${key}`)
|
||||
|
||||
if (!client) throw new Error('getOrDownloadAsset requires S3Client as first argument');
|
||||
if (!bucket) throw new Error('getOrDownloadAsset requires bucket as second argument');
|
||||
@ -115,7 +116,7 @@ export async function cleanExpiredFiles(): Promise<number> {
|
||||
const key = file.replace(cacheDir, '');
|
||||
const cacheKey = `${bucket}:${key}`;
|
||||
const fullPath = join(cacheDir, file);
|
||||
// console.log(`file=${file} key=${key} cacheKey=${cacheKey}`)
|
||||
logger.debug(`file=${file} key=${key} cacheKey=${cacheKey}`)
|
||||
if (file === bucket) continue;
|
||||
|
||||
const stillCached = await cache.get(cacheKey);
|
||||
@ -123,7 +124,7 @@ export async function cleanExpiredFiles(): Promise<number> {
|
||||
try {
|
||||
rmSync(fullPath, { recursive: true, force: true });
|
||||
deletedCount++;
|
||||
console.log(`Deleted expired file: ${fullPath}`);
|
||||
logger.debug(`Deleted expired file: ${fullPath}`);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to delete file ${fullPath}:`, err);
|
||||
}
|
||||
|
36
services/our/src/utils/logger.ts
Normal file
36
services/our/src/utils/logger.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import pino from 'pino'
|
||||
import { env } from '../config/env'
|
||||
|
||||
let hooks
|
||||
const isTest = env.NODE_ENV === 'test'
|
||||
|
||||
if (env.NODE_ENV === 'test') {
|
||||
const { prettyFactory } = require('pino-pretty')
|
||||
const prettify = prettyFactory({ sync: true, colorize: true })
|
||||
hooks = {
|
||||
streamWrite: (s: string) => {
|
||||
console.log(prettify(s)) // Mirror to console.log during tests. @see https://github.com/pinojs/pino/issues/2148
|
||||
return s
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const isProd = env.NODE_ENV === 'production'
|
||||
const logger = pino({
|
||||
level: env.LOG_LEVEL,
|
||||
...(isTest && { hooks }),
|
||||
...(isProd
|
||||
? {}
|
||||
: {
|
||||
transport: {
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
sync: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
export default logger
|
@ -7,64 +7,42 @@ import { existsSync } from "fs-extra";
|
||||
export async function preparePython() {
|
||||
const spawn = await getNanoSpawn();
|
||||
const venvPath = env.VENV;
|
||||
const venvBin = join(venvPath, "bin");
|
||||
|
||||
// Determine Python executable
|
||||
// 1. Locate python3
|
||||
let pythonCmd;
|
||||
try {
|
||||
pythonCmd = which.sync("python3");
|
||||
} catch {
|
||||
console.error("Python is not installed or not in PATH.");
|
||||
throw new Error("Python not found in PATH.");
|
||||
}
|
||||
|
||||
// If venv doesn't exist, create it
|
||||
// 2. Create venv if missing
|
||||
if (!existsSync(venvPath)) {
|
||||
console.error("Python venv not found. Creating one...");
|
||||
|
||||
try {
|
||||
await spawn(pythonCmd, ["-m", "venv", venvPath], {
|
||||
cwd: env.APP_DIR,
|
||||
});
|
||||
|
||||
console.log("Python venv successfully created.");
|
||||
} catch (err) {
|
||||
console.error("Failed to create Python venv:", err);
|
||||
throw new Error(
|
||||
"Python venv creation failed. Check if python3 and python3-venv are installed."
|
||||
);
|
||||
}
|
||||
console.log("Creating Python venv...");
|
||||
await spawn(pythonCmd, ["-m", "venv", venvPath], {
|
||||
cwd: env.APP_DIR,
|
||||
});
|
||||
console.log("✅ Python venv created.");
|
||||
} else {
|
||||
console.log("Using existing Python venv.");
|
||||
}
|
||||
|
||||
// Activate pip in the venv
|
||||
const pipCmd = join(venvPath, "bin", "pip");
|
||||
// 3. Install requirements.txt
|
||||
const pipCmd = join(venvBin, "pip");
|
||||
console.log("Installing requirements.txt...");
|
||||
await spawn(pipCmd, ["install", "-r", "requirements.txt"], {
|
||||
cwd: env.APP_DIR,
|
||||
});
|
||||
console.log("✅ requirements.txt installed.");
|
||||
|
||||
// Check if YOLO exists (example: checking if 'yolo' package is installed)
|
||||
// This check can be customized to your specific condition (e.g., check a file or run `pip show yolo`)
|
||||
let yoloExists = false;
|
||||
try {
|
||||
// Run `pip show ultralytics` or your yolo package name to check if installed
|
||||
await spawn(pipCmd, ["show", "ultralytics"], { cwd: env.APP_DIR });
|
||||
yoloExists = true;
|
||||
} catch {
|
||||
yoloExists = false;
|
||||
// 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("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.");
|
||||
}
|
||||
|
||||
if (!yoloExists) {
|
||||
console.log("YOLO not found in venv. Installing requirements.txt...");
|
||||
|
||||
try {
|
||||
// Install requirements.txt using pip inside venv
|
||||
await spawn(pipCmd, ["install", "-r", "requirements.txt"], {
|
||||
cwd: env.APP_DIR,
|
||||
});
|
||||
console.log("✅ requirements.txt installed successfully.");
|
||||
} catch (err) {
|
||||
console.error("Failed to install requirements.txt:", err);
|
||||
throw new Error("requirements.txt installation failed.");
|
||||
}
|
||||
} else {
|
||||
console.log("YOLO detected in venv, skipping requirements installation.");
|
||||
}
|
||||
console.log("✅ vcsi CLI is available at", vcsiBinary);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
} from "@aws-sdk/client-s3"; // @see https://www.backblaze.com/docs/cloud-storage-use-the-aws-sdk-for-javascript-v3-with-backblaze-b2
|
||||
import { readFile } from 'fs/promises';
|
||||
import { env } from '../config/env' // adjust this path based on your project structure
|
||||
import logger from "./logger";
|
||||
|
||||
let client: S3Client | null = null
|
||||
|
||||
@ -21,7 +22,7 @@ export async function uploadFile(
|
||||
if (!key) throw new Error('uploadFile requires key as third param');
|
||||
if (!filePath) throw new Error('uploadFile requires filePath as fourth param');
|
||||
if (!mimetype) throw new Error('uploadFile requires mimetype as fifth param');
|
||||
console.log(`uploadFile filePath=${filePath} with key=${key} bucket=${bucket}`);
|
||||
logger.debug(`[uploadFile] filePath=${filePath} with key=${key} bucket=${bucket}`);
|
||||
|
||||
try {
|
||||
// Read file content from disk
|
||||
@ -41,11 +42,11 @@ export async function uploadFile(
|
||||
|
||||
} catch (caught) {
|
||||
if (caught instanceof S3ServiceException) {
|
||||
console.error(
|
||||
logger.error(
|
||||
`Error from S3 while uploading to ${bucket}. ${caught.name}: ${caught.message}`,
|
||||
);
|
||||
} else {
|
||||
console.error(`Unexpected error during upload:`, caught);
|
||||
logger.error(`Unexpected error during upload:`, caught);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to upload ${filePath} to s3://${bucket}/${key}`);
|
||||
|
142
services/our/src/utils/storyboard2.ts
Normal file
142
services/our/src/utils/storyboard2.ts
Normal file
@ -0,0 +1,142 @@
|
||||
// storyboard2.ts
|
||||
// ported from https://github.com/Dibyakshu/go-video-storyboard-generator/blob/main/main.go
|
||||
|
||||
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { getNanoSpawn } from './nanoSpawn';
|
||||
|
||||
// Entry point
|
||||
(async function main() {
|
||||
if (process.argv.length < 5) {
|
||||
showUsage();
|
||||
}
|
||||
|
||||
const fps = parseInt(process.argv[2], 10);
|
||||
const inputFile = process.argv[3];
|
||||
const outputDir = process.argv[4];
|
||||
|
||||
if (isNaN(fps) || fps <= 0) {
|
||||
console.error('Error: Invalid FPS value. Please provide a positive integer.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
ensureDirectoryExists(outputDir);
|
||||
|
||||
const startTime = Date.now();
|
||||
const thumbnailsDir = join(outputDir, 'thumbnails');
|
||||
ensureDirectoryExists(thumbnailsDir);
|
||||
|
||||
generateThumbnails(fps, inputFile, thumbnailsDir);
|
||||
const storyboardImage = join(outputDir, 'storyboard.jpg');
|
||||
generateStoryboard(thumbnailsDir, storyboardImage);
|
||||
const vttFile = join(outputDir, 'storyboard.vtt');
|
||||
generateVTT(fps, thumbnailsDir, vttFile);
|
||||
cleanup(thumbnailsDir);
|
||||
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
console.log(`Process completed in ${elapsed.toFixed(2)} seconds.`);
|
||||
})();
|
||||
|
||||
function showUsage(): never {
|
||||
console.log('Usage: ts-node storyboard.ts <fps> <input_file> <output_dir>');
|
||||
console.log('Example: ts-node storyboard.ts 1 example.mp4 ./output');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function ensureDirectoryExists(path: string) {
|
||||
if (!existsSync(path)) {
|
||||
mkdirSync(path, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateThumbnails(fps: number, inputFile: string, thumbnailsDir: string) {
|
||||
const spawn = await getNanoSpawn()
|
||||
console.log('Generating thumbnails...');
|
||||
try {
|
||||
spawn('ffmpeg', [
|
||||
'-i', inputFile,
|
||||
'-vf', `fps=1/${fps},scale=384:160`,
|
||||
join(thumbnailsDir, 'thumb%05d.jpg'),
|
||||
], { stdio: 'inherit' });
|
||||
console.log('Thumbnails generated successfully.');
|
||||
} catch (err) {
|
||||
console.error('Error generating thumbnails:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStoryboard(thumbnailsDir: string, storyboardImage: string) {
|
||||
const spawn = await getNanoSpawn()
|
||||
console.log('Creating storyboard.jpg...');
|
||||
const thumbnails = readdirSync(thumbnailsDir).filter(f => f.endsWith('.jpg'));
|
||||
if (thumbnails.length === 0) {
|
||||
console.error('No thumbnails found. Exiting.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const columns = 10;
|
||||
const rows = Math.ceil(thumbnails.length / columns);
|
||||
const tileFilter = `tile=${columns}x${rows}`;
|
||||
|
||||
try {
|
||||
await spawn('ffmpeg', [
|
||||
'-pattern_type', 'glob',
|
||||
'-i', join(thumbnailsDir, '*.jpg'),
|
||||
'-filter_complex', tileFilter,
|
||||
storyboardImage,
|
||||
], { stdio: 'inherit' });
|
||||
console.log('Storyboard image created successfully.');
|
||||
} catch (err) {
|
||||
console.error('Error creating storyboard image:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function generateVTT(fps: number, thumbnailsDir: string, vttFile: string) {
|
||||
console.log('Generating storyboard.vtt...');
|
||||
const thumbnails = readdirSync(thumbnailsDir).filter(f => f.startsWith('thumb') && f.endsWith('.jpg'));
|
||||
const durationPerThumb = fps;
|
||||
let vttContent = 'WEBVTT\n\n';
|
||||
|
||||
for (let i = 0; i < thumbnails.length; i++) {
|
||||
const start = i * durationPerThumb * 1000;
|
||||
const end = start + durationPerThumb * 1000;
|
||||
|
||||
const x = (i % 10) * 384;
|
||||
const y = Math.floor(i / 10) * 160;
|
||||
|
||||
vttContent += `${formatDuration(start)} --> ${formatDuration(end)}\n`;
|
||||
vttContent += `https://insertlinkhere.com/storyboard.jpg#xywh=${x},${y},384,160\n\n`;
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(vttFile, vttContent, 'utf8');
|
||||
console.log('Storyboard VTT file generated successfully.');
|
||||
} catch (err) {
|
||||
console.error('Error writing VTT file:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup(thumbnailsDir: string) {
|
||||
console.log('Cleaning up temporary files...');
|
||||
try {
|
||||
rmSync(thumbnailsDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error('Error cleaning up thumbnails:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
const milliseconds = ms % 1000;
|
||||
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(milliseconds, 3)}`;
|
||||
}
|
||||
|
||||
function pad(n: number, width = 2): string {
|
||||
return n.toString().padStart(width, '0');
|
||||
}
|
85
services/our/src/utils/transcription.ts
Normal file
85
services/our/src/utils/transcription.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { getNanoSpawn } from "./nanoSpawn";
|
||||
import { SubprocessError } from "nano-spawn";
|
||||
import { mkdir } from "fs-extra";
|
||||
import { join, extname, basename, dirname } from "node:path";
|
||||
import { env } from "../config/env";
|
||||
import { nanoid } from "nanoid";
|
||||
import logger from "./logger";
|
||||
|
||||
// @see https://github.com/ggml-org/whisper.cpp/tree/master#quick-start
|
||||
// ffmpeg -i input.mp4 -vn -ar 16000 -ac 1 -c:a pcm_s16le output.wav
|
||||
export async function extractAudio(videoFilePath: string): Promise<string> {
|
||||
if (!videoFilePath) throw new Error('extractAudio was missing first arg videoFilePath');
|
||||
const spawn = await getNanoSpawn();
|
||||
const tmpFile = join(env.CACHE_ROOT, `${nanoid()}.wav`);
|
||||
logger.info(`tmpFile=${tmpFile}`)
|
||||
|
||||
// Make sure CACHE_ROOT exists
|
||||
await mkdir(env.CACHE_ROOT, { recursive: true });
|
||||
|
||||
try {
|
||||
await spawn('ffmpeg', [
|
||||
'-i', videoFilePath,
|
||||
'-vn',
|
||||
'-ar', '16000',
|
||||
'-ac', '1',
|
||||
'-c:a', 'pcm_s16le',
|
||||
tmpFile,
|
||||
]);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to extract audio from ${videoFilePath}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
return tmpFile;
|
||||
}
|
||||
|
||||
|
||||
// @see https://github.com/ggml-org/whisper.cpp/blob/master/.devops/main.Dockerfile
|
||||
// @see https://github.com/ggml-org/whisper.cpp/tree/master
|
||||
export async function generateClosedCaptions(filePath: string): Promise<string> {
|
||||
const spawn = await getNanoSpawn()
|
||||
|
||||
|
||||
const ext = extname(filePath).toLowerCase()
|
||||
let wavFilePath = filePath
|
||||
|
||||
if (ext !== '.wav') {
|
||||
wavFilePath = await extractAudio(filePath)
|
||||
}
|
||||
|
||||
const tmpVttFile = join(env.CACHE_ROOT, `${nanoid()}.vtt`)
|
||||
const tmpVttFileNoExt = join(dirname(tmpVttFile), basename(tmpVttFile, '.vtt'))
|
||||
logger.info(`let us create tmpVttFile=${tmpVttFile} tmpVttFileNoExt=${tmpVttFileNoExt}`)
|
||||
|
||||
try {
|
||||
logger.info('we are calling whisper-cli now.');
|
||||
const { stdout, stderr } = await spawn('whisper-cli', [
|
||||
'--file', wavFilePath,
|
||||
'--output-vtt',
|
||||
'--output-file', tmpVttFileNoExt, // whisper-cli appends .vtt to whatever filename we give it.
|
||||
], {
|
||||
cwd: env.WHISPER_DIR
|
||||
});
|
||||
|
||||
logger.info('we just finished calling whisper-cli.');
|
||||
logger.info({ stdout, stderr });
|
||||
|
||||
} catch (err) {
|
||||
logger.error('CAUGHT an error:', err);
|
||||
|
||||
if (err instanceof SubprocessError) {
|
||||
logger.error(`whisper-cli failed with exit code ${err.exitCode}`);
|
||||
logger.error(`stdout: ${err.stdout}`);
|
||||
logger.error(`stderr: ${err.stderr}`);
|
||||
logger.error(`command: ${err.command}`);
|
||||
} else {
|
||||
logger.error('Unexpected error:');
|
||||
logger.error(err)
|
||||
}
|
||||
|
||||
throw new Error(`Failed to generate closed captions from ${wavFilePath}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
logger.info(`>>>>>>>>>>>>>>>>>> ${tmpVttFile} should exist`)
|
||||
return tmpVttFile
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
<li><a href="/vods">VODs</a></li>
|
||||
<li><a href="/streams"><s>🚧 Streams</s></a></li>
|
||||
<li><a href="/vtubers">VTubers</a></li>
|
||||
<li><a href="/pricing">Pricing</a></li>
|
||||
{{#if (hasRole "supporterTier1" "moderator" "admin" user)}}
|
||||
<li><a href="/uploads">Uploads</a></li>
|
||||
{{/if}}
|
||||
|
98
services/our/src/views/pricing.hbs
Normal file
98
services/our/src/views/pricing.hbs
Normal file
@ -0,0 +1,98 @@
|
||||
{{#> main}}
|
||||
|
||||
<header class="container">
|
||||
{{> navbar}}
|
||||
</header>
|
||||
|
||||
<main class="container pico">
|
||||
<section id="pricing">
|
||||
<h2>Pricing</h2>
|
||||
|
||||
<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>
|
||||
<th>Feature</th>
|
||||
<th>User</th>
|
||||
<th>Supporter Tier 1</th>
|
||||
<th>Supporter Tier 6</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>View</td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Torrent Downloads</td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CDN Downloads</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ad-Free</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Upload</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><abbr title="Sex toy playback syncronization">Funscripts</abbr></td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Closed Captions</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><abbr title="Closed Captions">CC</abbr> Search</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CSV Export</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SQL Export</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vibeui PyTorch Model</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</section>
|
||||
|
||||
{{> footer}}
|
||||
</main>
|
||||
|
||||
{{/main}}
|
@ -13,6 +13,9 @@
|
||||
<p><strong>Identicon:</strong> {{{identicon user.id 48}}}</p>
|
||||
<p><strong>Roles:</strong> {{#each user.roles}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
|
||||
</p>
|
||||
<p><strong>Perks:</strong> @todo
|
||||
{{!-- @todo {{#each user.perks}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}} --}}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<a href="/logout">Logout</a>
|
||||
|
@ -1,7 +1,10 @@
|
||||
{{#> main}}
|
||||
<!-- Header -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/video.js@8.23.3/dist/video-js.min.css">
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/videojs-vtt-thumbnails@0.0.13/dist/videojs-vtt-thumbnails.min.css">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/gh/Teyuto/videojs-vtt-thumbnails@main/src/videojs-vtt-thumbnails.min.css">
|
||||
<style>
|
||||
.vjs-funscript-indicator {
|
||||
color: #0f0;
|
||||
@ -46,6 +49,7 @@
|
||||
<video id="player" class="video-js vjs-fluid" controls preload="auto" poster="{{getCdnUrl vod.thumbnail}}"
|
||||
data-setup='{}' data-playlist="{{signedHlsUrl vod.hlsPlaylist}}">
|
||||
<source src="/hls/{{vod.id}}/master.m3u8" type="application/x-mpegURL">
|
||||
<track kind="captions" src="{{getCdnUrl vod.asrVttKey}}" srclang="en" label="English" default>
|
||||
<p class="vjs-no-js">
|
||||
To view this video please enable JavaScript, and consider upgrading to a
|
||||
web browser that
|
||||
@ -191,8 +195,34 @@
|
||||
{{/if}}
|
||||
|
||||
|
||||
<h3>Closed Captions / Subtitles</h3>
|
||||
{{#if vod.asrVttKey}}
|
||||
<a id="asr-vtt" data-url="{{getCdnUrl vod.asrVttKey}}" data-file-name="{{basename vod.asrVttKey}}"
|
||||
x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.asrVttKey}}"
|
||||
alt="Closed Captions VTT file">{{icon "download" 24}} Closed Captions</a>
|
||||
{{else}}
|
||||
<article>
|
||||
Closed captions are processing.
|
||||
</article>
|
||||
{{/if}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{#if (isModerator user)}}
|
||||
|
||||
<h3>Storyboard Images</h3>
|
||||
{{#if vod.slvttVTTKey}}
|
||||
<a id="slvtt" data-url="{{getCdnUrl vod.slvttVTTKey}}" data-file-name="{{basename vod.slvttVTTKey}}"
|
||||
x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.slvttVTTKey}}"
|
||||
alt="slvttVTTKey">{{icon "download" 24}} slvttVTTKey</a>
|
||||
{{else}}
|
||||
<article>
|
||||
Storyboard Images are processing
|
||||
</article>
|
||||
{{/if}}
|
||||
|
||||
<h2>Moderator Controls</h2>
|
||||
<button hx-post="/vods/{{vod.id}}/process" hx-target="body">{{icon "processing" 24}} Re-Schedule Vod
|
||||
Processing</button>
|
||||
@ -210,13 +240,10 @@
|
||||
|
||||
{{>footer}}
|
||||
</main>
|
||||
{{!-- <script src=" https://cdn.jsdelivr.net/npm/video.js@8.23.3/dist/video.min.js "></script> --}}
|
||||
<script src=" https://cdn.jsdelivr.net/npm/video.js@8.23.3/dist/video.min.js "></script>
|
||||
<script>
|
||||
|
||||
|
||||
//var player = videojs('vid1', {
|
||||
//fluid: true
|
||||
//});
|
||||
|
||||
async function download(cdnUrl, fileName) {
|
||||
|
||||
@ -242,26 +269,68 @@
|
||||
|
||||
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/video.js@8.23.3/dist/video.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/Teyuto/videojs-vtt-thumbnails@main/src/videojs-vtt-thumbnails.min.js"></script>
|
||||
{{!-- <script src="https://cdn.jsdelivr.net/npm/video.js@8.23.3/dist/video.min.js"></script> --}}
|
||||
{{!-- <script src="https://cdn.jsdelivr.net/npm/videojs-vtt-thumbnails@0.0.13/dist/videojs-vtt-thumbnails.min.js"></script> --}}
|
||||
{{!-- <script type="module">
|
||||
import { default as caca } from "https://esm.sh/gh/mayeaux/videojs-vtt-thumbnails@260a63a/es2022/videojs-vtt-thumbnails.mjs";
|
||||
console.log('caca as follows')
|
||||
console.log(caca)
|
||||
caca()
|
||||
</script> --}}
|
||||
|
||||
{{!--
|
||||
Script 4: Initialize the buttplug plugin and pass in the funscript URL from the DOM
|
||||
--}}
|
||||
<script type="module">
|
||||
//import videojs from 'https://cdn.jsdelivr.net/npm/video.js@8/+esm'
|
||||
//import 'https://esm.sh/gh/mayeaux/videojs-vtt-thumbnails';
|
||||
|
||||
const player = videojs('#player');
|
||||
const funscriptElement = document.querySelector('#funscript');
|
||||
|
||||
//if (funscriptElement) {
|
||||
// player.buttplug({
|
||||
// funscriptUrl: funscriptElement.dataset.url
|
||||
// });
|
||||
//} else {
|
||||
// console.error('Element with id "funscript" not found.');
|
||||
//}
|
||||
|
||||
//console.log('vtt-thumbnails')
|
||||
//console.log(player.vttThumbnails)
|
||||
// Initialize videojs-vtt-thumbnails
|
||||
//player.vttThumbnails({
|
||||
// src: "{{getCdnUrl vod.slvttVTTKey}}",
|
||||
// showTimestamp: true
|
||||
//});
|
||||
player.vttThumbnails({
|
||||
//spriteUrl: 'path/to/sprite.jpg',
|
||||
vttData: {
|
||||
url: '{{getCdnUrl vod.slvttVTTKey}}'
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{{!--
|
||||
Script 1: Load Buttplug.js from Skypack CDN and expose it to window.buttplug
|
||||
--}}
|
||||
<script type="module">
|
||||
{{!-- <script type="module">
|
||||
import {
|
||||
ButtplugClient,
|
||||
ButtplugBrowserWebsocketClientConnector
|
||||
} from 'https://cdn.skypack.dev/buttplug';
|
||||
|
||||
window.buttplug = { ButtplugClient, ButtplugBrowserWebsocketClientConnector };
|
||||
</script>
|
||||
</script> --}}
|
||||
|
||||
|
||||
|
||||
{{!--
|
||||
Script 2: Define reusable utility components for funscript and buttplug indicators
|
||||
--}}
|
||||
<script type="module">
|
||||
{{!-- <script type="module">
|
||||
const Plugin = videojs.getPlugin('plugin');
|
||||
|
||||
const createIndicator = (Component, className, defaultText) => {
|
||||
@ -283,13 +352,13 @@
|
||||
'ButtplugIndicator',
|
||||
createIndicator(videojs.getComponent('Component'), 'vjs-buttplug-indicator', 'Buttplug.js not connected')
|
||||
);
|
||||
</script>
|
||||
</script> --}}
|
||||
|
||||
|
||||
{{!--
|
||||
Script 3: Main ButtplugPlugin class — handles connection, syncing, and device control
|
||||
--}}
|
||||
<script type="module">
|
||||
{{!-- <script type="module">
|
||||
class ButtplugPlugin extends videojs.getPlugin('plugin') {
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
@ -481,22 +550,10 @@
|
||||
}
|
||||
|
||||
videojs.registerPlugin('buttplug', ButtplugPlugin);
|
||||
</script>
|
||||
</script> --}}
|
||||
|
||||
|
||||
|
||||
{{!--
|
||||
Script 4: Initialize the plugin and pass in the funscript URL from the DOM
|
||||
--}}
|
||||
<script type="module">
|
||||
const player = videojs('#player');
|
||||
const funscriptElement = document.querySelector('#funscript');
|
||||
|
||||
if (funscriptElement) {
|
||||
player.buttplug({
|
||||
funscriptUrl: funscriptElement.dataset.url
|
||||
});
|
||||
} else {
|
||||
console.error('Element with id "funscript" not found.');
|
||||
}
|
||||
</script>
|
||||
|
||||
{{/main}}
|
@ -12,11 +12,17 @@
|
||||
#### Alby Event numbers and their meanings
|
||||
|
||||
Action 0: heartbeat
|
||||
Action 4:
|
||||
Action 2: connection request
|
||||
Action 4: connection acknowledgment
|
||||
Action 10: Presence?
|
||||
Action 11:
|
||||
Action 15: Broadcast
|
||||
|
||||
### Ideas
|
||||
|
||||
There is the url `https://chaturbate.com/api/public/affiliates/onlinerooms/?wm=DiPkB&client_ip=request_ip` which shows all live channels. We could query this.
|
||||
There is the url `https://chaturbate.com/api/public/affiliates/onlinerooms/?wm=DiPkB&client_ip=request_ip` which shows all live channels. We could query this.
|
||||
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Lots of `{"action":4}` Getting lots of Action 4 frames? Might be rate limiting or something? Simple fix is to reboot PC.
|
Loading…
x
Reference in New Issue
Block a user