@ -4,7 +4,7 @@ from modules import anidb, anilist, icheckmovies, imdb, letterboxd, mal, plex, r
from modules . util import Failed , ImageData
from PIL import Image
from plexapi . exceptions import BadRequest , NotFound
from plexapi . video import Movie , Show
from plexapi . video import Movie , Show , Season , Episode
from urllib . parse import quote
logger = logging . getLogger ( " Plex Meta Manager " )
@ -64,7 +64,7 @@ filter_translation = {
modifier_alias = { " .greater " : " .gt " , " .less " : " .lt " }
all_builders = anidb . builders + anilist . builders + icheckmovies . builders + imdb . builders + letterboxd . builders + \
mal . builders + plex . builders + stevenlu . builders + tautulli . builders + tmdb . builders + trakt . builders + tvdb . builders
show_only_builders = [ " tmdb_network " , " tmdb_show " , " tmdb_show_details " , " tvdb_show " , " tvdb_show_details " ]
show_only_builders = [ " tmdb_network " , " tmdb_show " , " tmdb_show_details " , " tvdb_show " , " tvdb_show_details " , " collection_level " ]
movie_only_builders = [
" letterboxd_list " , " letterboxd_list_details " , " icheckmovies_list " , " icheckmovies_list_details " , " stevenlu_popular " ,
" tmdb_collection " , " tmdb_collection_details " , " tmdb_movie " , " tmdb_movie_details " , " tmdb_now_playing " ,
@ -78,13 +78,19 @@ poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "f
background_details = [ " url_background " , " tmdb_background " , " tvdb_background " , " file_background " ]
boolean_details = [ " visible_library " , " visible_home " , " visible_shared " , " show_filtered " , " show_missing " , " save_missing " , " item_assets " , " missing_only_released " ]
string_details = [ " sort_title " , " content_rating " , " name_mapping " ]
ignored_details = [ " smart_filter " , " smart_label " , " smart_url " , " run_again " , " schedule " , " sync_mode " , " template " , " test " , " tmdb_person " , " build_collection " , " collection_order " , " validate_builders " ]
details = [ " collection_mode " , " collection_order " , " label " ] + boolean_details + string_details
ignored_details = [
" smart_filter " , " smart_label " , " smart_url " , " run_again " , " schedule " , " sync_mode " , " template " , " test " ,
" tmdb_person " , " build_collection " , " collection_order " , " collection_level " , " validate_builders "
]
details = [ " collection_mode " , " collection_order " , " collection_level " , " label " ] + boolean_details + string_details
collectionless_details = [ " collection_order " , " plex_collectionless " , " label " , " label_sync_mode " , " test " ] + \
poster_details + background_details + summary_details + string_details
item_details = [ " item_label " , " item_radarr_tag " , " item_sonarr_tag " , " item_overlay " ] + list ( plex . item_advance_keys . keys ( ) )
radarr_details = [ " radarr_add " , " radarr_add_existing " , " radarr_folder " , " radarr_monitor " , " radarr_search " , " radarr_availability " , " radarr_quality " , " radarr_tag " ]
sonarr_details = [ " sonarr_add " , " sonarr_add_existing " , " sonarr_folder " , " sonarr_monitor " , " sonarr_language " , " sonarr_series " , " sonarr_quality " , " sonarr_season " , " sonarr_search " , " sonarr_cutoff_search " , " sonarr_tag " ]
sonarr_details = [
" sonarr_add " , " sonarr_add_existing " , " sonarr_folder " , " sonarr_monitor " , " sonarr_language " , " sonarr_series " ,
" sonarr_quality " , " sonarr_season " , " sonarr_search " , " sonarr_cutoff_search " , " sonarr_tag "
]
all_filters = [
" actor " , " actor.not " ,
" audio_language " , " audio_language.not " ,
@ -130,7 +136,7 @@ movie_only_filters = [
" writer " , " writer.not "
]
show_only_filters = [ " first_episode_aired " , " last_episode_aired " , " network " ]
smart_invalid = [ " collection_order " ]
smart_invalid = [ " collection_order " , " collection_level " ]
smart_url_invalid = [ " filters " , " run_again " , " sync_mode " , " show_filtered " , " show_missing " , " save_missing " , " smart_label " ] + radarr_details + sonarr_details
custom_sort_builders = [
" tmdb_list " , " tmdb_popular " , " tmdb_now_playing " , " tmdb_top_rated " ,
@ -142,6 +148,10 @@ custom_sort_builders = [
" mal_all " , " mal_airing " , " mal_upcoming " , " mal_tv " , " mal_movie " , " mal_ova " , " mal_special " ,
" mal_popular " , " mal_favorite " , " mal_suggested " , " mal_userlist " , " mal_season " , " mal_genre " , " mal_studio "
]
parts_collection_valid = [
" plex_search " , " trakt_list " , " trakt_list_details " , " collection_mode " , " label " , " visible_library " ,
" visible_home " , " visible_shared " , " show_missing " , " save_missing " , " missing_only_released "
] + summary_details + poster_details + background_details + string_details
class CollectionBuilder :
def __init__ ( self , config , library , metadata , name , no_missing , data ) :
@ -165,6 +175,7 @@ class CollectionBuilder:
self . sonarr_details = { }
self . missing_movies = [ ]
self . missing_shows = [ ]
self . missing_parts = [ ]
self . builders = [ ]
self . filters = [ ]
self . tmdb_filters = [ ]
@ -411,6 +422,21 @@ class CollectionBuilder:
else :
raise Failed ( f " Collection Error: { self . data [ methods [ ' collection_order ' ] ] } collection_order invalid \n \t release (Order Collection by release dates) \n \t alpha (Order Collection Alphabetically) \n \t custom (Custom Order Collection) " )
self . collection_level = " movie " if self . library . is_movie else " show "
if " collection_level " in methods :
logger . debug ( " " )
logger . debug ( " Validating Method: collection_level " )
if self . data [ methods [ " collection_level " ] ] is None :
raise Failed ( f " Collection Warning: collection_level attribute is blank " )
else :
logger . debug ( f " Value: { self . data [ methods [ ' collection_level ' ] ] } " )
if self . data [ methods [ " collection_level " ] ] . lower ( ) in plex . collection_level_options :
self . collection_level = self . data [ methods [ " collection_level " ] ] . lower ( )
else :
raise Failed ( f " Collection Error: { self . data [ methods [ ' collection_level ' ] ] } collection_level invalid \n \t season (Collection at the Season Level) \n \t episode (Collection at the Episode Level) " )
self . parts_collection = self . collection_level in [ " season " , " episode " ]
self . media_type = self . collection_level . capitalize ( )
if " tmdb_person " in methods :
logger . debug ( " " )
logger . debug ( " Validating Method: tmdb_person " )
@ -479,6 +505,8 @@ class CollectionBuilder:
cant_interact ( " smart_url " , " collectionless " )
cant_interact ( " smart_url " , " run_again " )
cant_interact ( " smart_label_collection " , " smart_url " , fail = True )
cant_interact ( " smart_label_collection " , " parts_collection " , fail = True )
cant_interact ( " smart_url " , " parts_collection " , fail = True )
self . smart = self . smart_url or self . smart_label_collection
@ -501,6 +529,7 @@ class CollectionBuilder:
elif self . library . is_show and method_name in movie_only_builders : raise Failed ( f " Collection Error: { method_final } attribute only works for movie libraries " )
elif self . library . is_show and method_name in plex . movie_only_searches : raise Failed ( f " Collection Error: { method_final } plex search only works for movie libraries " )
elif self . library . is_movie and method_name in plex . show_only_searches : raise Failed ( f " Collection Error: { method_final } plex search only works for show libraries " )
elif self . parts_collection and method_name not in parts_collection_valid : raise Failed ( f " Collection Error: { method_final } attribute does not work with Collection Level: { self . details [ ' collection_level ' ] . capitalize ( ) } " )
elif self . smart and method_name in smart_invalid : raise Failed ( f " Collection Error: { method_final } attribute only works with normal collections " )
elif self . collectionless and method_name not in collectionless_details : raise Failed ( f " Collection Error: { method_final } attribute does not work for Collectionless collection " )
elif self . smart_url and method_name in all_builders + smart_url_invalid : raise Failed ( f " Collection Error: { method_final } builder not allowed when using smart_filter " )
@ -869,7 +898,8 @@ class CollectionBuilder:
for dict_data , dict_methods in util . parse ( method_name , method_data , datatype = " dictlist " ) :
new_dictionary = { }
if method_name == " plex_search " :
new_dictionary = self . build_filter ( " plex_search " , dict_data )
type_override = f " { self . collection_level } s " if self . collection_level in plex . collection_level_options else None
new_dictionary = self . build_filter ( " plex_search " , dict_data , type_override = type_override )
elif method_name == " plex_collectionless " :
prefix_list = util . parse ( " exclude_prefix " , dict_data , datatype = " list " , methods = dict_methods )
exact_list = util . parse ( " exclude " , dict_data , datatype = " list " , methods = dict_methods )
@ -1070,12 +1100,12 @@ class CollectionBuilder:
for i , input_data in enumerate ( ids , 1 ) :
input_id , id_type = input_data
util . print_return ( f " Parsing ID { i } / { total_ids } " )
if id_type == " tmdb " :
if id_type == " tmdb " and not self . parts_collection :
if input_id in self . library . movie_map :
rating_keys . append ( self . library . movie_map [ input_id ] [ 0 ] )
elif input_id not in self . missing_movies :
self . missing_movies . append ( input_id )
elif id_type in [ " tvdb " , " tmdb_show " ] :
elif id_type in [ " tvdb " , " tmdb_show " ] and not self . parts_collection :
if id_type == " tmdb_show " :
try :
input_id = self . config . Convert . tmdb_to_tvdb ( input_id , fail = True )
@ -1086,7 +1116,7 @@ class CollectionBuilder:
rating_keys . append ( self . library . show_map [ input_id ] [ 0 ] )
elif input_id not in self . missing_shows :
self . missing_shows . append ( input_id )
elif id_type == " imdb " :
elif id_type == " imdb " and not self . parts_collection :
if input_id in self . library . imdb_map :
rating_keys . append ( self . library . imdb_map [ input_id ] [ 0 ] )
else :
@ -1103,6 +1133,28 @@ class CollectionBuilder:
except Failed as e :
logger . error ( e )
continue
elif id_type == " tvdb_season " and self . collection_level == " season " :
show_id , season_num = input_id . split ( " _ " )
if int ( show_id ) in self . library . show_map :
show_item = self . library . fetchItem ( self . library . show_map [ int ( show_id ) ] [ 0 ] )
try :
episode_item = show_item . season ( season = int ( season_num ) )
rating_keys . append ( episode_item . ratingKey )
except NotFound :
self . missing_parts . append ( f " { show_item . title } Season: { season_num } Missing " )
elif int ( show_id ) not in self . missing_shows :
self . missing_shows . append ( int ( show_id ) )
elif id_type == " tvdb_episode " and self . collection_level == " episode " :
show_id , season_num , episode_num = input_id . split ( " _ " )
if int ( show_id ) in self . library . show_map :
show_item = self . library . fetchItem ( self . library . show_map [ int ( show_id ) ] [ 0 ] )
try :
episode_item = show_item . episode ( season = int ( season_num ) , episode = int ( episode_num ) )
rating_keys . append ( episode_item . ratingKey )
except NotFound :
self . missing_parts . append ( f " { show_item . title } Season: { season_num } Episode: { episode_num } Missing " )
elif int ( show_id ) not in self . missing_shows :
self . missing_shows . append ( int ( show_id ) )
util . print_end ( )
if len ( rating_keys ) > 0 :
@ -1125,7 +1177,7 @@ class CollectionBuilder:
except Failed as e :
logger . error ( e )
continue
current_title = f " { current . title } ( { current . year } ) " if current . year else current . title
current_title = self . item_title ( current )
if self . check_filters ( current , f " { ( ' ' * ( max_length - len ( str ( i ) ) ) ) } { i } / { total } " ) :
self . rating_keys . append ( key )
else :
@ -1133,7 +1185,7 @@ class CollectionBuilder:
if self . details [ " show_filtered " ] is True :
logger . info ( f " { name } Collection | X | { current_title } " )
def build_filter ( self , method , plex_filter , smart = False ):
def build_filter ( self , method , plex_filter , smart = False , type_override = None ):
if smart :
logger . info ( " " )
logger . info ( f " Validating Method: { method } " )
@ -1149,7 +1201,9 @@ class CollectionBuilder:
if " any " in filter_alias and " all " in filter_alias :
raise Failed ( f " Collection Error: Cannot have more then one base " )
if smart and " type " in filter_alias and self . library . is_show :
if type_override :
sort_type = type_override
elif smart and " type " in filter_alias and self . library . is_show :
if plex_filter [ filter_alias [ " type " ] ] not in [ " shows " , " seasons " , " episodes " ] :
raise Failed ( f " Collection Error: type: { plex_filter [ filter_alias [ ' type ' ] ] } is invalid, must be either shows, season, or episodes " )
sort_type = plex_filter [ filter_alias [ " type " ] ]
@ -1388,8 +1442,8 @@ class CollectionBuilder:
def fetch_item ( self , item ) :
try :
current = self . library . fetchItem ( item . ratingKey if isinstance ( item , ( Movie , Show )) else int ( item ) )
if not isinstance ( current , ( Movie , Show )) :
current = self . library . fetchItem ( item . ratingKey if isinstance ( item , ( Movie , Show , Season , Episode )) else int ( item ) )
if not isinstance ( current , ( Movie , Show , Season , Episode )) :
raise NotFound
return current
except ( BadRequest , NotFound ) :
@ -1404,19 +1458,17 @@ class CollectionBuilder:
except Failed as e :
logger . error ( e )
continue
current_title = f " { current . title } ( { current . year } ) " if current . year else current . title
current_operation = " = " if current in collection_items else " + "
logger . info ( util . adjust_space ( f " { name } Collection | { current_operation } | { current_title } " ) )
logger . info ( util . adjust_space ( f " { name } Collection | { current_operation } | { self . item_title ( current ) } " ) )
if current in collection_items :
self . plex_map [ current . ratingKey ] = None
elif self . smart_label_collection :
self . library . query_data ( current . addLabel , name )
else :
self . library . query_data ( current . addCollection , name )
media_type = f " { ' Movie ' if self . library . is_movie else ' Show ' } { ' s ' if total > 1 else ' ' } "
util . print_end ( )
logger . info ( " " )
logger . info ( f " { total } { media_type } Processed " )
logger . info ( f " { total } { self . collection_level . capitalize ( ) } { ' s ' if total > 1 else ' ' } Processed " )
def check_tmdb_filter ( self , item_id , is_movie , item = None , check_released = False ) :
if self . tmdb_filters or check_released :
@ -1610,6 +1662,26 @@ class CollectionBuilder:
logger . error ( e )
if self . run_again :
self . run_again_shows . extend ( missing_tvdb_ids )
if len ( self . missing_parts ) > 0 and self . library . is_show and self . details [ " save_missing " ] is True :
for missing in self . missing_parts :
logger . info ( f " { self . name } Collection | X | { missing } " )
def item_title ( self , item ) :
if self . collection_level == " season " :
if f " Season { item . index } " == item . title :
return f " { item . parentTitle } { item . title } "
else :
return f " { item . parentTitle } Season { item . index } : { item . title } "
elif self . collection_level == " episode " :
text = f " { item . grandparentTitle } S { util . add_zero ( item . parentIndex ) } E { util . add_zero ( item . index ) } "
if f " Season { item . parentIndex } " == item . parentTitle :
return f " { text } : { item . title } "
else :
return f " { text } : { item . parentTitle } : { item . title } "
elif self . collection_level == " movie " and item . year :
return f " { item . title } ( { item . year } ) "
else :
return item . title
def sync_collection ( self ) :
count_removed = 0
@ -1620,7 +1692,7 @@ class CollectionBuilder:
util . separator ( f " Removed from { self . name } Collection " , space = False , border = False )
logger . info ( " " )
self . library . reload ( item )
logger . info ( f " { self . name } Collection | - | { item . title } " )
logger . info ( f " { self . name } Collection | - | { self . item_title ( item ) } " )
if self . smart_label_collection :
self . library . query_data ( item . removeLabel , self . name )
else :
@ -1628,7 +1700,7 @@ class CollectionBuilder:
count_removed + = 1
if count_removed > 0 :
logger . info ( " " )
logger . info ( f " { count_removed } { ' Movie ' if self . library . is_movie else ' Show ' } { ' s ' if count_removed == 1 else ' ' } Removed " )
logger . info ( f " { count_removed } { self . collection_level . capitalize ( ) } { ' s ' if count_removed == 1 else ' ' } Removed " )
def update_item_details ( self ) :
add_tags = self . item_details [ " item_label " ] if " item_label " in self . item_details else None
@ -1697,7 +1769,7 @@ class CollectionBuilder:
key , options = plex . item_advance_keys [ method_name ]
if getattr ( item , key ) != options [ method_data ] :
advance_edits [ key ] = options [ method_data ]
self . library . edit_item ( item , item . title , " Movie " if self . library . is_movie else " Show " , advance_edits , advanced = True )
self . library . edit_item ( item , item . title , self . collection_level . capitalize ( ) , advance_edits , advanced = True )
if len ( tmdb_ids ) > 0 :
if " item_radarr_tag " in self . item_details :
@ -1890,7 +1962,8 @@ class CollectionBuilder:
logger . debug ( keys )
logger . debug ( self . rating_keys )
for key in self . rating_keys :
logger . info ( f " Moving { keys [ key ] . title } { ' after {} ' . format ( keys [ previous ] . title ) if previous else ' to the beginning ' } " )
text = f " after { self . item_title ( keys [ previous ] ) } " if previous else " to the beginning "
logger . info ( f " Moving { self . item_title ( keys [ key ] ) } { text } " )
self . library . move_item ( self . obj , key , after = previous )
previous = key
@ -1912,13 +1985,12 @@ class CollectionBuilder:
except ( BadRequest , NotFound ) :
logger . error ( f " Plex Error: Item { rating_key } not found " )
continue
current_title = f " { current . title } ( { current . year } ) " if current . year else current . title
if current in collection_items :
logger . info ( f " { name } Collection | = | { current_title } " )
logger . info ( f " { name } Collection | = | { self . item_title ( current ) } " )
else :
self . library . query_data ( current . addLabel if self . smart_label_collection else current . addCollection , name )
logger . info ( f " { name } Collection | + | { current_title } " )
logger . info ( f " { len ( rating_keys ) } { ' Movie ' if self . library . is_movie else ' Show ' } { ' s ' if len ( rating_keys ) > 1 else ' ' } Processed " )
logger . info ( f " { name } Collection | + | { self . item_title ( current ) } " )
logger . info ( f " { len ( rating_keys ) } { self . collection_level . capitalize ( ) } { ' s ' if len ( rating_keys ) > 1 else ' ' } Processed " )
if len ( self . run_again_movies ) > 0 :
logger . info ( " " )