CODEX - More changes to the binary files, listing and storing

This commit is contained in:
2026-02-23 13:58:40 +02:00
parent d390bdac0d
commit 04b2a0bcb8
5 changed files with 154 additions and 22 deletions

View File

@@ -232,7 +232,7 @@ async def delete_melody(melody_id: str) -> None:
doc_ref.delete()
# Delete storage files
_delete_storage_files(melody_id)
_delete_storage_files(melody_id, row["data"].get("uid"))
# Delete from SQLite
await melody_db.delete_melody(melody_id)
@@ -256,14 +256,116 @@ def upload_file(melody_id: str, file_bytes: bytes, filename: str, content_type:
return blob.public_url
def delete_file(melody_id: str, file_type: str) -> None:
def _is_binary_blob_name(blob_name: str) -> bool:
lower = (blob_name or "").lower()
base = lower.rsplit("/", 1)[-1]
if "preview" in base:
return False
return ("binary" in base) or base.endswith(".bin") or base.endswith(".bsm")
def _safe_storage_segment(raw: str | None, fallback: str) -> str:
value = (raw or "").strip()
if not value:
value = fallback
chars = []
for ch in value:
if ch.isalnum() or ch in ("-", "_", "."):
chars.append(ch)
else:
chars.append("_")
cleaned = "".join(chars).strip("._")
return cleaned or fallback
def _storage_prefixes(melody_id: str, melody_uid: str | None) -> list[str]:
uid_seg = _safe_storage_segment(melody_uid, melody_id)
id_seg = _safe_storage_segment(melody_id, melody_id)
prefixes = [f"melodies/{uid_seg}/"]
if uid_seg != id_seg:
# Legacy path support
prefixes.append(f"melodies/{id_seg}/")
return prefixes
def _list_blobs_for_prefixes(bucket, prefixes: list[str]):
all_blobs = []
seen = set()
for prefix in prefixes:
for blob in bucket.list_blobs(prefix=prefix):
if blob.name in seen:
continue
seen.add(blob.name)
all_blobs.append(blob)
return all_blobs
def upload_file_for_melody(melody_id: str, melody_uid: str | None, melody_pid: str | None, file_bytes: bytes, filename: str, content_type: str) -> str:
"""Upload a file to Firebase Storage under melodies/{melody_uid or melody_id}/.
Binary files are stored as {pid}.bsm and replace previous melody binaries.
"""
bucket = get_bucket()
if not bucket:
raise RuntimeError("Firebase Storage not initialized")
prefixes = _storage_prefixes(melody_id, melody_uid)
primary_prefix = prefixes[0]
if content_type in ("application/octet-stream", "application/macbinary"):
# Keep one active binary per melody, clean older binaries in both legacy/current prefixes.
for blob in _list_blobs_for_prefixes(bucket, prefixes):
if _is_binary_blob_name(blob.name):
blob.delete()
stem = filename.rsplit(".", 1)[0] if "." in filename else filename
pid_seg = _safe_storage_segment(stem or melody_pid, "binary")
storage_path = f"{primary_prefix}{pid_seg}.bsm"
binary_content_type = "application/octet-stream"
blob = bucket.blob(storage_path)
blob.upload_from_string(file_bytes, content_type=binary_content_type)
blob.make_public()
return blob.public_url
ext = filename.rsplit(".", 1)[-1] if "." in filename else "mp3"
storage_path = f"{primary_prefix}preview.{ext}"
blob = bucket.blob(storage_path)
blob.upload_from_string(file_bytes, content_type=content_type)
blob.make_public()
return blob.public_url
def get_binary_file_bytes(melody_id: str, melody_uid: str | None = None) -> tuple[bytes, str]:
"""Fetch current binary bytes for a melody from Firebase Storage."""
bucket = get_bucket()
if not bucket:
raise RuntimeError("Firebase Storage not initialized")
prefixes = _storage_prefixes(melody_id, melody_uid)
blobs = [b for b in _list_blobs_for_prefixes(bucket, prefixes) if _is_binary_blob_name(b.name)]
if not blobs:
raise NotFoundError("Binary file")
# Prefer explicit binary.* naming, then newest.
blobs.sort(
key=lambda b: (
0 if "binary" in b.name.rsplit("/", 1)[-1].lower() else 1,
-(int(b.time_created.timestamp()) if getattr(b, "time_created", None) else 0),
)
)
chosen = blobs[0]
data = chosen.download_as_bytes()
content_type = chosen.content_type or "application/octet-stream"
return data, content_type
def delete_file(melody_id: str, file_type: str, melody_uid: str | None = None) -> None:
"""Delete a specific file from storage. file_type is 'binary' or 'preview'."""
bucket = get_bucket()
if not bucket:
return
prefix = f"melodies/{melody_id}/"
blobs = list(bucket.list_blobs(prefix=prefix))
prefixes = _storage_prefixes(melody_id, melody_uid)
blobs = _list_blobs_for_prefixes(bucket, prefixes)
for blob in blobs:
if file_type == "binary" and "binary" in blob.name:
@@ -272,31 +374,31 @@ def delete_file(melody_id: str, file_type: str) -> None:
blob.delete()
def _delete_storage_files(melody_id: str) -> None:
def _delete_storage_files(melody_id: str, melody_uid: str | None = None) -> None:
"""Delete all storage files for a melody."""
bucket = get_bucket()
if not bucket:
return
prefix = f"melodies/{melody_id}/"
blobs = list(bucket.list_blobs(prefix=prefix))
prefixes = _storage_prefixes(melody_id, melody_uid)
blobs = _list_blobs_for_prefixes(bucket, prefixes)
for blob in blobs:
blob.delete()
def get_storage_files(melody_id: str) -> dict:
def get_storage_files(melody_id: str, melody_uid: str | None = None) -> dict:
"""List storage files for a melody, returning URLs."""
bucket = get_bucket()
if not bucket:
return {"binary_url": None, "preview_url": None}
prefix = f"melodies/{melody_id}/"
blobs = list(bucket.list_blobs(prefix=prefix))
prefixes = _storage_prefixes(melody_id, melody_uid)
blobs = _list_blobs_for_prefixes(bucket, prefixes)
result = {"binary_url": None, "preview_url": None}
for blob in blobs:
blob.make_public()
if "binary" in blob.name:
if _is_binary_blob_name(blob.name):
result["binary_url"] = blob.public_url
elif "preview" in blob.name:
result["preview_url"] = blob.public_url