mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2025-02-16 22:08:25 +00:00
commit
82584fabd3
19 changed files with 1190 additions and 991 deletions
22
README.md
22
README.md
|
@ -1,5 +1,5 @@
|
|||
# Plex Meta Manager
|
||||
#### Version 1.6.4
|
||||
#### Version 1.7.0
|
||||
|
||||
The original concept for Plex Meta Manager is [Plex Auto Collections](https://github.com/mza921/Plex-Auto-Collections), but this is rewritten from the ground up to be able to include a scheduler, metadata edits, multiple libraries, and logging. Plex Meta Manager is a Python 3 script that can be continuously run using YAML configuration files to update on a schedule the metadata of the movies, shows, and collections in your libraries as well as automatically build collections based on various methods all detailed in the wiki. Some collection examples that the script can automatically build and update daily include Plex Based Searches like actor, genre, or studio collections or Collections based on TMDb, IMDb, Trakt, TVDb, AniDB, or MyAnimeList lists and various other services.
|
||||
|
||||
|
@ -11,15 +11,17 @@ The script is designed to work with most Metadata agents including the new Plex
|
|||
|
||||
## Getting Started
|
||||
|
||||
* [Wiki](https://github.com/meisnate12/Plex-Meta-Manager/wiki)
|
||||
* [Local Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Local-Installation)
|
||||
* [Docker Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Docker)
|
||||
1. Install Plex Meta Manager either by installing Python3 and following the [Local Installation Guide](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Local-Installation)
|
||||
or by installing Docker and following the [Docker Installation Guide](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Docker-Installation) or the [unRAID Installation Guide](https://github.com/meisnate12/Plex-Meta-Manager/wiki/unRAID-Installation)
|
||||
2. Once installed, you have to create a [Configuration File](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Configuration-File) filled with all your values to connect to the various services.
|
||||
3. After that you can start updating Metadata and building automatic Collections by creating a [Metadata File](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Metadata-File) for each Library you want to interact with.
|
||||
4. Explore the [Wiki](https://github.com/meisnate12/Plex-Meta-Manager/wiki) to see all the different Collection Builders that can be used to create collections.
|
||||
|
||||
## Support
|
||||
|
||||
* Before posting on Github about an enhancement, error, or configuration question please visit the [Plex Meta Manager Discord Server](https://discord.gg/NfH6mGFuAB)
|
||||
* If you're getting an error or have an enhancement post in the [Issues](https://github.com/meisnate12/Plex-Meta-Manager/issues)
|
||||
* If you have a configuration question visit the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions)
|
||||
* To see user submitted Metadata configuration files and you could even add your own go to the [Plex Meta Manager Configs](https://github.com/meisnate12/Plex-Meta-Manager-Configs)
|
||||
* Pull Request are welcome but please submit them to the develop branch
|
||||
* If you wish to contribute to the Wiki please fork and send a pull request on the [Plex Meta Manager Wiki Repository](https://github.com/meisnate12/Plex-Meta-Manager-Wiki)
|
||||
* Before posting on Github about an enhancement, error, or configuration question please visit the [Plex Meta Manager Discord Server](https://discord.gg/NfH6mGFuAB).
|
||||
* If you're getting an Error or have an Enhancement post in the [Issues](https://github.com/meisnate12/Plex-Meta-Manager/issues).
|
||||
* If you have a configuration question post in the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions).
|
||||
* To see user submitted Metadata configuration files, and you to even add your own, go to the [Plex Meta Manager Configs](https://github.com/meisnate12/Plex-Meta-Manager-Configs).
|
||||
* Pull Request are welcome but please submit them to the develop branch.
|
||||
* If you wish to contribute to the Wiki please fork and send a pull request on the [Plex Meta Manager Wiki Repository](https://github.com/meisnate12/Plex-Meta-Manager-Wiki).
|
||||
|
|
|
@ -1292,5 +1292,4 @@ collections:
|
|||
- +
|
||||
- ~
|
||||
sort_title: ~_Collectionless
|
||||
collection_order: alpha
|
||||
collection_order: alpha
|
|
@ -30,19 +30,28 @@ tautulli: # Can be individually specified
|
|||
radarr: # Can be individually specified per library as well
|
||||
url: http://192.168.1.12:7878
|
||||
token: ################################
|
||||
version: v2
|
||||
quality_profile: HD-1080p
|
||||
root_folder_path: S:/Movies
|
||||
version: v3
|
||||
add: false
|
||||
root_folder_path: S:/Movies
|
||||
monitor: true
|
||||
availability: announced
|
||||
quality_profile: HD-1080p
|
||||
tag:
|
||||
search: false
|
||||
sonarr: # Can be individually specified per library as well
|
||||
url: http://192.168.1.12:8989
|
||||
token: ################################
|
||||
version: v2
|
||||
quality_profile: HD-1080p
|
||||
root_folder_path: "S:/TV Shows"
|
||||
version: v3
|
||||
add: false
|
||||
root_folder_path: "S:/TV Shows"
|
||||
monitor: all
|
||||
quality_profile: HD-1080p
|
||||
language_profile: English
|
||||
series_type: standard
|
||||
season_folder: true
|
||||
tag:
|
||||
search: false
|
||||
cutoff_search: false
|
||||
omdb:
|
||||
apikey: ########
|
||||
trakt:
|
||||
|
|
|
@ -6,6 +6,8 @@ from retrying import retry
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = ["anidb_id", "anidb_relation", "anidb_popular"]
|
||||
|
||||
class AniDBAPI:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
|
|
@ -5,6 +5,21 @@ from retrying import retry
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = [
|
||||
"anilist_genre",
|
||||
"anilist_id",
|
||||
"anilist_popular",
|
||||
"anilist_relations",
|
||||
"anilist_season",
|
||||
"anilist_studio",
|
||||
"anilist_tag",
|
||||
"anilist_top_rated"
|
||||
]
|
||||
pretty_names = {
|
||||
"score": "Average Score",
|
||||
"popular": "Popularity"
|
||||
}
|
||||
|
||||
class AniListAPI:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
@ -223,15 +238,15 @@ class AniListAPI:
|
|||
elif method == "anilist_season":
|
||||
mal_ids = self.season(data["season"], data["year"], data["sort_by"], data["limit"])
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {util.anilist_pretty[data['sort_by']]}")
|
||||
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {pretty_names[data['sort_by']]}")
|
||||
elif method == "anilist_genre":
|
||||
mal_ids = self.genre(data["genre"], data["sort_by"], data["limit"])
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Genre: {data['genre']} sorted by {util.anilist_pretty[data['sort_by']]}")
|
||||
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Genre: {data['genre']} sorted by {pretty_names[data['sort_by']]}")
|
||||
elif method == "anilist_tag":
|
||||
mal_ids = self.tag(data["tag"], data["sort_by"], data["limit"])
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag: {data['tag']} sorted by {util.anilist_pretty[data['sort_by']]}")
|
||||
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag: {data['tag']} sorted by {pretty_names[data['sort_by']]}")
|
||||
elif method in ["anilist_studio", "anilist_relations"]:
|
||||
if method == "anilist_studio": mal_ids, name = self.studio(data)
|
||||
else: mal_ids, _, name = self.relations(data)
|
||||
|
|
|
@ -1,12 +1,148 @@
|
|||
import glob, logging, os, re
|
||||
from datetime import datetime, timedelta
|
||||
from modules import util
|
||||
from modules import anidb, anilist, imdb, letterboxd, mal, plex, radarr, sonarr, tautulli, tmdb, trakttv, tvdb, util
|
||||
from modules.util import Failed
|
||||
from plexapi.collection import Collections
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
image_file_details = ["file_poster", "file_background", "asset_directory"]
|
||||
method_alias = {
|
||||
"actors": "actor", "role": "actor", "roles": "actor",
|
||||
"content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating",
|
||||
"countries": "country",
|
||||
"decades": "decade",
|
||||
"directors": "director",
|
||||
"genres": "genre",
|
||||
"labels": "label",
|
||||
"rating": "critic_rating",
|
||||
"studios": "studio",
|
||||
"networks": "network",
|
||||
"producers": "producer",
|
||||
"writers": "writer",
|
||||
"years": "year"
|
||||
}
|
||||
all_builders = anidb.builders + anilist.builders + imdb.builders + letterboxd.builders + mal.builders + plex.builders + tautulli.builders + tmdb.builders + trakttv.builders + tvdb.builders
|
||||
dictionary_builders = [
|
||||
"filters",
|
||||
"anilist_genre",
|
||||
"anilist_season",
|
||||
"anilist_tag",
|
||||
"mal_season",
|
||||
"mal_userlist",
|
||||
"plex_collectionless",
|
||||
"plex_search",
|
||||
"tautulli_popular",
|
||||
"tautulli_watched",
|
||||
"tmdb_discover"
|
||||
]
|
||||
show_only_builders = [
|
||||
"tmdb_network",
|
||||
"tmdb_show",
|
||||
"tmdb_show_details",
|
||||
"tvdb_show",
|
||||
"tvdb_show_details"
|
||||
]
|
||||
movie_only_builders = [
|
||||
"letterboxd_list",
|
||||
"letterboxd_list_details",
|
||||
"tmdb_collection",
|
||||
"tmdb_collection_details",
|
||||
"tmdb_movie",
|
||||
"tmdb_movie_details",
|
||||
"tmdb_now_playing",
|
||||
"tvdb_movie",
|
||||
"tvdb_movie_details"
|
||||
]
|
||||
numbered_builders = [
|
||||
"anidb_popular",
|
||||
"anilist_popular",
|
||||
"anilist_top_rated",
|
||||
"mal_all",
|
||||
"mal_airing",
|
||||
"mal_upcoming",
|
||||
"mal_tv",
|
||||
"mal_ova",
|
||||
"mal_movie",
|
||||
"mal_special",
|
||||
"mal_popular",
|
||||
"mal_favorite",
|
||||
"mal_suggested",
|
||||
"tmdb_popular",
|
||||
"tmdb_top_rated",
|
||||
"tmdb_now_playing",
|
||||
"tmdb_trending_daily",
|
||||
"tmdb_trending_weekly",
|
||||
"trakt_trending",
|
||||
"trakt_popular",
|
||||
"trakt_recommended",
|
||||
"trakt_watched",
|
||||
"trakt_collected"
|
||||
]
|
||||
all_details = [
|
||||
"sort_title", "content_rating", "collection_mode", "collection_order",
|
||||
"summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary",
|
||||
"tvdb_description", "trakt_description", "letterboxd_description",
|
||||
"url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster",
|
||||
"url_background", "tmdb_background", "tvdb_background", "file_background",
|
||||
"name_mapping", "label", "show_filtered", "show_missing", "save_missing"
|
||||
]
|
||||
collectionless_details = [
|
||||
"sort_title", "content_rating",
|
||||
"summary", "tmdb_summary", "tmdb_description", "tmdb_biography",
|
||||
"collection_order", "plex_collectionless",
|
||||
"url_poster", "tmdb_poster", "tmdb_profile", "file_poster",
|
||||
"url_background", "file_background",
|
||||
"name_mapping", "label", "label_sync_mode", "test"
|
||||
]
|
||||
ignored_details = [
|
||||
"run_again",
|
||||
"schedule",
|
||||
"sync_mode",
|
||||
"template",
|
||||
"test",
|
||||
"tmdb_person"
|
||||
]
|
||||
boolean_details = [
|
||||
"show_filtered",
|
||||
"show_missing",
|
||||
"save_missing"
|
||||
]
|
||||
all_filters = [
|
||||
"actor", "actor.not",
|
||||
"audio_language", "audio_language.not",
|
||||
"audio_track_title", "audio_track_title.not",
|
||||
"collection", "collection.not",
|
||||
"content_rating", "content_rating.not",
|
||||
"country", "country.not",
|
||||
"director", "director.not",
|
||||
"genre", "genre.not",
|
||||
"max_age",
|
||||
"originally_available.gte", "originally_available.lte",
|
||||
"tmdb_vote_count.gte", "tmdb_vote_count.lte",
|
||||
"duration.gte", "duration.lte",
|
||||
"original_language", "original_language.not",
|
||||
"audience_rating.gte", "audience_rating.lte",
|
||||
"critic_rating.gte", "critic_rating.lte",
|
||||
"studio", "studio.not",
|
||||
"subtitle_language", "subtitle_language.not",
|
||||
"video_resolution", "video_resolution.not",
|
||||
"writer", "writer.not",
|
||||
"year", "year.gte", "year.lte", "year.not"
|
||||
]
|
||||
movie_only_filters = [
|
||||
"audio_language", "audio_language.not",
|
||||
"audio_track_title", "audio_track_title.not",
|
||||
"country", "country.not",
|
||||
"director", "director.not",
|
||||
"duration.gte", "duration.lte",
|
||||
"original_language", "original_language.not",
|
||||
"subtitle_language", "subtitle_language.not",
|
||||
"video_resolution", "video_resolution.not",
|
||||
"writer", "writer.not"
|
||||
]
|
||||
|
||||
class CollectionBuilder:
|
||||
def __init__(self, config, library, name, data):
|
||||
self.config = config
|
||||
|
@ -14,11 +150,12 @@ class CollectionBuilder:
|
|||
self.name = name
|
||||
self.data = data
|
||||
self.details = {
|
||||
"arr_tag": None,
|
||||
"show_filtered": library.show_filtered,
|
||||
"show_missing": library.show_missing,
|
||||
"save_missing": library.save_missing
|
||||
}
|
||||
self.radarr_options = {}
|
||||
self.sonarr_options = {}
|
||||
self.missing_movies = []
|
||||
self.missing_shows = []
|
||||
self.methods = []
|
||||
|
@ -28,6 +165,8 @@ class CollectionBuilder:
|
|||
self.summaries = {}
|
||||
self.schedule = ""
|
||||
self.rating_key_map = {}
|
||||
self.add_to_radarr = None
|
||||
self.add_to_sonarr = None
|
||||
current_time = datetime.now()
|
||||
current_year = current_time.year
|
||||
|
||||
|
@ -53,7 +192,7 @@ class CollectionBuilder:
|
|||
else:
|
||||
for tm in data_template:
|
||||
if not data_template[tm]:
|
||||
raise Failed(f"Collection Error: template sub-attribute {data_template[tm]} is blank")
|
||||
raise Failed(f"Collection Error: template sub-attribute {tm} is blank")
|
||||
|
||||
template_name = data_template["name"]
|
||||
template = self.library.templates[template_name]
|
||||
|
@ -95,18 +234,22 @@ class CollectionBuilder:
|
|||
if option not in data_template and f"<<{option}>>" in txt:
|
||||
raise Failed("remove attribute")
|
||||
for template_method in data_template:
|
||||
if template_method != "name" and f"<<{template_method}>>" in txt:
|
||||
if template_method != "name" and txt == f"<<{template_method}>>":
|
||||
return data_template[template_method]
|
||||
elif template_method != "name" and f"<<{template_method}>>" in txt:
|
||||
txt = txt.replace(f"<<{template_method}>>", str(data_template[template_method]))
|
||||
if "<<collection_name>>" in txt:
|
||||
txt = txt.replace("<<collection_name>>", str(self.name))
|
||||
for dm in default:
|
||||
if f"<<{dm}>>" in txt:
|
||||
if txt == f"<<{dm}>>":
|
||||
txt = default[dm]
|
||||
elif f"<<{dm}>>" in txt:
|
||||
txt = txt.replace(f"<<{dm}>>", str(default[dm]))
|
||||
if txt in ["true", "True"]: return True
|
||||
elif txt in ["false", "False"]: return False
|
||||
else:
|
||||
try: return int(txt)
|
||||
except ValueError: return txt
|
||||
except (ValueError, TypeError): return txt
|
||||
try:
|
||||
if isinstance(attr_data, dict):
|
||||
final_data = {}
|
||||
|
@ -151,7 +294,7 @@ class CollectionBuilder:
|
|||
run_time = str(schedule).lower()
|
||||
if run_time.startswith("day") or run_time.startswith("daily"):
|
||||
skip_collection = False
|
||||
if run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"):
|
||||
elif run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"):
|
||||
match = re.search("\\(([^)]+)\\)", run_time)
|
||||
if match:
|
||||
param = match.group(1)
|
||||
|
@ -212,28 +355,37 @@ class CollectionBuilder:
|
|||
else:
|
||||
raise Failed("Collection Error: tmdb_person attribute is blank")
|
||||
|
||||
for method_name, method_data in self.data.items():
|
||||
if "tmdb" in method_name.lower() and not config.TMDb: raise Failed(f"Collection Error: {method_name} requires TMDb to be configured")
|
||||
elif "trakt" in method_name.lower() and not config.Trakt: raise Failed(f"Collection Error: {method_name} requires Trakt todo be configured")
|
||||
elif "imdb" in method_name.lower() and not config.IMDb: raise Failed(f"Collection Error: {method_name} requires TMDb or Trakt to be configured")
|
||||
elif "tautulli" in method_name.lower() and not self.library.Tautulli: raise Failed(f"Collection Error: {method_name} requires Tautulli to be configured")
|
||||
elif "mal" in method_name.lower() and not config.MyAnimeList: raise Failed(f"Collection Error: {method_name} requires MyAnimeList to be configured")
|
||||
for method_key, method_data in self.data.items():
|
||||
if "trakt" in method_key.lower() and not config.Trakt: raise Failed(f"Collection Error: {method_key} requires Trakt todo be configured")
|
||||
elif "imdb" in method_key.lower() and not config.IMDb: raise Failed(f"Collection Error: {method_key} requires TMDb or Trakt to be configured")
|
||||
elif "radarr" in method_key.lower() and not self.library.Radarr: raise Failed(f"Collection Error: {method_key} requires Radarr to be configured")
|
||||
elif "sonarr" in method_key.lower() and not self.library.Sonarr: raise Failed(f"Collection Error: {method_key} requires Sonarr to be configured")
|
||||
elif "tautulli" in method_key.lower() and not self.library.Tautulli: raise Failed(f"Collection Error: {method_key} requires Tautulli to be configured")
|
||||
elif "mal" in method_key.lower() and not config.MyAnimeList: raise Failed(f"Collection Error: {method_key} requires MyAnimeList to be configured")
|
||||
elif method_data is not None:
|
||||
logger.debug("")
|
||||
logger.debug(f"Method: {method_name}")
|
||||
logger.debug(f"Validating Method: {method_key}")
|
||||
logger.debug(f"Value: {method_data}")
|
||||
if method_name.lower() in util.method_alias:
|
||||
method_name = util.method_alias[method_name.lower()]
|
||||
logger.warning(f"Collection Warning: {method_name} attribute will run as {method_name}")
|
||||
if method_key.lower() in method_alias:
|
||||
method_name = method_alias[method_key.lower()]
|
||||
logger.warning(f"Collection Warning: {method_key} attribute will run as {method_name}")
|
||||
elif method_key.lower() == "add_to_arr":
|
||||
method_name = "radarr_add" if self.library.is_movie else "sonarr_add"
|
||||
logger.warning(f"Collection Warning: {method_key} attribute will run as {method_name}")
|
||||
elif method_key.lower() in ["arr_tag", "arr_folder"]:
|
||||
method_name = f"{'rad' if self.library.is_movie else 'son'}{method_key.lower()}"
|
||||
logger.warning(f"Collection Warning: {method_key} attribute will run as {method_name}")
|
||||
else:
|
||||
method_name = method_name.lower()
|
||||
if method_name in util.show_only_lists and self.library.is_movie:
|
||||
method_name = method_key.lower()
|
||||
if method_name in show_only_builders and self.library.is_movie:
|
||||
raise Failed(f"Collection Error: {method_name} attribute only works for show libraries")
|
||||
elif method_name in util.movie_only_lists and self.library.is_show:
|
||||
elif method_name in movie_only_builders and self.library.is_show:
|
||||
raise Failed(f"Collection Error: {method_name} attribute only works for movie libraries")
|
||||
elif method_name in util.movie_only_searches and self.library.is_show:
|
||||
elif method_name in plex.movie_only_searches and self.library.is_show:
|
||||
raise Failed(f"Collection Error: {method_name} plex search only works for movie libraries")
|
||||
elif method_name not in util.collectionless_lists and self.collectionless:
|
||||
elif method_name in plex.show_only_searches and self.library.is_movie:
|
||||
raise Failed(f"Collection Error: {method_name} plex search only works for show libraries")
|
||||
elif method_name not in collectionless_details and self.collectionless:
|
||||
raise Failed(f"Collection Error: {method_name} attribute does not work for Collectionless collection")
|
||||
elif method_name == "summary":
|
||||
self.summaries[method_name] = method_data
|
||||
|
@ -297,36 +449,75 @@ class CollectionBuilder:
|
|||
elif method_name == "sync_mode":
|
||||
if str(method_data).lower() in ["append", "sync"]: self.details[method_name] = method_data.lower()
|
||||
else: raise Failed("Collection Error: sync_mode attribute must be either 'append' or 'sync'")
|
||||
elif method_name in ["arr_tag", "label"]:
|
||||
elif method_name == "label":
|
||||
self.details[method_name] = util.get_list(method_data)
|
||||
elif method_name in util.boolean_details:
|
||||
elif method_name in boolean_details:
|
||||
if isinstance(method_data, bool): self.details[method_name] = method_data
|
||||
elif str(method_data).lower() in ["t", "true"]: self.details[method_name] = True
|
||||
elif str(method_data).lower() in ["f", "false"]: self.details[method_name] = False
|
||||
else: raise Failed(f"Collection Error: {method_name} attribute must be either true or false")
|
||||
elif method_name in util.all_details:
|
||||
elif method_name in all_details:
|
||||
self.details[method_name] = method_data
|
||||
elif method_name == "radarr_add":
|
||||
self.add_to_radarr = True
|
||||
elif method_name == "radarr_folder":
|
||||
self.radarr_options["folder"] = method_data
|
||||
elif method_name in ["radarr_monitor", "radarr_search"]:
|
||||
if isinstance(method_data, bool): self.radarr_options[method_name[7:]] = method_data
|
||||
elif str(method_data).lower() in ["t", "true"]: self.radarr_options[method_name[7:]] = True
|
||||
elif str(method_data).lower() in ["f", "false"]: self.radarr_options[method_name[7:]] = False
|
||||
else: raise Failed(f"Collection Error: {method_name} attribute must be either true or false")
|
||||
elif method_name == "radarr_availability":
|
||||
if str(method_data).lower() in radarr.availability_translation:
|
||||
self.radarr_options["availability"] = str(method_data).lower()
|
||||
else:
|
||||
raise Failed(f"Collection Error: {method_name} attribute must be either announced, cinemas, released or db")
|
||||
elif method_name == "radarr_quality":
|
||||
self.library.Radarr.get_profile_id(method_data)
|
||||
self.radarr_options["quality"] = method_data
|
||||
elif method_name == "radarr_tag":
|
||||
self.radarr_options["tag"] = util.get_list(method_data)
|
||||
elif method_name == "sonarr_add":
|
||||
self.add_to_sonarr = True
|
||||
elif method_name == "sonarr_folder":
|
||||
self.sonarr_options["folder"] = method_data
|
||||
elif method_name == "sonarr_monitor":
|
||||
if str(method_data).lower() in sonarr.monitor_translation:
|
||||
self.sonarr_options["monitor"] = str(method_data).lower()
|
||||
else:
|
||||
raise Failed(f"Collection Error: {method_name} attribute must be either all, future, missing, existing, pilot, first, latest or none")
|
||||
elif method_name == "sonarr_quality":
|
||||
self.library.Sonarr.get_profile_id(method_data, "quality_profile")
|
||||
self.sonarr_options["quality"] = method_data
|
||||
elif method_name == "sonarr_language":
|
||||
self.library.Sonarr.get_profile_id(method_data, "language_profile")
|
||||
self.sonarr_options["language"] = method_data
|
||||
elif method_name == "sonarr_series":
|
||||
if str(method_data).lower() in sonarr.series_type:
|
||||
self.sonarr_options["series"] = str(method_data).lower()
|
||||
else:
|
||||
raise Failed(f"Collection Error: {method_name} attribute must be either standard, daily, or anime")
|
||||
elif method_name in ["sonarr_season", "sonarr_search", "sonarr_cutoff_search"]:
|
||||
if isinstance(method_data, bool): self.sonarr_options[method_name[7:]] = method_data
|
||||
elif str(method_data).lower() in ["t", "true"]: self.sonarr_options[method_name[7:]] = True
|
||||
elif str(method_data).lower() in ["f", "false"]: self.sonarr_options[method_name[7:]] = False
|
||||
else: raise Failed(f"Collection Error: {method_name} attribute must be either true or false")
|
||||
elif method_name == "sonarr_tag":
|
||||
self.sonarr_options["tag"] = util.get_list(method_data)
|
||||
elif method_name in ["title", "title.and", "title.not", "title.begins", "title.ends"]:
|
||||
self.methods.append(("plex_search", [{method_name: util.get_list(method_data, split=False)}]))
|
||||
elif method_name in ["decade", "year.greater", "year.less"]:
|
||||
self.methods.append(("plex_search", [{method_name: [util.check_year(method_data, current_year, method_name)]}]))
|
||||
elif method_name in ["year.greater", "year.less"]:
|
||||
self.methods.append(("plex_search", [{method_name: util.check_year(method_data, current_year, method_name)}]))
|
||||
elif method_name in ["added.before", "added.after", "originally_available.before", "originally_available.after"]:
|
||||
self.methods.append(("plex_search", [{method_name: [util.check_date(method_data, method_name, return_string=True, plex_date=True)]}]))
|
||||
elif method_name in ["duration.greater", "duration.less", "rating.greater", "rating.less"]:
|
||||
self.methods.append(("plex_search", [{method_name: [util.check_number(method_data, method_name, minimum=0)]}]))
|
||||
elif method_name in ["year", "year.not"]:
|
||||
self.methods.append(("plex_search", [{method_name: util.check_date(method_data, method_name, return_string=True, plex_date=True)}]))
|
||||
elif method_name in ["added", "added.not", "originally_available", "originally_available.not", "duration.greater", "duration.less"]:
|
||||
self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, minimum=1)}]))
|
||||
elif method_name in ["critic_rating.greater", "critic_rating.less", "audience_rating.greater", "audience_rating.less"]:
|
||||
self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, number_type="float", minimum=0, maximum=10)}]))
|
||||
elif method_name in ["decade", "year", "year.not"]:
|
||||
self.methods.append(("plex_search", [{method_name: util.get_year_list(method_data, current_year, method_name)}]))
|
||||
elif method_name in util.tmdb_searches:
|
||||
final_values = []
|
||||
for value in util.get_list(method_data):
|
||||
if value.lower() == "tmdb" and "tmdb_person" in self.details:
|
||||
for name in self.details["tmdb_person"]:
|
||||
final_values.append(name)
|
||||
else:
|
||||
final_values.append(value)
|
||||
self.methods.append(("plex_search", [{method_name: self.library.validate_search_list(final_values, os.path.splitext(method_name)[0])}]))
|
||||
elif method_name in util.plex_searches:
|
||||
if method_name in util.tmdb_searches:
|
||||
elif method_name in plex.searches:
|
||||
if method_name in plex.tmdb_searches:
|
||||
final_values = []
|
||||
for value in util.get_list(method_data):
|
||||
if value.lower() == "tmdb" and "tmdb_person" in self.details:
|
||||
|
@ -336,7 +527,12 @@ class CollectionBuilder:
|
|||
final_values.append(value)
|
||||
else:
|
||||
final_values = method_data
|
||||
self.methods.append(("plex_search", [{method_name: self.library.validate_search_list(final_values, os.path.splitext(method_name)[0])}]))
|
||||
search = os.path.splitext(method_name)[0]
|
||||
valid_values = self.library.validate_search_list(final_values, search)
|
||||
if valid_values:
|
||||
self.methods.append(("plex_search", [{method_name: valid_values}]))
|
||||
else:
|
||||
logger.warning(f"Collection Warning: No valid {search} values found in {final_values}")
|
||||
elif method_name == "plex_all":
|
||||
self.methods.append((method_name, [""]))
|
||||
elif method_name == "plex_collection":
|
||||
|
@ -363,19 +559,22 @@ class CollectionBuilder:
|
|||
self.summaries[method_name] = item.description
|
||||
self.methods.append((method_name[:-8], valid_list))
|
||||
elif method_name in ["trakt_watchlist", "trakt_collection"]:
|
||||
self.methods.append((method_name, config.Trakt.validate_trakt(method_name[6:], util.get_list(method_data), self.library.is_movie)))
|
||||
self.methods.append((method_name, config.Trakt.validate_trakt(util.get_list(method_data), trakt_type=method_name[6:], is_movie=self.library.is_movie)))
|
||||
elif method_name == "imdb_list":
|
||||
new_list = []
|
||||
for imdb_list in util.get_list(method_data, split=False):
|
||||
if isinstance(imdb_list, dict):
|
||||
dict_methods = {dm.lower(): dm for dm in imdb_list}
|
||||
if "url" in dict_methods and imdb_list[dict_methods["url"]]:
|
||||
imdb_url = imdb_list[dict_methods["url"]]
|
||||
imdb_url = config.IMDb.validate_imdb_url(imdb_list[dict_methods["url"]])
|
||||
else:
|
||||
raise Failed("Collection Error: imdb_list attribute url is required")
|
||||
list_count = util.regex_first_int(imdb_list[dict_methods["limit"]], "List Limit", default=0) if "limit" in dict_methods and imdb_list[dict_methods["limit"]] else 0
|
||||
if "limit" in dict_methods and imdb_list[dict_methods["limit"]]:
|
||||
list_count = util.regex_first_int(imdb_list[dict_methods["limit"]], "List Limit", default=0)
|
||||
else:
|
||||
list_count = 0
|
||||
else:
|
||||
imdb_url = str(imdb_list)
|
||||
imdb_url = config.IMDb.validate_imdb_url(str(imdb_list))
|
||||
list_count = 0
|
||||
new_list.append({"url": imdb_url, "limit": list_count})
|
||||
self.methods.append((method_name, new_list))
|
||||
|
@ -385,11 +584,11 @@ class CollectionBuilder:
|
|||
values = util.get_list(method_data, split=False)
|
||||
self.summaries[method_name] = config.Letterboxd.get_list_description(values[0], self.library.Plex.language)
|
||||
self.methods.append((method_name[:-8], values))
|
||||
elif method_name in util.dictionary_lists:
|
||||
elif method_name in dictionary_builders:
|
||||
if isinstance(method_data, dict):
|
||||
def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum=None):
|
||||
if method not in methods_in:
|
||||
logger.warning(f"Collection Warning: {parent} {methods_in[method]} attribute not found using {default_in} as default")
|
||||
logger.warning(f"Collection Warning: {parent} {method} attribute not found using {default_in} as default")
|
||||
elif not data_in[methods_in[method]]:
|
||||
logger.warning(f"Collection Warning: {parent} {methods_in[method]} attribute is blank using {default_in} as default")
|
||||
elif isinstance(data_in[methods_in[method]], int) and data_in[methods_in[method]] >= minimum:
|
||||
|
@ -402,12 +601,14 @@ class CollectionBuilder:
|
|||
return default_in
|
||||
if method_name == "filters":
|
||||
for filter_name, filter_data in method_data.items():
|
||||
if filter_name.lower() in util.method_alias or (filter_name.lower().endswith(".not") and filter_name.lower()[:-4] in util.method_alias):
|
||||
filter_method = (util.method_alias[filter_name.lower()[:-4]] + filter_name.lower()[-4:]) if filter_name.lower().endswith(".not") else util.method_alias[filter_name.lower()]
|
||||
modifier = filter_name[-4:].lower()
|
||||
method = filter_name[:-4].lower() if modifier in [".not", ".lte", ".gte"] else filter_name.lower()
|
||||
if method in method_alias:
|
||||
filter_method = f"{method_alias[method]}{modifier}"
|
||||
logger.warning(f"Collection Warning: {filter_name} filter will run as {filter_method}")
|
||||
else:
|
||||
filter_method = filter_name.lower()
|
||||
if filter_method in util.movie_only_filters and self.library.is_show:
|
||||
filter_method = f"{method}{modifier}"
|
||||
if filter_method in movie_only_filters and self.library.is_show:
|
||||
raise Failed(f"Collection Error: {filter_method} filter only works for movie libraries")
|
||||
elif filter_data is None:
|
||||
raise Failed(f"Collection Error: {filter_method} filter is blank")
|
||||
|
@ -417,7 +618,7 @@ class CollectionBuilder:
|
|||
valid_data = util.check_number(filter_data, f"{filter_method} filter", minimum=1)
|
||||
elif filter_method in ["year.gte", "year.lte"]:
|
||||
valid_data = util.check_year(filter_data, current_year, f"{filter_method} filter")
|
||||
elif filter_method in ["rating.gte", "rating.lte"]:
|
||||
elif filter_method in ["audience_rating.gte", "audience_rating.lte", "critic_rating.gte", "critic_rating.lte"]:
|
||||
valid_data = util.check_number(filter_data, f"{filter_method} filter", number_type="float", minimum=0.1, maximum=10)
|
||||
elif filter_method in ["originally_available.gte", "originally_available.lte"]:
|
||||
valid_data = util.check_date(filter_data, f"{filter_method} filter")
|
||||
|
@ -425,7 +626,7 @@ class CollectionBuilder:
|
|||
valid_data = util.get_list(filter_data, lower=True)
|
||||
elif filter_method == "collection":
|
||||
valid_data = filter_data if isinstance(filter_data, list) else [filter_data]
|
||||
elif filter_method in util.all_filters:
|
||||
elif filter_method in all_filters:
|
||||
valid_data = util.get_list(filter_data)
|
||||
else:
|
||||
raise Failed(f"Collection Error: {filter_method} filter not supported")
|
||||
|
@ -454,16 +655,18 @@ class CollectionBuilder:
|
|||
searches = {}
|
||||
for search_name, search_data in method_data.items():
|
||||
search, modifier = os.path.splitext(str(search_name).lower())
|
||||
if search in util.method_alias:
|
||||
search = util.method_alias[search]
|
||||
if search in method_alias:
|
||||
search = method_alias[search]
|
||||
logger.warning(f"Collection Warning: {str(search_name).lower()} plex search attribute will run as {search}{modifier if modifier else ''}")
|
||||
search_final = f"{search}{modifier}"
|
||||
if search_final in util.movie_only_searches and self.library.is_show:
|
||||
if search_final in plex.movie_only_searches and self.library.is_show:
|
||||
raise Failed(f"Collection Error: {search_final} plex search attribute only works for movie libraries")
|
||||
if search_final in plex.show_only_searches and self.library.is_movie:
|
||||
raise Failed(f"Collection Error: {search_final} plex search attribute only works for show libraries")
|
||||
elif search_data is None:
|
||||
raise Failed(f"Collection Error: {search_final} plex search attribute is blank")
|
||||
elif search == "sort_by":
|
||||
if str(search_data).lower() in util.plex_sort:
|
||||
if str(search_data).lower() in plex.sorts:
|
||||
searches[search] = str(search_data).lower()
|
||||
else:
|
||||
logger.warning(f"Collection Error: {search_data} is not a valid plex search sort defaulting to title.asc")
|
||||
|
@ -477,9 +680,9 @@ class CollectionBuilder:
|
|||
elif search == "title" and modifier in ["", ".and", ".not", ".begins", ".ends"]:
|
||||
searches[search_final] = util.get_list(search_data, split=False)
|
||||
elif (search == "studio" and modifier in ["", ".and", ".not", ".begins", ".ends"]) \
|
||||
or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "producer", "subtitle_language", "writer"] and modifier in ["", ".and", ".not"]) \
|
||||
or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "producer", "subtitle_language", "writer"] and modifier in ["", ".and", ".not"]) \
|
||||
or (search == "resolution" and modifier in [""]):
|
||||
if search_final in util.tmdb_searches:
|
||||
if search_final in plex.tmdb_searches:
|
||||
final_values = []
|
||||
for value in util.get_list(search_data):
|
||||
if value.lower() == "tmdb" and "tmdb_person" in self.details:
|
||||
|
@ -489,21 +692,26 @@ class CollectionBuilder:
|
|||
final_values.append(value)
|
||||
else:
|
||||
final_values = search_data
|
||||
searches[search_final] = self.library.validate_search_list(final_values, search)
|
||||
elif (search == "decade" and modifier in [""]) \
|
||||
or (search == "year" and modifier in [".greater", ".less"]):
|
||||
searches[search_final] = [util.check_year(search_data, current_year, search_final)]
|
||||
valid_values = self.library.validate_search_list(final_values, search)
|
||||
if valid_values:
|
||||
searches[search_final] = valid_values
|
||||
else:
|
||||
logger.warning(f"Collection Warning: No valid {search} values found in {final_values}")
|
||||
elif search == "year" and modifier in [".greater", ".less"]:
|
||||
searches[search_final] = util.check_year(search_data, current_year, search_final)
|
||||
elif search in ["added", "originally_available"] and modifier in [".before", ".after"]:
|
||||
searches[search_final] = [util.check_date(search_data, search_final, return_string=True, plex_date=True)]
|
||||
elif search in ["duration", "rating"] and modifier in [".greater", ".less"]:
|
||||
searches[search_final] = [util.check_number(search_data, search_final, minimum=0)]
|
||||
elif search == "year" and modifier in ["", ".not"]:
|
||||
searches[search_final] = util.check_date(search_data, search_final, return_string=True, plex_date=True)
|
||||
elif (search in ["added", "originally_available"] and modifier in ["", ".not"]) or (search in ["duration"] and modifier in [".greater", ".less"]):
|
||||
searches[search_final] = util.check_number(search_data, search_final, minimum=1)
|
||||
elif search in ["critic_rating", "audience_rating"] and modifier in [".greater", ".less"]:
|
||||
searches[search_final] = util.check_number(search_data, search_final, number_type="float", minimum=0, maximum=10)
|
||||
elif (search == "decade" and modifier in [""]) or (search == "year" and modifier in ["", ".not"]):
|
||||
searches[search_final] = util.get_year_list(search_data, current_year, search_final)
|
||||
elif (search in ["title", "studio"] and modifier not in ["", ".and", ".not", ".begins", ".ends"]) \
|
||||
or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "producer", "subtitle_language", "writer"] and modifier not in ["", ".and", ".not"]) \
|
||||
or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "producer", "subtitle_language", "writer"] and modifier not in ["", ".and", ".not"]) \
|
||||
or (search in ["resolution", "decade"] and modifier not in [""]) \
|
||||
or (search in ["added", "originally_available"] and modifier not in [".before", ".after"]) \
|
||||
or (search in ["duration", "rating"] and modifier not in [".greater", ".less"]) \
|
||||
or (search in ["added", "originally_available"] and modifier not in ["", ".not", ".before", ".after"]) \
|
||||
or (search in ["duration", "critic_rating", "audience_rating"] and modifier not in [".greater", ".less"]) \
|
||||
or (search in ["year"] and modifier not in ["", ".not", ".greater", ".less"]):
|
||||
raise Failed(f"Collection Error: modifier: {modifier} not supported with the {search} plex search attribute")
|
||||
else:
|
||||
|
@ -514,7 +722,7 @@ class CollectionBuilder:
|
|||
for discover_name, discover_data in method_data.items():
|
||||
discover_final = discover_name.lower()
|
||||
if discover_data:
|
||||
if (self.library.is_movie and discover_final in util.discover_movie) or (self.library.is_show and discover_final in util.discover_tv):
|
||||
if (self.library.is_movie and discover_final in tmdb.discover_movie) or (self.library.is_show and discover_final in tmdb.discover_tv):
|
||||
if discover_final == "language":
|
||||
if re.compile("([a-z]{2})-([A-Z]{2})").match(str(discover_data)):
|
||||
new_dictionary[discover_final] = str(discover_data)
|
||||
|
@ -526,7 +734,7 @@ class CollectionBuilder:
|
|||
else:
|
||||
raise Failed(f"Collection Error: {method_name} attribute {discover_final}: {discover_data} must match pattern ^[A-Z]{{2}}$ e.g. US")
|
||||
elif discover_final == "sort_by":
|
||||
if (self.library.is_movie and discover_data in util.discover_movie_sort) or (self.library.is_show and discover_data in util.discover_tv_sort):
|
||||
if (self.library.is_movie and discover_data in tmdb.discover_movie_sort) or (self.library.is_show and discover_data in tmdb.discover_tv_sort):
|
||||
new_dictionary[discover_final] = discover_data
|
||||
else:
|
||||
raise Failed(f"Collection Error: {method_name} attribute {discover_final}: {discover_data} is invalid")
|
||||
|
@ -543,7 +751,7 @@ class CollectionBuilder:
|
|||
elif discover_final in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]:
|
||||
if discover_data is True:
|
||||
new_dictionary[discover_final] = discover_data
|
||||
elif discover_final in util.discover_dates:
|
||||
elif discover_final in tmdb.discover_dates:
|
||||
new_dictionary[discover_final] = util.check_date(discover_data, f"{method_name} attribute {discover_final}", return_string=True)
|
||||
elif discover_final in ["primary_release_year", "year", "first_air_date_year"]:
|
||||
new_dictionary[discover_final] = util.check_number(discover_data, f"{method_name} attribute {discover_final}", minimum=1800, maximum=current_year + 1)
|
||||
|
@ -586,10 +794,10 @@ class CollectionBuilder:
|
|||
logger.warning("Collection Warning: mal_season sort_by attribute not found using members as default")
|
||||
elif not method_data[dict_methods["sort_by"]]:
|
||||
logger.warning("Collection Warning: mal_season sort_by attribute is blank using members as default")
|
||||
elif method_data[dict_methods["sort_by"]] not in util.mal_season_sort:
|
||||
elif method_data[dict_methods["sort_by"]] not in mal.season_sort:
|
||||
logger.warning(f"Collection Warning: mal_season sort_by attribute {method_data[dict_methods['sort_by']]} invalid must be either 'members' or 'score' using members as default")
|
||||
else:
|
||||
new_dictionary["sort_by"] = util.mal_season_sort[method_data[dict_methods["sort_by"]]]
|
||||
new_dictionary["sort_by"] = mal.season_sort[method_data[dict_methods["sort_by"]]]
|
||||
|
||||
if current_time.month in [1, 2, 3]: new_dictionary["season"] = "winter"
|
||||
elif current_time.month in [4, 5, 6]: new_dictionary["season"] = "spring"
|
||||
|
@ -622,19 +830,19 @@ class CollectionBuilder:
|
|||
logger.warning("Collection Warning: mal_season status attribute not found using all as default")
|
||||
elif not method_data[dict_methods["status"]]:
|
||||
logger.warning("Collection Warning: mal_season status attribute is blank using all as default")
|
||||
elif method_data[dict_methods["status"]] not in util.mal_userlist_status:
|
||||
elif method_data[dict_methods["status"]] not in mal.userlist_status:
|
||||
logger.warning(f"Collection Warning: mal_season status attribute {method_data[dict_methods['status']]} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default")
|
||||
else:
|
||||
new_dictionary["status"] = util.mal_userlist_status[method_data[dict_methods["status"]]]
|
||||
new_dictionary["status"] = mal.userlist_status[method_data[dict_methods["status"]]]
|
||||
|
||||
if "sort_by" not in dict_methods:
|
||||
logger.warning("Collection Warning: mal_season sort_by attribute not found using score as default")
|
||||
elif not method_data[dict_methods["sort_by"]]:
|
||||
logger.warning("Collection Warning: mal_season sort_by attribute is blank using score as default")
|
||||
elif method_data[dict_methods["sort_by"]] not in util.mal_userlist_sort:
|
||||
elif method_data[dict_methods["sort_by"]] not in mal.userlist_sort:
|
||||
logger.warning(f"Collection Warning: mal_season sort_by attribute {method_data[dict_methods['sort_by']]} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default")
|
||||
else:
|
||||
new_dictionary["sort_by"] = util.mal_userlist_sort[method_data[dict_methods["sort_by"]]]
|
||||
new_dictionary["sort_by"] = mal.userlist_sort[method_data[dict_methods["sort_by"]]]
|
||||
|
||||
new_dictionary["limit"] = get_int(method_name, "limit", method_data, dict_methods, 100, maximum=1000)
|
||||
self.methods.append((method_name, [new_dictionary]))
|
||||
|
@ -686,7 +894,7 @@ class CollectionBuilder:
|
|||
self.methods.append((method_name, [new_dictionary]))
|
||||
else:
|
||||
raise Failed(f"Collection Error: {method_name} attribute is not a dictionary: {method_data}")
|
||||
elif method_name in util.count_lists:
|
||||
elif method_name in numbered_builders:
|
||||
list_count = util.regex_first_int(method_data, "List Size", default=10)
|
||||
if list_count < 1:
|
||||
logger.warning(f"Collection Warning: {method_name} must be an integer greater then 0 defaulting to 10")
|
||||
|
@ -716,8 +924,8 @@ class CollectionBuilder:
|
|||
self.methods.append((method_name[:-8], values))
|
||||
else:
|
||||
self.methods.append((method_name, values))
|
||||
elif method_name in util.tmdb_lists:
|
||||
values = config.TMDb.validate_tmdb_list(util.get_int_list(method_data, f"TMDb {util.tmdb_type[method_name]} ID"), util.tmdb_type[method_name])
|
||||
elif method_name in tmdb.builders:
|
||||
values = config.TMDb.validate_tmdb_list(util.get_int_list(method_data, f"TMDb {tmdb.type_map[method_name]} ID"), tmdb.type_map[method_name])
|
||||
if method_name[-8:] == "_details":
|
||||
if method_name in ["tmdb_collection_details", "tmdb_movie_details", "tmdb_show_details"]:
|
||||
item = config.TMDb.get_movie_show_or_collection(values[0], self.library.is_movie)
|
||||
|
@ -740,14 +948,14 @@ class CollectionBuilder:
|
|||
self.methods.append((method_name[:-8], values))
|
||||
else:
|
||||
self.methods.append((method_name, values))
|
||||
elif method_name in util.all_lists:
|
||||
elif method_name in all_builders:
|
||||
self.methods.append((method_name, util.get_list(method_data)))
|
||||
elif method_name not in util.other_attributes:
|
||||
elif method_name not in ignored_details:
|
||||
raise Failed(f"Collection Error: {method_name} attribute not supported")
|
||||
elif method_name in util.all_lists or method_name in util.method_alias or method_name in util.plex_searches:
|
||||
raise Failed(f"Collection Error: {method_name} attribute is blank")
|
||||
elif method_key.lower() in all_builders or method_key.lower() in method_alias or method_key.lower() in plex.searches:
|
||||
raise Failed(f"Collection Error: {method_key} attribute is blank")
|
||||
else:
|
||||
logger.warning(f"Collection Warning: {method_name} attribute is blank")
|
||||
logger.warning(f"Collection Warning: {method_key} attribute is blank")
|
||||
|
||||
self.sync = self.library.sync_mode == "sync"
|
||||
if "sync_mode" in methods:
|
||||
|
@ -758,14 +966,14 @@ class CollectionBuilder:
|
|||
else:
|
||||
self.sync = self.data[methods["sync_mode"]].lower() == "sync"
|
||||
|
||||
self.do_arr = False
|
||||
if self.library.Radarr:
|
||||
self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Radarr.add
|
||||
if self.library.Sonarr:
|
||||
self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Sonarr.add
|
||||
if self.add_to_radarr is None:
|
||||
self.add_to_radarr = self.library.Radarr.add if self.library.Radarr else False
|
||||
if self.add_to_sonarr is None:
|
||||
self.add_to_sonarr = self.library.Sonarr.add if self.library.Sonarr else False
|
||||
|
||||
if self.collectionless:
|
||||
self.details["add_to_arr"] = False
|
||||
self.add_to_radarr = False
|
||||
self.add_to_sonarr = False
|
||||
self.details["collection_mode"] = "hide"
|
||||
self.sync = True
|
||||
|
||||
|
@ -775,7 +983,6 @@ class CollectionBuilder:
|
|||
logger.debug("")
|
||||
logger.debug(f"Method: {method}")
|
||||
logger.debug(f"Values: {values}")
|
||||
pretty = util.pretty_names[method] if method in util.pretty_names else method
|
||||
for value in values:
|
||||
items = []
|
||||
missing_movies = []
|
||||
|
@ -786,8 +993,10 @@ class CollectionBuilder:
|
|||
if len(movie_ids) > 0:
|
||||
items_found_inside += len(movie_ids)
|
||||
for movie_id in movie_ids:
|
||||
if movie_id in movie_map: items.append(movie_map[movie_id])
|
||||
else: missing_movies.append(movie_id)
|
||||
if movie_id in movie_map:
|
||||
items.append(movie_map[movie_id])
|
||||
else:
|
||||
missing_movies.append(movie_id)
|
||||
if len(show_ids) > 0:
|
||||
items_found_inside += len(show_ids)
|
||||
for show_id in show_ids:
|
||||
|
@ -796,70 +1005,9 @@ class CollectionBuilder:
|
|||
return items_found_inside
|
||||
logger.info("")
|
||||
logger.debug(f"Value: {value}")
|
||||
if method == "plex_all":
|
||||
logger.info(f"Processing {pretty} {'Movies' if self.library.is_movie else 'Shows'}")
|
||||
items = self.library.Plex.all()
|
||||
if "plex" in method:
|
||||
items = self.library.get_items(method, value)
|
||||
items_found += len(items)
|
||||
elif method == "plex_collection":
|
||||
items = value.items()
|
||||
items_found += len(items)
|
||||
elif method == "plex_search":
|
||||
search_terms = {}
|
||||
has_processed = False
|
||||
search_limit = None
|
||||
search_sort = None
|
||||
for search_method, search_data in value.items():
|
||||
if search_method == "limit":
|
||||
search_limit = search_data
|
||||
elif search_method == "sort_by":
|
||||
search_sort = util.plex_sort[search_data]
|
||||
else:
|
||||
search, modifier = os.path.splitext(str(search_method).lower())
|
||||
final_search = util.search_alias[search] if search in util.search_alias else search
|
||||
final_mod = util.plex_modifiers[modifier] if modifier in util.plex_modifiers else ""
|
||||
final_method = f"{final_search}{final_mod}"
|
||||
search_terms[final_method] = search_data * 60000 if final_search == "duration" else search_data
|
||||
ors = ""
|
||||
conjunction = " AND " if final_mod == "&" else " OR "
|
||||
for o, param in enumerate(search_data):
|
||||
or_des = conjunction if o > 0 else f"{search_method}("
|
||||
ors += f"{or_des}{param}"
|
||||
if has_processed:
|
||||
logger.info(f"\t\t AND {ors})")
|
||||
else:
|
||||
logger.info(f"Processing {pretty}: {ors})")
|
||||
has_processed = True
|
||||
items = self.library.Plex.search(sort=search_sort, maxresults=search_limit, **search_terms)
|
||||
items_found += len(items)
|
||||
elif method == "plex_collectionless":
|
||||
good_collections = []
|
||||
for col in self.library.get_all_collections():
|
||||
keep_collection = True
|
||||
for pre in value["exclude_prefix"]:
|
||||
if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)):
|
||||
keep_collection = False
|
||||
break
|
||||
if keep_collection:
|
||||
for ext in value["exclude"]:
|
||||
if col.title == ext or (col.titleSort and col.titleSort == ext):
|
||||
keep_collection = False
|
||||
break
|
||||
if keep_collection:
|
||||
good_collections.append(col.index)
|
||||
all_items = self.library.Plex.all()
|
||||
length = 0
|
||||
for i, item in enumerate(all_items, 1):
|
||||
length = util.print_return(length, f"Processing: {i}/{len(all_items)} {item.title}")
|
||||
add_item = True
|
||||
item.reload()
|
||||
for collection in item.collections:
|
||||
if collection.id in good_collections:
|
||||
add_item = False
|
||||
break
|
||||
if add_item:
|
||||
items.append(item)
|
||||
items_found += len(items)
|
||||
util.print_end(length, f"Processed {len(all_items)} {'Movies' if self.library.is_movie else 'Shows'}")
|
||||
elif "tautulli" in method:
|
||||
items = self.library.Tautulli.get_items(self.library, time_range=value["list_days"], stats_count=value["list_size"], list_type=value["list_type"], stats_count_buffer=value["list_buffer"])
|
||||
items_found += len(items)
|
||||
|
@ -907,8 +1055,11 @@ class CollectionBuilder:
|
|||
logger.info(f"{len(missing_movies_with_names)} Movie{'s' if len(missing_movies_with_names) > 1 else ''} Missing")
|
||||
if self.details["save_missing"] is True:
|
||||
self.library.add_missing(collection_name, missing_movies_with_names, True)
|
||||
if self.do_arr and self.library.Radarr:
|
||||
self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=self.details["arr_tag"])
|
||||
if self.add_to_radarr and self.library.Radarr:
|
||||
try:
|
||||
self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], **self.radarr_options)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
if self.run_again:
|
||||
self.missing_movies.extend([missing_id for title, missing_id in missing_movies_with_names])
|
||||
if len(missing_shows) > 0 and self.library.is_show:
|
||||
|
@ -936,8 +1087,11 @@ class CollectionBuilder:
|
|||
logger.info(f"{len(missing_shows_with_names)} Show{'s' if len(missing_shows_with_names) > 1 else ''} Missing")
|
||||
if self.details["save_missing"] is True:
|
||||
self.library.add_missing(collection_name, missing_shows_with_names, False)
|
||||
if self.do_arr and self.library.Sonarr:
|
||||
self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=self.details["arr_tag"])
|
||||
if self.add_to_sonarr and self.library.Sonarr:
|
||||
try:
|
||||
self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], **self.sonarr_options)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
if self.run_again:
|
||||
self.missing_shows.extend([missing_id for title, missing_id in missing_shows_with_names])
|
||||
|
||||
|
@ -1061,15 +1215,19 @@ class CollectionBuilder:
|
|||
logger.warning(f"No Folder: {os.path.join(path, folder)}")
|
||||
|
||||
def set_image(image_method, images, is_background=False):
|
||||
if image_method in ["file_poster", "file_background", "asset_directory"]:
|
||||
if is_background: collection.uploadArt(filepath=images[image_method])
|
||||
else: collection.uploadPoster(filepath=images[image_method])
|
||||
image_location = "File"
|
||||
else:
|
||||
if is_background: collection.uploadArt(url=images[image_method])
|
||||
else: collection.uploadPoster(url=images[image_method])
|
||||
image_location = "URL"
|
||||
logger.info(f"Detail: {image_method} updated collection {'background' if is_background else 'poster'} to [{image_location}] {images[image_method]}")
|
||||
message = f"{'background' if is_background else 'poster'} to [{'File' if image_method in image_file_details else 'URL'}] {images[image_method]}"
|
||||
try:
|
||||
if image_method in image_file_details and is_background:
|
||||
collection.uploadArt(filepath=images[image_method])
|
||||
elif image_method in image_file_details:
|
||||
collection.uploadPoster(filepath=images[image_method])
|
||||
elif is_background:
|
||||
collection.uploadArt(url=images[image_method])
|
||||
else:
|
||||
collection.uploadPoster(url=images[image_method])
|
||||
logger.info(f"Detail: {image_method} updated collection {message}")
|
||||
except BadRequest:
|
||||
logger.error(f"Detail: {image_method} failed to update {message}")
|
||||
|
||||
if len(self.posters) > 1:
|
||||
logger.info(f"{len(self.posters)} posters found:")
|
||||
|
|
|
@ -22,6 +22,33 @@ from ruamel import yaml
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remove Items from the Collection"}
|
||||
radarr_versions = {"v2": "For Radarr 0.2", "v3": "For Radarr 3.0"}
|
||||
radarr_availabilities = {
|
||||
"announced": "For Announced",
|
||||
"cinemas": "For In Cinemas",
|
||||
"released": "For Released",
|
||||
"db": "For PreDB"
|
||||
}
|
||||
sonarr_versions = {"v2": "For Sonarr 0.2", "v3": "For Sonarr 3.0"}
|
||||
sonarr_monitors = {
|
||||
"all": "Monitor all episodes except specials",
|
||||
"future": "Monitor episodes that have not aired yet",
|
||||
"missing": "Monitor episodes that do not have files or have not aired yet",
|
||||
"existing": "Monitor episodes that have files or have not aired yet",
|
||||
"pilot": "Monitor the first episode. All other episodes will be ignored",
|
||||
"first": "Monitor all episodes of the first season. All other seasons will be ignored",
|
||||
"latest": "Monitor all episodes of the latest season and future seasons",
|
||||
"none": "No episodes will be monitored"
|
||||
}
|
||||
sonarr_series_types = {
|
||||
"standard": "Episodes released with SxxEyy pattern",
|
||||
"daily": "Episodes released daily or less frequently that use year-month-day (2017-05-25)",
|
||||
"anime": "Episodes released using an absolute episode number"
|
||||
}
|
||||
mass_genre_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"}
|
||||
library_types = {"movie": "For Movie Libraries", "show": "For Show Libraries"}
|
||||
|
||||
class Config:
|
||||
def __init__(self, default_dir, config_path=None):
|
||||
logger.info("Locating config...")
|
||||
|
@ -33,7 +60,7 @@ class Config:
|
|||
|
||||
yaml.YAML().allow_duplicate_keys = True
|
||||
try:
|
||||
new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path))
|
||||
new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path, encoding="utf-8"))
|
||||
def replace_attr(all_data, attr, par):
|
||||
if "settings" not in all_data:
|
||||
all_data["settings"] = {}
|
||||
|
@ -75,12 +102,15 @@ class Config:
|
|||
if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb")
|
||||
if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt")
|
||||
if "mal" in new_config: new_config["mal"] = new_config.pop("mal")
|
||||
yaml.round_trip_dump(new_config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi)
|
||||
yaml.round_trip_dump(new_config, open(self.config_path, "w", encoding="utf-8"), indent=ind, block_seq_indent=bsi)
|
||||
self.data = new_config
|
||||
except yaml.scanner.ScannerError as e:
|
||||
raise Failed(f"YAML Error: {util.tab_new_lines(e)}")
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
raise Failed(f"YAML Error: {e}")
|
||||
|
||||
def check_for_attribute(data, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, req_default=False, var_type="str", throw=False, save=True):
|
||||
def check_for_attribute(data, attribute, parent=None, test_list=None, default=None, do_print=True, default_is_none=False, req_default=False, var_type="str", throw=False, save=True):
|
||||
endline = ""
|
||||
if parent is not None:
|
||||
if parent in data:
|
||||
|
@ -122,16 +152,22 @@ class Config:
|
|||
if var_type == "path" and default and os.path.exists(os.path.abspath(default)):
|
||||
return default
|
||||
elif var_type == "path" and default:
|
||||
default = None
|
||||
if attribute in data and data[attribute]:
|
||||
message = f"neither {data[attribute]} or the default path {default} could be found"
|
||||
else:
|
||||
message = f"no {text} found and the default path {default} could be found"
|
||||
message = f"no {text} found and the default path {default} could not be found"
|
||||
default = None
|
||||
if default is not None or default_is_none:
|
||||
message = message + f" using {default} as default"
|
||||
message = message + endline
|
||||
if req_default and default is None:
|
||||
raise Failed(f"Config Error: {attribute} attribute must be set under {parent} globally or under this specific Library")
|
||||
options = ""
|
||||
if test_list:
|
||||
for option, description in test_list.items():
|
||||
if len(options) > 0:
|
||||
options = f"{options}\n"
|
||||
options = f"{options} {option} ({description})"
|
||||
if (default is None and not default_is_none) or throw:
|
||||
if len(options) > 0:
|
||||
message = message + "\n" + options
|
||||
|
@ -143,7 +179,7 @@ class Config:
|
|||
return default
|
||||
|
||||
self.general = {}
|
||||
self.general["cache"] = check_for_attribute(self.data, "cache", parent="settings", options=" true (Create a cache to store ids)\n false (Do not create a cache to store ids)", var_type="bool", default=True)
|
||||
self.general["cache"] = check_for_attribute(self.data, "cache", parent="settings", var_type="bool", default=True)
|
||||
self.general["cache_expiration"] = check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60)
|
||||
if self.general["cache"]:
|
||||
util.separator()
|
||||
|
@ -151,7 +187,7 @@ class Config:
|
|||
else:
|
||||
self.Cache = None
|
||||
self.general["asset_directory"] = check_for_attribute(self.data, "asset_directory", parent="settings", var_type="list_path", default=[os.path.join(default_dir, "assets")])
|
||||
self.general["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="settings", default="append", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)")
|
||||
self.general["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="settings", default="append", test_list=sync_modes)
|
||||
self.general["run_again_delay"] = check_for_attribute(self.data, "run_again_delay", parent="settings", var_type="int", default=0)
|
||||
self.general["show_unmanaged"] = check_for_attribute(self.data, "show_unmanaged", parent="settings", var_type="bool", default=True)
|
||||
self.general["show_filtered"] = check_for_attribute(self.data, "show_filtered", parent="settings", var_type="bool", default=False)
|
||||
|
@ -241,24 +277,30 @@ class Config:
|
|||
|
||||
self.general["radarr"] = {}
|
||||
self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True)
|
||||
self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=["v2", "v3"], options=" v2 (For Radarr 0.2)\n v3 (For Radarr 3.0)", default="v2")
|
||||
self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True)
|
||||
self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True)
|
||||
self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True)
|
||||
self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=radarr_versions, default="v3")
|
||||
self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False)
|
||||
self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False)
|
||||
self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True)
|
||||
self.general["radarr"]["monitor"] = check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True)
|
||||
self.general["radarr"]["availability"] = check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr_availabilities, default="announced")
|
||||
self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True)
|
||||
self.general["radarr"]["tag"] = check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True)
|
||||
self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False)
|
||||
|
||||
self.general["sonarr"] = {}
|
||||
self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", default_is_none=True)
|
||||
self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True)
|
||||
self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default="v2")
|
||||
self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True)
|
||||
self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True)
|
||||
self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=sonarr_versions, default="v3")
|
||||
self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False)
|
||||
self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False)
|
||||
self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True)
|
||||
self.general["sonarr"]["monitor"] = check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr_monitors, default="all")
|
||||
self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True)
|
||||
self.general["sonarr"]["language_profile"] = check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True)
|
||||
self.general["sonarr"]["series_type"] = check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr_series_types, default="standard")
|
||||
self.general["sonarr"]["season_folder"] = check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True)
|
||||
self.general["sonarr"]["tag"] = check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True)
|
||||
self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False)
|
||||
self.general["sonarr"]["cutoff_search"] = check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False)
|
||||
|
||||
self.general["tautulli"] = {}
|
||||
self.general["tautulli"]["url"] = check_for_attribute(self.data, "url", parent="tautulli", default_is_none=True)
|
||||
|
@ -282,9 +324,9 @@ class Config:
|
|||
logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library")
|
||||
|
||||
if "settings" in lib and lib["settings"] and "sync_mode" in lib["settings"]:
|
||||
params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)", default=self.general["sync_mode"], do_print=False, save=False)
|
||||
params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False)
|
||||
else:
|
||||
params["sync_mode"] = check_for_attribute(lib, "sync_mode", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)", default=self.general["sync_mode"], do_print=False, save=False)
|
||||
params["sync_mode"] = check_for_attribute(lib, "sync_mode", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False)
|
||||
|
||||
if "settings" in lib and lib["settings"] and "show_unmanaged" in lib["settings"]:
|
||||
params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False)
|
||||
|
@ -307,7 +349,7 @@ class Config:
|
|||
params["save_missing"] = check_for_attribute(lib, "save_missing", var_type="bool", default=self.general["save_missing"], do_print=False, save=False)
|
||||
|
||||
if "mass_genre_update" in lib and lib["mass_genre_update"]:
|
||||
params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=["tmdb", "omdb"], options=" tmdb (Use TMDb Metadata)\n omdb (Use IMDb Metadata through OMDb)", default_is_none=True, save=False)
|
||||
params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_genre_update_options, default_is_none=True, save=False)
|
||||
else:
|
||||
params["mass_genre_update"] = None
|
||||
|
||||
|
@ -317,7 +359,7 @@ class Config:
|
|||
|
||||
try:
|
||||
params["metadata_path"] = check_for_attribute(lib, "metadata_path", var_type="path", default=os.path.join(default_dir, f"{library_name}.yml"), throw=True)
|
||||
params["library_type"] = check_for_attribute(lib, "library_type", test_list=["movie", "show"], options=" movie (For Movie Libraries)\n show (For Show Libraries)", throw=True)
|
||||
params["library_type"] = check_for_attribute(lib, "library_type", test_list=library_types, throw=True)
|
||||
params["plex"] = {}
|
||||
params["plex"]["url"] = check_for_attribute(lib, "url", parent="plex", default=self.general["plex"]["url"], req_default=True, save=False)
|
||||
params["plex"]["token"] = check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False)
|
||||
|
@ -335,13 +377,15 @@ class Config:
|
|||
try:
|
||||
radarr_params["url"] = check_for_attribute(lib, "url", parent="radarr", default=self.general["radarr"]["url"], req_default=True, save=False)
|
||||
radarr_params["token"] = check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False)
|
||||
radarr_params["version"] = check_for_attribute(lib, "version", parent="radarr", test_list=["v2", "v3"], options=" v2 (For Radarr 0.2)\n v3 (For Radarr 3.0)", default=self.general["radarr"]["version"], save=False)
|
||||
radarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="radarr", default=self.general["radarr"]["quality_profile"], req_default=True, save=False)
|
||||
radarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False)
|
||||
radarr_params["version"] = check_for_attribute(lib, "version", parent="radarr", test_list=radarr_versions, default=self.general["radarr"]["version"], save=False)
|
||||
radarr_params["add"] = check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False)
|
||||
radarr_params["search"] = check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False)
|
||||
radarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False)
|
||||
radarr_params["monitor"] = check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False)
|
||||
radarr_params["availability"] = check_for_attribute(lib, "availability", parent="radarr", test_list=radarr_availabilities, default=self.general["radarr"]["availability"], save=False)
|
||||
radarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="radarr", default=self.general["radarr"]["quality_profile"], req_default=True, save=False)
|
||||
radarr_params["tag"] = check_for_attribute(lib, "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False)
|
||||
library.Radarr = RadarrAPI(self.TMDb, radarr_params)
|
||||
radarr_params["search"] = check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False)
|
||||
library.Radarr = RadarrAPI(radarr_params)
|
||||
except Failed as e:
|
||||
util.print_multiline(e, error=True)
|
||||
logger.info(f"{params['name']} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}")
|
||||
|
@ -352,14 +396,21 @@ class Config:
|
|||
try:
|
||||
sonarr_params["url"] = check_for_attribute(lib, "url", parent="sonarr", default=self.general["sonarr"]["url"], req_default=True, save=False)
|
||||
sonarr_params["token"] = check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False)
|
||||
sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default=self.general["sonarr"]["version"], save=False)
|
||||
sonarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False)
|
||||
sonarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False)
|
||||
sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=sonarr_versions, default=self.general["sonarr"]["version"], save=False)
|
||||
sonarr_params["add"] = check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False)
|
||||
sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False)
|
||||
sonarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False)
|
||||
sonarr_params["monitor"] = check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr_monitors, default=self.general["sonarr"]["monitor"], save=False)
|
||||
sonarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False)
|
||||
if self.general["sonarr"]["language_profile"]:
|
||||
sonarr_params["language_profile"] = check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False)
|
||||
else:
|
||||
sonarr_params["language_profile"] = check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False)
|
||||
sonarr_params["series_type"] = check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr_series_types, default=self.general["sonarr"]["series_type"], save=False)
|
||||
sonarr_params["season_folder"] = check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False)
|
||||
sonarr_params["tag"] = check_for_attribute(lib, "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False)
|
||||
library.Sonarr = SonarrAPI(self.TVDb, sonarr_params, library.Plex.language)
|
||||
sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False)
|
||||
sonarr_params["cutoff_search"] = check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False)
|
||||
library.Sonarr = SonarrAPI(sonarr_params, library.Plex.language)
|
||||
except Failed as e:
|
||||
util.print_multiline(e, error=True)
|
||||
logger.info(f"{params['name']} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}")
|
||||
|
@ -382,11 +433,11 @@ class Config:
|
|||
if len(self.libraries) > 0:
|
||||
logger.info(f"{len(self.libraries)} Plex Library Connection{'s' if len(self.libraries) > 1 else ''} Successful")
|
||||
else:
|
||||
raise Failed("Plex Error: No Plex libraries were found")
|
||||
raise Failed("Plex Error: No Plex libraries were connected to")
|
||||
|
||||
util.separator()
|
||||
|
||||
def update_libraries(self, test, requested_collections):
|
||||
def update_libraries(self, test, requested_collections, resume_from):
|
||||
for library in self.libraries:
|
||||
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
|
||||
logger.info("")
|
||||
|
@ -395,7 +446,7 @@ class Config:
|
|||
util.separator(f"Mapping {library.name} Library")
|
||||
logger.info("")
|
||||
movie_map, show_map = self.map_guids(library)
|
||||
if not test:
|
||||
if not test and not resume_from:
|
||||
if library.mass_genre_update:
|
||||
self.mass_metadata(library, movie_map, show_map)
|
||||
try: library.update_metadata(self.TMDb, test)
|
||||
|
@ -403,6 +454,9 @@ class Config:
|
|||
logger.info("")
|
||||
util.separator(f"{library.name} Library {'Test ' if test else ''}Collections")
|
||||
collections = {c: library.collections[c] for c in util.get_list(requested_collections) if c in library.collections} if requested_collections else library.collections
|
||||
if resume_from and resume_from not in collections:
|
||||
logger.warning(f"Collection: {resume_from} not in {library.name}")
|
||||
continue
|
||||
if collections:
|
||||
for mapping_name, collection_attrs in collections.items():
|
||||
if test and ("test" not in collection_attrs or collection_attrs["test"] is not True):
|
||||
|
@ -420,6 +474,13 @@ class Config:
|
|||
if no_template_test:
|
||||
continue
|
||||
try:
|
||||
if resume_from and resume_from != mapping_name:
|
||||
continue
|
||||
elif resume_from == mapping_name:
|
||||
resume_from = None
|
||||
logger.info("")
|
||||
util.separator(f"Resuming Collections")
|
||||
|
||||
logger.info("")
|
||||
util.separator(f"{mapping_name} Collection")
|
||||
logger.info("")
|
||||
|
|
|
@ -6,6 +6,8 @@ from retrying import retry
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = ["imdb_list", "imdb_id"]
|
||||
|
||||
class IMDbAPI:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
@ -14,11 +16,13 @@ class IMDbAPI:
|
|||
"search": "https://www.imdb.com/search/title/?"
|
||||
}
|
||||
|
||||
def get_imdb_ids_from_url(self, imdb_url, language, limit):
|
||||
def validate_imdb_url(self, imdb_url):
|
||||
imdb_url = imdb_url.strip()
|
||||
if not imdb_url.startswith(self.urls["list"]) and not imdb_url.startswith(self.urls["search"]):
|
||||
raise Failed(f"IMDb Error: {imdb_url} must begin with either:\n| {self.urls['list']} (For Lists)\n| {self.urls['search']} (For Searches)")
|
||||
raise Failed(f"IMDb Error: {imdb_url} must begin with either:\n{self.urls['list']} (For Lists)\n{self.urls['search']} (For Searches)")
|
||||
return imdb_url
|
||||
|
||||
def get_imdb_ids_from_url(self, imdb_url, language, limit):
|
||||
if imdb_url.startswith(self.urls["list"]):
|
||||
try: list_id = re.search("(\\d+)", str(imdb_url)).group(1)
|
||||
except AttributeError: raise Failed(f"IMDb Error: Failed to parse List ID from {imdb_url}")
|
||||
|
|
|
@ -6,6 +6,8 @@ from retrying import retry
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = ["letterboxd_list", "letterboxd_list_details"]
|
||||
|
||||
class LetterboxdAPI:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
|
|
@ -6,6 +6,73 @@ from ruamel import yaml
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = [
|
||||
"mal_id",
|
||||
"mal_all",
|
||||
"mal_airing",
|
||||
"mal_upcoming",
|
||||
"mal_tv",
|
||||
"mal_ova",
|
||||
"mal_movie",
|
||||
"mal_special",
|
||||
"mal_popular",
|
||||
"mal_favorite",
|
||||
"mal_season",
|
||||
"mal_suggested",
|
||||
"mal_userlist"
|
||||
]
|
||||
mal_ranked_name = {
|
||||
"mal_all": "all",
|
||||
"mal_airing": "airing",
|
||||
"mal_upcoming": "upcoming",
|
||||
"mal_tv": "tv",
|
||||
"mal_ova": "ova",
|
||||
"mal_movie": "movie",
|
||||
"mal_special": "special",
|
||||
"mal_popular": "bypopularity",
|
||||
"mal_favorite": "favorite"
|
||||
}
|
||||
season_sort = {
|
||||
"anime_score": "anime_score",
|
||||
"anime_num_list_users": "anime_num_list_users",
|
||||
"score": "anime_score",
|
||||
"members": "anime_num_list_users"
|
||||
}
|
||||
pretty_names = {
|
||||
"anime_score": "Score",
|
||||
"anime_num_list_users": "Members",
|
||||
"list_score": "Score",
|
||||
"list_updated_at": "Last Updated",
|
||||
"anime_title": "Title",
|
||||
"anime_start_date": "Start Date",
|
||||
"all": "All Anime",
|
||||
"watching": "Currently Watching",
|
||||
"completed": "Completed",
|
||||
"on_hold": "On Hold",
|
||||
"dropped": "Dropped",
|
||||
"plan_to_watch": "Plan to Watch"
|
||||
}
|
||||
userlist_sort = {
|
||||
"score": "list_score",
|
||||
"list_score": "list_score",
|
||||
"last_updated": "list_updated_at",
|
||||
"list_updated": "list_updated_at",
|
||||
"list_updated_at": "list_updated_at",
|
||||
"title": "anime_title",
|
||||
"anime_title": "anime_title",
|
||||
"start_date": "anime_start_date",
|
||||
"anime_start_date": "anime_start_date"
|
||||
}
|
||||
userlist_status = [
|
||||
"all",
|
||||
"watching",
|
||||
"completed",
|
||||
"on_hold",
|
||||
"dropped",
|
||||
"plan_to_watch"
|
||||
]
|
||||
|
||||
|
||||
class MyAnimeListIDList:
|
||||
def __init__(self):
|
||||
self.ids = json.loads(requests.get("https://raw.githubusercontent.com/Fribb/anime-lists/master/animeMapping_full.json").content)
|
||||
|
@ -155,14 +222,14 @@ class MyAnimeListAPI:
|
|||
mal_ids = [data]
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty}: {data}")
|
||||
elif method in util.mal_ranked_name:
|
||||
mal_ids = self.get_ranked(util.mal_ranked_name[method], data)
|
||||
elif method in mal_ranked_name:
|
||||
mal_ids = self.get_ranked(mal_ranked_name[method], data)
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty}: {data} Anime")
|
||||
elif method == "mal_season":
|
||||
mal_ids = self.get_season(data["season"], data["year"], data["sort_by"], data["limit"])
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty}: {data['limit']} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {util.mal_pretty[data['sort_by']]}")
|
||||
logger.info(f"Processing {pretty}: {data['limit']} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {pretty_names[data['sort_by']]}")
|
||||
elif method == "mal_suggested":
|
||||
mal_ids = self.get_suggestions(data)
|
||||
if status_message:
|
||||
|
@ -170,7 +237,7 @@ class MyAnimeListAPI:
|
|||
elif method == "mal_userlist":
|
||||
mal_ids = self.get_userlist(data["username"], data["status"], data["sort_by"], data["limit"])
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty}: {data['limit']} Anime from {self.get_username() if data['username'] == '@me' else data['username']}'s {util.mal_pretty[data['status']]} list sorted by {util.mal_pretty[data['sort_by']]}")
|
||||
logger.info(f"Processing {pretty}: {data['limit']} Anime from {self.get_username() if data['username'] == '@me' else data['username']}'s {pretty_names[data['status']]} list sorted by {pretty_names[data['sort_by']]}")
|
||||
else:
|
||||
raise Failed(f"MyAnimeList Error: Method {method} not supported")
|
||||
show_ids = []
|
||||
|
|
499
modules/plex.py
499
modules/plex.py
|
@ -12,6 +12,100 @@ from ruamel import yaml
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = ["plex_all", "plex_collection", "plex_collectionless", "plex_search",]
|
||||
search_translation = {
|
||||
"audio_language": "audioLanguage",
|
||||
"content_rating": "contentRating",
|
||||
"subtitle_language": "subtitleLanguage",
|
||||
"added": "addedAt",
|
||||
"originally_available": "originallyAvailableAt",
|
||||
"audience_rating": "audienceRating",
|
||||
"critic_rating": "rating"
|
||||
}
|
||||
episode_sorting_options = {"default": "-1", "oldest": "0", "newest": "1"}
|
||||
keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30}
|
||||
delete_episodes_options = {"never": 0, "day": 1, "week": 7, "refresh": 100}
|
||||
season_display_options = {"default": -1, "show": 0, "hide": 1}
|
||||
episode_ordering_options = {"default": None, "tmdb_aired": "tmdbAiring", "tvdb_aired": "airing", "tvdb_dvd": "dvd", "tvdb_absolute": "absolute"}
|
||||
plex_languages = ["default", "ar-SA", "ca-ES", "cs-CZ", "da-DK", "de-DE", "el-GR", "en-AU", "en-CA", "en-GB", "en-US",
|
||||
"es-ES", "es-MX", "et-EE", "fa-IR", "fi-FI", "fr-CA", "fr-FR", "he-IL", "hi-IN", "hu-HU", "id-ID",
|
||||
"it-IT", "ja-JP", "ko-KR", "lt-LT", "lv-LV", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro-RO",
|
||||
"ru-RU", "sk-SK", "sv-SE", "th-TH", "tr-TR", "uk-UA", "vi-VN", "zh-CN", "zh-HK", "zh-TW"]
|
||||
metadata_language_options = {lang.lower(): lang for lang in plex_languages}
|
||||
metadata_language_options["default"] = None
|
||||
filter_alias = {
|
||||
"actor": "actors",
|
||||
"audience_rating": "audienceRating",
|
||||
"collection": "collections",
|
||||
"content_rating": "contentRating",
|
||||
"country": "countries",
|
||||
"critic_rating": "rating",
|
||||
"director": "directors",
|
||||
"genre": "genres",
|
||||
"originally_available": "originallyAvailableAt",
|
||||
"tmdb_vote_count": "vote_count",
|
||||
"writer": "writers"
|
||||
}
|
||||
searches = [
|
||||
"title", "title.and", "title.not", "title.begins", "title.ends",
|
||||
"studio", "studio.and", "studio.not", "studio.begins", "studio.ends",
|
||||
"actor", "actor.and", "actor.not",
|
||||
"audio_language", "audio_language.and", "audio_language.not",
|
||||
"collection", "collection.and", "collection.not",
|
||||
"content_rating", "content_rating.and", "content_rating.not",
|
||||
"country", "country.and", "country.not",
|
||||
"director", "director.and", "director.not",
|
||||
"genre", "genre.and", "genre.not",
|
||||
"label", "label.and", "label.not",
|
||||
"network", "network.and", "network.not",
|
||||
"producer", "producer.and", "producer.not",
|
||||
"subtitle_language", "subtitle_language.and", "subtitle_language.not",
|
||||
"writer", "writer.and", "writer.not",
|
||||
"decade", "resolution",
|
||||
"added.before", "added.after",
|
||||
"originally_available.before", "originally_available.after",
|
||||
"duration.greater", "duration.less",
|
||||
"audience_rating.greater", "audience_rating.less",
|
||||
"critic_rating.greater", "critic_rating.less",
|
||||
"year", "year.not", "year.greater", "year.less"
|
||||
]
|
||||
movie_only_searches = [
|
||||
"audio_language", "audio_language.and", "audio_language.not",
|
||||
"country", "country.and", "country.not",
|
||||
"subtitle_language", "subtitle_language.and", "subtitle_language.not",
|
||||
"decade", "resolution",
|
||||
"originally_available.before", "originally_available.after",
|
||||
"duration.greater", "duration.less"
|
||||
]
|
||||
show_only_searches = [
|
||||
"network", "network.and", "network.not",
|
||||
]
|
||||
tmdb_searches = [
|
||||
"actor", "actor.and", "actor.not",
|
||||
"director", "director.and", "director.not",
|
||||
"producer", "producer.and", "producer.not",
|
||||
"writer", "writer.and", "writer.not"
|
||||
]
|
||||
sorts = {
|
||||
None: None,
|
||||
"title.asc": "titleSort:asc", "title.desc": "titleSort:desc",
|
||||
"originally_available.asc": "originallyAvailableAt:asc", "originally_available.desc": "originallyAvailableAt:desc",
|
||||
"critic_rating.asc": "rating:asc", "critic_rating.desc": "rating:desc",
|
||||
"audience_rating.asc": "audienceRating:asc", "audience_rating.desc": "audienceRating:desc",
|
||||
"duration.asc": "duration:asc", "duration.desc": "duration:desc",
|
||||
"added.asc": "addedAt:asc", "added.desc": "addedAt:desc"
|
||||
}
|
||||
modifiers = {
|
||||
".and": "&",
|
||||
".not": "!",
|
||||
".begins": "<",
|
||||
".ends": ">",
|
||||
".before": "<<",
|
||||
".after": ">>",
|
||||
".greater": ">>",
|
||||
".less": "<<"
|
||||
}
|
||||
|
||||
class PlexAPI:
|
||||
def __init__(self, params, TMDb, TVDb):
|
||||
try:
|
||||
|
@ -28,10 +122,15 @@ class PlexAPI:
|
|||
self.Plex = next((s for s in self.PlexServer.library.sections() if s.title == params["name"] and ((self.is_movie and isinstance(s, MovieSection)) or (self.is_show and isinstance(s, ShowSection)))), None)
|
||||
if not self.Plex:
|
||||
raise Failed(f"Plex Error: Plex Library {params['name']} not found")
|
||||
|
||||
logger.info(f"Using Metadata File: {params['metadata_path']}")
|
||||
try:
|
||||
self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8"))
|
||||
except yaml.scanner.ScannerError as e:
|
||||
raise Failed(f"YAML Error: {util.tab_new_lines(e)}")
|
||||
except yaml.scanner.ScannerError as ye:
|
||||
raise Failed(f"YAML Error: {util.tab_new_lines(ye)}")
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
raise Failed(f"YAML Error: {e}")
|
||||
|
||||
def get_dict(attribute):
|
||||
if attribute in self.data:
|
||||
|
@ -91,18 +190,21 @@ class PlexAPI:
|
|||
return self.PlexServer.search(data)
|
||||
|
||||
def get_search_choices(self, search_name, key=False):
|
||||
if key: return {c.key.lower(): c.key for c in self.Plex.listFilterChoices(search_name)}
|
||||
else: return {c.title.lower(): c.title for c in self.Plex.listFilterChoices(search_name)}
|
||||
try:
|
||||
if key: return {c.key.lower(): c.key for c in self.Plex.listFilterChoices(search_name)}
|
||||
else: return {c.title.lower(): c.title for c in self.Plex.listFilterChoices(search_name)}
|
||||
except NotFound:
|
||||
raise Failed(f"Collection Error: plex search attribute: {search_name} only supported with Plex's New TV Agent")
|
||||
|
||||
def validate_search_list(self, data, search_name):
|
||||
final_search = util.search_alias[search_name] if search_name in util.search_alias else search_name
|
||||
final_search = search_translation[search_name] if search_name in search_translation else search_name
|
||||
search_choices = self.get_search_choices(final_search, key=final_search.endswith("Language"))
|
||||
valid_list = []
|
||||
for value in util.get_list(data):
|
||||
if str(value).lower() in search_choices:
|
||||
valid_list.append(search_choices[str(value).lower()])
|
||||
else:
|
||||
raise Failed(f"Plex Error: {search_name}: {value} not found")
|
||||
logger.error(f"Plex Error: {search_name}: {value} not found")
|
||||
return valid_list
|
||||
|
||||
def get_all_collections(self):
|
||||
|
@ -122,6 +224,108 @@ class PlexAPI:
|
|||
raise Failed(f"Collection Error: No valid Plex Collections in {collections}")
|
||||
return valid_collections
|
||||
|
||||
def get_items(self, method, data, status_message=True):
|
||||
if status_message:
|
||||
logger.debug(f"Data: {data}")
|
||||
pretty = util.pretty_names[method] if method in util.pretty_names else method
|
||||
media_type = "Movie" if self.is_movie else "Show"
|
||||
items = []
|
||||
if method == "plex_all":
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty} {media_type}s")
|
||||
items = self.Plex.all()
|
||||
elif method == "plex_collection":
|
||||
if status_message:
|
||||
logger.info(f"Processing {pretty} {data}")
|
||||
items = data.items()
|
||||
elif method == "plex_search":
|
||||
search_terms = {}
|
||||
has_processed = False
|
||||
search_limit = None
|
||||
search_sort = None
|
||||
for search_method, search_data in data.items():
|
||||
if search_method == "limit":
|
||||
search_limit = search_data
|
||||
elif search_method == "sort_by":
|
||||
search_sort = search_data
|
||||
else:
|
||||
search, modifier = os.path.splitext(str(search_method).lower())
|
||||
final_search = search_translation[search] if search in search_translation else search
|
||||
if search == "originally_available" and modifier == "":
|
||||
final_mod = ">>"
|
||||
elif search == "originally_available" and modifier == ".not":
|
||||
final_mod = "<<"
|
||||
elif search in ["critic_rating", "audience_rating"] and modifier == ".greater":
|
||||
final_mod = "__gte"
|
||||
elif search in ["critic_rating", "audience_rating"] and modifier == ".less":
|
||||
final_mod = "__lt"
|
||||
else:
|
||||
final_mod = modifiers[modifier] if modifier in modifiers else ""
|
||||
final_method = f"{final_search}{final_mod}"
|
||||
|
||||
if search == "duration":
|
||||
search_terms[final_method] = search_data * 60000
|
||||
elif search in ["added", "originally_available"] and modifier in ["", ".not"]:
|
||||
search_terms[final_method] = f"{search_data}d"
|
||||
else:
|
||||
search_terms[final_method] = search_data
|
||||
|
||||
if status_message:
|
||||
if search in ["added", "originally_available"] or modifier in [".greater", ".less", ".before", ".after"]:
|
||||
ors = f"{search_method}({search_data}"
|
||||
else:
|
||||
ors = ""
|
||||
conjunction = " AND " if final_mod == "&" else " OR "
|
||||
for o, param in enumerate(search_data):
|
||||
or_des = conjunction if o > 0 else f"{search_method}("
|
||||
ors += f"{or_des}{param}"
|
||||
if has_processed:
|
||||
logger.info(f"\t\t AND {ors})")
|
||||
else:
|
||||
logger.info(f"Processing {pretty}: {ors})")
|
||||
has_processed = True
|
||||
if status_message:
|
||||
if search_sort:
|
||||
logger.info(f"\t\t SORT BY {search_sort})")
|
||||
if search_limit:
|
||||
logger.info(f"\t\t LIMIT {search_limit})")
|
||||
logger.debug(f"Search: {search_terms}")
|
||||
return self.Plex.search(sort=sorts[search_sort], maxresults=search_limit, **search_terms)
|
||||
elif method == "plex_collectionless":
|
||||
good_collections = []
|
||||
for col in self.get_all_collections():
|
||||
keep_collection = True
|
||||
for pre in data["exclude_prefix"]:
|
||||
if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)):
|
||||
keep_collection = False
|
||||
break
|
||||
if keep_collection:
|
||||
for ext in data["exclude"]:
|
||||
if col.title == ext or (col.titleSort and col.titleSort == ext):
|
||||
keep_collection = False
|
||||
break
|
||||
if keep_collection:
|
||||
good_collections.append(col.index)
|
||||
all_items = self.Plex.all()
|
||||
length = 0
|
||||
for i, item in enumerate(all_items, 1):
|
||||
length = util.print_return(length, f"Processing: {i}/{len(all_items)} {item.title}")
|
||||
add_item = True
|
||||
item.reload()
|
||||
for collection in item.collections:
|
||||
if collection.id in good_collections:
|
||||
add_item = False
|
||||
break
|
||||
if add_item:
|
||||
items.append(item)
|
||||
util.print_end(length, f"Processed {len(all_items)} {'Movies' if self.is_movie else 'Shows'}")
|
||||
else:
|
||||
raise Failed(f"Plex Error: Method {method} not supported")
|
||||
if len(items) > 0:
|
||||
return items
|
||||
else:
|
||||
raise Failed("Plex Error: No Items found in Plex")
|
||||
|
||||
def add_missing(self, collection, items, is_movie):
|
||||
col_name = collection.encode("ascii", "replace").decode()
|
||||
if col_name not in self.missing:
|
||||
|
@ -157,7 +361,7 @@ class PlexAPI:
|
|||
for filter_method, filter_data in filters:
|
||||
modifier = filter_method[-4:]
|
||||
method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method
|
||||
method_name = util.filter_alias[method] if method in util.filter_alias else method
|
||||
method_name = filter_alias[method] if method in filter_alias else method
|
||||
if method_name == "max_age":
|
||||
threshold_date = datetime.now() - timedelta(days=filter_data)
|
||||
if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date:
|
||||
|
@ -210,7 +414,7 @@ class PlexAPI:
|
|||
attr = tmdb_item.vote_count
|
||||
else:
|
||||
attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name)
|
||||
if (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data):
|
||||
if attr is None or (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data):
|
||||
match = False
|
||||
break
|
||||
else:
|
||||
|
@ -306,29 +510,34 @@ class PlexAPI:
|
|||
tagline = tmdb_item.tagline if tmdb_item and len(tmdb_item.tagline) > 0 else None
|
||||
summary = tmdb_item.overview if tmdb_item else None
|
||||
|
||||
details_updated = False
|
||||
advance_details_updated = False
|
||||
genre_updated = False
|
||||
label_updated = False
|
||||
season_updated = False
|
||||
episode_updated = False
|
||||
updated = False
|
||||
|
||||
edits = {}
|
||||
def add_edit(name, current, group, alias, key=None, value=None):
|
||||
def add_edit(name, current, group, alias, key=None, value=None, var_type="str"):
|
||||
if value or name in alias:
|
||||
if value or group[alias[name]]:
|
||||
if key is None: key = name
|
||||
if value is None: value = group[alias[name]]
|
||||
if str(current) != str(value):
|
||||
edits[f"{key}.value"] = value
|
||||
edits[f"{key}.locked"] = 1
|
||||
logger.info(f"Detail: {name} updated to {value}")
|
||||
try:
|
||||
if var_type == "date":
|
||||
final_value = util.check_date(value, name, return_string=True, plex_date=True)
|
||||
elif var_type == "float":
|
||||
final_value = util.check_number(value, name, number_type="float", minimum=0, maximum=10)
|
||||
else:
|
||||
final_value = value
|
||||
if str(current) != str(final_value):
|
||||
edits[f"{key}.value"] = final_value
|
||||
edits[f"{key}.locked"] = 1
|
||||
logger.info(f"Detail: {name} updated to {final_value}")
|
||||
except Failed as ee:
|
||||
logger.error(ee)
|
||||
else:
|
||||
logger.error(f"Metadata Error: {name} attribute is blank")
|
||||
add_edit("title", item.title, meta, methods, value=title)
|
||||
add_edit("sort_title", item.titleSort, meta, methods, key="titleSort")
|
||||
add_edit("originally_available", str(item.originallyAvailableAt)[:-9], meta, methods, key="originallyAvailableAt", value=originally_available)
|
||||
add_edit("rating", item.rating, meta, methods, value=rating)
|
||||
add_edit("originally_available", str(item.originallyAvailableAt)[:-9], meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date")
|
||||
add_edit("critic_rating", item.rating, meta, methods, value=rating, key="rating", var_type="float")
|
||||
add_edit("audience_rating", item.audienceRating, meta, methods, key="audienceRating", var_type="float")
|
||||
add_edit("content_rating", item.contentRating, meta, methods, key="contentRating")
|
||||
add_edit("original_title", item.originalTitle, meta, methods, key="originalTitle", value=original_title)
|
||||
add_edit("studio", item.studio, meta, methods, value=studio)
|
||||
|
@ -336,7 +545,7 @@ class PlexAPI:
|
|||
add_edit("summary", item.summary, meta, methods, value=summary)
|
||||
if len(edits) > 0:
|
||||
logger.debug(f"Details Update: {edits}")
|
||||
details_updated = True
|
||||
updated = True
|
||||
try:
|
||||
item.edit(**edits)
|
||||
item.reload()
|
||||
|
@ -346,143 +555,35 @@ class PlexAPI:
|
|||
logger.error(f"{item_type}: {mapping_name} Details Update Failed")
|
||||
|
||||
advance_edits = {}
|
||||
if self.is_show:
|
||||
|
||||
if "episode_sorting" in methods:
|
||||
if meta[methods["episode_sorting"]]:
|
||||
method_data = str(meta[methods["episode_sorting"]]).lower()
|
||||
if method_data in ["default", "oldest", "newest"]:
|
||||
if method_data == "default" and item.episodeSort != "-1":
|
||||
advance_edits["episodeSort"] = "-1"
|
||||
elif method_data == "oldest" and item.episodeSort != "0":
|
||||
advance_edits["episodeSort"] = "0"
|
||||
elif method_data == "newest" and item.episodeSort != "1":
|
||||
advance_edits["episodeSort"] = "1"
|
||||
if "episodeSort" in advance_edits:
|
||||
logger.info(f"Detail: episode_sorting updated to {method_data}")
|
||||
def add_advanced_edit(attr, options, key=None, show_library=False):
|
||||
if key is None:
|
||||
key = attr
|
||||
if attr in methods:
|
||||
if show_library and not self.is_show:
|
||||
logger.error(f"Metadata Error: {attr} attribute only works for show libraries")
|
||||
elif meta[methods[attr]]:
|
||||
method_data = str(meta[methods[attr]]).lower()
|
||||
if method_data in options and getattr(item, key) != options[method_data]:
|
||||
advance_edits[key] = options[method_data]
|
||||
logger.info(f"Detail: {attr} updated to {method_data}")
|
||||
else:
|
||||
logger.error(f"Metadata Error: {meta[methods['episode_sorting']]} episode_sorting attribute invalid")
|
||||
logger.error(f"Metadata Error: {meta[methods[attr]]} {attr} attribute invalid")
|
||||
else:
|
||||
logger.error(f"Metadata Error: episode_sorting attribute is blank")
|
||||
logger.error(f"Metadata Error: {attr} attribute is blank")
|
||||
|
||||
if "keep_episodes" in methods:
|
||||
if meta[methods["keep_episodes"]]:
|
||||
method_data = str(meta[methods["keep_episodes"]]).lower()
|
||||
if method_data in ["all", "5_latest", "3_latest", "latest", "past_3", "past_7", "past_30"]:
|
||||
if method_data == "all" and item.autoDeletionItemPolicyUnwatchedLibrary != 0:
|
||||
advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 0
|
||||
elif method_data == "5_latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 5:
|
||||
advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 5
|
||||
elif method_data == "3_latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 3:
|
||||
advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 3
|
||||
elif method_data == "latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 1:
|
||||
advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 1
|
||||
elif method_data == "past_3" and item.autoDeletionItemPolicyUnwatchedLibrary != -3:
|
||||
advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -3
|
||||
elif method_data == "past_7" and item.autoDeletionItemPolicyUnwatchedLibrary != -7:
|
||||
advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -7
|
||||
elif method_data == "past_30" and item.autoDeletionItemPolicyUnwatchedLibrary != -30:
|
||||
advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -30
|
||||
if "autoDeletionItemPolicyUnwatchedLibrary" in advance_edits:
|
||||
logger.info(f"Detail: keep_episodes updated to {method_data}")
|
||||
else:
|
||||
logger.error(f"Metadata Error: {meta[methods['keep_episodes']]} keep_episodes attribute invalid")
|
||||
else:
|
||||
logger.error(f"Metadata Error: keep_episodes attribute is blank")
|
||||
add_advanced_edit("episode_sorting", episode_sorting_options, key="episodeSort", show_library=True)
|
||||
add_advanced_edit("keep_episodes", keep_episodes_options, key="autoDeletionItemPolicyUnwatchedLibrary", show_library=True)
|
||||
add_advanced_edit("delete_episodes", delete_episodes_options, key="autoDeletionItemPolicyWatchedLibrary", show_library=True)
|
||||
add_advanced_edit("season_display", season_display_options, key="flattenSeasons", show_library=True)
|
||||
add_advanced_edit("episode_ordering", episode_ordering_options, key="showOrdering", show_library=True)
|
||||
add_advanced_edit("metadata_language", metadata_language_options, key="languageOverride")
|
||||
|
||||
if "delete_episodes" in methods:
|
||||
if meta[methods["delete_episodes"]]:
|
||||
method_data = str(meta[methods["delete_episodes"]]).lower()
|
||||
if method_data in ["never", "day", "week", "refresh"]:
|
||||
if method_data == "never" and item.autoDeletionItemPolicyWatchedLibrary != 0:
|
||||
advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 0
|
||||
elif method_data == "day" and item.autoDeletionItemPolicyWatchedLibrary != 1:
|
||||
advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 1
|
||||
elif method_data == "week" and item.autoDeletionItemPolicyWatchedLibrary != 7:
|
||||
advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 7
|
||||
elif method_data == "refresh" and item.autoDeletionItemPolicyWatchedLibrary != 100:
|
||||
advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 100
|
||||
if "autoDeletionItemPolicyWatchedLibrary" in advance_edits:
|
||||
logger.info(f"Detail: delete_episodes updated to {method_data}")
|
||||
else:
|
||||
logger.error(f"Metadata Error: {meta[methods['delete_episodes']]} delete_episodes attribute invalid")
|
||||
else:
|
||||
logger.error(f"Metadata Error: delete_episodes attribute is blank")
|
||||
|
||||
if "season_display" in methods:
|
||||
if meta[methods["season_display"]]:
|
||||
method_data = str(meta[methods["season_display"]]).lower()
|
||||
if method_data in ["default", "hide", "show"]:
|
||||
if method_data == "default" and item.flattenSeasons != -1:
|
||||
advance_edits["flattenSeasons"] = -1
|
||||
elif method_data == "show" and item.flattenSeasons != 0:
|
||||
advance_edits["flattenSeasons"] = 0
|
||||
elif method_data == "hide" and item.flattenSeasons != 1:
|
||||
advance_edits["flattenSeasons"] = 1
|
||||
if "flattenSeasons" in advance_edits:
|
||||
logger.info(f"Detail: season_display updated to {method_data}")
|
||||
else:
|
||||
logger.error(f"Metadata Error: {meta[methods['season_display']]} season_display attribute invalid")
|
||||
else:
|
||||
logger.error(f"Metadata Error: season_display attribute is blank")
|
||||
|
||||
if "episode_ordering" in methods:
|
||||
if meta[methods["episode_ordering"]]:
|
||||
method_data = str(meta[methods["episode_ordering"]]).lower()
|
||||
if method_data in ["default", "tmdb_aired", "tvdb_aired", "tvdb_dvd", "tvdb_absolute"]:
|
||||
if method_data == "default" and item.showOrdering is not None:
|
||||
advance_edits["showOrdering"] = None
|
||||
elif method_data == "tmdb_aired" and item.showOrdering != "tmdbAiring":
|
||||
advance_edits["showOrdering"] = "tmdbAiring"
|
||||
elif method_data == "tvdb_aired" and item.showOrdering != "airing":
|
||||
advance_edits["showOrdering"] = "airing"
|
||||
elif method_data == "tvdb_dvd" and item.showOrdering != "dvd":
|
||||
advance_edits["showOrdering"] = "dvd"
|
||||
elif method_data == "tvdb_absolute" and item.showOrdering != "absolute":
|
||||
advance_edits["showOrdering"] = "absolute"
|
||||
if "showOrdering" in advance_edits:
|
||||
logger.info(f"Detail: episode_ordering updated to {method_data}")
|
||||
else:
|
||||
logger.error(f"Metadata Error: {meta[methods['episode_ordering']]} episode_ordering attribute invalid")
|
||||
else:
|
||||
logger.error(f"Metadata Error: episode_ordering attribute is blank")
|
||||
|
||||
if "metadata_language" in methods:
|
||||
if meta[methods["metadata_language"]]:
|
||||
method_data = str(meta[methods["metadata_language"]]).lower()
|
||||
lower_languages = {la.lower(): la for la in util.plex_languages}
|
||||
if method_data in lower_languages:
|
||||
if method_data == "default" and item.languageOverride is None:
|
||||
advance_edits["languageOverride"] = None
|
||||
elif str(item.languageOverride).lower() != lower_languages[method_data]:
|
||||
advance_edits["languageOverride"] = lower_languages[method_data]
|
||||
if "languageOverride" in advance_edits:
|
||||
logger.info(f"Detail: metadata_language updated to {method_data}")
|
||||
else:
|
||||
logger.error(f"Metadata Error: {meta[methods['metadata_language']]} metadata_language attribute invalid")
|
||||
else:
|
||||
logger.error(f"Metadata Error: metadata_language attribute is blank")
|
||||
|
||||
if "use_original_title" in methods:
|
||||
if meta[methods["use_original_title"]]:
|
||||
method_data = str(meta[methods["use_original_title"]]).lower()
|
||||
if method_data in ["default", "no", "yes"]:
|
||||
if method_data == "default" and item.useOriginalTitle != -1:
|
||||
advance_edits["useOriginalTitle"] = -1
|
||||
elif method_data == "no" and item.useOriginalTitle != 0:
|
||||
advance_edits["useOriginalTitle"] = 0
|
||||
elif method_data == "yes" and item.useOriginalTitle != 1:
|
||||
advance_edits["useOriginalTitle"] = 1
|
||||
if "useOriginalTitle" in advance_edits:
|
||||
logger.info(f"Detail: use_original_title updated to {method_data}")
|
||||
else:
|
||||
logger.error(f"Metadata Error: {meta[methods['use_original_title']]} use_original_title attribute invalid")
|
||||
else:
|
||||
logger.error(f"Metadata Error: use_original_title attribute is blank")
|
||||
use_original_title_options = {"default": -1, "no": 0, "yes": 1}
|
||||
add_advanced_edit("use_original_title", use_original_title_options, key="useOriginalTitle")
|
||||
|
||||
if len(advance_edits) > 0:
|
||||
logger.debug(f"Details Update: {advance_edits}")
|
||||
advance_details_updated = True
|
||||
updated = True
|
||||
try:
|
||||
check_dict = {pref.id: list(pref.enumValues.keys()) for pref in item.preferences()}
|
||||
logger.info(check_dict)
|
||||
|
@ -493,51 +594,44 @@ class PlexAPI:
|
|||
util.print_stacktrace()
|
||||
logger.error(f"{item_type}: {mapping_name} Advanced Details Update Failed")
|
||||
|
||||
genres = []
|
||||
if tmdb_item:
|
||||
genres.extend([genre.name for genre in tmdb_item.genres])
|
||||
if "genre" in methods:
|
||||
if meta[methods["genre"]]:
|
||||
genres.extend(util.get_list(meta[methods["genre"]]))
|
||||
else:
|
||||
logger.error("Metadata Error: genre attribute is blank")
|
||||
if len(genres) > 0:
|
||||
item_genres = [genre.tag for genre in item.genres]
|
||||
if "genre_sync_mode" in methods:
|
||||
if meta[methods["genre_sync_mode"]] is None:
|
||||
logger.error("Metadata Error: genre_sync_mode attribute is blank defaulting to append")
|
||||
elif str(meta[methods["genre_sync_mode"]]).lower() not in ["append", "sync"]:
|
||||
logger.error("Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
|
||||
elif str(meta["genre_sync_mode"]).lower() == "sync":
|
||||
for genre in (g for g in item_genres if g not in genres):
|
||||
genre_updated = True
|
||||
item.removeGenre(genre)
|
||||
logger.info(f"Detail: Genre {genre} removed")
|
||||
for genre in (g for g in genres if g not in item_genres):
|
||||
genre_updated = True
|
||||
item.addGenre(genre)
|
||||
logger.info(f"Detail: Genre {genre} added")
|
||||
def edit_tags(attr, obj, key=None, extra=None, movie_library=False):
|
||||
if key is None:
|
||||
key = f"{attr}s"
|
||||
if attr in methods and f"{attr}.sync" in methods:
|
||||
logger.error(f"Metadata Error: Cannot use {attr} and {attr}.sync together")
|
||||
elif attr in methods or f"{attr}.sync" in methods:
|
||||
attr_key = attr if attr in methods else f"{attr}.sync"
|
||||
if movie_library and not self.is_movie:
|
||||
logger.error(f"Metadata Error: {attr_key} attribute only works for movie libraries")
|
||||
elif meta[methods[attr_key]] or extra:
|
||||
item_tags = [item_tag.tag for item_tag in getattr(obj, key)]
|
||||
input_tags = []
|
||||
if meta[methods[attr_key]]:
|
||||
input_tags.extend(util.get_list(meta[methods[attr_key]]))
|
||||
if extra:
|
||||
input_tags.extend(extra)
|
||||
if f"{attr}.sync" in methods:
|
||||
remove_method = getattr(obj, f"remove{attr.capitalize()}")
|
||||
for tag in (t for t in item_tags if t not in input_tags):
|
||||
updated = True
|
||||
remove_method(tag)
|
||||
logger.info(f"Detail: {attr.capitalize()} {tag} removed")
|
||||
add_method = getattr(obj, f"add{attr.capitalize()}")
|
||||
for tag in (t for t in input_tags if t not in item_tags):
|
||||
updated = True
|
||||
add_method(tag)
|
||||
logger.info(f"Detail: {attr.capitalize()} {tag} added")
|
||||
else:
|
||||
logger.error(f"Metadata Error: {attr} attribute is blank")
|
||||
|
||||
if "label" in methods:
|
||||
if meta[methods["label"]]:
|
||||
item_labels = [label.tag for label in item.labels]
|
||||
labels = util.get_list(meta[methods["label"]])
|
||||
if "label_sync_mode" in methods:
|
||||
if meta[methods["label_sync_mode"]] is None:
|
||||
logger.error("Metadata Error: label_sync_mode attribute is blank defaulting to append")
|
||||
elif str(meta[methods["label_sync_mode"]]).lower() not in ["append", "sync"]:
|
||||
logger.error("Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
|
||||
elif str(meta[methods["label_sync_mode"]]).lower() == "sync":
|
||||
for label in (la for la in item_labels if la not in labels):
|
||||
label_updated = True
|
||||
item.removeLabel(label)
|
||||
logger.info(f"Detail: Label {label} removed")
|
||||
for label in (la for la in labels if la not in item_labels):
|
||||
label_updated = True
|
||||
item.addLabel(label)
|
||||
logger.info(f"Detail: Label {label} added")
|
||||
else:
|
||||
logger.error("Metadata Error: label attribute is blank")
|
||||
genres = [genre.name for genre in tmdb_item.genres] if tmdb_item else []
|
||||
edit_tags("genre", item, extra=genres)
|
||||
edit_tags("label", item)
|
||||
edit_tags("collection", item)
|
||||
edit_tags("country", item, key="countries", movie_library=True)
|
||||
edit_tags("director", item, movie_library=True)
|
||||
edit_tags("producer", item, movie_library=True)
|
||||
edit_tags("writer", item, movie_library=True)
|
||||
|
||||
if "seasons" in methods and self.is_show:
|
||||
if meta[methods["seasons"]]:
|
||||
|
@ -570,7 +664,7 @@ class PlexAPI:
|
|||
add_edit("summary", season.summary, season_methods, season_dict)
|
||||
if len(edits) > 0:
|
||||
logger.debug(f"Season: {season_id} Details Update: {edits}")
|
||||
season_updated = True
|
||||
updated = True
|
||||
try:
|
||||
season.edit(**edits)
|
||||
season.reload()
|
||||
|
@ -582,6 +676,8 @@ class PlexAPI:
|
|||
logger.error(f"Metadata Error: Season: {season_id} invalid, it must be an integer")
|
||||
else:
|
||||
logger.error("Metadata Error: seasons attribute is blank")
|
||||
elif "seasons" in methods:
|
||||
logger.error("Metadata Error: seasons attribute only works for show libraries")
|
||||
|
||||
if "episodes" in methods and self.is_show:
|
||||
if meta[methods["episodes"]]:
|
||||
|
@ -620,7 +716,7 @@ class PlexAPI:
|
|||
add_edit("summary", episode.summary, episode_dict, episode_methods)
|
||||
if len(edits) > 0:
|
||||
logger.debug(f"Season: {season_id} Episode: {episode_id} Details Update: {edits}")
|
||||
episode_updated = True
|
||||
updated = True
|
||||
try:
|
||||
episode.edit(**edits)
|
||||
episode.reload()
|
||||
|
@ -629,10 +725,15 @@ class PlexAPI:
|
|||
except BadRequest:
|
||||
util.print_stacktrace()
|
||||
logger.error(f"Season: {season_id} Episode: {episode_id} Details Update Failed")
|
||||
edit_tags("director", episode)
|
||||
edit_tags("writer", episode)
|
||||
|
||||
else:
|
||||
logger.error(f"Metadata Error: episode {episode_str} invalid must have S##E## format")
|
||||
else:
|
||||
logger.error("Metadata Error: episodes attribute is blank")
|
||||
elif "episodes" in methods:
|
||||
logger.error("Metadata Error: episodes attribute only works for show libraries")
|
||||
|
||||
if not details_updated and not advance_details_updated and not genre_updated and not label_updated and not season_updated and not episode_updated:
|
||||
if not updated:
|
||||
logger.info(f"{item_type}: {mapping_name} Details Update Not Needed")
|
|
@ -1,108 +1,130 @@
|
|||
import logging, re, requests
|
||||
import logging, requests
|
||||
from modules import util
|
||||
from modules.util import Failed
|
||||
from retrying import retry
|
||||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
availability_translation = {
|
||||
"announced": "announced",
|
||||
"cinemas": "inCinemas",
|
||||
"released": "released",
|
||||
"db": "preDB"
|
||||
}
|
||||
|
||||
class RadarrAPI:
|
||||
def __init__(self, tmdb, params):
|
||||
self.url_params = {"apikey": f"{params['token']}"}
|
||||
self.base_url = f"{params['url']}/api{'/v3' if params['version'] == 'v3' else ''}/"
|
||||
def __init__(self, params):
|
||||
self.url = params["url"]
|
||||
self.token = params["token"]
|
||||
self.version = params["version"]
|
||||
self.base_url = f"{self.url}/api{'/v3' if self.version == 'v3' else ''}/"
|
||||
try:
|
||||
result = requests.get(f"{self.base_url}system/status", params=self.url_params).json()
|
||||
result = requests.get(f"{self.base_url}system/status", params={"apikey": f"{self.token}"}).json()
|
||||
except Exception:
|
||||
util.print_stacktrace()
|
||||
raise Failed(f"Radarr Error: Could not connect to Radarr at {params['url']}")
|
||||
raise Failed(f"Radarr Error: Could not connect to Radarr at {self.url}")
|
||||
if "error" in result and result["error"] == "Unauthorized":
|
||||
raise Failed("Radarr Error: Invalid API Key")
|
||||
if "version" not in result:
|
||||
raise Failed("Radarr Error: Unexpected Response Check URL")
|
||||
self.quality_profile_id = None
|
||||
self.add = params["add"]
|
||||
self.root_folder_path = params["root_folder_path"]
|
||||
self.monitor = params["monitor"]
|
||||
self.availability = params["availability"]
|
||||
self.quality_profile_id = self.get_profile_id(params["quality_profile"])
|
||||
self.tag = params["tag"]
|
||||
self.tags = self.get_tags()
|
||||
self.search = params["search"]
|
||||
|
||||
def get_profile_id(self, profile_name):
|
||||
profiles = ""
|
||||
for profile in self.send_get(f"{self.base_url}{'qualityProfile' if params['version'] == 'v3' else 'profile'}"):
|
||||
for profile in self.send_get("qualityProfile" if self.version == "v3" else "profile"):
|
||||
if len(profiles) > 0:
|
||||
profiles += ", "
|
||||
profiles += profile["name"]
|
||||
if profile["name"] == params["quality_profile"]:
|
||||
self.quality_profile_id = profile["id"]
|
||||
if not self.quality_profile_id:
|
||||
raise Failed(f"Radarr Error: quality_profile: {params['quality_profile']} does not exist in radarr. Profiles available: {profiles}")
|
||||
self.tmdb = tmdb
|
||||
self.url = params["url"]
|
||||
self.version = params["version"]
|
||||
self.token = params["token"]
|
||||
self.root_folder_path = params["root_folder_path"]
|
||||
self.add = params["add"]
|
||||
self.search = params["search"]
|
||||
self.tag = params["tag"]
|
||||
if profile["name"] == profile_name:
|
||||
return profile["id"]
|
||||
raise Failed(f"Radarr Error: quality_profile: {profile_name} does not exist in radarr. Profiles available: {profiles}")
|
||||
|
||||
def add_tmdb(self, tmdb_ids, tag=None):
|
||||
def get_tags(self):
|
||||
return {tag["label"]: tag["id"] for tag in self.send_get("tag")}
|
||||
|
||||
def add_tags(self, tags):
|
||||
added = False
|
||||
for label in tags:
|
||||
if label not in self.tags:
|
||||
added = True
|
||||
self.send_post("tag", {"label": str(label)})
|
||||
if added:
|
||||
self.tags = self.get_tags()
|
||||
|
||||
def lookup(self, tmdb_id):
|
||||
results = self.send_get("movie/lookup", params={"term": f"tmdb:{tmdb_id}"})
|
||||
if results:
|
||||
return results[0]
|
||||
else:
|
||||
raise Failed(f"Sonarr Error: TMDb ID: {tmdb_id} not found")
|
||||
|
||||
def add_tmdb(self, tmdb_ids, **options):
|
||||
logger.info("")
|
||||
logger.debug(f"TMDb IDs: {tmdb_ids}")
|
||||
tag_nums = []
|
||||
add_count = 0
|
||||
if tag is None:
|
||||
tag = self.tag
|
||||
if tag:
|
||||
tag_cache = {}
|
||||
for label in tag:
|
||||
self.send_post(f"{self.base_url}tag", {"label": str(label)})
|
||||
for t in self.send_get(f"{self.base_url}tag"):
|
||||
tag_cache[t["label"]] = t["id"]
|
||||
for label in tag:
|
||||
if label in tag_cache:
|
||||
tag_nums.append(tag_cache[label])
|
||||
folder = options["folder"] if "folder" in options else self.root_folder_path
|
||||
monitor = options["monitor"] if "monitor" in options else self.monitor
|
||||
availability = options["availability"] if "availability" in options else self.availability
|
||||
quality_profile_id = self.get_profile_id(options["quality"]) if "quality" in options else self.quality_profile_id
|
||||
tags = options["tag"] if "tag" in options else self.tag
|
||||
search = options["search"] if "search" in options else self.search
|
||||
if tags:
|
||||
self.add_tags(tags)
|
||||
tag_nums = [self.tags[label] for label in tags if label in self.tags]
|
||||
for tmdb_id in tmdb_ids:
|
||||
try:
|
||||
movie = self.tmdb.get_movie(tmdb_id)
|
||||
movie_info = self.lookup(tmdb_id)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
try:
|
||||
year = movie.release_date.split("-")[0]
|
||||
except AttributeError:
|
||||
logger.error(f"TMDb Error: No year for ({tmdb_id}) {movie.title}")
|
||||
continue
|
||||
|
||||
if year.isdigit() is False:
|
||||
logger.error(f"TMDb Error: No release date yet for ({tmdb_id}) {movie.title}")
|
||||
continue
|
||||
|
||||
poster = f"https://image.tmdb.org/t/p/original{movie.poster_path}"
|
||||
|
||||
titleslug = re.sub(r"([^\s\w]|_)+", "", f"{movie.title} {year}").replace(" ", "-").lower()
|
||||
poster_url = None
|
||||
for image in movie_info["images"]:
|
||||
if "coverType" in image and image["coverType"] == "poster" and "remoteUrl" in image:
|
||||
poster_url = image["remoteUrl"]
|
||||
|
||||
url_json = {
|
||||
"title": movie.title,
|
||||
f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": self.quality_profile_id,
|
||||
"year": int(year),
|
||||
"title": movie_info["title"],
|
||||
f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": quality_profile_id,
|
||||
"year": int(movie_info["year"]),
|
||||
"tmdbid": int(tmdb_id),
|
||||
"titleslug": titleslug,
|
||||
"monitored": True,
|
||||
"rootFolderPath": self.root_folder_path,
|
||||
"images": [{"covertype": "poster", "url": poster}],
|
||||
"addOptions": {"searchForMovie": self.search}
|
||||
"titleslug": movie_info["titleSlug"],
|
||||
"minimumAvailability": availability_translation[availability],
|
||||
"monitored": monitor,
|
||||
"rootFolderPath": folder,
|
||||
"images": [{"covertype": "poster", "url": poster_url}],
|
||||
"addOptions": {"searchForMovie": search}
|
||||
}
|
||||
if tag_nums:
|
||||
url_json["tags"] = tag_nums
|
||||
response = self.send_post(f"{self.base_url}movie", url_json)
|
||||
response = self.send_post("movie", url_json)
|
||||
if response.status_code < 400:
|
||||
logger.info(f"Added to Radarr | {tmdb_id:<6} | {movie.title}")
|
||||
logger.info(f"Added to Radarr | {tmdb_id:<6} | {movie_info['title']}")
|
||||
add_count += 1
|
||||
else:
|
||||
try:
|
||||
logger.error(f"Radarr Error: ({tmdb_id}) {movie.title}: ({response.status_code}) {response.json()[0]['errorMessage']}")
|
||||
logger.error(f"Radarr Error: ({tmdb_id}) {movie_info['title']}: ({response.status_code}) {response.json()[0]['errorMessage']}")
|
||||
except KeyError:
|
||||
logger.debug(url_json)
|
||||
logger.error(f"Radarr Error: {response.json()}")
|
||||
logger.info(f"{add_count} Movie{'s' if add_count > 1 else ''} added to Radarr")
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000)
|
||||
def send_get(self, url):
|
||||
return requests.get(url, params=self.url_params).json()
|
||||
def send_get(self, url, params=None):
|
||||
url_params = {"apikey": f"{self.token}"}
|
||||
if params:
|
||||
for param in params:
|
||||
url_params[param] = params[param]
|
||||
return requests.get(f"{self.base_url}{url}", params=url_params).json()
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000)
|
||||
def send_post(self, url, url_json):
|
||||
return requests.post(url, json=url_json, params=self.url_params)
|
||||
return requests.post(f"{self.base_url}{url}", json=url_json, params={"apikey": f"{self.token}"})
|
||||
|
|
|
@ -1,101 +1,164 @@
|
|||
import logging, re, requests
|
||||
import logging, requests
|
||||
from json.decoder import JSONDecodeError
|
||||
from modules import util
|
||||
from modules.util import Failed
|
||||
from retrying import retry
|
||||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
series_type = ["standard", "daily", "anime"]
|
||||
monitor_translation = {
|
||||
"all": "all",
|
||||
"future": "future",
|
||||
"missing": "missing",
|
||||
"existing": "existing",
|
||||
"pilot": "pilot",
|
||||
"first": "firstSeason",
|
||||
"latest": "latestSeason",
|
||||
"none": "none"
|
||||
}
|
||||
|
||||
class SonarrAPI:
|
||||
def __init__(self, tvdb, params, language):
|
||||
self.url_params = {"apikey": f"{params['token']}"}
|
||||
self.base_url = f"{params['url']}/api{'/v3/' if params['version'] == 'v3' else '/'}"
|
||||
def __init__(self, params, language):
|
||||
self.url = params["url"]
|
||||
self.token = params["token"]
|
||||
self.version = params["version"]
|
||||
self.base_url = f"{self.url}/api{'/v3/' if self.version == 'v3' else '/'}"
|
||||
try:
|
||||
result = requests.get(f"{self.base_url}system/status", params=self.url_params).json()
|
||||
result = requests.get(f"{self.base_url}system/status", params={"apikey": f"{self.token}"}).json()
|
||||
except Exception:
|
||||
util.print_stacktrace()
|
||||
raise Failed(f"Sonarr Error: Could not connect to Sonarr at {params['url']}")
|
||||
raise Failed(f"Sonarr Error: Could not connect to Sonarr at {self.url}")
|
||||
if "error" in result and result["error"] == "Unauthorized":
|
||||
raise Failed("Sonarr Error: Invalid API Key")
|
||||
if "version" not in result:
|
||||
raise Failed("Sonarr Error: Unexpected Response Check URL")
|
||||
self.quality_profile_id = None
|
||||
self.add = params["add"]
|
||||
self.root_folder_path = params["root_folder_path"]
|
||||
self.monitor = params["monitor"]
|
||||
self.quality_profile_id = self.get_profile_id(params["quality_profile"], "quality_profile")
|
||||
self.language_profile_id = None
|
||||
if self.version == "v3" and params["language_profile"] is not None:
|
||||
self.language_profile_id = self.get_profile_id(params["language_profile"], "language_profile")
|
||||
if self.language_profile_id is None:
|
||||
self.language_profile_id = 1
|
||||
self.series_type = params["series_type"]
|
||||
self.season_folder = params["season_folder"]
|
||||
self.tag = params["tag"]
|
||||
self.tags = self.get_tags()
|
||||
self.search = params["search"]
|
||||
self.cutoff_search = params["cutoff_search"]
|
||||
self.language = language
|
||||
|
||||
def get_profile_id(self, profile_name, profile_type):
|
||||
profiles = ""
|
||||
for profile in self.send_get(f"{self.base_url}{'qualityProfile' if params['version'] == 'v3' else 'profile'}"):
|
||||
if profile_type == "quality_profile" and self.version == "v3":
|
||||
endpoint = "qualityProfile"
|
||||
elif profile_type == "language_profile":
|
||||
endpoint = "languageProfile"
|
||||
else:
|
||||
endpoint = "profile"
|
||||
for profile in self.send_get(endpoint):
|
||||
if len(profiles) > 0:
|
||||
profiles += ", "
|
||||
profiles += profile["name"]
|
||||
if profile["name"] == params["quality_profile"]:
|
||||
self.quality_profile_id = profile["id"]
|
||||
if not self.quality_profile_id:
|
||||
raise Failed(f"Sonarr Error: quality_profile: {params['quality_profile']} does not exist in sonarr. Profiles available: {profiles}")
|
||||
self.tvdb = tvdb
|
||||
self.language = language
|
||||
self.url = params["url"]
|
||||
self.version = params["version"]
|
||||
self.token = params["token"]
|
||||
self.root_folder_path = params["root_folder_path"]
|
||||
self.add = params["add"]
|
||||
self.search = params["search"]
|
||||
self.season_folder = params["season_folder"]
|
||||
self.tag = params["tag"]
|
||||
if profile["name"] == profile_name:
|
||||
return profile["id"]
|
||||
raise Failed(f"Sonarr Error: {profile_type}: {profile_name} does not exist in sonarr. Profiles available: {profiles}")
|
||||
|
||||
def add_tvdb(self, tvdb_ids, tag=None):
|
||||
def get_tags(self):
|
||||
return {tag["label"]: tag["id"] for tag in self.send_get("tag")}
|
||||
|
||||
def add_tags(self, tags):
|
||||
added = False
|
||||
for label in tags:
|
||||
if label not in self.tags:
|
||||
added = True
|
||||
self.send_post("tag", {"label": str(label)})
|
||||
if added:
|
||||
self.tags = self.get_tags()
|
||||
|
||||
def lookup(self, tvdb_id):
|
||||
results = self.send_get("series/lookup", params={"term": f"tvdb:{tvdb_id}"})
|
||||
if results:
|
||||
return results[0]
|
||||
else:
|
||||
raise Failed(f"Sonarr Error: TVDb ID: {tvdb_id} not found")
|
||||
|
||||
def add_tvdb(self, tvdb_ids, **options):
|
||||
logger.info("")
|
||||
logger.debug(f"TVDb IDs: {tvdb_ids}")
|
||||
tag_nums = []
|
||||
add_count = 0
|
||||
if tag is None:
|
||||
tag = self.tag
|
||||
if tag:
|
||||
tag_cache = {}
|
||||
for label in tag:
|
||||
self.send_post(f"{self.base_url}tag", {"label": str(label)})
|
||||
for t in self.send_get(f"{self.base_url}tag"):
|
||||
tag_cache[t["label"]] = t["id"]
|
||||
for label in tag:
|
||||
if label in tag_cache:
|
||||
tag_nums.append(tag_cache[label])
|
||||
folder = options["folder"] if "folder" in options else self.root_folder_path
|
||||
monitor = options["monitor"] if "monitor" in options else self.monitor
|
||||
quality_profile_id = self.get_profile_id(options["quality"], "quality_profile") if "quality" in options else self.quality_profile_id
|
||||
language_profile_id = self.get_profile_id(options["language"], "language_profile") if "quality" in options else self.quality_profile_id
|
||||
series = options["series"] if "series" in options else self.series_type
|
||||
season = options["season"] if "season" in options else self.season_folder
|
||||
tags = options["tag"] if "tag" in options else self.tag
|
||||
search = options["search"] if "search" in options else self.search
|
||||
cutoff_search = options["cutoff_search"] if "cutoff_search" in options else self.cutoff_search
|
||||
if tags:
|
||||
self.add_tags(tags)
|
||||
tag_nums = [self.tags[label] for label in tags if label in self.tags]
|
||||
for tvdb_id in tvdb_ids:
|
||||
try:
|
||||
show = self.tvdb.get_series(self.language, tvdb_id)
|
||||
show_info = self.lookup(tvdb_id)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
titleslug = re.sub(r"([^\s\w]|_)+", "", show.title).replace(" ", "-").lower()
|
||||
poster_url = None
|
||||
for image in show_info["images"]:
|
||||
if "coverType" in image and image["coverType"] == "poster" and "remoteUrl" in image:
|
||||
poster_url = image["remoteUrl"]
|
||||
|
||||
url_json = {
|
||||
"title": show.title,
|
||||
f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": self.quality_profile_id,
|
||||
"languageProfileId": 1,
|
||||
"title": show_info["title"],
|
||||
f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": quality_profile_id,
|
||||
"languageProfileId": language_profile_id,
|
||||
"tvdbId": int(tvdb_id),
|
||||
"titleslug": titleslug,
|
||||
"titleslug": show_info["titleSlug"],
|
||||
"language": self.language,
|
||||
"monitored": True,
|
||||
"seasonFolder": self.season_folder,
|
||||
"rootFolderPath": self.root_folder_path,
|
||||
"monitored": monitor != "none",
|
||||
"seasonFolder": season,
|
||||
"seriesType": series,
|
||||
"rootFolderPath": folder,
|
||||
"seasons": [],
|
||||
"images": [{"covertype": "poster", "url": show.poster_path}],
|
||||
"addOptions": {"searchForMissingEpisodes": self.search}
|
||||
"images": [{"covertype": "poster", "url": poster_url}],
|
||||
"addOptions": {
|
||||
"searchForMissingEpisodes": search,
|
||||
"searchForCutoffUnmetEpisodes": cutoff_search,
|
||||
"monitor": monitor_translation[monitor]
|
||||
}
|
||||
}
|
||||
if tag_nums:
|
||||
url_json["tags"] = tag_nums
|
||||
response = self.send_post(f"{self.base_url}series", url_json)
|
||||
response = self.send_post("series", url_json)
|
||||
if response.status_code < 400:
|
||||
logger.info(f"Added to Sonarr | {tvdb_id:<6} | {show.title}")
|
||||
logger.info(f"Added to Sonarr | {tvdb_id:<6} | {show_info['title']}")
|
||||
add_count += 1
|
||||
else:
|
||||
try:
|
||||
logger.error(f"Sonarr Error: ({tvdb_id}) {show.title}: ({response.status_code}) {response.json()[0]['errorMessage']}")
|
||||
logger.error(f"Sonarr Error: ({tvdb_id}) {show_info['title']}: ({response.status_code}) {response.json()[0]['errorMessage']}")
|
||||
except KeyError:
|
||||
logger.debug(url_json)
|
||||
logger.error(f"Sonarr Error: {response.json()}")
|
||||
except JSONDecodeError:
|
||||
logger.debug(url_json)
|
||||
logger.error(f"Sonarr Error: {response}")
|
||||
|
||||
logger.info(f"{add_count} Show{'s' if add_count > 1 else ''} added to Sonarr")
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000)
|
||||
def send_get(self, url):
|
||||
return requests.get(url, params=self.url_params).json()
|
||||
def send_get(self, url, params=None):
|
||||
url_params = {"apikey": f"{self.token}"}
|
||||
if params:
|
||||
for param in params:
|
||||
url_params[param] = params[param]
|
||||
return requests.get(f"{self.base_url}{url}", params=url_params).json()
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000)
|
||||
def send_post(self, url, url_json):
|
||||
return requests.post(url, json=url_json, params=self.url_params)
|
||||
return requests.post(f"{self.base_url}{url}", json=url_json, params={"apikey": f"{self.token}"})
|
||||
|
|
|
@ -5,6 +5,8 @@ from retrying import retry
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = ["tautulli_popular", "tautulli_watched"]
|
||||
|
||||
class TautulliAPI:
|
||||
def __init__(self, params):
|
||||
try:
|
||||
|
|
103
modules/tmdb.py
103
modules/tmdb.py
|
@ -7,6 +7,107 @@ from tmdbv3api.exceptions import TMDbException
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = [
|
||||
"tmdb_actor",
|
||||
"tmdb_actor_details",
|
||||
"tmdb_collection",
|
||||
"tmdb_collection_details",
|
||||
"tmdb_company",
|
||||
"tmdb_crew",
|
||||
"tmdb_crew_details",
|
||||
"tmdb_director",
|
||||
"tmdb_director_details",
|
||||
"tmdb_discover",
|
||||
"tmdb_keyword",
|
||||
"tmdb_list",
|
||||
"tmdb_list_details",
|
||||
"tmdb_movie",
|
||||
"tmdb_movie_details",
|
||||
"tmdb_network",
|
||||
"tmdb_now_playing",
|
||||
"tmdb_popular",
|
||||
"tmdb_producer",
|
||||
"tmdb_producer_details",
|
||||
"tmdb_show",
|
||||
"tmdb_show_details",
|
||||
"tmdb_top_rated",
|
||||
"tmdb_trending_daily",
|
||||
"tmdb_trending_weekly",
|
||||
"tmdb_writer",
|
||||
"tmdb_writer_details"
|
||||
]
|
||||
type_map = {
|
||||
"tmdb_actor": "Person",
|
||||
"tmdb_actor_details": "Person",
|
||||
"tmdb_collection": "Collection",
|
||||
"tmdb_collection_details": "Collection",
|
||||
"tmdb_company": "Company",
|
||||
"tmdb_crew": "Person",
|
||||
"tmdb_crew_details": "Person",
|
||||
"tmdb_director": "Person",
|
||||
"tmdb_director_details": "Person",
|
||||
"tmdb_keyword": "Keyword",
|
||||
"tmdb_list": "List",
|
||||
"tmdb_list_details": "List",
|
||||
"tmdb_movie": "Movie",
|
||||
"tmdb_movie_details": "Movie",
|
||||
"tmdb_network": "Network",
|
||||
"tmdb_person": "Person",
|
||||
"tmdb_producer": "Person",
|
||||
"tmdb_producer_details": "Person",
|
||||
"tmdb_show": "Show",
|
||||
"tmdb_show_details": "Show",
|
||||
"tmdb_writer": "Person",
|
||||
"tmdb_writer_details": "Person"
|
||||
}
|
||||
discover_movie = [
|
||||
"language", "with_original_language", "region", "sort_by",
|
||||
"certification_country", "certification", "certification.lte", "certification.gte",
|
||||
"include_adult",
|
||||
"primary_release_year", "primary_release_date.gte", "primary_release_date.lte",
|
||||
"release_date.gte", "release_date.lte", "year",
|
||||
"vote_count.gte", "vote_count.lte",
|
||||
"vote_average.gte", "vote_average.lte",
|
||||
"with_cast", "with_crew", "with_people",
|
||||
"with_companies",
|
||||
"with_genres", "without_genres",
|
||||
"with_keywords", "without_keywords",
|
||||
"with_runtime.gte", "with_runtime.lte"
|
||||
]
|
||||
discover_tv = [
|
||||
"language", "with_original_language", "timezone", "sort_by",
|
||||
"air_date.gte", "air_date.lte",
|
||||
"first_air_date.gte", "first_air_date.lte", "first_air_date_year",
|
||||
"vote_count.gte", "vote_count.lte",
|
||||
"vote_average.gte", "vote_average.lte",
|
||||
"with_genres", "without_genres",
|
||||
"with_keywords", "without_keywords",
|
||||
"with_networks", "with_companies",
|
||||
"with_runtime.gte", "with_runtime.lte",
|
||||
"include_null_first_air_dates",
|
||||
"screened_theatrically"
|
||||
]
|
||||
discover_dates = [
|
||||
"primary_release_date.gte", "primary_release_date.lte",
|
||||
"release_date.gte", "release_date.lte",
|
||||
"air_date.gte", "air_date.lte",
|
||||
"first_air_date.gte", "first_air_date.lte"
|
||||
]
|
||||
discover_movie_sort = [
|
||||
"popularity.asc", "popularity.desc",
|
||||
"release_date.asc", "release_date.desc",
|
||||
"revenue.asc", "revenue.desc",
|
||||
"primary_release_date.asc", "primary_release_date.desc",
|
||||
"original_title.asc", "original_title.desc",
|
||||
"vote_average.asc", "vote_average.desc",
|
||||
"vote_count.asc", "vote_count.desc"
|
||||
]
|
||||
discover_tv_sort = [
|
||||
"vote_average.desc", "vote_average.asc",
|
||||
"first_air_date.desc", "first_air_date.asc",
|
||||
"popularity.desc", "popularity.asc"
|
||||
]
|
||||
|
||||
class TMDbAPI:
|
||||
def __init__(self, params):
|
||||
self.TMDb = tmdbv3api.TMDb()
|
||||
|
@ -156,7 +257,7 @@ class TMDbAPI:
|
|||
def get_discover(self, attrs, amount, is_movie):
|
||||
ids = []
|
||||
count = 0
|
||||
for date_attr in util.discover_dates:
|
||||
for date_attr in discover_dates:
|
||||
if date_attr in attrs:
|
||||
attrs[date_attr] = datetime.strftime(datetime.strptime(attrs[date_attr], "%m/%d/%Y"), "%Y-%m-%d")
|
||||
self.Discover.discover_movies(attrs) if is_movie else self.Discover.discover_tv_shows(attrs)
|
||||
|
|
|
@ -11,6 +11,18 @@ from trakt.objects.show import Show
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = [
|
||||
"trakt_collected",
|
||||
"trakt_collection",
|
||||
"trakt_list",
|
||||
"trakt_list_details",
|
||||
"trakt_popular",
|
||||
"trakt_recommended",
|
||||
"trakt_trending",
|
||||
"trakt_watched",
|
||||
"trakt_watchlist"
|
||||
]
|
||||
|
||||
class TraktAPI:
|
||||
def __init__(self, params, authorization=None):
|
||||
self.base_url = "https://api.trakt.tv"
|
||||
|
@ -94,9 +106,15 @@ class TraktAPI:
|
|||
return lookup.get_key(to_source) if to_source == "imdb" else int(lookup.get_key(to_source))
|
||||
raise Failed(f"No {to_source.upper().replace('B', 'b')} ID found for {from_source.upper().replace('B', 'b')} ID {external_id}")
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
|
||||
def collection(self, data, is_movie):
|
||||
return self.user_list("collection", data, is_movie)
|
||||
|
||||
def watchlist(self, data, is_movie):
|
||||
items = Trakt[f"users/{data}/watchlist"].movies() if is_movie else Trakt[f"users/{data}/watchlist"].shows()
|
||||
return self.user_list("watchlist", data, is_movie)
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
|
||||
def user_list(self, list_type, data, is_movie):
|
||||
items = Trakt[f"users/{data}/{list_type}"].movies() if is_movie else Trakt[f"users/{data}/{list_type}"].shows()
|
||||
if items is None: raise Failed("Trakt Error: No List found")
|
||||
else: return [i for i in items]
|
||||
|
||||
|
|
|
@ -6,6 +6,15 @@ from retrying import retry
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
builders = [
|
||||
"tvdb_list",
|
||||
"tvdb_list_details",
|
||||
"tvdb_movie",
|
||||
"tvdb_movie_details",
|
||||
"tvdb_show",
|
||||
"tvdb_show_details"
|
||||
]
|
||||
|
||||
class TVDbObj:
|
||||
def __init__(self, tvdb_url, language, is_movie, TVDb):
|
||||
tvdb_url = tvdb_url.strip()
|
||||
|
|
442
modules/util.py
442
modules/util.py
|
@ -22,38 +22,6 @@ def retry_if_not_failed(exception):
|
|||
separating_character = "="
|
||||
screen_width = 100
|
||||
|
||||
method_alias = {
|
||||
"actors": "actor", "role": "actor", "roles": "actor",
|
||||
"content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating",
|
||||
"countries": "country",
|
||||
"decades": "decade",
|
||||
"directors": "director",
|
||||
"genres": "genre",
|
||||
"labels": "label",
|
||||
"studios": "studio", "network": "studio", "networks": "studio",
|
||||
"producers": "producer",
|
||||
"writers": "writer",
|
||||
"years": "year"
|
||||
}
|
||||
search_alias = {
|
||||
"audio_language": "audioLanguage",
|
||||
"content_rating": "contentRating",
|
||||
"subtitle_language": "subtitleLanguage",
|
||||
"added": "addedAt",
|
||||
"originally_available": "originallyAvailableAt",
|
||||
"rating": "userRating"
|
||||
}
|
||||
filter_alias = {
|
||||
"actor": "actors",
|
||||
"collection": "collections",
|
||||
"content_rating": "contentRating",
|
||||
"country": "countries",
|
||||
"director": "directors",
|
||||
"genre": "genres",
|
||||
"originally_available": "originallyAvailableAt",
|
||||
"tmdb_vote_count": "vote_count",
|
||||
"writer": "writers"
|
||||
}
|
||||
days_alias = {
|
||||
"monday": 0, "mon": 0, "m": 0,
|
||||
"tuesday": 1, "tues": 1, "tue": 1, "tu": 1, "t": 1,
|
||||
|
@ -170,64 +138,6 @@ pretty_names = {
|
|||
"tvdb_show": "TVDb Show",
|
||||
"tvdb_show_details": "TVDb Show"
|
||||
}
|
||||
plex_languages = ["default", "ar-SA", "ca-ES", "cs-CZ", "da-DK", "de-DE", "el-GR", "en-AU", "en-CA", "en-GB", "en-US", "es-ES",
|
||||
"es-MX", "et-EE", "fa-IR", "fi-FI", "fr-CA", "fr-FR", "he-IL", "hi-IN", "hu-HU", "id-ID", "it-IT",
|
||||
"ja-JP", "ko-KR", "lt-LT", "lv-LV", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro-RO", "ru-RU",
|
||||
"sk-SK", "sv-SE", "th-TH", "tr-TR", "uk-UA", "vi-VN", "zh-CN", "zh-HK", "zh-TW"]
|
||||
mal_ranked_name = {
|
||||
"mal_all": "all",
|
||||
"mal_airing": "airing",
|
||||
"mal_upcoming": "upcoming",
|
||||
"mal_tv": "tv",
|
||||
"mal_ova": "ova",
|
||||
"mal_movie": "movie",
|
||||
"mal_special": "special",
|
||||
"mal_popular": "bypopularity",
|
||||
"mal_favorite": "favorite"
|
||||
}
|
||||
mal_season_sort = {
|
||||
"anime_score": "anime_score",
|
||||
"anime_num_list_users": "anime_num_list_users",
|
||||
"score": "anime_score",
|
||||
"members": "anime_num_list_users"
|
||||
}
|
||||
mal_pretty = {
|
||||
"anime_score": "Score",
|
||||
"anime_num_list_users": "Members",
|
||||
"list_score": "Score",
|
||||
"list_updated_at": "Last Updated",
|
||||
"anime_title": "Title",
|
||||
"anime_start_date": "Start Date",
|
||||
"all": "All Anime",
|
||||
"watching": "Currently Watching",
|
||||
"completed": "Completed",
|
||||
"on_hold": "On Hold",
|
||||
"dropped": "Dropped",
|
||||
"plan_to_watch": "Plan to Watch"
|
||||
}
|
||||
mal_userlist_sort = {
|
||||
"score": "list_score",
|
||||
"list_score": "list_score",
|
||||
"last_updated": "list_updated_at",
|
||||
"list_updated": "list_updated_at",
|
||||
"list_updated_at": "list_updated_at",
|
||||
"title": "anime_title",
|
||||
"anime_title": "anime_title",
|
||||
"start_date": "anime_start_date",
|
||||
"anime_start_date": "anime_start_date"
|
||||
}
|
||||
mal_userlist_status = [
|
||||
"all",
|
||||
"watching",
|
||||
"completed",
|
||||
"on_hold",
|
||||
"dropped",
|
||||
"plan_to_watch"
|
||||
]
|
||||
anilist_pretty = {
|
||||
"score": "Average Score",
|
||||
"popular": "Popularity"
|
||||
}
|
||||
pretty_ids = {
|
||||
"anidbid": "AniDB",
|
||||
"imdbid": "IMDb",
|
||||
|
@ -236,354 +146,6 @@ pretty_ids = {
|
|||
"thetvdb_id": "TVDb",
|
||||
"tvdbid": "TVDb"
|
||||
}
|
||||
all_lists = [
|
||||
"anidb_id",
|
||||
"anidb_relation",
|
||||
"anidb_popular",
|
||||
"anilist_genre",
|
||||
"anilist_id",
|
||||
"anilist_popular",
|
||||
"anilist_relations",
|
||||
"anilist_season",
|
||||
"anilist_studio",
|
||||
"anilist_tag",
|
||||
"anilist_top_rated",
|
||||
"imdb_list",
|
||||
"imdb_id",
|
||||
"letterboxd_list",
|
||||
"letterboxd_list_details",
|
||||
"mal_id",
|
||||
"mal_all",
|
||||
"mal_airing",
|
||||
"mal_upcoming",
|
||||
"mal_tv",
|
||||
"mal_ova",
|
||||
"mal_movie",
|
||||
"mal_special",
|
||||
"mal_popular",
|
||||
"mal_favorite",
|
||||
"mal_season",
|
||||
"mal_suggested",
|
||||
"mal_userlist",
|
||||
"plex_collection",
|
||||
"plex_search",
|
||||
"tautulli_popular",
|
||||
"tautulli_watched",
|
||||
"tmdb_actor",
|
||||
"tmdb_actor_details",
|
||||
"tmdb_collection",
|
||||
"tmdb_collection_details",
|
||||
"tmdb_company",
|
||||
"tmdb_crew",
|
||||
"tmdb_crew_details",
|
||||
"tmdb_director",
|
||||
"tmdb_director_details",
|
||||
"tmdb_discover",
|
||||
"tmdb_keyword",
|
||||
"tmdb_list",
|
||||
"tmdb_list_details",
|
||||
"tmdb_movie",
|
||||
"tmdb_movie_details",
|
||||
"tmdb_network",
|
||||
"tmdb_now_playing",
|
||||
"tmdb_popular",
|
||||
"tmdb_producer",
|
||||
"tmdb_producer_details",
|
||||
"tmdb_show",
|
||||
"tmdb_show_details",
|
||||
"tmdb_top_rated",
|
||||
"tmdb_trending_daily",
|
||||
"tmdb_trending_weekly",
|
||||
"tmdb_writer",
|
||||
"tmdb_writer_details",
|
||||
"trakt_collected",
|
||||
"trakt_collection",
|
||||
"trakt_list",
|
||||
"trakt_list_details",
|
||||
"trakt_popular",
|
||||
"trakt_recommended",
|
||||
"trakt_trending",
|
||||
"trakt_watched",
|
||||
"trakt_watchlist",
|
||||
"tvdb_list",
|
||||
"tvdb_list_details",
|
||||
"tvdb_movie",
|
||||
"tvdb_movie_details",
|
||||
"tvdb_show",
|
||||
"tvdb_show_details"
|
||||
]
|
||||
collectionless_lists = [
|
||||
"sort_title", "content_rating",
|
||||
"summary", "tmdb_summary", "tmdb_description", "tmdb_biography",
|
||||
"collection_order", "plex_collectionless",
|
||||
"url_poster", "tmdb_poster", "tmdb_profile", "file_poster",
|
||||
"url_background", "file_background",
|
||||
"name_mapping", "label", "label_sync_mode", "test"
|
||||
]
|
||||
other_attributes = [
|
||||
"run_again",
|
||||
"schedule",
|
||||
"sync_mode",
|
||||
"template",
|
||||
"test",
|
||||
"tmdb_person"
|
||||
]
|
||||
dictionary_lists = [
|
||||
"filters",
|
||||
"anilist_genre",
|
||||
"anilist_season",
|
||||
"anilist_tag",
|
||||
"mal_season",
|
||||
"mal_userlist",
|
||||
"plex_collectionless",
|
||||
"plex_search",
|
||||
"tautulli_popular",
|
||||
"tautulli_watched",
|
||||
"tmdb_discover"
|
||||
]
|
||||
show_only_lists = [
|
||||
"tmdb_network",
|
||||
"tmdb_show",
|
||||
"tmdb_show_details",
|
||||
"tvdb_show",
|
||||
"tvdb_show_details"
|
||||
]
|
||||
movie_only_lists = [
|
||||
"letterboxd_list",
|
||||
"letterboxd_list_details",
|
||||
"tmdb_collection",
|
||||
"tmdb_collection_details",
|
||||
"tmdb_movie",
|
||||
"tmdb_movie_details",
|
||||
"tmdb_now_playing",
|
||||
"tvdb_movie",
|
||||
"tvdb_movie_details"
|
||||
]
|
||||
count_lists = [
|
||||
"anidb_popular",
|
||||
"anilist_popular",
|
||||
"anilist_top_rated",
|
||||
"mal_all",
|
||||
"mal_airing",
|
||||
"mal_upcoming",
|
||||
"mal_tv",
|
||||
"mal_ova",
|
||||
"mal_movie",
|
||||
"mal_special",
|
||||
"mal_popular",
|
||||
"mal_favorite",
|
||||
"mal_suggested",
|
||||
"tmdb_popular",
|
||||
"tmdb_top_rated",
|
||||
"tmdb_now_playing",
|
||||
"tmdb_trending_daily",
|
||||
"tmdb_trending_weekly",
|
||||
"trakt_trending",
|
||||
"trakt_popular",
|
||||
"trakt_recommended",
|
||||
"trakt_watched",
|
||||
"trakt_collected"
|
||||
]
|
||||
tmdb_lists = [
|
||||
"tmdb_actor",
|
||||
"tmdb_actor_details",
|
||||
"tmdb_collection",
|
||||
"tmdb_collection_details",
|
||||
"tmdb_company",
|
||||
"tmdb_crew",
|
||||
"tmdb_crew_details",
|
||||
"tmdb_director",
|
||||
"tmdb_director_details",
|
||||
"tmdb_discover",
|
||||
"tmdb_keyword",
|
||||
"tmdb_list",
|
||||
"tmdb_list_details",
|
||||
"tmdb_movie",
|
||||
"tmdb_movie_details",
|
||||
"tmdb_network",
|
||||
"tmdb_now_playing",
|
||||
"tmdb_popular",
|
||||
"tmdb_producer",
|
||||
"tmdb_producer_details",
|
||||
"tmdb_show",
|
||||
"tmdb_show_details",
|
||||
"tmdb_top_rated",
|
||||
"tmdb_trending_daily",
|
||||
"tmdb_trending_weekly",
|
||||
"tmdb_writer",
|
||||
"tmdb_writer_details"
|
||||
]
|
||||
tmdb_type = {
|
||||
"tmdb_actor": "Person",
|
||||
"tmdb_actor_details": "Person",
|
||||
"tmdb_collection": "Collection",
|
||||
"tmdb_collection_details": "Collection",
|
||||
"tmdb_company": "Company",
|
||||
"tmdb_crew": "Person",
|
||||
"tmdb_crew_details": "Person",
|
||||
"tmdb_director": "Person",
|
||||
"tmdb_director_details": "Person",
|
||||
"tmdb_keyword": "Keyword",
|
||||
"tmdb_list": "List",
|
||||
"tmdb_list_details": "List",
|
||||
"tmdb_movie": "Movie",
|
||||
"tmdb_movie_details": "Movie",
|
||||
"tmdb_network": "Network",
|
||||
"tmdb_person": "Person",
|
||||
"tmdb_producer": "Person",
|
||||
"tmdb_producer_details": "Person",
|
||||
"tmdb_show": "Show",
|
||||
"tmdb_show_details": "Show",
|
||||
"tmdb_writer": "Person",
|
||||
"tmdb_writer_details": "Person"
|
||||
}
|
||||
plex_searches = [
|
||||
"title", "title.and", "title.not", "title.begins", "title.ends",
|
||||
"studio", "studio.and", "studio.not", "studio.begins", "studio.ends",
|
||||
"actor", "actor.and", "actor.not",
|
||||
"audio_language", "audio_language.and", "audio_language.not",
|
||||
"collection", "collection.and", "collection.not",
|
||||
"content_rating", "content_rating.and", "content_rating.not",
|
||||
"country", "country.and", "country.not",
|
||||
"director", "director.and", "director.not",
|
||||
"genre", "genre.and", "genre.not",
|
||||
"label", "label.and", "label.not",
|
||||
"producer", "producer.and", "producer.not",
|
||||
"subtitle_language", "subtitle_language.and", "subtitle_language.not",
|
||||
"writer", "writer.and", "writer.not",
|
||||
"decade", "resolution",
|
||||
"added.before", "added.after",
|
||||
"originally_available.before", "originally_available.after",
|
||||
"duration.greater", "duration.less",
|
||||
"rating.greater", "rating.less",
|
||||
"year", "year.not", "year.greater", "year.less"
|
||||
]
|
||||
plex_sort = {
|
||||
"title.asc": "titleSort:asc", "title.desc": "titleSort:desc",
|
||||
"originally_available.asc": "originallyAvailableAt:asc", "originally_available.desc": "originallyAvailableAt:desc",
|
||||
"critic_rating.asc": "rating:asc", "critic_rating.desc": "rating:desc",
|
||||
"audience_rating.asc": "audienceRating:asc", "audience_rating.desc": "audienceRating:desc",
|
||||
"duration.asc": "duration:asc", "duration.desc": "duration:desc",
|
||||
"added.asc": "addedAt:asc", "added.desc": "addedAt:desc"
|
||||
}
|
||||
plex_modifiers = {
|
||||
".and": "&",
|
||||
".not": "!",
|
||||
".begins": "<",
|
||||
".ends": ">",
|
||||
".before": "<<",
|
||||
".after": ">>",
|
||||
".greater": ">>",
|
||||
".less": "<<"
|
||||
}
|
||||
movie_only_searches = [
|
||||
"audio_language", "audio_language.and", "audio_language.not",
|
||||
"country", "country.and", "country.not",
|
||||
"subtitle_language", "subtitle_language.and", "subtitle_language.not",
|
||||
"decade", "resolution",
|
||||
"originally_available.before", "originally_available.after",
|
||||
"duration.greater", "duration.less"
|
||||
]
|
||||
tmdb_searches = [
|
||||
"actor", "actor.and", "actor.not",
|
||||
"director", "director.and", "director.not",
|
||||
"producer", "producer.and", "producer.not",
|
||||
"writer", "writer.and", "writer.not"
|
||||
]
|
||||
all_filters = [
|
||||
"actor", "actor.not",
|
||||
"audio_language", "audio_language.not",
|
||||
"audio_track_title", "audio_track_title.not",
|
||||
"collection", "collection.not",
|
||||
"content_rating", "content_rating.not",
|
||||
"country", "country.not",
|
||||
"director", "director.not",
|
||||
"genre", "genre.not",
|
||||
"max_age",
|
||||
"originally_available.gte", "originally_available.lte",
|
||||
"tmdb_vote_count.gte", "tmdb_vote_count.lte",
|
||||
"duration.gte", "duration.lte",
|
||||
"original_language", "original_language.not",
|
||||
"rating.gte", "rating.lte",
|
||||
"studio", "studio.not",
|
||||
"subtitle_language", "subtitle_language.not",
|
||||
"video_resolution", "video_resolution.not",
|
||||
"writer", "writer.not",
|
||||
"year", "year.gte", "year.lte", "year.not"
|
||||
]
|
||||
movie_only_filters = [
|
||||
"audio_language", "audio_language.not",
|
||||
"audio_track_title", "audio_track_title.not",
|
||||
"country", "country.not",
|
||||
"director", "director.not",
|
||||
"duration.gte", "duration.lte",
|
||||
"original_language", "original_language.not",
|
||||
"subtitle_language", "subtitle_language.not",
|
||||
"video_resolution", "video_resolution.not",
|
||||
"writer", "writer.not"
|
||||
]
|
||||
boolean_details = [
|
||||
"add_to_arr",
|
||||
"show_filtered",
|
||||
"show_missing",
|
||||
"save_missing"
|
||||
]
|
||||
all_details = [
|
||||
"sort_title", "content_rating",
|
||||
"summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary", "tvdb_description", "trakt_description", "letterboxd_description",
|
||||
"collection_mode", "collection_order",
|
||||
"url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster",
|
||||
"url_background", "tmdb_background", "tvdb_background", "file_background",
|
||||
"name_mapping", "add_to_arr", "arr_tag", "label",
|
||||
"show_filtered", "show_missing", "save_missing"
|
||||
]
|
||||
discover_movie = [
|
||||
"language", "with_original_language", "region", "sort_by",
|
||||
"certification_country", "certification", "certification.lte", "certification.gte",
|
||||
"include_adult",
|
||||
"primary_release_year", "primary_release_date.gte", "primary_release_date.lte",
|
||||
"release_date.gte", "release_date.lte", "year",
|
||||
"vote_count.gte", "vote_count.lte",
|
||||
"vote_average.gte", "vote_average.lte",
|
||||
"with_cast", "with_crew", "with_people",
|
||||
"with_companies",
|
||||
"with_genres", "without_genres",
|
||||
"with_keywords", "without_keywords",
|
||||
"with_runtime.gte", "with_runtime.lte"
|
||||
]
|
||||
discover_tv = [
|
||||
"language", "with_original_language", "timezone", "sort_by",
|
||||
"air_date.gte", "air_date.lte",
|
||||
"first_air_date.gte", "first_air_date.lte", "first_air_date_year",
|
||||
"vote_count.gte", "vote_count.lte",
|
||||
"vote_average.gte", "vote_average.lte",
|
||||
"with_genres", "without_genres",
|
||||
"with_keywords", "without_keywords",
|
||||
"with_networks", "with_companies",
|
||||
"with_runtime.gte", "with_runtime.lte",
|
||||
"include_null_first_air_dates",
|
||||
"screened_theatrically"
|
||||
]
|
||||
discover_dates = [
|
||||
"primary_release_date.gte", "primary_release_date.lte",
|
||||
"release_date.gte", "release_date.lte",
|
||||
"air_date.gte", "air_date.lte",
|
||||
"first_air_date.gte", "first_air_date.lte"
|
||||
]
|
||||
discover_movie_sort = [
|
||||
"popularity.asc", "popularity.desc",
|
||||
"release_date.asc", "release_date.desc",
|
||||
"revenue.asc", "revenue.desc",
|
||||
"primary_release_date.asc", "primary_release_date.desc",
|
||||
"original_title.asc", "original_title.desc",
|
||||
"vote_average.asc", "vote_average.desc",
|
||||
"vote_count.asc", "vote_count.desc"
|
||||
]
|
||||
discover_tv_sort = [
|
||||
"vote_average.desc", "vote_average.asc",
|
||||
"first_air_date.desc", "first_air_date.asc",
|
||||
"popularity.desc", "popularity.asc"
|
||||
]
|
||||
|
||||
def tab_new_lines(data):
|
||||
return str(data).replace("\n", "\n|\t ") if "\n" in str(data) else str(data)
|
||||
|
@ -678,8 +240,8 @@ def check_number(value, method, number_type="int", minimum=None, maximum=None):
|
|||
return num_value
|
||||
|
||||
def check_date(date_text, method, return_string=False, plex_date=False):
|
||||
try: date_obg = datetime.strptime(str(date_text), "%Y/%m/%d" if plex_date else "%m/%d/%Y")
|
||||
except ValueError: raise Failed(f"Collection Error: {method}: {date_text} must match pattern {'YYYY/MM/DD e.g. 2020/12/25' if plex_date else 'MM/DD/YYYY e.g. 12/25/2020'}")
|
||||
try: date_obg = datetime.strptime(str(date_text), "%Y-%m-%d" if plex_date else "%m/%d/%Y")
|
||||
except ValueError: raise Failed(f"Collection Error: {method}: {date_text} must match pattern {'YYYY-MM-DD e.g. 2020-12-25' if plex_date else 'MM/DD/YYYY e.g. 12/25/2020'}")
|
||||
return str(date_text) if return_string else date_obg
|
||||
|
||||
def logger_input(prompt, timeout=60):
|
||||
|
|
|
@ -13,6 +13,7 @@ parser.add_argument("--my-tests", dest="tests", help=argparse.SUPPRESS, action="
|
|||
parser.add_argument("--debug", dest="debug", help=argparse.SUPPRESS, action="store_true", default=False)
|
||||
parser.add_argument("-c", "--config", dest="config", help="Run with desired *.yml file", type=str)
|
||||
parser.add_argument("-t", "--time", dest="time", help="Time to update each day use format HH:MM (Default: 03:00)", default="03:00", type=str)
|
||||
parser.add_argument("-re", "--resume", dest="resume", help="Resume collection run from a specific collection", type=str)
|
||||
parser.add_argument("-r", "--run", dest="run", help="Run without the scheduler", action="store_true", default=False)
|
||||
parser.add_argument("-rt", "--test", "--tests", "--run-test", "--run-tests", dest="test", help="Run in debug mode with only collections that have test: true", action="store_true", default=False)
|
||||
parser.add_argument("-cl", "--collection", "--collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str)
|
||||
|
@ -37,6 +38,7 @@ test = check_bool("PMM_TEST", args.test)
|
|||
debug = check_bool("PMM_DEBUG", args.debug)
|
||||
run = check_bool("PMM_RUN", args.run)
|
||||
collections = os.environ.get("PMM_COLLECTIONS") if os.environ.get("PMM_COLLECTIONS") else args.collections
|
||||
resume = os.environ.get("PMM_RESUME") if os.environ.get("PMM_RESUME") else args.resume
|
||||
|
||||
time_to_run = os.environ.get("PMM_TIME") if os.environ.get("PMM_TIME") else args.time
|
||||
if not re.match("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$", time_to_run):
|
||||
|
@ -87,14 +89,14 @@ util.centered("| |_) | |/ _ \\ \\/ / | |\\/| |/ _ \\ __/ _` | | |\\/| |/ _` | '_
|
|||
util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ")
|
||||
util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")
|
||||
util.centered(" |___/ ")
|
||||
util.centered(" Version: 1.6.4 ")
|
||||
util.centered(" Version: 1.7.0 ")
|
||||
util.separator()
|
||||
|
||||
if my_tests:
|
||||
tests.run_tests(default_dir)
|
||||
sys.exit(0)
|
||||
|
||||
def start(config_path, is_test, daily, collections_to_run):
|
||||
def start(config_path, is_test, daily, collections_to_run, resume_from):
|
||||
if daily: start_type = "Daily "
|
||||
elif is_test: start_type = "Test "
|
||||
elif collections_to_run: start_type = "Collections "
|
||||
|
@ -103,7 +105,7 @@ def start(config_path, is_test, daily, collections_to_run):
|
|||
util.separator(f"Starting {start_type}Run")
|
||||
try:
|
||||
config = Config(default_dir, config_path)
|
||||
config.update_libraries(is_test, collections_to_run)
|
||||
config.update_libraries(is_test, collections_to_run, resume_from)
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
logger.critical(e)
|
||||
|
@ -111,11 +113,11 @@ def start(config_path, is_test, daily, collections_to_run):
|
|||
util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}")
|
||||
|
||||
try:
|
||||
if run or test or collections:
|
||||
start(config_file, test, False, collections)
|
||||
if run or test or collections or resume:
|
||||
start(config_file, test, False, collections, resume)
|
||||
else:
|
||||
length = 0
|
||||
schedule.every().day.at(time_to_run).do(start, config_file, False, True, None)
|
||||
schedule.every().day.at(time_to_run).do(start, config_file, False, True, None, None)
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
current = datetime.now().strftime("%H:%M")
|
||||
|
|
Loading…
Add table
Reference in a new issue