[FEATURE] Filter Duration prebuilt preset (#1259)

Adds the prebuilt preset ``Filter Duration``, which can include/exclude media based on its duration.

Supports the following override variables:

* ``filter_duration_min_s``
* ``filter_duration_max_s``

Usage:

```
      Plex TV Show by Date | Filter Duration:

        = Documentaries:
          "~NOVA PBS":
            url: "https://www.youtube.com/@novapbs"
            filter_duration_min_s: 120  # Only download videos at least 2m long

        = Sports:
          "~Maple Leafs Highlights":
            url: "https://www.youtube.com/@NHL"
            filter_duration_max_s: 180  # Only get highlight videos less than 3m long
```
This commit is contained in:
Jesse Bannon 2025-07-04 13:41:54 -07:00 committed by GitHub
parent 6c180c9478
commit 00e9ceaf3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 249 additions and 3 deletions

View file

@ -4,10 +4,15 @@ Common
.. highlight:: yaml
Filtering
-------------
Filter Keywords
---------------
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/filtering.yaml
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/filter_keywords.yaml
Filter Duration
---------------
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml
Media Quality
-------------

View file

@ -71,6 +71,35 @@ Supports the following override variables:
- "maple leafs"
- "highlights"
Filter Duration
---------------
``Filter Duration`` can include or exclude media based on its duration.
Supports the following override variables:
* ``filter_duration_min_s``
* ``filter_duration_max_s``
.. tip::
Use the `~` tilda subscription mode to set a subscription's list override variables.
Tilda mode allows override variables to be set directly underneath it.
.. code-block:: yaml
Plex TV Show by Date | Filter Duration:
= Documentaries:
"~NOVA PBS":
url: "https://www.youtube.com/@novapbs"
filter_duration_min_s: 120 # Only download videos at least 2m long
= Sports:
"~Maple Leafs Highlights":
url: "https://www.youtube.com/@NHL"
filter_duration_max_s: 180 # Only get highlight videos less than 3m long
Chunk Downloads
---------------

View file

@ -0,0 +1,35 @@
presets:
#############################################################################
# Include Keywords
# Include or exclude media with any of the listed keywords in their titles
# Keywords will check a lower-cased title or description
Filter Duration:
overrides:
filter_duration_min_s: 0
filter_duration_max_s: 4294967296
"%filter_duration_ensure_numeric": >-
{
%assert_then(
%is_numeric($0),
$0,
"filter_duration args must be numeric"
)
}
filter_duration_zero_msg: "Duration metadata for {title} is missing, cannot perform filter."
"%filter_duration_eval": >-
{
%if(
%eq(duration, 0),
%print(filter_duration_zero_msg, False)
$0
)
}
filter_exclude:
- "{ %filter_duration_eval( %lt(duration, %filter_duration_ensure_numeric(filter_duration_min_s)) ) }"
- "{ %filter_duration_eval( %gt(duration, %filter_duration_ensure_numeric(filter_duration_max_s)) ) }"

View file

@ -0,0 +1,111 @@
import pytest
from expected_transaction_log import assert_transaction_log_matches
from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError
from ytdl_sub.subscriptions.subscription import Subscription
@pytest.fixture
def filter_subscription_dict(output_directory):
return {
"preset": [
"Plex TV Show by Date",
"Filter Duration",
],
"overrides": {"url": "https://your.name.here", "tv_show_directory": output_directory},
}
class TestFilterKeywords:
def test_no_overrides(
self,
config,
filter_subscription_dict,
output_directory,
subscription_name,
mock_download_collection_entries,
):
subscription = Subscription.from_dict(
config=config,
preset_name=subscription_name,
preset_dict=filter_subscription_dict,
)
with mock_download_collection_entries(
is_youtube_channel=False, num_urls=1, is_dry_run=True
):
transaction_log = subscription.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name=f"integration/prebuilt_presets/filter_keywords_empty.txt",
)
@pytest.mark.parametrize(
"filter_eval, filter_value, filters_all",
[
("min", 10, False),
("max", 100, False),
("min", 100, True),
("max", 10, True),
],
)
def test_filter_duration(
self,
config,
filter_subscription_dict,
output_directory,
subscription_name,
mock_download_collection_entries,
filter_eval: str,
filter_value: int,
filters_all: bool,
):
filter_subscription_dict["overrides"][f"filter_duration_{filter_eval}_s"] = filter_value
subscription = Subscription.from_dict(
config=config,
preset_name=subscription_name,
preset_dict=filter_subscription_dict,
)
with mock_download_collection_entries(
is_youtube_channel=False, num_urls=1, is_dry_run=True
):
transaction_log = subscription.download(dry_run=True)
if filters_all:
assert transaction_log.is_empty
else:
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name=f"integration/prebuilt_presets/filter_duration.txt",
)
@pytest.mark.parametrize(
"filter_eval",
["min", "max"],
)
def test_error_not_numeric(
self,
config,
filter_subscription_dict,
output_directory,
subscription_name,
mock_download_collection_entries,
filter_eval,
):
filter_subscription_dict["overrides"][f"filter_duration_{filter_eval}_s"] = "egg"
subscription = Subscription.from_dict(
config=config,
preset_name=subscription_name,
preset_dict=filter_subscription_dict,
)
with (
mock_download_collection_entries(is_youtube_channel=False, num_urls=1, is_dry_run=True),
pytest.raises(UserThrownRuntimeError, match=f"filter_duration args must be numeric"),
):
_ = subscription.download(dry_run=True)

View file

@ -0,0 +1,66 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/Season 2020
s2020.e080701 - Mock Entry --thumb.jpg
s2020.e080701 - Mock Entry -.info.json
s2020.e080701 - Mock Entry -.mp4
Video Tags:
contentRating: TV-14
date: 2020-08-07
episode_id: 80701
genre: ytdl-sub
show: subscription_test
synopsis:
https://20-3.com
The Description
title: 2020-08-07 - Mock Entry 20-3
year: 2020
s2020.e080801 - Mock Entry --thumb.jpg
s2020.e080801 - Mock Entry -.info.json
s2020.e080801 - Mock Entry -.mp4
Video Tags:
contentRating: TV-14
date: 2020-08-08
episode_id: 80801
genre: ytdl-sub
show: subscription_test
synopsis:
https://20-2.com
The Description
title: 2020-08-08 - Mock Entry 20-2
year: 2020
s2020.e080802 - Mock Entry --thumb.jpg
s2020.e080802 - Mock Entry -.info.json
s2020.e080802 - Mock Entry -.mp4
Video Tags:
contentRating: TV-14
date: 2020-08-08
episode_id: 80802
genre: ytdl-sub
show: subscription_test
synopsis:
https://20-1.com
The Description
title: 2020-08-08 - Mock Entry 20-1
year: 2020
{output_directory}/Season 2021
s2021.e080801 - Mock Entry --thumb.jpg
s2021.e080801 - Mock Entry -.info.json
s2021.e080801 - Mock Entry -.mp4
Video Tags:
contentRating: TV-14
date: 2021-08-08
episode_id: 80801
genre: ytdl-sub
show: subscription_test
synopsis:
https://21-1.com
The Description
title: 2021-08-08 - Mock Entry 21-1
year: 2021