[Enhancement] Improve ordering of models (#164)

* [WIP] ordering app queries

* Refactored media queries to be self-contained
This commit is contained in:
Kieran
2024-04-03 17:26:46 -07:00
committed by GitHub
parent 4b12764f45
commit e55bcaddd0
10 changed files with 160 additions and 136 deletions
+4 -33
View File
@@ -30,7 +30,6 @@ defmodule Pinchflat.Media do
"""
def list_cullable_media_items do
MediaQuery.new()
|> MediaQuery.join_sources()
|> MediaQuery.with_media_filepath()
|> MediaQuery.with_passed_retention_period()
|> MediaQuery.with_no_culling_prevention()
@@ -42,34 +41,15 @@ defmodule Pinchflat.Media do
pending means the `media_filepath` is `nil` AND the media_item
matches the format selection rules of the parent media_profile.
See `matching_download_criteria_for` but tl;dr is it _may_ filter based
See `where_pending` but tl;dr is it _may_ filter based
on shorts livestreams depending on the media_profile settings.
Returns [%MediaItem{}, ...].
"""
def list_pending_media_items_for(%Source{} = source, opts \\ []) do
limit = Keyword.get(opts, :limit, nil)
source = Repo.preload(source, :media_profile)
def list_pending_media_items_for(%Source{} = source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> matching_download_criteria_for(source)
|> Repo.maybe_limit(limit)
|> Repo.all()
end
@doc """
Returns a list of downloaded media_items for a given source.
Returns [%MediaItem{}, ...].
"""
def list_downloaded_media_items_for(%Source{} = source, opts \\ []) do
limit = Keyword.get(opts, :limit, nil)
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> Repo.maybe_limit(limit)
|> MediaQuery.with_media_pending_download()
|> Repo.all()
end
@@ -87,7 +67,7 @@ defmodule Pinchflat.Media do
MediaQuery.new()
|> MediaQuery.with_id(media_item.id)
|> matching_download_criteria_for(media_item.source)
|> MediaQuery.with_media_pending_download()
|> Repo.exists?()
end
@@ -234,13 +214,4 @@ defmodule Pinchflat.Media do
|> Enum.filter(&is_binary/1)
|> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1)
end
defp matching_download_criteria_for(query, source_with_preloads) do
query
|> MediaQuery.with_no_prevented_download()
|> MediaQuery.with_no_media_filepath()
|> MediaQuery.with_upload_date_after(source_with_preloads.download_cutoff_date)
|> MediaQuery.with_format_preference(source_with_preloads.media_profile)
|> MediaQuery.matching_title_regex(source_with_preloads.title_filter_regex)
end
end
+66 -54
View File
@@ -3,13 +3,10 @@ defmodule Pinchflat.Media.MediaQuery do
Query helpers for the Media context.
These methods are made to be one-ish liners used
to compose queries for media items. Each method should
strive to do _one_ thing. These don't need to be tested
as they are just building blocks for other functionality
to compose queries. Each method should strive to do
_one_ thing. These don't need to be tested as
they are just building blocks for other functionality
which, itself, will be tested.
ALSO, this is me trying something new. If I like it,
I'll refactor other contexts to use this pattern.
"""
import Ecto.Query, warn: false
@@ -37,13 +34,14 @@ defmodule Pinchflat.Media.MediaQuery do
end
def with_passed_retention_period(query) do
where(
query,
[mi, sources],
query
|> require_assoc(:source)
|> where(
[mi, source],
fragment(
"IFNULL(?, 0) > 0 AND DATETIME('now', '-' || ? || ' day') > ?",
sources.retention_period_days,
sources.retention_period_days,
source.retention_period_days,
source.retention_period_days,
mi.media_downloaded_at
)
)
@@ -69,20 +67,23 @@ defmodule Pinchflat.Media.MediaQuery do
where(query, [mi], is_nil(mi.media_filepath))
end
def with_upload_date_after(query, nil), do: query
def with_upload_date_after(query, date) do
where(query, [mi], mi.upload_date >= ^date)
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)
end
def with_no_prevented_download(query) do
where(query, [mi], mi.prevent_download == false)
end
def matching_title_regex(query, nil), do: query
def matching_title_regex(query, regex) do
where(query, [mi], fragment("regexp_like(?, ?)", mi.title, ^regex))
def matching_source_title_regex(query) do
query
|> require_assoc(:source)
|> where(
[mi, source],
is_nil(source.title_filter_regex) or fragment("regexp_like(?, ?)", mi.title, source.title_filter_regex)
)
end
def matching_search_term(query, nil), do: query
@@ -103,44 +104,55 @@ defmodule Pinchflat.Media.MediaQuery do
)
end
# NOTE: this method breaks the contract set by other methods in that it
# takes a media_profile struct instead of taking just the attributes it
# cares about. Consider refactoring but low priority.
def with_format_preference(query, media_profile) do
mapped_struct = Map.from_struct(media_profile)
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
finders =
Enum.reduce(mapped_struct, dynamic(true), fn attr, dynamic ->
case {attr, media_profile} do
{{:shorts_behaviour, :only}, %{livestream_behaviour: :only}} ->
dynamic(
[mi],
^dynamic and (mi.livestream == true or mi.short_form_content == true)
)
def with_media_pending_download(query) do
query
|> with_no_prevented_download()
|> with_no_media_filepath()
|> with_upload_date_after_source_cutoff()
|> with_format_matching_profile_preference()
|> matching_source_title_regex()
end
# Technically redundant, but makes the other clauses easier to parse
# (redundant because this condition is the same as the condition above, just flipped)
{{:livestream_behaviour, :only}, %{shorts_behaviour: :only}} ->
dynamic
defp require_assoc(query, identifier) do
if has_named_binding?(query, identifier) do
query
else
do_require_assoc(query, identifier)
end
end
{{:shorts_behaviour, :only}, _} ->
dynamic([mi], ^dynamic and mi.short_form_content == true)
defp do_require_assoc(query, :source) do
from(mi in query, join: s in assoc(mi, :source), as: :source)
end
{{:livestream_behaviour, :only}, _} ->
dynamic([mi], ^dynamic and mi.livestream == true)
{{:shorts_behaviour, :exclude}, %{livestream_behaviour: lb}} when lb != :only ->
dynamic([mi], ^dynamic and mi.short_form_content == false)
{{:livestream_behaviour, :exclude}, %{shorts_behaviour: sb}} when sb != :only ->
# return records with livestream: false
dynamic([mi], ^dynamic and mi.livestream == false)
_ ->
dynamic
end
end)
where(query, ^finders)
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
+6 -3
View File
@@ -5,7 +5,7 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
"""
alias Pinchflat.Repo
alias Pinchflat.Media
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Metadata.SourceMetadata
@@ -25,8 +25,11 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
def persisted_media_items_for(source, opts \\ []) do
limit = Keyword.get(opts, :limit, 500)
source
|> Media.list_downloaded_media_items_for(limit: limit)
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> Repo.maybe_limit(limit)
|> Repo.all()
|> Enum.filter(fn media_item -> File.exists?(media_item.media_filepath) end)
end
+31
View File
@@ -0,0 +1,31 @@
defmodule Pinchflat.Sources.SourcesQuery do
@moduledoc """
Query helpers for the Sources context.
These methods are made to be one-ish liners used
to compose queries. Each method should strive to do
_one_ thing. These don't need to be tested as
they are just building blocks for other functionality
which, itself, will be tested.
"""
import Ecto.Query, warn: false
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
def new do
Source
end
def for_media_profile(query, media_profile) do
where(query, [s], s.media_profile_id == ^media_profile.id)
end
end