Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.git
.github
.claude
.gitattributes
Dockerfile
.dockerignore
docker-compose.yml
*.md

# Gerado dentro do container a partir das variáveis de ambiente.
config.js
manifest.json
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# Auto detect text files and perform LF normalization
* text=auto

# Arquivos consumidos dentro do container Linux: CRLF quebra o shebang do
# entrypoint e a configuração do nginx.
*.sh text eol=lf
Dockerfile text eol=lf
docker/* text eol=lf
64 changes: 64 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Docker

on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]

env:
REGISTRY: ghcr.io

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- uses: actions/checkout@v4

# O GHCR só aceita nome de imagem em minúsculas e este repositório tem
# maiúscula no nome (RadioPlayer).
- name: Nome da imagem em minúsculas
id: image
run: echo "name=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"

- uses: docker/setup-qemu-action@v3

- uses: docker/setup-buildx-action@v3

# Em pull request só buildamos para validar; publicar é só na main/tag.
- name: Login no GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Metadados (tags e labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short

- name: Build e push
uses: docker/build-push-action@v6
with:
context: .
# arm64 cobre Raspberry Pi e NAS ARM, público comum de self-hosting.
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
41 changes: 41 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
FROM nginx:1.27-alpine

LABEL org.opencontainers.image.title="RadioPlayer" \
org.opencontainers.image.description="Player de rádio HTML5 com now playing, capa do álbum, letra e PWA" \
org.opencontainers.image.source="https://github.com/jailsonsb2/RadioPlayer" \
org.opencontainers.image.licenses="AGPL-3.0-or-later"

# Configuração padrão: sem nenhuma variável de ambiente o container sobe
# funcionando, tocando a rádio da demo.
ENV RADIO_NAME="Jailson Web Rádio" \
URL_STREAMING="https://stream.zeno.fm/yn65fsaurfhvv" \
API_URL="" \
FALLBACK_API_URL="" \
ACCENT_COLOR="" \
BG_COLOR="" \
THEME_COLOR="#0b0e13"

COPY css/ /usr/share/nginx/html/css/
COPY fonts/ /usr/share/nginx/html/fonts/
COPY img/ /usr/share/nginx/html/img/
COPY js/ /usr/share/nginx/html/js/
COPY index.html service-worker.js llms.txt /usr/share/nginx/html/

# A AGPL-3.0 acompanha o binário: quem redistribui ou hospeda esta imagem
# precisa dela junto.
COPY LICENSE /usr/share/nginx/html/LICENSE

COPY docker/nginx.conf /etc/nginx/conf.d/default.conf

# A imagem oficial do nginx executa tudo em /docker-entrypoint.d/ antes de
# subir o servidor — é onde geramos config.js e manifest.json.
COPY docker/40-radioplayer-config.sh /docker-entrypoint.d/40-radioplayer-config.sh
RUN chmod +x /docker-entrypoint.d/40-radioplayer-config.sh

EXPOSE 80

# 127.0.0.1 e não localhost: dentro do container o localhost resolve primeiro
# para ::1, e o nginx escuta só em IPv4 (listen [::]:80 quebra em container sem
# IPv6) — o healthcheck levaria "connection refused" com o site no ar.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -q --spider http://127.0.0.1/index.html || exit 1
75 changes: 66 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
[![No API Key](https://img.shields.io/badge/API_key-not_required-orange)](#data-sources)
[![PWA Ready](https://img.shields.io/badge/PWA-installable-5A0FC8)](#progressive-web-app-pwa)
[![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?logo=docker&logoColor=white)](#-docker)

**[▶ Try the live demo](https://jailsonsb2.github.io/RadioPlayer/)** — a modern, dependency-free **HTML5 radio player** for any stream (**Icecast, Shoutcast, Zeno.FM, RadioJar, Azuracast** and more): the album art of the current song becomes a blurred full-page backdrop, with glass-style circular controls on top. Now playing metadata, album art, song lyrics, recently played history, **YouTube clip mode** and installable **PWA** — no Bootstrap, no jQuery, no API key. Just HTML, CSS and vanilla JavaScript.

> 🇧🇷 **Player de rádio online para o seu site — grátis e sem chave de API.** Mostra a música tocando agora com capa do álbum, letra da música, histórico das últimas tocadas e até o **clipe do YouTube sincronizado com a rádio**. Compatível com Icecast, Shoutcast, Zeno.FM e Azuracast. É só editar duas linhas e hospedar em qualquer servidor (ou de graça na Vercel/Netlify/GitHub Pages).
> 🇧🇷 **Player de rádio online para o seu site — grátis e sem chave de API.** Mostra a música tocando agora com capa do álbum, letra da música, histórico das últimas tocadas e até o **clipe do YouTube sincronizado com a rádio**. Compatível com Icecast, Shoutcast, Zeno.FM e Azuracast. É só editar duas linhas e hospedar em qualquer servidor (ou de graça na Vercel/Netlify/GitHub Pages) — ou subir com **Docker** em um comando, configurando tudo por variável de ambiente.

## Demo Screenshots

Expand Down Expand Up @@ -46,20 +47,24 @@
* Responsive design — mobile-first single column, side-by-side layout on desktop
* Progressive Web App (PWA) with an "Install app" button when the browser allows it
* Accent color and surfaces themeable via CSS variables
* Single-file configuration in [config.js](config.js) — no need to touch the player source
* 🐳 **Docker image** ready for self-hosting, fully configurable by environment variables ([see below](#-docker))

## How do I add this radio player to my website?

Open [js/script.js](js/script.js) and edit the lines below:
Open [config.js](config.js) and edit the lines below:

```javascript
// RADIO NAME
const RADIO_NAME = 'Your Radio Name';
window.RADIO_CONFIG = {
// RADIO NAME
RADIO_NAME: 'Your Radio Name',

// Change Stream URL Here. Supports ICECAST, ZENO, SHOUTCAST, RADIOJAR and any other stream service.
const URL_STREAMING = 'https://stream.zeno.fm/yn65fsaurfhvv';
// Change Stream URL Here. Supports ICECAST, ZENO, SHOUTCAST, RADIOJAR and any other stream service.
URL_STREAMING: 'https://stream.zeno.fm/yn65fsaurfhvv',
};
```

The metadata API URLs are derived from `URL_STREAMING` automatically.
The metadata API URLs are derived from `URL_STREAMING` automatically. If `config.js` is missing, the player falls back to the defaults in [js/script.js](js/script.js).

### Change Logo

Expand All @@ -81,16 +86,68 @@ All the design tokens live at the top of [css/style.css](css/style.css) as CSS v

## Installation

Just put the files in your server or use Free Hosting:
Just put the files in your server, run the [Docker image](#-docker), or use Free Hosting:

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/jailsonsb2/RadioPlayer)
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/jailsonsb2/RadioPlayer)

### 🐳 Docker

Self-hosting? Run it in one line — no need to clone the repo or edit any file:

```bash
docker run -d -p 8080:80 \
-e RADIO_NAME="My Radio" \
-e URL_STREAMING="https://stream.zeno.fm/yn65fsaurfhvv" \
--name radioplayer ghcr.io/jailsonsb2/radioplayer:latest
```

The player is at `http://localhost:8080`. Or with Docker Compose — see [docker-compose.yml](docker-compose.yml):

```bash
docker compose up -d
```

**Environment variables**

| Variable | Default | Description |
|---|---|---|
| `RADIO_NAME` | `Jailson Web Rádio` | Radio name (header, page title, PWA) |
| `URL_STREAMING` | Zeno.FM demo stream | Stream URL (Icecast, Shoutcast, Zeno.FM, Azuracast...) |
| `API_URL` | derived from the stream | Only if you run your own metadata API |
| `FALLBACK_API_URL` | derived from the stream | Fallback metadata endpoint |
| `ACCENT_COLOR` | `#00e1e7` | Accent color (`--accent`) |
| `BG_COLOR` | `#0b0e13` | Page background (`--bg`) |
| `THEME_COLOR` | `#0b0e13` | PWA theme color |
| `PWA_NAME` | same as `RADIO_NAME` | App name on install |
| `PWA_SHORT_NAME` | same as `PWA_NAME` | Short name (home screen icon) |

The container generates `config.js` and `manifest.json` from these on startup, so changing a variable and restarting is enough — **no rebuild required**.

**Your own logo** — mount it over the default cover:

```bash
-v ./my-logo.png:/usr/share/nginx/html/img/cover.png:ro
```

**Building locally** instead of pulling the image:

```bash
docker build -t radioplayer .
```

**Notes**

* Built for `linux/amd64` and `linux/arm64` — Raspberry Pi and ARM NAS included. Image size ≈ 76 MB (nginx alpine + ~1.7 MB of player files).
* Ships a `HEALTHCHECK`, so Portainer / Unraid / `docker ps` show the real container health.
* Everything is served over plain HTTP on port 80 inside the container. **Put it behind a reverse proxy with HTTPS** if you expose it to the internet — browsers block the PWA install prompt and the Media Session API on insecure origins.
* The full album art, lyrics and now playing metadata still come from the public APIs listed in [Data Sources](#data-sources) — the container serves the player, not the metadata.

### Progressive Web App (PWA)

When the browser signals that installation is available, an "Install app" button appears (top-right on desktop, bottom of the screen on mobile).

**Note:** after deploying an update, bump the service worker cache version in `service-worker.js` (`CACHE_NAME`) so returning visitors get the new files.
**Note:** the service worker fetches the HTML and `config.js` from the network first, so a configuration change reaches returning visitors on the next load. After a bigger update (CSS/JS), still bump the cache version in `service-worker.js` (`CACHE_NAME`) to force everything to refresh at once.

### Configuring Radio Name and Colors (PWA)

Expand Down
23 changes: 23 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Configuração do player — edite este arquivo para apontar para a sua rádio.
//
// Se você estiver rodando via Docker, NÃO edite aqui: este arquivo é gerado
// automaticamente na subida do container a partir das variáveis de ambiente
// (RADIO_NAME, URL_STREAMING, ...). Veja a seção Docker do README.
//
// Se este arquivo não existir, o js/script.js cai nos valores padrão dele.
window.RADIO_CONFIG = {
// Nome da rádio, exibido no topo e no título da página
RADIO_NAME: 'Jailson Web Rádio',

// URL do stream. Suporta ICECAST, ZENO, SHOUTCAST, RADIOJAR e outros.
URL_STREAMING: 'https://stream.zeno.fm/yn65fsaurfhvv',

// Opcionais: por padrão as URLs da API de metadados são derivadas do
// URL_STREAMING. Só preencha se você usa a sua própria API.
// API_URL: '',
// FALLBACK_API_URL: '',

// Opcionais: sobrescrevem as variáveis de tema do css/style.css
// ACCENT_COLOR: '#00e1e7',
// BG_COLOR: '#0b0e13',
};
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
radioplayer:
# Para usar a imagem pronta, troque o build por:
# image: ghcr.io/jailsonsb2/radioplayer:latest
build: .
image: ghcr.io/jailsonsb2/radioplayer:latest
container_name: radioplayer
ports:
- "8080:80"
environment:
RADIO_NAME: "Jailson Web Rádio"
URL_STREAMING: "https://stream.zeno.fm/yn65fsaurfhvv"
# ACCENT_COLOR: "#00e1e7"
# BG_COLOR: "#0b0e13"
# THEME_COLOR: "#0b0e13"
# Logo da sua rádio (192x192 ou maior, quadrado)
# volumes:
# - ./cover.png:/usr/share/nginx/html/img/cover.png:ro
restart: unless-stopped
51 changes: 51 additions & 0 deletions docker/40-radioplayer-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/sh
# Gera config.js e manifest.json a partir das variáveis de ambiente.
# Executado pelo entrypoint da imagem oficial do nginx, antes do servidor subir.
set -e

HTML_DIR="${HTML_DIR:-/usr/share/nginx/html}"

# Escapa para uso dentro de uma string JSON/JS entre aspas duplas.
esc() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}

# --- config.js -------------------------------------------------------------
{
echo '// Gerado automaticamente pelo container a partir das variáveis de'
echo '// ambiente. Não edite: qualquer alteração some no próximo restart.'
echo 'window.RADIO_CONFIG = {'
echo " RADIO_NAME: \"$(esc "$RADIO_NAME")\","
echo " URL_STREAMING: \"$(esc "$URL_STREAMING")\","
if [ -n "$API_URL" ]; then echo " API_URL: \"$(esc "$API_URL")\","; fi
if [ -n "$FALLBACK_API_URL" ]; then echo " FALLBACK_API_URL: \"$(esc "$FALLBACK_API_URL")\","; fi
if [ -n "$ACCENT_COLOR" ]; then echo " ACCENT_COLOR: \"$(esc "$ACCENT_COLOR")\","; fi
if [ -n "$BG_COLOR" ]; then echo " BG_COLOR: \"$(esc "$BG_COLOR")\","; fi
echo '};'
} > "$HTML_DIR/config.js"

# --- manifest.json ---------------------------------------------------------
# PWA_NAME/PWA_SHORT_NAME são opcionais; por padrão seguem o nome da rádio.
PWA_NAME="${PWA_NAME:-$RADIO_NAME}"
PWA_SHORT_NAME="${PWA_SHORT_NAME:-$PWA_NAME}"
BG="${BG_COLOR:-#0b0e13}"

cat > "$HTML_DIR/manifest.json" <<EOF
{
"name": "$(esc "$PWA_NAME")",
"short_name": "$(esc "$PWA_SHORT_NAME")",
"start_url": "./index.html",
"display": "standalone",
"background_color": "$(esc "$BG")",
"theme_color": "$(esc "$THEME_COLOR")",
"icons": [
{
"src": "img/cover.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
EOF

echo "radioplayer: configurado para \"$RADIO_NAME\" ($URL_STREAMING)"
53 changes: 53 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
server {
listen 80;
server_name _;

root /usr/share/nginx/html;
index index.html;

# O nome da rádio pode ter acento e vai parar no config.js/manifest.json.
# Sem o charset declarado o browser precisa adivinhar a codificação.
charset utf-8;
charset_types text/css text/plain application/javascript application/json
application/manifest+json image/svg+xml;

gzip on;
gzip_min_length 256;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;

# Arquivos gerados a partir das variáveis de ambiente: se o browser cachear,
# trocar a env var não surte efeito. Nunca cachear.
location = /config.js {
add_header Cache-Control "no-store" always;
}
location = /manifest.json {
add_header Cache-Control "no-store" always;
}

# O service worker precisa ser revalidado sempre, senão uma versão antiga
# continua servindo o app do cache dela para sempre.
location = /service-worker.js {
add_header Cache-Control "no-cache" always;
}

location = /index.html {
add_header Cache-Control "no-cache" always;
}

# Imagens e fontes praticamente não mudam.
# Só o `expires` — somado a um add_header Cache-Control o header sai
# duplicado na resposta.
location ~* \.(png|jpe?g|gif|svg|ico|woff2?|ttf|eot)$ {
expires 7d;
}

# CSS e JS mudam a cada atualização da imagem — cache curto para que um
# `docker pull` novo apareça rápido.
location ~* \.(css|js)$ {
expires 1h;
}

location / {
try_files $uri $uri/ /index.html;
}
}
Loading
Loading