feat: Add API and frontend to upload files
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m40s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 10s
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m40s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 10s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
This commit is contained in:
@@ -271,6 +271,12 @@
|
|||||||
<button id="playlist-filebrowser-cancel">Cancel</button>
|
<button id="playlist-filebrowser-cancel">Cancel</button>
|
||||||
<button id="playlist-filebrowser-addtrack">Add track(s)</button>
|
<button id="playlist-filebrowser-addtrack">Add track(s)</button>
|
||||||
</div>
|
</div>
|
||||||
|
<label>Upload files</label>
|
||||||
|
<div class="flex-horizontal">
|
||||||
|
<input type="file" id="playlist-filebrowser-upload-files" multiple accept="audio/mpeg" />
|
||||||
|
<button id="playlist-filebrowser-upload">Upload</button>
|
||||||
|
<progress id="playlist-filebrowser-upload-progress" max="100" value="0">0%</progress>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -482,7 +488,8 @@
|
|||||||
document.getElementById('playlist-exist-button-gettag')
|
document.getElementById('playlist-exist-button-gettag')
|
||||||
.addEventListener('click', selectLastTag);
|
.addEventListener('click', selectLastTag);
|
||||||
document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => {
|
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) => {
|
document.getElementById('playlist-exist-list').addEventListener("click", (e) => {
|
||||||
const node = e.target.closest("li");
|
const node = e.target.closest("li");
|
||||||
@@ -564,6 +571,7 @@
|
|||||||
.forEach(n => {
|
.forEach(n => {
|
||||||
if (n.innerText == tagtext) {
|
if (n.innerText == tagtext) {
|
||||||
n.classList.add("selected");
|
n.classList.add("selected");
|
||||||
|
lastSelected = n;
|
||||||
} else {
|
} else {
|
||||||
n.classList.remove("selected");
|
n.classList.remove("selected");
|
||||||
}
|
}
|
||||||
@@ -733,6 +741,9 @@
|
|||||||
document.getElementById('playlist-filebrowser-addtrack').addEventListener("click", (e) => {
|
document.getElementById('playlist-filebrowser-addtrack').addEventListener("click", (e) => {
|
||||||
returnSelectedTracks();
|
returnSelectedTracks();
|
||||||
});
|
});
|
||||||
|
document.getElementById('playlist-filebrowser-upload').addEventListener("click", (e) => {
|
||||||
|
uploadFiles();
|
||||||
|
});
|
||||||
tree.init();
|
tree.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,6 +833,54 @@
|
|||||||
}
|
}
|
||||||
showScreen("playlist_edit", {addtracks: tracks});
|
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.statusText} (${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 = (() => {
|
||||||
let tree = null;
|
let tree = null;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from microdot import Microdot, redirect, send_file
|
from array import array
|
||||||
|
from microdot import Microdot, redirect, send_file, Request
|
||||||
|
|
||||||
webapp = Microdot()
|
webapp = Microdot()
|
||||||
server = None
|
server = None
|
||||||
@@ -16,6 +17,8 @@ app = None
|
|||||||
nfc = None
|
nfc = None
|
||||||
playlist_db = None
|
playlist_db = None
|
||||||
|
|
||||||
|
Request.max_content_length = 128 * 1024 * 1024 # 128MB requests allowed
|
||||||
|
|
||||||
|
|
||||||
def start_webserver(config_, app_):
|
def start_webserver(config_, app_):
|
||||||
global server, config, app, nfc, playlist_db
|
global server, config, app, nfc, playlist_db
|
||||||
@@ -28,7 +31,7 @@ def start_webserver(config_, app_):
|
|||||||
|
|
||||||
@webapp.before_request
|
@webapp.before_request
|
||||||
async def before_request_handler(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
|
return "Cannot write to device while playback is active", 503
|
||||||
app.reset_idle_timeout()
|
app.reset_idle_timeout()
|
||||||
|
|
||||||
@@ -179,3 +182,39 @@ async def audiofiles_get(request):
|
|||||||
yield ']'
|
yield ']'
|
||||||
|
|
||||||
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user