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 {
|
model Vtuber {
|
||||||
id String @id @default(cuid(2))
|
id String @id @default(cuid(2))
|
||||||
image String?
|
image String?
|
||||||
slug String?
|
slug String? @unique
|
||||||
displayName String?
|
displayName String?
|
||||||
chaturbate String?
|
chaturbate String?
|
||||||
twitter 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
|
// @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)
|
||||||
|
@ -29,9 +29,12 @@ 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({
|
||||||
@ -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', {
|
return reply.viewAsync('vods.hbs', {
|
||||||
user,
|
user,
|
||||||
vods,
|
vods,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance, FastifyReply, FastifyRequest } 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,12 +12,14 @@ 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}`)
|
||||||
|
|
||||||
@ -39,10 +41,18 @@ 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('/vtubers/new', async function (request, reply) {
|
fastify.get('/vt/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({
|
||||||
@ -64,7 +74,7 @@ export default async function vtubersRoutes(
|
|||||||
}, { layout: 'layouts/main.hbs' })
|
}, { layout: 'layouts/main.hbs' })
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.post('/vtubers/create', async function (request, reply) {
|
fastify.post('/vt/create', async function (request, reply) {
|
||||||
const {
|
const {
|
||||||
displayName,
|
displayName,
|
||||||
themeColor,
|
themeColor,
|
||||||
@ -123,7 +133,7 @@ export default async function vtubersRoutes(
|
|||||||
|
|
||||||
if (!uppyResult) {
|
if (!uppyResult) {
|
||||||
request.flash('error', '❌ missing uppyResult')
|
request.flash('error', '❌ missing uppyResult')
|
||||||
reply.redirect('/vtubers/new')
|
reply.redirect('/vt/new')
|
||||||
|
|
||||||
// return reply.status(400).view('vtubers/new.hbs', {
|
// return reply.status(400).view('vtubers/new.hbs', {
|
||||||
// message: '❌ Missing uppyResult',
|
// message: '❌ Missing uppyResult',
|
||||||
@ -135,7 +145,7 @@ export default async function vtubersRoutes(
|
|||||||
|
|
||||||
if (!themeColor) {
|
if (!themeColor) {
|
||||||
request.flash('error', '❌ Missing themeColor')
|
request.flash('error', '❌ Missing themeColor')
|
||||||
reply.redirect('/vtubers/new')
|
reply.redirect('/vt/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,
|
||||||
@ -240,14 +250,15 @@ export default async function vtubersRoutes(
|
|||||||
|
|
||||||
|
|
||||||
// successful upload
|
// successful upload
|
||||||
request.flash('info', `✅ Successfully created vtuber <a href="/vtubers/${vtuber.id}">${vtuber.id}</a>`)
|
request.flash('info', `✅ Successfully created vtuber <a href="/vt/${vtuber.id}">${vtuber.id}</a>`)
|
||||||
return reply.redirect('/vtubers/new')
|
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 { idOrSlug } = request.params as { idOrSlug: string };
|
||||||
|
|
||||||
const userId = request.session.get('userId');
|
const userId = request.session.get('userId');
|
||||||
|
|
||||||
|
|
||||||
@ -257,7 +268,7 @@ export default async function vtubersRoutes(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!idOrSlug) {
|
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)
|
// Determine if it's a CUID (starts with "c" and length of 24)
|
||||||
@ -273,7 +284,11 @@ export default async function vtubersRoutes(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: {
|
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 { 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);
|
||||||
|
|
||||||
@ -337,7 +359,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}/vtubers/${vtuber.slug || vtuber.id}`,
|
link: `${env.ORIGIN}/vt/${vtuber.slug || vtuber.id}`,
|
||||||
items,
|
items,
|
||||||
}, { layout: 'layouts/xml.hbs' });
|
}, { layout: 'layouts/xml.hbs' });
|
||||||
});
|
});
|
||||||
|
@ -1,86 +1,78 @@
|
|||||||
{{#> main}}
|
{{#> main}}
|
||||||
<!-- Header -->
|
|
||||||
<header class="container">
|
<header>
|
||||||
<hgroup>
|
|
||||||
<h1>{{ site.title }}</h1>
|
|
||||||
<p>{{ site.description }}</p>
|
|
||||||
</hgroup>
|
|
||||||
{{> navbar}}
|
{{> navbar}}
|
||||||
</header>
|
</header>
|
||||||
<!-- ./ Header -->
|
|
||||||
|
|
||||||
<!-- Main -->
|
<main class="container">
|
||||||
<main class="container pico">
|
|
||||||
|
|
||||||
<!-- Latest Vods -->
|
<section class="hero">
|
||||||
<section id="tables">
|
<div class="hero-body">
|
||||||
<h2>Latest VODs</h2>
|
<h1 class="title is-1">{{ site.title }}</h1>
|
||||||
<div class="overflow-auto">
|
<p class="subtitle">{{ site.description }}</p>
|
||||||
<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">
|
||||||
<!-- Latest VTubers -->
|
<h2 class="title is-2">Latest VODs</h2>
|
||||||
<section id="tables">
|
<table class="table striped">
|
||||||
<h2>Latest VTubers</h2>
|
<thead>
|
||||||
<div class="overflow-auto">
|
<tr>
|
||||||
<table class="striped">
|
<th scope="col">ID</th>
|
||||||
<thead>
|
<th scope="col">Vtubers</th>
|
||||||
<tr>
|
<th scope="col">Uploader</th>
|
||||||
<th scope="col">ID</th>
|
<th scope="col">Status</th>
|
||||||
<th scope="col">Name</th>
|
</tr>
|
||||||
<th scope="col">Image</th>
|
</thead>
|
||||||
|
<tbody>
|
||||||
</tr>
|
{{#each vods}}
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>
|
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
|
||||||
{{#each vtubers}}
|
<td>
|
||||||
<tr>
|
{{#each this.vtubers}}
|
||||||
<td><a href="/vtubers/{{this.id}}">{{this.id}}</a></td>
|
{{this.displayName}}
|
||||||
<td>
|
{{/each}}
|
||||||
{{this.displayName}}
|
</td>
|
||||||
</td>
|
<td>{{{identicon this.upload.user.id 24}}}</td>
|
||||||
<td><img class="avatar" src="{{getCdnUrl this.image}}"></td>
|
<td>{{this.status}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
</section>
|
</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 -->
|
</tr>
|
||||||
<section id="tables">
|
</thead>
|
||||||
<h2>Latest Streams</h2>
|
<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="striped">
|
<table class="title striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">🚧 ID</th>
|
<th scope="col">🚧 ID</th>
|
||||||
@ -121,13 +113,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<!-- ./ Latest Streams -->
|
|
||||||
|
|
||||||
<!-- Latest Tags -->
|
<section>
|
||||||
<section id="tables">
|
<h2 class="title is-2">Latest Tags</h2>
|
||||||
<h2>Latest Tags</h2>
|
|
||||||
<div class="overflow-auto">
|
<div class="overflow-auto">
|
||||||
<table class="striped">
|
<table class="title striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">🚧 ID</th>
|
<th scope="col">🚧 ID</th>
|
||||||
@ -166,8 +156,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section> --}}
|
||||||
<!-- ./ Latest Tags -->
|
|
||||||
|
|
||||||
{{>footer}}
|
{{>footer}}
|
||||||
</main>
|
</main>
|
||||||
|
@ -11,8 +11,9 @@
|
|||||||
<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"> --}}
|
||||||
<style>
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||||
|
{{!-- <style>
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -33,14 +34,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>
|
||||||
@ -56,6 +57,31 @@
|
|||||||
</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>
|
||||||
|
|
||||||
|
@ -1,45 +1,55 @@
|
|||||||
<div class="pico">
|
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||||
<nav>
|
<div class="navbar-brand">
|
||||||
<ul>
|
<a class="navbar-item" href="/">
|
||||||
<li>
|
<div class="logo">
|
||||||
<a href="/">
|
<img class="logo" src="/favicon.ico" alt="Logo">
|
||||||
<div class="logo">
|
</div>
|
||||||
<img class="logo" src="/favicon.ico">
|
</a>
|
||||||
</div>
|
|
||||||
</a>
|
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navMenu">
|
||||||
</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>
|
<span aria-hidden="true"></span>
|
||||||
<li><a href="/vtubers">VTubers</a></li>
|
</a>
|
||||||
<li><a href="/perks">Perks</a></li>
|
</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)}}
|
{{#if (hasRole "supporterTier1" "moderator" "admin" user)}}
|
||||||
<li><a href="/uploads">Uploads</a></li>
|
<a class="navbar-item" href="/uploads">Uploads</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}}
|
{{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}}
|
||||||
<li>
|
<a class="navbar-item" href="/upload">
|
||||||
<a href="/upload">
|
<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"
|
<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>
|
||||||
Upload
|
</span>
|
||||||
</a>
|
Upload
|
||||||
</li>
|
</a>
|
||||||
<li>
|
|
||||||
<a href="/vtubers/new">
|
<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">
|
<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>
|
||||||
Add VTuber
|
</span>
|
||||||
</a>
|
Add VTuber
|
||||||
</li>
|
</a>
|
||||||
{{/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}}
|
||||||
|
|
||||||
</ul>
|
{{#if user.id}}
|
||||||
</nav>
|
<a class="navbar-item" href="/profile">Profile</a>
|
||||||
</div>
|
{{else}}
|
||||||
|
<a class="navbar-item" href="/auth/patreon">Log in via Patreon</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
@ -1,18 +1,19 @@
|
|||||||
{{#> main}}
|
{{#> main}}
|
||||||
|
|
||||||
<header class="container">
|
<header>
|
||||||
{{> navbar}}
|
{{> navbar}}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container pico">
|
<main class="container">
|
||||||
<section id="perks">
|
<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>
|
to supporters.</p>
|
||||||
|
|
||||||
|
|
||||||
<table>
|
<table class="table striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Feature</th>
|
<th>Feature</th>
|
||||||
@ -24,78 +25,90 @@
|
|||||||
<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>
|
<article class="notification">
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
{{#> main}}
|
{{#> main}}
|
||||||
|
|
||||||
<header class="container">
|
<header>
|
||||||
{{> navbar}}
|
{{> navbar}}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container pico">
|
<main class="container">
|
||||||
|
|
||||||
|
|
||||||
<section id="tables">
|
<section class="section">
|
||||||
<h2>Profile</h2>
|
<h2 class="title is-1">Profile</h2>
|
||||||
<p><strong>ID:</strong> <small>{{user.id}}</small></p>
|
<div class="box">
|
||||||
<p><strong>Identicon:</strong> {{{identicon user.id 48}}}</p>
|
<p><strong>Identicon:</strong> {{{identicon user.id 48}}}</p>
|
||||||
<p><strong>Roles:</strong> {{#each user.roles}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
|
<p><strong>ID:</strong> <small>{{user.id}}</small></p>
|
||||||
</p>
|
<p><strong>Roles:</strong> {{#each user.roles}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
|
||||||
<p><strong>Perks:</strong> @todo
|
</p>
|
||||||
{{!-- @todo {{#each user.perks}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}} --}}
|
</div>
|
||||||
</p>
|
{{!-- <p><strong>Perks:</strong> @todo
|
||||||
|
@todo {{#each user.perks}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}
|
||||||
|
</p> --}}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<a href="/logout">Logout</a>
|
<a href="/logout">Logout</a>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
|
||||||
<section id="tables">
|
<section>
|
||||||
<h1>
|
<h1>
|
||||||
{{#each vod.vtubers}}
|
{{#each vod.vtubers}}
|
||||||
{{this.displayName}}{{#unless @last}}, {{/unless}}
|
{{this.displayName}}{{#unless @last}}, {{/unless}}
|
||||||
|
@ -4,25 +4,25 @@
|
|||||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
|
|
||||||
|
|
||||||
<header class="container">
|
<header>
|
||||||
{{> navbar }}
|
{{> navbar }}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
<main class="container pico">
|
<main class="main">
|
||||||
|
|
||||||
{{#each info}}
|
{{#each info}}
|
||||||
<article id="article">
|
<section id="article">
|
||||||
<p>
|
<p>
|
||||||
{{{this}}}
|
{{{this}}}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</section>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
<section id="tables">
|
<section class="section">
|
||||||
<h2>Uploads</h2>
|
<h2 class="title is-1">Uploads</h2>
|
||||||
<div class="overflow-auto">
|
<div class="overflow-auto">
|
||||||
<table class="striped">
|
<table class="table striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Upload ID</th>
|
<th>Upload ID</th>
|
||||||
|
@ -4,52 +4,71 @@
|
|||||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
|
|
||||||
|
|
||||||
<header class="container">
|
<header>
|
||||||
{{> navbar }}
|
{{> navbar }}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<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
|
<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">
|
||||||
<div class="pico">
|
<!-- VTuber select input -->
|
||||||
<!-- VTuber select input -->
|
<div class="field">
|
||||||
<label for="vtuberIds">VTuber(s)</label>
|
<label class="label" for="vtuberIds">VTuber(s)</label>
|
||||||
<select id="vtuberIds" name="vtuberIds" multiple required>
|
<div class="control">
|
||||||
{{#each vtubers}}
|
<div class="select is-multiple is-fullwidth">
|
||||||
<option value="{{this.id}}">{{this.displayName}}</option>
|
<select id="vtuberIds" name="vtuberIds" multiple required>
|
||||||
{{/each}}
|
{{#each vtubers}}
|
||||||
</select>
|
<option value="{{this.id}}">{{this.displayName}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
<!-- Original stream date -->
|
</div>
|
||||||
<label for="streamDate">Original Stream Datetime</label>
|
</div>
|
||||||
<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>
|
||||||
|
|
||||||
<div id="uploader"></div>
|
<!-- Original stream date -->
|
||||||
<h3></h3>
|
<div class="field">
|
||||||
|
<label class="label" for="streamDate">Original Stream Datetime</label>
|
||||||
|
<div class="control">
|
||||||
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
|
<input class="input" type="datetime-local" id="streamDate" name="streamDate" required>
|
||||||
<div class="pico">
|
</div>
|
||||||
<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>
|
||||||
|
|
||||||
@ -60,6 +79,7 @@
|
|||||||
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 }}
|
||||||
@ -93,6 +113,7 @@
|
|||||||
// 'Authorization': `Bearer @todo`
|
// 'Authorization': `Bearer @todo`
|
||||||
// }
|
// }
|
||||||
//})
|
//})
|
||||||
|
// .use(Link) requires companion instance 💢
|
||||||
.use(AwsS3, {
|
.use(AwsS3, {
|
||||||
id: 's3Plugin',
|
id: 's3Plugin',
|
||||||
endpoint: '/',
|
endpoint: '/',
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<main class="container pico">
|
<main class="container">
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
||||||
|
@ -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,14 +35,12 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<header class="container">
|
<header>
|
||||||
{{> navbar}}
|
{{> navbar}}
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
<!-- ./ Header -->
|
|
||||||
|
|
||||||
|
|
||||||
<main class="container" x-data="{}">
|
<div class="section" x-data="{}">
|
||||||
<section>
|
<section>
|
||||||
{{#if vod.hlsPlaylist}}
|
{{#if vod.hlsPlaylist}}
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
@ -63,15 +61,15 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<video id="player" class="hidden"></video>
|
<video id="player" class="hidden"></video>
|
||||||
|
|
||||||
<div class="pico">
|
<div class="section">
|
||||||
<article>
|
<div class="notification">
|
||||||
|
|
||||||
{{icon "processing" 24}} HTTP Live Streaming is processing.
|
{{icon "processing" 24}} HTTP Live Streaming is processing.
|
||||||
</article>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</section>
|
</section>
|
||||||
<section id="tables" class="pico">
|
<div class="container">
|
||||||
|
|
||||||
{{!--
|
{{!--
|
||||||
<h2>Details</h2>
|
<h2>Details</h2>
|
||||||
@ -98,32 +96,38 @@
|
|||||||
type="application/x-mpegURL">
|
type="application/x-mpegURL">
|
||||||
</video> --}}
|
</video> --}}
|
||||||
|
|
||||||
<h1>
|
<nav class="level">
|
||||||
{{#each vod.vtubers}}
|
<div class="level-left">
|
||||||
<a href="/vtubers/{{this.slug}}">{{this.displayName}}</a>{{#unless @last}}, {{/unless}}
|
<div class="level-item">
|
||||||
{{/each}}
|
{{#each vod.vtubers}}
|
||||||
- {{formatDate vod.streamDate}}
|
<a href="/vt/{{this.slug}}">{{this.displayName}}</a>{{#unless @last}}, {{/unless}}
|
||||||
</h1>
|
{{/each}}
|
||||||
|
</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>Notes</h2>
|
<h2 id="notes" class="title is-4 mb-5">Notes</h2>
|
||||||
<article>
|
<div class="card">
|
||||||
<p class="breaklines">{{vod.notes}}</p>
|
<div class="card-content">
|
||||||
<footer>
|
<div class="content breaklines">{{vod.notes}}</div>
|
||||||
— {{{identicon vod.uploader.id 24}}} ❦
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
<div class="card-footer-item">— {{{identicon vod.uploader.id 24}}} ❦</div>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h2>Downloads</h2>
|
<h2 class="title is-4">Downloads</h2>
|
||||||
|
|
||||||
|
|
||||||
<h3>VOD</h3>
|
<h3 class="title is-5">VOD</h3>
|
||||||
<article>
|
<div class="box">
|
||||||
{{#if vod.sourceVideo}}
|
{{#if vod.sourceVideo}}
|
||||||
|
|
||||||
{{#if (hasRole "supporterTier1" user)}}
|
{{#if (hasRole "supporterTier1" user)}}
|
||||||
@ -133,7 +137,7 @@
|
|||||||
target="_blank">{{icon "download" 24}} Download</a>
|
target="_blank">{{icon "download" 24}} Download</a>
|
||||||
</p>
|
</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>
|
<p class="mb-3">
|
||||||
<a href="/perks">{{icon "patreon" 24}}</a>
|
<a href="/perks">{{icon "patreon" 24}}</a>
|
||||||
<del>
|
<del>
|
||||||
CDN Download
|
CDN Download
|
||||||
@ -145,22 +149,22 @@
|
|||||||
{{#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>
|
<article class="notification">
|
||||||
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>
|
<article class="notification">
|
||||||
Magnet Link is processing.
|
Magnet Link is processing.
|
||||||
</article>
|
</article>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<h4>Raw Segments</h4>
|
<h4 class="title is-5">Raw Segments</h4>
|
||||||
<article>
|
<div class="mb-5">
|
||||||
{{#if vod.segmentKeys}}
|
{{#if vod.segmentKeys}}
|
||||||
{{#if (hasRole "supporterTier1" user)}}
|
{{#if (hasRole "supporterTier1" user)}}
|
||||||
<ul>
|
<ul>
|
||||||
@ -181,28 +185,28 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
||||||
<article>
|
<div class="notification">
|
||||||
This VOD has no file segments.
|
This VOD has no file segments.
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
{{/if}} {{!-- end of raw segments --}}
|
{{/if}} {{!-- end of raw segments --}}
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<article>
|
<div class="notification">
|
||||||
Video Source is processing.
|
Video Source is processing.
|
||||||
</article>
|
</div>
|
||||||
{{/if}} {{!-- end of vod.sourceVideo --}}
|
{{/if}} {{!-- end of vod.sourceVideo --}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h4>HLS Playlist</h4>
|
<h4 class="title is-5">HLS Playlist</h4>
|
||||||
<article>
|
<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>
|
||||||
@ -215,26 +219,28 @@
|
|||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<article>
|
<div class="notification">
|
||||||
HLS Playlist is processing.
|
HLS Playlist is processing.
|
||||||
</article>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
<h4>Thumbnail Image</h4>
|
<div class="mb-5">
|
||||||
{{#if vod.thumbnail}}
|
<h4 class="title is-5">Thumbnail Image</h4>
|
||||||
<img src="{{getCdnUrl vod.thumbnail}}" alt="{{vtuber.displayName}} thumbnail">
|
{{#if vod.thumbnail}}
|
||||||
<div class="mx-5"></div>
|
<img src="{{getCdnUrl vod.thumbnail}}" alt="{{vtuber.displayName}} thumbnail">
|
||||||
{{else}}
|
<div class="mx-5"></div>
|
||||||
<article>
|
{{else}}
|
||||||
Thumbnail is processing.
|
<div class="notification">
|
||||||
</article>
|
Thumbnail is processing.
|
||||||
{{/if}}
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h4>Funscripts (sex toy sync)</h4>
|
<div class="mb-5">
|
||||||
<article>
|
<h4 class="title is-5">Funscripts (sex toy sync)</h4>
|
||||||
{{#if vod.funscript}}
|
{{#if vod.funscript}}
|
||||||
|
|
||||||
{{#if (hasRole "supporterTier1" user)}}
|
{{#if (hasRole "supporterTier1" user)}}
|
||||||
@ -259,35 +265,37 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="mx-5"></div>
|
<div class="mx-5"></div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<article>
|
<div class="notification">
|
||||||
Funscript file is processing.
|
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>
|
</article>
|
||||||
{{/if}}
|
</div>
|
||||||
</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>
|
|
||||||
|
|
||||||
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<article>
|
<div class="notification">
|
||||||
Closed captions are processing.
|
Closed captions are processing.
|
||||||
</article>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
||||||
@ -295,23 +303,30 @@
|
|||||||
|
|
||||||
|
|
||||||
{{#if (isModerator user)}}
|
{{#if (isModerator user)}}
|
||||||
<article>
|
<article class="mb-5">
|
||||||
<h2>Moderator section</h2>
|
<h2 class="title is-3">Moderator section</h2>
|
||||||
|
<div class="box">
|
||||||
|
|
||||||
<h3>Storyboard Images</h3>
|
<div class="mb-5">
|
||||||
{{#if vod.slvttVTTKey}}
|
<h3 class="title is-4">Storyboard Images</h3>
|
||||||
<a id="slvtt" data-url="{{getCdnUrl vod.slvttVTTKey}}" data-file-name="{{basename vod.slvttVTTKey}}"
|
{{#if vod.slvttVTTKey}}
|
||||||
x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.slvttVTTKey}}"
|
<a id="slvtt" data-url="{{getCdnUrl vod.slvttVTTKey}}" data-file-name="{{basename vod.slvttVTTKey}}"
|
||||||
alt="slvttVTTKey">{{icon "download" 24}} slvttVTTKey</a>
|
x-on:click.prevent="download($el.dataset.url, $el.dataset.fileName)" href="{{getCdnUrl vod.slvttVTTKey}}"
|
||||||
{{else}}
|
alt="slvttVTTKey">{{icon "download" 24}} slvttVTTKey</a>
|
||||||
<article>
|
{{else}}
|
||||||
Storyboard Images are processing
|
<article class="notification">
|
||||||
</article>
|
Storyboard Images are processing
|
||||||
{{/if}}
|
</article>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Controls</h3>
|
<h3 class="title is-4">Controls</h3>
|
||||||
<button hx-post="/vods/{{vod.id}}/process" hx-target="body">{{icon "processing" 24}} Re-Schedule Vod
|
<button class="button" hx-post="/vods/{{vod.id}}/process" hx-target="body"><span
|
||||||
Processing</button>
|
class="icon mr-2">{{icon "processing" 24}}</span>
|
||||||
|
Re-Schedule
|
||||||
|
Vod
|
||||||
|
Processing</button>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@ -322,11 +337,11 @@
|
|||||||
{{!-- <h2>Comments</h2>
|
{{!-- <h2>Comments</h2>
|
||||||
{{>commentForm}} --}}
|
{{>commentForm}} --}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{{>footer}}
|
{{>footer}}
|
||||||
</main>
|
</div>
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -1,44 +1,47 @@
|
|||||||
{{#> 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 id="tables">
|
<section>
|
||||||
<h2>VODs</h2>
|
<h2 class="title is-1">VODs
|
||||||
<div class="overflow-auto">
|
<a href="/vods?format=rss" alt="RSS feed for all VODs">
|
||||||
<table class="striped">
|
{{icon "rss" 32}}</a>
|
||||||
<thead>
|
</h2>
|
||||||
<tr>
|
<table class="table striped">
|
||||||
<th>Upload Date</th>
|
<thead>
|
||||||
<th>VOD ID</th>
|
<tr>
|
||||||
<th>VTuber</th>
|
<th>Upload Date</th>
|
||||||
<th>Stream Date</th>
|
<th>VOD ID</th>
|
||||||
<th>Uploader</th>
|
<th>VTuber</th>
|
||||||
<th>Notes</th>
|
<th>Stream Date</th>
|
||||||
<th>Status</th>
|
<th>Uploader</th>
|
||||||
</tr>
|
<th>Notes</th>
|
||||||
</thead>
|
<th>Status</th>
|
||||||
<tbody>
|
</tr>
|
||||||
{{#each vods}}
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td>{{formatDate this.createdAt}}</td>
|
{{#each vods}}
|
||||||
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
|
<tr>
|
||||||
<td>
|
<td>{{formatDate this.createdAt}}</td>
|
||||||
{{#each this.vtubers}}
|
<td><a href="/vods/{{this.id}}">{{this.id}}</a></td>
|
||||||
{{this.displayName}}
|
<td>
|
||||||
{{/each}}
|
{{#each this.vtubers}}
|
||||||
</td>
|
{{this.displayName}}
|
||||||
<td>{{formatDate this.stream.date}}</td>
|
{{/each}}
|
||||||
<td>{{{identicon this.upload.user.id 24}}}</td>
|
</td>
|
||||||
<td>{{#if this.notes }}yes{{else}}no{{/if}}</td>
|
<td>{{formatDate this.stream.date}}</td>
|
||||||
<td>{{this.status}}</td>
|
<td>{{{identicon this.upload.user.id 24}}}</td>
|
||||||
</tr>
|
<td>{{#if this.notes }}yes{{else}}no{{/if}}</td>
|
||||||
{{/each}}
|
<td>{{this.status}}</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
{{/each}}
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{{#> main}}
|
{{#> main}}
|
||||||
|
|
||||||
<header class="container">
|
<header>
|
||||||
{{> navbar}}
|
{{> navbar}}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container pico">
|
<main class="container">
|
||||||
|
|
||||||
{{#each info}}
|
{{#each info}}
|
||||||
<article id="article">
|
<article id="article">
|
||||||
@ -15,10 +15,10 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
|
||||||
<section id="tables">
|
<section>
|
||||||
<h2>VTubers</h2>
|
<h2 class="title is-1">VTubers</h2>
|
||||||
<div class="overflow-auto">
|
<div class="overflow-auto">
|
||||||
<table class="striped">
|
<table class="table 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="/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>{{#if this.image}}<img src="{{getCdnUrl this.image}}" alt="{{this.displayName}}" class="avatar">{{/if}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<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}}
|
{{#if message}}<p id="messages" hx-swap-oob="true">{{message}}</p>{{/if}}
|
||||||
{{#each info}}
|
{{#each info}}
|
||||||
@ -27,74 +27,172 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
|
||||||
<h3></h3>
|
<form id="details" method="POST" action="/vt/create">
|
||||||
<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>
|
|
||||||
|
|
||||||
<label for="themeColor">Theme Color <span style="color: red;">*</span></label>
|
{{!-- VTuber Name --}}
|
||||||
<input type="color" id="themeColor" name="themeColor" required>
|
<div class="field">
|
||||||
|
<label class="label" for="displayName">VTuber Name <span class="has-text-danger">*</span></label>
|
||||||
<label for="chaturbate">Chaturbate URL</label>
|
<div class="control">
|
||||||
<input type="url" id="chaturbate" name="chaturbate">
|
<input class="input" type="text" id="displayName" name="displayName" required>
|
||||||
|
</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>
|
||||||
|
|
||||||
<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>
|
{{!-- Social / Platform URLs --}}
|
||||||
<div class="pico">
|
{{!-- Chaturbate --}}
|
||||||
<button type="submit">Submit</button>
|
<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>
|
</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
Loading…
x
Reference in New Issue
Block a user