mirror of
https://github.com/krateng/maloja.git
synced 2025-05-05 17:41:02 +03:00
436 lines
14 KiB
Python
436 lines
14 KiB
Python
import datetime
|
|
from calendar import monthrange
|
|
from os.path import commonprefix
|
|
|
|
|
|
FIRST_SCROBBLE = int(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
|
|
def register_scrobbletime(timestamp):
|
|
global FIRST_SCROBBLE
|
|
if timestamp < FIRST_SCROBBLE:
|
|
FIRST_SCROBBLE = int(timestamp)
|
|
|
|
|
|
# This is meant to be a time object that is aware of its own precision
|
|
# now I know what you're saying
|
|
# "This is total overengineering, Jimmy!"
|
|
# "Just convert all user input into timestamps right at the entry into the database and only work with those"
|
|
# "You can get the range descriptions right at the start as well or even generate them from timestamps with a simple comparison"
|
|
# and you are absolutely correct
|
|
# but my name isn't Jimmy
|
|
# so we're doing objects
|
|
#class Time():
|
|
#
|
|
# precision = 0
|
|
# # 0 unused, no information at all, embrace eternity
|
|
# # 1 year
|
|
# # 2 month
|
|
# # 3 day
|
|
# # 4 specified by exact timestamp
|
|
#
|
|
# def __init__(self,*time):
|
|
# # time can be a int (timestamp), list or string (/-delimited list)
|
|
#
|
|
# if len(time) == 1:
|
|
# time = time[0] #otherwise we will already have a tuple and we can deal with that
|
|
# if isinstance(time,int) and time < 10000: time = [time] # if we have a low number, it's not a timestamp, but a year
|
|
#
|
|
#
|
|
# if isinstance(time,str):
|
|
# time = time.split("/")
|
|
# if isinstance(time,list) or isinstance(time,tuple):
|
|
# time = [int(x) for x in time][:3]
|
|
# self.precision = len(time)
|
|
# if len(time) > 0: self.YEAR = time[0]
|
|
# if len(time) > 1: self.MONTH = time[1]
|
|
# if len(time) > 2: self.DAY = time[2]
|
|
# elif isinstance(time,int):
|
|
# self.precision = 4
|
|
# self.TIMESTAMP = time
|
|
# dt = datetime.datetime.utcfromtimestamp(time)
|
|
# self.YEAR, self.MONTH, self.DAY = dt.year, dt.month, dt.day
|
|
#
|
|
#
|
|
# def _array(self):
|
|
# if self.precision == 4:
|
|
# timeobject = datetime.datetime.utcfromtimestamp(self.TIMESTAMP)
|
|
# return [timeobject.year,timeobject.month,timeobject.day]
|
|
# if self.precision == 3: return [self.YEAR,self.MONTH,self.DAY]
|
|
# if self.precision == 2: return [self.YEAR,self.MONTH]
|
|
# if self.precision == 1: return [self.YEAR]
|
|
#
|
|
#
|
|
# def get(self):
|
|
# if self.precision == 4: return self.TIMESTAMP
|
|
# if self.precision == 3: return [self.YEAR,self.MONTH,self.DAY]
|
|
# if self.precision == 2: return [self.YEAR,self.MONTH]
|
|
# if self.precision == 1: return [self.YEAR]
|
|
#
|
|
# def getStartTimestamp(self):
|
|
# if self.precision == 4: return self.TIMESTAMP
|
|
# else:
|
|
# YEAR = self.YEAR if self.precision > 0 else 1970
|
|
# MONTH = self.MONTH if self.precision > 1 else 1
|
|
# DAY = self.DAY if self.precision > 2 else 1
|
|
# return int(datetime.datetime(YEAR,MONTH,DAY,tzinfo=datetime.timezone.utc).timestamp())
|
|
#
|
|
# def getEndTimestamp(self):
|
|
# if self.precision == 4: return self.TIMESTAMP
|
|
# else: return self.getNext().getStartTimestamp()-1
|
|
#
|
|
# # get next time of the same precision, e.g. next month if month of this time was defined (even if it's 1 or 12)
|
|
# def getNext(self,obj=True):
|
|
# if self.precision == 4: result = self.TIMESTAMP + 1
|
|
# else: result = _getNext(self._array())
|
|
#
|
|
# if obj: return Time(result)
|
|
# else: return result
|
|
#
|
|
#
|
|
# def pad(self,precision=3):
|
|
# arrayA, arrayB = self._array(), self._array()
|
|
# if self.precision < min(2,precision):
|
|
# arrayA.append(1)
|
|
# arrayB.append(12)
|
|
# if self.precision+1 < min(3,precision):
|
|
# arrayA.append(1)
|
|
# arrayB.append(monthrange(*arrayB)[1])
|
|
#
|
|
# return (arrayA,arrayB)
|
|
#
|
|
# def describe(self,short=False):
|
|
# if self.precision == 4:
|
|
# if short:
|
|
# now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
# difference = int(now.timestamp() - self.TIMESTAMP)
|
|
#
|
|
# if difference < 10: return "just now"
|
|
# if difference < 60: return str(difference) + " seconds ago"
|
|
# difference = int(difference/60)
|
|
# if difference < 60: return str(difference) + " minutes ago" if difference>1 else str(difference) + " minute ago"
|
|
# difference = int(difference/60)
|
|
# if difference < 24: return str(difference) + " hours ago" if difference>1 else str(difference) + " hour ago"
|
|
# difference = int(difference/24)
|
|
# timeobject = datetime.datetime.utcfromtimestamp(self.TIMESTAMP)
|
|
# if difference < 5: return timeobject.strftime("%A")
|
|
# if difference < 31: return str(difference) + " days ago" if difference>1 else str(difference) + " day ago"
|
|
# #if difference < 300 and tim.year == now.year: return tim.strftime("%B")
|
|
# #if difference < 300: return tim.strftime("%B %Y")
|
|
#
|
|
# return timeobject.strftime("%d. %B %Y")
|
|
# else:
|
|
# timeobject = datetime.datetime.utcfromtimestamp(self.TIMESTAMP)
|
|
# return tim.strftime("%d. %b %Y %I:%M %p")
|
|
#
|
|
# else:
|
|
# YEAR = self.YEAR if self.precision > 0 else 2022
|
|
# MONTH = self.MONTH if self.precision > 1 else 5 #else numbers dont matter, just needed to create the datetime object
|
|
# DAY = self.DAY if self.precision > 2 else 4
|
|
# timeobject = datetime.datetime(YEAR,MONTH,DAY,tzinfo=datetime.timezone.utc)
|
|
# if self.precision == 3: return timeobject.strftime("%d. %B %Y")
|
|
# if self.precision == 2: return timeobject.strftime("%B %Y")
|
|
# if self.precision == 1: return timeobject.strftime("%Y")
|
|
# if self.precision == 0: return "Embrace Eternity"
|
|
|
|
|
|
#def getRange(timeA,timeB):
|
|
# return (timeA.getStartTimestamp(),timeB.getEndTimestamp())
|
|
#
|
|
#def getRangeDesc(timeA,timeB):
|
|
# aA, aB = timeA.get(), timeB.get()
|
|
# if len(aA) != len(aB):
|
|
# prec = max(len(aA),len(aB))
|
|
# aA, aB = timeA.pad(prec)[0], timeB.pad(prec)[1]
|
|
# if aA == aB:
|
|
# return Time(aA).describe()
|
|
# if aA[:-1] == aB[:-1]:
|
|
# return " ".join(Time(aA).describe().split(" ")[:-1]) + " to " + Time(aB).describe() #what
|
|
#
|
|
|
|
|
|
# alright forget everything I've just told you
|
|
# so how bout this:
|
|
# we completely ignore times
|
|
# all singular times (only used for scrobbles) are only ever expressed in timestamps anyway and remain simple ints
|
|
# ranges specified in any kind of list are completely separated from them
|
|
# even if you specify the pulse
|
|
# holy feck this is so much better
|
|
|
|
|
|
# converts strings and stuff to lists
|
|
def time_fix(t):
|
|
|
|
|
|
if isinstance(t, str) and t.lower() == "today":
|
|
tod = datetime.datetime.utcnow()
|
|
t = [tod.year,tod.month,tod.day]
|
|
if isinstance(t, str) and t.lower() == "month":
|
|
tod = datetime.datetime.utcnow()
|
|
t = [tod.year,tod.month]
|
|
if isinstance(t, str) and t.lower() == "year":
|
|
tod = datetime.datetime.utcnow()
|
|
t = [tod.year]
|
|
|
|
|
|
if isinstance(t,str): t = t.split("/")
|
|
#if isinstance(t,tuple): t = list(t)
|
|
|
|
t = [int(p) for p in t]
|
|
|
|
return t[:3]
|
|
|
|
# makes times the same precision level
|
|
def time_pad(f,t):
|
|
f,t = time_fix(f), time_fix(t)
|
|
while len(f) < len(t):
|
|
if len(f) == 1: f.append(1)
|
|
elif len(f) == 2: f.append(1)
|
|
while len(f) > len(t):
|
|
if len(t) == 1: t.append(12)
|
|
elif len(t) == 2: t.append(monthrange(*t)[1])
|
|
|
|
return (f,t)
|
|
|
|
|
|
def time_desc(t,short=False):
|
|
if isinstance(t,int):
|
|
if short:
|
|
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
difference = int(now.timestamp() - t)
|
|
|
|
if difference < 10: return "just now"
|
|
if difference < 60: return str(difference) + " seconds ago"
|
|
difference = int(difference/60)
|
|
if difference < 60: return str(difference) + " minutes ago" if difference>1 else str(difference) + " minute ago"
|
|
difference = int(difference/60)
|
|
if difference < 24: return str(difference) + " hours ago" if difference>1 else str(difference) + " hour ago"
|
|
difference = int(difference/24)
|
|
timeobject = datetime.datetime.utcfromtimestamp(t)
|
|
if difference < 5: return timeobject.strftime("%A")
|
|
if difference < 31: return str(difference) + " days ago" if difference>1 else str(difference) + " day ago"
|
|
#if difference < 300 and tim.year == now.year: return tim.strftime("%B")
|
|
#if difference < 300: return tim.strftime("%B %Y")
|
|
|
|
return timeobject.strftime("%d. %B %Y")
|
|
else:
|
|
timeobject = datetime.datetime.utcfromtimestamp(t)
|
|
return timeobject.strftime("%d. %b %Y %I:%M %p")
|
|
|
|
else:
|
|
t = time_fix(t)
|
|
date = [1970,1,1]
|
|
date[:len(t)] = t
|
|
timeobject = datetime.datetime(date[0],date[1],date[2],tzinfo=datetime.timezone.utc)
|
|
if len(t) == 3: return timeobject.strftime("%d. %B %Y")
|
|
if len(t) == 2: return timeobject.strftime("%B %Y")
|
|
if len(t) == 1: return timeobject.strftime("%Y")
|
|
|
|
def range_desc(since=None,to=None,within=None,short=False):
|
|
|
|
if within is not None:
|
|
since = within
|
|
to = within
|
|
if since is None:
|
|
sincestr = ""
|
|
if to is None:
|
|
tostr = ""
|
|
|
|
if isinstance(since,int) and to is None:
|
|
sincestr = "since " + time_desc(since)
|
|
shortsincestr = sincestr
|
|
elif isinstance(to,int) and since is None:
|
|
tostr = "up until " + time_desc(to)
|
|
elif isinstance(since,int) and not isinstance(to,int):
|
|
sincestr = "from " + time_desc(since)
|
|
shortsincestr = time_desc(since)
|
|
tostr = "to the end of " + time_desc(to)
|
|
elif isinstance(to,int) and not isinstance(since,int):
|
|
sincestr = "from the start of " + time_desc(since)
|
|
shortsincestr = time_desc(since)
|
|
tostr = "to " + time_desc(to)
|
|
|
|
# if isinstance(since,int) and isinstance(to,int): result = "from " + time_desc(since) + " to " + time_desc(to)
|
|
# elif isinstance(since,int): result = "from " + time_desc(since) + " to the end of " + time_desc(to)
|
|
# elif isinstance(to,int): result = "from the start of " + time_desc(since) + " to " + time_desc(to)
|
|
else:
|
|
if since is not None and to is not None:
|
|
since,to = time_pad(since,to)
|
|
if since == to:
|
|
if len(since) == 3:
|
|
sincestr = "on " + time_desc(since)
|
|
else:
|
|
sincestr = "in " + time_desc(since)
|
|
shortsincestr = time_desc(since)
|
|
tostr = ""
|
|
else:
|
|
fparts = time_desc(since).split(" ")
|
|
tparts = time_desc(to).split(" ")
|
|
|
|
fparts.reverse()
|
|
tparts.reverse()
|
|
|
|
fparts = fparts[len(commonprefix([fparts,tparts])):]
|
|
|
|
fparts.reverse()
|
|
tparts.reverse()
|
|
|
|
sincestr = "from " + " ".join(fparts)
|
|
shortsincestr = " ".join(fparts)
|
|
tostr = "to " + " ".join(tparts)
|
|
|
|
else:
|
|
if since is not None:
|
|
sincestr = "since " + time_desc(since)
|
|
shortsincestr = sincestr
|
|
if to is not None:
|
|
tostr = "up until " + time_desc(to)
|
|
|
|
if short: return shortsincestr + " " + tostr
|
|
else: return sincestr + " " + tostr
|
|
|
|
|
|
|
|
|
|
|
|
def time_stamps(since=None,to=None,within=None):
|
|
|
|
|
|
if within is not None:
|
|
since = within
|
|
to = within
|
|
|
|
|
|
|
|
|
|
if (since==None): stamp1 = FIRST_SCROBBLE
|
|
else:
|
|
since = time_fix(since)
|
|
date = [1970,1,1]
|
|
date[:len(since)] = since
|
|
stamp1 = int(datetime.datetime(date[0],date[1],date[2],tzinfo=datetime.timezone.utc).timestamp())
|
|
|
|
if (to==None): stamp2 = int(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
else:
|
|
to = time_fix(to)
|
|
to = _get_next(to)
|
|
date = [1970,1,1]
|
|
date[:len(to)] = to
|
|
stamp2 = int(datetime.datetime(date[0],date[1],date[2],tzinfo=datetime.timezone.utc).timestamp())
|
|
|
|
|
|
return (stamp1,stamp2)
|
|
|
|
|
|
|
|
|
|
def delimit_desc(step="month",stepn=1,trail=1):
|
|
txt = ""
|
|
if stepn is not 1: txt += _num(stepn) + "-"
|
|
txt += {"year":"Yearly","month":"Monthly","day":"Daily"}[step.lower()]
|
|
#if trail is not 1: txt += " " + _num(trail) + "-Trailing"
|
|
if trail is not 1: txt += " Trailing" #we don't need all the info in the title
|
|
|
|
return txt
|
|
|
|
|
|
def _num(i):
|
|
names = ["Zero","One","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Eleven","Twelve"]
|
|
if i < len(names): return names[i]
|
|
else: return str(i)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ranges(since=None,to=None,within=None,step="month",stepn=1,trail=1,max_=None):
|
|
|
|
(firstincluded,lastincluded) = time_stamps(since=since,to=to,within=within)
|
|
|
|
d_start = _get_start_of(firstincluded,step)
|
|
d_end = _get_start_of(lastincluded,step)
|
|
d_start = _get_next(d_start,step,stepn) # first range should end right after the first active scrobbling week / month / whatever relevant step
|
|
d_start = _get_next(d_start,step,stepn * trail * -1) # go one range back to begin
|
|
|
|
|
|
i = 0
|
|
d_current = d_start
|
|
while not _is_past(d_current,d_end) and (max_ is None or i < max_):
|
|
d_current_end = _get_end(d_current,step,stepn * trail)
|
|
yield (d_current,d_current_end)
|
|
d_current = _get_next(d_current,step,stepn)
|
|
i += 1
|
|
|
|
|
|
|
|
def _get_start_of(timestamp,unit):
|
|
date = datetime.datetime.utcfromtimestamp(timestamp)
|
|
if unit == "year":
|
|
#return [date.year,1,1]
|
|
return [date.year]
|
|
elif unit == "month":
|
|
#return [date.year,date.month,1]
|
|
return [date.year,date.month]
|
|
elif unit == "day":
|
|
return [date.year,date.month,date.day]
|
|
elif unit == "week":
|
|
change = (date.weekday() + 1) % 7
|
|
d = datetime.timedelta(days=change)
|
|
newdate = date - d
|
|
return [newdate.year,newdate.month,newdate.day]
|
|
|
|
def _get_next(time,unit="auto",step=1):
|
|
result = time[:]
|
|
if unit == "auto":
|
|
# see how long the list is, increment by the last specified unit
|
|
unit = [None,"year","month","day"][len(time)]
|
|
#while len(time) < 3:
|
|
# time.append(1)
|
|
|
|
if unit == "year":
|
|
#return [time[0] + step,time[1],time[2]]
|
|
result[0] += step
|
|
return result
|
|
elif unit == "month":
|
|
#result = [time[0],time[1] + step,time[2]]
|
|
result[1] += step
|
|
while result[1] > 12:
|
|
result[1] -= 12
|
|
result[0] += 1
|
|
while result[1] < 1:
|
|
result[1] += 12
|
|
result[0] -= 1
|
|
return result
|
|
elif unit == "day":
|
|
dt = datetime.datetime(time[0],time[1],time[2])
|
|
d = datetime.timedelta(days=step)
|
|
newdate = dt + d
|
|
return [newdate.year,newdate.month,newdate.day]
|
|
#eugh
|
|
elif unit == "week":
|
|
return _get_next(time,"day",step * 7)
|
|
|
|
# like _get_next(), but gets the last INCLUDED day / month whatever
|
|
def _get_end(time,unit="auto",step=1):
|
|
if step == 1:
|
|
if unit == "auto": return time[:]
|
|
if unit == "year" and len(time) == 1: return time[:]
|
|
if unit == "month" and len(time) == 2: return time[:]
|
|
if unit == "day" and len(time) == 3: return time[:]
|
|
exc = _get_next(time,unit,step)
|
|
inc = _get_next(exc,"auto",-1)
|
|
return inc
|
|
|
|
|
|
|
|
def _is_past(date,limit):
|
|
date_, limit_ = date[:], limit[:]
|
|
while len(date_) != 3: date_.append(1)
|
|
while len(limit_) != 3: limit_.append(1)
|
|
if not date_[0] == limit_[0]:
|
|
return date_[0] > limit_[0]
|
|
if not date_[1] == limit_[1]:
|
|
return date_[1] > limit_[1]
|
|
return (date_[2] > limit_[2])
|
|
|