From 35008908fc78b1bb6fc208a7005ea3379fcf1d2a Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sun, 27 Aug 2023 12:06:07 -0700 Subject: [PATCH] Improve parsing to datetime and add error handling (#1203) * Improve parsing to datetime and add error handling * Fix utils.millisecondToHumanstr for days and negatives * Update tests for millisecondToHumanstr --- plexapi/utils.py | 38 ++++++++++++++++++++++---------------- tests/test_utils.py | 8 +++++++- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/plexapi/utils.py b/plexapi/utils.py index d1882fbbc..f81211c36 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -11,13 +11,13 @@ import warnings import zipfile from collections import deque -from datetime import datetime +from datetime import datetime, timedelta from getpass import getpass from threading import Event, Thread from urllib.parse import quote -from requests.status_codes import _codes as codes import requests +from requests.status_codes import _codes as codes from plexapi.exceptions import BadRequest, NotFound, Unauthorized @@ -313,33 +313,39 @@ def toDatetime(value, format=None): value (str): value to return as a datetime format (str): Format to pass strftime (optional; if value is a str). """ - if value and value is not None: + if value is not None: if format: try: - value = datetime.strptime(value, format) + return datetime.strptime(value, format) except ValueError: - log.info('Failed to parse %s to datetime, defaulting to None', value) + log.info('Failed to parse "%s" to datetime as format "%s", defaulting to None', value, format) return None else: - # https://bugs.python.org/issue30684 - # And platform support for before epoch seems to be flaky. - # Also limit to max 32-bit integer - value = min(max(int(value), 86400), 2**31 - 1) - value = datetime.fromtimestamp(int(value)) + try: + return datetime.utcfromtimestamp(0) + timedelta(seconds=int(value)) + except ValueError: + log.info('Failed to parse "%s" to datetime as timestamp, defaulting to None', value) + return None + except OverflowError: + log.info('Failed to parse "%s" to datetime as timestamp (out-of-bounds), defaulting to None', value) + return None return value def millisecondToHumanstr(milliseconds): - """ Returns human readable time duration from milliseconds. - HH:MM:SS:MMMM + """ Returns human readable time duration [D day[s], ]HH:MM:SS.UUU from milliseconds. Parameters: - milliseconds (str,int): time duration in milliseconds. + milliseconds (str, int): time duration in milliseconds. """ milliseconds = int(milliseconds) - r = datetime.utcfromtimestamp(milliseconds / 1000) - f = r.strftime("%H:%M:%S.%f") - return f[:-2] + if milliseconds < 0: + return '-' + millisecondToHumanstr(abs(milliseconds)) + secs, ms = divmod(milliseconds, 1000) + mins, secs = divmod(secs, 60) + hours, mins = divmod(mins, 60) + days, hours = divmod(hours, 24) + return ('' if days == 0 else f'{days} day{"s" if days > 1 else ""}, ') + f'{hours:02d}:{mins:02d}:{secs:02d}.{ms:03d}' def toList(value, itemcast=None, delim=','): diff --git a/tests/test_utils.py b/tests/test_utils.py index fe16218ff..4d7ad4b84 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -90,7 +90,13 @@ def test_utils_download(plex, episode): def test_millisecondToHumanstr(): res = utils.millisecondToHumanstr(1000) - assert res == "00:00:01.0000" + assert res == "00:00:01.000" + res = utils.millisecondToHumanstr(-1000) + assert res == "-00:00:01.000" + res = utils.millisecondToHumanstr(123456789) + assert res == "1 day, 10:17:36.789" + res = utils.millisecondToHumanstr(-123456789) + assert res == "-1 day, 10:17:36.789" def test_toJson(movie):