Merge pull request 'Click-to-listen on the live wall + HTTP Range on /media (#233)' (#253) from feat/233-wall-audio into main
This commit was merged in pull request #253.
This commit is contained in:
@@ -121,6 +121,67 @@ async def download_audio(gcs_uri: str) -> Optional[bytes]:
|
||||
return None
|
||||
|
||||
|
||||
# ── Ranged reads for /media playback (server-26#233) ────────────────────────
|
||||
# A 600 s FLAC call is ~13 MB. download_audio() above pulls the whole object
|
||||
# into memory, so /media used to make the browser wait for all of it before
|
||||
# the first sample played. These two read only what a request asks for: the
|
||||
# object size (one metadata GET) and then an inclusive byte span, which the
|
||||
# router streams out in chunks. One cached client, because the router issues
|
||||
# several span reads per playback and a fresh Client() per read re-resolves
|
||||
# credentials every time.
|
||||
_gcs_client_cache = None
|
||||
|
||||
|
||||
def _gcs_client():
|
||||
global _gcs_client_cache
|
||||
if _gcs_client_cache is None:
|
||||
from google.cloud import storage
|
||||
if settings.gcp_credentials_path:
|
||||
_gcs_client_cache = storage.Client.from_service_account_json(settings.gcp_credentials_path)
|
||||
else:
|
||||
_gcs_client_cache = storage.Client()
|
||||
return _gcs_client_cache
|
||||
|
||||
|
||||
async def audio_object_size(gcs_uri: str) -> Optional[int]:
|
||||
"""Size in bytes of a stored audio object, or None if it is missing/unreadable."""
|
||||
bucket_name, blob_path = split_gcs_uri(gcs_uri)
|
||||
if not bucket_name:
|
||||
return None
|
||||
|
||||
def _size() -> Optional[int]:
|
||||
blob = _gcs_client().bucket(bucket_name).blob(blob_path)
|
||||
blob.reload()
|
||||
return blob.size
|
||||
|
||||
try:
|
||||
size = await asyncio.to_thread(_size)
|
||||
except Exception as e:
|
||||
logger.warning(f"GCS metadata read failed for {gcs_uri}: {e}")
|
||||
return None
|
||||
return int(size) if size is not None else None
|
||||
|
||||
|
||||
async def download_audio_range(gcs_uri: str, start: int, end: int) -> Optional[bytes]:
|
||||
"""Bytes ``start``..``end`` INCLUSIVE of a stored object (GCS's own convention).
|
||||
|
||||
Reads only that span from GCS — never the whole object.
|
||||
"""
|
||||
bucket_name, blob_path = split_gcs_uri(gcs_uri)
|
||||
if not bucket_name:
|
||||
return None
|
||||
|
||||
def _read() -> bytes:
|
||||
blob = _gcs_client().bucket(bucket_name).blob(blob_path)
|
||||
return blob.download_as_bytes(start=start, end=end)
|
||||
|
||||
try:
|
||||
return await asyncio.to_thread(_read)
|
||||
except Exception as e:
|
||||
logger.warning(f"GCS ranged read failed for {gcs_uri} [{start}-{end}]: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def split_gcs_uri(gcs_uri: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""``gs://bucket/path/to.mp3`` → ``("bucket", "path/to.mp3")``."""
|
||||
if not gcs_uri or not gcs_uri.startswith("gs://"):
|
||||
|
||||
Reference in New Issue
Block a user