diff --git a/software/frontend/index.html b/software/frontend/index.html
index 39681e9..d8a9103 100644
--- a/software/frontend/index.html
+++ b/software/frontend/index.html
@@ -271,6 +271,12 @@
+
+
@@ -482,7 +488,8 @@
document.getElementById('playlist-exist-button-gettag')
.addEventListener('click', selectLastTag);
document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => {
- showScreen("playlist_edit", {load: lastSelected.innerText});
+ if (lastSelected !== null)
+ showScreen("playlist_edit", {load: lastSelected.innerText});
});
document.getElementById('playlist-exist-list').addEventListener("click", (e) => {
const node = e.target.closest("li");
@@ -564,6 +571,7 @@
.forEach(n => {
if (n.innerText == tagtext) {
n.classList.add("selected");
+ lastSelected = n;
} else {
n.classList.remove("selected");
}
@@ -733,6 +741,9 @@
document.getElementById('playlist-filebrowser-addtrack').addEventListener("click", (e) => {
returnSelectedTracks();
});
+ document.getElementById('playlist-filebrowser-upload').addEventListener("click", (e) => {
+ uploadFiles();
+ });
tree.init();
}
@@ -822,6 +833,54 @@
}
showScreen("playlist_edit", {addtracks: tracks});
}
+
+ function uploadFiles() {
+ const tree = document.getElementById("playlist-filebrowser-tree");
+ const selectedNodes = [...tree.querySelectorAll(".selected")];
+ const files = [...document.getElementById("playlist-filebrowser-upload-files").files];
+ if (selectedNodes.length !== 1 ||
+ selectedNodes[0].getAttribute('data-type') !== "directory") {
+ alert("Please select a single directory for upload");
+ return;
+ }
+ uploadFileHelper(files.length, 0, selectedNodes[0].getAttribute('data-path'), files);
+ }
+
+ function uploadFileHelper(totalcount, donecount, destdir, files) {
+ // Upload files sequentially by recursivly calling this function from the 'load' callback when
+ // the previous upload is completed.
+ if (files.length === 0)
+ return;
+ const reader = new FileReader();
+ const ctrl = document.getElementById("playlist-filebrowser-upload-progress");
+ const xhr = new XMLHttpRequest();
+ const location = destdir + '/' + files[0].name;
+
+ xhr.upload.addEventListener("progress", (e) => {
+ if (e.lengthComputable) {
+ const percentage = Math.round((e.loaded * 100) / e.total / totalcount + donecount * 100 / totalcount);
+ ctrl.value = percentage;
+ }
+ });
+ //xhr.upload.addEventListener("load", (e) => {
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status !== 204) {
+ alert(`File upload failed: ${xhr.responseText} (${xhr.status})`);
+ return;
+ }
+ if (donecount + 1 === totalcount) {
+ // Reload file list from device
+ onShow();
+ } else {
+ uploadFileHelper(totalcount, donecount + 1, destdir, files.slice(1));
+ }
+ }
+ };
+ xhr.open("POST", `/api/v1/audiofiles?type=file&location=${location}`);
+ xhr.overrideMimeType("audio/mpeg");
+ xhr.send(files[0]);
+ }
let tree = (() => {
let tree = null;
diff --git a/software/src/webserver.py b/software/src/webserver.py
index 5af66fa..85f4917 100644
--- a/software/src/webserver.py
+++ b/software/src/webserver.py
@@ -7,7 +7,8 @@ import asyncio
import json
import os
-from microdot import Microdot, redirect, send_file
+from array import array
+from microdot import Microdot, redirect, send_file, Request
webapp = Microdot()
server = None
@@ -16,6 +17,8 @@ app = None
nfc = None
playlist_db = None
+Request.max_content_length = 128 * 1024 * 1024 # 128MB requests allowed
+
def start_webserver(config_, app_):
global server, config, app, nfc, playlist_db
@@ -28,7 +31,7 @@ def start_webserver(config_, app_):
@webapp.before_request
async def before_request_handler(request):
- if request.method in ['PUT', 'POST'] and app.is_playing():
+ if request.method in ['PUT', 'POST', 'DELETE'] and app.is_playing():
return "Cannot write to device while playback is active", 503
app.reset_idle_timeout()
@@ -179,3 +182,39 @@ async def audiofiles_get(request):
yield ']'
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}
+
+
+@webapp.route('/api/v1/audiofiles', methods=['POST'])
+async def audiofile_upload(request):
+ if 'type' not in request.args or request.args['type'] not in ['file', 'directory']:
+ return 'invalid or missing type', 400
+ if 'location' not in request.args:
+ return 'missing location', 400
+ path = fsroot + '/' + request.args['location']
+ type_ = request.args['type']
+ length = request.content_length
+ print(f'Got upload request of type {type_} to {path} with length {length}')
+ if type_ == 'directory':
+ if length != 0:
+ return 'directory request may not have content', 400
+ os.mkdir(path)
+ return '', 204
+ with open(path, 'wb') as newfile:
+ data = array('b', range(4096))
+ bytes_copied = 0
+ while True:
+ bytes_read = await request.stream.readinto(data)
+ if bytes_read == 0:
+ # End of body
+ break
+ bytes_written = newfile.write(data[:bytes_read])
+ if bytes_written != bytes_read:
+ # short writes shouldn't happen
+ return 'write failure', 500
+ bytes_copied += bytes_written
+ if bytes_copied == length:
+ break
+ if bytes_copied == length:
+ return '', 204
+ else:
+ return 'size mismatch', 500