-
Notifications
You must be signed in to change notification settings - Fork 58
[Advanced] Custom lifecycle scripts
Note
This feature is considered to be in alpha and may introduce breaking changes or be removed at any time without warning
Important
This is advanced functionality and could easily cause Pinchflat to work unexpectedly if misused. Read this entire document (especially the Golden Rules) before writing your own scripts
Use at your own risk and know that no support will be provided for writing or maintaining custom scripts
It's common to need to notify other services when some action has completed within Pinchflat. Instead of creating an adapter for every possible service, this is a hands-off way for people to create custom scripts to suit their needs.
Essentially, you'll know if you need to use this feature.
Important
If you read only one section, let it be this one
These are a list of best practices when it comes to writing scripts. Pinchflat has a strict internal knowledge of where and how to store your data that can very easily be severed when using scripts incorrectly. Ultimately it's your computer and you can do what you want, but these rules will give you the best chance of success:
- This is alpha functionality that could introduce breaking changes or be removed at any time
- No support will be offered if Pinchflat works in unexpected ways due to usage of custom scripts
- Do not use custom scripts to move or delete files
- Copying files (leaving the original intact) for the purpose of changing a filename is considered relatively safe
- There is an exception if you create/modify a file to have the exact same name as an existing file. This is playing with fire, but it may work
- Scripts are currently run synchronously relative to the event that called them. Long-running scripts are not recommended
- Scripts may not always run synchronously in the future if this delay proves to be a problem. There may be a time delta between when an event happens and the script is called
- Script failures will be ignored and Pinchflat will continue without retrying
- A script may run multiple times for the same event. Ensure your script behaves idempotently
- Your container paths are different than your host paths
- The script may think an object is stored at
/foo/bar/baz.mp4
(which it is from the container's perspective) but your host may see the same file at/data/pinchflat/bar/baz.mp4
. Your script may need to account for this
- The script may think an object is stored at
- You script should only listen for events it cares about and ignore other events
- This prevents new events from breaking your scripts
On app boot, a file is created at <pinchflat config directory>/extras/user-scripts/lifecycle
. This singular script is called every time an event takes place and must contain logic to delegate based on event type.
Tip
Set the LOG_LEVEL
environment variable to debug
so your script's status code and output will show up in the logs
This lifecycle
file can either use bash or Python 3. jq
has been installed in the container to help with parsing. The script is called with 2 arguments: the event type and a JSON representation of relevant data for that event (usually this is the JSON that represents a database record).
At current there are several events - app_init
, media_pre_download
, media_downloaded
, and media_deleted
. It's likely I'll forget to update this document if new events get added so it's always worth checking out this file to see the latest. Look for a line that starts with @event_types
and that'll tell you what's available!
The app_init
event runs very early in the app's startup process. One common use of this is to ensure your desired yt-dlp plugins are installed
The media_pre_download
event is special because it'll prevent download of a piece of media if the script returns a non-zero status code. This means that you can reject media that doesn't meet complex criteria by returning a non-zero code.
This callback is run immediately before download of media.
#!/bin/bash
EVENT_TYPE=$1
EVENT_DATA=$2
echo "Script called with event type '$EVENT_TYPE' and a record ID #$(echo $EVENT_DATA | jq -r '.id')"
#!/bin/python3
import sys
import json
event_type = sys.argv[1]
event_data = json.loads(sys.argv[2])
print("Script called with event type '{}' and an ID #{}".format(event_type, event_data["id"]))
It can be a pain to test your script since you need to trigger a download or deletion each time. Here are some rough guidelines to help with testing:
- Have at least one piece of media downloaded in Pinchflat
- Edit the
lifecycle
script to have this contents:
#!/bin/bash
EVENT_TYPE=$1
EVENT_DATA=$2
echo $EVENT_DATA >/tmp/example.json
echo "Data for '$EVENT_TYPE' saved to /tmp/example.json"
- In Pinchflat, find a piece of media, click "Actions" in the upper right, then "Force Download"
- If successful, this will save an example output in
/tmp/example.json
once the download completes
- If successful, this will save an example output in
- Modify the
lifecycle
script to have the behaviour you actually want - Exec into the Pinchflat container
- You can inspect the generated JSON with
cat /tmp/example.json
- Run this command to test your script:
/app/tmp/extras/user-scripts/lifecycle media_downloaded "$(cat /tmp/example.json)"
Tip
I won't be keeping this example up-to-date so it's best to generate example JSON using the steps above. This is more of a rough guideline.
Note that the payload may be different per-event in the future
{
"id": 1,
"description": "...",
"title": "...",
"source_id": 1,
"media_filepath": "...",
"upload_date": "2000-01-01",
"original_url": "https://www.youtube.com/watch?v=abc123",
"uuid": "0c496b24-e012-42d1-8f6d-bd2d59c2b1a3",
"prevent_download": false,
"prevent_culling": false,
"inserted_at": "2000-01-01T00:00:00Z",
"updated_at": "2000-01-01T00:00:00Z",
"culled_at": null,
"media_size_bytes": 40687134,
"thumbnail_filepath": "..",
"duration_seconds": 500,
"nfo_filepath": "...",
"livestream": false,
"media_downloaded_at": "2000-01-01T00:00:00Z",
"media_id": "abc123",
"media_redownloaded_at": null,
"metadata_filepath": null,
"short_form_content": false,
"subtitle_filepaths": [],
"upload_date_index": 0,
"source": {
"id": 1,
"description": "...",
"custom_name": "...",
"original_url": "https://www.youtube.com/playlist?list=321cba",
"uuid": "bb645fa8-9f7b-4dbf-a8e2-0f6a7d55f4d5",
"media_profile_id": 1,
"index_frequency_minutes": -1,
"fast_index": false,
"download_media": true,
"download_cutoff_date": null,
"retention_period_days": null,
"title_filter_regex": null,
"output_path_template_override": null,
"collection_type": "playlist",
"collection_name": "...",
"inserted_at": "2000-01-01T00:00:00Z",
"updated_at": "2000-01-01T00:00:00Z",
"banner_filepath": null,
"collection_id": "321cba",
"fanart_filepath": null,
"last_indexed_at": "2000-01-01T00:00:00Z",
"nfo_filepath": "...",
"poster_filepath": null,
"series_directory": "...",
"media_profile": {
"id": 1,
"name": "...",
"output_path_template": "...",
"download_subs": true,
"download_auto_subs": false,
"embed_subs": true,
"sub_langs": "en",
"download_thumbnail": true,
"embed_thumbnail": true,
"download_metadata": false,
"embed_metadata": true,
"shorts_behaviour": "exclude",
"livestream_behaviour": "exclude",
"preferred_resolution": "1080p",
"redownload_delay_days": 1,
"download_nfo": true,
"download_source_images": true,
"sponsorblock_behaviour": "disabled",
"sponsorblock_categories": [],
"inserted_at": "2000-01-01T00:00:00Z",
"updated_at": "2000-01-01T00:00:00Z"
}
}
}