Many tests
This commit is contained in:
parent
7879e7202f
commit
b7e7992f4d
12 changed files with 410 additions and 50 deletions
|
|
@ -14,8 +14,6 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Settings
|
alias Pinchflat.Settings
|
||||||
alias Pinchflat.Sources.Source
|
|
||||||
alias Pinchflat.Media.MediaItem
|
|
||||||
alias Pinchflat.Filesystem.FilesystemHelpers
|
alias Pinchflat.Filesystem.FilesystemHelpers
|
||||||
|
|
||||||
def start_link(opts \\ []) do
|
def start_link(opts \\ []) do
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,27 @@
|
||||||
defmodule Pinchflat.Podcasts.PodcastHelpers do
|
defmodule Pinchflat.Podcasts.PodcastHelpers do
|
||||||
|
@moduledoc """
|
||||||
|
Methods for fetching postcast-related data from a source
|
||||||
|
or its media items
|
||||||
|
"""
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
|
alias Pinchflat.Metadata.MediaMetadata
|
||||||
|
alias Pinchflat.Metadata.SourceMetadata
|
||||||
|
|
||||||
# TODO: test
|
@doc """
|
||||||
|
Returns a list of media items that have been downloaded to disk
|
||||||
|
and have been proven to still exist there.
|
||||||
|
|
||||||
|
Useful for podcasts since we don't want to serve media that
|
||||||
|
has been deleted or moved, but it's also fairly generally useful
|
||||||
|
so I could see this being moved in the future.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- limit: integer - the maximum number of media items to return
|
||||||
|
|
||||||
|
Returns: [%MediaItem{}]
|
||||||
|
"""
|
||||||
def persisted_media_items_for(source, opts \\ []) do
|
def persisted_media_items_for(source, opts \\ []) do
|
||||||
limit = Keyword.get(opts, :limit, 500)
|
limit = Keyword.get(opts, :limit, 500)
|
||||||
|
|
||||||
|
|
@ -11,8 +30,19 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
|
||||||
|> Enum.filter(fn media_item -> File.exists?(media_item.media_filepath) end)
|
|> Enum.filter(fn media_item -> File.exists?(media_item.media_filepath) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: test
|
@doc """
|
||||||
# Returns string or nil
|
Selects a cover image for a source based on the source's metadata
|
||||||
|
and the metadata of the media items associated with the source. Also
|
||||||
|
ensures images exist on disk.
|
||||||
|
|
||||||
|
Only one media item should need to be returned since this is using the
|
||||||
|
internal metadata which, so long as the media_item was _downloaded_, should
|
||||||
|
be guaranteed to exist.
|
||||||
|
|
||||||
|
Prefers the source's poster, then fanart, then the media item's thumbnail.
|
||||||
|
|
||||||
|
Returns: {:ok, filepath} | {:error, :no_suitable_image}
|
||||||
|
"""
|
||||||
def select_cover_image(source, media_items) do
|
def select_cover_image(source, media_items) do
|
||||||
source_with_preloads = Repo.preload(source, :metadata)
|
source_with_preloads = Repo.preload(source, :metadata)
|
||||||
|
|
||||||
|
|
@ -20,20 +50,24 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
|
||||||
|> get_images_by_preference(media_items)
|
|> get_images_by_preference(media_items)
|
||||||
|> Enum.reject(&is_nil(&1))
|
|> Enum.reject(&is_nil(&1))
|
||||||
|> Enum.find(&File.exists?/1)
|
|> Enum.find(&File.exists?/1)
|
||||||
|
|> case do
|
||||||
|
nil -> {:error, :no_suitable_image}
|
||||||
|
filepath -> {:ok, filepath}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_images_by_preference(source_with_preloads, []) do
|
defp get_images_by_preference(source_with_preloads, []) do
|
||||||
source_metadata = source_with_preloads.metadata
|
source_metadata = source_with_preloads.metadata || %SourceMetadata{}
|
||||||
|
|
||||||
[
|
[
|
||||||
source_metadata.poster_filepath,
|
source_metadata.poster_filepath,
|
||||||
source_metadata.banner_filepath
|
source_metadata.fanart_filepath
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_images_by_preference(source_with_preloads, [media_item | _]) do
|
defp get_images_by_preference(source_with_preloads, [media_item | _]) do
|
||||||
media_item_with_preloads = Repo.preload(media_item, :metadata)
|
media_item_with_preloads = Repo.preload(media_item, :metadata)
|
||||||
media_item_metadata = media_item_with_preloads.metadata
|
media_item_metadata = media_item_with_preloads.metadata || %MediaMetadata{}
|
||||||
source_images = get_images_by_preference(source_with_preloads, [])
|
source_images = get_images_by_preference(source_with_preloads, [])
|
||||||
|
|
||||||
source_images ++ [media_item_metadata.thumbnail_filepath]
|
source_images ++ [media_item_metadata.thumbnail_filepath]
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,34 @@
|
||||||
defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
||||||
|
@moduledoc """
|
||||||
|
Methods for building an RSS feed for a source and its media items.
|
||||||
|
"""
|
||||||
|
|
||||||
@datetime_format "%a, %d %b %Y %H:%M:%S %z"
|
@datetime_format "%a, %d %b %Y %H:%M:%S %z"
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.DatetimeUtils
|
||||||
alias Pinchflat.Podcasts.PodcastHelpers
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
alias PinchflatWeb.Router.Helpers, as: Routes
|
alias PinchflatWeb.Router.Helpers, as: Routes
|
||||||
|
|
||||||
# TODO: test
|
@doc """
|
||||||
# TODO: only MIs that are confirmed to exist on-disk should be provided
|
Builds an RSS feed for a given source and its media items.
|
||||||
def build(source, media_items) do
|
Only MediaItems that have been persisted will be included in the feed.
|
||||||
|
|
||||||
|
## Options:
|
||||||
|
- `:limit` - The maximum number of media items to include in the feed. Defaults to 300.
|
||||||
|
|
||||||
|
Returns an XML document as a string.
|
||||||
|
"""
|
||||||
|
def build(source, opts \\ []) do
|
||||||
|
limit = Keyword.get(opts, :limit, 300)
|
||||||
|
|
||||||
|
media_items = PodcastHelpers.persisted_media_items_for(source, limit: limit)
|
||||||
build_source_xml(source, media_items)
|
build_source_xml(source, media_items)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_source_xml(source, media_items) do
|
defp build_source_xml(source, media_items) do
|
||||||
media_item_xml = Enum.map(media_items, &build_media_item_xml(source, &1))
|
media_item_xml = Enum.map(media_items, &build_media_item_xml(source, &1))
|
||||||
|
# "caching" the image path since it requires some DB calls and is used twice
|
||||||
|
feed_image_path = feed_image_path(source, media_items)
|
||||||
|
|
||||||
# Useful: resources:
|
# Useful: resources:
|
||||||
# - https://validator.w3.org/feed/#validate_by_input
|
# - https://validator.w3.org/feed/#validate_by_input
|
||||||
|
|
@ -30,20 +47,20 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
||||||
<category>TV & Film</category>
|
<category>TV & Film</category>
|
||||||
<generator>Generated by Pinchflat</generator>
|
<generator>Generated by Pinchflat</generator>
|
||||||
<language>en-us</language>
|
<language>en-us</language>
|
||||||
<lastBuildDate>#{Calendar.strftime(DateTime.utc_now(), @datetime_format)}</lastBuildDate>
|
<lastBuildDate>#{Calendar.strftime(source.updated_at, @datetime_format)}</lastBuildDate>
|
||||||
<pubDate>#{Calendar.strftime(source.inserted_at, @datetime_format)}</pubDate>
|
<pubDate>#{Calendar.strftime(source.inserted_at, @datetime_format)}</pubDate>
|
||||||
<atom:link href="#{generate_self_link(source)}" rel="self" type="application/rss+xml" />
|
<atom:link href="#{generate_self_link(source)}" rel="self" type="application/rss+xml" />
|
||||||
<podcast:locked>yes</podcast:locked>
|
<podcast:locked>yes</podcast:locked>
|
||||||
<podcast:guid>#{source.uuid}</podcast:guid>
|
<podcast:guid>#{source.uuid}</podcast:guid>
|
||||||
<image>
|
<image>
|
||||||
<url>#{feed_image_path(source, media_items)}</url>
|
<url>#{feed_image_path}</url>
|
||||||
<title>#{source.custom_name}</title>
|
<title>#{source.custom_name}</title>
|
||||||
<link>#{source.original_url}</link>
|
<link>#{source.original_url}</link>
|
||||||
</image>
|
</image>
|
||||||
<itunes:author>#{source.custom_name}</itunes:author>
|
<itunes:author>#{source.custom_name}</itunes:author>
|
||||||
<itunes:subtitle>#{source.custom_name}</itunes:subtitle>
|
<itunes:subtitle>#{source.custom_name}</itunes:subtitle>
|
||||||
<itunes:block>yes</itunes:block>
|
<itunes:block>yes</itunes:block>
|
||||||
<itunes:image href="#{feed_image_path(source, media_items)}"></itunes:image>
|
<itunes:image href="#{feed_image_path}"></itunes:image>
|
||||||
<itunes:explicit>false</itunes:explicit>
|
<itunes:explicit>false</itunes:explicit>
|
||||||
<itunes:category text="TV & Film"></itunes:category>
|
<itunes:category text="TV & Film"></itunes:category>
|
||||||
|
|
||||||
|
|
@ -65,8 +82,8 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
||||||
<enclosure
|
<enclosure
|
||||||
url="#{media_stream_path(media_item)}"
|
url="#{media_stream_path(media_item)}"
|
||||||
length="#{media_item.media_size_bytes}"
|
length="#{media_item.media_size_bytes}"
|
||||||
type="#{determine_content_type(media_item)}">
|
type="#{MIME.from_path(media_item.media_filepath)}"
|
||||||
</enclosure>
|
/>
|
||||||
<itunes:author>#{source.custom_name}</itunes:author>
|
<itunes:author>#{source.custom_name}</itunes:author>
|
||||||
<itunes:subtitle>#{media_item.title}</itunes:subtitle>
|
<itunes:subtitle>#{media_item.title}</itunes:subtitle>
|
||||||
<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>
|
<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>
|
||||||
|
|
@ -86,30 +103,22 @@ defmodule Pinchflat.Podcasts.RssFeedBuilder do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp feed_image_path(source, media_items) do
|
defp feed_image_path(source, media_items) do
|
||||||
image_path_on_disk = PodcastHelpers.select_cover_image(source, media_items)
|
case PodcastHelpers.select_cover_image(source, media_items) do
|
||||||
|
{:error, _} ->
|
||||||
case image_path_on_disk do
|
|
||||||
nil ->
|
|
||||||
""
|
""
|
||||||
|
|
||||||
_ ->
|
{:ok, filepath} ->
|
||||||
extension = Path.extname(image_path_on_disk)
|
extension = Path.extname(filepath)
|
||||||
Path.join(url_base(), "#{podcast_route(:feed_image, source.uuid)}#{extension}")
|
Path.join(url_base(), "#{podcast_route(:feed_image, source.uuid)}#{extension}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp generate_upload_date(media_item) do
|
defp generate_upload_date(media_item) do
|
||||||
media_item.upload_date
|
media_item.upload_date
|
||||||
|> Date.to_gregorian_days()
|
|> DatetimeUtils.date_to_datetime()
|
||||||
|> Kernel.*(86400)
|
|
||||||
|> DateTime.from_gregorian_seconds()
|
|
||||||
|> Calendar.strftime(@datetime_format)
|
|> Calendar.strftime(@datetime_format)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp determine_content_type(media_item) do
|
|
||||||
MIME.from_path(media_item.media_filepath)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp podcast_route(action, params) do
|
defp podcast_route(action, params) do
|
||||||
Routes.podcast_path(PinchflatWeb.Endpoint, action, params)
|
Routes.podcast_path(PinchflatWeb.Endpoint, action, params)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
17
lib/pinchflat/utils/datetime_utils.ex
Normal file
17
lib/pinchflat/utils/datetime_utils.ex
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
defmodule Pinchflat.Utils.DatetimeUtils do
|
||||||
|
@moduledoc """
|
||||||
|
Utility methods for working with dates and datetimes
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Converts a Date to a DateTime
|
||||||
|
|
||||||
|
Returns %DateTime{}
|
||||||
|
"""
|
||||||
|
def date_to_datetime(date) do
|
||||||
|
date
|
||||||
|
|> Date.to_gregorian_days()
|
||||||
|
|> Kernel.*(86_400)
|
||||||
|
|> DateTime.from_gregorian_seconds()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -3,15 +3,13 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Podcasts.RssFeedBuilder
|
alias Pinchflat.Podcasts.RssFeedBuilder
|
||||||
alias Pinchflat.Podcasts.PodcastHelpers
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
|
|
||||||
# TODO: test
|
|
||||||
def rss_feed(conn, %{"uuid" => uuid}) do
|
def rss_feed(conn, %{"uuid" => uuid}) do
|
||||||
# TODO: change this to UUID
|
source = Repo.get_by!(Source, uuid: uuid)
|
||||||
source = Repo.get_by!(Source, id: uuid)
|
xml = RssFeedBuilder.build(source, limit: 300)
|
||||||
media_items = PodcastHelpers.persisted_media_items_for(source)
|
|
||||||
xml = RssFeedBuilder.build(source, media_items)
|
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/rss+xml")
|
|> put_resp_content_type("application/rss+xml")
|
||||||
|
|
@ -19,21 +17,21 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
|
||||||
|> send_resp(200, xml)
|
|> send_resp(200, xml)
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: test
|
|
||||||
def feed_image(conn, %{"uuid" => uuid}) do
|
def feed_image(conn, %{"uuid" => uuid}) do
|
||||||
source = Repo.get_by!(Source, uuid: uuid)
|
source = Repo.get_by!(Source, uuid: uuid)
|
||||||
# This provides a fallback image if the source has none.
|
# This provides a fallback image if the source has none.
|
||||||
# We only need one since we're using the internal metadata image which
|
# We only need one since we're using the internal metadata image which
|
||||||
# we know exists.
|
# we know exists.
|
||||||
media_items = Media.list_downloaded_media_items_for(source, limit: 1)
|
media_items = Media.list_downloaded_media_items_for(source, limit: 1)
|
||||||
filepath = PodcastHelpers.select_cover_image(source, media_items)
|
|
||||||
|
|
||||||
if filepath && File.exists?(filepath) do
|
case PodcastHelpers.select_cover_image(source, media_items) do
|
||||||
|
{:error, _} ->
|
||||||
|
send_resp(conn, 404, "Image not found")
|
||||||
|
|
||||||
|
{:ok, filepath} ->
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type(MIME.from_path(filepath))
|
|> put_resp_content_type(MIME.from_path(filepath))
|
||||||
|> send_file(200, filepath)
|
|> send_file(200, filepath)
|
||||||
else
|
|
||||||
send_resp(conn, 404, "File not found")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||||
_url, _opts, ot when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
_url, _opts, ot when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture(%{description: nil})
|
||||||
|
|
||||||
refute source.description
|
refute source.description
|
||||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
|
|
|
||||||
64
test/pinchflat/podcasts/podcast_helpers_test.exs
Normal file
64
test/pinchflat/podcasts/podcast_helpers_test.exs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
defmodule Pinchflat.Podcasts.PodcastHelpersTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Pinchflat.SourcesFixtures
|
||||||
|
import Pinchflat.MediaFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
|
|
||||||
|
describe "persisted_media_items_for/2" do
|
||||||
|
test "returns media items with files that exist on-disk" do
|
||||||
|
source = source_fixture()
|
||||||
|
good_media = media_item_with_attachments(%{source_id: source.id})
|
||||||
|
_bad_media = media_item_fixture(%{source_id: source.id, media_filepath: "/tmp/existing_file.mp3"})
|
||||||
|
|
||||||
|
assert [persisted_media] = PodcastHelpers.persisted_media_items_for(source)
|
||||||
|
assert persisted_media.id == good_media.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "lets you specify a limit" do
|
||||||
|
source = source_fixture()
|
||||||
|
_good_media = media_item_with_attachments(%{source_id: source.id})
|
||||||
|
|
||||||
|
assert [] = PodcastHelpers.persisted_media_items_for(source, limit: 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "select_cover_image/2" do
|
||||||
|
test "returns a source's poster, if present" do
|
||||||
|
source = source_with_metadata_attachments()
|
||||||
|
|
||||||
|
{:ok, res} = PodcastHelpers.select_cover_image(source, [])
|
||||||
|
|
||||||
|
assert res == source.metadata.poster_filepath
|
||||||
|
end
|
||||||
|
|
||||||
|
test "falls back to a source's fanart, if present" do
|
||||||
|
source = source_with_metadata_attachments()
|
||||||
|
|
||||||
|
File.rm(source.metadata.poster_filepath)
|
||||||
|
|
||||||
|
{:ok, res} = PodcastHelpers.select_cover_image(source, [])
|
||||||
|
|
||||||
|
assert res == source.metadata.fanart_filepath
|
||||||
|
end
|
||||||
|
|
||||||
|
test "falls back to a media item's thumbnail, if present" do
|
||||||
|
source = source_with_metadata_attachments()
|
||||||
|
media_item = media_item_with_metadata_attachments(%{source_id: source.id})
|
||||||
|
|
||||||
|
File.rm(source.metadata.poster_filepath)
|
||||||
|
File.rm(source.metadata.fanart_filepath)
|
||||||
|
|
||||||
|
{:ok, res} = PodcastHelpers.select_cover_image(source, [media_item])
|
||||||
|
|
||||||
|
assert res == media_item.metadata.thumbnail_filepath
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error if no artwork can be found" do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
assert PodcastHelpers.select_cover_image(source, []) == {:error, :no_suitable_image}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
132
test/pinchflat/podcasts/rss_feed_builder_test.exs
Normal file
132
test/pinchflat/podcasts/rss_feed_builder_test.exs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
defmodule Pinchflat.Podcasts.RssFeedBuilderTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Pinchflat.MediaFixtures
|
||||||
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Podcasts.RssFeedBuilder
|
||||||
|
|
||||||
|
@datetime_format "%a, %d %b %Y %H:%M:%S %z"
|
||||||
|
|
||||||
|
setup do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
{:ok, source: source}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build/2" do
|
||||||
|
test "returns an XML document", %{source: source} do
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(<?xml version="1.0" encoding="UTF-8"?>))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can optionally apply a limit to media items", %{source: source} do
|
||||||
|
good_media = media_item_with_attachments(%{source_id: source.id})
|
||||||
|
|
||||||
|
res = RssFeedBuilder.build(source, limit: 0)
|
||||||
|
|
||||||
|
refute String.contains?(res, ~s(<title>#{good_media.title}</title>))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build/2 when testing source XML" do
|
||||||
|
test "returns XML for static source attributes", %{source: source} do
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(<title>#{source.custom_name}</title>))
|
||||||
|
assert String.contains?(res, ~s(<link>#{source.original_url}</link>))
|
||||||
|
assert String.contains?(res, ~s(<description>#{source.description}</description>))
|
||||||
|
assert String.contains?(res, ~s(<itunes:author>#{source.custom_name}</itunes:author>))
|
||||||
|
assert String.contains?(res, ~s(<itunes:subtitle>#{source.custom_name}</itunes:subtitle>))
|
||||||
|
assert String.contains?(res, ~s(<description>#{source.description}</description>))
|
||||||
|
assert String.contains?(res, ~s(<podcast:guid>#{source.uuid}</podcast:guid>))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the lastBuildDate and pubDate based off the source's timestamps", %{source: source} do
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(<lastBuildDate>#{format_date(source.updated_at)}</lastBuildDate>))
|
||||||
|
assert String.contains?(res, ~s(<pubDate>#{format_date(source.inserted_at)}</pubDate>))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a self-link", %{source: source} do
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
|
||||||
|
assert String.contains?(
|
||||||
|
res,
|
||||||
|
~s(<atom:link href="http://localhost:4008/sources/#{source.uuid}/feed.xml" rel="self" type="application/rss+xml" />)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a link to the feed image" do
|
||||||
|
source = source_with_metadata_attachments()
|
||||||
|
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
[_before, image_block, _after] = String.split(res, ~r(</?image>))
|
||||||
|
|
||||||
|
assert String.contains?(image_block, ~s(<url>http://localhost:4008/sources/#{source.uuid}/feed_image.jpg</url>))
|
||||||
|
assert String.contains?(image_block, ~s(<title>#{source.custom_name}</title>))
|
||||||
|
assert String.contains?(image_block, ~s(<link>#{source.original_url}</link>))
|
||||||
|
|
||||||
|
assert String.contains?(
|
||||||
|
res,
|
||||||
|
~s(<itunes:image href="http://localhost:4008/sources/#{source.uuid}/feed_image.jpg"></itunes:image>)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build/2 when testing media XML" do
|
||||||
|
test "only includes media persisted to disk", %{source: source} do
|
||||||
|
good_media = media_item_with_attachments(%{source_id: source.id})
|
||||||
|
bad_media = media_item_fixture(%{source_id: source.id, media_filepath: "/tmp/existing_file.mp3"})
|
||||||
|
pending_media = media_item_fixture(%{source_id: source.id, media_filepath: nil})
|
||||||
|
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(<title>#{good_media.title}</title>))
|
||||||
|
refute String.contains?(res, ~s(<title>#{bad_media.title}</title>))
|
||||||
|
refute String.contains?(res, ~s(<title>#{pending_media.title}</title>))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns XML for static media attributes", %{source: source} do
|
||||||
|
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||||
|
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
[_before, item_xml, _after] = String.split(res, ~r(</?item>))
|
||||||
|
|
||||||
|
assert String.contains?(item_xml, ~s(<guid isPermaLink="false">#{media_item.uuid}</guid>))
|
||||||
|
assert String.contains?(item_xml, ~s(<title>#{media_item.title}</title>))
|
||||||
|
assert String.contains?(item_xml, ~s(<link>#{media_item.original_url}</link>))
|
||||||
|
assert String.contains?(item_xml, ~s(<description>#{media_item.description}</description>))
|
||||||
|
assert String.contains?(item_xml, ~s(<itunes:author>#{source.custom_name}</itunes:author>))
|
||||||
|
assert String.contains?(item_xml, ~s(<itunes:subtitle>#{media_item.title}</itunes:subtitle>))
|
||||||
|
assert String.contains?(item_xml, ~s(<itunes:summary><![CDATA[#{media_item.description}]]></itunes:summary>))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns pubDate based off the media's upload_date", %{source: source} do
|
||||||
|
media_item_with_attachments(%{source_id: source.id, upload_date: ~D[2020-01-01]})
|
||||||
|
|
||||||
|
res = RssFeedBuilder.build(source)
|
||||||
|
[_before, item_xml, _after] = String.split(res, ~r(</?item>))
|
||||||
|
|
||||||
|
assert String.contains?(item_xml, ~s(<pubDate>Wed, 01 Jan 2020 00:00:00 +0000</pubDate>))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an enclosure tag with the media's stream URL", %{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(<enclosure))
|
||||||
|
assert String.contains?(item_xml, ~s(url="http://localhost:4008/media/#{media_item.uuid}/stream.mp4"))
|
||||||
|
assert String.contains?(item_xml, ~s(length="1234"))
|
||||||
|
assert String.contains?(item_xml, ~s(type="video/mp4"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_date(date) do
|
||||||
|
Calendar.strftime(date, @datetime_format)
|
||||||
|
end
|
||||||
|
end
|
||||||
14
test/pinchflat/utils/datetime_utils_test.exs
Normal file
14
test/pinchflat/utils/datetime_utils_test.exs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
defmodule Pinchflat.Utils.DatetimeUtilsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.DatetimeUtils
|
||||||
|
|
||||||
|
describe "date_to_datetime/1" do
|
||||||
|
test "converts a Date to a DateTime" do
|
||||||
|
date = ~D[2022-01-01]
|
||||||
|
datetime = DatetimeUtils.date_to_datetime(date)
|
||||||
|
|
||||||
|
assert datetime == ~U[2022-01-01 00:00:00Z]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
38
test/pinchflat_web/controllers/podcast_controller_test.exs
Normal file
38
test/pinchflat_web/controllers/podcast_controller_test.exs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
defmodule PinchflatWeb.PodcastControllerTest do
|
||||||
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
|
describe "rss_feed" do
|
||||||
|
test "renders the XML document", %{conn: conn} do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/sources/#{source.uuid}/feed" <> ".xml")
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
assert {"content-type", "application/rss+xml; charset=utf-8"} in conn.resp_headers
|
||||||
|
assert {"content-disposition", "inline"} in conn.resp_headers
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "feed_image" do
|
||||||
|
test "returns a feed image if one can be found", %{conn: conn} do
|
||||||
|
source = source_with_metadata_attachments()
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/sources/#{source.uuid}/feed_image" <> ".jpg")
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
assert {"content-type", "image/jpeg; charset=utf-8"} in conn.resp_headers
|
||||||
|
assert conn.resp_body == File.read!(source.metadata.poster_filepath)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 if an image cannot be found", %{conn: conn} do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/sources/#{source.uuid}/feed_image" <> ".jpg")
|
||||||
|
|
||||||
|
assert conn.status == 404
|
||||||
|
assert conn.resp_body == "Image not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -5,6 +5,7 @@ defmodule Pinchflat.MediaFixtures do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Pinchflat.SourcesFixtures
|
alias Pinchflat.SourcesFixtures
|
||||||
|
alias Pinchflat.Filesystem.FilesystemHelpers
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generate a media_item.
|
Generate a media_item.
|
||||||
|
|
@ -44,6 +45,27 @@ defmodule Pinchflat.MediaFixtures do
|
||||||
media_item_fixture(merged_attrs)
|
media_item_fixture(merged_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def media_item_with_metadata_attachments(attrs \\ %{}) do
|
||||||
|
metadata_dir =
|
||||||
|
Path.join(Application.get_env(:pinchflat, :metadata_directory), "#{:rand.uniform(1_000_000)}")
|
||||||
|
|
||||||
|
json_gz_filepath = Path.join(metadata_dir, "metadata.json.gz")
|
||||||
|
thumbnail_filepath = Path.join(metadata_dir, "thumbnail.jpg")
|
||||||
|
|
||||||
|
FilesystemHelpers.cp_p!(media_metadata_filepath_fixture(), json_gz_filepath)
|
||||||
|
FilesystemHelpers.cp_p!(thumbnail_filepath_fixture(), thumbnail_filepath)
|
||||||
|
|
||||||
|
merged_attrs =
|
||||||
|
Map.merge(attrs, %{
|
||||||
|
metadata: %{
|
||||||
|
metadata_filepath: json_gz_filepath,
|
||||||
|
thumbnail_filepath: thumbnail_filepath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
media_item_with_attachments(merged_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
def media_item_with_attachments(attrs \\ %{}) do
|
def media_item_with_attachments(attrs \\ %{}) do
|
||||||
stored_media_filepath =
|
stored_media_filepath =
|
||||||
Path.join([
|
Path.join([
|
||||||
|
|
@ -52,10 +74,7 @@ defmodule Pinchflat.MediaFixtures do
|
||||||
"#{:rand.uniform(1_000_000)}_media.mp4"
|
"#{:rand.uniform(1_000_000)}_media.mp4"
|
||||||
])
|
])
|
||||||
|
|
||||||
fixture_media_filepath = media_filepath_fixture()
|
FilesystemHelpers.cp_p!(media_filepath_fixture(), stored_media_filepath)
|
||||||
|
|
||||||
:ok = File.mkdir_p(Path.dirname(stored_media_filepath))
|
|
||||||
:ok = File.cp(fixture_media_filepath, stored_media_filepath)
|
|
||||||
|
|
||||||
merged_attrs = Map.merge(attrs, %{media_filepath: stored_media_filepath})
|
merged_attrs = Map.merge(attrs, %{media_filepath: stored_media_filepath})
|
||||||
media_item_fixture(merged_attrs)
|
media_item_fixture(merged_attrs)
|
||||||
|
|
@ -105,4 +124,14 @@ defmodule Pinchflat.MediaFixtures do
|
||||||
"example.info.json"
|
"example.info.json"
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def media_metadata_filepath_fixture do
|
||||||
|
Path.join([
|
||||||
|
File.cwd!(),
|
||||||
|
"test",
|
||||||
|
"support",
|
||||||
|
"files",
|
||||||
|
"media_metadata.json"
|
||||||
|
])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ defmodule Pinchflat.SourcesFixtures do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.ProfilesFixtures
|
alias Pinchflat.MediaFixtures
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
|
alias Pinchflat.ProfilesFixtures
|
||||||
|
alias Pinchflat.Filesystem.FilesystemHelpers
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generate a source.
|
Generate a source.
|
||||||
|
|
@ -22,6 +24,7 @@ defmodule Pinchflat.SourcesFixtures do
|
||||||
collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
|
collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
|
||||||
collection_type: "channel",
|
collection_type: "channel",
|
||||||
custom_name: "Cool and good internal name!",
|
custom_name: "Cool and good internal name!",
|
||||||
|
description: "This is a description",
|
||||||
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||||
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
||||||
index_frequency_minutes: 60
|
index_frequency_minutes: 60
|
||||||
|
|
@ -48,6 +51,30 @@ defmodule Pinchflat.SourcesFixtures do
|
||||||
source_fixture(merged_attrs)
|
source_fixture(merged_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def source_with_metadata_attachments(attrs \\ %{}) do
|
||||||
|
metadata_dir =
|
||||||
|
Path.join(Application.get_env(:pinchflat, :metadata_directory), "#{:rand.uniform(1_000_000)}")
|
||||||
|
|
||||||
|
json_gz_filepath = Path.join(metadata_dir, "metadata.json.gz")
|
||||||
|
poster_filepath = Path.join(metadata_dir, "poster.jpg")
|
||||||
|
fanart_filepath = Path.join(metadata_dir, "fanart.jpg")
|
||||||
|
|
||||||
|
FilesystemHelpers.cp_p!(MediaFixtures.media_metadata_filepath_fixture(), json_gz_filepath)
|
||||||
|
FilesystemHelpers.cp_p!(MediaFixtures.thumbnail_filepath_fixture(), poster_filepath)
|
||||||
|
FilesystemHelpers.cp_p!(MediaFixtures.thumbnail_filepath_fixture(), fanart_filepath)
|
||||||
|
|
||||||
|
merged_attrs =
|
||||||
|
Map.merge(attrs, %{
|
||||||
|
metadata: %{
|
||||||
|
metadata_filepath: json_gz_filepath,
|
||||||
|
poster_filepath: poster_filepath,
|
||||||
|
fanart_filepath: fanart_filepath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
source_fixture(merged_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
def source_attributes_return_fixture do
|
def source_attributes_return_fixture do
|
||||||
source_attributes = [
|
source_attributes = [
|
||||||
%{
|
%{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue