Added media preview to MI show page

This commit is contained in:
Kieran Eglin 2024-03-23 12:32:27 -07:00
parent 8c8943e3be
commit 3a8913d13a
No known key found for this signature in database
GPG key ID: 193984967FCF432D
7 changed files with 56 additions and 28 deletions

View file

@ -85,6 +85,10 @@ NOTE: it's recommended to not run the container as root. Doing so can create per
HTTP basic authentication is optionally supported. To use it, set the `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environment variables when starting the container. No authentication will be required unless you set _both_ of these. HTTP basic authentication is optionally supported. To use it, set the `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environment variables when starting the container. No authentication will be required unless you set _both_ of these.
### Important note:
The media streaming endpoint is not protected by basic auth. To help protect your media, these endpoints work with UUIDs instead of sequential IDs but this is still essentially security through obscurity. If you're concerned about the security of your media, consider using a reverse proxy with authentication or a VPN.
## EFF donations ## EFF donations
A portion of all donations to Pinchflat will be donated to the [Electronic Frontier Foundation](https://www.eff.org/). The EFF defends your online liberties and [backed](https://github.com/github/dmca/blob/9a85e0f021f7967af80e186b890776a50443f06c/2020/11/2020-11-16-RIAA-reversal-effletter.pdf) `youtube-dl` when Google took them down. [See here](https://github.com/kieraneglin/pinchflat/wiki/EFF-Donation-Receipts) for a list of donation receipts. A portion of all donations to Pinchflat will be donated to the [Electronic Frontier Foundation](https://www.eff.org/). The EFF defends your online liberties and [backed](https://github.com/github/dmca/blob/9a85e0f021f7967af80e186b890776a50443f06c/2020/11/2020-11-16-RIAA-reversal-effletter.pdf) `youtube-dl` when Google took them down. [See here](https://github.com/kieraneglin/pinchflat/wiki/EFF-Donation-Receipts) for a list of donation receipts.

View file

@ -3,6 +3,7 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Media.MediaItem
def show(conn, %{"id" => id}) do def show(conn, %{"id" => id}) do
media_item = media_item =
@ -32,13 +33,11 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do
# See here for details on streaming files and range requests: # See here for details on streaming files and range requests:
# https://www.zeng.dev/post/2023-http-range-and-play-mp4-in-browser/ # https://www.zeng.dev/post/2023-http-range-and-play-mp4-in-browser/
def stream(conn, %{"id" => id}) do #
media_item = Media.get_media_item!(id) # Uses the UUID instead of the ID to avoid enumeration attacks
# since streaming is a public endpoint (ie: no auth required)
# TODO: show audio vs. video element in UI depending on media type def stream(conn, %{"id" => uuid}) do
# TODO: consider how a podcast RSS feed would interact with HTTP basic auth media_item = Repo.get_by!(MediaItem, uuid: uuid)
# TODO: reconsider the sobelow changes I made
# TODO: UUID stuff
if File.exists?(media_item.media_filepath) do if File.exists?(media_item.media_filepath) do
file_size = File.stat!(media_item.media_filepath).size file_size = File.stat!(media_item.media_filepath).size

View file

@ -2,4 +2,16 @@ defmodule PinchflatWeb.MediaItems.MediaItemHTML do
use PinchflatWeb, :html use PinchflatWeb, :html
embed_templates "media_item_html/*" embed_templates "media_item_html/*"
def media_file_exists?(media_item) do
!!media_item.media_filepath and File.exists?(media_item.media_filepath)
end
def media_type(media_item) do
case Path.extname(media_item.media_filepath) do
ext when ext in [".mp4", ".webm", ".mkv"] -> :video
ext when ext in [".mp3", ".m4a"] -> :audio
_ -> :unknown
end
end
end end

View file

@ -0,0 +1,13 @@
<%= if media_type(@media_item) == :video do %>
<video controls class="max-h-96 w-full lg:w-2/3 xl:w-1/2">
<source src={~p"/media/#{@media_item.uuid}/stream"} type="video/mp4" />
Your browser does not support the video element.
</video>
<% end %>
<%= if media_type(@media_item) == :audio do %>
<audio controls class="w-full lg:w-2/3 xl:w-1/2">
<source src={~p"/media/#{@media_item.uuid}/stream"} type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
<% end %>

View file

@ -8,19 +8,16 @@
</h2> </h2>
</div> </div>
</div> </div>
<div class="rounded-sm border border-stroke bg-white py-5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5"> <div class="rounded-sm border border-stroke bg-white py-5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark px-7.5">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full overflow-x-auto">
<.tabbed_layout> <.tabbed_layout>
<:tab title="Attributes"> <:tab title="Attributes">
<div class="flex flex-col gap-10 dark:text-white"> <div class="flex flex-col gap-10 dark:text-white">
<%!-- <audio controls class="w-full"> <%= if media_file_exists?(@media_item) do %>
<source src={~p"/media/#{@media_item}/stream"} type="audio/mpeg" /> <h3 class="font-bold text-xl">Preview</h3>
Your browser does not support the audio element. <.media_preview media_item={@media_item} />
</audio> --%> <% end %>
<video controls class="w-full">
<source src={~p"/media/#{@media_item}/stream"} type="video/mp4" />
Your browser does not support the video element.
</video>
<h3 class="font-bold text-xl">Attributes</h3> <h3 class="font-bold text-xl">Attributes</h3>
<section> <section>
<strong>Source:</strong> <strong>Source:</strong>

View file

@ -30,6 +30,9 @@ defmodule PinchflatWeb.Router do
# Routes in here are NOT protected by basic auth. This is necessary for # Routes in here are NOT protected by basic auth. This is necessary for
# media streaming to work for RSS podcast feeds. # media streaming to work for RSS podcast feeds.
#
# TODO: consider putting the basic auth here behind a config flag
# so people that want RSS feeds to work can enable it.
scope "/", PinchflatWeb do scope "/", PinchflatWeb do
get "/media/:id/stream", MediaItems.MediaItemController, :stream get "/media/:id/stream", MediaItems.MediaItemController, :stream
end end

View file

@ -69,14 +69,14 @@ defmodule PinchflatWeb.MediaItemControllerTest do
describe "streaming media" do describe "streaming media" do
test "returns 404 if the media isn't found", %{conn: conn} do test "returns 404 if the media isn't found", %{conn: conn} do
media_item = media_item_fixture() media_item = media_item_fixture()
conn = get(conn, ~p"/media/#{media_item.id}/stream") conn = get(conn, ~p"/media/#{media_item.uuid}/stream")
assert conn.status == 404 assert conn.status == 404
end end
test "automatically sets the content type", %{conn: conn} do test "automatically sets the content type", %{conn: conn} do
media_item = media_item_with_attachments() media_item = media_item_with_attachments()
conn = get(conn, ~p"/media/#{media_item.id}/stream") conn = get(conn, ~p"/media/#{media_item.uuid}/stream")
assert {"content-type", "video/mp4; charset=utf-8"} in conn.resp_headers assert {"content-type", "video/mp4; charset=utf-8"} in conn.resp_headers
end end
@ -85,7 +85,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
media_item = media_item_with_attachments() media_item = media_item_with_attachments()
filesize = File.stat!(media_item.media_filepath).size filesize = File.stat!(media_item.media_filepath).size
conn = get(conn, ~p"/media/#{media_item.id}/stream") conn = get(conn, ~p"/media/#{media_item.uuid}/stream")
assert {"content-length", to_string(filesize)} in conn.resp_headers assert {"content-length", to_string(filesize)} in conn.resp_headers
end end
@ -104,7 +104,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn = conn =
conn conn
|> put_req_header("range", "bytes=0-100") |> put_req_header("range", "bytes=0-100")
|> get(~p"/media/#{media_item.id}/stream") |> get(~p"/media/#{media_item.uuid}/stream")
assert conn.status == 206 assert conn.status == 206
assert {"content-range", "bytes 0-100/#{filesize}"} in conn.resp_headers assert {"content-range", "bytes 0-100/#{filesize}"} in conn.resp_headers
@ -115,7 +115,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn = conn =
conn conn
|> put_req_header("range", "bytes=0-100") |> put_req_header("range", "bytes=0-100")
|> get(~p"/media/#{media_item.id}/stream") |> get(~p"/media/#{media_item.uuid}/stream")
assert byte_size(conn.resp_body) == 101 assert byte_size(conn.resp_body) == 101
end end
@ -127,7 +127,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn = conn =
conn conn
|> put_req_header("range", "bytes=100-200") |> put_req_header("range", "bytes=100-200")
|> get(~p"/media/#{media_item.id}/stream") |> get(~p"/media/#{media_item.uuid}/stream")
assert conn.resp_body == expected assert conn.resp_body == expected
end end
@ -139,7 +139,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn = conn =
conn conn
|> put_req_header("range", "bytes=0-#{filesize * 10}") |> put_req_header("range", "bytes=0-#{filesize * 10}")
|> get(~p"/media/#{media_item.id}/stream") |> get(~p"/media/#{media_item.uuid}/stream")
assert conn.resp_body == contents assert conn.resp_body == contents
assert {"content-range", "bytes 0-#{filesize - 1}/#{filesize}"} in conn.resp_headers assert {"content-range", "bytes 0-#{filesize - 1}/#{filesize}"} in conn.resp_headers
@ -152,7 +152,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn = conn =
conn conn
|> put_req_header("range", "bytes=0-") |> put_req_header("range", "bytes=0-")
|> get(~p"/media/#{media_item.id}/stream") |> get(~p"/media/#{media_item.uuid}/stream")
assert conn.resp_body == contents assert conn.resp_body == contents
end end
@ -164,7 +164,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn = conn =
conn conn
|> put_req_header("range", "bytes=100-") |> put_req_header("range", "bytes=100-")
|> get(~p"/media/#{media_item.id}/stream") |> get(~p"/media/#{media_item.uuid}/stream")
assert conn.resp_body == expected assert conn.resp_body == expected
end end
@ -180,7 +180,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
test "sets the correct status and headers", %{conn: conn, media_item: media_item} do test "sets the correct status and headers", %{conn: conn, media_item: media_item} do
filesize = File.stat!(media_item.media_filepath).size filesize = File.stat!(media_item.media_filepath).size
conn = get(conn, ~p"/media/#{media_item.id}/stream") conn = get(conn, ~p"/media/#{media_item.uuid}/stream")
assert conn.status == 200 assert conn.status == 200
assert {"content-length", to_string(filesize)} in conn.resp_headers assert {"content-length", to_string(filesize)} in conn.resp_headers
@ -189,7 +189,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
test "streams the entire file", %{conn: conn, media_item: media_item} do test "streams the entire file", %{conn: conn, media_item: media_item} do
contents = File.read!(media_item.media_filepath) contents = File.read!(media_item.media_filepath)
conn = get(conn, ~p"/media/#{media_item.id}/stream") conn = get(conn, ~p"/media/#{media_item.uuid}/stream")
assert conn.resp_body == contents assert conn.resp_body == contents
end end
@ -200,7 +200,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn = conn =
conn conn
|> put_req_header("range", "bytes=-") |> put_req_header("range", "bytes=-")
|> get(~p"/media/#{media_item.id}/stream") |> get(~p"/media/#{media_item.uuid}/stream")
assert conn.status == 200 assert conn.status == 200
assert conn.resp_body == contents assert conn.resp_body == contents