2017-04-10 09:19:08 +10:00
|
|
|
# coding=utf-8
|
2017-03-17 15:41:46 +11:00
|
|
|
# Gnu General Public License - see LICENSE.TXT
|
|
|
|
|
|
2014-09-28 10:30:07 +10:00
|
|
|
import xbmc
|
2017-05-25 18:16:01 +10:00
|
|
|
import xbmcaddon
|
2017-06-20 10:10:51 +10:00
|
|
|
import xbmcgui
|
2014-09-28 10:30:07 +10:00
|
|
|
import time
|
2017-06-10 13:01:04 +10:00
|
|
|
import json
|
2017-08-13 13:14:51 +10:00
|
|
|
import traceback
|
2017-03-10 10:45:38 +11:00
|
|
|
|
2017-03-17 15:41:46 +11:00
|
|
|
from resources.lib.downloadutils import DownloadUtils
|
|
|
|
|
from resources.lib.simple_logging import SimpleLogging
|
2017-04-14 20:21:20 +10:00
|
|
|
from resources.lib.play_utils import playFile
|
2017-05-26 03:48:48 -04:00
|
|
|
from resources.lib.kodi_utils import HomeWindow
|
2017-06-20 10:10:51 +10:00
|
|
|
from resources.lib.translation import i18n
|
2017-09-29 09:24:01 +10:00
|
|
|
from resources.lib.widgets import checkForNewContent
|
2014-09-28 10:30:07 +10:00
|
|
|
|
2017-04-29 10:37:20 +10:00
|
|
|
# clear user and token when logging in
|
2017-05-26 03:48:48 -04:00
|
|
|
home_window = HomeWindow()
|
2017-05-25 18:16:01 +10:00
|
|
|
home_window.clearProperty("userid")
|
|
|
|
|
home_window.clearProperty("AccessToken")
|
2017-05-26 03:48:48 -04:00
|
|
|
home_window.clearProperty("Params")
|
2017-04-29 10:37:20 +10:00
|
|
|
|
2017-05-29 06:19:33 -04:00
|
|
|
log = SimpleLogging('service')
|
2017-04-10 09:19:08 +10:00
|
|
|
download_utils = DownloadUtils()
|
2014-09-28 10:30:07 +10:00
|
|
|
|
2014-10-03 19:46:33 +10:00
|
|
|
# auth the service
|
|
|
|
|
try:
|
2017-04-10 09:19:08 +10:00
|
|
|
download_utils.authenticate()
|
2017-07-24 08:33:04 +10:00
|
|
|
except Exception as error:
|
|
|
|
|
log.error("Error with initial service auth: " + str(error))
|
2014-09-28 10:30:07 +10:00
|
|
|
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2014-09-28 10:30:07 +10:00
|
|
|
def hasData(data):
|
2017-04-10 09:19:08 +10:00
|
|
|
if data is None or len(data) == 0 or data == "None":
|
2014-09-28 10:30:07 +10:00
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
return True
|
2017-04-10 09:19:08 +10:00
|
|
|
|
2017-05-25 18:16:01 +10:00
|
|
|
|
2017-05-29 06:19:33 -04:00
|
|
|
def sendProgress():
|
2017-05-25 18:16:01 +10:00
|
|
|
playing_file = xbmc.Player().getPlayingFile()
|
|
|
|
|
play_data = monitor.played_information.get(playing_file)
|
|
|
|
|
|
|
|
|
|
if play_data is None:
|
|
|
|
|
return
|
|
|
|
|
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("Sending Progress Update")
|
2017-05-25 18:16:01 +10:00
|
|
|
|
|
|
|
|
play_time = xbmc.Player().getTime()
|
|
|
|
|
play_data["currentPossition"] = play_time
|
|
|
|
|
|
|
|
|
|
item_id = play_data.get("item_id")
|
|
|
|
|
if item_id is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
ticks = int(play_time * 10000000)
|
|
|
|
|
paused = play_data.get("paused", False)
|
|
|
|
|
playback_type = play_data.get("playback_type")
|
2017-11-14 19:11:25 +11:00
|
|
|
play_session_id = play_data.get("play_session_id")
|
2017-05-25 18:16:01 +10:00
|
|
|
|
|
|
|
|
postdata = {
|
|
|
|
|
'QueueableMediaTypes': "Video",
|
|
|
|
|
'CanSeek': True,
|
|
|
|
|
'ItemId': item_id,
|
|
|
|
|
'MediaSourceId': item_id,
|
|
|
|
|
'PositionTicks': ticks,
|
|
|
|
|
'IsPaused': paused,
|
|
|
|
|
'IsMuted': False,
|
2017-11-14 19:11:25 +11:00
|
|
|
'PlayMethod': playback_type,
|
|
|
|
|
'PlaySessionId': play_session_id
|
2017-05-25 18:16:01 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.debug("Sending POST progress started: %s." % postdata)
|
|
|
|
|
|
2017-06-19 11:49:22 +10:00
|
|
|
url = "{server}/emby/Sessions/Playing/Progress"
|
2017-05-26 03:48:48 -04:00
|
|
|
download_utils.downloadUrl(url, postBody=postdata, method="POST")
|
2017-05-25 18:16:01 +10:00
|
|
|
|
2017-06-20 10:10:51 +10:00
|
|
|
def promptForStopActions(item_id, current_possition):
|
|
|
|
|
|
|
|
|
|
settings = xbmcaddon.Addon(id='plugin.video.embycon')
|
|
|
|
|
|
2017-06-21 17:42:04 +10:00
|
|
|
prompt_next_percentage = int(settings.getSetting('promptPlayNextEpisodePercentage'))
|
2017-10-03 15:04:20 +11:00
|
|
|
play_prompt = settings.getSetting('promptPlayNextEpisodePercentage_prompt') == "true"
|
2017-06-21 17:42:04 +10:00
|
|
|
prompt_delete_episode_percentage = int(settings.getSetting('promptDeleteEpisodePercentage'))
|
|
|
|
|
prompt_delete_movie_percentage = int(settings.getSetting('promptDeleteMoviePercentage'))
|
2017-06-20 10:10:51 +10:00
|
|
|
|
2017-06-21 17:42:04 +10:00
|
|
|
# everything is off so return
|
|
|
|
|
if prompt_next_percentage == 100 and prompt_delete_episode_percentage == 100 and prompt_delete_movie_percentage == 100:
|
2017-06-20 10:10:51 +10:00
|
|
|
return
|
2017-06-20 07:04:55 +10:00
|
|
|
|
|
|
|
|
jsonData = download_utils.downloadUrl("{server}/emby/Users/{userid}/Items/" +
|
2017-06-20 10:10:51 +10:00
|
|
|
item_id + "?format=json",
|
2017-06-20 07:04:55 +10:00
|
|
|
suppress=False, popup=1)
|
|
|
|
|
result = json.loads(jsonData)
|
2017-06-21 17:42:04 +10:00
|
|
|
prompt_to_delete = False
|
|
|
|
|
runtime = result.get("RunTimeTicks", 0)
|
2017-06-20 07:04:55 +10:00
|
|
|
|
2017-06-21 17:42:04 +10:00
|
|
|
# if no runtime we cant calculate perceantge so just return
|
|
|
|
|
if runtime == 0:
|
|
|
|
|
log.debug("No runtime so returing")
|
2017-06-20 10:10:51 +10:00
|
|
|
return
|
2017-06-20 07:04:55 +10:00
|
|
|
|
2017-06-21 17:42:04 +10:00
|
|
|
# item percentage complete
|
|
|
|
|
percenatge_complete = int(((current_possition * 10000000) / runtime) * 100)
|
|
|
|
|
log.debug("Episode Percentage Complete: %s" % percenatge_complete)
|
|
|
|
|
|
|
|
|
|
if (prompt_delete_episode_percentage < 100 and
|
|
|
|
|
result.get("Type", "na") == "Episode" and
|
|
|
|
|
percenatge_complete > prompt_delete_episode_percentage):
|
|
|
|
|
prompt_to_delete = True
|
|
|
|
|
|
|
|
|
|
if (prompt_delete_movie_percentage < 100 and
|
|
|
|
|
result.get("Type", "na") == "Movie" and
|
|
|
|
|
percenatge_complete > prompt_delete_movie_percentage):
|
|
|
|
|
prompt_to_delete = True
|
|
|
|
|
|
|
|
|
|
if prompt_to_delete:
|
|
|
|
|
log.debug("Prompting for delete")
|
|
|
|
|
resp = xbmcgui.Dialog().yesno(i18n('confirm_file_delete'), i18n('file_delete_confirm'), autoclose=10000)
|
|
|
|
|
if resp:
|
|
|
|
|
log.debug("Deleting item: %s" % item_id)
|
|
|
|
|
url = "{server}/emby/Items/%s?format=json" % item_id
|
|
|
|
|
download_utils.downloadUrl(url, method="DELETE")
|
|
|
|
|
xbmc.executebuiltin("Container.Refresh")
|
|
|
|
|
|
|
|
|
|
# prompt for next episode
|
|
|
|
|
if (prompt_next_percentage < 100 and
|
|
|
|
|
result.get("Type", "na") == "Episode" and
|
|
|
|
|
percenatge_complete > prompt_next_percentage):
|
2017-06-20 10:10:51 +10:00
|
|
|
parendId = result.get("ParentId", "na")
|
2017-06-21 17:42:04 +10:00
|
|
|
item_index = result.get("IndexNumber", -1)
|
|
|
|
|
|
2017-06-20 10:10:51 +10:00
|
|
|
if parendId == "na":
|
|
|
|
|
log.debug("No parent id, can not prompt for next episode")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if item_index == -1:
|
|
|
|
|
log.debug("No episode number, can not prompt for next episode")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
jsonData = download_utils.downloadUrl('{server}/emby/Users/{userid}/Items?' +
|
|
|
|
|
'?Recursive=true' +
|
|
|
|
|
'&ParentId=' + parendId +
|
|
|
|
|
#'&Filters=IsUnplayed,IsNotFolder' +
|
|
|
|
|
'&IsVirtualUnaired=false' +
|
|
|
|
|
'&IsMissing=False' +
|
|
|
|
|
'&IncludeItemTypes=Episode' +
|
|
|
|
|
'&ImageTypeLimit=1' +
|
|
|
|
|
'&format=json',
|
|
|
|
|
suppress=False, popup=1)
|
|
|
|
|
|
|
|
|
|
items_result = json.loads(jsonData)
|
|
|
|
|
log.debug("Prompt Next Item Details: %s" % items_result)
|
|
|
|
|
# find next episode
|
|
|
|
|
item_list = items_result.get("Items", [])
|
|
|
|
|
for item in item_list:
|
|
|
|
|
index = item.get("IndexNumber", -1)
|
2017-08-15 20:19:28 +10:00
|
|
|
if index == item_index + 1: # find the very next episode in the season
|
2017-10-03 15:04:20 +11:00
|
|
|
|
|
|
|
|
resp = True
|
|
|
|
|
if play_prompt:
|
|
|
|
|
#next_epp_name = str(index) + " of " + str(item_list[-1].get("IndexNumber", -1)) + " - " + item.get("Name", "n/a")
|
|
|
|
|
next_epp_name = ("%02d - " % (index,)) + item.get("Name", "n/a")
|
|
|
|
|
resp = xbmcgui.Dialog().yesno(i18n("play_next_title"), i18n("play_next_question"), next_epp_name, autoclose=10000)
|
|
|
|
|
|
2017-06-20 10:10:51 +10:00
|
|
|
if resp:
|
|
|
|
|
next_item_id = item.get("Id")
|
|
|
|
|
log.debug("Playing Next Episode: %s" % next_item_id)
|
|
|
|
|
|
|
|
|
|
play_info = {}
|
|
|
|
|
play_info["item_id"] = next_item_id
|
|
|
|
|
play_info["auto_resume"] = "-1"
|
|
|
|
|
play_info["force_transcode"] = False
|
|
|
|
|
play_data = json.dumps(play_info)
|
|
|
|
|
|
|
|
|
|
home_window = HomeWindow()
|
|
|
|
|
home_window.setProperty("item_id", next_item_id)
|
|
|
|
|
home_window.setProperty("play_item_message", play_data)
|
|
|
|
|
|
|
|
|
|
break
|
2017-06-20 07:04:55 +10:00
|
|
|
|
2017-04-10 09:19:08 +10:00
|
|
|
|
2014-09-28 10:30:07 +10:00
|
|
|
def stopAll(played_information):
|
2017-04-10 09:19:08 +10:00
|
|
|
if len(played_information) == 0:
|
|
|
|
|
return
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("played_information : " + str(played_information))
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2014-09-28 10:30:07 +10:00
|
|
|
for item_url in played_information:
|
|
|
|
|
data = played_information.get(item_url)
|
2017-04-10 09:19:08 +10:00
|
|
|
if data is not None:
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("item_url : " + item_url)
|
|
|
|
|
log.debug("item_data : " + str(data))
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-06-20 10:10:51 +10:00
|
|
|
current_possition = data.get("currentPossition", 0)
|
2017-04-10 09:19:08 +10:00
|
|
|
emby_item_id = data.get("item_id")
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-04-10 09:19:08 +10:00
|
|
|
if hasData(emby_item_id):
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("Playback Stopped at: " + str(int(current_possition * 10000000)))
|
2017-05-25 18:16:01 +10:00
|
|
|
|
2017-06-19 11:49:22 +10:00
|
|
|
url = "{server}/emby/Sessions/Playing/Stopped"
|
2017-05-25 18:16:01 +10:00
|
|
|
postdata = {
|
|
|
|
|
'ItemId': emby_item_id,
|
|
|
|
|
'MediaSourceId': emby_item_id,
|
|
|
|
|
'PositionTicks': int(current_possition * 10000000)
|
|
|
|
|
}
|
2017-05-26 03:48:48 -04:00
|
|
|
download_utils.downloadUrl(url, postBody=postdata, method="POST")
|
2017-05-25 18:16:01 +10:00
|
|
|
|
2017-06-20 07:04:55 +10:00
|
|
|
promptForStopActions(emby_item_id, current_possition)
|
|
|
|
|
|
2014-09-28 10:30:07 +10:00
|
|
|
played_information.clear()
|
|
|
|
|
|
2017-05-29 06:19:33 -04:00
|
|
|
|
|
|
|
|
class Service(xbmc.Player):
|
2014-09-28 10:30:07 +10:00
|
|
|
played_information = {}
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-04-10 09:19:08 +10:00
|
|
|
def __init__(self, *args):
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("Starting monitor service: " + str(args))
|
2014-09-28 10:30:07 +10:00
|
|
|
self.played_information = {}
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-04-10 09:19:08 +10:00
|
|
|
def onPlayBackStarted(self):
|
2014-09-28 10:30:07 +10:00
|
|
|
# Will be called when xbmc starts playing a file
|
|
|
|
|
stopAll(self.played_information)
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-04-10 09:19:08 +10:00
|
|
|
current_playing_file = xbmc.Player().getPlayingFile()
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("onPlayBackStarted: " + current_playing_file)
|
2017-04-14 20:21:20 +10:00
|
|
|
|
2017-05-26 03:48:48 -04:00
|
|
|
home_window = HomeWindow()
|
|
|
|
|
emby_item_id = home_window.getProperty("item_id")
|
|
|
|
|
playback_type = home_window.getProperty("PlaybackType_" + emby_item_id)
|
2017-11-14 19:11:25 +11:00
|
|
|
play_session_id = home_window.getProperty("PlaySessionId_" + emby_item_id)
|
2017-04-09 10:25:58 +10:00
|
|
|
|
2017-04-14 20:21:20 +10:00
|
|
|
# if we could not find the ID of the current item then return
|
2017-04-10 09:19:08 +10:00
|
|
|
if emby_item_id is None or len(emby_item_id) == 0:
|
2014-09-28 10:30:07 +10:00
|
|
|
return
|
2017-04-14 20:21:20 +10:00
|
|
|
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("Sending Playback Started")
|
2017-05-25 18:16:01 +10:00
|
|
|
postdata = {
|
|
|
|
|
'QueueableMediaTypes': "Video",
|
|
|
|
|
'CanSeek': True,
|
|
|
|
|
'ItemId': emby_item_id,
|
|
|
|
|
'MediaSourceId': emby_item_id,
|
2017-11-14 19:11:25 +11:00
|
|
|
'PlayMethod': playback_type,
|
|
|
|
|
'PlaySessionId': play_session_id
|
2017-05-25 18:16:01 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.debug("Sending POST play started: %s." % postdata)
|
|
|
|
|
|
2017-06-19 11:49:22 +10:00
|
|
|
url = "{server}/emby/Sessions/Playing"
|
2017-05-26 03:48:48 -04:00
|
|
|
download_utils.downloadUrl(url, postBody=postdata, method="POST")
|
2017-05-25 18:16:01 +10:00
|
|
|
|
2014-10-03 19:46:33 +10:00
|
|
|
data = {}
|
2017-04-10 09:19:08 +10:00
|
|
|
data["item_id"] = emby_item_id
|
2017-05-25 18:16:01 +10:00
|
|
|
data["paused"] = False
|
|
|
|
|
data["playback_type"] = playback_type
|
2017-11-14 19:11:25 +11:00
|
|
|
data["play_session_id"] = play_session_id
|
2017-04-10 09:19:08 +10:00
|
|
|
self.played_information[current_playing_file] = data
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("ADDING_FILE : " + current_playing_file)
|
|
|
|
|
log.debug("ADDING_FILE : " + str(self.played_information))
|
2014-09-28 10:30:07 +10:00
|
|
|
|
2017-04-10 09:19:08 +10:00
|
|
|
def onPlayBackEnded(self):
|
2017-05-25 18:16:01 +10:00
|
|
|
# Will be called when kodi stops playing a file
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("EmbyCon Service -> onPlayBackEnded")
|
2017-05-26 03:48:48 -04:00
|
|
|
home_window = HomeWindow()
|
|
|
|
|
home_window.clearProperty("item_id")
|
2014-09-28 10:30:07 +10:00
|
|
|
stopAll(self.played_information)
|
|
|
|
|
|
2017-04-10 09:19:08 +10:00
|
|
|
def onPlayBackStopped(self):
|
2017-05-25 18:16:01 +10:00
|
|
|
# Will be called when user stops kodi playing a file
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("onPlayBackStopped")
|
2017-05-26 03:48:48 -04:00
|
|
|
home_window = HomeWindow()
|
|
|
|
|
home_window.clearProperty("item_id")
|
2014-09-28 10:30:07 +10:00
|
|
|
stopAll(self.played_information)
|
|
|
|
|
|
2017-05-25 18:16:01 +10:00
|
|
|
def onPlayBackPaused(self):
|
|
|
|
|
# Will be called when kodi pauses the video
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("onPlayBackPaused")
|
2017-05-25 18:16:01 +10:00
|
|
|
current_file = xbmc.Player().getPlayingFile()
|
|
|
|
|
play_data = monitor.played_information.get(current_file)
|
|
|
|
|
|
|
|
|
|
if play_data is not None:
|
|
|
|
|
play_data['paused'] = True
|
|
|
|
|
sendProgress()
|
|
|
|
|
|
|
|
|
|
def onPlayBackResumed(self):
|
|
|
|
|
# Will be called when kodi resumes the video
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("onPlayBackResumed")
|
2017-05-25 18:16:01 +10:00
|
|
|
current_file = xbmc.Player().getPlayingFile()
|
|
|
|
|
play_data = monitor.played_information.get(current_file)
|
|
|
|
|
|
|
|
|
|
if play_data is not None:
|
|
|
|
|
play_data['paused'] = False
|
|
|
|
|
sendProgress()
|
|
|
|
|
|
|
|
|
|
def onPlayBackSeek(self, time, seekOffset):
|
|
|
|
|
# Will be called when kodi seeks in video
|
2017-07-08 10:34:30 +10:00
|
|
|
log.debug("onPlayBackSeek")
|
2017-05-25 18:16:01 +10:00
|
|
|
sendProgress()
|
|
|
|
|
|
|
|
|
|
|
2014-09-28 10:30:07 +10:00
|
|
|
monitor = Service()
|
2017-08-01 08:22:30 +10:00
|
|
|
home_window = HomeWindow()
|
2017-09-29 09:42:59 +10:00
|
|
|
last_progress_update = time.time()
|
2017-09-29 09:24:01 +10:00
|
|
|
last_content_check = time.time()
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-08-01 08:22:30 +10:00
|
|
|
# monitor.abortRequested() is causes issues, it currently triggers for all addon cancelations which causes
|
|
|
|
|
# the service to exit when a user cancels an addon load action. This is a bug in Kodi.
|
|
|
|
|
# I am switching back to xbmc.abortRequested approach until kodi is fixed or I find a work arround
|
2017-04-14 20:21:20 +10:00
|
|
|
|
2017-08-01 08:22:30 +10:00
|
|
|
while not xbmc.abortRequested:
|
2017-05-29 06:19:33 -04:00
|
|
|
|
2017-08-13 13:14:51 +10:00
|
|
|
try:
|
|
|
|
|
if xbmc.Player().isPlaying():
|
2017-09-29 09:24:01 +10:00
|
|
|
# if playing every 10 seconds updated the server with progress
|
2017-07-08 10:34:30 +10:00
|
|
|
if (time.time() - last_progress_update) > 10:
|
|
|
|
|
last_progress_update = time.time()
|
|
|
|
|
sendProgress()
|
2017-08-13 13:14:51 +10:00
|
|
|
else:
|
2017-09-29 09:24:01 +10:00
|
|
|
# if we have a play item them trigger playback
|
2017-08-13 13:14:51 +10:00
|
|
|
play_data = home_window.getProperty("play_item_message")
|
|
|
|
|
if play_data:
|
|
|
|
|
home_window.clearProperty("play_item_message")
|
|
|
|
|
play_info = json.loads(play_data)
|
|
|
|
|
playFile(play_info)
|
2017-09-29 09:24:01 +10:00
|
|
|
|
|
|
|
|
# if not playing every 60 seonds check for new widget content
|
|
|
|
|
if (time.time() - last_content_check) > 60:
|
|
|
|
|
last_content_check = time.time()
|
|
|
|
|
checkForNewContent()
|
|
|
|
|
|
2017-08-13 13:14:51 +10:00
|
|
|
except Exception as error:
|
|
|
|
|
log.error("Exception in Playback Monitor : " + str(error))
|
|
|
|
|
log.error(traceback.format_exc())
|
2017-07-04 20:05:04 +10:00
|
|
|
|
2017-08-01 08:22:30 +10:00
|
|
|
xbmc.sleep(1000)
|
2017-04-15 15:54:14 +10:00
|
|
|
|
|
|
|
|
# clear user and token when loggin off
|
2017-05-25 18:16:01 +10:00
|
|
|
home_window.clearProperty("userid")
|
|
|
|
|
home_window.clearProperty("AccessToken")
|
2017-05-26 03:48:48 -04:00
|
|
|
home_window.clearProperty("Params")
|
2014-09-28 10:30:07 +10:00
|
|
|
|
2017-08-01 08:22:30 +10:00
|
|
|
log.error("Service shutting down")
|