From 265c2b82942118609d044de3c945c4e7d438fe47 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 3 Apr 2024 08:45:43 -0400 Subject: [PATCH] [57] TVDb Filters --- CHANGELOG | 3 ++- VERSION | 2 +- docs/files/filters.md | 33 ++++++++++++++++++--------------- modules/builder.py | 31 ++++++++++++++++++++++++++++--- modules/cache.py | 39 ++++++++++++++------------------------- modules/config.py | 29 +++++++++++++++++++++++------ modules/tvdb.py | 31 ++++++++++++++++++++++++++++++- 7 files changed, 116 insertions(+), 52 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index db045c2e..7eef2873 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,7 @@ Updated gitpython requirement to 3.1.42 Updated plexapi requirement to 4.5.10 Updated setuptools requirement to 69.1.1 -Updated tmdbapis requirement to 1.2.7 +Updated tmdbapis requirement to 1.2.9 # Removed Features Due to FlixPatrol moving a lot of their data behind a paywall and them reworking their pages to remove IMDb IDs and TMDb IDs the flixpatrol builders and default files have been removed. There currently are no plans to re-add them. @@ -12,6 +12,7 @@ Added new [BoxOfficeMojo Builder](https://metamanager.wiki/en/latest/files/build 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. 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 +Added TVDb filters # Updates Reworked PMM Default Streaming [Collections](https://metamanager.wiki/en/latest/defaults/both/streaming) and [Overlays](https://metamanager.wiki/en/latest/defaults/overlays/streaming) to utilize TMDB Watch Provider data, allowing users to customize regions without relying on mdblist. This data will be more accurate and up-to-date now. diff --git a/VERSION b/VERSION index b0d758ad..9b7729ef 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.0-develop56 +1.20.0-develop57 diff --git a/docs/files/filters.md b/docs/files/filters.md index 0abd1c23..b702f4d0 100644 --- a/docs/files/filters.md +++ b/docs/files/filters.md @@ -56,21 +56,23 @@ String filters can take multiple values **only as a list**. ### Attribute -| String Filter | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | -|:---------------------------------------------------|:-----------------------------------------|:------------------------------------------:|:--------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------:|:-------------------------------------------:|:--------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------:|:------------------------------------------:| -| `title` | Uses the title attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| `tmdb_title`**[2](#table-annotations)** | Uses the title from TMDb to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| `summary` | Uses the summary attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| `studio` | Uses the studio attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| `edition` | Uses the edition attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| `record_label` | Uses the record label attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| `folder` | Uses the item's folder to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| `filepath` | Uses the item's filepath to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | -| `audio_track_title` | Uses the audio track titles to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | -| `video_codec` | Uses the video codec tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| `video_profile` | Uses the video profile tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| `audio_codec` | Uses the audio codec tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| `audio_profile` | Uses the audio profile tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| String Filter | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | +|:----------------------------------------------------|:-----------------------------------------|:------------------------------------------:|:--------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------:|:-------------------------------------------:|:--------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------:|:------------------------------------------:| +| `title` | Uses the title attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `tmdb_title`**[2](#table-annotations)** | Uses the title from TMDb to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `tvdb_title`**[2](#table-annotations)** | Uses the title from TVDb to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `tvdb_status`**[2](#table-annotations)** | Uses the status from TVDb to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `summary` | Uses the summary attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `studio` | Uses the studio attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `edition` | Uses the edition attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `record_label` | Uses the record label attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `folder` | Uses the item's folder to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `filepath` | Uses the item's filepath to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | +| `audio_track_title` | Uses the audio track titles to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | +| `video_codec` | Uses the video codec tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `video_profile` | Uses the video profile tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `audio_codec` | Uses the audio codec tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `audio_profile` | Uses the audio profile tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green }**[1](#table-annotations)** | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | ## Tag Filters @@ -111,6 +113,7 @@ Tag filters can take multiple values as a **list or a comma-separated string**. | `tmdb_genre`**[2](#table-annotations)** | Uses the genres from TMDb to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | | `tmdb_keyword`**[2](#table-annotations)** | Uses the keywords from TMDb to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | | `origin_country`**[2](#table-annotations)** | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `tvdb_genre`**[2](#table-annotations)** | Uses the genres from TVDb to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | | `imdb_keyword`**[2](#table-annotations)** | Uses the keywords from IMDb to match See [Special](#special-filters) for more attributes | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | ## Boolean Filters diff --git a/modules/builder.py b/modules/builder.py index a583cf76..90787b53 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -89,7 +89,7 @@ filters_by_type = { "show_season": ["episodes"], "artist_album": ["tracks"], "movie": ["edition", "has_edition", "stinger_rating", "has_stinger"], - "show": ["seasons", "tmdb_status", "tmdb_type", "origin_country", "network", "first_episode_aired", "last_episode_aired", "last_episode_aired_or_never"], + "show": ["seasons", "tmdb_status", "tmdb_type", "origin_country", "network", "first_episode_aired", "last_episode_aired", "last_episode_aired_or_never", "tvdb_title", "tvdb_status", "tvdb_genre"], "artist": ["albums"], "album": ["record_label"] } @@ -106,15 +106,16 @@ tmdb_filters = [ "original_language", "origin_country", "tmdb_vote_count", "tmdb_vote_average", "tmdb_year", "tmdb_keyword", "tmdb_genre", "first_episode_aired", "last_episode_aired", "last_episode_aired_or_never", "tmdb_status", "tmdb_type", "tmdb_title" ] +tvdb_filters = ["tvdb_title", "tvdb_status", "tvdb_genre"] imdb_filters = ["imdb_keyword"] string_filters = [ "title", "summary", "studio", "edition", "record_label", "folder", "filepath", "audio_track_title", "subtitle_track_title", "tmdb_title", - "audio_codec", "audio_profile", "video_codec", "video_profile" + "audio_codec", "audio_profile", "video_codec", "video_profile", "tvdb_title", "tvdb_status" ] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"] tag_filters = [ "actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", - "origin_country", "writer", "resolution", "audio_language", "subtitle_language", "tmdb_keyword", "tmdb_genre", "imdb_keyword" + "origin_country", "writer", "resolution", "audio_language", "subtitle_language", "tmdb_keyword", "tmdb_genre", "imdb_keyword", "tvdb_genre" ] tag_modifiers = ["", ".not", ".regex", ".count_gt", ".count_gte", ".count_lt", ".count_lte"] boolean_filters = ["has_collection", "has_edition", "has_overlay", "has_dolby_vision", "has_stinger"] @@ -2886,6 +2887,13 @@ class CollectionBuilder: return False return True + def check_tvdb_filters(self, tvdb_item, filters_in): + for filter_method, filter_data in filters_in: + filter_attr, modifier, filter_final = self.library.split(filter_method) + if self.config.TVDb.item_filter(tvdb_item, filter_attr, modifier, filter_final, filter_data) is False: + return False + return True + def check_imdb_filters(self, imdb_info, filters_in): for filter_method, filter_data in filters_in: filter_attr, modifier, filter_final = self.library.split(filter_method) @@ -2943,14 +2951,18 @@ class CollectionBuilder: item = self.library.reload(item) final_return = False tmdb_item = None + tvdb_item = None imdb_info = None for filter_list in self.filters: tmdb_f = [] + tvdb_f = [] imdb_f = [] plex_f = [] for k, v in filter_list: if k.split(".")[0] in tmdb_filters: tmdb_f.append((k, v)) + elif k.split(".")[0] in tvdb_filters: + tvdb_f.append((k, v)) elif k.split(".")[0] in imdb_filters: imdb_f.append((k, v)) else: @@ -2972,6 +2984,19 @@ class CollectionBuilder: or_result = False if not tmdb_item or self.check_tmdb_filters(tmdb_item, tmdb_f, item.ratingKey in self.library.movie_rating_key_map) is False: or_result = False + if tvdb_f: + if not tvdb_item and isinstance(item, Show): + if item.ratingKey not in self.library.show_rating_key_map: + logger.warning(f"Filter Error: No TVDb ID found for {item.title}") + or_result = False + else: + try: + tvdb_item = self.config.TVDb.get_tvdb_obj(self.library.show_rating_key_map[item.ratingKey]) + except Failed as e: + logger.error(e) + or_result = False + if not tvdb_item or self.check_tvdb_filters(tvdb_item, tvdb_f) is False: + or_result = False if imdb_f: if not imdb_info and isinstance(item, (Movie, Show)): if item.ratingKey not in self.library.imdb_rating_key_map: diff --git a/modules/cache.py b/modules/cache.py index 34d886c9..cbee7fc6 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -17,26 +17,13 @@ class Cache: logger.info(f"Initializing cache database at {self.cache_path}") else: logger.info(f"Using cache database at {self.cache_path}") - cursor.execute("DROP TABLE IF EXISTS guids") - cursor.execute("DROP TABLE IF EXISTS guid_map") - cursor.execute("DROP TABLE IF EXISTS imdb_to_tvdb_map") - cursor.execute("DROP TABLE IF EXISTS tmdb_to_tvdb_map") - cursor.execute("DROP TABLE IF EXISTS imdb_map") - 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") - cursor.execute("DROP TABLE IF EXISTS tvdb_data2") - cursor.execute("DROP TABLE IF EXISTS tmdb_show_data") - cursor.execute("DROP TABLE IF EXISTS tmdb_show_data2") - cursor.execute("DROP TABLE IF EXISTS overlay_ratings") - cursor.execute("DROP TABLE IF EXISTS anidb_data") - cursor.execute("DROP TABLE IF EXISTS anidb_data2") - cursor.execute("DROP TABLE IF EXISTS anidb_data3") - cursor.execute("DROP TABLE IF EXISTS mal_data") + for old_table in [ + "guids", "guid_map", "imdb_to_tvdb_map", "tmdb_to_tvdb_map", "imdb_map", + "mdb_data", "mdb_data2", "mdb_data3", "mdb_data4", "omdb_data", "omdb_data2", + "tvdb_data", "tvdb_data2", "tvdb_data3", "tmdb_show_data", "tmdb_show_data2", + "overlay_ratings", "anidb_data", "anidb_data2", "anidb_data3", "mal_data" + ]: + cursor.execute(f"DROP TABLE IF EXISTS {old_table}") cursor.execute( """CREATE TABLE IF NOT EXISTS guids_map ( key INTEGER PRIMARY KEY, @@ -231,11 +218,12 @@ class Cache: expiration_date TEXT)""" ) cursor.execute( - """CREATE TABLE IF NOT EXISTS tvdb_data3 ( + """CREATE TABLE IF NOT EXISTS tvdb_data4 ( key INTEGER PRIMARY KEY, tvdb_id INTEGER UNIQUE, type TEXT, title TEXT, + status TEXT, summary TEXT, poster_url TEXT, background_url TEXT, @@ -783,12 +771,13 @@ 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 tvdb_data3 WHERE tvdb_id = ? and type = ?", (tvdb_id, "movie" if is_movie else "show")) + cursor.execute("SELECT * FROM tvdb_data4 WHERE tvdb_id = ? and type = ?", (tvdb_id, "movie" if is_movie else "show")) row = cursor.fetchone() if row: tvdb_dict["tvdb_id"] = int(row["tvdb_id"]) if row["tvdb_id"] else 0 tvdb_dict["type"] = row["type"] if row["type"] else "" tvdb_dict["title"] = row["title"] if row["title"] else "" + tvdb_dict["status"] = row["status"] if row["status"] else "" tvdb_dict["summary"] = row["summary"] if row["summary"] else "" tvdb_dict["poster_url"] = row["poster_url"] if row["poster_url"] else "" tvdb_dict["background_url"] = row["background_url"] if row["background_url"] else "" @@ -804,12 +793,12 @@ 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 tvdb_data3(tvdb_id, type) VALUES(?, ?)", (obj.tvdb_id, "movie" if obj.is_movie else "show")) - update_sql = "UPDATE tvdb_data3 SET title = ?, summary = ?, poster_url = ?, background_url = ?, " \ + cursor.execute("INSERT OR IGNORE INTO tvdb_data4(tvdb_id, type) VALUES(?, ?)", (obj.tvdb_id, "movie" if obj.is_movie else "show")) + update_sql = "UPDATE tvdb_data4 SET title = ?, status = ?, summary = ?, poster_url = ?, background_url = ?, " \ "release_date = ?, genres = ?, expiration_date = ? WHERE tvdb_id = ? AND type = ?" tvdb_date = f"{str(obj.release_date.year).zfill(4)}-{str(obj.release_date.month).zfill(2)}-{str(obj.release_date.day).zfill(2)}" if obj.release_date else None cursor.execute(update_sql, ( - obj.title, obj.summary, obj.poster_url, obj.background_url, tvdb_date, "|".join(obj.genres), + obj.title, obj.status, obj.summary, obj.poster_url, obj.background_url, tvdb_date, "|".join(obj.genres), expiration_date.strftime("%Y-%m-%d"), obj.tvdb_id, "movie" if obj.is_movie else "show" )) diff --git a/modules/config.py b/modules/config.py index f9b847e1..3b09bae2 100644 --- a/modules/config.py +++ b/modules/config.py @@ -63,6 +63,17 @@ mass_content_options = { "mdb_age_rating": "Use MDbList Age Rating", "mdb_age_rating0": "Use MDbList Age Rating with Zero Padding", "mal": "Use MyAnimeList Rating" } +mass_collection_content_options = { + "lock": "Lock Rating", "unlock": "Unlock Rating", "remove": "Remove and Lock Rating", "reset": "Remove and Unlock Rating", + "highest": "Highest Rating in the collection", "lowest": "Lowest Rating in the collection", + "average": "Highest Rating in the collection" +} +content_rating_default = { + 1: [ + + ] + +} mass_studio_options = { "lock": "Lock Rating", "unlock": "Unlock Rating", "remove": "Remove and Lock Rating", "reset": "Remove and Unlock Rating", "tmdb": "Use TMDb Studio", "anidb": "Use AniDB Animation Work", "mal": "Use MyAnimeList Studio" @@ -113,7 +124,8 @@ reset_overlay_options = {"tmdb": "Reset to TMDb poster", "plex": "Reset to Plex 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": "str", "sonarr_add_all_existing": "bool", "sonarr_remove_by_tag": "str", - "mass_genre_update": mass_genre_options, "mass_content_rating_update": mass_content_options, "mass_studio_update": mass_studio_options, + "mass_content_rating_update": mass_content_options, "mass_collection_content_rating_update": "dict", + "mass_genre_update": mass_genre_options, "mass_studio_update": mass_studio_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, @@ -904,18 +916,18 @@ class ConfigFile: section_final[op] = util.check_collection_mode(config_op[op]) elif data_type == "dict": input_dict = config_op[op] - if op in ["mass_poster_update", "mass_background_update"] and input_dict and not isinstance(input_dict, dict): + if op in ["mass_poster_update", "mass_background_update", "mass_collection_content_rating_update"] and input_dict and not isinstance(input_dict, dict): input_dict = {"source": input_dict} if not input_dict or not isinstance(input_dict, dict): raise Failed(f"Config Error: {op} must be a dictionary") - if op in ["mass_poster_update", "mass_background_update"]: + elif op in ["mass_poster_update", "mass_background_update"]: section_final[op] = { "source": check_for_attribute(input_dict, "source", test_list=mass_image_options, default_is_none=True, save=False), "seasons": check_for_attribute(input_dict, "seasons", var_type="bool", default=True, save=False), "episodes": check_for_attribute(input_dict, "episodes", var_type="bool", default=True, save=False), } - if op == "metadata_backup": + elif op == "metadata_backup": default_path = os.path.join(default_dir, f"{str(library_name)}_Metadata_Backup.yml") if "path" not in input_dict: logger.warning(f"Config Warning: path attribute not found using default: {default_path}") @@ -929,7 +941,7 @@ class ConfigFile: "sync_tags": check_for_attribute(input_dict, "sync_tags", var_type="bool", default=False, save=False), "add_blank_entries": check_for_attribute(input_dict, "add_blank_entries", var_type="bool", default=True, save=False) } - if "mapper" in op: + elif "mapper" in op: section_final[op] = {} for old_value, new_value in input_dict.items(): if not old_value: @@ -938,12 +950,17 @@ class ConfigFile: logger.warning(f"Config Warning: {op} value '{new_value}' ignored as it cannot be mapped to itself") else: section_final[op][str(old_value)] = str(new_value) if new_value else None # noqa - if op == "delete_collections": + elif op == "delete_collections": section_final[op] = { "managed": check_for_attribute(input_dict, "managed", var_type="bool", default_is_none=True, save=False), "configured": check_for_attribute(input_dict, "configured", var_type="bool", default_is_none=True, save=False), "less": check_for_attribute(input_dict, "less", var_type="int", default_is_none=True, save=False, int_min=1), } + elif op == "mass_collection_content_rating_update": + section_final[op] = { + "source": check_for_attribute(input_dict, "source", test_list=mass_collection_content_options, default_is_none=True, save=False), + "ranking": check_for_attribute(input_dict, "ranking", var_type="list", default=content_rating_default, save=False), + } else: section_final[op] = check_for_attribute(config_op, op, var_type=data_type, default=False, save=False) diff --git a/modules/tvdb.py b/modules/tvdb.py index 3237e67b..7ea8a4f0 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -1,4 +1,4 @@ -import requests, time +import re, requests, time from datetime import datetime from lxml import html from lxml.etree import ParserError @@ -74,6 +74,7 @@ class TVDbObj: self.poster_url = data["poster_url"] self.background_url = data["background_url"] self.release_date = data["release_date"] + self.status = data["status"] self.genres = data["genres"].split("|") else: self.title, self.summary = parse_title_summary(lang=self._tvdb.language) @@ -95,6 +96,7 @@ class TVDbObj: self.release_date = datetime.strptime(released, "%B %d, %Y") if released else released # noqa except ValueError: self.release_date = None + self.status = parse_page("//strong[text()='Status']/parent::li/span/text()[normalize-space()]") self.genres = parse_page("//strong[text()='Genres']/parent::li/span/a/text()[normalize-space()]", is_list=True) @@ -249,3 +251,30 @@ class TVDb: return self._ids_from_url(data) else: raise Failed(f"TVDb Error: Method {method} not supported") + + def item_filter(self, item, filter_attr, modifier, filter_final, filter_data): + if filter_attr == "tvdb_title": + if util.is_string_filter([item.title], modifier, filter_data): + return False + elif filter_attr == "tvdb_status": + if util.is_string_filter([item.status], modifier, filter_data): + return False + elif filter_attr == "tvdb_genre": + attrs = item.genres + if modifier == ".regex": + has_match = False + for reg in filter_data: + for name in attrs: + if re.compile(reg).search(name): + has_match = True + if has_match is False: + return False + elif modifier in [".count_gt", ".count_gte", ".count_lt", ".count_lte"]: + test_number = len(attrs) if attrs else 0 + modifier = f".{modifier[7:]}" + if test_number is None or util.is_number_filter(test_number, modifier, filter_data): + return False + elif (not list(set(filter_data) & set(attrs)) and modifier == "") \ + or (list(set(filter_data) & set(attrs)) and modifier == ".not"): + return False + return True