From fc93e5c529bffe779935f82a7361e63c382c8198 Mon Sep 17 00:00:00 2001 From: iloveicedgreentea <31193909+iloveicedgreentea@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:29:52 -0400 Subject: [PATCH] ui! --- api/handlers.go | 5 +- changelog.txt | 5 +- cmd/main.go | 2 +- readme.md | 20 +- web/app.js | 247 +++++++++++++++++++++---- web/index.html | 481 ++++++++++++++++++++++++++++++++++++++++++++++-- web/styles.css | 117 ++++++++++++ 7 files changed, 823 insertions(+), 54 deletions(-) create mode 100644 web/styles.css diff --git a/api/handlers.go b/api/handlers.go index 2a74ca7..8053540 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -3,6 +3,7 @@ package api import ( "fmt" "os" + "path/filepath" "github.com/gin-gonic/gin" @@ -53,8 +54,10 @@ func GetConfig(c *gin.Context) { func SaveConfig(c *gin.Context) { var jsonData map[string]interface{} + if err := c.ShouldBindJSON(&jsonData); err != nil { - c.JSON(400, gin.H{"error": "Invalid Payload"}) + c.JSON(400, gin.H{"error": err.Error()}) + fmt.Println(c.Request.Body) return } diff --git a/changelog.txt b/changelog.txt index 3695831..ea67906 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,4 +20,7 @@ Web UI flow for configuring server New MQTT automations: * "topicbeqcurrentprofile": -> which current profile is loaded * "topicminidspmutestatus": -> true/false if minidsp(s) muted - * "topicplayingstatus": -> true/false if the tool is playing \ No newline at end of file + * "topicplayingstatus": -> true/false if the tool is playing +Some functionality changes, check the readme +HDMI Sync automation in testing +*Breaking changes* All config fields are lowercase now \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index f14bdd7..18c96e6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -71,7 +71,7 @@ func main() { // TODO: Engine.SetTrustedProxies(nil) port := config.GetString("main.listenPort") if port == "" { - log.Fatal("error getting port") + port = "9999" } log.Infof("Starting server on port %v", port) if err := r.Run(fmt.Sprintf(":%s", port)); err != nil { diff --git a/readme.md b/readme.md index e661a53..7a26f26 100644 --- a/readme.md +++ b/readme.md @@ -11,10 +11,10 @@ Meat & Potatos: * Set Volume based on media type (movie, TV, etc) * Trigger lights when playing, pausing, or stopping automatically * HDMI Sync detection and automation (pause while HDMI is syncing so you don't sit embarrassed with a audio playing to a black screen) +* Web based UI for configuration Other cool stuff: * Mute/Unmute Minidsp automation for things like turning off subs at night -* Simple web based UI for configuration * Detect aspect ratio and send command to HA to adjust accordingly * Also supports using my MadVR Envy Home Assistant integration * Various MQTT sensors for volume control, lights, mute status, and current BEQ profile @@ -27,11 +27,23 @@ Note: this assumes you have ezBEQ, Plex, and HomeAssistant working. Refer to the You don't strictly need HA and you can use your own systems but I recommend HA. +Simple Way: + +1) Deploy via Docker -> `ghcr.io/iloveicedgreentea/plex-webhook-automation:$version` +2) Set up config via web ui -> `http://(server-ip):9999` + * can get UUID from `https://plex.tv/devices.xml` +3) Set up Plex to send webhooks to your server IP, `listenPort`, and the handler endpoint of `/plexwebhook` + * `(server-ip):9999/plexwebhook` +3) Whitelist your server IP in Plex so it can call the API without authentication. +4) Play a movie and check server logs. It should say what it loaded and you should see whatever options you enabled work. + +Manual Way: + 0) Create `config.json` and set the values appropriately. See below. 1) Either pull `ghcr.io/iloveicedgreentea/plex-webhook-automation:$version` or build the binary directly * if you deploy a container, mount config.json to a volume called exactly `/config.json` 2) Set up Plex to send webhooks to your server IP, `listenPort`, and the handler endpoint of `/plexwebhook` - * `locahost:9999/plexwebhook` + * `(server-ip):9999/plexwebhook` 3) Whitelist your server IP in Plex so it can call the API without authentication. Plex refuses to implement local server auth with an API, so I don't want to implement their locked-in auth method that has historically had outages (8/30/23 is the latest one by the way). 4) Add your UUID to the config.json so it filters by device * can get UUID from `https://plex.tv/devices.xml` or run the tool and play something, check the logs @@ -330,11 +342,11 @@ create file named config.json, paste this in, remove the comments after ```json { - // note the case is lowercase now + // note the case is lowercase "homeassistant": { + "enabled": true, "url": "http://123.123.123.123", "port": "8123", - "enabled": true, // get a token from your user profile "token": "ey.xyzjwt", // trigger functions to change the following diff --git a/web/app.js b/web/app.js index ef388ae..ead17bc 100644 --- a/web/app.js +++ b/web/app.js @@ -1,39 +1,220 @@ -// Check if config exists first -fetch('/config-exists') - .then(response => response.json()) - .then(data => { - if (data.exists) { - // Populate fields from existing config - fetch('/get-config') - .then(response => response.json()) - .then(config => populateForm(config)); - } +function populateFields(config) { + document.getElementById('listenport').value = config.main.listenport; + // .checked or .value for each kind for each item in HTML + + // EZBeq + document.getElementById('ezbeq-adjustmastervolumewithprofile').checked = config.ezbeq.adjustmastervolumewithprofile; + document.getElementById('ezbeq-enabled').checked = config.ezbeq.enabled; + document.getElementById('ezbeq-denonip').value = config.ezbeq.denonip; + document.getElementById('ezbeq-denonport').value = config.ezbeq.denonport; + document.getElementById('ezbeq-dryrun').checked = config.ezbeq.dryrun; + document.getElementById('ezbeq-enabletvbeq').checked = config.ezbeq.enabletvbeq; + document.getElementById('ezbeq-notifyendpointname').value = config.ezbeq.notifyendpointname; + document.getElementById('ezbeq-notifyonload').checked = config.ezbeq.notifyonload; + document.getElementById('ezbeq-port').value = config.ezbeq.port; + document.getElementById('ezbeq-preferredauthor').value = config.ezbeq.preferredauthor; + const slotsArray = config.ezbeq.slots; + slotsArray.forEach(slot => { + document.getElementById(`slot${slot}`).checked = true; }); + document.getElementById('ezbeq-stopplexifmismatch').checked = config.ezbeq.stopplexifmismatch; + document.getElementById('ezbeq-url').value = config.ezbeq.url; + document.getElementById('ezbeq-useavrcodecsearch').checked = config.ezbeq.useavrcodecsearch; + + + // HomeAssistant + document.getElementById('homeassistant-enabled').checked = config.homeassistant.enabled; + document.getElementById('homeassistant-url').value = config.homeassistant.url; + document.getElementById('homeassistant-port').value = config.homeassistant.port; + document.getElementById('homeassistant-token').value = config.homeassistant.token; + document.getElementById('homeassistant-triggeraspectratiochangeonevent').checked = config.homeassistant.triggeraspectratiochangeonevent; + document.getElementById('homeassistant-triggerlightsonevent').checked = config.homeassistant.triggerlightsonevent; + document.getElementById('homeassistant-triggeravrmastervolumechangeonevent').checked = config.homeassistant.triggeravrmastervolumechangeonevent; + document.getElementById('homeassistant-remoteentityname').value = config.homeassistant.remoteentityname; + document.getElementById('homeassistant-playscriptname').value = config.homeassistant.playscriptname; + document.getElementById('homeassistant-pausescriptname').value = config.homeassistant.pausescriptname; + document.getElementById('homeassistant-stopscriptname').value = config.homeassistant.stopscriptname; + + // MQTT + document.getElementById('mqtt-url').value = config.mqtt.url; + document.getElementById('mqtt-username').value = config.mqtt.username; + document.getElementById('mqtt-password').value = config.mqtt.password; + document.getElementById('mqtt-topiclights').value = config.mqtt.topiclights; + document.getElementById('mqtt-topicvolume').value = config.mqtt.topicvolume; + document.getElementById('mqtt-topicaspectratio').value = config.mqtt.topicaspectratio; + document.getElementById('mqtt-topicbeqcurrentprofile').value = config.mqtt.topicbeqcurrentprofile; + document.getElementById('mqtt-topicminidspmutestatus').value = config.mqtt.topicminidspmutestatus; + document.getElementById('mqtt-topicplayingstatus').value = config.mqtt.topicplayingstatus; -// Function to populate form -function populateForm(config) { - document.getElementById('url').value = config.homeassistant.url; - document.getElementById('port').value = config.homeassistant.port; - // Populate other fields + // Plex + document.getElementById('plex-enabled').checked = config.plex.enabled; + document.getElementById('plex-url').value = config.plex.url; + document.getElementById('plex-port').value = config.plex.port; + document.getElementById('plex-ownernamefilter').value = config.plex.ownernamefilter; + document.getElementById('plex-deviceuuidfilter').value = config.plex.deviceuuidfilter; + document.getElementById('plex-playermachineidentifier').value = config.plex.playermachineidentifier; + document.getElementById('plex-playerip').value = config.plex.playerip; + document.getElementById('plex-enabletrailersupport').checked = config.plex.enabletrailersupport; + + // Signal + document.getElementById('signal-enabled').checked = config.signal.enabled; + document.getElementById('signal-source').value = config.signal.source; } -// Function to submit form -function submitConfig() { - const formData = { - homeassistant: { - url: document.getElementById('url').value, - port: document.getElementById('port').value, - // ...other fields - }, - // ...other sections +function buildFinalConfig() { + const slotsArray = []; + for (let i = 1; i <= 4; i++) { + if (document.getElementById(`slot${i}`).checked) { + slotsArray.push(i); + } + } + const ezbeqConfig = { + "adjustmastervolumewithprofile": document.getElementById('ezbeq-adjustmastervolumewithprofile').checked, + "enabled": document.getElementById('ezbeq-enabled').checked, + "denonip": document.getElementById('ezbeq-denonip').value, + "denonport": document.getElementById('ezbeq-denonport').value, + "dryrun": document.getElementById('ezbeq-dryrun').checked, + "enabletvbeq": document.getElementById('ezbeq-enabletvbeq').checked, + "notifyendpointname": document.getElementById('ezbeq-notifyendpointname').value, + "notifyonload": document.getElementById('ezbeq-notifyonload').checked, + "port": document.getElementById('ezbeq-port').value, + "preferredauthor": document.getElementById('ezbeq-preferredauthor').value, + "slots": slotsArray, + "stopplexifmismatch": document.getElementById('ezbeq-stopplexifmismatch').checked, + "url": document.getElementById('ezbeq-url').value, + "useavrcodecsearch": document.getElementById('ezbeq-useavrcodecsearch').checked + }; + const homeAssistantConfig = { + "enabled": document.getElementById('homeassistant-enabled').checked, + "url": document.getElementById('homeassistant-url').value, + "port": document.getElementById('homeassistant-port').value, + "token": document.getElementById('homeassistant-token').value, + "triggeraspectratiochangeonevent": document.getElementById('homeassistant-triggeraspectratiochangeonevent').checked, + "triggerlightsonevent": document.getElementById('homeassistant-triggerlightsonevent').checked, + "triggeravrmastervolumechangeonevent": document.getElementById('homeassistant-triggeravrmastervolumechangeonevent').checked, + "remoteentityname": document.getElementById('homeassistant-remoteentityname').value, + "playscriptname": document.getElementById('homeassistant-playscriptname').value, + "pausescriptname": document.getElementById('homeassistant-pausescriptname').value, + "stopscriptname": document.getElementById('homeassistant-stopscriptname').value + }; + + const mainConfig = { + "listenport": document.getElementById('listenport').value }; - fetch('/save-config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData), - }) - .then(response => response.json()) - .then(data => alert(data.message)); + + const mqttConfig = { + "url": document.getElementById('mqtt-url').value, + "username": document.getElementById('mqtt-username').value, + "password": document.getElementById('mqtt-password').value, + "topiclights": document.getElementById('mqtt-topiclights').value, + "topicvolume": document.getElementById('mqtt-topicvolume').value, + "topicaspectratio": document.getElementById('mqtt-topicaspectratio').value, + "topicbeqcurrentprofile": document.getElementById('mqtt-topicbeqcurrentprofile').value, + "topicminidspmutestatus": document.getElementById('mqtt-topicminidspmutestatus').value, + "topicplayingstatus": document.getElementById('mqtt-topicplayingstatus').value + }; + + const plexConfig = { + "enabled": document.getElementById('plex-enabled').checked, + "url": document.getElementById('plex-url').value, + "port": document.getElementById('plex-port').value, + "ownernamefilter": document.getElementById('plex-ownernamefilter').value, + "deviceuuidfilter": document.getElementById('plex-deviceuuidfilter').value, + "playermachineidentifier": document.getElementById('plex-playermachineidentifier').value, + "playerip": document.getElementById('plex-playerip').value, + "enabletrailersupport": document.getElementById('plex-enabletrailersupport').checked + }; + const signalConfig = { + "enabled": document.getElementById('signal-enabled').checked, + "source": document.getElementById('signal-source').value + }; + // Build the final config JSON + const finalConfig = { + "ezbeq": ezbeqConfig, + "homeassistant": homeAssistantConfig, + "main": mainConfig, + "mqtt": mqttConfig, + "plex": plexConfig, + "signal": signalConfig + }; + + return finalConfig; + +} + + +document.addEventListener('DOMContentLoaded', function () { + async function loadConfig() { + const response = await fetch('/get-config'); + if (response.ok) { + return await response.json(); + } else { + throw new Error('Failed to fetch config'); + } + } + + loadConfig() + .then(config => { + // Populate fields from config + populateFields(config); + }) + .catch(error => { + console.error('Error in loading config:', error); + }); + + + document.getElementById('ezbeqForm').addEventListener('submit', async function (e) { + e.preventDefault(); + + // We moved the logic to build the finalConfig object here + const finalConfig = buildFinalConfig(); + console.log(JSON.stringify(finalConfig)) + try { + await submitConfig(finalConfig); + showNotification("Configuration saved successfully."); + } catch (error) { + showNotification("Failed to save configuration.", false); + } + }); + +}); +async function submitConfig(data) { + try { + const response = await fetch('/save-config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + if (response.ok) { + return await response.json(); + } else { + const text = await response.text(); // Get more information from the server + throw new Error(`Failed to save configuration, server says: ${text}`); + } + } catch (error) { + console.error("An error occurred while trying to send the request:", error); + throw error; // Re-throw to be caught by the caller + } +} + + + +function showNotification(message, isSuccess = true) { + const notification = document.getElementById("notification"); + notification.textContent = message; + + if (isSuccess) { + notification.className = "notification-success"; + } else { + notification.className = "notification-error"; + } + + // Remove the notification after 4 seconds + setTimeout(() => { + notification.textContent = ""; + notification.className = ""; + }, 4000); } diff --git a/web/index.html b/web/index.html index 543e0ec..879d95d 100644 --- a/web/index.html +++ b/web/index.html @@ -2,24 +2,477 @@
-