mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
2ef9ac7be1
Introduce an optional Kindle EPUB compatibility fixer and integrate it into the ebook organization flow. Adds a new config key (ebook_kindle_fix_enabled, default false), a settings API update, and a UI toggle (visible when preferred format is EPUB). Implements src/lib/utils/epub-fixer.ts (uses adm-zip and cheerio) to apply fixes: add UTF-8 XML declarations, remove body/#bodymatter fragments from links, validate/normalize dc:language, and remove stray <img> tags without src. organize-files.processor now detects EPUB downloads, runs the fixer (produces a temp fixed EPUB), uses the fixed file for organization, logs fixes, and cleans up temporary files; fix failures are non-blocking and the original download is preserved. Adds dependencies adm-zip and @types/adm-zip and updates documentation and types/UI to expose the new setting. Also includes helper functions to detect EPUB paths in downloads.
168 lines
7.8 KiB
TypeScript
168 lines
7.8 KiB
TypeScript
/**
|
|
* Component: Admin Settings API
|
|
* Documentation: documentation/settings-pages.md
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
|
import { prisma } from '@/lib/db';
|
|
import { RMABLogger } from '@/lib/utils/logger';
|
|
|
|
const logger = RMABLogger.create('API.Admin.Settings');
|
|
|
|
export async function GET(request: NextRequest) {
|
|
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
|
return requireAdmin(req, async () => {
|
|
try {
|
|
// Fetch all configuration
|
|
const configs = await prisma.configuration.findMany();
|
|
const configMap = new Map(configs.map((c) => [c.key, c.value]));
|
|
|
|
// Check if any local users exist (for validation)
|
|
const hasLocalUsers = (await prisma.user.count({
|
|
where: { authProvider: 'local' }
|
|
})) > 0;
|
|
|
|
// Check if any local admin users exist (for validation)
|
|
const hasLocalAdmins = (await prisma.user.count({
|
|
where: {
|
|
authProvider: 'local',
|
|
role: 'admin'
|
|
}
|
|
})) > 0;
|
|
|
|
// Mask sensitive values
|
|
const maskValue = (key: string, value: string | null | undefined) => {
|
|
const sensitiveKeys = ['token', 'api_key', 'password', 'secret'];
|
|
if (value && sensitiveKeys.some((k) => key.includes(k))) {
|
|
return '••••••••••••';
|
|
}
|
|
return value || '';
|
|
};
|
|
|
|
// Build response object
|
|
const settings = {
|
|
backendMode: configMap.get('system.backend_mode') || 'plex',
|
|
hasLocalUsers,
|
|
hasLocalAdmins,
|
|
audibleRegion: configMap.get('audible.region') || 'us',
|
|
plex: {
|
|
url: configMap.get('plex_url') || '',
|
|
token: maskValue('token', configMap.get('plex_token')),
|
|
libraryId: configMap.get('plex_audiobook_library_id') || '',
|
|
triggerScanAfterImport: configMap.get('plex.trigger_scan_after_import') === 'true',
|
|
},
|
|
audiobookshelf: {
|
|
serverUrl: configMap.get('audiobookshelf.server_url') || '',
|
|
apiToken: maskValue('api_token', configMap.get('audiobookshelf.api_token')),
|
|
libraryId: configMap.get('audiobookshelf.library_id') || '',
|
|
triggerScanAfterImport: configMap.get('audiobookshelf.trigger_scan_after_import') === 'true',
|
|
},
|
|
oidc: {
|
|
enabled: configMap.get('oidc.enabled') === 'true',
|
|
providerName: configMap.get('oidc.provider_name') || '',
|
|
issuerUrl: configMap.get('oidc.issuer_url') || '',
|
|
clientId: configMap.get('oidc.client_id') || '',
|
|
clientSecret: maskValue('client_secret', configMap.get('oidc.client_secret')),
|
|
accessControlMethod: configMap.get('oidc.access_control_method') || 'open',
|
|
accessGroupClaim: configMap.get('oidc.access_group_claim') || 'groups',
|
|
accessGroupValue: configMap.get('oidc.access_group_value') || '',
|
|
allowedEmails: configMap.get('oidc.allowed_emails') || '[]',
|
|
allowedUsernames: configMap.get('oidc.allowed_usernames') || '[]',
|
|
adminClaimEnabled: configMap.get('oidc.admin_claim_enabled') === 'true',
|
|
adminClaimName: configMap.get('oidc.admin_claim_name') || 'groups',
|
|
adminClaimValue: configMap.get('oidc.admin_claim_value') || '',
|
|
},
|
|
registration: {
|
|
enabled: configMap.get('auth.registration_enabled') === 'true',
|
|
requireAdminApproval: configMap.get('auth.require_admin_approval') === 'true',
|
|
},
|
|
prowlarr: {
|
|
url: configMap.get('prowlarr_url') || '',
|
|
apiKey: maskValue('api_key', configMap.get('prowlarr_api_key')),
|
|
},
|
|
// downloadClient is populated from multi-client format for backward compatibility
|
|
// The DownloadTab component now uses DownloadClientManagement which reads from /api/admin/settings/download-clients
|
|
downloadClient: (() => {
|
|
// Try to read from new multi-client format first
|
|
const downloadClientsJson = configMap.get('download_clients');
|
|
if (downloadClientsJson) {
|
|
try {
|
|
const clients = JSON.parse(downloadClientsJson);
|
|
// Return the first enabled client for backward compatibility
|
|
const firstClient = clients.find((c: any) => c.enabled) || clients[0];
|
|
if (firstClient) {
|
|
return {
|
|
type: firstClient.type || 'qbittorrent',
|
|
url: firstClient.url || '',
|
|
username: firstClient.username || '',
|
|
password: maskValue('password', firstClient.password),
|
|
disableSSLVerify: firstClient.disableSSLVerify === true,
|
|
seedingTimeMinutes: parseInt(configMap.get('seeding_time_minutes') || '0'),
|
|
remotePathMappingEnabled: firstClient.remotePathMappingEnabled === true,
|
|
remotePath: firstClient.remotePath || '',
|
|
localPath: firstClient.localPath || '',
|
|
};
|
|
}
|
|
} catch {
|
|
// Fall through to legacy format
|
|
}
|
|
}
|
|
// Fall back to legacy flat keys
|
|
return {
|
|
type: configMap.get('download_client_type') || 'qbittorrent',
|
|
url: configMap.get('download_client_url') || '',
|
|
username: configMap.get('download_client_username') || '',
|
|
password: maskValue('password', configMap.get('download_client_password')),
|
|
disableSSLVerify: configMap.get('download_client_disable_ssl_verify') === 'true',
|
|
seedingTimeMinutes: parseInt(configMap.get('seeding_time_minutes') || '0'),
|
|
remotePathMappingEnabled: configMap.get('download_client_remote_path_mapping_enabled') === 'true',
|
|
remotePath: configMap.get('download_client_remote_path') || '',
|
|
localPath: configMap.get('download_client_local_path') || '',
|
|
};
|
|
})(),
|
|
paths: {
|
|
downloadDir: configMap.get('download_dir') || '/downloads',
|
|
mediaDir: configMap.get('media_dir') || '/media/audiobooks',
|
|
audiobookPathTemplate: configMap.get('audiobook_path_template') || '{author}/{title} {asin}',
|
|
metadataTaggingEnabled: configMap.get('metadata_tagging_enabled') === 'true',
|
|
chapterMergingEnabled: configMap.get('chapter_merging_enabled') === 'true',
|
|
},
|
|
ebook: {
|
|
// New granular source toggles (with migration from legacy ebook_sidecar_enabled)
|
|
annasArchiveEnabled: configMap.get('ebook_annas_archive_enabled') === 'true' ||
|
|
// Migration: if old key is true and new key doesn't exist, use old value
|
|
(configMap.get('ebook_annas_archive_enabled') === undefined && configMap.get('ebook_sidecar_enabled') === 'true'),
|
|
indexerSearchEnabled: configMap.get('ebook_indexer_search_enabled') === 'true',
|
|
// Anna's Archive specific settings
|
|
baseUrl: configMap.get('ebook_sidecar_base_url') || 'https://annas-archive.li',
|
|
flaresolverrUrl: configMap.get('ebook_sidecar_flaresolverr_url') || '',
|
|
// General settings
|
|
preferredFormat: configMap.get('ebook_sidecar_preferred_format') || 'epub',
|
|
// Auto-grab: default true to preserve existing behavior
|
|
autoGrabEnabled: configMap.get('ebook_auto_grab_enabled') !== 'false',
|
|
// Kindle compatibility fixes: default false
|
|
kindleFixEnabled: configMap.get('ebook_kindle_fix_enabled') === 'true',
|
|
},
|
|
general: {
|
|
appName: configMap.get('app_name') || 'ReadMeABook',
|
|
allowRegistrations: configMap.get('allow_registrations') === 'true',
|
|
maxConcurrentDownloads: parseInt(
|
|
configMap.get('max_concurrent_downloads') || '3'
|
|
),
|
|
autoApproveRequests: configMap.get('auto_approve_requests') === 'true',
|
|
},
|
|
};
|
|
|
|
return NextResponse.json(settings);
|
|
} catch (error) {
|
|
logger.error('Failed to fetch settings', { error: error instanceof Error ? error.message : String(error) });
|
|
return NextResponse.json(
|
|
{ error: 'Failed to fetch settings' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|