feat: enhance scanning functionality with library selection

- Updated the scan API to allow scanning of specific libraries or all libraries based on user input.
- Enhanced the SettingsPage component to support selection of multiple libraries for scanning.
- Added visual feedback for scanning progress and status updates for individual libraries.
- Introduced new functions for scanning selected libraries and specific libraries, improving user experience and control over the scanning process.
This commit is contained in:
tigeren 2025-08-27 15:39:31 +00:00
parent 6fe6a43cf0
commit dab3ec5f84
4 changed files with 225 additions and 33 deletions

BIN
media.db

Binary file not shown.

View File

@ -1,11 +1,21 @@
import { NextResponse } from "next/server";
import { scanAllLibraries } from "@/lib/scanner";
import { scanAllLibraries, scanSelectedLibrary } from "@/lib/scanner";
export async function POST() {
export async function POST(request: Request) {
try {
const body = await request.json();
const { libraryId } = body;
if (libraryId) {
// Scan specific library
await scanSelectedLibrary(libraryId);
return NextResponse.json({ message: `Library ${libraryId} scan complete` });
} else {
// Scan all libraries
await scanAllLibraries();
return NextResponse.json({ message: "Scan complete" });
return NextResponse.json({ message: "All libraries scan complete" });
}
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}

View File

@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Header } from "@/components/ui/header";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Trash2, Plus, Folder, HardDrive } from "lucide-react";
import { Trash2, Plus, Folder, HardDrive, Scan, CheckSquare, Square, RefreshCw } from "lucide-react";
interface Library {
id: number;
@ -20,6 +20,8 @@ const SettingsPage = () => {
const [newLibraryPath, setNewLibraryPath] = useState("");
const [error, setError] = useState<string | null>(null);
const [scanStatus, setScanStatus] = useState<string>("");
const [selectedLibraries, setSelectedLibraries] = useState<number[]>([]);
const [scanProgress, setScanProgress] = useState<Record<number, boolean>>({});
useEffect(() => {
fetchLibraries();
@ -82,17 +84,37 @@ const SettingsPage = () => {
const [isScanning, setIsScanning] = useState(false);
const toggleLibrarySelection = (libraryId: number) => {
setSelectedLibraries(prev =>
prev.includes(libraryId)
? prev.filter(id => id !== libraryId)
: [...prev, libraryId]
);
};
const selectAllLibraries = () => {
if (selectedLibraries.length === libraries.length) {
setSelectedLibraries([]);
} else {
setSelectedLibraries(libraries.map(lib => lib.id));
}
};
const scanLibraries = async () => {
setIsScanning(true);
setScanStatus("Scanning libraries...");
setScanStatus("Scanning all libraries...");
try {
const res = await fetch("/api/scan", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
if (res.ok) {
setScanStatus("Scan completed successfully!");
setScanStatus("All libraries scan completed successfully!");
setTimeout(() => setScanStatus(""), 3000);
} else {
setScanStatus("Scan failed. Please try again.");
@ -104,6 +126,80 @@ const SettingsPage = () => {
}
};
const scanSelectedLibraries = async () => {
if (selectedLibraries.length === 0) return;
setIsScanning(true);
const libraryMap = new Map(libraries.map(lib => [lib.id, lib.path]));
try {
setScanStatus(`Scanning ${selectedLibraries.length} selected library${selectedLibraries.length > 1 ? 'ies' : ''}...`);
// Set all selected libraries as scanning
const progressMap: Record<number, boolean> = {};
selectedLibraries.forEach(id => progressMap[id] = true);
setScanProgress(progressMap);
// Scan each library sequentially to avoid overwhelming the server
for (const libraryId of selectedLibraries) {
const res = await fetch("/api/scan", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ libraryId }),
});
// Update progress for this library
setScanProgress(prev => ({ ...prev, [libraryId]: false }));
if (!res.ok) {
const data = await res.json();
setScanStatus(`Error scanning library ${libraryMap.get(libraryId)}: ${data.error || 'Unknown error'}`);
break;
}
}
setScanStatus(`${selectedLibraries.length} library${selectedLibraries.length > 1 ? 'ies' : ''} scan completed successfully!`);
setSelectedLibraries([]); // Clear selection after scan
setTimeout(() => setScanStatus(""), 3000);
} catch (error) {
setScanStatus("Network error during scan");
} finally {
setIsScanning(false);
setScanProgress({});
}
};
const scanSpecificLibrary = async (libraryId: number, libraryPath: string) => {
setIsScanning(true);
setScanProgress(prev => ({ ...prev, [libraryId]: true }));
setScanStatus(`Scanning library: ${libraryPath}...`);
try {
const res = await fetch("/api/scan", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ libraryId }),
});
if (res.ok) {
setScanStatus(`Library "${libraryPath}" scan completed successfully!`);
setTimeout(() => setScanStatus(""), 3000);
} else {
const data = await res.json();
setScanStatus(`Scan failed: ${data.error || 'Please try again.'}`);
}
} catch (error) {
setScanStatus("Network error during scan");
} finally {
setIsScanning(false);
setScanProgress(prev => ({ ...prev, [libraryId]: false }));
}
};
const getTotalStorage = () => {
return libraries.reduce((total, lib) => {
// Rough estimation based on path length
@ -167,28 +263,75 @@ const SettingsPage = () => {
{libraries.length > 0 && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-zinc-400 uppercase tracking-wider">
{libraries.length} {libraries.length === 1 ? 'Library' : 'Libraries'}
</h3>
{libraries.length > 1 && (
<button
onClick={selectAllLibraries}
className="text-xs text-zinc-400 hover:text-white transition-colors flex items-center gap-1"
>
{selectedLibraries.length === libraries.length ? (
<>
<CheckSquare size={14} />
Deselect All
</>
) : (
<>
<Square size={14} />
Select All
</>
)}
</button>
)}
</div>
<div className="space-y-2">
{libraries.map((lib) => (
<div key={lib.id} className="flex items-center justify-between p-4 bg-zinc-800 rounded-lg border border-zinc-700 group hover:border-zinc-600 transition-all">
<div className="flex items-center gap-3 flex-1 min-w-0">
<button
onClick={() => toggleLibrarySelection(lib.id)}
className="text-zinc-400 hover:text-white transition-colors"
>
{selectedLibraries.includes(lib.id) ? (
<CheckSquare size={16} className="text-green-400" />
) : (
<Square size={16} />
)}
</button>
<div className="w-8 h-8 bg-zinc-700 rounded-lg flex items-center justify-center">
<HardDrive className="h-4 w-4 text-zinc-300" />
</div>
<div className="min-w-0">
<p className="text-sm font-mono text-zinc-100 truncate">{lib.path}</p>
<p className="text-xs text-zinc-500">Ready to scan</p>
<p className="text-xs text-zinc-500">
{scanProgress[lib.id] ? 'Scanning...' : 'Ready to scan'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => scanSpecificLibrary(lib.id, lib.path)}
disabled={isScanning}
className="p-2 text-zinc-400 hover:text-green-400 hover:bg-green-900/20 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
title={`Scan ${lib.path}`}
>
{scanProgress[lib.id] ? (
<RefreshCw size={16} className="animate-spin" />
) : (
<Scan size={16} />
)}
</button>
<button
onClick={() => deleteLibrary(lib.id)}
className="p-2 text-zinc-400 hover:text-red-500 hover:bg-red-900/20 rounded-lg transition-all"
title="Delete library"
>
<Trash2 size={16} />
</button>
</div>
</div>
))}
</div>
</div>
@ -215,15 +358,41 @@ const SettingsPage = () => {
</div>
<div>
<h2 className="text-xl font-bold text-white">Media Scanner</h2>
<p className="text-sm text-zinc-400">Discover new media files</p>
<p className="text-sm text-zinc-400">Discover new media files in your libraries</p>
</div>
</div>
<div className="space-y-4">
{libraries.length > 0 && (
<div className="space-y-3">
{selectedLibraries.length > 0 && (
<button
onClick={scanSelectedLibraries}
disabled={isScanning}
className="w-full px-4 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white font-medium rounded-xl hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isScanning ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
Scanning {selectedLibraries.length} library{selectedLibraries.length > 1 ? 'ies' : ''}...
</>
) : (
<>
<Scan size={16} />
Scan {selectedLibraries.length} Selected Library{selectedLibraries.length > 1 ? 'ies' : ''}
</>
)}
</button>
)}
<button
onClick={scanLibraries}
disabled={isScanning || libraries.length === 0}
className="w-full px-4 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white font-medium rounded-xl hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
disabled={isScanning}
className={`w-full px-4 py-3 font-medium rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 ${
selectedLibraries.length > 0
? 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'
: 'bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:shadow-lg'
}`}
>
{isScanning ? (
<>
@ -231,9 +400,14 @@ const SettingsPage = () => {
Scanning...
</>
) : (
<>Scan Libraries</>
<>
<Scan size={16} />
Scan All Libraries
</>
)}
</button>
</div>
)}
{scanStatus && (
<div className={`p-3 rounded-lg ${scanStatus.includes('success') ? 'bg-green-900/20 border border-green-800' : 'bg-zinc-800 border border-zinc-700'}`}>
@ -246,7 +420,7 @@ const SettingsPage = () => {
<p className="text-sm text-zinc-500 text-center">
{libraries.length === 0
? "Add at least one library to enable scanning"
: "Scan will discover new videos and photos"}
: `Select libraries above or use individual scan buttons`}
</p>
</div>
</div>

View File

@ -115,3 +115,11 @@ export const scanAllLibraries = async () => {
await scanLibrary(library);
}
};
export const scanSelectedLibrary = async (libraryId: number) => {
const library = db.prepare("SELECT * FROM libraries WHERE id = ?").get(libraryId) as { id: number; path: string } | undefined;
if (!library) {
throw new Error(`Library with ID ${libraryId} not found`);
}
await scanLibrary(library);
};