2021-08-10 15:33:32 +00:00
import glob , logging , os , re , signal , sys , time , traceback
2021-08-06 23:02:33 +00:00
from datetime import datetime , timedelta
2021-08-10 15:33:32 +00:00
from logging . handlers import RotatingFileHandler
2021-05-19 21:30:20 +00:00
from pathvalidate import is_valid_filename , sanitize_filename
2021-05-11 01:22:18 +00:00
from plexapi . exceptions import BadRequest , NotFound , Unauthorized
2021-12-17 14:24:46 +00:00
from plexapi . video import Season , Episode , Movie
2021-01-20 21:37:59 +00:00
try :
import msvcrt
windows = True
except ModuleNotFoundError :
windows = False
logger = logging . getLogger ( " Plex Meta Manager " )
class TimeoutExpired ( Exception ) :
pass
class Failed ( Exception ) :
pass
2021-10-21 14:00:16 +00:00
class NotScheduled ( Exception ) :
pass
2021-06-30 15:02:55 +00:00
class ImageData :
2021-06-12 15:29:17 +00:00
def __init__ ( self , attribute , location , prefix = " " , is_poster = True , is_url = True ) :
self . attribute = attribute
self . location = location
self . prefix = prefix
self . is_poster = is_poster
self . is_url = is_url
self . compare = location if is_url else os . stat ( location ) . st_size
self . message = f " { prefix } { ' poster ' if is_poster else ' background ' } to [ { ' URL ' if is_url else ' File ' } ] { location } "
2021-10-04 17:51:32 +00:00
def __str__ ( self ) :
return str ( self . __dict__ )
2021-01-20 21:37:59 +00:00
def retry_if_not_failed ( exception ) :
return not isinstance ( exception , Failed )
2021-05-11 01:22:18 +00:00
def retry_if_not_plex ( exception ) :
return not isinstance ( exception , ( BadRequest , NotFound , Unauthorized ) )
2021-02-24 06:42:58 +00:00
separating_character = " = "
2021-01-20 21:37:59 +00:00
screen_width = 100
2021-05-26 13:25:32 +00:00
spacing = 0
2021-01-20 21:37:59 +00:00
days_alias = {
" monday " : 0 , " mon " : 0 , " m " : 0 ,
" tuesday " : 1 , " tues " : 1 , " tue " : 1 , " tu " : 1 , " t " : 1 ,
" wednesday " : 2 , " wed " : 2 , " w " : 2 ,
" thursday " : 3 , " thurs " : 3 , " thur " : 3 , " thu " : 3 , " th " : 3 , " r " : 3 ,
" friday " : 4 , " fri " : 4 , " f " : 4 ,
" saturday " : 5 , " sat " : 5 , " s " : 5 ,
" sunday " : 6 , " sun " : 6 , " su " : 6 , " u " : 6
}
2021-08-14 22:59:35 +00:00
mod_displays = {
2021-09-13 02:42:33 +00:00
" " : " is " , " .not " : " is not " , " .is " : " is " , " .isnot " : " is not " , " .begins " : " begins with " , " .ends " : " ends with " , " .before " : " is before " , " .after " : " is after " ,
2021-08-14 22:59:35 +00:00
" .gt " : " is greater than " , " .gte " : " is greater than or equal " , " .lt " : " is less than " , " .lte " : " is less than or equal "
}
2021-07-23 19:44:21 +00:00
pretty_days = { 0 : " Monday " , 1 : " Tuesday " , 2 : " Wednesday " , 3 : " Thursday " , 4 : " Friday " , 5 : " Saturday " , 6 : " Sunday " }
2021-01-20 21:37:59 +00:00
pretty_months = {
2021-07-23 19:44:21 +00:00
1 : " January " , 2 : " February " , 3 : " March " , 4 : " April " , 5 : " May " , 6 : " June " ,
7 : " July " , 8 : " August " , 9 : " September " , 10 : " October " , 11 : " November " , 12 : " December "
2021-01-20 21:37:59 +00:00
}
2021-08-14 22:59:35 +00:00
seasons = [ " winter " , " spring " , " summer " , " fall " ]
2021-07-23 19:44:21 +00:00
pretty_ids = { " anidbid " : " AniDB " , " imdbid " : " IMDb " , " mal_id " : " MyAnimeList " , " themoviedb_id " : " TMDb " , " thetvdb_id " : " TVDb " , " tvdbid " : " TVDb " }
2021-01-20 21:37:59 +00:00
2021-02-24 06:44:06 +00:00
def tab_new_lines ( data ) :
return str ( data ) . replace ( " \n " , " \n | \t " ) if " \n " in str ( data ) else str ( data )
2021-01-20 21:37:59 +00:00
def make_ordinal ( n ) :
2021-07-26 20:29:28 +00:00
return f " { n } { ' th ' if 11 < = ( n % 100 ) < = 13 else [ ' th ' , ' st ' , ' nd ' , ' rd ' , ' th ' ] [ min ( n % 10 , 4 ) ] } "
2021-01-20 21:37:59 +00:00
2021-08-22 15:54:33 +00:00
def add_zero ( number ) :
return str ( number ) if len ( str ( number ) ) > 1 else f " 0 { number } "
2021-08-04 14:20:52 +00:00
def add_dict_list ( keys , value , dict_map ) :
for key in keys :
if key in dict_map :
dict_map [ key ] . append ( value )
else :
dict_map [ key ] = [ value ]
2021-05-08 23:49:55 +00:00
def get_list ( data , lower = False , split = True , int_list = False ) :
2021-05-25 22:22:59 +00:00
if data is None : return None
elif isinstance ( data , list ) : return data
2021-01-20 21:37:59 +00:00
elif isinstance ( data , dict ) : return [ data ]
2021-02-17 06:10:50 +00:00
elif split is False : return [ str ( data ) ]
2021-02-16 04:54:47 +00:00
elif lower is True : return [ d . strip ( ) . lower ( ) for d in str ( data ) . split ( " , " ) ]
2021-11-28 08:18:12 +00:00
elif int_list is True :
try : return [ int ( d . strip ( ) ) for d in str ( data ) . split ( " , " ) ]
except ValueError : return [ ]
2021-02-16 04:54:47 +00:00
else : return [ d . strip ( ) for d in str ( data ) . split ( " , " ) ]
2021-01-20 21:37:59 +00:00
def get_int_list ( data , id_type ) :
int_values = [ ]
2021-07-30 19:19:43 +00:00
for value in get_list ( data ) :
2021-01-20 21:37:59 +00:00
try : int_values . append ( regex_first_int ( value , id_type ) )
except Failed as e : logger . error ( e )
return int_values
2021-07-21 19:25:29 +00:00
def validate_date ( date_text , method , return_as = None ) :
try : date_obg = datetime . strptime ( str ( date_text ) , " % Y- % m- %d " if " - " in str ( date_text ) 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) or MM/DD/YYYY (e.g. 12/25/2020) " )
return datetime . strftime ( date_obg , return_as ) if return_as else date_obg
2021-02-25 21:59:56 +00:00
2021-01-20 21:37:59 +00:00
def logger_input ( prompt , timeout = 60 ) :
if windows : return windows_input ( prompt , timeout )
elif hasattr ( signal , " SIGALRM " ) : return unix_input ( prompt , timeout )
else : raise SystemError ( " Input Timeout not supported on this system " )
2021-07-14 14:47:20 +00:00
def header ( language = " en-US,en;q=0.5 " ) :
2021-10-26 15:01:08 +00:00
return { " Accept-Language " : " eng " if language == " default " else language , " User-Agent " : " Mozilla/5.0 x64 " }
2021-07-14 14:47:20 +00:00
2021-01-20 21:37:59 +00:00
def alarm_handler ( signum , frame ) :
raise TimeoutExpired
def unix_input ( prompt , timeout = 60 ) :
2021-02-24 06:44:06 +00:00
prompt = f " | { prompt } : "
2021-01-20 21:37:59 +00:00
signal . signal ( signal . SIGALRM , alarm_handler )
signal . alarm ( timeout )
2021-07-04 04:13:06 +00:00
try : return input ( prompt )
except EOFError : raise Failed ( " Input Failed " )
finally : signal . alarm ( 0 )
2021-01-20 21:37:59 +00:00
def windows_input ( prompt , timeout = 5 ) :
2021-02-24 06:44:06 +00:00
sys . stdout . write ( f " | { prompt } : " )
2021-01-20 21:37:59 +00:00
sys . stdout . flush ( )
result = [ ]
start_time = time . time ( )
while True :
if msvcrt . kbhit ( ) :
2021-02-24 06:42:58 +00:00
char = msvcrt . getwche ( )
if ord ( char ) == 13 : # enter_key
2021-01-20 21:37:59 +00:00
out = " " . join ( result )
print ( " " )
2021-02-24 06:44:06 +00:00
logger . debug ( f " { prompt } : { out } " )
2021-01-20 21:37:59 +00:00
return out
2021-02-24 06:42:58 +00:00
elif ord ( char ) > = 32 : #space_char
result . append ( char )
2021-01-20 21:37:59 +00:00
if ( time . time ( ) - start_time ) > timeout :
print ( " " )
raise TimeoutExpired
def print_multiline ( lines , info = False , warning = False , error = False , critical = False ) :
2021-02-21 17:01:10 +00:00
for i , line in enumerate ( str ( lines ) . split ( " \n " ) ) :
2021-01-20 21:37:59 +00:00
if critical : logger . critical ( line )
elif error : logger . error ( line )
elif warning : logger . warning ( line )
elif info : logger . info ( line )
else : logger . debug ( line )
if i == 0 :
logger . handlers [ 1 ] . setFormatter ( logging . Formatter ( " " * 65 + " | %(message)s " ) )
logger . handlers [ 1 ] . setFormatter ( logging . Formatter ( " [ %(asctime)s ] %(filename)-27s %(levelname)-10s | %(message)s " ) )
def print_stacktrace ( ) :
print_multiline ( traceback . format_exc ( ) )
def my_except_hook ( exctype , value , tb ) :
for line in traceback . format_exception ( etype = exctype , value = value , tb = tb ) :
print_multiline ( line , critical = True )
def get_id_from_imdb_url ( imdb_url ) :
match = re . search ( " (tt \\ d+) " , str ( imdb_url ) )
if match : return match . group ( 1 )
2021-02-24 06:44:06 +00:00
else : raise Failed ( f " Regex Error: Failed to parse IMDb ID from IMDb URL: { imdb_url } " )
2021-01-20 21:37:59 +00:00
def regex_first_int ( data , id_type , default = None ) :
match = re . search ( " ( \\ d+) " , str ( data ) )
if match :
return int ( match . group ( 1 ) )
elif default :
2021-02-24 06:44:06 +00:00
logger . warning ( f " Regex Warning: Failed to parse { id_type } from { data } using { default } as default " )
2021-01-20 21:37:59 +00:00
return int ( default )
else :
2021-02-24 06:44:06 +00:00
raise Failed ( f " Regex Error: Failed to parse { id_type } from { data } " )
2021-01-20 21:37:59 +00:00
2021-05-24 03:38:46 +00:00
def centered ( text , sep = " " ) :
2021-01-20 21:37:59 +00:00
if len ( text ) > screen_width - 2 :
2021-06-11 14:26:11 +00:00
return text
2021-01-20 21:37:59 +00:00
space = screen_width - len ( text ) - 2
2021-05-24 03:38:46 +00:00
text = f " { text } "
2021-01-20 21:37:59 +00:00
if space % 2 == 1 :
2021-05-24 03:38:46 +00:00
text + = sep
2021-01-20 21:37:59 +00:00
space - = 1
2021-05-24 03:38:46 +00:00
side = int ( space / 2 ) - 1
final_text = f " { sep * side } { text } { sep * side } "
2021-03-21 23:00:37 +00:00
return final_text
2021-01-20 21:37:59 +00:00
2021-05-24 03:38:46 +00:00
def separator ( text = None , space = True , border = True , debug = False ) :
sep = " " if space else separating_character
2021-05-19 17:12:34 +00:00
for handler in logger . handlers :
apply_formatter ( handler , border = False )
2021-05-24 03:38:46 +00:00
border_text = f " | { separating_character * screen_width } | "
if border and debug :
logger . debug ( border_text )
elif border :
logger . info ( border_text )
2021-01-20 21:37:59 +00:00
if text :
2021-02-28 03:56:49 +00:00
text_list = text . split ( " \n " )
for t in text_list :
2021-12-06 22:30:38 +00:00
if debug :
logger . debug ( f " | { sep } { centered ( t , sep = sep ) } { sep } | " )
else :
logger . info ( f " | { sep } { centered ( t , sep = sep ) } { sep } | " )
2021-05-24 03:38:46 +00:00
if border and debug :
logger . debug ( border_text )
elif border :
logger . info ( border_text )
2021-05-19 17:12:34 +00:00
for handler in logger . handlers :
apply_formatter ( handler )
def apply_formatter ( handler , border = True ) :
text = f " | %(message)- { screen_width - 2 } s | " if border else f " %(message)- { screen_width - 2 } s "
2021-08-10 15:33:32 +00:00
if isinstance ( handler , RotatingFileHandler ) :
2021-05-19 17:12:34 +00:00
text = f " [%(asctime)s] %(filename)-27s %(levelname)-10s { text } "
handler . setFormatter ( logging . Formatter ( text ) )
2021-01-20 21:37:59 +00:00
2021-05-26 13:25:32 +00:00
def adjust_space ( display_title ) :
display_title = str ( display_title )
space_length = spacing - len ( display_title )
if space_length > 0 :
display_title + = " " * space_length
return display_title
def print_return ( text ) :
print ( adjust_space ( f " | { text } " ) , end = " \r " )
global spacing
spacing = len ( text ) + 2
2021-01-20 21:37:59 +00:00
2021-05-26 13:25:32 +00:00
def print_end ( ) :
print ( adjust_space ( " " ) , end = " \r " )
global spacing
spacing = 0
2021-05-19 21:30:20 +00:00
def validate_filename ( filename ) :
if is_valid_filename ( filename ) :
2021-05-20 20:38:48 +00:00
return filename , None
2021-05-19 21:30:20 +00:00
else :
mapping_name = sanitize_filename ( filename )
2021-05-20 20:38:48 +00:00
return mapping_name , f " Log Folder Name: { filename } is invalid using { mapping_name } "
2021-07-06 15:46:29 +00:00
2021-12-17 14:24:46 +00:00
def item_title ( item ) :
if isinstance ( item , 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 isinstance ( item , Episode ) :
text = f " { item . grandparentTitle } S { add_zero ( item . parentIndex ) } E { 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 isinstance ( item , Movie ) and item . year :
return f " { item . title } ( { item . year } ) "
else :
return item . title
2021-12-17 23:18:34 +00:00
def item_set ( item , item_id ) :
return { " title " : item_title ( item ) , " tmdb " if isinstance ( item , Movie ) else " tvdb " : item_id }
2021-07-06 15:46:29 +00:00
def is_locked ( filepath ) :
locked = None
file_object = None
if os . path . exists ( filepath ) :
try :
file_object = open ( filepath , ' a ' , 8 )
if file_object :
locked = False
2021-07-30 19:19:43 +00:00
except IOError :
2021-07-06 15:46:29 +00:00
locked = True
finally :
if file_object :
file_object . close ( )
return locked
2021-07-22 21:00:45 +00:00
2021-11-27 00:30:41 +00:00
def time_window ( time_window ) :
today = datetime . now ( )
if time_window == " today " :
return f " { today : %Y-%m-%d } "
elif time_window == " yesterday " :
return f " { today - timedelta ( days = 1 ) : %Y-%m-%d } "
elif time_window == " this_week " :
return f " { today : %Y-0%V } "
elif time_window == " last_week " :
return f " { today - timedelta ( weeks = 1 ) : %Y-0%V } "
elif time_window == " this_month " :
return f " { today : %Y-%m } "
elif time_window == " last_month " :
return f " { today . year } - { today . month - 1 or 12 } "
elif time_window == " this_year " :
return f " { today . year } "
elif time_window == " last_year " :
return f " { today . year - 1 } "
else :
return time_window
2021-08-10 15:33:32 +00:00
def glob_filter ( filter_in ) :
filter_in = filter_in . translate ( { ord ( " [ " ) : " [[] " , ord ( " ] " ) : " []] " } ) if " [ " in filter_in else filter_in
2021-12-13 07:30:19 +00:00
return glob . glob ( filter_in )
2021-08-10 15:33:32 +00:00
2021-08-07 06:01:21 +00:00
def is_date_filter ( value , modifier , data , final , current_time ) :
if value is None :
return True
2021-08-06 23:02:33 +00:00
if modifier in [ " " , " .not " ] :
threshold_date = current_time - timedelta ( days = data )
2021-08-07 06:01:21 +00:00
if ( modifier == " " and ( value is None or value < threshold_date ) ) \
or ( modifier == " .not " and value and value > = threshold_date ) :
return True
2021-08-06 23:02:33 +00:00
elif modifier in [ " .before " , " .after " ] :
filter_date = validate_date ( data , final )
2021-08-07 06:01:21 +00:00
if ( modifier == " .before " and value > = filter_date ) or ( modifier == " .after " and value < = filter_date ) :
return True
2021-08-06 23:02:33 +00:00
elif modifier == " .regex " :
2021-08-07 06:01:21 +00:00
jailbreak = True
2021-08-06 23:02:33 +00:00
for check_data in data :
2021-08-07 06:01:21 +00:00
if re . compile ( check_data ) . match ( value . strftime ( " % m/ %d / % Y " ) ) :
2021-08-06 23:02:33 +00:00
jailbreak = True
break
if not jailbreak :
2021-08-07 06:01:21 +00:00
return True
return False
def is_number_filter ( value , modifier , data ) :
return value is None or ( modifier == " .gt " and value < = data ) \
or ( modifier == " .gte " and value < data ) \
or ( modifier == " .lt " and value > = data ) \
or ( modifier == " .lte " and value > data )
2021-12-10 16:17:50 +00:00
def is_boolean_filter ( value , data ) :
return ( data and not value ) or ( not data and value )
2021-08-07 06:01:21 +00:00
def is_string_filter ( values , modifier , data ) :
jailbreak = False
for value in values :
for check_value in data :
if ( modifier in [ " " , " .not " ] and check_value . lower ( ) in value . lower ( ) ) \
2021-09-13 02:42:33 +00:00
or ( modifier in [ " .is " , " .isnot " ] and value . lower ( ) == check_value . lower ( ) ) \
2021-08-07 06:01:21 +00:00
or ( modifier == " .begins " and value . lower ( ) . startswith ( check_value . lower ( ) ) ) \
or ( modifier == " .ends " and value . lower ( ) . endswith ( check_value . lower ( ) ) ) \
or ( modifier == " .regex " and re . compile ( check_value ) . match ( value ) ) :
jailbreak = True
break
if jailbreak : break
2021-09-13 02:42:33 +00:00
return ( jailbreak and modifier in [ " .not " , " .isnot " ] ) or ( not jailbreak and modifier in [ " " , " .is " , " .begins " , " .ends " , " .regex " ] )
2021-08-06 23:02:33 +00:00