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'; const DEFAULT_FPS = parseInt(process.env.DEFAULT_FPS || '30'); const SCREENSHOT_FORMAT = process.env.SCREENSHOT_FORMAT || 'jpeg'; const SCREENSHOT_QUALITY = parseInt(process.env.SCREENSHOT_QUALITY || '95'); const DEBUG_MODE = process.env.DEBUG_MODE === 'true'; /** * 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 (with optional music and scroll parameters) app.get('/stream', (req, res) => { const { music = 'false', debug = DEBUG_MODE ? 'true' : 'false' } = req.query; const useMusic = music === 'true'; const startTime = Date.now(); streamHandler(req, res, { useMusic, musicPath: MUSIC_PATH, startTime, defaultFps: DEFAULT_FPS, screenshotFormat: SCREENSHOT_FORMAT, screenshotQuality: SCREENSHOT_QUALITY, debugMode: debug === 'true' }); }); // 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 }; // Generate session ID for logging const sessionId = Math.floor(Math.random() * 100000); let lateGeocodePromise = null; let geocodeDataPromise = null; let initialUrl = 'data:text/html,'; if (city && city !== 'Toronto, ON, CAN') { // Start geocoding in background - don't wait for it const geocodePromise = geocodeCity(city, sessionId); geocodeDataPromise = geocodePromise; // Save for city name overlay // Always start with black screen immediately initialUrl = 'data:text/html,'; // Late geocode promise will load the actual weather page 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); // Create resolved promise with Toronto data geocodeDataPromise = Promise.resolve({ cityName: 'Toronto' }); } 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 const { debug = DEBUG_MODE ? 'true' : 'false' } = req.query; // Build request path for logging const requestPath = `/weather?city=${encodeURIComponent(city)}`; return streamHandler(req, res, { useMusic: true, musicPath: MUSIC_PATH, lateGeocodePromise, geocodeDataPromise, startTime, defaultFps: DEFAULT_FPS, screenshotFormat: SCREENSHOT_FORMAT, screenshotQuality: SCREENSHOT_QUALITY, debugMode: debug === 'true', sessionId, requestPath }); }); // 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(`\nConfiguration:`); console.log(` Default FPS: ${DEFAULT_FPS}`); console.log(` Screenshot Format: ${SCREENSHOT_FORMAT}`); console.log(` Screenshot Quality: ${SCREENSHOT_QUALITY}${SCREENSHOT_FORMAT === 'jpeg' ? '%' : ' (ignored for PNG)'}`); console.log(`\nUsage: 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`); } } });