421 lines
17 KiB
Python
421 lines
17 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
"""
|
||
|
|
||
|
Copyright (C) 2018-2018 plugin.video.youtube
|
||
|
|
||
|
SPDX-License-Identifier: GPL-2.0-only
|
||
|
See LICENSES/GPL-2.0-only for more information.
|
||
|
"""
|
||
|
|
||
|
import json
|
||
|
import re
|
||
|
import threading
|
||
|
|
||
|
import xbmc
|
||
|
|
||
|
|
||
|
class PlaybackMonitorThread(threading.Thread):
|
||
|
def __init__(self, provider, context, playback_json):
|
||
|
super(PlaybackMonitorThread, self).__init__()
|
||
|
|
||
|
self._stopped = threading.Event()
|
||
|
self._ended = threading.Event()
|
||
|
|
||
|
self.context = context
|
||
|
self.provider = provider
|
||
|
self.ui = self.context.get_ui()
|
||
|
|
||
|
self.player = xbmc.Player()
|
||
|
|
||
|
self.playback_json = playback_json
|
||
|
self.video_id = self.playback_json.get('video_id')
|
||
|
self.channel_id = self.playback_json.get('channel_id')
|
||
|
self.video_status = self.playback_json.get('video_status')
|
||
|
|
||
|
self.total_time = 0.0
|
||
|
self.current_time = 0.0
|
||
|
self.segment_start = 0.0
|
||
|
self.percent_complete = 0
|
||
|
|
||
|
self.daemon = True
|
||
|
self.start()
|
||
|
|
||
|
def update_times(self, total_time, current_time, segment_start, percent_complete):
|
||
|
self.total_time = total_time
|
||
|
self.current_time = current_time
|
||
|
self.segment_start = segment_start
|
||
|
self.percent_complete = percent_complete
|
||
|
|
||
|
def abort_now(self):
|
||
|
return not self.player.isPlaying() or self.context.abort_requested() or self.stopped()
|
||
|
|
||
|
def run(self):
|
||
|
playing_file = self.playback_json.get('playing_file')
|
||
|
play_count = self.playback_json.get('play_count', 0)
|
||
|
use_history = self.playback_json.get('use_history', False)
|
||
|
playback_history = self.playback_json.get('playback_history', False)
|
||
|
playback_stats = self.playback_json.get('playback_stats')
|
||
|
refresh_only = self.playback_json.get('refresh_only', False)
|
||
|
try:
|
||
|
seek_time = float(self.playback_json.get('seek_time'))
|
||
|
except (ValueError, TypeError):
|
||
|
seek_time = None
|
||
|
|
||
|
player = self.player
|
||
|
|
||
|
self.context.log_debug('PlaybackMonitorThread[%s]: Starting...' % self.video_id)
|
||
|
access_manager = self.context.get_access_manager()
|
||
|
|
||
|
settings = self.context.get_settings()
|
||
|
|
||
|
if playback_stats is None:
|
||
|
playback_stats = {}
|
||
|
|
||
|
play_count = str(play_count)
|
||
|
|
||
|
played_time = -1.0
|
||
|
|
||
|
state = 'playing'
|
||
|
last_state = 'playing'
|
||
|
|
||
|
np_wait_time = 0.2
|
||
|
np_waited = 0.0
|
||
|
p_wait_time = 0.5
|
||
|
p_waited = 0.0
|
||
|
|
||
|
report_interval = 10.0
|
||
|
first_report = True
|
||
|
|
||
|
report_url = playback_stats.get('playback_url', '')
|
||
|
|
||
|
while not player.isPlaying() and not self.context.abort_requested():
|
||
|
self.context.log_debug('Waiting for playback to start')
|
||
|
|
||
|
xbmc.sleep(int(np_wait_time * 1000))
|
||
|
if np_waited >= 5:
|
||
|
self.end()
|
||
|
return
|
||
|
|
||
|
np_waited += np_wait_time
|
||
|
|
||
|
client = self.provider.get_client(self.context)
|
||
|
is_logged_in = self.provider.is_logged_in()
|
||
|
|
||
|
if is_logged_in and report_url and use_history:
|
||
|
client.update_watch_history(self.video_id, report_url)
|
||
|
self.context.log_debug('Playback start reported: |%s|' % self.video_id)
|
||
|
|
||
|
report_url = playback_stats.get('watchtime_url', '')
|
||
|
|
||
|
plugin_play_path = 'plugin://plugin.video.youtube/play/'
|
||
|
video_id_param = 'video_id=%s' % self.video_id
|
||
|
|
||
|
notification_sent = False
|
||
|
|
||
|
while player.isPlaying() and not self.context.abort_requested() and not self.stopped():
|
||
|
if not notification_sent:
|
||
|
notification_sent = True
|
||
|
self.context.send_notification('PlaybackStarted', {
|
||
|
'video_id': self.video_id,
|
||
|
'channel_id': self.channel_id,
|
||
|
'status': self.video_status,
|
||
|
})
|
||
|
|
||
|
last_total_time = self.total_time
|
||
|
last_current_time = self.current_time
|
||
|
last_segment_start = self.segment_start
|
||
|
last_percent_complete = self.percent_complete
|
||
|
|
||
|
try:
|
||
|
current_file = player.getPlayingFile()
|
||
|
if (current_file != playing_file and
|
||
|
not (current_file.startswith(plugin_play_path) and
|
||
|
video_id_param in current_file)) or self.stopped():
|
||
|
self.stop()
|
||
|
break
|
||
|
except RuntimeError:
|
||
|
pass
|
||
|
|
||
|
if self.abort_now():
|
||
|
self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete)
|
||
|
break
|
||
|
|
||
|
try:
|
||
|
self.current_time = float(player.getTime())
|
||
|
self.total_time = float(player.getTotalTime())
|
||
|
except RuntimeError:
|
||
|
pass
|
||
|
|
||
|
if self.current_time < 0.0:
|
||
|
self.current_time = 0.0
|
||
|
|
||
|
if self.abort_now():
|
||
|
self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete)
|
||
|
break
|
||
|
|
||
|
try:
|
||
|
self.percent_complete = int(float(self.current_time) / float(self.total_time) * 100)
|
||
|
except ZeroDivisionError:
|
||
|
self.percent_complete = 0
|
||
|
|
||
|
if self.abort_now():
|
||
|
self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete)
|
||
|
break
|
||
|
|
||
|
if seek_time and seek_time != 0.0:
|
||
|
player.seekTime(seek_time)
|
||
|
try:
|
||
|
self.current_time = float(player.getTime())
|
||
|
except RuntimeError:
|
||
|
pass
|
||
|
if self.current_time >= seek_time:
|
||
|
seek_time = None
|
||
|
|
||
|
if self.abort_now():
|
||
|
self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete)
|
||
|
break
|
||
|
|
||
|
if p_waited >= report_interval:
|
||
|
if is_logged_in:
|
||
|
self.provider.reset_client() # refresh client, tokens may need refreshing
|
||
|
client = self.provider.get_client(self.context)
|
||
|
is_logged_in = self.provider.is_logged_in()
|
||
|
|
||
|
if self.current_time == played_time:
|
||
|
last_state = state
|
||
|
state = 'paused'
|
||
|
else:
|
||
|
last_state = state
|
||
|
state = 'playing'
|
||
|
|
||
|
played_time = self.current_time
|
||
|
|
||
|
if self.abort_now():
|
||
|
self.update_times(last_total_time, last_current_time, last_segment_start, last_percent_complete)
|
||
|
break
|
||
|
|
||
|
if is_logged_in and report_url and use_history:
|
||
|
if first_report or (p_waited >= report_interval):
|
||
|
if first_report:
|
||
|
first_report = False
|
||
|
self.segment_start = 0.0
|
||
|
self.current_time = 0.0
|
||
|
self.percent_complete = 0
|
||
|
|
||
|
p_waited = 0.0
|
||
|
|
||
|
if self.segment_start < 0.0:
|
||
|
self.segment_start = 0.0
|
||
|
|
||
|
if state == 'playing':
|
||
|
segment_end = self.current_time
|
||
|
else:
|
||
|
segment_end = self.segment_start
|
||
|
|
||
|
if segment_end > float(self.total_time):
|
||
|
segment_end = float(self.total_time)
|
||
|
|
||
|
if self.segment_start > segment_end:
|
||
|
segment_end = self.segment_start + 10.0
|
||
|
|
||
|
if state == 'playing' or last_state == 'playing': # only report state='paused' once
|
||
|
client.update_watch_history(self.video_id, report_url
|
||
|
.format(st=format(self.segment_start, '.3f'),
|
||
|
et=format(segment_end, '.3f'),
|
||
|
state=state))
|
||
|
self.context.log_debug(
|
||
|
'Playback reported [%s]: %s segment start, %s segment end @ %s%% state=%s' %
|
||
|
(self.video_id,
|
||
|
format(self.segment_start, '.3f'),
|
||
|
format(segment_end, '.3f'),
|
||
|
self.percent_complete, state))
|
||
|
|
||
|
self.segment_start = segment_end
|
||
|
|
||
|
if self.abort_now():
|
||
|
break
|
||
|
|
||
|
xbmc.sleep(int(p_wait_time * 1000))
|
||
|
|
||
|
p_waited += p_wait_time
|
||
|
|
||
|
if is_logged_in and report_url and use_history:
|
||
|
client.update_watch_history(self.video_id, report_url
|
||
|
.format(st=format(self.segment_start, '.3f'),
|
||
|
et=format(self.current_time, '.3f'),
|
||
|
state=state))
|
||
|
self.context.log_debug('Playback reported [%s]: %s segment start, %s segment end @ %s%% state=%s' %
|
||
|
(self.video_id,
|
||
|
format(self.segment_start, '.3f'),
|
||
|
format(self.current_time, '.3f'),
|
||
|
self.percent_complete, state))
|
||
|
|
||
|
self.context.send_notification('PlaybackStopped', {
|
||
|
'video_id': self.video_id,
|
||
|
'channel_id': self.channel_id,
|
||
|
'status': self.video_status,
|
||
|
})
|
||
|
self.context.log_debug('Playback stopped [%s]: %s secs of %s @ %s%%' %
|
||
|
(self.video_id, format(self.current_time, '.3f'),
|
||
|
format(self.total_time, '.3f'), self.percent_complete))
|
||
|
|
||
|
state = 'stopped'
|
||
|
if is_logged_in:
|
||
|
self.provider.reset_client() # refresh client, tokens may need refreshing
|
||
|
client = self.provider.get_client(self.context)
|
||
|
is_logged_in = self.provider.is_logged_in()
|
||
|
|
||
|
if self.percent_complete >= settings.get_play_count_min_percent():
|
||
|
play_count = '1'
|
||
|
self.current_time = 0.0
|
||
|
if is_logged_in and report_url and use_history:
|
||
|
client.update_watch_history(self.video_id, report_url
|
||
|
.format(st=format(self.total_time, '.3f'),
|
||
|
et=format(self.total_time, '.3f'),
|
||
|
state=state))
|
||
|
self.context.log_debug('Playback reported [%s] @ 100%% state=%s' % (self.video_id, state))
|
||
|
|
||
|
else:
|
||
|
if is_logged_in and report_url and use_history:
|
||
|
client.update_watch_history(self.video_id, report_url
|
||
|
.format(st=format(self.current_time, '.3f'),
|
||
|
et=format(self.current_time, '.3f'),
|
||
|
state=state))
|
||
|
self.context.log_debug('Playback reported [%s]: %s segment start, %s segment end @ %s%% state=%s' %
|
||
|
(self.video_id, format(self.current_time, '.3f'),
|
||
|
format(self.current_time, '.3f'),
|
||
|
self.percent_complete, state))
|
||
|
|
||
|
refresh_only = True
|
||
|
|
||
|
if playback_history:
|
||
|
self.context.get_playback_history().update(self.video_id, play_count, self.total_time,
|
||
|
self.current_time, self.percent_complete)
|
||
|
|
||
|
if not refresh_only:
|
||
|
if is_logged_in:
|
||
|
|
||
|
if settings.get_bool('youtube.playlist.watchlater.autoremove', True):
|
||
|
watch_later_id = access_manager.get_watch_later_id()
|
||
|
|
||
|
if watch_later_id and watch_later_id.strip().lower() != 'wl':
|
||
|
playlist_item_id = \
|
||
|
client.get_playlist_item_id_of_video_id(playlist_id=watch_later_id, video_id=self.video_id)
|
||
|
if playlist_item_id:
|
||
|
json_data = client.remove_video_from_playlist(watch_later_id, playlist_item_id)
|
||
|
_ = self.provider.v3_handle_error(self.provider, self.context, json_data)
|
||
|
|
||
|
history_playlist_id = access_manager.get_watch_history_id()
|
||
|
if history_playlist_id and history_playlist_id != 'HL':
|
||
|
json_data = client.add_video_to_playlist(history_playlist_id, self.video_id)
|
||
|
_ = self.provider.v3_handle_error(self.provider, self.context, json_data)
|
||
|
|
||
|
# rate video
|
||
|
if settings.get_bool('youtube.post.play.rate', False):
|
||
|
do_rating = True
|
||
|
if not settings.get_bool('youtube.post.play.rate.playlists', False):
|
||
|
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||
|
do_rating = int(playlist.size()) < 2
|
||
|
|
||
|
if do_rating:
|
||
|
json_data = client.get_video_rating(self.video_id)
|
||
|
success = self.provider.v3_handle_error(self.provider, self.context, json_data)
|
||
|
if success:
|
||
|
items = json_data.get('items', [{'rating': 'none'}])
|
||
|
rating = items[0].get('rating', 'none')
|
||
|
if rating == 'none':
|
||
|
rating_match = \
|
||
|
re.search('/(?P<video_id>[^/]+)/(?P<rating>[^/]+)', '/%s/%s/' %
|
||
|
(self.video_id, rating))
|
||
|
self.provider.yt_video.process('rate', self.provider, self.context, rating_match)
|
||
|
|
||
|
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||
|
do_refresh = (int(playlist.size()) < 2) or (playlist.getposition() == -1)
|
||
|
|
||
|
if do_refresh and settings.get_bool('youtube.post.play.refresh', False) and \
|
||
|
not xbmc.getInfoLabel('Container.FolderPath') \
|
||
|
.startswith(self.context.create_uri(['kodion', 'search', 'input'])):
|
||
|
# don't refresh search input it causes request for new input,
|
||
|
# (Container.Update in abstract_provider /kodion/search/input/
|
||
|
# would resolve this but doesn't work with Remotes(Yatse))
|
||
|
self.ui.refresh_container()
|
||
|
|
||
|
self.end()
|
||
|
|
||
|
def stop(self):
|
||
|
self.context.log_debug('PlaybackMonitorThread[%s]: Stop event set...' % self.video_id)
|
||
|
self._stopped.set()
|
||
|
|
||
|
def stopped(self):
|
||
|
return self._stopped.is_set()
|
||
|
|
||
|
def end(self):
|
||
|
self.context.log_debug('PlaybackMonitorThread[%s]: End event set...' % self.video_id)
|
||
|
self._ended.set()
|
||
|
|
||
|
def ended(self):
|
||
|
return self._ended.is_set()
|
||
|
|
||
|
|
||
|
class YouTubePlayer(xbmc.Player):
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
self.context = kwargs.get('context')
|
||
|
self.provider = kwargs.get('provider')
|
||
|
self.ui = self.context.get_ui()
|
||
|
self.threads = []
|
||
|
|
||
|
def stop_threads(self):
|
||
|
for thread in self.threads:
|
||
|
if thread.ended():
|
||
|
continue
|
||
|
|
||
|
if not thread.stopped():
|
||
|
self.context.log_debug('PlaybackMonitorThread[%s]: stopping...' % thread.video_id)
|
||
|
thread.stop()
|
||
|
|
||
|
for thread in self.threads:
|
||
|
if thread.stopped() and not thread.ended():
|
||
|
try:
|
||
|
thread.join()
|
||
|
except RuntimeError:
|
||
|
pass
|
||
|
|
||
|
def cleanup_threads(self, only_ended=True):
|
||
|
active_threads = []
|
||
|
for thread in self.threads:
|
||
|
if only_ended and not thread.ended():
|
||
|
active_threads.append(thread)
|
||
|
continue
|
||
|
|
||
|
if thread.ended():
|
||
|
self.context.log_debug('PlaybackMonitorThread[%s]: clean up...' % thread.video_id)
|
||
|
else:
|
||
|
self.context.log_debug('PlaybackMonitorThread[%s]: stopping...' % thread.video_id)
|
||
|
if not thread.stopped():
|
||
|
thread.stop()
|
||
|
try:
|
||
|
thread.join()
|
||
|
except RuntimeError:
|
||
|
pass
|
||
|
|
||
|
self.context.log_debug('PlaybackMonitor active threads: |%s|' %
|
||
|
', '.join([thread.video_id for thread in active_threads]))
|
||
|
self.threads = active_threads
|
||
|
|
||
|
def onPlayBackStarted(self):
|
||
|
if self.ui.get_home_window_property('playback_json'):
|
||
|
playback_json = json.loads(self.ui.get_home_window_property('playback_json'))
|
||
|
self.ui.clear_home_window_property('playback_json')
|
||
|
self.cleanup_threads()
|
||
|
self.threads.append(PlaybackMonitorThread(self.provider, self.context, playback_json))
|
||
|
|
||
|
def onPlayBackEnded(self):
|
||
|
self.stop_threads()
|
||
|
self.cleanup_threads()
|
||
|
|
||
|
def onPlayBackStopped(self):
|
||
|
self.onPlayBackEnded()
|
||
|
|
||
|
def onPlayBackError(self):
|
||
|
self.onPlayBackEnded()
|