Compare commits

...

11 commits

Author SHA1 Message Date
XenGi cdf12411a2
add save playlist feature 2024-04-18 15:06:12 +02:00
XenGi 26f230e039
update docs 2024-04-18 14:28:37 +02:00
XenGi 9248852713
add delete playlist feature 2024-04-18 14:28:02 +02:00
XenGi 1fc5dc2bf8
add attach playlist feature 2024-04-18 12:12:52 +02:00
XenGi aeaef11341
add replace playlist feature 2024-04-18 11:46:09 +02:00
XenGi 44dd0ddd9c
update dependencies 2024-04-18 10:38:52 +02:00
XenGi cfbb8a47f7
switch primary git repo 2024-04-18 10:37:22 +02:00
XenGi 58f59017dd
udate flake 2024-04-18 10:28:52 +02:00
XenGi 2c2efaaca2
Merge branch 'main' of gitlab.com:XenGi/sanic 2024-04-17 15:12:12 +02:00
XenGi bb19b095b6
load playlist content into result 2024-04-17 00:24:18 +02:00
XenGi bddc82399b
add list playlists 2024-04-16 23:37:48 +02:00
8 changed files with 257 additions and 44 deletions

View file

@ -45,12 +45,12 @@
- [ ] Select tracks in results
- [ ] `Add` selected tracks to queue button
- Playlist browser
- [ ] Show current playlists
- [ ] `Replace` current queue with playlist button
- [ ] `Attach` playlist to current queue button
- [ ] `Save` current queue as playlist button
- [x] Show current playlists
- [x] `Replace` current queue with playlist button
- [x] `Attach` playlist to current queue button
- [x] `Save` current queue as playlist button
- [x] Show dialog
- [ ] `Delete` selected playlist button
- [x] `Delete` selected playlist button
## backend
@ -77,8 +77,10 @@
- [ ] POST `/api/queue` `{"song_id": 123}`
- [x] GET `/api/queue/:song_id/delete`
- [x] GET `/api/queue/:song_id/move/:position`
- [x] GET `/api/queue/replace/:playlist_name`
- [x] GET `/api/queue/attach/:playlist_name`
- [ ] `/api/list_database/:path`
- [ ] `/api/list_playlists`
- [ ] `/api/save_playlist`
- [ ] `/api/delete_playlist`
- [x] GET `/api/playlists`
- [x] POST `/api/playlists/:name`
- [x] GET `/api/playlists/:name`
- [x] DELETE `/api/playlists/:name`

View file

@ -43,11 +43,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1710889954,
"narHash": "sha256-Pr6F5Pmd7JnNEMHHmspZ0qVqIBVxyZ13ik1pJtm2QXk=",
"lastModified": 1713284584,
"narHash": "sha256-rRuPBJD9+yjz7tY3xC/BvFUwloutynR9piiVE6fhGqo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7872526e9c5332274ea5932a0c3270d6e4724f3b",
"rev": "2b6ee326ad047870526d9a3ae88dfd0197da898d",
"type": "github"
},
"original": {

8
go.mod
View file

@ -1,11 +1,11 @@
module git.berlin.ccc.de/cccb/sanic
module gitlab.com/XenGi/sanic
go 1.21
require (
github.com/fhs/gompd/v2 v2.3.0
github.com/labstack/echo-contrib v0.16.0
github.com/labstack/echo/v4 v4.11.4
github.com/labstack/echo-contrib v0.17.0
github.com/labstack/echo/v4 v4.12.0
golang.org/x/net v0.24.0
gopkg.in/ini.v1 v1.67.0
)
@ -19,7 +19,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.52.2 // indirect
github.com/prometheus/common v0.52.3 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect

12
go.sum
View file

@ -10,10 +10,10 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/labstack/echo-contrib v0.16.0 h1:vk5Kd+egpTOJxD3l+3IvZzQWPbrXiYxhkkgkJL99j/w=
github.com/labstack/echo-contrib v0.16.0/go.mod h1:mjX5VB3OqJcroIEycptBOY9Hr7rK+unq79W8QFKGNV0=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/echo-contrib v0.17.0 h1:xam8wakZOsiQYM14Z0og1xF3w/heWNeDF5AtC5PlX8E=
github.com/labstack/echo-contrib v0.17.0/go.mod h1:mjX5VB3OqJcroIEycptBOY9Hr7rK+unq79W8QFKGNV0=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -27,8 +27,8 @@ github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7km
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=

View file

@ -14,11 +14,11 @@ schema = 3
version = "v3.2.2+incompatible"
hash = "sha256-LOkpuXhWrFayvVf1GOaOmZI5YKEsgqVSb22aF8LnCEM="
[mod."github.com/labstack/echo-contrib"]
version = "v0.16.0"
hash = "sha256-YnO4Ngu+gb/upIo856FDCtcTev36Vs/xUvP2qMiSGnA="
version = "v0.17.0"
hash = "sha256-J5S8vO8Zg9uV9A3zbILswn/oPdwLKRXncsXdEjDOmU8="
[mod."github.com/labstack/echo/v4"]
version = "v4.11.4"
hash = "sha256-pVKfkZtxi5e/1MTK2RcKWSgNpEbRDo3lKUVKo01WYO0="
version = "v4.12.0"
hash = "sha256-TPXJv/6C53bnmcEYxa9g5Mft8u/rLT96q64tQ9+RtKU="
[mod."github.com/labstack/gommon"]
version = "v0.4.2"
hash = "sha256-395+BETDpv15L2lsCiEccwakXgEQxKHlYBAU0Ot3qhY="
@ -35,8 +35,8 @@ schema = 3
version = "v0.6.1"
hash = "sha256-rIDyUzNfxRA934PIoySR0EhuBbZVRK/25Jlc/r8WODw="
[mod."github.com/prometheus/common"]
version = "v0.52.2"
hash = "sha256-XQUvk9/Kwf9NDlDUVl7mOWRD7z7z9QEbLH/rNU4D2nI="
version = "v0.52.3"
hash = "sha256-JzNAt7pimXZMnPxv8droAHJq+5OKWi9BYkGtMvqpyd0="
[mod."github.com/prometheus/procfs"]
version = "v0.13.0"
hash = "sha256-J31K36TkIiQU2EGOcmqDa+dkoKXiVuxafPVT4rKbEsg="

119
mpd.go
View file

@ -273,3 +273,122 @@ func moveTrackInQueue(c echo.Context) error {
return c.String(http.StatusOK, fmt.Sprintf("Moved song %d to position %d", songId, position))
}
func attachPlaylist(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
c.Logger().Error(err)
}
defer conn.Close()
name := c.Param("playlist_name")
err = conn.PlaylistLoad(name, -1, -1)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, "")
}
func replaceQueue(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
c.Logger().Error(err)
}
defer conn.Close()
name := c.Param("playlist_name")
err = conn.Clear()
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusInternalServerError, err.Error())
}
err = conn.PlaylistLoad(name, -1, -1)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, "")
}
// Playlists
func listPlaylists(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
c.Logger().Error(err)
}
defer conn.Close()
playlists, err := conn.ListPlaylists()
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, playlists)
}
func listPlaylist(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
c.Logger().Error(err)
}
defer conn.Close()
name := c.Param("name")
playlist, err := conn.PlaylistContents(name)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, playlist)
}
func deletePlaylist(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
c.Logger().Error(err)
}
defer conn.Close()
name := c.Param("name")
err = conn.PlaylistRemove(name)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusNoContent, "")
}
func savePlaylist(c echo.Context) error {
// Connect to MPD server
conn, err := mpd.Dial("tcp", "localhost:6600")
if err != nil {
c.Logger().Error(err)
}
defer conn.Close()
name := c.Param("name")
err = conn.PlaylistSave(name)
if err != nil {
c.Logger().Error(err)
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusCreated, "")
}

View file

@ -91,6 +91,13 @@ func main() {
g.GET("/queue/:song_id/delete", deleteTrackFromQueue)
g.GET("/queue/:song_id/move/:position", moveTrackInQueue)
g.GET("/queue/replace/:playlist_name", replaceQueue)
g.GET("/queue/attach/:playlist_name", attachPlaylist)
g.GET("/playlists", listPlaylists)
g.POST("/playlists/:name", savePlaylist)
g.GET("/playlists/:name", listPlaylist)
g.DELETE("/playlists/:name", deletePlaylist)
g.GET("/download", downloadTrack)

View file

@ -7,7 +7,7 @@ const VOLUME_STEP = 5;
const dialog_save_playlist = document.getElementById("save-playlist");
const control_playlist_name = document.getElementById("control-playlist-name");
const dialog_save_playlist_submit = document.querySelector("#save-playlist button");
const dialog_save_playlist_submit = document.querySelector("#save-playlist form button");
const dialog_save_playlist_close = document.querySelector("#save-playlist .close");
const connection_state = document.getElementById("connection-state");
@ -37,6 +37,7 @@ const control_replace_playlist = document.getElementById("control-replace-playli
const control_attach_playlist = document.getElementById("control-attach-playlist");
const control_save_playlist = document.getElementById("control-save-playlist");
const control_delete_playlist = document.getElementById("control-delete-playlist");
const result_table = document.querySelector("#result tbody");
// Utility functions
@ -68,6 +69,46 @@ moveTrackInQueue = (event, direction) => {
});
}
refreshPlaylists = () => {
console.log("Refreshing playlists from MPD server")
fetch(`${API_URL}/playlists`).then(async r => {
if (r.status === 200) {
const playlists = await r.json();
control_playlist_list.options.length = 0; // clear playlists
playlists.forEach(p => {
const option = document.createElement("option")
option.innerText = p["playlist"];
option.value = p["playlist"];
option.addEventListener("click", () => {
fetch(`${API_URL}/playlists/${p["playlist"]}`).then(async r => {
if (r.status === 200) {
const songs = await r.json();
console.log(songs)
result_table.innerHTML = "";
songs.forEach(song => {
const tr = document.createElement("tr");
const artist = document.createElement("td");
artist.innerText = song["Artist"];
const title = document.createElement("td");
title.innerText = song["Title"];
const time = document.createElement("td");
time.innerText = secondsToTrackTime(parseInt(song["Time"]))
tr.appendChild(artist);
tr.appendChild(title);
tr.appendChild(document.createElement("td")); // album
tr.appendChild(document.createElement("td")); // genre
tr.appendChild(time);
result_table.appendChild(tr);
});
}
})
});
control_playlist_list.appendChild(option)
});
}
});
}
// UI controls
tab_browser.addEventListener("click", () => {
@ -103,10 +144,12 @@ tab_playlists.addEventListener("click", () => {
}
});
// Show "Save playlist" modal
control_save_playlist.addEventListener("click", () => {
dialog_save_playlist.showModal()
});
// Close "Save playlist" modal
dialog_save_playlist_close.addEventListener("click", () => {
dialog_save_playlist.close()
});
@ -117,42 +160,83 @@ dialog_save_playlist_close.addEventListener("click", () => {
// Add API calls to controls
control_replace_playlist.addEventListener("click", e => {
fetch(`${API_URL}/`).then(async r => {
control_replace_playlist.addEventListener("click", () => {
fetch(`${API_URL}/queue/replace/${control_playlist_list.value}`).then(async r => {
if (r.status !== 200) {
console.error(`API returned ${r.status}: ${r.statusText}`);
}
});
});
control_attach_playlist.addEventListener("click", e => {
fetch(`${API_URL}/`).then(async r => {
control_attach_playlist.addEventListener("click", () => {
fetch(`${API_URL}/queue/attach/${control_playlist_list.value}`).then(async r => {
if (r.status !== 200) {
console.error(`API returned ${r.status}: ${r.statusText}`);
}
});
});
// Save current queue as new playlist and refresh playlist list
dialog_save_playlist_submit.addEventListener("click", () => {
fetch(`${API_URL}/playlists`, {method: "PUT"}).then(async r => {
fetch(`${API_URL}/playlists/${control_playlist_name.value}`, {method: "POST"}).then(async r => {
if (r.status === 201) {
console.log(`Playlist "${control_playlist_name.value}" saved`)
}
});
});
control_delete_playlist.addEventListener("click", () => {
const playlist_id = control_playlist_list.value;
fetch(`${API_URL}/playlists/${playlist_id}`, {method: "DELETE"}).then(r => {
if (r.status === 204) {
console.log(`Playlist ${playlist_id} successfully deleted.`);
refreshPlaylists()
} else {
console.error(`API returned ${r.status}: ${r.statusText}`);
}
});
});
control_update_db.addEventListener("click", event => {
control_delete_playlist.addEventListener("click", () => {
const playlist_name = control_playlist_list.value;
fetch(`${API_URL}/playlists/${control_playlist_list.value}`, {method: "DELETE"}).then(r => {
if (r.status === 204) {
console.log(`Playlist "${playlist_name}" successfully deleted.`);
refreshPlaylists();
} else {
console.error(`API returned ${r.status}: ${r.statusText}`);
}
});
});
tab_browser.addEventListener("click", () => {
if (!tab_browser.classList.contains("active")) {
tab_browser.classList.add("active");
tab_search.classList.remove("active")
tab_playlists.classList.remove("active")
document.getElementById("file-browser").style.display = "block";
document.getElementById("search").style.display = "none";
document.getElementById("playlist-browser").style.display = "none";
}
});
tab_search.addEventListener("click", () => {
if (!tab_search.classList.contains("active")) {
tab_browser.classList.remove("active");
tab_search.classList.add("active")
tab_playlists.classList.remove("active")
document.getElementById("file-browser").style.display = "none";
document.getElementById("search").style.display = "block";
document.getElementById("playlist-browser").style.display = "none";
}
});
tab_playlists.addEventListener("click", () => {
refreshPlaylists();
if (!tab_playlists.classList.contains("active")) {
tab_browser.classList.remove("active");
tab_search.classList.remove("active")
tab_playlists.classList.add("active")
document.getElementById("file-browser").style.display = "none";
document.getElementById("search").style.display = "none";
document.getElementById("playlist-browser").style.display = "block";
}
});
// Add API calls to controls
control_update_db.addEventListener("click", () => {
console.log("Issuing database update")
fetch(`${API_URL}/update_db`).then(async r => {
if (r.status === 200) {
@ -472,3 +556,4 @@ window.setInterval(() => {
connection_state.innerHTML = "❌ Disconnected"; // ❌ Cross Mark
}
}, 1000);