feat: stream MP3s through backend proxy endpoint
- Replace /audio/<token>/<v>.mp3 with /api/stream/<token>/<v>.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.
This commit is contained in:
parent
5863c32e35
commit
3057eb8eda
2 changed files with 73 additions and 11 deletions
80
app.py
80
app.py
|
|
@ -634,27 +634,89 @@ def revise(token):
|
||||||
return redirect(url_for('play', token=token))
|
return redirect(url_for('play', token=token))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/audio/<token>/<version>.mp3')
|
@app.route('/api/stream/<token>/<version>.mp3')
|
||||||
def audio(token, version):
|
def stream_audio(token, version):
|
||||||
"""
|
"""
|
||||||
Serve an uploaded MP3 file for a specific request token and version ('a' or 'b').
|
Stream an uploaded MP3 through a backend proxy endpoint.
|
||||||
This keeps the files off the public static path and ties them to the private token.
|
|
||||||
|
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)
|
req = get_request_by_token(token)
|
||||||
if not req:
|
if not req:
|
||||||
abort(404)
|
abort(404)
|
||||||
if version not in ('a', 'b'):
|
if version not in ('a', 'b'):
|
||||||
abort(404)
|
abort(404)
|
||||||
field = f'song_{version}_path'
|
|
||||||
path = req.get(field)
|
# Both versions must exist before any streaming happens.
|
||||||
if not path or not Path(path).exists():
|
a_path = req.get('song_a_path')
|
||||||
|
b_path = req.get('song_b_path')
|
||||||
|
if not a_path or not b_path:
|
||||||
abort(404)
|
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['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
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/audio/<token>/<version>.mp3')
|
||||||
|
def audio(token, version):
|
||||||
|
"""
|
||||||
|
Legacy audio endpoint. Replaced by /api/stream/<token>/<version>.mp3.
|
||||||
|
Returns 404 so old direct links do not work.
|
||||||
|
"""
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Admin routes
|
# Admin routes
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,8 @@
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
// Audio sources are assigned via JS so they do not appear in static HTML source.
|
// Audio sources are assigned via JS so they do not appear in static HTML source.
|
||||||
document.getElementById('player-a').src = "{{ url_for('audio', token=req.player_token, version='a') }}";
|
document.getElementById('player-a').src = "{{ url_for('stream_audio', token=req.player_token, version='a') }}";
|
||||||
document.getElementById('player-b').src = "{{ url_for('audio', token=req.player_token, version='b') }}";
|
document.getElementById('player-b').src = "{{ url_for('stream_audio', token=req.player_token, version='b') }}";
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue