Skip to content

Commit

Permalink
[#34] New Feature: programming Timers with full date, not only day of…
Browse files Browse the repository at this point in the history
… the week

- Refactor scheduler / timer classes (probably only apply method)
- Changed concurrency logics
- Written and updated unit tests
  • Loading branch information
Heckie75 committed Aug 18, 2024
1 parent 7315ff4 commit 68eabf0
Show file tree
Hide file tree
Showing 29 changed files with 6,089 additions and 304 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ The result is a powerful timer addon.
<img src="script.timers/resources/assets/screenshot_09.png?raw=true">

## Changelog
v4.0.0 (2024-09-xx)
- New Feature: programming timers with full date (not only day within upcoming 7 days, feature request #34)
- Limitations: Forecast of concurring timers is still only one week (depends of type of scheduling)

v3.9.4 (2024-08-18)
- Bugfix: [Kodi v21] Addon can't play PVR items anymore, issue #42

Expand Down
Binary file added script.timers.4.0.0-pre-202408160015.zip
Binary file not shown.
7 changes: 4 additions & 3 deletions script.timers/addon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.timers" name="Timers" version="4.0.0" provider-name="Heckie">
<addon id="script.timers" name="Timers" version="4.0.0-pre-202408160015" provider-name="Heckie">
<requires>
<import addon="xbmc.python" version="3.0.0" />
</requires>
Expand Down Expand Up @@ -66,8 +66,9 @@
<website>https://github.com/Heckie75/kodi-addon-timers</website>
<source>https://github.com/Heckie75/kodi-addon-timers</source>
<news>
v4.0.0 (2024-08-xx)
- New Feature: programming timers with full date, not only day of the week (feature request #34)
v4.0.0 (2024-09-xx)
- New Feature: programming timers with full date (not only day within upcoming 7 days, feature request #34)
- Limitations: Forecast of concurring timers is still only one week (depends of type of scheduling)

v3.9.4 (2024-08-18)
- Bugfix: [Kodi v21] Addon can't play PVR items anymore, issue #42
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,6 @@ msgctxt "#32044"
msgid "Date"
msgstr "Datum"

msgctxt "#32045"
msgid "on"
msgstr "am"

msgctxt "#32050"
msgid "This timer interrupts other timers"
msgstr "Dieser Timer unterbricht andere Timer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,6 @@ msgctxt "#32044"
msgid "Date"
msgstr ""

msgctxt "#32045"
msgid "on"
msgstr ""

msgctxt "#32050"
msgid "This timer interrupts other timers"
msgstr ""
Expand Down
10 changes: 4 additions & 6 deletions script.timers/resources/lib/contextmenu/abstract_set_timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,12 @@ def __init__(self, label: str, path: str, timerid=-1) -> None:
else:
timer.days = days

if TIMER_BY_DATE in days:
timer.days = [TIMER_BY_DATE]
if timer.is_timer_by_date():
date = self.ask_date(timer.label, path, is_epg, timer)
if date == None:
return
else:
timer.date = date
timer.set_timer_by_date(date)

starttime = self.ask_starttime(timer.label, path, is_epg, timer)
if starttime == None:
Expand Down Expand Up @@ -113,7 +112,7 @@ def __init__(self, label: str, path: str, timerid=-1) -> None:

timer.init()
overlappings = determine_overlappings(
timer, self.storage.load_timers_from_storage(), ignore_extra_prio=True)
timer, self.storage.load_timers_from_storage(), ignore_extra_prio=True, to_display=True, base=datetime.now())
if overlappings:
answer = self.handle_overlapping_timers(
timer, overlapping_timers=overlappings)
Expand Down Expand Up @@ -236,8 +235,7 @@ def _get_timer_preselection(self, timerid: int, label: str, path: str) -> 'tuple
startDate = datetime_utils.parse_xbmc_shortdate(
xbmc.getInfoLabel("ListItem.Date").split(" ")[0])

timer.days = [TIMER_BY_DATE]
timer.date = startDate.strftime("%Y-%m-%d")
timer.set_timer_by_date(date=startDate.strftime("%Y-%m-%d"))
timer.start = xbmc.getInfoLabel("ListItem.StartTime")
duration = xbmc.getInfoLabel("ListItem.Duration")
if len(duration) == 5:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def perform_ahead(self, timer: Timer) -> bool:
for i, t in enumerate(timers):
if (found == -1
and timer.days == t.days
and timer.date == t.date
and timer.start == t.start
and timer.path == t.path):

Expand Down
12 changes: 6 additions & 6 deletions script.timers/resources/lib/player/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self) -> None:
self._resume_status: 'dict[PlayerStatus]' = dict()

self._running_stop_at_end_timer: 'tuple[Timer, bool]' = (None, False)

self.__is_unit_test__: bool = False

def playTimer(self, timer: Timer, dtd: datetime_utils.DateTimeDelta) -> None:
Expand Down Expand Up @@ -63,8 +63,8 @@ def _get_delay_for_seektime(_timer: Timer, _dtd: datetime_utils.DateTimeDelta) -
seektime = None
if self._seek_delayed_timer and _timer.is_play_at_start_timer():
if timer.current_period:
seektime = datetime_utils.abs_time_diff(
_dtd.td, timer.current_period.start)
seektime = datetime_utils.datetime_diff(
timer.current_period.start, _dtd.dt)
seektime = None if seektime * 1000 <= self._RESPITE else seektime

return seektime
Expand All @@ -91,8 +91,8 @@ def _get_delay_for_seektime(_timer: Timer, _dtd: datetime_utils.DateTimeDelta) -
len(files)] if seektime else None

if timer.is_stop_at_end_timer():
amountOfSlides = datetime_utils.abs_time_diff(
timer.current_period.end, dtd.td) // stayTime + 1
amountOfSlides = datetime_utils.datetime_diff(
dtd.dt, timer.current_period.end) // stayTime + 1
else:
amountOfSlides = 0

Expand Down Expand Up @@ -128,7 +128,7 @@ def _playAV(self, playlist: PlayList, startpos=0, seektime=None, repeat=player_u

if self.__is_unit_test__:
self.setRepeat(repeat)

self.play(playlist.directUrl or playlist, startpos=startpos)
self.setRepeat(repeat)
self.setShuffled(shuffled)
Expand Down
76 changes: 47 additions & 29 deletions script.timers/resources/lib/timer/concurrency.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

import xbmcaddon
import xbmcgui
from resources.lib.player.player_utils import get_types_replaced_by_type
Expand Down Expand Up @@ -33,38 +35,45 @@ def get_next_higher_prio(timers: 'list[Timer]') -> int:
return _max + 1 if _max < HIGH_PRIO_MARK - 1 else _max


def determine_overlappings(timer: Timer, timers: 'list[Timer]', ignore_extra_prio=False) -> 'list[Timer]':
def determine_overlappings(timer: Timer, timers: 'list[Timer]', base: datetime, ignore_extra_prio=False, to_display=False) -> 'list[Timer]':

def _is_exact_match(period1: Period, period2: Period, base: datetime) -> bool:

if type(period1.start) == type(period2.start):
return period1.start == period2.start and period1.end == period2.end

return _is_exact_match(Period.to_datetime_period(period1, base), Period.to_datetime_period(period2, base), base)

def _disturbs(types: 'list[str]', type2: str, media_action1: int, media_action2: int, period1: Period, period2: Period) -> bool:
def _disturbs(types: 'list[str]', type2: str, media_action1: int, media_action2: int, period1: Period, period2: Period, base: datetime) -> bool:

if media_action1 == MEDIA_ACTION_START_STOP:
td_play_media1 = period1.start
td_stop_media1 = period1.end
play_media1 = period1.start
stop_media1 = period1.end
replace = type2 in types

elif media_action1 == MEDIA_ACTION_START:
td_play_media1 = period1.start
td_stop_media1 = None
play_media1 = period1.start
stop_media1 = None
replace = type2 in types

elif media_action1 == MEDIA_ACTION_START_AT_END:
td_play_media1 = period1.end
td_stop_media1 = None
play_media1 = period1.end
stop_media1 = None
replace = type2 in types

elif media_action1 == MEDIA_ACTION_STOP_START:
td_play_media1 = period1.end
td_stop_media1 = period1.start
play_media1 = period1.end
stop_media1 = period1.start
replace = type2 in types

elif media_action1 == MEDIA_ACTION_STOP:
td_play_media1 = None
td_stop_media1 = period1.start
play_media1 = None
stop_media1 = period1.start
replace = True

elif media_action1 == MEDIA_ACTION_STOP_AT_END:
td_play_media1 = None
td_stop_media1 = period1.end
play_media1 = None
stop_media1 = period1.end
replace = True

else:
Expand All @@ -73,27 +82,27 @@ def _disturbs(types: 'list[str]', type2: str, media_action1: int, media_action2:
if not replace:
return False

if period1.start == period2.start and period1.end == period2.end:
if _is_exact_match(period1, period2, base):
return True

elif media_action2 == MEDIA_ACTION_START_STOP:

if td_play_media1:
s, e, hit = period2.hit(td_play_media1)
if play_media1:
s, e, hit = period2.hit(play_media1, base)
if s and e and hit:
return True

if td_stop_media1:
s, e, hit = period2.hit(td_stop_media1)
if stop_media1:
s, e, hit = period2.hit(stop_media1, base)
if s and e and hit:
return True

return False

elif media_action2 == MEDIA_ACTION_STOP_START:

if td_play_media1:
s, e, hit = period2.hit(td_play_media1)
if play_media1:
s, e, hit = period2.hit(play_media1, base)
return s and e and hit

return False
Expand All @@ -113,18 +122,26 @@ def _disturbs(types: 'list[str]', type2: str, media_action1: int, media_action2:

for n in timer.periods:

if _disturbs(timer_replace_types, t.media_type, timer.media_action, t.media_action, n, p) or _disturbs(t_replace_types, timer.media_type, t.media_action, timer.media_action, p, n):
if _disturbs(timer_replace_types, t.media_type, timer.media_action, t.media_action, n, p, base) or _disturbs(t_replace_types, timer.media_type, t.media_action, timer.media_action, p, n, base):
overlapping_periods.append(p)

if overlapping_periods:
days = [
datetime_utils.WEEKLY] if datetime_utils.WEEKLY in t.days else list()
days.extend([p.start.days for p in overlapping_periods])
t.days = days
t.periods = overlapping_periods

if t.is_timer_by_date():
days = [p.start.weekday()]

else:
days = [
datetime_utils.WEEKLY] if datetime_utils.WEEKLY in t.days else list()
days.extend([p.start.days for p in overlapping_periods])

if to_display:
t.days = days
t.periods = overlapping_periods

overlapping_timers.append(t)

overlapping_timers.sort(key=lambda t: (t.days, t.start,
overlapping_timers.sort(key=lambda t: (t.days, t.date, t.start,
t.media_action, t.system_action))

return overlapping_timers
Expand All @@ -133,9 +150,10 @@ def _disturbs(types: 'list[str]', type2: str, media_action1: int, media_action2:
def ask_overlapping_timers(timer: Timer, overlapping_timers: 'list[Timer]') -> int:

addon = xbmcaddon.Addon()
now = datetime.now()

earlier_timers = [
t for t in overlapping_timers if t.periods[0].start < timer.periods[0].start]
t for t in overlapping_timers if datetime_utils.time_diff(t.periods[0].start, timer.periods[0].start, now) > 0]

lines = list()
for t in overlapping_timers:
Expand Down
77 changes: 67 additions & 10 deletions script.timers/resources/lib/timer/period.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
from datetime import timedelta
from datetime import datetime, timedelta

from resources.lib.utils.datetime_utils import apply_for_now


class Period:

def __init__(self, start: timedelta, end: timedelta) -> None:
def __init__(self, start: timedelta | datetime, end: timedelta | datetime) -> None:

if type(start) != type(end):
raise Exception(
"types of <start> and <end> must be identically!!!")

self.start: timedelta = start
self.end: timedelta = end
self.start: timedelta | datetime = start
self.end: timedelta | datetime = end

def _compare(self, period_start: timedelta, period_end: timedelta) -> 'tuple[timedelta,timedelta,timedelta]':
def _compareByWeekdays(self, period_start: timedelta, period_end: timedelta) -> 'tuple[timedelta,timedelta,timedelta]':

self_start = self.start
self_end = self.end
Expand All @@ -31,14 +37,65 @@ def _compare(self, period_start: timedelta, period_end: timedelta) -> 'tuple[tim

return self_start - period_start, self_end - period_end, min_end - max_start if max_start <= min_end else None

def _compareByDates(self, period_start: datetime, period_end: datetime) -> 'tuple[timedelta,timedelta,timedelta]':

max_start = max(self.start, period_start)
min_end = min(self.end, period_end)

return self.start - period_start, self.end - period_end, min_end - max_start if max_start <= min_end else None

def compare(self, period: 'Period') -> 'tuple[timedelta,timedelta,timedelta]':

return self._compare(period.start, period.end)
if type(self.start) != type(period.start):
raise Exception(
f"can't compare {str(self)} with {str(period)} caused by different types")

if type(self.start) == timedelta:
return self._compareByWeekdays(period.start, period.end)
else:
return self._compareByDates(period.start, period.end)

def hit(self, timestamp: timedelta | datetime, base: datetime = None) -> 'tuple[timedelta,timedelta,bool]':

def hit(self, timestamp: timedelta) -> 'tuple[timedelta,timedelta, bool]':
if type(self.start) == timedelta and type(timestamp) == timedelta:
s, e, l = self._compareByWeekdays(timestamp, timestamp)
return s, e, l is not None
elif type(self.start) == datetime and type(timestamp) == datetime:
s, e, l = self._compareByDates(timestamp, timestamp)
return s, e, l is not None

s, e, l = self._compare(timestamp, timestamp)
return s, e, l is not None
if type(timestamp) == datetime:
period = Period.to_datetime_period(
period=self, base=base or timestamp)
s, e, l = period._compareByDates(timestamp, timestamp)
return s, e, l is not None

elif type(self.start) == datetime:
if not base:
raise("This type of comparision requires a base-datetime")

timestamp = apply_for_now(base, timestamp, force_future=True)
s, e, l = self._compareByDates(timestamp, timestamp)
return s, e, l is not None

def __str__(self) -> str:
return "Period[start=%s, end=%s]" % (self.start, self.end)

start = self.start if type(
self.start) == timedelta else self.start.strftime("%Y-%m-%d %H:%M:%S")
end = self.end if type(self.end) == timedelta else self.end.strftime(
"%Y-%m-%d %H:%M:%S")
return f"Period[start={start}, end={end}]"

@staticmethod
def to_datetime_period(period: 'Period', base: datetime) -> 'Period':

if type(period.start) == datetime:
return period

start = apply_for_now(base, period.start)
end = apply_for_now(base, period.end)
if start < end < base:
start += timedelta(days=7)
end += timedelta(days=7)

return Period(start, end)
Loading

0 comments on commit 68eabf0

Please sign in to comment.