Merge branch 'master' into j/staged-helper
This commit is contained in:
commit
3680f98a42
11 changed files with 111 additions and 69 deletions
|
|
@ -30,14 +30,14 @@ You can modularize your presets via preset inheritance. For example,
|
||||||
presets:
|
presets:
|
||||||
|
|
||||||
TV Show:
|
TV Show:
|
||||||
presets:
|
preset:
|
||||||
- "Jellyfin TV Show by Date"
|
- "Jellyfin TV Show by Date"
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
tv_show_directory: "/ytdl_sub_tv_shows"
|
tv_show_directory: "/ytdl_sub_tv_shows"
|
||||||
|
|
||||||
TV Show Only Recent:
|
TV Show Only Recent:
|
||||||
presets:
|
preset:
|
||||||
- "TV Show"
|
- "TV Show"
|
||||||
- "Only Recent"
|
- "Only Recent"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ Docker and Unraid
|
||||||
|
|
||||||
.. tab-item:: GUI Image
|
.. tab-item:: GUI Image
|
||||||
|
|
||||||
The script that will execute automatically is located at ``/config/ytdl-sub-configs/run-cron``.
|
The script that will execute automatically is located at ``/config/ytdl-sub-configs/run_cron``.
|
||||||
|
|
||||||
Access your container at http://localhost:8443/, then in the GUI terminal run these commands:
|
Access your container at http://localhost:8443/, then in the GUI terminal run these commands:
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ Docker and Unraid
|
||||||
|
|
||||||
docker compose restart
|
docker compose restart
|
||||||
|
|
||||||
The script that will execute automatically is located at ``/config/run-cron``.
|
The script that will execute automatically is located at ``/config/run_cron``.
|
||||||
|
|
||||||
Access your container from the terminal by running:
|
Access your container from the terminal by running:
|
||||||
|
|
||||||
|
|
@ -93,13 +93,13 @@ Docker and Unraid
|
||||||
|
|
||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
echo '#!/bin/bash' > /config/ytdl-sub-configs/run_cron
|
echo '#!/bin/bash' > /config/run_cron
|
||||||
echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" >> /config/ytdl-sub-configs/run_cron
|
echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" >> /config/run_cron
|
||||||
echo "echo 'Cron started, running ytdl-sub...'" >> /config/ytdl-sub-configs/run_cron
|
echo "echo 'Cron started, running ytdl-sub...'" >> /config/run_cron
|
||||||
echo "cd /config/ytdl-sub-configs" >> /config/ytdl-sub-configs/run_cron
|
echo "cd /config" >> /config/run_cron
|
||||||
echo "ytdl-sub --config=config.yaml sub subscriptions.yaml" >> /config/ytdl-sub-configs/run_cron
|
echo "ytdl-sub --config=config.yaml sub subscriptions.yaml" >> /config/run_cron
|
||||||
chmod +x /config/ytdl-sub-configs/run_cron
|
chmod +x /config/run_cron
|
||||||
chown abc:abc /config/ytdl-sub-configs/run_cron
|
chown abc:abc /config/run_cron
|
||||||
|
|
||||||
You can test the newly created script by running:
|
You can test the newly created script by running:
|
||||||
|
|
||||||
|
|
@ -137,4 +137,4 @@ To be tested (please contact code owner or join the discord server if you can te
|
||||||
|
|
||||||
.. code-block:: powershell
|
.. code-block:: powershell
|
||||||
|
|
||||||
ytdl-sub.exe --config \path\to\config\config.yaml sub \path\to\config\subscriptions.yaml
|
ytdl-sub.exe --config \path\to\config\config.yaml sub \path\to\config\subscriptions.yaml
|
||||||
|
|
|
||||||
|
|
@ -49,22 +49,22 @@ Docker Compose is an easy "set it and forget it" install method. Follow the inst
|
||||||
:caption: compose.yaml
|
:caption: compose.yaml
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ytdl-sub:
|
ytdl-sub:
|
||||||
image: ghcr.io/jmbannon/ytdl-sub-gui:latest
|
image: ghcr.io/jmbannon/ytdl-sub-gui:latest
|
||||||
container_name: ytdl-sub
|
container_name: ytdl-sub
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
- TZ=America/Los_Angeles
|
- TZ=America/Los_Angeles
|
||||||
volumes:
|
volumes:
|
||||||
- <path/to/ytdl-sub/config>:/config
|
- <path/to/ytdl-sub/config>:/config
|
||||||
- <path/to/tv_shows>:/tv_shows # optional
|
- <path/to/tv_shows>:/tv_shows # optional
|
||||||
- <path/to/movies>:/movies # optional
|
- <path/to/movies>:/movies # optional
|
||||||
- <path/to/music_videos>:/music_videos # optional
|
- <path/to/music_videos>:/music_videos # optional
|
||||||
- <path/to/music>:/music # optional
|
- <path/to/music>:/music # optional
|
||||||
ports:
|
ports:
|
||||||
- 8443:8443
|
- 8443:8443
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
.. tab-item:: Headless Image
|
.. tab-item:: Headless Image
|
||||||
|
|
||||||
|
|
@ -72,21 +72,21 @@ Docker Compose is an easy "set it and forget it" install method. Follow the inst
|
||||||
:caption: compose.yaml
|
:caption: compose.yaml
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ytdl-sub:
|
ytdl-sub:
|
||||||
image: ghcr.io/jmbannon/ytdl-sub:latest
|
image: ghcr.io/jmbannon/ytdl-sub:latest
|
||||||
container_name: ytdl-sub
|
container_name: ytdl-sub
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
- TZ=America/Los_Angeles
|
- TZ=America/Los_Angeles
|
||||||
- DOCKER_MODS=linuxserver/mods:universal-cron
|
- DOCKER_MODS=linuxserver/mods:universal-cron
|
||||||
volumes:
|
volumes:
|
||||||
- <path/to/ytdl-sub/config>:/config
|
- <path/to/ytdl-sub/config>:/config
|
||||||
- <path/to/tv_shows>:/tv_shows # optional
|
- <path/to/tv_shows>:/tv_shows # optional
|
||||||
- <path/to/movies>:/movies # optional
|
- <path/to/movies>:/movies # optional
|
||||||
- <path/to/music_videos>:/music_videos # optional
|
- <path/to/music_videos>:/music_videos # optional
|
||||||
- <path/to/music>:/music # optional
|
- <path/to/music>:/music # optional
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
Device Passthrough
|
Device Passthrough
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
@ -120,19 +120,19 @@ GPU Passthrough
|
||||||
:emphasize-lines: 5-13
|
:emphasize-lines: 5-13
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ytdl-sub:
|
ytdl-sub:
|
||||||
image: ghcr.io/jmbannon/ytdl-sub-gui:latest
|
image: ghcr.io/jmbannon/ytdl-sub-gui:latest
|
||||||
container_name: ytdl-sub
|
container_name: ytdl-sub
|
||||||
environment:
|
environment:
|
||||||
- ..
|
- ..
|
||||||
- NVIDIA_DRIVER_CAPABILITIES=all # Nvidia ENV args
|
- NVIDIA_DRIVER_CAPABILITIES=all # Nvidia ENV args
|
||||||
- NVIDIA_VISIBLE_DEVICES=all
|
- NVIDIA_VISIBLE_DEVICES=all
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
reservations:
|
reservations:
|
||||||
devices:
|
devices:
|
||||||
- capabilities: ["gpu"] # GPU passthrough
|
- capabilities: ["gpu"] # GPU passthrough
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
Docker CLI
|
Docker CLI
|
||||||
----------
|
----------
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME
|
from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME
|
||||||
|
from ytdl_sub.config.overrides import Overrides
|
||||||
from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator
|
from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator
|
||||||
from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator
|
from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator
|
||||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||||
|
|
@ -48,6 +50,16 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator):
|
||||||
where each key is a ytdl argument. Include in the example are some popular ytdl_options.
|
where each key is a ytdl argument. Include in the example are some popular ytdl_options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def to_native_dict(self, overrides: Overrides) -> Dict:
|
||||||
|
"""
|
||||||
|
Materializes the entire ytdl-options dict from OverrideStringFormatters into
|
||||||
|
native python
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
key: overrides.apply_overrides_formatter_to_native(val)
|
||||||
|
for key, val in self.dict.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Disable for proper docstring formatting
|
# Disable for proper docstring formatting
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
||||||
|
|
||||||
def _download_metadata(self, url: str, validator: UrlValidator) -> Iterable[Entry]:
|
def _download_metadata(self, url: str, validator: UrlValidator) -> Iterable[Entry]:
|
||||||
metadata_ytdl_options = self.metadata_ytdl_options(
|
metadata_ytdl_options = self.metadata_ytdl_options(
|
||||||
ytdl_option_overrides=validator.ytdl_options.dict
|
ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides)
|
||||||
)
|
)
|
||||||
download_reversed = ScriptUtils.bool_formatter_output(
|
download_reversed = ScriptUtils.bool_formatter_output(
|
||||||
self.overrides.apply_formatter(validator.download_reverse)
|
self.overrides.apply_formatter(validator.download_reverse)
|
||||||
|
|
|
||||||
|
|
@ -301,8 +301,16 @@ class _Parser:
|
||||||
self._pos += len(str_open_token)
|
self._pos += len(str_open_token)
|
||||||
return String(value=string_value)
|
return String(value=string_value)
|
||||||
|
|
||||||
self._pos += 1
|
# Read literal "\n" as newlines
|
||||||
string_value += ch
|
if self._read(increment_pos=False, length=2) == "\\n":
|
||||||
|
string_value += "\n"
|
||||||
|
self._pos += 2
|
||||||
|
elif self._read(increment_pos=False, length=2) == "\\t":
|
||||||
|
string_value += "\t"
|
||||||
|
self._pos += 2
|
||||||
|
else:
|
||||||
|
self._pos += 1
|
||||||
|
string_value += ch
|
||||||
|
|
||||||
raise STRINGS_NOT_CLOSED
|
raise STRINGS_NOT_CLOSED
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -490,15 +490,18 @@ class Script:
|
||||||
name: definition for name, definition in variables.items() if not _is_function(name)
|
name: definition for name, definition in variables.items() if not _is_function(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
custom_function_names = set(self._functions.keys()) | functions_to_add.keys()
|
||||||
|
variable_names = (
|
||||||
|
set(self._variables.keys()) | variables_to_add.keys() | (unresolvable or set())
|
||||||
|
)
|
||||||
|
|
||||||
for definitions in [functions_to_add, variables_to_add]:
|
for definitions in [functions_to_add, variables_to_add]:
|
||||||
for name, definition in definitions.items():
|
for name, definition in definitions.items():
|
||||||
parsed = parse(
|
parsed = parse(
|
||||||
text=definition,
|
text=definition,
|
||||||
name=name,
|
name=name,
|
||||||
custom_function_names=set(self._functions.keys()),
|
custom_function_names=custom_function_names,
|
||||||
variable_names=set(self._variables.keys())
|
variable_names=variable_names,
|
||||||
.union(variables.keys())
|
|
||||||
.union(unresolvable or set()),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if parsed.maybe_resolvable is None:
|
if parsed.maybe_resolvable is None:
|
||||||
|
|
|
||||||
|
|
@ -110,11 +110,7 @@ class SubscriptionYTDLOptions:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _user_ytdl_options(self) -> Dict:
|
def _user_ytdl_options(self) -> Dict:
|
||||||
native_ytdl_options = {
|
return self._preset.ytdl_options.to_native_dict(self._overrides)
|
||||||
key: self._overrides.apply_overrides_formatter_to_native(val)
|
|
||||||
for key, val in self._preset.ytdl_options.dict.items()
|
|
||||||
}
|
|
||||||
return native_ytdl_options
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _plugin_match_filters(self) -> Dict:
|
def _plugin_match_filters(self) -> Dict:
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ class TestNumericFunctions:
|
||||||
("no splits", " | ", None, ["no splits"]),
|
("no splits", " | ", None, ["no splits"]),
|
||||||
("one | split", " | ", None, ["one", "split"]),
|
("one | split", " | ", None, ["one", "split"]),
|
||||||
("max | split | one", " | ", 1, ["max", "split | one"]),
|
("max | split | one", " | ", 1, ["max", "split | one"]),
|
||||||
|
("multiline\ndescription", "\\n", None, ["multiline", "description"]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_split(
|
def test_split(
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,24 @@ class TestCustomFunction:
|
||||||
}
|
}
|
||||||
).resolve() == ScriptOutput({"output": Integer(9)})
|
).resolve() == ScriptOutput({"output": Integer(9)})
|
||||||
|
|
||||||
|
def test_custom_functions_any_order_via_add(self):
|
||||||
|
assert Script({}).add(
|
||||||
|
{
|
||||||
|
"%custom_cubed": "{%mul(%custom_square($0),$0)}",
|
||||||
|
"%custom_square": "{%mul($0, $0)}",
|
||||||
|
"output": "{%custom_cubed(3)}",
|
||||||
|
}
|
||||||
|
).resolve() == ScriptOutput({"output": Integer(27)})
|
||||||
|
|
||||||
|
def test_custom_functions_any_order_via_init(self):
|
||||||
|
assert Script(
|
||||||
|
{
|
||||||
|
"%custom_cubed": "{%mul(%custom_square($0),$0)}",
|
||||||
|
"%custom_square": "{%mul($0, $0)}",
|
||||||
|
"output": "{%custom_cubed(3)}",
|
||||||
|
}
|
||||||
|
).resolve() == ScriptOutput({"output": Integer(27)})
|
||||||
|
|
||||||
def test_custom_function_cycle(self):
|
def test_custom_function_cycle(self):
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
CycleDetected, match=re.escape("The custom function %cycle_func cannot call itself.")
|
CycleDetected, match=re.escape("The custom function %cycle_func cannot call itself.")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from unit.script.conftest import single_variable_output
|
||||||
|
|
||||||
from ytdl_sub.script.parser import STRINGS_NOT_CLOSED
|
from ytdl_sub.script.parser import STRINGS_NOT_CLOSED
|
||||||
from ytdl_sub.script.parser import STRINGS_ONLY_ARGS
|
from ytdl_sub.script.parser import STRINGS_ONLY_ARGS
|
||||||
|
|
@ -45,10 +46,13 @@ class TestString:
|
||||||
("{%string('backslash \\\\')}", "backslash \\\\"),
|
("{%string('backslash \\\\')}", "backslash \\\\"),
|
||||||
("{%string('''triple quote with \" ' \\''')}", "triple quote with \" ' \\"),
|
("{%string('''triple quote with \" ' \\''')}", "triple quote with \" ' \\"),
|
||||||
('{%string("""triple quote with " \' \\""")}', "triple quote with \" ' \\"),
|
('{%string("""triple quote with " \' \\""")}', "triple quote with \" ' \\"),
|
||||||
|
("{%string('literal \\n newlines')}", "literal \n newlines"),
|
||||||
|
("{%string('supports \t tabs')}", "supports \t tabs"),
|
||||||
|
("{%string('literal \\t tabs')}", "literal \t tabs"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_string(self, string: str, expected_string: str):
|
def test_string(self, string: str, expected_string: str):
|
||||||
assert Script({"out": string}).resolve() == ScriptOutput({"out": String(expected_string)})
|
assert single_variable_output(string) == expected_string
|
||||||
|
|
||||||
def test_null_is_empty_string(self):
|
def test_null_is_empty_string(self):
|
||||||
assert Script({"out": "{%string(null)}"}).resolve() == ScriptOutput({"out": String("")})
|
assert Script({"out": "{%string(null)}"}).resolve() == ScriptOutput({"out": String("")})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue