diff --git a/Makefile b/Makefile
index 6737e08..7b566c5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,4 @@
mpd:
mkdir -p /tmp/sanic/{music,playlists}
+ touch /tmp/sanic/mpd_db
mpd --no-daemon ./mpd.conf
diff --git a/mpd.go b/mpd.go
index 80943d9..8a3803e 100644
--- a/mpd.go
+++ b/mpd.go
@@ -3,7 +3,6 @@ package main
import (
"github.com/fhs/gompd/v2/mpd"
"github.com/labstack/echo/v4"
- "log"
"net/http"
"strconv"
"time"
@@ -15,13 +14,13 @@ func updateDb(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
jobId, err := conn.Update("")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, strconv.Itoa(jobId))
@@ -31,13 +30,13 @@ func previousTrack(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
err = conn.Previous()
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, "")
@@ -47,13 +46,13 @@ func nextTrack(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
err = conn.Next()
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, "")
@@ -63,13 +62,13 @@ func stopPlayback(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
err = conn.Stop()
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, "")
@@ -79,13 +78,24 @@ func resumePlayback(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
- err = conn.Pause(false)
+ status, err := conn.Status()
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
+ }
+ if status["state"] == "stop" {
+ err := conn.Play(-1)
+ if err != nil {
+ c.Logger().Error(err)
+ }
+ } else {
+ err = conn.Pause(false)
+ if err != nil {
+ c.Logger().Error(err)
+ }
}
return c.String(http.StatusOK, "")
@@ -95,13 +105,13 @@ func pausePlayback(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
err = conn.Pause(true)
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, "")
@@ -111,13 +121,13 @@ func seek(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
seconds, err := strconv.Atoi(c.Param("seconds"))
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
if seconds < 0 {
@@ -126,7 +136,7 @@ func seek(c echo.Context) error {
err = conn.SeekCur(time.Duration(seconds)*time.Second, false)
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, "")
@@ -136,13 +146,13 @@ func toggleRepeat(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
status, err := conn.Status()
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
if status["repeat"] == "1" {
err = conn.Repeat(false)
@@ -150,7 +160,7 @@ func toggleRepeat(c echo.Context) error {
err = conn.Repeat(true)
}
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, "")
@@ -160,21 +170,21 @@ func toggleRandom(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
status, err := conn.Status()
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
- if status["toggleRandom"] == "1" {
+ if status["random"] == "1" {
err = conn.Random(false)
} else {
err = conn.Random(true)
}
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, "")
@@ -184,13 +194,13 @@ func setVolume(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
defer conn.Close()
level, err := strconv.Atoi(c.Param("level"))
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
if level > 100 || level < 0 {
@@ -199,7 +209,7 @@ func setVolume(c echo.Context) error {
err = conn.SetVolume(level)
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
return c.String(http.StatusOK, "")
diff --git a/server.go b/server.go
index 8095f32..ae7e321 100644
--- a/server.go
+++ b/server.go
@@ -107,7 +107,12 @@ func wsServe(c echo.Context) error {
// Connect to MPD server
mpdConn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
- log.Fatalln(err)
+ //log.Fatalln(err)
+ c.Logger().Error(err)
+ err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_error\":\"%s\"}", err.Error()))
+ if err != nil {
+ c.Logger().Error(err)
+ }
}
defer mpdConn.Close()
@@ -121,16 +126,23 @@ func wsServe(c echo.Context) error {
} else {
log.Println(msg)
if strings.ToLower(msg) == "#status" {
- // TODO: Get current MPD status and return it
status, err := mpdConn.Status()
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
- jsonData, err := json.Marshal(status)
+ currentsong, err := mpdConn.CurrentSong()
if err != nil {
- log.Fatalln(err)
+ c.Logger().Error(err)
}
- err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_status\":%s}", string(jsonData)))
+ jsonStatus, err := json.Marshal(status)
+ if err != nil {
+ c.Logger().Error(err)
+ }
+ jsonCurrentSong, err := json.Marshal(currentsong)
+ if err != nil {
+ c.Logger().Error(err)
+ }
+ err = websocket.Message.Send(ws, fmt.Sprintf("{\"mpd_status\":%s,\"mpd_current_song\":%s}", string(jsonStatus), string(jsonCurrentSong)))
if err != nil {
c.Logger().Error(err)
}
diff --git a/static/controls.js b/static/controls.js
index f63d153..67b2610 100644
--- a/static/controls.js
+++ b/static/controls.js
@@ -1,7 +1,9 @@
-const API_URL = `${document.location.protocol}://${document.location.host}/api`;
+const API_URL = `${document.location.protocol}//${document.location.host}/api`;
+const VOLUME_STEP = 5;
// Get control elements
+const connection_state = document.getElementById("connection-state");
const control_update_db = document.getElementById("control-update-db");
const control_previous = document.getElementById("control-previous");
const control_play_pause = document.getElementById("control-play-pause");
@@ -13,18 +15,31 @@ const control_shuffle = document.getElementById("control-shuffle");
const control_xfade = document.getElementById("control-xfade");
const control_xfade_minus = document.getElementById("control-xfade-minus");
const control_xfade_plus = document.getElementById("control-xfade-plus");
+const control_volume = document.getElementById("control-volume");
+const control_volume_up = document.getElementById("control-volume-up");
+const control_volume_down = document.getElementById("control-volume-down");
const queue_table = document.querySelector("#queue tbody");
+const control_track = document.getElementById("control-track");
+const control_time = document.getElementById("control-time");
// Add API calls to controls
control_update_db.addEventListener("click", e => {
- fetch(`${API_URL}/update_db`);
+ console.log("Issuing database update")
+ fetch(`${API_URL}/update_db`).then(async r => {
+ if (r.status === 200) {
+ const job_id = await r.text();
+ console.log(`Update started (Job ID: ${job_id})`)
+ } else {
+ console.error(`API returned ${r.status}: ${r.statusText}`)
+ }
+ });
});
control_previous.addEventListener("click", e => {
fetch(`${API_URL}/previous_track`);
});
control_play_pause.addEventListener("click", e => {
- if (e.target.innerText === "⏵︎") { // Play
+ if (e.target.innerHTML === "⏵︎") { // TODO: check is never true
fetch(`${API_URL}/pause`);
} else { // Pause
fetch(`${API_URL}/play`);
@@ -40,10 +55,20 @@ control_progress.addEventListener("change", e => {
fetch(`${API_URL}/seek/${e.target.value}`)
});
control_repeat.addEventListener("click", e => {
+ if (e.target.innerHTML === "🔴 repeat") { // TODO: check is never true
+ e.target.innerHTML = "🔘 repeat";
+ } else {
+ e.target.innerHTML = "🔴 repeat";
+ }
fetch(`${API_URL}/repeat`);
});
control_shuffle.addEventListener("click", e => {
- fetch(`${API_URL}/shuffle`);
+ if (e.target.innerHTML === "🔴 shuffle") { // TODO: check is never true
+ e.target.innerHTML = "🔘 shuffle";
+ } else {
+ e.target.innerHTML = "🔴 shuffle";
+ }
+ fetch(`${API_URL}/random`);
});
control_xfade_minus.addEventListener("click", e => {
// TODO: not yet implemented
@@ -53,46 +78,129 @@ control_xfade_plus.addEventListener("click", e => {
// TODO: not yet implemented
fetch(`${API_URL}/xfade`);
});
+control_volume_up.addEventListener("click", e => {
+ const v = Math.min(parseInt(control_volume.value) + VOLUME_STEP, 100);
+ fetch(`${API_URL}/volume/${v}`);
+ control_volume.value = v;
+
+});
+control_volume_down.addEventListener("click", e => {
+ const v = Math.max(parseInt(control_volume.value) - VOLUME_STEP, 0);
+ fetch(`${API_URL}/volume/${v}`);
+ control_volume.value = v;
+});
+control_volume.addEventListener("change", e => {
+ fetch(`${API_URL}/volume/${e.target.value}`);
+});
// Create WebSocket connection.
-const socket = new WebSocket(`${document.location.protocol === "https" ? "wss" : "ws"}://${document.location.host}/ws`);
+const socket = new WebSocket(`${document.location.protocol === "https:" ? "wss" : "ws"}://${document.location.host}/ws`);
// Connection opened
socket.addEventListener("open", (e) => {
socket.send("Hello Server!");
});
-// Listen for messages
+// Listen for messages and update UI state
socket.addEventListener("message", (e) => {
- console.log("Message from server");
+ // Print out mpd response
+ console.log(`DEBUG: ${e.data}`); // DEBUG
+
const msg = JSON.parse(e.data);
- if ("error" in msg.mpd_status) {
- console.error(msg.mpd_status.error);
+
+ if ("mpd_status" in msg) {
+ if (msg.mpd_status == null) {
+ connection_state.innerHTML = "❌ Disconnected"; // â
Check Mark Button
+ } else {
+ // print error if present
+ if ("error" in msg.mpd_status) {
+ console.error(msg.mpd_status.error);
+ }
+
+ // update "Update DB" button
+ if ("updating_db" in msg.mpd_status) {
+ control_update_db.disabled = true;
+ } else {
+ if (control_update_db.disabled) {
+ console.log("Database update done.")
+ }
+ control_update_db.disabled = false;
+ }
+
+ // update play/pause button
+ if ("state" in msg.mpd_status && msg.mpd_status.state === "play") {
+ control_play_pause.innerHTML = "⏸︎"; // Pause
+ } else {
+ control_play_pause.innerHTML = "⏵︎"; // Play
+ }
+
+ // update playback time
+ if ("elapsed" in msg.mpd_status && "duration" in msg.mpd_status) {
+ const elapsed_hours = Math.floor(msg.mpd_status.elapsed / 3600);
+ const elapsed_minutes = Math.floor((msg.mpd_status.elapsed - elapsed_hours * 3600) / 60);
+ const elapsed_seconds = Math.floor(msg.mpd_status.elapsed - elapsed_hours * 3600 - elapsed_minutes * 60);
+ const duration_hours = Math.floor(msg.mpd_status.duration / 3600);
+ const duration_minutes = Math.floor((msg.mpd_status.duration - duration_hours * 3600) / 60);
+ const duration_seconds = Math.floor(msg.mpd_status.duration - duration_hours * 3600 - duration_minutes * 60);
+ control_time.value = `${elapsed_hours}:${elapsed_minutes.toString().padStart(2, '0')}:${elapsed_seconds.toString().padStart(2, '0')}/${duration_hours}:${duration_minutes.toString().padStart(2, '0')}:${duration_seconds.toString().padStart(2, '0')}`;
+ }
+ if ("elapsed" in msg.mpd_status) {
+ control_progress.value = msg.mpd_status.elapsed;
+ }
+ if ("duration" in msg.mpd_status) {
+ control_progress.max = msg.mpd_status.duration;
+ }
+
+ // update repeat state
+ if ("repeat" in msg.mpd_status) {
+ if (msg.mpd_status.repeat === "1") {
+ control_repeat.innerHTML = "🔴 repeat"; // đ´ Red Circle
+ } else {
+ control_repeat.innerHTML = "🔘 repeat"; // đ Radio Button
+ }
+ }
+
+ // update shuffle state
+ if ("random" in msg.mpd_status) {
+ if (msg.mpd_status.random === "1") {
+ control_shuffle.innerHTML = "🔴 shuffle"; // đ´ Red Circle
+ } else {
+ control_shuffle.innerHTML = "🔘 shuffle"; // đ Radio Button
+ }
+ }
+
+ // update crossfade state
+ if ("xfade" in msg.mpd_status) {
+ control_xfade.value = msg.mpd_status.xfade;
+ }
+
+ // update volume
+ if ("volume" in msg.mpd_status) {
+ control_volume.value = msg.mpd_status.volume;
+ }
+ }
}
- if ("updating_db" in msg.mpd_status) {
- control_update_db.disable();
- } else {
- control_update_db.enable();
+ // update song info
+ if ("mpd_current_song" in msg && msg.mpd_current_song != null) {
+ if ("Artist" in msg.mpd_current_song && "Title" in msg.mpd_current_song) {
+ control_track.value = `${msg.mpd_current_song.Artist} - ${msg.mpd_current_song.Title}`
+ } else {
+ control_track.value = msg.mpd_current_song.file;
+ }
}
- if ("state" in msg.mpd_status && msg.mpd_status.state === "play") {
- control_play_pause.innerText = "⏸︎"; // Pause
- } else {
- control_play_pause.innerText = "⏵︎"; // Play
- }
- if ("elapsed" in msg.mpd_status) {
- control_progress.value = msg.mpd_status.elapsed;
- }
- if ("duration" in msg.mpd_status) {
- control_progress.max = msg.mpd_status.duration;
- }
- if ("repeat" in msg.mpd_status) {
- control_repeat.checked = msg.mpd_status.repeat;
- }
- if ("random" in msg.mpd_status) {
- control_shuffle.checked = msg.mpd_status.random;
- }
- if ("xfade" in msg.mpd_status) {
- control_xfade.value = msg.mpd_status.xfade;
+
+ if ("mpd_error" in msg) {
+ console.error(`MPD Error: ${msg.mpd_error}`)
}
});
+
+// Request MPD status every second
+window.setInterval(() => {
+ if (socket.readyState === socket.OPEN) {
+ socket.send("#status");
+ connection_state.innerHTML = "✅ Connected"; // â Cross Mark
+ } else {
+ connection_state.innerHTML = "❌ Disconnected"; // â
Check Mark Button
+ }
+}, 1000);
diff --git a/static/index.html b/static/index.html
index 8934465..de40c89 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1,5 +1,5 @@
-
+
Sanic
@@ -9,13 +9,14 @@
-
+
+
-
+
@@ -27,24 +28,38 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
Now playing:
+
+
+
+
+
+
+
+
+
+
+
+
Sanic © 2023
diff --git a/static/style.css b/static/style.css
index 0e0863e..3b9adae 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,5 +1,5 @@
/* #################### */
-/* ####### main ####### */
+/* #### structure ##### */
/* #################### */
html, body { margin: 0; height: 100%; }
@@ -37,6 +37,21 @@ table {
width: 100%;
}
+#control-admin {
+ display: flex;
+ flex-direction: column;
+}
+
+#sanic-logo {
+ max-width: 80%;
+ max-height: 80%;
+}
+
+.spaced {
+ display: flex;
+ justify-content: space-between;
+}
+
/* #################### */
/* ###### debug ####### */
/* #################### */
@@ -62,3 +77,58 @@ div {
#control-xfade[type=number] {
-moz-appearance: textfield;
}
+
+#control-xfade {
+ width: 2em;
+}
+
+#control-previous,
+#control-play-pause,
+#control-stop,
+#control-next {
+ width: 2.5em;
+ height: 2.5em;
+ text-align: center;
+}
+
+/*
+#control-track {
+ transform: translateX(100%);
+ -moz-transform: translateX(100%);
+ -webkit-transform: translateX(100%);
+ animation: scroll-left 20s linear infinite;
+ -moz-animation: scroll-left 2s linear infinite;
+ -webkit-animation: scroll-left 2s linear infinite;
+}
+
+@keyframes scroll-left {
+ 0% {
+ transform: translateX(100%);
+ -moz-transform: translateX(100%);
+ -webkit-transform: translateX(100%);
+ }
+ 100% {
+ transform: translateX(-100%);
+ -moz-transform: translateX(-100%);
+ -webkit-transform: translateX(-100%);
+ }
+}
+
+@-moz-keyframes scroll-left {
+ 0% {
+ -moz-transform: translateX(100%);
+ }
+ 100% {
+ -moz-transform: translateX(-100%);
+ }
+}
+
+@-webkit-keyframes scroll-left {
+ 0% {
+ -webkit-transform: translateX(100%);
+ }
+ 100% {
+ -webkit-transform: translateX(-100%);
+ }
+}
+*/