[191] added delete_collections operation

pull/1165/head
meisnate12 2 years ago
parent d14e9f461a
commit becd838eae

@ -1 +1 @@
1.17.3-develop190
1.17.3-develop191

@ -50,7 +50,7 @@
{% elif subsublink|length == 5 %}
<a tabindex="-1" href="{{ pathto(subsublink[2]) }}{{ subsublink[3] }}">{{ subsublink[1] }}</a>
{% else %}
<a tabindex="-1" href="#">{{ subsubsublink[1] }}</a>
<a tabindex="-1" href="#">{{ subsublink[1] }}</a>
{% endif %}
<ul class="dropdown-menu">
{%- for subsubsublink in subsublink[-1] %}

@ -19,8 +19,7 @@ The available attributes for the operations attribute are as follows
| Attribute | Description |
|:----------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Assets For All](#assets-for-all) | Search in assets for images for every item in your library. |
| [Delete Collections With Less](#delete-collections-with-less) | Deletes every collection with less than the given number of items. |
| [Delete Unmanaged Collections](#delete-unmanaged-collections) | Deletes every unmanaged collection. |
| [Delete Collections](#delete-collections) | Deletes collections based on a set of given attributes. |
| [Mass Genre Update](#mass-genre-update) | Updates every item's genres in the library to the chosen site's genres. |
| [Mass Content Rating Update](#mass-content-rating-update) | Updates every item's content rating in the library to the chosen site's content rating. |
| [Mass Original Title Update](#mass-original-title-update) | Updates every item's original title in the library to the chosen site's original title. |
@ -50,21 +49,34 @@ Search in assets for images for every item in your library.
**Values:** `true` or `false`
## Delete Collections With Less
## Delete Collections
Deletes every collection with less than the given number of items.
Deletes collections based on a set of given attributes. The Collection must match all set attributes to be deleted.
**Attribute:** `delete_collections_with_less`
**Attribute:** `delete_collections`
**Values:** number greater than 0
**Values:** There are a few different options to determine how the `delete_collections` works.
## Delete Unmanaged Collections
| Attribute | Description |
|:---------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|
| `managed` | Matches with a Collection Managed by PMM (the collection has the `PMM` label).<br>**Default:** `false`<br>**Values:** `true` or `false` |
| `unmanaged` | Matches with a Collection Unmanaged by PMM (the collection does not have the `PMM` label).<br>**Default:** `false`<br>**Values:** `true` or `false` |
| `configured` | Matches with a Collection Configured in the specific PMM run.<br>**Default:** `false`<br>**Values:** `true` or `false` |
| `unconfigured` | Matches with a Collection Not Configured in the specific PMM run.<br>**Default:** `false`<br>**Values:** `true` or `false` |
| `less` | Matches with a Collection that contains less then the given number of items.<br>**Default:** ` `<br>**Values:** Number Greater then 0 |
Deletes every collection that doesn't have the PMM label.
**Example:**
**Attribute:** `delete_unmanaged_collections`
Removes all Managed Collections (Collections with the `PMM` Label) that are not configured in the Current Run.
**Values:** `true` or `false`
```yaml
library:
Movies:
operations:
delete_collections:
unconfigured: true
managed: true
```
## Mass Genre Update

@ -61,7 +61,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -58,7 +58,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -32,7 +32,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -23,7 +23,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -37,7 +37,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) except `horizontal_offset`, `horizontal_align`, `vertical_offset`, and `vertical_align` are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) except `horizontal_offset`, `horizontal_align`, `vertical_offset`, and `vertical_align` are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------------:|

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 553 KiB

@ -41,7 +41,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -6,7 +6,7 @@ The `languages` Default Overlay File is used to create an overlay of a flag and
**Designed for [TRaSH Guides](https://trash-guides.info/) filename naming scheme.**
![](images/languages.png)
![](images/language.png)
## Supported Audio/Subtitle Language Flags
@ -78,13 +78,13 @@ The `languages` Default Overlay File is used to create an overlay of a flag and
Below is a screenshot of the alternative Square (`square`) style which can be set via the `style` template variable.
![](../images/languages2.png)
![](images/language2.png)
#### Half Style
Below is a screenshot of the alternative Half (`half`) style which can be set via the `style` template variable.
![](../images/languages3.png)
![](images/language3.png)
## Config
@ -112,7 +112,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:----------------------------:|

@ -23,7 +23,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -60,9 +60,9 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables.md) can be appended by `rating1_`, `rating2_`, or `rating3_` to change that attribute on each rating individually.
All [Shared Overlay Variables](../overlay_variables) can be appended by `rating1_`, `rating2_`, or `rating3_` to change that attribute on each rating individually.
| Variable | Default |
|:--------------------|:-----------:|

@ -66,7 +66,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -36,7 +36,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:--------:|

@ -26,7 +26,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -35,7 +35,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -48,7 +48,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -32,7 +32,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:----------------:|

@ -45,7 +45,7 @@ Template Variables can be used to manipulate the file in various ways to slightl
Note that the `templates_variables:` section only needs to be used if you do want to actually change how the defaults work. Any value not specified is its default value if it has one if not it's just ignored.
All [Shared Overlay Variables](../overlay_variables.md) are available with the default values below as well as the additional Variables below which can be used to customize the file.
All [Shared Overlay Variables](../overlay_variables) are available with the default values below as well as the additional Variables below which can be used to customize the file.
| Variable | Default |
|:--------------------|:-----------:|

@ -380,4 +380,4 @@ docker run -d \
That will create a container that will run in the background until you explicitly stop it, surviving reboots, and waking up every morning at 3AM to process collections.
There are of course [other flags you can add](../environmental.md), but this is the minimal command to create this container.
There are of course [other flags you can add](../environmental), but this is the minimal command to create this container.

@ -34,7 +34,7 @@ Steps.
For example: `/tini -s python3 plex_meta_manager.py -- --run`
Information on available command line argument can be found [here](../environmental.md)
Information on available command line argument can be found [here](../environmental)
Click "Advanced Settings >>"
@ -44,7 +44,7 @@ Steps.
3. Environment Variables can be added here:
Information on available Environment Variables can be found [here](../environmental.md)
Information on available Environment Variables can be found [here](../environmental)
![](qnap/qnap5.png)

@ -61,7 +61,7 @@ This is a quick walkthrough of setting up the Plex-Meta-Manager Docker container
- To add an Environment Variable Click "Add".
- To use Command line arguments put the arguments in the "Command" text field.
Information on available command line argument and Environment Variables can be found [here](../environmental.md)
Information on available command line argument and Environment Variables can be found [here](../environmental)
![](synology/synology-10.png)

@ -5,7 +5,7 @@ To install a container from docker hub, you will need community applications - a
## Basic Installation
1. Head to the `Apps` tab of unRAID (Community Applications), and search `plex-meta-manager` in the upper right search box. There will be a couple of results shown, but you should ignore them ([Why?](images.md)) and use the official image, which is on DockerHub. Click `Click Here To Get More Results From DockerHub`.
1. Head to the `Apps` tab of unRAID (Community Applications), and search `plex-meta-manager` in the upper right search box. There will be a couple of results shown, but you should ignore them ([Why?](images)) and use the official image, which is on DockerHub. Click `Click Here To Get More Results From DockerHub`.
2. Click the download icon on the `plex meta manager` container by `meisnate12`.

@ -466,7 +466,7 @@ class CollectionBuilder:
suffix = ""
if self.details["delete_not_scheduled"]:
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, force_search=True)
logger.info(self.delete())
self.deleted = True
suffix = f" and was deleted"
@ -790,7 +790,7 @@ class CollectionBuilder:
or (self.library.Sonarr and self.sonarr_details["add_missing"]))
if self.build_collection:
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, force_search=True)
if (self.smart and not self.obj.smart) or (not self.smart and self.obj.smart):
logger.info("")
logger.error(f"{self.Type} Error: Converting {self.obj.title} to a {'smart' if self.smart else 'normal'} collection")
@ -2518,7 +2518,7 @@ class CollectionBuilder:
self.library.create_smart_collection(self.name, smart_type, self.smart_url, self.ignore_blank_results)
except Failed:
raise Failed(f"{self.Type} Error: Label: {self.name} was not added to any items in the Library")
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, force_search=True)
if not self.exists:
self.created = True
@ -2791,7 +2791,7 @@ class CollectionBuilder:
logger.error(f"Webhooks Error: {e}")
def run_collections_again(self):
self.obj = self.library.get_collection(self.name)
self.obj = self.library.get_collection(self.name, force_search=True)
name, collection_items = self.library.get_collection_name_and_items(self.obj, self.smart_label_collection)
self.created = False
rating_keys = []

@ -86,6 +86,18 @@ mass_rating_options = {
"mal": "Use MyAnimeList Rating"
}
reset_overlay_options = {"tmdb": "Reset to TMDb poster", "plex": "Reset to Plex Poster"}
library_operations = {
"assets_for_all": "bool", "split_duplicates": "bool", "update_blank_track_titles": "bool", "remove_title_parentheses": "bool",
"radarr_add_all_existing": "bool", "radarr_remove_by_tag": "bool", "sonarr_add_all_existing": "bool", "sonarr_remove_by_tag": "bool",
"mass_genre_update": mass_genre_options, "mass_content_rating_update": mass_content_options,
"mass_audience_rating_update": mass_rating_options, "mass_episode_audience_rating_update": mass_episode_rating_options,
"mass_critic_rating_update": mass_rating_options, "mass_episode_critic_rating_update": mass_episode_rating_options,
"mass_user_rating_update": mass_rating_options, "mass_episode_user_rating_update": mass_episode_rating_options,
"mass_original_title_update": mass_original_title_options, "mass_originally_available_update": mass_available_options,
"mass_imdb_parental_labels": imdb_label_options, "mass_poster_update": mass_image_options, "mass_background_update": mass_image_options,
"mass_collection_mode": "mass_collection_mode", "metadata_backup": "metadata_backup", "delete_collections": "delete_collections",
"genre_mapper": "mapper", "content_rating_mapper": "mapper",
}
class ConfigFile:
def __init__(self, default_dir, attrs):
@ -342,6 +354,7 @@ class ConfigFile:
"missing_only_released": check_for_attribute(self.data, "missing_only_released", parent="settings", var_type="bool", default=False),
"only_filter_missing": check_for_attribute(self.data, "only_filter_missing", parent="settings", var_type="bool", default=False),
"show_unmanaged": check_for_attribute(self.data, "show_unmanaged", parent="settings", var_type="bool", default=True),
"show_unconfigured": check_for_attribute(self.data, "show_unconfigured", parent="settings", var_type="bool", default=True),
"show_filtered": check_for_attribute(self.data, "show_filtered", parent="settings", var_type="bool", default=False),
"show_options": check_for_attribute(self.data, "show_options", parent="settings", var_type="bool", default=False),
"show_missing": check_for_attribute(self.data, "show_missing", parent="settings", var_type="bool", default=True),
@ -634,28 +647,9 @@ class ConfigFile:
for library_name, lib in libs.items():
if self.requested_libraries and library_name not in self.requested_libraries:
continue
params = {
"mapping_name": str(library_name),
"name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name),
"genre_mapper": None,
"content_rating_mapper": None,
"radarr_remove_by_tag": None,
"sonarr_remove_by_tag": None,
"mass_collection_mode": None,
"metadata_backup": None,
"update_blank_track_titles": None,
"mass_content_rating_update": None,
"mass_original_title_update": None,
"mass_originally_available_update": None,
"mass_imdb_parental_labels": None,
"remove_title_parentheses": None,
"mass_user_rating_update": None,
"mass_episode_audience_rating_update": None,
"mass_episode_critic_rating_update": None,
"mass_episode_user_rating_update": None,
"mass_poster_update": None,
"mass_background_update": None,
}
params = {o: None for o in library_operations}
params["mapping_name"] = str(library_name)
params["name"] = str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name)
display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"]
logger.separator(f"{display_name} Configuration")
@ -668,6 +662,7 @@ class ConfigFile:
params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False)
params["default_collection_order"] = check_for_attribute(lib, "default_collection_order", parent="settings", default=self.general["default_collection_order"], default_is_none=True, do_print=False, save=False)
params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False)
params["show_unconfigured"] = check_for_attribute(lib, "show_unconfigured", parent="settings", var_type="bool", default=self.general["show_unconfigured"], do_print=False, save=False)
params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False)
params["show_options"] = check_for_attribute(lib, "show_options", parent="settings", var_type="bool", default=self.general["show_options"], do_print=False, save=False)
params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False)
@ -686,131 +681,68 @@ class ConfigFile:
params["item_refresh_delay"] = check_for_attribute(lib, "item_refresh_delay", parent="settings", var_type="int", default=self.general["item_refresh_delay"], do_print=False, save=False)
params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False)
params["delete_not_scheduled"] = check_for_attribute(lib, "delete_not_scheduled", parent="settings", var_type="bool", default=self.general["delete_not_scheduled"], do_print=False, save=False)
params["delete_unmanaged_collections"] = check_for_attribute(lib, "delete_unmanaged_collections", parent="settings", var_type="bool", default=False, do_print=False, save=False)
params["delete_collections_with_less"] = check_for_attribute(lib, "delete_collections_with_less", parent="settings", var_type="int", default_is_none=True, do_print=False, save=False)
params["ignore_ids"] = check_for_attribute(lib, "ignore_ids", parent="settings", var_type="int_list", default_is_none=True, do_print=False, save=False)
params["ignore_ids"].extend([i for i in self.general["ignore_ids"] if i not in params["ignore_ids"]])
params["ignore_imdb_ids"] = check_for_attribute(lib, "ignore_imdb_ids", parent="settings", var_type="list", default_is_none=True, do_print=False, save=False)
params["ignore_imdb_ids"].extend([i for i in self.general["ignore_imdb_ids"] if i not in params["ignore_imdb_ids"]])
params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False, default_is_none=True)
params["changes_webhooks"] = check_for_attribute(lib, "changes", parent="webhooks", var_type="list", default=self.webhooks["changes"], do_print=False, save=False, default_is_none=True)
params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False)
params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_genre_options, default_is_none=True, save=False, do_print=False)
params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_rating_options, default_is_none=True, save=False, do_print=False)
params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_rating_options, default_is_none=True, save=False, do_print=False)
params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=False)
params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=False)
params["radarr_add_all_existing"] = check_for_attribute(lib, "radarr_add_all_existing", var_type="bool", default=False, save=False, do_print=False)
params["sonarr_add_all_existing"] = check_for_attribute(lib, "sonarr_add_all_existing", var_type="bool", default=False, save=False, do_print=False)
params["report_path"] = None
if lib and "report_path" in lib and lib["report_path"]:
if os.path.exists(os.path.dirname(os.path.abspath(lib["report_path"]))):
params["report_path"] = lib["report_path"]
else:
logger.error(f"Config Error: Folder {os.path.dirname(os.path.abspath(lib['report_path']))} does not exist")
if lib and "operations" in lib and lib["operations"]:
if isinstance(lib["operations"], dict):
if "assets_for_all" in lib["operations"]:
params["assets_for_all"] = check_for_attribute(lib["operations"], "assets_for_all", var_type="bool", default=False, save=False)
if "delete_unmanaged_collections" in lib["operations"]:
params["delete_unmanaged_collections"] = check_for_attribute(lib["operations"], "delete_unmanaged_collections", var_type="bool", default=False, save=False)
if "delete_collections_with_less" in lib["operations"]:
params["delete_collections_with_less"] = check_for_attribute(lib["operations"], "delete_collections_with_less", var_type="int", default_is_none=True, save=False)
if "mass_genre_update" in lib["operations"]:
params["mass_genre_update"] = check_for_attribute(lib["operations"], "mass_genre_update", test_list=mass_genre_options, default_is_none=True, save=False)
if "mass_audience_rating_update" in lib["operations"]:
params["mass_audience_rating_update"] = check_for_attribute(lib["operations"], "mass_audience_rating_update", test_list=mass_rating_options, default_is_none=True, save=False)
if "mass_critic_rating_update" in lib["operations"]:
params["mass_critic_rating_update"] = check_for_attribute(lib["operations"], "mass_critic_rating_update", test_list=mass_rating_options, default_is_none=True, save=False)
if "mass_user_rating_update" in lib["operations"]:
params["mass_user_rating_update"] = check_for_attribute(lib["operations"], "mass_user_rating_update", test_list=mass_rating_options, default_is_none=True, save=False)
if "mass_episode_audience_rating_update" in lib["operations"]:
params["mass_episode_audience_rating_update"] = check_for_attribute(lib["operations"], "mass_episode_audience_rating_update", test_list=mass_episode_rating_options, default_is_none=True, save=False)
if "mass_episode_critic_rating_update" in lib["operations"]:
params["mass_episode_critic_rating_update"] = check_for_attribute(lib["operations"], "mass_episode_critic_rating_update", test_list=mass_episode_rating_options, default_is_none=True, save=False)
if "mass_episode_user_rating_update" in lib["operations"]:
params["mass_episode_user_rating_update"] = check_for_attribute(lib["operations"], "mass_episode_user_rating_update", test_list=mass_episode_rating_options, default_is_none=True, save=False)
if "mass_content_rating_update" in lib["operations"]:
params["mass_content_rating_update"] = check_for_attribute(lib["operations"], "mass_content_rating_update", test_list=mass_content_options, default_is_none=True, save=False)
if "mass_original_title_update" in lib["operations"]:
params["mass_original_title_update"] = check_for_attribute(lib["operations"], "mass_original_title_update", test_list=mass_original_title_options, default_is_none=True, save=False)
if "mass_originally_available_update" in lib["operations"]:
params["mass_originally_available_update"] = check_for_attribute(lib["operations"], "mass_originally_available_update", test_list=mass_available_options, default_is_none=True, save=False)
if "mass_imdb_parental_labels" in lib["operations"]:
params["mass_imdb_parental_labels"] = check_for_attribute(lib["operations"], "mass_imdb_parental_labels", test_list=imdb_label_options, default_is_none=True, save=False)
if "mass_poster_update" in lib["operations"]:
params["mass_poster_update"] = check_for_attribute(lib["operations"], "mass_poster_update", test_list=mass_image_options, default_is_none=True, save=False)
if "mass_background_update" in lib["operations"]:
params["mass_background_update"] = check_for_attribute(lib["operations"], "mass_background_update", test_list=mass_image_options, default_is_none=True, save=False)
if "mass_trakt_rating_update" in lib["operations"]:
params["mass_trakt_rating_update"] = check_for_attribute(lib["operations"], "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=False)
if "split_duplicates" in lib["operations"]:
params["split_duplicates"] = check_for_attribute(lib["operations"], "split_duplicates", var_type="bool", default=False, save=False)
if "radarr_add_all_existing" in lib["operations"]:
params["radarr_add_all_existing"] = check_for_attribute(lib["operations"], "radarr_add_all_existing", var_type="bool", default=False, save=False)
if "radarr_remove_by_tag" in lib["operations"]:
params["radarr_remove_by_tag"] = check_for_attribute(lib["operations"], "radarr_remove_by_tag", var_type="comma_list", default=False, save=False)
if "sonarr_add_all_existing" in lib["operations"]:
params["sonarr_add_all_existing"] = check_for_attribute(lib["operations"], "sonarr_add_all_existing", var_type="bool", default=False, save=False)
if "sonarr_remove_by_tag" in lib["operations"]:
params["sonarr_remove_by_tag"] = check_for_attribute(lib["operations"], "sonarr_remove_by_tag", var_type="comma_list", default=False, save=False)
if "update_blank_track_titles" in lib["operations"]:
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"]:
params["remove_title_parentheses"] = check_for_attribute(lib["operations"], "remove_title_parentheses", var_type="bool", default=False, save=False)
if "mass_collection_mode" in lib["operations"]:
try:
params["mass_collection_mode"] = util.check_collection_mode(lib["operations"]["mass_collection_mode"])
except Failed as e:
logger.error(e)
if "metadata_backup" in lib["operations"]:
params["metadata_backup"] = {
"path": os.path.join(default_dir, f"{str(library_name)}_Metadata_Backup.yml"),
"exclude": [],
"sync_tags": False,
"add_blank_entries": True
}
if lib["operations"]["metadata_backup"] and isinstance(lib["operations"]["metadata_backup"], dict):
try:
params["metadata_backup"]["path"] = check_for_attribute(lib["operations"]["metadata_backup"], "path", var_type="path", save=False)
except Failed as e:
logger.debug(f"{e} using default {params['metadata_backup']['path']}")
params["metadata_backup"]["exclude"] = check_for_attribute(lib["operations"]["metadata_backup"], "exclude", var_type="comma_list", default_is_none=True, save=False)
params["metadata_backup"]["sync_tags"] = check_for_attribute(lib["operations"]["metadata_backup"], "sync_tags", var_type="bool", default=False, save=False)
params["metadata_backup"]["add_blank_entries"] = check_for_attribute(lib["operations"]["metadata_backup"], "add_blank_entries", var_type="bool", default=True, save=False)
if "genre_mapper" in lib["operations"]:
if lib["operations"]["genre_mapper"] and isinstance(lib["operations"]["genre_mapper"], dict):
params["genre_mapper"] = lib["operations"]["genre_mapper"]
for old_genre, new_genre in lib["operations"]["genre_mapper"].items():
if old_genre == new_genre:
logger.error("Config Error: genres cannot be mapped to themselves")
else:
params["genre_mapper"][old_genre] = new_genre if new_genre else None
else:
logger.error("Config Error: genre_mapper is blank")
if "content_rating_mapper" in lib["operations"]:
if lib["operations"]["content_rating_mapper"] and isinstance(lib["operations"]["content_rating_mapper"], dict):
params["content_rating_mapper"] = lib["operations"]["content_rating_mapper"]
for old_content, new_content in lib["operations"]["content_rating_mapper"].items():
if old_content == new_content:
logger.error("Config Error: content rating cannot be mapped to themselves")
else:
params["content_rating_mapper"][old_content] = new_content if new_content else None
if "delete_collections" not in lib["operations"] and ("delete_unmanaged_collections" in lib["operations"] or "delete_collections_with_less" in lib["operations"]):
lib["operations"]["delete_collections"] = {}
if "delete_unmanaged_collections" in lib["operations"]:
lib["operations"]["delete_collections"]["unmanaged"] = check_for_attribute(lib["operations"], "delete_unmanaged_collections", var_type="bool", default=False, save=False)
if "delete_collections_with_less" in lib["operations"]:
lib["operations"]["delete_collections"]["less"] = check_for_attribute(lib["operations"], "delete_collections_with_less", var_type="int", default_is_none=True, save=False)
for op, data_type in library_operations.items():
if op not in lib["operations"]:
continue
elif isinstance(data_type, list):
params[op] = check_for_attribute(lib["operations"], op, test_list=data_type, default_is_none=True, save=False)
elif data_type == "mass_collection_mode":
params[op] = util.check_collection_mode(lib["operations"][op])
elif data_type in ["metadata_backup", "mapper", "delete_collections"]:
if not lib["operations"][op] or not isinstance(lib["operations"][op], dict):
raise Failed(f"Config Error: {op} must be a dictionary")
if data_type == "metadata_backup":
default_path = os.path.join(default_dir, f"{str(library_name)}_Metadata_Backup.yml")
try:
default_path = check_for_attribute(lib["operations"][op], "path", var_type="path", save=False)
except Failed as e:
logger.debug(f"{e} using default {default_path}")
params[op] = {
"path": default_path,
"exclude": check_for_attribute(lib["operations"][op], "exclude", var_type="comma_list", default_is_none=True, save=False),
"sync_tags": check_for_attribute(lib["operations"][op], "sync_tags", var_type="bool", default=False, save=False),
"add_blank_entries": check_for_attribute(lib["operations"][op], "add_blank_entries", var_type="bool", default=True, save=False)
}
if data_type == "mapper":
params[op] = lib["operations"][op]
for old_value, new_value in lib["operations"][op].items():
if old_value == new_value:
logger.warning(f"Config Warning: {op} value '{new_value}' ignored as it cannot be mapped to itself")
else:
params[op][old_value] = new_value if new_value else None
if data_type == "delete_collections":
params[op] = {
"managed": check_for_attribute(lib["operations"][op], "managed", var_type="bool", default=False, save=False),
"unmanaged": check_for_attribute(lib["operations"][op], "unmanaged", var_type="bool", default=False, save=False),
"configured": check_for_attribute(lib["operations"][op], "configured", var_type="bool", default=False, save=False),
"unconfigured": check_for_attribute(lib["operations"][op], "unconfigured", var_type="bool", default=False, save=False),
"less": check_for_attribute(lib["operations"][op], "less", var_type="int", default_is_none=True, save=False, int_min=1),
}
else:
logger.error("Config Error: content_rating_mapper is blank")
for atr in ["tmdb_collections", "genre_collections"]:
if atr in lib["operations"]:
logger.error(f"Deprecated Error: {atr} has been replaced with dynamic collections")
params[op] = check_for_attribute(lib["operations"], op, var_type=data_type, default=False, save=False)
else:
logger.error("Config Error: operations must be a dictionary")
if params["mass_trakt_rating_update"]:
if params["mass_user_rating_update"]:
logger.error("Config Error: User Rating is already being set by mass_user_rating_update")
else:
params["mass_user_rating_update"] = "trakt_user"
def error_check(attr, service):
logger.error(f"Config Error: Operation {attr} cannot be {params[attr]} without a successful {service} Connection")
params[attr] = None

@ -65,6 +65,7 @@ class Library(ABC):
self.delete_not_scheduled = params["delete_not_scheduled"]
self.missing_only_released = params["missing_only_released"]
self.show_unmanaged = params["show_unmanaged"]
self.show_unconfigured = params["show_unconfigured"]
self.show_filtered = params["show_filtered"]
self.show_options = params["show_options"]
self.show_missing = params["show_missing"]
@ -75,7 +76,10 @@ class Library(ABC):
self.ignore_imdb_ids = params["ignore_imdb_ids"]
self.assets_for_all = params["assets_for_all"]
self.delete_unmanaged_collections = params["delete_unmanaged_collections"]
self.delete_unconfigured_collections = params["delete_unconfigured_collections"]
self.delete_managed_unconfigured_collections = params["delete_managed_unconfigured_collections"]
self.delete_collections_with_less = params["delete_collections_with_less"]
self.delete_collections = params["delete_collections"]
self.mass_genre_update = params["mass_genre_update"]
self.mass_audience_rating_update = params["mass_audience_rating_update"]
self.mass_critic_rating_update = params["mass_critic_rating_update"]
@ -117,9 +121,9 @@ class Library(ABC):
or self.mass_content_rating_update or self.mass_originally_available_update or self.mass_original_title_update\
or self.mass_imdb_parental_labels or self.genre_mapper or self.content_rating_mapper \
or self.radarr_add_all_existing or self.sonarr_add_all_existing or self.mass_poster_update or self.mass_background_update else False
self.library_operation = True if self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \
or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \
or self.show_unmanaged or self.metadata_backup or self.update_blank_track_titles else False
self.library_operation = True if self.items_library_operation or self.delete_collections or self.mass_collection_mode \
or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.show_unmanaged or self.show_unconfigured \
or self.metadata_backup or self.update_blank_track_titles else False
self.meta_operations = [i for i in [getattr(self, o) for o in operations.meta_operations] if i]
if self.asset_directory:

@ -23,8 +23,7 @@ class Operations:
logger.separator(f"{self.library.name} Library Operations")
logger.info("")
logger.debug(f"Assets For All: {self.library.assets_for_all}")
logger.debug(f"Delete Collections With Less: {self.library.delete_collections_with_less}")
logger.debug(f"Delete Unmanaged Collections: {self.library.delete_unmanaged_collections}")
logger.debug(f"Delete Collections: {self.library.delete_collections}")
logger.debug(f"Show Unmanaged Collections: {self.library.show_unmanaged}")
logger.debug(f"Mass Genre Update: {self.library.mass_genre_update}")
logger.debug(f"Mass Audience Rating Update: {self.library.mass_audience_rating_update}")
@ -577,30 +576,41 @@ class Operations:
if self.library.sonarr_remove_by_tag:
self.library.Sonarr.remove_all_with_tags(self.library.sonarr_remove_by_tag)
if self.library.delete_collections_with_less is not None or self.library.delete_unmanaged_collections:
less = None
managed = False
unmanaged = False
configured = False
unconfigured = False
if self.library.delete_collections:
logger.info("")
print_suffix = ""
unmanaged = ""
if self.library.delete_collections_with_less is not None and self.library.delete_collections_with_less > 0:
print_suffix = f" with less then {self.library.delete_collections_with_less} item{'s' if self.library.delete_collections_with_less > 1 else ''}"
if self.library.delete_unmanaged_collections:
if self.library.delete_collections_with_less is None:
unmanaged = "Unmanaged Collections "
elif self.library.delete_collections_with_less > 0:
unmanaged = "Unmanaged Collections and "
logger.separator(f"Deleting All {unmanaged}Collections{print_suffix}", space=False, border=False)
logger.separator(f"Deleting All Collections", space=False, border=False)
logger.info("")
if self.library.delete_collections["less"] is not None:
less = self.library.delete_collections["less"]
managed = self.library.delete_collections["managed"]
unmanaged = self.library.delete_collections["unmanaged"]
configured = self.library.delete_collections["configured"]
unconfigured = self.library.delete_collections["unconfigured"]
unmanaged_collections = []
unconfigured_collections = []
all_collections = self.library.get_all_collections()
for i, col in enumerate(all_collections, 1):
logger.ghost(f"Reading Collection: {i}/{len(all_collections)} {col.title}")
labels = [la.tag for la in self.library.item_labels(col)]
if (self.library.delete_collections_with_less and col.childCount < self.library.delete_collections_with_less) \
or (self.library.delete_unmanaged_collections and "PMM" not in labels):
if (less is not None or unmanaged or managed or unconfigured or configured) \
and (less is None or col.childCount < less) \
and (unmanaged is False or "PMM" not in labels) \
and (managed is False or "PMM" in labels) \
and (unconfigured is False or col.title not in self.library.collections) \
and (configured is False or col.title in self.library.collections):
self.library.query(col.delete)
logger.info(f"{col.title} Deleted")
elif "PMM" not in labels:
unmanaged_collections.append(col)
else:
if "PMM" not in labels:
unmanaged_collections.append(col)
if col.title not in self.library.collections:
unconfigured_collections.append(col)
if self.library.show_unmanaged and len(unmanaged_collections) > 0:
logger.info("")
@ -615,11 +625,24 @@ class Operations:
logger.separator(f"No Unmanaged Collections in {self.library.name} Library", space=False, border=False)
logger.info("")
if self.library.assets_for_all and len(unmanaged_collections) > 0:
if self.library.show_unconfigured and len(unconfigured_collections) > 0:
logger.info("")
logger.separator(f"Unmanaged Collection Assets Check for {self.library.name} Library", space=False, border=False)
logger.separator(f"Unconfigured Collections in {self.library.name} Library", space=False, border=False)
logger.info("")
for col in unmanaged_collections:
for col in unconfigured_collections:
logger.info(col.title)
logger.info("")
logger.info(f"{len(unconfigured_collections)} Unconfigured Collection{'s' if len(unconfigured_collections) > 1 else ''}")
elif self.library.show_unconfigured:
logger.info("")
logger.separator(f"No Unconfigured Collections in {self.library.name} Library", space=False, border=False)
logger.info("")
if self.library.assets_for_all and len(unconfigured_collections) > 0:
logger.info("")
logger.separator(f"Unconfigured Collection Assets Check for {self.library.name} Library", space=False, border=False)
logger.info("")
for col in unconfigured_collections:
try:
poster, background, item_dir, name = self.library.find_item_assets(col)
if poster or background:
@ -631,9 +654,9 @@ class Operations:
if self.library.mass_collection_mode:
logger.info("")
logger.separator(f"Unmanaged Mass Collection Mode to {self.library.mass_collection_mode} for {self.library.name} Library", space=False, border=False)
logger.separator(f"Unconfigured Mass Collection Mode to {self.library.mass_collection_mode} for {self.library.name} Library", space=False, border=False)
logger.info("")
for col in unmanaged_collections:
for col in unconfigured_collections:
if int(col.collectionMode) not in plex.collection_mode_keys \
or plex.collection_mode_keys[int(col.collectionMode)] != self.library.mass_collection_mode:
self.library.collection_mode_query(col, self.library.mass_collection_mode)

@ -822,11 +822,11 @@ class Plex(Library):
except NotFound:
raise Failed(f"Plex Error: Playlist {title} not found")
def get_collection(self, data):
if isinstance(data, int):
return self.fetchItem(data)
elif isinstance(data, Collection):
def get_collection(self, data, force_search=False):
if isinstance(data, Collection):
return data
elif isinstance(data, int) and not force_search:
return self.fetchItem(data)
else:
cols = self.search(title=str(data), libtype="collection")
for d in cols:
@ -841,8 +841,10 @@ class Plex(Library):
def validate_collections(self, collections):
valid_collections = []
for collection in collections:
try: valid_collections.append(self.get_collection(collection))
except Failed as e: logger.error(e)
try:
valid_collections.append(self.get_collection(collection))
except Failed as e:
logger.error(e)
if len(valid_collections) == 0:
raise Failed(f"Collection Error: No valid Plex Collections in {collections}")
return valid_collections

Loading…
Cancel
Save