@ -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,12 +840,16 @@ 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 ) :
self . data = { " name " : str ( self . data ) }
logger . warning ( f " Overlay Warning: No overlay attribute using mapping name { self . data } as the overlay name " )
if " name " not in self . data or not self . data [ " name " ] :
if " name " not in self . data or not self . data [ " name " ] :
raise Failed ( f " Overlay Error: overlay must have the name attribute " )
raise Failed ( f " Overlay Error: overlay must have the name attribute " )
self . name = str ( self . data [ " name " ] )
self . name = str ( self . data [ " name " ] )
@ -860,18 +864,52 @@ class Overlay:
if ( " group " in self . data or " weight " in self . data ) and ( self . weight is None or not self . group ) :
if ( " group " in self . data or " weight " in self . data ) and ( self . weight is None or not self . group ) :
raise Failed ( f " Overlay Error: overlay attribute ' s group and weight must be used together " )
raise Failed ( f " Overlay Error: overlay attribute ' s group and weight must be used together " )
self . x_align = parse ( " Overlay " , " x_align " , self . data [ " x_align " ] , options = [ " left " , " center " , " right " ] ) if " x_align " in self . data else " left "
self . y_align = parse ( " Overlay " , " y_align " , self . data [ " y_align " ] , options = [ " top " , " center " , " bottom " ] ) if " y_align " in self . data else " top "
x_cord = None
x_cord = None
y_cord = None
if " x_coordinate " in self . data and self . data [ " x_coordinate " ] is not None :
if " x_coordinate " in self . data and self . data [ " x_coordinate " ] is not None :
x_cord = check_num ( self . data [ " x_coordinate " ] )
x_cord = self . data [ " x_coordinate " ]
if x_cord is None or x_cord < 0 :
per = False
raise Failed ( f " Overlay Error: overlay x_coordinate: { self . data [ ' x_coordinate ' ] } must be a number 0 or greater " )
if str ( x_cord ) . endswith ( " % " ) :
x_cord = x_cord [ : - 1 ]
per = True
x_cord = check_num ( x_cord )
error = f " Overlay Error: overlay x_coordinate: { self . data [ ' x_coordinate ' ] } must be a number "
if x_cord is None :
raise Failed ( error )
if self . x_align != " center " and not per and x_cord < 0 :
raise Failed ( f " { error } 0 or greater " )
elif self . x_align != " center " and per and x_cord > 100 :
raise Failed ( f " { error } between 0% and 100% " )
elif self . x_align == " center " and per and ( x_cord > 50 or x_cord < - 50 ) :
raise Failed ( f " { error } between -50% and 50% " )
if per :
x_cord = f " { x_cord } % "
y_cord = None
if " y_coordinate " in self . data and self . data [ " y_coordinate " ] is not None :
if " y_coordinate " in self . data and self . data [ " y_coordinate " ] is not None :
y_cord = check_num ( self . data [ " y_coordinate " ] )
y_cord = self . data [ " y_coordinate " ]
if y_cord is None or y_cord < 0 :
per = False
raise Failed ( f " Overlay Error: overlay y_coordinate: { self . data [ ' y_coordinate ' ] } must be a number 0 or greater " )
if str ( y_cord ) . endswith ( " % " ) :
y_cord = y_cord [ : - 1 ]
per = True
y_cord = check_num ( y_cord )
error = f " Overlay Error: overlay y_coordinate: { self . data [ ' y_coordinate ' ] } must be a number "
if y_cord is None :
raise Failed ( error )
if self . y_align != " center " and not per and y_cord < 0 :
raise Failed ( f " { error } 0 or greater " )
elif self . y_align != " center " and per and y_cord > 100 :
raise Failed ( f " { error } between 0% and 100% " )
elif self . y_align == " center " and per and ( y_cord > 50 or y_cord < - 50 ) :
raise Failed ( f " { error } between -50% and 50% " )
if per :
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 ) :
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 " )
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 :
if x_cord is not None or y_cord is not None :
self . coordinates = ( x_cord , y_cord )
self . coordinates = ( x_cord , y_cord )
@ -920,19 +958,22 @@ class Overlay:
if not match :
if not match :
raise Failed ( f " Overlay Error: failed to parse overlay text name: { self . name } " )
raise Failed ( f " Overlay Error: failed to parse overlay text name: { self . name } " )
self . name = f " text( { match . group ( 1 ) } ) "
self . name = f " text( { match . group ( 1 ) } ) "
if " font " in self . data and self . data [ " font " ] :
if os . path . exists ( " Salma.otf " ) :
font = str ( self . data [ " font " ] )
self . font_name = " Salma.otf "
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 :
if " font_size " in self . data and self . data [ " font_size " ] is not None :
font_size = check_num ( self . data [ " font_size " ] )
font_size = check_num ( self . data [ " font_size " ] )
if font_size is None or font_size < 1 :
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 " )
logger . error ( f " Overlay Error: overlay font_size: { self . data [ ' font_size ' ] } invalid must be a greater than 0 " )
else :
else :
self . font_size = font_size
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 " ] :
if " font_color " in self . data and self . data [ " font_color " ] :
try :
try :
color_str = self . data [ " font_color " ]
color_str = self . data [ " font_color " ]
@ -948,18 +989,46 @@ class Overlay:
self . path = os . path . join ( library . overlay_folder , f " { clean_name } .png " )
self . path = os . path . join ( library . overlay_folder , f " { clean_name } .png " )
if not os . path . exists ( self . path ) :
if not os . path . exists ( self . path ) :
raise Failed ( f " Overlay Error: Overlay Image not found at: { self . path } " )
raise Failed ( f " Overlay Error: Overlay Image not found at: { self . path } " )
else :
image_compare = None
self . name = str ( self . data )
if self . config . Cache :
logger . warning ( f " Overlay Warning: No overlay attribute using mapping name { self . data } as the overlay name " )
_ , 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