1215 lines
No EOL
58 KiB
Python
1215 lines
No EOL
58 KiB
Python
import re
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Dict
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from ytdl_sub.config.config_file import ConfigFile
|
|
from ytdl_sub.config.validators.variable_validation import ResolutionLevel
|
|
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
|
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
|
|
from ytdl_sub.subscriptions.subscription import Subscription
|
|
from ytdl_sub.utils.exceptions import ValidationException
|
|
from ytdl_sub.utils.script import ScriptUtils
|
|
|
|
|
|
@contextmanager
|
|
def mock_load_yaml(preset_dict: Dict) -> None:
|
|
with patch("ytdl_sub.subscriptions.subscription.load_yaml") as mock_load_yaml:
|
|
mock_load_yaml.return_value = preset_dict
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_file_preset(youtube_video: Dict, output_options: Dict):
|
|
return {
|
|
"__preset__": {
|
|
"download": youtube_video,
|
|
"output_options": output_options,
|
|
"nfo_tags": {
|
|
"nfo_name": "eh",
|
|
"nfo_root": "eh",
|
|
"tags": {"key-3": "file_preset"},
|
|
},
|
|
"overrides": {
|
|
"subscription_indent_1": "original_1",
|
|
"subscription_indent_2": "original_2",
|
|
"current_override": "__preset__",
|
|
},
|
|
},
|
|
"test_preset": {
|
|
"preset": "parent_preset_3",
|
|
"nfo_tags": {
|
|
"tags": {"key-4": "test_preset"},
|
|
},
|
|
"overrides": {"current_override": "test_preset"},
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_value(preset_with_file_preset: Dict):
|
|
return dict(
|
|
preset_with_file_preset,
|
|
**{
|
|
"test_value": "is_overwritten",
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def subscription_with_period_in_name(preset_with_file_preset: Dict):
|
|
return dict(
|
|
preset_with_file_preset,
|
|
**{
|
|
"Mr. Beast": "is_overwritten",
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_value_nested_presets(preset_with_subscription_value: Dict):
|
|
return dict(
|
|
preset_with_subscription_value,
|
|
**{
|
|
"parent_preset_2": {
|
|
"parent_preset_1": {"test_2_1": "is_2_1_overwritten"},
|
|
"test_1": "is_1_overwritten",
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_value_nested_presets_and_indent_variables(
|
|
preset_with_subscription_value: Dict,
|
|
):
|
|
return dict(
|
|
preset_with_subscription_value,
|
|
**{
|
|
"parent_preset_2": {
|
|
"= INDENT_1 ": {
|
|
"parent_preset_1": {"test_2_1": "is_2_1_overwritten"},
|
|
"=INDENT_2": {
|
|
"test_1": "is_1_overwritten",
|
|
},
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_value_nested_presets_and_indent_variables_same_line(
|
|
preset_with_subscription_value: Dict,
|
|
):
|
|
return dict(
|
|
preset_with_subscription_value,
|
|
**{
|
|
"parent_preset_2": {
|
|
"=INDENT_1": {
|
|
"parent_preset_1": {"test_2_1": "is_2_1_overwritten"},
|
|
"= INDENT_2 | = INDENT_3 ": {
|
|
"test_1": "is_1_overwritten",
|
|
},
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_value_nested_presets_and_indent_variables_all_same_line(
|
|
preset_with_subscription_value: Dict,
|
|
):
|
|
return dict(
|
|
preset_with_subscription_value,
|
|
**{
|
|
"parent_preset_2 | =INDENT_1 | parent_preset_1": {
|
|
"test_2_1": "is_2_1_overwritten",
|
|
},
|
|
"parent_preset_2 | =INDENT_1 |= INDENT_2 | = INDENT_3 ": {
|
|
"test_1": "is_1_overwritten",
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_list(
|
|
preset_with_subscription_value: Dict,
|
|
):
|
|
return dict(
|
|
preset_with_subscription_value,
|
|
**{
|
|
"parent_preset_2 | parent_preset_1": {
|
|
"test_2_1": ["is_2_1_overwritten", "is_2_1_list_2"]
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_overrides_tilda(
|
|
preset_with_subscription_value: Dict,
|
|
):
|
|
return dict(
|
|
preset_with_subscription_value,
|
|
**{
|
|
"parent_preset_2 | parent_preset_1": {
|
|
"~ test_2_1": {
|
|
"current_override": "test_2_1",
|
|
"custom_list": [
|
|
"elem1",
|
|
"elem2",
|
|
"elem3",
|
|
],
|
|
"passed_list_elem": "{%contains_any('elem2', custom_list)}",
|
|
}
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_overrides_map(
|
|
preset_with_subscription_value: Dict,
|
|
):
|
|
return dict(
|
|
preset_with_subscription_value,
|
|
**{
|
|
"parent_preset_2 | parent_preset_1": {
|
|
"+ test_2_1": {
|
|
"custom_key": "custom_value",
|
|
"custom_list": [
|
|
"elem1",
|
|
"elem2",
|
|
"elem3",
|
|
],
|
|
"custom_map": {"custom_map_key": ["custom_map_list_value"]},
|
|
}
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors(
|
|
preset_with_subscription_value: Dict,
|
|
):
|
|
return dict(
|
|
preset_with_subscription_value,
|
|
**{
|
|
"parent_preset_2": {
|
|
"=INDENT_1": {
|
|
"parent_preset_1": {"test_2_1": "is_2_1_overwritten"},
|
|
"= INDENT_2 | INDENT_3 ": {
|
|
"test_1": "is_1_overwritten",
|
|
},
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
def test_subscription_file_preset_applies(config_file: ConfigFile, preset_with_file_preset: Dict):
|
|
with mock_load_yaml(preset_dict=preset_with_file_preset):
|
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
assert len(subs) == 1
|
|
|
|
# Test __preset__ worked correctly
|
|
preset_sub = subs[0]
|
|
assert preset_sub.name == "test_preset"
|
|
|
|
nfo_options: NfoTagsOptions = preset_sub.plugins.get(NfoTagsOptions)
|
|
tags_string_dict = {
|
|
key: formatter[0].format_string for key, formatter in nfo_options.tags.string_tags.items()
|
|
}
|
|
|
|
assert tags_string_dict == {
|
|
"key-1": "preset_0",
|
|
"key-2": "preset_2",
|
|
"key-3": "file_preset",
|
|
"key-4": "test_preset",
|
|
}
|
|
|
|
overrides = preset_sub.overrides.script
|
|
# preset overrides take precedence over __preset__
|
|
assert overrides.get("current_override").native == "test_preset"
|
|
|
|
|
|
def test_subscription_list(
|
|
config_file: ConfigFile,
|
|
preset_with_subscription_list: Dict,
|
|
):
|
|
with mock_load_yaml(preset_dict=preset_with_subscription_list):
|
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
assert len(subs) == 3
|
|
|
|
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
|
|
|
|
assert sub_2_1.get("subscription_name").native == "test_2_1"
|
|
assert sub_2_1.get("subscription_value").native == "is_2_1_overwritten"
|
|
assert sub_2_1.get("subscription_value_1").native == "is_2_1_overwritten"
|
|
assert sub_2_1.get("subscription_value_2").native == "is_2_1_list_2"
|
|
assert (
|
|
sub_2_1.get("current_override").native == "__preset__"
|
|
) # ensure __preset__ takes precedence
|
|
|
|
|
|
def test_subscription_overrides_tilda(
|
|
config_file: ConfigFile,
|
|
preset_with_subscription_overrides_tilda: Dict,
|
|
):
|
|
with mock_load_yaml(preset_dict=preset_with_subscription_overrides_tilda):
|
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
assert len(subs) == 3
|
|
|
|
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
|
|
|
|
assert sub_2_1.get("subscription_name").native == "test_2_1"
|
|
assert sub_2_1.get("current_override").native == "test_2_1" # tilda sub takes precedence
|
|
assert sub_2_1.get("passed_list_elem").native is True
|
|
assert sub_2_1.get("custom_list").native == ["elem1", "elem2", "elem3"]
|
|
|
|
|
|
def test_subscription_overrides_map(
|
|
config_file: ConfigFile,
|
|
preset_with_subscription_overrides_map: Dict,
|
|
):
|
|
with mock_load_yaml(preset_dict=preset_with_subscription_overrides_map):
|
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
assert len(subs) == 3
|
|
|
|
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
|
|
|
|
assert sub_2_1.get("subscription_name").native == "test_2_1"
|
|
assert sub_2_1.get("subscription_map").native == {
|
|
"custom_key": "custom_value",
|
|
"custom_list": [
|
|
"elem1",
|
|
"elem2",
|
|
"elem3",
|
|
],
|
|
"custom_map": {"custom_map_key": ["custom_map_list_value"]},
|
|
}
|
|
|
|
|
|
def test_subscription_with_period_in_name(
|
|
config_file: ConfigFile,
|
|
subscription_with_period_in_name: Dict,
|
|
):
|
|
with mock_load_yaml(preset_dict=subscription_with_period_in_name):
|
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
assert len(subs) == 2
|
|
|
|
assert subs[1].name == "Mr. Beast"
|
|
assert subs[1].overrides.script.get("subscription_name").native == "Mr. Beast"
|
|
|
|
|
|
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables(
|
|
config_file: ConfigFile,
|
|
preset_with_subscription_value_nested_presets_and_indent_variables: Dict,
|
|
):
|
|
with mock_load_yaml(
|
|
preset_dict=preset_with_subscription_value_nested_presets_and_indent_variables
|
|
):
|
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
assert len(subs) == 4
|
|
|
|
sub_test_value = [sub for sub in subs if sub.name == "test_value"][0].overrides.script
|
|
sub_1 = [sub for sub in subs if sub.name == "test_1"][0].overrides.script
|
|
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
|
|
|
|
assert sub_test_value.get("subscription_indent_1").native == "original_1"
|
|
assert sub_test_value.get("subscription_indent_2").native == "original_2"
|
|
|
|
assert sub_1.get("subscription_name").native == "test_1"
|
|
assert sub_1.get("subscription_value").native == "is_1_overwritten"
|
|
assert sub_1.get("subscription_indent_1").native == "INDENT_1"
|
|
assert sub_1.get("subscription_indent_2").native == "INDENT_2"
|
|
assert (
|
|
sub_1.get("current_override").native == "__preset__"
|
|
) # ensure __preset__ takes precedence
|
|
|
|
assert sub_2_1.get("subscription_name").native == "test_2_1"
|
|
assert sub_2_1.get("subscription_value").native == "is_2_1_overwritten"
|
|
assert sub_2_1.get("subscription_indent_1").native == "INDENT_1"
|
|
assert sub_2_1.get("subscription_indent_2").native == "original_2"
|
|
assert (
|
|
sub_2_1.get("current_override").native == "__preset__"
|
|
) # ensure __preset__ takes precedence
|
|
|
|
|
|
@pytest.mark.parametrize("all_same_line", [True, False])
|
|
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line(
|
|
config_file: ConfigFile,
|
|
preset_with_subscription_value_nested_presets_and_indent_variables_same_line: Dict,
|
|
preset_with_subscription_value_nested_presets_and_indent_variables_all_same_line: Dict,
|
|
all_same_line: bool,
|
|
):
|
|
preset_dict = preset_with_subscription_value_nested_presets_and_indent_variables_same_line
|
|
if all_same_line:
|
|
preset_dict = (
|
|
preset_with_subscription_value_nested_presets_and_indent_variables_all_same_line
|
|
)
|
|
|
|
with mock_load_yaml(preset_dict=preset_dict):
|
|
subs = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
assert len(subs) == 4
|
|
|
|
sub_test_value = [sub for sub in subs if sub.name == "test_value"][0].overrides.script
|
|
sub_1 = [sub for sub in subs if sub.name == "test_1"][0].overrides.script
|
|
sub_2_1 = [sub for sub in subs if sub.name == "test_2_1"][0].overrides.script
|
|
|
|
assert sub_test_value.get("subscription_indent_1").native == "original_1"
|
|
assert sub_test_value.get("subscription_indent_2").native == "original_2"
|
|
|
|
assert sub_1.get("subscription_name").native == "test_1"
|
|
assert sub_1.get("subscription_value").native == "is_1_overwritten"
|
|
assert sub_1.get("subscription_indent_1").native == "INDENT_1"
|
|
assert sub_1.get("subscription_indent_2").native == "INDENT_2"
|
|
assert sub_1.get("subscription_indent_3").native == "INDENT_3"
|
|
# ensure __preset__ takes precedence
|
|
assert sub_1.get("current_override").native == "__preset__"
|
|
|
|
assert sub_2_1.get("subscription_name").native == "test_2_1"
|
|
assert sub_2_1.get("subscription_value").native == "is_2_1_overwritten"
|
|
assert sub_2_1.get("subscription_indent_1").native == "INDENT_1"
|
|
assert sub_2_1.get("subscription_indent_2").native == "original_2"
|
|
# ensure __preset__ takes precedence
|
|
assert sub_2_1.get("current_override").native == "__preset__"
|
|
assert "subscription_indent_3" not in sub_2_1.variable_names
|
|
|
|
|
|
def test_subscription_file_value_applies_from_config_and_nested_and_indent_variables_same_line_old_format_errors(
|
|
config_file: ConfigFile,
|
|
preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors: Dict,
|
|
):
|
|
with (
|
|
mock_load_yaml(
|
|
preset_dict=preset_with_subscription_value_nested_presets_and_indent_variables_same_line_old_format_errors
|
|
),
|
|
pytest.raises(
|
|
ValidationException,
|
|
match=re.escape(
|
|
"Validation error in parent_preset_2.=INDENT_1: 'INDENT_3' in '= INDENT_2 | INDENT_3' is not a preset name. "
|
|
"To use as a subscription indent value, define it as '= INDENT_3'"
|
|
),
|
|
),
|
|
):
|
|
Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
|
|
|
|
def test_subscription_file_using_conflicting_preset_name(config_file: ConfigFile):
|
|
with (
|
|
mock_load_yaml(
|
|
preset_dict={
|
|
"= INDENTS_IN_ERR_MSG ": {"=ANOTHER": {"jellyfin_tv_show_by_date": "single value"}}
|
|
}
|
|
),
|
|
pytest.raises(
|
|
ValidationException,
|
|
match=re.escape(
|
|
"Validation error in = INDENTS_IN_ERR_MSG .=ANOTHER.jellyfin_tv_show_by_date: "
|
|
"jellyfin_tv_show_by_date conflicts with an existing preset name and cannot be used "
|
|
"as a subscription name"
|
|
),
|
|
),
|
|
):
|
|
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
|
|
|
|
def test_subscription_file_invalid_form(config_file: ConfigFile):
|
|
with (
|
|
mock_load_yaml(preset_dict={"sub_name": 4332}),
|
|
pytest.raises(
|
|
ValidationException,
|
|
match=re.escape(f"Subscription value should either be a string, list, or object"),
|
|
),
|
|
):
|
|
_ = Subscription.from_file_path(config=config_file, subscription_path="mocked")
|
|
|
|
|
|
def test_tv_show_subscriptions(config_file: ConfigFile, tv_show_subscriptions_path: Path):
|
|
subs = Subscription.from_file_path(
|
|
config=config_file, subscription_path=tv_show_subscriptions_path
|
|
)
|
|
|
|
assert len(subs) == 8
|
|
assert subs[3].name == "Jake Trains"
|
|
jake_train_overrides = subs[3].overrides.script
|
|
|
|
assert jake_train_overrides.get("subscription_name").native == "Jake Trains"
|
|
assert (
|
|
jake_train_overrides.get("subscription_value").native
|
|
== "https://www.youtube.com/@JakeTrains"
|
|
)
|
|
assert jake_train_overrides.get("subscription_indent_1").native == "Kids"
|
|
assert jake_train_overrides.get("subscription_indent_2").native == "TV-Y"
|
|
|
|
|
|
def test_advanced_tv_show_subscriptions(
|
|
tv_show_config: ConfigFile, advanced_tv_show_subscriptions_path: Path
|
|
):
|
|
subs = Subscription.from_file_path(
|
|
config=tv_show_config, subscription_path=advanced_tv_show_subscriptions_path
|
|
)
|
|
|
|
assert len(subs) == 9
|
|
assert subs[3].name == "Jake Trains"
|
|
jake_train_overrides = subs[3].overrides.script
|
|
|
|
assert jake_train_overrides.get("subscription_name").native == "Jake Trains"
|
|
assert (
|
|
jake_train_overrides.get("subscription_value").native
|
|
== "https://www.youtube.com/@JakeTrains"
|
|
)
|
|
assert jake_train_overrides.get("subscription_indent_1").native == "Kids"
|
|
assert jake_train_overrides.get("subscription_indent_2").native == "TV-Y"
|
|
|
|
assert subs[5].name == "Gardening with Ciscoe"
|
|
overrides = subs[5].overrides
|
|
|
|
assert overrides.script.get("subscription_name").native == "Gardening with Ciscoe"
|
|
|
|
assert overrides.apply_overrides_formatter_to_native(overrides.dict["subscription_array"]) == [
|
|
"https://www.youtube.com/@gardeningwithciscoe4430",
|
|
"https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_",
|
|
"https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn",
|
|
"https://www.youtube.com/watch?v=2vq-vPubS5I",
|
|
]
|
|
assert overrides.apply_overrides_formatter_to_native(overrides.dict["urls"]) == [
|
|
"https://www.youtube.com/@gardeningwithciscoe4430",
|
|
"https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_",
|
|
"https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn",
|
|
"https://www.youtube.com/watch?v=2vq-vPubS5I",
|
|
]
|
|
|
|
|
|
def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_path: Path):
|
|
subs = Subscription.from_file_path(
|
|
config=default_config, subscription_path=music_subscriptions_path
|
|
)
|
|
|
|
assert len(subs) == 14
|
|
assert subs[2].name == "Stan Getz"
|
|
monk = subs[2].overrides.script
|
|
|
|
assert monk.get("subscription_name").native == "Stan Getz"
|
|
assert (
|
|
monk.get("subscription_value").native
|
|
== "https://www.youtube.com/@stangetzofficial/releases"
|
|
)
|
|
assert monk.get("subscription_indent_1").native == "Jazz"
|
|
|
|
|
|
def test_music_video_subscriptions(default_config: ConfigFile, music_video_subscription_path: Path):
|
|
subs = Subscription.from_file_path(
|
|
config=default_config, subscription_path=music_video_subscription_path
|
|
)
|
|
|
|
assert len(subs) == 4
|
|
assert subs[1].name == "Michael Jackson"
|
|
jackson = subs[1].overrides.script
|
|
|
|
assert jackson.get("subscription_name").native == "Michael Jackson"
|
|
assert (
|
|
jackson.get("subscription_value").native
|
|
== "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"
|
|
)
|
|
assert jackson.get("subscription_indent_1").native == "Pop"
|
|
assert (
|
|
jackson.get("urls").native[0]
|
|
== "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"
|
|
)
|
|
|
|
assert subs[3].name == "Guns N' Roses"
|
|
gnr = subs[3].overrides.script
|
|
|
|
assert gnr.get("subscription_name").native == "Guns N' Roses"
|
|
gnr_urls = gnr.get("urls").native
|
|
assert gnr_urls[0] == "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ"
|
|
assert gnr.get("subscription_indent_1").native == "Rock"
|
|
assert gnr_urls[1] == "https://www.youtube.com/watch?v=OldpIhHPsbs"
|
|
|
|
|
|
def test_default_docker_config_and_subscriptions(
|
|
docker_default_subscription_path: Path, output_directory: str
|
|
):
|
|
default_config = ConfigFile.from_file_path("docker/root/defaults/config.yaml")
|
|
default_subs = Subscription.from_file_path(
|
|
config=default_config, subscription_path=docker_default_subscription_path
|
|
)
|
|
assert len(default_subs) == 1
|
|
|
|
resolved_yaml_as_json = yaml.safe_load(default_subs[0].resolved_yaml())
|
|
|
|
assert resolved_yaml_as_json == {
|
|
"chapters": {
|
|
"allow_chapters_from_comments": False,
|
|
"embed_chapters": True,
|
|
"enable": "True",
|
|
"force_key_frames": False,
|
|
},
|
|
"date_range": {"breaks": "True", "enable": "True", "type": "upload_date"},
|
|
"download": [
|
|
{
|
|
"download_reverse": "True",
|
|
"include_sibling_metadata": False,
|
|
"playlist_thumbnails": [
|
|
{"name": "{avatar_uncropped_thumbnail_file_name}", "uid": "avatar_uncropped"},
|
|
{"name": "{banner_uncropped_thumbnail_file_name}", "uid": "banner_uncropped"},
|
|
],
|
|
"source_thumbnails": [
|
|
{"name": "{avatar_uncropped_thumbnail_file_name}", "uid": "avatar_uncropped"},
|
|
{"name": "{banner_uncropped_thumbnail_file_name}", "uid": "banner_uncropped"},
|
|
],
|
|
"url": "https://www.youtube.com/@novapbs",
|
|
"variables": {},
|
|
"webpage_url": "{modified_webpage_url}",
|
|
"ytdl_options": {},
|
|
}
|
|
],
|
|
"file_convert": {"convert_to": "mp4", "convert_with": "yt-dlp", "enable": "True"},
|
|
"format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)",
|
|
"output_options": {
|
|
"download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json",
|
|
"file_name": "{episode_file_path}.{ext}",
|
|
"info_json_name": "{episode_file_path}.{info_json_ext}",
|
|
"keep_files_date_eval": "{episode_date_standardized}",
|
|
"maintain_download_archive": True,
|
|
"output_directory": f"{output_directory}/NOVA PBS",
|
|
"preserve_mtime": False,
|
|
"thumbnail_name": "{thumbnail_file_name}",
|
|
},
|
|
'overrides': {'%bilateral_url': '{ %if( %and( enable_bilateral_scraping, '
|
|
'subscription_has_download_archive, '
|
|
"%is_bilateral_url( $0 ) ), $0, '' ) }",
|
|
'%episode_ordering_': '{ %eq( %lower( '
|
|
'tv_show_by_date_episode_ordering ), $0 ) '
|
|
'}',
|
|
'%is_bilateral_url': '{ %contains( $0, "youtube.com/playlist" ) '
|
|
'}',
|
|
'%ordering_pair_eq': '{ %eq( [ tv_show_by_date_season_ordering, '
|
|
'tv_show_by_date_episode_ordering ], [ $0, '
|
|
'$1 ] ) }',
|
|
'%season_ordering_': '{ %eq( %lower( '
|
|
'tv_show_by_date_season_ordering ), $0 ) }',
|
|
'assert_not_collection': '{ %assert( %and( %not( %bool( s01_url '
|
|
') ), %not( %bool( s01_name ) ) ), '
|
|
'"Provided `s01_url` or `s01_name` '
|
|
'variable to TV Show by Date preset '
|
|
'when it expects `url`. Perhaps you '
|
|
'meant to use the `TV Show Collection` '
|
|
'preset?" ) }',
|
|
'avatar_uncropped_thumbnail_file_name': '{ '
|
|
'tv_show_poster_file_name '
|
|
'}',
|
|
'banner_uncropped_thumbnail_file_name': '{ '
|
|
'tv_show_fanart_file_name '
|
|
'}',
|
|
'enable_bilateral_scraping': '{ %bool(True) }',
|
|
'enable_resolution_assert': '{ %bool(True) }',
|
|
'enable_throttle_protection': '{ %bool(True) }',
|
|
'episode_content_rating': '{ tv_show_content_rating }',
|
|
'episode_date_standardized': '{ %if( %contains( '
|
|
'tv_show_by_date_season_ordering, '
|
|
'"release" ), '
|
|
'release_date_standardized, '
|
|
'upload_date_standardized ) }',
|
|
'episode_file_name': '{ %concat( %string( "s" ), %string( '
|
|
'season_number_padded ), %string( ".e" ), '
|
|
'%string( episode_number_padded ), '
|
|
'%string( " - " ), %string( file_title ) ) '
|
|
'}',
|
|
'episode_file_path': '{ %concat( %string( '
|
|
'season_directory_name_sanitized ), '
|
|
'%string( "/" ), %string( '
|
|
'episode_file_name_sanitized ) ) }',
|
|
'episode_number': '{ %array_at( episode_number_and_padded_, 0 ) '
|
|
'}',
|
|
'episode_number_and_padded_': '{ %elif( %episode_ordering_( '
|
|
'"upload-day" ), [ %concat( '
|
|
'upload_day, '
|
|
'upload_date_index_padded ), 4 ], '
|
|
'%episode_ordering_( '
|
|
'"upload-month-day" ), [ %concat( '
|
|
'upload_month, upload_day_padded, '
|
|
'upload_date_index_padded ), 6 ], '
|
|
'%episode_ordering_( '
|
|
'"upload-month-day-reversed" ), [ '
|
|
'%concat( '
|
|
'upload_day_of_year_reversed, '
|
|
'upload_date_index_reversed_padded '
|
|
'), 5 ], %episode_ordering_( '
|
|
'"release-day" ), [ %concat( '
|
|
'release_day, '
|
|
'upload_date_index_padded ), 4 ], '
|
|
'%episode_ordering_( '
|
|
'"release-month-day" ), [ '
|
|
'%concat( release_month, '
|
|
'release_day_padded, '
|
|
'upload_date_index_padded ), 6 ], '
|
|
'%episode_ordering_( '
|
|
'"release-month-day-reversed" ), '
|
|
'[ %concat( '
|
|
'release_day_of_year_reversed, '
|
|
'upload_date_index_reversed_padded '
|
|
'), 5 ], %episode_ordering_( '
|
|
'"download-index" ), [ '
|
|
'download_index, 6 ], %throw( '
|
|
"'tv_show_by_date_episode_ordering "
|
|
'must be one of the following: '
|
|
'"upload-day", '
|
|
'"upload-month-day", '
|
|
'"upload-month-day-reversed", '
|
|
'"release-day", '
|
|
'"release-month-day", '
|
|
'"release-month-day-reversed", '
|
|
'"download-index"\' ) ) }',
|
|
'episode_number_padded': '{ %pad_zero( %int( episode_number ), '
|
|
'%int( %array_at( '
|
|
'episode_number_and_padded_, 1 ) ) ) }',
|
|
'episode_plot': '{ %concat( %string( webpage_url ), %string( "\n'
|
|
'\n'
|
|
'" ), %string( description ) ) }',
|
|
'episode_title': '{ %concat( %string( episode_date_standardized '
|
|
'), %string( " - " ), %string( title ) ) }',
|
|
'episode_year': '{ %slice( episode_date_standardized, 0, 4 ) }',
|
|
'file_title': '{ title_sanitized_plex }',
|
|
'file_uid': '{ uid_sanitized_plex }',
|
|
'include_sibling_metadata': '{ %bool(False) }',
|
|
'modified_webpage_url': '{ webpage_url }',
|
|
'music_directory': '/music',
|
|
'music_video_directory': '/music_videos',
|
|
'resolution_assert': '{ %if( %and( enable_resolution_assert, '
|
|
'%ne( height, 0 ), %not( '
|
|
'resolution_assert_is_ignored ) ), '
|
|
'%assert( %gte( height, '
|
|
'resolution_assert_height_gte ), %concat( '
|
|
'"Entry ", title, " downloaded at a low '
|
|
'resolution (", resolution_readable, "), '
|
|
'you\'ve probably been throttled. ", '
|
|
'"Stopping further downloads, wait a few '
|
|
'hours and try again. ", "Disable using '
|
|
'the override variable '
|
|
'`enable_resolution_assert: False`." ) ), '
|
|
'"false is no-op" ) }',
|
|
'resolution_assert_height_gte': '{ %int(361) }',
|
|
'resolution_assert_ignore_titles': '{ [ ] }',
|
|
'resolution_assert_is_ignored': '{ %print_if_true( %concat( '
|
|
'title, " has a match in '
|
|
'resolution_assert_ignore_titles, '
|
|
'skipping resolution assert." '
|
|
'), %contains_any( title, '
|
|
'resolution_assert_ignore_titles '
|
|
') ) }',
|
|
'resolution_assert_print': '{ %print( %if( '
|
|
'enable_resolution_assert, '
|
|
'"Resolution assert is enabled, will '
|
|
'fail on low-quality video downloads '
|
|
'and presume throttle. Disable using '
|
|
'the override variable '
|
|
'`enable_resolution_assert: False`", '
|
|
'"Resolution assert is disabled. Use '
|
|
'at your own risk!" ), '
|
|
'enable_resolution_assert, -1 ) }',
|
|
'resolution_readable': '{ %concat( %string( width ), %string( '
|
|
'"x" ), %string( height ) ) }',
|
|
's01_name': '',
|
|
's01_url': '',
|
|
'season_directory_name': '{ %concat( %string( "Season " ), '
|
|
'%string( season_number_padded ) ) }',
|
|
'season_number': '{ %elif( %season_ordering_( "upload-year" ), '
|
|
'upload_year, %season_ordering_( '
|
|
'"upload-year-month" ), %concat( upload_year, '
|
|
'upload_month_padded ), %season_ordering_( '
|
|
'"release-year" ), release_year, '
|
|
'%season_ordering_( "release-year-month" ), '
|
|
'%concat( release_year, release_month_padded '
|
|
"), %throw( 'tv_show_by_date_season_ordering "
|
|
'must be one of the following: "upload-year", '
|
|
'"upload-year-month", "release-year", '
|
|
'"release-year-month"\' ) ) }',
|
|
'season_number_padded': '{ season_number }',
|
|
'season_poster_file_name': '{ %concat( %string( '
|
|
'season_directory_name_sanitized ), '
|
|
'%string( "/Season" ), %string( '
|
|
'season_number_padded ), %string( '
|
|
'".jpg" ) ) }',
|
|
'subscription_array': '{ %from_json( '
|
|
'\'["https://www.youtube.com/@novapbs"]\' '
|
|
') }',
|
|
'subscription_indent_1': 'Documentaries',
|
|
'subscription_indent_2': '{ tv_show_content_rating_default }',
|
|
'subscription_value': 'https://www.youtube.com/@novapbs',
|
|
'subscription_value_1': 'https://www.youtube.com/@novapbs',
|
|
'thumbnail_file_name': '{ %concat( %string( episode_file_path '
|
|
'), %string( "-thumb.jpg" ) ) }',
|
|
'tv_show_by_date_episode_ordering': 'upload-month-day',
|
|
'tv_show_by_date_ordering_pair_validation_': '{ %assert_then( '
|
|
'%or( '
|
|
'%ordering_pair_eq( '
|
|
'"upload-year", '
|
|
'"upload-month-day" '
|
|
'), '
|
|
'%ordering_pair_eq( '
|
|
'"upload-year", '
|
|
'"upload-month-day-reversed" '
|
|
'), '
|
|
'%ordering_pair_eq( '
|
|
'"upload-year", '
|
|
'"download-index" '
|
|
'), '
|
|
'%ordering_pair_eq( '
|
|
'"upload-year-month", '
|
|
'"upload-day" ), '
|
|
'%ordering_pair_eq( '
|
|
'"release-year", '
|
|
'"release-month-day" '
|
|
'), '
|
|
'%ordering_pair_eq( '
|
|
'"release-year", '
|
|
'"release-month-day-reversed" '
|
|
'), '
|
|
'%ordering_pair_eq( '
|
|
'"release-year", '
|
|
'"download-index" '
|
|
'), '
|
|
'%ordering_pair_eq( '
|
|
'"release-year-month", '
|
|
'"release-day" ) '
|
|
'), '
|
|
'episode_number_and_padded_, '
|
|
'"Detected '
|
|
'incompatibility '
|
|
'between '
|
|
'tv_show_by_date_season_ordering '
|
|
'and '
|
|
'tv_show_by_date_episode_ordering. '
|
|
'Ensure you are '
|
|
'not using both '
|
|
'upload and '
|
|
'release date, and '
|
|
'that the '
|
|
'year/month/day '
|
|
'are included in '
|
|
'the combined '
|
|
'season and '
|
|
'episode." ) }',
|
|
'tv_show_by_date_season_ordering': 'upload-year',
|
|
'tv_show_content_rating': '{ subscription_indent_2 }',
|
|
'tv_show_content_rating_default': 'TV-14',
|
|
'tv_show_date_range_type': '{ %if( %contains( '
|
|
'tv_show_by_date_season_ordering, '
|
|
'"release" ), "release_date", '
|
|
'"upload_date" ) }',
|
|
'tv_show_directory': output_directory,
|
|
'tv_show_fanart_file_name': 'fanart.jpg',
|
|
'tv_show_genre': '{ subscription_indent_1 }',
|
|
'tv_show_genre_default': 'ytdl-sub',
|
|
'tv_show_name': '{ subscription_name }',
|
|
'tv_show_poster_file_name': 'poster.jpg',
|
|
'url': '{ subscription_value }',
|
|
'url10': '',
|
|
'url100': '',
|
|
'url11': '',
|
|
'url12': '',
|
|
'url13': '',
|
|
'url14': '',
|
|
'url15': '',
|
|
'url16': '',
|
|
'url17': '',
|
|
'url18': '',
|
|
'url19': '',
|
|
'url2': '',
|
|
'url20': '',
|
|
'url21': '',
|
|
'url22': '',
|
|
'url23': '',
|
|
'url24': '',
|
|
'url25': '',
|
|
'url26': '',
|
|
'url27': '',
|
|
'url28': '',
|
|
'url29': '',
|
|
'url3': '',
|
|
'url30': '',
|
|
'url31': '',
|
|
'url32': '',
|
|
'url33': '',
|
|
'url34': '',
|
|
'url35': '',
|
|
'url36': '',
|
|
'url37': '',
|
|
'url38': '',
|
|
'url39': '',
|
|
'url4': '',
|
|
'url40': '',
|
|
'url41': '',
|
|
'url42': '',
|
|
'url43': '',
|
|
'url44': '',
|
|
'url45': '',
|
|
'url46': '',
|
|
'url47': '',
|
|
'url48': '',
|
|
'url49': '',
|
|
'url5': '',
|
|
'url50': '',
|
|
'url51': '',
|
|
'url52': '',
|
|
'url53': '',
|
|
'url54': '',
|
|
'url55': '',
|
|
'url56': '',
|
|
'url57': '',
|
|
'url58': '',
|
|
'url59': '',
|
|
'url6': '',
|
|
'url60': '',
|
|
'url61': '',
|
|
'url62': '',
|
|
'url63': '',
|
|
'url64': '',
|
|
'url65': '',
|
|
'url66': '',
|
|
'url67': '',
|
|
'url68': '',
|
|
'url69': '',
|
|
'url7': '',
|
|
'url70': '',
|
|
'url71': '',
|
|
'url72': '',
|
|
'url73': '',
|
|
'url74': '',
|
|
'url75': '',
|
|
'url76': '',
|
|
'url77': '',
|
|
'url78': '',
|
|
'url79': '',
|
|
'url8': '',
|
|
'url80': '',
|
|
'url81': '',
|
|
'url82': '',
|
|
'url83': '',
|
|
'url84': '',
|
|
'url85': '',
|
|
'url86': '',
|
|
'url87': '',
|
|
'url88': '',
|
|
'url89': '',
|
|
'url9': '',
|
|
'url90': '',
|
|
'url91': '',
|
|
'url92': '',
|
|
'url93': '',
|
|
'url94': '',
|
|
'url95': '',
|
|
'url96': '',
|
|
'url97': '',
|
|
'url98': '',
|
|
'url99': '',
|
|
'urls': '{ subscription_array }'},
|
|
"throttle_protection": {
|
|
"enable": True,
|
|
"sleep_per_download_s": {"max": "28.4", "min": "13.8"},
|
|
"sleep_per_request_s": {"max": "0.75", "min": "0.0"},
|
|
"sleep_per_subscription_s": {"max": "26.1", "min": "16.3"},
|
|
},
|
|
"video_tags": {
|
|
"contentRating": "{episode_content_rating}",
|
|
"date": "{episode_date_standardized}",
|
|
"episode_id": "{episode_number}",
|
|
"genre": "{tv_show_genre}",
|
|
"show": "{tv_show_name}",
|
|
"synopsis": "{episode_plot}",
|
|
"title": "{episode_title}",
|
|
"year": "{episode_year}",
|
|
},
|
|
}
|
|
|
|
|
|
def test_default_docker_config_and_subscriptions_resolved_resolution(
|
|
docker_default_subscription_path: Path, output_directory: str
|
|
):
|
|
default_config = ConfigFile.from_file_path("docker/root/defaults/config.yaml")
|
|
default_subs = Subscription.from_file_path(
|
|
config=default_config, subscription_path=docker_default_subscription_path
|
|
)
|
|
assert len(default_subs) == 1
|
|
|
|
resolved_yaml_as_json = yaml.safe_load(default_subs[0].resolved_yaml(resolution_level=ResolutionLevel.RESOLVE))
|
|
|
|
assert resolved_yaml_as_json == {
|
|
"chapters": {
|
|
"allow_chapters_from_comments": False,
|
|
"embed_chapters": True,
|
|
"enable": "True",
|
|
"force_key_frames": False,
|
|
},
|
|
"date_range": {"breaks": "True", "enable": "True", "type": "upload_date"},
|
|
"download": [
|
|
{
|
|
"download_reverse": "True",
|
|
"include_sibling_metadata": False,
|
|
"playlist_thumbnails": [
|
|
{"name": "{avatar_uncropped_thumbnail_file_name}", "uid": "avatar_uncropped"},
|
|
{"name": "{banner_uncropped_thumbnail_file_name}", "uid": "banner_uncropped"},
|
|
],
|
|
"source_thumbnails": [
|
|
{"name": "{avatar_uncropped_thumbnail_file_name}", "uid": "avatar_uncropped"},
|
|
{"name": "{banner_uncropped_thumbnail_file_name}", "uid": "banner_uncropped"},
|
|
],
|
|
"url": "https://www.youtube.com/@novapbs",
|
|
"variables": {},
|
|
"webpage_url": "{modified_webpage_url}",
|
|
"ytdl_options": {},
|
|
}
|
|
],
|
|
"file_convert": {"convert_to": "mp4", "convert_with": "yt-dlp", "enable": "True"},
|
|
"format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)",
|
|
"output_options": {
|
|
"download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json",
|
|
"file_name": "{episode_file_path}.{ext}",
|
|
"info_json_name": "{episode_file_path}.{info_json_ext}",
|
|
"keep_files_date_eval": "{episode_date_standardized}",
|
|
"maintain_download_archive": True,
|
|
"output_directory": f"{output_directory}/NOVA PBS",
|
|
"preserve_mtime": False,
|
|
"thumbnail_name": "{thumbnail_file_name}",
|
|
},
|
|
'overrides': {'%bilateral_url': '{ %if( %and( enable_bilateral_scraping, '
|
|
'subscription_has_download_archive, '
|
|
"%is_bilateral_url( $0 ) ), $0, '' ) }",
|
|
'%episode_ordering_': '{ %eq( %lower( '
|
|
'tv_show_by_date_episode_ordering ), $0 ) '
|
|
'}',
|
|
'%is_bilateral_url': '{ %contains( $0, "youtube.com/playlist" ) '
|
|
'}',
|
|
'%ordering_pair_eq': '{ %eq( [ tv_show_by_date_season_ordering, '
|
|
'tv_show_by_date_episode_ordering ], [ $0, '
|
|
'$1 ] ) }',
|
|
'%season_ordering_': '{ %eq( %lower( '
|
|
'tv_show_by_date_season_ordering ), $0 ) }',
|
|
'assert_not_collection': True,
|
|
'avatar_uncropped_thumbnail_file_name': 'poster.jpg',
|
|
'banner_uncropped_thumbnail_file_name': 'fanart.jpg',
|
|
'enable_bilateral_scraping': True,
|
|
'enable_resolution_assert': True,
|
|
'enable_throttle_protection': True,
|
|
'episode_content_rating': 'TV-14',
|
|
'episode_date_standardized': '{ upload_date_standardized }',
|
|
'episode_file_name': 's{ upload_year }.e{ %pad_zero( %int( '
|
|
'%concat( upload_month, upload_day_padded, '
|
|
'upload_date_index_padded ) ), 6 ) } - { '
|
|
'title_sanitized_plex }',
|
|
'episode_file_path': '{ %sanitize( %concat( "Season ", %string( '
|
|
'upload_year ) ) ) }/{ %sanitize( %concat( '
|
|
'"s", %string( upload_year ), ".e", '
|
|
'%string( %pad_zero( %int( %concat( '
|
|
'upload_month, upload_day_padded, '
|
|
'upload_date_index_padded ) ), 6 ) ), " - '
|
|
'", %string( title_sanitized_plex ) ) ) }',
|
|
'episode_number': '{ upload_month }{ upload_day_padded }{ '
|
|
'upload_date_index_padded }',
|
|
'episode_number_and_padded_': '{ [ %concat( upload_month, '
|
|
'upload_day_padded, '
|
|
'upload_date_index_padded ), 6 ] '
|
|
'}',
|
|
'episode_number_padded': '{ %pad_zero( %int( %concat( '
|
|
'upload_month, upload_day_padded, '
|
|
'upload_date_index_padded ) ), 6 ) }',
|
|
'episode_plot': '{ webpage_url }\n\n{ description }',
|
|
'episode_title': '{ upload_date_standardized } - { title }',
|
|
'episode_year': '{ %slice( upload_date_standardized, 0, 4 ) }',
|
|
'file_title': '{ title_sanitized_plex }',
|
|
'file_uid': '{ uid_sanitized_plex }',
|
|
'include_sibling_metadata': False,
|
|
'modified_webpage_url': '{ webpage_url }',
|
|
'music_directory': '/music',
|
|
'music_video_directory': '/music_videos',
|
|
'resolution_assert': '{ %if( %and( enable_resolution_assert, '
|
|
'%ne( height, 0 ), %not( '
|
|
'resolution_assert_is_ignored ) ), '
|
|
'%assert( %gte( height, '
|
|
'resolution_assert_height_gte ), %concat( '
|
|
'"Entry ", title, " downloaded at a low '
|
|
'resolution (", resolution_readable, "), '
|
|
'you\'ve probably been throttled. ", '
|
|
'"Stopping further downloads, wait a few '
|
|
'hours and try again. ", "Disable using '
|
|
'the override variable '
|
|
'`enable_resolution_assert: False`." ) ), '
|
|
'"false is no-op" ) }',
|
|
'resolution_assert_height_gte': 361,
|
|
'resolution_assert_ignore_titles': '{ [ ] }',
|
|
'resolution_assert_is_ignored': '{ %print_if_true( %concat( '
|
|
'title, " has a match in '
|
|
'resolution_assert_ignore_titles, '
|
|
'skipping resolution assert." '
|
|
'), %contains_any( title, '
|
|
'resolution_assert_ignore_titles '
|
|
') ) }',
|
|
'resolution_assert_print': True,
|
|
'resolution_readable': '{ width }x{ height }',
|
|
's01_name': '',
|
|
's01_url': '',
|
|
'season_directory_name': 'Season { upload_year }',
|
|
'season_number': '{ upload_year }',
|
|
'season_number_padded': '{ upload_year }',
|
|
'season_poster_file_name': '{ %sanitize( %concat( "Season ", '
|
|
'%string( upload_year ) ) ) '
|
|
'}/Season{ upload_year }.jpg',
|
|
'subscription_array': ['https://www.youtube.com/@novapbs'],
|
|
'subscription_indent_1': 'Documentaries',
|
|
'subscription_indent_2': 'TV-14',
|
|
'subscription_value': 'https://www.youtube.com/@novapbs',
|
|
'subscription_value_1': 'https://www.youtube.com/@novapbs',
|
|
'thumbnail_file_name': '{ %concat( %string( %sanitize( %concat( '
|
|
'"Season ", %string( upload_year ) ) ) '
|
|
'), "/", %string( %sanitize( %concat( '
|
|
'"s", %string( upload_year ), ".e", '
|
|
'%string( %pad_zero( %int( %concat( '
|
|
'upload_month, upload_day_padded, '
|
|
'upload_date_index_padded ) ), 6 ) ), " '
|
|
'- ", %string( title_sanitized_plex ) ) '
|
|
') ) ) }-thumb.jpg',
|
|
'tv_show_by_date_episode_ordering': 'upload-month-day',
|
|
'tv_show_by_date_ordering_pair_validation_': '{ [ %concat( '
|
|
'upload_month, '
|
|
'upload_day_padded, '
|
|
'upload_date_index_padded '
|
|
'), 6 ] }',
|
|
'tv_show_by_date_season_ordering': 'upload-year',
|
|
'tv_show_content_rating': 'TV-14',
|
|
'tv_show_content_rating_default': 'TV-14',
|
|
'tv_show_date_range_type': 'upload_date',
|
|
'tv_show_directory': output_directory,
|
|
'tv_show_fanart_file_name': 'fanart.jpg',
|
|
'tv_show_genre': 'Documentaries',
|
|
'tv_show_genre_default': 'ytdl-sub',
|
|
'tv_show_name': 'NOVA PBS',
|
|
'tv_show_poster_file_name': 'poster.jpg',
|
|
'url': 'https://www.youtube.com/@novapbs',
|
|
'url10': '',
|
|
'url100': '',
|
|
'url11': '',
|
|
'url12': '',
|
|
'url13': '',
|
|
'url14': '',
|
|
'url15': '',
|
|
'url16': '',
|
|
'url17': '',
|
|
'url18': '',
|
|
'url19': '',
|
|
'url2': '',
|
|
'url20': '',
|
|
'url21': '',
|
|
'url22': '',
|
|
'url23': '',
|
|
'url24': '',
|
|
'url25': '',
|
|
'url26': '',
|
|
'url27': '',
|
|
'url28': '',
|
|
'url29': '',
|
|
'url3': '',
|
|
'url30': '',
|
|
'url31': '',
|
|
'url32': '',
|
|
'url33': '',
|
|
'url34': '',
|
|
'url35': '',
|
|
'url36': '',
|
|
'url37': '',
|
|
'url38': '',
|
|
'url39': '',
|
|
'url4': '',
|
|
'url40': '',
|
|
'url41': '',
|
|
'url42': '',
|
|
'url43': '',
|
|
'url44': '',
|
|
'url45': '',
|
|
'url46': '',
|
|
'url47': '',
|
|
'url48': '',
|
|
'url49': '',
|
|
'url5': '',
|
|
'url50': '',
|
|
'url51': '',
|
|
'url52': '',
|
|
'url53': '',
|
|
'url54': '',
|
|
'url55': '',
|
|
'url56': '',
|
|
'url57': '',
|
|
'url58': '',
|
|
'url59': '',
|
|
'url6': '',
|
|
'url60': '',
|
|
'url61': '',
|
|
'url62': '',
|
|
'url63': '',
|
|
'url64': '',
|
|
'url65': '',
|
|
'url66': '',
|
|
'url67': '',
|
|
'url68': '',
|
|
'url69': '',
|
|
'url7': '',
|
|
'url70': '',
|
|
'url71': '',
|
|
'url72': '',
|
|
'url73': '',
|
|
'url74': '',
|
|
'url75': '',
|
|
'url76': '',
|
|
'url77': '',
|
|
'url78': '',
|
|
'url79': '',
|
|
'url8': '',
|
|
'url80': '',
|
|
'url81': '',
|
|
'url82': '',
|
|
'url83': '',
|
|
'url84': '',
|
|
'url85': '',
|
|
'url86': '',
|
|
'url87': '',
|
|
'url88': '',
|
|
'url89': '',
|
|
'url9': '',
|
|
'url90': '',
|
|
'url91': '',
|
|
'url92': '',
|
|
'url93': '',
|
|
'url94': '',
|
|
'url95': '',
|
|
'url96': '',
|
|
'url97': '',
|
|
'url98': '',
|
|
'url99': '',
|
|
'urls': ['https://www.youtube.com/@novapbs']},
|
|
"throttle_protection": {
|
|
"enable": True,
|
|
"sleep_per_download_s": {"max": "28.4", "min": "13.8"},
|
|
"sleep_per_request_s": {"max": "0.75", "min": "0.0"},
|
|
"sleep_per_subscription_s": {"max": "26.1", "min": "16.3"},
|
|
},
|
|
"video_tags": {
|
|
"contentRating": "{episode_content_rating}",
|
|
"date": "{episode_date_standardized}",
|
|
"episode_id": "{episode_number}",
|
|
"genre": "{tv_show_genre}",
|
|
"show": "{tv_show_name}",
|
|
"synopsis": "{episode_plot}",
|
|
"title": "{episode_title}",
|
|
"year": "{episode_year}",
|
|
},
|
|
} |