import { useEffect, useState } from 'react' import { Film, Tv, RefreshCw, Trash2, Plus, Check, X, ChevronDown, ChevronUp, Loader, Pencil } from 'lucide-react' import api from '../lib/api' interface JellyfinConfig { id: number name: string url: string api_key: string is_active: boolean movies_library_id: string | null series_library_id: string | null movies_category_name: string series_category_name: string last_sync_at: string | null item_count: number } interface Library { id: string name: string type: string } interface SyncStatus { status: 'idle' | 'running' | 'done' | 'error' message: string synced: number } const EMPTY_FORM = { name: '', url: 'http://10.10.10.15:8096', api_key: 'c339ccd3b7234c9a874bcfb70e92364a', movies_category_name: 'Películas KiraTV', series_category_name: 'Series KiraTV', movies_library_id: '', series_library_id: '', } export default function Jellyfin() { const [configs, setConfigs] = useState([]) const [loading, setLoading] = useState(true) const [showForm, setShowForm] = useState(false) const [editId, setEditId] = useState(null) const [form, setForm] = useState({ ...EMPTY_FORM }) const [saving, setSaving] = useState(false) const [testResult, setTestResult] = useState<{ ok: boolean; server_name?: string; error?: string } | null>(null) const [testing, setTesting] = useState(false) const [libraries, setLibraries] = useState([]) const [libsLoading, setLibsLoading] = useState(false) const [syncStatus, setSyncStatus] = useState>({}) const [expanded, setExpanded] = useState(null) useEffect(() => { load() }, []) async function load() { setLoading(true) try { const { data } = await api.get('/jellyfin') setConfigs(data) } finally { setLoading(false) } } function openCreate() { setEditId(null) setForm({ ...EMPTY_FORM }) setTestResult(null) setLibraries([]) setShowForm(true) } function openEdit(cfg: JellyfinConfig) { setEditId(cfg.id) setForm({ name: cfg.name, url: cfg.url, api_key: cfg.api_key, movies_category_name: cfg.movies_category_name, series_category_name: cfg.series_category_name, movies_library_id: cfg.movies_library_id ?? '', series_library_id: cfg.series_library_id ?? '', }) setTestResult(null) setLibraries([]) setShowForm(true) } async function testConnection() { if (!form.url || !form.api_key) return setTesting(true) setTestResult(null) try { // Test via temporary save-less call using an existing config if editId, else test inline if (editId) { const { data } = await api.post(`/jellyfin/${editId}/test`) setTestResult(data) } else { // Create temp config, test, then delete const { data: created } = await api.post('/jellyfin', { name: '__test__', url: form.url, api_key: form.api_key, movies_category_name: form.movies_category_name, series_category_name: form.series_category_name, }) const { data: result } = await api.post(`/jellyfin/${created.id}/test`) await api.delete(`/jellyfin/${created.id}`) setTestResult(result) } } catch { setTestResult({ ok: false, error: 'Error de red' }) } finally { setTesting(false) } } async function loadLibraries() { if (!editId) { // Need to save first await save() return } setLibsLoading(true) try { const { data } = await api.get(`/jellyfin/${editId}/libraries`) setLibraries(data) } catch { setLibraries([]) } finally { setLibsLoading(false) } } async function save() { setSaving(true) try { const payload = { ...form, movies_library_id: form.movies_library_id || null, series_library_id: form.series_library_id || null, } if (editId) { await api.put(`/jellyfin/${editId}`, payload) } else { const { data } = await api.post('/jellyfin', payload) setEditId(data.id) } await load() } finally { setSaving(false) } } async function deleteCfg(id: number) { if (!confirm('¿Eliminar esta configuración de Jellyfin? Se borrarán todos los canales sincronizados.')) return await api.delete(`/jellyfin/${id}`) setConfigs(prev => prev.filter(c => c.id !== id)) if (editId === id) setShowForm(false) } async function startSync(id: number) { setSyncStatus(prev => ({ ...prev, [id]: { status: 'running', message: 'Iniciando…', synced: 0 } })) await api.post(`/jellyfin/${id}/sync`) pollSync(id) } function pollSync(id: number) { const iv = setInterval(async () => { try { const { data } = await api.get(`/jellyfin/${id}/sync-status`) setSyncStatus(prev => ({ ...prev, [id]: data })) if (data.status !== 'running') { clearInterval(iv) if (data.status === 'done') load() } } catch { clearInterval(iv) } }, 1500) } const movieLibs = libraries.filter(l => l.type === 'movies' || l.type === '') const tvLibs = libraries.filter(l => l.type === 'tvshows' || l.type === '') return (

Biblioteca Jellyfin

Integra tu catálogo local de Jellyfin como VOD y Series en KiraStream

{/* Config list */} {loading ? (
) : configs.length === 0 ? (

No hay conexiones Jellyfin configuradas

) : (
{configs.map(cfg => { const ss = syncStatus[cfg.id] return (
{cfg.name} {cfg.is_active ? 'activo' : 'inactivo'}
{cfg.url}
{cfg.item_count} elementos {cfg.last_sync_at && ` · Última sync: ${new Date(cfg.last_sync_at).toLocaleString('es-ES')}`}
{ss?.status === 'running' ? ( {ss.message} ) : ( )}
{ss?.status === 'done' && (
{ss.message} · {ss.synced} elementos importados
)} {ss?.status === 'error' && (
Error: {ss.message}
)} {expanded === cfg.id && (
Películas
Categoría: {cfg.movies_category_name}
Biblioteca: {cfg.movies_library_id || '—'}
Series
Categoría: {cfg.series_category_name}
Biblioteca: {cfg.series_library_id || '—'}
)}
) })}
)} {/* Form modal */} {showForm && (

{editId ? 'Editar conexión' : 'Nueva conexión Jellyfin'}

setForm(f => ({ ...f, name: e.target.value }))} placeholder="Mi Jellyfin" />
setForm(f => ({ ...f, url: e.target.value }))} placeholder="http://192.168.1.x:8096" />
setForm(f => ({ ...f, api_key: e.target.value }))} placeholder="Tu API key de Jellyfin" />
{/* Test connection */}
{testResult && ( {testResult.ok ? ( <> {testResult.server_name} ) : ( <> {testResult.error} )} )}

{/* Library selectors */}
Bibliotecas
{libraries.length > 0 ? ( ) : ( setForm(f => ({ ...f, movies_library_id: e.target.value }))} placeholder="ID de biblioteca" /> )}
{libraries.length > 0 ? ( ) : ( setForm(f => ({ ...f, series_library_id: e.target.value }))} placeholder="ID de biblioteca" /> )}
setForm(f => ({ ...f, movies_category_name: e.target.value }))} />
setForm(f => ({ ...f, series_category_name: e.target.value }))} />
)}
) }