commit 037983ab348ba0b5baa20b385c4df440b36a3a97 Author: jarianc Date: Sat May 9 08:57:22 2026 -0500 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa9f6bd --- /dev/null +++ b/README.md @@ -0,0 +1,226 @@ +# 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 \ No newline at end of file