diff --git a/lib/mave_core/embeds.ex b/lib/mave_core/embeds.ex
index 94eb515..879ea14 100644
--- a/lib/mave_core/embeds.ex
+++ b/lib/mave_core/embeds.ex
@@ -1404,14 +1404,21 @@ defmodule MaveCore.Embeds do
end
defp preview_poster_url(%Space{} = space, %Embed{} = embed, settings, cache_buster) do
+ private_upload? = uploaded_poster_settings?(settings) and MaveCore.Playback.protected?(embed)
+
url =
- if generated_thumbnail_poster_settings?(settings) and not is_nil(cache_buster) do
- thumbnail_url(space, embed, cache_buster)
- else
- SettingsSerializer.preview_poster_url(space, embed, settings)
+ cond do
+ private_upload? ->
+ thumbnail_url(space, embed, cache_buster)
+
+ generated_thumbnail_poster_settings?(settings) and not is_nil(cache_buster) ->
+ thumbnail_url(space, embed, cache_buster)
+
+ true ->
+ SettingsSerializer.preview_poster_url(space, embed, settings)
end
- if uploaded_poster_settings?(settings) do
+ if uploaded_poster_settings?(settings) and not private_upload? do
with_cache_buster(url, cache_buster)
else
url
diff --git a/lib/mave_core/media/storage.ex b/lib/mave_core/media/storage.ex
index 48db20f..26fbb39 100644
--- a/lib/mave_core/media/storage.ex
+++ b/lib/mave_core/media/storage.ex
@@ -140,6 +140,12 @@ defmodule MaveCore.Media.Storage do
|> handle_response()
end
+ @doc "Lists current object keys under a prefix, following all result pages."
+ def list_keys(bucket, prefix, storage_profile \\ nil)
+ when is_binary(bucket) and is_binary(prefix) do
+ list_prefix_keys(bucket, prefix, storage_profile)
+ end
+
@doc "Fetches at most `max_bytes` from the beginning of an object."
def get_prefix(bucket, path, storage_profile, max_bytes)
when is_binary(bucket) and is_binary(path) do
diff --git a/test/mave_core/embeds_test.exs b/test/mave_core/embeds_test.exs
index b421e1b..3701228 100644
--- a/test/mave_core/embeds_test.exs
+++ b/test/mave_core/embeds_test.exs
@@ -2399,6 +2399,25 @@ defmodule MaveCore.EmbedsTest do
assert payload.thumbnail == versioned_thumbnail_url
assert payload.preview_poster == versioned_thumbnail_url
assert payload.snippet_player_poster == versioned_thumbnail_url
+
+ private_video =
+ video
+ |> Ecto.Changeset.change(playback_visibility: :private, playback_status: :private)
+ |> Repo.update!()
+
+ private_payload = Embeds.get_video_dashboard_payload(space, private_video)
+ uri = URI.parse(private_payload.preview_poster)
+ query = URI.decode_query(uri.query)
+
+ assert private_payload.preview_poster == private_payload.thumbnail
+ assert String.ends_with?(uri.path, "/#{video.hash}/thumbnail.jpg")
+ assert query["X-Amz-Signature"]
+ assert query["X-Amz-Expires"] == "86400"
+ assert query["response-cache-control"] == "private, max-age=300"
+ assert query["e"] == to_string(DateTime.to_unix(inserted_at, :microsecond))
+ assert length(URI.query_decoder(uri.query) |> Enum.filter(fn {key, _} -> key == "e" end)) == 1
+ refute private_payload.snippet_player_poster =~ "X-Amz-Signature"
+ assert private_video.settings.external_poster == thumbnail_url
end
test "dashboard video rows cache-bust completed custom thumbnail URLs" do
diff --git a/test/mave_core/media/storage_test.exs b/test/mave_core/media/storage_test.exs
index 4d438b2..2db16a4 100644
--- a/test/mave_core/media/storage_test.exs
+++ b/test/mave_core/media/storage_test.exs
@@ -9,6 +9,42 @@ defmodule MaveCore.Media.StorageTest do
setup {Req.Test, :verify_on_exit!}
+ test "list_keys follows object-store pagination without guessing version prefixes" do
+ Req.default_options(plug: {Req.Test, __MODULE__})
+
+ profile = [
+ endpoint: "https://storage.example.test",
+ access_key_id: "key",
+ secret_access_key: "secret"
+ ]
+
+ Req.Test.expect(__MODULE__, fn conn ->
+ conn = fetch_query_params(conn)
+ assert conn.query_params["prefix"] == "embed/"
+ refute conn.query_params["continuation-token"]
+
+ send_resp(
+ conn,
+ 200,
+ "embed/manifest.jsontruenext-page"
+ )
+ end)
+
+ Req.Test.expect(__MODULE__, fn conn ->
+ conn = fetch_query_params(conn)
+ assert conn.query_params["continuation-token"] == "next-page"
+
+ send_resp(
+ conn,
+ 200,
+ "embed/v8/player.htmlfalse"
+ )
+ end)
+
+ assert {:ok, ["embed/manifest.json", "embed/v8/player.html"]} =
+ Storage.list_keys("bucket", "embed/", profile)
+ end
+
test "signed reads support a full dashboard day while remaining bounded" do
profile = [
endpoint: "https://storage.example.test",
diff --git a/test/mave_core_web/live/dashboard/videos/show_test.exs b/test/mave_core_web/live/dashboard/videos/show_test.exs
index bcc68e0..d6c0a01 100644
--- a/test/mave_core_web/live/dashboard/videos/show_test.exs
+++ b/test/mave_core_web/live/dashboard/videos/show_test.exs
@@ -252,6 +252,37 @@ defmodule MaveCoreWeb.Live.Dashboard.Videos.ShowTest do
assert html =~ versioned_thumbnail_url
end
+ @tag :private_playback_ui
+ test "private custom thumbnails use signed URLs in the player and upload preview", %{conn: conn} do
+ {conn, space} = authenticated_conn(conn)
+ video = video_embed_fixture(space, %{name: "Private custom thumbnail"})
+
+ thumbnail_url =
+ SettingsSerializer.storage_object_url("space-#{space.hash}", "#{video.hash}/thumbnail.jpg")
+
+ {:ok, video} =
+ MaveCore.Embeds.update_settings(video, %{poster: :upload, external_poster: thumbnail_url})
+
+ video
+ |> Ecto.Changeset.change(playback_visibility: :private, playback_status: :private)
+ |> Repo.update!()
+
+ {:ok, view, _html} = live(conn, "/videos/#{video.id}")
+
+ assert has_element?(
+ view,
+ ~s(mave-player[poster*="/#{video.hash}/thumbnail.jpg?"][poster*="X-Amz-Signature="])
+ )
+
+ assert has_element?(
+ view,
+ ~s(#droparea[style*="/#{video.hash}/thumbnail.jpg?"][style*="X-Amz-Signature="])
+ )
+
+ refute has_element?(view, ~s(mave-player[poster="#{thumbnail_url}"]))
+ refute has_element?(view, "#snippet-code", "X-Amz-Signature")
+ end
+
@tag :poster_upload
test "poster processing remains visible until a delayed conversion completes", %{conn: conn} do
{conn, space} = authenticated_conn(conn)