Add series fields to audiobooks and update related logic

Introduces 'series' and 'seriesPart' fields to the Audiobook model and database schema. Updates API routes, file organization, and path template utilities to support series metadata. Enhances chapter merging logic, improves notification backend testing, and expands test coverage for admin and API routes.
This commit is contained in:
kikootwo
2026-01-22 15:56:55 -05:00
parent dc7e557694
commit 31bca0052f
105 changed files with 10384 additions and 75 deletions
+78 -6
View File
@@ -8,12 +8,29 @@ import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middlewar
import { getNotificationService, NotificationBackendType, NotificationPayload } from '@/lib/services/notification.service';
import { RMABLogger } from '@/lib/utils/logger';
import { z } from 'zod';
import { prisma } from '@/lib/db';
const logger = RMABLogger.create('API.Admin.Notifications.Test');
const TestNotificationSchema = z.object({
type: z.enum(['discord', 'pushover', 'email', 'slack', 'telegram', 'webhook']),
config: z.record(z.any()),
const TestNotificationSchema = z.discriminatedUnion('mode', [
// Test existing backend by ID (uses stored config)
z.object({
mode: z.literal('backend'),
backendId: z.string(),
}),
// Test new config before saving
z.object({
mode: z.literal('config'),
type: z.enum(['discord', 'pushover', 'email', 'slack', 'telegram', 'webhook']),
config: z.record(z.any()),
}),
]);
// Support legacy format without mode
const LegacyTestNotificationSchema = z.object({
backendId: z.string().optional(),
type: z.enum(['discord', 'pushover', 'email', 'slack', 'telegram', 'webhook']).optional(),
config: z.record(z.any()).optional(),
});
/**
@@ -25,12 +42,67 @@ export async function POST(request: NextRequest) {
return requireAdmin(req, async () => {
try {
const body = await request.json();
const { type, config } = TestNotificationSchema.parse(body);
// Support legacy format for backward compatibility
const legacyParsed = LegacyTestNotificationSchema.safeParse(body);
let type: NotificationBackendType;
let encryptedConfig: any;
const notificationService = getNotificationService();
// Encrypt config values
const encryptedConfig = notificationService.encryptConfig(type, config);
if (legacyParsed.success) {
// Legacy format
if (legacyParsed.data.backendId) {
// Test existing backend
const backend = await prisma.notificationBackend.findUnique({
where: { id: legacyParsed.data.backendId },
});
if (!backend) {
return NextResponse.json(
{ error: 'NotFound', message: 'Backend not found' },
{ status: 404 }
);
}
type = backend.type as NotificationBackendType;
encryptedConfig = backend.config; // Already encrypted in DB
} else if (legacyParsed.data.type && legacyParsed.data.config) {
// Test new config
type = legacyParsed.data.type as NotificationBackendType;
encryptedConfig = notificationService.encryptConfig(type, legacyParsed.data.config);
} else {
return NextResponse.json(
{ error: 'ValidationError', message: 'Must provide either backendId or type+config' },
{ status: 400 }
);
}
} else {
// New format with discriminated union
const parsed = TestNotificationSchema.parse(body);
if (parsed.mode === 'backend') {
// Test existing backend
const backend = await prisma.notificationBackend.findUnique({
where: { id: parsed.backendId },
});
if (!backend) {
return NextResponse.json(
{ error: 'NotFound', message: 'Backend not found' },
{ status: 404 }
);
}
type = backend.type as NotificationBackendType;
encryptedConfig = backend.config; // Already encrypted in DB
} else {
// Test new config
type = parsed.type;
encryptedConfig = notificationService.encryptConfig(type, parsed.config);
}
}
// Create test payload
const testPayload: NotificationPayload = {