[WIP] Working on refactoring feed

This commit is contained in:
Kieran Eglin 2024-03-25 15:22:20 -07:00
parent d199f02a57
commit ab47bdbdf5
No known key found for this signature in database
GPG key ID: 193984967FCF432D
7 changed files with 111 additions and 56 deletions

View file

@ -0,0 +1,44 @@
defmodule Pinchflat.Podcasts.PostcastHelpers do
alias Pinchflat.Repo
alias Pinchflat.Media
# TODO: test
def persisted_media_items_for(source, opts \\ []) do
limit = Keyword.get(opts, :limit, 500)
source
|> Media.list_downloaded_media_items_for(limit: limit)
|> Enum.filter(fn media_item -> File.exists?(media_item.media_filepath) end)
end
# TODO: test
# Returns string or nil
def select_cover_image(source) do
source_with_preloads = Repo.preload(source, :metadata)
# Since we're looking for the metadata image, _any_ downloaded media
# items should be fine
media_items = Media.list_downloaded_media_items_for(source, limit: 1)
source_with_preloads
|> get_images_by_preference(media_items)
|> Enum.reject(&is_nil(&1))
|> Enum.find(&File.exists?/1)
end
def get_images_by_preference(source_with_preloads, []) do
source_metadata = source_with_preloads.metadata
[
source_metadata.poster_filepath,
source_metadata.banner_filepath
]
end
def get_images_by_preference(source_with_preloads, [media_item | _]) do
media_item_with_preloads = Repo.preload(media_item, :metadata)
media_item_metadata = media_item_with_preloads.metadata
source_images = get_images_by_preference(source_with_preloads, [])
source_images ++ [media_item_metadata.thumbnail_filepath]
end
end

View file

@ -1,6 +1,9 @@
defmodule Pinchflat.Podcasts.RssFeedBuilder do
@datetime_format "%a, %d %b %Y %H:%M:%S %z"
alias Pinchflat.Podcasts.PostcastHelpers
alias PinchflatWeb.Router.Helpers, as: Routes
# TODO: test
# TODO: only MIs that are confirmed to exist on-disk should be provided
def build(source, media_items) do
@ -36,14 +39,14 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
<podcast:locked>yes</podcast:locked>
<podcast:guid>#{source.uuid}</podcast:guid>
<image>
<url>#{generate_source_image_path(source)}</url>
<url>#{feed_image_path(source)}</url>
<title>#{source.custom_name}</title>
<link>#{source.original_url}</link>
</image>
<itunes:author>#{source.custom_name}</itunes:author>
<itunes:subtitle>#{source.custom_name}</itunes:subtitle>
<itunes:block>yes</itunes:block>
<itunes:image href="#{generate_source_image_path(source)}"></itunes:image>
<itunes:image href="#{feed_image_path(source)}"></itunes:image>
<itunes:explicit>false</itunes:explicit>
<itunes:category text="TV &amp; Film"></itunes:category>
@ -63,7 +66,7 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
<description>#{media_item.description}</description>
<pubDate>#{generate_upload_date(media_item)}</pubDate>
<enclosure
url="#{generate_media_stream_path(media_item)}"
url="#{media_stream_path(media_item)}"
length="#{media_item.media_size_bytes}"
type="#{determine_content_type(media_item)}">
</enclosure>
@ -76,18 +79,26 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
end
defp generate_self_link(source) do
"#{url_base()}/sources/#{source.uuid}/feed.xml"
Path.join(url_base(), "#{podcast_route(:rss_feed, source.uuid)}.xml")
end
defp generate_media_stream_path(media_item) do
defp media_stream_path(media_item) do
extension = Path.extname(media_item.media_filepath)
"#{url_base()}/media/#{media_item.uuid}/stream#{extension}"
Path.join(url_base(), "#{media_route(:stream, media_item.uuid)}#{extension}")
end
# TODO: add extension maybe. maybe refactor controller to handle this
defp generate_source_image_path(source) do
"#{url_base()}/sources/#{source.uuid}/feed_image"
defp feed_image_path(source) do
image_path_on_disk = PostcastHelpers.select_cover_image(source)
case image_path_on_disk do
nil ->
""
_ ->
extension = Path.extname(image_path_on_disk)
Path.join(url_base(), "#{podcast_route(:feed_image, source.uuid)}#{extension}")
end
end
defp generate_upload_date(media_item) do
@ -102,6 +113,14 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
MIME.from_path(media_item.media_filepath)
end
defp podcast_route(action, params) do
Routes.podcast_path(PinchflatWeb.Endpoint, action, params)
end
defp media_route(action, params) do
Routes.media_item_path(PinchflatWeb.Endpoint, action, params)
end
defp url_base do
Application.get_env(:pinchflat, :url_base)
end

View file

@ -21,7 +21,7 @@ defmodule PinchflatWeb do
def router do
quote do
use Phoenix.Router, helpers: false
use Phoenix.Router, helpers: true
# Import common connection and controller functions to use in pipelines
import Plug.Conn

View file

@ -36,7 +36,7 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do
#
# 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
def stream(conn, %{"uuid" => uuid}) do
media_item = Repo.get_by!(MediaItem, uuid: uuid)
if File.exists?(media_item.media_filepath) do

View file

@ -0,0 +1,34 @@
defmodule PinchflatWeb.Podcasts.PodcastController do
use PinchflatWeb, :controller
alias Pinchflat.Repo
alias Pinchflat.Podcasts.RssFeedBuilder
alias Pinchflat.Podcasts.PostcastHelpers
# TODO: test
def rss_feed(conn, %{"uuid" => uuid}) do
# TODO: change this to UUID
source = Repo.get_by!(Source, id: uuid)
media_items = PostcastHelpers.persisted_media_items_for(source)
xml = RssFeedBuilder.build(source, media_items)
conn
|> put_resp_content_type("application/rss+xml")
|> put_resp_header("content-disposition", "inline")
|> send_resp(200, xml)
end
# TODO: test
def feed_image(conn, %{"uuid" => uuid}) do
source = Repo.get_by!(Source, uuid: uuid)
filepath = PostcastHelpers.select_cover_image(source)
if filepath && File.exists?(filepath) do
conn
|> put_resp_content_type(MIME.from_path(filepath))
|> send_file(200, filepath)
else
send_resp(conn, 404, "File not found")
end
end
end

View file

@ -7,7 +7,6 @@ defmodule PinchflatWeb.Sources.SourceController do
alias Pinchflat.Sources
alias Pinchflat.Profiles
alias Pinchflat.Sources.Source
alias Pinchflat.Podcasts.RssFeedBuilder
def index(conn, _params) do
sources = Repo.preload(Sources.list_sources(), :media_profile)
@ -59,47 +58,6 @@ defmodule PinchflatWeb.Sources.SourceController do
)
end
# TODO: test
# TODO: maybe move both of these to a separate controller
def feed(conn, %{"id" => uuid}) do
# TODO: change this to UUID
source = Repo.get_by!(Source, id: uuid)
media_items = Media.list_downloaded_media_items_for(source, limit: 100)
xml = RssFeedBuilder.build(source, media_items)
conn
|> put_resp_content_type("application/rss+xml")
|> put_resp_header("content-disposition", "inline")
|> send_resp(200, xml)
end
# TODO: test
# TODO: look into media_items having a thumbnail path when the image is embedded but not on disk
# TODO: pull these images from the internal metadata instead. this implies I'll have to hook up
# metadata images for sources
def feed_image(conn, %{"id" => uuid}) do
source = Repo.get_by!(Source, uuid: uuid)
media_item = Media.list_downloaded_media_items_for(source, limit: 1)
filepath =
case {source.poster_filepath, media_item} do
{poster, _} when poster != nil -> poster
{nil, [media_item]} -> media_item.thumbnail_filepath
_ -> nil
end
IO.inspect(filepath)
if filepath && File.exists?(filepath) do
conn
|> put_resp_content_type(MIME.from_path(filepath))
|> send_file(200, filepath)
else
send_resp(conn, 404, "File not found")
end
end
def edit(conn, %{"id" => id}) do
source = Sources.get_source!(id)
changeset = Sources.change_source(source)

View file

@ -36,10 +36,10 @@ defmodule PinchflatWeb.Router do
# 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 "/sources/:id/feed", Sources.SourceController, :feed
get "/sources/:id/feed_image", Sources.SourceController, :feed_image
get "/sources/:uuid/rss_feed", Podcasts.PodcastController, :rss_feed
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
get "/media/:id/stream", MediaItems.MediaItemController, :stream
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
end
# Enable LiveDashboard and Swoosh mailbox preview in development