Deno installed in builder stage but never used. Removes ~75MB from image and eliminates unused runtime ENV vars. Closes #34
Lofi Radio
A web app that streams live audio from 20 popular lo-fi YouTube channels as a continuous "radio" feed. No YouTube UI, no downloads — just raw audio playback.
How It Works
- Discover — queries the YouTube Data API v3 to find the current live broadcast for each of the 20 curated channels
- Extract — uses yt-dlp to retrieve the HLS (m3u8) stream URL from the live video
- Validate — checks that the stream is accessible before offering it for playback
- Play — the frontend uses HLS.js to play the stream in-browser with full audio controls
Features
- 20 curated lo-fi YouTube channels (Lofi Girl, Chillhop Music, The Japanese 100, and more)
- Live broadcast discovery via YouTube Data API v3
- HLS stream extraction via yt-dlp
- Stream validation before playback
- HLS-aware audio player (HLS.js with native fallback)
- Play/pause/stop, volume, next/previous channel controls
- Auto-advance to the next live channel when a stream ends
- Favorites list
- Dark-themed responsive UI
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, uv (package manager) |
| Stream extraction | yt-dlp |
| HTTP client | httpx |
| Frontend | React 18, TypeScript, Vite, Tailwind CSS |
| State management | Zustand |
| Audio playback | HLS.js |
| Testing (backend) | pytest, pytest-asyncio |
| Testing (frontend) | vitest, @testing-library/react |
| Linting | ruff (Python), ESLint (TypeScript) |
| Deployment | Docker Compose, nginx |
Prerequisites
- Docker and Docker Compose (v2+)
- A YouTube Data API v3 key (get one here)
Quick Start
# 1. Clone the repo
git clone <repo-url> && cd lofi-app
# 2. Configure environment
cp .env.example .env
# Edit .env and set your YOUTUBE_API_KEY
# 3. Build and start
cd docker
docker compose up -d --build
# 4. Open http://localhost:5173
The app runs two containers:
- lofi-backend — FastAPI server on port 8000
- lofi-frontend — Nginx serving the React build on port 5173 (reverse-proxies
/apito backend)
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
YOUTUBE_API_KEY |
Yes | — | YouTube Data API v3 key |
BACKEND_PORT |
No | 8000 |
Backend listen port |
APP_ENV |
No | development |
Runtime environment |
LOG_LEVEL |
No | info |
Logging verbosity (debug/info/warning/error) |
VITE_API_URL |
No | http://localhost:8000 |
Backend API URL for frontend |
VITE_APP_TITLE |
No | Lofi Radio |
App title shown in browser tab |
VITE_FRONTEND_PORT |
No | 5173 |
Frontend exposed port |
COMPOSE_PROJECT_NAME |
No | lofi-app |
Docker Compose project name |
Project Structure
lofi-app/
├── .env # Environment variables (not in git)
├── .env.example # Template — copy to .env
├── .dockerignore # Docker build context exclusions
├── .gitignore
├── .hadolint.yaml # Dockerfile linting config
├── pyproject.toml # Python project + dependencies (uv)
├── uv.lock # uv dependency lock file
├── docker/ # Docker configuration
│ ├── docker-compose.yml # Service orchestration
│ └── Dockerfile.backend # Python 3.12 + uv backend image
├── src/ # Backend source
│ ├── main.py # FastAPI app (4 endpoints)
│ ├── config.py # Settings from environment
│ ├── channels.py # 20 channel definitions
│ └── modules/
│ ├── discovery.py # YouTube API live broadcast lookup
│ ├── stream_extractor.py # yt-dlp HLS stream extraction
│ └── validator.py # Stream URL validation
├── frontend/ # React frontend
│ ├── Dockerfile # Node 22 build → nginx runtime
│ ├── nginx.conf # Reverse proxy to backend /api
│ ├── package.json # pnpm dependencies
│ ├── pnpm-lock.yaml # pnpm lock file
│ ├── vite.config.ts # Vite build config + proxy
│ ├── tsconfig.json
│ ├── vitest.config.ts # Vitest test config
│ └── src/
│ ├── main.tsx # React entry point
│ ├── App.tsx # Root component
│ ├── types.ts # TypeScript interfaces
│ ├── components/ # UI components
│ │ ├── AudioPlayer.tsx # Player controls + HLS.js
│ │ ├── ChannelList.tsx # Channel grid
│ │ └── Header.tsx # App header
│ ├── hooks/
│ │ └── useAudioPlayer.ts # HLS.js audio player hook
│ ├── services/
│ │ └── api.ts # API client functions
│ ├── store/
│ │ └── audioStore.ts # Zustand state store
│ ├── styles/ # CSS / Tailwind
│ └__tests__/ # Vitest test suites
├── tests/ # Backend tests
│ ├── conftest.py # Pytest fixtures
│ ├── unit/ # Module unit tests
│ │ ├── test_discovery.py
│ │ ├── test_stream_extractor.py
│ │ └── test_validator.py
│ └── integration/
│ └── test_api.py # API endpoint tests
└── data/ # Runtime data (mounted as volume)
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/channels |
GET | List all 20 channels with live status |
/api/channels/{id}/live |
GET | Check if a specific channel is currently live |
/api/stream/{videoId} |
GET | Get HLS/direct stream URL for a video |
/api/now-playing |
GET | Get the current active stream from any channel |
All responses use camelCase keys (videoId, isLive, streamType).
Dockerfile Design
Both Dockerfiles follow a build-only philosophy — runtime configuration (ports, volumes, restart policies) lives in docker-compose.yml exclusively.
- Backend: Single-stage Python 3.12-slim image with uv-managed virtualenv
- Frontend: Multi-stage build (Node 22 for build → nginx:alpine for runtime)
- No
EXPOSEdirectives — ports are mapped in Compose only - All images lint clean with hadolint (warning-level failure threshold)
Development
Backend (local)
# Install dependencies
uv sync
# Run linter
ruff format .
ruff check --fix .
# Run tests
uv run pytest tests/ -v
Frontend (local)
cd frontend
# Install dependencies
pnpm install
# Run linter
pnpm lint
# Type check
pnpm typecheck
# Run tests
pnpm test
# Development server (proxies /api to backend)
pnpm dev
Docker Operations
cd docker
# Start everything
docker compose up -d --build
# View logs
docker compose logs -f backend
docker compose logs -f frontend
# Rebuild a single service
docker compose down backend
docker compose up -d backend --build
# Stop everything
docker compose down
# Cleanup unused images
docker system prune -f
Testing
| Suite | Command | Tests | Coverage |
|---|---|---|---|
| Backend unit | uv run pytest tests/unit/ |
30 | discovery, stream_extractor, validator |
| Backend integration | uv run pytest tests/integration/ |
12 | API endpoints, CORS |
| Frontend unit | pnpm --dir=frontend test |
13 | audioStore, api service |
| Frontend component | pnpm --dir=frontend test |
7 | AudioPlayer, ChannelList |
License
MIT
Description
Lofi radio — streams live audio from 20 curated lofi YouTube channels as a continuous radio feed, with a companion Android client.
Languages
Python
71%
TypeScript
26.6%
JavaScript
1.1%
CSS
0.6%
HTML
0.4%
Other
0.3%