1818from threading import Event , Thread
1919from urllib .parse import quote
2020from xml .etree import ElementTree
21+ from zoneinfo import ZoneInfo , ZoneInfoNotFoundError
2122
2223import requests
2324from requests .status_codes import _codes as codes
103104# Plex Objects - Populated at runtime
104105PLEXOBJECTS = {}
105106
107+ # Global timezone for toDatetime() conversions, set by setDatetimeTimezone()
108+ DATETIME_TIMEZONE = None
109+
106110
107111class SecretsFilter (logging .Filter ):
108112 """ Logging filter to hide secrets. """
@@ -326,6 +330,66 @@ def threaded(callback, listargs):
326330 return [r for r in results if r is not None ]
327331
328332
333+ def setDatetimeTimezone (value ):
334+ """ Sets the timezone to use when converting values with :func:`toDatetime`.
335+
336+ Parameters:
337+ value (bool, str):
338+ - ``False`` or ``None`` to disable timezone (default).
339+ - ``True`` or ``"local"`` to use the local timezone.
340+ - A valid IANA timezone (e.g. ``UTC`` or ``America/New_York``).
341+
342+ Returns:
343+ datetime.tzinfo: Resolved timezone object or ``None`` if disabled or invalid.
344+ """
345+ global DATETIME_TIMEZONE
346+
347+ # Disable timezone if value is False or None
348+ if value is None or value is False :
349+ tzinfo = None
350+ # Use local timezone if value is True or "local"
351+ elif value is True or str (value ).strip ().lower () == 'local' :
352+ tzinfo = datetime .now ().astimezone ().tzinfo
353+ # Attempt to resolve value as a boolean-like string or IANA timezone string
354+ else :
355+ setting = str (value ).strip ()
356+ # Try to cast as boolean first (normalize to lowercase for case-insensitive matching)
357+ try :
358+ is_enabled = cast (bool , setting .lower ())
359+ tzinfo = datetime .now ().astimezone ().tzinfo if is_enabled else None
360+ except ValueError :
361+ # Not a boolean string, try parsing as IANA timezone
362+ try :
363+ tzinfo = ZoneInfo (setting )
364+ except ZoneInfoNotFoundError :
365+ tzinfo = None
366+ log .warning ('Failed to set timezone to "%s", defaulting to None' , value )
367+
368+ DATETIME_TIMEZONE = tzinfo
369+ return DATETIME_TIMEZONE
370+
371+
372+ def _parseTimestamp (value , tzinfo ):
373+ """ Helper function to parse a timestamp value into a datetime object. """
374+ try :
375+ value = int (value )
376+ except ValueError :
377+ log .info ('Failed to parse "%s" to datetime as timestamp, defaulting to None' , value )
378+ return None
379+ try :
380+ if tzinfo :
381+ return datetime .fromtimestamp (value , tz = tzinfo )
382+ return datetime .fromtimestamp (value )
383+ except (OSError , OverflowError , ValueError ):
384+ try :
385+ if tzinfo :
386+ return datetime .fromtimestamp (0 , tz = tzinfo ) + timedelta (seconds = value )
387+ return datetime .fromtimestamp (0 ) + timedelta (seconds = value )
388+ except OverflowError :
389+ log .info ('Failed to parse "%s" to datetime as timestamp (out-of-bounds), defaulting to None' , value )
390+ return None
391+
392+
329393def toDatetime (value , format = None ):
330394 """ Returns a datetime object from the specified value.
331395
@@ -334,26 +398,20 @@ def toDatetime(value, format=None):
334398 format (str): Format to pass strftime (optional; if value is a str).
335399 """
336400 if value is not None :
401+ tzinfo = DATETIME_TIMEZONE
337402 if format :
338403 try :
339- return datetime .strptime (value , format )
404+ dt = datetime .strptime (value , format )
405+ # If parsed datetime already contains timezone
406+ if dt .tzinfo is not None :
407+ return dt .astimezone (tzinfo ) if tzinfo else dt
408+ else :
409+ return dt .replace (tzinfo = tzinfo ) if tzinfo else dt
340410 except ValueError :
341411 log .info ('Failed to parse "%s" to datetime as format "%s", defaulting to None' , value , format )
342412 return None
343413 else :
344- try :
345- value = int (value )
346- except ValueError :
347- log .info ('Failed to parse "%s" to datetime as timestamp, defaulting to None' , value )
348- return None
349- try :
350- return datetime .fromtimestamp (value )
351- except (OSError , OverflowError , ValueError ):
352- try :
353- return datetime .fromtimestamp (0 ) + timedelta (seconds = value )
354- except OverflowError :
355- log .info ('Failed to parse "%s" to datetime as timestamp (out-of-bounds), defaulting to None' , value )
356- return None
414+ return _parseTimestamp (value , tzinfo )
357415 return value
358416
359417
0 commit comments