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