mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-11 19:45:06 +00:00
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.
This commit is contained in:
@@ -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`:
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -686,16 +686,16 @@ By default, `mkvmerge` muxes the tracks of a movie or an episode into a single M
|
||||
|
||||
`--postscript "<command>"` 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
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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] = ""
|
||||
|
||||
Reference in New Issue
Block a user