Add admin Bulk Import feature

Introduce a Bulk Import feature for admins to scan server folders, match discovered audiobook folders against Audible, review matches, and queue batch imports.

What changed:
- Added documentation: documentation/features/bulk-import.md and TABLEOFCONTENTS update.
- Backend: SSE scan endpoint (POST /api/admin/bulk-import/scan) streams discovery and matching events; execute endpoint (POST /api/admin/bulk-import/execute) validates paths, creates/resolves audiobook & request records, and queues organize_files jobs. Both endpoints enforce admin-only access and validate allowed root directories (download_dir, media_dir, /bookdrop).
- Frontend: Modal wizard and steps for folder selection, scan progress, and match review (BulkImportWizard + ScanFolderStep, ScanProgressStep, MatchReviewStep + shared types).
- Utilities: bulk-import-scanner for folder discovery and ffprobe metadata extraction; shared types for scanned books/events.
- UI: Added Bulk Import quick action to admin dashboard (src/app/admin/page.tsx).

Key details:
- Audible searches are rate-limited (≈1.5s) and matching results include library/request status checks.
- Reuses existing organize_files job queue and manual-import pipeline; no new database tables introduced (state is ephemeral during the wizard).
- Includes error handling, path normalization, and security checks for allowed directories.

This commit wires frontend, backend, and docs together to provide an admin-only multi-step bulk import workflow.
This commit is contained in:
kikootwo
2026-03-13 12:03:21 -04:00
parent 6fc622c4e7
commit c8bfcdb611
11 changed files with 2275 additions and 1 deletions
+34 -1
View File
@@ -14,6 +14,7 @@ import { RecentRequestsTable } from './components/RecentRequestsTable';
import { ToastProvider, useToast } from '@/components/ui/Toast';
import { ReportedIssuesSection } from './components/ReportedIssuesSection';
import { InteractiveTorrentSearchModal } from '@/components/requests/InteractiveTorrentSearchModal';
import { BulkImportWizard } from '@/components/admin/BulkImportWizard';
import { TorrentResult } from '@/lib/utils/ranking-algorithm';
import { formatDistanceToNow } from 'date-fns';
import { useState } from 'react';
@@ -379,6 +380,8 @@ function PendingApprovalSection({ requests }: { requests: PendingApprovalRequest
}
function AdminDashboardContent() {
const [isBulkImportOpen, setIsBulkImportOpen] = useState(false);
// Fetch data with auto-refresh every 10 seconds
const { data: metrics, error: metricsError } = useSWR(
'/api/admin/metrics',
@@ -572,7 +575,7 @@ function AdminDashboardContent() {
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
<Link
href="/admin/settings"
className="block p-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:shadow-md transition-all"
@@ -657,8 +660,38 @@ function AdminDashboardContent() {
</span>
</div>
</Link>
<button
onClick={() => setIsBulkImportOpen(true)}
className="block p-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-left"
>
<div className="flex items-center gap-3">
<svg
className="w-6 h-6 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<span className="font-medium text-gray-900 dark:text-gray-100">
Bulk Import
</span>
</div>
</button>
</div>
{/* Bulk Import Wizard Modal */}
<BulkImportWizard
isOpen={isBulkImportOpen}
onClose={() => setIsBulkImportOpen(false)}
/>
{/* Requests Awaiting Approval */}
{pendingApprovalData?.requests && pendingApprovalData.requests.length > 0 && (
<PendingApprovalSection requests={pendingApprovalData.requests} />