Strip Songs2VID to a payment-free self-hosted OSS core.

Remove Stripe/billing/pricing/marketing, simplify schema and entitlements for unlimited self-host use, and keep auth, encode, and YouTube upload.
This commit is contained in:
Atakan Doğan Özban
2026-08-03 06:52:00 +02:00
parent 7d7043b150
commit d9cdaff521
108 changed files with 397 additions and 8061 deletions
+10 -53
View File
@@ -1,6 +1,4 @@
import { Plan } from "@prisma/client";
import { getResolution, RESOLUTIONS } from "./constants";
import { isSelfHostedEdition } from "./edition";
export type PlanLimits = {
monthlyQuota: number;
@@ -18,35 +16,8 @@ export type PlanLimits = {
id3TagSupport: boolean;
};
export const PLAN_LIMITS: Record<Plan, PlanLimits> = {
FREE: {
monthlyQuota: 10,
maxBatchSize: 3,
maxResolutionHeight: 720,
maxFileSizeBytes: 30 * 1024 * 1024,
watermarkOptional: true,
customWatermark: false,
perItemImages: false,
artTrackLayouts: false,
allowedAudioExtensions: [".mp3"],
id3TagSupport: true,
},
PREMIUM: {
monthlyQuota: 50,
maxBatchSize: 5,
maxResolutionHeight: 1080,
maxFileSizeBytes: 30 * 1024 * 1024,
watermarkOptional: true,
customWatermark: true,
perItemImages: true,
artTrackLayouts: true,
allowedAudioExtensions: [".mp3", ".wav", ".flac"],
id3TagSupport: true,
},
};
export const SELFHOSTED_LIMITS: PlanLimits = {
monthlyQuota: 1_000_000,
export const APP_LIMITS: PlanLimits = {
monthlyQuota: Number.MAX_SAFE_INTEGER,
maxBatchSize: 100,
maxResolutionHeight: Math.max(...RESOLUTIONS.map((r) => r.height)),
maxFileSizeBytes: 500 * 1024 * 1024,
@@ -58,11 +29,7 @@ export const SELFHOSTED_LIMITS: PlanLimits = {
id3TagSupport: true,
};
export const SALES_EMAIL = "songs2vid@atakanozban.com";
export const SUPPORT_EMAIL = "songs2vid@atakanozban.com";
/** Pro quota reset / extension requests */
export const QUOTA_REQUEST_EMAIL = "songs2vid@atakanozban.com";
export const UPGRADE_URL = "/#pricing";
export const GITEA_ISSUES_URL =
process.env.NEXT_PUBLIC_GITEA_ISSUES_URL ?? "https://git.atakanozban.com/Songs2VID/songs2vid/issues";
export const GITEA_URL =
@@ -70,42 +37,32 @@ export const GITEA_URL =
export const DOCKER_HUB_URL =
process.env.NEXT_PUBLIC_DOCKER_HUB_URL ?? "https://hub.docker.com/r/atakanozban/songs2vid";
/** Docusaurus docs base URL. Local: http://localhost:3001 (npm run docs:dev). */
export function resolveDocsUrl(): string {
if (process.env.NEXT_PUBLIC_DOCS_URL?.trim()) {
return process.env.NEXT_PUBLIC_DOCS_URL.replace(/\/$/, "");
}
const auth = process.env.NEXTAUTH_URL ?? "";
if (/localhost|127\.0\.0\.1/i.test(auth)) {
return "http://localhost:3001";
}
return "https://docs.songs2vid.com";
}
export const DOCS_URL = resolveDocsUrl();
export const API_DOCS_URL = `${DOCS_URL}/docs/api/overview`;
export function getPlanLimits(plan: Plan): PlanLimits {
if (isSelfHostedEdition()) return SELFHOSTED_LIMITS;
return PLAN_LIMITS[plan];
export function getPlanLimits(): PlanLimits {
return APP_LIMITS;
}
export function getResolutionsForPlan(plan: Plan) {
const maxHeight = getPlanLimits(plan).maxResolutionHeight;
export function getResolutionsForPlan() {
const maxHeight = APP_LIMITS.maxResolutionHeight;
return RESOLUTIONS.filter((r) => r.height <= maxHeight);
}
export function isResolutionAllowedForPlan(resolution: string, plan: Plan): boolean {
export function isResolutionAllowedForPlan(resolution: string): boolean {
const res = getResolution(resolution);
if (!res) return false;
return res.height <= getPlanLimits(plan).maxResolutionHeight;
return res.height <= APP_LIMITS.maxResolutionHeight;
}
export function isAudioExtensionAllowed(filename: string, plan: Plan): boolean {
export function isAudioExtensionAllowed(filename: string): boolean {
const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
return getPlanLimits(plan).allowedAudioExtensions.includes(ext);
}
export function getNextMonthlyQuotaReset(from: Date = new Date()): Date {
return new Date(from.getFullYear(), from.getMonth() + 1, 1, 0, 0, 0, 0);
return APP_LIMITS.allowedAudioExtensions.includes(ext);
}