From 3057eb8edaecee17456b5d77e87da9452fac00e3 Mon Sep 17 00:00:00 2001 From: "Troll (Hermes Agent)" Date: Mon, 3 Aug 2026 18:59:00 +0000 Subject: [PATCH] feat: stream MP3s through backend proxy endpoint - Replace /audio//.mp3 with /api/stream//.mp3. - Stream bytes from private storage with Accept-Ranges and Content-Range support. - Keep old /audio route returning 404. - Update player page JS to use new stream endpoint. --- app.py | 80 ++++++++++++++++++++++++++++++++++++++----- templates/player.html | 4 +-- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/app.py b/app.py index 5caff8a..a7d6444 100644 --- a/app.py +++ b/app.py @@ -634,27 +634,89 @@ def revise(token): return redirect(url_for('play', token=token)) -@app.route('/audio//.mp3') -def audio(token, version): +@app.route('/api/stream//.mp3') +def stream_audio(token, version): """ - Serve an uploaded MP3 file for a specific request token and version ('a' or 'b'). - This keeps the files off the public static path and ties them to the private token. + Stream an uploaded MP3 through a backend proxy endpoint. + + This hides the real file path from the customer. The endpoint checks the + player token and serves bytes with Range request support so the HTML audio + player can seek. The URL is still interceptable in-browser, but it is not a + direct file path and can be gated or expired later. """ req = get_request_by_token(token) if not req: abort(404) if version not in ('a', 'b'): abort(404) - field = f'song_{version}_path' - path = req.get(field) - if not path or not Path(path).exists(): + + # Both versions must exist before any streaming happens. + a_path = req.get('song_a_path') + b_path = req.get('song_b_path') + if not a_path or not b_path: abort(404) - response = send_from_directory(Path(path).parent, Path(path).name) + for p in (a_path, b_path): + if not Path(p).exists(): + abort(404) + + path = a_path if version == 'a' else b_path + file_path = Path(path) + file_size = file_path.stat().st_size + + range_header = request.headers.get('Range', '') + start = 0 + end = file_size - 1 + status_code = 200 + + if range_header and range_header.startswith('bytes='): + try: + range_value = range_header[len('bytes='):].strip() + if '-' in range_value: + parts = range_value.split('-') + if parts[0]: + start = int(parts[0]) + if parts[1]: + end = min(int(parts[1]), file_size - 1) + if start >= file_size or start > end: + abort(416) + status_code = 206 + except ValueError: + start = 0 + end = file_size - 1 + status_code = 200 + + def generate(): + with open(file_path, 'rb') as f: + f.seek(start) + remaining = end - start + 1 + chunk_size = 64 * 1024 + while remaining > 0: + to_read = min(chunk_size, remaining) + data = f.read(to_read) + if not data: + break + yield data + remaining -= len(data) + + response = current_app.response_class(generate(), mimetype='audio/mpeg') + response.status_code = status_code + response.headers['Accept-Ranges'] = 'bytes' response.headers['Content-Disposition'] = 'inline' - response.headers['X-Content-Type-Options'] = 'nosniff' + response.headers['Content-Length'] = str(end - start + 1) + if status_code == 206: + response.headers['Content-Range'] = f'bytes {start}-{end}/{file_size}' return response +@app.route('/audio//.mp3') +def audio(token, version): + """ + Legacy audio endpoint. Replaced by /api/stream//.mp3. + Returns 404 so old direct links do not work. + """ + abort(404) + + # --------------------------------------------------------------------------- # Admin routes # --------------------------------------------------------------------------- diff --git a/templates/player.html b/templates/player.html index 27a2c7e..716ff89 100644 --- a/templates/player.html +++ b/templates/player.html @@ -143,8 +143,8 @@