Env Vars & Geocode Data Caching
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules
|
|||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
|
cache/
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -20,6 +20,34 @@ docker-compose up
|
|||||||
# ws4kp interface available at http://localhost:8080
|
# ws4kp interface available at http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Configure ports and services via environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default values
|
||||||
|
PORT=3000 # Main streaming server port
|
||||||
|
WS4KP_PORT=8080 # WS4KP weather service port
|
||||||
|
MUSIC_PATH=/music # Path to music files
|
||||||
|
|
||||||
|
# Example with custom ports
|
||||||
|
PORT=8000 WS4KP_PORT=9090 docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use a `.env` file with docker-compose:
|
||||||
|
```env
|
||||||
|
PORT=8000
|
||||||
|
WS4KP_PORT=9090
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistent Geocoding Cache
|
||||||
|
|
||||||
|
Geocoding results are cached in the `./cache` directory. When using docker-compose, this directory is automatically mounted to persist between container restarts. If using Docker directly, mount the cache volume to persist data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 3000:3000 -p 8080:8080 -v $(pwd)/cache:/streaming-app/cache ghcr.io/sethwv/ws4kp-to-hls:latest
|
||||||
|
```
|
||||||
|
|
||||||
### Using Docker directly
|
### Using Docker directly
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -72,7 +100,7 @@ http://localhost:3000/weather?city=Miami,FL,USA&width=1280&height=720&fps=25
|
|||||||
http://localhost:3000/weather?city=Toronto,ON,Canada&hideLogo=true
|
http://localhost:3000/weather?city=Toronto,ON,Canada&hideLogo=true
|
||||||
```
|
```
|
||||||
|
|
||||||
**City Format**: Use `City,State,Country` format for best accuracy (e.g., `Toronto,ON,Canada` or `Miami,FL,USA`). The service automatically geocodes the city using OpenStreetMap's Nominatim API and falls back to Toronto coordinates if geocoding fails.
|
**City Format**: Use `City,State,Country` format for best accuracy (e.g., `Toronto,ON,Canada` or `Miami,FL,USA`). The service automatically geocodes the city using OpenStreetMap's Nominatim API and falls back to Toronto coordinates if geocoding fails. Geocoding results are cached in the `./cache` directory to improve performance and reduce API calls.
|
||||||
|
|
||||||
**Units**: Use `units=metric` (default) for Celsius/kph/km/mb or `units=imperial` for Fahrenheit/mph/miles/inHg.
|
**Units**: Use `units=metric` (default) for Celsius/kph/km/mb or `units=imperial` for Fahrenheit/mph/miles/inHg.
|
||||||
|
|
||||||
@@ -146,4 +174,13 @@ ffmpeg -i "http://localhost:3000/stream?url=http://example.com" \
|
|||||||
3. FFmpeg encodes the screenshots into an HLS stream (H.264 video, AAC audio for weather)
|
3. FFmpeg encodes the screenshots into an HLS stream (H.264 video, AAC audio for weather)
|
||||||
4. For weather streams: background music is shuffled and played from the Weatherscan collection
|
4. For weather streams: background music is shuffled and played from the Weatherscan collection
|
||||||
5. The HLS stream is piped directly to the HTTP response
|
5. The HLS stream is piped directly to the HTTP response
|
||||||
6. City names are automatically geocoded to coordinates via OpenStreetMap's Nominatim API
|
6. City names are automatically geocoded to coordinates via OpenStreetMap's Nominatim API (results are cached locally for performance)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Configurable Ports**: Both streaming server and WS4KP service ports are configurable via environment variables
|
||||||
|
- **Geocoding Cache**: City geocoding results are cached on the filesystem to reduce API calls and improve response times
|
||||||
|
- **Background Music**: Weather streams include shuffled Weatherscan music collection
|
||||||
|
- **Multi-platform**: Docker images for AMD64 and ARM64 architectures
|
||||||
|
- **Automatic Geocoding**: City names are converted to coordinates automatically
|
||||||
|
- **Flexible Display Options**: Control which weather forecast sections to show/hide
|
||||||
@@ -2,10 +2,12 @@ services:
|
|||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "${PORT:-3000}:${PORT:-3000}"
|
||||||
- "8080:8080"
|
- "${WS4KP_PORT:-8080}:${WS4KP_PORT:-8080}"
|
||||||
shm_size: 2gb
|
shm_size: 2gb
|
||||||
environment:
|
environment:
|
||||||
- PORT=3000
|
- PORT=${PORT:-3000}
|
||||||
- WS4KP_PORT=8080
|
- WS4KP_PORT=${WS4KP_PORT:-8080}
|
||||||
|
volumes:
|
||||||
|
- ./cache:/streaming-app/cache
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
5
index.js
5
index.js
@@ -5,6 +5,7 @@ const { getAllMusicFiles } = require('./src/musicPlaylist');
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const WS4KP_PORT = process.env.WS4KP_PORT || 8080;
|
||||||
const MUSIC_PATH = process.env.MUSIC_PATH || '/music';
|
const MUSIC_PATH = process.env.MUSIC_PATH || '/music';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +38,8 @@ function buildWeatherUrl(latitude, longitude, settings) {
|
|||||||
const pressureUnit = isMetric ? '1.00' : '2.00';
|
const pressureUnit = isMetric ? '1.00' : '2.00';
|
||||||
const hoursFormat = timeFormat === '12h' ? '1.00' : '2.00';
|
const hoursFormat = timeFormat === '12h' ? '1.00' : '2.00';
|
||||||
|
|
||||||
const ws4kpBaseUrl = process.env.WS4KP_URL || 'http://localhost:8080';
|
const ws4kpPort = process.env.WS4KP_PORT || 8080;
|
||||||
|
const ws4kpBaseUrl = process.env.WS4KP_URL || `http://localhost:${ws4kpPort}`;
|
||||||
|
|
||||||
const ws4kpParams = new URLSearchParams({
|
const ws4kpParams = new URLSearchParams({
|
||||||
'hazards-checkbox': showHazards,
|
'hazards-checkbox': showHazards,
|
||||||
@@ -187,6 +189,7 @@ app.get('/health', (req, res) => {
|
|||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Webpage to HLS server running on port ${PORT}`);
|
console.log(`Webpage to HLS server running on port ${PORT}`);
|
||||||
|
console.log(`WS4KP weather service on port ${WS4KP_PORT}`);
|
||||||
console.log(`Usage: http://localhost:${PORT}/stream?url=http://example.com`);
|
console.log(`Usage: http://localhost:${PORT}/stream?url=http://example.com`);
|
||||||
console.log(`Weather: http://localhost:${PORT}/weather?city=YourCity`);
|
console.log(`Weather: http://localhost:${PORT}/weather?city=YourCity`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,59 @@
|
|||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const CACHE_DIR = path.join(__dirname, '..', 'cache');
|
||||||
|
|
||||||
|
// Ensure cache directory exists
|
||||||
|
if (!fs.existsSync(CACHE_DIR)) {
|
||||||
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a safe filename from a city query
|
||||||
|
* @param {string} cityQuery - City name
|
||||||
|
* @returns {string} Safe filename
|
||||||
|
*/
|
||||||
|
function getCacheFileName(cityQuery) {
|
||||||
|
const hash = crypto.createHash('md5').update(cityQuery.toLowerCase().trim()).digest('hex');
|
||||||
|
return `geocode_${hash}.json`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached geocode data if available
|
||||||
|
* @param {string} cityQuery - City name
|
||||||
|
* @returns {Object|null} Cached data or null
|
||||||
|
*/
|
||||||
|
function getCachedGeocode(cityQuery) {
|
||||||
|
try {
|
||||||
|
const cacheFile = path.join(CACHE_DIR, getCacheFileName(cityQuery));
|
||||||
|
if (fs.existsSync(cacheFile)) {
|
||||||
|
const data = fs.readFileSync(cacheFile, 'utf8');
|
||||||
|
const cached = JSON.parse(data);
|
||||||
|
console.log(`Using cached geocode for: ${cityQuery}`);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Cache read error for ${cityQuery}:`, error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save geocode data to cache
|
||||||
|
* @param {string} cityQuery - City name
|
||||||
|
* @param {Object} data - Geocode data to cache
|
||||||
|
*/
|
||||||
|
function saveCachedGeocode(cityQuery, data) {
|
||||||
|
try {
|
||||||
|
const cacheFile = path.join(CACHE_DIR, getCacheFileName(cityQuery));
|
||||||
|
fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
console.log(`Cached geocode for: ${cityQuery}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Cache write error for ${cityQuery}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Geocode city to lat/lon using Nominatim (OpenStreetMap)
|
* Geocode city to lat/lon using Nominatim (OpenStreetMap)
|
||||||
@@ -6,6 +61,11 @@ const https = require('https');
|
|||||||
* @returns {Promise<{lat: number, lon: number, displayName: string}>}
|
* @returns {Promise<{lat: number, lon: number, displayName: string}>}
|
||||||
*/
|
*/
|
||||||
async function geocodeCity(cityQuery) {
|
async function geocodeCity(cityQuery) {
|
||||||
|
// Check cache first
|
||||||
|
const cached = getCachedGeocode(cityQuery);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const encodedQuery = encodeURIComponent(cityQuery);
|
const encodedQuery = encodeURIComponent(cityQuery);
|
||||||
const url = `https://nominatim.openstreetmap.org/search?q=${encodedQuery}&format=json&limit=1`;
|
const url = `https://nominatim.openstreetmap.org/search?q=${encodedQuery}&format=json&limit=1`;
|
||||||
@@ -27,11 +87,14 @@ async function geocodeCity(cityQuery) {
|
|||||||
try {
|
try {
|
||||||
const results = JSON.parse(data);
|
const results = JSON.parse(data);
|
||||||
if (results && results.length > 0) {
|
if (results && results.length > 0) {
|
||||||
resolve({
|
const geocodeResult = {
|
||||||
lat: parseFloat(results[0].lat),
|
lat: parseFloat(results[0].lat),
|
||||||
lon: parseFloat(results[0].lon),
|
lon: parseFloat(results[0].lon),
|
||||||
displayName: results[0].display_name
|
displayName: results[0].display_name
|
||||||
});
|
};
|
||||||
|
// Save to cache
|
||||||
|
saveCachedGeocode(cityQuery, geocodeResult);
|
||||||
|
resolve(geocodeResult);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('No results found'));
|
reject(new Error('No results found'));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user