From b94f33861902b33ab432e0465009700c93163611 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Tue, 25 Aug 2026 19:57:41 -0600 Subject: [PATCH] docs(post-scripts): use post-script as the one term for hooks Add the post-script row to the ste-writing terminology contract and replace hook/postscript/post-download script across docs and docstrings. --- docs/dev/rest-api/endpoints.md | 4 +- docs/getting-started/configuration-file.md | 2 +- docs/guide/cli-reference.md | 2 +- docs/guide/downloading.md | 10 +-- docs/reference/configuration.md | 2 +- docs/reference/configuration/index.md | 2 +- docs/reference/configuration/post-scripts.md | 64 ++++++++++---------- mkdocs.yml | 2 +- tests/test_post_scripts_music.py | 22 +++---- unshackle/core/api/routes.py | 2 +- unshackle/core/utils/post_scripts.py | 2 +- 11 files changed, 57 insertions(+), 57 deletions(-) diff --git a/docs/dev/rest-api/endpoints.md b/docs/dev/rest-api/endpoints.md index 0865a368..d9cf52de 100644 --- a/docs/dev/rest-api/endpoints.md +++ b/docs/dev/rest-api/endpoints.md @@ -425,9 +425,9 @@ Make a download job. It requires `service` and `title_id`. Every other field is !!! danger "Rejected parameters" `postscript`, `post_script` and `post_scripts` are never accepted. A body containing any of them gets `400 INVALID_PARAMETERS`, because a command string that arrives from - an HTTP caller would be remote code execution. You configure post-download hooks only in + an HTTP caller would be remote code execution. You configure post-scripts only in `unshackle.yaml`, where they still operate for API jobs. See - [Post-download scripts](../../reference/configuration/post-scripts.md). + [Post-scripts](../../reference/configuration/post-scripts.md). !!! warning "Gated parameters (developer)" The server accepts a few keys, but permits them only when the server config opts in. Otherwise the server rejects the request with `403 FORBIDDEN`: diff --git a/docs/getting-started/configuration-file.md b/docs/getting-started/configuration-file.md index f0ab48b7..ec2a3e02 100644 --- a/docs/getting-started/configuration-file.md +++ b/docs/getting-started/configuration-file.md @@ -87,7 +87,7 @@ The top-level keys group loosely into these areas: | Naming & tagging | `tag`, `output_template`, `chapter_fallback_name` | | External API keys | `tmdb_api_key`, `tvdb_api_key`, `simkl_client_id`, `ipinfo_api_key`, `metadata_providers` | | Behaviour & logging | `update_checks`, `redact_paths`, `debug`, `unicode_filenames` | -| Post-download scripts | `post_scripts` | +| Post-scripts | `post_scripts` | | Paths | `directories`, `filenames` | For the full list with types and defaults, see the [Configuration Reference](../reference/configuration/index.md). diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md index 10c5dc06..de323e04 100644 --- a/docs/guide/cli-reference.md +++ b/docs/guide/cli-reference.md @@ -148,7 +148,7 @@ Keep only certain track types, or skip certain track types. | `--no-mux` | Do not mux; keep individual track files. | | `--tag` | Group tag override. | | `--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-download 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). | ### Metadata & tagging diff --git a/docs/guide/downloading.md b/docs/guide/downloading.md index e1e14aff..4e34b980 100644 --- a/docs/guide/downloading.md +++ b/docs/guide/downloading.md @@ -686,16 +686,16 @@ By default, `mkvmerge` muxes the tracks of a movie or an episode into a single M `--postscript ""` runs your own command once per output file, with unshackle's metadata substituted into `{variable}` placeholders. It is repeatable, and it replaces the -`post_scripts` config for that run. No `success` hook operates under `--no-mux`, because -that run writes no muxed output. A `failure` hook still operates if the download fails. +`post_scripts` config for that run. No `success` post-script operates under `--no-mux`, because +that run writes no muxed output. A `failure` post-script still operates if the download fails. ```shell title="Hand each finished file to an uploader" unshackle dl --postscript "python /opt/upload.py {filepath} --service={service}" EXAMPLE 81234567 ``` -For hooks that continue across runs, for the `season` and `run` modes, for `failure` hooks -and for the full variable list, see -[Post-download scripts](../reference/configuration/post-scripts.md). +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](../reference/configuration/post-scripts.md). ### Naming tags diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 0ac1bf50..14113d03 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -14,6 +14,6 @@ This page has moved. The configuration reference is now split by area under | DRM & CDM | [configuration/drm.md](configuration/drm.md) | | Network & proxy | [configuration/network.md](configuration/network.md) | | Key vaults | [configuration/vaults.md](configuration/vaults.md) | -| Post-download scripts | [configuration/post-scripts.md](configuration/post-scripts.md) | +| Post-scripts | [configuration/post-scripts.md](configuration/post-scripts.md) | | Other settings | [configuration/misc.md](configuration/misc.md) | | Complete annotated example | [configuration/example.md](configuration/example.md) | diff --git a/docs/reference/configuration/index.md b/docs/reference/configuration/index.md index 2fceb625..4b3dff1f 100644 --- a/docs/reference/configuration/index.md +++ b/docs/reference/configuration/index.md @@ -36,7 +36,7 @@ Where the file lives, how unshackle discovers it, and how to edit it: | [DRM & CDM](drm.md) | `cdm`, `remote_cdm`, `decryption` | | [Network & proxy](network.md) | `network`, `headers`, `proxy_providers` | | [Key vaults](vaults.md) | `key_vaults`, `vault_timeout` | -| [Post-download scripts](post-scripts.md) | `post_scripts` | +| [Post-scripts](post-scripts.md) | `post_scripts` | | [External API keys](misc.md#external-api-keys) | `omdb_api_key`, `tmdb_api_key`, `tvdb_api_key`, `tvdb_pin`, `tvdb_order`, `simkl_client_id`, `metadata_providers`, `anilist_title_language`, `disable_metadata`, `decrypt_labs_api_key`, `ipinfo_api_key` | | [Caching & updates](misc.md#caching-updates) | `title_cache_enabled`, `title_cache_time`, `title_cache_max_retention`, `update_checks`, `update_check_interval` | | [Appearance](misc.md#appearance) | `theme` | diff --git a/docs/reference/configuration/post-scripts.md b/docs/reference/configuration/post-scripts.md index ca4c1980..20e9c910 100644 --- a/docs/reference/configuration/post-scripts.md +++ b/docs/reference/configuration/post-scripts.md @@ -1,4 +1,4 @@ -# Post-download scripts { #post-scripts } +# Post-scripts { #post-scripts } Operate your own script when a download finishes, with unshackle's metadata passed in as `{variable}` placeholders. Use it to hand a finished file to an uploader, send a @@ -87,7 +87,7 @@ post_scripts: ``` With `wait: true`, unshackle waits until the script exits, then writes the exit code to the -debug log and carries on. Because the `file` hook runs between titles, unshackle runs one +debug log and carries on. Because the `file` post-script runs between titles, unshackle runs one script at a time over a season pack instead of starting them all at once. unshackle only logs the exit code: a failing script does not fail the download or change unshackle's own exit code. `--postscript` entries never wait. @@ -132,7 +132,7 @@ Every variable from your `output_template` is available here as well, including file, because SDR is the absence of an HDR tag in a filename rather than a tag that reads `SDR`. `{edition}`, `{atmos}`, `{multi}`, `{dual}` and `{hfr}` behave the same way. If your script needs the word `SDR`, derive it from an empty `{hdr}` yourself. - - The hook is a `failure` hook. A failed download has no output file to read metadata + - The post-script is a `failure` one. A failed download has no output file to read metadata from, so everything that comes from the naming context is empty. See [Events](#events). @@ -146,7 +146,7 @@ Every variable from your `output_template` is available here as well, including ### Music variables -A music download builds its variables from the music naming context, so a hook gets the +A music download builds its variables from the music naming context, so a post-script gets the release fields instead of the season and episode ones. `{season}`, `{episode}` and `{episode_name}` are always empty for music. @@ -161,7 +161,7 @@ release fields instead of the season and episode ones. `{season}`, `{episode}` a | `{release_type}` | `album`, `single`, `ep`, and so on. A service that reports nothing gives `album` | | `{explicit}` | The word `Explicit` when the track is flagged, otherwise empty | -In a `file` hook `{title}` is the track name. See [Modes](#modes) for what an album hook +In a `file` post-script `{title}` is the track name. See [Modes](#modes) for what an album post-script puts in `{title}`. `{year}` is empty when the service gives the release no year. `{ext}` is the container the @@ -170,11 +170,11 @@ track arrived in, such as `.flac`, because a music download is not muxed. ### Tagging IDs { #tagging-ids } `{tmdb}`, `{imdb}` and `{tvdb}` carry the IDs unshackle tags the file with. unshackle reads -them at the moment it sends the hook, and not at the start of the run. The title search inside -the download loop finds an episode's TMDB ID, so a hook fires with it already filled in. +them at the moment it dispatches the post-script, and not at the start of the run. The title search +inside the download loop finds an episode's TMDB ID, so a post-script fires with it already filled in. They hold an ID you passed with `--tmdb`, `--imdb` or `--tvdb`, plus a TMDB ID the episode -search resolved. No such search runs for a movie, so a movie hook gets them empty unless you +search resolved. No such search runs for a movie, so a movie post-script gets them empty unless you passed one. ### Sidecar files @@ -206,19 +206,19 @@ subs = [p for p in sys.argv[i].removeprefix("--subs=").split("\n") if p] | `run` | Once per folder written, at the end of the run | Empty | Empty | !!! warning "`season` means everything this run asked for" - A `season` hook fires when the last title **this run queued** for that season finishes. - With `-w S01-S02` the S01 hook runs while S02 is still downloading. It says nothing - about whether the season is complete on disk. Downloading a single episode fires the - hook for that season as soon as the episode lands. + A `season` post-script fires when the last title **this run queued** for that season + finishes. With `-w S01-S02` the S01 post-script runs while S02 is still downloading. It says + nothing about whether the season is complete on disk. Downloading a single episode fires + the post-script for that season as soon as the episode lands. - A title that fails stops the whole run. unshackle dispatches the `failure` hook for + A title that fails stops the whole run. unshackle dispatches the `failure` post-script for that title and then returns. It downloads no further title, and no `season` or `run` - hook fires after that point. This applies to every title type. If one episode of S01 - fails, the S01 hook does not fire at all. + post-script fires after that point. This applies to every title type. If one episode of S01 + fails, the S01 post-script does not fire at all. -unshackle attaches the hook to the folder, not to the season, so one season landing in several -folders (`-q 1080,720` with a quality-dependent folder template) fires it once per folder, -each with its own `{folder}`. A movie is a group of its own: a `season` hook fires for a +unshackle attaches the post-script to the folder, not to the season, so one season landing in +several folders (`-q 1080,720` with a quality-dependent folder template) fires it once per +folder, each with its own `{folder}`. A movie is a group of its own: a `season` post-script fires for a movie's folder as soon as the movie finishes. In `run` mode every variable except `{folder}` is empty, because one run can cover several @@ -235,27 +235,27 @@ Music uses the same three modes, with the album standing in for the season. | `season` | Once per album folder, after every queued track of that album has landed | | `run` | Once per folder written, at the end of the run | -In a `file` hook `{title}` and `{title_raw}` are the track name. In an album hook they are -the album name, and the per-track variables `{track_number}`, `{disc}` and `{isrc}` are -empty, in the same way that `{episode}` is empty in a season hook. +In a `file` post-script `{title}` and `{title_raw}` are the track name. In an album post-script they +are the album name, and the per-track variables `{track_number}`, `{disc}` and `{isrc}` are +empty, in the same way that `{episode}` is empty in a season post-script. -An album hook obeys the same rule as a season hook. It fires when the last track **this run -queued** for that album lands. This does not mean that the album is complete on disk. A -one-track download fires the album hook as soon as that track lands. If a track fails, the -run stops there, so the album hook never fires, in the same way as an episode and its +An album post-script obeys the same rule as a season post-script. It fires when the last track +**this run queued** for that album lands. This does not mean that the album is complete on disk. +A one-track download fires the album post-script as soon as that track lands. If a track fails, +the run stops there, so the album post-script never fires, in the same way as an episode and its season. ## Events { #events } `success` runs after the file has moved to its final path. `failure` runs when the download fails, with `{filepath}` empty and `{error}` set. Each track of an album is its own title. -A music `failure` hook therefore describes the track that failed, not the release. +A music `failure` post-script therefore describes the track that failed, not the release. !!! warning "`failure` only fires in `file` mode" A `failure` entry with `mode: season` or `mode: run` never runs. Leave `mode` at its default on a failure entry. -A `failure` hook has no output file to read metadata from, so it carries only what the +A `failure` post-script has no output file to read metadata from, so it carries only what the title object already knew: `{title}`, `{title_raw}`, `{title_id}`, `{year}`, `{season}`, `{episode}`, `{episode_name}`, `{service}`, the tagging IDs and `{error}`. Everything that comes from the naming context, such as `{quality}`, `{hdr}`, `{artist}` and `{album}`, is @@ -266,7 +266,7 @@ sees your script's exit code only when that entry sets `wait: true`, and even th writes the code to the debug log. Nothing else reports a failing script. Your script owns its own error handling and logging. -A `success` hook always names a file this run wrote. There is no skip-if-exists path in +A `success` post-script always names a file this run wrote. There is no skip-if-exists path in `dl` today, so nothing fires for a title that was not downloaded. ## `--postscript` @@ -300,7 +300,7 @@ unshackle logs the command it operated twice: at `DEBUG` level on the console, a when that log is on. unshackle masks both the same way, and honours [`redact_paths`](misc.md), which is on by default. -Debug logs get shared in issue reports, and a hook command line is mostly paths. URL +Debug logs get shared in issue reports, and a post-script command line is mostly paths. URL credentials and `token=` / `api_key=` query parameters are also masked. !!! warning "Do not hardcode secrets in the command" @@ -310,8 +310,8 @@ credentials and `token=` / `api_key=` query parameters are also masked. ## Limitations -- No `success` hook operates under `--no-mux` or `--skip-dl`, because neither writes a muxed - output. A `failure` hook still runs under `--no-mux` if the download itself fails. -- `failure` hooks only operate in `file` mode. +- No `success` post-script operates under `--no-mux` or `--skip-dl`, because neither writes a + muxed output. A `failure` post-script still runs under `--no-mux` if the download itself fails. +- `failure` post-scripts only operate in `file` mode. - There is no webhook sender. Call `curl` from a script instead. - Script output is not captured. Redirect it inside your own script if you want a log. diff --git a/mkdocs.yml b/mkdocs.yml index 95f24827..97fc9c13 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -128,7 +128,7 @@ nav: - DRM & CDM: reference/configuration/drm.md - Network & Proxy: reference/configuration/network.md - Key Vaults: reference/configuration/vaults.md - - Post-Download Scripts: reference/configuration/post-scripts.md + - Post-Scripts: reference/configuration/post-scripts.md - Other Settings: reference/configuration/misc.md - Annotated Example: reference/configuration/example.md - Developer: diff --git a/tests/test_post_scripts_music.py b/tests/test_post_scripts_music.py index 5f2d3849..9d38f616 100644 --- a/tests/test_post_scripts_music.py +++ b/tests/test_post_scripts_music.py @@ -1,9 +1,9 @@ -"""Music post-script hooks, which no live service was available to test against. +"""Music post-scripts, which no live service was available to test against. -A Song runs through the same title loop as an Episode, so its hooks come from the same +A Song runs through the same title loop as an Episode, so its post-scripts come from the same two functions: ``build_context`` for a file and ``season_context`` for the release folder. These check what those two feed a script: that a Song builds a usable context, that an -album groups the way a season does, and that the album hook describes the album rather +album groups the way a season does, and that the album post-script describes the album rather than whichever track landed last. """ @@ -18,7 +18,7 @@ from unshackle.core.utils.post_scripts import build_context, dispatch, season_co class FakeSong(SimpleNamespace): - """A Song as the hook sees it: name/artist/album, no season or episode.""" + """A Song as the post-script sees it: name/artist/album, no season or episode.""" def build_template_context(self, media_info, show_service=True): return { @@ -45,7 +45,7 @@ def song(**kwargs): class FakeEpisode(SimpleNamespace): - """An Episode as the hook sees it: a show title, a season, and a number.""" + """An Episode as the post-script sees it: a show title, a season, and a number.""" def build_template_context(self, media_info, show_service=True): return {"title": self.title, "season": f"{self.season:02}", "audio": "EAC3", "video": "H.265"} @@ -109,7 +109,7 @@ def test_album_grouping_matches_the_season_grouping_rule(): @pytest.mark.parametrize("mode", ["season", "run"]) def test_album_and_run_contexts_blank_the_file_variables(mode): - """Mirrors the blanking dl.py applies before dispatching an album or run hook.""" + """Mirrors the blanking dl.py applies before dispatching an album or run post-script.""" base = build_context( song(), media_info=object(), @@ -133,7 +133,7 @@ def test_album_and_run_contexts_blank_the_file_variables(mode): assert context["album"] == "Example Album, Live & Remastered" -def test_a_music_hook_command_builds_the_argv_a_script_would_get(monkeypatch, tmp_path): +def test_a_music_post_script_command_builds_the_argv_a_script_would_get(monkeypatch, tmp_path): """End to end through the real dispatcher: config entry in, argv out.""" from unshackle.core.config import config @@ -163,7 +163,7 @@ def test_a_music_hook_command_builds_the_argv_a_script_would_get(monkeypatch, tm import time time.sleep(0.05) - assert marker.exists(), "the hook process never ran" + assert marker.exists(), "the post-script process never ran" argv = marker.read_text().splitlines() assert "--album=Example Album, Live & Remastered" in argv assert "--artist=Example Artist" in argv @@ -172,7 +172,7 @@ def test_a_music_hook_command_builds_the_argv_a_script_would_get(monkeypatch, tm def test_season_context_blanks_the_file_variables_and_sets_the_folder(): - """An album hook describes the release folder, so no track's file may leak into it.""" + """An album post-script describes the release folder, so no track's file may leak into it.""" folder = Path("/dl/Example Artist - Example Album, Live & Remastered (1983)") context = build_context( song(), @@ -194,7 +194,7 @@ def test_season_context_blanks_the_file_variables_and_sets_the_folder(): def test_season_context_identifies_the_album_not_the_last_track(): """A Song's ``{title}`` is the song, so blanking only the file fields is not enough. - Guards a regression where an album hook was handed whichever track landed last: + Guards a regression where an album post-script was handed whichever track landed last: ``{title}`` was a song name and ``{track_number}``/``{isrc}`` were that track's. """ folder = Path("/dl/Example Artist - Example Album (1983)") @@ -219,7 +219,7 @@ def test_season_context_identifies_the_album_not_the_last_track(): def test_season_context_leaves_a_non_music_context_untouched(): - """The music branch is gated on ``{album}``; a season hook must not lose its own keys.""" + """The music branch is gated on ``{album}``; a season post-script must not lose its own keys.""" folder = Path("/dl/Some Show/Season 1") context = build_context( episode(), diff --git a/unshackle/core/api/routes.py b/unshackle/core/api/routes.py index 5ce0a249..ca851760 100644 --- a/unshackle/core/api/routes.py +++ b/unshackle/core/api/routes.py @@ -597,7 +597,7 @@ async def download(request: web.Request) -> web.Response: description: >- Download a video title based on the specified parameters. 'postscript', 'post_script' and 'post_scripts' are never accepted and return 400. - You configure post-download hooks only in unshackle.yaml. + You configure post-scripts only in unshackle.yaml. requestBody: required: true content: diff --git a/unshackle/core/utils/post_scripts.py b/unshackle/core/utils/post_scripts.py index c5f447b2..afdca3f2 100644 --- a/unshackle/core/utils/post_scripts.py +++ b/unshackle/core/utils/post_scripts.py @@ -142,7 +142,7 @@ def build_context( def season_context(context: dict[str, str], folder: Path) -> dict[str, str]: - """Blank the file-level variables so a season/album hook describes the folder.""" + """Blank the file-level variables so a season/album post-script describes the folder.""" out = dict(context) for key in ("filepath", "filename", "ext", "sidecars", "episode", "episode_name"): out[key] = ""