switch to bulma
This commit is contained in:
parent
db3977940f
commit
7f871b6b0a
@ -0,0 +1,8 @@
|
||||
/*
|
||||
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");
|
@ -96,7 +96,7 @@ model Vod {
|
||||
model Vtuber {
|
||||
id String @id @default(cuid(2))
|
||||
image String?
|
||||
slug String?
|
||||
slug String? @unique
|
||||
displayName String?
|
||||
chaturbate String?
|
||||
twitter String?
|
||||
|
117
services/our/scripts/2025-08-12-migrate-from-v1.ts
Normal file
117
services/our/scripts/2025-08-12-migrate-from-v1.ts
Normal file
@ -0,0 +1,117 @@
|
||||
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();
|
269
services/our/scripts/2025-08-12-migrate-from-v1.ts.prompt
Normal file
269
services/our/scripts/2025-08-12-migrate-from-v1.ts.prompt
Normal file
@ -0,0 +1,269 @@
|
||||
// 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])
|
||||
}
|
||||
|
||||
```
|
22
services/our/scripts/README.md
Normal file
22
services/our/scripts/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# 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.
|
@ -181,11 +181,11 @@ export function buildApp() {
|
||||
})
|
||||
|
||||
// @todo we are going to need to use redis or similar https://github.com/fastify/fastify-caching
|
||||
app.register(fastifyCaching, {
|
||||
expiresIn: 300,
|
||||
privacy: 'private',
|
||||
serverExpiresIn: 300,
|
||||
})
|
||||
// app.register(fastifyCaching, {
|
||||
// expiresIn: 300,
|
||||
// privacy: 'private',
|
||||
// serverExpiresIn: 300,
|
||||
// })
|
||||
|
||||
app.register(graphileWorker)
|
||||
app.register(prismaPlugin)
|
||||
|
@ -29,9 +29,12 @@ export default async function vodsRoutes(
|
||||
fastify: FastifyInstance,
|
||||
): Promise<void> {
|
||||
fastify.get('/vods', async function (request, reply) {
|
||||
const { format } = request.query as { format: 'rss' | 'html' };
|
||||
|
||||
const userId = request.session.get('userId');
|
||||
|
||||
|
||||
|
||||
let user = null
|
||||
if (userId !== undefined) {
|
||||
user = await prisma.user.findUnique({
|
||||
@ -65,6 +68,34 @@ 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', {
|
||||
user,
|
||||
vods,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"
|
||||
import { constants } from "../config/constants";
|
||||
|
||||
import { PrismaClient, Vtuber } from '../../generated/prisma'
|
||||
@ -12,12 +12,14 @@ const prisma = new PrismaClient().$extends(withAccelerate())
|
||||
const hexColorRegex = /^#([0-9a-fA-F]{6})$/;
|
||||
|
||||
|
||||
|
||||
export default async function vtubersRoutes(
|
||||
fastify: FastifyInstance,
|
||||
): Promise<void> {
|
||||
|
||||
|
||||
fastify.get('/vtubers', async function (request, reply) {
|
||||
|
||||
const vtuberIndexHandler = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const userId = request.session.get('userId')
|
||||
console.log(`userId=${userId}`)
|
||||
|
||||
@ -39,10 +41,18 @@ export default async function vtubersRoutes(
|
||||
vtubers,
|
||||
site: constants.site
|
||||
}, { layout: 'layouts/main.hbs' });
|
||||
});
|
||||
};
|
||||
|
||||
['/vtubers', '/vt'].forEach(path => {
|
||||
fastify.route({
|
||||
method: ['GET'], // you could define multiple methods
|
||||
url: path,
|
||||
handler: vtuberIndexHandler
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
fastify.get('/vtubers/new', async function (request, reply) {
|
||||
fastify.get('/vt/new', async function (request, reply) {
|
||||
const userId = request.session.get('userId');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
@ -64,7 +74,7 @@ export default async function vtubersRoutes(
|
||||
}, { layout: 'layouts/main.hbs' })
|
||||
})
|
||||
|
||||
fastify.post('/vtubers/create', async function (request, reply) {
|
||||
fastify.post('/vt/create', async function (request, reply) {
|
||||
const {
|
||||
displayName,
|
||||
themeColor,
|
||||
@ -123,7 +133,7 @@ export default async function vtubersRoutes(
|
||||
|
||||
if (!uppyResult) {
|
||||
request.flash('error', '❌ missing uppyResult')
|
||||
reply.redirect('/vtubers/new')
|
||||
reply.redirect('/vt/new')
|
||||
|
||||
// return reply.status(400).view('vtubers/new.hbs', {
|
||||
// message: '❌ Missing uppyResult',
|
||||
@ -135,7 +145,7 @@ export default async function vtubersRoutes(
|
||||
|
||||
if (!themeColor) {
|
||||
request.flash('error', '❌ Missing themeColor')
|
||||
reply.redirect('/vtubers/new')
|
||||
reply.redirect('/vt/new')
|
||||
// return reply.status(400).view('vtubers/new.hbs', {
|
||||
// message: '❌ Missing themeColor',
|
||||
// vtubers,
|
||||
@ -240,14 +250,15 @@ export default async function vtubersRoutes(
|
||||
|
||||
|
||||
// successful upload
|
||||
request.flash('info', `✅ Successfully created vtuber <a href="/vtubers/${vtuber.id}">${vtuber.id}</a>`)
|
||||
return reply.redirect('/vtubers/new')
|
||||
request.flash('info', `✅ Successfully created vtuber <a href="/vt/${vtuber.id}">${vtuber.id}</a>`)
|
||||
return reply.redirect('/vt/new')
|
||||
|
||||
|
||||
})
|
||||
|
||||
fastify.get('/vtubers/:idOrSlug', async function (request, reply) {
|
||||
fastify.get('/vt/:idOrSlug', async function (request, reply) {
|
||||
const { idOrSlug } = request.params as { idOrSlug: string };
|
||||
|
||||
const userId = request.session.get('userId');
|
||||
|
||||
|
||||
@ -257,7 +268,7 @@ export default async function vtubersRoutes(
|
||||
});
|
||||
|
||||
if (!idOrSlug) {
|
||||
return reply.status(400).send({ error: 'Invalid VTuber identifier' });
|
||||
return reply.redirect('/vt')
|
||||
}
|
||||
|
||||
// Determine if it's a CUID (starts with "c" and length of 24)
|
||||
@ -273,7 +284,11 @@ export default async function vtubersRoutes(
|
||||
],
|
||||
},
|
||||
include: {
|
||||
vods: true,
|
||||
vods: {
|
||||
orderBy: {
|
||||
streamDate: 'desc'
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -289,14 +304,21 @@ export default async function vtubersRoutes(
|
||||
});
|
||||
|
||||
|
||||
fastify.get('/vtubers/:idOrSlug/rss', async function (request, reply) {
|
||||
fastify.get('/vt/:idOrSlug/vods', async function (request, reply) {
|
||||
const { idOrSlug } = request.params as { idOrSlug: string };
|
||||
const { format } = request.query as { format: 'rss' | 'html' };
|
||||
|
||||
|
||||
|
||||
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)
|
||||
const isCuid = /^c[a-z0-9]{23}$/i.test(idOrSlug);
|
||||
|
||||
@ -337,7 +359,7 @@ export default async function vtubersRoutes(
|
||||
.view('/feed.hbs', {
|
||||
title,
|
||||
description: vtuber.description || title,
|
||||
link: `${env.ORIGIN}/vtubers/${vtuber.slug || vtuber.id}`,
|
||||
link: `${env.ORIGIN}/vt/${vtuber.slug || vtuber.id}`,
|
||||
items,
|
||||
}, { layout: 'layouts/xml.hbs' });
|
||||
});
|
||||
|
@ -1,86 +1,78 @@
|
||||
{{#> main}}
|
||||
<!-- Header -->
|
||||
<header class="container">
|
||||
<hgroup>
|
||||
<h1>{{ site.title }}</h1>
|
||||
<p>{{ site.description }}</p>
|
||||
</hgroup>
|
||||
|
||||
<header>
|
||||
{{> navbar}}
|
||||
</header>
|
||||
<!-- ./ Header -->
|
||||
|
||||
<!-- Main -->
|
||||
<main class="container pico">
|
||||
<main class="container">
|
||||
|
||||
<!-- Latest Vods -->
|
||||
<section id="tables">
|
||||
<h2>Latest VODs</h2>
|
||||
<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>
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<h1 class="title is-1">{{ site.title }}</h1>
|
||||
<p class="subtitle">{{ site.description }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ./ Latest Vods -->
|
||||
|
||||
|
||||
<!-- Latest VTubers -->
|
||||
<section id="tables">
|
||||
<h2>Latest VTubers</h2>
|
||||
<div class="overflow-auto">
|
||||
<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 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>
|
||||
<!-- ./ Latest Streams -->
|
||||
|
||||
|
||||
<section class="section">
|
||||
<h2 class="title is-2">Latest VTubers</h2>
|
||||
<table class="table striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Image</th>
|
||||
|
||||
<!-- Latest Streams -->
|
||||
<section id="tables">
|
||||
<h2>Latest Streams</h2>
|
||||
</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">
|
||||
<table class="striped">
|
||||
<table class="title striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">🚧 ID</th>
|
||||
@ -121,13 +113,11 @@
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<!-- ./ Latest Streams -->
|
||||
|
||||
<!-- Latest Tags -->
|
||||
<section id="tables">
|
||||
<h2>Latest Tags</h2>
|
||||
<section>
|
||||
<h2 class="title is-2">Latest Tags</h2>
|
||||
<div class="overflow-auto">
|
||||
<table class="striped">
|
||||
<table class="title striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">🚧 ID</th>
|
||||
@ -166,8 +156,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ./ Latest Tags -->
|
||||
</section> --}}
|
||||
|
||||
{{>footer}}
|
||||
</main>
|
||||
|
@ -11,8 +11,9 @@
|
||||
<meta name="twitter:creator" content="@cj_clippy" />
|
||||
<meta name="twitter:title" content="Futureporn.net" />
|
||||
<meta name="twitter:description" content="{{site.description}}" />
|
||||
<link rel="stylesheet" href="/css/pico.conditional.pink.min.css">
|
||||
<style>
|
||||
{{!-- <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>
|
||||
.logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -33,14 +34,14 @@
|
||||
img.avatar {
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
||||
</style> --}}
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
{{{body}}}
|
||||
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
|
||||
<script>
|
||||
@ -56,6 +57,31 @@
|
||||
</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>
|
||||
|
||||
|
@ -1,45 +1,55 @@
|
||||
<div class="pico">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">
|
||||
<div class="logo">
|
||||
<img class="logo" src="/favicon.ico">
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<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="/perks">Perks</a></li>
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
<div class="logo">
|
||||
<img class="logo" src="/favicon.ico" alt="Logo">
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navMenu">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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)}}
|
||||
<li><a href="/uploads">Uploads</a></li>
|
||||
<a class="navbar-item" href="/uploads">Uploads</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}}
|
||||
<li>
|
||||
<a href="/upload">
|
||||
<a class="navbar-item" href="/upload">
|
||||
<span class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<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" />
|
||||
</svg>
|
||||
Upload
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/vtubers/new">
|
||||
</span>
|
||||
Upload
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="/vt/new">
|
||||
<span class="icon">
|
||||
<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" />
|
||||
</svg>
|
||||
Add VTuber
|
||||
</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>
|
||||
</span>
|
||||
Add VTuber
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{{#if user.id}}
|
||||
<a class="navbar-item" href="/profile">Profile</a>
|
||||
{{else}}
|
||||
<a class="navbar-item" href="/auth/patreon">Log in via Patreon</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
@ -1,18 +1,19 @@
|
||||
{{#> main}}
|
||||
|
||||
<header class="container">
|
||||
<header>
|
||||
{{> navbar}}
|
||||
</header>
|
||||
|
||||
<main class="container pico">
|
||||
<main class="container">
|
||||
<section id="perks">
|
||||
<h2>Perks</h2>
|
||||
<h2 class="title is-1">Perks</h2>
|
||||
|
||||
<p>future.porn is free to use, but to keep the site running we need your help! In return, we offer extra perks
|
||||
<p class="subtitle">We need your help to keep the site running! In return, we offer
|
||||
extra perks
|
||||
to supporters.</p>
|
||||
|
||||
|
||||
<table>
|
||||
<table class="table striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
@ -24,78 +25,90 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<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>
|
||||
<td>Torrent Downloads</td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CDN Downloads</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ad-Free</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Upload</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</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>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Closed Captions</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
<td>❌</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
{{!--
|
||||
@todo add these things
|
||||
<tr>
|
||||
<td><abbr title="Closed Captions">CC</abbr> Search</td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✔️</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CSV Export</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SQL Export</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>vibeui PyTorch Model</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>✔️</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
--}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<article>
|
||||
<article class="notification">
|
||||
<p>Become a patron at <a target="_blank" href="https://patreon.com/CJ_Clippy">patreon.com/CJ_Clippy</a></p>
|
||||
</article>
|
||||
|
||||
|
@ -1,21 +1,23 @@
|
||||
{{#> main}}
|
||||
|
||||
<header class="container">
|
||||
<header>
|
||||
{{> navbar}}
|
||||
</header>
|
||||
|
||||
<main class="container pico">
|
||||
<main class="container">
|
||||
|
||||
|
||||
<section id="tables">
|
||||
<h2>Profile</h2>
|
||||
<p><strong>ID:</strong> <small>{{user.id}}</small></p>
|
||||
<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 class="section">
|
||||
<h2 class="title is-1">Profile</h2>
|
||||
<div class="box">
|
||||
<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>
|
||||
</div>
|
||||
{{!-- <p><strong>Perks:</strong> @todo
|
||||
@todo {{#each user.perks}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
|
||||
</p> --}}
|
||||
</section>
|
||||
|
||||
<a href="/logout">Logout</a>
|
||||
|
@ -8,7 +8,7 @@
|
||||
<!-- Main -->
|
||||
<main class="container">
|
||||
|
||||
<section id="tables">
|
||||
<section>
|
||||
<h1>
|
||||
{{#each vod.vtubers}}
|
||||
{{this.displayName}}{{#unless @last}}, {{/unless}}
|
||||
|
@ -4,25 +4,25 @@
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
|
||||
|
||||
<header class="container">
|
||||
<header>
|
||||
{{> navbar }}
|
||||
</header>
|
||||
|
||||
|
||||
<main class="container pico">
|
||||
<main class="main">
|
||||
|
||||
{{#each info}}
|
||||
<article id="article">
|
||||
<section id="article">
|
||||
<p>
|
||||
{{{this}}}
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
{{/each}}
|
||||
|
||||
<section id="tables">
|
||||
<h2>Uploads</h2>
|
||||
<section class="section">
|
||||
<h2 class="title is-1">Uploads</h2>
|
||||
<div class="overflow-auto">
|
||||
<table class="striped">
|
||||
<table class="table striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Upload ID</th>
|
||||
|
@ -4,52 +4,71 @@
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
|
||||
|
||||
<header class="container">
|
||||
<header>
|
||||
{{> navbar }}
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<h1>Upload Lewdtuber VODs here.</h1>
|
||||
<h1 class="title is-1">Upload</h1>
|
||||
<p class="subtitle">Upload Lewdtuber VODs here.</p>
|
||||
|
||||
<article>
|
||||
<article class="notification">
|
||||
<p><strong>Instructions:</strong> Enter the metadata first. Vtuber name and original stream date is required. Then
|
||||
upload the vod
|
||||
segment(s). Wait for the vod segment(s) to fully upload before pressing the submit button.</p>
|
||||
</article>
|
||||
|
||||
<form id="details" method="POST" action="/upload">
|
||||
<div class="pico">
|
||||
<!-- VTuber select input -->
|
||||
<label for="vtuberIds">VTuber(s)</label>
|
||||
<select id="vtuberIds" name="vtuberIds" multiple required>
|
||||
{{#each vtubers}}
|
||||
<option value="{{this.id}}">{{this.displayName}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
|
||||
|
||||
<!-- Original stream date -->
|
||||
<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>
|
||||
<!-- VTuber select input -->
|
||||
<div class="field">
|
||||
<label class="label" for="vtuberIds">VTuber(s)</label>
|
||||
<div class="control">
|
||||
<div class="select is-multiple is-fullwidth">
|
||||
<select id="vtuberIds" name="vtuberIds" multiple required>
|
||||
{{#each vtubers}}
|
||||
<option value="{{this.id}}">{{this.displayName}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="uploader"></div>
|
||||
<h3></h3>
|
||||
|
||||
|
||||
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
|
||||
<div class="pico">
|
||||
<button>Submit</button>
|
||||
<!-- Original stream date -->
|
||||
<div class="field">
|
||||
<label class="label" for="streamDate">Original Stream Datetime</label>
|
||||
<div class="control">
|
||||
<input class="input" type="datetime-local" id="streamDate" name="streamDate" required>
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
|
||||
{{> footer }}
|
||||
</main>
|
||||
|
||||
@ -60,6 +79,7 @@
|
||||
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 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 }}
|
||||
@ -93,6 +113,7 @@
|
||||
// 'Authorization': `Bearer @todo`
|
||||
// }
|
||||
//})
|
||||
// .use(Link) requires companion instance 💢
|
||||
.use(AwsS3, {
|
||||
id: 's3Plugin',
|
||||
endpoint: '/',
|
||||
|
@ -15,7 +15,7 @@
|
||||
</style>
|
||||
|
||||
|
||||
<main class="container pico">
|
||||
<main class="container">
|
||||
|
||||
<section>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{#> 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">
|
||||
@ -35,14 +35,12 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<header class="container">
|
||||
<header>
|
||||
{{> navbar}}
|
||||
|
||||
</header>
|
||||
<!-- ./ Header -->
|
||||
|
||||
|
||||
<main class="container" x-data="{}">
|
||||
<div class="section" x-data="{}">
|
||||
<section>
|
||||
{{#if vod.hlsPlaylist}}
|
||||
<div class="video-container">
|
||||
@ -63,15 +61,15 @@
|
||||
{{else}}
|
||||
<video id="player" class="hidden"></video>
|
||||
|
||||
<div class="pico">
|
||||
<article>
|
||||
<div class="section">
|
||||
<div class="notification">
|
||||
|
||||
{{icon "processing" 24}} HTTP Live Streaming is processing.
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
<section id="tables" class="pico">
|
||||
<div class="container">
|
||||
|
||||
{{!--
|
||||
<h2>Details</h2>
|
||||
@ -98,32 +96,38 @@
|
||||
type="application/x-mpegURL">
|
||||
</video> --}}
|
||||
|
||||
<h1>
|
||||
{{#each vod.vtubers}}
|
||||
<a href="/vtubers/{{this.slug}}">{{this.displayName}}</a>{{#unless @last}}, {{/unless}}
|
||||
{{/each}}
|
||||
- {{formatDate vod.streamDate}}
|
||||
</h1>
|
||||
<nav class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
{{#each vod.vtubers}}
|
||||
<a href="/vt/{{this.slug}}">{{this.displayName}}</a>{{#unless @last}}, {{/unless}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="level-item">{{formatDate vod.streamDate}}</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="overflow-auto">
|
||||
|
||||
{{#if vod.notes}}
|
||||
<h2>Notes</h2>
|
||||
<article>
|
||||
<p class="breaklines">{{vod.notes}}</p>
|
||||
<footer>
|
||||
— {{{identicon vod.uploader.id 24}}} ❦
|
||||
<h2 id="notes" class="title is-4 mb-5">Notes</h2>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="content breaklines">{{vod.notes}}</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<div class="card-footer-item">— {{{identicon vod.uploader.id 24}}} ❦</div>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
|
||||
<h2>Downloads</h2>
|
||||
<h2 class="title is-4">Downloads</h2>
|
||||
|
||||
|
||||
<h3>VOD</h3>
|
||||
<article>
|
||||
<h3 class="title is-5">VOD</h3>
|
||||
<div class="box">
|
||||
{{#if vod.sourceVideo}}
|
||||
|
||||
{{#if (hasRole "supporterTier1" user)}}
|
||||
@ -133,7 +137,7 @@
|
||||
target="_blank">{{icon "download" 24}} Download</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<p>
|
||||
<p class="mb-3">
|
||||
<a href="/perks">{{icon "patreon" 24}}</a>
|
||||
<del>
|
||||
CDN Download
|
||||
@ -145,22 +149,22 @@
|
||||
{{#if vod.cidv1}}
|
||||
<p><b>IPFS cidv1</b> {{vod.cidv1}}</p>
|
||||
{{else}}
|
||||
<article>
|
||||
<article class="notification">
|
||||
IPFS CID is processing.
|
||||
</article>
|
||||
{{/if}}
|
||||
{{#if vod.magnetLink}}
|
||||
<p><a href="{{vod.magnetLink}}">{{icon "magnet" 24}} Magnet Link</a></p>
|
||||
{{else}}
|
||||
<article>
|
||||
<article class="notification">
|
||||
Magnet Link is processing.
|
||||
</article>
|
||||
{{/if}}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
<h4>Raw Segments</h4>
|
||||
<article>
|
||||
<h4 class="title is-5">Raw Segments</h4>
|
||||
<div class="mb-5">
|
||||
{{#if vod.segmentKeys}}
|
||||
{{#if (hasRole "supporterTier1" user)}}
|
||||
<ul>
|
||||
@ -181,28 +185,28 @@
|
||||
{{/if}}
|
||||
{{else}}
|
||||
|
||||
<article>
|
||||
<div class="notification">
|
||||
This VOD has no file segments.
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{{/if}} {{!-- end of raw segments --}}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{else}}
|
||||
<article>
|
||||
<div class="notification">
|
||||
Video Source is processing.
|
||||
</article>
|
||||
</div>
|
||||
{{/if}} {{!-- end of vod.sourceVideo --}}
|
||||
|
||||
|
||||
|
||||
|
||||
<h4>HLS Playlist</h4>
|
||||
<article>
|
||||
<h4 class="title is-5">HLS Playlist</h4>
|
||||
<div class="mb-5">
|
||||
{{#if vod.hlsPlaylist}}
|
||||
{{#if (hasRole "supporterTier1" user)}}
|
||||
<a href="{{signedHlsUrl vod.hlsPlaylist}}">{{signedHlsUrl vod.hlsPlaylist}}</a>
|
||||
@ -215,26 +219,28 @@
|
||||
</p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<article>
|
||||
<div class="notification">
|
||||
HLS Playlist is processing.
|
||||
</article>
|
||||
</div>
|
||||
{{/if}}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h4>Thumbnail Image</h4>
|
||||
{{#if vod.thumbnail}}
|
||||
<img src="{{getCdnUrl vod.thumbnail}}" alt="{{vtuber.displayName}} thumbnail">
|
||||
<div class="mx-5"></div>
|
||||
{{else}}
|
||||
<article>
|
||||
Thumbnail is processing.
|
||||
</article>
|
||||
{{/if}}
|
||||
<div class="mb-5">
|
||||
<h4 class="title is-5">Thumbnail Image</h4>
|
||||
{{#if vod.thumbnail}}
|
||||
<img src="{{getCdnUrl vod.thumbnail}}" alt="{{vtuber.displayName}} thumbnail">
|
||||
<div class="mx-5"></div>
|
||||
{{else}}
|
||||
<div class="notification">
|
||||
Thumbnail is processing.
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h4>Funscripts (sex toy sync)</h4>
|
||||
<article>
|
||||
<div class="mb-5">
|
||||
<h4 class="title is-5">Funscripts (sex toy sync)</h4>
|
||||
{{#if vod.funscript}}
|
||||
|
||||
{{#if (hasRole "supporterTier1" user)}}
|
||||
@ -259,35 +265,37 @@
|
||||
{{/if}}
|
||||
<div class="mx-5"></div>
|
||||
{{else}}
|
||||
<article>
|
||||
<div class="notification">
|
||||
Funscript file is processing.
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<h4 class="title is-5">Closed Captions / Subtitles</h4>
|
||||
<article>
|
||||
|
||||
{{#if vod.asrVttKey}}
|
||||
{{#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}}"
|
||||
alt="Closed Captions VTT file">{{icon "download" 24}} Closed Captions</a>
|
||||
{{else}}
|
||||
<p>
|
||||
<a href="/perks">{{icon "patreon" 24}}</a>
|
||||
<del>
|
||||
Closed Captions / Subtitles
|
||||
</del>
|
||||
</p>
|
||||
{{/if}}
|
||||
</article>
|
||||
{{/if}}
|
||||
</article>
|
||||
|
||||
<h4>Closed Captions / Subtitles</h4>
|
||||
<article>
|
||||
|
||||
{{#if vod.asrVttKey}}
|
||||
{{#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}}"
|
||||
alt="Closed Captions VTT file">{{icon "download" 24}} Closed Captions</a>
|
||||
{{else}}
|
||||
<p>
|
||||
<a href="/perks">{{icon "patreon" 24}}</a>
|
||||
<del>
|
||||
Closed Captions / Subtitles
|
||||
</del>
|
||||
</p>
|
||||
{{/if}}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
{{else}}
|
||||
<article>
|
||||
<div class="notification">
|
||||
Closed captions are processing.
|
||||
</article>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@ -295,23 +303,30 @@
|
||||
|
||||
|
||||
{{#if (isModerator user)}}
|
||||
<article>
|
||||
<h2>Moderator section</h2>
|
||||
<article class="mb-5">
|
||||
<h2 class="title is-3">Moderator section</h2>
|
||||
<div class="box">
|
||||
|
||||
<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}}
|
||||
<div class="mb-5">
|
||||
<h3 class="title is-4">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 class="notification">
|
||||
Storyboard Images are processing
|
||||
</article>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<h3>Controls</h3>
|
||||
<button hx-post="/vods/{{vod.id}}/process" hx-target="body">{{icon "processing" 24}} Re-Schedule Vod
|
||||
Processing</button>
|
||||
<h3 class="title is-4">Controls</h3>
|
||||
<button class="button" hx-post="/vods/{{vod.id}}/process" hx-target="body"><span
|
||||
class="icon mr-2">{{icon "processing" 24}}</span>
|
||||
Re-Schedule
|
||||
Vod
|
||||
Processing</button>
|
||||
</div>
|
||||
</article>
|
||||
{{/if}}
|
||||
|
||||
@ -322,11 +337,11 @@
|
||||
{{!-- <h2>Comments</h2>
|
||||
{{>commentForm}} --}}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
{{>footer}}
|
||||
</main>
|
||||
</div>
|
||||
<script src=" https://cdn.jsdelivr.net/npm/video.js@8.23.3/dist/video.min.js "></script>
|
||||
<script>
|
||||
|
||||
|
@ -1,44 +1,47 @@
|
||||
{{#> main}}
|
||||
<header class="container">
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" href="/vods?format=rss" />
|
||||
<header>
|
||||
{{> navbar}}
|
||||
</header>
|
||||
|
||||
<main class="container pico">
|
||||
|
||||
<section id="tables">
|
||||
<h2>VODs</h2>
|
||||
<div class="overflow-auto">
|
||||
<table class="striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Upload Date</th>
|
||||
<th>VOD ID</th>
|
||||
<th>VTuber</th>
|
||||
<th>Stream Date</th>
|
||||
<th>Uploader</th>
|
||||
<th>Notes</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each vods}}
|
||||
<tr>
|
||||
<td>{{formatDate this.createdAt}}</td>
|
||||
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
|
||||
<td>
|
||||
{{#each this.vtubers}}
|
||||
{{this.displayName}}
|
||||
{{/each}}
|
||||
</td>
|
||||
<td>{{formatDate this.stream.date}}</td>
|
||||
<td>{{{identicon this.upload.user.id 24}}}</td>
|
||||
<td>{{#if this.notes }}yes{{else}}no{{/if}}</td>
|
||||
<td>{{this.status}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<section>
|
||||
<h2 class="title is-1">VODs
|
||||
<a href="/vods?format=rss" alt="RSS feed for all VODs">
|
||||
{{icon "rss" 32}}</a>
|
||||
</h2>
|
||||
<table class="table striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Upload Date</th>
|
||||
<th>VOD ID</th>
|
||||
<th>VTuber</th>
|
||||
<th>Stream Date</th>
|
||||
<th>Uploader</th>
|
||||
<th>Notes</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each vods}}
|
||||
<tr>
|
||||
<td>{{formatDate this.createdAt}}</td>
|
||||
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
|
||||
<td>
|
||||
{{#each this.vtubers}}
|
||||
{{this.displayName}}
|
||||
{{/each}}
|
||||
</td>
|
||||
<td>{{formatDate this.stream.date}}</td>
|
||||
<td>{{{identicon this.upload.user.id 24}}}</td>
|
||||
<td>{{#if this.notes }}yes{{else}}no{{/if}}</td>
|
||||
<td>{{this.status}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
{{#> main}}
|
||||
|
||||
<header class="container">
|
||||
<header>
|
||||
{{> navbar}}
|
||||
</header>
|
||||
|
||||
<main class="container pico">
|
||||
<main class="container">
|
||||
|
||||
{{#each info}}
|
||||
<article id="article">
|
||||
@ -15,10 +15,10 @@
|
||||
{{/each}}
|
||||
|
||||
|
||||
<section id="tables">
|
||||
<h2>VTubers</h2>
|
||||
<section>
|
||||
<h2 class="title is-1">VTubers</h2>
|
||||
<div class="overflow-auto">
|
||||
<table class="striped">
|
||||
<table class="table striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@ -30,7 +30,7 @@
|
||||
<tbody>
|
||||
{{#each vtubers}}
|
||||
<tr>
|
||||
<td><a href="/vtubers/{{this.slug}}">{{this.displayName}}</a></td>
|
||||
<td><a href="/vt/{{this.slug}}">{{this.displayName}}</a></td>
|
||||
<td>{{#if this.image}}<img src="{{getCdnUrl this.image}}" alt="{{this.displayName}}" class="avatar">{{/if}}
|
||||
</td>
|
||||
<td>
|
||||
|
@ -8,7 +8,7 @@
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<h1>Add a new VTuber</h1>
|
||||
<h1 class="title is-1">Add a new VTuber</h1>
|
||||
|
||||
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
|
||||
{{#each info}}
|
||||
@ -27,74 +27,172 @@
|
||||
{{/each}}
|
||||
|
||||
|
||||
<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>
|
||||
<form id="details" method="POST" action="/vt/create">
|
||||
|
||||
<label for="themeColor">Theme Color <span style="color: red;">*</span></label>
|
||||
<input type="color" id="themeColor" name="themeColor" required>
|
||||
|
||||
<label for="chaturbate">Chaturbate URL</label>
|
||||
<input type="url" id="chaturbate" name="chaturbate">
|
||||
|
||||
<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">
|
||||
{{!-- VTuber Name --}}
|
||||
<div class="field">
|
||||
<label class="label" for="displayName">VTuber Name <span class="has-text-danger">*</span></label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="displayName" name="displayName" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="uploader"></div>
|
||||
{{!-- Theme Color --}}
|
||||
<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>
|
||||
|
||||
<h3></h3>
|
||||
<div class="pico">
|
||||
<button type="submit">Submit</button>
|
||||
{{!-- Social / Platform URLs --}}
|
||||
{{!-- Chaturbate --}}
|
||||
<div class="field">
|
||||
<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>
|
||||
</form>
|
||||
|
||||
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
|
||||
|
||||
{{>footer}}
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user