Initial open-source self-hosted Songs2YT edition

This commit is contained in:
Songs2YT
2026-07-17 02:23:59 +02:00
commit 2aa8a84781
134 changed files with 17758 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
import { QuotaExtensionRequestStatus } from "@prisma/client";
import { MAX_ADMIN_API_RATE_BONUS } from "./api-rate-limit";
import { prisma } from "./db";
import { getPlanLimits } from "./plans";
export const QUOTA_EXTENSION_ANNUAL_LIMIT = 5;
/** Max bonus videos an admin may grant per approval. */
export const MAX_ADMIN_BONUS_QUOTA = 50;
export const EXTENSION_KIND = {
VIDEO_QUOTA: "VIDEO_QUOTA",
API_RATE_LIMIT: "API_RATE_LIMIT",
} as const;
export type ExtensionKind = (typeof EXTENSION_KIND)[keyof typeof EXTENSION_KIND];
function getCalendarYearBounds(year = new Date().getFullYear()) {
return {
start: new Date(year, 0, 1),
end: new Date(year + 1, 0, 1),
};
}
export async function getQuotaExtensionUsage(
userId: string,
kind: ExtensionKind = EXTENSION_KIND.VIDEO_QUOTA,
) {
const { start, end } = getCalendarYearBounds();
const requests = await prisma.quotaExtensionRequest.findMany({
where: {
userId,
kind,
requestedAt: { gte: start, lt: end },
},
orderBy: { requestedAt: "desc" },
});
const used = requests.filter((r) => r.status !== QuotaExtensionRequestStatus.REJECTED).length;
return {
used,
limit: QUOTA_EXTENSION_ANNUAL_LIMIT,
remaining: Math.max(0, QUOTA_EXTENSION_ANNUAL_LIMIT - used),
kind,
requests: requests.map((r) => ({
id: r.id,
kind: r.kind,
status: r.status,
message: r.message,
requestedAt: r.requestedAt.toISOString(),
processedAt: r.processedAt?.toISOString() ?? null,
adminNote: r.adminNote,
})),
};
}
export async function createQuotaExtensionRequest(
userId: string,
message = "",
kind: ExtensionKind = EXTENSION_KIND.VIDEO_QUOTA,
) {
const user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
if (user.plan !== "PREMIUM") {
throw new Error("Only Pro subscribers can request quota resets or extensions.");
}
const usage = await getQuotaExtensionUsage(userId, kind);
if (usage.remaining <= 0) {
throw new Error(
`You have used all ${QUOTA_EXTENSION_ANNUAL_LIMIT} ${
kind === EXTENSION_KIND.API_RATE_LIMIT ? "API rate limit" : "quota"
} extension requests for this year.`,
);
}
const pending = await prisma.quotaExtensionRequest.findFirst({
where: { userId, kind, status: QuotaExtensionRequestStatus.PENDING },
});
if (pending) {
throw new Error("You already have a pending request. Please wait for it to be processed.");
}
const request = await prisma.quotaExtensionRequest.create({
data: {
userId,
kind,
message: message.trim().slice(0, 1000),
status: QuotaExtensionRequestStatus.PENDING,
},
});
const updatedUsage = await getQuotaExtensionUsage(userId, kind);
return {
requestId: request.id,
...updatedUsage,
};
}
/** Call when approving a request in the database (admin / support). */
export async function approveQuotaExtensionRequest(
requestId: string,
options?: { bonusQuota?: number; bonusRateLimit?: number; adminNote?: string },
) {
const request = await prisma.quotaExtensionRequest.findUniqueOrThrow({
where: { id: requestId },
include: { user: true },
});
if (request.status !== QuotaExtensionRequestStatus.PENDING) {
throw new Error("Request is not pending.");
}
if (request.kind === EXTENSION_KIND.API_RATE_LIMIT) {
const bonus = Math.min(
MAX_ADMIN_API_RATE_BONUS,
Math.max(0, Math.floor(options?.bonusRateLimit ?? 30)),
);
await prisma.$transaction([
prisma.quotaExtensionRequest.update({
where: { id: requestId },
data: {
status: QuotaExtensionRequestStatus.APPROVED,
processedAt: new Date(),
adminNote: options?.adminNote?.trim().slice(0, 500) ?? null,
},
}),
prisma.user.update({
where: { id: request.userId },
data: {
apiRateLimitBonus: request.user.apiRateLimitBonus + bonus,
},
}),
]);
return;
}
const bonus = Math.min(
MAX_ADMIN_BONUS_QUOTA,
Math.max(0, Math.floor(options?.bonusQuota ?? 0)),
);
await prisma.$transaction([
prisma.quotaExtensionRequest.update({
where: { id: requestId },
data: {
status: QuotaExtensionRequestStatus.APPROVED,
processedAt: new Date(),
adminNote: options?.adminNote?.trim().slice(0, 500) ?? null,
},
}),
prisma.user.update({
where: { id: request.userId },
data: {
videosUsed: 0,
bonusQuota: request.user.bonusQuota + bonus,
},
}),
]);
}
export async function rejectQuotaExtensionRequest(requestId: string, adminNote?: string) {
const request = await prisma.quotaExtensionRequest.findUniqueOrThrow({
where: { id: requestId },
});
if (request.status !== QuotaExtensionRequestStatus.PENDING) {
throw new Error("Request is not pending.");
}
await prisma.quotaExtensionRequest.update({
where: { id: requestId },
data: {
status: QuotaExtensionRequestStatus.REJECTED,
processedAt: new Date(),
adminNote: adminNote?.trim().slice(0, 500) ?? null,
},
});
}
export function getEffectiveQuotaLimit(plan: Parameters<typeof getPlanLimits>[0], bonusQuota: number) {
return getPlanLimits(plan).monthlyQuota + bonusQuota;
}