尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
Meu Player de Música
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗No silêncio que por vezes nos cerca, a busca por uma melodia, uma voz ou uma notícia se faz presente. Este projeto nasce como um humilde portal para esse universo sonoro: um Rádio Player Online, desenhado para ser simples, direto e sempre à mão.
App web de player de rádios online: busca (API Radio Browser), favoritos por categoria, estatísticas de uso, player com sleep timer e um Modo Carro (tela cheia, botões grandes, presets das rádios mais ouvidas) para usar com o celular preso no carro. A tela inicial fica com favoritos, recentes e mais ouvidas; busca e "adicionar manualmente" (uso eventual) ficam numa tela própria, aberta pelo 🔍 da barra superior. Opcionalmente toca também as músicas de um servidor Jellyfin pessoal (ver "Minha biblioteca"). HTML/CSS/JS puro, sem build e sem framework. É um PWA — instalável e com o "casco" funcionando offline.
Servir a pasta em qualquer host estático (GitHub Pages, incl. subpasta de
repositório). Todos os caminhos são relativos — não há nada para editar.
Para desenvolver, qualquer servidor local serve (python -m http.server), já
que o Service Worker não registra em file://.
| Arquivo | Responsabilidade |
|---|---|
index.html | Estrutura e marcação de todas as seções. |
style.css | Estilos. Design tokens (espaçamento, raios, cores, sombras) em :root; tema escuro e cores de destaque sobrescrevem tokens em body.dark-theme / body.title-color-*. |
app.js | Tudo o mais: player (HLS.js + <audio>), Media Session, busca e filtros, favoritos e categorias, recentes, Modo Carro, sleep timer, modais in-app, tema, PWA. |
jellyfin.js | Cliente do Jellyfin (login, listagens, URLs de áudio e capa) — window.Jellyfin. Fila e telas da biblioteca ficam no app.js. |
radioData.js | Estatísticas de uso — classe RadioAnalytics, isolada, grava em IndexedDB. window.radioData. |
wakeLock.js | Screen Wake Lock. Expõe window.RadioWakeLock; o app.js chama request()/release() ao tocar/parar. |
screensaver.js | Descanso de tela (barras estilo Winamp, capa, relógio) e o visualizador sincronizado experimental. window.RadioScreensaver. |
presence.js | Contador anônimo de ouvintes: manda o sinal enquanto toca e lê o painel do dono. window.RadioPresence. |
sw.js | Service Worker: cacheia o app shell (rede primeiro, cache como fallback offline); nunca cacheia áudio nem a API. Bump em CACHE_NAME a cada release. |
manifest.json | Metadados do PWA. |
cloudflare-worker/nowplaying.js | Worker opcional (deploy separado, fora do app shell) que lê o metadado ICY do stream pra mostrar capa/faixa na barra fixa e no Modo Carro. Ver seção abaixo. |
cloudflare-worker/presence/ | Worker opcional do contador de ouvintes (Durable Object). Ver "Ouvintes". |
Tudo local. As exceções são opcionais e descritas nas seções próprias: o "tocando agora" das rádios (Worker), a biblioteca (seu Jellyfin) e o contador anônimo de ouvintes (Worker, dá pra desligar).
localStorage
| Chave | Conteúdo |
|---|---|
radioFavorites | { categoria: [estações] } |
categoryOrder | ordem das categorias |
radioRecents | últimas 25 rádios tocadas |
radioTheme | "light" / "dark" (ausente = segue o sistema) |
radioTitleColor | cor de destaque |
radioSearchFilters | último país/tag/idioma usados |
radioVolume | último volume |
radioJellyfin | endereço do Jellyfin, usuário, token da sessão e qualidade. A senha nunca é salva. |
radioJellyfinPlayed | ids das últimas ~400 faixas tocadas (o "tocar tudo no aleatório" evita repetir) |
radioScreensaverStyle | estilo do descanso de tela (hoje só bars) |
radioPresenceId | código aleatório deste aparelho no contador de ouvintes |
radioPresenceOff | "1" = não enviar o sinal anônimo |
radioPresenceToken | chave do painel de ouvintes (só no aparelho do dono; também o marca como "seu") |
IndexedDB — banco radioAnalyticsDB, store analytics: clicks e tempo de
escuta por estação. Alimenta "Estatísticas" e "Top mais ouvidas".
O "Exportar favoritos" cobre só radioFavorites. O "Exportar dados" (em
Estatísticas) cobre só o IndexedDB. Não há backup unificado.
http:// não toca em página servida por https://
(bloqueio de conteúdo misto do navegador) — comum em rádios pequenas. O player
tenta automaticamente a versão https:// e a URL alternativa da estação antes
de desistir, então isso só afeta rádios sem HTTPS de verdade.O app é feito e testado para o Chrome no Android. Alguns recursos dependem de APIs que outros navegadores não têm ou limitam — nesses casos o recurso simplesmente não aparece, sem quebrar o resto:
| Recurso | Android (Chrome) | iPhone (Safari) |
|---|---|---|
| Modo Carro em tela cheia (esconde barra de status/navegação) | ✅ Fullscreen API | ❌ Safari só permite tela cheia em vídeo; as barras continuam visíveis |
| Capa/faixa na notificação e tela de bloqueio | ✅ Media Session | ⚠️ parcial |
| Capa/faixa no painel do carro (Bluetooth) | ✅ se o carro suportar (ver abaixo) | ⚠️ parcial |
| Botões do volante (play/stop, próxima/anterior) | ✅ | ⚠️ parcial |
| "Voltar" fecha a busca/biblioteca/Modo Carro em vez de sair do app | ✅ | n/a |
O app publica o que está tocando pela Media Session API — o Android repassa isso para a notificação, para a tela de bloqueio e, via Bluetooth (AVRCP), para o painel do carro:
O Modo Carro pede tela cheia ao abrir e de novo a cada toque nele — o Android sai da tela cheia ao alternar para outro app (GPS, por exemplo), e o navegador só permite voltar a ela a partir de um toque.
Por padrão o player mostra só nome da estação, país/codec/bitrate (na barra fixa) e tempo de escuta (no Modo Carro). Pra mostrar também "Artista — Faixa" e a capa do álbum — na barra fixa e no Modo Carro — é preciso um pequeno proxy, porque o navegador não consegue ler o metadado ICY que o Shoutcast/ Icecast intercala dentro do áudio bruto — isso só dá pra ler no servidor.
cloudflare-worker/nowplaying.js como um Worker no Cloudflare
(dashboard → Workers & Pages → Create application → Create Worker, cole o
arquivo, publique).ALLOWED_ORIGINS no topo do arquivo para o(s) domínio(s) reais do
app antes de publicar.NOWPLAYING_WORKER_URL, no topo
de app.js.Sem isso configurado (valor padrão: string vazia), o app funciona normalmente, só sem capa/faixa — nada quebra. Quando a estação não manda metadado (rádio falada, por exemplo) ou não dá pra achar a capa, o app também cai de volta no comportamento padrão silenciosamente. A busca de capa usa a API pública da iTunes (sem chave, sem custo) como alternativa para estações que não mandam a própria arte junto do metadado.
Toca as músicas de um servidor Jellyfin pessoal dentro do app: playlists, álbuns, artistas, busca, "tocar tudo no aleatório" e "mix parecido" (o Instant Mix do Jellyfin). Fica invisível até ser conectada: o botão 🎵 da barra superior só aparece depois de conectar em Configurações → Minha biblioteca.
Por que o Tailscale Serve: o app é servido em HTTPS, e o navegador
bloqueia áudio e chamadas HTTP numa página HTTPS — então
http://ip-do-servidor:8096 não funciona. O Tailscale Serve dá ao servidor
um endereço https://<máquina>.<tailnet>.ts.net com certificado válido,
acessível só de dentro da tailnet (nada fica exposto na internet). Em casa o
Tailscale conecta direto pela rede local, então não há perda de velocidade.
Configuração (uma vez só):
No painel do Tailscale (admin → DNS), ligar MagicDNS e HTTPS Certificates.
No servidor, publicar a porta do Jellyfin. Com o Tailscale num container
em modo host (app da BigBear no CasaOS), a regra precisa ficar no
arquivo que o container lê ao subir (TS_SERVE_CONFIG=/config/serve-config.json).
Um tailscale serve feito à mão só vale até o container reiniciar: com o
arquivo ausente, ele sobe sem regra nenhuma. Crie a regra e grave-a no
arquivo:
sudo docker exec big-bear-tailscale tailscale serve --bg 8096
F=/DATA/AppData/big-bear-tailscale/config/serve-config.json
sudo docker exec big-bear-tailscale tailscale serve status --json | sudo tee "$F"
Para conferir que sobrevive a um reinício:
sudo docker restart big-bear-tailscale
sleep 15
sudo docker exec big-bear-tailscale tailscale serve status
Deve mostrar https://<máquina>.<tailnet>.ts.net → proxy http://127.0.0.1:8096.
Sintoma de regra perdida: o Jellyfin funciona na rede de casa, mas o
app não conecta nem toca, serve status diz "No serve config" e o log do
container (docker logs big-bear-tailscale) mostra
could not connect to local backend server at 127.0.0.1:443. Refaça os
comandos acima.
Ao renomear a máquina no Tailscale, rode tailscale serve reset e os
comandos acima de novo — a regra (e o arquivo) guardam o nome antigo
(sintoma: "404 not found" no endereço novo).
No app: ⚙️ → Minha biblioteca → Conectar servidor, com o endereço https://
do passo 2, usuário e senha do Jellyfin. O celular precisa estar com o
Tailscale ligado (no Android, "VPN sempre ativa" evita esquecer).
Detalhes:
radioJellyfinPlayed) e
para no fim do lote.localStorage deste aparelho e vai
na URL do áudio (o <audio> não manda headers). Ele vale só pro seu
servidor e pode ser revogado no Jellyfin (Painel → Dispositivos) ou com
"Desconectar" no app.Etag do item (v=), pra que um
arquivo corrigido no servidor não seja misturado com pedaços antigos no
cache do Chrome.ffprobe mostra Could not read mimetype from an attached picture). O
Chrome recusa o arquivo inteiro. Correção no servidor, sem reconverter o
áudio: ffmpeg -i in.flac -map 0:a -c copy -map_metadata 0 out.flac. O app
não contorna isso de propósito — mostra o erro com o detalhe do navegador
pra que o arquivo seja corrigido.now using …) e linhas
derp-11 does not know about peer. Hipóteses: troca automática de banda
2,4/5 GHz do modem e o modo TS_USERSPACE=true do container, que lida mal
com essas trocas. O buffer da próxima faixa (acima) reduz o efeito; ainda
não há proteção pra uma queda no meio da faixa atual. No momento do
travamento, rodar:
sudo docker exec big-bear-tailscale tailscale status
sudo docker logs --since 5m big-bear-tailscale 2>&1 | grep -iE "proxy|timed out|error|derp|now using"
relay em vez de direct, ou muitas linhas now using/derp, confirmam
que é a conexão. Próximos passos possíveis, se continuar: detector de
travamento com reconexão visível na faixa atual, buffer de 2 faixas, e
tirar o modo userspace do Tailscale no servidor.Botão 🌌 no Modo Carro e no painel do player (▴). Tela cheia preta com barras estilo Winamp (com "picos" que caem devagar), capa, nome do que toca e relógio; o bloco de informações muda de lugar a cada 20 s pra não marcar a tela. Toque na tela (ou "voltar") pra sair. Mantém a tela ligada enquanto aberto.
O modo é escolhido sozinho pelo que está tocando:
OfflineAudioContext) e o espectro (FFT de 2048 pontos, 32 faixas
de 40 Hz a 16 kHz) e a onda são calculados no ponto em que o player está.
O <audio> que toca não é ligado ao Web Audio: isso deixaria mudas as
rádios sem CORS e poderia atrapalhar tocar com a tela desligada. Custo: se
a faixa não estiver na memória (buffer), ela é baixada de novo; a análise
ocupa ~40 MB por faixa de 4 min (liberados ao sair). Faixas com mais de
15 min ficam na animação. No canto aparece "analisando a faixa…" e depois
"sincronizado com a música".Estilos: ficam em STYLES, no screensaver.js. Cada estilo é uma função
draw que recebe os níveis já suavizados (barras e picos, 0 a 1), a onda (só
no sincronizado) e a cor de destaque. Hoje há um, "Barras (Winamp)"; um
estilo novo aparece sozinho em Configurações → Descanso de tela.
O app é público: qualquer pessoa com o link ouve as rádios (a biblioteca depende do seu Tailscale e login). Dois jeitos de saber se há mais gente:
1. Cloudflare Web Analytics (visitas por dia, país, aparelho). O domínio
não está na conta da Cloudflare, então o modo automático não serve: o
index.html carrega o script da Cloudflare (beacon.min.js, com o token do
site — público por natureza). Os números ficam no painel da Cloudflare →
Analytics & Logs → Web Analytics. Não usa cookies.
2. "Ouvindo agora" no app (Configurações → 👥 Ouvintes). Enquanto algo
toca, o app manda a cada 2 min um sinal anônimo pro Worker
cloudflare-worker/presence: um código aleatório do aparelho, o nome da
rádio (faixas da biblioteca aparecem só como "Minha biblioteca"), o tipo de
aparelho e se é um aparelho do dono. A Cloudflare acrescenta país e cidade
aproximados. Ao parar ou fechar, o app avisa que saiu; sem sinal por 5 min, o
aparelho some da lista. Por dia, o Worker guarda só os códigos dos aparelhos
que usaram (60 dias), pra contagem. O aviso aparece nas Configurações e dá pra
desligar o envio ali.
Só quem tem a chave do painel vê a lista (quem está ouvindo, de onde e há quanto tempo, e aparelhos por dia). Ter a chave no aparelho também o marca como "seu" na contagem.
Deploy do Worker (uma vez só, na pasta cloudflare-worker/presence):
npx wrangler login
npx wrangler deploy
npx wrangler secret put READ_TOKEN # invente uma chave longa
Depois: cole a URL do Worker em PRESENCE_WORKER_URL no topo do app.js,
publique, e no app vá em ⚙️ → 👥 Ouvintes → "Tenho a chave do painel". Com a
URL em branco, o app não manda nada e a seção diz que não está configurado.
Usa Durable Object com SQLite, disponível no plano gratuito. Tudo fica no
armazenamento do objeto (não só na memória): a Cloudflare o tira da memória
quando fica parado entre os sinais de 2 min.
CC BY-NC 4.0 — livre para compartilhar e adaptar, com atribuição e para uso não comercial.