From 945c12e6c6a8faf27282b7d49787bde5461e7348 Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sat, 28 Jun 2025 00:22:47 -0500 Subject: [PATCH] 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 @@ +