[57] TVDb Filters

pull/1964/head
meisnate12 8 months ago
parent 437f86f942
commit 265c2b8294

@ -2,7 +2,7 @@
Updated gitpython requirement to 3.1.42 Updated gitpython requirement to 3.1.42
Updated plexapi requirement to 4.5.10 Updated plexapi requirement to 4.5.10
Updated setuptools requirement to 69.1.1 Updated setuptools requirement to 69.1.1
Updated tmdbapis requirement to 1.2.7 Updated tmdbapis requirement to 1.2.9
# Removed Features # 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. 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 `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 [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 [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 # 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. 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.

@ -1 +1 @@
1.20.0-develop56 1.20.0-develop57

@ -57,9 +57,11 @@ String filters can take multiple values **only as a list**.
### Attribute ### Attribute
| String Filter | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | | 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 } | | `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`<sup>**[2](#table-annotations)**</sup> | 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 } | | `tmdb_title`<sup>**[2](#table-annotations)**</sup> | 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`<sup>**[2](#table-annotations)**</sup> | 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`<sup>**[2](#table-annotations)**</sup> | 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 } | | `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 } | | `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 } | | `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 } |
@ -111,6 +113,7 @@ Tag filters can take multiple values as a **list or a comma-separated string**.
| `tmdb_genre`<sup>**[2](#table-annotations)**</sup> | 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_genre`<sup>**[2](#table-annotations)**</sup> | 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`<sup>**[2](#table-annotations)**</sup> | 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 } | | `tmdb_keyword`<sup>**[2](#table-annotations)**</sup> | 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`<sup>**[2](#table-annotations)**</sup> | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match<br>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 } | | `origin_country`<sup>**[2](#table-annotations)**</sup> | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match<br>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`<sup>**[2](#table-annotations)**</sup> | 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`<sup>**[2](#table-annotations)**</sup> | 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 } | | `imdb_keyword`<sup>**[2](#table-annotations)**</sup> | 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 ## Boolean Filters

@ -89,7 +89,7 @@ filters_by_type = {
"show_season": ["episodes"], "show_season": ["episodes"],
"artist_album": ["tracks"], "artist_album": ["tracks"],
"movie": ["edition", "has_edition", "stinger_rating", "has_stinger"], "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"], "artist": ["albums"],
"album": ["record_label"] "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", "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" "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"] imdb_filters = ["imdb_keyword"]
string_filters = [ string_filters = [
"title", "summary", "studio", "edition", "record_label", "folder", "filepath", "audio_track_title", "subtitle_track_title", "tmdb_title", "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"] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"]
tag_filters = [ tag_filters = [
"actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", "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"] 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"] boolean_filters = ["has_collection", "has_edition", "has_overlay", "has_dolby_vision", "has_stinger"]
@ -2886,6 +2887,13 @@ class CollectionBuilder:
return False return False
return True 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): def check_imdb_filters(self, imdb_info, filters_in):
for filter_method, filter_data in filters_in: for filter_method, filter_data in filters_in:
filter_attr, modifier, filter_final = self.library.split(filter_method) filter_attr, modifier, filter_final = self.library.split(filter_method)
@ -2943,14 +2951,18 @@ class CollectionBuilder:
item = self.library.reload(item) item = self.library.reload(item)
final_return = False final_return = False
tmdb_item = None tmdb_item = None
tvdb_item = None
imdb_info = None imdb_info = None
for filter_list in self.filters: for filter_list in self.filters:
tmdb_f = [] tmdb_f = []
tvdb_f = []
imdb_f = [] imdb_f = []
plex_f = [] plex_f = []
for k, v in filter_list: for k, v in filter_list:
if k.split(".")[0] in tmdb_filters: if k.split(".")[0] in tmdb_filters:
tmdb_f.append((k, v)) tmdb_f.append((k, v))
elif k.split(".")[0] in tvdb_filters:
tvdb_f.append((k, v))
elif k.split(".")[0] in imdb_filters: elif k.split(".")[0] in imdb_filters:
imdb_f.append((k, v)) imdb_f.append((k, v))
else: else:
@ -2972,6 +2984,19 @@ class CollectionBuilder:
or_result = False 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: 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 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 imdb_f:
if not imdb_info and isinstance(item, (Movie, Show)): if not imdb_info and isinstance(item, (Movie, Show)):
if item.ratingKey not in self.library.imdb_rating_key_map: if item.ratingKey not in self.library.imdb_rating_key_map:

@ -17,26 +17,13 @@ class Cache:
logger.info(f"Initializing cache database at {self.cache_path}") logger.info(f"Initializing cache database at {self.cache_path}")
else: else:
logger.info(f"Using cache database at {self.cache_path}") logger.info(f"Using cache database at {self.cache_path}")
cursor.execute("DROP TABLE IF EXISTS guids") for old_table in [
cursor.execute("DROP TABLE IF EXISTS guid_map") "guids", "guid_map", "imdb_to_tvdb_map", "tmdb_to_tvdb_map", "imdb_map",
cursor.execute("DROP TABLE IF EXISTS imdb_to_tvdb_map") "mdb_data", "mdb_data2", "mdb_data3", "mdb_data4", "omdb_data", "omdb_data2",
cursor.execute("DROP TABLE IF EXISTS tmdb_to_tvdb_map") "tvdb_data", "tvdb_data2", "tvdb_data3", "tmdb_show_data", "tmdb_show_data2",
cursor.execute("DROP TABLE IF EXISTS imdb_map") "overlay_ratings", "anidb_data", "anidb_data2", "anidb_data3", "mal_data"
cursor.execute("DROP TABLE IF EXISTS mdb_data") ]:
cursor.execute("DROP TABLE IF EXISTS mdb_data2") cursor.execute(f"DROP TABLE IF EXISTS {old_table}")
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")
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS guids_map ( """CREATE TABLE IF NOT EXISTS guids_map (
key INTEGER PRIMARY KEY, key INTEGER PRIMARY KEY,
@ -231,11 +218,12 @@ class Cache:
expiration_date TEXT)""" expiration_date TEXT)"""
) )
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS tvdb_data3 ( """CREATE TABLE IF NOT EXISTS tvdb_data4 (
key INTEGER PRIMARY KEY, key INTEGER PRIMARY KEY,
tvdb_id INTEGER UNIQUE, tvdb_id INTEGER UNIQUE,
type TEXT, type TEXT,
title TEXT, title TEXT,
status TEXT,
summary TEXT, summary TEXT,
poster_url TEXT, poster_url TEXT,
background_url TEXT, background_url TEXT,
@ -783,12 +771,13 @@ class Cache:
with sqlite3.connect(self.cache_path) as connection: with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: 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() row = cursor.fetchone()
if row: if row:
tvdb_dict["tvdb_id"] = int(row["tvdb_id"]) if row["tvdb_id"] else 0 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["type"] = row["type"] if row["type"] else ""
tvdb_dict["title"] = row["title"] if row["title"] 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["summary"] = row["summary"] if row["summary"] else ""
tvdb_dict["poster_url"] = row["poster_url"] if row["poster_url"] 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 "" 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: with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: 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")) 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_data3 SET title = ?, summary = ?, poster_url = ?, background_url = ?, " \ update_sql = "UPDATE tvdb_data4 SET title = ?, status = ?, summary = ?, poster_url = ?, background_url = ?, " \
"release_date = ?, genres = ?, expiration_date = ? WHERE tvdb_id = ? AND type = ?" "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 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, ( 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" expiration_date.strftime("%Y-%m-%d"), obj.tvdb_id, "movie" if obj.is_movie else "show"
)) ))

@ -63,6 +63,17 @@ mass_content_options = {
"mdb_age_rating": "Use MDbList Age Rating", "mdb_age_rating0": "Use MDbList Age Rating with Zero Padding", "mdb_age_rating": "Use MDbList Age Rating", "mdb_age_rating0": "Use MDbList Age Rating with Zero Padding",
"mal": "Use MyAnimeList Rating" "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 = { mass_studio_options = {
"lock": "Lock Rating", "unlock": "Unlock Rating", "remove": "Remove and Lock Rating", "reset": "Remove and Unlock Rating", "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" "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 = { library_operations = {
"assets_for_all": "bool", "split_duplicates": "bool", "update_blank_track_titles": "bool", "remove_title_parentheses": "bool", "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", "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_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_critic_rating_update": mass_rating_options, "mass_episode_critic_rating_update": mass_episode_rating_options,
"mass_user_rating_update": mass_rating_options, "mass_episode_user_rating_update": mass_episode_rating_options, "mass_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]) section_final[op] = util.check_collection_mode(config_op[op])
elif data_type == "dict": elif data_type == "dict":
input_dict = config_op[op] 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} input_dict = {"source": input_dict}
if not input_dict or not isinstance(input_dict, dict): if not input_dict or not isinstance(input_dict, dict):
raise Failed(f"Config Error: {op} must be a dictionary") 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] = { section_final[op] = {
"source": check_for_attribute(input_dict, "source", test_list=mass_image_options, default_is_none=True, save=False), "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), "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), "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") default_path = os.path.join(default_dir, f"{str(library_name)}_Metadata_Backup.yml")
if "path" not in input_dict: if "path" not in input_dict:
logger.warning(f"Config Warning: path attribute not found using default: {default_path}") 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), "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) "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] = {} section_final[op] = {}
for old_value, new_value in input_dict.items(): for old_value, new_value in input_dict.items():
if not old_value: 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") logger.warning(f"Config Warning: {op} value '{new_value}' ignored as it cannot be mapped to itself")
else: else:
section_final[op][str(old_value)] = str(new_value) if new_value else None # noqa 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] = { section_final[op] = {
"managed": check_for_attribute(input_dict, "managed", var_type="bool", default_is_none=True, save=False), "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), "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), "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: else:
section_final[op] = check_for_attribute(config_op, op, var_type=data_type, default=False, save=False) section_final[op] = check_for_attribute(config_op, op, var_type=data_type, default=False, save=False)

@ -1,4 +1,4 @@
import requests, time import re, requests, time
from datetime import datetime from datetime import datetime
from lxml import html from lxml import html
from lxml.etree import ParserError from lxml.etree import ParserError
@ -74,6 +74,7 @@ class TVDbObj:
self.poster_url = data["poster_url"] self.poster_url = data["poster_url"]
self.background_url = data["background_url"] self.background_url = data["background_url"]
self.release_date = data["release_date"] self.release_date = data["release_date"]
self.status = data["status"]
self.genres = data["genres"].split("|") self.genres = data["genres"].split("|")
else: else:
self.title, self.summary = parse_title_summary(lang=self._tvdb.language) 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 self.release_date = datetime.strptime(released, "%B %d, %Y") if released else released # noqa
except ValueError: except ValueError:
self.release_date = None 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) 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) return self._ids_from_url(data)
else: else:
raise Failed(f"TVDb Error: Method {method} not supported") 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

Loading…
Cancel
Save