A comprehensive Node.js backend API for FM4 (Austria's alternative radio station) that provides historical broadcast data, live streaming information, and music metadata. Built with Express.js, SQLite with FTS5 full-text search, and real-time monitoring capabilities.
- Live Broadcast Monitoring - Real-time tracking of currently playing broadcasts and items
- Historical Data - Access to 30+ days of broadcast history with detailed metadata
- Full-Text Search - Advanced search across broadcasts and items using SQLite FTS5
- Image Management - Dual-resolution image processing and serving
- RESTful API - Clean, well-documented API endpoints with Swagger/OpenAPI documentation
- Real-time Updates - Automatic live broadcast item detection and database updates
- Graceful Shutdown - Proper signal handling (SIGTERM, SIGINT, SIGHUP, SIGBREAK)
- Input Validation - Comprehensive request validation middleware
- Error Handling - Global error handler with structured responses
- Logging - Winston-based logging with daily rotation and zstd compression
- Scheduled Tasks - Automated scraping, cleanup, and monitoring via node-cron
- Database Optimization - Indexed queries, prepared statements, and FTS5 search
- Compression - Response compression and image optimization with Sharp
- Security - Helmet.js security headers and CORS support
- Requirements
- Installation
- Configuration
- Usage
- API Documentation
- Database Schema
- Architecture
- Scripts
- Benchmarking
- Development
- Deployment
- Troubleshooting
- License
- Node.js >= 24.0.0
- NPM or Yarn
- Storage - Minimum 500MB for database and images
- Memory - Minimum 512MB RAM
- OS - Windows, Linux, or macOS
Windows (PowerShell):
pwsh start.ps1Windows (Batch):
start.batLinux/macOS (Bash):
chmod +x start.sh
./start.sh-
Clone the repository:
git clone <repository-url> cd node-fm4-backend
-
Install dependencies:
npm install
-
Configure environment:
cp .env.example .env # Edit .env with your settings -
Initialize database:
npm run init-db
-
Initial data scraping (optional but recommended):
npm run scrape
-
Start the server:
npm start
Create a .env file in the root directory:
# Server Configuration
PORT=3000
NODE_ENV=development
# Database
DATABASE_PATH=./data/fm4.db
# FM4 API Endpoints
FM4_API_BASE_URL=https://audioapi.orf.at/fm4/json/4.0
FM4_LIVE_STREAM_URL=https://orf-live.ors-shoutcast.at/fm4-q2a
FM4_LOOPSTREAM_BASE_URL=https://loopstreamfm4.apa.at
# Scraper Settings
SCRAPE_INTERVAL_HOURS=6
KEEP_HISTORY_DAYS=31
# Image Settings
IMAGE_STORAGE_PATH=./data/images
IMAGE_MAX_WIDTH=1750
# Logging
LOG_LEVEL=info
LOG_FILE=./logs/app.log| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP server port |
NODE_ENV |
development |
Environment mode (development/production) |
DATABASE_PATH |
./data/fm4.db |
SQLite database file path |
SCRAPE_INTERVAL_HOURS |
6 |
Hours between automatic scrapes |
KEEP_HISTORY_DAYS |
31 |
Days of history to retain |
IMAGE_MAX_WIDTH |
1750 |
Maximum image width in pixels |
LOG_LEVEL |
info |
Logging level (error/warn/info/debug) |
Development mode (with auto-reload):
npm run devProduction mode:
npm startUsing PM2 (recommended for production):
npm install -g pm2
pm2 start src/server.js --name fm4-api
pm2 save
pm2 startup- API Base URL:
http://localhost:3000 - API Documentation:
http://localhost:3000/api-docs - Health Check:
http://localhost:3000/health
Visit http://localhost:3000/api-docs for full Swagger/OpenAPI interactive documentation.
GET /health- Health checkGET /- API information and endpoint list
GET /api/live- Get currently live broadcast
GET /api/broadcasts- Get all broadcasts (last 30 days)GET /api/broadcasts/:day- Get broadcasts for specific day (YYYYMMDD)GET /api/broadcast/:programKey/:day- Get specific broadcast
GET /api/item/:id- Get specific broadcast item by FM4 item ID
GET /api/search?q=<query>- Full-text search- Query parameters:
q(required) - Search query (2-200 characters)type(optional) - Filter by type:broadcasts,items, oralllimit(optional) - Results per page (1-100, default: 50)offset(optional) - Pagination offset (default: 0)
- Query parameters:
GET /api/program-keys- Get all known program keys
GET /images/:hash- Get image by SHA-256 hash- Query parameters:
resolution(optional) -highorlow(default:high)
- Query parameters:
Admin Endpoints (Hidden)
Admin endpoints are functional but not listed in public documentation:
POST /admin/scrape/full- Trigger full historical scrapePOST /admin/scrape/recent- Trigger recent broadcasts scrapePOST /admin/scrape/broadcast- Scrape specific broadcastPOST /admin/discover-keys- Discover new program keysPOST /admin/cleanup- Trigger cleanup of old dataGET /admin/stats- Get database statisticsGET /admin/live-monitor- Get live monitor statusPOST /admin/rotate-logs- Manually rotate logs
Stores main broadcast information:
id- Primary keybroadcast_day- YYYYMMDD formatprogram_key- Unique program identifiertitle,subtitle,description- Broadcast metadatamoderator- Show host/moderatorstart_time,end_time- Unix timestampsloop_stream_id- Loopstream identifier for on-demand playbackdone- Flag indicating completed broadcast- Unique constraint:
(broadcast_day, program_key)
Individual songs, jingles, ads within broadcasts:
id- Primary keybroadcast_id- Foreign key to broadcastsitem_id- FM4 API item IDtype- Item type (song, jingle, ad, etc.)title,interpreter- Track informationstart_time,end_time- Unix timestampsstart_offset,end_offset- Offsets within loopstream
Image metadata and file paths:
id- Primary keyhash- SHA-256 hash of imageresolution_type-highorlowfile_path- Local storage pathwidth,height- Image dimensions
Links images to broadcasts/items:
entity_type-broadcastorbroadcast_itementity_id- ID of broadcast or itemimage_id- Foreign key to imagesresolution_type- Resolution variant
Known program identifiers:
program_key- Unique program identifiertitle- Program title
Virtual table for broadcast search:
- Indexed fields:
program_key,title,subtitle,description,moderator,program - Uses porter stemming and unicode61 tokenizer
Virtual table for item search:
- Indexed fields:
type,title,interpreter,description - Linked via
rowidto main tables
node-fm4-backend/
βββ src/
β βββ server.js # Main application entry point
β βββ config/
β β βββ config.js # Configuration management
β β βββ swagger.js # OpenAPI/Swagger setup
β βββ database/
β β βββ database.js # Database service layer
β β βββ schema.js # Table definitions
β βββ middleware/
β β βββ validation.js # Request validation
β β βββ error-handler.js # Global error handling
β βββ routes/
β β βββ api.js # Public API routes
β β βββ images.js # Image serving routes
β β βββ admin.js # Admin routes
β βββ services/
β β βββ broadcast-scraper.js # Scraping logic
β β βββ fm4-api.js # FM4 API client
β β βββ image-service.js # Image processing
β β βββ live-monitor.js # Live broadcast monitoring
β β βββ scheduled-tasks.js # Cron jobs
β βββ utils/
β β βββ broadcast-transformer.js # Data transformation
β β βββ logger.js # Winston logger
β β βββ log-rotation.js # Log rotation
β βββ scripts/
β βββ init-database.js # Database initialization
β βββ scrape-broadcasts.js # Manual scraping
β βββ benchmark-database.js # DB benchmarking
β βββ benchmark-api.js # API benchmarking
β βββ migrate-add-fts.js # FTS setup
βββ data/
β βββ fm4.db # SQLite database
β βββ images/ # Stored images
βββ logs/
β βββ app.log # Application logs
βββ benchmark-results/ # Benchmark outputs
βββ .env # Environment configuration
βββ package.json
βββ start.ps1 # Windows PowerShell setup
βββ start.bat # Windows batch setup
βββ start.sh # Linux/macOS bash setup
- Express application setup
- Middleware configuration
- Route mounting
- Graceful shutdown handling
- Automatic FTS initialization
- First-run detection and bootstrap
- Singleton pattern for database access
- Prepared statements for performance
- Transaction support
- FTS search methods
- CRUD operations for all entities
- Historical data fetching
- Recent broadcast updates
- Program key discovery
- Optimized scraping strategy (done flag)
- Error handling and retry logic
- 30-second polling interval
- Real-time item detection
- Broadcast state tracking
- Memory-efficient caching
- Duplicate prevention
- Dual-resolution processing
- SHA-256 hash generation
- Sharp-based optimization
- Orphaned file cleanup
# Start server (production)
npm start
# Start server (development with auto-reload)
npm run dev
# Initialize/reset database
npm run init-db
# Scrape broadcasts (manual)
npm run scrape
# Run database benchmarks
npm run benchmark
# Run API benchmarks (requires running server)
npm run benchmark:api
# Show setup instructions
npm run setup# Initialize FTS tables
node src/scripts/migrate-add-fts.js
# Check broadcast done status
node src/scripts/check-done-status.js
# Test FM4 API connectivity
node src/scripts/test-fm4-api.js
# Test live endpoint
node src/scripts/test-live-endpoint.js
# Test search functionality
node src/scripts/test-search.jsTest database performance with 32+ comprehensive tests:
npm run benchmarkTest categories:
- Broadcast queries (indexed, by ID, date ranges)
- Broadcast item queries
- Full-text search (FTS5)
- Image queries
- Aggregations
- Write operations (simulated)
- Transaction overhead
- Index efficiency
- Stress tests (10k+ records)
Sample output:
Total Tests: 44
Total Time: 15019.01ms
Average Time: 341.34ms
Fastest: 0.008ms (Count all broadcasts)
Slowest: 13.062ms (Complex JOIN)
Results saved to: benchmark-results/benchmark-[timestamp].json
Test API endpoint performance (requires running server):
# In terminal 1
npm start
# In terminal 2
npm run benchmark:apiTest coverage:
- System endpoints (health, docs)
- Live broadcast
- All broadcast endpoints
- Broadcast items
- Program keys
- Full-text search with various parameters
- Image serving (high/low resolution)
- Error handling (404, 400 responses)
- Edge cases and validation
- Concurrent requests (50 simultaneous)
Sample output:
Total Tests: 45
Passed: 45
Failed: 0
Success Rate: 100.0%
Total Time: 325.266ms
Average Time: 7.228ms
Results saved to: benchmark-results/api-benchmark-[timestamp].json
- ES Modules - Modern
import/exportsyntax - Async/Await - Promise-based asynchronous code
- JSDoc Comments - For API documentation and Swagger generation
- Error Handling - Try-catch blocks with proper logging
-
Add route in appropriate router:
// src/routes/api.js router.get('/new-endpoint', validateMiddleware, asyncHandler(async (req, res) => { // Implementation }) );
-
Add OpenAPI documentation:
/** * @openapi * /api/new-endpoint: * get: * summary: Endpoint description * tags: * - Category * responses: * 200: * description: Success response */
-
Update tests and benchmarks
Create migration scripts in src/scripts/:
// migrate-add-new-column.js
import Database from 'better-sqlite3';
const db = new Database('./data/fm4.db');
db.exec(`
ALTER TABLE broadcasts
ADD COLUMN new_column TEXT;
`);
console.log('Migration complete');
db.close();Run with: node src/scripts/migrate-add-new-column.js
While formal tests aren't included, use these approaches:
-
Manual testing:
npm run dev # Test endpoints with curl/Postman -
API benchmarks as tests:
npm run benchmark:api
-
Utility test scripts:
node src/scripts/test-fm4-api.js node src/scripts/test-search.js
-
Environment Configuration:
NODE_ENV=production LOG_LEVEL=warn SCRAPE_INTERVAL_HOURS=6 KEEP_HISTORY_DAYS=31
-
Process Management:
npm install -g pm2 pm2 start src/server.js --name fm4-api -i max pm2 startup pm2 save
-
Reverse Proxy (nginx):
server { listen 80; server_name api.example.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }
-
Monitoring:
pm2 monit pm2 logs fm4-api
-
Backups:
# Database backup cp data/fm4.db data/fm4.db.backup # Or use SQLite backup command sqlite3 data/fm4.db ".backup data/fm4-backup-$(date +%Y%m%d).db"
Create Dockerfile:
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN mkdir -p data/images logs
EXPOSE 3000
CMD ["node", "src/server.js"]Create docker-compose.yml:
version: '3.8'
services:
fm4-api:
build: .
ports:
- "3000:3000"
volumes:
- ./data:/app/data
- ./logs:/app/logs
environment:
- NODE_ENV=production
- PORT=3000
restart: unless-stoppedDeploy:
docker-compose up -dError: Cannot find module 'express'
Solution: Install dependencies
npm installError: SQLITE_CANTOPEN: unable to open database file
Solution: Create data directory
mkdir -p data
npm run init-dbError: no such table: broadcasts_fts
Solution: Initialize FTS tables (should happen automatically)
node src/scripts/migrate-add-fts.jsError: listen EADDRINUSE: address already in use :::3000
Solution: Change port or kill existing process
# Change port in .env
PORT=3001
# Or kill existing process (Windows)
Get-Process -Name node | Stop-Process -Force
# Or kill existing process (Linux/Mac)
lsof -ti:3000 | xargs killError: Image not found
Solution: Check image storage path and permissions
ls -la data/images/
chmod -R 755 data/images/Solution: Adjust scraping intervals and history retention
SCRAPE_INTERVAL_HOURS=12
KEEP_HISTORY_DAYS=14Check logs for detailed error information:
# View recent logs
tail -f logs/app.log
# Search for errors
grep ERROR logs/app.log
# View compressed logs
zstd -d logs/2025-11-01.log.zstd -o - | lessEnable debug logging:
LOG_LEVEL=debugCheck server health:
curl http://localhost:3000/health
# Expected response:
{
"status": "ok",
"timestamp": "2025-11-01T...",
"uptime": 3600.5
}MIT License - see LICENSE file for details
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
For issues and questions:
- Open an issue on GitHub
- Check existing documentation
- Review logs for error details
- FM4 - For providing the API and broadcast data
- ORF - Austrian Broadcasting Corporation
- SQLite - For the excellent database engine
- Express.js - For the web framework
- Node.js - For the runtime environment
Built with β€οΈ for FM4 listeners and developers