diff --git a/VERSION b/VERSION
index f3656878..da54d471 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.17.0-develop24
+1.17.0-develop25
diff --git a/docs/config/plex.md b/docs/config/plex.md
index 913f8df4..80a767a6 100644
--- a/docs/config/plex.md
+++ b/docs/config/plex.md
@@ -24,6 +24,8 @@ plex:
| `empty_trash` | Runs Empty Trash on the Server after all Metadata Files are run | false | ❌ |
| `optimize` | Runs Optimize on the Server after all Metadata Files are run | false | ❌ |
+* **Do Not Use the Plex Token found in Plex's Preferences.xml file**
+
* This script can be run on a remote Plex server, but be sure that the `url` provided is publicly addressable, and it's recommended to use `HTTPS`.
* If you need help finding your Plex authentication token, please see Plex's [support article](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/).
diff --git a/docs/home/environmental.md b/docs/home/environmental.md
index 36d8d019..87eecbdb 100644
--- a/docs/home/environmental.md
+++ b/docs/home/environmental.md
@@ -106,7 +106,7 @@ Specify the time of day that Plex Meta Manager will run.
Default Value |
- 03:00 |
+ 05:00 |
Available Values |
diff --git a/docs/metadata/overlay.md b/docs/metadata/overlay.md
index 61ae9542..e61aaa24 100644
--- a/docs/metadata/overlay.md
+++ b/docs/metadata/overlay.md
@@ -16,6 +16,7 @@ These are the attributes which can be used within the Overlay File:
|:--------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------|
| [`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 |
+| [`queues`](#overlay-queues) | contains the positional attributes of queues |
| [`overlays`](#overlays-attributes) | contains definitions of overlays you wish to add |
* `overlays` is required in order to run the Overlay File.
@@ -66,29 +67,30 @@ overlays:
There are many attributes available when using overlays to edit how they work.
-| Attribute | Description | Required |
-|:--------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|
-| `name` | Name of the overlay. Each overlay name should be unique. | ✅ |
-| `file` | Local location of the Overlay Image. | ❌ |
-| `url` | URL of Overlay Image Online. | ❌ |
-| `git` | Location in the [Configs Repo](https://github.com/meisnate12/Plex-Meta-Manager-Configs) of the Overlay Image. | ❌ |
-| `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image. | ❌ |
-| `group` | Name of the Grouping for this overlay. Only one overlay with the highest weight per group will be applied.
**`weight` is required when using `group`**
**Values:** group name | ❌ |
-| `weight` | Weight of this overlay in its group.
**`group` is required when using `weight`**
**Values:** Integer | ❌ |
-| `horizontal_offset` | Horizontal Offset of this overlay. Can be a %.
**`vertical_offset` is required when using `horizontal_offset`**
**Value:** Integer 0 or greater or 0%-100% | ❌ |
-| `horizontal_align` | Horizontal Alignment of the overlay.
**Values:** `left`, `center`, `right` | ❌ |
-| `vertical_offset` | Vertical Offset of this overlay. Can be a %.
**`horizontal_offset` is required when using `vertical_offset`**
**Value:** Integer 0 or greater or 0%-100% | ❌ |
-| `vertical_align` | Vertical Alignment of the overlay.
**Values:** `top`, `center`, `bottom` | ❌ |
-| `font` | System Font Filename or path to font file for the Text Overlay.
**Value:** System Font Filename or path to font file | ❌ |
-| `font_size` | Font Size for the Text Overlay.
**Value:** Integer greater than 0 | ❌ |
-| `font_color` | Font Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ |
-| `back_color` | Backdrop Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ |
-| `back_width` | Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text
**`back_height` is required when using `back_width`**
**Value:** Integer greater than 0 | ❌ |
-| `back_height` | Backdrop Height for the Text Overlay. If `back_height` is not specified the Backdrop Sizes to the text
**`back_width` is required when using `back_height`**
**Value:** Integer greater than 0 | ❌ |
-| `back_padding` | Backdrop Padding for the Text Overlay.
**Value:** Integer greater than 0 | ❌ |
-| `back_radius` | Backdrop Radius for the Text Overlay.
**Value:** Integer greater than 0 | ❌ |
-| `back_line_color` | Backdrop Line Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ |
-| `back_line_width` | Backdrop Line Width for the Text Overlay.
**Value:** Integer greater than 0 | ❌ |
+| Attribute | Description | Required |
+|:---------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|
+| `name` | Name of the overlay. Each overlay name should be unique. | ✅ |
+| `file` | Local location of the Overlay Image. | ❌ |
+| `url` | URL of Overlay Image Online. | ❌ |
+| `git` | Location in the [Configs Repo](https://github.com/meisnate12/Plex-Meta-Manager-Configs) of the Overlay Image. | ❌ |
+| `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image. | ❌ |
+| [`group`](#overlay-groups) | Name of the Grouping for this overlay. Only one overlay with the highest weight per group will be applied.
**`weight` is required when using `group`**
**Values:** group name | ❌ |
+| [`queue`](#overlay-queues) | Name of the Queue for this overlay. Define `queue` positions using the `queues` attribute at the top level of an Overlay File. Overlay with the highest weight is applied to the first position and so on.
**`weight` is required when using `queue`**
**Values:** queue name | ❌ |
+| `weight` | Weight of this overlay in its group or queue.
**`group` or `queue` is required when using `weight`**
**Values:** Integer 0 or greater | ❌ |
+| `horizontal_offset` | Horizontal Offset of this overlay. Can be a %.
**`vertical_offset` is required when using `horizontal_offset`**
**Value:** Integer 0 or greater or 0%-100% | ❌ |
+| `horizontal_align` | Horizontal Alignment of the overlay.
**Values:** `left`, `center`, `right` | ❌ |
+| `vertical_offset` | Vertical Offset of this overlay. Can be a %.
**`horizontal_offset` is required when using `vertical_offset`**
**Value:** Integer 0 or greater or 0%-100% | ❌ |
+| `vertical_align` | Vertical Alignment of the overlay.
**Values:** `top`, `center`, `bottom` | ❌ |
+| `font` | System Font Filename or path to font file for the Text Overlay.
**Value:** System Font Filename or path to font file | ❌ |
+| `font_size` | Font Size for the Text Overlay.
**Value:** Integer greater than 0 | ❌ |
+| `font_color` | Font Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ |
+| `back_color` | Backdrop Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ |
+| `back_width` | Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text
**`back_height` is required when using `back_width`**
**Value:** Integer greater than 0 | ❌ |
+| `back_height` | Backdrop Height for the Text Overlay. If `back_height` is not specified the Backdrop Sizes to the text
**`back_width` is required when using `back_height`**
**Value:** Integer greater than 0 | ❌ |
+| `back_padding` | Backdrop Padding for the Text Overlay.
**Value:** Integer greater than 0 | ❌ |
+| `back_radius` | Backdrop Radius for the Text Overlay.
**Value:** Integer greater than 0 | ❌ |
+| `back_line_color` | Backdrop Line Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ |
+| `back_line_width` | Backdrop Line Width for the Text Overlay.
**Value:** Integer greater than 0 | ❌ |
* 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.
@@ -108,7 +110,7 @@ overlays:
imdb_chart: top_movies
overlay:
name: IMDB-Top-250
- repo: PMM/overlays/images/IMDB-Top-250
+ git: PMM/overlays/images/IMDB-Top-250
horizontal_offset: 0
horizontal_align: right
vertical_offset: 0
@@ -134,7 +136,6 @@ overlays:
![](blur.png)
-
### Text Overlay
You can add text as an overlay using the special `text()` overlay name. Anything inside the parentheses will be added as an overlay onto the image. Ex `text(4K)` adds `4K` to the image.
@@ -173,6 +174,89 @@ overlays:
back_height: 105
```
+### Overlay Groups
+
+Overlay groups are defined by the name given to the `group` attribute. Only one overlay with the highest weight per group will be applied.
+
+This is an example where the Multi-Audio overlay will be applied over the Dual-Audio overlay for every item found by both.
+
+```yaml
+overlays:
+ Dual-Audio:
+ overlay:
+ name: Dual-Audio
+ git: PMM/overlays/images/Dual-Audio
+ group: audio_language
+ weight: 10
+ horizontal_offset: 0
+ horizontal_align: center
+ vertical_offset: 15
+ vertical_align: bottom
+ plex_all: true
+ filters:
+ audio_language.count_gt: 1
+ Multi-Audio:
+ overlay:
+ name: Multi-Audio
+ git: PMM/overlays/images/Multi-Audio
+ group: audio_language
+ weight: 20
+ horizontal_offset: 0
+ horizontal_align: center
+ vertical_offset: 15
+ vertical_align: bottom
+ plex_all: true
+ filters:
+ audio_language.count_gt: 2
+```
+
+### Overlay Queues
+
+Overlay queues are defined by the name given to the `queue` attribute. The overlay with the highest weight is put into the first queue position, then the second highest is placed in the second queue position and so on.
+
+You can define the queue positions by using the `queues` attribute at the top level of an Overlay File. You can define as many positions as you want.
+
+```yaml
+queues:
+ custom_queue_name:
+ - horizontal_offset: 300 # This is the first position
+ horizontal_align: center
+ vertical_offset: 1375
+ vertical_align: top
+ - horizontal_offset: 300 # This is the second position
+ horizontal_align: center
+ vertical_offset: 1250
+ vertical_align: top
+
+overlays:
+ IMDb:
+ imdb_chart: popular_movies
+ overlay:
+ name: text(IMDb Popular)
+ queue: custom_queue_name
+ weight: 20
+ font: fonts/Inter-Medium.ttf
+ font_size: 65
+ font_color: "#FFFFFF"
+ back_color: "#00000099"
+ back_radius: 30
+ back_width: 380
+ back_height: 105
+ TMDb:
+ tmdb_popular: 100
+ overlay:
+ name: text(TMDb Popular)
+ queue: custom_queue_name
+ weight: 10
+ font: fonts/Inter-Medium.ttf
+ font_size: 65
+ font_color: "#FFFFFF"
+ back_color: "#00000099"
+ back_radius: 30
+ back_width: 400
+ back_height: 105
+```
+
## Suppress Overlays
You can add `suppress_overlays` to an overlay definition and give it a list or comma separated string of overlay names you want suppressed from this item if this overlay is attached to the item.
diff --git a/modules/builder.py b/modules/builder.py
index 53066a98..dcabdc31 100644
--- a/modules/builder.py
+++ b/modules/builder.py
@@ -2293,7 +2293,7 @@ class CollectionBuilder:
tmdb_paths = []
tvdb_paths = []
for item in self.items:
- if "item_assets" in self.item_details and self.library.asset_directory and "Overlay" not in [la.tag for la in item.labels]:
+ if "item_assets" in self.item_details and self.library.asset_directory and "Overlay" not in [la.tag for la in self.library.item_labels(item)]:
self.library.find_and_upload_assets(item)
self.library.edit_tags("label", item, add_tags=add_tags, remove_tags=remove_tags, sync_tags=sync_tags)
path = os.path.dirname(str(item.locations[0])) if self.library.is_movie else str(item.locations[0])
diff --git a/modules/operations.py b/modules/operations.py
index 83b8a690..42c6872e 100644
--- a/modules/operations.py
+++ b/modules/operations.py
@@ -79,7 +79,7 @@ class Operations:
logger.error(e)
continue
logger.ghost(f"Processing: {i}/{len(items)} {item.title}")
- current_labels = [la.tag for la in item.labels] if self.library.assets_for_all or self.library.mass_imdb_parental_labels else []
+ current_labels = [la.tag for la in self.library.item_labels(item)] if self.library.assets_for_all or self.library.mass_imdb_parental_labels else []
if self.library.assets_for_all and self.library.asset_directory and "Overlay" not in current_labels:
self.library.find_and_upload_assets(item)
diff --git a/modules/overlays.py b/modules/overlays.py
index a6dc2893..8c56ceb3 100644
--- a/modules/overlays.py
+++ b/modules/overlays.py
@@ -86,7 +86,7 @@ class Overlays:
image, image_compare, overlay_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, split="|")
- has_overlay = any([item_tag.tag.lower() == "overlay" for item_tag in item.labels])
+ has_overlay = any([item_tag.tag.lower() == "overlay" for item_tag in self.library.item_labels(item)])
compare_names = {properties[ov].get_overlay_compare(): ov for ov in over_names}
blur_num = 0
diff --git a/modules/plex.py b/modules/plex.py
index 805a8ea0..db49c5f8 100644
--- a/modules/plex.py
+++ b/modules/plex.py
@@ -529,6 +529,10 @@ class Plex(Library):
def collection_order_query(self, collection, data):
collection.sortUpdate(sort=data)
+ @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
+ def item_labels(self, item):
+ return item.labels
+
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def reload(self, item, force=False):
is_full = False
@@ -640,9 +644,10 @@ class Plex(Library):
def scan_user(server, username):
try:
for playlist in server.playlists():
- if playlist.title not in playlists:
- playlists[playlist.title] = []
- playlists[playlist.title].append(username)
+ if isinstance(playlist, Playlist):
+ if playlist.title not in playlists:
+ playlists[playlist.title] = []
+ playlists[playlist.title].append(username)
except requests.exceptions.ConnectionError:
pass
scan_user(self.PlexServer, self.account.title)
@@ -1244,7 +1249,7 @@ class Plex(Library):
if filter_attr == "has_collection":
filter_check = len(item.collections) > 0
elif filter_attr == "has_overlay":
- for label in item.labels:
+ for label in self.item_labels(item):
if label.tag.lower().endswith(" overlay") or label.tag.lower() == "overlay":
filter_check = True
break
diff --git a/modules/util.py b/modules/util.py
index 90cf0f6d..692e7264 100644
--- a/modules/util.py
+++ b/modules/util.py
@@ -945,7 +945,7 @@ class Overlay:
self.horizontal_align, self.horizontal_offset, self.vertical_align, self.vertical_offset = parse_cords(self.data, "overlay")
if (self.horizontal_offset is None and self.vertical_offset is not None) or (self.vertical_offset is None and self.horizontal_offset is not None):
- raise Failed(f"Overlay Error: overlay attribute's must be used together")
+ raise Failed(f"Overlay Error: overlay attribute's horizontal_offset and vertical_offset must be used together")
def color(attr):
if attr in self.data and self.data[attr]:
@@ -966,7 +966,7 @@ class Overlay:
elif back_width >= 0 and back_height >= 0:
self.back_box = (back_width, back_height)
self.has_back = True if self.back_color or self.back_line_color else False
- if self.has_back and not self.has_coordinates():
+ if self.has_back and not self.has_coordinates() and not self.queue:
raise Failed(f"Overlay Error: horizontal_offset and vertical_offset are required when using a backdrop")
def get_and_save_image(image_url):
diff --git a/plex_meta_manager.py b/plex_meta_manager.py
index 2345d5f5..39845269 100644
--- a/plex_meta_manager.py
+++ b/plex_meta_manager.py
@@ -1,4 +1,5 @@
-import argparse, os, sys, time, traceback, uuid
+import argparse, os, sys, time, uuid
+from concurrent.futures import ProcessPoolExecutor
from datetime import datetime
try:
@@ -18,7 +19,7 @@ parser = argparse.ArgumentParser()
parser.add_argument("-db", "--debug", dest="debug", help=argparse.SUPPRESS, action="store_true", default=False)
parser.add_argument("-tr", "--trace", dest="trace", help=argparse.SUPPRESS, action="store_true", default=False)
parser.add_argument("-c", "--config", dest="config", help="Run with desired *.yml file", type=str)
-parser.add_argument("-t", "--time", "--times", dest="times", help="Times to update each day use format HH:MM (Default: 03:00) (comma-separated list)", default="05:00", type=str)
+parser.add_argument("-t", "--time", "--times", dest="times", help="Times to update each day use format HH:MM (Default: 05:00) (comma-separated list)", default="05:00", type=str)
parser.add_argument("-re", "--resume", dest="resume", help="Resume collection run from a specific collection", type=str)
parser.add_argument("-r", "--run", dest="run", help="Run without the scheduler", action="store_true", default=False)
parser.add_argument("-is", "--ignore-schedules", dest="ignore_schedules", help="Run ignoring collection schedules", action="store_true", default=False)
@@ -115,8 +116,10 @@ from modules.config import ConfigFile
from modules.util import Failed, NotScheduled, Deleted
def my_except_hook(exctype, value, tb):
- for _line in traceback.format_exception(etype=exctype, value=value, tb=tb):
- logger.critical(_line)
+ if issubclass(exctype, KeyboardInterrupt):
+ sys.__excepthook__(exctype, value, tb)
+ else:
+ logger.critical("Uncaught Exception", exc_info=(exctype, value, tb))
sys.excepthook = my_except_hook
@@ -144,6 +147,10 @@ if not uuid_num:
plexapi.BASE_HEADERS["X-Plex-Client-Identifier"] = str(uuid_num)
+def process(attrs):
+ with ProcessPoolExecutor(max_workers=1) as executor:
+ executor.submit(start, *[attrs])
+
def start(attrs):
logger.add_main_handler()
logger.separator()
@@ -845,54 +852,55 @@ def run_playlists(config):
logger.remove_playlist_handler(playlist_log_name)
return status, stats
-try:
- if run or test or collections or libraries or metadata_files or resume:
- start({
- "config_file": config_file,
- "test": test,
- "delete": delete,
- "ignore_schedules": ignore_schedules,
- "collections": collections,
- "libraries": libraries,
- "metadata_files": metadata_files,
- "library_first": library_first,
- "resume": resume,
- "trace": trace
- })
- else:
- times_to_run = util.get_list(times)
- valid_times = []
- for time_to_run in times_to_run:
- try:
- valid_times.append(datetime.strftime(datetime.strptime(time_to_run, "%H:%M"), "%H:%M"))
- except ValueError:
- if time_to_run:
- raise Failed(f"Argument Error: time argument invalid: {time_to_run} must be in the HH:MM format between 00:00-23:59")
- else:
- raise Failed(f"Argument Error: blank time argument")
- for time_to_run in valid_times:
- schedule.every().day.at(time_to_run).do(start, {"config_file": config_file, "time": time_to_run, "delete": delete, "library_first": library_first, "trace": trace})
- while True:
- schedule.run_pending()
- if not no_countdown:
- current_time = datetime.now().strftime("%H:%M")
- seconds = None
- og_time_str = ""
- for time_to_run in valid_times:
- new_seconds = (datetime.strptime(time_to_run, "%H:%M") - datetime.strptime(current_time, "%H:%M")).total_seconds()
- if new_seconds < 0:
- new_seconds += 86400
- if (seconds is None or new_seconds < seconds) and new_seconds > 0:
- seconds = new_seconds
- og_time_str = time_to_run
- if seconds is not None:
- hours = int(seconds // 3600)
- minutes = int((seconds % 3600) // 60)
- time_str = f"{hours} Hour{'s' if hours > 1 else ''} and " if hours > 0 else ""
- time_str += f"{minutes} Minute{'s' if minutes > 1 else ''}"
- logger.ghost(f"Current Time: {current_time} | {time_str} until the next run at {og_time_str} | Runs: {', '.join(times_to_run)}")
- else:
- logger.error(f"Time Error: {valid_times}")
- time.sleep(60)
-except KeyboardInterrupt:
- logger.separator("Exiting Plex Meta Manager")
+if __name__ == "__main__":
+ try:
+ if run or test or collections or libraries or metadata_files or resume:
+ process({
+ "config_file": config_file,
+ "test": test,
+ "delete": delete,
+ "ignore_schedules": ignore_schedules,
+ "collections": collections,
+ "libraries": libraries,
+ "metadata_files": metadata_files,
+ "library_first": library_first,
+ "resume": resume,
+ "trace": trace
+ })
+ else:
+ times_to_run = util.get_list(times)
+ valid_times = []
+ for time_to_run in times_to_run:
+ try:
+ valid_times.append(datetime.strftime(datetime.strptime(time_to_run, "%H:%M"), "%H:%M"))
+ except ValueError:
+ if time_to_run:
+ raise Failed(f"Argument Error: time argument invalid: {time_to_run} must be in the HH:MM format between 00:00-23:59")
+ else:
+ raise Failed(f"Argument Error: blank time argument")
+ for time_to_run in valid_times:
+ schedule.every().day.at(time_to_run).do(process, {"config_file": config_file, "time": time_to_run, "delete": delete, "library_first": library_first, "trace": trace})
+ while True:
+ schedule.run_pending()
+ if not no_countdown:
+ current_time = datetime.now().strftime("%H:%M")
+ seconds = None
+ og_time_str = ""
+ for time_to_run in valid_times:
+ new_seconds = (datetime.strptime(time_to_run, "%H:%M") - datetime.strptime(current_time, "%H:%M")).total_seconds()
+ if new_seconds < 0:
+ new_seconds += 86400
+ if (seconds is None or new_seconds < seconds) and new_seconds > 0:
+ seconds = new_seconds
+ og_time_str = time_to_run
+ if seconds is not None:
+ hours = int(seconds // 3600)
+ minutes = int((seconds % 3600) // 60)
+ time_str = f"{hours} Hour{'s' if hours > 1 else ''} and " if hours > 0 else ""
+ time_str += f"{minutes} Minute{'s' if minutes > 1 else ''}"
+ logger.ghost(f"Current Time: {current_time} | {time_str} until the next run at {og_time_str} | Runs: {', '.join(times_to_run)}")
+ else:
+ logger.error(f"Time Error: {valid_times}")
+ time.sleep(60)
+ except KeyboardInterrupt:
+ logger.separator("Exiting Plex Meta Manager")