From a7eaf28c5e500cf138c326ab89210413f11fe972 Mon Sep 17 00:00:00 2001 From: ravirajsinh45 Date: Mon, 13 Apr 2026 11:09:28 +0530 Subject: [PATCH] fix: append /master.m3u8 for video stream URLs in share endpoint (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /share/{token} asset overview built stream_url directly from s3_key_processed, which is the HLS folder prefix for videos — leaving viewers with a folder URL instead of the playlist. Mirror the existing fix already applied in get_share_stream_url and assets.py. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/routers/share.py | 6 +- apps/api/tests/test_share_video_stream.py | 127 ++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 apps/api/tests/test_share_video_stream.py diff --git a/apps/api/routers/share.py b/apps/api/routers/share.py index d5ee717..47e4b03 100644 --- a/apps/api/routers/share.py +++ b/apps/api/routers/share.py @@ -295,7 +295,11 @@ def validate_share_link_endpoint( stream_url = None if media_file: if media_file.s3_key_processed: - stream_url = generate_presigned_get_url(media_file.s3_key_processed) + if asset.asset_type == AssetType.video: + s3_key = f"{media_file.s3_key_processed}/master.m3u8" + else: + s3_key = media_file.s3_key_processed + stream_url = generate_presigned_get_url(s3_key) elif media_file.s3_key_raw: stream_url = generate_presigned_get_url(media_file.s3_key_raw) diff --git a/apps/api/tests/test_share_video_stream.py b/apps/api/tests/test_share_video_stream.py new file mode 100644 index 0000000..9003aa6 --- /dev/null +++ b/apps/api/tests/test_share_video_stream.py @@ -0,0 +1,127 @@ +"""Regression test for issue #45 — share endpoint must return master.m3u8 for video assets.""" +import uuid +from unittest.mock import MagicMock, patch + + +@patch("apps.api.routers.share.generate_presigned_get_url") +@patch("apps.api.routers.share._get_latest_media_file") +@patch("apps.api.routers.share._get_asset") +@patch("apps.api.routers.share.validate_share_link") +def test_validate_share_link_video_returns_master_m3u8( + mock_validate, + mock_get_asset, + mock_get_latest_media_file, + mock_presign, + client, + mock_db, +): + from apps.api.models.asset import AssetType + + asset_id = uuid.uuid4() + project_id = uuid.uuid4() + + link = MagicMock() + link.id = uuid.uuid4() + link.asset_id = asset_id + link.folder_id = None + link.project_id = None + link.visibility = "public" + link.password_hash = None + link.title = "test" + link.description = None + link.permission = "view" + link.allow_download = False + link.show_versions = False + link.show_watermark = False + link.appearance = None + link.created_by = uuid.uuid4() + mock_validate.return_value = link + + asset = MagicMock() + asset.id = asset_id + asset.name = "demo video" + asset.asset_type = AssetType.video + asset.description = None + asset.project_id = project_id + mock_get_asset.return_value = asset + + media_file = MagicMock() + media_file.s3_key_processed = "processed/proj/version-abc" + media_file.s3_key_raw = "raw/proj/version-abc/input.mp4" + media_file.s3_key_thumbnail = None + mock_get_latest_media_file.return_value = media_file + + mock_db.first.return_value = None + mock_presign.side_effect = lambda key, **kwargs: f"https://s3.example/{key}?sig=x" + + response = client.get("/share/some-token") + + assert response.status_code == 200 + body = response.json() + assert body["asset"] is not None + assert body["asset"]["stream_url"] is not None + assert body["asset"]["stream_url"].startswith( + "https://s3.example/processed/proj/version-abc/master.m3u8" + ), f"Expected master.m3u8 suffix, got: {body['asset']['stream_url']}" + + +@patch("apps.api.routers.share.generate_presigned_get_url") +@patch("apps.api.routers.share._get_latest_media_file") +@patch("apps.api.routers.share._get_asset") +@patch("apps.api.routers.share.validate_share_link") +def test_validate_share_link_image_does_not_append_master_m3u8( + mock_validate, + mock_get_asset, + mock_get_latest_media_file, + mock_presign, + client, + mock_db, +): + from apps.api.models.asset import AssetType + + asset_id = uuid.uuid4() + project_id = uuid.uuid4() + + link = MagicMock() + link.id = uuid.uuid4() + link.asset_id = asset_id + link.folder_id = None + link.project_id = None + link.visibility = "public" + link.password_hash = None + link.title = "test" + link.description = None + link.permission = "view" + link.allow_download = False + link.show_versions = False + link.show_watermark = False + link.appearance = None + link.created_by = uuid.uuid4() + mock_validate.return_value = link + + asset = MagicMock() + asset.id = asset_id + asset.name = "demo image" + asset.asset_type = AssetType.image + asset.description = None + asset.project_id = project_id + mock_get_asset.return_value = asset + + media_file = MagicMock() + media_file.s3_key_processed = "processed/proj/version-img/out.webp" + media_file.s3_key_raw = "raw/proj/version-img/input.jpg" + media_file.s3_key_thumbnail = None + mock_get_latest_media_file.return_value = media_file + + mock_db.first.return_value = None + mock_presign.side_effect = lambda key, **kwargs: f"https://s3.example/{key}?sig=x" + + response = client.get("/share/some-token") + + assert response.status_code == 200 + body = response.json() + assert body["asset"]["stream_url"] is not None + assert "master.m3u8" not in body["asset"]["stream_url"] + assert body["asset"]["stream_url"].startswith( + "https://s3.example/processed/proj/version-img/out.webp" + )