const express = require('express'); const { streamHandler } = require('./src/streamHandler'); const { geocodeCity } = require('./src/geocode'); const { getAllMusicFiles, initializeSharedPlaylist } = 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'; /** * Build WS4KP weather URL with given coordinates and settings */ function buildWeatherUrl(latitude, longitude, settings) { const { city, showHazards, showCurrent, showLatestObservations, showHourly, showHourlyGraph, showTravel, showRegionalForecast, showLocalForecast, showExtendedForecast, showAlmanac, showRadar, showMarineForecast, showAQI, units, timeFormat } = settings; const isMetric = units.toLowerCase() === 'metric'; const temperatureUnit = isMetric ? '1.00' : '2.00'; const windUnit = isMetric ? '1.00' : '2.00'; const distanceUnit = isMetric ? '1.00' : '2.00'; const pressureUnit = isMetric ? '1.00' : '2.00'; const hoursFormat = timeFormat === '12h' ? '1.00' : '2.00'; const ws4kpPort = process.env.WS4KP_PORT || 8080; const ws4kpBaseUrl = process.env.WS4KP_URL || `http://localhost:${ws4kpPort}`; const ws4kpParams = new URLSearchParams({ 'hazards-checkbox': showHazards, 'current-weather-checkbox': showCurrent, 'latest-observations-checkbox': showLatestObservations, 'hourly-checkbox': showHourly, 'hourly-graph-checkbox': showHourlyGraph, 'travel-checkbox': showTravel, 'regional-forecast-checkbox': showRegionalForecast, 'local-forecast-checkbox': showLocalForecast, 'extended-forecast-checkbox': showExtendedForecast, 'almanac-checkbox': showAlmanac, 'radar-checkbox': showRadar, 'marine-forecast-checkbox': showMarineForecast, 'aqi-forecast-checkbox': showAQI, 'settings-experimentalFeatures-checkbox': 'false', 'settings-hideWebamp-checkbox': 'true', 'settings-kiosk-checkbox': 'false', 'settings-scanLines-checkbox': 'false', 'settings-wide-checkbox': 'true', 'chkAutoRefresh': 'true', 'settings-windUnits-select': windUnit, 'settings-marineWindUnits-select': '1.00', 'settings-marineWaveHeightUnits-select': '1.00', 'settings-temperatureUnits-select': temperatureUnit, 'settings-distanceUnits-select': distanceUnit, 'settings-pressureUnits-select': pressureUnit, 'settings-hoursFormat-select': hoursFormat, 'settings-speed-select': '1.00', 'latLonQuery': city, 'latLon': JSON.stringify({ lat: latitude, lon: longitude }), 'kiosk': 'true' }); return `${ws4kpBaseUrl}/?${ws4kpParams.toString()}`; } // Basic stream endpoint (no music) app.get('/stream', (req, res) => { const startTime = Date.now(); streamHandler(req, res, { useMusic: false, musicPath: MUSIC_PATH, startTime }); }); // Weather endpoint (with music) app.get('/weather', async (req, res) => { const { city = 'Toronto, ON, CAN', width = 1920, height = 1080, fps = 30, hideLogo = 'false', units = 'metric', timeFormat = '24h', showHazards = 'true', showCurrent = 'true', showHourly = 'true', showHourlyGraph = 'true', showLocalForecast = 'true', showExtendedForecast = 'true', showRadar = 'true', showAQI = 'true', showAlmanac = 'true', showLatestObservations = 'false', showRegionalForecast = 'false', showTravel = 'false', showMarineForecast = 'false' } = req.query; const weatherSettings = { city, showHazards, showCurrent, showLatestObservations, showHourly, showHourlyGraph, showTravel, showRegionalForecast, showLocalForecast, showExtendedForecast, showAlmanac, showRadar, showMarineForecast, showAQI, units, timeFormat }; let lateGeocodePromise = null; let initialUrl = 'data:text/html,'; if (city && city !== 'Toronto, ON, CAN') { // Start geocoding (only call once) const geocodePromise = geocodeCity(city); // Try to use quick result if available within 1 second const quickResult = Promise.race([ geocodePromise, new Promise((_, reject) => setTimeout(() => reject(new Error('Geocoding timeout')), 1000)) ]).catch(() => null); // Timeout = null, will use late result // Build URL from quick result if available const urlPromise = quickResult.then(geoResult => { if (geoResult) { // Got quick result return buildWeatherUrl(geoResult.lat, geoResult.lon, weatherSettings); } return null; // Will use initial black screen }); // Late geocode promise (reuses the same geocode call) lateGeocodePromise = geocodePromise.then(geoResult => { return buildWeatherUrl(geoResult.lat, geoResult.lon, weatherSettings); }).catch(err => { console.warn(`Geocoding failed for ${city}, using fallback`); // Fallback to Toronto return buildWeatherUrl(43.6532, -79.3832, weatherSettings); }); } else { // Toronto default initialUrl = buildWeatherUrl(43.6532, -79.3832, weatherSettings); } const startTime = Date.now(); // Update request query for stream handler req.query.url = initialUrl; req.query.width = width; req.query.height = height; req.query.fps = fps; req.query.hideLogo = hideLogo; // Call stream handler with music enabled return streamHandler(req, res, { useMusic: true, musicPath: MUSIC_PATH, lateGeocodePromise, startTime }); }); // Health check endpoint app.get('/health', (req, res) => { res.send('OK'); }); // Start server app.listen(PORT, () => { console.log(`Webpage to HLS server running on port ${PORT}`); if (process.env.WS4KP_EXTERNAL_PORT) { console.log(`WS4KP weather service on port ${process.env.WS4KP_EXTERNAL_PORT}`); } console.log(`Usage: http://localhost:${PORT}/stream?url=http://example.com`); console.log(`Weather: http://localhost:${PORT}/weather?city=YourCity`); // Pre-validate music files on startup to cache results if (MUSIC_PATH) { console.log(`\nInitializing music library at ${MUSIC_PATH}...`); const validFiles = getAllMusicFiles(MUSIC_PATH); if (validFiles.length > 0) { console.log(`Music library ready: ${validFiles.length} tracks validated`); // Initialize shared playlist at startup initializeSharedPlaylist(MUSIC_PATH); console.log(''); } else { console.log(`Warning: No valid music files found in ${MUSIC_PATH}\n`); } } });