diff --git a/CHANGELOG b/CHANGELOG index 9c6dd3d5..612ac9e3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,17 +1,58 @@ # Requirements Update (requirements will need to be reinstalled) -Updated arrapi requirement to 1.4.2 -Updated pillow requirement to 9.4.0 -Updated requests requirement to 2.28.2 +Updated pillow requirement to 9.5.0 +Updated plexapi requirement to 4.13.4 +New requirement GitPython version 3.1.31 # New Features -Added new collection_order `custom.desc` ([FR](https://features.metamanager.wiki/features/p/reverse-sort-collectionorder-custom)) -Added webp Image Support ([FR](https://features.metamanager.wiki/features/p/support-webp-image-extensions)) -Added Spanish Defaults Translation -Added Delete Webhooks -Added collection detail `delete_collections_named` to delete any collections listed while running this collection definition. +Added `episode_year` as a dynamic collection option. +Added `mass_studio_update` [library operation](https://metamanager.wiki/en/latest/config/operations.html#mass-studio-update). +Changes Environment Variable/Run Argument list separator from `,` to `|`. +Added `PMM_LOG_REQUESTS`/`--log-requests` Environment Variable/Run Argument which will log every single HTTP request in the log. +Added EXIF Tags to Overlayed Images to be able to determine if they have an overlay or not. +Added `anidb`, `anidb_3_0`, `anidb_2_5`, `anidb_2_0`, `anidb_1_5`, `anidb_1_0`, `anidb_0_5` options to the [`mass_genre_update` Library Operation](https://metamanager.wiki/en/latest/config/operations.html#mass-genre-update). +Added `ignore_cache` to [`radarr`](https://metamanager.wiki/en/latest/config/radarr.html) and [`sonarr`](https://metamanager.wiki/en/latest/config/sonarr.html) Settings and `radarr_ignore_cache` and `sonarr_ignore_cache` to [Radarr/Sonarr Definition Settings](https://metamanager.wiki/en/latest/metadata/details/arr.html). +Closes #1286 Updates Synology Walkthrough with DSM7 images. +Closes #1159 Adds support for official trakt lists. +Closes #1251 When resetting Overlays Seasons where theres no poster will use the show poster. +Templates can now be used with metadata updates. +`allowed_library_types` Definition Setting has been changed to `run_definition` the old attribute will still work in the same way. +Added `mapping_id`, `run_definition`, `update_seasons`, and `update_episodes` to Metadata definitions. +Added a [Ratings Explained](https://metamanager.wiki/en/latest/home/guides/ratings.html) page to the Wiki to help explain how PMM interacts with the various Ratings. +Add more options to the [`mass_imdb_parental_labels` Library Operation](https://metamanager.wiki/en/latest/config/operations.html#mass-imdb-parental-labels). +Added `imdb_keyword` as a [Tag Filter](https://metamanager.wiki/en/latest/metadata/filters.html#tag-filters). +Added `has_edition` as a [Boolean Filter](https://metamanager.wiki/en/latest/metadata/filters.html#boolean-filters). +Added `has_stinger` and `stinger_rating` as [Filters](https://metamanager.wiki/en/latest/metadata/filters.html) based on http://www.mediastinger.com +When editing episode metadata the key can now be either episode number, episode title, or episodeoriginally released date. +The Collectionless builder now can work with other builders. + +# New Defaults Features +Removed Translations from the defaults directory and in to their own [repo](https://github.com/meisnate12/PMM-Translations) which is managed at [translations.metamanager.wiki](https://translations.metamanager.wiki/projects/plex-meta-manager/defaults/). +Added `minimum_rating`, `fresh_rating`, and `maximum_rating` as template variable options to the [Ratings Overlays](https://metamanager.wiki/en/latest/defaults/overlays/ratings.html) to control which ratings get displayed. +Added the ability to update Overlay Defaults Positioning with just setting the alignment variables. +Added [Based On...](https://metamanager.wiki/en/latest/defaults/both/based.html) Collection Default. +Added Signature Style, DIIIVOY Style, and DIIVOY Color Style to [`actor`](https://metamanager.wiki/en/latest/defaults/both/actor.html), [`directors`](https://metamanager.wiki/en/latest/defaults/movie/director.html), [`producers`](https://metamanager.wiki/en/latest/defaults/movie/producer.html), and [`writers`](https://metamanager.wiki/en/latest/defaults/movie/writer.html). +Added new editions to the [editions Overlay File](https://metamanager.wiki/en/latest/defaults/overlays/resolution.html). +Added `delete_playlist` and `delete_playlist_<>` as template variable options to the [Playlist Default](https://metamanager.wiki/en/latest/defaults/playlist.html). +Added `region` as a template variable options to the [`streaming` Overlay](https://metamanager.wiki/en/latest/defaults/overlays/streaming.html) and [`streaming` Collection](https://metamanager.wiki/en/latest/defaults/both/streaming.html) to allow these lists to show items in that region. +Added AppleTV to te [FlixPatrol Default](https://metamanager.wiki/en/latest/defaults/overlays/flixpatrol.html). +Added `radarr_search` and `sonarr_search` as template variable options to all Collection Defaults. +Updated `network` and `franchise` defaults # Bug Fixes -Fixes #1187 Franchise Defaults no longer ignore collection_section and sort_title -Fixed Italian Defaults Translation -Fixed TMDb Modified Filters -Fixed ValueError from Anime IDs \ No newline at end of file +Fixes Bug with `--time` that caused the times not to display correctly. +Fixes `mal_search` search bug. +Fixes #1277 corrects bug setting TMDb region. +Fixes a Bug where missing items items wouldn't be sent to radarr if no items were found in the library. +Fixes a Bug with template conditionals causing them to sometimes use the wrong result. +Fixes #1285 Wiki error. +Fixes a Bug with the `mass_poster_update` and `mass_background_update` Library Operations where they would sometimes throw a 406 Error. +Fixes a Bug with the `mass_poster_update` Library Operation where it would also update backgrounds in addition to posters. +Fixes multiple unnecessary items loads from plex. +Fixes a Bug with using year filters with no modifier. +Fixes a Bug where the `dimensional_asset_rename` Setting would rename title cards and season posters to show posters. +Fixes [`trakt_userlist` Builder](https://metamanager.wiki/en/latest/metadata/builders/trakt.html#trakt-userlist) where option `recommended` should have been `recommendations`. +Fixes overlay remove/reset operations. +Closes #1325 Fixes a Bug where `tmdb_vote_count` would be rejected as a filter. +Closes #1189 Fixes a Bug in the Resolution Default where the position would be completely off when changed + +Various other Minor Fixes \ No newline at end of file diff --git a/README.md b/README.md index 90a51cd6..76cc77be 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Discord](https://img.shields.io/discord/822460010649878528?color=%2300bc8c&label=Discord&style=plastic)](https://discord.gg/NfH6mGFuAB) [![Reddit](https://img.shields.io/reddit/subreddit-subscribers/PlexMetaManager?color=%2300bc8c&label=r%2FPlexMetaManager&style=plastic)](https://www.reddit.com/r/PlexMetaManager/) [![Wiki](https://img.shields.io/readthedocs/plex-meta-manager?color=%2300bc8c&style=plastic)](https://metamanager.wiki) -[![Translations](https://img.shields.io/weblate/progress/plex-meta-manager?color=00bc8c&server=https%3A%2F%2Ftranslations.metamanager.wiki&style=plastic)](https://translations.metamanager.wiki/engage/plex-meta-manager/) +[![Translations](https://img.shields.io/weblate/progress/plex-meta-manager?color=00bc8c&server=https%3A%2F%2Ftranslations.metamanager.wiki&style=plastic)](https://translations.metamanager.wiki/projects/plex-meta-manager/#languages) [![GitHub Sponsors](https://img.shields.io/github/sponsors/meisnate12?color=%238a2be2&style=plastic)](https://github.com/sponsors/meisnate12) [![Sponsor or Donate](https://img.shields.io/badge/-Sponsor%2FDonate-blueviolet?style=plastic)](https://github.com/sponsors/meisnate12) [![Feature Requests](https://img.shields.io/badge/Feature%20Requests-blueviolet?style=plastic)](https://features.metamanager.wiki/) diff --git a/VERSION b/VERSION index a464cc23..0201a8a7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.3-develop166 +1.18.3-develop167 diff --git a/defaults/award/golden.yml b/defaults/award/golden.yml index 06982ca6..4ae38c32 100644 --- a/defaults/award/golden.yml +++ b/defaults/award/golden.yml @@ -20,7 +20,7 @@ collections: sort: Golden Globes !1 allowed_libraries: movie image: award/golden/best_picture_winner - translation_key: golden_best + translation_key: golden_picture - name: arr - name: custom collection_order: release.desc diff --git a/defaults/both/based.yml b/defaults/both/based.yml index e45ad8ce..5df92fa8 100644 --- a/defaults/both/based.yml +++ b/defaults/both/based.yml @@ -11,11 +11,11 @@ external_templates: collection_section: "085" collections: - Based On... Collections: + Based on... Collections: template: - name: separator separator: based - key_name: Based On... + key_name: Based on... translation_key: separator dynamic_collections: diff --git a/defaults/overlays/resolution.yml b/defaults/overlays/resolution.yml index 60d2044f..e046294d 100644 --- a/defaults/overlays/resolution.yml +++ b/defaults/overlays/resolution.yml @@ -189,10 +189,10 @@ templates: conditions: - alt: hdr value: true - hdr10plus: + plus: conditions: - - alt: hdr10plus - value: '(?i)\bHDR10(\+|P(lus)?\b)' + - alt: plus + value: '(?i)\bhdr10(\+|p(lus)?\b)' optional: - all - use_<> @@ -215,12 +215,12 @@ templates: hdr: <> filters: has_dolby_vision: <> - - filepath.regex: <> + filepath.regex: <> overlays: 4K-HDR10PLUS-Dovetail: - variables: {key: 4k, alt: hdr10plus, weight: 160, type: resolution_dovetail, allowed_libraries: movie} + variables: {key: 4k, alt: plus, weight: 160, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 4K-DV-Dovetail: variables: {key: 4k, alt: dv, weight: 150, type: resolution_dovetail, allowed_libraries: movie} @@ -232,7 +232,7 @@ overlays: variables: {key: 4k, alt: "", weight: 130, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 1080P-HDR10PLUS-Dovetail: - variables: {key: 1080p, alt: hdr10plus, weight: 125, type: resolution_dovetail, allowed_libraries: movie} + variables: {key: 1080p, alt: plus, weight: 125, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 1080P-DV-Dovetail: variables: {key: 1080p, alt: dv, weight: 120, type: resolution_dovetail, allowed_libraries: movie} @@ -244,7 +244,7 @@ overlays: variables: {key: 1080p, alt: "", weight: 100, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 720P-HDR10PLUS-Dovetail: - variables: {key: 720p, alt: hdr10plus, weight: 95, type: resolution_dovetail, allowed_libraries: movie} + variables: {key: 720p, alt: plus, weight: 95, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 720P-DV-Dovetail: variables: {key: 720p, alt: dv, weight: 90, type: resolution_dovetail, allowed_libraries: movie} @@ -256,7 +256,7 @@ overlays: variables: {key: 720p, alt: "", weight: 70, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 576P-HDR10PLUS-Dovetail: - variables: {key: 576p, alt: hdr10plus, weight: 65, type: resolution_dovetail, allowed_libraries: movie} + variables: {key: 576p, alt: plus, weight: 65, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 576P-DV-Dovetail: variables: {key: 576p, alt: dv, weight: 60, type: resolution_dovetail, allowed_libraries: movie} @@ -268,7 +268,7 @@ overlays: variables: {key: 576p, alt: "", weight: 40, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 480P-HDR10PLUS-Dovetail: - variables: {key: 480p, alt: hdr10plus, weight: 35, type: resolution_dovetail, allowed_libraries: movie} + variables: {key: 480p, alt: plus, weight: 35, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] 480P-DV-Dovetail: variables: {key: 480p, alt: dv, weight: 30, type: resolution_dovetail, allowed_libraries: movie} @@ -280,7 +280,7 @@ overlays: variables: {key: 480p, alt: "", weight: 10, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] HDR10PLUS-Dovetail: - variables: {key: "", alt: hdr10plus, weight: 7, type: resolution_dovetail, allowed_libraries: movie} + variables: {key: "", alt: plus, weight: 7, type: resolution_dovetail, allowed_libraries: movie} template: [name: resolution, name: standard] DV-Dovetail: variables: {key: "", alt: dv, weight: 5, type: resolution_dovetail, all: true, allowed_libraries: movie} @@ -360,7 +360,7 @@ overlays: template: [name: edition, name: standard] 4K-HDR10PLUS: - variables: {key: 4k, alt: hdr10plus, weight: 160, type: resolution} + variables: {key: 4k, alt: plus, weight: 160, type: resolution} template: [name: resolution, name: standard] 4K-DV: variables: {key: 4k, alt: dv, weight: 150, type: resolution} @@ -372,7 +372,7 @@ overlays: variables: {key: 4k, alt: "", weight: 130, type: resolution} template: [name: resolution, name: standard] 1080P-HDR10PLUS: - variables: {key: 1080p, alt: hdr10plus, weight: 125, type: resolution} + variables: {key: 1080p, alt: plus, weight: 125, type: resolution} template: [name: resolution, name: standard] 1080P-DV: variables: {key: 1080p, alt: dv, weight: 120, type: resolution} @@ -384,7 +384,7 @@ overlays: variables: {key: 1080p, alt: "", weight: 100, type: resolution} template: [name: resolution, name: standard] 720P-HDR10PLUS: - variables: {key: 720p, alt: hdr10plus, weight: 95, type: resolution} + variables: {key: 720p, alt: plus, weight: 95, type: resolution} template: [name: resolution, name: standard] 720P-DV: variables: {key: 720p, alt: dv, weight: 90, type: resolution} @@ -396,7 +396,7 @@ overlays: variables: {key: 720p, alt: "", weight: 70, type: resolution} template: [name: resolution, name: standard] 576P-HDR10PLUS: - variables: {key: 576p, alt: hdr10plus, weight: 65, type: resolution} + variables: {key: 576p, alt: plus, weight: 65, type: resolution} template: [name: resolution, name: standard] 576P-DV: variables: {key: 576p, alt: dv, weight: 60, type: resolution} @@ -408,7 +408,7 @@ overlays: variables: {key: 576p, alt: "", weight: 40, type: resolution} template: [name: resolution, name: standard] 480P-HDR10PLUS: - variables: {key: 480p, alt: hdr10plus, weight: 35, type: resolution} + variables: {key: 480p, alt: plus, weight: 35, type: resolution} template: [name: resolution, name: standard] 480P-DV: variables: {key: 480p, alt: dv, weight: 30, type: resolution} @@ -420,7 +420,7 @@ overlays: variables: {key: 480p, alt: "", weight: 10, type: resolution} template: [name: resolution, name: standard] HDR10PLUS: - variables: {key: "", alt: hdr10plus, weight: 7, type: resolution} + variables: {key: "", alt: plus, weight: 7, type: resolution} template: [name: resolution, name: standard] DV: variables: {key: "", alt: dv, weight: 5, type: resolution, all: true} diff --git a/defaults/posters/backgrounds/bottom_fade.png b/defaults/posters/backgrounds/bottom_fade.png new file mode 100644 index 00000000..07150640 Binary files /dev/null and b/defaults/posters/backgrounds/bottom_fade.png differ diff --git a/defaults/posters/backgrounds/center_fade.png b/defaults/posters/backgrounds/center_fade.png new file mode 100644 index 00000000..8e337a3d Binary files /dev/null and b/defaults/posters/backgrounds/center_fade.png differ diff --git a/defaults/posters/backgrounds/top_bottom_fade.png b/defaults/posters/backgrounds/top_bottom_fade.png new file mode 100644 index 00000000..068071f7 Binary files /dev/null and b/defaults/posters/backgrounds/top_bottom_fade.png differ diff --git a/defaults/posters/backgrounds/top_fade.png b/defaults/posters/backgrounds/top_fade.png new file mode 100644 index 00000000..8b146d7d Binary files /dev/null and b/defaults/posters/backgrounds/top_fade.png differ diff --git a/defaults/posters/images/plex.png b/defaults/posters/images/plex.png new file mode 100644 index 00000000..f26f7702 Binary files /dev/null and b/defaults/posters/images/plex.png differ diff --git a/docs/conf.py b/docs/conf.py index b78216a8..da1d3085 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -177,6 +177,7 @@ html_theme_options = { ("Mass Genre Update", "config/operations", "#mass-genre-update"), ("Mass Content Rating Update", "config/operations", "#mass-content-rating-update"), ("Mass Original Title Update", "config/operations", "#mass-original-title-update"), + ("Mass Studio Update", "config/operations", "#mass-studio-update"), ("Mass Originally Available Update", "config/operations", "#mass-originally-available-update"), ("Mass * Rating Update", "config/operations", "#mass--rating-update"), ("Mass Episode * Rating Update", "config/operations", "#mass-episode--rating-update"), diff --git a/docs/defaults/both/actor.md b/docs/defaults/both/actor.md index e90b1d5f..e926f488 100644 --- a/docs/defaults/both/actor.md +++ b/docs/defaults/both/actor.md @@ -42,7 +42,7 @@ This file contains a [Separator](../separators) so all [Shared Separator Variabl | Variable | Description & Values | |:------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `style` | **Description:** Controls the visual theme of the collections created.
**Default:** `bw`
**Values:** `bw`, `rainier`, `signature`, `diivoy`, or `diivoycolor` | +| `style` | **Description:** Controls the visual theme of the collections created.
**Default:** `bw`
**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` | | `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults file.
**Values:** Number Greater then 0 | | `limit_<>`1 | **Description:** Changes the Builder Limit of the specified key's collection.
**Default:** `limit`
**Values:** Number Greater then 0 | | `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults file.
**Default:** `release.desc`
**Values:** [Any `smart_filter` Sort Option](../../metadata/builders/smart.md#sort-options) | @@ -66,7 +66,7 @@ libraries: data: depth: 10 limit: 20 - style: diivoy + style: diiivoy sort_by: title.asc use_separator: false sep_style: purple diff --git a/docs/defaults/both/based.md b/docs/defaults/both/based.md index db441577..50cb276d 100644 --- a/docs/defaults/both/based.md +++ b/docs/defaults/both/based.md @@ -12,7 +12,7 @@ Supported Library Types: Movie, Show | Collection | Key | Description | |:---------------------------|:--------------|:----------------------------------------------------------------------------| -| `Based On... Collections` | `separator` | [Separator Collection](../separators) to denote the Section of Collections. | +| `Based on... Collections` | `separator` | [Separator Collection](../separators) to denote the Section of Collections. | | `Based on a Book` | `books` | Collection of Movies/Shows based on or inspired by books | | `Based on a Comic` | `comics` | Collection of Movies/Shows based on or inspired by comics | | `Based on a True Story` | `true_story` | Collection of Movies/Shows based on or inspired by true stories | @@ -51,8 +51,8 @@ This file contains a [Separator](../separators) so all [Shared Separator Variabl | `sync_mode` | **Description:** Changes the Sync Mode for all collections in a Defaults file.
**Default:** `sync`
**Values:**
`sync`Add and Remove Items based on Builders
`append`Only Add Items based on Builders
| | `sync_mode_<>`1 | **Description:** Changes the Sync Mode of the specified key's collection.
**Default:** `sync_mode`
**Values:**
`sync`Add and Remove Items based on Builders
`append`Only Add Items based on Builders
| | `exclude` | **Description:** Exclude these Media Outlets from creating a Dynamic Collection.
**Values:** List of Media Outlet Keys | -| `based_name` | **Description:** Changes the title format of the Dynamic Collections.
**Default:** `Based on a <>`
**Values:** Any string with `<>` in it. | -| `based_summary` | **Description:** Changes the summary format of the Dynamic Collections.
**Default:** `<>s based on or inspired by <>s.`
**Values:** Any string. | +| `name_format` | **Description:** Changes the title format of the Dynamic Collections.
**Default:** `Based on a <>`
**Values:** Any string with `<>` in it. | +| `summary_format` | **Description:** Changes the summary format of the Dynamic Collections.
**Default:** `<>s based on or inspired by <>s.`
**Values:** Any string. | 1. Each default collection has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. diff --git a/docs/defaults/both/streaming.md b/docs/defaults/both/streaming.md index 745c6b8c..09c98f26 100644 --- a/docs/defaults/both/streaming.md +++ b/docs/defaults/both/streaming.md @@ -64,7 +64,7 @@ This file contains a [Separator](../separators) so all [Shared Separator Variabl | `sync_mode` | **Description:** Changes the Sync Mode for all collections in a Defaults file.
**Default:** `sync`
**Values:**
`sync`Add and Remove Items based on Builders
`append`Only Add Items based on Builders
| | `sync_mode_<>`1 | **Description:** Changes the Sync Mode of the specified key's collection.
**Default:** `sync_mode`
**Values:**
`sync`Add and Remove Items based on Builders
`append`Only Add Items based on Builders
| | `exclude` | **Description:** Exclude these Streaming Services from creating a Dynamic Collection.
**Values:** List of Streaming Service Keys | -| `region` | **Description:** Changes some Streaming Service lists to regional variants (see below table for more information.
**Default:** `us`
**Values:** `us,`uk`,`ca`, `da`, `de`, `es`, `fr`, `it`, `pt-br` | +| `region` | **Description:** Changes some Streaming Service lists to regional variants (see below table for more information.
**Default:** `us`
**Values:** `us`,`uk`,`ca`, `da`, `de`, `es`, `fr`, `it`, `pt-br` | | `streaming_name` | **Description:** Changes the title format of the Dynamic Collections.
**Default:** `<> <>s`
**Values:** Any string with `<>` in it. | | `streaming_summary` | **Description:** Changes the summary format of the Dynamic Collections.
**Default:** `<>s streaming on <>.`
**Values:** Any string. | diff --git a/docs/defaults/collection_variables.md b/docs/defaults/collection_variables.md index 4931b3cc..8c5af2dc 100644 --- a/docs/defaults/collection_variables.md +++ b/docs/defaults/collection_variables.md @@ -30,6 +30,8 @@ Below are the available variables which can be used to customize the file. | `radarr_add_missing_<>`1 | **Description:** Override Radarr `add_missing` attribute of the specified key's collection.
**Default:** `radarr_add_missing`
**Values:** `true` or `false` | | `radarr_folder` | **Description:** Override Radarr `root_folder_path` attribute for all collections in a Defaults file.
**Values:** Folder Path | | `radarr_folder_<>`1 | **Description:** Override Radarr `root_folder_path` attribute of the specified key's collection.
**Default:** `radarr_folder`
**Values:** Folder Path | +| `radarr_search` | **Description:** Override Radarr `search` attribute or all collections in a Defaults file.
**Values:** `true` or `false` | +| `radarr_search_<>`1 | **Description:** Override Radarr `search` attribute of the specified key's collection.
**Default:** `radarr_search`
**Values:** `true` or `false` | | `radarr_tag` | **Description:** Override Radarr `tag` attribute for all collections in a Defaults file.
**Values:** List or comma-separated string of tags | | `radarr_tag_<>`1 | **Description:** Override Radarr `tag` attribute of the specified key's collection.
**Default:** `radarr_tag`
**Values:** List or comma-separated string of tags | | `item_radarr_tag` | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr for all collections in a Defaults file.
**Values:** List or comma-separated string of tags | @@ -38,6 +40,8 @@ Below are the available variables which can be used to customize the file. | `sonarr_add_missing_<>`1 | **Description:** Override Sonarr `add_missing` attribute of the specified key's collection.
**Default:** `sonarr_add_missing`
**Values:** `true` or `false` | | `sonarr_folder` | **Description:** Override Sonarr `root_folder_path` attribute for all collections in a Defaults file.
**Values:** Folder Path | | `sonarr_folder_<>`1 | **Description:** Override Sonarr `root_folder_path` attribute of the specified key's collection.
**Default:** `sonarr_folder`
**Values:** Folder Path | +| `sonarr_search` | **Description:** Override Sonarr `search` attribute or all collections in a Defaults file.
**Values:** `true` or `false` | +| `sonarr_search_<>`1 | **Description:** Override Sonarr `search` attribute of the specified key's collection.
**Default:** `sonarr_search`
**Values:** `true` or `false` | | `sonarr_tag` | **Description:** Override Sonarr `tag` attribute for all collections in a Defaults file.
**Values:** List or comma-separated string of tags | | `sonarr_tag_<>`1 | **Description:** Override Sonarr `tag` attribute of the specified key's collection.
**Default:** `sonarr_tag`
**Values:** List or comma-separated string of tags | | `item_sonarr_tag` | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr for all collections in a Defaults file.
**Values:** List or comma-separated string of tags | diff --git a/docs/defaults/images/person_diivoy.png b/docs/defaults/images/person_diiivoy.png similarity index 100% rename from docs/defaults/images/person_diivoy.png rename to docs/defaults/images/person_diiivoy.png diff --git a/docs/defaults/images/person_diivoycolor.png b/docs/defaults/images/person_diiivoycolor.png similarity index 100% rename from docs/defaults/images/person_diivoycolor.png rename to docs/defaults/images/person_diiivoycolor.png diff --git a/docs/defaults/movie/director.md b/docs/defaults/movie/director.md index ca4d3ae8..3c8f9fa7 100644 --- a/docs/defaults/movie/director.md +++ b/docs/defaults/movie/director.md @@ -39,7 +39,7 @@ This file contains a [Separator](../separators) so all [Shared Separator Variabl | Variable | Description & Values | |:------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `style` | **Description:** Controls the visual theme of the collections created.
**Default:** `bw`
**Values:** `bw`, `rainier`, `signature`, `diivoy`, or `diivoycolor` | +| `style` | **Description:** Controls the visual theme of the collections created.
**Default:** `bw`
**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` | | `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults file.
**Values:** Number Greater then 0 | | `limit_<>`1 | **Description:** Changes the Builder Limit of the specified key's collection.
**Default:** `limit`
**Values:** Number Greater then 0 | | `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults file.
**Default:** `release.desc`
**Values:** [Any `smart_filter` Sort Option](../../metadata/builders/smart.md#sort-options) | diff --git a/docs/defaults/movie/producer.md b/docs/defaults/movie/producer.md index e05f7b31..4e281582 100644 --- a/docs/defaults/movie/producer.md +++ b/docs/defaults/movie/producer.md @@ -39,7 +39,7 @@ This file contains a [Separator](../separators) so all [Shared Separator Variabl | Variable | Description & Values | |:------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `style` | **Description:** Controls the visual theme of the collections created.
**Default:** `bw`
**Values:** `bw`, `rainier`, `signature`, `diivoy`, or `diivoycolor` | +| `style` | **Description:** Controls the visual theme of the collections created.
**Default:** `bw`
**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` | | `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults file.
**Values:** Number Greater then 0 | | `limit_<>`1 | **Description:** Changes the Builder Limit of the specified key's collection.
**Default:** `limit`
**Values:** Number Greater then 0 | | `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults file.
**Default:** `release.desc`
**Values:** [Any `smart_filter` Sort Option](../../metadata/builders/smart.md#sort-options) | @@ -60,7 +60,7 @@ libraries: metadata_path: - pmm: producer template_variables: - style: diivoycolor + style: diiivoycolor use_separator: false sep_style: purple data: diff --git a/docs/defaults/movie/writer.md b/docs/defaults/movie/writer.md index 4b53127a..365b5a0f 100644 --- a/docs/defaults/movie/writer.md +++ b/docs/defaults/movie/writer.md @@ -39,7 +39,7 @@ This file contains a [Separator](../separators) so all [Shared Separator Variabl | Variable | Description & Values | |:------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `style` | **Description:** Controls the visual theme of the collections created.
**Default:** `bw`
**Values:** `bw`, `rainier`, `signature`, `diivoy`, or `diivoycolor` | +| `style` | **Description:** Controls the visual theme of the collections created.
**Default:** `bw`
**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` | | `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults file.
**Values:** Number Greater then 0 | | `limit_<>`1 | **Description:** Changes the Builder Limit of the specified key's collection.
**Default:** `limit`
**Values:** Number Greater then 0 | | `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults file.
**Default:** `release.desc`
**Values:** [Any `smart_filter` Sort Option](../../metadata/builders/smart.md#sort-options) | diff --git a/docs/defaults/overlay_variables.md b/docs/defaults/overlay_variables.md index 314f8914..c0285917 100644 --- a/docs/defaults/overlay_variables.md +++ b/docs/defaults/overlay_variables.md @@ -6,27 +6,28 @@ Note that the `template_variables:` section only needs to be used if you do want Below are the available variables which can be used to customize the file. -**NOTE:** `file`, `url`, `git`, `repo`, and `pmm` can all be used with `_<>` -
For example, with the audio_codec overlay `file_dtsx: config/overlays/dtsx.png` - -| Variable | Description & Values | -|:--------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `use_<>`1 | **Description:** Turns off individual Overlays in a Defaults file.
**Values:** `false` to turn off the overlay | -| `file` | **Description:** Controls the image associated with the Overlay to a local file.
**Values:** Filepath to Overlay Image | -| `url` | **Description:** Controls the image associated with the Overlay to a url.
**Values:** URL to Overlay Image | -| `git` | **Description:** Controls the image associated with the Overlay to the git repo.
**Values:** Git Path to Overlay Image | -| `repo` | **Description:** Controls the image associated with the Overlay to a custom repo.
**Values:** Repo Path to Overlay Image | -| `horizontal_offset` | **Description:** Controls the Horizontal Offset of this overlay. Can be a %.
**Values:** Number 0 or greater or 0%-100% | -| `horizontal_align` | **Description:** Controls the Horizontal Alignment of the overlay.
**Values:** `left`, `center`, or `right` | -| `vertical_offset` | **Description:** Controls the Vertical Offset of this overlay. Can be a %.
**Values:** Number 0 or greater or 0%-100% | -| `vertical_align` | **Description:** Controls the Vertical Alignment of the overlay.
**Values:** `top`, `center`, or `bottom` | -| `back_color` | **Description:** Controls the Backdrop Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | -| `back_width` | **Description:** Controls the Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text
**Values:** Any Number greater then 0 | -| `back_height` | **Description:** Controls the Backdrop Height for the Text Overlay. If `back_height` is not specified the Backdrop Sizes to the text
**Values:** Any Number greater then 0 | -| `back_align` | **Description:** Controls the Alignment for the Text Overlay inside the backdrop. If `back_align` is not specified the Backdrop Centers the text.
**Values:** `left`, `right`, `center`, `top`, or `bottom` | -| `back_padding` | **Description:** Controls the Backdrop Padding for the Text Overlay.
**Values:** Any Number greater then 0 | -| `back_radius` | **Description:** Controls the Backdrop Radius for the Text Overlay.
**Values:** Any Number greater then 0 | -| `back_line_color` | **Description:** Controls the Backdrop Line Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | -| `back_line_width` | **Description:** Controls the Backdrop Line Width for the Text Overlay.
**Values:** Any Number greater then 0 | +| Variable | Description & Values | +|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `use_<>`1 | **Description:** Turns off individual Overlays in a Defaults file.
**Values:** `false` to turn off the overlay | +| `file` | **Description:** Controls the images associated with all the Overlays to a local file.
**Values:** Filepath to Overlay Image | +| `file_<>`1 | **Description:** Controls the image associated with this key's Overlay to a local file.
**Values:** Filepath to Overlay Image | +| `url` | **Description:** Controls the images associated with all the Overlays to a url.
**Values:** URL to Overlay Image | +| `url_<>`1 | **Description:** Controls the image associated with this key's Overlay to a url.
**Values:** URL to Overlay Image | +| `git` | **Description:** Controls the images associated with all the Overlays to the git repo.
**Values:** Git Path to Overlay Image | +| `git_<>`1 | **Description:** Controls the image associated with this key's Overlay to the git repo.
**Values:** Git Path to Overlay Image | +| `repo` | **Description:** Controls the images associated with all the Overlays to a custom repo.
**Values:** Repo Path to Overlay Image | +| `repo_<>`1 | **Description:** Controls the image associated with this key's Overlay to a custom repo.
**Values:** Repo Path to Overlay Image | +| `horizontal_offset` | **Description:** Controls the Horizontal Offset of this overlay. Can be a %.
**Values:** Number 0 or greater or 0%-100% | +| `horizontal_align` | **Description:** Controls the Horizontal Alignment of the overlay.
**Values:** `left`, `center`, or `right` | +| `vertical_offset` | **Description:** Controls the Vertical Offset of this overlay. Can be a %.
**Values:** Number 0 or greater or 0%-100% | +| `vertical_align` | **Description:** Controls the Vertical Alignment of the overlay.
**Values:** `top`, `center`, or `bottom` | +| `back_color` | **Description:** Controls the Backdrop Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | +| `back_width` | **Description:** Controls the Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text
**Values:** Any Number greater then 0 | +| `back_height` | **Description:** Controls the Backdrop Height for the Text Overlay. If `back_height` is not specified the Backdrop Sizes to the text
**Values:** Any Number greater then 0 | +| `back_align` | **Description:** Controls the Alignment for the Text Overlay inside the backdrop. If `back_align` is not specified the Backdrop Centers the text.
**Values:** `left`, `right`, `center`, `top`, or `bottom` | +| `back_padding` | **Description:** Controls the Backdrop Padding for the Text Overlay.
**Values:** Any Number greater then 0 | +| `back_radius` | **Description:** Controls the Backdrop Radius for the Text Overlay.
**Values:** Any Number greater then 0 | +| `back_line_color` | **Description:** Controls the Backdrop Line Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | +| `back_line_width` | **Description:** Controls the Backdrop Line Width for the Text Overlay.
**Values:** Any Number greater then 0 | 1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. diff --git a/docs/defaults/overlays/audio_codec.md b/docs/defaults/overlays/audio_codec.md index 10e9391f..76a1b13a 100644 --- a/docs/defaults/overlays/audio_codec.md +++ b/docs/defaults/overlays/audio_codec.md @@ -83,7 +83,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa | `weight_<>`1 | **Description:** Controls the weight of the Overlay. Higher numbers have priority.
**Values:** Any Number | | `regex_<>`1 | **Description:** Controls the regex of the Overlay Search.
**Values:** Any Proper Regex | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/overlays/flixpatrol.md b/docs/defaults/overlays/flixpatrol.md index b8a4a6f5..da1ca874 100644 --- a/docs/defaults/overlays/flixpatrol.md +++ b/docs/defaults/overlays/flixpatrol.md @@ -75,7 +75,7 @@ All [Shared Overlay Variables](../overlay_variables) except `horizontal_offset`, | `addon_offset` | **Description:** Text Addon Image Offset from the text.
**Default:** `30`
**Values:** Any Number greater then 0 | | `addon_position` | **Description:** Text Addon Image Alignment in relation to the text.
**Default:** `top`
**Values:** `left`, `right`, `top`, `bottom` | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/overlays/language_count.md b/docs/defaults/overlays/language_count.md index e5cdf29b..fc927d08 100644 --- a/docs/defaults/overlays/language_count.md +++ b/docs/defaults/overlays/language_count.md @@ -61,7 +61,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa | `overlay_level` | **Description:** Choose the Overlay Level.
**Values:** `season` or `episode` | | `weight_<>`1 | **Description:** Controls the weight of the Overlay. Higher numbers have priority.
**Values:** Any Number | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/overlays/languages.md b/docs/defaults/overlays/languages.md index b36feed5..f4130211 100644 --- a/docs/defaults/overlays/languages.md +++ b/docs/defaults/overlays/languages.md @@ -145,7 +145,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa | `country_<>`1 | **Description:** Controls the country image for the Overlay.
**Default:** Listed in the [Table](#supported-audiosubtitle-language-flags) above
**Values:** [ISO 3166-1 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) for the flag desired | | `weight_<>`1 | **Description:** Controls the weight of the Overlay. Higher numbers have priority.
**Values:** Any Number | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/overlays/resolution.md b/docs/defaults/overlays/resolution.md index e7a025af..9b2076e8 100644 --- a/docs/defaults/overlays/resolution.md +++ b/docs/defaults/overlays/resolution.md @@ -12,15 +12,31 @@ Recommendations: Editions overlay is designed to use the Editions field within P ## Supported Resolutions -| Resolution | Key | -|:---------------|:----------| -| 4K | `4k` | -| 1080P | `1080p` | -| 720P | `720p` | -| 576P | `576p` | -| 480P | `480p` | -| DV | `dv` | -| HDR | `hdr` | +| Resolution | Key | Weight | +|:-------------|:-------------|:-------| +| 4K HDR10+ | `4k_plus` | `160` | +| 4K DV | `4k_dv` | `150` | +| 4K HDR | `4k_hdr` | `140` | +| 4K | `4k` | `130` | +| 1080P HDR10+ | `1080p_plus` | `125` | +| 1080P DV | `1080p_dv` | `120` | +| 1080P HDR | `1080p_hdr` | `110` | +| 1080P | `1080p` | `100` | +| 720P HDR10+ | `720p_plus` | `95` | +| 720P DV | `720p_dv` | `90` | +| 720P HDR | `720p_hdr` | `80` | +| 720P | `720p` | `70` | +| 576P HDR10+ | `576p_plus` | `65` | +| 576P DV | `576p_dv` | `60` | +| 576P HDR | `576p_hdr` | `50` | +| 576P | `576p` | `40` | +| 480P HDR10+ | `480p_plus` | `35` | +| 480P DV | `480p_dv` | `30` | +| 480P HDR | `480p_hdr` | `20` | +| 480P | `480p` | `10` | +| HDR10+ | `plus` | `7` | +| DV | `dv` | `5` | +| HDR | `hdr` | `1` | ## Supported Editions @@ -96,7 +112,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa | `overlay_level` | **Description:** Choose the Overlay Level.
**Values:** `season` or `episode` | | `weight_<>`1 | **Description:** Controls the weight of the Overlay. Higher numbers have priority. **Only works with Edition keys.**
**Values:** Any Number | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/overlays/ribbon.md b/docs/defaults/overlays/ribbon.md index 3cd8a6bd..8f7d7c90 100644 --- a/docs/defaults/overlays/ribbon.md +++ b/docs/defaults/overlays/ribbon.md @@ -51,7 +51,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa |:-------------------------------|:-------------------------------------------------------------------------------------------------------------| | `weight_<>`1 | **Description:** Controls the weight of the Overlay. Higher numbers have priority.
**Values:** Any Number | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/overlays/status.md b/docs/defaults/overlays/status.md index bbc92e53..0637fb05 100644 --- a/docs/defaults/overlays/status.md +++ b/docs/defaults/overlays/status.md @@ -59,7 +59,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa | `stroke_width` | **Description:** Font Stroke Width for the Text Overlay.
**Values:** Any Number greater then 0 | | `stroke_color` | **Description:** Font Stroke Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/overlays/streaming.md b/docs/defaults/overlays/streaming.md index 5c89e172..0acc2f59 100644 --- a/docs/defaults/overlays/streaming.md +++ b/docs/defaults/overlays/streaming.md @@ -70,18 +70,18 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa |:-----------------------------|:-------------------------------------------------------------------------------------------------------------| | `weight_<>`1 | **Description:** Controls the weight of the Overlay. Higher numbers have priority.
**Values:** Any Number | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. ## Regional Variants Some logic is applied to allow for regional streaming service lists to be available to users depending on where they are, as detailed below: -| Region | Key | Description | -|:-----------------|:---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| -| any besides `us` | `amazon`, `disney`, `netflix` | These collections will use regional variant lists to ensure the lists populate with what is available in the region specified | -| any besides `uk` | `all4`, `britbox`, `hayu`, `now` | These collections will not be created if the region is not `uk` as these streaming services are UK-focused | -| any besides `ca` | `crave` | These collections will not be created if the region is not `ca` as these streaming services are Canada-focused | -| `ca` | `hbomax`, `showtime` | These collections will not be created if the region is `ca` as these streaming services are part of the Crave streaming service in Canada | +| Region | Key | Description | +|:-----------------|:---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| any besides `us` | `amazon`, `disney`, `netflix` | These overlays will use regional variant lists to ensure the overlays are applied to what is available in the region specified | +| any besides `uk` | `all4`, `britbox`, `hayu`, `now` | These overlays will not be used if the region is not `uk` as these streaming services are UK-focused | +| any besides `ca` | `crave` | These overlays will not be used if the region is not `ca` as these streaming services are Canada-focused | +| `ca` | `hbomax`, `showtime` | These overlays will not be used if the region is `ca` as these streaming services are part of the Crave streaming service in Canada | The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/overlays/video_format.md b/docs/defaults/overlays/video_format.md index ec242258..a9403cd3 100644 --- a/docs/defaults/overlays/video_format.md +++ b/docs/defaults/overlays/video_format.md @@ -73,7 +73,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa | `weight_<>`1 | **Description:** Controls the weight of the Overlay. Higher numbers have priority.
**Values:** Any Number | | `regex_<>`1 | **Description:** Controls the regex of the Overlay Search.
**Values:** Any Proper Regex | -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. +1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<>` with when calling. The below is an example config.yml extract with some Template Variables added in to change how the file works. diff --git a/docs/defaults/people.md b/docs/defaults/people.md index c8d1fc8c..8ca41158 100644 --- a/docs/defaults/people.md +++ b/docs/defaults/people.md @@ -14,10 +14,10 @@ This Default can use the `style` template variable to easily change the posters ![](../images/person_signature.png) -### Diivoy Style +### Diiivoy Style -![](../images/person_diivoy.png) +![](../images/person_diiivoy.png) -### Diivoy Color Style +### Diiivoy Color Style -![](../images/person_diivoycolor.png) +![](../images/person_diiivoycolor.png) diff --git a/docs/defaults/playlist.md b/docs/defaults/playlist.md index 5816f2a9..b42209d1 100644 --- a/docs/defaults/playlist.md +++ b/docs/defaults/playlist.md @@ -48,6 +48,8 @@ Note that the `template_variables:` section only needs to be used if you do want | `exclude_user` | **Description:** Sets the users to exclude from sync for all playlists.
**Default:** `playlist_sync_to_user` Global Setting Value
**Values:** Comma-separated string or list of user names. | | `exclude_user_<>`1 | **Description:** Sets the users to exclude from sync the specified key's playlist.
**Default:** `sync_to_user` Value
**Values:** Comma-separated string or list of user names. | | `trakt_list_<>`1 | **Description:** Adds the Movies in the Trakt List to the specified key's playlist. Overrides the [default trakt_list](#default-trakt_list) for that playlist if used.
**Values:** List of Trakt List URLs | | | | +| `delete_playlist` | **Description:** Will delete all playlists for the users defined by sync_to_users.
**Values:** `true` or `false` | +| `delete_playlist_<>`1 | **Description:** Will delete the specified key's playlists for the users defined by sync_to_users.
**Values:** `true` or `false` | | `ignore_ids` | **Description:** Set a list or comma-separated string of TMDb/TVDb IDs to ignore in all playlists.
**Values:** List or comma-separated string of TMDb/TVDb IDs | | `ignore_imdb_ids` | **Description:** Set a list or comma-separated string of IMDb IDs to ignore in all playlists.
**Values:** List or comma-separated string of IMDb IDs | | `url_poster_<>`1 | **Description:** Changes the poster url of the specified key's playlist.
**Values:** URL directly to the Image | diff --git a/docs/home/environmental.md b/docs/home/environmental.md index 138b937d..ef82cea5 100644 --- a/docs/home/environmental.md +++ b/docs/home/environmental.md @@ -280,7 +280,7 @@ Run with every network request printed to the Logs. **This can potentially have Example --log-requests - PMM_NETWORK=true + PMM_LOG_REQUESTS=true diff --git a/docs/index.md b/docs/index.md index ef7278ca..54b31346 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ [![Discord](https://img.shields.io/discord/822460010649878528?color=%2300bc8c&label=Discord&style=plastic)](https://discord.gg/NfH6mGFuAB) [![Reddit](https://img.shields.io/reddit/subreddit-subscribers/PlexMetaManager?color=%2300bc8c&label=r%2FPlexMetaManager&style=plastic)](https://www.reddit.com/r/PlexMetaManager/) [![Wiki](https://img.shields.io/readthedocs/plex-meta-manager?color=%2300bc8c&style=plastic)](https://metamanager.wiki) -[![Translations](https://img.shields.io/weblate/progress/plex-meta-manager?color=00bc8c&server=https%3A%2F%2Ftranslations.metamanager.wiki&style=plastic)](https://translations.metamanager.wiki/engage/plex-meta-manager/) +[![Translations](https://img.shields.io/weblate/progress/plex-meta-manager?color=00bc8c&server=https%3A%2F%2Ftranslations.metamanager.wiki&style=plastic)](https://translations.metamanager.wiki/projects/plex-meta-manager/#languages) [![GitHub Sponsors](https://img.shields.io/github/sponsors/meisnate12?color=%238a2be2&style=plastic)](https://github.com/sponsors/meisnate12) [![Sponsor or Donate](https://img.shields.io/badge/-Sponsor%2FDonate-blueviolet?style=plastic)](https://github.com/sponsors/meisnate12) [![Feature Requests](https://img.shields.io/badge/Feature%20Requests-blueviolet?style=plastic)](https://features.metamanager.wiki/) diff --git a/docs/metadata/metadata/movie.md b/docs/metadata/metadata/movie.md index 1f2e7c75..8e98b988 100644 --- a/docs/metadata/metadata/movie.md +++ b/docs/metadata/metadata/movie.md @@ -103,16 +103,17 @@ The available attributes for editing movies are as follows ### Special Attributes -| Attribute | Allowed Values | -|:-------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `title` | Title if different from the mapping value useful when you have multiple movies with the same name. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | -| `alt_title` | Alternative title to look for and then change to the mapping name. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | -| `year` | Year of movie for better identification. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | -| `blank_edition`1 | Movie has no Edition. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | -| `edition_filter`1 | Edition of movie for better identification. Can be a list (only one needs to match). See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | -| `edition_contains`1 | Edition of movie must contain the given string for better identification. Can be a list (only one needs to match). See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | -| `tmdb_show` | TMDb Show ID to use for metadata useful for miniseries that have been compiled into a movie. **This is not used to say this show is the given ID.** | -| `tmdb_movie` | TMDb Movie ID to use for metadata useful for movies that have been split into segments **This is not used to say this show is the given ID.** | +| Attribute | Allowed Values | +|:-------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `title` | Title if different from the mapping value useful when you have multiple movies with the same name. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | +| `alt_title` | Alternative title to look for and then change to the mapping name. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | +| `year` | Year of movie for better identification. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | +| `blank_edition`1 | Movie has no Edition. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | +| `edition_filter`1 | Edition of movie for better identification. Can be a list (only one needs to match). See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | +| `edition_contains`1 | Edition of movie must contain the given string for better identification. Can be a list (only one needs to match). See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | +| `tmdb_show` | TMDb Show ID to use for metadata useful for miniseries that have been compiled into a movie. **This is not used to say this show is the given ID.** | +| `tmdb_movie` | TMDb Movie ID to use for metadata useful for movies that have been split into segments **This is not used to say this show is the given ID.** | +| `run_definition` | Used to specify if this definition runs.
Multiple can be used for one definition as a list or comma separated string. One `false` or unmatched library type will cause it to fail.
**Values:** `movie`, `show`, `artist`, `true`, `false` | 1. If the server does not have a Plex Pass then the Edition Field is not accessible. In this case PMM will check the movies filepath for `{edition-MOVIES EDITION}` to determine what the edition is. diff --git a/docs/metadata/metadata/music.md b/docs/metadata/metadata/music.md index 023622ad..c27497fe 100644 --- a/docs/metadata/metadata/music.md +++ b/docs/metadata/metadata/music.md @@ -67,18 +67,19 @@ The available attributes for editing artists, albums, and tracks are as follows `` ### General Attributes -| Attribute | Values | Artists | Album | Tracks | -|:-----------------------|:--------------------------------------------------------------|:--------:|:--------:|:--------:| -| `title` | Text to change Title | ❌ | ❌ | ✅ | -| `sort_title` | Text to change Sort Title | ✅ | ✅ | ✅ | -| `user_rating` | Number to change User Rating | ✅ | ✅ | ✅ | -| `critic_rating` | Number to change Critic Rating | ❌ | ✅ | ❌ | -| `originally_available` | Date to change Originally Available
**Format:** YYYY-MM-DD | ❌ | ✅ | ❌ | -| `record_label` | Text to change Record Label | ❌ | ✅ | ❌ | -| `summary` | Text to change Summary | ✅ | ✅ | ✅ | -| `track` | Text to change Track | ❌ | ❌ | ✅ | -| `disc` | Text to change Disc | ❌ | ❌ | ✅ | -| `original_artist` | Text to change Original Artist | ❌ | ❌ | ✅ | +| Attribute | Values | Artists | Album | Tracks | +|:-----------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:| +| `title` | Text to change Title | ❌ | ❌ | ✅ | +| `sort_title` | Text to change Sort Title | ✅ | ✅ | ✅ | +| `user_rating` | Number to change User Rating | ✅ | ✅ | ✅ | +| `critic_rating` | Number to change Critic Rating | ❌ | ✅ | ❌ | +| `originally_available` | Date to change Originally Available
**Format:** YYYY-MM-DD | ❌ | ✅ | ❌ | +| `record_label` | Text to change Record Label | ❌ | ✅ | ❌ | +| `summary` | Text to change Summary | ✅ | ✅ | ✅ | +| `track` | Text to change Track | ❌ | ❌ | ✅ | +| `disc` | Text to change Disc | ❌ | ❌ | ✅ | +| `original_artist` | Text to change Original Artist | ❌ | ❌ | ✅ | +| `run_definition` | Used to specify if this definition runs.
Multiple can be used for one definition as a list or comma separated string. One `false` or unmatched library type will cause it to fail.
**Values:** `movie`, `show`, `artist`, `true`, `false` | ✅ | ❌ | ❌ | ### Tag Attributes diff --git a/docs/metadata/metadata/show.md b/docs/metadata/metadata/show.md index 8bef3135..c2628f17 100644 --- a/docs/metadata/metadata/show.md +++ b/docs/metadata/metadata/show.md @@ -88,7 +88,7 @@ The mapping name is the season number (use 0 for specials) or the season name. To edit the metadata of a particular Episode in a Season use the `episodes` attribute on its season. -The mapping name is the episode number in that season or the title of the episode. +The mapping name is the episode number in that season, the title of the episode, or the Originally Available date in the format `MM/DD`. ## Metadata Edits diff --git a/docs/metadata/overlay.md b/docs/metadata/overlay.md index 654f9512..9c9dad76 100644 --- a/docs/metadata/overlay.md +++ b/docs/metadata/overlay.md @@ -200,24 +200,24 @@ You can use the item's metadata to determine the text by adding Special Text Var There are multiple Special Text Variables that can be used when formatting the text. The variables are defined like so `<>` and some can have modifiers like so `<>` where `$` is the modifier. The available options are: -| Special Text Variables & Mods | Movies | Shows | Seasons | Episodes | -|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:| -| `<>`: audience rating (`8.7`, `9.0`)
`<>`: audience rating out of 100 (`87`, `90`)
`<>`: audience rating removing `.0` as needed (`8.7`, `9`)
`<>`: audience rating on a 5 point scale (`8.6` shows as `4.3`) | ✅ | ✅ | ❌ | ✅ | -| `<>`: critic rating (`8.7`, `9.0`)
`<>`: critic rating out of 100 (`87`, `90`)
`<>`: critic rating removing `.0` as needed (`8.7`, `9`)
`<>`: critic rating on a 5 point scale (`8.6` shows as `4.3`) | ✅ | ✅ | ❌ | ✅ | -| `<>`: user rating (`8.7`, `9.0`)
`<>`: user rating out of 100 (`87`, `90`)
`<>`: user rating removing `.0` as needed (`8.7`, `9`)
`<>`: user rating on a 5 point scale (`8.6` shows as `4.3`) | ✅ | ✅ | ✅ | ✅ | -| `<>`: Title of the Item<br>`<<titleU>>`: Uppercase Title of the Item<br>`<<titleL>>`Lowercase Title of the Item<br>`<<titleP>>`Proper Title of the Item | ✅ | ✅ | ✅ | ✅ | -| `<<show_title>>`: Title of the Item's Show<br>`<<show_itleU>>`: Uppercase Title of the Item's Show<br>`<<show_titleL>>`Lowercase Title of the Item's Show<br>`<<show_titleP>>`Proper Title of the Item's Show | ❌ | ❌ | ✅ | ✅ | -| `<<season_title>>`: Title of the Item's Season<br>`<<season_titleU>>`: Uppercase Title of the Item's Season<br>`<<season_titleL>>`Lowercase title of the Item's Season<br>`<<season_titleP>>`Proper title of the Item's Season | ❌ | ❌ | ❌ | ✅ | -| `<<original_title>>`: Original Title of the Item<br>`<<original_titleU>>`: Original Title of the Item<br>`<<original_titleL>>`Lowercase Original Title of the Item<br>`<<original_titleP>>`Proper Original Title of the Item | ✅ | ✅ | ❌ | ❌ | -| `<<edition>>`: Edition of the Item<br>`<<editionU>>`: Uppercase Edition of the Item<br>`<<editionL>>`Lowercase Edition of the Item<br>`<<editionP>>`Proper Edition of the Item | ✅ | ❌ | ❌ | ❌ | -| `<<content_rating>>`: Content Rating of the Item<br>`<<content_ratingU>>`: Uppercase Content Rating of the Item<br>`<<content_ratingL>>`Lowercase Content Rating of the Item<br>`<<content_ratingP>>`Proper Content Rating of the Item | ✅ | ✅ | ❌ | ✅ | -| `<<episode_count>>`: Number of Episodes (`1`)<br>`<<episode_countW>>`: Number of Episodes As Words (`One`)<br>`<<episode_count0>>`: Number of Episodes With 10s Padding (`01`)<br>`<<episode_count00>>`: Number of Episodes With 100s Padding (`001`) | ❌ | ✅ | ✅ | ❌ | -| `<<season_number>>`: Season Number (`1`)<br>`<<season_numberW>>`: Season Number As Words (`One`)<br>`<<season_number0>>`: Season Number With 10s Padding (`01`)<br>`<<season_number00>>`: Season Number With 100s Padding (`001`) | ❌ | ❌ | ✅ | ✅ | -| `<<episode_number>>`: Episode Number (`1`)<br>`<<episode_numberW>>`: Episode Number As Words (`One`)<br>`<<episode_number0>>`: Episode Number With 10s Padding (`01`)<br>`<<episode_number00>>`: Episode Number With 100s Padding (`001`) | ❌ | ❌ | ❌ | ✅ | -| `<<versions>>`: Number of Versions of the Item (`1`)<br>`<<versionsW>>`: Number of Versions of the Item As Words (`One`)<br>`<<versions0>>`: Number of Versions of the Item With 10s Padding (`01`)<br>`<<versions00>>`: Number of Versions of the Item With 100s Padding (`001`) | ✅ | ❌ | ❌ | ✅ | -| `<<runtime>>`: Complete Runtime of the Item in minutes (`150`)<br>`<<runtimeH>>`: Hours in runtime of the Item (`2`)<br>`<<runtimeM>>`: Minutes remaining in the hour in the runtime of the Item (`30`) | ✅ | ❌ | ❌ | ✅ | -| `<<bitrate>>`: Bitrate of the first media file for an item.<br>`<<bitrateH>>`: Bitrate of the media file with the highest bitrate<br>`<<bitrateL>>`: Bitrate of the media file with the lowest bitrate | ✅ | ❌ | ❌ | ✅ | -| `<<originally_available>>`: Original Available Date of the Item<br>`<<originally_available[FORMAT]>>`: Original Available Date of the Item in the given format. [Format Options](https://strftime.org/) | ✅ | ✅ | ❌ | ✅ | +| Special Text Variables & Mods | Movies | Shows | Seasons | Episodes | +|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:| +| `<<audience_rating>>`: audience rating (`8.7`, `9.0`)<br>`<<audience_rating%>>`: audience rating out of 100 (`87`, `90`)<br>`<<audience_rating#>>`: audience rating removing `.0` as needed (`8.7`, `9`)<br>`<<audience_rating/>>`: audience rating on a 5 point scale (`8.6` shows as `4.3`) | ✅ | ✅ | ❌ | ✅ | +| `<<critic_rating>>`: critic rating (`8.7`, `9.0`)<br>`<<critic_rating%>>`: critic rating out of 100 (`87`, `90`)<br>`<<critic_rating#>>`: critic rating removing `.0` as needed (`8.7`, `9`)<br>`<<critic_rating/>>`: critic rating on a 5 point scale (`8.6` shows as `4.3`) | ✅ | ✅ | ❌ | ✅ | +| `<<user_rating>>`: user rating (`8.7`, `9.0`)<br>`<<user_rating%>>`: user rating out of 100 (`87`, `90`)<br>`<<user_rating#>>`: user rating removing `.0` as needed (`8.7`, `9`)<br>`<<user_rating/>>`: user rating on a 5 point scale (`8.6` shows as `4.3`) | ✅ | ✅ | ✅ | ✅ | +| `<<title>>`: Title of the Item<br>`<<titleU>>`: Uppercase Title of the Item<br>`<<titleL>>`Lowercase Title of the Item<br>`<<titleP>>`Proper Title of the Item | ✅ | ✅ | ✅ | ✅ | +| `<<show_title>>`: Title of the Item's Show<br>`<<show_itleU>>`: Uppercase Title of the Item's Show<br>`<<show_titleL>>`Lowercase Title of the Item's Show<br>`<<show_titleP>>`Proper Title of the Item's Show | ❌ | ❌ | ✅ | ✅ | +| `<<season_title>>`: Title of the Item's Season<br>`<<season_titleU>>`: Uppercase Title of the Item's Season<br>`<<season_titleL>>`Lowercase title of the Item's Season<br>`<<season_titleP>>`Proper title of the Item's Season | ❌ | ❌ | ❌ | ✅ | +| `<<original_title>>`: Original Title of the Item<br>`<<original_titleU>>`: Original Title of the Item<br>`<<original_titleL>>`Lowercase Original Title of the Item<br>`<<original_titleP>>`Proper Original Title of the Item | ✅ | ✅ | ❌ | ❌ | +| `<<edition>>`: Edition of the Item<br>`<<editionU>>`: Uppercase Edition of the Item<br>`<<editionL>>`Lowercase Edition of the Item<br>`<<editionP>>`Proper Edition of the Item | ✅ | ❌ | ❌ | ❌ | +| `<<content_rating>>`: Content Rating of the Item<br>`<<content_ratingU>>`: Uppercase Content Rating of the Item<br>`<<content_ratingL>>`Lowercase Content Rating of the Item<br>`<<content_ratingP>>`Proper Content Rating of the Item | ✅ | ✅ | ❌ | ✅ | +| `<<episode_count>>`: Number of Episodes (`1`)<br>`<<episode_countW>>`: Number of Episodes As Words (`One`)<br>`<<episode_countWU>>`: Number of Episodes As Uppercase Words (`ONE`)<br>`<<episode_countWL>>`: Number of Episodes As Lowercase Words (`one`)<br>`<<episode_count0>>`: Number of Episodes With 10s Padding (`01`)<br>`<<episode_count00>>`: Number of Episodes With 100s Padding (`001`) | ❌ | ✅ | ✅ | ❌ | +| `<<season_number>>`: Season Number (`1`)<br>`<<season_numberW>>`: Season Number As Words (`One`)<br>`<<season_numberWU>>`: Season Number As Uppercase Words (`ONE`)<br>`<<season_numberWL>>`: Season Number As Lowercase Words (`one`)<br>`<<season_number0>>`: Season Number With 10s Padding (`01`)<br>`<<season_number00>>`: Season Number With 100s Padding (`001`) | ❌ | ❌ | ✅ | ✅ | +| `<<episode_number>>`: Episode Number (`1`)<br>`<<episode_numberW>>`: Episode Number As Words (`One`)<br>`<<episode_numberWU>>`: Episode Number As Uppercase Words (`One`)<br>`<<episode_numberWL>>`: Episode Number As Lowercase Words (`one`)<br>`<<episode_number0>>`: Episode Number With 10s Padding (`01`)<br>`<<episode_number00>>`: Episode Number With 100s Padding (`001`) | ❌ | ❌ | ❌ | ✅ | +| `<<versions>>`: Number of Versions of the Item (`1`)<br>`<<versionsW>>`: Number of Versions of the Item As Words (`One`)<br>`<<versionsWO>>`: Number of Versions of the Item As Uppercase Words (`ONE`)<br>`<<versionsWL>>`: Number of Versions of the Item As Words (`one`)<br>`<<versions0>>`: Number of Versions of the Item With 10s Padding (`01`)<br>`<<versions00>>`: Number of Versions of the Item With 100s Padding (`001`) | ✅ | ❌ | ❌ | ✅ | +| `<<runtime>>`: Complete Runtime of the Item in minutes (`150`)<br>`<<runtimeH>>`: Hours in runtime of the Item (`2`)<br>`<<runtimeM>>`: Minutes remaining in the hour in the runtime of the Item (`30`) | ✅ | ❌ | ❌ | ✅ | +| `<<bitrate>>`: Bitrate of the first media file for an item.<br>`<<bitrateH>>`: Bitrate of the media file with the highest bitrate<br>`<<bitrateL>>`: Bitrate of the media file with the lowest bitrate | ✅ | ❌ | ❌ | ✅ | +| `<<originally_available>>`: Original Available Date of the Item<br>`<<originally_available[FORMAT]>>`: Original Available Date of the Item in the given format. [Format Options](https://strftime.org/) | ✅ | ✅ | ❌ | ✅ | Note: You can use the `mass_audience_rating_update` or `mass_critic_rating_update` [Library Operation](../config/operations) to update your plex ratings to various services like `tmdb`, `imdb`, `mdb`, `metacritic`, `letterboxd` and many more. diff --git a/docs/metadata/templates.md b/docs/metadata/templates.md index 9eb0c326..57b2715c 100644 --- a/docs/metadata/templates.md +++ b/docs/metadata/templates.md @@ -1,6 +1,6 @@ # Templates -Collection and Overlay Definitions often share a lot of common or generalizable configuration details. Templates allow you to define these details so they can be used across multiple definitions. +Collection, Playlist, Metadata, and Overlay Definitions often share a lot of common or generalizable configuration details. Templates allow you to define these details so they can be used across multiple definitions. For example, an actor collection might look like this: diff --git a/modules/builder.py b/modules/builder.py index f721fdc2..856dc9f3 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -4,6 +4,7 @@ from datetime import datetime from modules import anidb, anilist, flixpatrol, icheckmovies, imdb, letterboxd, mal, plex, radarr, reciperr, sonarr, tautulli, tmdb, trakt, tvdb, mdblist, util from modules.util import Failed, FilterFailed, NonExisting, NotScheduled, NotScheduledRange, Deleted from modules.overlay import Overlay +from modules.poster import PMMImage from plexapi.audio import Artist, Album, Track from plexapi.exceptions import NotFound from plexapi.video import Movie, Show, Season, Episode @@ -467,6 +468,19 @@ class CollectionBuilder: raise Failed(f"{self.Type} Error: {self.data[methods['builder_level']]} builder_level invalid{options}") self.parts_collection = self.builder_level in plex.builder_level_options + self.posters = {} + self.backgrounds = {} + if "pmm_poster" in methods: + logger.debug("") + logger.debug("Validating Method: pmm_poster") + if self.data[methods["pmm_poster"]] is None: + logger.error(f"{self.Type} Error: pmm_poster attribute is blank") + logger.debug(f"Value: {data[methods['pmm_poster']]}") + try: + self.posters["pmm_poster"] = PMMImage(self.config, self.data[methods["pmm_poster"]], "pmm_poster", playlist=self.playlist) + except Failed as e: + logger.error(e) + if self.overlay: if "overlay" in methods: overlay_data = data[methods["overlay"]] @@ -578,8 +592,6 @@ class CollectionBuilder: self.notification_removals = [] self.items = [] self.remove_item_map = {} - self.posters = {} - self.backgrounds = {} self.schedule = "" self.beginning_count = 0 self.default_percent = 50 @@ -3010,11 +3022,24 @@ class CollectionBuilder: self.collection_poster = util.pick_image(self.obj.title, self.posters, self.library.prioritize_assets, self.library.download_url_assets, asset_location) self.collection_background = util.pick_image(self.obj.title, self.backgrounds, self.library.prioritize_assets, self.library.download_url_assets, asset_location, is_poster=False) + clean_temp = False + if isinstance(self.collection_poster, PMMImage): + clean_temp = True + item_vars = {"title": self.name, "titleU": self.name.upper(), "titleL": self.name.lower()} + self.collection_poster = self.collection_poster.save(item_vars) + if self.collection_poster or self.collection_background: pu, bu = self.library.upload_images(self.obj, poster=self.collection_poster, background=self.collection_background) if pu or bu: updated_details.append("Image") + if clean_temp: + code_base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + posters_dir = os.path.join(code_base, "defaults", "posters") + for filename in os.listdir(posters_dir): + if "temp" in filename: + os.remove(os.path.join(posters_dir, filename)) + if self.url_theme: # TODO: cache theme path to not constantly upload self.library.upload_theme(self.obj, url=self.url_theme) elif self.file_theme: diff --git a/modules/plex.py b/modules/plex.py index d2591858..4a9ebc49 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -490,7 +490,7 @@ class Plex(Library): def get_all_collections(self, label=None): args = "?type=18" if label: - label_id = next((c.key for c in self.get_tags("label") if c.title == label), None) + label_id = next((c.key for c in self.get_tags("label") if c.title == label), None) # noqa if label_id: args = f"{args}&label={label_id}" else: @@ -677,12 +677,11 @@ class Plex(Library): item, is_full = self.cached_items[item.ratingKey] try: if not is_full or force: - item.reload(checkFiles=False, includeAllConcerts=False, includeBandwidths=False, - includeChapters=False, includeChildren=False, includeConcerts=False, - includeExternalMedia=False, includeExtras=False, includeFields=False, - includeGeolocation=False, includeLoudnessRamps=False, includeMarkers=False, - includeOnDeck=False, includePopularLeaves=False, includeRelated=False, - includeRelatedCount=0, includeReviews=False, includeStations=False) + item.reload(checkFiles=False, includeAllConcerts=False, includeBandwidths=False, includeChapters=False, + includeChildren=False, includeConcerts=False, includeExternalMedia=False, includeExtras=False, + includeFields=False, includeGeolocation=False, includeLoudnessRamps=False, includeMarkers=False, + includeOnDeck=False, includePopularLeaves=False, includeRelated=False, includeRelatedCount=0, + includeReviews=False, includeStations=False) item._autoReload = False self.cached_items[item.ratingKey] = (item, True) except (BadRequest, NotFound) as e: @@ -843,7 +842,7 @@ class Plex(Library): self._query(key, put=True) def smart_label_check(self, label): - labels = [la.title for la in self.get_tags("label")] + labels = [la.title for la in self.get_tags("label")] # noqa if label in labels: return True logger.trace(f"Label not found in Plex. Options: {labels}") @@ -878,7 +877,7 @@ class Plex(Library): self._query(f"/library/collections{utils.joinArgs(args)}", post=True) def get_smart_filter_from_uri(self, uri): - smart_filter = parse.parse_qs(parse.urlparse(uri.replace("/#!/", "/")).query)["key"][0] + smart_filter = parse.parse_qs(parse.urlparse(uri.replace("/#!/", "/")).query)["key"][0] # noqa args = smart_filter[smart_filter.index("?"):] return self.build_smart_filter(args), int(args[args.index("type=") + 5:args.index("type=") + 6]) diff --git a/modules/poster.py b/modules/poster.py new file mode 100644 index 00000000..46e344b2 --- /dev/null +++ b/modules/poster.py @@ -0,0 +1,385 @@ +import os, time +from modules import util +from modules.util import Failed, ImageData +from PIL import Image, ImageFont, ImageDraw, ImageColor + +logger = util.logger + +class ImageBase: + def __init__(self, config, data): + self.config = config + self.data = data + self.methods = {str(m).lower(): m for m in self.data} + self.code_base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.posters_dir = os.path.join(self.code_base, "defaults", "posters") + + def check_data(self, attr): + if attr not in self.methods or not self.data[self.methods[attr]]: + return None + return self.data[self.methods[attr]] + + def check_file(self, attr, pmm_items, local=False, required=False): + if attr not in self.methods or not self.data[self.methods[attr]]: + if required: + raise Failed(f"Posters Error: {attr} not found or is blank") + return None + file_data = self.data[self.methods[attr]] + if isinstance(file_data, list): + file_data = file_data[0] + if not isinstance(file_data, dict): + file_data = {"pmm": str(file_data)} + if "pmm" in file_data and file_data["pmm"]: + file_path = pmm_items[file_data["pmm"]] if file_data["pmm"] in pmm_items else file_data["pmm"] + if os.path.exists(file_path): + return file_path, os.path.getsize(file_path) + raise Failed(f"Poster Error: {attr} pmm invalid. Options: {', '.join(pmm_items.keys())}") + elif "file" in file_data and file_data["file"]: + if os.path.exists(file_data["file"]): + return file_data["file"], os.path.getsize(file_data["file"]) + raise Failed(f"Poster Error: {attr} file not found: {os.path.abspath(file_data['file'])}") + elif local: + return None, None + elif "git" in file_data and file_data["git"]: + url = f"{self.config.GitHub.configs_url}{file_data['git']}" + elif "repo" in file_data and file_data["repo"]: + url = f"{self.config.custom_repo}{file_data['repo']}" + elif "url" in file_data and file_data["url"]: + url = file_data["url"] + else: + return None, None + + response = self.config.get(url) + if response.status_code >= 400: + raise Failed(f"Poster Error: {attr} not found at: {url}") + if "Content-Type" not in response.headers or response.headers["Content-Type"] not in util.image_content_types: + raise Failed(f"Poster Error: {attr} not a png, jpg, or webp: {url}") + if response.headers["Content-Type"] == "image/jpeg": + ext = "jpg" + elif response.headers["Content-Type"] == "image/webp": + ext = "webp" + else: + ext = "png" + num = "" + image_path = os.path.join(self.posters_dir, f"temp{num}.{ext}") + while os.path.exists(image_path): + if not num: + num = 1 + else: + num += 1 + image_path = os.path.join(self.posters_dir, f"temp{num}.{ext}") + with open(image_path, "wb") as handler: + handler.write(response.content) + while util.is_locked(image_path): + time.sleep(1) + return image_path, url + + def check_color(self, attr): + if attr not in self.methods or not self.data[self.methods[attr]]: + return None + try: + return ImageColor.getcolor(self.data[self.methods[attr]], "RGBA") + except ValueError: + raise Failed(f"Poster Error: {attr}: {self.data[self.methods[attr]]} invalid") + +class Component(ImageBase): + def __init__(self, config, data): + super().__init__(config, data) + self.draw = ImageDraw.Draw(Image.new("RGBA", (0, 0))) + self.back_color = self.check_color("back_color") + self.back_radius = util.parse("Posters", "back_radius", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "back_radius" in self.methods else 0 + self.back_line_width = util.parse("Posters", "back_line_width", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "back_line_width" in self.methods else 0 + self.back_line_color = self.check_color("back_line_color") + self.back_padding = util.parse("Posters", "back_padding", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "back_padding" in self.methods else 0 + self.back_align = util.parse("Posters", "back_align", self.data, methods=self.methods, default="center", options=["left", "right", "center", "top", "bottom"]) if "back_align" in self.methods else "center" + + self.back_width = 0 + if "back_width" in self.methods: + if str(self.methods["back_width"]).lower() == "max": + self.back_width = "max" + else: + self.back_width = util.parse("Posters", "back_width", self.data, methods=self.methods, datatype="int", minimum=0) + self.back_height = 0 + if "back_height" in self.methods: + if str(self.methods["back_height"]).lower() == "max": + self.back_height = "max" + else: + self.back_height = util.parse("Posters", "back_height", self.data, methods=self.methods, datatype="int", minimum=0) + self.has_back = True if self.back_color or self.back_line_color else False + self.horizontal_offset, self.horizontal_align, self.vertical_offset, self.vertical_align = util.parse_cords(self.data, "component", err_type="Posters", default=(0, "center", 0, "center")) + + self.images_dir = os.path.join(self.posters_dir, "images") + self.pmm_images = {k[:-4]: os.path.join(self.images_dir, k) for k in os.listdir(self.images_dir)} + self.image, self.image_compare = self.check_file("image", self.pmm_images) + self.image_width = util.parse("Posters", "image_width", self.data, datatype="int", methods=self.methods, default=0, minimum=0, maximum=2000) if "image_width" in self.methods else 0 + self.image_color = self.check_color("image_color") + + self.text = None + self.font_name = None + self.font = None + self.font_style = None + self.addon_position = None + self.text_align = util.parse("Posters", "text_align", self.data, methods=self.methods, default="center", options=["left", "right", "center"]) if "text_align" in self.methods else "center" + self.font_size = util.parse("Posters", "font_size", self.data, datatype="int", methods=self.methods, default=163, minimum=1) if "font_size" in self.methods else 163 + self.font_color = self.check_color("font_color") + self.stroke_color = self.check_color("stroke_color") + self.stroke_width = util.parse("Posters", "stroke_width", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "stroke_width" in self.methods else 0 + self.addon_offset = util.parse("Posters", "addon_offset", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "stroke_width" in self.methods else 0 + if "text" in self.methods: + font_base = os.path.join(self.code_base, "fonts") + pmm_fonts = os.listdir(font_base) + all_fonts = {s: s for s in util.get_system_fonts()} + for font_name in pmm_fonts: + all_fonts[font_name] = os.path.join(font_base, font_name) + self.text = util.parse("Posters", "text", self.data, methods=self.methods, default="<<title>>") + self.font_name, self.font_compare = self.check_file("font", all_fonts, local=True) + if not self.font_name: + self.font_name = all_fonts["Roboto-Medium.ttf"] + self.font = ImageFont.truetype(self.font_name, self.font_size) + if "font_style" in self.methods and self.data[self.methods["font_style"]]: + try: + variation_names = [n.decode("utf-8") for n in self.font.get_variation_names()] + if self.data[self.methods["font_style"]] in variation_names: + self.font.set_variation_by_name(self.data[self.methods["font_style"]]) + self.font_style = self.data[self.methods["font_style"]] + else: + raise Failed(f"Posters Error: Font Style {self.data[self.methods['font_style']]} not found. Options: {','.join(variation_names)}") + except OSError: + raise Failed(f"Posters Warning: font: {self.font} does not have variations") + self.addon_position = util.parse("Posters", "addon_position", self.data, methods=self.methods, options=["left", "right", "top", "bottom"]) if "addon_position" in self.methods else "left" + + if not self.image and not self.text: + raise Failed("Posters Error: An image or text is required for each component") + + def apply_vars(self, item_vars): + for var_key, var_data in item_vars.items(): + self.text = self.text.replace(f"<<{var_key}>>", str(var_data)) + + def adjust_text_width(self, max_width): + lines = [] + for line in self.text.split("\n"): + for word in line.split(" "): + word_length = self.draw.textlength(word, font=self.font) + while word_length > max_width: + self.font_size -= 1 + self.font = ImageFont.truetype(self.font_name, self.font_size) + word_length = self.draw.textlength(word, font=self.font) + for line in self.text.split("\n"): + line_length = self.draw.textlength(line, font=self.font) + if line_length <= max_width: + lines.append(line) + continue + current_line = "" + line_length = 0 + for word in line.split(" "): + if current_line: + word = f" {word}" + word_length = self.draw.textlength(word, font=self.font) + if line_length + word_length <= max_width: + current_line += word + line_length += word_length + else: + if current_line: + lines.append(current_line) + word = word.strip() + word_length = self.draw.textlength(word, font=self.font) + current_line = word + line_length = word_length + if current_line: + lines.append(current_line) + self.text = "\n".join(lines) + + def get_compare_string(self): + output = "" + if self.text: + output += f"{self.text} {self.text_align} {self.font_compare}" + output += str(self.font_size) + for value in [self.font_color, self.font_style, self.stroke_color, self.stroke_width]: + if value: + output += f"{value}" + if self.image: + output += f"{self.addon_position} {self.addon_offset}" + + if self.image: + output += str(self.image_compare) + for value in [self.image_width, self.image_color]: + if value: + output += str(value) + + output += f"({self.horizontal_offset},{self.horizontal_align},{self.vertical_offset},{self.vertical_align})" + if self.has_back: + for value in [self.back_color, self.back_radius, self.back_padding, self.back_align, + self.back_width, self.back_height, self.back_line_color, self.back_line_width]: + if value is not None: + output += f"{value}" + return output + + def get_text_size(self, text): + return self.draw.multiline_textbbox((0, 0), text, font=self.font) + + def get_coordinates(self, canvas_box, box, new_cords=None): + canvas_width, canvas_height = canvas_box + box_width, box_height = box + + def get_cord(value, image_value, over_value, align): + value = int(image_value * 0.01 * int(value[:-1])) if str(value).endswith("%") else int(value) + if align in ["right", "bottom"]: + return image_value - over_value - value + elif align == "center": + return int(image_value / 2) - int(over_value / 2) + value + else: + return value + if new_cords: + ho, ha, vo, va = new_cords + else: + ho, ha, vo, va = self.horizontal_offset, self.horizontal_align, self.vertical_offset, self.vertical_align + + return get_cord(ho, canvas_width, box_width, ha), get_cord(vo, canvas_height, box_height, va) + + def get_generated_layer(self, canvas_box, new_cords=None): + canvas_width, canvas_height = canvas_box + generated_layer = None + text_width, text_height = None, None + if self.image: + image = Image.open(self.image) + image_width, image_height = image.size + if self.image_width: + image_height = int(float(image_height) * float(self.image_width / float(image_width))) + image_width = self.image_width + image = image.resize((image_width, image_height), Image.Resampling.LANCZOS) # noqa + if self.image_color: + r, g, b = self.image_color + pixels = image.load() + for x in range(image_width): + for y in range(image_height): + if pixels[x, y][3] > 0: # noqa + pixels[x, y] = (r, g, b, pixels[x, y][3]) # noqa + else: + image, image_width, image_height = None, 0, 0 + if self.text is not None: + _, _, text_width, text_height = self.get_text_size(self.text) + if image_width and self.addon_position in ["left", "right"]: + box = (text_width + image_width + self.addon_offset, text_height if text_height > image_height else image_height) + elif image_width: + box = (text_width if text_width > image_width else image_width, text_height + image_height + self.addon_offset) + else: + box = (text_width, text_height) + else: + box = (image_width, image_height) + box_width, box_height = box + back_width = canvas_width if self.back_width == "max" else self.back_width if self.back_width else box_width + back_height = canvas_height if self.back_height == "max" else self.back_height if self.back_height else box_height + main_point = self.get_coordinates(canvas_box, (back_width, back_height), new_cords=new_cords) + start_x, start_y = main_point + + if self.text is not None or self.has_back: + generated_layer = Image.new("RGBA", canvas_box, (255, 255, 255, 0)) + drawing = ImageDraw.Draw(generated_layer) + if self.has_back: + cords = ( + start_x - self.back_padding, + start_y - self.back_padding, + start_x + back_width + self.back_padding, + start_y + back_height + self.back_padding + ) + if self.back_radius: + drawing.rounded_rectangle(cords, fill=self.back_color, outline=self.back_line_color, width=self.back_line_width, radius=self.back_radius) + else: + drawing.rectangle(cords, fill=self.back_color, outline=self.back_line_color, width=self.back_line_width) + + main_x, main_y = main_point + if self.back_height and self.back_align in ["left", "right", "center", "bottom"]: + main_y = start_y + (back_height - box_height) // (1 if self.back_align == "bottom" else 2) + if self.back_width and self.back_align in ["top", "bottom", "center", "right"]: + main_x = start_x + (back_width - box_width) // (1 if self.back_align == "right" else 2) + + addon_x = None + addon_y = None + if self.text is not None and self.image: + addon_x = main_x + addon_y = main_y + if self.addon_position == "left": + main_x = main_x + image_width + self.addon_offset + elif self.addon_position == "right": + addon_x = main_x + text_width + self.addon_offset + elif text_width < image_width: + main_x = main_x + ((image_width - text_width) / 2) + elif text_width > image_width: + addon_x = main_x + ((text_width - image_width) / 2) + + if self.addon_position == "top": + main_y = main_y + image_height + self.addon_offset + elif self.addon_position == "bottom": + addon_y = main_y + text_height + self.addon_offset + elif text_height < image_height: + main_y = main_y + ((image_height - text_height) / 2) + elif text_height > image_height: + addon_y = main_y + ((text_height - image_height) / 2) + main_point = (int(main_x), int(main_y)) + + if self.text is not None: + drawing.multiline_text(main_point, self.text, font=self.font, fill=self.font_color, align=self.text_align, + stroke_fill=self.stroke_color, stroke_width=self.stroke_width) + if addon_x is not None: + main_point = (addon_x, addon_y) + + return generated_layer, main_point, image + +class PMMImage(ImageBase): + def __init__(self, config, data, image_attr, playlist=False): + super().__init__(config, data) + self.image_attr = image_attr + self.backgrounds_dir = os.path.join(self.posters_dir, "backgrounds") + self.playlist = playlist + self.pmm_backgrounds = {k[:-4]: os.path.join(self.backgrounds_dir, k) for k in os.listdir(self.backgrounds_dir)} + + self.background_image, self.background_compare = self.check_file("background_image", self.pmm_backgrounds) + self.background_color = self.check_color("background_color") + self.border_width = util.parse("Posters", "border_width", self.data, datatype="int", methods=self.methods, default=0, minimum=0) if "border_width" in self.methods else 0 + self.border_color = self.check_color("border_color") + if "components" not in self.methods or not self.data[self.methods["components"]]: + raise Failed("Posters Error: components attribute is required") + self.components = [Component(self.config, d) for d in util.parse("Posters", "components", self.data, datatype="listdict", methods=self.methods)] + + def get_compare_string(self): + output = "" + for value in [self.background_compare, self.background_color, self.border_width, self.border_color]: + if value: + output += f"{value}" + for component in self.components: + output += component.get_compare_string() + return output + + def save(self, item_vars): + image_path = os.path.join(self.posters_dir, "temp_poster.png") + if os.path.exists(image_path): + os.remove(image_path) + canvas_width = 1000 + canvas_height = 1000 if self.playlist else 1500 + canvas_box = (canvas_width, canvas_height) + + pmm_image = Image.new(mode="RGB", size=canvas_box, color=self.background_color) + if self.background_image: + bkg_image = Image.open(self.background_image) + bkg_image = bkg_image.resize(canvas_box, Image.Resampling.LANCZOS) # noqa + pmm_image.paste(bkg_image, (0, 0), bkg_image) + + if self.border_width: + draw = ImageDraw.Draw(pmm_image) + draw.rectangle(((0, 0), canvas_box), outline=self.border_color, width=self.border_width) + + max_border_width = canvas_width - self.border_width - 100 + + for component in self.components: + if component.text: + component.apply_vars(item_vars) + component.adjust_text_width(component.back_width if component.back_width and component.back_width != "max" else max_border_width) + generated_layer, image_point, image = component.get_generated_layer(canvas_box) + if generated_layer: + pmm_image.paste(generated_layer, (0, 0), generated_layer) + if image: + pmm_image.paste(image, image_point, image) + + pmm_image.save(image_path) + + return ImageData(self.image_attr, image_path, is_url=False, compare=self.get_compare_string()) + diff --git a/modules/util.py b/modules/util.py index 70ddb83e..82fca6f2 100644 --- a/modules/util.py +++ b/modules/util.py @@ -44,13 +44,13 @@ class NotScheduledRange(NotScheduled): pass class ImageData: - def __init__(self, attribute, location, prefix="", is_poster=True, is_url=True): + def __init__(self, attribute, location, prefix="", is_poster=True, is_url=True, compare=None): self.attribute = attribute self.location = location self.prefix = prefix self.is_poster = is_poster self.is_url = is_url - self.compare = location if is_url else os.stat(location).st_size + self.compare = compare if compare else location if is_url else os.stat(location).st_size self.message = f"{prefix}{'poster' if is_poster else 'background'} to [{'URL' if is_url else 'File'}] {location}" def __str__(self): @@ -211,8 +211,8 @@ def pick_image(title, images, prioritize_assets, download_url_assets, item_dir, if prioritize_assets and "asset_directory" in images: return images["asset_directory"] for attr in ["style_data", f"url_{image_type}", f"file_{image_type}", f"tmdb_{image_type}", "tmdb_profile", - "tmdb_list_poster", "tvdb_list_poster", f"tvdb_{image_type}", "asset_directory", "tmdb_person", - "tmdb_collection_details", "tmdb_actor_details", "tmdb_crew_details", "tmdb_director_details", + "tmdb_list_poster", "tvdb_list_poster", f"tvdb_{image_type}", "asset_directory", f"pmm_{image_type}", + "tmdb_person", "tmdb_collection_details", "tmdb_actor_details", "tmdb_crew_details", "tmdb_director_details", "tmdb_producer_details", "tmdb_writer_details", "tmdb_movie_details", "tmdb_list_details", "tvdb_list_details", "tvdb_movie_details", "tvdb_show_details", "tmdb_show_details"]: if attr in images: @@ -224,7 +224,7 @@ def pick_image(title, images, prioritize_assets, download_url_assets, item_dir, return download_image(title, images[attr], item_dir, image_name) except Failed as e: logger.error(e) - if attr == "asset_directory": + if attr in ["asset_directory", f"pmm_{image_type}"]: return images[attr] return ImageData(attr, images[attr], is_poster=is_poster, is_url=attr != f"file_{image_type}") @@ -738,7 +738,7 @@ def parse(error, attribute, data, datatype=None, methods=None, parent=None, defa return [] elif datatype == "listdict": final_list = [] - for dict_data in get_list(value): + for dict_data in get_list(value, split=False): if isinstance(dict_data, dict): final_list.append(dict_data) else: @@ -816,16 +816,21 @@ def parse(error, attribute, data, datatype=None, methods=None, parent=None, defa logger.warning(f"{error} Warning: {message} using {default} as default") return translation[default] if translation is not None else default -def parse_cords(data, parent, required=False): - horizontal_align = parse("Overlay", "horizontal_align", data["horizontal_align"], parent=parent, +def parse_cords(data, parent, required=False, err_type="Overlay", default=None): + dho, dha, dvo, dva = default if default else (None, None, None, None) + horizontal_align = parse(err_type, "horizontal_align", data["horizontal_align"], parent=parent, options=["left", "center", "right"]) if "horizontal_align" in data else None - if required and horizontal_align is None: - raise Failed(f"Overlay Error: {parent} horizontal_align is required") + if horizontal_align is None: + if required: + raise Failed(f"{err_type} Error: {parent} horizontal_align is required") + horizontal_align = dha - vertical_align = parse("Overlay", "vertical_align", data["vertical_align"], parent=parent, + vertical_align = parse(err_type, "vertical_align", data["vertical_align"], parent=parent, options=["top", "center", "bottom"]) if "vertical_align" in data else None - if required and vertical_align is None: - raise Failed(f"Overlay Error: {parent} vertical_align is required") + if vertical_align is None: + if required: + raise Failed(f"{err_type} Error: {parent} vertical_align is required") + vertical_align = dva horizontal_offset = None if "horizontal_offset" in data and data["horizontal_offset"] is not None: @@ -835,7 +840,7 @@ def parse_cords(data, parent, required=False): x_off = x_off[:-1] per = True x_off = check_num(x_off) - error = f"Overlay Error: {parent} horizontal_offset: {data['horizontal_offset']} must be a number" + error = f"{err_type} Error: {parent} horizontal_offset: {data['horizontal_offset']} must be a number" if x_off is None: raise Failed(error) if horizontal_align != "center" and not per and x_off < 0: @@ -845,8 +850,10 @@ def parse_cords(data, parent, required=False): elif horizontal_align == "center" and per and (x_off > 50 or x_off < -50): raise Failed(f"{error} between -50% and 50%") horizontal_offset = f"{x_off}%" if per else x_off - if required and horizontal_offset is None: - raise Failed(f"Overlay Error: {parent} horizontal_offset is required") + if horizontal_offset is None: + if required: + raise Failed(f"{err_type} Error: {parent} horizontal_offset is required") + horizontal_offset = dho vertical_offset = None if "vertical_offset" in data and data["vertical_offset"] is not None: @@ -856,7 +863,7 @@ def parse_cords(data, parent, required=False): y_off = y_off[:-1] per = True y_off = check_num(y_off) - error = f"Overlay Error: {parent} vertical_offset: {data['vertical_offset']} must be a number" + error = f"{err_type} Error: {parent} vertical_offset: {data['vertical_offset']} must be a number" if y_off is None: raise Failed(error) if vertical_align != "center" and not per and y_off < 0: @@ -866,8 +873,10 @@ def parse_cords(data, parent, required=False): elif vertical_align == "center" and per and (y_off > 50 or y_off < -50): raise Failed(f"{error} between -50% and 50%") vertical_offset = f"{y_off}%" if per else y_off - if required and vertical_offset is None: - raise Failed(f"Overlay Error: {parent} vertical_offset is required") + if vertical_offset is None: + if required: + raise Failed(f"{err_type} Error: {parent} vertical_offset is required") + vertical_offset = dvo return horizontal_offset, horizontal_align, vertical_offset, vertical_align