# 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 1. **Discover** — queries the YouTube Data API v3 to find the current live broadcast for each of the 20 curated channels 2. **Extract** — uses yt-dlp to retrieve the HLS (m3u8) stream URL from the live video 3. **Validate** — checks that the stream is accessible before offering it for playback 4. **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](https://console.cloud.google.com/apis/credentials)) ## Quick Start ```bash # 1. Clone the repo git clone && 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 `/api` to 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 `EXPOSE` directives — ports are mapped in Compose only - All images lint clean with hadolint (warning-level failure threshold) ## Development ### Backend (local) ```bash # Install dependencies uv sync # Run linter ruff format . ruff check --fix . # Run tests uv run pytest tests/ -v ``` ### Frontend (local) ```bash 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 ```bash 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