opencode 55244b5027 fix: resolve 35 open issues across backend, Android, frontend, Docker
Backend (src/main.py):
- Add shared httpx.Client singleton for connection reuse (#24)
- Add cache eviction for expired entries (#23)
- Expand CORS to allow POST and OPTIONS (#25)
- Preserve HLS tags (EXT-X-TARGETDURATION, EXT-X-MAP, etc) in rewrite (#12)
- Replace urllib with httpx.stream for direct streaming

Backend (src/modules/stream_extractor.py):
- Use extract_flat='in_playlist' for reduced latency (#35)

Tests (tests/integration/test_api.py):
- Parameterize channel count assertion against CHANNELS (#20)

Android (Channel.kt):
- Change mutable vars to immutable vals, use copy() pattern (#19)

Android (ServerApi.kt):
- Fix JSON parsing for bare array response (#21)
- Close response body on error path (#29)

Android (YouTubeExtractor.kt):
- Add ConnectionPool config (10 idle, 30s keepalive) (#28)
- Tighten findHlsUrl to require audio codec (#26)

Android (AudioPlayer.kt):
- Remove false error on STATE_READY + !isPlaying (#33)

Android (MainActivity.kt):
- Reuse fragments via findFragmentByTag + show/hide (#18)
- Move AudioPlayer.release() to Activity.onDestroy (#17)

Android (LofiViewModel.kt):
- Wrap YouTubeExtractor calls in withContext(Dispatchers.IO) (#16)
- Use immutable channel copies in discoverAllChannels (#19)
- Remove audioPlayer.release() from onCleared (#17)

Android (ChannelListFragment.kt):
- Tie swipe refresh to isDiscovering LiveData (#32)

Android (AndroidManifest.xml):
- Set allowBackup=false (#15)

Android (Preferences.kt):
- Remove hardcoded IP, default to empty string (#14)

Android (proguard-rules.pro):
- Add ExoPlayer media3 HLS ProGuard rules (#34)

Frontend (useAudioPlayer.ts):
- Add retry limit (3) with exponential backoff for NETWORK_ERROR (#27)

Docker (Dockerfile.backend):
- Add playwright install chromium step (#22)
2026-07-05 12:33:26 +00:00
2026-05-09 08:57:54 -05: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 307 KiB
Languages
Python 71%
TypeScript 26.6%
JavaScript 1.1%
CSS 0.6%
HTML 0.4%
Other 0.3%