diff --git a/.gitignore b/.gitignore index 607c9a0..f89bd97 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules *.log .DS_Store .env +cache/ diff --git a/README.md b/README.md index 24c9e9b..8684625 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,34 @@ docker-compose up # 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 ```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 ``` -**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. @@ -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) 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 -6. City names are automatically geocoded to coordinates via OpenStreetMap's Nominatim API \ No newline at end of file +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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d2ebe1c..39042fe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,12 @@ services: app: build: . ports: - - "3000:3000" - - "8080:8080" + - "${PORT:-3000}:${PORT:-3000}" + - "${WS4KP_PORT:-8080}:${WS4KP_PORT:-8080}" shm_size: 2gb environment: - - PORT=3000 - - WS4KP_PORT=8080 + - PORT=${PORT:-3000} + - WS4KP_PORT=${WS4KP_PORT:-8080} + volumes: + - ./cache:/streaming-app/cache restart: unless-stopped diff --git a/index.js b/index.js index 3fa9b35..2dcc706 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ const { getAllMusicFiles } = require('./src/musicPlaylist'); const app = express(); const PORT = process.env.PORT || 3000; +const WS4KP_PORT = process.env.WS4KP_PORT || 8080; 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 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({ 'hazards-checkbox': showHazards, @@ -187,6 +189,7 @@ app.get('/health', (req, res) => { // Start server app.listen(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(`Weather: http://localhost:${PORT}/weather?city=YourCity`); diff --git a/src/geocode.js b/src/geocode.js index 919863b..be38161 100644 --- a/src/geocode.js +++ b/src/geocode.js @@ -1,4 +1,59 @@ 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) @@ -6,6 +61,11 @@ const https = require('https'); * @returns {Promise<{lat: number, lon: number, displayName: string}>} */ async function geocodeCity(cityQuery) { + // Check cache first + const cached = getCachedGeocode(cityQuery); + if (cached) { + return cached; + } return new Promise((resolve, reject) => { const encodedQuery = encodeURIComponent(cityQuery); const url = `https://nominatim.openstreetmap.org/search?q=${encodedQuery}&format=json&limit=1`; @@ -27,11 +87,14 @@ async function geocodeCity(cityQuery) { try { const results = JSON.parse(data); if (results && results.length > 0) { - resolve({ + const geocodeResult = { lat: parseFloat(results[0].lat), lon: parseFloat(results[0].lon), displayName: results[0].display_name - }); + }; + // Save to cache + saveCachedGeocode(cityQuery, geocodeResult); + resolve(geocodeResult); } else { reject(new Error('No results found')); }