Added media preview to MI show page
This commit is contained in:
parent
8c8943e3be
commit
3a8913d13a
7 changed files with 56 additions and 28 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue