feat(dl): add --no-postscript to skip post-scripts for one run

This commit is contained in:
imSp4rky
2026-09-06 21:02:58 -06:00
parent 80f2b8f5b4
commit d36797641d
6 changed files with 46 additions and 2 deletions
+1
View File
@@ -149,6 +149,7 @@ Keep only certain track types, or skip certain track types.
| `--tag` | Group tag override. | | `--tag` | Group tag override. |
| `--repack` | Add a `REPACK` tag to the filename. | | `--repack` | Add a `REPACK` tag to the filename. |
| `--postscript` | Run a command after each output file, with `{variable}` placeholders substituted. Repeatable. Replaces the `post_scripts` config for this run. See [Post-scripts](../reference/configuration/post-scripts.md). | | `--postscript` | Run a command after each output file, with `{variable}` placeholders substituted. Repeatable. Replaces the `post_scripts` config for this run. See [Post-scripts](../reference/configuration/post-scripts.md). |
| `--no-postscript` | Run no post-script for this run, neither the configured ones nor `--postscript`. |
### Metadata & tagging ### Metadata & tagging
+8 -1
View File
@@ -698,6 +698,13 @@ that run writes no muxed output. A `failure` post-script still operates if the d
unshackle dl --postscript "python /opt/upload.py {filepath} --service={service}" EXAMPLE 81234567 unshackle dl --postscript "python /opt/upload.py {filepath} --service={service}" EXAMPLE 81234567
``` ```
`--no-postscript` operates the run with no post-script at all. It silences the `post_scripts`
config and `--postscript` together, for every event and mode.
```shell title="Skip the uploader for one run"
unshackle dl --no-postscript EXAMPLE 81234567
```
For post-scripts that continue across runs, for the `season` and `run` modes, for `failure` For post-scripts that continue across runs, for the `season` and `run` modes, for `failure`
post-scripts and for the full variable list, see post-scripts and for the full variable list, see
[Post-scripts](../reference/configuration/post-scripts.md). [Post-scripts](../reference/configuration/post-scripts.md).
@@ -946,7 +953,7 @@ authoritative list.
| `--best-available` | | Degrade gracefully instead of failing. | | `--best-available` | | Degrade gracefully instead of failing. |
| `--output` | `-o` | Output directory for this run. | | `--output` | `-o` | Output directory for this run. |
| `--split-audio` / `--merge-video` / `--no-mux` | | Muxing behaviour. | | `--split-audio` / `--merge-video` / `--no-mux` | | Muxing behaviour. |
| `--postscript` | | Run a command after each output file. Repeatable. | | `--postscript` / `--no-postscript` | | Run a command after each output file, or run none. |
| `--proxy` / `--no-proxy` / `--no-proxy-download` | | Proxy control. | | `--proxy` / `--no-proxy` / `--no-proxy-download` | | Proxy control. |
| `--workers` / `--downloads` / `--slow` | | Concurrency and pacing. | | `--workers` / `--downloads` / `--slow` | | Concurrency and pacing. |
| `--list` / `--list-titles` / `--skip-dl` | | Dry runs. | | `--list` / `--list-titles` / `--skip-dl` | | Dry runs. |
@@ -282,6 +282,16 @@ It replaces the `post_scripts` config for that run rather than adding to it, so
configured entry runs while it is in use. That includes your `season`, `run` and `failure` configured entry runs while it is in use. That includes your `season`, `run` and `failure`
entries, which have no `--postscript` equivalent. entries, which have no `--postscript` equivalent.
## `--no-postscript`
`dl --no-postscript` operates the run with no post-script. It silences every configured entry
and every `--postscript` command, for each event and mode. Use it to skip an uploader or a
notifier for one run without an edit to `unshackle.yaml`.
```console
$ unshackle dl --no-postscript SERVICE TITLE_ID
```
## The REST API ## The REST API
The API never accepts a post-script command. `postscript`, `post_script` and `post_scripts` The API never accepts a post-script command. `postscript`, `post_script` and `post_scripts`
+15
View File
@@ -7,8 +7,12 @@ from types import SimpleNamespace
import pytest import pytest
import unshackle.core.utils.post_scripts as ps
from unshackle.core.config import config from unshackle.core.config import config
from unshackle.core.utils.post_scripts import ( from unshackle.core.utils.post_scripts import (
EVENTS,
MODES,
NO_POST_SCRIPTS,
SIDECAR_SEPARATOR, SIDECAR_SEPARATOR,
_entries, _entries,
build_context, build_context,
@@ -217,3 +221,14 @@ def test_wait_true_waits_for_the_script_and_logs_the_exit_code(monkeypatch, capl
def test_wait_defaults_to_fire_and_forget(monkeypatch): def test_wait_defaults_to_fire_and_forget(monkeypatch):
slow = 'python -c "import time; time.sleep(0.3)"' slow = 'python -c "import time; time.sleep(0.3)"'
assert _dispatch_seconds(monkeypatch, {"command": slow}) < 0.3 assert _dispatch_seconds(monkeypatch, {"command": slow}) < 0.3
def test_no_postscript_override_silences_every_event_and_mode(monkeypatch):
"""``dl --no-postscript`` passes NO_POST_SCRIPTS as the override list. Nothing may spawn."""
monkeypatch.setattr(config, "post_scripts", [{"command": "should_not_run.py", "mode": mode} for mode in MODES])
spawned: list[list[str]] = []
monkeypatch.setattr(ps.subprocess, "Popen", lambda argv, **kw: spawned.append(argv))
for event in EVENTS:
for mode in MODES:
dispatch(event, mode, {"filepath": "/x/y.mkv"}, NO_POST_SCRIPTS)
assert spawned == []
+10 -1
View File
@@ -99,7 +99,7 @@ from unshackle.core.utils.click_types import (
SubtitleCodecChoice, SubtitleCodecChoice,
) )
from unshackle.core.utils.collections import ci_get, merge_dict from unshackle.core.utils.collections import ci_get, merge_dict
from unshackle.core.utils.post_scripts import build_context, dispatch, season_context from unshackle.core.utils.post_scripts import NO_POST_SCRIPTS, build_context, dispatch, season_context
from unshackle.core.utils.redact import mask_proxy from unshackle.core.utils.redact import mask_proxy
from unshackle.core.utils.selector import select_multiple from unshackle.core.utils.selector import select_multiple
from unshackle.core.utils.subprocess import ffprobe from unshackle.core.utils.subprocess import ffprobe
@@ -875,6 +875,12 @@ class dl:
"variables substituted. Repeatable. Replaces the post_scripts config for this run." "variables substituted. Repeatable. Replaces the post_scripts config for this run."
), ),
) )
@click.option(
"--no-postscript",
is_flag=True,
default=False,
help="Do not run any post-script for this run, configured or --postscript.",
)
@click.option( @click.option(
"--workers", "--workers",
type=int, type=int,
@@ -1533,9 +1539,12 @@ class dl:
real_audio_bitrate: bool = False, real_audio_bitrate: bool = False,
progress_sink: Optional[Callable[[dict[str, Any]], None]] = None, progress_sink: Optional[Callable[[dict[str, Any]], None]] = None,
postscript: Sequence[str] = (), postscript: Sequence[str] = (),
no_postscript: bool = False,
*_: Any, *_: Any,
**__: Any, **__: Any,
) -> None: ) -> None:
if no_postscript:
postscript = NO_POST_SCRIPTS
if continue_downloads: if continue_downloads:
config.continue_downloads = True config.continue_downloads = True
self.tmdb_searched = False self.tmdb_searched = False
+2
View File
@@ -35,6 +35,8 @@ EVENTS = ("success", "failure")
MODES = ("file", "season", "run") MODES = ("file", "season", "run")
SIDECAR_SEPARATOR = "\n" SIDECAR_SEPARATOR = "\n"
NO_POST_SCRIPTS: tuple[str, ...] = ("",)
"""``dl --no-postscript``: an override list that replaces the config and holds no command to run."""
_warned_entries: set[str] = set() _warned_entries: set[str] = set()