Relationships
@@ -35,10 +35,10 @@
<.list_items_from_map map={Map.from_struct(@source)} />
Downloaded Media
- <%= if length(@downloaded_media) > 0 do %>
+ <%= if match?([_|_], @downloaded_media) do %>
<.table rows={@downloaded_media} table_class="text-black dark:text-white">
<:col :let={media_item} label="Title">
- <%= String.slice(media_item.title, 0..50) %>...
+ <%= StringUtils.truncate(media_item.title, 50) %>
<:col :let={media_item} label="" class="flex place-content-evenly">
<.link
@@ -54,10 +54,10 @@
<% end %>
Pending Media
- <%= if length(@pending_media) > 0 do %>
+ <%= if match?([_|_], @pending_media) do %>
<.table rows={@pending_media} table_class="text-black dark:text-white">
<:col :let={media_item} label="Title">
- <%= String.slice(media_item.title, 0..50) %>...
+ <%= StringUtils.truncate(media_item.title, 50) %>
<:col :let={media_item} label="" class="flex place-content-evenly">
<.link
diff --git a/lib/pinchflat_web/controllers/searches/search_controller.ex b/lib/pinchflat_web/controllers/searches/search_controller.ex
new file mode 100644
index 0000000..333abea
--- /dev/null
+++ b/lib/pinchflat_web/controllers/searches/search_controller.ex
@@ -0,0 +1,12 @@
+defmodule PinchflatWeb.Searches.SearchController do
+ use PinchflatWeb, :controller
+
+ alias Pinchflat.Media
+
+ def show(conn, params) do
+ search_term = Map.get(params, "q", "")
+ search_results = Media.search(search_term)
+
+ render(conn, :show, search_term: search_term, search_results: search_results)
+ end
+end
diff --git a/lib/pinchflat_web/controllers/searches/search_html.ex b/lib/pinchflat_web/controllers/searches/search_html.ex
new file mode 100644
index 0000000..1e049be
--- /dev/null
+++ b/lib/pinchflat_web/controllers/searches/search_html.ex
@@ -0,0 +1,25 @@
+defmodule PinchflatWeb.Searches.SearchHTML do
+ use PinchflatWeb, :html
+
+ embed_templates "search_html/*"
+
+ @doc """
+ Highlight search terms in a string of text based on `[PF_HIGHLIGHT]` and `[/PF_HIGHLIGHT]` tags
+ """
+ attr :text, :string, required: true
+
+ def highlight_search_terms(assigns) do
+ split_string = String.split(assigns.text, ~r{\[PF_HIGHLIGHT\]|\[/PF_HIGHLIGHT\]}, include_captures: true)
+ assigns = assign(assigns, split_string: split_string)
+
+ ~H"""
+ <%= for fragment <- @split_string do %>
+ <%= render_fragment(fragment) %>
+ <% end %>
+ """
+ end
+
+ defp render_fragment("[PF_HIGHLIGHT]"), do: raw(~s(
))
+ defp render_fragment("[/PF_HIGHLIGHT]"), do: raw("")
+ defp render_fragment(text), do: text
+end
diff --git a/lib/pinchflat_web/controllers/searches/search_html/show.html.heex b/lib/pinchflat_web/controllers/searches/search_html/show.html.heex
new file mode 100644
index 0000000..996701e
--- /dev/null
+++ b/lib/pinchflat_web/controllers/searches/search_html/show.html.heex
@@ -0,0 +1,29 @@
+
+
+ Search Results for "<%= StringUtils.truncate(@search_term, 50) %>"
+
+
+
+
+
+
+ <%= if match?([_|_], @search_results) do %>
+ <.table rows={@search_results} table_class="text-black dark:text-white">
+ <:col :let={result} label="Title">
+ <%= StringUtils.truncate(result.title, 40) %>
+
+ <:col :let={result} label="Excerpt">
+ <.highlight_search_terms text={result.matching_search_term} />
+
+ <:col :let={result} label="" class="flex place-content-evenly">
+ <.link navigate={~p"/media/#{result.id}"} class="hover:text-secondary duration-200 ease-in-out mx-0.5">
+ <.icon name="hero-eye" />
+
+
+
+ <% else %>
+
No results found
+ <% end %>
+
+
+
diff --git a/lib/pinchflat_web/router.ex b/lib/pinchflat_web/router.ex
index 55f177a..55c9fcb 100644
--- a/lib/pinchflat_web/router.ex
+++ b/lib/pinchflat_web/router.ex
@@ -21,6 +21,7 @@ defmodule PinchflatWeb.Router do
resources "/media_profiles", MediaProfiles.MediaProfileController
resources "/media", Media.MediaItemController, only: [:show]
+ resources "/search", Searches.SearchController, only: [:show], singleton: true
resources "/sources", MediaSources.SourceController do
resources "/media", Media.MediaItemController, only: [:show]
diff --git a/priv/repo/migrations/20240210045410_add_description_to_media_items.exs b/priv/repo/migrations/20240210045410_add_description_to_media_items.exs
new file mode 100644
index 0000000..f03a34b
--- /dev/null
+++ b/priv/repo/migrations/20240210045410_add_description_to_media_items.exs
@@ -0,0 +1,9 @@
+defmodule Pinchflat.Repo.Migrations.AddDescriptionToMediaItems do
+ use Ecto.Migration
+
+ def change do
+ alter table(:media_items) do
+ add :description, :text
+ end
+ end
+end
diff --git a/priv/repo/migrations/20240210051038_add_search_field_to_media_items.exs b/priv/repo/migrations/20240210051038_add_search_field_to_media_items.exs
new file mode 100644
index 0000000..04de1d7
--- /dev/null
+++ b/priv/repo/migrations/20240210051038_add_search_field_to_media_items.exs
@@ -0,0 +1,28 @@
+defmodule Pinchflat.Repo.Migrations.AddSearchFieldToMediaItems do
+ use Ecto.Migration
+
+ def up do
+ execute """
+ ALTER TABLE media_items
+ ADD COLUMN searchable tsvector
+ GENERATED ALWAYS AS (
+ setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
+ setweight(to_tsvector('english', coalesce(description, '')), 'B')
+ ) STORED;
+ """
+
+ execute """
+ CREATE INDEX media_items_searchable_idx ON media_items USING gin(searchable);
+ """
+ end
+
+ def down do
+ execute """
+ DROP INDEX media_items_searchable_idx;
+ """
+
+ alter table(:media_items) do
+ remove :searchable
+ end
+ end
+end
diff --git a/test/pinchflat/media_client/backends/yt_dlp/metadata_parser_test.exs b/test/pinchflat/media_client/backends/yt_dlp/metadata_parser_test.exs
index e0e032f..c29675d 100644
--- a/test/pinchflat/media_client/backends/yt_dlp/metadata_parser_test.exs
+++ b/test/pinchflat/media_client/backends/yt_dlp/metadata_parser_test.exs
@@ -36,6 +36,12 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.MediaParserTest do
assert result.title == "Trying to Wheelie Without the Rear Brake"
end
+ test "it extracts the description", %{metadata: metadata} do
+ result = Parser.parse_for_media_item(metadata)
+
+ assert is_binary(result.description)
+ end
+
test "it returns the metadata as a map", %{metadata: metadata} do
result = Parser.parse_for_media_item(metadata)
diff --git a/test/pinchflat/media_client/backends/yt_dlp/video_collection_test.exs b/test/pinchflat/media_client/backends/yt_dlp/video_collection_test.exs
index 5230f5e..0fd13dc 100644
--- a/test/pinchflat/media_client/backends/yt_dlp/video_collection_test.exs
+++ b/test/pinchflat/media_client/backends/yt_dlp/video_collection_test.exs
@@ -20,7 +20,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do
test "it passes the expected default args" do
expect(YtDlpRunnerMock, :run, fn _url, opts, ot ->
assert opts == [:simulate, :skip_download]
- assert ot == "%(.{id,title,was_live,original_url})j"
+ assert ot == "%(.{id,title,was_live,original_url,description})j"
{:ok, ""}
end)
diff --git a/test/pinchflat/media_client/source_details_test.exs b/test/pinchflat/media_client/source_details_test.exs
index 2e1bd61..8ddb517 100644
--- a/test/pinchflat/media_client/source_details_test.exs
+++ b/test/pinchflat/media_client/source_details_test.exs
@@ -47,7 +47,7 @@ defmodule Pinchflat.MediaClient.SourceDetailsTest do
test "it passes the expected arguments to the backend" do
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
assert opts == [:simulate, :skip_download]
- assert ot == "%(.{id,title,was_live,original_url})j"
+ assert ot == "%(.{id,title,was_live,original_url,description})j"
{:ok, ""}
end)
diff --git a/test/pinchflat/media_client/video_downloader_test.exs b/test/pinchflat/media_client/video_downloader_test.exs
index cfc362a..794f6b5 100644
--- a/test/pinchflat/media_client/video_downloader_test.exs
+++ b/test/pinchflat/media_client/video_downloader_test.exs
@@ -60,7 +60,7 @@ defmodule Pinchflat.MediaClient.VideoDownloaderTest do
test "it sets the media_downloaded_at", %{media_item: media_item} do
assert media_item.media_downloaded_at == nil
assert {:ok, updated_media_item} = VideoDownloader.download_for_media_item(media_item)
- assert DateTime.diff(DateTime.utc_now(), updated_media_item.media_downloaded_at) < 1
+ assert DateTime.diff(DateTime.utc_now(), updated_media_item.media_downloaded_at) < 2
end
test "it extracts the title", %{media_item: media_item} do
@@ -68,6 +68,11 @@ defmodule Pinchflat.MediaClient.VideoDownloaderTest do
assert updated_media_item.title == "Trying to Wheelie Without the Rear Brake"
end
+ test "it extracts the description", %{media_item: media_item} do
+ assert {:ok, updated_media_item} = VideoDownloader.download_for_media_item(media_item)
+ assert is_binary(updated_media_item.description)
+ end
+
test "it extracts the media_filepath", %{media_item: media_item} do
assert media_item.media_filepath == nil
assert {:ok, updated_media_item} = VideoDownloader.download_for_media_item(media_item)
diff --git a/test/pinchflat/media_test.exs b/test/pinchflat/media_test.exs
index bcf758c..8fbe8d7 100644
--- a/test/pinchflat/media_test.exs
+++ b/test/pinchflat/media_test.exs
@@ -183,6 +183,37 @@ defmodule Pinchflat.MediaTest do
end
end
+ describe "search/1" do
+ setup do
+ media_item =
+ media_item_fixture(%{
+ title: "The quick brown fox",
+ description: "jumps over the lazy dog"
+ })
+
+ {:ok, %{media_item_id: media_item.id}}
+ end
+
+ test "searches based on title", %{media_item_id: media_item_id} do
+ assert [%{id: ^media_item_id}] = Media.search("quick")
+ end
+
+ test "searches based on description", %{media_item_id: media_item_id} do
+ assert [%{id: ^media_item_id}] = Media.search("lazy")
+ end
+
+ test "adds a matching_search_term attribute with the relevant text" do
+ assert [res] = Media.search("quick")
+ assert String.contains?(res.matching_search_term, "The [PF_HIGHLIGHT]quick[/PF_HIGHLIGHT] brown fox")
+ end
+
+ test "optionally lets you specify a limit" do
+ media_item_fixture(%{title: "The small gray dog"})
+
+ assert [_] = Media.search("dog", limit: 1)
+ end
+ end
+
describe "get_media_item!/1" do
test "it returns the media_item with given id" do
media_item = media_item_fixture()
diff --git a/test/pinchflat/tasks/source_tasks_test.exs b/test/pinchflat/tasks/source_tasks_test.exs
index 6917401..c2499ec 100644
--- a/test/pinchflat/tasks/source_tasks_test.exs
+++ b/test/pinchflat/tasks/source_tasks_test.exs
@@ -63,6 +63,7 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
assert Enum.count(media_items) == 3
assert ["video1", "video2", "video3"] == Enum.map(media_items, & &1.media_id)
assert ["Video 1", "Video 2", "Video 3"] == Enum.map(media_items, & &1.title)
+ assert ["desc1", "desc2", "desc3"] == Enum.map(media_items, & &1.description)
assert Enum.all?(media_items, fn mi -> mi.original_url end)
assert Enum.all?(media_items, fn %MediaItem{} -> true end)
end
@@ -110,7 +111,7 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
SourceTasks.index_media_items(source)
source = Repo.reload!(source)
- assert DateTime.diff(DateTime.utc_now(), source.last_indexed_at) < 1
+ assert DateTime.diff(DateTime.utc_now(), source.last_indexed_at) < 2
end
end
diff --git a/test/pinchflat/utils/string_utils_test.exs b/test/pinchflat/utils/string_utils_test.exs
index d77e468..51172e3 100644
--- a/test/pinchflat/utils/string_utils_test.exs
+++ b/test/pinchflat/utils/string_utils_test.exs
@@ -27,4 +27,18 @@ defmodule Pinchflat.Utils.StringUtilsTest do
assert String.length(StringUtils.random_string(64)) == 64
end
end
+
+ describe "truncate/2" do
+ test "truncates a string to the given length and adds ..." do
+ assert StringUtils.truncate("hello world", 5) == "hello..."
+ end
+
+ test "breaks on a word boundary" do
+ assert StringUtils.truncate("hello world", 7) == "hello..."
+ end
+
+ test "does not truncate a string shorter than the given length" do
+ assert StringUtils.truncate("hello", 10) == "hello"
+ end
+ end
end
diff --git a/test/pinchflat_web/controllers/search_controller_test.exs b/test/pinchflat_web/controllers/search_controller_test.exs
new file mode 100644
index 0000000..073ebc8
--- /dev/null
+++ b/test/pinchflat_web/controllers/search_controller_test.exs
@@ -0,0 +1,10 @@
+defmodule PinchflatWeb.SearchControllerTest do
+ use PinchflatWeb.ConnCase
+
+ describe "show search" do
+ test "renders the page", %{conn: conn} do
+ conn = get(conn, ~p"/search")
+ assert html_response(conn, 200) =~ "Results"
+ end
+ end
+end
diff --git a/test/support/fixtures/media_source_fixtures.ex b/test/support/fixtures/media_source_fixtures.ex
index 958854e..7784c54 100644
--- a/test/support/fixtures/media_source_fixtures.ex
+++ b/test/support/fixtures/media_source_fixtures.ex
@@ -36,19 +36,22 @@ defmodule Pinchflat.MediaSourceFixtures do
id: "video1",
title: "Video 1",
original_url: "https://example.com/video1",
- was_live: false
+ was_live: false,
+ description: "desc1"
},
%{
id: "video2",
title: "Video 2",
original_url: "https://example.com/video2",
- was_live: true
+ was_live: true,
+ description: "desc2"
},
%{
id: "video3",
title: "Video 3",
original_url: "https://example.com/video3",
- was_live: false
+ was_live: false,
+ description: "desc3"
}
]