mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 17:19:30 -07:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d508d7f50 | ||
|
|
d85a5ed3b1 | ||
|
|
831e1680e9 | ||
|
|
73cbc0aa81 | ||
|
|
9150d42802 | ||
|
|
54257e4667 | ||
|
|
7d50ce28bd | ||
|
|
2db7f30de7 | ||
|
|
5c7a6ab1a4 | ||
|
|
4b63328b74 | ||
|
|
ae1d004f60 | ||
|
|
7dd4c1dd24 | ||
|
|
1120247c99 | ||
|
|
c5c01e5450 | ||
|
|
0a65221905 | ||
|
|
9f9667c895 | ||
|
|
fda44e95fc | ||
|
|
945c12e6c6 | ||
|
|
0fde88cd8f |
13
.github/ISSUE_TEMPLATE/naming _issue.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/naming _issue.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: Naming issue
|
||||||
|
about: A city, airport or other location is not named correctly
|
||||||
|
title: 'Name Issue: '
|
||||||
|
labels: naming
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
This form is not for reporting a location that you can not find from the search box.
|
||||||
|
|
||||||
|
Use this form to help us rename airports, points of interest and other data provided from the API (rarely updated) to a better name. For example the airport in Broomfield colorado was renamed from "Jeffco" in the API to "Rocky Mountain Metro" it's new name.
|
||||||
|
|
||||||
|
You can also make a pull request on the `[station-overrides.mjs](https://github.com/netbymatt/ws4kp/blob/main/datagenerators/stations-states.mjs)` file which includes instructions on how to make the change directly. This is the preferred method.
|
||||||
@@ -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.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
19
datagenerators/stations-overrides.mjs
Normal file
19
datagenerators/stations-overrides.mjs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// station overrides are used to change the data for a station that is provided by the api
|
||||||
|
// the most common use is to adjust the city (station name) for formatting or to update an outdated name
|
||||||
|
// a complete station object looks like this:
|
||||||
|
// {
|
||||||
|
// "id": "KMCO", // 4-letter station identifier and key for lookups
|
||||||
|
// "city": "Orlando International Airport", // name displayed for this station
|
||||||
|
// "state": "FL", // state
|
||||||
|
// "lat": 28.41826, // latitude of station
|
||||||
|
// "lon": -81.32413 // longitude of station
|
||||||
|
// }
|
||||||
|
// any or all of the data for a station can be overwritten, follow the existing override patterns below
|
||||||
|
|
||||||
|
const overrides = {
|
||||||
|
KBJC: {
|
||||||
|
city: 'Rocky Mountain Metro',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default overrides;
|
||||||
@@ -5,6 +5,7 @@ import { writeFileSync } from 'fs';
|
|||||||
import https from './https.mjs';
|
import https from './https.mjs';
|
||||||
import states from './stations-states.mjs';
|
import states from './stations-states.mjs';
|
||||||
import chunk from './chunk.mjs';
|
import chunk from './chunk.mjs';
|
||||||
|
import overrides from './stations-overrides.mjs';
|
||||||
|
|
||||||
// skip stations starting with these letters
|
// skip stations starting with these letters
|
||||||
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
|
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
|
||||||
@@ -43,12 +44,16 @@ for (let i = 0; i < chunkStates.length; i += 1) {
|
|||||||
console.log(`Duplicate station: ${state}-${id}`);
|
console.log(`Duplicate station: ${state}-${id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// get any overrides if available
|
||||||
|
const override = overrides[id] ?? {};
|
||||||
output[id] = {
|
output[id] = {
|
||||||
id,
|
id,
|
||||||
city: station.properties.name,
|
city: station.properties.name,
|
||||||
state,
|
state,
|
||||||
lat: station.geometry.coordinates[1],
|
lat: station.geometry.coordinates[1],
|
||||||
lon: station.geometry.coordinates[0],
|
lon: station.geometry.coordinates[0],
|
||||||
|
// finally add the overrides
|
||||||
|
...override,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
next = stations?.pagination?.next;
|
next = stations?.pagination?.next;
|
||||||
@@ -59,7 +64,7 @@ for (let i = 0; i < chunkStates.length; i += 1) {
|
|||||||
while (next && stations.features.length > 0);
|
while (next && stations.features.length > 0);
|
||||||
console.log(`Complete: ${state}`);
|
console.log(`Complete: ${state}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch {
|
||||||
console.error(`Unable to get state: ${state}`);
|
console.error(`Unable to get state: ${state}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const mjsSources = [
|
|||||||
'server/scripts/modules/travelforecast.mjs',
|
'server/scripts/modules/travelforecast.mjs',
|
||||||
'server/scripts/modules/progress.mjs',
|
'server/scripts/modules/progress.mjs',
|
||||||
'server/scripts/modules/media.mjs',
|
'server/scripts/modules/media.mjs',
|
||||||
|
'server/scripts/modules/custom-rss-feed.mjs',
|
||||||
'server/scripts/index.mjs',
|
'server/scripts/index.mjs',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
715
package-lock.json
generated
715
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "5.26.2",
|
"version": "5.27.3",
|
||||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"webpack-stream": "^7.0.0"
|
"webpack-stream": "^7.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^17.0.1",
|
||||||
"ejs": "^3.1.5",
|
"ejs": "^3.1.5",
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
server/images/icons/current-conditions/No-Data.png
Normal file
BIN
server/images/icons/current-conditions/No-Data.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -13350,6 +13350,13 @@ const StationInfo = {
|
|||||||
lat: 38.7578,
|
lat: 38.7578,
|
||||||
lon: -104.3013,
|
lon: -104.3013,
|
||||||
},
|
},
|
||||||
|
KAEJ: {
|
||||||
|
id: 'KAEJ',
|
||||||
|
city: 'Central Colorado Regional Airport',
|
||||||
|
state: 'CO',
|
||||||
|
lat: 38.81416,
|
||||||
|
lon: -106.12069,
|
||||||
|
},
|
||||||
KAFF: {
|
KAFF: {
|
||||||
id: 'KAFF',
|
id: 'KAFF',
|
||||||
city: 'Air Force Academy',
|
city: 'Air Force Academy',
|
||||||
@@ -13357,13 +13364,6 @@ const StationInfo = {
|
|||||||
lat: 38.96667,
|
lat: 38.96667,
|
||||||
lon: -104.81667,
|
lon: -104.81667,
|
||||||
},
|
},
|
||||||
KAIB: {
|
|
||||||
id: 'KAIB',
|
|
||||||
city: 'Nucla Hopkins Field Airport',
|
|
||||||
state: 'CO',
|
|
||||||
lat: 38.23875,
|
|
||||||
lon: -108.563277,
|
|
||||||
},
|
|
||||||
KAJZ: {
|
KAJZ: {
|
||||||
id: 'KAJZ',
|
id: 'KAJZ',
|
||||||
city: 'Delta/Blake Field Airport',
|
city: 'Delta/Blake Field Airport',
|
||||||
@@ -13415,7 +13415,7 @@ const StationInfo = {
|
|||||||
},
|
},
|
||||||
KBJC: {
|
KBJC: {
|
||||||
id: 'KBJC',
|
id: 'KBJC',
|
||||||
city: 'Broomfield / Jeffco',
|
city: 'Rocky Mountain Metro',
|
||||||
state: 'CO',
|
state: 'CO',
|
||||||
lat: 39.90085,
|
lat: 39.90085,
|
||||||
lon: -105.10417,
|
lon: -105.10417,
|
||||||
|
|||||||
@@ -114,11 +114,14 @@ class CurrentWeather extends WeatherDisplay {
|
|||||||
|
|
||||||
const wind = (typeof this.data.WindSpeed === 'number') ? this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' ') : this.data.WindSpeed;
|
const wind = (typeof this.data.WindSpeed === 'number') ? this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' ') : this.data.WindSpeed;
|
||||||
|
|
||||||
|
// get location (city name) from StationInfo if available (allows for overrides)
|
||||||
|
const location = (StationInfo[this.data.station.properties.stationIdentifier]?.city ?? locationCleanup(this.data.station.properties.name)).substr(0, 20);
|
||||||
|
|
||||||
const fill = {
|
const fill = {
|
||||||
temp: this.data.Temperature + String.fromCharCode(176),
|
temp: this.data.Temperature + String.fromCharCode(176),
|
||||||
condition,
|
condition,
|
||||||
wind,
|
wind,
|
||||||
location: locationCleanup(this.data.station.properties.name).substr(0, 20),
|
location,
|
||||||
humidity: `${this.data.Humidity}%`,
|
humidity: `${this.data.Humidity}%`,
|
||||||
dewpoint: this.data.DewPoint + String.fromCharCode(176),
|
dewpoint: this.data.DewPoint + String.fromCharCode(176),
|
||||||
ceiling: (this.data.Ceiling === 0 ? 'Unlimited' : this.data.Ceiling + this.data.CeilingUnit),
|
ceiling: (this.data.Ceiling === 0 ? 'Unlimited' : this.data.Ceiling + this.data.CeilingUnit),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ let screenIndex = 0;
|
|||||||
let sinceLastUpdate = 0;
|
let sinceLastUpdate = 0;
|
||||||
let nextUpdate = DEFAULT_UPDATE;
|
let nextUpdate = DEFAULT_UPDATE;
|
||||||
let resetFlag;
|
let resetFlag;
|
||||||
|
let defaultScreensLoaded = true;
|
||||||
|
|
||||||
// start drawing conditions
|
// start drawing conditions
|
||||||
// reset starts from the first item in the text scroll list
|
// reset starts from the first item in the text scroll list
|
||||||
@@ -60,7 +61,7 @@ const incrementInterval = (force) => {
|
|||||||
stop(display?.elemId === 'progress');
|
stop(display?.elemId === 'progress');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
screenIndex = (screenIndex + 1) % (lastScreen);
|
screenIndex = (screenIndex + 1) % (workingScreens.length);
|
||||||
|
|
||||||
// draw new text
|
// draw new text
|
||||||
drawScreen();
|
drawScreen();
|
||||||
@@ -78,7 +79,7 @@ const drawScreen = async () => {
|
|||||||
// nothing to do if there's no data yet
|
// nothing to do if there's no data yet
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const thisScreen = screens[screenIndex](data);
|
const thisScreen = workingScreens[screenIndex](data);
|
||||||
|
|
||||||
// update classes on the scroll area
|
// update classes on the scroll area
|
||||||
elemForEach('.weather-display .scroll', (elem) => {
|
elemForEach('.weather-display .scroll', (elem) => {
|
||||||
@@ -125,12 +126,17 @@ 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
|
// the "screens" are stored in an array for easy addition and removal
|
||||||
const screens = [
|
const baseScreens = [
|
||||||
// hazards
|
// hazards
|
||||||
hazards,
|
hazards,
|
||||||
// station name
|
// station name
|
||||||
(data) => `Conditions at ${locationCleanup(data.station.properties.name).substr(0, 20)}`,
|
(data) => {
|
||||||
|
const location = (StationInfo[data.station.properties.stationIdentifier]?.city ?? locationCleanup(data.station.properties.name)).substr(0, 20);
|
||||||
|
return `Conditions at ${location}`;
|
||||||
|
},
|
||||||
|
|
||||||
// temperature
|
// temperature
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -168,6 +174,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
|
// internal draw function with preset parameters
|
||||||
const drawCondition = (text) => {
|
const drawCondition = (text) => {
|
||||||
// update all html scroll elements
|
// update all html scroll elements
|
||||||
@@ -183,19 +192,18 @@ const setHeader = (text) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// store the original number of screens
|
// reset the screens back to the original set
|
||||||
const originalScreens = screens.length;
|
|
||||||
let lastScreen = originalScreens;
|
|
||||||
|
|
||||||
// reset the number of screens
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
lastScreen = originalScreens;
|
workingScreens = [...baseScreens];
|
||||||
|
additionalScreens = [];
|
||||||
|
defaultScreensLoaded = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// add screen
|
// add screen, keepBase keeps the regular weather crawl
|
||||||
const addScreen = (screen) => {
|
const addScreen = (screen, keepBase = true) => {
|
||||||
screens.push(screen);
|
defaultScreensLoaded = false;
|
||||||
lastScreen += 1;
|
additionalScreens.push(screen);
|
||||||
|
workingScreens = [...(keepBase ? baseScreens : []), ...additionalScreens];
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawScrollCondition = (screen) => {
|
const drawScrollCondition = (screen) => {
|
||||||
@@ -238,6 +246,9 @@ const parseMessage = (event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const screenCount = () => workingScreens.length;
|
||||||
|
const atDefault = () => defaultScreensLoaded;
|
||||||
|
|
||||||
// add event listener for start message
|
// add event listener for start message
|
||||||
window.addEventListener('message', parseMessage);
|
window.addEventListener('message', parseMessage);
|
||||||
|
|
||||||
@@ -245,10 +256,14 @@ window.CurrentWeatherScroll = {
|
|||||||
addScreen,
|
addScreen,
|
||||||
reset,
|
reset,
|
||||||
start,
|
start,
|
||||||
|
screenCount,
|
||||||
|
atDefault,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
addScreen,
|
addScreen,
|
||||||
reset,
|
reset,
|
||||||
start,
|
start,
|
||||||
|
screenCount,
|
||||||
|
atDefault,
|
||||||
};
|
};
|
||||||
|
|||||||
132
server/scripts/modules/custom-rss-feed.mjs
Normal file
132
server/scripts/modules/custom-rss-feed.mjs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import Setting from './utils/setting.mjs';
|
||||||
|
import { reset as resetScroll, addScreen as addScroll } 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
|
||||||
|
parseFeed(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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse the feed/text provided
|
||||||
|
const parseFeed = (textInput) => {
|
||||||
|
// skip getting the feed on first run
|
||||||
|
if (firstRun) return;
|
||||||
|
|
||||||
|
// test validity
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
// this returns a data url
|
||||||
|
// a few sanity checks
|
||||||
|
if (rssResponse.status.content_type.indexOf('xml') < 0) return;
|
||||||
|
// determine return type
|
||||||
|
const isBase64 = rssResponse.status.content_type.substring(0, 8) !== 'text/xml';
|
||||||
|
|
||||||
|
// base 64 decode everything after the comma
|
||||||
|
const rss = isBase64 ? atob(rssResponse.contents.split('base64,')[1]) : rssResponse.contents;
|
||||||
|
|
||||||
|
// parse the rss
|
||||||
|
const doc = parser.parseFromString(rss, 'text/xml');
|
||||||
|
|
||||||
|
// get the title
|
||||||
|
const rssTitle = doc.querySelector('channel title').textContent;
|
||||||
|
|
||||||
|
// get each item
|
||||||
|
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
|
||||||
|
const changeFeed = (newValue) => {
|
||||||
|
// first pass through won't have custom feed enable ready
|
||||||
|
if (firstRun) return;
|
||||||
|
|
||||||
|
if (customFeedEnable.value) {
|
||||||
|
parseFeed(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const customFeed = new Setting('customFeed', {
|
||||||
|
name: 'Custom RSS Feed',
|
||||||
|
defaultValue: '',
|
||||||
|
type: 'string',
|
||||||
|
changeAction: changeFeed,
|
||||||
|
placeholder: 'Text or URL',
|
||||||
|
});
|
||||||
|
|
||||||
|
const customFeedEnable = new Setting('customFeedEnable', {
|
||||||
|
name: 'Enable RSS Feed/Text',
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -20,45 +20,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const scanMusicDirectory = async () => {
|
const scanMusicDirectory = async () => {
|
||||||
const parseDirectory = async (path, prefix = "") => {
|
const parseDirectory = async (path, prefix = '') => {
|
||||||
const listing = await text(path);
|
const listing = await text(path);
|
||||||
const matches = [...listing.matchAll(/href="([^\"]+\.mp3)"/gi)];
|
const matches = [...listing.matchAll(/href="([^"]+\.mp3)"/gi)];
|
||||||
return matches.map((m) => `${prefix}${m[1]}`);
|
return matches.map((m) => `${prefix}${m[1]}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let files = await parseDirectory("music/");
|
let files = await parseDirectory('music/');
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
files = await parseDirectory("music/default/", "default/");
|
files = await parseDirectory('music/default/', 'default/');
|
||||||
}
|
}
|
||||||
return { availableFiles: files };
|
return { availableFiles: files };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Unable to scan music directory");
|
console.error('Unable to scan music directory');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return { availableFiles: [] };
|
return { availableFiles: [] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const getMedia = async () => {
|
const getMedia = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('playlist.json');
|
const response = await fetch('playlist.json');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
playlist = await response.json();
|
playlist = await response.json();
|
||||||
} else if (response.status === 404
|
} else if (response.status === 404
|
||||||
&& response.headers.get('X-Weatherstar') === 'true') {
|
&& response.headers.get('X-Weatherstar') === 'true') {
|
||||||
console.warn("Couldn't get playlist.json, falling back to directory scan");
|
console.warn("Couldn't get playlist.json, falling back to directory scan");
|
||||||
playlist = await scanMusicDirectory();
|
playlist = await scanMusicDirectory();
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Couldn't get playlist.json: ${response.status} ${response.statusText}`);
|
console.warn(`Couldn't get playlist.json: ${response.status} ${response.statusText}`);
|
||||||
playlist = { availableFiles: [] };
|
playlist = { availableFiles: [] };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Couldn't get playlist.json, falling back to directory scan");
|
console.warn("Couldn't get playlist.json, falling back to directory scan");
|
||||||
playlist = await scanMusicDirectory();
|
playlist = await scanMusicDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
enableMediaPlayer();
|
enableMediaPlayer();
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableMediaPlayer = () => {
|
const enableMediaPlayer = () => {
|
||||||
@@ -219,11 +218,11 @@ const playerEnded = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setTrackName = (fileName) => {
|
const setTrackName = (fileName) => {
|
||||||
const baseName = fileName.split('/').pop();
|
const baseName = fileName.split('/').pop();
|
||||||
const trackName = decodeURIComponent(
|
const trackName = decodeURIComponent(
|
||||||
baseName.replace(/\.mp3/gi, '').replace(/(_-)/gi, '')
|
baseName.replace(/\.mp3/gi, '').replace(/(_-)/gi, ''),
|
||||||
);
|
);
|
||||||
document.getElementById('musicTrack').innerHTML = trackName;
|
document.getElementById('musicTrack').innerHTML = trackName;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -34,10 +34,17 @@ const createLink = async (e) => {
|
|||||||
// get all select boxes
|
// get all select boxes
|
||||||
elemForEach('select', (elem) => {
|
elemForEach('select', (elem) => {
|
||||||
if (elem?.id) {
|
if (elem?.id) {
|
||||||
queryStringElements[elem.id] = elem?.value ?? 0;
|
queryStringElements[elem.id] = encodeURIComponent(elem?.value ?? '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// get all text boxes
|
||||||
|
elemForEach('input[type=text]', ((elem) => {
|
||||||
|
if (elem?.id) {
|
||||||
|
queryStringElements[elem.id] = elem?.value ?? 0;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// add the location string
|
// add the location string
|
||||||
queryStringElements.latLonQuery = localStorage.getItem('latLonQuery');
|
queryStringElements.latLonQuery = localStorage.getItem('latLonQuery');
|
||||||
queryStringElements.latLon = localStorage.getItem('latLon');
|
queryStringElements.latLon = localStorage.getItem('latLon');
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const DEFAULTS = {
|
|||||||
sticky: true,
|
sticky: true,
|
||||||
values: [],
|
values: [],
|
||||||
visible: true,
|
visible: true,
|
||||||
|
placeholder: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
class Setting {
|
class Setting {
|
||||||
@@ -31,6 +32,7 @@ class Setting {
|
|||||||
this.values = options.values;
|
this.values = options.values;
|
||||||
this.visible = options.visible;
|
this.visible = options.visible;
|
||||||
this.changeAction = options.changeAction;
|
this.changeAction = options.changeAction;
|
||||||
|
this.placeholder = options.placeholder;
|
||||||
|
|
||||||
// get value from url
|
// get value from url
|
||||||
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
|
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
|
||||||
@@ -48,6 +50,9 @@ class Setting {
|
|||||||
// couldn't parse as a float, store as a string
|
// couldn't parse as a float, store as a string
|
||||||
urlState = urlValue;
|
urlState = urlValue;
|
||||||
}
|
}
|
||||||
|
if (this.type === 'string' && urlValue !== undefined) {
|
||||||
|
urlState = urlValue;
|
||||||
|
}
|
||||||
|
|
||||||
// get existing value if present
|
// get existing value if present
|
||||||
const storedValue = urlState ?? this.getFromLocalStorage();
|
const storedValue = urlState ?? this.getFromLocalStorage();
|
||||||
@@ -60,6 +65,9 @@ class Setting {
|
|||||||
case 'select':
|
case 'select':
|
||||||
this.selectChange({ target: { value: this.myValue } });
|
this.selectChange({ target: { value: this.myValue } });
|
||||||
break;
|
break;
|
||||||
|
case 'string':
|
||||||
|
this.stringChange({ target: { value: this.myValue } });
|
||||||
|
break;
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
default:
|
default:
|
||||||
this.checkboxChange({ target: { checked: this.myValue } });
|
this.checkboxChange({ target: { checked: this.myValue } });
|
||||||
@@ -124,6 +132,34 @@ class Setting {
|
|||||||
return label;
|
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`;
|
||||||
|
textInput.placeholder = this.placeholder;
|
||||||
|
// 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) {
|
checkboxChange(e) {
|
||||||
// update the state
|
// update the state
|
||||||
this.myValue = e.target.checked;
|
this.myValue = e.target.checked;
|
||||||
@@ -146,6 +182,15 @@ class Setting {
|
|||||||
this.changeAction(this.myValue);
|
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) {
|
storeToLocalStorage(value) {
|
||||||
if (!this.sticky) return;
|
if (!this.sticky) return;
|
||||||
const allSettingsString = localStorage?.getItem(SETTINGS_KEY) ?? '{}';
|
const allSettingsString = localStorage?.getItem(SETTINGS_KEY) ?? '{}';
|
||||||
@@ -163,8 +208,8 @@ class Setting {
|
|||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return storedValue;
|
|
||||||
case 'select':
|
case 'select':
|
||||||
|
case 'string':
|
||||||
return storedValue;
|
return storedValue;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@@ -214,6 +259,8 @@ class Setting {
|
|||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case 'select':
|
case 'select':
|
||||||
return this.generateSelect();
|
return this.generateSelect();
|
||||||
|
case 'string':
|
||||||
|
return this.generateString();
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
default:
|
default:
|
||||||
return this.generateCheckbox();
|
return this.generateCheckbox();
|
||||||
|
|||||||
35
static-env-handler.sh
Normal file → Executable file
35
static-env-handler.sh
Normal file → Executable file
@@ -4,22 +4,27 @@ set -eu
|
|||||||
ROOT="/usr/share/nginx/html"
|
ROOT="/usr/share/nginx/html"
|
||||||
QS=""
|
QS=""
|
||||||
|
|
||||||
|
# URL encode a string
|
||||||
|
url_encode() {
|
||||||
|
local string="$1"
|
||||||
|
printf '%s' "$string" | sed 's/ /%20/g; s/"/%22/g; s/</%3C/g; s/>/%3E/g; s/&/%26/g; s/#/%23/g; s/+/%2B/g'
|
||||||
|
}
|
||||||
|
|
||||||
# build query string from WSQS_ env vars
|
# build query string from WSQS_ env vars
|
||||||
for var in $(env); do
|
while IFS='=' read -r key val; do
|
||||||
case "$var" in
|
# Remove WSQS_ prefix and convert underscores to hyphens
|
||||||
WSQS_*=*)
|
key="${key#WSQS_}"
|
||||||
key="${var%%=*}"
|
key="${key//_/-}"
|
||||||
val="${var#*=}"
|
# URL encode the value
|
||||||
key="${key#WSQS_}"
|
encoded_val=$(url_encode "$val")
|
||||||
key="${key//_/-}"
|
if [ -n "$QS" ]; then
|
||||||
if [ -n "$QS" ]; then
|
QS="$QS&${key}=${encoded_val}"
|
||||||
QS="$QS&${key}=${val}"
|
else
|
||||||
else
|
QS="${key}=${encoded_val}"
|
||||||
QS="${key}=${val}"
|
fi
|
||||||
fi
|
done << EOF
|
||||||
;;
|
$(env | grep '^WSQS_')
|
||||||
esac
|
EOF
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
if [ -n "$QS" ]; then
|
if [ -n "$QS" ]; then
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/media.mjs"></script>
|
<script type="module" src="scripts/modules/media.mjs"></script>
|
||||||
|
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
|
||||||
<script type="module" src="scripts/index.mjs"></script>
|
<script type="module" src="scripts/index.mjs"></script>
|
||||||
<!-- data -->
|
<!-- data -->
|
||||||
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
|
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user