import { useState, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowLeft, Download, Settings, RefreshCw, CheckCircle, XCircle, AlertCircle, Users, Film, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { importFromXBVR, XBVRConfig, ImportProgress } from '@/lib/xbvrImporter'; import { importFromStashAPP, StashAPPConfig, updateActorsFromStashAPP } from '@/lib/stashappImporter'; import { importFromPlaynite, PlayniteConfig } from '@/lib/playniteImporter'; import { importFromJellyfin, cleanupJellyfinMedia, JellyfinConfig, JellyfinImportOptions, LibraryMapping, fetchJellyfinLibraries } from '@/lib/jellyfinImporter'; import { fetchSettings, updateSettings } from '@/api'; const BASE_URL = import.meta.env.VITE_BASE_URL || 'http://localhost:3000'; export default function ImporterView() { const navigate = useNavigate(); const [xbvrConfig, setXbvrConfig] = useState({ url: import.meta.env.VITE_XBVR_URL || BASE_URL, updateExisting: true }); const [stashappConfig, setStashappConfig] = useState({ url: import.meta.env.VITE_STASHAPP_URL || '', apiKey: import.meta.env.VITE_STASHAPP_API_KEY || '', updateExisting: true }); const [playniteConfig, setPlayniteConfig] = useState({ ip: import.meta.env.VITE_PLAYNITE_IP || '', apiToken: import.meta.env.VITE_PLAYNITE_API_TOKEN || '', port: import.meta.env.VITE_PLAYNITE_PORT ? parseInt(import.meta.env.VITE_PLAYNITE_PORT) : undefined, updateExisting: true }); const [jellyfinConfig, setJellyfinConfig] = useState({ url: import.meta.env.VITE_JELLYFIN_URL || '', apiKey: import.meta.env.VITE_JELLYFIN_API_KEY || '' }); const [jellyfinOptions, setJellyfinOptions] = useState({ importMovies: true, importSeries: true, importMusic: true, importCast: true, limit: undefined, libraryMappings: [], updateExisting: true }); const [jellyfinLibraries, setJellyfinLibraries] = useState>([]); const [libraryMappings, setLibraryMappings] = useState([]); const [showLibraryMapping, setShowLibraryMapping] = useState(false); const [isInitialLoad, setIsInitialLoad] = useState(true); // Load library mappings from API on mount useEffect(() => { const loadMappings = async () => { try { const settings = await fetchSettings(); if (settings?.jellyfinLibraryMappings) { const mappings = JSON.parse(settings.jellyfinLibraryMappings); setLibraryMappings(mappings); setShowLibraryMapping(true); } } catch (error) { console.error('Failed to load library mappings from API:', error); // Fallback to localStorage const savedMappings = localStorage.getItem('jellyfinLibraryMappings'); if (savedMappings) { try { setLibraryMappings(JSON.parse(savedMappings)); setShowLibraryMapping(true); } catch (error) { console.error('Failed to parse saved library mappings:', error); } } } setIsInitialLoad(false); }; loadMappings(); }, []); // Save library mappings to API and localStorage when they change useEffect(() => { if (libraryMappings.length > 0 && !isInitialLoad) { // Save to localStorage as fallback localStorage.setItem('jellyfinLibraryMappings', JSON.stringify(libraryMappings)); // Save to API const saveMappings = async () => { try { const settings = await fetchSettings(); if (settings) { settings.jellyfinLibraryMappings = JSON.stringify(libraryMappings); await updateSettings(settings); } } catch (error) { console.error('Failed to save library mappings to API:', error); } }; saveMappings(); } }, [libraryMappings, isInitialLoad]); const [progress, setProgress] = useState({ current: 0, total: 0, stage: 'idle', message: '', videosImported: 0, actorsImported: 0, errors: [] }); const [importLog, setImportLog] = useState([]); const logContainerRef = useRef(null); // Auto-scroll to bottom when log updates useEffect(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [importLog]); const addLog = (message: string) => { const timestamp = new Date().toLocaleTimeString(); setImportLog(prev => [...prev, `[${timestamp}] ${message}`]); }; const handleXBVRImport = async () => { setProgress({ current: 0, total: 0, stage: 'fetching', message: 'Connecting to DeoVR API...', videosImported: 0, actorsImported: 0, errors: [] }); setImportLog([]); const result = await importFromXBVR( xbvrConfig, addLog, (progressUpdate) => { setProgress(prev => ({ ...prev, ...progressUpdate })); } ); setProgress(result); }; const handleStashAPPImport = async () => { setProgress({ current: 0, total: 0, stage: 'fetching', message: 'Connecting to StashAPP...', videosImported: 0, actorsImported: 0, errors: [] }); setImportLog([]); const result = await importFromStashAPP( stashappConfig, addLog, (progressUpdate) => { setProgress(prev => ({ ...prev, ...progressUpdate })); } ); setProgress(result); }; const handleStashAPPActorUpdate = async () => { setProgress({ current: 0, total: 0, stage: 'fetching', message: 'Connecting to StashAPP...', videosImported: 0, actorsImported: 0, errors: [] }); setImportLog([]); const result = await updateActorsFromStashAPP( stashappConfig, addLog, (progressUpdate) => { setProgress(prev => ({ ...prev, ...progressUpdate })); } ); setProgress(result); }; const handlePlayniteImport = async () => { setProgress({ current: 0, total: 0, stage: 'fetching', message: 'Connecting to Playnite API...', videosImported: 0, actorsImported: 0, errors: [] }); setImportLog([]); const result = await importFromPlaynite( playniteConfig, addLog, (progressUpdate) => { setProgress(prev => ({ ...prev, ...progressUpdate })); } ); setProgress(result); }; const handleJellyfinImport = async () => { setProgress({ current: 0, total: 0, stage: 'fetching', message: 'Connecting to Jellyfin API...', videosImported: 0, actorsImported: 0, errors: [] }); setImportLog([]); // Update options with current library mappings const optionsWithMappings = { ...jellyfinOptions, libraryMappings: libraryMappings }; const result = await importFromJellyfin( jellyfinConfig, optionsWithMappings, addLog, (progressUpdate) => { setProgress(prev => ({ ...prev, ...progressUpdate })); } ); setProgress(result); }; const handleJellyfinCleanup = async () => { setProgress({ current: 0, total: 0, stage: 'fetching', message: 'Connecting to Jellyfin API for cleanup...', videosImported: 0, actorsImported: 0, errors: [] }); setImportLog([]); const result = await cleanupJellyfinMedia( jellyfinConfig, jellyfinOptions, addLog, (progressUpdate) => { setProgress(prev => ({ ...prev, ...progressUpdate })); } ); setProgress(result); }; const handleFetchJellyfinLibraries = async () => { try { const libraries = await fetchJellyfinLibraries(jellyfinConfig); setJellyfinLibraries(libraries); // Merge existing mappings with new libraries const newMappings: LibraryMapping[] = libraries.map(lib => { // Check if mapping already exists const existing = libraryMappings.find(m => m.libraryName === lib.Name); if (existing) { return existing; } // Create new mapping with default category let defaultCategory: 'TV Series' | 'Anime' | 'Movies' | 'Music' = 'TV Series'; if (lib.CollectionType === 'movies') { defaultCategory = 'Movies'; } else if (lib.CollectionType === 'music') { defaultCategory = 'Music'; } else if (lib.CollectionType === 'tvshows') { defaultCategory = 'TV Series'; } return { libraryName: lib.Name, category: defaultCategory }; }); setLibraryMappings(newMappings); setShowLibraryMapping(true); addLog(`Fetched ${libraries.length} libraries from Jellyfin`); } catch (error) { addLog(`Failed to fetch libraries: ${error}`); } }; const handleLibraryMappingChange = (libraryName: string, category: 'TV Series' | 'Anime' | 'Movies' | 'Music' | 'skip') => { setLibraryMappings(prev => { const existing = prev.find(m => m.libraryName === libraryName); if (existing) { return prev.map(m => m.libraryName === libraryName ? { ...m, category } : m); } else { return [...prev, { libraryName, category }]; } }); }; const handleLibraryPathSegmentsChange = (libraryName: string, value: string) => { const segments = value.split(',').map(s => s.trim()).filter(s => s.length > 0); setLibraryMappings(prev => { const existing = prev.find(m => m.libraryName === libraryName); if (existing) { return prev.map(m => m.libraryName === libraryName ? { ...m, pathSegments: segments } : m); } else { return [...prev, { libraryName, category: 'TV Series', pathSegments: segments }]; } }); }; const resetImport = () => { setProgress({ current: 0, total: 0, stage: 'idle', message: '', videosImported: 0, actorsImported: 0, errors: [] }); setImportLog([]); }; const getProgressPercentage = () => { if (progress.total === 0) return 0; return Math.round((progress.current / progress.total) * 100); }; return (
{/* Header */}

Media Importers

Import media from external platforms

{/* Importer Cards */}
{/* XBVR Importer Card */} {xbvrConfig.url && (

XBVR

Adult Video Manager

Import adult videos and actors from your XBVR database.

setXbvrConfig({ ...xbvrConfig, url: e.target.value })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="http://192.168.1.102:10001" />
setXbvrConfig({ ...xbvrConfig, updateExisting: e.target.checked })} disabled={progress.stage !== 'idle'} className="rounded border-border" />
)} {/* StashAPP Importer Card */} {stashappConfig.url && (

StashAPP

Adult Content Manager

Import adult videos and performers from your StashAPP database.

setStashappConfig({ ...stashappConfig, url: e.target.value })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="http://192.168.1.102:10001" />
setStashappConfig({ ...stashappConfig, apiKey: e.target.value })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="Enter API key if required" />
setStashappConfig({ ...stashappConfig, updateExisting: e.target.checked })} disabled={progress.stage !== 'idle'} className="rounded border-border" />
)} {/* StashAPP Actor Updater Card */} {stashappConfig.url && (

StashAPP Actor Updater

Update existing actors

Update existing actors with fresh data from StashAPP and create missing ones.

setStashappConfig({ ...stashappConfig, apiKey: e.target.value })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="Enter API key if required" />
)} {/* Playnite Importer Card */} {playniteConfig.ip && playniteConfig.apiToken && (

Playnite

Game Library Manager

Import games from your Playnite library via Bridge API.

setPlayniteConfig({ ...playniteConfig, ip: e.target.value })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="localhost" />
setPlayniteConfig({ ...playniteConfig, port: parseInt(e.target.value) || 19821 })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="19821" />
setPlayniteConfig({ ...playniteConfig, apiToken: e.target.value })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="pb_your_token_here" />
setPlayniteConfig({ ...playniteConfig, updateExisting: e.target.checked })} disabled={progress.stage !== 'idle'} className="rounded border-border" />
)} {/* Jellyfin Importer Card */} {jellyfinConfig.url && (

Jellyfin

Media Server

Import movies, series, music and cast from your Jellyfin server.

setJellyfinConfig({ ...jellyfinConfig, url: e.target.value })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="http://192.168.1.102:8096" />
setJellyfinConfig({ ...jellyfinConfig, apiKey: e.target.value })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="Enter API key" />
setJellyfinOptions({ ...jellyfinOptions, limit: e.target.value ? parseInt(e.target.value) : undefined })} disabled={progress.stage !== 'idle'} className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed" placeholder="e.g. 10" />
setJellyfinOptions({ ...jellyfinOptions, updateExisting: e.target.checked })} disabled={progress.stage !== 'idle'} className="rounded border-border" />
{showLibraryMapping && libraryMappings.length > 0 && (
{libraryMappings.map(mapping => (
{mapping.libraryName}
Pfad-Segmente (kommagetrennt): handleLibraryPathSegmentsChange(mapping.libraryName, e.target.value)} disabled={progress.stage !== 'idle'} placeholder="z.B. Serien, Animes" className="text-xs px-2 py-1 border border-border rounded-lg focus:ring-2 focus:ring-[#6d28d9] focus:border-transparent outline-none disabled:bg-muted disabled:cursor-not-allowed flex-1" />
))}
)}
)} {/* Jellyfin Cleanup Card */} {jellyfinConfig.url && (

Jellyfin Cleanup

Remove deleted media

Remove Jellyfin media and cast that no longer exist in your Jellyfin server.

)}
{/* Progress Section */} {progress.stage !== 'idle' && (
{progress.stage === 'complete' ? (
) : progress.stage === 'error' ? (
) : (
)}

{progress.message}

{progress.stage === 'fetching' && 'Connecting to external service...'} {progress.stage === 'importing' && `Processing items... ${getProgressPercentage()}%`} {progress.stage === 'complete' && 'Import finished'} {progress.stage === 'error' && 'An error occurred'}

{progress.stage === 'complete' || progress.stage === 'error' ? ( ) : null}
{/* Progress Bar */} {progress.stage === 'fetching' || progress.stage === 'importing' ? (
{progress.current} / {progress.total} items {getProgressPercentage()}%
) : null} {/* Stats */}
{(progress as any).gamesImported !== undefined ? 'Games' : (progress as any).moviesImported !== undefined ? 'Movies' : (progress as any).seriesImported !== undefined ? 'Series' : (progress as any).musicImported !== undefined ? 'Music' : 'Videos'}

{(progress as any).gamesImported !== undefined ? (progress as any).gamesImported : (progress as any).moviesImported !== undefined ? (progress as any).moviesImported : (progress as any).seriesImported !== undefined ? (progress as any).seriesImported : (progress as any).musicImported !== undefined ? (progress as any).musicImported : progress.videosImported}

{(progress as any).castImported !== undefined ? 'Cast' : 'Actors'}

{(progress as any).castImported !== undefined ? (progress as any).castImported : progress.actorsImported}

Errors

{progress.errors.length}

{/* Log */} {importLog.length > 0 && (
                {importLog.join('\n')}
              
)} {/* Errors */} {progress.errors.length > 0 && (

Errors

{progress.errors.map((error, index) => (

• {error}

))}
)}
)}
); }