[Enhancement] Filter media based on min and/or max duration (#356)
* 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
This commit is contained in:
@@ -75,6 +75,14 @@ defmodule Pinchflat.Media.MediaQuery do
|
|||||||
)
|
)
|
||||||
end
|
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
|
def past_retention_period do
|
||||||
dynamic(
|
dynamic(
|
||||||
[mi, source],
|
[mi, source],
|
||||||
@@ -123,7 +131,8 @@ defmodule Pinchflat.Media.MediaQuery do
|
|||||||
not (^download_prevented()) and
|
not (^download_prevented()) and
|
||||||
^upload_date_after_source_cutoff() and
|
^upload_date_after_source_cutoff() and
|
||||||
^format_matching_profile_preference() and
|
^format_matching_profile_preference() and
|
||||||
^matches_source_title_regex()
|
^matches_source_title_regex() and
|
||||||
|
^meets_min_and_max_duration()
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
media_profile_id
|
media_profile_id
|
||||||
output_path_template_override
|
output_path_template_override
|
||||||
marked_for_deletion_at
|
marked_for_deletion_at
|
||||||
|
min_duration_seconds
|
||||||
|
max_duration_seconds
|
||||||
)a
|
)a
|
||||||
|
|
||||||
# Expensive API calls are made when a source is inserted/updated so
|
# Expensive API calls are made when a source is inserted/updated so
|
||||||
@@ -84,6 +86,9 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
field :title_filter_regex, :string
|
field :title_filter_regex, :string
|
||||||
field :output_path_template_override, :string
|
field :output_path_template_override, :string
|
||||||
|
|
||||||
|
field :min_duration_seconds, :integer
|
||||||
|
field :max_duration_seconds, :integer
|
||||||
|
|
||||||
field :series_directory, :string
|
field :series_directory, :string
|
||||||
field :nfo_filepath, :string
|
field :nfo_filepath, :string
|
||||||
field :poster_filepath, :string
|
field :poster_filepath, :string
|
||||||
@@ -118,6 +123,7 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
|> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end)
|
|> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end)
|
||||||
|> validate_required(required_fields)
|
|> validate_required(required_fields)
|
||||||
|> validate_title_regex()
|
|> validate_title_regex()
|
||||||
|
|> validate_min_and_max_durations()
|
||||||
|> validate_number(:retention_period_days, greater_than_or_equal_to: 0)
|
|> 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)
|
# 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 }}")
|
|> validate_format(:output_path_template_override, MediaProfile.ext_regex(), message: "must end with .{{ ext }}")
|
||||||
@@ -164,6 +170,17 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
|
|
||||||
defp validate_title_regex(changeset), do: changeset
|
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
|
defimpl Jason.Encoder, for: Source do
|
||||||
def encode(value, opts) do
|
def encode(value, opts) do
|
||||||
value
|
value
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|
|||||||
| id: nil,
|
| id: nil,
|
||||||
uuid: nil,
|
uuid: nil,
|
||||||
custom_name: nil,
|
custom_name: nil,
|
||||||
|
description: nil,
|
||||||
collection_name: nil,
|
collection_name: nil,
|
||||||
collection_id: nil,
|
collection_id: nil,
|
||||||
collection_type: nil,
|
collection_type: nil,
|
||||||
|
|||||||
@@ -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"
|
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
|
<.input
|
||||||
field={f[:download_cutoff_date]}
|
field={f[:download_cutoff_date]}
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 449 KiB After Width: | Height: | Size: 436 KiB |
@@ -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
|
||||||
@@ -355,6 +355,48 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
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
|
describe "list_pending_media_items_for/1 when testing download prevention" do
|
||||||
test "returns only media items that are not prevented from downloading" do
|
test "returns only media items that are not prevented from downloading" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -434,6 +476,34 @@ defmodule Pinchflat.MediaTest do
|
|||||||
assert Media.pending_download?(media_item)
|
assert Media.pending_download?(media_item)
|
||||||
end
|
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
|
test "returns true if the media item is not prevented from downloading" do
|
||||||
media_item = media_item_fixture(%{media_filepath: nil, prevent_download: false})
|
media_item = media_item_fixture(%{media_filepath: nil, prevent_download: false})
|
||||||
|
|
||||||
|
|||||||
@@ -681,6 +681,34 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
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
|
describe "change_source/3 when testing original_url validation" do
|
||||||
test "succeeds when an original URL is valid" do
|
test "succeeds when an original URL is valid" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|||||||
Reference in New Issue
Block a user