diff --git a/editor.py b/editor.py
index dc98067..d8aa13d 100644
--- a/editor.py
+++ b/editor.py
@@ -41,6 +41,8 @@ FILE_OBSERVER = None
# ---------------------------------------------------------------------------
app = Flask(__name__)
+MAX_UPLOAD_MB = int(os.environ.get("MAX_UPLOAD_MB", "2048"))
+app.config["MAX_CONTENT_LENGTH"] = MAX_UPLOAD_MB * 1024 * 1024
# ---------------------------------------------------------------------------
@@ -1070,6 +1072,12 @@ h1 { margin:0 0 6px; }
.filter-bar input, .filter-bar select { width:100%; margin-top:5px; padding:9px; background:#111; color:#eee; border:1px solid #555; border-radius:5px; font-size:15px; }
.filter-bar .search { grid-column:auto; }
.filter-info { color:#999; font-size:13px; margin-bottom:10px; }
+.upload-bar { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:16px; }
+.upload-info { color:#999; font-size:13px; }
+.upload-progress { display:none; flex:1 1 240px; min-width:180px; height:18px; background:#292929; border-radius:9px; overflow:hidden; }
+.upload-progress.active { display:block; }
+.upload-progress-bar { width:0; height:100%; background:#1769aa; transition:width .15s ease; }
+
table { width:100%; border-collapse:collapse; }
th,td { padding:10px 8px; border-bottom:1px solid #333; text-align:left; }
th { color:#aaa; font-weight:normal; }
@@ -1100,6 +1108,12 @@ th { color:#aaa; font-weight:normal; }
Aufnahmen bearbeiten
+
+
+
+
MP3-Datei vom PC auswählen
+
+
{% if recordings %}
| Dateiname | Größe | Dauer | |
@@ -1255,6 +1269,66 @@ async function refreshFileLists() {
}
}
+
+const uploadFile = document.getElementById('uploadFile');
+const uploadInfo = document.getElementById('uploadInfo');
+const uploadProgress = document.getElementById('uploadProgress');
+const uploadProgressBar = document.getElementById('uploadProgressBar');
+
+uploadFile?.addEventListener('change', () => {
+ const file = uploadFile.files?.[0];
+ if (!file) return;
+
+ if (!file.name.toLowerCase().endsWith('.mp3')) {
+ alert('Bitte eine MP3-Datei auswählen.');
+ uploadFile.value = '';
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('file', file);
+ const xhr = new XMLHttpRequest();
+
+ uploadProgress.classList.add('active');
+ uploadProgressBar.style.width = '0%';
+ uploadInfo.textContent = `Upload: ${file.name}`;
+
+ xhr.open('POST', '/api/upload');
+ xhr.upload.addEventListener('progress', event => {
+ if (!event.lengthComputable) return;
+ const percent = Math.round(event.loaded / event.total * 100);
+ uploadProgressBar.style.width = percent + '%';
+ uploadInfo.textContent = `Upload: ${file.name} – ${percent} %`;
+ });
+
+ xhr.addEventListener('load', () => {
+ let result = {};
+ try { result = JSON.parse(xhr.responseText); } catch (_) {}
+ if (xhr.status >= 200 && xhr.status < 300 && result.success) {
+ uploadProgressBar.style.width = '100%';
+ uploadInfo.textContent = `${result.filename} wurde hochgeladen.`;
+ setTimeout(() => location.reload(), 500);
+ } else {
+ uploadProgress.classList.remove('active');
+ uploadInfo.textContent = 'Upload fehlgeschlagen';
+ alert(result.error || `Upload fehlgeschlagen (HTTP ${xhr.status})`);
+ }
+ });
+
+ xhr.addEventListener('error', () => {
+ uploadProgress.classList.remove('active');
+ uploadInfo.textContent = 'Upload fehlgeschlagen';
+ alert('Netzwerkfehler beim Upload.');
+ });
+
+ xhr.addEventListener('abort', () => {
+ uploadProgress.classList.remove('active');
+ uploadInfo.textContent = 'Upload abgebrochen';
+ });
+
+ xhr.send(formData);
+});
+
const events = new EventSource('/api/events');
events.onmessage = () => refreshFileLists();
events.onerror = () => { /* EventSource reconnects automatically. */ };
@@ -1484,6 +1558,40 @@ def finished_file(filename):
return send_from_directory(FINISHED_DIR, path.name, mimetype="audio/mpeg", as_attachment=False)
+@app.route("/api/upload", methods=["POST"])
+def api_upload():
+ upload = request.files.get("file")
+ if upload is None or not upload.filename:
+ return jsonify({"success": False, "error": "Keine Datei ausgewählt"}), 400
+
+ original_name = Path(upload.filename).name
+ if original_name != upload.filename:
+ return jsonify({"success": False, "error": "Ungültiger Dateiname"}), 400
+
+ if not original_name.lower().endswith(".mp3"):
+ return jsonify({"success": False, "error": "Bitte eine MP3-Datei auswählen"}), 400
+
+ filename = original_name
+ target = RECORDINGS_DIR / filename
+ if target.exists():
+ return jsonify({"success": False, "error": f"Eine Datei mit dem Namen '{filename}' existiert bereits"}), 409
+
+ temp = RECORDINGS_DIR / f".{filename}.{uuid.uuid4().hex}.uploading"
+ try:
+ RECORDINGS_DIR.mkdir(parents=True, exist_ok=True)
+ upload.save(temp)
+ os.replace(temp, target)
+ print(f"[upload] Uploaded recording: {filename}", flush=True)
+ return jsonify({"success": True, "filename": filename})
+ except OSError as exc:
+ try:
+ temp.unlink(missing_ok=True)
+ except OSError:
+ pass
+ print(f"[upload] upload failed for {filename}: {exc}", flush=True)
+ return jsonify({"success": False, "error": "Upload konnte nicht gespeichert werden"}), 500
+
+
@app.route("/api/file", methods=["POST"])
def api_file():
data = request.get_json(silent=True) or {}