Skip to content

Commit

Permalink
[Enhancement] Filter media based on min and/or max duration (#356)
Browse files Browse the repository at this point in the history
* Added duration limits to source model

* Added duration limits to source form

* Added validation for min/max amounts

* Added duration checks to pending query

* Moved min/max filters up in source form

* Removed debugger
  • Loading branch information
kieraneglin authored Aug 14, 2024
1 parent 8e9f02c commit af8235c
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 1 deletion.
11 changes: 10 additions & 1 deletion lib/pinchflat/media/media_query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ defmodule Pinchflat.Media.MediaQuery do
)
end

def meets_min_and_max_duration do
dynamic(
[mi, source],
(is_nil(source.min_duration_seconds) or fragment("duration_seconds >= ?", source.min_duration_seconds)) and
(is_nil(source.max_duration_seconds) or fragment("duration_seconds <= ?", source.max_duration_seconds))
)
end

def past_retention_period do
dynamic(
[mi, source],
Expand Down Expand Up @@ -123,7 +131,8 @@ defmodule Pinchflat.Media.MediaQuery do
not (^download_prevented()) and
^upload_date_after_source_cutoff() and
^format_matching_profile_preference() and
^matches_source_title_regex()
^matches_source_title_regex() and
^meets_min_and_max_duration()
)
end

Expand Down
17 changes: 17 additions & 0 deletions lib/pinchflat/sources/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ defmodule Pinchflat.Sources.Source do
media_profile_id
output_path_template_override
marked_for_deletion_at
min_duration_seconds
max_duration_seconds
)a

# Expensive API calls are made when a source is inserted/updated so
Expand Down Expand Up @@ -84,6 +86,9 @@ defmodule Pinchflat.Sources.Source do
field :title_filter_regex, :string
field :output_path_template_override, :string

field :min_duration_seconds, :integer
field :max_duration_seconds, :integer

field :series_directory, :string
field :nfo_filepath, :string
field :poster_filepath, :string
Expand Down Expand Up @@ -118,6 +123,7 @@ defmodule Pinchflat.Sources.Source do
|> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end)
|> validate_required(required_fields)
|> validate_title_regex()
|> validate_min_and_max_durations()
|> validate_number(:retention_period_days, greater_than_or_equal_to: 0)
# Ensures it ends with `.{{ ext }}` or `.%(ext)s` or similar (with a little wiggle room)
|> validate_format(:output_path_template_override, MediaProfile.ext_regex(), message: "must end with .{{ ext }}")
Expand Down Expand Up @@ -164,6 +170,17 @@ defmodule Pinchflat.Sources.Source do

defp validate_title_regex(changeset), do: changeset

defp validate_min_and_max_durations(changeset) do
min_duration = get_change(changeset, :min_duration_seconds)
max_duration = get_change(changeset, :max_duration_seconds)

case {min_duration, max_duration} do
{min, max} when is_nil(min) or is_nil(max) -> changeset
{min, max} when min >= max -> add_error(changeset, :max_duration_seconds, "must be greater than minumum duration")
_ -> changeset
end
end

defimpl Jason.Encoder, for: Source do
def encode(value, opts) do
value
Expand Down
1 change: 1 addition & 0 deletions lib/pinchflat_web/controllers/sources/source_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ defmodule PinchflatWeb.Sources.SourceController do
| id: nil,
uuid: nil,
custom_name: nil,
description: nil,
collection_name: nil,
collection_id: nil,
collection_type: nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@
help="Uses your YouTube cookies for this source (if configured). Used for downloading private playlists and videos. See docs for important details"
/>

<section x-show="advancedMode">
<.input
field={f[:min_duration_seconds]}
type="number"
label="Minimum Duration (seconds)"
min="0"
help="Minimum duration of the media to be downloaded. Can be blank"
/>

<.input
field={f[:max_duration_seconds]}
type="number"
label="Maximum Duration (seconds)"
min="0"
help="Maximum duration of the media to be downloaded. Can be blank"
/>
</section>

<.input
field={f[:download_cutoff_date]}
type="text"
Expand Down
Binary file modified priv/repo/erd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Pinchflat.Repo.Migrations.AddDurationLimitsToSources do
use Ecto.Migration

def change do
alter table(:sources) do
add :min_duration_seconds, :integer
add :max_duration_seconds, :integer
end
end
end
70 changes: 70 additions & 0 deletions test/pinchflat/media_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,48 @@ defmodule Pinchflat.MediaTest do
end
end

describe "list_pending_media_items_for/1 when min and max durations" do
test "returns media items that meet the min and max duration" do
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: 20})

_short_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
normal_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
_long_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})

assert Media.list_pending_media_items_for(source) == [normal_media_item]
end

test "does not apply a min duration if none is specified" do
source = source_fixture(%{min_duration_seconds: nil, max_duration_seconds: 20})

short_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
normal_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
_long_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})

assert Media.list_pending_media_items_for(source) == [short_media_item, normal_media_item]
end

test "does not apply a max duration if none is specified" do
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: nil})

_short_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
normal_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
long_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})

assert Media.list_pending_media_items_for(source) == [normal_media_item, long_media_item]
end

test "does not apply a min or max duration if none are specified" do
source = source_fixture(%{min_duration_seconds: nil, max_duration_seconds: nil})

short_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
normal_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
long_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})

assert Media.list_pending_media_items_for(source) == [short_media_item, normal_media_item, long_media_item]
end
end

describe "list_pending_media_items_for/1 when testing download prevention" do
test "returns only media items that are not prevented from downloading" do
source = source_fixture()
Expand Down Expand Up @@ -434,6 +476,34 @@ defmodule Pinchflat.MediaTest do
assert Media.pending_download?(media_item)
end

test "returns true if the duration is between the min and max" do
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: 20})
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})

assert Media.pending_download?(media_item)
end

test "returns false if the duration is below the min" do
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: 20})
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})

refute Media.pending_download?(media_item)
end

test "returns false if the duration is above the max" do
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: 20})
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})

refute Media.pending_download?(media_item)
end

test "returns true if there is no min or max duration" do
source = source_fixture(%{min_duration_seconds: nil, max_duration_seconds: nil})
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})

assert Media.pending_download?(media_item)
end

test "returns true if the media item is not prevented from downloading" do
media_item = media_item_fixture(%{media_filepath: nil, prevent_download: false})

Expand Down
28 changes: 28 additions & 0 deletions test/pinchflat/sources_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,34 @@ defmodule Pinchflat.SourcesTest do
end
end

describe "change_source/3 when testing min/max duration validations" do
test "succeeds if min and max are nil" do
source = source_fixture()

assert %{errors: []} = Sources.change_source(source, %{min_duration_seconds: nil, max_duration_seconds: nil})
end

test "succeeds if either min or max is nil" do
source = source_fixture()

assert %{errors: []} = Sources.change_source(source, %{min_duration_seconds: nil, max_duration_seconds: 100})
assert %{errors: []} = Sources.change_source(source, %{min_duration_seconds: 100, max_duration_seconds: nil})
end

test "succeeds if min is less than max" do
source = source_fixture()

assert %{errors: []} = Sources.change_source(source, %{min_duration_seconds: 100, max_duration_seconds: 200})
end

test "fails if min is greater than or equal to max" do
source = source_fixture()

assert %{errors: [_]} = Sources.change_source(source, %{min_duration_seconds: 200, max_duration_seconds: 100})
assert %{errors: [_]} = Sources.change_source(source, %{min_duration_seconds: 100, max_duration_seconds: 100})
end
end

describe "change_source/3 when testing original_url validation" do
test "succeeds when an original URL is valid" do
source = source_fixture()
Expand Down

0 comments on commit af8235c

Please sign in to comment.