pinchflat/lib/pinchflat_web/controllers/media_items/media_item_controller.ex
Kieran 10880e9699
[Enhancement] Added link to local stream on media item show page (#265)
* Adds link for opening local stream

* Put conditional on correct element
2024-05-27 11:07:23 -07:00

133 lines
4.5 KiB
Elixir

defmodule PinchflatWeb.MediaItems.MediaItemController do
use PinchflatWeb, :controller
require Logger
alias Pinchflat.Repo
alias Pinchflat.Media
alias Pinchflat.Media.MediaItem
alias Pinchflat.Downloading.MediaDownloadWorker
def show(conn, %{"id" => id}) do
media_item =
id
|> Media.get_media_item!()
|> Repo.preload([:source, tasks: [:job]])
render(conn, :show, media_item: media_item)
end
def edit(conn, %{"id" => id}) do
media_item = Media.get_media_item!(id)
changeset = Media.change_media_item(media_item)
render(conn, :edit, media_item: media_item, changeset: changeset)
end
def update(conn, %{"id" => id, "media_item" => params}) do
media_item = Media.get_media_item!(id)
case Media.update_media_item(media_item, params) do
{:ok, media_item} ->
conn
|> put_flash(:info, "Media Item updated successfully.")
|> redirect(to: ~p"/sources/#{media_item.source_id}/media/#{media_item}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, media_item: media_item, changeset: changeset)
end
end
def delete(conn, %{"id" => id} = params) do
prevent_download = Map.get(params, "prevent_download", false)
media_item = Media.get_media_item!(id)
{:ok, _} = Media.delete_media_files(media_item, %{prevent_download: prevent_download})
conn
|> put_flash(:info, "Files deleted successfully.")
|> redirect(to: ~p"/sources/#{media_item.source_id}")
end
def force_download(conn, %{"media_item_id" => id}) do
media_item = Media.get_media_item!(id)
:ok =
case MediaDownloadWorker.kickoff_with_task(media_item, %{force: true}) do
{:ok, _} -> :ok
{:error, :duplicate_job} -> :ok
err -> err
end
conn
|> put_flash(:info, "Download task enqueued.")
|> redirect(to: ~p"/sources/#{media_item.source_id}/media/#{media_item}")
end
# See here for details on streaming files and range requests:
# https://www.zeng.dev/post/2023-http-range-and-play-mp4-in-browser/
#
# Uses the UUID instead of the ID to avoid enumeration attacks
# since streaming is a public endpoint (ie: no auth required)
def stream(conn, %{"uuid" => 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
mime_type = MIME.from_path(media_item.media_filepath)
case parse_range(conn, file_size) do
{:ok, {start_pos, end_pos}} ->
Logger.debug("Streaming media item: #{media_item.uuid} from #{start_pos} to #{end_pos}")
length = end_pos - start_pos + 1
conn
|> put_resp_content_type(mime_type)
|> put_resp_header("accept-ranges", "bytes")
|> put_resp_header("content-range", "bytes #{start_pos}-#{end_pos}/#{file_size}")
|> put_resp_header("content-length", to_string(length))
|> put_resp_header("content-disposition", "inline; filename=\"#{media_item.title}\"")
|> send_file(206, media_item.media_filepath, start_pos, length)
{:error, :invalid_range} ->
Logger.debug("Invalid range request for media item: #{media_item.uuid} - serving full file")
conn
|> put_resp_content_type(mime_type)
|> put_resp_header("accept-ranges", "bytes")
|> put_resp_header("content-range", "bytes 0-#{file_size - 1}/#{file_size}")
|> put_resp_header("content-length", to_string(file_size))
|> put_resp_header("content-disposition", "inline; filename=\"#{media_item.title}\"")
|> send_file(200, media_item.media_filepath)
end
else
send_resp(conn, 404, "File not found")
end
end
defp parse_range(conn, file_size) do
with [range_header | _] <- get_req_header(conn, "range"),
["bytes", range] <- String.split(range_header, "="),
[start_pos, end_pos] <- String.split(range, "-") do
validate_range(start_pos, end_pos, file_size)
else
_ -> {:error, :invalid_range}
end
end
defp validate_range(start_pos, end_pos, file_size) do
case {Integer.parse(start_pos), Integer.parse(end_pos)} do
{:error, :error} ->
{:error, :invalid_range}
{{start_pos, _}, :error} ->
{:ok, {start_pos, file_size - 1}}
# See RFC7233
{{start_pos, _}, {end_pos, _}} when end_pos >= file_size ->
{:ok, {start_pos, file_size - 1}}
{{start_pos, _}, {end_pos, _}} ->
{:ok, {start_pos, end_pos}}
end
end
end