HTML Automation System to Robust Cross-Platform Software | Shared Grok Conversation
index - ondaire-v16.html
te paso un html que es un sistema de automatización de radioemisora (broadcast).
me gustaria que lo analices y me digas si seria posible implementarlo en otro tipo de lenguaje como ser C, C++, python, o cualquiera mas robusto que html para esta aplicación. El problema con html es que es muy propenso a que el operador cometa errores y por ejemplo actualice la pagina cortando la transmisión o borrando la programación, etc. Quiero mantener el diseño que tiene actualmente, ya que es el resultado mucho tiempo de refinación y optimización de la metodología de trabajo, lo que necesito es robustecer el sistema y que sea mas practico para instalar en cualquier dispositivo. Actualmente lo estoy probando sobre windows. pero si fuera posible que sea multiplataforma (linux, android, windows) seria mucho mejor ya que es la idea con la que nació el proyecto.
no generes codigo. solo quiero respuestas y opciones de desarrollo. Cuando hayamos visto las opciones y elegido las mejores recién vamos a dedicarnos al codigo.
Worked for 21s
Sí, es totalmente posible y recomendable migrar o envolver este sistema a un lenguaje/runtime más robusto. El HTML/JS actual es un SPA cliente-side completo (LocalStorage + IndexedDB para estado y blobs de audio, múltiples elementos
Problemas principales del enfoque HTML actual
- Interrupción por refresh, navegación o cierre de pestaña.
- Storage volátil/limitado y dependiente del navegador (IndexedDB puede fallar o limpiarse).
- Sin control nativo de cierre, wake-lock fiable en todos los entornos, o acceso a FS de forma consistente.
- Instalación = “abrir el archivo” → frágil para operadores y multi-dispositivo.
- Android es especialmente problemático (background audio, permisos, UX táctil, batería).
Opciones de desarrollo viables (sin generar código todavía)
1. Envolver el HTML existente (máxima fidelidad de diseño, menor esfuerzo inicial)
- Electron (Chromium + Node): empaqueta el HTML/JS tal cual. Añade APIs nativas para prevenir cierre accidental, mejor FS, persistencia fuera del browser, auto-start, tray icon, etc. Multiplataforma desktop (Windows/Linux/macOS). Android no nativo.
- Tauri (Rust + webview): mucho más liviano que Electron, mejor seguridad y tamaño de binario. Mismo enfoque de reutilizar el frontend. Excelente para Windows/Linux.
- Neutralino o similar: aún más ligero.
Ventaja: conservás el 100 % del diseño y lógica visual refinada. Desventaja: sigue siendo webview por debajo (aunque controlable). Ideal para transición rápida y robustez operativa inmediata.
2. Python (buen equilibrio robustez / productividad / multiplataforma)
- Frontend: PyQt6 / PySide6 (recrear la UI fielmente) o pywebview / Eel (embeber el HTML actual casi sin cambios).
- Audio: sounddevice, miniaudio, pygame, o bindings a FFmpeg/PortAudio.
- Persistencia: SQLite + archivos de audio en disco (mucho más robusto que IndexedDB).
- Empaquetado: PyInstaller / Nuitka → ejecutable único.
- Android: posible vía Kivy, BeeWare o empaquetado especial, pero no tan limpio.
Ventaja: desarrollo rápido, excelente para lógica de programación, fácil de mantener, multiplataforma desktop sólida. Desventaja: recrear o adaptar la UI toma algo de tiempo si no se embebe.
3. C++ (máxima robustez y control de bajo nivel)
- Qt (Widgets o Quick/QML): permite UI muy cercana al diseño actual y excelente soporte de audio (Multimedia module + FFmpeg).
- Alternativas: Dear ImGui + backend de audio (PortAudio/RtAudio), o frameworks más ligeros.
- Persistencia nativa + control total de hilos, prioridades de audio, watchdog, etc.
Ventaja: el más “broadcast-grade” (baja latencia, estabilidad extrema, embebible). Multiplataforma desktop excelente.
Desventaja: más tiempo de desarrollo y curva de aprendizaje. Android posible pero con más trabajo (Qt for Android o NDK).
4. Frameworks multiplataforma verdaderos (incluyendo Android)
- Flutter (Dart): UI declarativa muy flexible, buena performance de audio con plugins, un solo código base para Windows/Linux/Android (y más). Requiere reescribir la interfaz.
- React Native + Electron-like o Capacitor/Ionic (reutilizar partes del HTML/JS).
- Kotlin Multiplatform o .NET MAUI / Avalonia.
Ventaja: el objetivo original de “cualquier dispositivo”.
Desventaja: más rediseño de UI y pruebas de audio en background en Android.
5. Otras rutas híbridas
- PWA avanzada + Service Worker + mejor manejo de almacenamiento (sigue siendo navegador).
- Backend local en Python/Go/C++ + frontend web embebido.
- Motor de audio profesional (Jack, PulseAudio, etc.) + UI separada.
Criterios de decisión recomendados
| Prioridad | Mejor opción aproximada | Comentario |
|---|---|---|
| Conservar diseño actual al 100 % | Electron / Tauri | Más rápido |
| Robustez operativa + desktop fácil | Python + Qt o Electron/Tauri | Balance ideal |
| Máxima estabilidad y control | C++ + Qt | Para producción seria |
| Multiplataforma real (Android incluido) | Flutter o Capacitor | Más esfuerzo de UI |
| Instalación simple y “caja negra” | Cualquiera empaquetada (Electron, PyInstaller, Qt) | Evita que el operador “abra el HTML” |
Recomendación de camino
- Corto plazo (robustez rápida manteniendo diseño): Empezar con Tauri o Electron. Convertís la consola actual en una aplicación de escritorio nativa, agregás protección contra cierre/recarga, persistencia en disco real y empaquetado instalable. Probás en Windows y Linux.
- Mediano plazo: Si necesitás Android o más control de audio, migrar la lógica de negocio a Python o C++ y mantener (o recrear) la UI.
- Largo plazo: Si el proyecto crece a “producto comercial multi-dispositivo”, evaluar Flutter o una arquitectura cliente-servidor local.
El diseño actual es el resultado de mucha refinación operativa; por eso conviene no tirarlo a la basura. La estrategia más inteligente es reutilizarlo (webview empaquetado) o portarlo con fidelidad visual alta, mientras se mueve el motor de reproducción, la parrilla, las tandas y el almacenamiento a un runtime más seguro.
Decime qué priorizás más ahora mismo (conservar el look exacto, facilidad de instalación, Android sí/no, latencia de audio, tiempo de desarrollo disponible, etc.) y afinamos las 2-3 mejores opciones antes de pasar a cualquier implementación.
Conservar el look exacto no es lo mas importante sino mantener la logica de trabajo del diseño del ui. Pueden cambiar los colores, los diseños de los botones que en vez de rectangulares sean cuadrados o redondos, los icono, etc. Lo que no debe cambiar es el diseño de ubicación de cada sección y el funcionamiento de los mismos. La latencia de audio si bien puede ser una opcion a considerar en otro momento, este no es importante, salvo que los retrasos sean considerables e impidan el normal funcionamiento. No hay plazos de desarrollo, es un proyecto personal y no hay plazos de entrega, por supuesto que me gustaria no demorar demasiado. Lo importante es, en principio, que funcione bien sobre windows, de ser posible sobre linux (si existiera la posibilidd de ejecutarlo tambien sobre linux en armhf mejor pero no es determinante) y por ultimo la posibilidad de ejecutarlo sobre android, es la opción sobre la que nacio la idea y aprovechar dispositivos viejos pero que son funcionales. Me importa la robustez
Worked for 16s
Entendido. Prioridades claras:
- Mantener la lógica de trabajo y la ubicación/funcionamiento de las secciones (parrilla, botoneras, publicidad, categorías, cola, consola, etc.). Colores, formas de botones, iconos y detalles visuales pueden cambiar.
- Robustez como prioridad principal.
- Plataformas: Windows primero, Linux (incluyendo posibilidad de ARMHF) deseable, Android como meta original (aprovechar dispositivos viejos).
- Latencia de audio no crítica salvo que afecte el funcionamiento normal.
- Proyecto personal, sin plazos estrictos, pero preferible no alargar demasiado el desarrollo.
Opciones ordenadas según tus prioridades
1. Flutter (Dart) — Mejor equilibrio general (recomendación principal)
- Plataformas: Windows + Linux (x86 y buena compatibilidad ARM en muchas distros) + Android nativo. Un solo código base.
- Robustez: Muy buena. Aplicación compilada, no depende de un navegador. Control de ciclo de vida, persistencia en SQLite + archivos, servicios en foreground en Android (necesario para no cortar el audio cuando la pantalla se apaga o se cambia de app).
- UI/lógica: Se puede recrear fielmente la disposición de secciones y el flujo de trabajo. Los widgets permiten layouts complejos (tabs, grids de botonera, tarjetas de tandas, etc.).
- Audio: Plugins maduros (just_audio, audio_service + foreground service). Suficiente para este uso (no es broadcast de ultra-baja latencia).
- Esfuerzo: Medio. Hay que reescribir la UI y la lógica, pero el lenguaje es productivo y hay mucha documentación.
- Instalación: APK/AAB en Android, instaladores nativos en Windows/Linux.
- Puntos fuertes para vos: Cumple el sueño original de Android + robustez real + multiplataforma.
- Puntos débiles: Android requiere cuidar bien el servicio en segundo plano y los permisos de batería; en dispositivos muy viejos puede haber limitaciones de rendimiento.
2. Python + Qt (PySide6 / PyQt6) — Máxima robustez en escritorio
- Plataformas: Excelente en Windows y Linux (incluyendo ARMHF con algo de cuidado en el empaquetado). Android es posible pero más limitado/secundario (BeeWare o Kivy, no tan limpio).
- Robustez: De las más altas. Control total del proceso, SQLite + archivos en disco, fácil implementar watchdog, prevención de cierre accidental, manejo de hilos de audio.
- UI/lógica: Se puede replicar muy bien la estructura de pestañas, grids, botoneras y paneles. Qt es muy flexible para layouts.
- Audio: Buenas opciones (sounddevice, miniaudio, bindings a FFmpeg, etc.).
- Esfuerzo: Medio-bajo si ya conocés Python. Desarrollo rápido.
- Instalación: Ejecutable único con PyInstaller/Nuitka.
- Ideal si: Priorizás robustez extrema en Windows/Linux y Android puede esperar o ser una versión más simple después.
3. Tauri o Electron (envolver/adaptar el frontend actual)
- Plataformas: Muy buenos en Windows y Linux. Android no es realista.
- Robustez: Bastante mejor que el HTML puro (control de ventana, prevención de recarga, mejor acceso a disco). Sigue siendo un runtime web por debajo.
- UI/lógica: La más rápida de adaptar (se puede reutilizar gran parte del HTML/JS actual).
- Esfuerzo: El más bajo a corto plazo.
- Cuándo elegirlo: Si querés tener algo robusto en Windows/Linux rápido y dejar Android para una fase 2 (quizá con Flutter).
4. C++ + Qt
- La opción de máxima robustez y control de bajo nivel.
- Excelente Windows/Linux, Android posible pero más trabajo.
- Esfuerzo de desarrollo significativamente mayor. Solo tendría sentido si más adelante necesitás ultra-estabilidad o integración con hardware específico.
Ranking según tus criterios actuales
| Posición | Tecnología | Windows | Linux (+ARM) | Android | Robustez | Esfuerzo | Conserva lógica UI |
|---|---|---|---|---|---|---|---|
| 1 | Flutter | Excelente | Muy bueno | Nativo | Alta | Medio | Alta (recreación) |
| 2 | Python + Qt | Excelente | Excelente | Limitado | Muy alta | Medio-bajo | Alta |
| 3 | Tauri / Electron | Excelente | Excelente | No | Buena | Bajo | Muy alta (reuso) |
| 4 | C++ + Qt | Excelente | Excelente | Posible | Máxima | Alto | Alta |
Recomendación concreta
- Si Android es realmente importante (aprovechar tablets/celulares viejos): ir directo a Flutter. Es la opción que mejor cubre las tres plataformas con un solo esfuerzo razonable y buena robustez.
- Si preferís robustez máxima primero en PC y Android después: empezar con Python + Qt. Es más rápido de poner en producción en Windows/Linux y muy estable.
- Camino híbrido posible: hacer primero la versión de escritorio (Python/Qt o Tauri) para tener algo usable y robusto ya, y luego portar a Flutter para Android.
La lógica de trabajo (patrones de parrilla, botoneras con ducking, tandas manuales/auto, límites de minutos/hora, cola, categorías, etc.) se puede mantener intacta en cualquiera de estas opciones. Solo cambia la tecnología de base.
¿Cuál de estas tres te resulta más atractiva ahora mismo?
- Flutter (priorizar multiplataforma incluyendo Android)
- Python + Qt (priorizar robustez y velocidad en Windows/Linux)
- Empezar con Tauri/Electron (rápido y reutiliza mucho del actual)
O si querés que profundice en algún aspecto concreto (manejo de audio en background en Android, empaquetado, persistencia, etc.) antes de decidir.
¿podemos armar ramas de desarrollo para tomar como punto de partida el momento actual e ir explorando de la opcion mas recomendada hasta la mas robusta si fuera necesaria? No tengo intencion de explorar todas sino tener alternativas en caso de no conseguir resultados aceptables y al partir siempre de este punto evitar ruido, desvios y desvirtualizaciones de lo hecho hasta ahora por estar saltando de una opcion a otra.
Worked for 9s
Sí, es la forma correcta de proceder.
Vamos a definir ramas de desarrollo claras y secuenciales, todas partiendo del mismo punto actual (el sistema HTML/JS que ya tenés). Así evitamos ruido, desvíos y pérdida de la lógica de trabajo refinada.
Punto de partida (Baseline)
Rama 0 – Baseline (congelada)
- El HTML actual + toda la lógica de trabajo, estructura de secciones, flujos (parrilla por patrones, botoneras, tandas, publicidad, categorías, cola, etc.).
- Se mantiene intacta como referencia permanente.
- Cualquier rama nueva parte de aquí (extracción del modelo de datos + lógica de negocio + disposición de secciones).
Ramas de exploración (en orden de prioridad)
Rama 1 – Flutter (recomendación principal)
- Objetivo: Windows + Linux + Android con un solo código base.
- Se recrea la lógica de trabajo y la ubicación/funcionamiento de las secciones.
- Prioridad alta a robustez (servicio en foreground en Android, persistencia real, control de ciclo de vida).
- Es la primera en la que vamos a trabajar.
Rama 2 – Python + Qt (PySide6)
- Se activa solo si en la Rama 1 aparecen problemas relevantes (audio en background en Android, rendimiento en dispositivos viejos, complejidad excesiva, etc.).
- Enfoque: máxima robustez y estabilidad en Windows y Linux (ARMHF posible).
- Android queda en segundo plano o versión simplificada.
Rama 3 – Tauri (o Electron como alternativa)
- Se activa si las dos anteriores no dan resultados aceptables en tiempo razonable, o si necesitás algo usable muy rápido en escritorio.
- Reutiliza la mayor parte posible de la lógica actual (webview + backend nativo).
- Muy buena robustez en Windows/Linux, pero sin Android nativo.
Rama 4 – C++ + Qt (solo si es estrictamente necesario)
- Última opción.
- Solo se abre si se necesita el máximo control de bajo nivel y robustez extrema que las anteriores no puedan entregar.
Cómo se trabaja
- Siempre partimos del Baseline.
- Se extrae primero el modelo de datos + lógica de negocio + disposición de secciones (esto es el núcleo que no se toca conceptualmente).
- Cada rama implementa ese núcleo en su tecnología.
- Se avanza una rama a la vez. Solo se salta a la siguiente si los resultados no son aceptables según criterios claros (estabilidad, funcionamiento continuo, facilidad de uso en el dispositivo objetivo, etc.).
- Todo lo aprendido o mejorado en una rama puede realimentar a las siguientes (sin contaminar el Baseline).
Criterios mínimos de “aceptable” (para decidir si seguir o saltar de rama)
- Funciona de forma continua sin cortarse por acciones normales del operador.
- La lógica de trabajo y ubicación de secciones se mantiene.
- Se puede instalar y usar de forma práctica en Windows (prioridad 1).
- En la rama correspondiente: Linux y/o Android funcionan de manera usable.
- El operador no puede romper fácilmente la transmisión o la programación.
¿Te parece bien este esquema de ramas?
Si estás de acuerdo, el siguiente paso es definir con más detalle la Rama 1 (Flutter):
- Cómo vamos a extraer y documentar el núcleo (modelo de datos + flujos) desde el HTML actual.
- Estructura inicial del proyecto.
- Orden de implementación de las secciones.
Decime si aprobás las ramas tal como están o si querés ajustar el orden / agregar alguna condición.
Si, las ramas como esta planteado es lo que quiero. Comencemos con la primera
Worked for 13s
Perfecto. Arrancamos formalmente la Rama 1 – Flutter.
Paso 0 – Congelar el Baseline
El HTML actual queda como Baseline (referencia inmutable).
Todo lo que hagamos en Flutter (y en las ramas posteriores) debe respetar:
- La lógica de trabajo
- La ubicación y funcionamiento de las secciones
- El modelo de datos y flujos principales
Colores, formas de botones, iconos y detalles visuales pueden evolucionar.
Primer objetivo de la Rama 1: Extraer y documentar el Núcleo
Antes de escribir una sola línea de Flutter, vamos a sacar del HTML el Núcleo ONDAIRE.
Este núcleo será el contrato común para todas las ramas.
Voy a proponer que documentemos (en un archivo de texto o markdown que quede junto al proyecto) lo siguiente:
1. Modelo de datos principal
- config
- categories y jingleCategories
- parrilla (franjas + patrones)
- tandas + tandaManual
- clients
- songs, jingles, spots
- queue
- history
- day (counts, played, plan)
- buttoners + estado de botonera
- Índices de rotación y uso (patternIdx, songCategoryUsage, etc.)
2. Flujos de reproducción críticos
- advance() y lógica de cola / patrón / fallback
- Manejo de tandas (auto vs manual + límite de minutos/hora)
- Botoneras (pisador con ducking vs separador)
- Streaming
- Top de hora
- Stop-after, rewind, etc.
3. Estructura de secciones (layout lógico)
- Header + Deck + Transport + barra de progreso
- Tabs: Consola | Parrilla | Publicidad | Categorías | Música | Artística | Config | Historial
- Dentro de Consola: patrón activo + botonera + cola + explorador + pases del día + estado de tandas
4. Reglas de negocio importantes
- Rotación (shuffle / secuencial)
- Respeto de tandas
- Colchón musical (fallback)
- Límites de publicidad
- Asignación de categorías por carpeta, etc.
Propuesta de orden de trabajo en Flutter
- Documentar el Núcleo (este paso – sin código todavía).
- Crear la estructura base del proyecto Flutter (Windows + Linux + Android).
- Definir el modelo de datos en Dart (clases inmutables + serialización).
- Implementar la persistencia (SQLite o Isar + almacenamiento de archivos de audio).
- Montar el esqueleto de UI con las mismas secciones y navegación.
- Implementar el motor de reproducción (empezando por lo más crítico: advance + patrones + botoneras).
- Ir completando sección por sección.
Pregunta para avanzar ahora
¿Querés que empiece ya a extraer y escribir el documento del Núcleo (modelo de datos + flujos principales + estructura de secciones) a partir del HTML que me pasaste?
Puedo hacerlo de dos formas:
A. Documento completo y estructurado (recomendado)
B. Documento más resumido y luego profundizamos sección por sección
También decime si preferís que el documento del Núcleo quede en español técnico claro (para que te sea fácil de leer y validar).
Confirmame cómo querés que proceda con la extracción del Núcleo y seguimos.
Antes de nada hay un problema en el funcionamiento del html que estaba resolviendo antes de empezar este proyecto. Al cargar archivos de audio en la base de datos el html los copiaba a la carpeta temporal, duplicando los audios originales, con el consiguiente desperdicio de espacio físico. Para resolver eso tengo dos propuestas, la primera es crear una estructura de carpetas que partan de la raíz de la aplicación por ejemplo un carpeta madre llamada Audios y dentro sub carpetas por ejemplo: Artística, Publicidades, Musica. Y dentro de cada una la posibilidad de sub carpetas como ser en musica: Rock, Pop, Folklore, Tango, etc definidas por el usuario. Lo mismo en Artísticas: Separadores, pisadores, identificaciones, promos, etc. Esto permite que al agregar archivos nuevos de audio en cada una se pueda cargar automáticamente en la base de datos. Y por otro lado la posibilidad de cargar en la base de datos archivos que no esten dentro de esta estructura de raiz sino en cualquier otra ubicación de los disco o de la red, sin copiarlos sino cargando sus rutas. ¿Es posible esta solucion?
Worked for 16s
Sí, esa solución es totalmente posible y es la correcta para un sistema robusto.
Por qué el HTML actual duplica los archivos
El navegador no tiene acceso real y persistente al sistema de archivos del usuario por seguridad.
Cuando cargás un archivo o carpeta:
- El navegador crea un blob (copia en memoria/IndexedDB).
- Esa copia se guarda dentro del almacenamiento del navegador.
- El archivo original queda intacto → se duplica el espacio.
No hay forma limpia de “apuntar” a una ruta real del disco desde un HTML puro de forma fiable y multiplataforma.
La solución que proponés es viable y recomendable
Podemos implementar un modelo híbrido:
1. Biblioteca gestionada (estructura de carpetas de la aplicación)
text
CopyCopied
Audios/
├── Musica/
│ ├── Rock/
│ ├── Pop/
│ ├── Folklore/
│ ├── Tango/
│ └── ... (carpetas definidas por el usuario)
├── Artistica/
│ ├── Pisadores/
│ ├── Separadores/
│ ├── Promos/
│ ├── Identificaciones/
│ └── ...
└── Publicidades/
└── ...
- La aplicación conoce esta estructura.
- Al agregar archivos nuevos dentro de estas carpetas, se pueden detectar automáticamente (escaneo al iniciar o con un vigilante de carpetas).
- Las subcarpetas pueden mapearse directamente a categorías (como ya hace el “Por nombre de carpeta” actual).
- Los archivos pueden quedarse en su lugar (solo se guarda la ruta relativa) o copiarse/moverse según preferencia.
2. Archivos externos (cualquier ubicación)
- El usuario puede agregar archivos o carpetas desde cualquier ruta del disco o de la red.
- No se copian. Solo se guarda la ruta (absoluta o UNC/network).
- La base de datos almacena:
- Ruta completa
- Tipo de origen (managed / external)
- Metadatos (título, duración, categorías, etc.)
Ventajas de este enfoque
- Elimina la duplicación innecesaria de espacio.
- Permite tener una biblioteca ordenada y autoclasificada.
- Soporta material que está en discos externos, NAS, carpetas compartidas, etc.
- Es el modelo que usan los sistemas profesionales de automatización radial.
- Encaja perfectamente con las ramas que definimos (Flutter, Python+Qt, etc.), porque todas tienen acceso real al sistema de archivos.
Consideraciones importantes (para el diseño del Núcleo)
- En Android el acceso a rutas arbitrarias es más restringido (Scoped Storage). Habrá que manejar permisos y, en algunos casos, usar el Storage Access Framework. Los archivos “managed” dentro del directorio de la app funcionan mejor.
- En Windows y Linux las rutas externas (incluyendo unidades de red) funcionan sin problemas si el sistema operativo las tiene montadas.
- Conviene guardar también un hash o fecha de modificación para detectar si el archivo externo se movió, borró o cambió.
- Opción de “Importar a la biblioteca” (copiar/mover al árbol Audios/) vs “Agregar como externo” (solo referencia).
Conclusión:
Sí, es posible y es la forma correcta de resolver el problema de espacio.
Esta decisión forma parte del Núcleo (modelo de datos + lógica de importación/carga).
¿Querés que incorpore esta arquitectura de almacenamiento de audio como parte del documento del Núcleo que vamos a armar ahora?
Si estás de acuerdo, el siguiente paso es:
- Definir claramente el modelo de datos de audio (con soporte managed + external).
- Definir cómo se relacionan las carpetas con las categorías.
- Documentar el resto del Núcleo (flujos de reproducción, secciones, etc.).
Confirmame y arrancamos con la extracción del Núcleo incluyendo esta mejora.
quiero que esto lo incorpores a la rama base, porque tambien va a ser necesario con las demas ramas en caso de necesitarse.
luego sigamos con los pasos correspondientes:
- Definir claramente el modelo de datos de audio (con soporte managed + external).
- Definir cómo se relacionan las carpetas con las categorías.
- Documentar el resto del Núcleo (flujos de reproducción, secciones, etc.).
Worked for 6s
Hecho.
La arquitectura híbrida de audio (biblioteca gestionada + referencias externas) queda incorporada oficialmente en la Rama 0 – Baseline.
A partir de ahora forma parte del contrato común para todas las ramas (Flutter, Python+Qt, Tauri, etc.).
Núcleo ONDAIRE – Documento de Referencia
Versión: 1.0
Fecha de inicio: 12/09/2026
Estado: En construcción
Alcance: Modelo de datos + lógica de trabajo + estructura de secciones.
Este documento es la fuente de verdad. Cualquier implementación (Flutter u otras) debe respetarlo.
1. Arquitectura de Almacenamiento de Audio (Nueva – Baseline)
1.1 Principio general
- Nunca duplicar archivos innecesariamente.
- Soporte de dos orígenes:
| Tipo | Descripción | Copia física | Ruta guardada |
|---|---|---|---|
| managed | Archivo dentro de la estructura de carpetas de la aplicación | No (recomendado) o Sí (opcional) | Ruta relativa |
| external | Archivo en cualquier ubicación del disco o red | No | Ruta absoluta / UNC |
1.2 Estructura de carpetas gestionada (Managed)
text
CopyCopied
Audios/ ← Carpeta raíz de la aplicación
├── Musica/
│ ├── Rock/
│ ├── Pop/
│ ├── Folklore/
│ ├── Tango/
│ ├── Baladas/
│ └── ... (carpetas creadas por el usuario)
├── Artistica/
│ ├── Pisadores/
│ ├── Separadores/
│ ├── Promos/
│ ├── Identificaciones/
│ ├── Cortinas/
│ └── ...
└── Publicidades/
└── ... (opcionalmente subcarpetas por cliente o campaña)
- La aplicación es dueña de esta estructura.
- Se puede implementar un escáner automático (al inicio y/o con vigilancia de cambios) que detecte archivos nuevos y los registre en la base de datos.
- Las subcarpetas pueden mapearse automáticamente a categorías.
1.3 Archivos externos
- El usuario puede seleccionar cualquier archivo o carpeta del sistema.
- Solo se almacena la ruta + metadatos.
- No se realiza copia.
- Se recomienda guardar también:
- Tamaño
- Fecha de modificación
- Hash opcional (para detectar cambios o movimientos)
1.4 Modelo de datos de un Audio (común para Songs, Jingles y Spots)
text
CopyCopied
Audio {
id : string (único)
title : string
artist : string (opcional)
duration : number (segundos)
categories : string[] // IDs de categorías
sourceType : "managed" | "external"
path : string // relativa (managed) o absoluta (external)
fileSize : number (opcional)
lastModified : datetime (opcional)
hash : string (opcional)
// campos específicos según tipo (song / jingle / spot)
}
2. Relación entre Carpetas y Categorías
2.1 Principio
- Las categorías siguen siendo entidades independientes (tienen nombre + color).
- Las carpetas del sistema de archivos pueden mapearse a categorías de forma automática o manual.
2.2 Reglas propuestas
- Mapeo automático por nombre de carpeta
Si existe una categoría cuyo nombre coincide (case-insensitive) con el nombre de la subcarpeta → se asigna automáticamente esa categoría al audio. 2. Mapeo manual
El usuario puede asignar/quitar categorías independientemente de la ubicación del archivo. 3. Categoría especial “Sin categoría”
Para archivos que no coinciden con ninguna carpeta ni tienen asignación manual. 4. Comportamiento al escanear
- Archivo nuevo en Audios/Musica/Rock/ → se crea registro + se intenta asignar categoría “Rock”.
- Archivo externo → no se asigna categoría automáticamente (el usuario la elige).
- Cambio de carpeta
Si el usuario mueve un archivo managed de una subcarpeta a otra, se puede ofrecer actualizar la categoría o dejarla como está.
3. Resto del Núcleo (en construcción)
Voy a continuar documentando en el siguiente orden:
3.1 Modelo de datos completo
- Configuración general
- Categorías musicales y de artística
- Parrilla (franjas horarias + patrones)
- Tandas y tandaManual
- Clientes / Contratos publicitarios
- Cola de reproducción
- Historial
- Estado del día (counts, played, plan)
- Botoneras
- Índices de rotación y uso
3.2 Flujos de reproducción críticos
- Lógica de advance()
- Resolución de patrones (TODAS, categoría específica, CUÑA aleatoria, categoría de jingle)
- Manejo de tandas (auto / manual + límite de minutos/hora)
- Botoneras (pisador con ducking vs separador)
- Streaming
- Top de hora
- Stop-after / Rewind
3.3 Estructura lógica de la interfaz (secciones)
- Header + Deck + Transport + barra de progreso
- Pestañas y contenido de cada una
- Consola (patrón activo, botonera, cola, explorador, pases del día, estado de tandas)
Estado actual:
- Sección 1 (Almacenamiento de Audio) → Completada e incorporada al Baseline
- Sección 2 (Relación Carpetas ↔ Categorías) → Completada
- Sección 3 → En progreso
¿Querés que continúe ahora mismo con el Modelo de datos completo (punto 3.1), o preferís revisar/ajustar primero lo que ya escribí sobre almacenamiento y carpetas?
También puedo profundizar más en algún punto concreto (por ejemplo reglas exactas del escáner automático o manejo de rutas de red) antes de seguir.
1.2 Estructura de carpetas gestionada (Managed) textCopiarCopiado
creo que hay un error en esta estructura. la carpeta raíz de la aplicación no es "Audios", la carpeta rais es la carpeta inmediata superior a "Audios", corregí esto y continua
Worked for 2s
Corregido.
La carpeta raíz de la aplicación es la carpeta inmediata superior a Audios.
Audios es solo una subcarpeta dentro de la aplicación.
1.2 Estructura de carpetas gestionada (Managed) — Versión corregida
text
CopyCopied
[Application Root]/ ← Raíz real de la aplicación
├── Audios/ ← Carpeta de medios gestionados
│ ├── Musica/
│ │ ├── Rock/
│ │ ├── Pop/
│ │ ├── Folklore/
│ │ ├── Tango/
│ │ ├── Baladas/
│ │ └── ... (subcarpetas definidas por el usuario)
│ ├── Artistica/
│ │ ├── Pisadores/
│ │ ├── Separadores/
│ │ ├── Promos/
│ │ ├── Identificaciones/
│ │ ├── Cortinas/
│ │ └── ...
│ └── Publicidades/
│ └── ... (opcionalmente subcarpetas por cliente o campaña)
├── data/ ← (sugerido) base de datos, configuración, logs
├── (ejecutable / archivos de la aplicación)
└── ...
Notas importantes:
- Todas las rutas managed se guardan como rutas relativas a partir de [Application Root]/Audios/...
- La aplicación debe conocer en todo momento dónde está su raíz (especialmente importante en Flutter y en instalaciones portables).
- Los archivos external siguen usando rutas absolutas o de red.
Continúo con el resto del Núcleo.
3.1 Modelo de datos completo (primera versión)
Configuración general (config)
text
CopyCopied
config {
volume : float (0.0 – 1.0)
maxMinutesPerHour : int
duckLevel : float (0.0 – 1.0)
rotation : "shuffle" | "sequential"
fallback : bool // colchón musical
respectTandas : bool
fadeInTime : int (ms)
fadeOutTime : int (ms)
crossfadeTime : int (ms)
stopFadeTime : int (ms)
rewindSeconds : int
buttoneraName : string
topHourEnabled : bool
topHourVolume : float
streamUrl : string
streamEnabled : bool
streamFade : int (ms)
}
Categorías
text
CopyCopied
Category {
id : string
name : string
color : string (hex)
type : "music" | "jingle" // para distinguir
}
Audio (base común)
text
CopyCopied
Audio {
id : string
title : string
artist : string?
duration : float // segundos
categories : string[] // ids de Category
sourceType : "managed" | "external"
path : string // relativa (managed) o absoluta (external)
fileSize : int?
lastModified : datetime?
hash : string?
}
Especializaciones:
- Song extends Audio
- Jingle extends Audio
- Spot extends Audio
Parrilla
text
CopyCopied
ParrillaBlock {
id : string
desde : string ("HH:MM")
hasta : string ("HH:MM")
pattern : PatternItem[]
}
PatternItem =
| "ALL" // todas las canciones
| categoryId // categoría musical
| "JINGLE_RANDOM" // cuña aleatoria
| "JCAT_" + jingleCategoryId // categoría específica de artística
Tandas y Publicidad
text
CopyCopied
tandaManual {
[hora: string] : {
mode : "auto" | "manual"
spots : string[] // ids de Spot (solo en modo manual)
}
}
Client {
id : string
nombre : string
spotId : string
pases : int
desde : string ("HH:MM")
hasta : string ("HH:MM")
activo : bool
}
Botoneras
text
CopyCopied
Buttoner {
id : string
name : string
type : "pisador" | "separador"
loopAll : bool
cols : int
rows : int
items : ButtonItem[]
}
ButtonItem {
id : string
audioId : string? // referencia a Audio
name : string
loop : bool
}
Estado de reproducción y día
text
CopyCopied
DayState {
date : string ("YYYY-MM-DD")
counts : { [clientId]: int } // pases emitidos hoy
played : { [tandaHora]: bool }
plan : { [tandaHora]: string[] } // clientIds planificados
}
RuntimeState {
mode : "AUTO" | "MANUAL"
queue : string[] // ids de Audio
patternIdx : int
jingleIdx : int
pendingTanda : ...
stopAfterCurrent : bool
streaming : bool
// índices de uso para rotación (songCategoryUsage, allSongsUsage, etc.)
}
¿Querés que continúe ahora con los flujos de reproducción críticos (advance, resolución de patrones, tandas, botoneras, etc.) o preferís revisar/ajustar primero este modelo de datos?
continuá
Worked for 1s
Continuamos con el Núcleo.
3.2 Flujos de reproducción críticos
Estos flujos son el corazón operativo del sistema. Deben comportarse de la misma forma en cualquier rama de desarrollo.
3.2.1 Motor principal – advance()
Lógica de decisión (en orden de prioridad):
- Si está en modo Streaming → no hacer nada (el stream tiene control exclusivo).
- Si existe un elemento pendiente (pending):
- jingle → reproducir siguiente jingle
- tanda → iniciar tanda
- ad → reproducir spot específico
- Si la Cola (queue) tiene elementos → sacar el primero y reproducirlo.
- Si el modo es AUTO:
- Obtener el bloque de parrilla actual según la hora.
- Avanzar el patternIdx dentro del patrón del bloque.
- Resolver el ítem del patrón:
- "ALL" → siguiente canción de todo el repertorio (respetando rotación)
- categoryId → siguiente canción de esa categoría
- "JINGLE_RANDOM" → siguiente jingle aleatorio/rotativo
- "JCAT_" + id → siguiente jingle de esa categoría de artística
- Si no se pudo resolver nada y fallback = true → reproducir cualquier canción disponible.
- Si no hay nada → pasar a estado de espera (Standby).
Reglas de rotación:
- shuffle: evita repetir hasta agotar el pool disponible de la categoría/repertorio.
- sequential: orden fijo, cicla al terminar.
3.2.2 Resolución de patrones
El patrón es una lista ordenada que se repite indefinidamente mientras el bloque horario esté activo.
Ejemplos de ítems válidos:
- ALL
- cat_rock
- JINGLE_RANDOM
- JCAT_jcat_pisador
- JCAT_jcat_promo
El índice (patternIdx) se incrementa después de cada reproducción exitosa del patrón y se reinicia al llegar al final.
3.2.3 Tandas publicitarias
Disparadores:
- Automático: cuando la hora actual coincide con una tanda definida y aún no se reprodujo hoy (respectTandas = true).
- Manual: botón “TANDA” o “Emitir tanda pendiente”.
Lógica de armado de la tanda:
- Si la tanda está en modo manual → usar la lista de spots definida por el usuario.
- Si está en modo auto:
- Tomar los clientes activos cuyo horario cubra la tanda.
- Respetar la cantidad de pases diarios restantes.
- Aplicar el límite de minutos por hora (maxMinutesPerHour).
- Distribuir los pases de forma equilibrada entre las tandas elegibles (algoritmo de spread).
Durante la reproducción de la tanda:
- Se encola internamente el resto de los spots (tandaQ).
- Al terminar un spot se reproduce el siguiente de la tanda.
- Al finalizar toda la tanda se vuelve a advance().
3.2.4 Botoneras
Dos tipos de comportamiento:
Pisador (type = "pisador"):
- Baja el volumen del programa principal (ducking) según duckLevel.
- Reproduce el audio del botón por encima.
- Al terminar (si no está en loop) restaura el volumen del programa con fade-in.
- No detiene la reproducción principal.
Separador (type = "separador"):
- Hace fade-out del programa principal y lo pausa.
- Reproduce el audio del botón.
- Al terminar (si no está en loop) continúa con advance().
Comportamiento común:
- Primer toque → reproduce.
- Segundo toque mientras suena → detiene con fade.
- Soporte de loop individual o de toda la botonera.
- Se puede asignar cualquier audio (managed o external) a un botón.
3.2.5 Otros flujos importantes
- Stop After Current: al terminar el ítem actual va a Standby en lugar de advance().
- Rewind: retrocede N segundos en el ítem actual.
- Top de Hora: genera y reproduce un beep (o audio configurable) en minuto 00 y 30 si está activado.
- Streaming: toma el control exclusivo del reproductor principal. Al detenerlo vuelve a advance() si el modo es AUTO.
- Fade / Crossfade: todos los cambios de fuente usan los tiempos configurados (fadeInTime, fadeOutTime, crossfadeTime, stopFadeTime).
3.3 Estructura lógica de la interfaz (secciones)
La disposición y el funcionamiento de las secciones deben mantenerse, aunque cambien colores, formas de botones e iconos.
Layout principal (siempre visible)
- Header: logo/marca + estado (EN ESPERA / AL AIRE / PAUSA / STREAMING) + selector AUTO/MANUAL + reloj.
- Deck: badge de tipo (MÚSICA / ARTÍSTICA / PUBLICIDAD / STREAM) + título + subtítulo + tiempo restante + tiempo transcurrido.
- Barra de progreso (clicable para seek).
- “A continuación” (preview de lo que viene).
- Transport: Stop, Play/Pause, Rewind, Stop-After, Siguiente, Tanda, Top de Hora, Streaming, Volumen.
Pestañas (Tabs)
- Consola
- Patrón activo del bloque horario actual (con indicador de posición).
- Botonera (pestañas de botoneras + grilla configurable de botones).
- Cola de reproducción (con reordenamiento y borrado).
- Explorador (búsqueda + filtros por categoría + agregar a cola).
- Pases de publicidad del día.
- Estado de tandas del día.
- Parrilla
- Lista de bloques horarios (desde – hasta + patrón).
- Editor de patrones.
- Vista timeline del día.
- Reglas globales (rotación, respetar tandas, colchón musical).
- Publicidad
- Límite de minutos/hora (con indicador de uso).
- Gestión de tandas (crear, generar, modo auto/manual, spots manuales).
- Clientes/Contratos (altas, pases, horarios, estado del día).
- Categorías
- Categorías musicales (nombre + color).
- (Las de artística se gestionan en la pestaña Artística).
- Música
- Biblioteca de canciones.
- Carga de archivos/carpetas (con opción de categoría destino y “por nombre de carpeta”).
- Filtro por categoría.
- Asignación manual de categorías.
- Artística
- Categorías de artística.
- Biblioteca de jingles/cuñas.
- Misma lógica de carga y categorización que Música.
- Config
- Nombre de la botonera.
- Streaming (URL + fade).
- Fades y ducking.
- Top de hora.
- Respaldo (exportar/importar configuración y base de datos completa).
- Mantenimiento (demo, borrar todo).
- Historial
- Registro de lo emitido (hora, tipo, título, origen).
- Exportar CSV + limpiar.
Estado del documento del Núcleo:
- Arquitectura de almacenamiento de audio → Completada (con corrección de raíz)
- Relación Carpetas ↔ Categorías → Completada
- 3.1 Modelo de datos → Primera versión completa
- 3.2 Flujos de reproducción → Completado
- 3.3 Estructura de secciones → Completado
¿Querés que ahora:
A. Profundice algún flujo o sección específica,
B. Agregue reglas de importación/escaneo automático de la carpeta Audios,
C. Pase a definir la arquitectura técnica inicial de la Rama 1 – Flutter (estructura de proyecto, estado, audio, persistencia),
o preferís revisar primero todo lo documentado hasta aquí?
correcciones:
"Lógica de decisión (en orden de prioridad):
- Si está en modo Streaming → no hacer nada (el stream tiene control exclusivo)." aquí hay que agregar una condición a la exclusividad. Si es es el horario de la tanda tener la posibilidad de activar o desactivar el disparo automático de la tanda y luego retorna al streaming.
"Si el modo es AUTO:
- Obtener el bloque de parrilla actual según la hora." En este punto el bloque de parrilla debe ser según "el día de la semana y la hora" (es algo que la rama base no tiene pero que hay que agregar.
En "Lógica de armado de la tanda" agregar la posibilidad mixta de las dos opciones entre "auto" y "manual" pero que la opción auto primero verifique que no exista el spot ya cargado en modo manual. El modo manual da la opción de un orden concreto de reproducción de los spot, modelo elegido por algunos auspiciantes para reproducir mas de un spot propio en la misma tanda.
En "3.2.4 Botoneras": Para todos los casos si en el reproductor principal se da "reproducir al siguiente audio" o "activa streaming", el boton se detiene con el fade programado para el stop de la botonera aún estando en loop.
Layout principal (siempre visible): hay que agregar un vúmetro en el Header, a la deracha de todo lo existente salvo el reloj que debe quedar a la derecha de todo. No esta implementado en la rama base porque generaba problemas.
Worked for 10s
Correcciones incorporadas al Núcleo.
Actualizaciones realizadas
3.2.1 Motor principal – advance() (corregido)
Lógica de decisión (en orden de prioridad):
- Streaming activo:
- El stream tiene control exclusivo del reproductor principal.
- Excepción: Si la hora actual corresponde a una tanda y el disparo automático de tandas está habilitado para streaming, se puede interrumpir el stream, emitir la tanda, y al finalizar volver automáticamente al streaming.
- Debe existir una opción de configuración para activar/desactivar este comportamiento (“Permitir tandas durante streaming”).
- Si existe un elemento pendiente (pending) → resolverlo (jingle / tanda / ad).
- Si la Cola tiene elementos → reproducir el primero.
- Si el modo es AUTO:
- Obtener el bloque de parrilla actual según día de la semana + hora.
- Continuar con la resolución del patrón.
- Fallback / Standby.
3.1 Modelo de datos – Parrilla (ampliado)
text
CopyCopied
ParrillaBlock {
id : string
days : int[] // 0=Domingo ... 6=Sábado (o bandera “todos”)
desde : string ("HH:MM")
hasta : string ("HH:MM")
pattern : PatternItem[]
}
- Se agrega el campo days para soportar programación por día de la semana.
- Compatibilidad: si days está vacío o contiene todos los días → se comporta como el sistema actual (todos los días).
3.2.3 Tandas publicitarias – Lógica de armado (corregida)
Se incorpora modo mixto:
- Modo Manual:
- El usuario define una lista ordenada de spots.
- Se respeta estrictamente el orden (requerimiento de algunos auspiciantes que quieren varios spots propios en la misma tanda).
- Modo Auto:
- Primero se verifican los spots que ya estén cargados en la lista manual de esa tanda.
- Esos spots no se vuelven a agregar desde el armado automático (se evita duplicación).
- Luego se completan los pases restantes con la lógica automática de clientes (respetando pases diarios, horario del cliente y límite de minutos/hora).
- Modo Mixto(combinación de ambos):
- Se reproduce primero la lista manual (en el orden definido).
- Después se continúan con los spots que el modo auto haya calculado y que no estén ya incluidos en la lista manual.
3.2.4 Botoneras (comportamiento ampliado)
Regla nueva aplicable a todos los tipos (pisador y separador):
- Si mientras un botón está sonando (aunque esté en loop) el operador:
- Presiona “Siguiente” en el transport, o
- Activa Streaming,
- Entonces el botón de la botonera debe detenerse aplicando el fade configurado en stopFadeTime.
Esto evita que queden audios de botonera sonando por encima del nuevo contenido.
3.3 Layout principal (siempre visible) – Actualizado
Orden de elementos en el Header (de izquierda a derecha):
- Marca / Logo + estado (EN ESPERA / AL AIRE / PAUSA / STREAMING)
- Selector de modo AUTO / MANUAL
- Vúmetro (nuevo)
- Reloj (siempre a la extrema derecha)
Notas sobre el Vúmetro:
- Debe mostrar nivel de salida del programa principal (y opcionalmente de la botonera cuando está activa).
- No estaba implementado en la rama base por problemas de rendimiento/estabilidad en el navegador.
- En las nuevas ramas (Flutter y posteriores) se considera requisito de diseño.
Estado actual del Núcleo:
- Todas las correcciones solicitadas han sido incorporadas.
- El documento refleja ahora:
- Streaming con posibilidad de tandas intermedias.
- Parrilla por día de la semana + hora.
- Tandas con soporte manual / auto / mixto.
- Comportamiento de detención forzada de botoneras.
- Vúmetro en el Header.
¿Continuamos con el siguiente bloque o querés revisar/ajustar algo de estas correcciones antes de seguir?
Opciones naturales ahora:
- Reglas detalladas de importación y escaneo de la carpeta Audios
- Arquitectura técnica inicial de la Rama 1 (Flutter)
- Cualquier otro punto del Núcleo que quieras profundizar
nuevas correcciones:
"3.2.3 Tandas publicitarias – Lógica de armado (corregida)" Siempre al terminar la tanda se debe reproducir una artistica, de una categría configurable, antes de volver a la programación normal.
Worked for 6s
Corrección incorporada.
3.2.3 Tandas publicitarias – Lógica de armado (actualizada)
Comportamiento al finalizar una tanda:
Al terminar el último spot de la tanda (ya sea modo manual, auto o mixto), siempre se debe reproducir una pieza de Artística antes de volver a la programación normal (advance()).
Reglas:
- La categoría de artística a utilizar es configurable (ejemplo: “Separadores”, “Cortinas”, “Identificaciones”, etc.).
- Se elige el siguiente jingle de esa categoría respetando la rotación configurada (shuffle o secuencial).
- Si la categoría configurada no tiene audios disponibles, se omite la artística y se continúa directamente con advance().
- Este comportamiento es obligatorio y no depende del modo de la tanda.
Flujo completo de una tanda:
- Se dispara la tanda (automática o manual).
- Se reproducen los spots según el modo (manual / auto / mixto).
- Al terminar el último spot → se reproduce una artística de la categoría configurada.
- Recién después se vuelve a la lógica normal de advance().
Esta regla queda agregada al Núcleo como comportamiento obligatorio del sistema.
¿Hay más correcciones o continuamos con el siguiente bloque del documento?
continua
Worked for 4s
Continuamos con el Núcleo.
3.4 Reglas de Importación y Escaneo de Audio
3.4.1 Principios generales
- Nunca duplicar archivos innecesariamente.
- Soporte simultáneo de:
- Managed (dentro de [Application Root]/Audios/...)
- External (cualquier ruta del sistema o red)
- Las categorías se pueden asignar automáticamente por nombre de carpeta o manualmente.
3.4.2 Escaneo de la biblioteca gestionada (Audios/)
Cuándo se ejecuta:
- Al iniciar la aplicación.
- Manualmente (botón “Escanear biblioteca”).
- Opcionalmente con vigilancia de cambios en segundo plano (file watcher).
Comportamiento del escáner:
- Recorre recursivamente:
- Audios/Musica/
- Audios/Artistica/
- Audios/Publicidades/
- Por cada archivo de audio válido encontrado:
- Si ya existe en la base de datos (mismo path relativo) → actualiza metadatos si cambiaron (tamaño, fecha de modificación, duración).
- Si no existe → crea un nuevo registro.
- Asignación automática de categoría:
- Se toma el nombre de la subcarpeta inmediata (ejemplo: Audios/Musica/Rock/tema.mp3 → intenta categoría “Rock”).
- La comparación es case-insensitive y se normalizan espacios.
- Si existe una categoría con ese nombre → se asigna.
- Si no existe → el audio queda sin categoría (o se puede ofrecer crearla).
- Tipo de registro según carpeta raíz:
- Audios/Musica/ → se registra como Song
- Audios/Artistica/ → se registra como Jingle
- Audios/Publicidades/ → se registra como Spot
- Archivos que ya no están en disco:
- Se marcan como “faltantes” (no se borran automáticamente de la base de datos para no perder historial ni referencias en botoneras/tandas).
- El usuario puede limpiarlos manualmente.
3.4.3 Importación manual de archivos/carpetas
El usuario tiene dos modos claramente diferenciados:
A. Importar a la biblioteca gestionada
- Se copian o se mueven los archivos dentro de Audios/Musica, Audios/Artistica o Audios/Publicidades (según el tipo elegido).
- Se puede elegir subcarpeta de destino (o crear una nueva).
- Después de la copia/movimiento se ejecuta la lógica de registro + categorización automática.
B. Agregar como External
- No se copia ni se mueve nada.
- Solo se guarda la ruta absoluta (o de red) + metadatos.
- El usuario asigna manualmente el tipo (Song / Jingle / Spot) y las categorías.
- Útil para material que reside en discos externos, NAS, carpetas compartidas, etc.
3.4.4 Campos mínimos que se guardan al importar/escanear
text
CopyCopied
- id
- title (nombre del archivo sin extensión, editable después)
- artist (vacío por defecto, editable)
- duration
- categories[]
- sourceType ("managed" | "external")
- path
- fileSize
- lastModified
- hash (opcional pero recomendable)
3.4.5 Validaciones
- Solo se aceptan formatos de audio comunes (mp3, wav, ogg, m4a, aac, flac, opus, etc.).
- Se ignora cualquier archivo que no sea de audio.
- Se evita registrar el mismo archivo managed dos veces (clave = path relativo).
- En archivos external se puede permitir la misma ruta más de una vez solo si el usuario lo fuerza (caso raro).
Estado del Núcleo hasta aquí:
- Arquitectura de almacenamiento → Completa
- Relación carpetas ↔ categorías → Completa
- Modelo de datos → Completo (con días de la semana)
- Flujos de reproducción → Completos (con todas las correcciones)
- Estructura de secciones + Header con vúmetro → Completa
- Reglas de importación y escaneo → Completas
¿Seguimos ahora con la arquitectura técnica inicial de la Rama 1 – Flutter (estructura de proyecto, gestión de estado, motor de audio, persistencia, etc.), o preferís agregar/ajustar algo más del Núcleo antes de pasar a la implementación?
Correcciones y modificciones:
3.4.4 Campos mínimos que se guardan al importar/escanear agregar un campo "filename" con el nombre completo del archivo modificar la logica de asigancion de title y artist. En la configuracion de la carga de audios tiene que haber una opcion de definir formato de nombre de archivo donde segun el nombre se tomen automaticamente estos campos y si no existe esa estructura de nombre se utiliza la logica ya presentada para title y artist. Un ejemplo de nombre de archivo: "artista - titulo de cancion.mp3" donde artista corresponde al campo "artist" y titulo de la cancion corresponde a "title". La estructura debe ser configurble para adaptarse a la metodologia de trabajo de cada emisora.
Worked for 8s
Corrección incorporada.
3.4.4 Campos mínimos que se guardan al importar/escanear (actualizado)
text
CopyCopied
- id
- filename ← nombre completo del archivo (con extensión)
- title
- artist
- duration
- categories[]
- sourceType ("managed" | "external")
- path
- fileSize
- lastModified
- hash (opcional pero recomendable)
3.4.5 Lógica de asignación de title y artist (nueva)
En la configuración de carga de audios debe existir una opción para definir el formato de nombre de archivo.
Comportamiento:
- El usuario puede configurar un patrón de extracción (ejemplo clásico):
text
CopyCopied
{artist} - {title}
- Al importar o escanear un archivo:
- Se toma el filename sin la extensión.
- Se intenta aplicar el patrón configurado.
- Si el nombre coincide con el patrón → se extraen automáticamente los campos artist y title.
- Si no coincide con el patrón → se aplica la lógica por defecto:
- title = nombre del archivo sin extensión
- artist = vacío
- El patrón debe ser configurable para adaptarse a la metodología de trabajo de cada emisora.
Ejemplos de patrones posibles:
| Patrón configurado | Nombre de archivo | Resultado |
|---|---|---|
| {artist} - {title} | Coldplay - Yellow.mp3 | artist: Coldplay title: Yellow |
| {title} - {artist} | Yellow - Coldplay.mp3 | artist: Coldplay title: Yellow |
| {artist} – {title} | Luis Miguel – La Incondicional.mp3 | artist: Luis Miguel title: La Incondicional |
| {title} | Solo el titulo.mp3 | artist: (vacío) title: Solo el titulo |
| (sin patrón / no coincide) | tema_sin_formato.mp3 | artist: (vacío) title: tema_sin_formato |
Notas de implementación:
- Se deben soportar separadores comunes (-, –, —, _, etc.).
- El patrón debe ser flexible (el usuario puede definir el orden y el separador).
- Después de la extracción automática, tanto title como artist siguen siendo editables manualmente.
- Esta configuración es global (se guarda en config).
Estado: Corrección aplicada al Núcleo.
¿Continuamos con la arquitectura técnica de la Rama 1 (Flutter) o hay más ajustes al Núcleo?
observación:
tambien pueden existir otras estructuras de nombres configurables, por ejemplo: "01 - artista - titulo de cancion.mp3" o "Album - titulo de cancion - artista", etc. el filtro de estructura debe poder adaptarse a esas necesidades con la posibilidad de omitir campos innecesarios como ser el número de orden del archivo.
Agregar también en los campos el nombre del Album y el año de publicación (simplemente como año)
Worked for 3s
Observación incorporada.
3.4.5 Lógica de asignación de metadatos desde el nombre de archivo (actualizada)
El sistema de patrones debe ser lo suficientemente flexible para adaptarse a distintas metodologías de trabajo de cada emisora.
Requisitos del sistema de patrones:
- El usuario puede definir el orden de los campos.
- Puede elegir el separador ( -, –, —, _, |, etc.).
- Puede omitir campos que no le interesen (ejemplo: número de pista).
- Los campos soportados en el patrón son:
| Campo en el patrón | Se guarda en | Obligatorio |
|---|---|---|
| {track} | (se puede omitir) | No |
| {artist} | artist | No |
| {title} | title | No |
| {album} | album | No |
| {year} | year | No |
Ejemplos de patrones válidos:
| Patrón configurado | Nombre de archivo | Resultado extraído |
|---|---|---|
| {artist} - {title} | Coldplay - Yellow.mp3 | artist: Coldplay title: Yellow |
| {track} - {artist} - {title} | 01 - Coldplay - Yellow.mp3 | artist: Coldplay title: Yellow (track se omite) |
| {album} - {title} - {artist} | Parachutes - Yellow - Coldplay.mp3 | album: Parachutes title: Yellow artist: Coldplay |
| {year} - {artist} - {title} | 2000 - Coldplay - Yellow.mp3 | year: 2000 artist: Coldplay title: Yellow |
| {track} - {title} | 03 - Solo el titulo.mp3 | title: Solo el titulo |
| {title} | Tema sin formato.mp3 | title: Tema sin formato |
Si el nombre del archivo no coincide con el patrón configurado, se aplica la lógica por defecto:
- title = nombre del archivo sin extensión
- artist, album y year quedan vacíos.
3.4.4 Campos mínimos actualizados
text
CopyCopied
- id
- filename // nombre completo del archivo (con extensión)
- title
- artist
- album // nuevo
- year // nuevo (solo el año, ejemplo: 2000)
- duration
- categories[]
- sourceType // "managed" | "external"
- path
- fileSize
- lastModified
- hash // opcional
Notas:
- album y year son campos opcionales y editables manualmente después de la importación.
- El patrón de extracción se guarda en la configuración global (config).
- Se recomienda permitir varios patrones predefinidos + uno personalizado.
¿Hay más ajustes sobre metadatos o importación, o continuamos con la arquitectura técnica de la Rama 1 – Flutter?
Notas:
album y year son campos opcionales y editables manualmente después de la importación.
los campos title y artist tambien deben ser editables manualmente con posterioridad para realizar correcciones.
Worked for 3s
Corrección aplicada.
Notas actualizadas:
- Los campos title, artist, album y year son todos editables manualmente después de la importación o el escaneo.
- Esto permite corregir errores de extracción automática o completar información faltante.
- El patrón de nombre de archivo solo se usa como ayuda inicial al momento de la carga. Una vez importado el audio, el usuario tiene control total sobre estos cuatro campos.
¿Continuamos ahora con la arquitectura técnica de la Rama 1 (Flutter) o hay más puntos del Núcleo que quieras ajustar?
Explorar validación de metadatos
Worked for 3s
Exploración: Validación de Metadatos
A continuación se define una propuesta de reglas de validación para los metadatos de audio. Esto formará parte del Núcleo una vez que lo validemos.
1. Campos y nivel de obligatoriedad
| Campo | Obligatorio | Editable manual | Notas |
|---|---|---|---|
| id | Sí | No | Generado por el sistema |
| filename | Sí | No | Nombre original del archivo (con extensión) |
| title | Sí | Sí | Nunca puede quedar vacío |
| artist | No | Sí | Puede quedar vacío |
| album | No | Sí | Puede quedar vacío |
| year | No | Sí | Solo año (número) |
| duration | Sí | No* | Se obtiene del archivo. *Solo lectura salvo casos especiales |
| categories | No | Sí | Lista de IDs |
| sourceType | Sí | No | "managed" o "external" |
| path | Sí | No | Ruta relativa o absoluta |
| fileSize | No | No | Informativo |
| lastModified | No | No | Informativo |
| hash | No | No | Informativo / detección de cambios |
2. Reglas de validación por campo
title
- No puede estar vacío ni contener solo espacios.
- Longitud máxima recomendada: 200 caracteres.
- Se recortan espacios al inicio y al final.
- Caracteres de control se eliminan.
artist
- Puede estar vacío.
- Longitud máxima recomendada: 150 caracteres.
- Se recortan espacios.
album
- Puede estar vacío.
- Longitud máxima recomendada: 150 caracteres.
- Se recortan espacios.
year
- Puede estar vacío.
- Si se informa, debe ser un número entero de 4 dígitos.
- Rango válido recomendado: 1900 – año actual + 1.
- No se aceptan valores como “2024-2025”, “90s”, “unknown”, etc.
- Al pegar o importar, se intenta extraer solo los 4 dígitos si es posible.
filename
- Debe conservar el nombre original completo (incluyendo extensión).
- No se modifica nunca después de la importación.
duration
- Debe ser un número ≥ 0.
- Si no se puede leer del archivo, se guarda como 0 y se marca el audio como “sin duración detectada”.
categories
- Solo se aceptan IDs de categorías existentes.
- Se eliminan duplicados automáticamente.
3. Comportamiento en distintos momentos
Al importar / escanear:
- Se aplica el patrón de nombre de archivo (si está configurado).
- Se ejecutan las validaciones.
- Si title queda vacío después del patrón → se fuerza el nombre del archivo sin extensión.
- Si year no es válido → se deja vacío (no se rechaza el archivo).
- El audio se importa siempre (salvo que el archivo no sea de audio válido).
Al editar manualmente:
- Se aplican las mismas reglas de validación.
- Si el usuario deja title vacío → se impide guardar y se muestra aviso.
- year inválido → se limpia o se rechaza el valor según se defina (recomendación: limpiar y avisar).
Al detectar archivo faltante (managed):
- No se borran los metadatos.
- Se marca el registro como missing = true.
- Se mantiene toda la información para no romper botoneras, tandas e historial.
4. Recomendaciones adicionales
- Normalizar espacios múltiples a uno solo en title, artist y album.
- Opción de configuración: “Capitalizar automáticamente título y artista” (opcional).
- Guardar un flag metadataSource: "pattern" | "manual" | "default" para saber de dónde salieron los datos originales.
- Permitir re-aplicar el patrón de nombre de archivo sobre audios ya importados (acción manual “Re-extraer metadatos”).
Pregunta para cerrar este punto
¿Querés que adoptemos estas reglas tal como están, o preferís modificar alguno de estos puntos antes de fijarlas en el Núcleo?
En particular:
- ¿El rango de year (1900 – actual+1) te parece correcto?
- ¿Preferís que un year inválido se rechace o simplemente se limpie?
- ¿Agregamos el flag metadataSource?
continuemos
Worked for 5s
Validación de metadatos adoptada con las reglas propuestas:
- Rango de year: 1900 – año actual + 1
- year inválido → se limpia (no se rechaza el archivo)
- Se agrega el flag metadataSource ("pattern" | "manual" | "default")
Queda incorporado al Núcleo.
Rama 1 – Flutter
Arquitectura Técnica Inicial
1. Objetivo de esta etapa
Definir la base técnica sobre la cual se construirá la aplicación, respetando estrictamente el Núcleo (lógica de trabajo, modelo de datos, flujos y disposición de secciones).
2. Estructura general del proyecto (propuesta)
text
CopyCopied
ondaire/
├── lib/
│ ├── main.dart
│ ├── app.dart
│ ├── core/ # Núcleo compartido
│ │ ├── models/ # Modelos de datos (Audio, Category, Parrilla, etc.)
│ │ ├── enums/
│ │ ├── constants/
│ │ └── utils/
│ ├── services/ # Lógica de negocio
│ │ ├── audio_player_service.dart
│ │ ├── library_service.dart # Escaneo + importación
│ │ ├── scheduler_service.dart # advance(), patrones, tandas
│ │ ├── buttoner_service.dart
│ │ └── storage_service.dart
│ ├── data/ # Persistencia
│ │ ├── database/
│ │ ├── repositories/
│ │ └── file_storage/
│ ├── providers/ o bloc/ o riverpod/ # Gestión de estado
│ ├── ui/
│ │ ├── screens/
│ │ │ ├── console_screen.dart
│ │ │ ├── parrilla_screen.dart
│ │ │ ├── publicidad_screen.dart
│ │ │ ├── categorias_screen.dart
│ │ │ ├── musica_screen.dart
│ │ │ ├── artistica_screen.dart
│ │ │ ├── config_screen.dart
│ │ │ └── historial_screen.dart
│ │ ├── widgets/ # Deck, Transport, Botonera, Vúmetro, etc.
│ │ └── theme/
│ └── platform/ # Código específico Windows / Linux / Android
├── assets/
├── Audios/ # Carpeta managed (se crea en runtime)
└── ...
3. Decisiones técnicas recomendadas
| Área | Opción recomendada | Justificación |
|---|---|---|
| Gestión de estado | Riverpod | Flexible, testeable, bueno para lógica compleja |
| Base de datos | Isar o Drift (SQLite) | Rápida, buena para escritorio + móvil |
| Reproducción de audio | just_audio + audio_service | Soporte multiplataforma + background en Android |
| Escaneo de archivos | path_provider + watcher | Detección de cambios en carpeta Audios |
| Rutas y archivos | path + file_picker | Managed + External |
| Persistencia de config | Isar / SharedPreferences + JSON | — |
| Vúmetro | just_audio + análisis de niveles o paquete específico | — |
4. Capas principales
- Models → Representan exactamente el Núcleo (Audio, ParrillaBlock, Client, Buttoner, etc.).
- Services → Contienen la lógica de negocio (advance(), armado de tandas, ducking, escaneo, etc.).
- Repositories → Abstraen el acceso a base de datos y sistema de archivos.
- UI → Solo se encarga de mostrar estado y enviar intenciones (no contiene lógica de negocio).
5. Orden de implementación propuesto
- Modelos de datos + serialización
- Capa de persistencia (base de datos + manejo de rutas managed/external)
- Servicio de biblioteca (escaneo + importación + patrones de nombre)
- Motor de reproducción básico
- Servicio de programación (advance() + patrones + tandas)
- Esqueleto de UI con las 8 secciones + Header/Deck/Transport
- Botoneras + ducking
- Vúmetro
- Funciones avanzadas (streaming, top de hora, respaldos, etc.)
Pregunta clave antes de seguir:
¿Estás de acuerdo con esta arquitectura base (Riverpod + Isar/Drift + just_audio + audio_service), o preferís cambiar alguna de las decisiones técnicas principales (gestión de estado, base de datos o motor de audio)?
Una vez confirmada la base técnica, el siguiente paso concreto será definir los modelos de datos en Dart según el Núcleo.
si continuemos
Worked for 7s
Arquitectura base confirmada.
Continuamos con la definición de los modelos de datos en Dart según el Núcleo.
4. Modelos de Datos (Dart) – Primera versión
Estos modelos son la traducción directa del Núcleo. Se definirán como clases inmutables (o con copyWith) para facilitar el uso con Riverpod.
4.1 Enums principales
Dart
CopyCopied
enum SourceType { managed, external }
enum RotationMode { shuffle, sequential }
enum ButtonerType { pisador, separador }
enum TandaMode { auto, manual, mixed }
enum MetadataSource { pattern, manual, default_ }
enum AudioKind { song, jingle, spot }
4.2 Configuración general
Dart
CopyCopied
class AppConfig {
final double volume;
final int maxMinutesPerHour;
final double duckLevel;
final RotationMode rotation;
final bool fallback;
final bool respectTandas;
final bool allowTandasDuringStreaming; // nuevo
final int fadeInTime;
final int fadeOutTime;
final int crossfadeTime;
final int stopFadeTime;
final int rewindSeconds;
final String buttoneraName;
final bool topHourEnabled;
final double topHourVolume;
final String streamUrl;
final int streamFade;
final String filenamePattern; // ejemplo: "{track} - {artist} - {title}"
final String postTandaJingleCategoryId; // categoría de artística obligatoria al terminar tanda
}
4.3 Categoría
Dart
CopyCopied
class Category {
final String id;
final String name;
final String color; // hex
final bool isJingleCategory; // true = artística, false = música
}
4.4 Audio (base)
Dart
CopyCopied
class AudioItem {
final String id;
final String filename;
final String title;
final String? artist;
final String? album;
final int? year;
final double duration; // segundos
final List<String> categories; // ids
final SourceType sourceType;
final String path; // relativa o absoluta
final int? fileSize;
final DateTime? lastModified;
final String? hash;
final MetadataSource metadataSource;
final AudioKind kind; // song / jingle / spot
final bool missing; // true si el archivo ya no está
}
4.5 Parrilla
Dart
CopyCopied
class ParrillaBlock {
final String id;
final List<int> days; // 0=Domingo ... 6=Sábado (vacío = todos los días)
final String desde; // "HH:MM"
final String hasta; // "HH:MM"
final List<String> pattern; // ["ALL", "cat_xxx", "JINGLE_RANDOM", "JCAT_xxx", ...]
}
4.6 Publicidad
Dart
CopyCopied
class Client {
final String id;
final String nombre;
final String spotId;
final int pases;
final String desde;
final String hasta;
final bool activo;
}
class TandaConfig {
final TandaMode mode;
final List<String> manualSpots; // ids de spots (ordenados)
}
4.7 Botoneras
Dart
CopyCopied
class ButtonItem {
final String id;
final String? audioId;
final String name;
final bool loop;
}
class Buttoner {
final String id;
final String name;
final ButtonerType type;
final bool loopAll;
final int cols;
final int rows;
final List<ButtonItem> items;
}
4.8 Estado del día y runtime
Dart
CopyCopied
class DayState {
final String date; // "YYYY-MM-DD"
final Map<String, int> counts; // clientId → pases emitidos
final Map<String, bool> played; // hora de tanda → ya se emitió
final Map<String, List<String>> plan; // hora → lista de clientIds
}
class RuntimeState {
final String mode; // "AUTO" | "MANUAL"
final List<String> queue;
final int patternIdx;
final int jingleIdx;
final bool stopAfterCurrent;
final bool streaming;
// + mapas de uso para rotación (songCategoryUsage, etc.)
}
Próximos pasos inmediatos
- Definir cómo se serializan estos modelos (JSON / Isar).
- Crear los repositorios básicos.
- Implementar el servicio de biblioteca (escaneo + patrones de nombre).
- Empezar el motor de reproducción.
¿Querés que ahora baje al detalle de serialización + estructura de base de datos (Isar o Drift), o preferís que primero definamos las interfaces de los Services principales (LibraryService, SchedulerService, AudioPlayerService, etc.)?
Detalla la serialización JSON de los modelos
Worked for 6s
Serialización JSON de los modelos
A continuación se detalla cómo se representan los modelos principales en JSON.
Esta será la forma canónica tanto para guardado local como para exportar/importar configuración y base de datos.
1. Enums (se serializan como string)
JSON
CopyCopied
"sourceType": "managed" | "external"
"rotation": "shuffle" | "sequential"
"buttonerType": "pisador" | "separador"
"tandaMode": "auto" | "manual" | "mixed"
"metadataSource": "pattern" | "manual" | "default"
"kind": "song" | "jingle" | "spot"
2. AppConfig
JSON
CopyCopied
{
"volume": 0.9,
"maxMinutesPerHour": 12,
"duckLevel": 0.25,
"rotation": "shuffle",
"fallback": true,
"respectTandas": true,
"allowTandasDuringStreaming": false,
"fadeInTime": 500,
"fadeOutTime": 500,
"crossfadeTime": 300,
"stopFadeTime": 300,
"rewindSeconds": 10,
"buttoneraName": "BOTONERA",
"topHourEnabled": false,
"topHourVolume": 0.5,
"streamUrl": "",
"streamFade": 500,
"filenamePattern": "{artist} - {title}",
"postTandaJingleCategoryId": "jcat_separador"
}
3. Category
JSON
CopyCopied
{
"id": "cat_rock",
"name": "Rock",
"color": "#ef4444",
"isJingleCategory": false
}
4. AudioItem
JSON
CopyCopied
{
"id": "a1b2c3d4e5",
"filename": "01 - Coldplay - Yellow.mp3",
"title": "Yellow",
"artist": "Coldplay",
"album": "Parachutes",
"year": 2000,
"duration": 269.5,
"categories": ["cat_rock", "cat_pop"],
"sourceType": "managed",
"path": "Musica/Rock/01 - Coldplay - Yellow.mp3",
"fileSize": 8542144,
"lastModified": "2024-11-15T14:32:10.000Z",
"hash": "a1b2c3d4e5f6...",
"metadataSource": "pattern",
"kind": "song",
"missing": false
}
Notas:
- path en managed es relativo a la carpeta Audios/.
- path en external es ruta absoluta o UNC.
- year puede ser null.
- artist, album pueden ser null o string vacío.
5. ParrillaBlock
JSON
CopyCopied
{
"id": "p1a2b3c4",
"days": [1, 2, 3, 4, 5],
"desde": "06:00",
"hasta": "12:00",
"pattern": [\
"ALL",\
"ALL",\
"JINGLE_RANDOM",\
"cat_rock",\
"JCAT_jcat_promo"\
]
}
- days: array de enteros (0 = Domingo … 6 = Sábado).
Array vacío [] = todos los días.
6. Client
JSON
CopyCopied
{
"id": "c9f8e7d6",
"nombre": "AUSPICIO A",
"spotId": "s1p2o3t4",
"pases": 5,
"desde": "09:00",
"hasta": "13:00",
"activo": true
}
7. TandaConfig (por hora)
JSON
CopyCopied
{
"09:00": {
"mode": "mixed",
"manualSpots": ["s1p2o3t4", "s5t6u7v8"]
},
"09:30": {
"mode": "auto",
"manualSpots": []
}
}
8. Buttoner + ButtonItem
JSON
CopyCopied
{
"id": "btnr_pis",
"name": "Pisadores",
"type": "pisador",
"loopAll": false,
"cols": 4,
"rows": 2,
"items": [\
{\
"id": "it1",\
"audioId": "a1b2c3d4e5",\
"name": "Pisador 1",\
"loop": false\
},\
{\
"id": "it2",\
"audioId": null,\
"name": "Pisador 2",\
"loop": false\
}\
]
}
9. DayState
JSON
CopyCopied
{
"date": "2026-09-12",
"counts": {
"c9f8e7d6": 2,
"c1a2b3c4": 1
},
"played": {
"09:00": true,
"09:30": false
},
"plan": {
"09:00": ["c9f8e7d6", "c1a2b3c4"],
"09:30": ["c9f8e7d6"]
}
}
10. RuntimeState (ejemplo parcial)
JSON
CopyCopied
{
"mode": "AUTO",
"queue": ["a1b2c3d4e5", "j9k8l7m6"],
"patternIdx": 3,
"jingleIdx": 12,
"stopAfterCurrent": false,
"streaming": false
}
11. Estructura completa de exportación (ejemplo)
JSON
CopyCopied
{
"version": "ondaire_v1",
"exportDate": "2026-09-12T04:46:00.000Z",
"config": { ... },
"categories": [ ... ],
"parrilla": [ ... ],
"tandas": ["09:00", "09:30", "10:00"],
"tandaManual": { ... },
"clients": [ ... ],
"buttoners": [ ... ],
"songs": [ ... ],
"jingles": [ ... ],
"spots": [ ... ],
"day": { ... }
}
Nota: En la exportación “completa” (con audios) se agregará un objeto audioFiles o se usará un formato de empaquetado (zip) que contenga los archivos managed. Los external solo llevan la ruta.
¿Querés que ahora detalle también la estructura de colecciones en Isar (o Drift), o preferís que pasemos a definir las interfaces de los Services principales?
Define las interfaces de los Services
Worked for 7s
Interfaces de los Services principales
Estas interfaces definen el contrato que debe cumplir cada servicio.
La UI y los providers solo hablan con estas interfaces, nunca con la implementación concreta.
1. LibraryService
Responsable de todo lo relacionado con la biblioteca de audio (escaneo, importación, metadatos y categorización).
Dart
CopyCopied
abstract class LibraryService {
/// Escanea la carpeta managed (Audios/) y sincroniza la base de datos
Future<ScanResult> scanManagedLibrary({bool fullScan = false});
/// Importa archivos a la biblioteca managed (copia o mueve)
Future<List<AudioItem>> importToManaged({
required List<File> files,
required AudioKind kind,
String? targetSubfolder,
bool moveFiles = false,
});
/// Agrega archivos como external (solo guarda la ruta)
Future<List<AudioItem>> addExternal({
required List<File> files,
required AudioKind kind,
List<String>? categoryIds,
});
/// Re-extrae metadatos usando el patrón configurado
Future<AudioItem> reextractMetadata(String audioId);
/// Actualiza metadatos manualmente
Future<AudioItem> updateMetadata(String audioId, {
String? title,
String? artist,
String? album,
int? year,
List<String>? categories,
});
/// Marca o desmarca un audio como faltante
Future<void> setMissing(String audioId, bool missing);
/// Elimina un audio de la base de datos (no borra el archivo físico por defecto)
Future<void> deleteAudio(String audioId, {bool deleteFile = false});
/// Obtiene audios filtrados
Future<List<AudioItem>> getAudios({
AudioKind? kind,
String? categoryId,
bool includeMissing = false,
String? search,
});
}
2. AudioPlayerService
Controla el reproductor principal, fades, ducking, streaming y el estado de reproducción.
Dart
CopyCopied
abstract class AudioPlayerService {
// Estado
Stream<PlayerState> get playerStateStream;
Stream<Duration> get positionStream;
Stream<Duration?> get durationStream;
Stream<double> get volumeStream;
Stream<List<double>>? get vuMeterStream; // niveles para el vúmetro
// Reproducción principal
Future<void> playAudio(AudioItem audio, {String? subtitle});
Future<void> pause();
Future<void> resume();
Future<void> stop({bool fade = true});
Future<void> seek(Duration position);
Future<void> playNext(); // fuerza advance o siguiente de cola
// Volumen y fades
Future<void> setVolume(double volume);
Future<void> fadeTo(double targetVolume, Duration duration);
Future<void> duck(double duckLevel, Duration fadeDuration);
Future<void> unduck(Duration fadeDuration);
// Streaming
Future<void> startStreaming(String url);
Future<void> stopStreaming({bool returnToSchedule = true});
// Botonera (usado internamente por ButtonerService)
Future<void> playOverlay(AudioItem audio, {
required bool isPisador,
bool loop = false,
Duration? fadeOutMain,
});
Future<void> stopOverlay(String buttonId, {Duration? fadeDuration});
}
3. SchedulerService
Contiene toda la lógica de programación: advance(), patrones, tandas, rotación, etc.
Dart
CopyCopied
abstract class SchedulerService {
/// Intenta avanzar al siguiente elemento según la lógica del Núcleo
Future<void> advance();
/// Fuerza la reproducción de una tanda específica
Future<void> startTanda(String hora, {bool manual = false});
/// Notifica que terminó el ítem actual (llamado por el player)
Future<void> onCurrentItemEnded();
/// Obtiene el bloque de parrilla activo según día + hora
ParrillaBlock? getCurrentBlock(DateTime now);
/// Resuelve el siguiente audio según el patrón actual
Future<AudioItem?> resolveNextFromPattern();
/// Arma la lista de spots de una tanda (auto / manual / mixed)
Future<List<AudioItem>> buildTandaSpots(String hora);
/// Después de una tanda, reproduce la artística configurada
Future<void> playPostTandaJingle();
/// Estado actual de la programación
Stream<SchedulerState> get stateStream;
}
4. ButtonerService
Gestiona las botoneras, su estado y la interacción con el reproductor.
Dart
CopyCopied
abstract class ButtonerService {
Stream<Map<String, ButtonerPlaybackState>> get stateStream;
Future<void> fireButton(String buttonerId, String itemId);
Future<void> stopButton(String buttonerId, String itemId);
Future<void> stopAll();
/// Llamado cuando el transport hace “Siguiente” o se activa streaming
Future<void> forceStopAllWithFade();
Future<void> updateButtoner(Buttoner buttoner);
Future<void> assignAudioToButton(String buttonerId, String itemId, String? audioId);
}
5. StorageService (o ConfigRepository + LibraryRepository)
Abstrae la persistencia.
Dart
CopyCopied
abstract class StorageService {
// Configuración
Future<AppConfig> loadConfig();
Future<void> saveConfig(AppConfig config);
// Categorías
Future<List<Category>> getCategories({bool jinglesOnly = false});
Future<void> saveCategory(Category category);
Future<void> deleteCategory(String id);
// Audios
Future<void> upsertAudio(AudioItem audio);
Future<AudioItem?> getAudio(String id);
Future<List<AudioItem>> getAllAudios();
// Parrilla, Clientes, Botoneras, DayState, etc.
Future<List<ParrillaBlock>> getParrilla();
Future<void> saveParrilla(List<ParrillaBlock> blocks);
Future<List<Client>> getClients();
Future<void> saveClient(Client client);
Future<Map<String, TandaConfig>> getTandaConfigs();
Future<void> saveTandaConfig(String hora, TandaConfig config);
Future<List<Buttoner>> getButtoners();
Future<void> saveButtoner(Buttoner buttoner);
Future<DayState> getDayState();
Future<void> saveDayState(DayState state);
// Export / Import
Future<File> exportConfig();
Future<File> exportFullDatabase({bool includeManagedFiles = false});
Future<void> importConfig(File file);
Future<void> importFullDatabase(File file);
}
6. Servicios de apoyo (opcionales pero recomendados)
Dart
CopyCopied
abstract class HistoryService {
Future<void> log(String kind, String title, {String? origin, double? duration});
Future<List<HistoryEntry>> getHistory({int limit = 200});
Future<File> exportCsv();
Future<void> clear();
}
abstract class ClockService {
Stream<DateTime> get nowStream; // para reloj y disparo de tandas / top de hora
}
Resumen de responsabilidades
| Service | Responsabilidad principal |
|---|---|
| LibraryService | Escaneo, importación, metadatos, categorías |
| AudioPlayerService | Reproducción, fades, ducking, streaming, vúmetro |
| SchedulerService | Lógica de advance(), patrones, tandas, rotación |
| ButtonerService | Botoneras y su interacción con el player |
| StorageService | Persistencia de todo |
| HistoryService | Registro histórico |
¿Querés que ahora baje al detalle de alguna implementación concreta (por ejemplo cómo se comunican SchedulerService ↔ AudioPlayerService), o preferís que definamos el flujo de arranque de la aplicación y el estado global con Riverpod?
Implementar Riverpod con servicios
Worked for 6s
Implementación de Riverpod con los Services
A continuación se define una estructura limpia y escalable para conectar los services con Riverpod.
1. Principios de diseño
- Los Services contienen la lógica de negocio y no saben nada de la UI.
- Riverpod se usa para:
- Proveer las instancias de los services (singleton).
- Exponer estado reactivo a la UI (AsyncNotifier, StreamNotifier, Notifier).
- La UI solo lee providers y llama métodos de los notifiers/services.
2. Providers de los Services (capa de infraestructura)
Dart
CopyCopied
// lib/providers/service_providers.dart
final storageServiceProvider = Provider<StorageService>((ref) {
return StorageServiceImpl(); // implementación concreta
});
final libraryServiceProvider = Provider<LibraryService>((ref) {
final storage = ref.watch(storageServiceProvider);
return LibraryServiceImpl(storage);
});
final audioPlayerServiceProvider = Provider<AudioPlayerService>((ref) {
return AudioPlayerServiceImpl();
});
final schedulerServiceProvider = Provider<SchedulerService>((ref) {
final storage = ref.watch(storageServiceProvider);
final player = ref.watch(audioPlayerServiceProvider);
final library = ref.watch(libraryServiceProvider);
return SchedulerServiceImpl(storage, player, library);
});
final buttonerServiceProvider = Provider<ButtonerService>((ref) {
final player = ref.watch(audioPlayerServiceProvider);
final storage = ref.watch(storageServiceProvider);
return ButtonerServiceImpl(player, storage);
});
final historyServiceProvider = Provider<HistoryService>((ref) {
final storage = ref.watch(storageServiceProvider);
return HistoryServiceImpl(storage);
});
3. Estado global de la aplicación (Runtime)
Dart
CopyCopied
// lib/providers/runtime_provider.dart
class RuntimeNotifier extends Notifier<RuntimeState> {
@override
RuntimeState build() {
// Cargar estado inicial desde storage si es necesario
return const RuntimeState(
mode: 'AUTO',
queue: [],
patternIdx: 0,
jingleIdx: 0,
stopAfterCurrent: false,
streaming: false,
);
}
void setMode(String mode) {
state = state.copyWith(mode: mode);
}
void setQueue(List<String> queue) {
state = state.copyWith(queue: queue);
}
void setStreaming(bool value) {
state = state.copyWith(streaming: value);
}
void setStopAfterCurrent(bool value) {
state = state.copyWith(stopAfterCurrent: value);
}
// ... más métodos
}
final runtimeProvider = NotifierProvider<RuntimeNotifier, RuntimeState>(() {
return RuntimeNotifier();
});
4. Providers de estado derivados de Services (recomendado)
En lugar de que la UI llame directamente a los services para todo, creamos notifiers que orquestan:
Ejemplo: Player State
Dart
CopyCopied
final playerStateProvider = StreamProvider<PlayerState>((ref) {
final player = ref.watch(audioPlayerServiceProvider);
return player.playerStateStream;
});
final positionProvider = StreamProvider<Duration>((ref) {
final player = ref.watch(audioPlayerServiceProvider);
return player.positionStream;
});
final vuMeterProvider = StreamProvider<List<double>>((ref) {
final player = ref.watch(audioPlayerServiceProvider);
return player.vuMeterStream ?? const Stream.empty();
});
Ejemplo: Scheduler + Advance
Dart
CopyCopied
class SchedulerNotifier extends AsyncNotifier<void> {
@override
Future<void> build() async {
// inicialización si hace falta
}
Future<void> advance() async {
final scheduler = ref.read(schedulerServiceProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(() => scheduler.advance());
}
Future<void> startTanda(String hora, {bool manual = false}) async {
final scheduler = ref.read(schedulerServiceProvider);
await scheduler.startTanda(hora, manual: manual);
}
Future<void> onItemEnded() async {
final scheduler = ref.read(schedulerServiceProvider);
await scheduler.onCurrentItemEnded();
}
}
final schedulerNotifierProvider =
AsyncNotifierProvider<SchedulerNotifier, void>(() {
return SchedulerNotifier();
});
Ejemplo: Library / Biblioteca
Dart
CopyCopied
class LibraryNotifier extends AsyncNotifier<List<AudioItem>> {
@override
Future<List<AudioItem>> build() async {
final library = ref.watch(libraryServiceProvider);
return library.getAudios();
}
Future<void> scan() async {
final library = ref.read(libraryServiceProvider);
state = const AsyncLoading();
await library.scanManagedLibrary();
state = AsyncData(await library.getAudios());
}
Future<void> importFiles(List<File> files, AudioKind kind) async {
final library = ref.read(libraryServiceProvider);
await library.importToManaged(files: files, kind: kind);
ref.invalidateSelf(); // vuelve a cargar
}
}
final libraryProvider =
AsyncNotifierProvider<LibraryNotifier, List<AudioItem>>(() {
return LibraryNotifier();
});
5. Estructura de carpetas recomendada para providers
text
CopyCopied
lib/
├── providers/
│ ├── service_providers.dart // los Provider<Service>
│ ├── runtime_provider.dart
│ ├── player_providers.dart // streams del player
│ ├── scheduler_provider.dart
│ ├── library_provider.dart
│ ├── buttoner_provider.dart
│ ├── config_provider.dart
│ └── day_state_provider.dart
6. Ejemplo de uso en la UI
Dart
CopyCopied
class ConsoleScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final playerState = ref.watch(playerStateProvider);
final runtime = ref.watch(runtimeProvider);
final vuMeter = ref.watch(vuMeterProvider);
return Column(
children: [\
// Header + Vúmetro\
// Deck\
// Transport\
ElevatedButton(\
onPressed: () => ref.read(schedulerNotifierProvider.notifier).advance(),\
child: const Text('Siguiente'),\
),\
],
);
}
}
7. Orden de creación recomendado
- service_providers.dart (aunque las implementaciones todavía sean stubs)
- runtime_provider.dart
- player_providers.dart (streams)
- scheduler_provider.dart
- library_provider.dart
- Resto de notifiers según se vayan necesitando
¿Querés que ahora escriba el esqueleto concreto de una de las implementaciones de service (por ejemplo SchedulerServiceImpl con la lógica de advance()), o preferís que definamos primero el flujo de arranque de la aplicación (main.dart + inicialización de services + carga de estado)?
¿como recomendas continuar?
Worked for 8s
Recomendación de cómo continuar
Estamos en un muy buen punto: el Núcleo está sólido y la arquitectura de la Rama 1 (Flutter + Riverpod + Services) está definida. Seguir documentando más detalles ahora genera rendimiento decreciente. Es momento de pasar a algo tangible.
Camino recomendado (orden óptimo)
1. Crear el esqueleto real del proyecto Flutter (hoy)
- Crear el proyecto con soporte Windows, Linux y Android.
- Armar la estructura de carpetas que definimos (core, services, data, providers, ui, etc.).
- Configurar las dependencias básicas (Riverpod, just_audio, audio_service, path_provider, file_picker, isar o drift, etc.).
2. Implementar la capa de modelos + serialización
- Pasar los modelos que ya definimos a clases Dart reales (con freezed o equatable + json_serializable recomendado).
- Tener la serialización JSON funcionando.
3. Implementar StorageService (versión mínima viable)
- Primero puede ser una implementación en memoria o con Isar simple.
- Que permita guardar y cargar: Config, Categories, Audios, Parrilla, etc.
4. Hacer un “Vertical Slice” mínimo funcional Elegir una sola sección y hacerla funcionar de punta a punta. La más recomendada es:
Consola + Motor de reproducción básico
Incluye:
- Header + Deck + Transport (aunque sea simple)
- Reproducir un audio (managed o external)
- Botones Play / Pause / Stop / Siguiente
- Estado AUTO / MANUAL
- Vista previa de “A continuación” (aunque sea básica)
Esto permite validar temprano:
- Reproducción de audio
- Riverpod funcionando
- Comunicación Service ↔ Notifier ↔ UI
5. Recién después agregar:
- Escaneo de la carpeta Audios
- Lógica completa de advance() + patrones
- Tandas
- Botoneras
- Resto de secciones
¿Por qué este orden?
- Evita construir mucha infraestructura sin ver resultados.
- Permite detectar temprano problemas de audio en Windows/Linux/Android.
- Mantiene el foco en la lógica de trabajo (que es lo que más te importa).
- Cada paso entrega algo usable y testeable.
Pregunta para decidir el próximo paso concreto:
¿Preferís que arranquemos ahora mismo con:
A. Crear la estructura del proyecto Flutter + modelos + providers básicos
B. Definir primero con más detalle el Vertical Slice de la Consola (qué debe mostrar y qué interacciones mínimas debe tener)
C. Otra prioridad que tengas en mente
Decime cuál elegís y avanzamos en esa dirección.
Crear la estructura del proyecto Flutter + modelos + providers básicos
Worked for 4m
Estructura del proyecto creada.
Se generó el esqueleto completo de la Rama 1 – Flutter en la carpeta ondaire/.
Qué se creó
Estructura de carpetas
text
CopyCopied
ondaire/
├── Audios/ (Musica, Artistica, Publicidades)
├── lib/
│ ├── core/
│ │ ├── models/ ← todos los modelos del Núcleo
│ │ ├── enums/
│ │ └── constants/
│ ├── services/interfaces/ ← contratos de los 5 services principales
│ ├── providers/ ← Riverpod básico
│ ├── ui/
│ │ ├── screens/ ← HomeShell con Header + Deck + Tabs
│ │ └── theme/
│ ├── main.dart
│ └── app.dart
├── pubspec.yaml ← dependencias definidas
└── README.md
Modelos implementados
- AudioItem, Category, AppConfig
- ParrillaBlock, Client, TandaConfig
- Buttoner + ButtonItem
- DayState, RuntimeState
Providers básicos
- service_providers.dart (placeholders de los services)
- runtime_provider.dart (estado de reproducción)
- config_provider.dart
UI inicial
- Header con marca, selector AUTO/MANUAL, placeholder de vúmetro y reloj
- Deck placeholder
- 8 pestañas según el Núcleo (Consola, Parrilla, Publicidad, etc.)
Estado actual
| Componente | Estado |
|---|---|
| Estructura del proyecto | ✅ Completa |
| Modelos del Núcleo | ✅ Completos |
| Interfaces de Services | ✅ Completas |
| Providers básicos | ✅ Listos |
| Shell de UI | ✅ Básico |
| Implementaciones reales | ⏳ Pendiente |
Próximo paso recomendado
Implementar el primer vertical slice funcional:
- StorageService (versión en memoria o Isar simple)
- AudioPlayerService (con just_audio)
- Hacer que el botón “Siguiente” / Play de la Consola reproduzca un audio de prueba
¿Querés que continuemos ahora con la implementación de StorageService + AudioPlayerService, o preferís revisar primero los archivos generados?
continua
Worked for 3m 14s
Continuamos. Se implementaron las primeras piezas reales.
Lo que se agregó en este paso
1. StorageServiceImpl
- Persistencia en archivos JSON dentro del directorio de documentos de la aplicación.
- Crea automáticamente la estructura Audios/Musica, Audios/Artistica, Audios/Publicidades.
- Carga y guarda: Config, Categorías, Audios, Parrilla, Clientes, Tandas, Botoneras, DayState.
- Incluye datos por defecto (categorías musicales + de artística, una parrilla básica y dos botoneras).
- Exportación de configuración y base de datos (metadatos).
2. AudioPlayerServiceImpl
- Basado en just_audio.
- Reproductor principal + reproductor de overlay (botoneras).
- Soporte de:
- Play / Pause / Stop con fade
- Seek
- Volumen y ducking
- Streaming
- Overlay (pisador y separador)
- Emite estado reactivo (PlayerState) para la UI.
3. Providers actualizados
- storageServiceProvider + storageInitProvider (inicializa al arrancar).
- audioPlayerServiceProvider con dispose correcto.
4. UI mejorada
- Pantalla de carga mientras inicializa el storage.
- Reloj en tiempo real.
- Transport básico (botones visuales).
- Indicador de estado (punto rojo / streaming).
Estado actual del proyecto
| Componente | Estado |
|---|---|
| Estructura + Modelos | ✅ Completo |
| Interfaces de Services | ✅ Completo |
| StorageServiceImpl | ✅ Funcional (JSON) |
| AudioPlayerServiceImpl | ✅ Funcional (básico) |
| Providers | ✅ Conectados |
| Shell UI | ✅ Header + Deck + Transport + Tabs |
| LibraryService | ⏳ Pendiente |
| SchedulerService (advance) | ⏳ Pendiente |
| Botoneras reales | ⏳ Pendiente |
| Vertical slice de Consola | ⏳ Siguiente paso |
Próximo paso natural
Implementar LibraryService (escaneo de la carpeta Audios + importación + extracción de metadatos por patrón de nombre) y después el SchedulerService con la lógica de advance().
¿Seguimos con LibraryService ahora?
continua
Worked for 1m 50s
LibraryService implementado y conectado.
Qué incluye
Escaneo de biblioteca managed (scanManagedLibrary)
- Recorre Audios/Musica, Audios/Artistica, Audios/Publicidades
- Detecta archivos nuevos, actualizados y faltantes
- Asigna categoría automáticamente por nombre de subcarpeta
- Extrae title / artist / album / year según el patrón configurado
Importación
- importToManaged → copia o mueve archivos a la estructura de carpetas
- addExternal → solo guarda la ruta (sin copiar)
Metadatos
- Extracción por patrón configurable ({track} - {artist} - {title}, etc.)
- Re-extracción manual
- Edición manual con validación (título obligatorio, año 1900–actual+1)
- Flag metadataSource (pattern / manual / default)
Consultas
- Filtro por tipo (song / jingle / spot)
- Filtro por categoría
- Búsqueda por título, artista, álbum o filename
- Opción de incluir/excluir archivos missing
Providers agregados
- libraryServiceProvider
- libraryProvider (AsyncNotifier con scan, filtros y edición)
- songsProvider / jinglesProvider / spotsProvider
Estado actual
| Componente | Estado |
|---|---|
| StorageService | ✅ |
| AudioPlayerService | ✅ |
| LibraryService | ✅ |
| SchedulerService (advance) | ⏳ Siguiente |
| ButtonerService | ⏳ |
| UI de secciones | ⏳ Básica |
Próximo paso
Implementar SchedulerService con la lógica completa de:
- advance()
- Resolución de patrones (incluyendo día de la semana)
- Armado de tandas (auto / manual / mixed)
- Artística obligatoria al terminar tanda
- Integración con el player
¿Continuamos con SchedulerService?
continua
Worked for 1m 45s
SchedulerService implementado.
Qué incluye
advance() — orden de decisión:
- Si hay spots encolados de una tanda en curso → reproduce el siguiente
- Si corresponde la artística post-tanda → la reproduce
- Resuelve el bloque de parrilla actual ( día de la semana + hora)
- Avanza el patrón y resuelve el ítem:
- ALL → canción de todo el repertorio
- categoryId → canción de esa categoría
- JINGLE_RANDOM → jingle rotativo
- JCAT_xxx → jingle de categoría de artística
- Si no hay nada y fallback = true → cualquier canción
Rotación
- Shuffle (evita repetir hasta agotar el pool)
- Secuencial
Tandas
- Modo manual → orden fijo de spots
- Modo auto → clientes elegibles + límite de minutos/hora + pases diarios
- Modo mixed → primero manuales, luego auto (sin duplicar)
- Al terminar la tanda → siempre reproduce una artística de la categoría configurada (postTandaJingleCategoryId)
Integración
- Resuelve rutas managed → absolutas antes de reproducir
- onCurrentItemEnded() encadena tanda → post-tanda → advance
Services completos hasta ahora
| Service | Estado |
|---|---|
| StorageService | ✅ |
| AudioPlayerService | ✅ |
| LibraryService | ✅ |
| SchedulerService | ✅ |
| ButtonerService | ⏳ |
Próximo paso recomendado
- ButtonerService (pisadores/separadores + detención forzada), o
- Conectar la UI de Consola al motor (Play / Siguiente / estado del deck) para tener el primer vertical slice usable.
¿Con cuál seguimos?
ButtonerService
continua
Worked for 59s
ButtonerService implementado.
Comportamiento según el Núcleo
Pisador
- Baja el volumen del programa (ducking) según duckLevel
- Reproduce el audio por encima
- Al terminar (si no está en loop) restaura el volumen con fade-in
- No detiene la reproducción principal
Separador
- Fade-out del programa principal y lo pausa
- Reproduce el audio del botón
- Al terminar (si no está en loop) el flujo continúa con advance() (coordinado desde la capa superior)
Reglas comunes
- 1er toque → reproduce
- 2º toque → detiene con stopFadeTime
- Soporte de loop individual y loopAll de la botonera
- forceStopAllWithFade(): detiene todos los botones (incluso en loop) cuando el transport hace “Siguiente” o se activa Streaming
Gestión
- assignAudioToButton / updateButtoner
- Estado reactivo por itemId para la UI
Estado de los Services
| Service | Estado |
|---|---|
| StorageService | ✅ |
| AudioPlayerService | ✅ |
| LibraryService | ✅ |
| SchedulerService | ✅ |
| ButtonerService | ✅ |
Capa de dominio completa.
Próximo paso natural
Conectar la UI de Consola al motor:
- Deck reactivo (título, tiempo, estado)
- Transport funcional (Play/Pause, Stop, Siguiente, Tanda)
- Botonera usable
- Llamar a forceStopAllWithFade() al hacer Siguiente / Streaming
Eso cierra el primer vertical slice usable.
¿Continuamos con la UI de la Consola?
¿Continuamos con la UI de la Consola?
si
Worked for 2m 19s
UI de Consola conectada al motor.
Qué quedó funcional
Header
- Indicador de estado (EN ESPERA / AL AIRE / PAUSA / STREAMING)
- Selector AUTO / MANUAL
- Placeholder de vúmetro
- Reloj en tiempo real
Deck (reactivo)
- Título y subtítulo del audio actual
- Tiempo restante y transcurrido
- Barra de progreso
- Badge de tipo / streaming
Transport
| Botón | Acción |
|---|---|
| Stop | Detiene player + fuerza stop de botoneras |
| Play/Pause | Pausa, resume o arranca advance() |
| Rewind | Retrocede N segundos (config) |
| Stop-after | Marca detener al finalizar |
| SIG | Force-stop botoneras + advance() |
| TANDA | Dispara tanda de la hora actual |
| Volumen | Slider en vivo |
Consola (pestaña)
- Indicador de patrón/bloque activo
- Botonera completa: tabs por botonera, grilla, toggle play/stop, estilos pisador/separador
- Placeholders de cola y explorador (siguientes)
Estado del vertical slice
| Pieza | Estado |
|---|---|
| Services (5/5) | ✅ |
| Deck + Transport | ✅ |
| Botonera usable | ✅ |
| Play → advance → patrones | ✅ (lógica) |
| Carga real de audios en UI | ⏳ (pestaña Música) |
| Cola visual | ⏳ |
| Resto de pestañas | ⏳ |
Próximo paso recomendado
Implementar la pestaña Música (y/o Artística) para:
- Escanear / importar archivos
- Ver la biblioteca
- Poder probar el motor de verdad con audios reales
¿Seguimos con la pestaña Música?
Implementar la pestaña Música (y/o Artística)
si segui
Worked for 2m 3s
Pestañas Música y Artística implementadas.
Pestaña MÚSICA
- Lista de temas con título, artista, categorías, duración
- Escanear biblioteca managed (Audios/Musica/...)
- Importar archivos → managed (copia a la estructura)
- Importar carpeta completa
- External → solo guarda la ruta, sin copiar
- Búsqueda y filtro por categoría
- Reproducir, editar metadatos, eliminar
- Indicador de archivos faltantes / external
Pestaña ARTÍSTICA
- Misma lógica orientada a jingles
- Import managed / external
- Escaneo de Audios/Artistica/...
- Categorías de artística (Pisadores, Promos, etc.)
- Reproducir / editar / eliminar
Flujo de prueba posible
- Abrir pestaña Música
- Importar algunos MP3 (Archivos o Carpeta)
- Volver a Consola
- Pulsar Play → debería arrancar advance() y reproducir según el patrón
- Probar botonera asignando audios de Artística
Estado general del vertical slice
| Área | Estado |
|---|---|
| Services (5/5) | ✅ |
| Consola + Transport | ✅ |
| Botonera | ✅ |
| Música + Artística | ✅ |
| Parrilla / Publicidad / Config / Historial | ⏳ Placeholders |
Próximos pasos posibles
- Pestaña Config (patrón de nombre de archivo, fades, post-tanda jingle, streaming URL)
- Pestaña Parrilla (bloques por día + hora + editor de patrones)
- Cola de reproducción en Consola
- Asignar audio a botones desde la botonera
¿Con cuál seguimos?
Próximos pasos posibles
hace todas en ese orden
Worked for 2m 55s
Las 4 piezas quedaron implementadas.
1. Config
- Nombre de botonera
- Streaming (URL, fade, tandas durante stream)
- Fades y ducking
- Rotación, tandas, fallback, límite de minutos/hora
- Patrón de nombre de archivo ({artist} - {title}, etc.)
- Categoría de artística post-tanda
- Top de hora
- Guardado persistente
2. Parrilla
- Franjas con desde / hasta
- Selección de días de la semana (o todos)
- Vista previa del patrón
- Editor de patrón: TODAS, CUÑA, categorías musicales y de artística
- Alta / baja de franjas
3. Cola en Consola
- Lista ordenable (↑ ↓)
- Quitar ítems / vaciar
- Explorador con búsqueda
- Botón + para agregar a la cola
4. Asignar audio a botones
- Toque en botón vacío → abre selector
- Long-press o ✏️ → reasignar / quitar
- Busca en jingles + música
- Actualiza el nombre del botón con el título del audio
Pestañas del sistema
| Pestaña | Estado |
|---|---|
| Consola | ✅ Completa (deck, transport, botonera, cola) |
| Parrilla | ✅ |
| Publicidad | ⏳ Placeholder |
| Categorías | ⏳ Placeholder |
| Música | ✅ |
| Artística | ✅ |
| Config | ✅ |
| Historial | ⏳ Placeholder |
Siguiente bloque natural
- Publicidad (tandas + clientes)
- Categorías
- Historial
¿Seguimos con esas tres para cerrar el mapa de pestañas?
Siguiente bloque natural
si
Worked for 2m 11s
Publicidad, Categorías e Historial listos. Mapa de pestañas completo.
Categorías
- Listado de categorías musicales y de artística
- Alta / edición / baja
- Nombre + color hex
Publicidad
- Límite de minutos/hora (ligado a Config)
- Tandas: alta, baja, modo AUTO / MANUAL / MIXTO
- Spots manuales ordenados por tanda
- Clientes/contratos: nombre, spot, pases, franja horaria
Historial
- Lista de emisiones (hora, tipo, título, origen)
- Actualizar / limpiar
- Persistencia en storage (hasta 400 entradas)
Estado final de pestañas
| Pestaña | Estado |
|---|---|
| Consola | ✅ |
| Parrilla | ✅ |
| Publicidad | ✅ |
| Categorías | ✅ |
| Música | ✅ |
| Artística | ✅ |
| Config | ✅ |
| Historial | ✅ |
Capa de dominio
| Service | Estado |
|---|---|
| Storage | ✅ |
| AudioPlayer | ✅ |
| Library | ✅ |
| Scheduler | ✅ |
| Buttoner | ✅ |
El esqueleto de la Rama 1 – Flutter ya cubre el flujo operativo principal del Núcleo.
Próximos refinamientos útiles
- Registrar en historial cada reproducción (addHistory desde Scheduler/Player)
- Hacer que la cola alimente realmente a advance()
- Detectar duración real de los archivos al importar
- Pestaña Publicidad: carga de spots desde Audios/Publicidades
- Empaquetar y probar en Windows
¿Seguimos con el punto 1–2 (historial + cola en advance), o preferís preparar el proyecto para correrlo en tu máquina?
terminemos con el punto 1-2. una vez preparado todo recien preparamos el proyecto para correrlo en la mquina
Free tier limit reached
Try again later or upgrade to SuperGrok for much higher limits and premium features.
Sign up for free
Sign in to continue conversation
HTML Automation System to Robust Cross-Platform Software | Shared Grok Conversation
StripeM-Inner