[167] update changelog and missing documentation to prep for 1.19

This commit is contained in:
meisnate12 2023-04-05 11:58:46 -04:00
parent d15ab560aa
commit 6d57dc2a8a
44 changed files with 652 additions and 167 deletions

View file

@ -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_<<key>>` 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
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

View file

@ -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/)

View file

@ -1 +1 @@
1.18.3-develop166
1.18.3-develop167

View file

@ -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

View file

@ -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:

View file

@ -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_<<key>>
@ -215,12 +215,12 @@ templates:
hdr: <<hdr>>
filters:
has_dolby_vision: <<dolby_vision>>
- filepath.regex: <<hdr10plus>>
filepath.regex: <<plus>>
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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -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"),

View file

@ -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.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diivoy`, or `diivoycolor` |
| `style` | **Description:** Controls the visual theme of the collections created.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` |
| `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults file.<br>**Values:** Number Greater then 0 |
| `limit_<<key>>`<sup>1</sup> | **Description:** Changes the Builder Limit of the specified key's collection.<br>**Default:** `limit`<br>**Values:** Number Greater then 0 |
| `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults file.<br>**Default:** `release.desc`<br>**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

View file

@ -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.<br>**Default:** `sync`<br>**Values:**<table class="clearTable"><tr><td>`sync`</td><td>Add and Remove Items based on Builders</td></tr><tr><td>`append`</td><td>Only Add Items based on Builders</td></tr></table> |
| `sync_mode_<<key>>`<sup>1</sup> | **Description:** Changes the Sync Mode of the specified key's collection.<br>**Default:** `sync_mode`<br>**Values:**<table class="clearTable"><tr><td>`sync`</td><td>Add and Remove Items based on Builders</td></tr><tr><td>`append`</td><td>Only Add Items based on Builders</td></tr></table> |
| `exclude` | **Description:** Exclude these Media Outlets from creating a Dynamic Collection.<br>**Values:** List of Media Outlet Keys |
| `based_name` | **Description:** Changes the title format of the Dynamic Collections.<br>**Default:** `Based on a <<key_name>>`<br>**Values:** Any string with `<<key_name>>` in it. |
| `based_summary` | **Description:** Changes the summary format of the Dynamic Collections.<br>**Default:** `<<library_translationU>>s based on or inspired by <<translated_key_name>>s.`<br>**Values:** Any string. |
| `name_format` | **Description:** Changes the title format of the Dynamic Collections.<br>**Default:** `Based on a <<key_name>>`<br>**Values:** Any string with `<<key_name>>` in it. |
| `summary_format` | **Description:** Changes the summary format of the Dynamic Collections.<br>**Default:** `<<library_translationU>>s based on or inspired by <<translated_key_name>>s.`<br>**Values:** Any string. |
1. Each default collection has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.

View file

@ -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.<br>**Default:** `sync`<br>**Values:**<table class="clearTable"><tr><td>`sync`</td><td>Add and Remove Items based on Builders</td></tr><tr><td>`append`</td><td>Only Add Items based on Builders</td></tr></table> |
| `sync_mode_<<key>>`<sup>1</sup> | **Description:** Changes the Sync Mode of the specified key's collection.<br>**Default:** `sync_mode`<br>**Values:**<table class="clearTable"><tr><td>`sync`</td><td>Add and Remove Items based on Builders</td></tr><tr><td>`append`</td><td>Only Add Items based on Builders</td></tr></table> |
| `exclude` | **Description:** Exclude these Streaming Services from creating a Dynamic Collection.<br>**Values:** List of Streaming Service Keys |
| `region` | **Description:** Changes some Streaming Service lists to regional variants (see below table for more information.<br>**Default:** `us`<br>**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.<br>**Default:** `us`<br>**Values:** `us`,`uk`,`ca`, `da`, `de`, `es`, `fr`, `it`, `pt-br` |
| `streaming_name` | **Description:** Changes the title format of the Dynamic Collections.<br>**Default:** `<<key_name>> <<library_translationU>>s`<br>**Values:** Any string with `<<key_name>>` in it. |
| `streaming_summary` | **Description:** Changes the summary format of the Dynamic Collections.<br>**Default:** `<<library_translationU>>s streaming on <<key_name>>.`<br>**Values:** Any string. |

View file

@ -30,6 +30,8 @@ Below are the available variables which can be used to customize the file.
| `radarr_add_missing_<<key>>`<sup>1</sup> | **Description:** Override Radarr `add_missing` attribute of the specified key's collection.<br>**Default:** `radarr_add_missing`<br>**Values:** `true` or `false` |
| `radarr_folder` | **Description:** Override Radarr `root_folder_path` attribute for all collections in a Defaults file.<br>**Values:** Folder Path |
| `radarr_folder_<<key>>`<sup>1</sup> | **Description:** Override Radarr `root_folder_path` attribute of the specified key's collection.<br>**Default:** `radarr_folder`<br>**Values:** Folder Path |
| `radarr_search` | **Description:** Override Radarr `search` attribute or all collections in a Defaults file.<br>**Values:** `true` or `false` |
| `radarr_search_<<key>>`<sup>1</sup> | **Description:** Override Radarr `search` attribute of the specified key's collection.<br>**Default:** `radarr_search`<br>**Values:** `true` or `false` |
| `radarr_tag` | **Description:** Override Radarr `tag` attribute for all collections in a Defaults file.<br>**Values:** List or comma-separated string of tags |
| `radarr_tag_<<key>>`<sup>1</sup> | **Description:** Override Radarr `tag` attribute of the specified key's collection.<br>**Default:** `radarr_tag`<br>**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.<br>**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_<<key>>`<sup>1</sup> | **Description:** Override Sonarr `add_missing` attribute of the specified key's collection.<br>**Default:** `sonarr_add_missing`<br>**Values:** `true` or `false` |
| `sonarr_folder` | **Description:** Override Sonarr `root_folder_path` attribute for all collections in a Defaults file.<br>**Values:** Folder Path |
| `sonarr_folder_<<key>>`<sup>1</sup> | **Description:** Override Sonarr `root_folder_path` attribute of the specified key's collection.<br>**Default:** `sonarr_folder`<br>**Values:** Folder Path |
| `sonarr_search` | **Description:** Override Sonarr `search` attribute or all collections in a Defaults file.<br>**Values:** `true` or `false` |
| `sonarr_search_<<key>>`<sup>1</sup> | **Description:** Override Sonarr `search` attribute of the specified key's collection.<br>**Default:** `sonarr_search`<br>**Values:** `true` or `false` |
| `sonarr_tag` | **Description:** Override Sonarr `tag` attribute for all collections in a Defaults file.<br>**Values:** List or comma-separated string of tags |
| `sonarr_tag_<<key>>`<sup>1</sup> | **Description:** Override Sonarr `tag` attribute of the specified key's collection.<br>**Default:** `sonarr_tag`<br>**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.<br>**Values:** List or comma-separated string of tags |

View file

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

View file

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View file

@ -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.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diivoy`, or `diivoycolor` |
| `style` | **Description:** Controls the visual theme of the collections created.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` |
| `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults file.<br>**Values:** Number Greater then 0 |
| `limit_<<key>>`<sup>1</sup> | **Description:** Changes the Builder Limit of the specified key's collection.<br>**Default:** `limit`<br>**Values:** Number Greater then 0 |
| `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults file.<br>**Default:** `release.desc`<br>**Values:** [Any `smart_filter` Sort Option](../../metadata/builders/smart.md#sort-options) |

View file

@ -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.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diivoy`, or `diivoycolor` |
| `style` | **Description:** Controls the visual theme of the collections created.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` |
| `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults file.<br>**Values:** Number Greater then 0 |
| `limit_<<key>>`<sup>1</sup> | **Description:** Changes the Builder Limit of the specified key's collection.<br>**Default:** `limit`<br>**Values:** Number Greater then 0 |
| `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults file.<br>**Default:** `release.desc`<br>**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:

View file

@ -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.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diivoy`, or `diivoycolor` |
| `style` | **Description:** Controls the visual theme of the collections created.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` |
| `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults file.<br>**Values:** Number Greater then 0 |
| `limit_<<key>>`<sup>1</sup> | **Description:** Changes the Builder Limit of the specified key's collection.<br>**Default:** `limit`<br>**Values:** Number Greater then 0 |
| `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults file.<br>**Default:** `release.desc`<br>**Values:** [Any `smart_filter` Sort Option](../../metadata/builders/smart.md#sort-options) |

View file

@ -6,16 +6,17 @@ 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 `_<<key>>`
<br>For example, with the audio_codec overlay `file_dtsx: config/overlays/dtsx.png`
| Variable | Description & Values |
|:--------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `use_<<key>>`<sup>1</sup> | **Description:** Turns off individual Overlays in a Defaults file.<br>**Values:** `false` to turn off the overlay |
| `file` | **Description:** Controls the image associated with the Overlay to a local file.<br>**Values:** Filepath to Overlay Image |
| `url` | **Description:** Controls the image associated with the Overlay to a url.<br>**Values:** URL to Overlay Image |
| `git` | **Description:** Controls the image associated with the Overlay to the git repo.<br>**Values:** Git Path to Overlay Image |
| `repo` | **Description:** Controls the image associated with the Overlay to a custom repo.<br>**Values:** Repo Path to Overlay Image |
| `file` | **Description:** Controls the images associated with all the Overlays to a local file.<br>**Values:** Filepath to Overlay Image |
| `file_<<key>>`<sup>1</sup> | **Description:** Controls the image associated with this key's Overlay to a local file.<br>**Values:** Filepath to Overlay Image |
| `url` | **Description:** Controls the images associated with all the Overlays to a url.<br>**Values:** URL to Overlay Image |
| `url_<<key>>`<sup>1</sup> | **Description:** Controls the image associated with this key's Overlay to a url.<br>**Values:** URL to Overlay Image |
| `git` | **Description:** Controls the images associated with all the Overlays to the git repo.<br>**Values:** Git Path to Overlay Image |
| `git_<<key>>`<sup>1</sup> | **Description:** Controls the image associated with this key's Overlay to the git repo.<br>**Values:** Git Path to Overlay Image |
| `repo` | **Description:** Controls the images associated with all the Overlays to a custom repo.<br>**Values:** Repo Path to Overlay Image |
| `repo_<<key>>`<sup>1</sup> | **Description:** Controls the image associated with this key's Overlay to a custom repo.<br>**Values:** Repo Path to Overlay Image |
| `horizontal_offset` | **Description:** Controls the Horizontal Offset of this overlay. Can be a %.<br>**Values:** Number 0 or greater or 0%-100% |
| `horizontal_align` | **Description:** Controls the Horizontal Alignment of the overlay.<br>**Values:** `left`, `center`, or `right` |
| `vertical_offset` | **Description:** Controls the Vertical Offset of this overlay. Can be a %.<br>**Values:** Number 0 or greater or 0%-100% |

View file

@ -83,7 +83,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa
| `weight_<<key>>`<sup>1</sup> | **Description:** Controls the weight of the Overlay. Higher numbers have priority.<br>**Values:** Any Number |
| `regex_<<key>>`<sup>1</sup> | **Description:** Controls the regex of the Overlay Search.<br>**Values:** Any Proper Regex |
1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` with when calling.
The below is an example config.yml extract with some Template Variables added in to change how the file works.

View file

@ -75,7 +75,7 @@ All [Shared Overlay Variables](../overlay_variables) except `horizontal_offset`,
| `addon_offset` | **Description:** Text Addon Image Offset from the text.<br>**Default:** `30`<br>**Values:** Any Number greater then 0 |
| `addon_position` | **Description:** Text Addon Image Alignment in relation to the text.<br>**Default:** `top`<br>**Values:** `left`, `right`, `top`, `bottom` |
1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` with when calling.
The below is an example config.yml extract with some Template Variables added in to change how the file works.

View file

@ -61,7 +61,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa
| `overlay_level` | **Description:** Choose the Overlay Level.<br>**Values:** `season` or `episode` |
| `weight_<<key>>`<sup>1</sup> | **Description:** Controls the weight of the Overlay. Higher numbers have priority.<br>**Values:** Any Number |
1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` with when calling.
The below is an example config.yml extract with some Template Variables added in to change how the file works.

View file

@ -145,7 +145,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa
| `country_<<key>>`<sup>1</sup> | **Description:** Controls the country image for the Overlay.<br>**Default:** Listed in the [Table](#supported-audiosubtitle-language-flags) above<br>**Values:** [ISO 3166-1 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) for the flag desired |
| `weight_<<key>>`<sup>1</sup> | **Description:** Controls the weight of the Overlay. Higher numbers have priority.<br>**Values:** Any Number |
1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` with when calling.
The below is an example config.yml extract with some Template Variables added in to change how the file works.

View file

@ -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.<br>**Values:** `season` or `episode` |
| `weight_<<key>>`<sup>1</sup> | **Description:** Controls the weight of the Overlay. Higher numbers have priority. **Only works with Edition keys.**<br>**Values:** Any Number |
1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` with when calling.
The below is an example config.yml extract with some Template Variables added in to change how the file works.

View file

@ -51,7 +51,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa
|:-------------------------------|:-------------------------------------------------------------------------------------------------------------|
| `weight_<<key>>`<sup>1</sup> | **Description:** Controls the weight of the Overlay. Higher numbers have priority.<br>**Values:** Any Number |
1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` with when calling.
The below is an example config.yml extract with some Template Variables added in to change how the file works.

View file

@ -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.<br>**Values:** Any Number greater then 0 |
| `stroke_color` | **Description:** Font Stroke Color for the Text Overlay.<br>**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 `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` with when calling.
The below is an example config.yml extract with some Template Variables added in to change how the file works.

View file

@ -70,18 +70,18 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa
|:-----------------------------|:-------------------------------------------------------------------------------------------------------------|
| `weight_<<key>>`<sup>1</sup> | **Description:** Controls the weight of the Overlay. Higher numbers have priority.<br>**Values:** Any Number |
1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` 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 |
|:-----------------|:---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| 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.

View file

@ -73,7 +73,7 @@ All [Shared Overlay Variables](../overlay_variables) are available with the defa
| `weight_<<key>>`<sup>1</sup> | **Description:** Controls the weight of the Overlay. Higher numbers have priority.<br>**Values:** Any Number |
| `regex_<<key>>`<sup>1</sup> | **Description:** Controls the regex of the Overlay Search.<br>**Values:** Any Proper Regex |
1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling.
1. Each default overlay has a `key` that when calling to effect a specific overlay you must replace `<<key>>` with when calling.
The below is an example config.yml extract with some Template Variables added in to change how the file works.

View file

@ -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)

View file

@ -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.<br>**Default:** `playlist_sync_to_user` Global Setting Value<br>**Values:** Comma-separated string or list of user names. |
| `exclude_user_<<key>>`<sup>1</sup> | **Description:** Sets the users to exclude from sync the specified key's playlist.<br>**Default:** `sync_to_user` Value<br>**Values:** Comma-separated string or list of user names. |
| `trakt_list_<<key>>`<sup>1</sup> | **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.<br>**Values:** List of Trakt List URLs | | | |
| `delete_playlist` | **Description:** Will delete all playlists for the users defined by sync_to_users.<br>**Values:** `true` or `false` |
| `delete_playlist_<<key>>`<sup>1</sup> | **Description:** Will delete the specified key's playlists for the users defined by sync_to_users.<br>**Values:** `true` or `false` |
| `ignore_ids` | **Description:** Set a list or comma-separated string of TMDb/TVDb IDs to ignore in all playlists.<br>**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.<br>**Values:** List or comma-separated string of IMDb IDs |
| `url_poster_<<key>>`<sup>1</sup> | **Description:** Changes the poster url of the specified key's playlist.<br>**Values:** URL directly to the Image |

View file

@ -280,7 +280,7 @@ Run with every network request printed to the Logs. **This can potentially have
<tr>
<th>Example</th>
<td><code>--log-requests</code></td>
<td><code>PMM_NETWORK=true</code></td>
<td><code>PMM_LOG_REQUESTS=true</code></td>
</tr>
</table>

View file

@ -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/)

View file

@ -104,7 +104,7 @@ 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. |
@ -113,6 +113,7 @@ The available attributes for editing movies are as follows
| `edition_contains`<sup>1</sup> | 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.<br>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.<br>**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.

View file

@ -68,7 +68,7 @@ The available attributes for editing artists, albums, and tracks are as follows
### General Attributes
| Attribute | Values | Artists | Album | Tracks |
|:-----------------------|:--------------------------------------------------------------|:--------:|:--------:|:--------:|
|:-----------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|
| `title` | Text to change Title | &#10060; | &#10060; | &#9989; |
| `sort_title` | Text to change Sort Title | &#9989; | &#9989; | &#9989; |
| `user_rating` | Number to change User Rating | &#9989; | &#9989; | &#9989; |
@ -79,6 +79,7 @@ The available attributes for editing artists, albums, and tracks are as follows
| `track` | Text to change Track | &#10060; | &#10060; | &#9989; |
| `disc` | Text to change Disc | &#10060; | &#10060; | &#9989; |
| `original_artist` | Text to change Original Artist | &#10060; | &#10060; | &#9989; |
| `run_definition` | Used to specify if this definition runs.<br>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.<br>**Values:** `movie`, `show`, `artist`, `true`, `false` | &#9989; | &#10060; | &#10060; |
### Tag Attributes

View file

@ -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

View file

@ -201,7 +201,7 @@ 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 `<<name>>` and some can have modifiers like so `<<name$>>` where `$` is the modifier. The available options are:
| 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`) | &#9989; | &#9989; | &#10060; | &#9989; |
| `<<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`) | &#9989; | &#9989; | &#10060; | &#9989; |
| `<<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`) | &#9989; | &#9989; | &#9989; | &#9989; |
@ -211,10 +211,10 @@ There are multiple Special Text Variables that can be used when formatting the t
| `<<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 | &#9989; | &#9989; | &#10060; | &#10060; |
| `<<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 | &#9989; | &#10060; | &#10060; | &#10060; |
| `<<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 | &#9989; | &#9989; | &#10060; | &#9989; |
| `<<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`) | &#10060; | &#9989; | &#9989; | &#10060; |
| `<<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`) | &#10060; | &#10060; | &#9989; | &#9989; |
| `<<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`) | &#10060; | &#10060; | &#10060; | &#9989; |
| `<<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`) | &#9989; | &#10060; | &#10060; | &#9989; |
| `<<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`) | &#10060; | &#9989; | &#9989; | &#10060; |
| `<<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`) | &#10060; | &#10060; | &#9989; | &#9989; |
| `<<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`) | &#10060; | &#10060; | &#10060; | &#9989; |
| `<<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`) | &#9989; | &#10060; | &#10060; | &#9989; |
| `<<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`) | &#9989; | &#10060; | &#10060; | &#9989; |
| `<<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 | &#9989; | &#10060; | &#10060; | &#9989; |
| `<<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/) | &#9989; | &#9989; | &#10060; | &#9989; |

View file

@ -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:

View file

@ -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:

View file

@ -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])

385
modules/poster.py Normal file
View file

@ -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())

View file

@ -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