← Voltar ao arquivo

Local Player: vibecoding com invariantes e zero build step

Como um player local/offline em Express + vanilla JS organiza milhares de aulas sem bundler: AGENTS.md como contrato, estrutura física como fonte de verdade e scan com cache em disco.

Eu queria um jeito simples de assistir meus cursos em disco, sem nuvem e sem dor de cabeça — e acabei construindo um player inteiro com ajuda de IA.

O contexto

Eu tinha um problema bem concreto: um monte de curso espalhado em HD, SSD, pendrive e cartão SD, cada um numa pasta de um jeito, e nenhum player que organizasse aquilo direito. VLC toca, mas não lembra onde eu parei. Plataforma online não serve porque eu quero tudo offline, sem telemetria, sem mandar nada pra internet.

Foi aí que nasceu o Local Player. Ele é um servidor local em Node.js + Express com uma SPA em JS puro, sem nenhum build step. O server.js tem só 145 linhas e só amarra as coisas — o resto fica em submódulos em server/. Hoje o projeto tem uns 22 mil linhas no backend, quase 20 mil no frontend, 21 arquivos de teste e docs em inglês e português.

E sim, eu fiz ele como vibecoding: eu descrevia o comportamento, a IA gerava, eu revisava. O que salvou o projeto de virar um protótipo jogável foi um arquivo de 283 linhas chamado AGENTS.md, que virou o contrato do que nunca pode quebrar.


O problema que eu queria resolver

Assistir aula em disco parece fácil até você encarar o disco real. No meu caso era:

  • milhares de aulas num pendrive FAT lento;
  • .mp4 misturado com .mkv, .avi, .mov que o navegador às vezes toca, às vezes não;
  • progresso que sumia se o servidor morria no meio do save;
  • medo de um path malicioso escapar da biblioteca;
  • vontade de ter legenda com IA e um tutor, mas sem depender disso pra dar play.

Eu decidi de cara: Express como única dependência, nada de bundler pra não quebrar em drive externo, e a pasta em disco manda — o código só lê, nunca adivinha.


A decisão mais importante

Deixar o server.js fininho, só registrando rotas:

core.registerLibraryRoutes(app);
core.registerProgressRoutes(app);
core.registerMediaRoutes(app);
core.registerSystemRoutes(app);
core.registerAiRoutes(app);
core.registerTutorRoutes(app);
core.registerSubtitleRoutes(app);

Eu descartei duas ideias: jogar tudo num arquivo gigante (a IA gera rápido, mas eu nunca ia conseguir revisar) e usar React/Vite (ia exigir toolchain justo no pendrive do usuário).

O preço foi escrever mais JS vanilla na mão. O ganho: eu mudo server.js ou algo em public/, recarrego o navegador e pronto. Sem build, sem sourcemap vazando caminho.

flowchart LR
  Disco --> Scan[scan + tree-cache]
  Scan --> API["/api/tree por biblioteca"]
  API --> SPA["SPA hash routing"]
  SPA --> Media["/media com Range"]
  Media --> Player["<video> + sidebar"]

Como usei IA no desenvolvimento

Onde ela me ajudou de verdade:

  • gerar os módulos repetitivos de rotas a partir de um que eu já tinha revisado;
  • espelhar a documentação EN/pt-BR;
  • cuspir casos de borda de path traversal pra suíte de testes.

Onde ela me atrapalhou:

  • tentava adivinhar se pasta era tópico pelo “nome bonito” em vez de seguir a regra explícita;
  • queria converter vídeo pela extensão em vez de checar o codec com ffprobe;
  • tentava dar await na legenda antes do play().

O que eu aprendi: sem o AGENTS.md no contexto, ela relaxava as regras. Com ele aberto, o retrabalho despencava. E tudo que mexia com path, progresso ou fila eu testava na mão com npm start na porta 4173.


O que eu descobri implementando

  • Pasta só vira tópico se tem arquivo .topic ou o nome termina com (TP). Senão é curso/módulo. O (TP) some só do título na tela. Sem adivinhação.
  • Os títulos bonitos são calculados no servidor (server/core/titles.js): ele tira Aula 03 -, ~1, tags e sublinhados, e mantém siglas tipo SQL e Node.js.
  • O scan ignora a pasta do app, arquivos com . na frente e .ini/.db/.lnk. Capa ele acha por nome (cover, poster, banner) ou herda do filho — se não tem nada, gera um gradiente com as iniciais.
  • A ordem usa localeCompare pt-BR numérico. Parece detalhe, mas quebra a navegação se errar.
  • O npm install --no-bin-links é de propósito, pra funcionar em FAT/exFAT.

O que funcionou

  • ROOT sempre derivado de __dirname, nunca hardcoded. A biblioteca padrão é onde o app está.
  • Árvore com cache por biblioteca, rescan pelo botão de atualizar ou por API.
  • Todo caminho vindo do cliente passa por resolveSafeRelPath(). Sem express.static(ROOT), só a SPA em public/. Vídeo sai com Range 206.
  • Se já tem outra instância na porta, ele avisa claro e sai com exit(1) — porque duas instâncias iam corromper o progress.json.

O que não funcionou

  • O primeiro scan sem cache deixava biblioteca grande em pendrive inutilizável a cada boot.
  • Eu tentei servir HTML/SVG/JS dos materiais inline — risco de rodar código no origin do app. Agora sai tudo como attachment + nosniff.
  • Tentei persistir pastas expandidas da sidebar e desisti: a árvore re-renderiza direto e brigava com o estado salvo. Voltou a ser memória.

O que eu faria diferente hoje

Eu tinha extraído scan.js e titles.js bem mais cedo — foram os que a IA mais reescreveu. E tinha congelado o formato das chaves (libId\0rel, sha1(...)) no primeiro dia.


Resultado

O que já está valendo: scan, navegação por hash, vídeo compatível tocando direto, busca sem acento, favoritos, 14 atalhos configuráveis, node --check e node --test passando.

O que falta: AppImage final e os modelos do Whisper (que nunca vão pro git por serem gigantes).

Na próxima parte eu conto como resolvi o progresso pra nunca mais perder posição.


O que aprendi

1. Contrato antes de prompt

Um AGENTS.md com “remoção de biblioteca é só config” vale mais que prompt esperto.

2. Disco manda, código obedece

Regra explícita de tópico acabou com uma classe inteira de bug que a IA adorava criar.

3. Zero build é recurso

Em máquina alheia e drive externo, node server.js + F5 é liberdade.


Próximos passos


Conclusão

Vibecoding escala quando eu digo o que não pode quebrar e deixo o teste dizer não rápido. O resto é iteração barata.


Referências

  • server.js, server/core/scan.js, server/core/titles.js, server/libraries/registry.js
  • docs/DOCUMENTATION.md, docs/VALIDATION.md, AGENTS.md
  • Parte 2 da série
Diagrama ampliado
100%
↑↓ para navegar · Enter para abrir 8 itens no índice