diff --git a/backend/__pycache__/api_routes.cpython-311.pyc b/backend/__pycache__/api_routes.cpython-311.pyc index 6251856..c53c11b 100644 Binary files a/backend/__pycache__/api_routes.cpython-311.pyc and b/backend/__pycache__/api_routes.cpython-311.pyc differ diff --git a/backend/__pycache__/db_queries.cpython-311.pyc b/backend/__pycache__/db_queries.cpython-311.pyc index a64d1c4..ad51aaf 100644 Binary files a/backend/__pycache__/db_queries.cpython-311.pyc and b/backend/__pycache__/db_queries.cpython-311.pyc differ diff --git a/backend/api_routes.py b/backend/api_routes.py index 8cc83cb..6f283ff 100644 --- a/backend/api_routes.py +++ b/backend/api_routes.py @@ -22,6 +22,7 @@ from db_queries import ( create_audio_post, create_user, get_archive_metadata, + get_original_audio_url, get_archive_rights, get_audio_post_by_id, get_post_bundle, @@ -438,6 +439,34 @@ def api_post_bundle(post_id: int): return jsonify(bundle) +@api.get("/posts//audio-url") +def api_post_audio_url(post_id: int): + """ + Get signed URL for original audio/video so users can play it. + Private posts require owner user_id in query params. + """ + row = get_audio_post_by_id(post_id) + if not row: + return _error("Post not found.", 404) + + visibility = row.get("visibility") + owner_id = row.get("user_id") + requester_id = request.args.get("user_id", type=int) + expires_in = request.args.get("expires_in", default=3600, type=int) + expires_in = min(max(60, expires_in), 86400) + + if visibility == "private" and requester_id != owner_id: + return _error("Not authorized to access this private audio.", 403) + + try: + result = get_original_audio_url(post_id=post_id, expires_in=expires_in) + return jsonify(result) + except ValueError as e: + return _error(str(e), 404) + except Exception as e: + return _error(str(e), 500) + + @api.post("/posts//files") def api_add_file(post_id: int): payload = request.get_json(force=True, silent=False) or {} diff --git a/backend/db_queries.py b/backend/db_queries.py index 0aee46a..a9e3255 100644 --- a/backend/db_queries.py +++ b/backend/db_queries.py @@ -3,7 +3,7 @@ Supabase data layer aligned with TitanForge/schema.sql. """ import os -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from dotenv import load_dotenv from supabase import Client, create_client @@ -36,6 +36,17 @@ def _paginate(page: int, limit: int) -> tuple[int, int]: return start, end +def _parse_bucket_path(stored_path: str) -> Tuple[str, str]: + """ + Convert stored path like 'archives/user/uuid/original/file.mp4' + into ('archives', 'user/uuid/original/file.mp4'). + """ + parts = (stored_path or "").split("/", 1) + if len(parts) != 2 or not parts[0] or not parts[1]: + raise ValueError(f"Invalid storage path format: {stored_path}") + return parts[0], parts[1] + + def upload_storage_object( bucket: str, object_path: str, @@ -56,6 +67,50 @@ def upload_storage_object( ) +def get_original_audio_url(post_id: int, expires_in: int = 3600) -> Dict[str, Any]: + """ + Return a signed URL for the original audio/video archive file. + """ + response = ( + supabase.table("archive_files") + .select("path, content_type") + .eq("post_id", post_id) + .eq("role", "original_audio") + .limit(1) + .execute() + ) + row = _first(response) + if not row: + raise ValueError("Original audio file not found for this post.") + + bucket, object_path = _parse_bucket_path(row["path"]) + + signed = supabase.storage.from_(bucket).create_signed_url(object_path, expires_in) + + # Supabase python client can return dict or object with .get depending on version. + if isinstance(signed, dict): + signed_url = ( + signed.get("signedURL") + or signed.get("signedUrl") + or signed.get("data", {}).get("signedUrl") + or signed.get("data", {}).get("signedURL") + ) + else: + signed_url = None + + if not signed_url: + raise RuntimeError("Failed to create signed URL for original audio.") + + return { + "post_id": post_id, + "bucket": bucket, + "object_path": object_path, + "content_type": row.get("content_type"), + "signed_url": signed_url, + "expires_in": expires_in, + } + + # ==================== Users ==================== def create_user(payload: Dict[str, Any]) -> Dict[str, Any]: