# AutoDJ — Erros conhecidos e aprendizados Catálogo consolidado de **sintoma → causa-raiz → fix → como reconhecer de novo**. É o primeiro lugar a olhar quando algo "não toca", "some do deck", "trava em 00:00" ou "funciona local e não em prod" (ou o inverso). Regra de manutenção: **todo bug com causa-raiz provada entra aqui** no mesmo passo do fix, junto com a entrada em `web/app/lib/changelog.ts` e a linha no Change Log de `docs/architecture.md` (regra DOC SEMPRE ATUALIZADA). Detalhe de implementação fica no `CLAUDE.md`; aqui fica o **diagnóstico operacional**. Legenda de estado: ✅ corrigido · ⚠️ limitação aceita (não tem fix) · 🧪 armadilha de teste. --- ## Índice rápido — sintoma → seção | Sintoma | Seção | |---|---| | Clipe do YouTube **some ao arrastar pro deck** / pula sozinho | [YT-1](#yt-1--origem-ip-bloqueia-embed-de-clipe-monetizado-) (dev) e [YT-6](#yt-6--falso-positivo-do-detector-de-bloqueio-errorcode-stale-) | | Deck **preto** na transição Spotify→YouTube | [YT-5](#yt-5--esconder-o-overlay-parqueia-o-iframe--deck-preto-) | | YouTube **trava em 00:00** com metadados certos | [YT-4](#yt-4--handshake-postmessage-sem-origin--trava-em-0000-) | | YouTube **Erro 153** / "Video player configuration error" | [YT-3](#yt-3--erro-153-referer-obrigatório-desde-dez2025-) | | YouTube nunca toca, mesmo com tudo certo (A/B ping-pong) | [YT-2](#yt-2--reparent-do-iframe-recarrega-o-player-) | | Busca do YouTube devolve erro / para de funcionar no meio do dia | [YT-7](#yt-7--cota-da-data-api-v3-100-buscasdia-) | | Capa da faixa YouTube 404 / placeholder ♪ | [YT-8](#yt-8--artwork-do-youtube-sem-artwork_url-) | | Chip "Premium" do YouTube sempre false | [YT-9](#yt-9--premium-do-youtube-não-mapeia-) | | Acha no "Sem login" mas **arrastar pede conta** | [YT-10](#yt-10--buscava-deslogado-mas-arrastar-pro-deck-pedia-conta-) | | Dá pra rodar YouTube **sem API key nenhuma**? | [YT-11](#yt-11--youtube-100-sem-api-key-o-que-dá-e-o-que-não-dá-) | | Deck diz **no ar** mas transporte diz **RETOMAR**, relógio congelado | [PLY-1](#ply-1--troca-de-fonte-deixa-o-playout-pela-metade-) | | **Mixagem de 7 s parou de acontecer** (some o crossfade) | [PLY-1](#ply-1--troca-de-fonte-deixa-o-playout-pela-metade-) e [PLY-2](#ply-2--pause-agendado-passa-pelo-guarda-e-marca-a-estação-como-parada-) | | Com **SEM-VOZ ligado** a mixagem nunca acontece | [PLY-2](#ply-2--pause-agendado-passa-pelo-guarda-e-marca-a-estação-como-parada-) | | **Pod ligado sem processar nada** / conta do RunPod subindo | [GPU-1](#gpu-1--pod-fica-ligado-de-propósito-após-a-última-faixa-) | | **Separação na nuvem nunca termina** (fica em "processando" para sempre) | [GPU-5](#gpu-5--o-pod-morria-recém-nascido-o-relógio-de-ociosidade-em-duas-formas-) | | **CI reprova só no Linux**, passa no Mac | [DEV-11](#dev-11--o-ci-nunca-tinha-rodado-e-escondia-dois-bugs-só-de-linux-) | | **Estação abre VAZIA depois de um deploy** (dados intactos no volume) | [DEP-1](#dep-1--mover-o-home-do-container-invalida-todo-caminho-já-gravado-) | | Pod **reinicia em laço** e refaz o trabalho | [GPU-2](#gpu-2--pod-reinicia-em-laço-mkdir--p-workspace-e-auto-terminação-não-confiável-) | | Separação falha com "**no longer any instances available**" | [GPU-3](#gpu-3--supply_constraint-capacidade-de-gpu-some-e-volta-) | | Faixa **já preparada** exibida como "⏳ PROCESSANDO" | [PLY-3](#ply-3--endpoint-ampliado-sem-varrer-quem-consome-) | | Feature nova **não aparece** (mas textos novos aparecem) | [DEV-5](#dev-5--feature-nova-não-aparece-binário-velho-servindo-bundle-novo-) | | "**Dou refresh e some tudo**" no dev local | [DEV-6](#dev-6--dou-refresh-e-some-tudo-task-serve-era-efêmero-) | | Deploy subiu mas a tela continua **antiga** | [DEV-7](#dev-7--deploy-invisível-html-sem-cache-control-) | | **MP3 local** fica parado em 00:00, sem erro | [LOC-1](#loc-1--mp3-local-fica-parado-em-0000-sem-erro-) | | **Dei refresh e a faixa recomeçou / parou** | [PLY-1](#ply-1--refresh-parava-a-estação-e-a-faixa-voltava-pro-0000-) | | `curl` vê a faixa e o **browser não** (ou o inverso) | [DEV-8](#dev-8--curl-e-browser-caem-em-tenants-diferentes-) | | `fetch` novo devolve **404 silencioso** | [DEV-9](#dev-9--apibase-devolve-só-a-origem-não-o-prefixo-da-api-) | | `task cover` verde mas `go test ./...` vermelho | [DEV-10](#dev-10--o-gate-de-cobertura-dizia-passed-sobre-pacote-que-não-compilava-) | | Spotify acaba a faixa e **para tudo** | [SP-1](#sp-1--player_state_changednull-fim-natural-lido-como-takeover-) | | Mixagem **muda** e a fila **esvazia sozinha** | [SP-4](#sp-4--spotify-aceita-todo-comando-e-não-toca-a-fila-esvazia-sozinha-) | | `oauth state mismatch` no connect (YouTube ou Spotify) | [SP-2](#sp-2--oauth-state-mismatch-) | | Login do Spotify recusa o redirect | [SP-3](#sp-3--spotify-exige-loopback-ip-literal-no-redirect-) | | Build aborta reclamando de changelog | [DEV-2](#dev-2--gate-do-changelog-aborta-o-build--é-o-comportamento-desejado) | | Faixa YouTube/Spotify recusada pelo banco (CHECK) | [DEV-3](#dev-3--check-do-sqlite-não-altera-in-place-) | | Vídeo fica em BUFFERING eterno no browser de automação | [DEV-4](#dev-4--browser-de-automação-não-toca-clipe-monetizado-) | --- ## YouTube — embed e playout ### YT-1 — Origem IP bloqueia embed de clipe monetizado ✅ **Versão:** v0.2.10 (medido em 27/07/2026) · **Só afeta dev.** **Sintoma:** arrasta um clipe musical (VEVO/gravadora/canal oficial) pro deck, ele **some no ato** e a fila avança. Vídeo livre de direitos (ex. Big Buck Bunny `aqz-KE-bpKQ`) toca normal — por isso o bug parece **aleatório** ("só alguns vídeos somem"). **Causa-raiz:** o YouTube **recusa incorporar vídeo monetizado quando o host da origem da página é um IP literal** (`http://127.0.0.1:8099`). O player devolve `onError 150` + `getVideoData().errorCode === "auth"` e nunca sai de UNSTARTED — **mesmo com `status.embeddable === true`** na Data API. O app então faz o correto (`embed:150` → `useYouTubePlayout` effect 3a → `POST /playback/next`) e a faixa desaparece. **Prova A/B** — mesmo vídeo (`JGwWNGJdvx8`), mesmo `host: youtube-nocookie`, mesmos playerVars, mesmo Chrome, mesmo servidor, mesmo instante: | origem | `getPlayerState()` | `errorCode` | `onError` | |---|---|---|---| | `http://127.0.0.1:8099` | -1 (UNSTARTED) | `"auth"` | **150** | | `http://localhost:8099` | 3 (BUFFERING → carrega) | `null` | — | | `https://example.com` | 3 (BUFFERING → carrega) | `null` | — | Descartado por medição: **não** é o `youtube-nocookie` (cookieful também falha em IP); **não** é o esquema `http:` (localhost também é http); **não** é o vídeo. É o **host ser IP**. **Fix (v0.2.10)** — a lógica de skip está certa e não mudou; o que faltava era não falhar em silêncio: - `web/app/lib/ytOrigin.ts` — `isBareIpOrigin()` (IPv4 literal ou `[IPv6]`), `localhostEquivalentURL()`, `currentOrigin()`. - `web/app/components/YouTubeOriginWarning.tsx` — banner no topo do `YouTubePanel` (estado conectado **e** desconectado), com link pra URL equivalente em `localhost`. Fora de origem IP renderiza `null`. - `useYouTubePlayer.ts` `onError` — `console.warn` com a dica quando código morto ocorre em origem IP. - `scripts/serve-local.sh` / `serve-youtube-local.sh` — anunciam `http://localhost:$PORT` e passam `AUTODJ_{SPOTIFY,YOUTUBE}_RETURN` **absolutos p/ localhost**. **Regra:** em dev, abrir **sempre `http://localhost:`, nunca `127.0.0.1`**. **Prod nunca foi afetado** (domínio real). **Pegadinha do fix:** os `*_REDIRECT` de OAuth **continuam em `127.0.0.1`** — ver [SP-3](#sp-3--spotify-exige-loopback-ip-literal-no-redirect-). Só o **retorno pós-callback** virou localhost; o state-smuggling `~` é agnóstico de origem, então trocar de origem no meio do fluxo não quebra o login. **Como reconhecer de novo:** no console do browser, `window.YT.get('widget2').getVideoData().errorCode === "auth"` com `getPlayerState() === -1` e a barra de endereço num IP. --- ### YT-2 — Reparent do iframe recarrega o player ✅ **Versão:** v0.2.4. **Sintoma:** vídeo nunca chega a PLAYING em browser real; trava em 00:00 / mostra "Ocorreu um erro. Tente novamente mais tarde". Piora a cada troca A/B de deck. **Causa-raiz:** o `Deck.tsx` movia o host do player (`appendChild` pro slot de capa do deck ao vivo). Pela spec HTML, **mover um `