Compare commits

..

No commits in common. "b1c012b6fb22938b9ebac3e37f15be75c5fc3305" and "90e8da3246b2109367f00d3dc58c485a8f26a2b4" have entirely different histories.

23 changed files with 737 additions and 1368 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "futureporn", "name": "futureporn",
"private": true, "private": true,
"version": "2.3.2", "version": "2.2.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently npm:dev:serve npm:dev:build 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",

View File

@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Vtuber` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Vtuber_slug_key" ON "Vtuber"("slug");

View File

@ -96,7 +96,7 @@ model Vod {
model Vtuber { model Vtuber {
id String @id @default(cuid(2)) id String @id @default(cuid(2))
image String? image String?
slug String? @unique slug String?
displayName String? displayName String?
chaturbate String? chaturbate String?
twitter String? twitter String?

View File

@ -1,117 +0,0 @@
import { PrismaClient } from '../generated/prisma';
import pg from 'pg';
const prisma = new PrismaClient();
const v1 = new pg.Pool({
host: process.env.V1_DB_HOST || 'localhost',
port: +(process.env.V1_DB_PORT || '5444'),
user: process.env.V1_DB_USER || 'postgres',
password: process.env.V1_DB_PASS || 'password',
database: process.env.V1_DB_NAME || 'restoredb'
});
// Set this to an existing user ID in v2
const DEFAULT_UPLOADER_ID = process.env.DEFAULT_UPLOADER_ID || 'REPLACE_WITH_V2_USER_ID';
async function migrateVtubers() {
console.log('Migrating vtubers...');
const res = await v1.query(`SELECT * FROM vtubers`);
for (const vt of res.rows) {
await prisma.vtuber.create({
data: {
slug: vt.slug,
image: vt.image,
displayName: vt.display_name,
chaturbate: vt.chaturbate,
twitter: vt.twitter,
patreon: vt.patreon,
twitch: vt.twitch,
tiktok: vt.tiktok,
onlyfans: vt.onlyfans,
youtube: vt.youtube,
linktree: vt.linktree,
carrd: vt.carrd,
fansly: vt.fansly,
pornhub: vt.pornhub,
discord: vt.discord,
reddit: vt.reddit,
throne: vt.throne,
instagram: vt.instagram,
facebook: vt.facebook,
merch: vt.merch,
description: `${vt.description_1 ?? ''}\n${vt.description_2 ?? ''}`.trim() || null,
themeColor: vt.theme_color,
uploaderId: DEFAULT_UPLOADER_ID
}
});
}
console.log(`Migrated ${res.rows.length} vtubers`);
}
async function migrateVods() {
console.log('Migrating vods...');
const vods = await v1.query(`SELECT * FROM vods`);
for (const vod of vods.rows) {
// Get linked vtubers
const vtuberLinks = await v1.query(
`SELECT vtuber_id FROM vods_vtuber_links WHERE vod_id = $1`,
[vod.id]
);
let vtuberSlugs: string[] = [];
if (vtuberLinks.rows.length > 0) {
const vtuberRes = await v1.query(
`SELECT slug FROM vtubers WHERE id = ANY($1)`,
[vtuberLinks.rows.map(r => r.vtuber_id)]
);
vtuberSlugs = vtuberRes.rows.map(v => v.slug).filter(Boolean);
}
// Get thumbnail
const thumbLink = await v1.query(
`SELECT b2.cdn_url, b2.url FROM vods_thumbnail_links vtl
JOIN b2_files b2 ON vtl.b_2_file_id = b2.id
WHERE vtl.vod_id = $1 LIMIT 1`,
[vod.id]
);
// Get source video
const videoSrcLink = await v1.query(
`SELECT b2.cdn_url, b2.url FROM vods_video_src_b_2_links vsl
JOIN b2_files b2 ON vsl.b_2_file_id = b2.id
WHERE vsl.vod_id = $1 LIMIT 1`,
[vod.id]
);
await prisma.vod.create({
data: {
uploaderId: DEFAULT_UPLOADER_ID,
streamDate: vod.date ?? new Date(),
notes: vod.note,
sourceVideo: videoSrcLink.rows[0]?.cdn_url || videoSrcLink.rows[0]?.url || null,
thumbnail: thumbLink.rows[0]?.cdn_url || thumbLink.rows[0]?.url || null,
vtubers: {
connect: vtuberSlugs.map(slug => ({ slug }))
}
}
});
}
console.log(`Migrated ${vods.rows.length} vods`);
}
async function main() {
try {
await migrateVtubers();
await migrateVods();
} catch (err) {
console.error(err);
} finally {
await v1.end();
await prisma.$disconnect();
}
}
main();

View File

@ -1,269 +0,0 @@
// 2025-08-12-migrate-from-futureporn.ts
//
// we are migrating from v1 futureporn.net (a strapi site) to v2 future.porn (a fastify site with prisma)
//
//
// the idea is to read a local backup of the strapi site from a .sql
// and use the data contained within to populate the database on the v2 site using prisma.
//
// here's the schema from the v1 site
```sql
CREATE TABLE "public"."b2_files" (
"id" SERIAL,
"url" VARCHAR(255) NULL,
"key" VARCHAR(255) NULL,
"upload_id" VARCHAR(255) NULL,
"created_at" TIMESTAMP NULL,
"updated_at" TIMESTAMP NULL,
"created_by_id" INTEGER NULL,
"updated_by_id" INTEGER NULL,
"cdn_url" VARCHAR(255) NULL,
CONSTRAINT "b2_files_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "public"."mux_assets" (
"id" SERIAL,
"playback_id" VARCHAR(255) NULL,
"asset_id" VARCHAR(255) NULL,
"created_at" TIMESTAMP NULL,
"updated_at" TIMESTAMP NULL,
"created_by_id" INTEGER NULL,
"updated_by_id" INTEGER NULL,
"deletion_queued_at" TIMESTAMP NULL,
CONSTRAINT "mux_assets_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "public"."vods" (
"id" SERIAL,
"video_src_hash" VARCHAR(255) NULL,
"video_720_hash" VARCHAR(255) NULL,
"video_480_hash" VARCHAR(255) NULL,
"video_360_hash" VARCHAR(255) NULL,
"video_240_hash" VARCHAR(255) NULL,
"thin_hash" VARCHAR(255) NULL,
"thicc_hash" VARCHAR(255) NULL,
"announce_title" VARCHAR(255) NULL,
"announce_url" VARCHAR(255) NULL,
"note" TEXT NULL,
"date" TIMESTAMP NULL,
"spoilers" TEXT NULL,
"created_at" TIMESTAMP NULL,
"updated_at" TIMESTAMP NULL,
"published_at" TIMESTAMP NULL,
"created_by_id" INTEGER NULL,
"updated_by_id" INTEGER NULL,
"title" VARCHAR(255) NULL,
"chat_log" TEXT NULL,
"date_2" VARCHAR(255) NULL,
"cuid" VARCHAR(255) NULL,
"archive_status" VARCHAR(255) NULL,
CONSTRAINT "vods_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "public"."vods_mux_asset_links" (
"id" SERIAL,
"vod_id" INTEGER NULL,
"mux_asset_id" INTEGER NULL,
CONSTRAINT "vods_mux_asset_links_pkey" PRIMARY KEY ("id"),
CONSTRAINT "vods_mux_asset_links_unique" UNIQUE ("vod_id", "mux_asset_id")
);
CREATE TABLE "public"."vods_thumbnail_links" (
"id" SERIAL,
"vod_id" INTEGER NULL,
"b_2_file_id" INTEGER NULL,
CONSTRAINT "vods_thumbnail_links_pkey" PRIMARY KEY ("id"),
CONSTRAINT "vods_thumbnail_links_unique" UNIQUE ("vod_id", "b_2_file_id")
);
CREATE TABLE "public"."vods_video_src_b_2_links" (
"id" SERIAL,
"vod_id" INTEGER NULL,
"b_2_file_id" INTEGER NULL,
CONSTRAINT "vods_video_src_b_2_links_pkey" PRIMARY KEY ("id"),
CONSTRAINT "vods_video_src_b_2_links_unique" UNIQUE ("vod_id", "b_2_file_id")
);
CREATE TABLE "public"."vods_vtuber_links" (
"id" SERIAL,
"vod_id" INTEGER NULL,
"vtuber_id" INTEGER NULL,
"vod_order" DOUBLE PRECISION NULL,
CONSTRAINT "vods_vtuber_links_pkey" PRIMARY KEY ("id"),
CONSTRAINT "vods_vtuber_links_unique" UNIQUE ("vod_id", "vtuber_id")
);
CREATE TABLE "public"."vtubers" (
"id" SERIAL,
"chaturbate" VARCHAR(255) NULL,
"twitter" VARCHAR(255) NULL,
"patreon" VARCHAR(255) NULL,
"twitch" VARCHAR(255) NULL,
"tiktok" VARCHAR(255) NULL,
"onlyfans" VARCHAR(255) NULL,
"youtube" VARCHAR(255) NULL,
"linktree" VARCHAR(255) NULL,
"carrd" VARCHAR(255) NULL,
"fansly" VARCHAR(255) NULL,
"pornhub" VARCHAR(255) NULL,
"discord" VARCHAR(255) NULL,
"reddit" VARCHAR(255) NULL,
"throne" VARCHAR(255) NULL,
"instagram" VARCHAR(255) NULL,
"facebook" VARCHAR(255) NULL,
"merch" VARCHAR(255) NULL,
"slug" VARCHAR(255) NULL,
"image" VARCHAR(255) NULL,
"display_name" VARCHAR(255) NULL,
"description_1" TEXT NULL,
"description_2" TEXT NULL,
"created_at" TIMESTAMP NULL,
"updated_at" TIMESTAMP NULL,
"published_at" TIMESTAMP NULL,
"created_by_id" INTEGER NULL,
"updated_by_id" INTEGER NULL,
"theme_color" VARCHAR(255) NULL,
"image_blur" VARCHAR(255) NULL,
CONSTRAINT "vtubers_pkey" PRIMARY KEY ("id")
);
```
// here's the schema from the v2 site
```prisma
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
patreonId String @unique
patreonFullName String?
imageUrl String?
roles Role[]
vods Vod[]
Vtuber Vtuber[]
}
enum RoleName {
user
supporterTier1
supporterTier2
supporterTier3
supporterTier4
supporterTier5
supporterTier6
moderator
admin
}
model Role {
id String @id @default(cuid(2))
name String @unique
users User[]
}
model RateLimiterFlexible {
key String @id
points Int
expire DateTime?
}
model Stream {
id String @id @default(cuid(2))
date DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
announcementUrl String?
vods Vod[]
@@map("stream_entity")
}
enum VodStatus {
ordering
pending
approved
rejected
processing
processed
}
model Vod {
id String @id @default(cuid(2))
streamId String?
stream Stream? @relation(fields: [streamId], references: [id])
uploaderId String // previously in Upload
uploader User @relation(fields: [uploaderId], references: [id])
streamDate DateTime
notes String?
segmentKeys Json?
sourceVideo String?
hlsPlaylist String?
thumbnail String?
asrVttKey String?
slvttSheetKeys Json?
slvttVTTKey String?
magnetLink String?
status VodStatus @default(pending)
sha256sum String?
cidv1 String?
funscript String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
vtubers Vtuber[]
}
model Vtuber {
id String @id @default(cuid(2))
image String?
slug String?
displayName String?
chaturbate String?
twitter String?
patreon String?
twitch String?
tiktok String?
onlyfans String?
youtube String?
linktree String?
carrd String?
fansly String?
pornhub String?
discord String?
reddit String?
throne String?
instagram String?
facebook String?
merch String?
description String?
themeColor String?
fanslyId String?
chaturbateId String?
twitterId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
vods Vod[]
uploaderId String
uploader User @relation(fields: [uploaderId], references: [id])
}
```

View File

@ -1,22 +0,0 @@
# scripts
This directory is for **DATA MIGRATIONS ONLY**.
Data migrations are **not** the same as schema migrations.
- **Schema migrations** (handled by Prisma Migrate) change the database structure — adding/removing columns, altering data types, creating new tables, etc.
- **Data migrations** modify the *contents* of the database to align with business logic changes, clean up old data, or backfill new fields without altering the schema.
Examples of data migrations:
- Populating a new column with default or computed values.
- Normalizing inconsistent text formats (e.g., fixing casing, trimming whitespace).
- Converting old enum/string values to new ones.
- Moving data between tables after a structural change has already been applied.
Guidelines:
1. **Idempotent when possible** — running the script twice should not break data integrity.
2. **Version-controlled** — keep a clear history of changes.
3. **Document assumptions** — include comments explaining why the migration is needed and what it affects.
4. **Run after schema changes** — if both schema and data changes are required, update the schema first.
> ⚠️ Always back up your database before running data migrations in production.

View File

@ -180,12 +180,11 @@ export function buildApp() {
} }
}) })
// @todo we are going to need to use redis or similar https://github.com/fastify/fastify-caching app.register(fastifyCaching, {
// app.register(fastifyCaching, { expiresIn: 300,
// expiresIn: 300, privacy: 'private',
// privacy: 'private', serverExpiresIn: 300,
// serverExpiresIn: 300, })
// })
app.register(graphileWorker) app.register(graphileWorker)
app.register(prismaPlugin) app.register(prismaPlugin)

View File

@ -29,12 +29,9 @@ export default async function vodsRoutes(
fastify: FastifyInstance, fastify: FastifyInstance,
): Promise<void> { ): Promise<void> {
fastify.get('/vods', async function (request, reply) { fastify.get('/vods', async function (request, reply) {
const { format } = request.query as { format: 'rss' | 'html' };
const userId = request.session.get('userId'); const userId = request.session.get('userId');
let user = null let user = null
if (userId !== undefined) { if (userId !== undefined) {
user = await prisma.user.findUnique({ user = await prisma.user.findUnique({
@ -68,34 +65,6 @@ export default async function vodsRoutes(
}, },
}); });
// RSS branch
if (format === 'rss') {
const items = vods.map(vod => {
const vtuberNames = vod.vtubers.map(v => v.displayName || v.slug).join(', ');
return {
title: `${vtuberNames} stream on ${vod.streamDate.toDateString()}`,
link: `${env.ORIGIN}/vods/${vod.id}`,
guid: `${env.ORIGIN}/vods/${vod.id}`,
pubDate: vod.streamDate.toUTCString(),
description: vod.notes
? `${vod.notes}\n\nFeaturing: ${vtuberNames}`
: `Featuring: ${vtuberNames}`,
};
});
return reply
.type('application/rss+xml')
.view('/feed.hbs', {
title: 'future.porn - VODs',
description: 'All VODs and their featured VTubers',
link: `${env.ORIGIN}/vods`,
items,
}, { layout: 'layouts/xml.hbs' });
}
// rss branch
return reply.viewAsync('vods.hbs', { return reply.viewAsync('vods.hbs', {
user, user,
vods, vods,

View File

@ -1,4 +1,4 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify" import { FastifyInstance } from "fastify"
import { constants } from "../config/constants"; import { constants } from "../config/constants";
import { PrismaClient, Vtuber } from '../../generated/prisma' import { PrismaClient, Vtuber } from '../../generated/prisma'
@ -12,14 +12,12 @@ const prisma = new PrismaClient().$extends(withAccelerate())
const hexColorRegex = /^#([0-9a-fA-F]{6})$/; const hexColorRegex = /^#([0-9a-fA-F]{6})$/;
export default async function vtubersRoutes( export default async function vtubersRoutes(
fastify: FastifyInstance, fastify: FastifyInstance,
): Promise<void> { ): Promise<void> {
fastify.get('/vtubers', async function (request, reply) {
const vtuberIndexHandler = async (request: FastifyRequest, reply: FastifyReply) => {
const userId = request.session.get('userId') const userId = request.session.get('userId')
console.log(`userId=${userId}`) console.log(`userId=${userId}`)
@ -41,18 +39,10 @@ export default async function vtubersRoutes(
vtubers, vtubers,
site: constants.site site: constants.site
}, { layout: 'layouts/main.hbs' }); }, { layout: 'layouts/main.hbs' });
}; });
['/vtubers', '/vt'].forEach(path => {
fastify.route({
method: ['GET'], // you could define multiple methods
url: path,
handler: vtuberIndexHandler
})
})
fastify.get('/vt/new', async function (request, reply) { fastify.get('/vtubers/new', async function (request, reply) {
const userId = request.session.get('userId'); const userId = request.session.get('userId');
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
@ -74,7 +64,7 @@ export default async function vtubersRoutes(
}, { layout: 'layouts/main.hbs' }) }, { layout: 'layouts/main.hbs' })
}) })
fastify.post('/vt/create', async function (request, reply) { fastify.post('/vtubers/create', async function (request, reply) {
const { const {
displayName, displayName,
themeColor, themeColor,
@ -133,7 +123,7 @@ export default async function vtubersRoutes(
if (!uppyResult) { if (!uppyResult) {
request.flash('error', '❌ missing uppyResult') request.flash('error', '❌ missing uppyResult')
reply.redirect('/vt/new') reply.redirect('/vtubers/new')
// return reply.status(400).view('vtubers/new.hbs', { // return reply.status(400).view('vtubers/new.hbs', {
// message: '❌ Missing uppyResult', // message: '❌ Missing uppyResult',
@ -145,7 +135,7 @@ export default async function vtubersRoutes(
if (!themeColor) { if (!themeColor) {
request.flash('error', '❌ Missing themeColor') request.flash('error', '❌ Missing themeColor')
reply.redirect('/vt/new') reply.redirect('/vtubers/new')
// return reply.status(400).view('vtubers/new.hbs', { // return reply.status(400).view('vtubers/new.hbs', {
// message: '❌ Missing themeColor', // message: '❌ Missing themeColor',
// vtubers, // vtubers,
@ -250,15 +240,14 @@ export default async function vtubersRoutes(
// successful upload // successful upload
request.flash('info', `✅ Successfully created vtuber <a href="/vt/${vtuber.id}">${vtuber.id}</a>`) request.flash('info', `✅ Successfully created vtuber <a href="/vtubers/${vtuber.id}">${vtuber.id}</a>`)
return reply.redirect('/vt/new') return reply.redirect('/vtubers/new')
}) })
fastify.get('/vt/:idOrSlug', async function (request, reply) { fastify.get('/vtubers/:idOrSlug', async function (request, reply) {
const { idOrSlug } = request.params as { idOrSlug: string }; const { idOrSlug } = request.params as { idOrSlug: string };
const userId = request.session.get('userId'); const userId = request.session.get('userId');
@ -268,7 +257,7 @@ export default async function vtubersRoutes(
}); });
if (!idOrSlug) { if (!idOrSlug) {
return reply.redirect('/vt') return reply.status(400).send({ error: 'Invalid VTuber identifier' });
} }
// Determine if it's a CUID (starts with "c" and length of 24) // Determine if it's a CUID (starts with "c" and length of 24)
@ -284,11 +273,7 @@ export default async function vtubersRoutes(
], ],
}, },
include: { include: {
vods: { vods: true,
orderBy: {
streamDate: 'desc'
}
}
}, },
}); });
@ -304,21 +289,14 @@ export default async function vtubersRoutes(
}); });
fastify.get('/vt/:idOrSlug/vods', async function (request, reply) { fastify.get('/vtubers/:idOrSlug/rss', async function (request, reply) {
const { idOrSlug } = request.params as { idOrSlug: string }; const { idOrSlug } = request.params as { idOrSlug: string };
const { format } = request.query as { format: 'rss' | 'html' };
if (!idOrSlug) { if (!idOrSlug) {
return reply.status(400).send({ error: 'Invalid VTuber identifier ~' }); return reply.status(400).send({ error: 'Invalid VTuber identifier' });
} }
if (format !== 'rss') {
return reply.status(404).send({ error: 'the only available format for this endpoint is rss' });
}
// Determine if it's a CUID (starts with "c" and length of 24) // Determine if it's a CUID (starts with "c" and length of 24)
const isCuid = /^c[a-z0-9]{23}$/i.test(idOrSlug); const isCuid = /^c[a-z0-9]{23}$/i.test(idOrSlug);
@ -359,7 +337,7 @@ export default async function vtubersRoutes(
.view('/feed.hbs', { .view('/feed.hbs', {
title, title,
description: vtuber.description || title, description: vtuber.description || title,
link: `${env.ORIGIN}/vt/${vtuber.slug || vtuber.id}`, link: `${env.ORIGIN}/vtubers/${vtuber.slug || vtuber.id}`,
items, items,
}, { layout: 'layouts/xml.hbs' }); }, { layout: 'layouts/xml.hbs' });
}); });

View File

@ -1,78 +1,86 @@
{{#> main}} {{#> main}}
<!-- Header -->
<header> <header class="container">
<hgroup>
<h1>{{ site.title }}</h1>
<p>{{ site.description }}</p>
</hgroup>
{{> navbar}} {{> navbar}}
</header> </header>
<!-- ./ Header -->
<main class="container"> <!-- Main -->
<main class="container pico">
<section class="hero"> <!-- Latest Vods -->
<div class="hero-body"> <section id="tables">
<h1 class="title is-1">{{ site.title }}</h1> <h2>Latest VODs</h2>
<p class="subtitle">{{ site.description }}</p> <div class="overflow-auto">
<table class="striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Vtubers</th>
<th scope="col">Uploader</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
{{#each vods}}
<tr>
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
<td>
{{#each this.vtubers}}
{{this.displayName}}
{{/each}}
</td>
<td>{{{identicon this.upload.user.id 24}}}</td>
<td>{{this.status}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div> </div>
</section> </section>
<!-- ./ Latest Vods -->
<section class="section">
<h2 class="title is-2">Latest VODs</h2>
<table class="table striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Vtubers</th>
<th scope="col">Uploader</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
{{#each vods}}
<tr>
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
<td>
{{#each this.vtubers}}
{{this.displayName}}
{{/each}}
</td>
<td>{{{identicon this.upload.user.id 24}}}</td>
<td>{{this.status}}</td>
</tr>
{{/each}}
</tbody>
</table>
</section>
<section class="section"> <!-- Latest VTubers -->
<h2 class="title is-2">Latest VTubers</h2> <section id="tables">
<table class="table striped"> <h2>Latest VTubers</h2>
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Image</th>
</tr>
</thead>
<tbody>
{{#each vtubers}}
<tr>
<td><a href="/vt/{{this.id}}">{{this.id}}</a></td>
<td>
{{this.displayName}}
</td>
<td><img class="avatar" src="{{getCdnUrl this.image}}"></td>
</tr>
{{/each}}
</tbody>
</table>
</section>
{{!--
<section>
<h2 class="title is-2">Latest Streams</h2>
<div class="overflow-auto"> <div class="overflow-auto">
<table class="title striped"> <table class="striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Image</th>
</tr>
</thead>
<tbody>
{{#each vtubers}}
<tr>
<td><a href="/vtubers/{{this.id}}">{{this.id}}</a></td>
<td>
{{this.displayName}}
</td>
<td><img class="avatar" src="{{getCdnUrl this.image}}"></td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</section>
<!-- ./ Latest Streams -->
<!-- Latest Streams -->
<section id="tables">
<h2>Latest Streams</h2>
<div class="overflow-auto">
<table class="striped">
<thead> <thead>
<tr> <tr>
<th scope="col">🚧 ID</th> <th scope="col">🚧 ID</th>
@ -113,11 +121,13 @@
</div> </div>
</section> </section>
<!-- ./ Latest Streams -->
<section> <!-- Latest Tags -->
<h2 class="title is-2">Latest Tags</h2> <section id="tables">
<h2>Latest Tags</h2>
<div class="overflow-auto"> <div class="overflow-auto">
<table class="title striped"> <table class="striped">
<thead> <thead>
<tr> <tr>
<th scope="col">🚧 ID</th> <th scope="col">🚧 ID</th>
@ -156,7 +166,8 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</section> --}} </section>
<!-- ./ Latest Tags -->
{{>footer}} {{>footer}}
</main> </main>

View File

@ -11,9 +11,8 @@
<meta name="twitter:creator" content="@cj_clippy" /> <meta name="twitter:creator" content="@cj_clippy" />
<meta name="twitter:title" content="Futureporn.net" /> <meta name="twitter:title" content="Futureporn.net" />
<meta name="twitter:description" content="{{site.description}}" /> <meta name="twitter:description" content="{{site.description}}" />
{{!-- <link rel="stylesheet" href="/css/pico.conditional.pink.min.css"> --}} <link rel="stylesheet" href="/css/pico.conditional.pink.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css"> <style>
{{!-- <style>
.logo { .logo {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -34,14 +33,14 @@
img.avatar { img.avatar {
height: 24px; height: 24px;
} }
</style> --}} </style>
</head> </head>
<body> <body>
{{{body}}}
{{{body}}}
<script src="/js/htmx.min.js"></script> <script src="/js/htmx.min.js"></script>
<script> <script>
@ -57,31 +56,6 @@
</script> </script>
<script src="/js/alpine/cdn.min.js"></script> <script src="/js/alpine/cdn.min.js"></script>
{{!-- JS for Bulma's navbar --}}
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach(el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
</script>
</body> </body>

View File

@ -1,55 +1,45 @@
<nav class="navbar" role="navigation" aria-label="main navigation"> <div class="pico">
<div class="navbar-brand"> <nav>
<a class="navbar-item" href="/"> <ul>
<div class="logo"> <li>
<img class="logo" src="/favicon.ico" alt="Logo"> <a href="/">
</div> <div class="logo">
</a> <img class="logo" src="/favicon.ico">
</div>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navMenu"> </a>
<span aria-hidden="true"></span> </li>
<span aria-hidden="true"></span> <li><a href="/vods">VODs</a></li>
<span aria-hidden="true"></span> <li><a href="/streams"><s>🚧 Streams</s></a></li>
</a> <li><a href="/vtubers">VTubers</a></li>
</div> <li><a href="/perks">Perks</a></li>
<div id="navMenu" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/vods">VODs</a>
{{!-- <a class="navbar-item" href="/streams"><s>🚧 Streams</s></a> @todo --}}
<a class="navbar-item" href="/vt">VTubers</a>
<a class="navbar-item" href="/perks">Perks</a>
{{#if (hasRole "supporterTier1" "moderator" "admin" user)}} {{#if (hasRole "supporterTier1" "moderator" "admin" user)}}
<a class="navbar-item" href="/uploads">Uploads</a> <li><a href="/uploads">Uploads</a></li>
{{/if}} {{/if}}
{{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}} {{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}}
<a class="navbar-item" href="/upload"> <li>
<span class="icon"> <a href="/upload">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" <path fill="currentColor"
d="M6 20q-.825 0-1.412-.587T4 18v-2q0-.425.288-.712T5 15t.713.288T6 16v2h12v-2q0-.425.288-.712T19 15t.713.288T20 16v2q0 .825-.587 1.413T18 20zm5-12.15L9.125 9.725q-.3.3-.712.288T7.7 9.7q-.275-.3-.288-.7t.288-.7l3.6-3.6q.15-.15.325-.212T12 4.425t.375.063t.325.212l3.6 3.6q.3.3.288.7t-.288.7q-.3.3-.712.313t-.713-.288L13 7.85V15q0 .425-.288.713T12 16t-.712-.288T11 15z" /> d="M6 20q-.825 0-1.412-.587T4 18v-2q0-.425.288-.712T5 15t.713.288T6 16v2h12v-2q0-.425.288-.712T19 15t.713.288T20 16v2q0 .825-.587 1.413T18 20zm5-12.15L9.125 9.725q-.3.3-.712.288T7.7 9.7q-.275-.3-.288-.7t.288-.7l3.6-3.6q.15-.15.325-.212T12 4.425t.375.063t.325.212l3.6 3.6q.3.3.288.7t-.288.7q-.3.3-.712.313t-.713-.288L13 7.85V15q0 .425-.288.713T12 16t-.712-.288T11 15z" />
</svg> </svg>
</span> Upload
Upload </a>
</a> </li>
<li>
<a class="navbar-item" href="/vt/new"> <a href="/vtubers/new">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" /> <path fill="currentColor" d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" />
</svg> </svg>
</span> Add VTuber
Add VTuber </a>
</a> </li>
{{/if}}
{{#if user.id}}
<li><a href="/profile">Profile</a></li>
{{else}}
<li><a href="/auth/patreon">Log in via Patreon</a></li>
{{/if}} {{/if}}
{{#if user.id}} </ul>
<a class="navbar-item" href="/profile">Profile</a> </nav>
{{else}} </div>
<a class="navbar-item" href="/auth/patreon">Log in via Patreon</a>
{{/if}}
</div>
</div>
</nav>

View File

@ -1,19 +1,18 @@
{{#> main}} {{#> main}}
<header> <header class="container">
{{> navbar}} {{> navbar}}
</header> </header>
<main class="container"> <main class="container pico">
<section id="perks"> <section id="perks">
<h2 class="title is-1">Perks</h2> <h2>Perks</h2>
<p class="subtitle">We need your help to keep the site running! In return, we offer <p>future.porn is free to use, but to keep the site running we need your help! In return, we offer extra perks
extra perks
to supporters.</p> to supporters.</p>
<table class="table striped"> <table>
<thead> <thead>
<tr> <tr>
<th>Feature</th> <th>Feature</th>
@ -25,90 +24,78 @@
<tbody> <tbody>
<tr> <tr>
<td>View</td> <td>View</td>
<td>✅</td> <td>✔️</td>
<td>✅</td> <td>✔️</td>
<td>✅</td> <td>✔️</td>
</tr>
<tr>
<td>RSS</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>API</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
</tr> </tr>
<tr> <tr>
<td>Torrent Downloads</td> <td>Torrent Downloads</td>
<td></td> <td>✔️</td>
<td></td> <td>✔️</td>
<td></td> <td>✔️</td>
</tr> </tr>
<tr> <tr>
<td>CDN Downloads</td> <td>CDN Downloads</td>
<td></td> <td></td>
<td></td> <td>✔️</td>
<td></td> <td>✔️</td>
</tr> </tr>
<tr> <tr>
<td>Ad-Free</td> <td>Ad-Free</td>
<td></td> <td></td>
<td></td> <td>✔️</td>
<td></td> <td>✔️</td>
</tr> </tr>
<tr> <tr>
<td>Upload</td> <td>Upload</td>
<td></td> <td></td>
<td></td> <td>✔️</td>
<td></td> <td>✔️</td>
</tr> </tr>
<tr> <tr>
<td><abbr title="Sex toy playback syncronization">Funscripts</abbr></td> <td><abbr title="Sex toy playback syncronization">Funscripts</abbr></td>
<td></td> <td></td>
<td></td> <td>✔️</td>
<td></td> <td>✔️</td>
</tr> </tr>
<tr> <tr>
<td>Closed Captions</td> <td>Closed Captions</td>
<td></td> <td></td>
<td></td> <td>✔️</td>
<td></td> <td>✔️</td>
</tr> </tr>
{{!-- {{!--
@todo add these things @todo add these things
<tr> <tr>
<td><abbr title="Closed Captions">CC</abbr> Search</td> <td><abbr title="Closed Captions">CC</abbr> Search</td>
<td></td> <td></td>
<td></td> <td>✔️</td>
<td></td> <td>✔️</td>
</tr> </tr>
<tr> <tr>
<td>CSV Export</td> <td>CSV Export</td>
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td>✔️</td>
</tr> </tr>
<tr> <tr>
<td>SQL Export</td> <td>SQL Export</td>
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td>✔️</td>
</tr> </tr>
<tr> <tr>
<td>vibeui PyTorch Model</td> <td>vibeui PyTorch Model</td>
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td>✔️</td>
</tr> </tr>
--}} --}}
</tbody> </tbody>
</table> </table>
<article class="notification"> <article>
<p>Become a patron at <a target="_blank" href="https://patreon.com/CJ_Clippy">patreon.com/CJ_Clippy</a></p> <p>Become a patron at <a target="_blank" href="https://patreon.com/CJ_Clippy">patreon.com/CJ_Clippy</a></p>
</article> </article>

View File

@ -1,23 +1,21 @@
{{#> main}} {{#> main}}
<header> <header class="container">
{{> navbar}} {{> navbar}}
</header> </header>
<main class="container"> <main class="container pico">
<section class="section"> <section id="tables">
<h2 class="title is-1">Profile</h2> <h2>Profile</h2>
<div class="box"> <p><strong>ID:</strong> <small>{{user.id}}</small></p>
<p><strong>Identicon:</strong> {{{identicon user.id 48}}}</p> <p><strong>Identicon:</strong> {{{identicon user.id 48}}}</p>
<p><strong>ID:</strong> <small>{{user.id}}</small></p> <p><strong>Roles:</strong> {{#each user.roles}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
<p><strong>Roles:</strong> {{#each user.roles}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}} </p>
</p> <p><strong>Perks:</strong> @todo
</div> {{!-- @todo {{#each user.perks}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}} --}}
{{!-- <p><strong>Perks:</strong> @todo </p>
@todo {{#each user.perks}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
</p> --}}
</section> </section>
<a href="/logout">Logout</a> <a href="/logout">Logout</a>

View File

@ -8,7 +8,7 @@
<!-- Main --> <!-- Main -->
<main class="container"> <main class="container">
<section> <section id="tables">
<h1> <h1>
{{#each vod.vtubers}} {{#each vod.vtubers}}
{{this.displayName}}{{#unless @last}}, {{/unless}} {{this.displayName}}{{#unless @last}}, {{/unless}}

View File

@ -4,25 +4,25 @@
crossorigin="anonymous" referrerpolicy="no-referrer"> crossorigin="anonymous" referrerpolicy="no-referrer">
<header> <header class="container">
{{> navbar }} {{> navbar }}
</header> </header>
<main class="main"> <main class="container pico">
{{#each info}} {{#each info}}
<section id="article"> <article id="article">
<p> <p>
{{{this}}} {{{this}}}
</p> </p>
</section> </article>
{{/each}} {{/each}}
<section class="section"> <section id="tables">
<h2 class="title is-1">Uploads</h2> <h2>Uploads</h2>
<div class="overflow-auto"> <div class="overflow-auto">
<table class="table striped"> <table class="striped">
<thead> <thead>
<tr> <tr>
<th>Upload ID</th> <th>Upload ID</th>

View File

@ -4,71 +4,52 @@
crossorigin="anonymous" referrerpolicy="no-referrer"> crossorigin="anonymous" referrerpolicy="no-referrer">
<header> <header class="container">
{{> navbar }} {{> navbar }}
</header> </header>
<main class="container"> <main class="container">
<h1 class="title is-1">Upload</h1> <h1>Upload Lewdtuber VODs here.</h1>
<p class="subtitle">Upload Lewdtuber VODs here.</p>
<article class="notification"> <article>
<p><strong>Instructions:</strong> Enter the metadata first. Vtuber name and original stream date is required. Then <p><strong>Instructions:</strong> Enter the metadata first. Vtuber name and original stream date is required. Then
upload the vod upload the vod
segment(s). Wait for the vod segment(s) to fully upload before pressing the submit button.</p> segment(s). Wait for the vod segment(s) to fully upload before pressing the submit button.</p>
</article> </article>
<form id="details" method="POST" action="/upload"> <form id="details" method="POST" action="/upload">
<!-- VTuber select input --> <div class="pico">
<div class="field"> <!-- VTuber select input -->
<label class="label" for="vtuberIds">VTuber(s)</label> <label for="vtuberIds">VTuber(s)</label>
<div class="control"> <select id="vtuberIds" name="vtuberIds" multiple required>
<div class="select is-multiple is-fullwidth"> {{#each vtubers}}
<select id="vtuberIds" name="vtuberIds" multiple required> <option value="{{this.id}}">{{this.displayName}}</option>
{{#each vtubers}} {{/each}}
<option value="{{this.id}}">{{this.displayName}}</option> </select>
{{/each}}
</select>
</div> <!-- Original stream date -->
</div> <label for="streamDate">Original Stream Datetime</label>
<input type="datetime-local" id="streamDate" name="streamDate" required />
<!-- Notes textarea -->
<label for="notes">Notes (optional)</label>
<textarea id="notes" name="notes" rows="4" placeholder="Any notes about the upload…"></textarea>
</div> </div>
<!-- Original stream date --> <div id="uploader"></div>
<div class="field"> <h3></h3>
<label class="label" for="streamDate">Original Stream Datetime</label>
<div class="control">
<input class="input" type="datetime-local" id="streamDate" name="streamDate" required> {{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
</div> <div class="pico">
<button>Submit</button>
</div> </div>
<!-- Notes textarea -->
<div class="field">
<label class="label" for="notes">Notes (optional)</label>
<div class="control">
<textarea class="textarea" id="notes" name="notes" rows="4"
placeholder="Any notes about the upload…"></textarea>
</div>
</div>
<!-- File uploader -->
<div class="field mb-5" id="uploader"></div>
<!-- Message display -->
{{#if message}}
<p class="help is-info" id="messages" hx-swap-oob="true">{{message}}</p>
{{/if}}
<!-- Submit button -->
<div class="field mb-5">
<div class="control">
<button class="button is-primary">Submit</button>
</div>
</div>
</form> </form>
{{> footer }} {{> footer }}
</main> </main>
@ -79,7 +60,6 @@
import ImageEditor from 'https://esm.sh/@uppy/image-editor@3.3.3' import ImageEditor from 'https://esm.sh/@uppy/image-editor@3.3.3'
import AwsS3 from 'https://esm.sh/@uppy/aws-s3@4.2.3' import AwsS3 from 'https://esm.sh/@uppy/aws-s3@4.2.3'
import Form from 'https://esm.sh/@uppy/form@4.1.1' import Form from 'https://esm.sh/@uppy/form@4.1.1'
//import Link from 'https://esm.sh/@uppy/url@4.3.2' // requires companion instance 💢
window.vtuberOptions = {{ safeJson vtubers }} window.vtuberOptions = {{ safeJson vtubers }}
@ -113,7 +93,6 @@
// 'Authorization': `Bearer @todo` // 'Authorization': `Bearer @todo`
// } // }
//}) //})
// .use(Link) requires companion instance 💢
.use(AwsS3, { .use(AwsS3, {
id: 's3Plugin', id: 's3Plugin',
endpoint: '/', endpoint: '/',

View File

@ -15,7 +15,7 @@
</style> </style>
<main class="container"> <main class="container pico">
<section> <section>

View File

@ -1,5 +1,5 @@
{{#> main}} {{#> 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/video.js@8.23.3/dist/video-js.min.css">
<link rel="stylesheet" <link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/videojs-vtt-thumbnails@0.0.13/dist/videojs-vtt-thumbnails.min.css"> href="https://cdn.jsdelivr.net/npm/videojs-vtt-thumbnails@0.0.13/dist/videojs-vtt-thumbnails.min.css">
@ -35,12 +35,14 @@
} }
</style> </style>
<header> <header class="container">
{{> navbar}} {{> navbar}}
</header> </header>
<!-- ./ Header -->
<div class="section" x-data="{}"> <main class="container" x-data="{}">
<section> <section>
{{#if vod.hlsPlaylist}} {{#if vod.hlsPlaylist}}
<div class="video-container"> <div class="video-container">
@ -61,15 +63,15 @@
{{else}} {{else}}
<video id="player" class="hidden"></video> <video id="player" class="hidden"></video>
<div class="section"> <div class="pico">
<div class="notification"> <article>
{{icon "processing" 24}} HTTP Live Streaming is processing. {{icon "processing" 24}} HTTP Live Streaming is processing.
</div> </article>
</div> </div>
{{/if}} {{/if}}
</section> </section>
<div class="container"> <section id="tables" class="pico">
{{!-- {{!--
<h2>Details</h2> <h2>Details</h2>
@ -96,206 +98,189 @@
type="application/x-mpegURL"> type="application/x-mpegURL">
</video> --}} </video> --}}
<nav class="level"> <h1>
<div class="level-left"> {{#each vod.vtubers}}
<div class="level-item"> <a href="/vtubers/{{this.slug}}">{{this.displayName}}</a>{{#unless @last}}, {{/unless}}
{{#each vod.vtubers}} {{/each}}
<a href="/vt/{{this.slug}}">{{this.displayName}}</a>{{#unless @last}}, {{/unless}} - {{formatDate vod.streamDate}}
{{/each}} </h1>
</div>
<div class="level-item">{{formatDate vod.streamDate}}</div>
</div>
</nav>
<div class="overflow-auto"> <div class="overflow-auto">
{{#if vod.notes}} {{#if vod.notes}}
<h2 id="notes" class="title is-4 mb-5">Notes</h2> <h2>Notes</h2>
<div class="card"> <article>
<div class="card-content"> <p class="breaklines">{{vod.notes}}</p>
<div class="content breaklines">{{vod.notes}}</div> <footer>
</div> {{{identicon vod.uploader.id 24}}}
<footer class="card-footer">
<div class="card-footer-item">— {{{identicon vod.uploader.id 24}}} ❦</div>
</footer> </footer>
</div> </article>
{{/if}} {{/if}}
<h2 class="title is-4">Downloads</h2> <h2>Downloads</h2>
<h3 class="title is-5">VOD</h3> <h3>VOD</h3>
<div class="box"> {{#if vod.sourceVideo}}
{{#if vod.sourceVideo}}
{{#if (hasRole "supporterTier1" user)}} {{#if (hasRole "supporterTier1" user)}}
<p><a data-source-video="{{getCdnUrl vod.sourceVideo}}" data-file-name="{{basename vod.sourceVideo}}" <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)" x-on:click.prevent="download($el.dataset.sourceVideo, $el.dataset.fileName)"
href="{{getCdnUrl vod.sourceVideo}}" download="{{basename vod.sourceVideo}}" href="{{getCdnUrl vod.sourceVideo}}" download="{{basename vod.sourceVideo}}"
target="_blank">{{icon "download" 24}} Download</a> target="_blank">{{icon "download" 24}} Download</a>
</p> </p>
{{else}} {{else}}
<p class="mb-3"> <p>
<a href="/perks">{{icon "patreon" 24}}</a> <a href="/perks">{{icon "patreon" 24}}</a>
<del> <del>
CDN Download CDN Download
</del> </del>
</p> </p>
{{/if}} {{/if}}
<p>{{#if vod.sha256sum}}<span><b>sha256sum</b> {{vod.sha256sum}}</span>{{/if}}</p> <p>{{#if vod.sha256sum}}<span><b>sha256sum</b> {{vod.sha256sum}}</span>{{/if}}</p>
{{#if vod.cidv1}} {{#if vod.cidv1}}
<p><b>IPFS cidv1</b> {{vod.cidv1}}</p> <p><b>IPFS cidv1</b> {{vod.cidv1}}</p>
{{else}} {{else}}
<article class="notification"> <article>
IPFS CID is processing. IPFS CID is processing.
</article> </article>
{{/if}} {{/if}}
{{#if vod.magnetLink}} {{#if vod.magnetLink}}
<p><a href="{{vod.magnetLink}}">{{icon "magnet" 24}} Magnet Link</a></p> <p><a href="{{vod.magnetLink}}">{{icon "magnet" 24}} Magnet Link</a></p>
{{else}} {{else}}
<article class="notification"> <article>
Magnet Link is processing. Magnet Link is processing.
</article> </article>
{{/if}} {{/if}}
</div>
<h4 class="title is-5">Raw Segments</h4> <h4>Raw Segments</h4>
<div class="mb-5"> {{#if vod.segmentKeys}}
{{#if vod.segmentKeys}} {{#if (hasRole "supporterTier1" user)}}
{{#if (hasRole "supporterTier1" user)}} <ul>
<ul> {{#each vod.segmentKeys}}
{{#each vod.segmentKeys}} <li><a data-source-video="{{getCdnUrl this.key}}" data-file-name="{{this.name}}" target="_blank"
<li><a data-source-video="{{getCdnUrl this.key}}" data-file-name="{{this.name}}" target="_blank" download="{{this.name}}" x-on:click.prevent="download($el.dataset.sourceVideo, $el.dataset.fileName)"
download="{{this.name}}" x-on:click.prevent="download($el.dataset.sourceVideo, $el.dataset.fileName)" href="{{getCdnUrl this.key}}">{{icon "download" 24}} {{this.name}}</a>
href="{{getCdnUrl this.key}}">{{icon "download" 24}} {{this.name}}</a> </li>
</li> {{/each}}
{{/each}} </ul>
</ul> {{else}}
{{else}} <p>
<p> <a href="/perks">{{icon "patreon" 24}}</a>
<a href="/perks">{{icon "patreon" 24}}</a> <del>
<del> Raw Segments CDN Download
Raw Segments CDN Download </del>
</del> </p>
</p> {{/if}}
{{/if}} {{else}}
{{else}}
<div class="notification"> <article>
This VOD has no file segments. This VOD has no file segments.
</div> </article>
{{/if}} {{!-- end of raw segments --}}
{{/if}} {{!-- end of raw segments --}}
</div>
{{else}} {{else}}
<div class="notification"> <article>
Video Source is processing. Video Source is processing.
</div> </article>
{{/if}} {{!-- end of vod.sourceVideo --}} {{/if}} {{!-- end of vod.sourceVideo --}}
<h4 class="title is-5">HLS Playlist</h4> <h4>HLS Playlist</h4>
<div class="mb-5"> {{#if vod.hlsPlaylist}}
{{#if vod.hlsPlaylist}} {{#if (hasRole "supporterTier1" user)}}
{{#if (hasRole "supporterTier1" user)}} <a href="{{signedHlsUrl vod.hlsPlaylist}}">{{signedHlsUrl vod.hlsPlaylist}}</a>
<a href="{{signedHlsUrl vod.hlsPlaylist}}">{{signedHlsUrl vod.hlsPlaylist}}</a> {{else}}
{{else}} <p>
<p> <a href="/perks">{{icon "patreon" 24}}</a>
<a href="/perks">{{icon "patreon" 24}}</a> <del>
<del> HLS Playlist CDN Download
HLS Playlist CDN Download </del>
</del> </p>
</p> {{/if}}
{{/if}} {{else}}
{{else}} <article>
<div class="notification"> HLS Playlist is processing.
HLS Playlist is processing. </article>
</div> {{/if}}
{{/if}}
</div>
<div class="mb-5"> <h4>Thumbnail Image</h4>
<h4 class="title is-5">Thumbnail Image</h4> {{#if vod.thumbnail}}
{{#if vod.thumbnail}} <img src="{{getCdnUrl vod.thumbnail}}" alt="{{vtuber.displayName}} thumbnail">
<img src="{{getCdnUrl vod.thumbnail}}" alt="{{vtuber.displayName}} thumbnail"> <div class="mx-5"></div>
<div class="mx-5"></div> {{else}}
{{else}} <article>
<div class="notification"> Thumbnail is processing.
Thumbnail is processing. </article>
</div> {{/if}}
{{/if}}
</div>
<div class="mb-5"> <h4>Funscripts (sex toy sync)</h4>
<h4 class="title is-5">Funscripts (sex toy sync)</h4> {{#if vod.funscript}}
{{#if vod.funscript}}
{{#if (hasRole "supporterTier1" user)}} {{#if (hasRole "supporterTier1" user)}}
<p> <p>
{{!-- @todo change this id to funscript-vibrate --}} {{!-- @todo change this id to funscript-vibrate --}}
<a id="funscript" data-url="{{getCdnUrl vod.funscript}}" data-file-name="{{basename vod.funscript}}" <a id="funscript" data-url="{{getCdnUrl vod.funscript}}" data-file-name="{{basename vod.funscript}}"
x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.funscript}}" x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.funscript}}"
alt="{{this.vtuber.displayName}} funscript file">{{icon "download" 24}} alt="{{this.vtuber.displayName}} funscript file">{{icon "download" 24}}
{{this.vtuber.displayName}} {{this.vtuber.displayName}}
vibrate.funscript</a> vibrate.funscript</a>
</p> </p>
<p> <p>
<s><a id="funscript-thrust">{{icon "download" 24}} thrust.funscript</a></s> (coming soon) <s><a id="funscript-thrust">{{icon "download" 24}} thrust.funscript</a></s> (coming soon)
</p> </p>
{{else}} {{else}}
<p> <p>
<a href="/perks">{{icon "patreon" 24}}</a> <a href="/perks">{{icon "patreon" 24}}</a>
<del> <del>
Funscripts Funscripts
</del> </del>
</p> </p>
{{/if}} {{/if}}
<div class="mx-5"></div> <div class="mx-5"></div>
{{else}} {{else}}
<div class="notification"> <article>
Funscript file is processing. Funscript file is processing.
</div> </article>
{{/if}} {{/if}}
</div>
<div class="mb-5">
<h4 class="title is-5">Closed Captions / Subtitles</h4>
<article>
{{#if vod.asrVttKey}} <h4>Closed Captions / Subtitles</h4>
{{#if (hasRole "supporterTier1" user)}}
<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}}" {{#if vod.asrVttKey}}
alt="Closed Captions VTT file">{{icon "download" 24}} Closed Captions</a> {{#if (hasRole "supporterTier1" user)}}
{{else}} <a id="asr-vtt" data-url="{{getCdnUrl vod.asrVttKey}}" data-file-name="{{basename vod.asrVttKey}}"
<p> x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.asrVttKey}}"
<a href="/perks">{{icon "patreon" 24}}</a> alt="Closed Captions VTT file">{{icon "download" 24}} Closed Captions</a>
<del> {{else}}
Closed Captions / Subtitles <p>
</del> <a href="/perks">{{icon "patreon" 24}}</a>
</p> <del>
{{/if}} Closed Captions / Subtitles
</article> </del>
</div> </p>
{{/if}}
{{else}} {{else}}
<div class="notification"> <article>
Closed captions are processing. Closed captions are processing.
</div> </article>
{{/if}} {{/if}}
@ -303,30 +288,23 @@
{{#if (isModerator user)}} {{#if (isModerator user)}}
<article class="mb-5"> <article>
<h2 class="title is-3">Moderator section</h2> <h2>Moderator section</h2>
<div class="box">
<div class="mb-5"> <h3>Storyboard Images</h3>
<h3 class="title is-4">Storyboard Images</h3> {{#if vod.slvttVTTKey}}
{{#if vod.slvttVTTKey}} <a id="slvtt" data-url="{{getCdnUrl vod.slvttVTTKey}}" data-file-name="{{basename 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}}"
x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.slvttVTTKey}}" alt="slvttVTTKey">{{icon "download" 24}} slvttVTTKey</a>
alt="slvttVTTKey">{{icon "download" 24}} slvttVTTKey</a> {{else}}
{{else}} <article>
<article class="notification"> Storyboard Images are processing
Storyboard Images are processing </article>
</article> {{/if}}
{{/if}}
</div>
<h3 class="title is-4">Controls</h3> <h3>Controls</h3>
<button class="button" hx-post="/vods/{{vod.id}}/process" hx-target="body"><span <button hx-post="/vods/{{vod.id}}/process" hx-target="body">{{icon "processing" 24}} Re-Schedule Vod
class="icon mr-2">{{icon "processing" 24}}</span> Processing</button>
Re-Schedule
Vod
Processing</button>
</div>
</article> </article>
{{/if}} {{/if}}
@ -337,11 +315,11 @@
{{!-- <h2>Comments</h2> {{!-- <h2>Comments</h2>
{{>commentForm}} --}} {{>commentForm}} --}}
</div> </div>
</div> </section>
{{>footer}} {{>footer}}
</div> </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> <script>

View File

@ -1,47 +1,44 @@
{{#> main}} {{#> main}}
<header class="container">
<link rel="alternate" type="application/rss+xml" href="/vods?format=rss" />
<header>
{{> navbar}} {{> navbar}}
</header> </header>
<main class="container pico"> <main class="container pico">
<section> <section id="tables">
<h2 class="title is-1">VODs <h2>VODs</h2>
<a href="/vods?format=rss" alt="RSS feed for all VODs"> <div class="overflow-auto">
{{icon "rss" 32}}</a> <table class="striped">
</h2> <thead>
<table class="table striped"> <tr>
<thead> <th>Upload Date</th>
<tr> <th>VOD ID</th>
<th>Upload Date</th> <th>VTuber</th>
<th>VOD ID</th> <th>Stream Date</th>
<th>VTuber</th> <th>Uploader</th>
<th>Stream Date</th> <th>Notes</th>
<th>Uploader</th> <th>Status</th>
<th>Notes</th> </tr>
<th>Status</th> </thead>
</tr> <tbody>
</thead> {{#each vods}}
<tbody> <tr>
{{#each vods}} <td>{{formatDate this.createdAt}}</td>
<tr> <td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
<td>{{formatDate this.createdAt}}</td> <td>
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td> {{#each this.vtubers}}
<td> {{this.displayName}}
{{#each this.vtubers}} {{/each}}
{{this.displayName}} </td>
{{/each}} <td>{{formatDate this.stream.date}}</td>
</td> <td>{{{identicon this.upload.user.id 24}}}</td>
<td>{{formatDate this.stream.date}}</td> <td>{{#if this.notes }}yes{{else}}no{{/if}}</td>
<td>{{{identicon this.upload.user.id 24}}}</td> <td>{{this.status}}</td>
<td>{{#if this.notes }}yes{{else}}no{{/if}}</td> </tr>
<td>{{this.status}}</td> {{/each}}
</tr> </tbody>
{{/each}} </table>
</tbody> </div>
</table>
</section> </section>

View File

@ -1,10 +1,10 @@
{{#> main}} {{#> main}}
<header> <header class="container">
{{> navbar}} {{> navbar}}
</header> </header>
<main class="container"> <main class="container pico">
{{#each info}} {{#each info}}
<article id="article"> <article id="article">
@ -15,10 +15,10 @@
{{/each}} {{/each}}
<section> <section id="tables">
<h2 class="title is-1">VTubers</h2> <h2>VTubers</h2>
<div class="overflow-auto"> <div class="overflow-auto">
<table class="table striped"> <table class="striped">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@ -30,7 +30,7 @@
<tbody> <tbody>
{{#each vtubers}} {{#each vtubers}}
<tr> <tr>
<td><a href="/vt/{{this.slug}}">{{this.displayName}}</a></td> <td><a href="/vtubers/{{this.slug}}">{{this.displayName}}</a></td>
<td>{{#if this.image}}<img src="{{getCdnUrl this.image}}" alt="{{this.displayName}}" class="avatar">{{/if}} <td>{{#if this.image}}<img src="{{getCdnUrl this.image}}" alt="{{this.displayName}}" class="avatar">{{/if}}
</td> </td>
<td> <td>

View File

@ -8,7 +8,7 @@
</header> </header>
<main class="container"> <main class="container">
<h1 class="title is-1">Add a new VTuber</h1> <h1>Add a new VTuber</h1>
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}} {{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
{{#each info}} {{#each info}}
@ -27,172 +27,74 @@
{{/each}} {{/each}}
<form id="details" method="POST" action="/vt/create"> <h3></h3>
<form id="details" method="POST" action="/vtubers/create">
<div class="pico">
<label for="displayName">VTuber Name <span style="color: red;">*</span></label>
<input type="text" id="displayName" name="displayName" required>
{{!-- VTuber Name --}} <label for="themeColor">Theme Color <span style="color: red;">*</span></label>
<div class="field"> <input type="color" id="themeColor" name="themeColor" required>
<label class="label" for="displayName">VTuber Name <span class="has-text-danger">*</span></label>
<div class="control"> <label for="chaturbate">Chaturbate URL</label>
<input class="input" type="text" id="displayName" name="displayName" required> <input type="url" id="chaturbate" name="chaturbate">
</div>
<label for="twitter">Twitter URL</label>
<input type="url" id="twitter" name="twitter">
<label for="patreon">Patreon URL</label>
<input type="url" id="patreon" name="patreon">
<label for="twitch">Twitch URL</label>
<input type="url" id="twitch" name="twitch">
<label for="tiktok">TikTok URL</label>
<input type="url" id="tiktok" name="tiktok">
<label for="onlyfans">OnlyFans URL</label>
<input type="url" id="onlyfans" name="onlyfans">
<label for="youtube">YouTube URL</label>
<input type="url" id="youtube" name="youtube">
<label for="linktree">Linktree URL</label>
<input type="url" id="linktree" name="linktree">
<label for="carrd">Carrd URL</label>
<input type="url" id="carrd" name="carrd">
<label for="fansly">Fansly URL</label>
<input type="url" id="fansly" name="fansly">
<label for="pornhub">Pornhub URL</label>
<input type="url" id="pornhub" name="pornhub">
<label for="discord">Discord URL</label>
<input type="url" id="discord" name="discord">
<label for="reddit">Reddit URL</label>
<input type="url" id="reddit" name="reddit">
<label for="throne">Throne URL</label>
<input type="url" id="throne" name="throne">
<label for="instagram">Instagram URL</label>
<input type="url" id="instagram" name="instagram">
<label for="facebook">Facebook URL</label>
<input type="url" id="facebook" name="facebook">
<label for="merch">Merch URL</label>
<input type="url" id="merch" name="merch">
</div> </div>
{{!-- Theme Color --}} <div id="uploader"></div>
<div class="field">
<label class="label" for="themeColor">Theme Color <span class="has-text-danger">*</span></label>
<div class="control">
<input class="input" type="color" id="themeColor" name="themeColor" required>
</div>
</div>
{{!-- Social / Platform URLs --}} <h3></h3>
{{!-- Chaturbate --}} <div class="pico">
<div class="field"> <button type="submit">Submit</button>
<label class="label" for="chaturbate">Chaturbate URL</label>
<div class="control">
<input class="input" type="url" id="chaturbate" name="chaturbate">
</div>
</div>
{{!-- Twitter --}}
<div class="field">
<label class="label" for="twitter">Twitter URL</label>
<div class="control">
<input class="input" type="url" id="twitter" name="twitter">
</div>
</div>
{{!-- Patreon --}}
<div class="field">
<label class="label" for="patreon">Patreon URL</label>
<div class="control">
<input class="input" type="url" id="patreon" name="patreon">
</div>
</div>
{{!-- Twitch --}}
<div class="field">
<label class="label" for="twitch">Twitch URL</label>
<div class="control">
<input class="input" type="url" id="twitch" name="twitch">
</div>
</div>
{{!-- TikTok --}}
<div class="field">
<label class="label" for="tiktok">TikTok URL</label>
<div class="control">
<input class="input" type="url" id="tiktok" name="tiktok">
</div>
</div>
{{!-- OnlyFans --}}
<div class="field">
<label class="label" for="onlyfans">OnlyFans URL</label>
<div class="control">
<input class="input" type="url" id="onlyfans" name="onlyfans">
</div>
</div>
{{!-- YouTube --}}
<div class="field">
<label class="label" for="youtube">YouTube URL</label>
<div class="control">
<input class="input" type="url" id="youtube" name="youtube">
</div>
</div>
{{!-- Linktree --}}
<div class="field">
<label class="label" for="linktree">Linktree URL</label>
<div class="control">
<input class="input" type="url" id="linktree" name="linktree">
</div>
</div>
{{!-- Carrd --}}
<div class="field">
<label class="label" for="carrd">Carrd URL</label>
<div class="control">
<input class="input" type="url" id="carrd" name="carrd">
</div>
</div>
{{!-- Fansly --}}
<div class="field">
<label class="label" for="fansly">Fansly URL</label>
<div class="control">
<input class="input" type="url" id="fansly" name="fansly">
</div>
</div>
{{!-- Pornhub --}}
<div class="field">
<label class="label" for="pornhub">Pornhub URL</label>
<div class="control">
<input class="input" type="url" id="pornhub" name="pornhub">
</div>
</div>
{{!-- Discord --}}
<div class="field">
<label class="label" for="discord">Discord URL</label>
<div class="control">
<input class="input" type="url" id="discord" name="discord">
</div>
</div>
{{!-- Reddit --}}
<div class="field">
<label class="label" for="reddit">Reddit URL</label>
<div class="control">
<input class="input" type="url" id="reddit" name="reddit">
</div>
</div>
{{!-- Throne --}}
<div class="field">
<label class="label" for="throne">Throne URL</label>
<div class="control">
<input class="input" type="url" id="throne" name="throne">
</div>
</div>
{{!-- Instagram --}}
<div class="field">
<label class="label" for="instagram">Instagram URL</label>
<div class="control">
<input class="input" type="url" id="instagram" name="instagram">
</div>
</div>
{{!-- Facebook --}}
<div class="field">
<label class="label" for="facebook">Facebook URL</label>
<div class="control">
<input class="input" type="url" id="facebook" name="facebook">
</div>
</div>
{{!-- Merch --}}
<div class="field">
<label class="label" for="merch">Merch URL</label>
<div class="control">
<input class="input" type="url" id="merch" name="merch">
</div>
</div>
{{!-- File uploader --}}
<div class="field" id="uploader"></div>
{{!-- Submit button --}}
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Submit</button>
</div>
</div> </div>
</form> </form>
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}} {{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
{{>footer}} {{>footer}}

File diff suppressed because one or more lines are too long