Store badges in Postgres, expose public/admin APIs, render an infinite footer slider, and ship /admin for paste-embed CRUD.
210 lines
6.7 KiB
Plaintext
210 lines
6.7 KiB
Plaintext
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
enum Plan {
|
||
FREE
|
||
PREMIUM
|
||
}
|
||
|
||
enum Privacy {
|
||
PUBLIC
|
||
PRIVATE
|
||
UNLISTED
|
||
}
|
||
|
||
enum JobStatus {
|
||
PENDING
|
||
PROCESSING
|
||
COMPLETED
|
||
FAILED
|
||
PARTIAL
|
||
}
|
||
|
||
enum JobItemStatus {
|
||
PENDING
|
||
ENCODING
|
||
UPLOADING
|
||
COMPLETED
|
||
FAILED
|
||
}
|
||
|
||
enum QuotaExtensionRequestStatus {
|
||
PENDING
|
||
APPROVED
|
||
REJECTED
|
||
}
|
||
|
||
enum QuotaExtensionKind {
|
||
VIDEO_QUOTA
|
||
API_RATE_LIMIT
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(cuid())
|
||
email String @unique
|
||
name String?
|
||
image String?
|
||
/// FREE = free tier; PREMIUM = Pro (€5/mo, formerly starter_5eur)
|
||
plan Plan @default(FREE)
|
||
/// Credits consumed in the current billing cycle (credits_used)
|
||
videosUsed Int @default(0)
|
||
/// Monthly allocation for the current cycle (10 free / 50 pro)
|
||
monthlyCredits Int @default(10)
|
||
quotaResetAt DateTime
|
||
cardLast4 String?
|
||
subscribedAt DateTime?
|
||
bonusQuota Int @default(0)
|
||
apiRateLimitBonus Int @default(0)
|
||
/// Purchased extras that never expire (maps to legacy "videoCredits" column)
|
||
extraCredits Int @default(0) @map("videoCredits")
|
||
/// Free-tier one-time +15 top-up already purchased
|
||
freeTopUpPurchased Boolean @default(false)
|
||
stripeCustomerId String?
|
||
stripeSubscriptionId String?
|
||
apiKeyHash String? @unique
|
||
apiKeyPrefix String?
|
||
youtubeConnection YouTubeConnection?
|
||
jobs Job[]
|
||
quotaExtensionRequests QuotaExtensionRequest[]
|
||
creditPurchases CreditPurchase[]
|
||
createdAt DateTime @default(now())
|
||
}
|
||
|
||
enum CreditPurchaseStatus {
|
||
PENDING
|
||
COMPLETED
|
||
FAILED
|
||
}
|
||
|
||
model CreditPurchase {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
credits Int
|
||
amountCents Int
|
||
status CreditPurchaseStatus @default(PENDING)
|
||
stripeSessionId String? @unique
|
||
stripePaymentIntentId String?
|
||
createdAt DateTime @default(now())
|
||
completedAt DateTime?
|
||
}
|
||
|
||
enum JobItemBilling {
|
||
QUOTA
|
||
CREDIT
|
||
}
|
||
|
||
model QuotaExtensionRequest {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
kind QuotaExtensionKind @default(VIDEO_QUOTA)
|
||
status QuotaExtensionRequestStatus @default(PENDING)
|
||
message String @default("")
|
||
requestedAt DateTime @default(now())
|
||
processedAt DateTime?
|
||
adminNote String?
|
||
}
|
||
|
||
model YouTubeConnection {
|
||
id String @id @default(cuid())
|
||
userId String @unique
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
accessToken String
|
||
refreshToken String
|
||
expiresAt DateTime
|
||
channelId String
|
||
channelTitle String
|
||
}
|
||
|
||
model Job {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
status JobStatus @default(PENDING)
|
||
imagePath String
|
||
items JobItem[]
|
||
createdAt DateTime @default(now())
|
||
completedAt DateTime?
|
||
}
|
||
|
||
model JobItem {
|
||
id String @id @default(cuid())
|
||
jobId String
|
||
job Job @relation(fields: [jobId], references: [id], onDelete: Cascade)
|
||
audioPath String
|
||
audioFilename String
|
||
title String
|
||
description String @default("")
|
||
tags String @default("")
|
||
privacy Privacy @default(PUBLIC)
|
||
categoryId String @default("10")
|
||
resolution String @default("1280x720")
|
||
notifySubscribers Boolean @default(true)
|
||
madeForKids Boolean @default(false)
|
||
embeddable Boolean @default(true)
|
||
creativeCommons Boolean @default(false)
|
||
includeWatermark Boolean @default(true)
|
||
/// Optional per-item cover (Pro). Falls back to Job.imagePath when null.
|
||
itemImagePath String?
|
||
/// none | default | text | logo custom modes require Pro
|
||
watermarkMode String @default("default")
|
||
watermarkText String?
|
||
watermarkLogoPath String?
|
||
/// curated key | custom | system
|
||
watermarkFontKey String?
|
||
/// uploaded .ttf/.otf when fontKey=custom
|
||
watermarkFontPath String?
|
||
/// top-left | top-right | bottom-left | bottom-right | center
|
||
watermarkPosition String @default("bottom-right")
|
||
watermarkOffsetX Int @default(20)
|
||
watermarkOffsetY Int @default(20)
|
||
/// Optional artist line for art-track layouts (Pro, on-video only)
|
||
artist String?
|
||
/// Song title burned into art-track layouts (Pro, on-video only)
|
||
songTitle String?
|
||
/// COVER_LEFT_TEXT_RIGHT | COVER_TOP_TEXT_BOTTOM | COVER_RIGHT_TEXT_LEFT | CENTERED_COMPACT | null=classic
|
||
layoutTemplate String?
|
||
/// 0–100 Gaussian / boxblur intensity (Pro art-track)
|
||
blurAmount Int @default(55)
|
||
/// 0–100 visibility of blurred fill vs black
|
||
blurOpacity Int @default(100)
|
||
/// Padding around cover + text in art-track layouts
|
||
textPadding Int @default(48)
|
||
/// Extra gap between title and artist lines (px)
|
||
titleArtistGap Int @default(10)
|
||
/// Fine-tune text block within template (-120..120)
|
||
textOffsetX Int @default(0)
|
||
textOffsetY Int @default(0)
|
||
playlistId String?
|
||
billingSource JobItemBilling @default(QUOTA)
|
||
status JobItemStatus @default(PENDING)
|
||
outputPath String?
|
||
youtubeVideoId String?
|
||
error String?
|
||
}
|
||
|
||
/// Site footer badge / launch-directory embeds (managed via /admin)
|
||
model FooterBadge {
|
||
id String @id @default(cuid())
|
||
name String
|
||
/// Raw paste from Product Hunt / LaunchBuff / etc. (preferred source)
|
||
embedHtml String? @db.Text
|
||
/// Parsed or manually set image URL (used when embedHtml is empty)
|
||
imageUrl String?
|
||
/// Parsed or manually set destination URL
|
||
linkUrl String?
|
||
isActive Boolean @default(true)
|
||
sortOrder Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([isActive, sortOrder])
|
||
}
|