first commit
This commit is contained in:
commit
037983ab34
226
README.md
Normal file
226
README.md
Normal file
@ -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 <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 `/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
|
||||
Loading…
x
Reference in New Issue
Block a user