SickGear/sickgear/sgdatetime.py
2023-02-09 13:41:15 +00:00

303 lines
11 KiB
Python

#
# This file is part of SickGear.
#
# SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickGear is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import datetime
import functools
import locale
import re
import sys
import sickgear
from dateutil import tz
from six import integer_types, PY2, string_types
# noinspection PyUnreachableCode
if False:
from typing import Callable, Optional, Union
date_presets = ('%Y-%m-%d',
'%a, %Y-%m-%d',
'%A, %Y-%m-%d',
'%y-%m-%d',
'%a, %y-%m-%d',
'%A, %y-%m-%d',
'%m/%d/%Y',
'%a, %m/%d/%Y',
'%A, %m/%d/%Y',
'%m/%d/%y',
'%a, %m/%d/%y',
'%A, %m/%d/%y',
'%m-%d-%Y',
'%a, %m-%d-%Y',
'%A, %m-%d-%Y',
'%m-%d-%y',
'%a, %m-%d-%y',
'%A, %m-%d-%y',
'%m.%d.%Y',
'%a, %m.%d.%Y',
'%A, %m.%d.%Y',
'%m.%d.%y',
'%a, %m.%d.%y',
'%A, %m.%d.%y',
'%d-%m-%Y',
'%a, %d-%m-%Y',
'%A, %d-%m-%Y',
'%d-%m-%y',
'%a, %d-%m-%y',
'%A, %d-%m-%y',
'%d/%m/%Y',
'%a, %d/%m/%Y',
'%A, %d/%m/%Y',
'%d/%m/%y',
'%a, %d/%m/%y',
'%A, %d/%m/%y',
'%d.%m.%Y',
'%a, %d.%m.%Y',
'%A, %d.%m.%Y',
'%d.%m.%y',
'%a, %d.%m.%y',
'%A, %d.%m.%y',
'%d. %b %Y',
'%a, %d. %b %Y',
'%A, %d. %b %Y',
'%d. %b %y',
'%a, %d. %b %y',
'%A, %d. %b %y',
'%d. %B %Y',
'%a, %d. %B %Y',
'%A, %d. %B %Y',
'%d. %B %y',
'%a, %d. %B %y',
'%A, %d. %B %y',
'%b %d, %Y',
'%a, %b %d, %Y',
'%A, %b %d, %Y',
'%B %d, %Y',
'%a, %B %d, %Y',
'%A, %B %d, %Y')
time_presets = ('%I:%M:%S %p',
'%I:%M:%S %P',
'%H:%M:%S')
is_win = 'win32' == sys.platform
# helper decorator class
# noinspection PyPep8Naming
class static_or_instance(object):
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
return functools.partial(self.func, instance)
# subclass datetime.datetime to add function to display custom date and time formats
class SGDatetime(datetime.datetime):
has_locale = True
@static_or_instance
def is_locale_eng(self):
today = SGDatetime.sbfdate(SGDatetime.now(), '%A').lower()
return ('day' == today[-3::] and today[0:-3:] in ['sun', 'mon', 'tues', 'wednes', 'thurs', 'fri', 'satur']
and SGDatetime.sbfdate(SGDatetime.now(), '%B').lower() in [
'january', 'february', 'march', 'april', 'may', 'june',
'july', 'august', 'september', 'october', 'november', 'december'])
@static_or_instance
def convert_to_setting(self, dt=None, force_local=False):
# type: (Optional[datetime.datetime, SGDatetime], bool) -> Union[SGDatetime, datetime.datetime]
obj = (dt, self)[self is not None] # type: datetime.datetime
try:
if force_local or 'local' == sickgear.TIMEZONE_DISPLAY:
from sickgear.network_timezones import SG_TIMEZONE
return obj.astimezone(SG_TIMEZONE)
except (BaseException, Exception):
pass
return obj
@static_or_instance
def setlocale(self, setlocale=True, use_has_locale=None, locale_str=''):
if setlocale:
try:
if None is use_has_locale or use_has_locale:
locale.setlocale(locale.LC_TIME, locale_str)
except locale.Error:
if None is not use_has_locale:
SGDatetime.has_locale = False
pass
# display Time in SickGear Format
@static_or_instance
def sbftime(self, dt=None, show_seconds=False, t_preset=None, setlocale=True, markup=False):
SGDatetime.setlocale(setlocale=setlocale, use_has_locale=SGDatetime.has_locale, locale_str='us_US')
strt = ''
obj = (dt, self)[self is not None] # type: datetime.datetime
if None is not obj:
tmpl = (((sickgear.TIME_PRESET, sickgear.TIME_PRESET_W_SECONDS)[show_seconds]),
t_preset)[None is not t_preset]
tmpl = (tmpl.replace(':%S', ''), tmpl)[show_seconds]
strt = SGDatetime.sbstrftime(obj, tmpl.replace('%P', '%p'))
if sickgear.TRIM_ZERO:
strt = re.sub(r'^0(\d:\d\d)', r'\1', strt)
if re.search(r'(?im)%p$', tmpl):
if '%p' in tmpl:
strt = strt.upper()
elif '%P' in tmpl:
strt = strt.lower()
if sickgear.TRIM_ZERO:
strt = re.sub(r'(?im)^(\d+)(?::00)?(\s?[ap]m)', r'\1\2', strt)
if markup:
match = re.search(r'(?im)(\d{1,2})(?:(.)(\d\d)(?:(.)(\d\d))?)?(?:\s?([ap]m))?$', strt)
if match:
strt = ('%s%s%s%s%s%s' % (
('<span class="time-hr">%s</span>' % match.group(1), '')[None is match.group(1)],
('<span class="time-hr-min">%s</span>' % match.group(2), '')[None is match.group(2)],
('<span class="time-min">%s</span>' % match.group(3), '')[None is match.group(3)],
('<span class="time-min-sec">%s</span>' % match.group(4), '')[None is match.group(4)],
('<span class="time-sec">%s</span>' % match.group(5), '')[None is match.group(5)],
('<span class="time-am-pm">%s</span>' % match.group(6), '')[None is match.group(6)]))
SGDatetime.setlocale(setlocale=setlocale, use_has_locale=SGDatetime.has_locale)
return strt
# display Date in SickGear Format
@static_or_instance
def sbfdate(self, dt=None, d_preset=None, setlocale=True):
SGDatetime.setlocale(setlocale=setlocale)
strd = ''
try:
obj = (dt, self)[self is not None] # type: datetime.datetime
if None is not obj:
strd = SGDatetime.sbstrftime(obj, (sickgear.DATE_PRESET, d_preset)[None is not d_preset])
finally:
SGDatetime.setlocale(setlocale=setlocale)
return strd
# display Datetime in SickGear Format
@static_or_instance
def sbfdatetime(self, dt=None, show_seconds=False, d_preset=None, t_preset=None, markup=False):
SGDatetime.setlocale()
strd = ''
obj = (dt, self)[self is not None] # type: datetime.datetime
try:
if None is not obj:
strd = u'%s, %s' % (
SGDatetime.sbstrftime(obj, (sickgear.DATE_PRESET, d_preset)[None is not d_preset]),
SGDatetime.sbftime(dt, show_seconds, t_preset, False, markup))
finally:
SGDatetime.setlocale(use_has_locale=SGDatetime.has_locale)
return strd
@staticmethod
def sbstrftime(obj, str_format):
try:
result = obj.strftime(str_format),
except ValueError:
result = obj.replace(tzinfo=None).strftime(str_format)
return result if isinstance(result, string_types) else \
isinstance(result, tuple) and 1 == len(result) and '%s' % result[0] or ''
@static_or_instance
def to_file_timestamp(self, dt=None):
# type: (Optional[SGDatetime, datetime.datetime]) -> Union[float, integer_types]
"""
convert datetime to filetime
special handling for windows filetime issues
for pre Windows 7 this can result in an exception for pre 1970 dates
"""
obj = (dt, self)[self is not None] # type: datetime.datetime
if is_win:
from .network_timezones import EPOCH_START_WIN
return (obj.replace(tzinfo=tz.tzwinlocal()) - EPOCH_START_WIN).total_seconds()
return SGDatetime.timestamp_far(obj)
@staticmethod
def from_timestamp(ts, local_time=True, tz_aware=False, tzinfo=None):
# type: (Union[float, integer_types], bool, bool, datetime.tzinfo) -> datetime.datetime
"""
convert timestamp to datetime.datetime obj
:param ts: timestamp integer, float
:param local_time: return as local timezone (SG_TIMEZONE)
:param tz_aware: return tz aware datetime
:param tzinfo: tzinfo to be used
"""
from .network_timezones import EPOCH_START, SG_TIMEZONE
result = EPOCH_START + datetime.timedelta(seconds=ts)
if local_time and SG_TIMEZONE:
result = result.astimezone(SG_TIMEZONE)
if isinstance(tzinfo, datetime.tzinfo):
result = result.astimezone(tzinfo)
if not tz_aware:
return result.replace(tzinfo=None)
return result
@static_or_instance
def timestamp_far(self,
dt=None, # type: Optional[SGDatetime, datetime.datetime]
default=None # type: Optional[float, integer_types]
):
# type: (...) -> Union[float, integer_types, None]
"""
Use `timestamp_far` for a timezone aware UTC timestamp in far future or far past
"""
obj = (dt, self)[self is not None] # type: datetime.datetime
if isinstance(obj, datetime.datetime) and not isinstance(getattr(obj, 'tzinfo', None), datetime.tzinfo):
from sickgear.network_timezones import SG_TIMEZONE
obj = obj.replace(tzinfo=SG_TIMEZONE)
from .network_timezones import EPOCH_START
timestamp = default
try:
timestamp = (obj - EPOCH_START).total_seconds()
finally:
return (default, timestamp)[isinstance(timestamp, (float, integer_types))]
if PY2:
"""
Use `timestamp_near` for a timezone aware UTC timestamp in the near future or recent past.
Under py3, using the faster variable assigned cpython callable, so py2 is set up to mimic the signature types.
Note: the py3 callable is limited to datetime.datetime and does not work with datetime.date.
"""
def _py2timestamp(dt=None):
# type: (datetime.datetime) -> float
try:
import time
return int(time.mktime(dt.timetuple()))
except (BaseException, Exception):
return 0
timestamp_near = _py2timestamp # type: Callable[[datetime.datetime], float]
else:
# py3 native timestamp uses milliseconds
timestamp_near = datetime.datetime.timestamp # type: Callable[[datetime.datetime], float]