Separate YouTube video titles from on-video song/artist fields with Pro gating, serve curated fonts and watermark assets for 1:1 preview parity, and include billing/API/docs/deploy stack for self-hosted release.
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
import { Plan } from "@prisma/client";
|
|
import { getPlanLimits } from "./plans";
|
|
import { hasProFeatures } from "./edition";
|
|
import {
|
|
requiresArtTrackLayoutEntitlement,
|
|
type LayoutSettings,
|
|
} from "./layout";
|
|
import {
|
|
PREMIUM_REQUIRED_CODE,
|
|
requiresCustomWatermarkEntitlement,
|
|
type WatermarkSettings,
|
|
} from "./watermark";
|
|
|
|
export class PremiumRequiredError extends Error {
|
|
readonly code = PREMIUM_REQUIRED_CODE;
|
|
readonly status = 403;
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "PremiumRequiredError";
|
|
}
|
|
}
|
|
|
|
export function assertCustomWatermarkAllowed(plan: Plan, settings: WatermarkSettings) {
|
|
if (!requiresCustomWatermarkEntitlement(settings)) return;
|
|
if (getPlanLimits(plan).customWatermark || hasProFeatures(plan)) return;
|
|
throw new PremiumRequiredError(
|
|
"Custom watermark, typography, logo overlay, and position controls require Pro.",
|
|
);
|
|
}
|
|
|
|
export function assertArtTrackLayoutAllowed(plan: Plan, settings: LayoutSettings) {
|
|
if (!requiresArtTrackLayoutEntitlement(settings)) return;
|
|
if (getPlanLimits(plan).artTrackLayouts || hasProFeatures(plan)) return;
|
|
throw new PremiumRequiredError(
|
|
"Blurred backgrounds and art-track layout templates require Pro.",
|
|
);
|
|
}
|
|
|
|
export function assertPerItemImagesAllowed(
|
|
plan: Plan,
|
|
sharedImagePath: string,
|
|
itemImagePaths: Array<string | null | undefined>,
|
|
) {
|
|
const unique = new Set(
|
|
itemImagePaths
|
|
.map((p) => (p && p.trim() ? p.trim() : sharedImagePath))
|
|
.filter(Boolean),
|
|
);
|
|
// More than one distinct cover in the batch → Pro
|
|
if (unique.size <= 1) return;
|
|
if (getPlanLimits(plan).perItemImages || hasProFeatures(plan)) return;
|
|
throw new PremiumRequiredError(
|
|
"Matching a unique image to each audio file requires Pro. Free plan uses one shared cover image.",
|
|
);
|
|
}
|
|
|
|
export function premiumRequiredResponse(message: string) {
|
|
return {
|
|
error: message,
|
|
code: PREMIUM_REQUIRED_CODE,
|
|
};
|
|
} |