jarianc ae822ea505
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / docker-build (push) Waiting to run
CI / security (push) Waiting to run
CI / build-result (push) Blocked by required conditions
Merge pull request 'fix: fix docker build for CI' (#50) from ci-fix into main
Reviewed-on: https://git.home.ms/jarianc/lofi-app/pulls/50
2026-07-05 19:13:28 -05:00
2026-05-09 08:57:54 -05:00
2026-07-05 23:59:35 +00:00
2026-05-12 16:06:28 -05:00
2026-05-12 16:06:28 -05:00
2026-05-09 08:57:54 -05:00
2026-05-09 08:57:54 -05:00
2026-05-10 16:02:58 +00:00
2026-05-09 08:57:22 -05:00

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)

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 /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)

# 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.
Readme 302 KiB
Languages
Kotlin 42.2%
Python 40.7%
TypeScript 15.3%
Dockerfile 0.7%
JavaScript 0.6%
Other 0.5%