Biblioteca Jelly (Cliente offline para Jellyfin)
Aplicación Android nativa desarrollada en Kotlin/Jetpack Compose que actúa como biblioteca de consulta offline para un servidor Jellyfin. Importa los metadatos de películas y series, los almacena en una base de datos local y permite consultarlos sin conexión.
Changelog interno
v2.4 (release pública)
- Filtro de duplicados ampliado en Películas y Series con modos de coincidencia Flexible, Equilibrado y Estricto.
- Corrección visual del filtro de duplicados en móvil vertical para que los chips sigan siendo accesibles.
- Configuración de servidor mejorada:
- al probar conexión se muestra la versión del servidor Jellyfin conectado,
- la comprobación usa los datos actuales del formulario.
- Sincronización de
Últimos añadidosreforzada con opción para reconciliar eliminados del servidor sin lanzar una sincronización completa. - Reconciliación ligera de eliminados para películas, series y otros por biblioteca durante sincronización incremental cuando la opción está activa.
- Migración automática de favoritos cuando Jellyfin reindexa un elemento movido y genera un ID nuevo equivalente.
- Nuevo informe de duplicados en la pestaña Datos:
- exportación en TXT y CSV,
- compartir TXT o CSV,
- elección manual de ubicación de guardado,
- inclusión de rutas de archivo y resumen global,
- modos de coincidencia independientes para el informe de películas y series.
- Versión de aplicación actualizada a
2.4(versionCode = 11).
v2.3 (release pública)
- Ajuste de navegación con tecla atrás en móvil para evitar cierres accidentales de la app.
- Nuevo filtro de búsqueda Duplicados en Películas y Series:
- muestra únicamente elementos cuyo título coincide exactamente con otro,
- útil para localizar entradas repetidas del catálogo.
- En biblioteca:
- primero cierra diálogos abiertos,
- vuelve a «Todas las bibliotecas» cuando estás dentro de una biblioteca,
- y solo sale de la app con doble pulsación atrás cuando no hay más navegación posible.
- En configuración:
- primero cierra diálogos abiertos (borrar datos, conexión e historial),
- vuelve a biblioteca si venías desde ahí,
- y aplica doble pulsación atrás para salir cuando estás en la raíz.
- Versión de aplicación actualizada a
2.3(versionCode = 10).
v2.2 (release pública)
- Ajuste de navegación con tecla atrás en móvil para evitar cierres accidentales de la app.
- En biblioteca:
- primero cierra diálogos abiertos,
- vuelve a «Todas las bibliotecas» cuando estás dentro de una biblioteca,
- y solo sale de la app con doble pulsación atrás cuando no hay más navegación posible.
- En configuración:
- primero cierra diálogos abiertos (borrar datos, conexión e historial),
- vuelve a biblioteca si venías desde ahí,
- y aplica doble pulsación atrás para salir cuando estás en la raíz.
- Versión de aplicación actualizada a
2.2(versionCode = 9).
v2.1 (release pública)
- Consolidación funcional de la rama 2.x en una única salida pública.
- Pestaña Otros completa en modo clásico:
- agrupación ligera por tipo (Videos / Imágenes),
- filtro por biblioteca dentro de la pestaña,
- modal con búsqueda local y orden (
A-Z/Últimos añadidos). - Sincronización de Otros con botones dedicados:
- Normal,
- Rápida,
- Últimos añadidos,
- además de inclusión en sincronización
Todo. - Corrección de UX en novedades:
- «Marcar vistas» ahora limpia correctamente en modo clásico incluso con desfases de hora servidor/dispositivo.
- Reconciliación de datos en
other_media: - limpieza de elementos eliminados en servidor durante sincronización completa,
- sin borrados agresivos en incremental por fecha.
- Versión de aplicación actualizada a
2.1(versionCode = 8).
v2.0 (interna, no publicada de forma independiente)
- Nueva visualización de biblioteca en dos modos:
- Modo clásico (pestañas de Películas, Series y Otros),
- Modo avanzado por bibliotecas con filtrado visual.
- Nueva pestaña Otros para contenido ligero (
Video/Photo): - tarjetas por tipo (Videos e Imágenes),
- modal con listado por nombre,
- búsqueda local y orden (
A-Z/Últimos añadidos) dentro del modal, - filtro por biblioteca dentro de la propia pestaña.
- Enriquecimiento de catálogo con metadatos de biblioteca:
- asociación de películas y series a su biblioteca de origen,
- persistencia local para navegación y filtros más precisos.
- Mejoras de experiencia previas consolidadas:
- historial de sincronización gestionable,
- diálogo con scroll en historiales largos,
- año de producción visible en detalles,
- opción para mostrar/ocultar ruta de archivo.
- Localización ampliada y consistente (ES/EN) en interfaz y mensajes de estado.
- Versión de aplicación actualizada a
2.0(versionCode = 7). - Nota: esta versión se usó como base interna y se consolidó directamente en la salida pública
2.1.
v1.6
- Configuración de servidor más robusta:
- el botón pasa a Guardar,
- la configuración se guarda siempre aunque no haya conexión.
- Validación de conexión no bloqueante:
- comprobación en segundo plano con timeout,
- evita cuelgues de la app cuando el servidor no está disponible.
- Corrección de autenticación inicial intermitente:
- se evita enviar token en
AuthenticateByName, - normalización de usuario/API key para reducir falsos errores de credenciales.
- Versión de aplicación actualizada a
1.6(versionCode = 6).
v1.5
- Nueva sincronización específica de últimos añadidos:
Solo películas (últimos añadidos)Solo series (últimos añadidos)- Esta opción evita refrescar todo el catálogo para traer únicamente lo nuevo.
- Aviso automático de nueva versión:
- popup al detectar release más reciente en GitHub,
- botón directo para descargar la actualización,
- opción “Más tarde” para ocultar ese aviso hasta la siguiente release.
- Se mantiene el resto de modos existentes (Normal, Rápida y Solo detalles).
- Versión de aplicación actualizada a
1.5(versionCode = 5).
v1.4
- Sincronización diferenciada por modo:
- Normal (catálogo + detalles),
- Rápida (solo catálogo),
- Solo detalles (sin refrescar catálogo).
- Historial de sincronizaciones:
- acceso desde opciones,
- acceso rápido pulsando “Última actualización”.
- Configuración reorganizada por pestañas (
Servidor,Sincronización,Datos). - Nuevo botón Probar conexión con popup de resultado.
- Filtros mejorados:
- combinación de múltiples géneros,
- combinación de múltiples detalles técnicos,
- texto actualizado de acción a “Limpiar detalles”.
- Corrección de orden “Últimos añadidos” en series usando
DateCreated. - Versión de aplicación actualizada a
1.4(versionCode = 4).
v1.3
- Sincronización de películas reforzada para evitar saltos de fase y conservar metadatos clave.
- Corrección de mapeo técnico en películas:
- formato normalizado correctamente,
- calidad coherente con resoluciones reales (incluyendo casos límite).
- Nuevo orden de biblioteca por pestaña con persistencia local:
A-Z,Últimos añadidos.- Mejoras visuales del selector de orden:
- estilo chips compacto,
- aviso discreto cuando faltan fechas (
DateCreated). - Favoritos locales (películas y series):
- botón estrella en tarjetas,
- botón estrella en diálogos de detalle,
- filtro rápido “Favoritos”.
- Novedades desde última visita:
- contador por pestaña (
Películas (N),Series (N)), - acción “Marcar vistas”,
- límite visual
99+en contadores. - Ajustes responsive y de renderizado de pósters fallback para evitar solapes en tarjetas.
- Versión de aplicación actualizada a
1.3(versionCode = 3).
Estado
- Versión interna base: 2.0
- Versión pública actual: 2.2
- Android mínimo: 7.0 (API 24)
- Stack principal: Kotlin + Jetpack Compose + Material 3
Funcionalidades destacadas (v2.1)
Novedades de la versión
- Sincronización de “últimos añadidos” para películas y series sin refresco completo.
- Aviso in-app de actualización al detectar una release más reciente en GitHub.
- Modos de sincronización separados (normal / rápida / detalles).
- Historial de actualizaciones visible desde opciones y desde la fecha de última actualización.
- Botón “Probar conexión” en configuración.
- Filtros combinables por géneros y detalles técnicos.
- Modo avanzado de bibliotecas para explorar por colección, manteniendo modo clásico.
- Pestaña Otros con navegación ligera para vídeos e imágenes personales.
Roadmap consolidado (2.0 -> 2.1)
- Integración completa de bibliotecas en modo avanzado.
- Portadas de biblioteca automáticas + selección manual.
- Soporte de contenido Otros (Video/Photo) en modelo local, repositorio, ViewModel y UI.
- Botonera de sincronización de Otros (normal / rápida / últimos añadidos).
- Corrección de «Marcar vistas» en modo clásico.
- Reconciliación de eliminados para
other_mediaen sincronización completa.
Funcionalidades destacadas (v1.6)
Novedades de la versión
- Sincronización de “últimos añadidos” para películas y series sin refresco completo.
- Aviso in-app de actualización al detectar una release más reciente en GitHub.
- Modos de sincronización separados (normal / rápida / detalles).
- Historial de actualizaciones visible desde opciones y desde la fecha de última actualización.
- Botón “Probar conexión” en configuración.
- Filtros combinables por géneros y detalles técnicos.
Base consolidada (v1.3)
Biblioteca y orden
- Pestañas separadas de Películas y Series.
- Orden por pestaña con persistencia local:
A-ZÚltimos añadidos
Filtros
- Búsqueda por título.
- Filtro por género.
- Filtro técnico:
- calidad,
- formato,
- resolución.
- Limpieza rápida de filtros activos.
Favoritos
- Estrella en tarjetas de películas/series.
- Estrella en diálogos de detalle.
- Filtro “Favoritos” por pestaña.
Novedades
- Contador desde última visita:
- en pestañas (
Películas (N),Series (N)), - en bloque informativo dentro de la vista.
- Acción “Marcar vistas”.
- Formato visual
99+para contadores altos.
Arquitectura
- Arquitectura: MVVM
- UI: Jetpack Compose + Material 3
- Persistencia local: Room
- Red: Retrofit + OkHttp
- Sincronización en segundo plano: WorkManager
- Cifrado de credenciales: EncryptedSharedPreferences (androidx.security.crypto)
- Imágenes: Glide con caché local
Capas principales
- data/
LocalModels.kt: entidades Room (MovieEntity,SeriesEntity,SeasonEntity,EpisodeEntity), DAOs yBibliotecaDatabase.RemoteModels.kt: modelos de la API de Jellyfin (AuthenticationResult,BaseItemDto,ItemsResponse, etc.) e interfazJellyfinApi.Repository.kt:CredentialsStore: lectura/escritura segura de configuración y credenciales conEncryptedSharedPreferences.JellyfinRepository/DefaultJellyfinRepository: lógica de negocio, autenticación, sincronización incremental y acceso a Room.ServiceLocator: creación deRetrofit,OkHttpClient, base de datos y repositorio.enqueueManualSync(...): utilitario para lanzar sincronizaciones con WorkManager.data/sync/JellyfinSyncWorker.kt:CoroutineWorkerque ejecuta la sincronización en segundo plano.- ui/
MainViewModel: coordina configuración, estado de sincronización y búsquedas; exponemoviesyseriescomoStateFlow.MainActivity: host de Compose +MainViewModely navegación simple entre configuración y biblioteca.- Composables principales en
MainActivity.kt: ConfigScreen: pantalla de configuración del servidor.LibraryScreen: tabs “Películas” y “Series”.MoviesGrid,SeriesList: listados con grid y lista.MovieDetailDialog,SeriesDetailDialog: detalles con metadatos.
Modelo de datos
Películas (MovieEntity)
Tabla movies:
id(PK,String): identificador de Jellyfin.title(String): título.createdUtcMillis(Long?): fecha de creación del ítem (UTC millis).posterUrl(String?): URL de portada.format(String?): contenedor (MKV, MP4, AVI, etc.).quality(String?): 480p, 720p, 1080p, 4K (derivado de la resolución).resolution(String?): por ejemplo1920x1080.bitrateMbps(Double?): tasa de bits en Mbps (aproximada).fps(Double?): no se calcula actualmente, reservado.durationMinutes(Int?): duración en minutos.sizeGb(Double?): tamaño estimado en GB.audioLanguages(List<String>): idiomas de audio.subtitleLanguages(List<String>): idiomas de subtítulos.isFavorite(Boolean): favorito local.
Índices:
- Índice por
titlepara búsqueda eficiente.
Series, temporadas y episodios
SeriesEntity (tabla series)
id(PK,String)title(String)createdUtcMillis(Long?)posterUrl(String?)totalSeasons(Int)totalEpisodes(Int)isFavorite(Boolean)
Índice por title.
SeasonEntity (tabla seasons)
id(PK,String)seriesId(String): FK aSeriesEntity.seasonNumber(Int)format(String?)quality(String?)resolution(String?)bitrateMbps(Double?)fps(Double?)totalDurationMinutes(Int?)totalSizeGb(Double?)audioLanguages(List<String>)subtitleLanguages(List<String>)episodeCount(Int)
Relación uno-a-muchos con SeriesEntity mediante seriesId.
EpisodeEntity (tabla episodes)
id(PK,String)seriesId(String): FK aSeriesEntity.seasonId(String): FK aSeasonEntity.seasonNumber(Int)episodeNumber(Int)title(String)durationMinutes(Int?)sizeGb(Double?)
Relaciones:
SeriesWithSeasonsAndEpisodes: modelo que agrupaSeriesEntity→SeasonWithEpisodes→EpisodeEntity.
Conversores de tipos
RoomConvertersconvierteList<String>↔Stringpara almacenar listas de idiomas.
Sincronización
Modos de sincronización
- Al iniciar la app:
MainViewModeldetecta configuración previa y disparatriggerStartupSync(). - Al cerrarla:
MainActivity.onStopllama aenqueueManualSync(...), que encola unJellyfinSyncWorker. - Manual:
- Botón “Sincronizar” en la
TopAppBar. Swipe-to-refreshen la vista principal (usandoaccompanist-swiperefresh).
Lógica incremental
En DefaultJellyfinRepository.syncIncremental:
- Obtiene
lastSyncEpochMillisdelCredentialsStore. - Lo convierte a ISO-8601 y lo pasa como
MinDateLastSaveda: getItems(userId, IncludeItemTypes = "Movie", ...)getItems(userId, IncludeItemTypes = "Series,Season,Episode", ...)- Solo se procesan los elementos cambiados desde la última sincronización.
- En modo “solo nuevos”, si todavía no existe una sincronización previa (
lastSyncEpochMillisvacío), se realiza una carga completa inicial del catálogo para establecer la base incremental. - Al finalizar, actualiza el timestamp de última sincronización.
Progreso y logs
- El repositorio reporta progreso con
(processed, total): - La UI muestra barra de progreso y texto
Sincronizando X / Y. - El
JellyfinSyncWorkerpublica progreso enWorkManager. - Logs con
Log.d/Log.econ la etiquetaJellyfinSyncpara: - Inicio/fin de sincronización.
- Volumen de datos recibidos.
- Errores de red o HTTP durante la sincronización.
Conexión y credenciales
Pantalla de configuración
ConfigScreen incluye:
- Dirección del servidor (IP o dominio).
- Puerto.
- Usuario.
- Contraseña.
- API key (opcional).
Almacenamiento seguro
CredentialsStoreusaEncryptedSharedPreferencespara guardar:baseUrl(servidor + puerto).username,password.apiKey(si se usa).accessTokenyuserIdobtenidos por autenticación.- Timestamp de última sincronización.
Validación de conexión
JellyfinRepository.authenticateAndValidateConnection():
- Si hay API key, la considera válida directamente.
- Si no, llama a
POST /Users/AuthenticateByNamecon{ "Username": "...", "Pw": "..." }. - Manejo de errores:
- 401:
Usuario o contraseña incorrectos. SocketTimeoutException:Tiempo de conexión agotado.UnknownHostException:Servidor no disponible.- Otros:
Error HTTP xxxoError desconocido.
La UI muestra estos mensajes en la pantalla de configuración.
Uso de la API de Jellyfin
Autenticación
- Endpoint:
POST /Users/AuthenticateByName - Cuerpo esperado:
{
"Username": "usuario",
"Pw": "contraseña"
}
- Respuesta simplificada:
{
"AccessToken": "token",
"User": {
"Id": "user-id",
"Name": "usuario"
}
}
- El
AccessTokense envía en siguientes peticiones vía cabecera:
Authorization: MediaBrowser Token="TOKEN"
También se admite usar directamente una API key en esa cabecera.
Listado de elementos
- Endpoint:
GET /Users/{userId}/Items - Parámetros relevantes usados:
IncludeItemTypes=Moviepara películas.IncludeItemTypes=Series,Season,Episodepara series.Recursive=true.Fields=MediaStreams,MediaSources,Path,PremiereDate,DateCreated,Width,Height,Overview.MinDateLastSaved=...para sincronización incremental.
Respuesta simplificada:
{
"Items": [
{
"Id": "item-id",
"Name": "Título",
"Type": "Movie",
"Container": "mkv",
"RunTimeTicks": 6000000000,
"ImageTags": { "Primary": "tag123" },
"MediaStreams": [
{
"Type": "Video",
"Language": "es",
"Width": 1920,
"Height": 1080,
"Bitrate": 8000000
}
],
"MediaSources": [
{
"Container": "mkv",
"RunTimeTicks": 6000000000,
"Size": 2147483648,
"Bitrate": 8000000
}
]
}
]
}
Esta estructura se mapea a BaseItemDto y luego a las entidades de Room.
Interfaz de usuario
Tabs y listas
- Tabs principales:
- Películas: grid de portadas + título (
LazyVerticalGrid). - Series: lista con poster, título, número de temporadas y episodios (
LazyColumn). - Búsqueda:
- Campo de texto que filtra por título en ambas secciones.
Vista de detalle
- Películas:
- Diálogo con:
- Formato, calidad, resolución.
- Bitrate, duración, tamaño.
- Listado de idiomas de audio y subtítulos.
- Acción de favorito (estrella).
- Series:
- Diálogo con número total de temporadas y episodios.
- La base de datos incluye estadísticas agregadas por temporada (
SeasonEntity) para extender fácilmente la vista de detalle. - Acción de favorito (estrella).
Sincronización en UI
Swipe-to-refreshpara lanzar sincronización manual.- Botón “Sincronizar” en la barra superior.
- Barra de progreso lineal mostrando elementos procesados/total.
- Texto con timestamp de última sincronización.
Gestión de imágenes
PosterImageusaAndroidView+Glide:- Carga las portadas/pósters desde la URL generada a partir de
baseUrl+/Items/{id}/Images/Primary. - Usa
ic_launcher_foregroundcomo placeholder mientras se carga. - Glide gestiona caché en disco y memoria; se puede ajustar el tamaño de las imágenes para optimizar espacio.
Testing
Pruebas unitarias
JellyfinParsingTest:- Valida el parsing de la respuesta de autenticación (
AuthenticationResult). - Valida el parsing de
ItemsResponsecon un ejemplo simplificado de película.
Se pueden añadir más pruebas que instancien DefaultJellyfinRepository con dobles de JellyfinApi y Room in-memory para cubrir más lógica de sincronización.
Pruebas de integración (sugeridas)
- Usar
MockWebServerpara simular un servidor Jellyfin: - Responder a
AuthenticateByNameyItemscon JSON de prueba. - Verificar flujo completo: configuración → autenticación → sincronización → datos en Room.
- Añadir pruebas de UI con
compose-ui-testpara validar búsqueda, tabs y diálogos de detalle.
Instalación y ejecución
- Clonar el repositorio y abrirlo en Android Studio Iguana/Koala o superior.
- Sincronizar Gradle y esperar a que se descarguen las dependencias.
- Ejecutar en un emulador o dispositivo físico con Android 7.0 (API 24) o superior.
- En la primera ejecución: – Introducir dirección IP o dominio del servidor Jellyfin. – Introducir puerto (por defecto suele ser
8096para HTTP). – Rellenar usuario/contraseña o API key. – Pulsar “Guardar” para guardar la configuración. – La app valida la conexión en segundo plano (sin bloquear); si no hay red, la configuración queda guardada y podrás reintentar desde “Probar conexión” o al sincronizar.
Generar APK firmado
- En Android Studio, ir a Build > Generate Signed Bundle / APK….
- Seleccionar APK.
- Crear o elegir un keystore existente.
- Seleccionar el módulo
app. - Elegir tipo de build (
release) y finalizar el asistente. - El APK firmado quedará disponible en
app/build/outputs/apk/release/.
Este APK puede instalarse directamente en dispositivos Android compatibles.