Skip to content

Commit

Permalink
Improve resumepoints
Browse files Browse the repository at this point in the history
  • Loading branch information
mediaminister committed Dec 19, 2019
1 parent 98cd38f commit b729ab6
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 172 deletions.
58 changes: 7 additions & 51 deletions resources/lib/playerinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from xbmc import getInfoLabel, Player, PlayList

from apihelper import ApiHelper
from data import SECONDS_MARGIN, CHANNELS
from data import CHANNELS
from favorites import Favorites
from kodiutils import addon_id, container_reload, get_advanced_setting_int, get_setting, has_addon, log, notify
from kodiutils import addon_id, get_setting, has_addon, log, notify
from resumepoints import ResumePoints
from utils import assetpath_to_id, play_url_to_id, to_unicode, url_to_episode

Expand Down Expand Up @@ -84,7 +84,6 @@ def onAVStarted(self): # pylint: disable=invalid-name
self.quit.clear()
self.update_position()
self.update_total()
self.push_position(position=self.last_pos, total=self.total) # Update position at start so resumepoint gets deleted
self.push_upnext()

# StreamPosition thread keeps running when watching multiple episode with "Up Next"
Expand Down Expand Up @@ -123,8 +122,6 @@ def onPlayBackResumed(self): # pylint: disable=invalid-name
return
suffix = 'after pausing' if self.paused else 'after playlist change'
log(3, '[PlayerInfo %d] Event onPlayBackResumed %s' % (self.thread_id, suffix))
if not self.paused:
self.push_position(position=self.last_pos, total=self.total)
self.paused = False

def onPlayBackEnded(self): # pylint: disable=invalid-name
Expand Down Expand Up @@ -217,55 +214,14 @@ def push_position(self, position=0, total=100):
return

# Push resumepoint to VRT NU
self.resumepoints.update(
notify(sender='%s.SIGNAL' % addon_id(), message='resumepoint_busy', data=dict())
updated = self.resumepoints.update(
asset_id=self.asset_id,
title=self.title,
url=self.url,
position=position,
total=total,
whatson_id=self.whatson_id,
asynchronous=True
whatson_id=self.whatson_id
)

# Kodi internal watch status is only updated when the play action is initiated from the GUI, so this doesn't work after quitting "Up Next"
if (not self.path.startswith('plugin://plugin.video.vrt.nu/play/upnext')
and not self.overrule_kodi_watchstatus(position, total)):
return

# Do not reload container when playing or not stopped
if self.isPlaying() or not self.quit.is_set():
return

container_reload()

@staticmethod
def overrule_kodi_watchstatus(position, total):
"""Determine if we need to overrule the Kodi watch status"""

# Kodi uses different resumepoint margins than VRT NU, to obey to VRT NU resumepoint margins
# we sometimes need to overrule Kodi watch status.
# Use setting from advancedsettings.xml or default value
# https://github.com/xbmc/xbmc/blob/master/xbmc/settings/AdvancedSettings.cpp
# https://kodi.wiki/view/HOW-TO:Modify_automatic_watch_and_resume_points

ignoresecondsatstart = get_advanced_setting_int('video/ignoresecondsatstart', default=180)
ignorepercentatend = get_advanced_setting_int('video/ignorepercentatend', default=8)

# Convert percentage to seconds
ignoresecondsatend = round(total * (100 - ignorepercentatend) / 100.0)

if position <= max(SECONDS_MARGIN, ignoresecondsatstart):
# Check start margins
if SECONDS_MARGIN <= position <= ignoresecondsatstart:
return True
if ignoresecondsatstart <= position <= SECONDS_MARGIN:
return True

if position >= min(total - SECONDS_MARGIN, ignoresecondsatend):
# Check end margins
if total - SECONDS_MARGIN <= position <= ignoresecondsatend:
return True
if ignoresecondsatend <= position <= total - SECONDS_MARGIN:
return True

return False
if updated:
notify(sender='%s.SIGNAL' % addon_id(), message='resumepoint_ready', data=dict())
186 changes: 79 additions & 107 deletions resources/lib/resumepoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(self):
"""Initialize resumepoints, relies on XBMC vfs and a special VRT token"""
self._data = dict() # Our internal representation
install_opener(build_opener(ProxyHandler(get_proxies())))
self.refresh(ttl=0)

@staticmethod
def is_activated():
Expand Down Expand Up @@ -56,81 +57,84 @@ def refresh(self, ttl=None):
if resumepoints_json is not None:
self._data = resumepoints_json

def update(self, asset_id, title, url, watch_later=None, position=None, total=None, whatson_id=None, asynchronous=False):
def update(self, asset_id, title, url, watch_later=None, position=None, total=None, whatson_id=None):
"""Set program resumepoint or watchLater status and update local copy"""
log(3, "[Resumepoints] Update resumepoint '{asset_id}' {position}/{total}", asset_id=asset_id, position=position, total=total)
# The video has no assetPath, so we cannot update resumepoints
if asset_id is None:
return True

# Survive any recent updates
self.refresh(ttl=5)
menu_caches = []

# Disable watch_later when completely watched
if position is not None and total is not None and position >= total - SECONDS_MARGIN:
watch_later = False
# Update
if self.still_watching(position, total) or watch_later is True:

if watch_later is not None and position is None and total is None and watch_later is self.is_watchlater(asset_id):
# watchLater status is not changed, nothing to do
return True
log(3, "[Resumepoints] Update resumepoint '{asset_id}' {position}/{total}", asset_id=asset_id, position=position, total=total)

if asset_id is None:
return True

if watch_later is not None and position is None and total is None and watch_later is self.is_watchlater(asset_id):
# watchLater status is not changed, nothing to do
return True

if watch_later is None and position == self.get_position(asset_id) and total == self.get_total(asset_id):
# Resumepoint is not changed, nothing to do
return True

menu_caches.append('continue-*.json')

if asset_id in self._data:
# Update existing resumepoint values
payload = self._data[asset_id]['value']
payload['url'] = url
else:
# Create new resumepoint values
payload = dict(position=0, total=100, url=url)

if watch_later is not None:
payload['watchLater'] = watch_later
menu_caches.append('watchlater-*.json')

if position is not None:
payload['position'] = position

if total is not None:
payload['total'] = total

if whatson_id is not None:
payload['whatsonId'] = whatson_id

# First update resumepoints to a fast local cache because online resumpoints take a longer time to take effect
self.update_local(asset_id, dict(value=payload), menu_caches)

# Update online
self.update_online(asset_id, title, url, payload)

if watch_later is None and position == self.get_position(asset_id) and total == self.get_total(asset_id):
# Resumepoint is not changed, nothing to do
return True

# Preserve watchLater status if not set
if watch_later is None:
watch_later = self.is_watchlater(asset_id)
# Delete

# Check if there is still a need to keep this entry
if not watch_later:
if position is None and total is None:
return self.delete(asset_id, watch_later, asynchronous=asynchronous)
if not self.still_watching(position, total):
return self.delete(asset_id, watch_later, asynchronous=asynchronous)
log(3, "[Resumepoints] Delete resumepoint '{asset_id}' {position}/{total}", asset_id=asset_id, position=position, total=total)

from utils import reformat_url
url = reformat_url(url, 'short')

if asset_id in self._data:
# Update existing resumepoint values
payload = self._data[asset_id]['value']
payload['url'] = url
else:
# Create new resumepoint values
payload = dict(position=0, total=100, url=url)

if position is not None:
payload['position'] = position

if total is not None:
payload['total'] = total

if whatson_id is not None:
payload['whatsonId'] = whatson_id

removes = []
if position is not None or total is not None:
removes.append('continue-*.json')

if watch_later is not None:
# Add watchLater status to payload
payload['watchLater'] = watch_later
removes.append('watchlater-*.json')

# First update resumepoints to a fast local cache because online resumpoints take a longer time to take effect
self.update_local(asset_id, dict(value=payload))
invalidate_caches(*removes)

# Update online resumepoints
if asynchronous:
from threading import Thread
Thread(target=self.update_online, name='ResumePointsUpdate', args=(asset_id, title, url, payload)).start()
# Do nothing if there is no resumepoint for this asset_id
if not self._data.get(asset_id):
log(3, "[Resumepoints] '{asset_id}' not present, nothing to delete", asset_id=asset_id)
return True
return self.update_online(asset_id, title, url, payload)

# Delete online
self.delete_online(asset_id)

# Add menu caches
menu_caches.append('continue-*.json')

if self.is_watchlater(asset_id):
menu_caches.append('watchlater-*.json')

# Delete local representation and cache
self.delete_local(asset_id, menu_caches)

return True

def update_online(self, asset_id, title, url, payload):
"""Update resumepoint online"""
log(3, "[Resumepoints] Update online")
from json import dumps
try:
get_url_json('https://video-user-data.vrt.be/resume_points/%s' % asset_id,
Expand All @@ -141,45 +145,30 @@ def update_online(self, asset_id, title, url, payload):
return False
return True

def update_local(self, asset_id, resumepoint_json):
def update_local(self, asset_id, resumepoint_json, menu_caches=None):
"""Update resumepoint locally and update cache"""
log(3, "[Resumepoints] Update local")
self._data.update({asset_id: resumepoint_json})
from json import dumps
update_cache('resume_points.json', dumps(self._data))
if menu_caches:
invalidate_caches(*menu_caches)

def delete_local(self, asset_id):
def delete_local(self, asset_id, menu_caches=None):
"""Delete resumepoint locally and update cache"""
log(3, "[Resumepoints] Delete local")
try:
del self._data[asset_id]
from json import dumps
update_cache('resume_points.json', dumps(self._data))
if menu_caches:
invalidate_caches(*menu_caches)
except KeyError:
pass

def delete(self, asset_id, watch_later, asynchronous=False):
"""Remove an entry from resumepoints"""
# Do nothing if there is no resumepoint for this asset_id
if not self._data.get(asset_id):
log(3, "[Resumepoints] '{asset_id}' not present, nothing to delete", asset_id=asset_id)
return True

log(3, "[Resumepoints] Remove '{asset_id}' from resumepoints", asset_id=asset_id)
# Delete local representation and cache
self.delete_local(asset_id)

if watch_later is False:
self.refresh_watchlater()

self.refresh_continuewatching()

if asynchronous:
from threading import Thread
Thread(target=self.delete_online, name='ResumePointsDelete', args=(asset_id,)).start()
return True
return self.delete_online(asset_id)

def delete_online(self, asset_id):
"""Delete resumepoint online"""
log(3, "[Resumepoints] Delete online")
req = Request('https://video-user-data.vrt.be/resume_points/%s' % asset_id, headers=self.resumepoint_headers())
req.get_method = lambda: 'DELETE'
try:
Expand All @@ -190,18 +179,6 @@ def delete_online(self, asset_id):
return False
return True

@staticmethod
def refresh_watchlater():
"""Refresh Watch Later menu"""
invalidate_caches('watchlater-*.json')
container_refresh()

@staticmethod
def refresh_continuewatching():
"""Refresh Continue Watching menu"""
invalidate_caches('continue-*.json')
container_refresh()

def is_watchlater(self, asset_id):
"""Is a program set to watch later ?"""
return self._data.get(asset_id, {}).get('value', {}).get('watchLater') is True
Expand Down Expand Up @@ -243,16 +220,11 @@ def watchlater_urls(self):
def resumepoints_urls(self):
"""Return all urls that have not been finished watching"""
return [self.get_url(asset_id) for asset_id in self._data
if SECONDS_MARGIN < self.get_position(asset_id) < (self.get_total(asset_id) - SECONDS_MARGIN)]
if self.still_watching(self.get_position(asset_id), self.get_total(asset_id))]

@staticmethod
def still_watching(position, total):
"""Determine if the video is still being watched"""

if position <= SECONDS_MARGIN:
return False

if position >= total - SECONDS_MARGIN:
return False

return True
if None not in (position, total) and SECONDS_MARGIN < position < (total - SECONDS_MARGIN):
return True
return False
11 changes: 10 additions & 1 deletion resources/lib/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from xbmc import Monitor
from apihelper import ApiHelper
from favorites import Favorites
from kodiutils import container_refresh, invalidate_caches, log
from kodiutils import container_refresh, invalidate_caches, log, set_property
from playerinfo import PlayerInfo
from resumepoints import ResumePoints
from tokenresolver import TokenResolver
Expand Down Expand Up @@ -60,6 +60,15 @@ def onNotification(self, sender, method, data): # pylint: disable=invalid-name
log(2, '[Up Next notification] sender={sender}, method={method}, data={data}', sender=sender, method=method, data=to_unicode(data))
self._playerinfo.add_upnext(data.get('video_id'))

# Set window property while resumepoint is busy
if sender.startswith('plugin.video.vrt') and method.endswith('resumepoint_busy'):
log(2, 'Resumepoint busy')
set_property('resumepoint', 'busy')

if sender.startswith('plugin.video.vrt') and method.endswith('resumepoint_ready'):
log(2, 'Resumepoint ready')
set_property('resumepoint', None)

def onSettingsChanged(self): # pylint: disable=invalid-name
"""Handler for changes to settings"""

Expand Down
21 changes: 18 additions & 3 deletions resources/lib/vrtplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
from apihelper import ApiHelper
from favorites import Favorites
from helperobjects import TitleItem
from kodiutils import (delete_cached_thumbnail, end_of_directory, get_addon_info, get_setting,
has_credentials, localize, log_error, ok_dialog, play, set_setting,
show_listing, ttl, url_for)
from kodiutils import (delete_cached_thumbnail, end_of_directory, get_addon_info, get_property, get_setting, has_credentials,
localize, log_error, ok_dialog, play, set_setting, show_listing, ttl, url_for)
from resumepoints import ResumePoints
from utils import find_entry, realpage

Expand All @@ -21,6 +20,22 @@ def __init__(self):
self._favorites = Favorites()
self._resumepoints = ResumePoints()
self._apihelper = ApiHelper(self._favorites, self._resumepoints)
self.wait_for_resumepoint()

@staticmethod
def wait_for_resumepoint():
"""Wait until resumepoint has been updated"""
busy = get_property('resumepoint')
if busy:
import time
from xbmc import sleep
timeout = time.time() + 5
while busy:
# Always exit after 5 seconds in case something goes wrong
if time.time() > timeout:
break
sleep(50)
busy = get_property('resumepoint')

def show_main_menu(self):
"""The VRT NU add-on main menu"""
Expand Down
Loading

0 comments on commit b729ab6

Please sign in to comment.