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))
|
||||
|
||||
|
||||
@app.route('/audio/<token>/<version>.mp3')
|
||||
def audio(token, version):
|
||||
@app.route('/api/stream/<token>/<version>.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/<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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -143,8 +143,8 @@
|
|||
<script>
|
||||
(function(){
|
||||
// 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-b').src = "{{ url_for('audio', token=req.player_token, version='b') }}";
|
||||
document.getElementById('player-a').src = "{{ url_for('stream_audio', token=req.player_token, version='a') }}";
|
||||
document.getElementById('player-b').src = "{{ url_for('stream_audio', token=req.player_token, version='b') }}";
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Reference in a new issue