A privacy-focused radio station directory designed for Tor and I2P networks. This FastAPI application provides both a JSON API and a server-rendered HTML interface for discovering, submitting, and managing radio stations accessible through anonymous networks.
- Radio Station Directory - Browse and search stations by network (Tor/I2P), genre, and online status
- Station Submission - Submit new stations with automatic stream validation and approval
- Stream Validation - Validates that URLs point to actual audio streams (not HTML, images, etc.)
- Health Monitoring - Periodic checks to track station online/offline status
- Cover Art Mirroring - Downloads and hosts all cover art locally, serving via Tor-accessible URLs for privacy
- Admin Dashboard - Web-based admin panel for station moderation
- Admin CLI - Command-line tools for bulk operations (import, export, approve, reject)
- Network-Aware Routing - Automatic proxy routing through Tor SOCKS5 or I2P HTTP
- Rate Limiting - API protection with slowapi (60/min for listings, 5/min for submissions)
- No JavaScript
- FastAPI - Modern async web framework
- Uvicorn - ASGI server
- Pydantic - Data validation
- SQLite - Embedded database
- Jinja2 - HTML templating
- aiohttp - Async HTTP client with SOCKS proxy support
- slowapi - Rate limiting
Screenshot preview of the web-based admin panel can be found in /static/covers. Cover art is not available for clearnet mirrors, as all the cover art is hosted over Tor. The rest of the UI can be explored at the Radio Registry website: https://api.deutsia.com
- Python 3.8+
- Tor service running (for Tor station validation)
- I2P router with HTTP proxy (for I2P station validation)
# Clone the repository
git clone <repository-url>
cd Radio-Registry-API
# Run the setup script
./setup.sh
# Or manually:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python -c "import database; database.init_db()"Edit config.py to customize settings:
# Server settings
HOST = "127.0.0.1"
PORT = 8080
# Proxy settings (adjust to your Tor/I2P configuration)
TOR_SOCKS_PROXY = "socks5://127.0.0.1:9050"
I2P_HTTP_PROXY = "http://127.0.0.1:4444"
# IMPORTANT: Change these in production!
ADMIN_PASSWORD = "changeme"
ADMIN_SECRET_KEY = "super-secret-key-change-me"source venv/bin/activate
uvicorn main:app --host 127.0.0.1 --port 8080| Method | Endpoint | Description | Rate Limit |
|---|---|---|---|
| GET | /api/stations |
List approved stations (paginated) | 60/min |
| GET | /api/stations/{id} |
Get station details | - |
| GET | /api/stations/{id}/cover |
Get station cover art | - |
| POST | /api/submit |
Submit a new station | 5/min |
| GET | /api/stats |
Get directory statistics | - |
| GET | /api/genres |
List available genres | - |
| GET | /api/health |
API health check | - |
GET /api/stations
| Parameter | Type | Description |
|---|---|---|
network |
string | Filter by network: tor or i2p |
genre |
string | Filter by genre |
online_only |
boolean | Only show online stations |
page |
integer | Page number (default: 1) |
per_page |
integer | Items per page (default: 50, max: 200) |
# List all Tor stations
curl "http://localhost:8080/api/stations?network=tor"
# List online electronic stations
curl "http://localhost:8080/api/stations?genre=Electronic&online_only=true"
# Get directory stats
curl "http://localhost:8080/api/stats"
# Submit a new station
curl -X POST "http://localhost:8080/api/submit" \
-H "Content-Type: application/json" \
-d '{
"name": "My Radio Station",
"stream_url": "http://example.onion:8000/stream",
"network": "tor",
"genre": "Electronic"
}'Station List Response
{
"stations": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Example Radio",
"stream_url": "http://example.onion:8000/stream",
"homepage": "http://example.onion",
"network": "tor",
"genre": "Electronic",
"codec": "MP3",
"bitrate": 128,
"language": "English",
"country": "Unknown",
"is_online": true,
"last_check_time": "2024-01-15T12:00:00Z"
}
],
"total": 42,
"page": 1,
"per_page": 50,
"pages": 1
}Stats Response
{
"total_stations": 100,
"online_stations": 75,
"tor_stations": 60,
"i2p_stations": 40,
"pending_submissions": 5
}The server-rendered HTML interface is accessible at:
| Path | Description |
|---|---|
/ |
Station listing with filters |
/station/{id} |
Station detail page |
/submit |
Station submission form |
/about |
About page |
/admin |
Admin login |
/admin/dashboard |
Admin dashboard |
The admin.py script provides command-line management tools:
# List all stations
python admin.py list
# List pending submissions
python admin.py pending
# Show statistics
python admin.py stats
# Approve a station
python admin.py approve <station-id>
# Reject a station
python admin.py reject <station-id> --reason "Invalid stream"
# Delete a station
python admin.py delete <station-id>
# Get station info
python admin.py info <station-id>
# Import stations from JSON
python admin.py import stations.json --network tor --approve
# Export stations to JSON
python admin.py export output.json --network tor --status approved[
{
"name": "Station Name",
"stream_url": "http://example.onion:8000/stream",
"homepage": "http://example.onion",
"genre": "Electronic",
"codec": "MP3",
"bitrate": 128,
"language": "English",
"country": "Unknown"
}
]The checker.py script performs smart health checks with escalating rechecks to prevent false offline status from transient failures.
Stations have three health states:
| Status | Description |
|---|---|
| online | Last check succeeded |
| offline | Failed recently, but was online within 12 hours (being rechecked) |
| dead | No successful response for 12+ hours |
When a station fails a check, it doesn't immediately show as offline. Instead, the system performs escalating rechecks:
✓ Online → next check in 4 hours
✗ Fail #1 → recheck in 5 minutes
✗ Fail #2 → recheck in 15 minutes
✗ Fail #3 → recheck in 1 hour
✗ Fail #4+ → confirmed offline, regular 4-hour checks resume
If a station remains unreachable for 12+ hours, it's marked as dead.
This prevents reliable stations from appearing offline due to temporary network issues on Tor/I2P.
In config.py:
# Recheck intervals for failed stations (in minutes)
RECHECK_INTERVALS_MINUTES = [5, 15, 60] # 5 min, 15 min, 1 hour
# Time threshold for "dead" status (in hours)
DEAD_THRESHOLD_HOURS = 12
# Regular check interval for online stations
HEALTH_CHECK_INTERVAL_HOURS = 4# Run manually
python checker.py
# Set up cron job (every 5 minutes - uses smart scheduling)
*/5 * * * * /path/to/venv/bin/python /path/to/checker.py >> /path/to/checker.log 2>&1Note: The cron runs every 5 minutes, but the checker only checks stations that are actually due. Online stations won't be hammered - they're only checked every 4 hours. The frequent cron is to catch the escalating rechecks for recently-failed stations.
============================================================
Health check run - 2024-01-15 12:00:00
============================================================
Stations due for check: 5
Tor: 3, I2P: 2
Regular: 2, Rechecks: 2, Dead: 1
Checking 3 Tor stations via socks5://127.0.0.1:9050...
[regular] http://example.onion/stream
✓ online
[recheck #2] http://failing.onion/stream
✗ offline
[dead-check] http://dead.onion/stream
✗ offline
============================================================
Health check complete
Checked: 5
Online: 2, Offline: 3
Recovered: 1 (were offline, now online)
============================================================
A systemd service file is provided (radio-api.service):
# Copy and edit the service file
sudo cp radio-api.service /etc/systemd/system/
sudo nano /etc/systemd/system/radio-api.service
# Enable and start
sudo systemctl enable radio-api
sudo systemctl start radio-api
# Check status
sudo systemctl status radio-apiThe service file includes security features:
- Memory limit (200MB)
- CPU quota (50%)
- No new privileges
- Read-only home directory
- Private /tmp
For HTTPS access, use a reverse proxy like nginx or Cloudflare Tunnel:
server {
listen 443 ssl;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}.
├── main.py # FastAPI application and routes
├── database.py # SQLite database operations
├── models.py # Pydantic models
├── config.py # Configuration settings
├── stream_validator.py # Audio stream validation
├── cover_downloader.py # Cover art downloading
├── checker.py # Health check script
├── admin.py # CLI admin tool
├── requirements.txt # Python dependencies
├── setup.sh # Setup script
├── radio-api.service # Systemd service file
├── stations.db # SQLite database
├── templates/ # Jinja2 HTML templates
│ ├── base.html
│ ├── index.html
│ ├── station.html
│ ├── submit.html
│ ├── about.html
│ ├── admin_login.html
│ ├── admin.html
│ ├── 404.html
│ └── 500.html
└── static/ # Static files
└── covers/ # Cached cover art
Ensure Tor is running with SOCKS proxy on port 9050:
# Install Tor
sudo apt install tor
# Verify it's running
curl --socks5 127.0.0.1:9050 https://check.torproject.org/api/ipEnsure I2P HTTP proxy is available on port 4444:
# Download I2P installer
wget https://geti2p.net/en/download/2.10.0/clearnet/https/files.i2p-projekt.de/i2pinstall_2.10.0.jar/download -O i2pinstall.jar
# Install I2P (follow the GUI installer prompts)
java -jar i2pinstall.jar
# Start I2P router
~/i2p/i2prouter start
# Verify proxy (should return I2P content)
curl --proxy http://127.0.0.1:4444 http://i2p-projekt.i2pOnce deployed, configure your mirrors in config.py. Example:
| Network | URL | Notes |
|---|---|---|
| Tor | http://your-onion-address.onion |
Use OnionBalance if your service needs 24/7 Tor uptime |
| I2P | http://your-i2p-address.b32.i2p |
|
| Clearnet | https://your-domain.com |
Optional |
- MP3
- AAC
- OGG Vorbis
- Opus
- FLAC
- WAV
- WMA
- Direct streams (Icecast/Shoutcast with ICY metadata)
- HLS (HTTP Live Streaming / m3u8 playlists)
- DASH (Dynamic Adaptive Streaming)
- JPEG
- PNG
- GIF
- WebP
- SVG
To protect user privacy, all cover art is downloaded and hosted locally rather than loading from external sources. When a station is submitted with a cover art URL:
- The image is downloaded through Tor (even for clearnet URLs)
- Saved locally to
static/covers/with a hashed filename - Served via the Tor hidden service URL
This prevents external tracking and ensures cover art works reliably over Tor/I2P. Cover art is limited to 8MB per image.
Configuration in config.py:
# Base URL for Tor-accessible cover art
TOR_BASE_URL = "http://your-onion-address.onion"| Endpoint | Limit |
|---|---|
/api/stations |
60 requests/minute |
/api/submit |
5 requests/minute |
| Other endpoints | No limit |
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
This project is licensed under the Apache License 2.0. See LICENSE for details.