From 0fde88cd8f4ea8697feef37f60b366a8530eca6f Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Fri, 27 Jun 2025 22:51:22 -0500 Subject: [PATCH 1/7] restructure current weather scroll to allow add/remove of rss feed #57 --- .../scripts/modules/currentweatherscroll.mjs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/server/scripts/modules/currentweatherscroll.mjs b/server/scripts/modules/currentweatherscroll.mjs index 6064310..6bfb1e3 100644 --- a/server/scripts/modules/currentweatherscroll.mjs +++ b/server/scripts/modules/currentweatherscroll.mjs @@ -60,7 +60,7 @@ const incrementInterval = (force) => { stop(display?.elemId === 'progress'); return; } - screenIndex = (screenIndex + 1) % (lastScreen); + screenIndex = (screenIndex + 1) % (workingScreens.length); // draw new text drawScreen(); @@ -78,7 +78,7 @@ const drawScreen = async () => { // nothing to do if there's no data yet if (!data) return; - const thisScreen = screens[screenIndex](data); + const thisScreen = workingScreens[screenIndex](data); // update classes on the scroll area elemForEach('.weather-display .scroll', (elem) => { @@ -125,8 +125,10 @@ const hazards = (data) => { }; }; +// additional screens are stored in a separate for simple clearing/resettings +let additionalScreens = []; // the "screens" are stored in an array for easy addition and removal -const screens = [ +const baseScreens = [ // hazards hazards, // station name @@ -168,6 +170,9 @@ const screens = [ }, ]; +// working screens are the combination of base screens (when active) and additional screens +let workingScreens = [...baseScreens, ...additionalScreens]; + // internal draw function with preset parameters const drawCondition = (text) => { // update all html scroll elements @@ -183,19 +188,16 @@ const setHeader = (text) => { }); }; -// store the original number of screens -const originalScreens = screens.length; -let lastScreen = originalScreens; - -// reset the number of screens +// reset the screens back to the original set const reset = () => { - lastScreen = originalScreens; + workingScreens = [...baseScreens]; + additionalScreens = []; }; -// add screen -const addScreen = (screen) => { - screens.push(screen); - lastScreen += 1; +// add screen, keepBase keeps the regular weather crawl +const addScreen = (screen, keepBase = true) => { + additionalScreens.push(screen); + workingScreens = [...(keepBase ? baseScreens : []), ...additionalScreens]; }; const drawScrollCondition = (screen) => { From 945c12e6c6a8faf27282b7d49787bde5461e7348 Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sat, 28 Jun 2025 00:22:47 -0500 Subject: [PATCH 2/7] parse rss feed #57 --- server/scripts/modules/custom-rss-feed.mjs | 91 ++++++++++++++++++++++ server/scripts/modules/media.mjs | 77 +++++++++--------- server/scripts/modules/utils/setting.mjs | 46 ++++++++++- views/index.ejs | 1 + 4 files changed, 175 insertions(+), 40 deletions(-) create mode 100644 server/scripts/modules/custom-rss-feed.mjs diff --git a/server/scripts/modules/custom-rss-feed.mjs b/server/scripts/modules/custom-rss-feed.mjs new file mode 100644 index 0000000..5ee728b --- /dev/null +++ b/server/scripts/modules/custom-rss-feed.mjs @@ -0,0 +1,91 @@ +import Setting from './utils/setting.mjs'; +import { reset as resetScroll } from './currentweatherscroll.mjs'; +import { json } from './utils/fetch.mjs'; + +let firstRun = true; + +const parser = new DOMParser(); + +// change of enable handler +const changeEnable = (newValue) => { + let newDisplay; + if (newValue) { + // add the feed to the scroll + getFeed(customFeed.value); + // show the string box + newDisplay = 'block'; + } else { + // set scroll back to original + resetScroll(); + // hide the string entry + newDisplay = 'none'; + } + const stringEntry = document.getElementById('settings-customFeed-label'); + if (stringEntry) { + stringEntry.style.display = newDisplay; + } +}; + +// get the rss feed and then swap out the current weather scroll +const getFeed = async (url) => { + // skip getting the feed on first run + if (firstRun) return; + + // test validity + if (url === undefined || url === '') return; + + // get the text as a string + // it needs to be proxied, use a free service + const rssResponse = await json(`https://api.allorigins.win/get?url=${url}`); + + // this returns a data url + // a few sanity checks + if (rssResponse.status.content_type.indexOf('application/rss+xml') < 0) return; + if (rssResponse.contents.indexOf('base64') > 100) return; + // base 64 decode everything after the comma + const rss = atob(rssResponse.contents.split(',')[1]); + + // parse the rss + const doc = parser.parseFromString(rss, 'text/xml'); + + // get the title + const title = doc.querySelector('channel title').innerHTML; + + // get each item + const titles = [...doc.querySelectorAll('item title')].map((t) => t.innerHTML); +}; + +// change the feed source and re-load if necessary +const changeFeed = (newValue) => { + // first pass through won't have custom feed enable ready + if (firstRun) return; + + if (customFeedEnable.value) { + getFeed(newValue); + } +}; + +const customFeed = new Setting('customFeed', { + name: 'Custom RSS Feed', + defaultValue: '', + type: 'string', + changeAction: changeFeed, +}); + +const customFeedEnable = new Setting('customFeedEnable', { + name: 'Enable Custom RSS Feed', + defaultValue: false, + changeAction: changeEnable, +}); + +// initialize the custom feed inputs on the page +document.addEventListener('DOMContentLoaded', () => { + // add the controls to the page + const settingsSection = document.querySelector('#settings'); + settingsSection.append(customFeedEnable.generate(), customFeed.generate()); + // clear the first run value + firstRun = false; + // call change enable with the current value to show/hide the url box + // and make the call to get the feed if enabled + changeEnable(customFeedEnable.value); +}); diff --git a/server/scripts/modules/media.mjs b/server/scripts/modules/media.mjs index 9199340..353451e 100644 --- a/server/scripts/modules/media.mjs +++ b/server/scripts/modules/media.mjs @@ -20,45 +20,44 @@ document.addEventListener('DOMContentLoaded', () => { }); const scanMusicDirectory = async () => { - const parseDirectory = async (path, prefix = "") => { - const listing = await text(path); - const matches = [...listing.matchAll(/href="([^\"]+\.mp3)"/gi)]; - return matches.map((m) => `${prefix}${m[1]}`); - }; + const parseDirectory = async (path, prefix = '') => { + const listing = await text(path); + const matches = [...listing.matchAll(/href="([^"]+\.mp3)"/gi)]; + return matches.map((m) => `${prefix}${m[1]}`); + }; - try { - let files = await parseDirectory("music/"); - if (files.length === 0) { - files = await parseDirectory("music/default/", "default/"); - } - return { availableFiles: files }; - } catch (e) { - console.error("Unable to scan music directory"); - console.error(e); - return { availableFiles: [] }; - } + try { + let files = await parseDirectory('music/'); + if (files.length === 0) { + files = await parseDirectory('music/default/', 'default/'); + } + return { availableFiles: files }; + } catch (e) { + console.error('Unable to scan music directory'); + console.error(e); + return { availableFiles: [] }; + } }; - const getMedia = async () => { - try { - const response = await fetch('playlist.json'); - if (response.ok) { - playlist = await response.json(); - } else if (response.status === 404 - && response.headers.get('X-Weatherstar') === 'true') { - console.warn("Couldn't get playlist.json, falling back to directory scan"); - playlist = await scanMusicDirectory(); - } else { - console.warn(`Couldn't get playlist.json: ${response.status} ${response.statusText}`); - playlist = { availableFiles: [] }; - } - } catch (e) { - console.warn("Couldn't get playlist.json, falling back to directory scan"); - playlist = await scanMusicDirectory(); - } + try { + const response = await fetch('playlist.json'); + if (response.ok) { + playlist = await response.json(); + } else if (response.status === 404 + && response.headers.get('X-Weatherstar') === 'true') { + console.warn("Couldn't get playlist.json, falling back to directory scan"); + playlist = await scanMusicDirectory(); + } else { + console.warn(`Couldn't get playlist.json: ${response.status} ${response.statusText}`); + playlist = { availableFiles: [] }; + } + } catch (e) { + console.warn("Couldn't get playlist.json, falling back to directory scan"); + playlist = await scanMusicDirectory(); + } - enableMediaPlayer(); + enableMediaPlayer(); }; const enableMediaPlayer = () => { @@ -219,11 +218,11 @@ const playerEnded = () => { }; const setTrackName = (fileName) => { - const baseName = fileName.split('/').pop(); - const trackName = decodeURIComponent( - baseName.replace(/\.mp3/gi, '').replace(/(_-)/gi, '') - ); - document.getElementById('musicTrack').innerHTML = trackName; + const baseName = fileName.split('/').pop(); + const trackName = decodeURIComponent( + baseName.replace(/\.mp3/gi, '').replace(/(_-)/gi, ''), + ); + document.getElementById('musicTrack').innerHTML = trackName; }; export { diff --git a/server/scripts/modules/utils/setting.mjs b/server/scripts/modules/utils/setting.mjs index 2bfe853..7c70a56 100644 --- a/server/scripts/modules/utils/setting.mjs +++ b/server/scripts/modules/utils/setting.mjs @@ -48,6 +48,9 @@ class Setting { // couldn't parse as a float, store as a string urlState = urlValue; } + if (this.type === 'string' && urlValue !== undefined) { + urlState = urlValue; + } // get existing value if present const storedValue = urlState ?? this.getFromLocalStorage(); @@ -60,6 +63,9 @@ class Setting { case 'select': this.selectChange({ target: { value: this.myValue } }); break; + case 'string': + this.stringChange({ target: { value: this.myValue } }); + break; case 'checkbox': default: this.checkboxChange({ target: { checked: this.myValue } }); @@ -124,6 +130,33 @@ class Setting { return label; } + generateString() { + // create a string input and accompanying set button + const label = document.createElement('label'); + label.for = `settings-${this.shortName}-string`; + label.id = `settings-${this.shortName}-label`; + // text input box + const textInput = document.createElement('input'); + textInput.type = 'text'; + textInput.value = this.myValue; + textInput.id = `settings-${this.shortName}-string`; + textInput.name = `settings-${this.shortName}-string`; + // set button + const setButton = document.createElement('input'); + setButton.type = 'button'; + setButton.value = 'Set'; + setButton.id = `settings-${this.shortName}-button`; + setButton.name = `settings-${this.shortName}-button`; + setButton.addEventListener('click', () => { + this.stringChange({ target: { value: textInput.value } }); + }); + // assemble + label.append(textInput, setButton); + + this.element = label; + return label; + } + checkboxChange(e) { // update the state this.myValue = e.target.checked; @@ -146,6 +179,15 @@ class Setting { this.changeAction(this.myValue); } + stringChange(e) { + // update the value + this.myValue = e.target.value; + this.storeToLocalStorage(this.myValue); + + // call the change action + this.changeAction(this.myValue); + } + storeToLocalStorage(value) { if (!this.sticky) return; const allSettingsString = localStorage?.getItem(SETTINGS_KEY) ?? '{}'; @@ -163,8 +205,8 @@ class Setting { switch (this.type) { case 'boolean': case 'checkbox': - return storedValue; case 'select': + case 'string': return storedValue; default: return null; @@ -214,6 +256,8 @@ class Setting { switch (this.type) { case 'select': return this.generateSelect(); + case 'string': + return this.generateString(); case 'checkbox': default: return this.generateCheckbox(); diff --git a/views/index.ejs b/views/index.ejs index 85f8f49..005ec3e 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -54,6 +54,7 @@ + From fda44e95fc92d2e90c591d6435698933ce5bc2fa Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sat, 28 Jun 2025 00:59:40 -0500 Subject: [PATCH 3/7] rss feeds scroll, needs additional testing #57 --- .../scripts/modules/currentweatherscroll.mjs | 10 ++++++++ server/scripts/modules/custom-rss-feed.mjs | 25 +++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/server/scripts/modules/currentweatherscroll.mjs b/server/scripts/modules/currentweatherscroll.mjs index 6bfb1e3..2612699 100644 --- a/server/scripts/modules/currentweatherscroll.mjs +++ b/server/scripts/modules/currentweatherscroll.mjs @@ -15,6 +15,7 @@ let screenIndex = 0; let sinceLastUpdate = 0; let nextUpdate = DEFAULT_UPDATE; let resetFlag; +let defaultScreensLoaded = true; // start drawing conditions // reset starts from the first item in the text scroll list @@ -192,10 +193,12 @@ const setHeader = (text) => { const reset = () => { workingScreens = [...baseScreens]; additionalScreens = []; + defaultScreensLoaded = true; }; // add screen, keepBase keeps the regular weather crawl const addScreen = (screen, keepBase = true) => { + defaultScreensLoaded = false; additionalScreens.push(screen); workingScreens = [...(keepBase ? baseScreens : []), ...additionalScreens]; }; @@ -240,6 +243,9 @@ const parseMessage = (event) => { } }; +const screenCount = () => workingScreens.length; +const atDefault = () => defaultScreensLoaded; + // add event listener for start message window.addEventListener('message', parseMessage); @@ -247,10 +253,14 @@ window.CurrentWeatherScroll = { addScreen, reset, start, + screenCount, + atDefault, }; export { addScreen, reset, start, + screenCount, + atDefault, }; diff --git a/server/scripts/modules/custom-rss-feed.mjs b/server/scripts/modules/custom-rss-feed.mjs index 5ee728b..33e566b 100644 --- a/server/scripts/modules/custom-rss-feed.mjs +++ b/server/scripts/modules/custom-rss-feed.mjs @@ -1,5 +1,5 @@ import Setting from './utils/setting.mjs'; -import { reset as resetScroll } from './currentweatherscroll.mjs'; +import { reset as resetScroll, addScreen as addScroll } from './currentweatherscroll.mjs'; import { json } from './utils/fetch.mjs'; let firstRun = true; @@ -40,19 +40,34 @@ const getFeed = async (url) => { // this returns a data url // a few sanity checks - if (rssResponse.status.content_type.indexOf('application/rss+xml') < 0) return; + if (rssResponse.status.content_type.indexOf('xml') < 0) return; if (rssResponse.contents.indexOf('base64') > 100) return; // base 64 decode everything after the comma - const rss = atob(rssResponse.contents.split(',')[1]); + const rss = atob(rssResponse.contents.split('base64,')[1]); // parse the rss const doc = parser.parseFromString(rss, 'text/xml'); // get the title - const title = doc.querySelector('channel title').innerHTML; + const rssTitle = doc.querySelector('channel title').textContent; // get each item - const titles = [...doc.querySelectorAll('item title')].map((t) => t.innerHTML); + const titles = [...doc.querySelectorAll('item title')].map((t) => t.textContent); + + // reset the scroll, then add the screens + resetScroll(); + titles.forEach((title) => { + // data is provided to the screen handler, so we return a function + addScroll( + () => ({ + header: rssTitle, + type: 'scroll', + text: title, + }), + // false parameter does not include the default weather scrolls + false, + ); + }); }; // change the feed source and re-load if necessary From 9f9667c895bfef4f83b2ba1e6ce83c7e9c74b558 Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sat, 28 Jun 2025 09:20:10 -0500 Subject: [PATCH 4/7] add single text scroll option #57 --- server/scripts/modules/custom-rss-feed.mjs | 36 ++++++++++++++++++---- server/scripts/modules/utils/setting.mjs | 3 ++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/server/scripts/modules/custom-rss-feed.mjs b/server/scripts/modules/custom-rss-feed.mjs index 33e566b..059f8e6 100644 --- a/server/scripts/modules/custom-rss-feed.mjs +++ b/server/scripts/modules/custom-rss-feed.mjs @@ -11,7 +11,7 @@ const changeEnable = (newValue) => { let newDisplay; if (newValue) { // add the feed to the scroll - getFeed(customFeed.value); + parseFeed(customFeed.value); // show the string box newDisplay = 'block'; } else { @@ -26,14 +26,37 @@ const changeEnable = (newValue) => { } }; -// get the rss feed and then swap out the current weather scroll -const getFeed = async (url) => { +// parse the feed/text provided +const parseFeed = (textInput) => { // skip getting the feed on first run if (firstRun) return; // test validity - if (url === undefined || url === '') return; + if (textInput === undefined || textInput === '') { + resetScroll(); + } + // test for url + if (textInput.match(/https?:\/\//)) { + getFeed(textInput); + return; + } + + // add single text scroll + resetScroll(); + addScroll( + () => ( + { + type: 'scroll', + text: textInput, + }), + // keep the existing scroll + true, + ); +}; + +// get the rss feed and then swap out the current weather scroll +const getFeed = async (url) => { // get the text as a string // it needs to be proxied, use a free service const rssResponse = await json(`https://api.allorigins.win/get?url=${url}`); @@ -76,7 +99,7 @@ const changeFeed = (newValue) => { if (firstRun) return; if (customFeedEnable.value) { - getFeed(newValue); + parseFeed(newValue); } }; @@ -85,10 +108,11 @@ const customFeed = new Setting('customFeed', { defaultValue: '', type: 'string', changeAction: changeFeed, + placeholder: 'Text or URL', }); const customFeedEnable = new Setting('customFeedEnable', { - name: 'Enable Custom RSS Feed', + name: 'Enable RSS Feed/Text', defaultValue: false, changeAction: changeEnable, }); diff --git a/server/scripts/modules/utils/setting.mjs b/server/scripts/modules/utils/setting.mjs index 7c70a56..a4075fc 100644 --- a/server/scripts/modules/utils/setting.mjs +++ b/server/scripts/modules/utils/setting.mjs @@ -11,6 +11,7 @@ const DEFAULTS = { sticky: true, values: [], visible: true, + placeholder: '', }; class Setting { @@ -31,6 +32,7 @@ class Setting { this.values = options.values; this.visible = options.visible; this.changeAction = options.changeAction; + this.placeholder = options.placeholder; // get value from url const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`]; @@ -141,6 +143,7 @@ class Setting { textInput.value = this.myValue; textInput.id = `settings-${this.shortName}-string`; textInput.name = `settings-${this.shortName}-string`; + textInput.placeholder = this.placeholder; // set button const setButton = document.createElement('input'); setButton.type = 'button'; From 0a65221905e5aa11e055df612a6b64de98e37287 Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Mon, 30 Jun 2025 23:29:39 -0500 Subject: [PATCH 5/7] update readme for custom rss close #57 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8683319..fbe3eba 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,9 @@ A hook is provided as `/server/scripts/custom.js` to allow customizations to you When using Docker, mount your `custom.js` file to `/usr/share/nginx/html/scripts/custom.js` to customize the static build. +### RSS feeds and custom scroll +If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, or show headlines from an rss feed turn on the setting for `Enable RSS Feed/Text` and then enter a URL or text in the resulting text box. Then press set. + ## Issue reporting and feature requests Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. I've also observed that the API can go down on a regional basis (based on NWS office locations). This means that you may have problems getting data for, say, Chicago right now, but Dallas and others are working just fine. From c5c01e5450714257aa615a183e5f54ee7b2efabc Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Mon, 30 Jun 2025 23:29:46 -0500 Subject: [PATCH 6/7] 5.27.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30f3ae5..4ef0614 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ws4kp", - "version": "5.26.2", + "version": "5.27.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ws4kp", - "version": "5.26.2", + "version": "5.27.0", "license": "MIT", "dependencies": { "dotenv": "^16.5.0", diff --git a/package.json b/package.json index 2971c6d..3b10d93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws4kp", - "version": "5.26.2", + "version": "5.27.0", "description": "Welcome to the WeatherStar 4000+ project page!", "main": "index.mjs", "type": "module", From 1120247c990a7e4013fa2d114737b977cf118f07 Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Mon, 30 Jun 2025 23:32:30 -0500 Subject: [PATCH 7/7] include custom rss feed in build #57 --- gulp/publish-frontend.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/gulp/publish-frontend.mjs b/gulp/publish-frontend.mjs index 2f3f312..024dc51 100644 --- a/gulp/publish-frontend.mjs +++ b/gulp/publish-frontend.mjs @@ -89,6 +89,7 @@ const mjsSources = [ 'server/scripts/modules/travelforecast.mjs', 'server/scripts/modules/progress.mjs', 'server/scripts/modules/media.mjs', + 'server/scripts/modules/custom-rss-feed.mjs', 'server/scripts/index.mjs', ];