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

View file

@ -2,4 +2,16 @@ defmodule PinchflatWeb.MediaItems.MediaItemHTML do
use PinchflatWeb, :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

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>
</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">
<.tabbed_layout>
<:tab title="Attributes">
<div class="flex flex-col gap-10 dark:text-white">
<%!-- <audio controls class="w-full">
<source src={~p"/media/#{@media_item}/stream"} type="audio/mpeg" />
Your browser does not support the audio element.
</audio> --%>
<video controls class="w-full">
<source src={~p"/media/#{@media_item}/stream"} type="video/mp4" />
Your browser does not support the video element.
</video>
<%= if media_file_exists?(@media_item) do %>
<h3 class="font-bold text-xl">Preview</h3>
<.media_preview media_item={@media_item} />
<% end %>
<h3 class="font-bold text-xl">Attributes</h3>
<section>
<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
# 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
get "/media/:id/stream", MediaItems.MediaItemController, :stream
end

View file

@ -69,14 +69,14 @@ defmodule PinchflatWeb.MediaItemControllerTest do
describe "streaming media" do
test "returns 404 if the media isn't found", %{conn: conn} do
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
end
test "automatically sets the content type", %{conn: conn} do
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
end
@ -85,7 +85,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
media_item = media_item_with_attachments()
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
end
@ -104,7 +104,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn =
conn
|> 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 {"content-range", "bytes 0-100/#{filesize}"} in conn.resp_headers
@ -115,7 +115,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn =
conn
|> 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
end
@ -127,7 +127,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn =
conn
|> 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
end
@ -139,7 +139,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn =
conn
|> 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 {"content-range", "bytes 0-#{filesize - 1}/#{filesize}"} in conn.resp_headers
@ -152,7 +152,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn =
conn
|> 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
end
@ -164,7 +164,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn =
conn
|> 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
end
@ -180,7 +180,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
test "sets the correct status and headers", %{conn: conn, media_item: media_item} do
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 {"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
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
end
@ -200,7 +200,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
conn =
conn
|> 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.resp_body == contents