[2] Overlay Overhaul!

pull/858/head
meisnate12 2 years ago
parent 4d014c9512
commit adf2ad2073

5
.gitignore vendored

@ -13,9 +13,10 @@ __pycache__/
/test* /test*
logs/ logs/
config/* config/*
!config/overlays/*/overlay.png !config/overlays/
config/overlays/*/
config/overlays/temp.png
!config/*.template !config/*.template
!overlay.png
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/

@ -1 +1 @@
1.16.5-develop1 1.16.5-develop2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@ -127,7 +127,9 @@ html_theme_options = {
("Run Commands & Environment Variables", "home/environmental"), ("Run Commands & Environment Variables", "home/environmental"),
("_divider", ), ("_divider", ),
("Configuration File", "config/configuration"), ("Configuration File", "config/configuration"),
("Metadata File", "metadata/metadata"), ("Metadata Files", "metadata/metadata"),
("Overlay Files", "metadata/overlay"),
("Playlist Files", "metadata/playlist"),
("_divider", ), ("_divider", ),
("Scheduling Guide", "home/guides/scheduling"), ("Scheduling Guide", "home/guides/scheduling"),
("Image Asset Directory Guide", "home/guides/assets"), ("Image Asset Directory Guide", "home/guides/assets"),
@ -160,7 +162,9 @@ html_theme_options = {
("Notifiarr", "config/notifiarr"), ("Notifiarr", "config/notifiarr"),
]), ]),
("_menu", "Metadata", [ ("_menu", "Metadata", [
("Metadata and Playlist Files", "metadata/metadata"), ("Metadata Files", "metadata/metadata"),
("Overlay Files", "metadata/overlay"),
("Playlist Files", "metadata/playlist"),
("_divider", ), ("_divider", ),
("Templates", "metadata/templates"), ("Templates", "metadata/templates"),
("Filters", "metadata/filters"), ("Filters", "metadata/filters"),
@ -191,7 +195,6 @@ html_theme_options = {
("_menu", "Details", [ ("_menu", "Details", [
("Setting Details", "metadata/details/setting"), ("Setting Details", "metadata/details/setting"),
("Schedule Details", "metadata/details/schedule"), ("Schedule Details", "metadata/details/schedule"),
("Image Overlay Details", "metadata/details/overlay"),
("Metadata Details", "metadata/details/metadata"), ("Metadata Details", "metadata/details/metadata"),
("Radarr/Sonarr Details", "metadata/details/arr"), ("Radarr/Sonarr Details", "metadata/details/arr"),
]) ])

@ -8,19 +8,19 @@ A template Configuration File can be found in the [GitHub Repo](https://github.c
This table outlines the third-party services that Plex Meta Manager can make use of. Each service has specific requirements for setup that can be found by clicking the links within the table. This table outlines the third-party services that Plex Meta Manager can make use of. Each service has specific requirements for setup that can be found by clicking the links within the table.
| Attribute | Required | | Attribute | Required |
|:-----------------------------|:---------------------------------------:| |:----------------------------------------------------------|:---------------------------------------:|
| [`libraries`](libraries) | ✅ | | [`libraries`](libraries) | ✅ |
| [`playlist_files`](playlist) | ❌ | | [`playlist_files`](libraries.md#playlist-files-attribute) | ❌ |
| [`settings`](settings) | ❌ | | [`settings`](settings) | ❌ |
| [`webhooks`](webhooks) | ❌ | | [`webhooks`](webhooks) | ❌ |
| [`plex`](plex) | &#9989; <br/>Either here or per library | | [`plex`](plex) | &#9989; <br/>Either here or per library |
| [`tmdb`](tmdb) | &#9989; | | [`tmdb`](tmdb) | &#9989; |
| [`tautulli`](tautulli) | &#10060; | | [`tautulli`](tautulli) | &#10060; |
| [`omdb`](omdb) | &#10060; | | [`omdb`](omdb) | &#10060; |
| [`notifiarr`](notifiarr) | &#10060; | | [`notifiarr`](notifiarr) | &#10060; |
| [`anidb`](anidb) | &#10060; | | [`anidb`](anidb) | &#10060; |
| [`radarr`](radarr) | &#10060; | | [`radarr`](radarr) | &#10060; |
| [`sonarr`](sonarr) | &#10060; | | [`sonarr`](sonarr) | &#10060; |
| [`trakt`](trakt) | &#10060; | | [`trakt`](trakt) | &#10060; |
| [`mal`](myanimelist) | &#10060; | | [`mal`](myanimelist) | &#10060; |

@ -35,6 +35,8 @@ libraries:
- file: config/TV Shows.yml - file: config/TV Shows.yml
- git: meisnate12/ShowCharts - git: meisnate12/ShowCharts
- git: meisnate12/Networks - git: meisnate12/Networks
overlay_path:
- file: config/Overlays.yml
TV Shows On Second Plex: TV Shows On Second Plex:
library_name: TV Shows library_name: TV Shows
plex: plex:
@ -79,6 +81,7 @@ The available attributes for each library are as follows:
|:-------------------------------------------|:---------------------------------------------------------------------------------------------|:--------------------------------------:|:-------------------------------:| |:-------------------------------------------|:---------------------------------------------------------------------------------------------|:--------------------------------------:|:-------------------------------:|
| [`library_name`](#library-name) | Library name (required only when trying to use multiple libraries with the same name) | Base Attribute Name | &#10060; | | [`library_name`](#library-name) | Library name (required only when trying to use multiple libraries with the same name) | Base Attribute Name | &#10060; |
| [`metadata_path`](#metadata-path) | Location of Metadata YAML files | `/config/<<MAPPING_NAME>>.yml` | &#10060; | | [`metadata_path`](#metadata-path) | Location of Metadata YAML files | `/config/<<MAPPING_NAME>>.yml` | &#10060; |
| [`overlay_path`](#overlay-path) | Location of Overlay YAML files | None | &#10060; |
| [`missing_path`](#missing-path) | Location to create the YAML file listing missing items for this library | `/config/<<MAPPING_NAME>>_missing.yml` | &#10060; | | [`missing_path`](#missing-path) | Location to create the YAML file listing missing items for this library | `/config/<<MAPPING_NAME>>_missing.yml` | &#10060; |
| [`schedule`](../metadata/details/schedule) | Use any [schedule option](../metadata/details/schedule) to control when this library is run. | daily | &#10060; | | [`schedule`](../metadata/details/schedule) | Use any [schedule option](../metadata/details/schedule) to control when this library is run. | daily | &#10060; |
| [`operations`](operations) | Library Operations to run | N/A | &#10060; | | [`operations`](operations) | Library Operations to run | N/A | &#10060; |
@ -125,6 +128,19 @@ libraries:
TV Shows: TV Shows:
``` ```
### Overlay Path
The `overlay_path` attribute is used to define [Overlay Files](../metadata/metadata) by specifying the path type and path of the files that will be executed against the parent library. See [Path Types](paths) for how to define them.
```yaml
libraries:
TV Shows:
metadata_path:
- file: config/TV Shows.yml
overlay_path:
- file: config/Overlays.yml
```
### Missing Path ### Missing Path
The `missing_path` attribute is used to define where to save the "missing items" YAML file. This file is used to store information about media which is missing from the Plex library compared to what is expected from the Metadata file. The `missing_path` attribute is used to define where to save the "missing items" YAML file. This file is used to store information about media which is missing from the Plex library compared to what is expected from the Metadata file.

@ -30,6 +30,7 @@ The available attributes for the operations attribute are as follows
| `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode<br>**Values:** `default`: Library default<br>`hide`: Hide Collection<br>`hide_items`: Hide Items in this Collection<br>`show_items`: Show this Collection and its Items<table class="clearTable"><tr><td>`default`</td><td>Library default</td></tr><tr><td>`hide`</td><td>Hide Collection</td></tr><tr><td>`hide_items`</td><td>Hide Items in this Collection</td></tr><tr><td>`show_items`</td><td>Show this Collection and its Items</td></tr></table> | | `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode<br>**Values:** `default`: Library default<br>`hide`: Hide Collection<br>`hide_items`: Hide Items in this Collection<br>`show_items`: Show this Collection and its Items<table class="clearTable"><tr><td>`default`</td><td>Library default</td></tr><tr><td>`hide`</td><td>Hide Collection</td></tr><tr><td>`hide_items`</td><td>Hide Items in this Collection</td></tr><tr><td>`show_items`</td><td>Show this Collection and its Items</td></tr></table> |
| `update_blank_track_titles` | Search though every track in a music library and replace any blank track titles with the tracks sort title<br>**Values:** `true` or `false` | | `update_blank_track_titles` | Search though every track in a music library and replace any blank track titles with the tracks sort title<br>**Values:** `true` or `false` |
| `remove_title_parentheses` | Search through every title and remove all ending parentheses in an items title if the title isn not locked.<br>**Values:** `true` or `false` | | `remove_title_parentheses` | Search through every title and remove all ending parentheses in an items title if the title isn not locked.<br>**Values:** `true` or `false` |
| `remove_overlays` | Search through every title and removes all overlays.<br>**Values:** `true` or `false` |
| `split_duplicates` | Splits all duplicate movies/shows found in this library<br>**Values:** `true` or `false` | | `split_duplicates` | Splits all duplicate movies/shows found in this library<br>**Values:** `true` or `false` |
| `radarr_add_all` | Adds every item in the library to Radarr. The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Radarr paths you can use the `plex_path` and `radarr_path` [Radarr](radarr) details to convert the paths.<br>**Values:** `true` or `false` | | `radarr_add_all` | Adds every item in the library to Radarr. The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Radarr paths you can use the `plex_path` and `radarr_path` [Radarr](radarr) details to convert the paths.<br>**Values:** `true` or `false` |
| `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given<br>**Values:** List or comma separated string of tags | | `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given<br>**Values:** List or comma separated string of tags |

@ -13,7 +13,8 @@ These docs are assuming you have a basic understanding of Docker concepts. One
| [Run](#run) | `-r` or `--run` | `PMM_RUN` | | [Run](#run) | `-r` or `--run` | `PMM_RUN` |
| [Run Tests](#run-tests) | `-rt`, `--tests`, or `--run-tests` | `PMM_TEST` | | [Run Tests](#run-tests) | `-rt`, `--tests`, or `--run-tests` | `PMM_TEST` |
| [Collections Only](#collections-only) | `-co` or `--collections-only` | `PMM_COLLECTIONS_ONLY` | | [Collections Only](#collections-only) | `-co` or `--collections-only` | `PMM_COLLECTIONS_ONLY` |
| [Libraries Only](#libraries-only) | `-lo` or `--libraries-only` | `PMM_LIBRARIES_ONLY` | | [Operations](#operations) | `-op` or `--operations` | `PMM_OPERATIONS` |
| [Overlays](#overlays) | `-ov` or `--overlays` | `PMM_OVERLAYS` |
| [Run Collections](#run-collections) | `-rc` or `--run-collections` | `PMM_COLLECTIONS` | | [Run Collections](#run-collections) | `-rc` or `--run-collections` | `PMM_COLLECTIONS` |
| [Run Libraries](#run-libraries) | `-rl` or `--run-libraries` | `PMM_LIBRARIES` | | [Run Libraries](#run-libraries) | `-rl` or `--run-libraries` | `PMM_LIBRARIES` |
| [Run Metadata Files](#run-metadata-files) | `-rm` or `--run-metadata-files` | `PMM_METADATA_FILES` | | [Run Metadata Files](#run-metadata-files) | `-rm` or `--run-metadata-files` | `PMM_METADATA_FILES` |
@ -247,9 +248,9 @@ docker run -it -v "X:\Media\Plex Meta Manager\config:/config:rw" meisnate12/plex
</details> </details>
### Libraries Only ### Operations
Only run library operations, skip collections. Only run library operations skipping collections and overlays.
<table class="dualTable colwidths-auto align-default table"> <table class="dualTable colwidths-auto align-default table">
<tr> <tr>
@ -259,13 +260,13 @@ Only run library operations, skip collections.
</tr> </tr>
<tr> <tr>
<th>Flags</th> <th>Flags</th>
<td><code>-lo</code> or <code>--libraries-only</code></td> <td><code>-op</code> or <code>--operations</code></td>
<td><code>PMM_LIBRARIES_ONLY</code></td> <td><code>PMM_OPERATIONS</code></td>
</tr> </tr>
<tr> <tr>
<th>Example</th> <th>Example</th>
<td><code>--libraries-only</code></td> <td><code>--operations</code></td>
<td><code>PMM_LIBRARIES_ONLY=true</code></td> <td><code>PMM_OPERATIONS=true</code></td>
</tr> </tr>
</table> </table>
@ -273,7 +274,7 @@ Only run library operations, skip collections.
<summary>Local Environment</summary> <summary>Local Environment</summary>
```shell ```shell
python plex_meta_manager.py --libraries-only python plex_meta_manager.py --operations
``` ```
</details> </details>
@ -281,7 +282,46 @@ python plex_meta_manager.py --libraries-only
<summary>Docker Environment</summary> <summary>Docker Environment</summary>
```shell ```shell
docker run -it -v "X:\Media\Plex Meta Manager\config:/config:rw" meisnate12/plex-meta-manager --libraries-only docker run -it -v "X:\Media\Plex Meta Manager\config:/config:rw" meisnate12/plex-meta-manager --operations
```
</details>
### Overlays
Only run library overlays skipping operations and collections.
<table class="dualTable colwidths-auto align-default table">
<tr>
<th style="background-color: #222;"></th>
<th>Shell</th>
<th>Environment</th>
</tr>
<tr>
<th>Flags</th>
<td><code>-ov</code> or <code>--overlays</code></td>
<td><code>PMM_OVERLAYS</code></td>
</tr>
<tr>
<th>Example</th>
<td><code>--overlays</code></td>
<td><code>PMM_OVERLAYS=true</code></td>
</tr>
</table>
<details>
<summary>Local Environment</summary>
```shell
python plex_meta_manager.py --overlays
```
</details>
<details>
<summary>Docker Environment</summary>
```shell
docker run -it -v "X:\Media\Plex Meta Manager\config:/config:rw" meisnate12/plex-meta-manager --overlays
``` ```
</details> </details>

@ -25,10 +25,8 @@ By default [if no `asset_directory` is specified], the program will look in the
Assets are searched for only at specific times. Assets are searched for only at specific times.
1. Collection assets are searched for whenever that collection is run. 1. Collection and Playlist assets are searched for whenever that collection/playlist is run.
2. Item assets for items in a collection are searched for whenever that collection is run and has `item_assets: true` as a Collection Detail. 2. Item assets and Unmanaged Collections assets are searched for whenever the `assets_for_all` Library Operation is active.
3. Item assets and Unmanaged Collections assets are searched for whenever the `assets_for_all` Library Operation is active.
4. Item assets will be searched for any item that has an overlay applied to it.
* If you want to silence the `Asset Warning: No poster or background found in an assets folder for 'TITLE'` you can use the [`show_missing_assets` Setting Attribute](../../config/settings.md#show-missing-assets): * If you want to silence the `Asset Warning: No poster or background found in an assets folder for 'TITLE'` you can use the [`show_missing_assets` Setting Attribute](../../config/settings.md#show-missing-assets):
```yaml ```yaml
@ -48,6 +46,7 @@ The table below shows the asset folder path structures that will be searched for
| Season poster | `assets/ASSET_NAME/Season##.ext` | `assets/ASSET_NAME_Season##.ext` | | Season poster | `assets/ASSET_NAME/Season##.ext` | `assets/ASSET_NAME_Season##.ext` |
| Season background | `assets/ASSET_NAME/Season##_background.ext` | `assets/ASSET_NAME_Season##_background.ext` | | Season background | `assets/ASSET_NAME/Season##_background.ext` | `assets/ASSET_NAME_Season##_background.ext` |
| Episode poster | `assets/ASSET_NAME/S##E##.ext` | `assets/ASSET_NAME_S##E##.ext` | | Episode poster | `assets/ASSET_NAME/S##E##.ext` | `assets/ASSET_NAME_S##E##.ext` |
| Episode background | `assets/ASSET_NAME/S##E##_background.ext` | `assets/ASSET_NAME_S##E##_background.ext` |
* For **Collections** replace `ASSET_NAME` with the mapping name used with the collection unless `system_name` is specified, which you would then use what's specified in `system_name`. * For **Collections** replace `ASSET_NAME` with the mapping name used with the collection unless `system_name` is specified, which you would then use what's specified in `system_name`.
@ -67,7 +66,7 @@ The table below shows the asset folder path structures that will be searched for
Here's an example config folder structure with an assets directory with `asset_folders` set to true and false. Here's an example config folder structure with an assets directory with `asset_folders` set to true and false.
### `asset_folders: true` without nesting ### `asset_folders: true`
``` ```
config config
@ -119,58 +118,6 @@ config
│ ├── Season04_background.png │ ├── Season04_background.png
``` ```
### `asset_folders: true` with nesting
```
config
├── config.yml
├── Movies.yml
├── TV Shows.yml
├── assets
│ ├── The Lord of the Rings
│ ├── poster.png
│ ├── background.png
│ ├── The Lord of the Rings The Fellowship of the Ring (2001)
│ ├── poster.png
│ ├── background.png
│ ├── The Lord of the Rings The Two Towers (2002)
│ ├── poster.png
│ ├── background.png
│ ├── The Lord of the Rings The Return of the King (2003)
│ ├── poster.png
│ ├── background.png
│ ├── Star Wars (Animated)
│ ├── poster.png
│ ├── background.png
│ ├── Star Wars The Clone Wars
│ ├── poster.png
│ ├── background.png
│ ├── Season00.png
│ ├── Season01.png
│ ├── Season02.png
│ ├── Season03.png
│ ├── Season04.png
│ ├── Season05.png
│ ├── Season06.png
│ ├── Season07.png
│ ├── S07E01.png
│ ├── S07E02.png
│ ├── S07E03.png
│ ├── S07E04.png
│ ├── S07E05.png
│ ├── Star Wars Rebels
│ ├── poster.png
│ ├── background.png
│ ├── Season01.png
│ ├── Season01_background.png
│ ├── Season02.png
│ ├── Season02_background.png
│ ├── Season03.png
│ ├── Season03_background.png
│ ├── Season04.png
│ ├── Season04_background.png
```
### `asset_folders: false` ### `asset_folders: false`
``` ```

@ -45,8 +45,6 @@ None of these details work with Playlists.
| `item_lock_poster` | **Description:** Locks/Unlocks the poster of every movie/show in the collection<br>**Default:** `None`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Lock</td></tr><tr><td>`false`</td><td>Unlock</td></tr></table> | | `item_lock_poster` | **Description:** Locks/Unlocks the poster of every movie/show in the collection<br>**Default:** `None`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Lock</td></tr><tr><td>`false`</td><td>Unlock</td></tr></table> |
| `item_lock_background` | **Description:** Locks/Unlocks the background of every movie/show in the collection<br>**Default:** `None`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Lock</td></tr><tr><td>`false`</td><td>Unlock</td></tr></table> | | `item_lock_background` | **Description:** Locks/Unlocks the background of every movie/show in the collection<br>**Default:** `None`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Lock</td></tr><tr><td>`false`</td><td>Unlock</td></tr></table> |
| `item_lock_title` | **Description:** Locks/Unlocks the title of every movie/show in the collection<br>**Default:** `None`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Lock</td></tr><tr><td>`false`</td><td>Unlock</td></tr></table> | | `item_lock_title` | **Description:** Locks/Unlocks the title of every movie/show in the collection<br>**Default:** `None`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Lock</td></tr><tr><td>`false`</td><td>Unlock</td></tr></table> |
| `item_overlay` | **Description:** Adds and overlay image to the poster of every movie/show in the collection see [Overlay Details](overlay) for more information.<br>**Values:** Name of overlay to be applied |
| `item_assets` | **Description:** Checks your assets folders for assets of every movie/show in the collection<br>**Default:** `false`<br>**Values:** `true` or `false` |
| `item_refresh` | **Description:** Refreshes the metadata of every movie/show in the collection<br>**Default:** `false`<br>**Values:** `true` or `false` | | `item_refresh` | **Description:** Refreshes the metadata of every movie/show in the collection<br>**Default:** `false`<br>**Values:** `true` or `false` |
| `item_refresh_delay` | **Description:** Amount of time to wait between each `item_refresh` of every movie/show in the collection<br>**Default:** `0`<br>**Values:** Number greater then `0` | | `item_refresh_delay` | **Description:** Amount of time to wait between each `item_refresh` of every movie/show in the collection<br>**Default:** `0`<br>**Values:** Number greater then `0` |
| `item_tmdb_season_titles` | **Description:** Changes the season titles of every show in the collection to match TMDb<br>**Default:** `false`<br>**Values:** `true` or `false` | | `item_tmdb_season_titles` | **Description:** Changes the season titles of every show in the collection to match TMDb<br>**Default:** `false`<br>**Values:** `true` or `false` |

@ -1,75 +1,50 @@
# Metadata and Playlist Files ## Metadata Files
Metadata and Playlist files are used to create and maintain collections within the Plex libraries and playlists on the server.
If utilized to their fullest, these files can be used to maintain the entire server's collections and playlists, and can be used as a backup for these in the event of a restore requirement. Metadata files are used to create and maintain collections and metadata within the Plex libraries on the server.
## Metadata Files If utilized to their fullest, these files can be used to maintain the entire server's collections and metadata, and can be used as a backup for these in the event of a restore requirement.
Collections, templates, metadata, and dynamic collections are defined within one or more Metadata files, which are linked to libraries in the [Libraries Attribute](../config/libraries) within the [Configuration File](../config/configuration.md). Collections, templates, metadata, and dynamic collections are defined within one or more Metadata files, which are linked to libraries in the [Libraries Attribute](../config/libraries.md#metadata-path) within the [Configuration File](../config/configuration.md).
These are the attributes which can be used within the Metadata File: These are the attributes which can be used within the Metadata File:
| Attribute | Description | | Attribute | Description |
|:--------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |:--------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `metadata` | contains definitions of metadata changes to [movie](metadata/movie), [show](metadata/show), or [music](metadata/music) library's items [movie titles, episode descriptions, etc.] |
| [`templates`](templates) | contains definitions of templates that can be leveraged by multiple collections | | [`templates`](templates) | contains definitions of templates that can be leveraged by multiple collections |
| [`external_templates`](templates.md#external-templates) | contains [path types](../config/paths) that point to external templates that can be leveraged by multiple collections | | [`external_templates`](templates.md#external-templates) | contains [path types](../config/paths) that point to external templates that can be leveraged by multiple collections |
| [`collections`](#collections-and-playlists-mappings) | contains definitions of collections you wish to add to one or more libraries | | [`collections`](#collection-attributes) | contains definitions of collections you wish to add to one or more libraries |
| [`dynamic_collections`](dynamic) | contains definitions of dynamic collections you wish to create in one or more libraries | | [`dynamic_collections`](#dynamic-collection-attributes) | contains definitions of [dynamic collections](dynamic) you wish to create |
| [`metadata`](#metadata-attributes) | contains definitions of metadata changes to [movie](metadata/movie), [show](metadata/show), or [music](metadata/music) library's items [movie titles, episode descriptions, etc.] |
* One of `metadata`, `collections` or `dynamic_collections` must be present for the Metadata File to execute. * One of `metadata`, `collections` or `dynamic_collections` must be present for the Metadata File to execute.
* Example Metadata Files can be found in the [Plex Meta Manager Configs Repository](https://github.com/meisnate12/Plex-Meta-Manager-Configs) * Example Metadata Files can be found in the [Plex Meta Manager Configs Repository](https://github.com/meisnate12/Plex-Meta-Manager-Configs/tree/master/PMM)
## Playlist Files
Playlists are defined in one or more Playlist files that are mapped in the [Playlist Files Attribute](../config/playlist) within the Configuration File.
These are the attributes which can be utilized within the Playlist File:
| Attribute | Description |
|:--------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------|
| [`templates`](templates) | contains definitions of templates that can be leveraged by multiple playlists |
| [`external_templates`](templates.md#external-templates) | contains [path types](../config/paths) that point to external templates that can be leveraged by multiple playlists |
| [`playlists`](#additional-playlist-attributes) | contains definitions of playlists you wish to add to the server |
* `playlists` is required in order to run the Playlist File.
* You can find example Playlist Files in the [Plex Meta Manager Configs Repository](https://github.com/meisnate12/Plex-Meta-Manager-Configs)
* Plex does not support the "Continue Watching" feature for playlists, you can [vote for the feature here](https://forums.plex.tv/t/playlists-remember-position-for-subsequent-resume/84866/39)
## Collections and Playlists Mappings ## Collection Attributes
Plex Meta Manager can run a number of different operations within `collections:` and `playlists:` such as: Plex Meta Manager can run a number of different operations within `collections` and such as:
* Automatically build and update collections and playlists * Automatically build and update collections and playlists
* Sync the collection with the source list if one is used * Sync the collection with the source list if one is used
* Send missing media to Sonarr/Radarr (Lidarr not supported at this time) * Send missing media to Sonarr/Radarr (Lidarr not supported at this time)
* Show and Hide collections and playlists at set intervals (i.e. show Christmas collections in December only) * Show and Hide collections at set intervals (i.e. show Christmas collections in December only)
Each collection requires its own section within the `collections` attribute and unlike playlists, collections can be built using as many Builders as needed.
## Dynamic Collection Mappings
Plex Meta Manager can automatically create dynamic collections based on different criteria, such as
* Collections for the top `X` popular people on TMDb (Bruce Willis, Tom Hanks etc.)
* Collections for each decade represented in the library (Best of 1990s, Best of 2000s etc.)
* Collections for each of the moods/styles within a Music library (A Cappella, Pop Rock etc.)
Below is an example dynamic collection which will create a collection for each of the decades represented within the library:
```yaml ```yaml
dynamic_collections: collections:
Decades: Trending Movies:
type: decade # ... builders, details, and filters for this collection
Popular Movies:
# ... builders, details, and filters for this collection
etc:
# ... builders, details, and filters for this collection
``` ```
## Collection and Playlist Attributes There are three types of attributes that can be utilized within a collection:
There are three types of attributes that can be utilized within a collection/playlist:
### Builders ### Builders
Builders use third-party services to source items to be added to the collection/playlist. Multiple builders can be used in the same collection/playlist from a variety of sources listed below. Builders use third-party services to source items to be added to the collection. Multiple builders can be used in the same collection from a variety of sources listed below.
* [Plex Builders](builders/plex) * [Plex Builders](builders/plex)
* [Smart Builders](builders/smart) * [Smart Builders](builders/smart)
@ -86,37 +61,134 @@ Builders use third-party services to source items to be added to the collection/
* [AniList Builders](builders/anilist) * [AniList Builders](builders/anilist)
* [MyAnimeList Builders](builders/myanimelist) * [MyAnimeList Builders](builders/myanimelist)
## Details ### Details
These can alter any aspect of the collection/playlist or the media items within them. These can alter any aspect of the collection or the media items within them.
* [Setting Details](details/setting) * [Setting Details](details/setting)
* [Schedule Detail](details/schedule) * [Schedule Detail](details/schedule)
* [Image Overlay Detail](details/overlay)
* [Metadata Details](details/metadata) * [Metadata Details](details/metadata)
* [Arr Details](details/arr) * [Arr Details](details/arr)
## Filters ### Filters
These filter media items added to the collection by any of the Builders. These filter media items added to the collection by any of the Builders.
* [Filters](filters) * [Filters](filters)
## Additional Playlist Attributes ### Example
```yaml
collections:
Trending:
trakt_trending: 10
tmdb_trending_daily: 10
tmdb_trending_weekly: 10
sort_title: +1_Trending
sync_mode: sync
smart_label: random
summary: Movies Trending across the internet
Popular:
tmdb_popular: 40
imdb_list:
url: https://www.imdb.com/search/title/?title_type=feature,tv_movie,documentary,short
limit: 40
sort_title: +2_Popular
sync_mode: sync
smart_label: random
summary: Popular Movies across the internet
```
## Dynamic Collection Attributes
Plex Meta Manager can dynamically create collections based on a verity of different criteria, such as
* Collections for the top `X` popular people on TMDb (Bruce Willis, Tom Hanks etc.)
* Collections for each decade represented in the library (Best of 1990s, Best of 2000s etc.)
* Collections for each of the moods/styles within a Music library (A Cappella, Pop Rock etc.)
* Collections for each of a Trakt Users Lists.
Below is an example dynamic collection which will create a collection for each of the decades represented within the library:
Playlist operations requires the `libraries` attribute, which instructs the operation to look in the specified libraries. This allows media to be combined from multiple libraries into one playlist. The mappings that you define in the `libraries` attribute must match the library names in your [Configuration File](../config/configuration). ```yaml
dynamic_collections:
Decades:
type: decade
```
## Metadata Attributes
The playlist can also use the `sync_to_users` attributes to control who has visibility of the playlist. This will override the global [`playlist_sync_to_users` Setting](../config/settings.md#playlist-sync-to-users). `sync_to_users` can be set to `all` to sync to all users who have access to the Plex Media Server, or a list/comma-separated string of users. The Plex Media Server owner will always have visibility of the Playlists, so does not need to be defined within the attribute. Leaving `sync_to_users` empty will make the playlist visible to the Plex Media Server owner only. Plex Meta Manager can automatically update items in Plex based on what's defined within the `metadata` attribute.
In the following example, media is pulled from the `Movies` and `TV Shows` libraries into the one Playlist, and the playlist is shared with a specific set of users: Each metadata requires its own section within the `metadata` attribute. Each item is defined by the mapping name which must be the same as the item name in the library unless an `alt_title` is specified.
```yaml ```yaml
playlists: metadata:
Marvel Cinematic Universe: Godzilla vs. Mechagodzilla II:
sync_mode: sync # ... details to change for this itwm
libraries: Movies, TV Shows Godzilla vs. Megaguirus:
sync_to_users: User1, someone@somewhere.com, User3 # ... details to change for this itwm
trakt_list: https://trakt.tv/users/donxy/lists/marvel-cinematic-universe?sort=rank,asc Godzilla vs. Megalon:
summary: Marvel Cinematic Universe In Chronological Order # ... details to change for this itwm
Halloween (Rob Zombie):
# ... details to change for this itwm
etc:
# ... details to change for this itwm
``` ```
* Unlike collections, playlists can only be built using one Builder as their ordering is inherited from the builder; it is not possible to combine builders.
### Title & Year
YAML files cannot have two items with the same mapping name so if you have two movies with the same name you define each one with a name of your choosing. Then use the `title` attribute to specify the real title and the `year` attribute to specify which of the multiple movies is for this mapping.
```yaml
metadata:
Godzilla1:
title: Godzilla
year: 1954
content_rating: R
Godzilla2:
title: Godzilla
year: 1998
content_rating: PG-13
```
### Alt Title
To define an alternative title that the item may be called when searching use `alt_title`. When a title is found matching `alt_title` then the name of the itme will be changed to match the mapping name or `title` if specified.
For Example, the 2007 movie Halloween shares a name with another movie in the Halloween franchise so this changes the title to `Halloween (Rob Zombie)` if the title is currently Halloween.
```yaml
metadata:
Halloween (Rob Zombie):
alt_title: Halloween
year: 2007
```
### Example
```yaml
metadata:
Godzilla1:
title: Godzilla
year: 1954
content_rating: R
Godzilla2:
title: Godzilla
year: 1998
content_rating: PG-13
Godzilla vs. Mechagodzilla II:
content_rating: PG
Godzilla vs. Megaguirus:
content_rating: PG
originally_available: 2000-08-31
Godzilla vs. Megalon:
content_rating: G
originally_available: 1973-03-17
Halloween (Rob Zombie):
alt_title: Halloween
year: 2007
```

@ -86,35 +86,13 @@ The available attributes for editing movies are as follows
### Special Attributes ### Special Attributes
| Attribute | Allowed Values | | Attribute | Allowed Values |
|:-------------|:--------------------------------------------------------------------------------------------------| |:-------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `title` | Title if different from the mapping value useful when you have multiple movies with the same name | | `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 | | `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 | | `year` | Year of movie for better identification. 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 | | `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 | | `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.** |
* YAML files cannot have two items with the same mapping name so if you have two movies with the same name you would change the mapping values to whatever you want. Then use the `title` attribute to specify the real title and use the `year` attribute to specify which of the multiple movies to choose.
```yaml
metadata:
Godzilla1:
title: Godzilla
year: 1954
content_rating: R
Godzilla2:
title: Godzilla
year: 1998
content_rating: PG-13
```
* If you know of another Title your movie might exist under, but you want it titled differently you can use `alt_title` to specify another title to look under and then be changed to the mapping name. For Example TMDb uses the name `The Legend of Korra`, but I want it as `Avatar: The Legend of Korra` (Which must be surrounded by quotes since it uses the character `:`):
```yaml
metadata:
"Avatar: The Legend of Korra":
alt_title: The Legend of Korra
```
This would change the name of the TMDb default `The Legend of Korra` to `Avatar: The Legend of Korra` and would not mess up any subsequent runs.
### General Attributes ### General Attributes

@ -79,39 +79,18 @@ The available attributes for editing shows, seasons, and episodes are as follows
### Special Attributes ### Special Attributes
| Attribute | Values | Shows | Seasons | Episodes | | Attribute | Values | Shows | Seasons | Episodes |
|:---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:| |:---------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|
| `title` | Title if different from the mapping value useful when you have multiple shows with the same name | &#9989; | &#9989; | &#9989; | | `title` | Title if different from the mapping value useful when you have multiple shows with the same name. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | &#9989; | &#9989; | &#9989; |
| `alt_title` | Alternative title to look for | &#9989; | &#10060; | &#10060; | | `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. | &#9989; | &#10060; | &#10060; |
| `year` | Year of show for better identification | &#9989; | &#10060; | &#10060; | | `year` | Year of show for better identification. See the [Metadata Page](../metadata.md#metadata-attributes) for how searching for files works. | &#9989; | &#10060; | &#10060; |
| `tmdb_show` | TMDb Show ID to use for metadata useful for miniseries that have been compiled into a movie | &#9989; | &#10060; | &#10060; | | `tmdb_show` | TMDb Show ID to use for metadata useful for miniseries that have been compiled into a movie | &#9989; | &#10060; | &#10060; |
| `tmdb_movie` | TMDb Movie ID to use for metadata useful for movies that have been split into segments | &#9989; | &#10060; | &#10060; | | `tmdb_movie` | TMDb Movie ID to use for metadata useful for movies that have been split into segments | &#9989; | &#10060; | &#10060; |
| `f1_season` | F1 Season Year to make the Show represent a Season of F1 Races. See [Formula 1 Metadata Guide](../../home/guides/formula) for more information. | &#9989; | &#10060; | &#10060; | | `f1_season` | F1 Season Year to make the Show represent a Season of F1 Races. See [Formula 1 Metadata Guide](../../home/guides/formula) for more information. | &#9989; | &#10060; | &#10060; |
| `round_prefix` | Used only with `f1_season` to add the round as a prefix to the Season (Race) Titles i.e. `Australian Grand Prix` --> `01 - Australian Grand Prix` | &#9989; | &#10060; | &#10060; | | `round_prefix` | Used only with `f1_season` to add the round as a prefix to the Season (Race) Titles i.e. `Australian Grand Prix` --> `01 - Australian Grand Prix` | &#9989; | &#10060; | &#10060; |
| `shorten_gp` | Used only with `f1_season` to shorten `Grand Prix` to `GP` in the Season (Race) Titles i.e. `Australian Grand Prix` --> `Australian GP` | &#9989; | &#10060; | &#10060; | | `shorten_gp` | Used only with `f1_season` to shorten `Grand Prix` to `GP` in the Season (Race) Titles i.e. `Australian Grand Prix` --> `Australian GP` | &#9989; | &#10060; | &#10060; |
| `seasons` | Mapping to define Seasons | &#9989; | &#10060; | &#10060; | | `seasons` | Mapping to define Seasons | &#9989; | &#10060; | &#10060; |
| `episodes` | Mapping to define Episodes | &#10060; | &#9989; | &#10060; | | `episodes` | Mapping to define Episodes | &#10060; | &#9989; | &#10060; |
* YAML files cannot have two items with the same mapping name so if you have two shows with the same name you would change the mapping values to whatever you want. Then use the `title` attribute to specify the real title and use the `year` attribute to specify which of the multiple shows to choose.
```yaml
metadata:
Godzilla1:
title: Godzilla
year: 1954
content_rating: R
Godzilla2:
title: Godzilla
year: 1998
content_rating: PG-13
```
* If you know of another Title your show might exist under, but you want it titled differently you can use `alt_title` to specify another title to look under and then be changed to the mapping name. For Example TMDb uses the name `The Legend of Korra`, but I want it as `Avatar: The Legend of Korra` (Which must be surrounded by quotes since it uses the character `:`):
```yaml
metadata:
"Avatar: The Legend of Korra":
alt_title: The Legend of Korra
```
This would change the name of the TMDb default `The Legend of Korra` to `Avatar: The Legend of Korra` and would not mess up any subsequent runs.
### General Attributes ### General Attributes

@ -0,0 +1,114 @@
# Overlay Files
Overlay files are used to create and maintain overlays within the Plex libraries on the server.
Overlays and templates are defined within one or more Overlay files, which are linked to libraries in the [Libraries Attribute](../config/libraries.md#overlay-path) within the [Configuration File](../config/configuration.md).
**To remove all overlays use the `remove_overlays` library operation.**
**To change a single overlay original Image either replace the image in the assets folder or remove the `Overlay` shared label and then PMM will overlay the new image**
These are the attributes which can be used within the Overlay File:
| Attribute | Description |
|:--------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------|
| [`templates`](templates) | contains definitions of templates that can be leveraged by multiple overlays |
| [`external_templates`](templates.md#external-templates) | contains [path types](../config/paths) that point to external templates that can be leveraged by multiple overlays |
| [`overlays`](#overlay-attributes) | contains definitions of overlays you wish to add |
* `overlays` is required in order to run the Overlay File.
* Example Overlay Files can be found in the [Plex Meta Manager Configs Repository](https://github.com/meisnate12/Plex-Meta-Manager-Configs/tree/master/PMM)
## Overlay Attributes
Each overlay requires its own section within the `overalys` attribute.
```yaml
overlays:
IMDb Top 250:
# ... builders, details, and filters for this overlay
4K:
# ... builders, details, and filters for this overlay
etc:
# ... builders, details, and filters for this overlay
```
Each section must have the only required attribute, `overlay`.
| Attribute | Description | Required |
|:----------|:-------------------------------------------------------------------------------------------------------------|:--------:|
| `name` | Name of the overlay. Each overlay name should be unique. | &#9989; |
| `url` | URL of Overlay Image Online | &#10060; |
| `git` | Location in the [Configs Repo](https://github.com/meisnate12/Plex-Meta-Manager-Configs) of the Overlay Image | &#10060; |
| `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image | &#10060; |
* If `url`, `git`, and `repo` are all not defined then PMM will look in your `config/overlays` folder for a `.png` file named the same as the `name` attribute.
```yaml
overlays:
IMDb Top 250:
overlay:
name: IMDb Top 250
imdb_chart: top_movies
```
There are three types of attributes that can be utilized within an overlay:
### Builders
Builders use third-party services to source items for overlays. Multiple builders can be used in the same overlay from a variety of sources listed below.
* [Plex Builders](builders/plex)
* [Smart Builders](builders/smart)
* [TMDb Builders](builders/tmdb)
* [TVDb Builders](builders/tvdb)
* [IMDb Builders](builders/imdb)
* [Trakt Builders](builders/trakt)
* [Tautulli Builders](builders/tautulli)
* [Letterboxd Builders](builders/letterboxd)
* [ICheckMovies Builders](builders/icheckmovies)
* [FlixPatrol Builders](builders/flixpatrol)
* [StevenLu Builders](builders/stevenlu)
* [AniDB Builders](builders/anidb)
* [AniList Builders](builders/anilist)
* [MyAnimeList Builders](builders/myanimelist)
## Details
Only a few details can be used with overlays: `limit`, `show_missing`, `save_missing`, `missing_only_released`, `minimum_items`, `cache_builders`, `tmdb_region`
* [Setting Details](details/setting)
* [Metadata Details](details/metadata)
## Filters
These filter media items added to the collection by any of the Builders.
* [Filters](filters)
## Examples
```yaml
overlays:
4K:
overlay:
name: 4K # This will look for a local overlays/4K.png in your configs folder
plex_search:
all:
resolution: 4K
HDR:
overlay:
name: HDR
git: PMM/overlays/HDR
plex_search:
all:
hdr: true
Dolby:
overlay:
name: Dolby
url: https://somewebsite.com/dobly_overlay.png
plex_all: true
filters:
has_dolby_vision: true
```

@ -0,0 +1,94 @@
# Playlist Files
Playlist files are used to create and maintain playlists on the Plex Server.
If utilized to their fullest, these files can be used to maintain the entire server's collections and playlists, and can be used as a backup for these in the event of a restore requirement.
Playlists are defined in one or more Playlist files that are mapped in the [Playlist Files Attribute](../config/libraries.md#playlist-files-attribute) within the Configuration File.
These are the attributes which can be utilized within the Playlist File:
| Attribute | Description |
|:--------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------|
| [`templates`](templates) | contains definitions of templates that can be leveraged by multiple playlists |
| [`external_templates`](templates.md#external-templates) | contains [path types](../config/paths) that point to external templates that can be leveraged by multiple playlists |
| [`playlists`](#playlist-attributes) | contains definitions of playlists you wish to add to the server |
* `playlists` is required in order to run the Playlist File.
* You can find example Playlist Files in the [Plex Meta Manager Configs Repository](https://github.com/meisnate12/Plex-Meta-Manager-Configs/tree/master/PMM)
* Plex does not support the "Continue Watching" feature for playlists, you can [vote for the feature here](https://forums.plex.tv/t/playlists-remember-position-for-subsequent-resume/84866/39)
## Playlist Attributes
Plex Meta Manager can automatically build and update playlists defined within the `playlists` attribute.
Each playlist requires its own section within the `playlists` attribute and unlike collections, playlists can only be built using one Builder as their ordering is inherited from the builder; it is not possible to combine builders.
```yaml
playlists:
Marvel Cinematic Universe Chronological Order:
# ... builder, details, and filters for this playlist
Star Wars Clone Wars Chronological Order:
# ... builder, details, and filters for this playlist
etc:
# ... builder, details, and filters for this playlist
```
Playlists require the `libraries` attribute, which instructs the operation to look in the specified libraries. This allows media to be combined from multiple libraries into one playlist. The mappings that you define in the `libraries` attribute must match the library names in your [Configuration File](../config/configuration).
The playlist can also use the `sync_to_users` attributes to control who has visibility of the playlist. This will override the global [`playlist_sync_to_users` Setting](../config/settings.md#playlist-sync-to-users). `sync_to_users` can be set to `all` to sync to all users who have access to the Plex Media Server, or a list/comma-separated string of users. The Plex Media Server owner will always have visibility of the Playlists, so does not need to be defined within the attribute. Leaving `sync_to_users` empty will make the playlist visible to the Plex Media Server owner only.
There are three types of attributes that can be utilized within a playlist:
### Builders
Builders use third-party services to source items to be added to the playlist. Multiple builders can be used in the same playlist from a variety of sources listed below.
* [Plex Builders](builders/plex)
* [Smart Builders](builders/smart)
* [TMDb Builders](builders/tmdb)
* [TVDb Builders](builders/tvdb)
* [IMDb Builders](builders/imdb)
* [Trakt Builders](builders/trakt)
* [Tautulli Builders](builders/tautulli)
* [Letterboxd Builders](builders/letterboxd)
* [ICheckMovies Builders](builders/icheckmovies)
* [FlixPatrol Builders](builders/flixpatrol)
* [StevenLu Builders](builders/stevenlu)
* [AniDB Builders](builders/anidb)
* [AniList Builders](builders/anilist)
* [MyAnimeList Builders](builders/myanimelist)
### Details
These can alter any aspect of the playlist or the media items within them.
* [Setting Details](details/setting)
* [Schedule Detail](details/schedule)
* [Metadata Details](details/metadata)
* [Arr Details](details/arr)
### Filters
These filter media items added to the playlist by any of the Builders.
* [Filters](filters)
## Example
In the following example, media is pulled from the `Movies` and `TV Shows` libraries into the one Playlist, and the playlist is shared with a specific set of users:
```yaml
playlists:
Marvel Cinematic Universe Chronological Order:
sync_mode: sync
libraries: Movies, TV Shows
sync_to_users: User1, someone@somewhere.com, User3
trakt_list: https://trakt.tv/users/donxy/lists/marvel-cinematic-universe?sort=rank,asc
summary: Marvel Cinematic Universe In Chronological Order
Star Wars Clone Wars Chronological Order:
sync_to_users: all
sync_mode: sync
libraries: Movies, TV Shows
trakt_list: https://trakt.tv/users/tomfin46/lists/star-wars-the-clone-wars-chronological-episode-order
```

@ -98,7 +98,7 @@ scheduled_boolean = ["visible_library", "visible_home", "visible_shared"]
string_details = ["sort_title", "content_rating", "name_mapping"] string_details = ["sort_title", "content_rating", "name_mapping"]
ignored_details = [ ignored_details = [
"smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test",
"delete_not_scheduled", "tmdb_person", "build_collection", "collection_order", "collection_level", "delete_not_scheduled", "tmdb_person", "build_collection", "collection_order", "collection_level", "overlay",
"validate_builders", "libraries", "sync_to_users", "collection_name", "playlist_name", "name", "blank_collection" "validate_builders", "libraries", "sync_to_users", "collection_name", "playlist_name", "name", "blank_collection"
] ]
details = [ details = [
@ -108,7 +108,7 @@ details = [
collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \ collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \
poster_details + background_details + summary_details + string_details poster_details + background_details + summary_details + string_details
item_false_details = ["item_lock_background", "item_lock_poster", "item_lock_title"] item_false_details = ["item_lock_background", "item_lock_poster", "item_lock_title"]
item_bool_details = ["item_tmdb_season_titles", "item_assets", "revert_overlay", "item_refresh"] + item_false_details item_bool_details = ["item_tmdb_season_titles", "revert_overlay", "item_refresh"] + item_false_details
item_details = ["non_item_remove_label", "item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay", "item_refresh_delay"] + item_bool_details + list(plex.item_advance_keys.keys()) item_details = ["non_item_remove_label", "item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay", "item_refresh_delay"] + item_bool_details + list(plex.item_advance_keys.keys())
none_details = ["label.sync", "item_label.sync"] none_details = ["label.sync", "item_label.sync"]
radarr_details = ["radarr_add_missing", "radarr_add_existing", "radarr_folder", "radarr_monitor", "radarr_search", "radarr_availability", "radarr_quality", "radarr_tag"] radarr_details = ["radarr_add_missing", "radarr_add_existing", "radarr_folder", "radarr_monitor", "radarr_search", "radarr_availability", "radarr_quality", "radarr_tag"]
@ -190,6 +190,10 @@ custom_sort_builders = [
"mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio" "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio"
] ]
episode_parts_only = ["plex_pilots"] episode_parts_only = ["plex_pilots"]
overlay_only = ["overlay"]
overlay_attributes = [
"filters", "limit", "show_missing", "save_missing", "missing_only_released", "minimum_items", "cache_builders", "tmdb_region"
] + all_builders + overlay_only
parts_collection_valid = [ parts_collection_valid = [
"filters", "plex_all", "plex_search", "trakt_list", "trakt_list_details", "collection_filtering", "collection_mode", "label", "visible_library", "limit", "filters", "plex_all", "plex_search", "trakt_list", "trakt_list_details", "collection_filtering", "collection_mode", "label", "visible_library", "limit",
"visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released", "server_preroll", "changes_webhooks", "visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released", "server_preroll", "changes_webhooks",
@ -202,7 +206,7 @@ playlist_attributes = [
"server_preroll", "changes_webhooks", "minimum_items", "cache_builders" "server_preroll", "changes_webhooks", "minimum_items", "cache_builders"
] + custom_sort_builders + summary_details + poster_details + radarr_details + sonarr_details ] + custom_sort_builders + summary_details + poster_details + radarr_details + sonarr_details
music_attributes = [ music_attributes = [
"non_item_remove_label", "item_label", "item_assets", "collection_filtering", "item_lock_background", "item_lock_poster", "item_lock_title", "non_item_remove_label", "item_label", "collection_filtering", "item_lock_background", "item_lock_poster", "item_lock_title",
"item_refresh", "item_refresh_delay", "plex_search", "plex_all", "filters" "item_refresh", "item_refresh_delay", "plex_search", "plex_all", "filters"
] + details + summary_details + poster_details + background_details ] + details + summary_details + poster_details + background_details
@ -215,8 +219,14 @@ class CollectionBuilder:
self.library = library self.library = library
self.libraries = [] self.libraries = []
self.playlist = library is None self.playlist = library is None
self.overlay = overlay
methods = {m.lower(): m for m in self.data} methods = {m.lower(): m for m in self.data}
self.type = "playlist" if self.playlist else "collection" if self.playlist:
self.type = "playlist"
elif self.overlay:
self.type = "overlay"
else:
self.type = "collection"
self.Type = self.type.capitalize() self.Type = self.type.capitalize()
if "name" in methods: if "name" in methods:
@ -246,6 +256,48 @@ class CollectionBuilder:
self.data[attr] = new_attributes[attr] self.data[attr] = new_attributes[attr]
methods[attr.lower()] = attr methods[attr.lower()] = attr
if self.overlay:
if "overlay" in methods:
logger.debug("")
logger.debug("Validating Method: overlay")
logger.debug(f"Value: {data[methods['overlay']]}")
if isinstance(data[methods["overlay"]], dict):
if "name" not in data[methods["overlay"]] or not data[methods["overlay"]]["name"]:
raise Failed(f"{self.Type} Error: overlay must have the name attribute")
self.overlay = data[methods["overlay"]]["name"]
if "git" in data[methods["overlay"]] and data[methods["overlay"]]["git"]:
url = f"{util.github_base}{data[methods['overlay']]['git']}.png"
elif "repo" in data[methods["overlay"]] and data[methods["overlay"]]["repo"]:
url = f"{self.config.custom_repo}{data[methods['overlay']]['git']}.png"
elif "url" in data[methods["overlay"]] and data[methods["overlay"]]["url"]:
url = data[methods["overlay"]]["url"]
else:
url = None
if url:
response = self.config.get(url)
if response.status_code >= 400:
raise Failed(f"{self.Type} Error: Overlay Image not found at: {url}")
if "Content-Type" not in response.headers or response.headers["Content-Type"] != "image/png":
raise Failed(f"{self.Type} Error: Overlay Image not a png: {url}")
if not os.path.exists(library.overlay_folder) or not os.path.isdir(library.overlay_folder):
os.makedirs(library.overlay_folder, exist_ok=False)
logger.info(f"Creating Overlay Folder found at: {library.overlay_folder}")
clean_name, _ = util.validate_filename(self.overlay)
overlay_path = os.path.join(library.overlay_folder, f"{clean_name}.png")
if os.path.exists(overlay_path):
os.remove(overlay_path)
with open(overlay_path, "wb") as handler:
handler.write(response.content)
while util.is_locked(overlay_path):
time.sleep(1)
else:
self.overlay = data[methods["overlay"]]
else:
self.overlay = self.mapping_name
overlay_path = os.path.join(library.overlay_folder, f"{self.overlay}.png")
if not os.path.exists(overlay_path):
raise Failed(f"{self.Type} Error: Overlay Image not found at: {overlay_path}")
if self.playlist: if self.playlist:
if "libraries" in methods: if "libraries" in methods:
logger.debug("") logger.debug("")
@ -355,7 +407,7 @@ class CollectionBuilder:
else: else:
raise Failed(f"Playlist Error: User: {user} not found in plex\nOptions: {plex_users}") raise Failed(f"Playlist Error: User: {user} not found in plex\nOptions: {plex_users}")
if "delete_not_scheduled" in methods: if "delete_not_scheduled" in methods and not self.overlay:
logger.debug("") logger.debug("")
logger.debug("Validating Method: delete_not_scheduled") logger.debug("Validating Method: delete_not_scheduled")
logger.debug(f"Value: {data[methods['delete_not_scheduled']]}") logger.debug(f"Value: {data[methods['delete_not_scheduled']]}")
@ -388,38 +440,38 @@ class CollectionBuilder:
suffix = f" and could not be found to delete" suffix = f" and could not be found to delete"
raise NotScheduled(f"{err}\n\n{self.Type} {self.name} not scheduled to run{suffix}") raise NotScheduled(f"{err}\n\n{self.Type} {self.name} not scheduled to run{suffix}")
self.collectionless = "plex_collectionless" in methods and not self.playlist self.collectionless = "plex_collectionless" in methods and not self.playlist and not self.overlay
self.validate_builders = True self.validate_builders = True
if "validate_builders" in methods: if "validate_builders" in methods and not self.overlay:
logger.debug("") logger.debug("")
logger.debug("Validating Method: validate_builders") logger.debug("Validating Method: validate_builders")
logger.debug(f"Value: {data[methods['validate_builders']]}") logger.debug(f"Value: {data[methods['validate_builders']]}")
self.validate_builders = util.parse(self.Type, "validate_builders", self.data, datatype="bool", methods=methods, default=True) self.validate_builders = util.parse(self.Type, "validate_builders", self.data, datatype="bool", methods=methods, default=True)
self.run_again = False self.run_again = False
if "run_again" in methods: if "run_again" in methods and not self.overlay:
logger.debug("") logger.debug("")
logger.debug("Validating Method: run_again") logger.debug("Validating Method: run_again")
logger.debug(f"Value: {data[methods['run_again']]}") logger.debug(f"Value: {data[methods['run_again']]}")
self.run_again = util.parse(self.Type, "run_again", self.data, datatype="bool", methods=methods, default=False) self.run_again = util.parse(self.Type, "run_again", self.data, datatype="bool", methods=methods, default=False)
self.build_collection = True self.build_collection = False if self.overlay else True
if "build_collection" in methods and not self.playlist: if "build_collection" in methods and not self.playlist and not self.overlay:
logger.debug("") logger.debug("")
logger.debug("Validating Method: build_collection") logger.debug("Validating Method: build_collection")
logger.debug(f"Value: {data[methods['build_collection']]}") logger.debug(f"Value: {data[methods['build_collection']]}")
self.build_collection = util.parse(self.Type, "build_collection", self.data, datatype="bool", methods=methods, default=True) self.build_collection = util.parse(self.Type, "build_collection", self.data, datatype="bool", methods=methods, default=True)
self.blank_collection = False self.blank_collection = False
if "blank_collection" in methods and not self.playlist: if "blank_collection" in methods and not self.playlist and not self.overlay:
logger.debug("") logger.debug("")
logger.debug("Validating Method: blank_collection") logger.debug("Validating Method: blank_collection")
logger.debug(f"Value: {data[methods['blank_collection']]}") logger.debug(f"Value: {data[methods['blank_collection']]}")
self.blank_collection = util.parse(self.Type, "blank_collection", self.data, datatype="bool", methods=methods, default=False) self.blank_collection = util.parse(self.Type, "blank_collection", self.data, datatype="bool", methods=methods, default=False)
self.sync = self.library.sync_mode == "sync" self.sync = self.library.sync_mode == "sync"
if "sync_mode" in methods: if "sync_mode" in methods and not self.overlay:
logger.debug("") logger.debug("")
logger.debug("Validating Method: sync_mode") logger.debug("Validating Method: sync_mode")
if not self.data[methods["sync_mode"]]: if not self.data[methods["sync_mode"]]:
@ -493,7 +545,7 @@ class CollectionBuilder:
self.smart_filter_details = "" self.smart_filter_details = ""
self.smart_label = {"sort_by": "random", "all": {"label": [self.name]}} self.smart_label = {"sort_by": "random", "all": {"label": [self.name]}}
self.smart_label_collection = False self.smart_label_collection = False
if "smart_label" in methods and not self.playlist and not self.library.is_music: if "smart_label" in methods and not self.playlist and not self.overlay and not self.library.is_music:
logger.debug("") logger.debug("")
logger.debug("Validating Method: smart_label") logger.debug("Validating Method: smart_label")
self.smart_label_collection = True self.smart_label_collection = True
@ -516,7 +568,7 @@ class CollectionBuilder:
self.smart_url = None self.smart_url = None
self.smart_type_key = None self.smart_type_key = None
if "smart_url" in methods and not self.playlist: if "smart_url" in methods and not self.playlist and not self.overlay:
logger.debug("") logger.debug("")
logger.debug("Validating Method: smart_url") logger.debug("Validating Method: smart_url")
if not self.data[methods["smart_url"]]: if not self.data[methods["smart_url"]]:
@ -528,7 +580,7 @@ class CollectionBuilder:
except ValueError: except ValueError:
raise Failed(f"{self.Type} Error: smart_url is incorrectly formatted") raise Failed(f"{self.Type} Error: smart_url is incorrectly formatted")
if "smart_filter" in methods and not self.playlist: if "smart_filter" in methods and not self.playlist and not self.overlay:
self.smart_type_key, self.smart_filter_details, self.smart_url = self.build_filter("smart_filter", self.data[methods["smart_filter"]], display=True, default_sort="random") self.smart_type_key, self.smart_filter_details, self.smart_url = self.build_filter("smart_filter", self.data[methods["smart_filter"]], display=True, default_sort="random")
if self.collectionless: if self.collectionless:
@ -634,6 +686,10 @@ class CollectionBuilder:
raise Failed(f"{self.Type} Error: {method_final} attribute not allowed for Collectionless collection") raise Failed(f"{self.Type} Error: {method_final} attribute not allowed for Collectionless collection")
elif self.smart_url and method_name in all_builders + smart_url_invalid: elif self.smart_url and method_name in all_builders + smart_url_invalid:
raise Failed(f"{self.Type} Error: {method_final} builder not allowed when using smart_filter") raise Failed(f"{self.Type} Error: {method_final} builder not allowed when using smart_filter")
elif not self.overlay and method_name in overlay_only:
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed in an overlay file")
elif self.overlay and method_name not in overlay_attributes:
raise Failed(f"{self.Type} Error: {method_final} attribute not allowed in an overlay file")
elif method_name in summary_details: elif method_name in summary_details:
self._summary(method_name, method_data) self._summary(method_name, method_data)
elif method_name in poster_details: elif method_name in poster_details:
@ -722,7 +778,6 @@ class CollectionBuilder:
self.do_missing = not self.config.no_missing and (self.details["show_missing"] or self.details["save_missing"] self.do_missing = not self.config.no_missing and (self.details["show_missing"] or self.details["save_missing"]
or (self.library.Radarr and self.radarr_details["add_missing"]) or (self.library.Radarr and self.radarr_details["add_missing"])
or (self.library.Sonarr and self.sonarr_details["add_missing"])) or (self.library.Sonarr and self.sonarr_details["add_missing"]))
if self.build_collection: if self.build_collection:
try: try:
self.obj = self.library.get_playlist(self.name) if self.playlist else self.library.get_collection(self.name) self.obj = self.library.get_playlist(self.name) if self.playlist else self.library.get_collection(self.name)
@ -909,9 +964,9 @@ class CollectionBuilder:
name = method_data name = method_data
if not os.path.exists(overlay): if not os.path.exists(overlay):
raise Failed(f"{self.Type} Error: {name} overlay image not found at {overlay}") raise Failed(f"{self.Type} Error: {name} overlay image not found at {overlay}")
if name in self.library.overlays: if name in self.library.overlays_old:
raise Failed("Each Overlay can only be used once per Library") raise Failed("Each Overlay can only be used once per Library")
self.library.overlays.append(name) self.library.overlays_old.append(name)
self.item_details[method_name] = name self.item_details[method_name] = name
elif method_name == "item_refresh_delay": elif method_name == "item_refresh_delay":
self.item_details[method_name] = util.parse(self.Type, method_name, method_data, datatype="int", default=0, minimum=0) self.item_details[method_name] = util.parse(self.Type, method_name, method_data, datatype="int", default=0, minimum=0)
@ -2089,7 +2144,7 @@ class CollectionBuilder:
filter_check = len(item.collections) > 0 filter_check = len(item.collections) > 0
elif filter_attr == "has_overlay": elif filter_attr == "has_overlay":
for label in item.labels: for label in item.labels:
if label.tag.lower().endswith(" overlay"): if label.tag.lower().endswith(" overlay") or label.tag.lower() == "overlay":
filter_check = True filter_check = True
break break
elif filter_attr == "has_dolby_vision": elif filter_attr == "has_dolby_vision":
@ -2275,19 +2330,7 @@ class CollectionBuilder:
rating_keys = [] rating_keys = []
if "item_overlay" in self.item_details: if "item_overlay" in self.item_details:
overlay_name = self.item_details["item_overlay"] overlay_name = self.item_details["item_overlay"]
if self.config.Cache: rating_keys = [int(item.ratingKey) for item in self.library.search(label=f"{overlay_name} Overlay")]
cache_keys = self.config.Cache.query_image_map_overlay(self.library.image_table_name, overlay_name)
if cache_keys:
for rating_key in cache_keys:
try:
item = self.fetch_item(rating_key)
except Failed as e:
logger.error(e)
continue
if isinstance(item, (Movie, Show)):
self.library.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"])
self.config.Cache.update_remove_overlay(self.library.image_table_name, overlay_name)
rating_keys = [int(item.ratingKey) for item in self.library.get_labeled_items(f"{overlay_name} Overlay")]
overlay_folder = os.path.join(self.config.default_dir, "overlays", overlay_name) overlay_folder = os.path.join(self.config.default_dir, "overlays", overlay_name)
overlay_image = Image.open(os.path.join(overlay_folder, "overlay.png")).convert("RGBA") overlay_image = Image.open(os.path.join(overlay_folder, "overlay.png")).convert("RGBA")
overlay = (overlay_name, overlay_folder, overlay_image) overlay = (overlay_name, overlay_folder, overlay_image)
@ -2303,7 +2346,7 @@ class CollectionBuilder:
if "non_item_remove_label" in self.item_details: if "non_item_remove_label" in self.item_details:
rk_compare = [item.ratingKey for item in self.items] rk_compare = [item.ratingKey for item in self.items]
for remove_label in self.item_details["non_item_remove_label"]: for remove_label in self.item_details["non_item_remove_label"]:
for non_item in self.library.get_labeled_items(remove_label): for non_item in self.library.search(label=remove_label, libtype=self.collection_level):
if non_item.ratingKey not in rk_compare: if non_item.ratingKey not in rk_compare:
self.library.edit_tags("label", non_item, remove_tags=[remove_label]) self.library.edit_tags("label", non_item, remove_tags=[remove_label])
@ -2312,9 +2355,9 @@ class CollectionBuilder:
for item in self.items: for item in self.items:
if int(item.ratingKey) in rating_keys and not revert: if int(item.ratingKey) in rating_keys and not revert:
rating_keys.remove(int(item.ratingKey)) rating_keys.remove(int(item.ratingKey))
if "item_assets" in self.item_details or overlay is not None: if overlay is not None:
try: try:
self.library.find_assets(item, overlay=overlay, folders=self.details["asset_folders"], create=self.details["create_asset_folders"]) self.library.update_asset(item, overlay=overlay, folders=self.details["asset_folders"], create=self.details["create_asset_folders"])
except Failed as e: except Failed as e:
logger.error(e) logger.error(e)
self.library.edit_tags("label", item, add_tags=add_tags, remove_tags=remove_tags, sync_tags=sync_tags) self.library.edit_tags("label", item, add_tags=add_tags, remove_tags=remove_tags, sync_tags=sync_tags)
@ -2384,14 +2427,14 @@ class CollectionBuilder:
self.library.edit_tags("label", item, remove_tags=[f"{overlay_name} Overlay"]) self.library.edit_tags("label", item, remove_tags=[f"{overlay_name} Overlay"])
og_image = os.path.join(overlay_folder, f"{rating_key}.png") og_image = os.path.join(overlay_folder, f"{rating_key}.png")
if os.path.exists(og_image): if os.path.exists(og_image):
self.library.upload_file_poster(item, og_image) self.library.upload_poster(item, og_image)
os.remove(og_image) os.remove(og_image)
self.config.Cache.update_image_map(item.ratingKey, self.library.image_table_name, "", "") self.config.Cache.update_image_map(item.ratingKey, self.library.image_table_name, "", "")
def load_collection(self): def load_collection(self):
if not self.obj and self.smart_url: if self.obj is None and self.smart_url:
self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url) self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url)
elif not self.obj and self.blank_collection: elif self.obj is None and self.blank_collection:
self.library.create_blank_collection(self.name) self.library.create_blank_collection(self.name)
elif self.smart_label_collection: elif self.smart_label_collection:
try: try:
@ -2518,9 +2561,11 @@ class CollectionBuilder:
if "name_mapping" in self.details: if "name_mapping" in self.details:
if self.details["name_mapping"]: name_mapping = self.details["name_mapping"] if self.details["name_mapping"]: name_mapping = self.details["name_mapping"]
else: logger.error(f"{self.Type} Error: name_mapping attribute is blank") else: logger.error(f"{self.Type} Error: name_mapping attribute is blank")
final_name, _ = util.validate_filename(name_mapping)
poster_image, background_image, asset_location = self.library.find_assets( poster_image, background_image, asset_location = self.library.find_assets(
self.obj, name=name_mapping, upload=False, name="poster" if self.details["asset_folders"] else final_name,
folders=self.details["asset_folders"], create=self.details["create_asset_folders"] folder_name=final_name if self.details["asset_folders"] else None,
prefix=f"{name_mapping}'s "
) )
if poster_image: if poster_image:
self.posters["asset_directory"] = poster_image self.posters["asset_directory"] = poster_image

@ -599,6 +599,14 @@ class Cache:
compare TEXT, compare TEXT,
location TEXT)""" location TEXT)"""
) )
cursor.execute(
f"""CREATE TABLE IF NOT EXISTS {table_name}_overlays (
key INTEGER PRIMARY KEY,
rating_key TEXT UNIQUE,
overlay TEXT,
compare TEXT,
location TEXT)"""
)
return table_name return table_name
def query_image_map_overlay(self, table_name, overlay): def query_image_map_overlay(self, table_name, overlay):
@ -625,8 +633,8 @@ class Cache:
cursor.execute(f"SELECT * FROM {table_name} WHERE rating_key = ?", (rating_key,)) cursor.execute(f"SELECT * FROM {table_name} WHERE rating_key = ?", (rating_key,))
row = cursor.fetchone() row = cursor.fetchone()
if row and row["location"]: if row and row["location"]:
return row["location"], row["compare"] return row["location"], row["compare"], row["overlay"]
return None, None return None, None, None
def update_image_map(self, rating_key, table_name, location, compare, overlay=""): def update_image_map(self, rating_key, table_name, location, compare, overlay=""):
with sqlite3.connect(self.cache_path) as connection: with sqlite3.connect(self.cache_path) as connection:

@ -16,6 +16,7 @@ from modules.mal import MyAnimeList
from modules.meta import PlaylistFile from modules.meta import PlaylistFile
from modules.notifiarr import Notifiarr from modules.notifiarr import Notifiarr
from modules.omdb import OMDb from modules.omdb import OMDb
from modules.overlays import Overlays
from modules.plex import Plex from modules.plex import Plex
from modules.radarr import Radarr from modules.radarr import Radarr
from modules.sonarr import Sonarr from modules.sonarr import Sonarr
@ -483,7 +484,7 @@ class ConfigFile:
default_playlist_file = os.path.abspath(os.path.join(self.default_dir, "playlists.yml")) default_playlist_file = os.path.abspath(os.path.join(self.default_dir, "playlists.yml"))
logger.warning(f"Config Warning: playlist_files attribute is blank using default: {default_playlist_file}") logger.warning(f"Config Warning: playlist_files attribute is blank using default: {default_playlist_file}")
paths_to_check = [default_playlist_file] paths_to_check = [default_playlist_file]
files = util.load_yaml_files(paths_to_check) files = util.load_files(paths_to_check, "playlist_files")
if not files: if not files:
raise Failed("Config Error: No Paths Found for playlist_files") raise Failed("Config Error: No Paths Found for playlist_files")
for file_type, playlist_file, temp_vars in files: for file_type, playlist_file, temp_vars in files:
@ -575,7 +576,8 @@ class ConfigFile:
"mass_content_rating_update": None, "mass_content_rating_update": None,
"mass_originally_available_update": None, "mass_originally_available_update": None,
"mass_imdb_parental_labels": None, "mass_imdb_parental_labels": None,
"remove_title_parentheses": None "remove_title_parentheses": None,
"remove_overlays": None
} }
display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"] display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"]
@ -669,6 +671,8 @@ class ConfigFile:
params["update_blank_track_titles"] = check_for_attribute(lib["operations"], "update_blank_track_titles", var_type="bool", default=False, save=False) params["update_blank_track_titles"] = check_for_attribute(lib["operations"], "update_blank_track_titles", var_type="bool", default=False, save=False)
if "remove_title_parentheses" in lib["operations"]: if "remove_title_parentheses" in lib["operations"]:
params["remove_title_parentheses"] = check_for_attribute(lib["operations"], "remove_title_parentheses", var_type="bool", default=False, save=False) params["remove_title_parentheses"] = check_for_attribute(lib["operations"], "remove_title_parentheses", var_type="bool", default=False, save=False)
if "remove_overlays" in lib["operations"]:
params["remove_overlays"] = check_for_attribute(lib["operations"], "remove_overlays", var_type="bool", default=False, save=False)
if "mass_collection_mode" in lib["operations"]: if "mass_collection_mode" in lib["operations"]:
try: try:
params["mass_collection_mode"] = util.check_collection_mode(lib["operations"]["mass_collection_mode"]) params["mass_collection_mode"] = util.check_collection_mode(lib["operations"]["mass_collection_mode"])
@ -729,7 +733,7 @@ class ConfigFile:
if lib and "metadata_path" in lib: if lib and "metadata_path" in lib:
if not lib["metadata_path"]: if not lib["metadata_path"]:
raise Failed("Config Error: metadata_path attribute is blank") raise Failed("Config Error: metadata_path attribute is blank")
files = util.load_yaml_files(lib["metadata_path"]) files = util.load_files(lib["metadata_path"], "metadata_path")
if not files: if not files:
raise Failed("Config Error: No Paths Found for metadata_path") raise Failed("Config Error: No Paths Found for metadata_path")
params["metadata_path"] = files params["metadata_path"] = files
@ -748,6 +752,15 @@ class ConfigFile:
except NotScheduled: except NotScheduled:
params["skip_library"] = True params["skip_library"] = True
params["overlay_path"] = []
if lib and "overlay_path" in lib:
if not lib["overlay_path"]:
raise Failed("Config Error: overlay_path attribute is blank")
files = util.load_files(lib["overlay_path"], "overlay_path")
if not files:
raise Failed("Config Error: No Paths Found for overlay_path")
params["overlay_path"] = files
logger.info("") logger.info("")
logger.separator("Plex Configuration", space=False, border=False) logger.separator("Plex Configuration", space=False, border=False)
params["plex"] = { params["plex"] = {
@ -850,6 +863,7 @@ class ConfigFile:
logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}")
library.Webhooks = Webhooks(self, {"error_webhooks": library.error_webhooks}, library=library, notifiarr=self.NotifiarrFactory) library.Webhooks = Webhooks(self, {"error_webhooks": library.error_webhooks}, library=library, notifiarr=self.NotifiarrFactory)
library.Overlays = Overlays(self, library)
logger.info("") logger.info("")
self.libraries.append(library) self.libraries.append(library)

@ -1,9 +1,9 @@
import os, shutil, time import os, shutil, time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from modules import util from modules import util
from modules.meta import MetadataFile from modules.meta import MetadataFile, OverlayFile
from modules.util import Failed
from modules.operations import Operations from modules.operations import Operations
from modules.util import Failed, ImageData
from PIL import Image from PIL import Image
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from ruamel import yaml from ruamel import yaml
@ -17,10 +17,13 @@ class Library(ABC):
self.Tautulli = None self.Tautulli = None
self.Webhooks = None self.Webhooks = None
self.Operations = Operations(config, self) self.Operations = Operations(config, self)
self.Overlays = None
self.Notifiarr = None self.Notifiarr = None
self.collections = [] self.collections = []
self.metadatas = [] self.metadatas = []
self.overlays = []
self.metadata_files = [] self.metadata_files = []
self.overlay_files = []
self.missing = {} self.missing = {}
self.movie_map = {} self.movie_map = {}
self.show_map = {} self.show_map = {}
@ -30,18 +33,21 @@ class Library(ABC):
self.movie_rating_key_map = {} self.movie_rating_key_map = {}
self.show_rating_key_map = {} self.show_rating_key_map = {}
self.run_again = [] self.run_again = []
self.overlays = [] self.overlays_old = []
self.type = "" self.type = ""
self.config = config self.config = config
self.name = params["name"] self.name = params["name"]
self.original_mapping_name = params["mapping_name"] self.original_mapping_name = params["mapping_name"]
self.metadata_path = params["metadata_path"] self.metadata_path = params["metadata_path"]
self.overlay_path = params["overlay_path"]
self.skip_library = params["skip_library"] self.skip_library = params["skip_library"]
self.asset_depth = params["asset_depth"] self.asset_depth = params["asset_depth"]
self.asset_directory = params["asset_directory"] if params["asset_directory"] else [] self.asset_directory = params["asset_directory"] if params["asset_directory"] else []
self.default_dir = params["default_dir"] self.default_dir = params["default_dir"]
self.mapping_name, output = util.validate_filename(self.original_mapping_name) self.mapping_name, output = util.validate_filename(self.original_mapping_name)
self.image_table_name = self.config.Cache.get_image_table_name(self.original_mapping_name) if self.config.Cache else None self.image_table_name = self.config.Cache.get_image_table_name(self.original_mapping_name) if self.config.Cache else None
self.overlay_folder = os.path.join(self.config.default_dir, "overlays")
self.overlay_backup = os.path.join(self.overlay_folder, f"{self.mapping_name} Original Posters")
self.missing_path = params["missing_path"] if params["missing_path"] else os.path.join(self.default_dir, f"{self.mapping_name}_missing.yml") self.missing_path = params["missing_path"] if params["missing_path"] else os.path.join(self.default_dir, f"{self.mapping_name}_missing.yml")
self.asset_folders = params["asset_folders"] self.asset_folders = params["asset_folders"]
self.create_asset_folders = params["create_asset_folders"] self.create_asset_folders = params["create_asset_folders"]
@ -82,6 +88,7 @@ class Library(ABC):
self.sonarr_remove_by_tag = params["sonarr_remove_by_tag"] self.sonarr_remove_by_tag = params["sonarr_remove_by_tag"]
self.update_blank_track_titles = params["update_blank_track_titles"] self.update_blank_track_titles = params["update_blank_track_titles"]
self.remove_title_parentheses = params["remove_title_parentheses"] self.remove_title_parentheses = params["remove_title_parentheses"]
self.remove_overlays = params["remove_overlays"]
self.mass_collection_mode = params["mass_collection_mode"] self.mass_collection_mode = params["mass_collection_mode"]
self.metadata_backup = params["metadata_backup"] self.metadata_backup = params["metadata_backup"]
self.genre_mapper = params["genre_mapper"] self.genre_mapper = params["genre_mapper"]
@ -123,13 +130,20 @@ class Library(ABC):
self.metadata_files.append(meta_obj) self.metadata_files.append(meta_obj)
except Failed as e: except Failed as e:
logger.error(e) logger.error(e)
for file_type, overlay_file, temp_vars in self.overlay_path:
try:
over_obj = OverlayFile(self.config, self, file_type, overlay_file, temp_vars)
self.overlays.extend([o.lower() for o in over_obj.overlays])
self.overlay_files.append(over_obj)
except Failed as e:
logger.error(e)
def upload_images(self, item, poster=None, background=None, overlay=None): def upload_images(self, item, poster=None, background=None, overlay=None):
image = None image = None
image_compare = None image_compare = None
poster_uploaded = False poster_uploaded = False
if self.config.Cache: if self.config.Cache:
image, image_compare = self.config.Cache.query_image_map(item.ratingKey, self.image_table_name) image, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, self.image_table_name)
if poster is not None: if poster is not None:
try: try:
@ -158,11 +172,10 @@ class Library(ABC):
response = self.config.get(item.posterUrl) response = self.config.get(item.posterUrl)
if response.status_code >= 400: if response.status_code >= 400:
raise Failed(f"Overlay Error: Overlay Failed for {item.title}") raise Failed(f"Overlay Error: Overlay Failed for {item.title}")
og_image = response.content
ext = "jpg" if response.headers["Content-Type"] == "image/jpegss" else "png" ext = "jpg" if response.headers["Content-Type"] == "image/jpegss" else "png"
temp_image = os.path.join(overlay_folder, f"temp.{ext}") temp_image = os.path.join(overlay_folder, f"temp.{ext}")
with open(temp_image, "wb") as handler: with open(temp_image, "wb") as handler:
handler.write(og_image) handler.write(response.content)
shutil.copyfile(temp_image, os.path.join(overlay_folder, f"{item.ratingKey}.{ext}")) shutil.copyfile(temp_image, os.path.join(overlay_folder, f"{item.ratingKey}.{ext}"))
while util.is_locked(temp_image): while util.is_locked(temp_image):
time.sleep(1) time.sleep(1)
@ -171,8 +184,9 @@ class Library(ABC):
new_poster = new_poster.resize(overlay_image.size, Image.ANTIALIAS) new_poster = new_poster.resize(overlay_image.size, Image.ANTIALIAS)
new_poster.paste(overlay_image, (0, 0), overlay_image) new_poster.paste(overlay_image, (0, 0), overlay_image)
new_poster.save(temp_image) new_poster.save(temp_image)
self.upload_file_poster(item, temp_image) self.upload_poster(item, temp_image)
self.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"]) self.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"])
self.reload(item)
poster_uploaded = True poster_uploaded = True
logger.info(f"Detail: Overlay: {overlay_name} applied to {item.title}") logger.info(f"Detail: Overlay: {overlay_name} applied to {item.title}")
except (OSError, BadRequest) as e: except (OSError, BadRequest) as e:
@ -184,7 +198,7 @@ class Library(ABC):
try: try:
image = None image = None
if self.config.Cache: if self.config.Cache:
image, image_compare = self.config.Cache.query_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds") image, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds")
if str(background.compare) != str(image_compare): if str(background.compare) != str(image_compare):
image = None image = None
if image is None or image != item.art: if image is None or image != item.art:
@ -212,7 +226,7 @@ class Library(ABC):
pass pass
@abstractmethod @abstractmethod
def upload_file_poster(self, item, image): def upload_poster(self, item, image, url=False):
pass pass
@abstractmethod @abstractmethod
@ -227,6 +241,47 @@ class Library(ABC):
def get_all(self, collection_level=None, load=False): def get_all(self, collection_level=None, load=False):
pass pass
def find_assets(self, name="poster", folder_name=None, item_directory=None, prefix=""):
poster = None
background = None
item_dir = None
search_dir = item_directory if item_directory else None
for ad in self.asset_directory:
item_dir = None
if not search_dir:
search_dir = ad
if folder_name:
if os.path.isdir(os.path.join(ad, folder_name)):
item_dir = os.path.join(ad, folder_name)
else:
for n in range(1, self.asset_depth + 1):
new_path = ad
for i in range(1, n + 1):
new_path = os.path.join(new_path, "*")
matches = util.glob_filter(os.path.join(new_path, folder_name))
if len(matches) > 0:
item_dir = os.path.abspath(matches[0])
break
if item_dir is None:
continue
search_dir = item_dir
if item_directory:
item_dir = item_directory
file_name = name if item_dir else f"{folder_name}_{name}"
poster_filter = os.path.join(search_dir, f"{file_name}.*")
background_filter = os.path.join(search_dir, "background.*" if file_name == "poster" else f"{file_name}_background.*")
poster_matches = util.glob_filter(poster_filter)
if len(poster_matches) > 0:
poster = ImageData("asset_directory", os.path.abspath(poster_matches[0]), prefix=prefix, is_url=False)
background_matches = util.glob_filter(background_filter)
if len(background_matches) > 0:
background = ImageData("asset_directory", os.path.abspath(background_matches[0]), prefix=prefix, is_poster=False, is_url=False)
break
return poster, background, item_dir
def add_missing(self, collection, items, is_movie): def add_missing(self, collection, items, is_movie):
if collection not in self.missing: if collection not in self.missing:
self.missing[collection] = {} self.missing[collection] = {}

@ -239,10 +239,10 @@ class DataFile:
def external_templates(self, data): def external_templates(self, data):
if "external_templates" in data and data["external_templates"]: if "external_templates" in data and data["external_templates"]:
files = util.load_yaml_files(data["external_templates"]) files = util.load_files(data["external_templates"], "external_templates")
if not files: if not files:
logger.error("Config Error: No Paths Found for external_templates") logger.error("Config Error: No Paths Found for external_templates")
for file_type, template_file, temp_vars in util.load_yaml_files(data["external_templates"]): for file_type, template_file, temp_vars in util.load_files(data["external_templates"], "external_templates"):
temp_data = self.load_file(file_type, template_file) temp_data = self.load_file(file_type, template_file)
if temp_data and isinstance(temp_data, dict) and "templates" in temp_data and temp_data["templates"] and isinstance(temp_data["templates"], dict): if temp_data and isinstance(temp_data, dict) and "templates" in temp_data and temp_data["templates"] and isinstance(temp_data["templates"], dict):
for temp_key, temp_value in temp_data["templates"].items(): for temp_key, temp_value in temp_data["templates"].items():
@ -1083,3 +1083,18 @@ class PlaylistFile(DataFile):
if not self.playlists: if not self.playlists:
raise Failed("YAML Error: playlists attribute is required") raise Failed("YAML Error: playlists attribute is required")
logger.info(f"Playlist File Loaded Successfully") logger.info(f"Playlist File Loaded Successfully")
class OverlayFile(DataFile):
def __init__(self, config, library, file_type, path, temp_vars):
super().__init__(config, file_type, path, temp_vars)
self.library = library
self.data_type = "Overlay"
logger.info("")
logger.info(f"Loading Overlay File {file_type}: {path}")
data = self.load_file(self.type, self.path)
self.overlays = get_dict("overlays", data, self.library.overlays)
self.templates = get_dict("templates", data)
self.external_templates(data)
if not self.overlays:
raise Failed("YAML Error: overlays attribute is required")
logger.info(f"Overlay File Loaded Successfully")

@ -74,7 +74,7 @@ class Operations:
continue continue
logger.ghost(f"Processing: {i}/{len(items)} {item.title}") logger.ghost(f"Processing: {i}/{len(items)} {item.title}")
if self.library.assets_for_all: if self.library.assets_for_all:
self.library.find_assets(item) self.library.update_asset2(item)
tmdb_id, tvdb_id, imdb_id = self.library.get_ids(item) tmdb_id, tvdb_id, imdb_id = self.library.get_ids(item)
item.batchEdits() item.batchEdits()
@ -381,7 +381,7 @@ class Operations:
logger.separator(f"Unmanaged Collection Assets Check for {self.library.name} Library", space=False, border=False) logger.separator(f"Unmanaged Collection Assets Check for {self.library.name} Library", space=False, border=False)
logger.info("") logger.info("")
for col in unmanaged_collections: for col in unmanaged_collections:
self.library.find_assets(col) self.library.update_asset2(col)
if self.library.metadata_backup: if self.library.metadata_backup:
logger.info("") logger.info("")

@ -0,0 +1,203 @@
import os, time
from modules import util
from modules.builder import CollectionBuilder
from modules.util import Failed
from plexapi.exceptions import BadRequest
from plexapi.video import Show, Season, Episode
from PIL import Image
logger = util.logger
class Overlays:
def __init__(self, config, library):
self.config = config
self.library = library
self.overlays = []
def run_overlays(self):
logger.info("")
logger.separator(f"{self.library.name} Library Overlays")
logger.info("")
overlay_rating_keys = {}
item_keys = {}
os.makedirs(self.library.overlay_backup, exist_ok=True)
overlay_updated = {}
overlay_images = {}
item_overlays = {}
if not self.library.remove_overlays:
for overlay_file in self.library.overlay_files:
for k, v in overlay_file.overlays.items():
builder = CollectionBuilder(self.config, overlay_file, k, v, library=self.library, overlay=True)
logger.info("")
logger.separator(f"Running {k} Overlay", space=False, border=False)
if builder.filters or builder.tmdb_filters:
logger.info("")
for filter_key, filter_value in builder.filters:
logger.info(f"Collection Filter {filter_key}: {filter_value}")
for filter_key, filter_value in builder.tmdb_filters:
logger.info(f"Collection Filter {filter_key}: {filter_value}")
for method, value in builder.builders:
logger.debug("")
logger.debug(f"Builder: {method}: {value}")
logger.info("")
builder.filter_and_save_items(builder.gather_ids(method, value))
if builder.added_items:
if builder.overlay not in overlay_rating_keys:
overlay_rating_keys[builder.overlay] = []
for item in builder.added_items:
item_keys[item.ratingKey] = item
if item.ratingKey not in overlay_rating_keys[builder.overlay]:
overlay_rating_keys[builder.overlay].append(item.ratingKey)
for overlay_name, over_keys in overlay_rating_keys.items():
clean_name, _ = util.validate_filename(overlay_name)
image_compare = None
if self.config.Cache:
_, image_compare, _ = self.config.Cache.query_image_map(overlay_name, f"{self.library.image_table_name}_overlays")
overlay_file = os.path.join(self.library.overlay_folder, f"{clean_name}.png")
overlay_size = os.stat(overlay_file).st_size
overlay_updated[overlay_name] = not image_compare or str(overlay_size) != str(image_compare)
overlay_images[overlay_name] = Image.open(overlay_file).convert("RGBA")
for over_key in over_keys:
if over_key not in item_overlays:
item_overlays[over_key] = []
item_overlays[over_key].append(overlay_name)
if self.config.Cache:
self.config.Cache.update_image_map(overlay_name, f"{self.library.image_table_name}_overlays", overlay_name, overlay_size)
def get_overlay_items(libtype=None):
return [o for o in self.library.search(label="Overlay", libtype=libtype) if o.ratingKey not in item_overlays]
remove_overlays = get_overlay_items()
if self.library.is_show:
remove_overlays.extend(get_overlay_items(libtype="episode"))
remove_overlays.extend(get_overlay_items(libtype="season"))
elif self.library.is_music:
remove_overlays.extend(get_overlay_items(libtype="album"))
for i, item in enumerate(remove_overlays, 1):
logger.ghost(f"Restoring: {i}/{len(remove_overlays)} {item.title}")
clean_name, _ = util.validate_filename(item.title)
poster, _, item_dir = self.library.find_assets(
name="poster" if self.library.asset_folders else clean_name,
folder_name=clean_name if self.library.asset_folders else None,
prefix=f"{item.title}'s "
)
poster_location = None
is_url = False
if poster:
poster_location = poster.location
elif os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")):
poster_location = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")
elif os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")):
poster_location = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")
else:
is_url = True
if self.library.is_movie:
if item.ratingKey in self.library.movie_rating_key_map:
poster_location = self.config.TMDb.get_movie(self.library.movie_rating_key_map[item.ratingKey]).poster_url
elif self.library.is_show:
if item.ratingKey in self.library.show_rating_key_map:
poster_location = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(self.library.show_rating_key_map[item.ratingKey])).poster_url
if poster_location:
self.library.upload_poster(item, poster_location, url=is_url)
self.library.edit_tags("label", item, remove_tags=["Overlay"])
else:
logger.error(f"No Poster found to restore for {item.title}")
logger.exorcise()
for i, (over_key, over_names) in enumerate(item_overlays.items(), 1):
try:
item = item_keys[over_key]
logger.ghost(f"Overlaying: {i}/{len(item_overlays)} {item.title}")
image_compare = None
overlay_compare = None
if self.config.Cache:
image, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, f"{self.library.image_table_name}_overlays")
overlay_compare = [] if overlay_compare is None else util.get_list(overlay_compare)
has_overlay = any([item_tag.tag.lower() == "overlay" for item_tag in item.labels])
overlay_change = False if has_overlay else True
if not overlay_change:
for oc in overlay_compare:
if oc not in over_names:
overlay_change = True
if not overlay_change:
for over_name in over_names:
if over_name not in overlay_compare or overlay_updated[over_name]:
overlay_change = True
clean_name, _ = util.validate_filename(item.title)
poster, _, item_dir = self.library.find_assets(
name="poster" if self.library.asset_folders else clean_name,
folder_name=clean_name if self.library.asset_folders else None,
prefix=f"{item.title}'s "
)
has_original = False
changed_image = False
if poster:
if image_compare and str(poster.compare) != str(image_compare):
changed_image = True
else:
if os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")):
has_original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")
elif os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")):
has_original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")
else:
changed_image = True
self.library.reload(item)
poster_url = item.posterUrl
if has_overlay:
if self.library.is_movie:
if item.ratingKey in self.library.movie_rating_key_map:
poster_url = self.config.TMDb.get_movie(self.library.movie_rating_key_map[item.ratingKey]).poster_url
elif self.library.is_show:
check_key = item.ratingKey if isinstance(item, Show) else item.show().ratingKey
tmdb_id = self.config.Convert.tvdb_to_tmdb(self.library.show_rating_key_map[check_key])
if isinstance(item, Show) and item.ratingKey in self.library.show_rating_key_map:
poster_url = self.config.TMDb.get_show(tmdb_id).poster_url
elif isinstance(item, Season):
poster_url = self.config.TMDb.get_season(tmdb_id, item.seasonNumber).poster_url
elif isinstance(item, Episode):
poster_url = self.config.TMDb.get_episode(tmdb_id, item.seasonNumber, item.episodeNumber).still_url
response = self.config.get(poster_url)
if response.status_code >= 400:
raise Failed(f"Overlay Error: Poster Download Failed for {item.title}")
ext = "jpg" if response.headers["Content-Type"] == "image/jpeg" else "png"
backup_image = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.{ext}")
with open(backup_image, "wb") as handler:
handler.write(response.content)
while util.is_locked(backup_image):
time.sleep(1)
has_original = backup_image
poster_uploaded = False
if changed_image or overlay_change:
new_poster = Image.open(poster.location if poster else has_original).convert("RGBA")
temp = os.path.join(self.library.overlay_folder, f"temp.png")
try:
for over_name in over_names:
new_poster = new_poster.resize(overlay_images[over_name].size, Image.ANTIALIAS)
new_poster.paste(overlay_images[over_name], (0, 0), overlay_images[over_name])
new_poster.save(temp, "PNG")
self.library.upload_poster(item, temp)
self.library.edit_tags("label", item, add_tags=["Overlay"])
self.library.reload(item)
poster_uploaded = True
logger.info(f"Detail: Overlays: {', '.join(over_names)} applied to {item.title}")
except (OSError, BadRequest) as e:
logger.stacktrace()
raise Failed(f"Overlay Error: {e}")
if self.config.Cache:
if poster_uploaded:
self.config.Cache.update_image_map(
item.ratingKey, self.library.image_table_name, item.thumb,
poster.compare if poster else item.thumb, overlay=','.join(over_names)
)
except Failed as e:
logger.error(e)
logger.exorcise()

@ -459,7 +459,7 @@ class Plex(Library):
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def get_labeled_items(self, label): def get_labeled_items(self, label):
return self.Plex.search(label=label) return self.search(label=label)
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def fetchItem(self, data): def fetchItem(self, data):
@ -566,9 +566,11 @@ class Plex(Library):
raise Failed(e) raise Failed(e)
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def upload_file_poster(self, item, image): def upload_poster(self, item, image, url=False):
item.uploadPoster(filepath=image) if url:
self.reload(item) item.uploadPoster(url=image)
else:
item.uploadPoster(filepath=image)
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
def get_actor_id(self, name): def get_actor_id(self, name):
@ -798,7 +800,7 @@ class Plex(Library):
def get_collection_items(self, collection, smart_label_collection): def get_collection_items(self, collection, smart_label_collection):
if smart_label_collection: if smart_label_collection:
return self.get_labeled_items(collection.title if isinstance(collection, Collection) else str(collection)) return self.search(label=collection.title if isinstance(collection, Collection) else str(collection))
elif isinstance(collection, (Collection, Playlist)): elif isinstance(collection, (Collection, Playlist)):
if collection.smart: if collection.smart:
return self.get_filter_items(self.smart_filter(collection)) return self.get_filter_items(self.smart_filter(collection))
@ -873,161 +875,229 @@ class Plex(Library):
logger.info(final) logger.info(final)
return final return final
def find_assets(self, item, name=None, upload=True, overlay=None, folders=None, create=None): def update_asset(self, item, overlay=None, folders=None, create=None):
if isinstance(item, (Movie, Artist, Show)): if isinstance(item, (Movie, Artist, Show, Episode, Season)):
path_test = str(item.locations[0]) starting = item.show() if isinstance(item, (Episode, Season)) else item
path_test = str(starting.locations[0])
if not os.path.dirname(path_test): if not os.path.dirname(path_test):
path_test = path_test.replace("\\", "/") path_test = path_test.replace("\\", "/")
name = os.path.basename(os.path.dirname(path_test) if isinstance(item, Movie) else path_test) name = os.path.basename(os.path.dirname(path_test) if isinstance(starting, Movie) else path_test)
elif isinstance(item, (Collection, Playlist)): elif isinstance(item, (Collection, Playlist)):
name = name if name else item.title name = item.title
else: else:
return None, None, None return None, None, None
if not folders: if folders is None:
folders = self.asset_folders folders = self.asset_folders
if not create: if create is None:
create = self.create_asset_folders create = self.create_asset_folders
found_folder = None
poster = None
background = None
for ad in self.asset_directory:
item_dir = None
if folders:
if os.path.isdir(os.path.join(ad, name)):
item_dir = os.path.join(ad, name)
else:
for n in range(1, self.asset_depth + 1):
new_path = ad
for i in range(1, n + 1):
new_path = os.path.join(new_path, "*")
matches = util.glob_filter(os.path.join(new_path, name))
if len(matches) > 0:
item_dir = os.path.abspath(matches[0])
break
if item_dir is None:
continue
found_folder = item_dir
poster_filter = os.path.join(item_dir, "poster.*")
background_filter = os.path.join(item_dir, "background.*")
else:
poster_filter = os.path.join(ad, f"{name}.*")
background_filter = os.path.join(ad, f"{name}_background.*")
poster_matches = util.glob_filter(poster_filter)
if len(poster_matches) > 0:
poster = ImageData("asset_directory", os.path.abspath(poster_matches[0]), prefix=f"{item.title}'s ", is_url=False)
background_matches = util.glob_filter(background_filter)
if len(background_matches) > 0:
background = ImageData("asset_directory", os.path.abspath(background_matches[0]), prefix=f"{item.title}'s ", is_poster=False, is_url=False)
if item_dir and self.dimensional_asset_rename and (not poster or not background):
for file in util.glob_filter(os.path.join(item_dir, "*.*")):
if file.lower().endswith((".jpg", ".png", ".jpeg")):
image = Image.open(file)
_w, _h = image.size
image.close()
if not poster and _h >= _w:
new_path = os.path.join(os.path.dirname(file), f"poster{os.path.splitext(file)[1].lower()}")
os.rename(file, new_path)
poster = ImageData("asset_directory", os.path.abspath(new_path), prefix=f"{item.title}'s ", is_url=False)
elif not background and _w > _h:
new_path = os.path.join(os.path.dirname(file), f"background{os.path.splitext(file)[1].lower()}")
os.rename(file, new_path)
background = ImageData("asset_directory", os.path.abspath(new_path), prefix=f"{item.title}'s ", is_poster=False, is_url=False)
if poster and background:
break
if poster or background: poster, background, item_dir = self.find_assets(
if upload: name="poster" if folders else name,
self.upload_images(item, poster=poster, background=background, overlay=overlay) folder_name=name if folders else None,
prefix=f"{item.title}'s "
)
if item_dir and self.dimensional_asset_rename and (not poster or not background):
for file in util.glob_filter(os.path.join(item_dir, "*.*")):
if file.lower().endswith((".jpg", ".png", ".jpeg")):
image = Image.open(file)
_w, _h = image.size
image.close()
if not poster and _h >= _w:
new_path = os.path.join(os.path.dirname(file), f"poster{os.path.splitext(file)[1].lower()}")
os.rename(file, new_path)
poster = ImageData("asset_directory", os.path.abspath(new_path), prefix=f"{item.title}'s ", is_url=False)
elif not background and _w > _h:
new_path = os.path.join(os.path.dirname(file), f"background{os.path.splitext(file)[1].lower()}")
os.rename(file, new_path)
background = ImageData("asset_directory", os.path.abspath(new_path), prefix=f"{item.title}'s ", is_poster=False, is_url=False)
if poster and background:
break
if poster or background:
self.upload_images(item, poster=poster, background=background, overlay=overlay)
if isinstance(item, Show):
missing_seasons = ""
missing_episodes = ""
found_season = False
found_episode = False
for season in self.query(item.seasons):
season_name = f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}"
season_poster, season_background, _ = self.find_assets(
name=season_name,
folder_name=name,
item_directory=item_dir,
prefix=f"{item.title} Season {season.seasonNumber}'s "
)
if season_poster:
found_season = True
elif self.show_missing_season_assets and season.seasonNumber > 0:
missing_seasons += f"\nMissing Season {season.seasonNumber} Poster"
if season_poster or season_background:
self.upload_images(season, poster=season_poster, background=season_background)
for episode in self.query(season.episodes):
if episode.seasonEpisode:
episode_poster, episode_background, _ = self.find_assets(
name=episode.seasonEpisode.upper(),
folder_name=name,
item_directory=item_dir,
prefix=f"{item.title} {episode.seasonEpisode.upper()}'s "
)
if episode_poster:
found_episode = True
self.upload_images(episode, poster=episode_poster, background=episode_background)
elif self.show_missing_episode_assets:
missing_episodes += f"\nMissing {episode.seasonEpisode.upper()} Title Card"
if (found_season and missing_seasons) or (found_episode and missing_episodes):
output = f"Missing Posters for {item.title}"
if found_season:
output += missing_seasons
if found_episode:
output += missing_episodes
logger.info(output)
if isinstance(item, Artist):
missing_assets = ""
found_album = False
for album in self.query(item.albums):
album_poster, album_background, _ = self.find_assets(
name=album.title,
folder_name=name,
item_directory=item_dir,
prefix=f"{item.title} Album {album.title}'s "
)
if album_poster:
found_album = True
else: else:
return poster, background, item_dir missing_assets += f"\nMissing Album {album.title} Poster"
if isinstance(item, Show): if album_poster or album_background:
missing_seasons = "" self.upload_images(album, poster=album_poster, background=album_background)
missing_episodes = "" if self.show_missing_season_assets and found_album and missing_assets:
found_season = False logger.info(f"Missing Album Posters for {item.title}{missing_assets}")
found_episode = False
for season in self.query(item.seasons):
season_name = f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}"
if item_dir:
season_poster_filter = os.path.join(item_dir, f"{season_name}.*")
season_background_filter = os.path.join(item_dir, f"{season_name}_background.*")
else:
season_poster_filter = os.path.join(ad, f"{name}_{season_name}.*")
season_background_filter = os.path.join(ad, f"{name}_{season_name}_background.*")
season_poster = None
season_background = None
matches = util.glob_filter(season_poster_filter)
if len(matches) > 0:
season_poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Season {season.seasonNumber}'s ", is_url=False)
found_season = True
elif self.show_missing_season_assets and season.seasonNumber > 0:
missing_seasons += f"\nMissing Season {season.seasonNumber} Poster"
matches = util.glob_filter(season_background_filter)
if len(matches) > 0:
season_background = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Season {season.seasonNumber}'s ", is_poster=False, is_url=False)
if season_poster or season_background:
self.upload_images(season, poster=season_poster, background=season_background)
for episode in self.query(season.episodes):
if episode.seasonEpisode:
if item_dir:
episode_filter = os.path.join(item_dir, f"{episode.seasonEpisode.upper()}.*")
else:
episode_filter = os.path.join(ad, f"{name}_{episode.seasonEpisode.upper()}.*")
matches = util.glob_filter(episode_filter)
if len(matches) > 0:
episode_poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} {episode.seasonEpisode.upper()}'s ", is_url=False)
found_episode = True
self.upload_images(episode, poster=episode_poster)
elif self.show_missing_episode_assets:
missing_episodes += f"\nMissing {episode.seasonEpisode.upper()} Title Card"
if (found_season and missing_seasons) or (found_episode and missing_episodes):
output = f"Missing Posters for {item.title}"
if found_season:
output += missing_seasons
if found_episode:
output += missing_episodes
logger.info(output)
if isinstance(item, Artist):
missing_assets = ""
found_album = False
for album in self.query(item.albums):
if item_dir:
album_poster_filter = os.path.join(item_dir, f"{album.title}.*")
album_background_filter = os.path.join(item_dir, f"{album.title}_background.*")
else:
album_poster_filter = os.path.join(ad, f"{name}_{album.title}.*")
album_background_filter = os.path.join(ad, f"{name}_{album.title}_background.*")
album_poster = None
album_background = None
matches = util.glob_filter(album_poster_filter)
if len(matches) > 0:
album_poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Album {album.title}'s ", is_url=False)
found_album = True
else:
missing_assets += f"\nMissing Album {album.title} Poster"
matches = util.glob_filter(album_background_filter)
if len(matches) > 0:
album_background = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Album {album.title}'s ", is_poster=False, is_url=False)
if album_poster or album_background:
self.upload_images(album, poster=album_poster, background=album_background)
if self.show_missing_season_assets and found_album and missing_assets:
logger.info(f"Missing Album Posters for {item.title}{missing_assets}")
if isinstance(item, (Movie, Show)) and not poster and overlay: if isinstance(item, (Movie, Show)) and not poster and overlay:
self.upload_images(item, overlay=overlay) self.upload_images(item, overlay=overlay)
if create and folders and not found_folder: if create and folders and item_dir is None:
filename, _ = util.validate_filename(name) filename, _ = util.validate_filename(name)
found_folder = os.path.join(self.asset_directory[0], filename) item_dir = os.path.join(self.asset_directory[0], filename)
os.makedirs(found_folder, exist_ok=True) os.makedirs(item_dir, exist_ok=True)
logger.info(f"Asset Directory Created: {found_folder}") logger.info(f"Asset Directory Created: {item_dir}")
elif isinstance(item, (Movie, Show)) and not overlay and folders and not found_folder: elif isinstance(item, (Movie, Show)) and not overlay and folders and item_dir is None:
logger.warning(f"Asset Warning: No asset folder found called '{name}'") logger.warning(f"Asset Warning: No asset folder found called '{name}'")
elif isinstance(item, (Movie, Show)) and not poster and not background and self.show_missing_assets: elif isinstance(item, (Movie, Show)) and not poster and not background and self.show_missing_assets:
logger.warning(f"Asset Warning: No poster or background found in an assets folder for '{name}'") logger.warning(f"Asset Warning: No poster or background found in an assets folder for '{name}'")
return None, None, found_folder return None, None, item_dir
def update_asset2(self, item, folders=None, create=None):
if isinstance(item, (Movie, Artist, Show)):
starting = item.show() if isinstance(item, (Episode, Season)) else item
path_test = str(starting.locations[0])
if not os.path.dirname(path_test):
path_test = path_test.replace("\\", "/")
name = os.path.basename(os.path.dirname(path_test) if isinstance(starting, Movie) else path_test)
elif isinstance(item, (Collection, Playlist)):
name, _ = util.validate_filename(item.title)
else:
return None, None, None
if folders is None:
folders = self.asset_folders
if create is None:
create = self.create_asset_folders
poster, background, item_dir = self.find_assets(
name="poster" if folders else name,
folder_name=name if folders else None,
prefix=f"{item.title}'s "
)
if item_dir and self.dimensional_asset_rename and (not poster or not background):
for file in util.glob_filter(os.path.join(item_dir, "*.*")):
if file.lower().endswith((".jpg", ".png", ".jpeg")):
image = Image.open(file)
_w, _h = image.size
image.close()
if not poster and _h >= _w:
new_path = os.path.join(os.path.dirname(file), f"poster{os.path.splitext(file)[1].lower()}")
os.rename(file, new_path)
poster = ImageData("asset_directory", os.path.abspath(new_path), prefix=f"{item.title}'s ", is_url=False)
elif not background and _w > _h:
new_path = os.path.join(os.path.dirname(file), f"background{os.path.splitext(file)[1].lower()}")
os.rename(file, new_path)
background = ImageData("asset_directory", os.path.abspath(new_path), prefix=f"{item.title}'s ", is_poster=False, is_url=False)
if poster and background:
break
if poster or background:
self.upload_images(item, poster=poster, background=background)
if isinstance(item, Show):
missing_seasons = ""
missing_episodes = ""
found_season = False
found_episode = False
for season in self.query(item.seasons):
season_name = f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}"
season_poster, season_background, _ = self.find_assets(
name=season_name,
folder_name=name,
item_directory=item_dir,
prefix=f"{item.title} Season {season.seasonNumber}'s "
)
if season_poster:
found_season = True
elif self.show_missing_season_assets and season.seasonNumber > 0:
missing_seasons += f"\nMissing Season {season.seasonNumber} Poster"
if season_poster or season_background:
self.upload_images(season, poster=season_poster, background=season_background)
for episode in self.query(season.episodes):
if episode.seasonEpisode:
episode_poster, episode_background, _ = self.find_assets(
name=episode.seasonEpisode.upper(),
folder_name=name,
item_directory=item_dir,
prefix=f"{item.title} {episode.seasonEpisode.upper()}'s "
)
if episode_poster or episode_background:
found_episode = True
self.upload_images(episode, poster=episode_poster, background=episode_background)
elif self.show_missing_episode_assets:
missing_episodes += f"\nMissing {episode.seasonEpisode.upper()} Title Card"
if (found_season and missing_seasons) or (found_episode and missing_episodes):
output = f"Missing Posters for {item.title}"
if found_season:
output += missing_seasons
if found_episode:
output += missing_episodes
logger.info(output)
if isinstance(item, Artist):
missing_assets = ""
found_album = False
for album in self.query(item.albums):
album_poster, album_background, _ = self.find_assets(
name=album.title,
folder_name=name,
item_directory=item_dir,
prefix=f"{item.title} Album {album.title}'s "
)
if album_poster:
found_album = True
else:
missing_assets += f"\nMissing Album {album.title} Poster"
if album_poster or album_background:
self.upload_images(album, poster=album_poster, background=album_background)
if self.show_missing_season_assets and found_album and missing_assets:
logger.info(f"Missing Album Posters for {item.title}{missing_assets}")
if create and folders and item_dir is None:
filename, _ = util.validate_filename(name)
item_dir = os.path.join(self.asset_directory[0], filename)
os.makedirs(item_dir, exist_ok=True)
logger.info(f"Asset Directory Created: {item_dir}")
elif folders and item_dir is None:
logger.warning(f"Asset Warning: No asset folder found called '{name}'")
elif not poster and not background and self.show_missing_assets:
logger.warning(f"Asset Warning: No poster or background found in an assets folder for '{name}'")
return poster, background, item_dir
def get_ids(self, item): def get_ids(self, item):
tmdb_id = None tmdb_id = None

@ -206,6 +206,14 @@ class TMDb:
try: return TMDbShow(self, tmdb_id) try: return TMDbShow(self, tmdb_id)
except TMDbException as e: raise Failed(f"TMDb Error: No Show found for TMDb ID {tmdb_id}: {e}") except TMDbException as e: raise Failed(f"TMDb Error: No Show found for TMDb ID {tmdb_id}: {e}")
def get_season(self, tmdb_id, season_number, partial=None):
try: return self.TMDb.tv_season(tmdb_id, season_number, partial=partial)
except TMDbException as e: raise Failed(f"TMDb Error: No Season found for TMDb ID {tmdb_id} Season {season_number}: {e}")
def get_episode(self, tmdb_id, season_number, episode_number, partial=None):
try: return self.TMDb.tv_episode(tmdb_id, season_number, episode_number, partial=partial)
except TMDbException as e: raise Failed(f"TMDb Error: No Episode found for TMDb ID {tmdb_id} Season {season_number} Episode {episode_number}: {e}")
def get_collection(self, tmdb_id, partial=None): def get_collection(self, tmdb_id, partial=None):
try: return self.TMDb.collection(tmdb_id, partial=partial) try: return self.TMDb.collection(tmdb_id, partial=partial)
except TMDbException as e: raise Failed(f"TMDb Error: No Collection found for TMDb ID {tmdb_id}: {e}") except TMDbException as e: raise Failed(f"TMDb Error: No Collection found for TMDb ID {tmdb_id}: {e}")

@ -263,41 +263,41 @@ def time_window(tw):
else: else:
return tw return tw
def load_yaml_files(yaml_files): def load_files(files_to_load, method, file_type="yml"):
files = [] files = []
for yaml_file in get_list(yaml_files, split=False): for file in get_list(files_to_load, split=False):
if isinstance(yaml_file, dict): if isinstance(file, dict):
temp_vars = {} temp_vars = {}
if "template_variables" in yaml_file and yaml_file["template_variables"] and isinstance(yaml_file["template_variables"], dict): if "template_variables" in file and file["template_variables"] and isinstance(file["template_variables"], dict):
temp_vars = yaml_file["template_variables"] temp_vars = file["template_variables"]
def check_dict(attr, name): def check_dict(attr, name):
if attr in yaml_file: if attr in file:
if yaml_file[attr]: if file[attr]:
files.append((name, yaml_file[attr], temp_vars)) files.append((name, file[attr], temp_vars))
else: else:
logger.error(f"Config Error: metadata_path {attr} is blank") logger.error(f"Config Error: {method} {attr} is blank")
check_dict("url", "URL") check_dict("url", "URL")
check_dict("git", "Git") check_dict("git", "Git")
check_dict("repo", "Repo") check_dict("repo", "Repo")
check_dict("file", "File") check_dict("file", "File")
if "folder" in yaml_file: if "folder" in file:
if yaml_file["folder"] is None: if file["folder"] is None:
logger.error(f"Config Error: metadata_path folder is blank") logger.error(f"Config Error: {method} folder is blank")
elif not os.path.isdir(yaml_file["folder"]): elif not os.path.isdir(file["folder"]):
logger.error(f"Config Error: Folder not found: {yaml_file['folder']}") logger.error(f"Config Error: Folder not found: {file['folder']}")
else: else:
yml_files = glob_filter(os.path.join(yaml_file["folder"], "*.yml")) yml_files = glob_filter(os.path.join(file["folder"], f"*.{file_type}"))
if yml_files: if yml_files:
files.extend([("File", yml, temp_vars) for yml in yml_files]) files.extend([("File", yml, temp_vars) for yml in yml_files])
else: else:
logger.error(f"Config Error: No YAML (.yml) files found in {yaml_file['folder']}") logger.error(f"Config Error: No {file_type.upper()} (.{file_type}) files found in {file['folder']}")
else: else:
if os.path.exists(yaml_file): if os.path.exists(file):
files.append(("File", yaml_file, {})) files.append(("File", file, {}))
else: else:
logger.error(f"Config Error: Path not found: {yaml_file}") logger.error(f"Config Error: Path not found: {file}")
return files return files
def check_num(num, is_int=True): def check_num(num, is_int=True):

@ -26,7 +26,8 @@ parser.add_argument("-is", "--ignore-schedules", dest="ignore_schedules", help="
parser.add_argument("-ig", "--ignore-ghost", dest="ignore_ghost", help="Run ignoring ghost logging", action="store_true", default=False) parser.add_argument("-ig", "--ignore-ghost", dest="ignore_ghost", help="Run ignoring ghost logging", action="store_true", default=False)
parser.add_argument("-rt", "--test", "--tests", "--run-test", "--run-tests", dest="test", help="Run in debug mode with only collections that have test: true", action="store_true", default=False) parser.add_argument("-rt", "--test", "--tests", "--run-test", "--run-tests", dest="test", help="Run in debug mode with only collections that have test: true", action="store_true", default=False)
parser.add_argument("-co", "--collection-only", "--collections-only", dest="collection_only", help="Run only collection operations", action="store_true", default=False) parser.add_argument("-co", "--collection-only", "--collections-only", dest="collection_only", help="Run only collection operations", action="store_true", default=False)
parser.add_argument("-lo", "--library-only", "--libraries-only", dest="library_only", help="Run only library operations", action="store_true", default=False) parser.add_argument("-op", "--operation", "--operations", "-lo", "--library-only", "--libraries-only", "--operation-only", "--operations-only", dest="operations", help="Run only operations", action="store_true", default=False)
parser.add_argument("-ov", "--overlay", "--overlays", "--overlay-only", "--overlays-only", dest="overlays", help="Run only overlays", action="store_true", default=False)
parser.add_argument("-lf", "--library-first", "--libraries-first", dest="library_first", help="Run library operations before collections", action="store_true", default=False) parser.add_argument("-lf", "--library-first", "--libraries-first", dest="library_first", help="Run library operations before collections", action="store_true", default=False)
parser.add_argument("-rc", "-cl", "--collection", "--collections", "--run-collection", "--run-collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str) parser.add_argument("-rc", "-cl", "--collection", "--collections", "--run-collection", "--run-collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str)
parser.add_argument("-rl", "-l", "--library", "--libraries", "--run-library", "--run-libraries", dest="libraries", help="Process only specified libraries (comma-separated list)", type=str) parser.add_argument("-rl", "-l", "--library", "--libraries", "--run-library", "--run-libraries", dest="libraries", help="Process only specified libraries (comma-separated list)", type=str)
@ -40,19 +41,25 @@ parser.add_argument("-w", "--width", dest="width", help="Screen Width (Default:
args = parser.parse_args() args = parser.parse_args()
def get_arg(env_str, default, arg_bool=False, arg_int=False): def get_arg(env_str, default, arg_bool=False, arg_int=False):
env_var = os.environ.get(env_str) env_vars = [env_str] if not isinstance(env_str, list) else env_str
if env_var: final_value = None
for env_var in env_vars:
env_value = os.environ.get(env_var)
if env_value is not None:
final_value = env_value
break
if final_value is not None:
if arg_bool: if arg_bool:
if env_var is True or env_var is False: if final_value is True or final_value is False:
return env_var return final_value
elif env_var.lower() in ["t", "true"]: elif final_value.lower() in ["t", "true"]:
return True return True
else: else:
return False return False
elif arg_int: elif arg_int:
return int(env_var) return int(final_value)
else: else:
return str(env_var) return str(final_value)
else: else:
return default return default
@ -63,7 +70,8 @@ test = get_arg("PMM_TEST", args.test, arg_bool=True)
ignore_schedules = get_arg("PMM_IGNORE_SCHEDULES", args.ignore_schedules, arg_bool=True) ignore_schedules = get_arg("PMM_IGNORE_SCHEDULES", args.ignore_schedules, arg_bool=True)
ignore_ghost = get_arg("PMM_IGNORE_GHOST", args.ignore_ghost, arg_bool=True) ignore_ghost = get_arg("PMM_IGNORE_GHOST", args.ignore_ghost, arg_bool=True)
collection_only = get_arg("PMM_COLLECTIONS_ONLY", args.collection_only, arg_bool=True) collection_only = get_arg("PMM_COLLECTIONS_ONLY", args.collection_only, arg_bool=True)
library_only = get_arg("PMM_LIBRARIES_ONLY", args.library_only, arg_bool=True) operations_only = get_arg(["PMM_OPERATIONS", "PMM_LIBRARIES_ONLY"], args.operations, arg_bool=True)
overlays_only = get_arg(["PMM_OVERLAYS", "PMM_OVERLAYS_ONLY"], args.overlays, arg_bool=True)
library_first = get_arg("PMM_LIBRARIES_FIRST", args.library_first, arg_bool=True) library_first = get_arg("PMM_LIBRARIES_FIRST", args.library_first, arg_bool=True)
collections = get_arg("PMM_COLLECTIONS", args.collections) collections = get_arg("PMM_COLLECTIONS", args.collections)
libraries = get_arg("PMM_LIBRARIES", args.libraries) libraries = get_arg("PMM_LIBRARIES", args.libraries)
@ -152,7 +160,8 @@ def start(attrs):
logger.debug(f"--run (PMM_RUN): {run}") logger.debug(f"--run (PMM_RUN): {run}")
logger.debug(f"--run-tests (PMM_TEST): {test}") logger.debug(f"--run-tests (PMM_TEST): {test}")
logger.debug(f"--collections-only (PMM_COLLECTIONS_ONLY): {collection_only}") logger.debug(f"--collections-only (PMM_COLLECTIONS_ONLY): {collection_only}")
logger.debug(f"--libraries-only (PMM_LIBRARIES_ONLY): {library_only}") logger.debug(f"--operations (PMM_OPERATIONS): {operations_only}")
logger.debug(f"--overlays (PMM_OVERLAYS): {overlays_only}")
logger.debug(f"--libraries-first (PMM_LIBRARIES_FIRST): {library_first}") logger.debug(f"--libraries-first (PMM_LIBRARIES_FIRST): {library_first}")
logger.debug(f"--run-collections (PMM_COLLECTIONS): {collections}") logger.debug(f"--run-collections (PMM_COLLECTIONS): {collections}")
logger.debug(f"--run-libraries (PMM_LIBRARIES): {libraries}") logger.debug(f"--run-libraries (PMM_LIBRARIES): {libraries}")
@ -211,8 +220,11 @@ def update_libraries(config):
logger.info("") logger.info("")
logger.separator(f"{library.name} Library") logger.separator(f"{library.name} Library")
if config.library_first and library.library_operation and not config.test_mode and not collection_only: if config.library_first and not config.test_mode and not collection_only:
library.Operations.run_operations() if not overlays_only and library.library_operation:
library.Operations.run_operations()
if not operations_only and library.overlay_files or library.remove_overlays:
library.Overlays.run_overlays()
logger.debug("") logger.debug("")
logger.debug(f"Mapping Name: {library.original_mapping_name}") logger.debug(f"Mapping Name: {library.original_mapping_name}")
@ -247,7 +259,7 @@ def update_libraries(config):
for collection in library.get_all_collections(): for collection in library.get_all_collections():
logger.info(f"Collection {collection.title} Deleted") logger.info(f"Collection {collection.title} Deleted")
library.query(collection.delete) library.query(collection.delete)
if not library.is_other and not library.is_music and (library.metadata_files or library.original_mapping_name in config.library_map) and not library_only: if not library.is_other and not library.is_music and not operations_only and (library.metadata_files or library.overlay_files):
logger.info("") logger.info("")
logger.separator(f"Mapping {library.name} Library", space=False, border=False) logger.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("") logger.info("")
@ -271,15 +283,18 @@ def update_libraries(config):
logger.info("") logger.info("")
logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}") logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}")
continue continue
if collections_to_run and not library_only: if collections_to_run and not operations_only and not overlays_only:
logger.info("") logger.info("")
logger.separator(f"{'Test ' if config.test_mode else ''}Collections") logger.separator(f"{'Test ' if config.test_mode else ''}Collections")
logger.remove_library_handler(library.mapping_name) logger.remove_library_handler(library.mapping_name)
run_collection(config, library, metadata, collections_to_run) run_collection(config, library, metadata, collections_to_run)
logger.re_add_library_handler(library.mapping_name) logger.re_add_library_handler(library.mapping_name)
if not config.library_first and library.library_operation and not config.test_mode and not collection_only: if not config.library_first and not config.test_mode and not collection_only:
library.Operations.run_operations() if not overlays_only and library.library_operation:
library.Operations.run_operations()
if not operations_only and library.overlay_files or library.remove_overlays:
library.Overlays.run_overlays()
logger.remove_library_handler(library.mapping_name) logger.remove_library_handler(library.mapping_name)
except Exception as e: except Exception as e:
@ -301,7 +316,7 @@ def update_libraries(config):
break break
amount_added = 0 amount_added = 0
if has_run_again and not library_only: if has_run_again and not operations_only and not overlays_only:
logger.info("") logger.info("")
logger.separator("Run Again") logger.separator("Run Again")
logger.info("") logger.info("")

Loading…
Cancel
Save