diff --git a/assets/js/tabs.js b/assets/js/tabs.js index 964223d0..0d1cfa82 100644 --- a/assets/js/tabs.js +++ b/assets/js/tabs.js @@ -1,20 +1,20 @@ -window.setTabIndex = (index) => { - window.location.hash = `tab-${index}` +window.setTabByName = (tabName) => { + window.location.hash = `tab-${tabName}` - return index + return tabName } // The conditionals and currIndex stuff ensures that // the tab index is always set to 0 if the hash is empty // AND other hash values are ignored -window.getTabIndex = (currIndex) => { +window.getTabFromHash = (currentTabName, defaultTabName) => { if (window.location.hash === '' || window.location.hash === '#') { - return 0 + return defaultTabName } if (window.location.hash.startsWith('#tab-')) { - return parseInt(window.location.hash.replace('#tab-', '')) + return window.location.hash.replace('#tab-', '') } - return currIndex + return currentTabName } diff --git a/lib/pinchflat/downloading/downloading_helpers.ex b/lib/pinchflat/downloading/downloading_helpers.ex index d6f8966d..cb679a82 100644 --- a/lib/pinchflat/downloading/downloading_helpers.ex +++ b/lib/pinchflat/downloading/downloading_helpers.ex @@ -85,10 +85,16 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do """ def kickoff_redownload_for_existing_media(%Source{} = source) do MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.with_media_downloaded_at() - |> MediaQuery.where_download_not_prevented() - |> MediaQuery.where_not_culled() + |> MediaQuery.require_assoc(:media_profile) + |> where( + ^dynamic( + [m, s, mp], + ^MediaQuery.for_source(source) and + ^MediaQuery.downloaded() and + not (^MediaQuery.download_prevented()) and + not (^MediaQuery.culled()) + ) + ) |> Repo.all() |> Enum.map(&MediaDownloadWorker.kickoff_with_task/1) end diff --git a/lib/pinchflat/fast_indexing/fast_indexing_helpers.ex b/lib/pinchflat/fast_indexing/fast_indexing_helpers.ex index 366357fb..684ef49c 100644 --- a/lib/pinchflat/fast_indexing/fast_indexing_helpers.ex +++ b/lib/pinchflat/fast_indexing/fast_indexing_helpers.ex @@ -49,8 +49,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do defp list_media_items_by_media_id_for(source, media_ids) do MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.with_media_ids(media_ids) + |> where(^dynamic([mi], ^MediaQuery.for_source(source) and mi.media_id in ^media_ids)) |> Repo.all() end diff --git a/lib/pinchflat/lifecycle/notifications/source_notifications.ex b/lib/pinchflat/lifecycle/notifications/source_notifications.ex index 29a13967..e6e4c048 100644 --- a/lib/pinchflat/lifecycle/notifications/source_notifications.ex +++ b/lib/pinchflat/lifecycle/notifications/source_notifications.ex @@ -63,15 +63,14 @@ defmodule Pinchflat.Lifecycle.Notifications.SourceNotifications do defp pending_media_item_count(source) do MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.where_pending_download() + |> MediaQuery.require_assoc(:media_profile) + |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending())) |> Repo.aggregate(:count) end defp downloaded_media_item_count(source) do MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.with_media_filepath() + |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded())) |> Repo.aggregate(:count) end diff --git a/lib/pinchflat/media/media.ex b/lib/pinchflat/media/media.ex index c5e9a381..b8a35fa9 100644 --- a/lib/pinchflat/media/media.ex +++ b/lib/pinchflat/media/media.ex @@ -32,9 +32,8 @@ defmodule Pinchflat.Media do """ def list_cullable_media_items do MediaQuery.new() - |> MediaQuery.with_media_filepath() - |> MediaQuery.where_past_retention_period() - |> MediaQuery.where_culling_not_prevented() + |> MediaQuery.require_assoc(:source) + |> where(^MediaQuery.cullable()) |> Repo.all() end @@ -54,18 +53,14 @@ defmodule Pinchflat.Media do """ def list_redownloadable_media_items do MediaQuery.new() - |> MediaQuery.with_media_downloaded_at() - |> MediaQuery.where_download_not_prevented() - |> MediaQuery.where_not_culled() - |> MediaQuery.where_media_not_redownloaded() - |> MediaQuery.where_past_redownload_delay() + |> MediaQuery.require_assoc(:media_profile) + |> where(^MediaQuery.redownloadable()) |> Repo.all() end @doc """ Returns a list of pending media_items for a given source, where - pending means the `media_filepath` is `nil` AND the media_item - matches satisfies `MediaQuery.where_pending_download`. You + pending means the media_item satisfies `MediaQuery.pending`. You should really check out that function if you need to know more because it has a lot going on. @@ -73,15 +68,14 @@ defmodule Pinchflat.Media do """ def list_pending_media_items_for(%Source{} = source) do MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.where_pending_download() + |> MediaQuery.require_assoc(:media_profile) + |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending())) |> Repo.all() end @doc """ For a given media_item, tells you if it is pending download. This is defined as - the media_item having a `media_filepath` of `nil` and matching the format selection - rules of the parent media_profile. + the media_item satisfying `MediaQuery.pending` which you should really check out. Intentionally does not take the `download_media` setting of the source into account. @@ -91,8 +85,8 @@ defmodule Pinchflat.Media do media_item = Repo.preload(media_item, source: :media_profile) MediaQuery.new() - |> MediaQuery.with_id(media_item.id) - |> MediaQuery.where_pending_download() + |> MediaQuery.require_assoc(:media_profile) + |> where(^dynamic([m, s, mp], m.id == ^media_item.id and ^MediaQuery.pending())) |> Repo.exists?() end diff --git a/lib/pinchflat/media/media_item.ex b/lib/pinchflat/media/media_item.ex index 7018e39b..eab0ef5a 100644 --- a/lib/pinchflat/media/media_item.ex +++ b/lib/pinchflat/media/media_item.ex @@ -142,8 +142,7 @@ defmodule Pinchflat.Media.MediaItem do current_max = MediaQuery.new() - |> MediaQuery.for_source(source_id) - |> MediaQuery.where_uploaded_on_date(changes.upload_date) + |> where(^dynamic([mi], mi.upload_date == ^changes.upload_date and ^MediaQuery.for_source(source))) |> Repo.aggregate(aggregator, :upload_date_index) case current_max do diff --git a/lib/pinchflat/media/media_query.ex b/lib/pinchflat/media/media_query.ex index 0ae4777b..c3a1a652 100644 --- a/lib/pinchflat/media/media_query.ex +++ b/lib/pinchflat/media/media_query.ex @@ -23,51 +23,67 @@ defmodule Pinchflat.Media.MediaQuery do end end - # Prefixes: - # - for_* - belonging to a certain record - # - join_* - for joining on a certain record - # - with_*, where_* - for filtering based on full, concrete attributes - # - matching_* - for filtering based on partial attributes (e.g. LIKE, regex, full-text search) - # - # Suffixes: - # - _for - the arg passed is an association record - - # NOTE: that dyanmic query approach kinda rocked - should refactor in future - def new do MediaItem end - def for_source(query, source_id) when is_integer(source_id) do - where(query, [mi], mi.source_id == ^source_id) + def for_source(source_id) when is_integer(source_id), do: dynamic([mi], mi.source_id == ^source_id) + def for_source(source), do: dynamic([mi], mi.source_id == ^source.id) + + def downloaded, do: dynamic([mi], not is_nil(mi.media_filepath)) + def download_prevented, do: dynamic([mi], mi.prevent_download == true) + def culling_prevented, do: dynamic([mi], mi.prevent_culling == true) + def culled, do: dynamic([mi], not is_nil(mi.culled_at)) + def redownloaded, do: dynamic([mi], not is_nil(mi.media_redownloaded_at)) + + def upload_date_after_source_cutoff do + dynamic([mi, source], is_nil(source.download_cutoff_date) or mi.upload_date >= source.download_cutoff_date) end - def for_source(query, source) do - where(query, [mi], mi.source_id == ^source.id) + def format_matching_profile_preference do + dynamic( + [mi, source, media_profile], + fragment(""" + CASE + WHEN shorts_behaviour = 'only' AND livestream_behaviour = 'only' THEN + livestream = true OR short_form_content = true + WHEN shorts_behaviour = 'only' THEN + short_form_content = true + WHEN livestream_behaviour = 'only' THEN + livestream = true + WHEN shorts_behaviour = 'exclude' AND livestream_behaviour = 'exclude' THEN + short_form_content = false AND livestream = false + WHEN shorts_behaviour = 'exclude' THEN + short_form_content = false + WHEN livestream_behaviour = 'exclude' THEN + livestream = false + ELSE + true + END + """) + ) end - def join_sources(query) do - from(mi in query, join: s in assoc(mi, :source), as: :sources) + def matches_source_title_regex do + dynamic( + [mi, source], + is_nil(source.title_filter_regex) or fragment("regexp_like(?, ?)", mi.title, source.title_filter_regex) + ) end - def where_past_retention_period(query) do - query - |> require_assoc(:source) - |> where( + def past_retention_period do + dynamic( [mi, source], fragment(""" - IFNULL(retention_period_days, 0) > 0 AND - DATETIME('now', '-' || retention_period_days || ' day') > media_downloaded_at + IFNULL(retention_period_days, 0) > 0 AND + DATETIME('now', '-' || retention_period_days || ' day') > media_downloaded_at """) ) end - def where_past_redownload_delay(query) do - query - |> require_assoc(:source) - |> require_assoc(:media_profile) - |> where( - [_mi, _source, _media_profile], + def past_redownload_delay do + dynamic( + [mi, source, media_profile], # Returns media items where the upload_date is at least redownload_delay_days ago AND # downloaded_at minus the redownload_delay_days is before the upload date fragment(""" @@ -78,61 +94,57 @@ defmodule Pinchflat.Media.MediaQuery do ) end - def where_culling_not_prevented(query) do - where(query, [mi], mi.prevent_culling == false) - end - - def where_not_culled(query) do - where(query, [mi], is_nil(mi.culled_at)) - end - - def where_media_not_redownloaded(query) do - where(query, [mi], is_nil(mi.media_redownloaded_at)) - end - - def with_id(query, id) do - where(query, [mi], mi.id == ^id) - end - - def with_media_ids(query, media_ids) do - where(query, [mi], mi.media_id in ^media_ids) - end - - def with_media_downloaded_at(query) do - where(query, [mi], not is_nil(mi.media_downloaded_at)) - end - - def with_media_filepath(query) do - where(query, [mi], not is_nil(mi.media_filepath)) + def cullable do + dynamic( + [mi, source], + ^downloaded() and + ^past_retention_period() and + not (^culling_prevented()) + ) end - def with_no_media_filepath(query) do - where(query, [mi], is_nil(mi.media_filepath)) + def pending do + dynamic( + [mi], + not (^downloaded()) and + not (^download_prevented()) and + ^upload_date_after_source_cutoff() and + ^format_matching_profile_preference() and + ^matches_source_title_regex() + ) end - def with_upload_date_after_source_cutoff(query) do - query - |> require_assoc(:source) - |> where([mi, source], is_nil(source.download_cutoff_date) or mi.upload_date >= source.download_cutoff_date) + def redownloadable do + dynamic( + [mi, source], + ^downloaded() and + not (^download_prevented()) and + not (^culled()) and + not (^redownloaded()) and + ^past_redownload_delay() + ) end - def where_uploaded_on_date(query, date) do - where(query, [mi], mi.upload_date == ^date) + def require_assoc(query, identifier) do + if has_named_binding?(query, identifier) do + query + else + do_require_assoc(query, identifier) + end end - def where_download_not_prevented(query) do - where(query, [mi], mi.prevent_download == false) + defp do_require_assoc(query, :source) do + from(mi in query, join: s in assoc(mi, :source), as: :source) end - def matching_source_title_regex(query) do + defp do_require_assoc(query, :media_profile) do query |> require_assoc(:source) - |> where( - [mi, source], - is_nil(source.title_filter_regex) or fragment("regexp_like(?, ?)", mi.title, source.title_filter_regex) - ) + |> join(:inner, [mi, source], mp in assoc(source, :media_profile), as: :media_profile) end + # This needs to be a non-dynamic query because it alone should control things like + # ordering and `snippets` for full-text search def matching_search_term(query, nil), do: query def matching_search_term(query, term) do @@ -150,62 +162,4 @@ defmodule Pinchflat.Media.MediaQuery do order_by: [desc: fragment("rank")] ) end - - def with_format_matching_profile_preference(query) do - query - |> require_assoc(:media_profile) - |> where( - fragment(""" - CASE - WHEN shorts_behaviour = 'only' AND livestream_behaviour = 'only' THEN - livestream = true OR short_form_content = true - WHEN shorts_behaviour = 'only' THEN - short_form_content = true - WHEN livestream_behaviour = 'only' THEN - livestream = true - WHEN shorts_behaviour = 'exclude' AND livestream_behaviour = 'exclude' THEN - short_form_content = false AND livestream = false - WHEN shorts_behaviour = 'exclude' THEN - short_form_content = false - WHEN livestream_behaviour = 'exclude' THEN - livestream = false - ELSE - true - END - """) - ) - end - - def where_pending_download(query) do - query - |> where_download_not_prevented() - |> with_no_media_filepath() - |> with_upload_date_after_source_cutoff() - |> with_format_matching_profile_preference() - |> matching_source_title_regex() - end - - def where_pending_or_downloaded(query) do - query - |> where_pending_download() - |> or_where([mi], not is_nil(mi.media_downloaded_at)) - end - - defp require_assoc(query, identifier) do - if has_named_binding?(query, identifier) do - query - else - do_require_assoc(query, identifier) - end - end - - defp do_require_assoc(query, :source) do - from(mi in query, join: s in assoc(mi, :source), as: :source) - end - - defp do_require_assoc(query, :media_profile) do - query - |> require_assoc(:source) - |> join(:inner, [mi, source], mp in assoc(source, :media_profile), as: :media_profile) - end end diff --git a/lib/pinchflat/podcasts/podcast_helpers.ex b/lib/pinchflat/podcasts/podcast_helpers.ex index 013ee840..a40e63bf 100644 --- a/lib/pinchflat/podcasts/podcast_helpers.ex +++ b/lib/pinchflat/podcasts/podcast_helpers.ex @@ -27,8 +27,7 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do limit = Keyword.get(opts, :limit, 1_000) MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.with_media_filepath() + |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded())) |> order_by(desc: :upload_date) |> Repo.maybe_limit(limit) |> Repo.all() diff --git a/lib/pinchflat/sources/sources.ex b/lib/pinchflat/sources/sources.ex index 9fd66377..6cd45c1d 100644 --- a/lib/pinchflat/sources/sources.ex +++ b/lib/pinchflat/sources/sources.ex @@ -127,7 +127,7 @@ defmodule Pinchflat.Sources do Tasks.delete_tasks_for(source) MediaQuery.new() - |> MediaQuery.for_source(source) + |> where(^MediaQuery.for_source(source)) |> Repo.all() |> Enum.each(fn media_item -> Media.delete_media_item(media_item, delete_files: delete_files) diff --git a/lib/pinchflat/sources/sources_query.ex b/lib/pinchflat/sources/sources_query.ex index 81bd4498..850eef63 100644 --- a/lib/pinchflat/sources/sources_query.ex +++ b/lib/pinchflat/sources/sources_query.ex @@ -12,20 +12,26 @@ defmodule Pinchflat.Sources.SourcesQuery do alias Pinchflat.Sources.Source - # Prefixes: - # - for_* - belonging to a certain record - # - join_* - for joining on a certain record - # - with_* - for filtering based on full, concrete attributes - # - matching_* - for filtering based on partial attributes (e.g. LIKE, regex, full-text search) - # - # Suffixes: - # - _for - the arg passed is an association record + # This allows the module to be aliased and query methods to be used + # all in one go + # usage: use Pinchflat.Sources.SourcesQuery + defmacro __using__(_opts) do + quote do + import Ecto.Query, warn: false + + alias unquote(__MODULE__) + end + end def new do Source end - def for_media_profile(query, media_profile) do - where(query, [s], s.media_profile_id == ^media_profile.id) + def for_media_profile(media_profile_id) when is_integer(media_profile_id) do + dynamic([s], s.media_profile_id == ^media_profile_id) + end + + def for_media_profile(media_profile) do + dynamic([s], s.media_profile_id == ^media_profile.id) end end diff --git a/lib/pinchflat_web/components/custom_components/tab_components.ex b/lib/pinchflat_web/components/custom_components/tab_components.ex index fda08bd8..56cfe2a1 100644 --- a/lib/pinchflat_web/components/custom_components/tab_components.ex +++ b/lib/pinchflat_web/components/custom_components/tab_components.ex @@ -6,25 +6,32 @@ defmodule PinchflatWeb.CustomComponents.TabComponents do Takes a list of tabs and renders them in a tabbed layout. """ slot :tab, required: true do + attr :id, :string, required: true attr :title, :string, required: true end slot :tab_append, required: false def tabbed_layout(assigns) do + assigns = Map.put(assigns, :first_tab_id, hd(assigns.tab).id) + ~H"""
-
+
<%= render_slot(tab) %>
diff --git a/lib/pinchflat_web/components/custom_components/table_components.ex b/lib/pinchflat_web/components/custom_components/table_components.ex index fa283d38..166a3136 100644 --- a/lib/pinchflat_web/components/custom_components/table_components.ex +++ b/lib/pinchflat_web/components/custom_components/table_components.ex @@ -36,7 +36,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do - + - <:tab title="Media"> + <:tab title="Media" id="media">
<%= if media_file_exists?(@media_item) do %>
@@ -52,7 +52,7 @@
- <:tab title="Tasks"> + <:tab title="Tasks" id="tasks"> <%= if match?([_|_], @media_item.tasks) do %> <.table rows={@media_item.tasks} table_class="text-black dark:text-white"> <:col :let={task} label="Worker"> diff --git a/lib/pinchflat_web/controllers/media_profiles/media_profile_controller.ex b/lib/pinchflat_web/controllers/media_profiles/media_profile_controller.ex index cc638ec6..22a49160 100644 --- a/lib/pinchflat_web/controllers/media_profiles/media_profile_controller.ex +++ b/lib/pinchflat_web/controllers/media_profiles/media_profile_controller.ex @@ -1,11 +1,9 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do use PinchflatWeb, :controller - - import Ecto.Query, warn: false + use Pinchflat.Sources.SourcesQuery alias Pinchflat.Repo alias Pinchflat.Profiles - alias Pinchflat.Sources.SourcesQuery alias Pinchflat.Profiles.MediaProfile def index(conn, _params) do @@ -43,7 +41,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do sources = SourcesQuery.new() - |> SourcesQuery.for_media_profile(media_profile) + |> where(^SourcesQuery.for_media_profile(media_profile)) |> order_by(asc: :custom_name) |> Repo.all() diff --git a/lib/pinchflat_web/controllers/media_profiles/media_profile_html/index.html.heex b/lib/pinchflat_web/controllers/media_profiles/media_profile_html/index.html.heex index f5e1ae64..e3a2deea 100644 --- a/lib/pinchflat_web/controllers/media_profiles/media_profile_html/index.html.heex +++ b/lib/pinchflat_web/controllers/media_profiles/media_profile_html/index.html.heex @@ -23,9 +23,8 @@ <:col :let={media_profile} label="Preferred Resolution"> <%= media_profile.preferred_resolution %> - <:col :let={media_profile} label="" class="flex place-content-evenly"> - <.icon_link href={~p"/media_profiles/#{media_profile.id}"} icon="hero-eye" class="mx-1" /> - <.icon_link href={~p"/media_profiles/#{media_profile.id}/edit"} icon="hero-pencil-square" class="mx-1" /> + <:col :let={media_profile} label="" class="flex justify-end"> + <.icon_link href={~p"/media_profiles/#{media_profile.id}/edit"} icon="hero-pencil-square" class="mr-4" />
diff --git a/lib/pinchflat_web/controllers/media_profiles/media_profile_html/show.html.heex b/lib/pinchflat_web/controllers/media_profiles/media_profile_html/show.html.heex index 7a33fe88..ef0629f2 100644 --- a/lib/pinchflat_web/controllers/media_profiles/media_profile_html/show.html.heex +++ b/lib/pinchflat_web/controllers/media_profiles/media_profile_html/show.html.heex @@ -23,13 +23,13 @@ <.actions_dropdown media_profile={@media_profile} /> - <:tab title="Media Profile"> + <:tab title="Media Profile" id="media-profile">

Raw Attributes

<.list_items_from_map map={Map.from_struct(@media_profile)} />
- <:tab title="Sources"> + <:tab title="Sources" id="sources"> <.table rows={@sources} table_class="text-black dark:text-white"> <:col :let={source} label="Name"> <.subtle_link href={~p"/sources/#{source.id}"}> @@ -40,9 +40,8 @@ <:col :let={source} label="Should Download?"> <.icon name={if source.download_media, do: "hero-check", else: "hero-x-mark"} /> - <:col :let={source} label="" class="flex place-content-evenly"> - <.icon_link href={~p"/sources/#{source.id}"} icon="hero-eye" class="mx-1" /> - <.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" /> + <:col :let={source} label="" class="flex justify-end"> + <.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mr-4" /> diff --git a/lib/pinchflat_web/controllers/pages/page_controller.ex b/lib/pinchflat_web/controllers/pages/page_controller.ex index a6e36dad..60dd2d85 100644 --- a/lib/pinchflat_web/controllers/pages/page_controller.ex +++ b/lib/pinchflat_web/controllers/pages/page_controller.ex @@ -26,7 +26,7 @@ defmodule PinchflatWeb.Pages.PageController do source_count: Repo.aggregate(Source, :count, :id), media_item_count: MediaQuery.new() - |> MediaQuery.with_media_downloaded_at() + |> where(^MediaQuery.downloaded()) |> Repo.aggregate(:count, :id) ) end diff --git a/lib/pinchflat_web/controllers/pages/page_html/history_table_live.ex b/lib/pinchflat_web/controllers/pages/page_html/history_table_live.ex index 018bcef4..6ec85aea 100644 --- a/lib/pinchflat_web/controllers/pages/page_html/history_table_live.ex +++ b/lib/pinchflat_web/controllers/pages/page_html/history_table_live.ex @@ -97,7 +97,8 @@ defmodule Pinchflat.Pages.HistoryTableLive do defp generate_base_query do MediaQuery.new() - |> MediaQuery.where_pending_or_downloaded() + |> MediaQuery.require_assoc(:media_profile) + |> where(^dynamic(^MediaQuery.downloaded() or ^MediaQuery.pending())) |> order_by(desc: :id) end diff --git a/lib/pinchflat_web/controllers/podcasts/podcast_controller.ex b/lib/pinchflat_web/controllers/podcasts/podcast_controller.ex index cf7c7fce..d69e4f6d 100644 --- a/lib/pinchflat_web/controllers/podcasts/podcast_controller.ex +++ b/lib/pinchflat_web/controllers/podcasts/podcast_controller.ex @@ -26,8 +26,7 @@ defmodule PinchflatWeb.Podcasts.PodcastController do # if the source doesn't have any usable images media_items = MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.with_media_filepath() + |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded())) |> Repo.maybe_limit(1) |> Repo.all() diff --git a/lib/pinchflat_web/controllers/sources/source_controller.ex b/lib/pinchflat_web/controllers/sources/source_controller.ex index 3b9c5b2a..2df192f2 100644 --- a/lib/pinchflat_web/controllers/sources/source_controller.ex +++ b/lib/pinchflat_web/controllers/sources/source_controller.ex @@ -1,25 +1,44 @@ defmodule PinchflatWeb.Sources.SourceController do use PinchflatWeb, :controller - - import Ecto.Query, warn: false + use Pinchflat.Media.MediaQuery alias Pinchflat.Repo alias Pinchflat.Tasks alias Pinchflat.Sources alias Pinchflat.Sources.Source + alias Pinchflat.Media.MediaItem alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.SlowIndexing.SlowIndexingHelpers alias Pinchflat.Metadata.SourceMetadataStorageWorker def index(conn, _params) do - sources = - Source - |> order_by(asc: :custom_name) - |> Repo.all() - |> Repo.preload(:media_profile) - - render(conn, :index, sources: sources) + source_query = + from s in Source, + as: :source, + inner_join: mp in assoc(s, :media_profile), + preload: [media_profile: mp], + order_by: [asc: s.custom_name], + select: map(s, ^Source.__schema__(:fields)), + select_merge: %{ + downloaded_count: + subquery( + from m in MediaItem, + where: m.source_id == parent_as(:source).id, + where: ^MediaQuery.downloaded(), + select: count(m.id) + ), + pending_count: + subquery( + from m in MediaItem, + join: s in assoc(m, :source), + where: m.source_id == parent_as(:source).id, + where: ^MediaQuery.pending(), + select: count(m.id) + ) + } + + render(conn, :index, sources: Repo.all(source_query)) end def new(conn, _params) do diff --git a/lib/pinchflat_web/controllers/sources/source_html/index.html.heex b/lib/pinchflat_web/controllers/sources/source_html/index.html.heex index c912cfb8..a064d535 100644 --- a/lib/pinchflat_web/controllers/sources/source_html/index.html.heex +++ b/lib/pinchflat_web/controllers/sources/source_html/index.html.heex @@ -15,13 +15,12 @@ <.table rows={@sources} table_class="text-black dark:text-white"> <:col :let={source} label="Name"> <.subtle_link href={~p"/sources/#{source.id}"}> - <%= source.custom_name || source.collection_name %> + <%= StringUtils.truncate(source.custom_name || source.collection_name, 35) %> <:col :let={source} label="Type"><%= source.collection_type %> - <:col :let={source} label="Should Download?"> - <.icon name={if source.download_media, do: "hero-check", else: "hero-x-mark"} /> - + <:col :let={source} label="Pending"><%= source.pending_count %> + <:col :let={source} label="Downloaded"><%= source.downloaded_count %> <:col :let={source} label="Retention"> <%= if source.retention_period_days && source.retention_period_days > 0 do %> <%= source.retention_period_days %> day(s) @@ -35,7 +34,6 @@ <:col :let={source} label="" class="flex place-content-evenly"> - <.icon_link href={~p"/sources/#{source.id}"} icon="hero-eye" class="mx-1" /> <.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" /> diff --git a/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex b/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex index 0b986874..7ffac3c3 100644 --- a/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex +++ b/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex @@ -30,9 +30,8 @@ defmodule Pinchflat.Sources.MediaItemTableLive do <%= StringUtils.truncate(media_item.title, 50) %> - <:col :let={media_item} label="" class="flex place-content-evenly"> - <.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"} icon="hero-eye" class="mx-1" /> - <.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mx-1" /> + <:col :let={media_item} label="" class="flex justify-end"> + <.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" />
@@ -86,15 +85,14 @@ defmodule Pinchflat.Sources.MediaItemTableLive do defp generate_base_query(source, "pending") do MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.where_pending_download() + |> MediaQuery.require_assoc(:media_profile) + |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending())) |> order_by(desc: :id) end defp generate_base_query(source, "downloaded") do MediaQuery.new() - |> MediaQuery.for_source(source) - |> MediaQuery.with_media_filepath() + |> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded())) |> order_by(desc: :id) end end diff --git a/lib/pinchflat_web/controllers/sources/source_html/show.html.heex b/lib/pinchflat_web/controllers/sources/source_html/show.html.heex index 92104c8b..00ad375b 100644 --- a/lib/pinchflat_web/controllers/sources/source_html/show.html.heex +++ b/lib/pinchflat_web/controllers/sources/source_html/show.html.heex @@ -23,7 +23,7 @@ <.actions_dropdown source={@source} conn={@conn} /> - <:tab title="Source"> + <:tab title="Source" id="source">

Raw Attributes

@@ -36,21 +36,21 @@ <.list_items_from_map map={Map.from_struct(@source)} />
- <:tab title="Pending Media"> + <:tab title="Pending Media" id="pending"> <%= live_render( @conn, Pinchflat.Sources.MediaItemTableLive, session: %{"source_id" => @source.id, "media_state" => "pending"} ) %> - <:tab title="Downloaded Media"> + <:tab title="Downloaded Media" id="downloaded"> <%= live_render( @conn, Pinchflat.Sources.MediaItemTableLive, session: %{"source_id" => @source.id, "media_state" => "downloaded"} ) %> - <:tab title="Pending Tasks"> + <:tab title="Pending Tasks" id="tasks"> <%= if match?([_|_], @pending_tasks) do %> <.table rows={@pending_tasks} table_class="text-black dark:text-white"> <:col :let={task} label="Worker"> diff --git a/mix.exs b/mix.exs index a1b60282..e74e6b16 100644 --- a/mix.exs +++ b/mix.exs @@ -48,6 +48,7 @@ defmodule Pinchflat.MixProject do [ {:phoenix, "~> 1.7.10"}, {:phoenix_ecto, "~> 4.4"}, + {:ecto, "~> 3.11.2"}, {:ecto_sql, "~> 3.10"}, {:ecto_sqlite3, ">= 0.0.0"}, {:ecto_sqlite3_extras, "~> 1.2.0"}, diff --git a/mix.lock b/mix.lock index 7fe77ff7..e986d1bd 100644 --- a/mix.lock +++ b/mix.lock @@ -12,7 +12,7 @@ "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dns_cluster": {:hex, :dns_cluster, "0.1.2", "3eb5be824c7888dadf9781018e1a5f1d3d1113b333c50bce90fb1b83df1015f2", [:mix], [], "hexpm", "7494272040f847637bbdb01bcdf4b871e82daf09b813e7d3cb3b84f112c6f2f8"}, - "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"}, "ecto_sqlite3_extras": {:hex, :ecto_sqlite3_extras, "1.2.2", "36e60b561a11441d15f26c791817999269fb578b985162207ebb08b04ca71e40", [:mix], [{:exqlite, ">= 0.13.2", [hex: :exqlite, repo: "hexpm", optional: false]}, {:table_rex, "~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "2b66ba7246bb4f7e39e2578acd4a0e4e4be54f60ff52d450a01be95eeb78ff1e"}, diff --git a/test/pinchflat/downloading/downloading_helpers_test.exs b/test/pinchflat/downloading/downloading_helpers_test.exs index 23c493d6..01b0518c 100644 --- a/test/pinchflat/downloading/downloading_helpers_test.exs +++ b/test/pinchflat/downloading/downloading_helpers_test.exs @@ -114,7 +114,7 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do describe "kickoff_redownload_for_existing_media/1" do test "enqueues a download job for each downloaded media item" do source = source_fixture() - media_item = media_item_fixture(source_id: source.id, media_downloaded_at: now()) + media_item = media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4") assert [{:ok, _}] = DownloadingHelpers.kickoff_redownload_for_existing_media(source) @@ -124,14 +124,14 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do test "doesn't enqueue jobs for media that should be ignored" do source = source_fixture() other_source = source_fixture() - _not_downloaded = media_item_fixture(source_id: source.id, media_downloaded_at: nil) - _other_source = media_item_fixture(source_id: other_source.id, media_downloaded_at: now()) + _not_downloaded = media_item_fixture(source_id: source.id, media_filepath: nil) + _other_source = media_item_fixture(source_id: other_source.id, media_filepath: "some/filepath.mp4") _download_prevented = - media_item_fixture(source_id: source.id, media_downloaded_at: now(), prevent_download: true) + media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", prevent_download: true) _culled = - media_item_fixture(source_id: source.id, media_downloaded_at: now(), culled_at: now()) + media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", culled_at: now()) assert [] = DownloadingHelpers.kickoff_redownload_for_existing_media(source) diff --git a/test/pinchflat/media_test.exs b/test/pinchflat/media_test.exs index 412b9e31..f98afb16 100644 --- a/test/pinchflat/media_test.exs +++ b/test/pinchflat/media_test.exs @@ -405,7 +405,9 @@ defmodule Pinchflat.MediaTest do describe "list_pending_media_items_for/1" do test "it returns pending without a filepath for a given source" do source = source_fixture() + other_source = source_fixture() media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil}) + _other_media_item = media_item_fixture(%{source_id: other_source.id, media_filepath: nil}) assert Media.list_pending_media_items_for(source) == [media_item] end diff --git a/test/support/fixtures/media_fixtures.ex b/test/support/fixtures/media_fixtures.ex index 54dedcaa..53971a07 100644 --- a/test/support/fixtures/media_fixtures.ex +++ b/test/support/fixtures/media_fixtures.ex @@ -17,7 +17,7 @@ defmodule Pinchflat.MediaFixtures do attrs |> Enum.into(%{ media_id: media_id, - title: Faker.Commerce.product_name(), + title: Faker.Commerce.product_name() <> " #{media_id}", original_url: "https://www.youtube.com/watch?v=#{media_id}", livestream: false, short_form_content: false,