Files

107 lines
4.1 KiB
Python

"""MP3 delivery: local files with HTTP Range, and a streaming proxy to the home-server archive."""
import http.client
import os
import re
from urllib.parse import quote, unquote, urlparse
from radio.config import ARCHIVE_URL, REC_DIR
from radio.recordings import DAY_RE, FILE_RE, arch_index
def serve(h, rel, dl):
"""DAY/FILE (or DAY/noise/FILE) as audio/mpeg with Range support; ?dl=1 -> attachment."""
parts = unquote(rel).split("/")
if len(parts) == 3 and parts[1] == "noise":
parts = [parts[0], parts[2]]
if len(parts) != 2 or not DAY_RE.match(parts[0]) or not FILE_RE.match(parts[1]):
return h.send(404, "not found", "text/plain")
fh = None
# the classifier may move a file into noise/ while it is being played: try both places
for sub in ("", "noise"):
try:
fh = open(os.path.join(REC_DIR, parts[0], sub, parts[1]), "rb")
break
except OSError:
continue
if fh is None:
if parts[1] in arch_index(parts[0]):
return archive_proxy(h, parts[0], parts[1], dl)
return h.send(404, "not found", "text/plain")
with fh:
size = os.fstat(fh.fileno()).st_size
start, end = 0, size - 1
code = 200
rng = h.headers.get("Range")
if rng:
m = re.match(r"bytes=(\d*)-(\d*)$", rng.strip())
if not m or (m[1] == "" and m[2] == ""):
return h.send(416, "", "text/plain", {"Content-Range": f"bytes */{size}"})
if m[1] == "":
start = max(0, size - int(m[2]))
else:
start = int(m[1])
if m[2] != "":
end = min(int(m[2]), size - 1)
if start >= size or start > end:
return h.send(416, "", "text/plain", {"Content-Range": f"bytes */{size}"})
code = 206
length = end - start + 1
h.send_response(code)
h.send_header("Content-Type", "audio/mpeg")
h.send_header("Accept-Ranges", "bytes")
h.send_header("Content-Length", str(length))
h.send_header("Cache-Control", "public, max-age=86400")
if dl:
h.send_header("Content-Disposition", f'attachment; filename="{parts[1]}"')
if code == 206:
h.send_header("Content-Range", f"bytes {start}-{end}/{size}")
h.end_headers()
if h.command == "HEAD":
return
fh.seek(start)
remaining = length
try:
while remaining > 0:
chunk = fh.read(min(65536, remaining))
if not chunk:
break
h.wfile.write(chunk)
remaining -= len(chunk)
except (BrokenPipeError, ConnectionResetError):
pass
def archive_proxy(h, day, name, dl):
"""Stream an archived recording from the home-server HDD (Range passed through)."""
host = urlparse(ARCHIVE_URL)
conn = http.client.HTTPConnection(host.hostname, host.port or 80, timeout=10)
hdrs = {"Range": h.headers["Range"]} if h.headers.get("Range") else {}
try:
conn.request(h.command if h.command == "HEAD" else "GET", f"/{day}/{quote(name)}", headers=hdrs)
r = conn.getresponse()
except OSError:
return h.send(502, "archive unreachable", "text/plain")
try:
if r.status not in (200, 206):
r.read()
return h.send(404 if r.status == 404 else 502, "not found in archive", "text/plain")
h.send_response(r.status)
for k in ("Content-Type", "Content-Length", "Content-Range", "Accept-Ranges"):
if r.getheader(k):
h.send_header(k, r.getheader(k))
h.send_header("Cache-Control", "public, max-age=86400")
if dl:
h.send_header("Content-Disposition", f'attachment; filename="{name}"')
h.end_headers()
if h.command == "HEAD":
return
while True:
chunk = r.read(65536)
if not chunk:
break
h.wfile.write(chunk)
except (BrokenPipeError, ConnectionResetError, OSError):
pass
finally:
conn.close()