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? plan Plan @default(FREE) videosUsed Int @default(0) quotaResetAt DateTime cardLast4 String? subscribedAt DateTime? bonusQuota Int @default(0) apiRateLimitBonus Int @default(0) /// Legacy column retained for schema compatibility; unused in OSS (always 0). videoCredits Int @default(0) apiKeyHash String? @unique apiKeyPrefix String? youtubeConnection YouTubeConnection? jobs Job[] quotaExtensionRequests QuotaExtensionRequest[] createdAt DateTime @default(now()) } 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. Falls back to Job.imagePath when null. itemImagePath String? /// none | default | text | logo 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 artist String? /// COVER_LEFT_TEXT_RIGHT | COVER_TOP_TEXT_BOTTOM | COVER_RIGHT_TEXT_LEFT | CENTERED_COMPACT | null=classic layoutTemplate String? /// 0–100 Gaussian / boxblur intensity 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? }