Adds thumbnails as episode-level images for podcasts (#201)
This commit is contained in:
@@ -75,6 +75,8 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp build_media_item_xml(source, media_item, url_base) do
|
defp build_media_item_xml(source, media_item, url_base) do
|
||||||
|
item_image_path = item_image_path(url_base, media_item)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
<item>
|
<item>
|
||||||
<guid isPermaLink="false">#{media_item.uuid}</guid>
|
<guid isPermaLink="false">#{media_item.uuid}</guid>
|
||||||
@@ -91,6 +93,10 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
|||||||
<itunes:author>#{safe(source.custom_name)}</itunes:author>
|
<itunes:author>#{safe(source.custom_name)}</itunes:author>
|
||||||
<itunes:subtitle>#{safe(media_item.title)}</itunes:subtitle>
|
<itunes:subtitle>#{safe(media_item.title)}</itunes:subtitle>
|
||||||
<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>
|
<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>
|
||||||
|
|
||||||
|
#{item_image_path && ~s(<itunes:image href="#{safe(item_image_path)}"></itunes:image>)}
|
||||||
|
#{item_image_path && ~s(<podcast:images srcset="#{safe(item_image_path)}" />)}
|
||||||
|
|
||||||
<itunes:explicit>false</itunes:explicit>
|
<itunes:explicit>false</itunes:explicit>
|
||||||
</item>
|
</item>
|
||||||
"""
|
"""
|
||||||
@@ -117,6 +123,16 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def item_image_path(url_base, media_item) do
|
||||||
|
if media_item.thumbnail_filepath && File.exists?(media_item.thumbnail_filepath) do
|
||||||
|
extension = Path.extname(media_item.thumbnail_filepath)
|
||||||
|
|
||||||
|
Path.join(url_base, "#{podcast_route(:episode_image, media_item.uuid)}#{extension}")
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp generate_upload_date(media_item) do
|
defp generate_upload_date(media_item) do
|
||||||
media_item.upload_date
|
media_item.upload_date
|
||||||
|> DatetimeUtils.date_to_datetime()
|
|> DatetimeUtils.date_to_datetime()
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
|
|||||||
use PinchflatWeb, :controller
|
use PinchflatWeb, :controller
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media.MediaQuery
|
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
|
alias Pinchflat.Media.MediaItem
|
||||||
|
alias Pinchflat.Media.MediaQuery
|
||||||
alias Pinchflat.Podcasts.RssFeedBuilder
|
alias Pinchflat.Podcasts.RssFeedBuilder
|
||||||
alias Pinchflat.Podcasts.PodcastHelpers
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
|
|
||||||
@@ -40,4 +41,16 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
|
|||||||
|> send_file(200, filepath)
|
|> send_file(200, filepath)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def episode_image(conn, %{"uuid" => uuid}) do
|
||||||
|
media_item = Repo.get_by!(MediaItem, uuid: uuid)
|
||||||
|
|
||||||
|
if media_item.thumbnail_filepath && File.exists?(media_item.thumbnail_filepath) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type(MIME.from_path(media_item.thumbnail_filepath))
|
||||||
|
|> send_file(200, media_item.thumbnail_filepath)
|
||||||
|
else
|
||||||
|
send_resp(conn, 404, "Image not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ defmodule PinchflatWeb.Router do
|
|||||||
|
|
||||||
get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed
|
get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed
|
||||||
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
|
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
|
||||||
|
get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image
|
||||||
|
|
||||||
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
|
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -137,6 +137,34 @@ defmodule Pinchflat.Podcasts.RssFeedBuilderTest do
|
|||||||
assert String.contains?(item_xml, ~s(length="1234"))
|
assert String.contains?(item_xml, ~s(length="1234"))
|
||||||
assert String.contains?(item_xml, ~s(type="video/mp4"))
|
assert String.contains?(item_xml, ~s(type="video/mp4"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "returns image tags if the media has a thumbnail", %{source: source} do
|
||||||
|
media_item = media_item_with_attachments(%{source_id: source.id, media_size_bytes: 1234})
|
||||||
|
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
[_before, item_xml, _after] = String.split(res, ~r(</?item>))
|
||||||
|
|
||||||
|
assert String.contains?(
|
||||||
|
item_xml,
|
||||||
|
~s(<itunes:image href="http://localhost:8945/media/#{media_item.uuid}/episode_image.jpg"></itunes:image>)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert String.contains?(
|
||||||
|
item_xml,
|
||||||
|
~s(<podcast:images srcset="http://localhost:8945/media/#{media_item.uuid}/episode_image.jpg" />)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return image tags if the media does not have a thumbnail", %{source: source} do
|
||||||
|
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||||
|
File.rm!(media_item.thumbnail_filepath)
|
||||||
|
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
[_before, item_xml, _after] = String.split(res, ~r(</?item>))
|
||||||
|
|
||||||
|
refute String.contains?(item_xml, ~s(itunes:image))
|
||||||
|
refute String.contains?(item_xml, ~s(podcast:images))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp format_date(date) do
|
defp format_date(date) do
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
defmodule PinchflatWeb.PodcastControllerTest do
|
defmodule PinchflatWeb.PodcastControllerTest do
|
||||||
use PinchflatWeb.ConnCase
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
describe "rss_feed" do
|
describe "rss_feed" do
|
||||||
@@ -35,4 +36,25 @@ defmodule PinchflatWeb.PodcastControllerTest do
|
|||||||
assert conn.resp_body == "Image not found"
|
assert conn.resp_body == "Image not found"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "episode_image" do
|
||||||
|
test "returns an episode image if one can be found", %{conn: conn} do
|
||||||
|
media_item = media_item_with_attachments()
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/media/#{media_item.uuid}/episode_image" <> ".jpg")
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
assert {"content-type", "image/jpeg; charset=utf-8"} in conn.resp_headers
|
||||||
|
assert conn.resp_body == File.read!(media_item.thumbnail_filepath)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 if an image cannot be found", %{conn: conn} do
|
||||||
|
media_item = media_item_fixture()
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/media/#{media_item.uuid}/episode_image" <> ".jpg")
|
||||||
|
|
||||||
|
assert conn.status == 404
|
||||||
|
assert conn.resp_body == "Image not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -67,16 +67,24 @@ defmodule Pinchflat.MediaFixtures do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def media_item_with_attachments(attrs \\ %{}) do
|
def media_item_with_attachments(attrs \\ %{}) do
|
||||||
stored_media_filepath =
|
base_dir =
|
||||||
Path.join([
|
Path.join([
|
||||||
Application.get_env(:pinchflat, :media_directory),
|
Application.get_env(:pinchflat, :media_directory),
|
||||||
"#{:rand.uniform(1_000_000)}",
|
"#{:rand.uniform(1_000_000)}"
|
||||||
"#{:rand.uniform(1_000_000)}_media.mp4"
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
stored_media_filepath = Path.join(base_dir, "#media.mp4")
|
||||||
|
thumbnail_filepath = Path.join(base_dir, "thumbnail.jpg")
|
||||||
|
|
||||||
FilesystemUtils.cp_p!(media_filepath_fixture(), stored_media_filepath)
|
FilesystemUtils.cp_p!(media_filepath_fixture(), stored_media_filepath)
|
||||||
|
FilesystemUtils.cp_p!(thumbnail_filepath_fixture(), thumbnail_filepath)
|
||||||
|
|
||||||
|
merged_attrs =
|
||||||
|
Map.merge(attrs, %{
|
||||||
|
media_filepath: stored_media_filepath,
|
||||||
|
thumbnail_filepath: thumbnail_filepath
|
||||||
|
})
|
||||||
|
|
||||||
merged_attrs = Map.merge(attrs, %{media_filepath: stored_media_filepath})
|
|
||||||
media_item_fixture(merged_attrs)
|
media_item_fixture(merged_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user