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