@ -4,7 +4,7 @@ from pathvalidate import is_valid_filename, sanitize_filename
from plexapi . audio import Album , Track
from plexapi . audio import Album , Track
from plexapi . exceptions import BadRequest , NotFound , Unauthorized
from plexapi . exceptions import BadRequest , NotFound , Unauthorized
from plexapi . video import Season , Episode , Movie
from plexapi . video import Season , Episode , Movie
from PIL import Image Color
from PIL import Image , Image Color, ImageDraw , ImageFont
try :
try :
import msvcrt
import msvcrt
@ -840,126 +840,195 @@ class Overlay:
self . path = None
self . path = None
self . coordinates = None
self . coordinates = None
self . font = None
self . font = None
self . font_name = None
self . font_size = 12
self . font_size = 12
self . font_color = None
self . font_color = None
logger . debug ( " " )
logger . debug ( " " )
logger . debug ( " Validating Method: overlay " )
logger . debug ( " Validating Method: overlay " )
logger . debug ( f " Value: { self . data } " )
logger . debug ( f " Value: { self . data } " )
if isinstance ( self . data , dict ) :
if not isinstance ( self . data , dict ) :
if " name " not in self . data or not self . data [ " name " ] :
self . data = { " name " : str ( self . data ) }
raise Failed ( f " Overlay Error: overlay must have the name attribute " )
logger . warning ( f " Overlay Warning: No overlay attribute using mapping name { self . data } as the overlay name " )
self . name = str ( self . data [ " name " ] )
if " name " not in self . data or not self . data [ " name " ] :
if " group " in self . data and self . data [ " group " ] :
raise Failed ( f " Overlay Error: overlay must have the name attribute " )
self . group = str ( self . data [ " group " ] )
self . name = str ( self . data [ " name " ] )
if " weight " in self . data and self . data [ " weight " ] is not None :
pri = check_num ( self . data [ " weight " ] )
if " group " in self . data and self . data [ " group " ] :
if pri is None :
self . group = str ( self . data [ " group " ] )
raise Failed ( f " Overlay Error: overlay weight must be a number " )
if " weight " in self . data and self . data [ " weight " ] is not None :
self . weight = pri
pri = check_num ( self . data [ " weight " ] )
if ( " group " in self . data or " weight " in self . data ) and ( self . weight is None or not self . group ) :
if pri is None :
raise Failed ( f " Overlay Error: overlay attribute ' s group and weight must be used together " )
raise Failed ( f " Overlay Error: overlay weight must be a number " )
self . weight = pri
x_cord = None
if ( " group " in self . data or " weight " in self . data ) and ( self . weight is None or not self . group ) :
y_cord = None
raise Failed ( f " Overlay Error: overlay attribute ' s group and weight must be used together " )
if " x_coordinate " in self . data and self . data [ " x_coordinate " ] is not None :
x_cord = check_num ( self . data [ " x_coordinate " ] )
self . x_align = parse ( " Overlay " , " x_align " , self . data [ " x_align " ] , options = [ " left " , " center " , " right " ] ) if " x_align " in self . data else " left "
if x_cord is None or x_cord < 0 :
self . y_align = parse ( " Overlay " , " y_align " , self . data [ " y_align " ] , options = [ " top " , " center " , " bottom " ] ) if " y_align " in self . data else " top "
raise Failed ( f " Overlay Error: overlay x_coordinate: { self . data [ ' x_coordinate ' ] } must be a number 0 or greater " )
if " y_coordinate " in self . data and self . data [ " y_coordinate " ] is not None :
x_cord = None
y_cord = check_num ( self . data [ " y_coordinate " ] )
if " x_coordinate " in self . data and self . data [ " x_coordinate " ] is not None :
if y_cord is None or y_cord < 0 :
x_cord = self . data [ " x_coordinate " ]
raise Failed ( f " Overlay Error: overlay y_coordinate: { self . data [ ' y_coordinate ' ] } must be a number 0 or greater " )
per = False
if ( " x_coordinate " in self . data or " y_coordinate " in self . data ) and ( x_cord is None or y_cord is None ) :
if str ( x_cord ) . endswith ( " % " ) :
raise Failed ( f " Overlay Error: overlay x_coordinate and overlay y_coordinate must be used together " )
x_cord = x_cord [ : - 1 ]
if x_cord is not None or y_cord is not None :
per = True
self . coordinates = ( x_cord , y_cord )
x_cord = check_num ( x_cord )
error = f " Overlay Error: overlay x_coordinate: { self . data [ ' x_coordinate ' ] } must be a number "
def get_and_save_image ( image_url ) :
if x_cord is None :
response = self . config . get ( image_url )
raise Failed ( error )
if response . status_code > = 400 :
if self . x_align != " center " and not per and x_cord < 0 :
raise Failed ( f " Overlay Error: Overlay Image not found at: { image_url } " )
raise Failed ( f " { error } 0 or greater " )
if " Content-Type " not in response . headers or response . headers [ " Content-Type " ] != " image/png " :
elif self . x_align != " center " and per and x_cord > 100 :
raise Failed ( f " Overlay Error: Overlay Image not a png: { image_url } " )
raise Failed ( f " { error } between 0% and 100% " )
if not os . path . exists ( library . overlay_folder ) or not os . path . isdir ( library . overlay_folder ) :
elif self . x_align == " center " and per and ( x_cord > 50 or x_cord < - 50 ) :
os . makedirs ( library . overlay_folder , exist_ok = False )
raise Failed ( f " { error } between -50% and 50% " )
logger . info ( f " Creating Overlay Folder found at: { library . overlay_folder } " )
if per :
clean_image_name , _ = validate_filename ( self . name )
x_cord = f " { x_cord } % "
image_path = os . path . join ( library . overlay_folder , f " { clean_image_name } .png " )
if os . path . exists ( image_path ) :
y_cord = None
os . remove ( image_path )
if " y_coordinate " in self . data and self . data [ " y_coordinate " ] is not None :
with open ( image_path , " wb " ) as handler :
y_cord = self . data [ " y_coordinate " ]
handler . write ( response . content )
per = False
while is_locked ( image_path ) :
if str ( y_cord ) . endswith ( " % " ) :
time . sleep ( 1 )
y_cord = y_cord [ : - 1 ]
return image_path
per = True
y_cord = check_num ( y_cord )
if not self . name . startswith ( ( " blur " , " text " ) ) :
error = f " Overlay Error: overlay y_coordinate: { self . data [ ' y_coordinate ' ] } must be a number "
if " file " in self . data and self . data [ " file " ] :
if y_cord is None :
self . path = self . data [ " file " ]
raise Failed ( error )
elif " git " in self . data and self . data [ " git " ] :
if self . y_align != " center " and not per and y_cord < 0 :
self . path = get_and_save_image ( f " { github_base } { self . data [ ' git ' ] } .png " )
raise Failed ( f " { error } 0 or greater " )
elif " repo " in self . data and self . data [ " repo " ] :
elif self . y_align != " center " and per and y_cord > 100 :
self . path = get_and_save_image ( f " { self . config . custom_repo } { self . data [ ' repo ' ] } .png " )
raise Failed ( f " { error } between 0% and 100% " )
elif " url " in self . data and self . data [ " url " ] :
elif self . y_align == " center " and per and ( y_cord > 50 or y_cord < - 50 ) :
self . path = get_and_save_image ( self . data [ " url " ] )
raise Failed ( f " { error } between -50% and 50% " )
if per :
if self . name . startswith ( " blur " ) :
y_cord = f " { y_cord } % "
if ( " x_coordinate " in self . data or " y_coordinate " in self . data ) and ( x_cord is None or y_cord is None ) :
raise Failed ( f " Overlay Error: overlay x_coordinate and overlay y_coordinate must be used together " )
if x_cord is not None or y_cord is not None :
self . coordinates = ( x_cord , y_cord )
def get_and_save_image ( image_url ) :
response = self . config . get ( image_url )
if response . status_code > = 400 :
raise Failed ( f " Overlay Error: Overlay Image not found at: { image_url } " )
if " Content-Type " not in response . headers or response . headers [ " Content-Type " ] != " image/png " :
raise Failed ( f " Overlay Error: Overlay Image not a png: { image_url } " )
if not os . path . exists ( library . overlay_folder ) or not os . path . isdir ( library . overlay_folder ) :
os . makedirs ( library . overlay_folder , exist_ok = False )
logger . info ( f " Creating Overlay Folder found at: { library . overlay_folder } " )
clean_image_name , _ = validate_filename ( self . name )
image_path = os . path . join ( library . overlay_folder , f " { clean_image_name } .png " )
if os . path . exists ( image_path ) :
os . remove ( image_path )
with open ( image_path , " wb " ) as handler :
handler . write ( response . content )
while is_locked ( image_path ) :
time . sleep ( 1 )
return image_path
if not self . name . startswith ( ( " blur " , " text " ) ) :
if " file " in self . data and self . data [ " file " ] :
self . path = self . data [ " file " ]
elif " git " in self . data and self . data [ " git " ] :
self . path = get_and_save_image ( f " { github_base } { self . data [ ' git ' ] } .png " )
elif " repo " in self . data and self . data [ " repo " ] :
self . path = get_and_save_image ( f " { self . config . custom_repo } { self . data [ ' repo ' ] } .png " )
elif " url " in self . data and self . data [ " url " ] :
self . path = get_and_save_image ( self . data [ " url " ] )
if self . name . startswith ( " blur " ) :
try :
match = re . search ( " \\ (([^)]+) \\ ) " , self . name )
if not match or 0 > = int ( match . group ( 1 ) ) > 100 :
raise ValueError
self . name = f " blur( { match . group ( 1 ) } ) "
except ValueError :
logger . error ( f " Overlay Error: failed to parse overlay blur name: { self . name } defaulting to blur(50) " )
self . name = " blur(50) "
elif self . name . startswith ( " text " ) :
if not self . coordinates :
raise Failed ( f " Overlay Error: overlay attribute ' s x_coordinate and y_coordinate are required when using text " )
match = re . search ( " \\ (([^)]+) \\ ) " , self . name )
if not match :
raise Failed ( f " Overlay Error: failed to parse overlay text name: { self . name } " )
self . name = f " text( { match . group ( 1 ) } ) "
if os . path . exists ( " Salma.otf " ) :
self . font_name = " Salma.otf "
if " font_size " in self . data and self . data [ " font_size " ] is not None :
font_size = check_num ( self . data [ " font_size " ] )
if font_size is None or font_size < 1 :
logger . error ( f " Overlay Error: overlay font_size: { self . data [ ' font_size ' ] } invalid must be a greater than 0 " )
else :
self . font_size = font_size
if " font " in self . data and self . data [ " font " ] :
font = str ( self . data [ " font " ] )
if not os . path . exists ( font ) :
fonts = get_system_fonts ( )
if font not in fonts :
raise Failed ( f " Overlay Error: font: { font } not found. Options: { ' , ' . join ( fonts ) } " )
self . font_name = font
self . font = ImageFont . truetype ( self . font_name , self . font_size )
if " font_color " in self . data and self . data [ " font_color " ] :
try :
try :
match = re . search ( " \\ (([^)]+) \\ ) " , self . name )
color_str = self . data [ " font_color " ]
if not match or 0 > = int ( match . group ( 1 ) ) > 100 :
color_str = color_str if color_str . startswith ( " # " ) else f " # { color_str } "
raise ValueError
self . font_color = ImageColor . getcolor ( color_str , " RGB " )
self . name = f " blur( { match . group ( 1 ) } ) "
except ValueError :
except ValueError :
logger . error ( f " Overlay Error: failed to parse overlay blur name: { self . name } defaulting to blur(50) " )
logger . error ( f " Overlay Error: overlay color: { self . data [ ' color ' ] } invalid " )
self . name = " blur(50) "
elif self . name . startswith ( " text " ) :
if not self . coordinates :
raise Failed ( f " Overlay Error: overlay attribute ' s x_coordinate and y_coordinate are required when using text " )
match = re . search ( " \\ (([^)]+) \\ ) " , self . name )
if not match :
raise Failed ( f " Overlay Error: failed to parse overlay text name: { self . name } " )
self . name = f " text( { match . group ( 1 ) } ) "
if " font " in self . data and self . data [ " font " ] :
font = str ( self . data [ " font " ] )
if not os . path . exists ( font ) :
fonts = get_system_fonts ( )
if font not in fonts :
raise Failed ( f " Overlay Error: font: { font } not found. Options: { ' , ' . join ( fonts ) } " )
self . font = font
if " font_size " in self . data and self . data [ " font_size " ] is not None :
font_size = check_num ( self . data [ " font_size " ] )
if font_size is None or font_size < 1 :
logger . error ( f " Overlay Error: overlay font_size: { self . data [ ' font_size ' ] } invalid must be a greater than 0 " )
else :
self . font_size = font_size
if " font_color " in self . data and self . data [ " font_color " ] :
try :
color_str = self . data [ " font_color " ]
color_str = color_str if color_str . startswith ( " # " ) else f " # { color_str } "
self . font_color = ImageColor . getcolor ( color_str , " RGB " )
except ValueError :
logger . error ( f " Overlay Error: overlay color: { self . data [ ' color ' ] } invalid " )
else :
if " | " in self . name :
raise Failed ( f " Overlay Error: Overlay Name: { self . name } cannot contain ' | ' " )
if not self . path :
clean_name , _ = validate_filename ( self . name )
self . path = os . path . join ( library . overlay_folder , f " { clean_name } .png " )
if not os . path . exists ( self . path ) :
raise Failed ( f " Overlay Error: Overlay Image not found at: { self . path } " )
else :
else :
self . name = str ( self . data )
if " | " in self . name :
logger . warning ( f " Overlay Warning: No overlay attribute using mapping name { self . data } as the overlay name " )
raise Failed ( f " Overlay Error: Overlay Name: { self . name } cannot contain ' | ' " )
if not self . path :
clean_name , _ = validate_filename ( self . name )
self . path = os . path . join ( library . overlay_folder , f " { clean_name } .png " )
if not os . path . exists ( self . path ) :
raise Failed ( f " Overlay Error: Overlay Image not found at: { self . path } " )
image_compare = None
if self . config . Cache :
_ , image_compare , _ = self . config . Cache . query_image_map ( self . name , f " { self . library . image_table_name } _overlays " )
overlay_size = os . stat ( self . path ) . st_size
self . updated = not image_compare or str ( overlay_size ) != str ( image_compare )
try :
self . image = Image . open ( self . path ) . convert ( " RGBA " )
if self . config . Cache :
self . config . Cache . update_image_map ( self . name , f " { self . library . image_table_name } _overlays " , self . name , overlay_size )
except OSError :
raise Failed ( f " Overlay Error: overlay image { self . path } failed to load " )
def get_overlay_compare ( self ) :
def get_overlay_compare ( self ) :
output = self . name
output = self . name
if self . group :
if self . group :
output + = f " { self . group } { self . weight } "
output + = f " { self . group } { self . weight } "
if self . coordinates :
if self . coordinates :
output + = str ( self . coordinates )
output + = f " { self . coordinates } { self . x_align } { self . y_align } "
if self . font :
if self . font_name :
output + = f " { self . font } { self . font_size } "
output + = f " { self . font_name } { self . font_size } "
if self . font_color :
if self . font_color :
output + = str ( self . font_color )
output + = str ( self . font_color )
return output
return output
def get_coordinates ( self , image_width , image_length , text = None ) :
if text :
_ , _ , width , height = ImageDraw . Draw ( Image . new ( " RGB " , ( 0 , 0 ) ) ) . textbbox ( ( 0 , 0 ) , text , font = self . font )
else :
width , height = self . image . size
x_cord , y_cord = self . coordinates
if str ( x_cord ) . endswith ( " % " ) :
x_cord = image_width * 0.01 * int ( x_cord [ : - 1 ] )
if str ( y_cord ) . endswith ( " % " ) :
y_cord = image_length * 0.01 * int ( y_cord [ : - 1 ] )
if self . x_align == " right " :
x_cord = image_width - width - x_cord
elif self . x_align == " center " :
x_cord = ( image_width / 2 ) - ( width / 2 ) + x_cord
if self . x_align == " bottom " :
y_cord = image_length - height - y_cord
elif self . x_align == " center " :
y_cord = ( image_length / 2 ) - ( height / 2 ) + y_cord
return x_cord , y_cord