diff --git a/CHANGELOG b/CHANGELOG index 401803af..63df5551 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,13 +6,17 @@ Updated python-dotenv requirement to 1.0.1 # New Features Added `monitor_existing` to sonarr and radarr. To update the monitored status of items existing in plex to match the `monitor` declared. - +Added [Gotify](https://gotify.net/) as a notification service. Thanks @krstn420 for the initial code. # Updates Added new [BoxOfficeMojo Builder](https://metamanager.wiki/en/latest/files/builders/mojo/) - credit to @nwithan8 for the suggestion and initial code submission Added new [`trakt_chart` attributes](https://metamanager.wiki/en/latest/files/builders/trakt/#trakt-chart) `network_ids`, `studio_ids`, `votes`, `tmdb_ratings`, `tmdb_votes`, `imdb_ratings`, `imdb_votes`, `rt_meters`, `rt_user_meters`, `metascores` and removed the deprecated `network` attribute Added [Trakt and MyAnimeList Authentication Page](https://metamanager.wiki/en/latest/config/auth/) allowing users to authenticate against those services directly from the wiki. credit to @chazlarson for developing the script Trakt Builder `trakt_userlist` value `recommendations` removed and `favorites` added. +Mass Update operations now can be given a list of sources to fall back on when one fails including a manual source. +`mass_content_rating_update` has a new source `mdb_age_rating` +`mass_originally_available_update` has a new source `mdb_digital` +`plex` attributes `clean_bundles`, `empty_trash`, and `optimize` can now take any schedule options to be run only when desired. # Defaults diff --git a/VERSION b/VERSION index 40763fe3..4a449753 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.0-develop19 +1.20.0-develop24 diff --git a/config/config.yml.template b/config/config.yml.template index 79a00919..86590520 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -105,6 +105,9 @@ mdblist: cache_expiration: 60 notifiarr: apikey: #################################### +gotify: + url: http://192.168.1.12:80 + token: #################################### anidb: # Not required for AniDB builders unless you want mature content username: ###### password: ###### diff --git a/docs/config/gotify.md b/docs/config/gotify.md new file mode 100644 index 00000000..df4e621c --- /dev/null +++ b/docs/config/gotify.md @@ -0,0 +1,31 @@ +# Gotify Attributes + +Configuring [Gotify](https://gotify.net/) is optional but can allow you to send the [webhooks](webhooks.md) +straight to gotify. + +A `gotify` mapping is in the root of the config file. + +Below is a `gotify` mapping example and the full set of attributes: + +```yaml +gotify: + url: #################################### + token: #################################### +``` + +| Attribute | Allowed Values | Required | +|:----------|:-------------------------|:------------------------------------------:| +| `url` | Gotify Server Url | :fontawesome-solid-circle-check:{ .green } | +| `token` | Gotify Application Token | :fontawesome-solid-circle-check:{ .green } | + +Once you have added the apikey your config.yml you have to add `gotify` to any [webhook](webhooks.md) to send that +notification to Gotify. + +```yaml +webhooks: + error: gotify + version: gotify + run_start: gotify + run_end: gotify + changes: gotify +``` diff --git a/docs/config/operations.md b/docs/config/operations.md index 1b684617..489f2747 100644 --- a/docs/config/operations.md +++ b/docs/config/operations.md @@ -104,7 +104,7 @@ You can create individual blocks of operations by using a list under `operations **Attribute:** `mass_genre_update` - **Accepted Values:** + **Accepted Values:** Source or List of sources to use in that order @@ -123,6 +123,7 @@ You can create individual blocks of operations by using a list under `operations +
`tmdb`Use TMDb for Genres
`unlock`Unlock all Genre Field
`remove`Remove all Genres and Lock all Field
`reset`Remove all Genres and Unlock all Field
List of Strings for Genres (["String 1", "String 2"])
???+ example "Example" @@ -131,7 +132,10 @@ You can create individual blocks of operations by using a list under `operations libraries: Movies: operations: - mass_genre_update: imdb + mass_genre_update: + - imdb + - tmdb + - ["Unknown"] ``` ??? blank "`mass_content_rating_update` - Updates the content rating of every item in the library." @@ -143,18 +147,21 @@ You can create individual blocks of operations by using a list under `operations **Attribute:** `mass_content_rating_update` - **Accepted Values:** + **Accepted Values:** Source or List of sources to use in that order + + +
`mdb`Use MdbList for Content Ratings
`mdb_commonsense`Use Common Sense Rating through MDbList for Content Ratings
`mdb_commonsense0`Use Common Sense Rating with Zero Padding through MDbList for Content Ratings
`mdb_age_rating`Use MDbList Age Rating for Content Ratings
`mdb_age_rating0`Use MDbList Age Rating with Zero Padding for Content Ratings
`omdb`Use IMDb through OMDb for Content Ratings
`mal`Use MyAnimeList for Content Ratings
`lock`Lock Content Rating Field
`unlock`Unlock Content Rating Field
`remove`Remove Content Rating and Lock Field
`reset`Remove Content Rating and Unlock Field
Any String for Content Ratings
???+ example "Example" @@ -163,7 +170,10 @@ You can create individual blocks of operations by using a list under `operations libraries: Movies: operations: - mass_content_rating_update: omdb + mass_content_rating_update: + - mdb_commonsense + - mdb_age_rating + - NR ``` ??? blank "`mass_original_title_update` - Updates the original title of every item in the library." @@ -175,7 +185,7 @@ You can create individual blocks of operations by using a list under `operations **Attribute:** `mass_original_title_update` - **Accepted Values:** + **Accepted Values:** Source or List of sources to use in that order @@ -187,6 +197,7 @@ You can create individual blocks of operations by using a list under `operations +
`anidb`Use AniDB Main Title for Original Titles
`unlock`Unlock Original Title Field
`remove`Remove Original Title and Lock Field
`reset`Remove Original Title and Unlock Field
Any String for Original Titles
???+ example "Example" @@ -195,7 +206,10 @@ You can create individual blocks of operations by using a list under `operations libraries: Anime: operations: - mass_original_title_update: anidb_official + mass_original_title_update: + - anidb_official + - anidb + - Unknown ``` ??? blank "`mass_studio_update` - Updates the studio of every item in the library." @@ -206,16 +220,17 @@ You can create individual blocks of operations by using a list under `operations **Attribute:** `mass_studio_update` - **Accepted Values:** + **Accepted Values:** Source or List of sources to use in that order - - - - + + + + +
`anidb`Use AniDB Animation Work for Studio
`mal`Use MyAnimeList Studio for Studio
`tmdb`Use TMDb Studio for Studio
`lock`Lock Original Title Field
`unlock`Unlock Original Title Field
`remove`Remove Original Title and Lock Field
`reset`Remove Original Title and Unlock Field
`lock`Lock Studio Field
`unlock`Unlock Studio Field
`remove`Remove Studio and Lock Field
`reset`Remove Studio and Unlock Field
Any String for Studio
???+ example "Example" @@ -224,7 +239,10 @@ You can create individual blocks of operations by using a list under `operations libraries: Anime: operations: - mass_studio_update: mal + mass_studio_update: + - mal + - anidb + - Unknown ``` ??? blank "`mass_originally_available_update` - Updates the originally available date of every item in the library." @@ -241,19 +259,21 @@ You can create individual blocks of operations by using a list under `operations **Attribute:** `mass_originally_available_update` - **Accepted Values:** + **Accepted Values:** Source or List of sources to use in that order + +
`tmdb`Use TMDb Release Date
`tvdb`Use TVDb Release Date
`omdb`Use IMDb Release Date through OMDb
`mdb`Use MdbList Release Date
`mdb_digital`Use MdbList Digital Release Date
`anidb`Use AniDB Release Date
`mal`Use MyAnimeList Release Date
`lock`Lock Originally Available Field
`unlock`Unlock Originally Available Field
`remove`Remove Originally Available and Lock Field
`reset`Remove Originally Available and Unlock Field
Any String in the Format: YYYY-MM-DD for Originally Available (2022-05-28)
???+ example "Example" @@ -262,7 +282,10 @@ You can create individual blocks of operations by using a list under `operations libraries: TV Shows: operations: - mass_originally_available_update: tvdb + mass_originally_available_update: + - mdb_digital + - mdb + - 1900-01-01 ``` ??? blank "`mass_***_rating_update` - Updates the audience/critic/user rating of every item in the library." @@ -284,7 +307,7 @@ You can create individual blocks of operations by using a list under `operations **Attribute:** `mass_audience_rating_update`/`mass_critic_rating_update`/`mass_user_rating_update` - **Accepted Values:** + **Accepted Values:** Source or List of sources to use in that order @@ -310,6 +333,7 @@ You can create individual blocks of operations by using a list under `operations +
`tmdb`Use TMDb Rating
`unlock`Unlock Rating Field
`remove`Remove Rating and Lock Field
`reset`Remove Rating and Unlock Field
Any Number between 0.0-10.0 for Ratings
???+ example "Example" @@ -318,9 +342,17 @@ You can create individual blocks of operations by using a list under `operations libraries: Movies: operations: - mass_audience_rating_update: mdb_average - mass_critic_rating_update: mdb_metacritic - mass_user_rating_update: imdb + mass_audience_rating_update: + - mdb + - mdb_average + - 2.0 + mass_critic_rating_update: + - imdb + - omdb + - 2.0 + mass_user_rating_update: + - trakt_user + - 2.0 ``` ??? blank "`mass_episode_***_rating_update` - Updates the audience/critic/user rating of every episode in the library." @@ -342,7 +374,7 @@ You can create individual blocks of operations by using a list under `operations **Attribute:** `mass_episode_audience_rating_update`/`mass_episode_critic_rating_update`/`mass_episode_user_rating_update` - **Accepted Values:** + **Accepted Values:** Source or List of sources to use in that order @@ -351,6 +383,7 @@ You can create individual blocks of operations by using a list under `operations +
`tmdb`Use TMDb Rating
`unlock`Unlock Rating Field
`remove`Remove Rating and Lock Field
`reset`Remove Rating and Unlock Field
Any Number between 0.0-10.0 for Ratings
???+ example "Example" @@ -359,9 +392,17 @@ You can create individual blocks of operations by using a list under `operations libraries: TV Shows: operations: - mass_episode_audience_rating_update: tmdb - mass_episode_critic_rating_update: remove - mass_episode_user_rating_update: imdb + mass_episode_audience_rating_update: + - mdb + - mdb_average + - 2.0 + mass_episode_critic_rating_update: + - imdb + - omdb + - 2.0 + mass_episode_user_rating_update: + - trakt_user + - 2.0 ``` ??? blank "`mass_poster_update` - Updates the poster of every item in the library." diff --git a/docs/config/overview.md b/docs/config/overview.md index 5e939f5f..2f23ae8b 100644 --- a/docs/config/overview.md +++ b/docs/config/overview.md @@ -24,6 +24,7 @@ requirements for setup that can be found by clicking the links within the table. | [`tautulli`](tautulli.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`omdb`](omdb.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`notifiarr`](notifiarr.md) | :fontawesome-solid-circle-xmark:{ .red } | +| [`gotify`](gotify.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`anidb`](anidb.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`radarr`](radarr.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`sonarr`](sonarr.md) | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/config/plex.md b/docs/config/plex.md index 41f6de1b..cb3abb3a 100644 --- a/docs/config/plex.md +++ b/docs/config/plex.md @@ -22,15 +22,15 @@ plex: optimize: false ``` -| Attribute | Allowed Values | Default | Required | -|:----------------|:------------------------------------------------------------------------|:--------|:------------------------------------------:| -| `url` | Plex Server URL
Example: http://192.168.1.12:32400 | N/A | :fontawesome-solid-circle-check:{ .green } | -| `token` | Plex Server Authentication Token | N/A | :fontawesome-solid-circle-check:{ .green } | -| `timeout` | Plex Server Timeout | 60 | :fontawesome-solid-circle-xmark:{ .red } | -| `db_cache` | Plex Server Database Cache Size | None | :fontawesome-solid-circle-xmark:{ .red } | -| `clean_bundles` | Runs Clean Bundles on the Server after all Collection Files are run | false | :fontawesome-solid-circle-xmark:{ .red } | -| `empty_trash` | Runs Empty Trash on the Server after all Collection Files are run | false | :fontawesome-solid-circle-xmark:{ .red } | -| `optimize` | Runs Optimize on the Server after all Collection Files are run | false | :fontawesome-solid-circle-xmark:{ .red } | +| Attribute | Allowed Values | Default | Required | +|:----------------|:-------------------------------------------------------------------------------------------------------------------------------|:--------|:------------------------------------------:| +| `url` | Plex Server URL
Example: http://192.168.1.12:32400 | N/A | :fontawesome-solid-circle-check:{ .green } | +| `token` | Plex Server Authentication Token | N/A | :fontawesome-solid-circle-check:{ .green } | +| `timeout` | Plex Server Timeout | 60 | :fontawesome-solid-circle-xmark:{ .red } | +| `db_cache` | Plex Server Database Cache Size | None | :fontawesome-solid-circle-xmark:{ .red } | +| `clean_bundles` | Runs Clean Bundles on the Server after all Collection Files are run
(`true`, `false` or Any [schedule option](schedule.md)) | false | :fontawesome-solid-circle-xmark:{ .red } | +| `empty_trash` | Runs Empty Trash on the Server after all Collection Files are run
(`true`, `false` or Any [schedule option](schedule.md)) | false | :fontawesome-solid-circle-xmark:{ .red } | +| `optimize` | Runs Optimize on the Server after all Collection Files are run
(`true`, `false` or Any [schedule option](schedule.md)) | false | :fontawesome-solid-circle-xmark:{ .red } | ???+ warning diff --git a/docs/config/webhooks.md b/docs/config/webhooks.md index d760ec8d..30a22820 100644 --- a/docs/config/webhooks.md +++ b/docs/config/webhooks.md @@ -28,6 +28,7 @@ webhooks: * Each Attribute can be either a webhook url as a string or a comma-separated list of webhooks urls. * To send notifications to [Notifiarr](notifiarr.md) just add `notifiarr` to a webhook instead of the webhook url. +* To send notifications to [Gotify](gotify.md) just add `gotify` to a webhook instead of the webhook url. ## Error Notifications @@ -77,7 +78,6 @@ level the error occurs. "error": str, // Error Message "critical": bool, // Critical Error "server_name": str, // Server Name - "library_name": str, // Library Name "playlist": str // Playlist Name } ``` diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 014f81b5..8a1a5f51 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -293,17 +293,17 @@ table.dualTable td, table.dualTable th { right: 0; } - -.md-typeset :is(.admonition, details) { - background-color: #1b1b1b; -} - /* Custom tooltips */ .md-tooltip { background-color: var(--md-primary-fg-color); border-radius: 6px; } +[data-md-color-scheme="slate"] .md-typeset .admonition.builder, +.md-typeset details.quicklink { + background-color: #1b1b1b; +} + .md-typeset .admonition.builder, .md-typeset details.builder { border: 1px solid var(--pg-light-border); diff --git a/json-schema/config-schema.json b/json-schema/config-schema.json index cf239755..cd8767db 100644 --- a/json-schema/config-schema.json +++ b/json-schema/config-schema.json @@ -25,6 +25,9 @@ "notifiarr": { "$ref": "#/definitions/notifiarr-api" }, + "gotify": { + "$ref": "#/definitions/gotify-api" + }, "anidb": { "$ref": "#/definitions/anidb-api" }, @@ -283,6 +286,24 @@ ], "title": "notifiarr" }, + "gotify-api": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string" + }, + "token": { + "type": "string" + } + }, + "required": [ + "url", + "token" + + ], + "title": "gotify" + }, "anidb-api": { "type": "object", "additionalProperties": false, @@ -1116,7 +1137,7 @@ "type": "object", "additionalProperties": false, "patternProperties": { - "^(?!plex|tmdb|tautulli|webhooks|omdb|mdblist|notifiarr|anidb|radarr|sonarr|trakt|mal).+$": { + "^(?!plex|tmdb|tautulli|webhooks|omdb|mdblist|notifiarr|gotify|anidb|radarr|sonarr|trakt|mal).+$": { "additionalProperties": false, "properties": { "metadata_files": { diff --git a/json-schema/prototype_config.yml b/json-schema/prototype_config.yml index 3af505c4..74f09229 100644 --- a/json-schema/prototype_config.yml +++ b/json-schema/prototype_config.yml @@ -472,6 +472,9 @@ mdblist: cache_expiration: 60 notifiarr: apikey: this-is-a-placeholder-string +gotify: + url: http://192.168.1.12:80 + token: this-is-a-placeholder-string anidb: # Not required for AniDB builders unless you want mature content username: this-is-a-placeholder-string password: this-is-a-placeholder-string diff --git a/mkdocs.yml b/mkdocs.yml index 12af256d..2b4f0c11 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -178,6 +178,7 @@ nav: - Radarr: config/radarr.md - Sonarr: config/sonarr.md - Notifiarr: config/notifiarr.md + - Gotify: config/gotify.md - Tautulli: config/tautulli.md - Github: config/github.md - MdbList: config/mdblist.md diff --git a/modules/cache.py b/modules/cache.py index ae33f779..2514cb8c 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -25,6 +25,7 @@ class Cache: cursor.execute("DROP TABLE IF EXISTS mdb_data") cursor.execute("DROP TABLE IF EXISTS mdb_data2") cursor.execute("DROP TABLE IF EXISTS mdb_data3") + cursor.execute("DROP TABLE IF EXISTS mdb_data4") cursor.execute("DROP TABLE IF EXISTS omdb_data") cursor.execute("DROP TABLE IF EXISTS omdb_data2") cursor.execute("DROP TABLE IF EXISTS tvdb_data") @@ -98,12 +99,13 @@ class Cache: expiration_date TEXT)""" ) cursor.execute( - """CREATE TABLE IF NOT EXISTS mdb_data4 ( + """CREATE TABLE IF NOT EXISTS mdb_data5 ( key INTEGER PRIMARY KEY, key_id TEXT UNIQUE, title TEXT, year INTEGER, released TEXT, + released_digital TEXT, type TEXT, imdbid TEXT, traktid INTEGER, @@ -119,8 +121,9 @@ class Cache: tmdb_rating INTEGER, letterboxd_rating REAL, myanimelist_rating REAL, - commonsense TEXT, certification TEXT, + commonsense TEXT, + age_rating TEXT, expiration_date TEXT)""" ) cursor.execute( @@ -480,20 +483,22 @@ class Cache: with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: - cursor.execute("SELECT * FROM mdb_data4 WHERE key_id = ?", (key_id,)) + cursor.execute("SELECT * FROM mdb_data5 WHERE key_id = ?", (key_id,)) row = cursor.fetchone() if row: mdb_dict["title"] = row["title"] if row["title"] else None mdb_dict["year"] = row["year"] if row["year"] else None mdb_dict["released"] = row["released"] if row["released"] else None + mdb_dict["released_digital"] = row["released_digital"] if row["released_digital"] else None mdb_dict["type"] = row["type"] if row["type"] else None mdb_dict["imdbid"] = row["imdbid"] if row["imdbid"] else None mdb_dict["traktid"] = row["traktid"] if row["traktid"] else None mdb_dict["tmdbid"] = row["tmdbid"] if row["tmdbid"] else None mdb_dict["score"] = row["score"] if row["score"] else None mdb_dict["score_average"] = row["average"] if row["average"] else None - mdb_dict["commonsense"] = row["commonsense"] if row["commonsense"] else None mdb_dict["certification"] = row["certification"] if row["certification"] else None + mdb_dict["commonsense"] = row["commonsense"] if row["commonsense"] else None + mdb_dict["age_rating"] = row["age_rating"] if row["age_rating"] else None mdb_dict["ratings"] = [ {"source": "imdb", "value": row["imdb_rating"] if row["imdb_rating"] else None}, {"source": "metacritic", "value": row["metacritic_rating"] if row["metacritic_rating"] else None}, @@ -515,16 +520,17 @@ class Cache: with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: - cursor.execute("INSERT OR IGNORE INTO mdb_data4(key_id) VALUES(?)", (key_id,)) - update_sql = "UPDATE mdb_data4 SET title = ?, year = ?, released = ?, type = ?, imdbid = ?, traktid = ?, " \ + cursor.execute("INSERT OR IGNORE INTO mdb_data5(key_id) VALUES(?)", (key_id,)) + update_sql = "UPDATE mdb_data5 SET title = ?, year = ?, released = ?, released_digital = ?, type = ?, imdbid = ?, traktid = ?, " \ "tmdbid = ?, score = ?, average = ?, imdb_rating = ?, metacritic_rating = ?, metacriticuser_rating = ?, " \ "trakt_rating = ?, tomatoes_rating = ?, tomatoesaudience_rating = ?, tmdb_rating = ?, " \ - "letterboxd_rating = ?, myanimelist_rating = ?, certification = ?, commonsense = ?, expiration_date = ? WHERE key_id = ?" + "letterboxd_rating = ?, myanimelist_rating = ?, certification = ?, commonsense = ?, age_rating = ?, expiration_date = ? WHERE key_id = ?" cursor.execute(update_sql, ( - mdb.title, mdb.year, mdb.released.strftime("%Y-%m-%d") if mdb.released else None, mdb.type, + mdb.title, mdb.year, mdb.released.strftime("%Y-%m-%d") if mdb.released else None, + mdb.released_digital.strftime("%Y-%m-%d") if mdb.released_digital else None, mdb.type, mdb.imdbid, mdb.traktid, mdb.tmdbid, mdb.score, mdb.average, mdb.imdb_rating, mdb.metacritic_rating, mdb.metacriticuser_rating, mdb.trakt_rating, mdb.tomatoes_rating, mdb.tomatoesaudience_rating, - mdb.tmdb_rating, mdb.letterboxd_rating, mdb.myanimelist_rating, mdb.content_rating, mdb.commonsense, + mdb.tmdb_rating, mdb.letterboxd_rating, mdb.myanimelist_rating, mdb.content_rating, mdb.commonsense, mdb.age_rating, expiration_date.strftime("%Y-%m-%d"), key_id )) diff --git a/modules/config.py b/modules/config.py index 3912b8d7..a11edb28 100644 --- a/modules/config.py +++ b/modules/config.py @@ -16,6 +16,7 @@ from modules.mal import MyAnimeList from modules.meta import PlaylistFile from modules.mojo import BoxOfficeMojo from modules.notifiarr import Notifiarr +from modules.gotify import Gotify from modules.omdb import OMDb from modules.overlays import Overlays from modules.plex import Plex @@ -55,8 +56,10 @@ mass_genre_options = { } mass_content_options = { "lock": "Lock Rating", "unlock": "Unlock Rating", "remove": "Remove and Lock Rating", "reset": "Remove and Unlock Rating", - "omdb": "Use IMDb Rating through OMDb", "mdb": "Use MdbList Rating", "mdb_commonsense": "Use Commonsense Rating through MDbList", - "mdb_commonsense0": "Use Commonsense Rating with Zero Padding through MDbList", "mal": "Use MyAnimeList Rating" + "omdb": "Use IMDb Rating through OMDb", "mdb": "Use MdbList Rating", + "mdb_commonsense": "Use Commonsense Rating through MDbList", "mdb_commonsense0": "Use Commonsense Rating with Zero Padding through MDbList", + "mdb_age_rating": "Use MDbList Age Rating", "mdb_age_rating0": "Use MDbList Age Rating with Zero Padding", + "mal": "Use MyAnimeList Rating" } mass_studio_options = { "lock": "Lock Rating", "unlock": "Unlock Rating", "remove": "Remove and Lock Rating", "reset": "Remove and Unlock Rating", @@ -69,7 +72,7 @@ mass_original_title_options = { } mass_available_options = { "lock": "Lock Originally Available", "unlock": "Unlock Originally Available", "remove": "Remove and Lock Originally Available", "reset": "Remove and Unlock Originally Available", - "tmdb": "Use TMDb Release", "omdb": "Use IMDb Release through OMDb", "mdb": "Use MdbList Release", "tvdb": "Use TVDb Release", + "tmdb": "Use TMDb Release", "omdb": "Use IMDb Release through OMDb", "mdb": "Use MdbList Release", "mdb_digital": "Use MdbList Digital Release", "tvdb": "Use TVDb Release", "anidb": "Use AniDB Release", "mal": "Use MyAnimeList Release" } mass_image_options = { @@ -287,6 +290,7 @@ class ConfigFile: if "omdb" in self.data: self.data["omdb"] = self.data.pop("omdb") if "mdblist" in self.data: self.data["mdblist"] = self.data.pop("mdblist") if "notifiarr" in self.data: self.data["notifiarr"] = self.data.pop("notifiarr") + if "gotify" in self.data: self.data["gotify"] = self.data.pop("gotify") if "anidb" in self.data: self.data["anidb"] = self.data.pop("anidb") if "radarr" in self.data: if "monitor" in self.data["radarr"] and isinstance(self.data["radarr"]["monitor"], bool): @@ -524,6 +528,24 @@ class ConfigFile: else: logger.info("notifiarr attribute not found") + self.GotifyFactory = None + if "gotify" in self.data: + logger.info("Connecting to Gotify...") + try: + self.GotifyFactory = Gotify(self, { + "url": check_for_attribute(self.data, "url", parent="gotify", throw=True), + "token": check_for_attribute(self.data, "token", parent="gotify", throw=True) + }) + except Failed as e: + if str(e).endswith("is blank"): + logger.warning(e) + else: + logger.stacktrace() + logger.error(e) + logger.info(f"Gotify Connection {'Failed' if self.GotifyFactory is None else 'Successful'}") + else: + logger.info("gotify attribute not found") + self.webhooks = { "error": check_for_attribute(self.data, "error", parent="webhooks", var_type="list", default_is_none=True), "version": check_for_attribute(self.data, "version", parent="webhooks", var_type="list", default_is_none=True), @@ -532,7 +554,7 @@ class ConfigFile: "changes": check_for_attribute(self.data, "changes", parent="webhooks", var_type="list", default_is_none=True), "delete": check_for_attribute(self.data, "delete", parent="webhooks", var_type="list", default_is_none=True) } - self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.NotifiarrFactory) + self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.NotifiarrFactory, gotify=self.GotifyFactory) try: self.Webhooks.start_time_hooks(self.start_time) if self.version[0] != "Unknown" and self.latest_version[0] != "Unknown" and self.version[1] != self.latest_version[1] or (self.version[2] and self.version[2] < self.latest_version[2]): @@ -717,11 +739,17 @@ class ConfigFile: "url": check_for_attribute(self.data, "url", parent="plex", var_type="url", default_is_none=True), "token": check_for_attribute(self.data, "token", parent="plex", default_is_none=True), "timeout": check_for_attribute(self.data, "timeout", parent="plex", var_type="int", default=60), - "db_cache": check_for_attribute(self.data, "db_cache", parent="plex", var_type="int", default_is_none=True), - "clean_bundles": check_for_attribute(self.data, "clean_bundles", parent="plex", var_type="bool", default=False), - "empty_trash": check_for_attribute(self.data, "empty_trash", parent="plex", var_type="bool", default=False), - "optimize": check_for_attribute(self.data, "optimize", parent="plex", var_type="bool", default=False) + "db_cache": check_for_attribute(self.data, "db_cache", parent="plex", var_type="int", default_is_none=True) } + for attr in ["clean_bundles", "empty_trash", "optimize"]: + try: + self.general["plex"][attr] = check_for_attribute(self.data, attr, parent="plex", var_type="bool", default=False, throw=True) + except Failed as e: + if "plex" in self.data and attr in self.data["plex"] and self.data["plex"][attr]: + self.general["plex"][attr] = self.data["plex"][attr] + else: + self.general["plex"][attr] = False + logger.warning(str(e).replace("Error", "Warning")) self.general["radarr"] = { "url": check_for_attribute(self.data, "url", parent="radarr", var_type="url", default_is_none=True), "token": check_for_attribute(self.data, "token", parent="radarr", default_is_none=True), @@ -845,8 +873,32 @@ class ConfigFile: for op, data_type in library_operations.items(): if op not in config_op: continue - if isinstance(data_type, list): + if op == "mass_imdb_parental_labels": section_final[op] = check_for_attribute(config_op, op, test_list=data_type, default_is_none=True, save=False) + elif isinstance(data_type, dict): + try: + if not config_op[op]: + raise Failed("is blank") + input_list = config_op[op] if isinstance(config_op[op], list) else [config_op[op]] + final_list = [] + for list_attr in input_list: + if not list_attr: + raise Failed(f"has a blank value") + if str(list_attr).lower() in data_type: + final_list.append(str(list_attr).lower()) + elif op in ["mass_content_rating_update", "mass_studio_update", "mass_original_title_update"]: + final_list.append(str(list_attr)) + elif op == "mass_genre_update": + final_list.append(list_attr if isinstance(list_attr, list) else [list_attr]) + elif op == "mass_originally_available_update": + final_list.append(util.validate_date(list_attr)) + elif op.endswith("rating_update"): + final_list.append(util.check_int(list_attr, datatype="float", minimum=0, maximum=10, throw=True)) + else: + raise Failed(f"has an invalid value: {list_attr}") + section_final[op] = final_list + except Failed as e: + logger.error(f"Config Error: {op} {e}") elif op == "mass_collection_mode": section_final[op] = util.check_collection_mode(config_op[op]) elif data_type == "dict": @@ -901,24 +953,28 @@ class ConfigFile: logger.warning(f"Config Warning: Operation {k} already scheduled") for k, v in final_operations.items(): params[k] = v - def error_check(err_attr, service): - logger.error(f"Config Error: Operation {err_attr} cannot be {params[err_attr]} without a successful {service} Connection") - params[err_attr] = None for mass_key in operations.meta_operations: if not params[mass_key]: continue - source = params[mass_key]["source"] if isinstance(params[mass_key], dict) else params[mass_key] - if source == "omdb" and self.OMDb is None: - error_check(mass_key, "OMDb") - if source and source.startswith("mdb") and not self.Mdblist.has_key: - error_check(mass_key, "MdbList") - if source and source.startswith("anidb") and not self.AniDB.is_authorized: - error_check(mass_key, "AniDB") - if source and source.startswith("mal") and self.MyAnimeList is None: - error_check(mass_key, "MyAnimeList") - if source and source.startswith("trakt") and self.Trakt is None: - error_check(mass_key, "Trakt") + sources = params[mass_key]["source"] if isinstance(params[mass_key], dict) else params[mass_key] + if not isinstance(sources, list): + sources = [sources] + try: + for source in sources: + if source and source == "omdb" and self.OMDb is None: + raise Failed(f"{source} without a successful OMDb Connection") + if source and str(source).startswith("mdb") and not self.Mdblist.has_key: + raise Failed(f"{source} without a successful MdbList Connection") + if source and str(source).startswith("anidb") and not self.AniDB.is_authorized: + raise Failed(f"{source} without a successful AniDB Connection") + if source and str(source).startswith("mal") and self.MyAnimeList is None: + raise Failed(f"{source} without a successful MyAnimeList Connection") + if source and str(source).startswith("trakt") and self.Trakt is None: + raise Failed(f"{source} without a successful Trakt Connection") + except Failed as e: + logger.error(f"Config Error: {mass_key} cannot use {e}") + params[mass_key] = None lib_vars = {} if lib and "template_variables" in lib and lib["template_variables"] and isinstance(lib["template_variables"], dict): @@ -1071,11 +1127,21 @@ class ConfigFile: "url": check_for_attribute(lib, "url", parent="plex", var_type="url", default=self.general["plex"]["url"], req_default=True, save=False), "token": check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False), "timeout": check_for_attribute(lib, "timeout", parent="plex", var_type="int", default=self.general["plex"]["timeout"], save=False), - "db_cache": check_for_attribute(lib, "db_cache", parent="plex", var_type="int", default=self.general["plex"]["db_cache"], default_is_none=True, save=False), - "clean_bundles": check_for_attribute(lib, "clean_bundles", parent="plex", var_type="bool", default=self.general["plex"]["clean_bundles"], save=False), - "empty_trash": check_for_attribute(lib, "empty_trash", parent="plex", var_type="bool", default=self.general["plex"]["empty_trash"], save=False), - "optimize": check_for_attribute(lib, "optimize", parent="plex", var_type="bool", default=self.general["plex"]["optimize"], save=False) + "db_cache": check_for_attribute(lib, "db_cache", parent="plex", var_type="int", default=self.general["plex"]["db_cache"], default_is_none=True, save=False) } + for attr in ["clean_bundles", "empty_trash", "optimize"]: + try: + params["plex"][attr] = check_for_attribute(lib, attr, parent="plex", var_type="bool", save=False, throw=True) + except Failed as er: + test = lib["plex"][attr] if "plex" in lib and attr in lib["plex"] and lib["plex"][attr] else self.general["plex"][attr] + params["plex"][attr] = False + if test is not True and test is not False: + try: + util.schedule_check(attr, test, current_time, self.run_hour) + params["plex"][attr] = True + except NotScheduled: + logger.info(f"Skipping Operation Not Scheduled for {test}") + if params["plex"]["url"].lower() == "env": params["plex"]["url"] = self.env_plex_url if params["plex"]["token"].lower() == "env": @@ -1175,7 +1241,7 @@ class ConfigFile: logger.info("") logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") - library.Webhooks = Webhooks(self, {}, library=library, notifiarr=self.NotifiarrFactory) + library.Webhooks = Webhooks(self, {}, library=library, notifiarr=self.NotifiarrFactory, gotify=self.GotifyFactory) library.Overlays = Overlays(self, library) logger.info("") diff --git a/modules/gotify.py b/modules/gotify.py new file mode 100644 index 00000000..db71d075 --- /dev/null +++ b/modules/gotify.py @@ -0,0 +1,99 @@ +from json import JSONDecodeError +from modules import util +from modules.util import Failed + +logger = util.logger + +class Gotify: + def __init__(self, config, params): + self.config = config + self.token = params["token"] + self.url = params["url"].rstrip("/") + logger.secret(self.url) + logger.secret(self.token) + try: + logger.info(f"Gotify Version: {self._request(path='version', post=False)['version']}") + except Exception: + logger.stacktrace() + raise Failed("Gotify Error: Invalid URL") + + def _request(self, path="message", json=None, post=True): + if post: + response = self.config.post(f"{self.url}/{path}", headers={"X-Gotify-Key": self.token}, json=json) + else: + response = self.config.get(f"{self.url}/{path}") + try: + response_json = response.json() + except JSONDecodeError as e: + logger.error(response.content) + logger.debug(e) + raise e + if response.status_code >= 400: + raise Failed(f"({response.status_code} [{response.reason}]) {response_json['errorDescription']}") + return response_json + + def notification(self, json): + message = "" + if json["event"] == "run_end": + title = "Run Completed" + message = f"Start Time: {json['start_time']}\n" \ + f"End Time: {json['end_time']}\n" \ + f"Run Time: {json['run_time']}\n" \ + f"Collections Created: {json['collections_created']}\n" \ + f"Collections Modified: {json['collections_modified']}\n" \ + f"Collections Deleted: {json['collections_deleted']}\n" \ + f"Items Added: {json['items_added']}\n" \ + f"Items Removed: {json['items_removed']}" + if json["added_to_radarr"]: + message += f"\n{json['added_to_radarr']} Movies Added To Radarr" + if json["added_to_sonarr"]: + message += f"\n{json['added_to_sonarr']} Movies Added To Sonarr" + elif json["event"] == "run_start": + title = "Run Started" + message = json["start_time"] + elif json["event"] == "version": + title = "New Version Available" + message = f"Current: {json['current']}\n" \ + f"Latest: {json['latest']}\n" \ + f"Notes: {json['notes']}" + elif json["event"] == "delete": + if "library_name" in json: + title = "Collection Deleted" + else: + title = "Playlist Deleted" + message = json["message"] + else: + new_line = "\n" + if "server_name" in json: + message += f"{new_line if message else ''}Server: {json['server_name']}" + if "library_name" in json: + message += f"{new_line if message else ''}Library: {json['library_name']}" + if "collection" in json: + message += f"{new_line if message else ''}Collection: {json['collection']}" + if "playlist" in json: + message += f"{new_line if message else ''}Playlist: {json['playlist']}" + if json["event"] == "error": + if "collection" in json: + title_name = "Collection" + elif "playlist" in json: + title_name = "Playlist" + elif "library_name" in json: + title_name = "Library" + else: + title_name = "Global" + title = f"{'Critical ' if json['critical'] else ''}{title_name} Error" + message += f"{new_line if message else ''}Error Message: {json['error']}" + else: + title = f"{'Collection' if 'collection' in json else 'Playlist'} {'Created' if json['created'] else 'Modified'}" + if json['radarr_adds']: + message += f"{new_line if message else ''}{len(json['radarr_adds'])} Radarr Additions:" + if json['sonarr_adds']: + message += f"{new_line if message else ''}{len(json['sonarr_adds'])} Sonarr Additions:" + message += f"{new_line if message else ''}{len(json['additions'])} Additions:" + for add_dict in json['additions']: + message += f"\n{add_dict['title']}" + message += f"{new_line if message else ''}{len(json['removals'])} Removals:" + for add_dict in json['removals']: + message += f"\n{add_dict['title']}" + + self._request(json={"message": message, "title": title}) diff --git a/modules/imdb.py b/modules/imdb.py index 522956ca..76e912c2 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -643,6 +643,9 @@ class IMDb: def get_rating(self, imdb_id): return self.ratings[imdb_id] if imdb_id in self.ratings else None + def get_genres(self, imdb_id): + return self.genres[imdb_id] if imdb_id in self.genres else [] + def get_episode_rating(self, imdb_id, season_num, episode_num): season_num = str(season_num) episode_num = str(episode_num) diff --git a/modules/library.py b/modules/library.py index 1cf93597..30da97fa 100644 --- a/modules/library.py +++ b/modules/library.py @@ -16,7 +16,6 @@ class Library(ABC): self.Webhooks = None self.Operations = Operations(config, self) self.Overlays = None - self.Notifiarr = None self.collections = [] self.collection_names = [] self.metadatas = [] @@ -129,7 +128,6 @@ class Library(ABC): 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["source"] if isinstance(i, dict) else i for i in [getattr(self, o) for o in operations.meta_operations] if i] self.label_operations = True if self.assets_for_all or self.mass_imdb_parental_labels else False if self.asset_directory: diff --git a/modules/mdblist.py b/modules/mdblist.py index e4adb18e..a2c69482 100644 --- a/modules/mdblist.py +++ b/modules/mdblist.py @@ -28,6 +28,10 @@ class MDbObj: self.released = datetime.strptime(data["released"], "%Y-%m-%d") except (ValueError, TypeError): self.released = None + try: + self.released_digital = datetime.strptime(data["released_digital"], "%Y-%m-%d") + except (ValueError, TypeError): + self.released_digital = None self.type = data["type"] self.imdbid = data["imdbid"] self.traktid = util.check_num(data["traktid"]) @@ -64,6 +68,7 @@ class MDbObj: self.myanimelist_rating = util.check_num(rating["value"], is_int=False) self.content_rating = data["certification"] self.commonsense = data["commonsense"] + self.age_rating = data["age_rating"] class Mdblist: diff --git a/modules/operations.py b/modules/operations.py index 3f3ecbda..d0ffe013 100644 --- a/modules/operations.py +++ b/modules/operations.py @@ -98,8 +98,6 @@ class Operations: ep_lock_edits = {} ep_unlock_edits = {} - trakt_ratings = self.config.Trakt.user_ratings(self.library.is_movie) if any([o == "trakt_user" for o in self.library.meta_operations]) else [] - reverse_anidb = {} for k, v in self.library.anidb_map.items(): reverse_anidb[v] = k @@ -164,110 +162,163 @@ class Operations: path = path[:-1] if path.endswith(("/", "\\")) else path sonarr_adds.append((tvdb_id, path)) - tmdb_item = None - if any([o == "tmdb" for o in self.library.meta_operations]): - try: - tmdb_item = self.config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=self.library.is_movie) - except Failed as e: - logger.error(str(e)) - - omdb_item = None - if any([o == "omdb" for o in self.library.meta_operations]): - if self.config.OMDb.limit is not False: - logger.error("Daily OMDb Limit Reached") - elif not imdb_id: - logger.info(f" No IMDb ID for Guid: {item.guid}") - else: - try: - omdb_item = self.config.OMDb.get_omdb(imdb_id) - except Failed as e: - logger.error(str(e)) - except Exception: - logger.error(f"IMDb ID: {imdb_id}") - raise - - tvdb_item = None - if any([o == "tvdb" for o in self.library.meta_operations]): - if tvdb_id: + _trakt_ratings = None + def trakt_ratings(): + nonlocal _trakt_ratings + if _tmdb_obj is None: + _trakt_ratings = self.config.Trakt.user_ratings(self.library.is_movie) + if not _trakt_ratings: + raise Failed + return _trakt_ratings + + _tmdb_obj = None + def tmdb_obj(): + nonlocal _tmdb_obj + if _tmdb_obj is None: + _tmdb_obj = False try: - tvdb_item = self.config.TVDb.get_tvdb_obj(tvdb_id, is_movie=self.library.is_movie) - except Failed as e: - logger.error(str(e)) - else: - logger.info(f"No TVDb ID for Guid: {item.guid}") + _item = self.config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=self.library.is_movie) + if _item: + _tmdb_obj = _item + except Failed as err: + logger.error(str(err)) + if not _tmdb_obj: + raise Failed + return _tmdb_obj + + _omdb_obj = None + def omdb_obj(): + nonlocal _omdb_obj + if _omdb_obj is None: + _omdb_obj = False + if self.config.OMDb.limit is not False: + logger.error("Daily OMDb Limit Reached") + elif not imdb_id: + logger.info(f"No IMDb ID for Guid: {item.guid}") + else: + try: + _omdb_obj = self.config.OMDb.get_omdb(imdb_id) + except Failed as err: + logger.error(str(err)) + except Exception: + logger.error(f"IMDb ID: {imdb_id}") + raise + if not _omdb_obj: + raise Failed + return _omdb_obj + + _tvdb_obj = None + def tvdb_obj(): + nonlocal _tvdb_obj + if _tvdb_obj is None: + _tvdb_obj = False + if tvdb_id: + try: + _tvdb_obj = self.config.TVDb.get_tvdb_obj(tvdb_id, is_movie=self.library.is_movie) + except Failed as err: + logger.error(str(err)) + else: + logger.info(f"No TVDb ID for Guid: {item.guid}") + if not _tvdb_obj: + raise Failed + return _tvdb_obj + + _mdb_obj = None + def mdb_obj(): + nonlocal _mdb_obj + if _mdb_obj is None: + _mdb_obj = False + if self.config.Mdblist.limit is False: + if self.library.is_show and tvdb_id: + try: + _mdb_obj = self.config.Mdblist.get_series(tvdb_id) + except LimitReached as err: + logger.debug(err) + except Failed as err: + logger.error(str(err)) + except Exception: + logger.trace(f"TVDb ID: {tvdb_id}") + raise + if self.library.is_movie and tmdb_id: + try: + _mdb_obj = self.config.Mdblist.get_movie(tmdb_id) + except LimitReached as err: + logger.debug(err) + except Failed as err: + logger.error(str(err)) + except Exception: + logger.trace(f"TMDb ID: {tmdb_id}") + raise + if imdb_id and not _mdb_obj: + try: + _mdb_obj = self.config.Mdblist.get_imdb(imdb_id) + except LimitReached as err: + logger.debug(err) + except Failed as err: + logger.error(str(err)) + except Exception: + logger.trace(f"IMDb ID: {imdb_id}") + raise + if not _mdb_obj: + logger.warning(f"No MdbItem for {item.title} (Guid: {item.guid})") + if not _mdb_obj: + raise Failed + return _mdb_obj - anidb_item = None - mal_item = None - if any([o.startswith("anidb") or o.startswith("mal") for o in self.library.meta_operations]): + anidb_id = None + def get_anidb_id(): if item.ratingKey in reverse_anidb: - anidb_id = reverse_anidb[item.ratingKey] + return reverse_anidb[item.ratingKey] elif tvdb_id in self.config.Convert._tvdb_to_anidb: - anidb_id = self.config.Convert._tvdb_to_anidb[tvdb_id] + return self.config.Convert._tvdb_to_anidb[tvdb_id] elif imdb_id in self.config.Convert._imdb_to_anidb: - anidb_id = self.config.Convert._imdb_to_anidb[imdb_id] + return self.config.Convert._imdb_to_anidb[imdb_id] else: - anidb_id = None - if any([o.startswith("anidb") for o in self.library.meta_operations]): + return False + + _anidb_obj = None + def anidb_obj(): + nonlocal anidb_id, _anidb_obj + if _anidb_obj is None: + _anidb_obj = False + if anidb_id is None: + anidb_id = get_anidb_id() if anidb_id: try: - anidb_item = self.config.AniDB.get_anime(anidb_id) - except Failed as e: - logger.error(str(e)) + _anidb_obj = self.config.AniDB.get_anime(anidb_id) + except Failed as err: + logger.error(str(err)) else: logger.warning(f"No AniDB ID for Guid: {item.guid}") - if any([o.startswith("mal") for o in self.library.meta_operations]): + if not _anidb_obj: + raise Failed + return _anidb_obj + + _mal_obj = None + def mal_obj(): + nonlocal anidb_id, _mal_obj + if _mal_obj is None: + _mal_obj = False + if anidb_id is None: + anidb_id = get_anidb_id() + mal_id = None if item.ratingKey in reverse_mal: mal_id = reverse_mal[item.ratingKey] elif not anidb_id: logger.warning(f"Convert Warning: No AniDB ID to Convert to MyAnimeList ID for Guid: {item.guid}") - mal_id = None elif anidb_id not in self.config.Convert._anidb_to_mal: logger.warning(f"Convert Warning: No MyAnimeList Found for AniDB ID: {anidb_id} of Guid: {item.guid}") - mal_id = None else: mal_id = self.config.Convert._anidb_to_mal[anidb_id] if mal_id: try: - mal_item = self.config.MyAnimeList.get_anime(mal_id) - except Failed as e: - logger.error(str(e)) + _mal_obj = self.config.MyAnimeList.get_anime(mal_id) + except Failed as err: + logger.error(str(err)) + if not _mal_obj: + raise Failed + return _mal_obj - mdb_item = None - if any([o and o.startswith("mdb") for o in self.library.meta_operations]): - if self.config.Mdblist.limit is False: - try: - if self.library.is_show and tvdb_id and mdb_item is None: - try: - mdb_item = self.config.Mdblist.get_series(tvdb_id) - except Failed as e: - logger.trace(str(e)) - except Exception: - logger.trace(f"TVDb ID: {tvdb_id}") - raise - if tmdb_id and mdb_item is None: - try: - mdb_item = self.config.Mdblist.get_movie(tmdb_id) - except LimitReached as e: - logger.debug(e) - except Failed as e: - logger.trace(str(e)) - except Exception: - logger.trace(f"TMDb ID: {tmdb_id}") - raise - if imdb_id and mdb_item is None: - try: - mdb_item = self.config.Mdblist.get_imdb(imdb_id) - except LimitReached as e: - logger.debug(e) - except Failed as e: - logger.trace(str(e)) - except Exception: - logger.trace(f"IMDb ID: {imdb_id}") - raise - if mdb_item is None: - logger.warning(f"No MdbItem for {item.title} (Guid: {item.guid})") - except LimitReached as e: - logger.debug(e) for attribute, item_attr in [ (self.library.mass_audience_rating_update, "audienceRating"), (self.library.mass_critic_rating_update, "rating"), @@ -275,115 +326,133 @@ class Operations: ]: if attribute: current = getattr(item, item_attr) - if attribute == "remove" and current is not None: - if item_attr not in remove_edits: - remove_edits[item_attr] = [] - remove_edits[item_attr].append(item.ratingKey) - item_edits += f"\nRemove {name_display[item_attr]} (Batched)" - elif attribute == "reset" and current is not None: - if item_attr not in reset_edits: - reset_edits[item_attr] = [] - reset_edits[item_attr].append(item.ratingKey) - item_edits += f"\nReset {name_display[item_attr]} (Batched)" - elif attribute in ["unlock", "reset"] and item_attr in locked_fields: - if item_attr not in unlock_edits: - unlock_edits[item_attr] = [] - unlock_edits[item_attr].append(item.ratingKey) - item_edits += f"\nUnlock {name_display[item_attr]} (Batched)" - elif attribute in ["lock", "remove"] and item_attr not in locked_fields: - if item_attr not in lock_edits: - lock_edits[item_attr] = [] - lock_edits[item_attr].append(item.ratingKey) - item_edits += f"\nLock {name_display[item_attr]} (Batched)" - elif attribute not in ["lock", "unlock", "remove", "reset"]: - if tmdb_item and attribute == "tmdb": - found_rating = tmdb_item.vote_average - elif imdb_id and attribute == "imdb": - found_rating = self.config.IMDb.get_rating(imdb_id) - elif attribute == "trakt_user" and self.library.is_movie and tmdb_id in trakt_ratings: - found_rating = trakt_ratings[tmdb_id] - elif attribute == "trakt_user" and self.library.is_show and tvdb_id in trakt_ratings: - found_rating = trakt_ratings[tvdb_id] - elif omdb_item and attribute == "omdb": - found_rating = omdb_item.imdb_rating - elif mdb_item and attribute == "mdb": - found_rating = mdb_item.score / 10 if mdb_item.score else None - elif mdb_item and attribute == "mdb_average": - found_rating = mdb_item.average / 10 if mdb_item.average else None - elif mdb_item and attribute == "mdb_imdb": - found_rating = mdb_item.imdb_rating if mdb_item.imdb_rating else None - elif mdb_item and attribute == "mdb_metacritic": - found_rating = mdb_item.metacritic_rating / 10 if mdb_item.metacritic_rating else None - elif mdb_item and attribute == "mdb_metacriticuser": - found_rating = mdb_item.metacriticuser_rating if mdb_item.metacriticuser_rating else None - elif mdb_item and attribute == "mdb_trakt": - found_rating = mdb_item.trakt_rating / 10 if mdb_item.trakt_rating else None - elif mdb_item and attribute == "mdb_tomatoes": - found_rating = mdb_item.tomatoes_rating / 10 if mdb_item.tomatoes_rating else None - elif mdb_item and attribute == "mdb_tomatoesaudience": - found_rating = mdb_item.tomatoesaudience_rating / 10 if mdb_item.tomatoesaudience_rating else None - elif mdb_item and attribute == "mdb_tmdb": - found_rating = mdb_item.tmdb_rating / 10 if mdb_item.tmdb_rating else None - elif mdb_item and attribute == "mdb_letterboxd": - found_rating = mdb_item.letterboxd_rating * 2 if mdb_item.letterboxd_rating else None - elif mdb_item and attribute == "mdb_myanimelist": - found_rating = mdb_item.myanimelist_rating if mdb_item.myanimelist_rating else None - elif anidb_item and attribute == "anidb_rating": - found_rating = anidb_item.rating - elif anidb_item and attribute == "anidb_average": - found_rating = anidb_item.average - elif anidb_item and attribute == "anidb_score": - found_rating = anidb_item.score - elif mal_item and attribute == "mal": - found_rating = mal_item.score - else: - found_rating = None - - if found_rating and float(found_rating) > 0: - found_rating = f"{float(found_rating):.1f}" - if str(current) != found_rating: - if found_rating not in rating_edits[item_attr]: - rating_edits[item_attr][found_rating] = [] - rating_edits[item_attr][found_rating].append(item.ratingKey) - item_edits += f"\nUpdate {name_display[item_attr]} (Batched) | {found_rating}" + for option in attribute: + if option in ["lock", "remove"]: + if option == "remove" and current: + if item_attr not in remove_edits: + remove_edits[item_attr] = [] + remove_edits[item_attr].append(item.ratingKey) + item_edits += f"\nRemove {name_display[item_attr]} (Batched)" + elif item_attr not in locked_fields: + if item_attr not in lock_edits: + lock_edits[item_attr] = [] + lock_edits[item_attr].append(item.ratingKey) + item_edits += f"\nLock {name_display[item_attr]} (Batched)" + break + elif option in ["unlock", "reset"]: + if option == "reset" and current: + if item_attr not in reset_edits: + reset_edits[item_attr] = [] + reset_edits[item_attr].append(item.ratingKey) + item_edits += f"\nReset {name_display[item_attr]} (Batched)" + elif item_attr in locked_fields: + if item_attr not in unlock_edits: + unlock_edits[item_attr] = [] + unlock_edits[item_attr].append(item.ratingKey) + item_edits += f"\nUnlock {name_display[item_attr]} (Batched)" + break else: - logger.info(f"No {name_display[item_attr]} Found") + try: + if option == "tmdb": + found_rating = tmdb_obj().vote_average # noqa + elif option == "imdb": + found_rating = self.config.IMDb.get_rating(imdb_id) + elif option == "trakt_user": + _ratings = trakt_ratings() + _id = tmdb_id if self.library.is_movie else tvdb_id + if _id in _ratings: + found_rating = _ratings[_id] + else: + raise Failed + elif str(option).startswith("mdb"): + mdb_item = mdb_obj() + if option == "mdb_average": + found_rating = mdb_item.average / 10 if mdb_item.average else None # noqa + elif option == "mdb_imdb": + found_rating = mdb_item.imdb_rating if mdb_item.imdb_rating else None # noqa + elif option == "mdb_metacritic": + found_rating = mdb_item.metacritic_rating / 10 if mdb_item.metacritic_rating else None # noqa + elif option == "mdb_metacriticuser": + found_rating = mdb_item.metacriticuser_rating if mdb_item.metacriticuser_rating else None # noqa + elif option == "mdb_trakt": + found_rating = mdb_item.trakt_rating / 10 if mdb_item.trakt_rating else None # noqa + elif option == "mdb_tomatoes": + found_rating = mdb_item.tomatoes_rating / 10 if mdb_item.tomatoes_rating else None # noqa + elif option == "mdb_tomatoesaudience": + found_rating = mdb_item.tomatoesaudience_rating / 10 if mdb_item.tomatoesaudience_rating else None # noqa + elif option == "mdb_tmdb": + found_rating = mdb_item.tmdb_rating / 10 if mdb_item.tmdb_rating else None # noqa + elif option == "mdb_letterboxd": + found_rating = mdb_item.letterboxd_rating * 2 if mdb_item.letterboxd_rating else None # noqa + elif option == "mdb_myanimelist": + found_rating = mdb_item.myanimelist_rating if mdb_item.myanimelist_rating else None # noqa + else: + found_rating = mdb_item.score / 10 if mdb_item.score else None # noqa + elif option == "anidb_rating": + found_rating = anidb_obj().rating # noqa + elif option == "anidb_average": + found_rating = anidb_obj().average # noqa + elif option == "anidb_score": + found_rating = anidb_obj().score # noqa + elif option == "mal": + found_rating = mal_obj().score # noqa + else: + found_rating = option + if not found_rating: + logger.info(f"No {option} {name_display[item_attr]} Found") + raise Failed + found_rating = f"{float(found_rating):.1f}" + if str(current) != found_rating: + if found_rating not in rating_edits[item_attr]: + rating_edits[item_attr][found_rating] = [] + rating_edits[item_attr][found_rating].append(item.ratingKey) + item_edits += f"\nUpdate {name_display[item_attr]} (Batched) | {found_rating}" + break + except Failed: + continue if self.library.mass_genre_update or self.library.genre_mapper: - try: + if self.library.mass_genre_update: new_genres = [] - if self.library.mass_genre_update and self.library.mass_genre_update not in ["lock", "unlock", "remove", "reset"]: - if tmdb_item and self.library.mass_genre_update == "tmdb": - new_genres = tmdb_item.genres - elif imdb_id and self.library.mass_genre_update == "imdb" and imdb_id in self.config.IMDb.genres: - new_genres = self.config.IMDb.genres[imdb_id] - elif omdb_item and self.library.mass_genre_update == "omdb": - new_genres = omdb_item.genres - elif tvdb_item and self.library.mass_genre_update == "tvdb": - new_genres = tvdb_item.genres - elif anidb_item and self.library.mass_genre_update in anidb.weights: - logger.trace(anidb_item.main_title) - logger.trace(anidb_item.tags) - new_genres = [str(t).title() for t, w in anidb_item.tags.items() if w >= anidb.weights[self.library.mass_genre_update]] - elif mal_item and self.library.mass_genre_update == "mal": - new_genres = mal_item.genres - else: - raise Failed - if not new_genres: - logger.info("No Genres Found") - if self.library.genre_mapper or self.library.mass_genre_update in ["lock", "unlock"]: - if not new_genres and self.library.mass_genre_update not in ["remove", "reset"]: - new_genres = [g.tag for g in item.genres] - if self.library.genre_mapper: - mapped_genres = [] - for genre in new_genres: - if genre in self.library.genre_mapper: - if self.library.genre_mapper[genre]: - mapped_genres.append(self.library.genre_mapper[genre]) - else: - mapped_genres.append(genre) - new_genres = mapped_genres + extra_option = None + for option in self.library.mass_genre_update: + if option in ["lock", "unlock", "remove", "reset"]: + extra_option = option + break + try: + if option == "tmdb": + new_genres = tmdb_obj().genres # noqa + elif option == "imdb": + new_genres = self.config.IMDb.get_genres(imdb_id) + elif option == "omdb": + new_genres = omdb_obj().genres # noqa + elif option == "tvdb": + new_genres = tvdb_obj().genres # noqa + elif str(option) in anidb.weights: + new_genres = [str(t).title() for t, w in anidb_obj().tags.items() if w >= anidb.weights[str(option)]] # noqa + elif option == "mal": + new_genres = mal_obj().genres # noqa + else: + new_genres = option + if not new_genres: + logger.info(f"No {option} Genres Found") + raise Failed + break + except Failed: + continue + item_genres = [g.tag for g in item.genres] + if not new_genres and extra_option not in ["remove", "reset"]: + new_genres = item_genres + if self.library.genre_mapper: + mapped_genres = [] + for genre in new_genres: + if genre in self.library.genre_mapper: + if self.library.genre_mapper[genre]: + mapped_genres.append(self.library.genre_mapper[genre]) + else: + mapped_genres.append(genre) + new_genres = mapped_genres _add = list(set(new_genres) - set(item_genres)) _remove = list(set(item_genres) - set(new_genres)) for genre_list, edit_type in [(_add, "add"), (_remove, "remove")]: @@ -393,41 +462,63 @@ class Operations: genre_edits[edit_type][g] = [] genre_edits[edit_type][g].append(item.ratingKey) item_edits += f"\n{edit_type.capitalize()} Genres (Batched) | {', '.join(genre_list)}" - if self.library.mass_genre_update in ["unlock", "reset"] and ("genre" in locked_fields or _add or _remove): + if extra_option in ["unlock", "reset"] and ("genre" in locked_fields or _add or _remove): if "genre" not in unlock_edits: unlock_edits["genre"] = [] unlock_edits["genre"].append(item.ratingKey) item_edits += "\nUnlock Genre (Batched)" - elif self.library.mass_genre_update in ["lock", "remove"] and "genre" not in locked_fields and not _add and not _remove: + elif extra_option in ["lock", "remove"] and "genre" not in locked_fields and not _add and not _remove: if "genre" not in lock_edits: lock_edits["genre"] = [] lock_edits["genre"].append(item.ratingKey) item_edits += "\nLock Genre (Batched)" - except Failed: - pass if self.library.mass_content_rating_update or self.library.content_rating_mapper: - try: + if self.library.mass_content_rating_update: new_rating = None - if self.library.mass_content_rating_update and self.library.mass_content_rating_update not in ["lock", "unlock", "remove", "reset"]: - if omdb_item and self.library.mass_content_rating_update == "omdb": - new_rating = omdb_item.content_rating - elif mdb_item and self.library.mass_content_rating_update == "mdb": - new_rating = mdb_item.content_rating if mdb_item.content_rating else None - elif mdb_item and self.library.mass_content_rating_update == "mdb_commonsense": - new_rating = mdb_item.commonsense if mdb_item.commonsense else None - elif mdb_item and self.library.mass_content_rating_update == "mdb_commonsense0": - new_rating = str(mdb_item.commonsense).rjust(2, "0") if mdb_item.commonsense else None - elif mal_item and self.library.mass_content_rating_update == "mal": - new_rating = mal_item.rating - else: - raise Failed - if new_rating is None: - logger.info("No Content Rating Found") - else: - new_rating = str(new_rating) + extra_option = None + for option in self.library.mass_content_rating_update: + if option in ["lock", "unlock", "remove", "reset"]: + extra_option = option + break + try: + if option == "omdb": + new_rating = omdb_obj().content_rating # noqa + elif option == "mdb": + _rating = mdb_obj().content_rating # noqa + new_rating = _rating if _rating else None + elif str(option).startswith("mdb_commonsense"): + _rating = mdb_obj().commonsense # noqa + if not _rating: + new_rating = None + elif option == "mdb_commonsense0": + new_rating = str(_rating).rjust(2, "0") + else: + new_rating = _rating + elif str(option).startswith("mdb_age_rating"): + _rating = mdb_obj().age_rating # noqa + if not _rating: + new_rating = None + elif option == "mdb_age_rating0": + new_rating = str(_rating).rjust(2, "0") + else: + new_rating = _rating + elif option == "mal": + new_rating = mal_obj().rating # noqa + else: + new_rating = option + if new_rating is None: + logger.info(f"No {option} Content Rating Found") + raise Failed + else: + new_rating = str(new_rating) + break + except Failed: + continue is_none = False + do_lock = False + do_unlock = False current_rating = item.contentRating if not new_rating: new_rating = current_rating @@ -436,182 +527,199 @@ class Operations: new_rating = self.library.content_rating_mapper[new_rating] if not new_rating: is_none = True - has_edit = False - if (is_none or self.library.mass_content_rating_update == "remove") and current_rating: - if "contentRating" not in remove_edits: - remove_edits["contentRating"] = [] - remove_edits["contentRating"].append(item.ratingKey) - item_edits += "\nRemove Content Rating (Batched)" - elif self.library.mass_content_rating_update == "reset" and current_rating: - if "contentRating" not in reset_edits: - reset_edits["contentRating"] = [] - reset_edits["contentRating"].append(item.ratingKey) - item_edits += "\nReset Content Rating (Batched)" + if extra_option == "reset": + if current_rating: + if "contentRating" not in reset_edits: + reset_edits["contentRating"] = [] + reset_edits["contentRating"].append(item.ratingKey) + item_edits += "\nReset Content Rating (Batched)" + elif "contentRating" in locked_fields: + do_unlock = True + elif extra_option == "remove" or is_none: + if current_rating: + if "contentRating" not in remove_edits: + remove_edits["contentRating"] = [] + remove_edits["contentRating"].append(item.ratingKey) + item_edits += "\nRemove Content Rating (Batched)" + elif "contentRating" not in locked_fields: + do_lock = True elif new_rating and new_rating != current_rating: if new_rating not in content_edits: content_edits[new_rating] = [] content_edits[new_rating].append(item.ratingKey) item_edits += f"\nUpdate Content Rating (Batched) | {new_rating}" - has_edit = True + do_lock = False - if self.library.mass_content_rating_update in ["unlock", "reset"] and ("contentRating" in locked_fields or has_edit): - if "contentRating" not in unlock_edits: - unlock_edits["contentRating"] = [] - unlock_edits["contentRating"].append(item.ratingKey) - item_edits += "\nUnlock Content Rating (Batched)" - elif self.library.mass_content_rating_update in ["lock", "remove"] and "contentRating" not in locked_fields and not has_edit: + if extra_option == "lock" or do_lock: if "contentRating" not in lock_edits: lock_edits["contentRating"] = [] lock_edits["contentRating"].append(item.ratingKey) item_edits += "\nLock Content Rating (Batched)" - except Failed: - pass + elif extra_option == "unlock" or do_unlock: + if "contentRating" not in unlock_edits: + unlock_edits["contentRating"] = [] + unlock_edits["contentRating"].append(item.ratingKey) + item_edits += "\nUnlock Content Rating (Batched)" if self.library.mass_original_title_update: current_original = item.originalTitle - has_edit = False - if self.library.mass_original_title_update == "remove" and current_original: - if "originalTitle" not in remove_edits: - remove_edits["originalTitle"] = [] - remove_edits["originalTitle"].append(item.ratingKey) - item_edits += "\nRemove Original Title (Batched)" - elif self.library.mass_original_title_update == "reset" and current_original: - if "originalTitle" not in reset_edits: - reset_edits["originalTitle"] = [] - reset_edits["originalTitle"].append(item.ratingKey) - item_edits += "\nReset Original Title (Batched)" - elif self.library.mass_original_title_update not in ["lock", "unlock", "remove", "reset"]: - try: - if anidb_item and self.library.mass_original_title_update == "anidb": - new_original_title = anidb_item.main_title - elif anidb_item and self.library.mass_original_title_update == "anidb_official": - new_original_title = anidb_item.official_title - elif mal_item and self.library.mass_original_title_update == "mal": - new_original_title = mal_item.title - elif mal_item and self.library.mass_original_title_update == "mal_english": - new_original_title = mal_item.title_english - elif mal_item and self.library.mass_original_title_update == "mal_japanese": - new_original_title = mal_item.title_japanese - else: - raise Failed - if not new_original_title: - logger.info("No Original Title Found") - elif str(current_original) != str(new_original_title): - item.editOriginalTitle(new_original_title) - item_edits += f"\nUpdated Original Title | {new_original_title}" - has_edit = True - except Failed: - pass - if self.library.mass_original_title_update in ["unlock", "reset"] and ("originalTitle" in locked_fields or has_edit): - if "originalTitle" not in unlock_edits: - unlock_edits["originalTitle"] = [] - unlock_edits["originalTitle"].append(item.ratingKey) - item_edits += "\nUnlock Original Title (Batched)" - elif self.library.mass_original_title_update in ["lock", "remove"] and "originalTitle" not in locked_fields and not has_edit: - if "originalTitle" not in lock_edits: - lock_edits["originalTitle"] = [] - lock_edits["originalTitle"].append(item.ratingKey) - item_edits += "\nLock Original Title (Batched)" + for option in self.library.mass_original_title_update: + if option in ["lock", "remove"]: + if option == "remove" and current_original: + if "originalTitle" not in remove_edits: + remove_edits["originalTitle"] = [] + remove_edits["originalTitle"].append(item.ratingKey) + item_edits += "\nRemove Original Title (Batched)" + elif "originalTitle" not in locked_fields: + if "originalTitle" not in lock_edits: + lock_edits["originalTitle"] = [] + lock_edits["originalTitle"].append(item.ratingKey) + item_edits += "\nLock Original Title (Batched)" + break + elif option in ["unlock", "reset"]: + if option == "reset" and current_original: + if "originalTitle" not in reset_edits: + reset_edits["originalTitle"] = [] + reset_edits["originalTitle"].append(item.ratingKey) + item_edits += "\nReset Original Title (Batched)" + elif "originalTitle" in locked_fields: + if "originalTitle" not in unlock_edits: + unlock_edits["originalTitle"] = [] + unlock_edits["originalTitle"].append(item.ratingKey) + item_edits += "\nUnlock Original Title (Batched)" + break + else: + try: + if option == "anidb": + new_original_title = anidb_obj().main_title # noqa + elif option == "anidb_official": + new_original_title = anidb_obj().official_title # noqa + elif option == "mal": + new_original_title = mal_obj().title # noqa + elif option == "mal_english": + new_original_title = mal_obj().title_english # noqa + elif option == "mal_japanese": + new_original_title = mal_obj().title_japanese # noqa + else: + new_original_title = option + if not new_original_title: + logger.info(f"No {option} Original Title Found") + raise Failed + if str(current_original) != str(new_original_title): + item.editOriginalTitle(new_original_title) + item_edits += f"\nUpdated Original Title | {new_original_title}" + break + except Failed: + continue if self.library.mass_studio_update: current_studio = item.studio - has_edit = False - if self.library.mass_studio_update == "remove" and current_studio: - if "studio" not in remove_edits: - remove_edits["studio"] = [] - remove_edits["studio"].append(item.ratingKey) - item_edits += "\nRemove Studio (Batched)" - elif self.library.mass_studio_update == "reset" and current_studio: - if "studio" not in reset_edits: - reset_edits["studio"] = [] - reset_edits["studio"].append(item.ratingKey) - item_edits += "\nReset Studio (Batched)" - elif self.library.mass_studio_update not in ["lock", "unlock", "remove", "reset"]: - try: - if anidb_item and self.library.mass_studio_update == "anidb": - new_studio = anidb_item.studio - elif mal_item and self.library.mass_studio_update == "mal": - new_studio = mal_item.studio - elif tmdb_item and self.library.mass_studio_update == "tmdb": - new_studio = tmdb_item.studio - else: - raise Failed - if not new_studio: - logger.info("No Studio Found") - elif str(current_studio) != str(new_studio): - if new_studio not in studio_edits: - studio_edits[new_studio] = [] - studio_edits[new_studio].append(item.ratingKey) - item_edits += f"\nUpdate Studio (Batched) | {new_studio}" - has_edit = True - except Failed: - pass - - if self.library.mass_studio_update in ["unlock", "reset"] and ("studio" in locked_fields or has_edit): - if "studio" not in unlock_edits: - unlock_edits["studio"] = [] - unlock_edits["studio"].append(item.ratingKey) - item_edits += "\nUnlock Studio (Batched)" - elif self.library.mass_studio_update in ["lock", "remove"] and "studio" not in locked_fields and not has_edit: - if "studio" not in lock_edits: - lock_edits["studio"] = [] - lock_edits["studio"].append(item.ratingKey) - item_edits += "\nLock Studio (Batched)" + for option in self.library.mass_studio_update: + if option in ["lock", "remove"]: + if option == "remove" and current_studio: + if "studio" not in remove_edits: + remove_edits["studio"] = [] + remove_edits["studio"].append(item.ratingKey) + item_edits += "\nRemove Studio (Batched)" + elif "studio" not in locked_fields: + if "studio" not in lock_edits: + lock_edits["studio"] = [] + lock_edits["studio"].append(item.ratingKey) + item_edits += "\nLock Studio (Batched)" + break + elif option in ["unlock", "reset"]: + if option == "reset" and current_studio: + if "studio" not in reset_edits: + reset_edits["studio"] = [] + reset_edits["studio"].append(item.ratingKey) + item_edits += "\nReset Studio (Batched)" + elif "studio" in locked_fields: + if "studio" not in unlock_edits: + unlock_edits["studio"] = [] + unlock_edits["studio"].append(item.ratingKey) + item_edits += "\nUnlock Studio (Batched)" + break + else: + try: + if option == "tmdb": + new_studio = tmdb_obj().studio # noqa + elif option == "anidb": + new_studio = anidb_obj().studio # noqa + elif option == "mal": + new_studio = mal_obj().studio # noqa + else: + new_studio = option + if not new_studio: + logger.info(f"No {option} Studio Found") + raise Failed + if str(current_studio) != str(new_studio): + if new_studio not in studio_edits: + studio_edits[new_studio] = [] + studio_edits[new_studio].append(item.ratingKey) + item_edits += f"\nUpdate Studio (Batched) | {new_studio}" + break + except Failed: + continue if self.library.mass_originally_available_update: current_available = item.originallyAvailableAt if current_available: current_available = current_available.strftime("%Y-%m-%d") - has_edit = False - if self.library.mass_originally_available_update == "remove" and current_available: - if "originallyAvailableAt" not in remove_edits: - remove_edits["originallyAvailableAt"] = [] - remove_edits["originallyAvailableAt"].append(item.ratingKey) - item_edits += "\nRemove Originally Available Date (Batched)" - elif self.library.mass_originally_available_update == "reset" and current_available: - if "originallyAvailableAt" not in reset_edits: - reset_edits["originallyAvailableAt"] = [] - reset_edits["originallyAvailableAt"].append(item.ratingKey) - item_edits += "\nReset Originally Available Date (Batched)" - elif self.library.mass_originally_available_update not in ["lock", "unlock", "remove", "reset"]: - try: - if omdb_item and self.library.mass_originally_available_update == "omdb": - new_available = omdb_item.released - elif mdb_item and self.library.mass_originally_available_update == "mdb": - new_available = mdb_item.released - elif tvdb_item and self.library.mass_originally_available_update == "tvdb": - new_available = tvdb_item.release_date - elif tmdb_item and self.library.mass_originally_available_update == "tmdb": - new_available = tmdb_item.release_date if self.library.is_movie else tmdb_item.first_air_date - elif anidb_item and self.library.mass_originally_available_update == "anidb": - new_available = anidb_item.released - elif mal_item and self.library.mass_originally_available_update == "mal": - new_available = mal_item.aired - else: - raise Failed - if new_available: + for option in self.library.mass_originally_available_update: + if option in ["lock", "remove"]: + if option == "remove" and current_available: + if "originallyAvailableAt" not in remove_edits: + remove_edits["originallyAvailableAt"] = [] + remove_edits["originallyAvailableAt"].append(item.ratingKey) + item_edits += "\nRemove Originally Available Date (Batched)" + elif "originallyAvailableAt" not in locked_fields: + if "originallyAvailableAt" not in lock_edits: + lock_edits["originallyAvailableAt"] = [] + lock_edits["originallyAvailableAt"].append(item.ratingKey) + item_edits += "\nLock Originally Available Date (Batched)" + break + elif option in ["unlock", "reset"]: + if option == "reset" and current_available: + if "originallyAvailableAt" not in reset_edits: + reset_edits["originallyAvailableAt"] = [] + reset_edits["originallyAvailableAt"].append(item.ratingKey) + item_edits += "\nReset Originally Available Date (Batched)" + elif "originallyAvailableAt" in locked_fields: + if "originallyAvailableAt" not in unlock_edits: + unlock_edits["originallyAvailableAt"] = [] + unlock_edits["originallyAvailableAt"].append(item.ratingKey) + item_edits += "\nUnlock Originally Available Date (Batched)" + break + else: + try: + if option == "tmdb": + new_available = tmdb_obj().release_date if self.library.is_movie else tmdb_obj().first_air_date # noqa + elif option == "omdb": + new_available = omdb_obj().released # noqa + elif option == "tvdb": + new_available = tvdb_obj().release_date # noqa + elif option == "mdb": + new_available = mdb_obj().released # noqa + elif option == "mdb_digital": + new_available = mdb_obj().released_digital # noqa + elif option == "anidb": + new_available = anidb_obj().released # noqa + elif option == "mal": + new_available = mal_obj().aired # noqa + else: + new_available = option + if not new_available: + logger.info(f"No {option} Originally Available Date Found") + raise Failed new_available = new_available.strftime("%Y-%m-%d") if current_available != new_available: if new_available not in available_edits: available_edits[new_available] = [] available_edits[new_available].append(item.ratingKey) item_edits += f"\nUpdate Originally Available Date (Batched) | {new_available}" - has_edit = True - else: - logger.info("No Originally Available Date Found") - except Failed: - pass - - if self.library.mass_originally_available_update in ["unlock", "reset"] and ("originallyAvailableAt" in locked_fields or has_edit): - if "originallyAvailableAt" not in unlock_edits: - unlock_edits["originallyAvailableAt"] = [] - unlock_edits["originallyAvailableAt"].append(item.ratingKey) - item_edits += "\nUnlock Originally Available Date (Batched)" - elif self.library.mass_originally_available_update in ["lock", "remove"] and "originallyAvailableAt" not in locked_fields and not has_edit: - if "originallyAvailableAt" not in lock_edits: - lock_edits["originallyAvailableAt"] = [] - lock_edits["originallyAvailableAt"].append(item.ratingKey) - item_edits += "\nLock Originally Available Date (Batched)" + break + except Failed: + continue if len(item_edits) > 0: logger.info(f"Item Edits{item_edits}") @@ -626,10 +734,14 @@ class Operations: name = None new_poster = None new_background = None + try: + tmdb_item = tmdb_obj() + except Failed: + tmdb_item = None if self.library.mass_poster_update: - self.library.poster_update(item, new_poster, tmdb=tmdb_item.poster_url if tmdb_item else None, title=item.title) + self.library.poster_update(item, new_poster, tmdb=tmdb_item.poster_url if tmdb_item else None, title=item.title) # noqa if self.library.mass_background_update: - self.library.background_update(item, new_background, tmdb=tmdb_item.backdrop_url if tmdb_item else None, title=item.title) + self.library.background_update(item, new_background, tmdb=tmdb_item.backdrop_url if tmdb_item else None, title=item.title) # noqa if self.library.is_show and ( (self.library.mass_poster_update and @@ -639,7 +751,7 @@ class Operations: ): real_show = None try: - real_show = tmdb_item.load_show() if tmdb_item else None + real_show = tmdb_item.load_show() if tmdb_item else None # noqa except Failed as e: logger.error(e) tmdb_seasons = {s.season_number: s for s in real_show.seasons} if real_show else {} @@ -725,10 +837,14 @@ class Operations: ep_lock_edits[item_attr].append(ep) item_edits += f"\nLock {name_display[item_attr]} (Batched)" elif attribute not in ["lock", "unlock", "remove", "reset"]: + try: + tmdb_item = tmdb_obj() + except Failed: + tmdb_item = None found_rating = None if tmdb_item and attribute == "tmdb": try: - found_rating = self.config.TMDb.get_episode(tmdb_item.tmdb_id, ep.seasonNumber, ep.episodeNumber).vote_average + found_rating = self.config.TMDb.get_episode(tmdb_item.tmdb_id, ep.seasonNumber, ep.episodeNumber).vote_average # noqa except Failed as er: logger.error(er) elif imdb_id and attribute == "imdb": diff --git a/modules/util.py b/modules/util.py index a7df58de..f0deff2c 100644 --- a/modules/util.py +++ b/modules/util.py @@ -724,13 +724,16 @@ def schedule_check(attribute, data, current_time, run_hour, is_all=False): raise NotScheduled(schedule_str) return schedule_str -def check_int(value, datatype="int", minimum=1, maximum=None): +def check_int(value, datatype="int", minimum=1, maximum=None, throw=False): try: value = int(str(value)) if datatype == "int" else float(str(value)) if (maximum is None and minimum <= value) or (maximum is not None and minimum <= value <= maximum): return value except ValueError: - pass + if throw: + message = f"{value} must be {'an integer' if datatype == 'int' else 'a number'}" + raise Failed(f"{message} {minimum} or greater" if maximum is None else f"{message} between {minimum} and {maximum}") + return None def parse_and_or(error, attribute, data, test_list): out = "" diff --git a/modules/webhooks.py b/modules/webhooks.py index c7d776cf..d304f79c 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -5,7 +5,7 @@ from modules.util import Failed, YAML logger = util.logger class Webhooks: - def __init__(self, config, system_webhooks, library=None, notifiarr=None): + def __init__(self, config, system_webhooks, library=None, notifiarr=None, gotify=None): self.config = config self.error_webhooks = system_webhooks["error"] if "error" in system_webhooks else [] self.version_webhooks = system_webhooks["version"] if "version" in system_webhooks else [] @@ -14,6 +14,7 @@ class Webhooks: self.delete_webhooks = system_webhooks["delete"] if "delete" in system_webhooks else [] self.library = library self.notifiarr = notifiarr + self.gotify = gotify def _request(self, webhooks, json): logger.trace("") @@ -30,6 +31,9 @@ class Webhooks: response = self.notifiarr.notification(json) if response.status_code < 500: break + elif webhook == "gotify": + if self.gotify: + self.gotify.notification(json) else: if webhook.startswith("https://discord.com/api/webhooks"): json = self.discord(json) @@ -326,3 +330,4 @@ class Webhooks: fields.append(field) new_json["embeds"][0]["fields"] = fields return new_json + diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 8bfb817b..a2c14599 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -927,11 +927,11 @@ def run_playlists(config): #logger.add_playlist_handler(playlist_log_name) status[mapping_name] = {"status": "Unchanged", "errors": [], "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0} server_name = None - library_names = None try: builder = CollectionBuilder(config, playlist_file, mapping_name, playlist_attrs, extra=output_str) stats["names"].append(builder.name) logger.info("") + server_name = builder.libraries[0].PlexServer.friendlyName logger.separator(f"Running {mapping_name} Playlist", space=False, border=False) @@ -1049,7 +1049,7 @@ def run_playlists(config): except Deleted as e: logger.info(e) status[mapping_name]["status"] = "Deleted" - config.notify_delete(e) + config.notify_delete(e, server=server_name) except NotScheduled as e: logger.info(e) if str(e).endswith("and was deleted"): @@ -1059,13 +1059,13 @@ def run_playlists(config): else: status[mapping_name]["status"] = "Not Scheduled" except Failed as e: - config.notify(e, server=server_name, library=library_names, playlist=mapping_name) + config.notify(e, server=server_name, playlist=mapping_name) logger.stacktrace() logger.error(e) status[mapping_name]["status"] = "PMM Failure" status[mapping_name]["errors"].append(e) except Exception as e: - config.notify(f"Unknown Error: {e}", server=server_name, library=library_names, playlist=mapping_name) + config.notify(f"Unknown Error: {e}", server=server_name, playlist=mapping_name) logger.stacktrace() logger.error(f"Unknown Error: {e}") status[mapping_name]["status"] = "Unknown Error"