diff --git a/addon.xml b/addon.xml index 1b961e0..3c1434a 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ diff --git a/default.py b/default.py index 91b0a33..712c857 100644 --- a/default.py +++ b/default.py @@ -2,12 +2,20 @@ from resources.lib.simple_logging import SimpleLogging from resources.lib.functions import mainEntryPoint +from resources.lib.ga_client import GoogleAnalytics, log_error log = SimpleLogging('default') log.info("About to enter mainEntryPoint()") -mainEntryPoint() +try: + mainEntryPoint() +except Exception as error: + ga = GoogleAnalytics() + err_strings = ga.formatException() + ga.sendEventData("Exception", err_strings[0], err_strings[1]) + log.error(error) + raise # clear done and exit. # sys.modules.clear() diff --git a/resources/language/English/strings.po b/resources/language/English/strings.po index 5e584ff..d4127bb 100644 --- a/resources/language/English/strings.po +++ b/resources/language/English/strings.po @@ -549,3 +549,7 @@ msgstr "" msgctxt "#30286" msgid "Movies - Unwatched" msgstr "" + +msgctxt "#30287" +msgid "Log Errors and Anonymous Metrics" +msgstr "" diff --git a/resources/lib/ga_client.py b/resources/lib/ga_client.py new file mode 100644 index 0000000..1990842 --- /dev/null +++ b/resources/lib/ga_client.py @@ -0,0 +1,245 @@ +import sys +import os +import traceback +import hashlib +import time +import xbmcaddon +import xbmc +import urllib +import httplib +import ssl + +from clientinfo import ClientInformation +from simple_logging import SimpleLogging + +log = SimpleLogging(__name__) + +# for info on the metrics that can be sent to Google Analytics +# https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#events + +logEventHistory = {} + +# wrap a function to catch, log and then re throw an exception +def log_error(errors=(Exception, )): + def decorator(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except errors as error: + if not (hasattr(error, 'quiet') and error.quiet): + ga = GoogleAnalytics() + err_strings = ga.formatException() + ga.sendEventData("Exception", err_strings[0], err_strings[1], True) + log.error(error) + log.error("log_error: %s \n args: %s \n kwargs: %s" % (func.__name__, args, kwargs)) + raise + return wrapper + return decorator + +# main GA class +class GoogleAnalytics: + + testing = False + enabled = True + + def __init__(self): + + settings = xbmcaddon.Addon('plugin.video.embycon') + client_info = ClientInformation() + self.version = client_info.getVersion() + self.device_id = client_info.getDeviceId() + + self.enabled = settings.getSetting("metricLogging") == "true" + + # user agent string, used for OS and Kodi version identification + kodi_ver = xbmc.getInfoLabel("System.BuildVersion") + if not kodi_ver: + kodi_ver = "na" + kodi_ver = kodi_ver.strip() + if kodi_ver.find(" ") > 0: + kodi_ver = kodi_ver[0:kodi_ver.find(" ")] + self.userAgent = "Kodi/" + kodi_ver + " (" + self.getUserAgentOS() + ")" + + # Use set user name + self.user_name = settings.getSetting('username') or 'None' + + # use md5 for client and user for analytics + self.device_id = hashlib.md5(self.device_id).hexdigest() + self.user_name = hashlib.md5(self.user_name).hexdigest() + + # resolution + self.screen_mode = xbmc.getInfoLabel("System.ScreenMode") + self.screen_height = xbmc.getInfoLabel("System.ScreenHeight") + self.screen_width = xbmc.getInfoLabel("System.ScreenWidth") + + self.lang = xbmc.getInfoLabel("System.Language") + + def getUserAgentOS(self): + + if xbmc.getCondVisibility('system.platform.osx'): + return "Mac OS X" + elif xbmc.getCondVisibility('system.platform.ios'): + return "iOS" + elif xbmc.getCondVisibility('system.platform.windows'): + return "Windows NT" + elif xbmc.getCondVisibility('system.platform.android'): + return "Android" + elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'): + return "Linux Rpi" + elif xbmc.getCondVisibility('system.platform.linux'): + return "Linux" + else: + return "Other" + + def formatException(self): + + stack = traceback.extract_stack() + exc_type, exc_obj, exc_tb = sys.exc_info() + tb = traceback.extract_tb(exc_tb) + full_tb = stack[:-1] + tb + #log.error(str(full_tb)) + + # get last stack frame + latestStackFrame = None + if len(tb) > 0: + latestStackFrame = tb[-1] + #log.error(str(tb)) + + fileStackTrace = "" + try: + # get files from stack + stackFileList = [] + for frame in full_tb: + #log.error(str(frame)) + frameFile = (os.path.split(frame[0])[1])[:-3] + frameLine = frame[1] + if len(stackFileList) == 0 or stackFileList[-1][0] != frameFile: + stackFileList.append([frameFile, [str(frameLine)]]) + else: + file = stackFileList[-1][0] + lines = stackFileList[-1][1] + lines.append(str(frameLine)) + stackFileList[-1] = [file, lines] + #log.error(str(stackFileList)) + + for item in stackFileList: + lines = ",".join(item[1]) + fileStackTrace += item[0] + "," + lines + ":" + #log.error(str(fileStackTrace)) + except Exception as e: + fileStackTrace = None + log.error(e) + + errorType = "NA" + errorFile = "NA" + + if latestStackFrame is not None: + if fileStackTrace is None: + fileStackTrace = os.path.split(latestStackFrame[0])[1] + ":" + str(latestStackFrame[1]) + + codeLine = "NA" + if(len(latestStackFrame) > 3 and latestStackFrame[3] != None): + codeLine = latestStackFrame[3].strip() + + errorFile = "%s(%s)(%s)" % (fileStackTrace, exc_obj.message, codeLine) + errorFile = errorFile[0:499] + errorType = "%s" % (exc_type.__name__) + #log.error(errorType + " - " + errorFile) + + del(exc_type, exc_obj, exc_tb) + + return errorType, errorFile + + def getBaseData(self): + + # all the data we can send to Google Analytics + data = {} + data['v'] = '1' + data['tid'] = 'UA-101964432-1' # tracking id + + data['ds'] = 'plugin' # data source + + data['an'] = 'Kodi4Emby' # App Name + data['aid'] = '1' # App ID + data['av'] = self.version # App Version + #data['aiid'] = '1.1' # App installer ID + + data['cid'] = self.device_id # Client ID + #data['uid'] = self.user_name # User ID + + data['ua'] = self.userAgent # user agent string + + # add width and height, only add if full screen + if self.screen_mode.lower().find("window") == -1: + data['sr'] = str(self.screen_width) + "x" + str(self.screen_height) + + data["ul"] = self.lang + + return data + + def sendScreenView(self, name): + + data = self.getBaseData() + data['t'] = 'screenview' # action type + data['cd'] = name + + self.sendData(data) + + def sendEventData(self, eventCategory, eventAction, eventLabel=None, throttle=False): + + # if throttling is enabled then only log the same event every 5 min + if throttle: + throttleKey = eventCategory + "-" + eventAction + "-" + str(eventLabel) + lastLogged = logEventHistory.get(throttleKey) + if lastLogged != None: + timeSinceLastLog = time.time() - lastLogged + if timeSinceLastLog < 300 : + log.info("SKIPPING_LOG_EVENT : " + str(timeSinceLastLog) + " " + throttleKey) + return + logEventHistory[throttleKey] = time.time() + + data = self.getBaseData() + data['t'] = 'event' # action type + data['ec'] = eventCategory # Event Category + data['ea'] = eventAction # Event Action + + if eventLabel is not None : + data['el'] = eventLabel # Event Label + + self.sendData(data) + + def sendData(self, data): + + if not self.enabled: + return + + if self.testing: + log.info("GA: " + str(data)) + + postData = "" + for key in data: + postData = postData + key + "=" + urllib.quote(data[key]) + "&" + + server = "www.google-analytics.com:443" + if self.testing: + url_path = "/debug/collect" + else: + url_path = "/collect" + + ret_data = None + try: + conn = httplib.HTTPSConnection(server, timeout=40, context=ssl._create_unverified_context()) + head = {} + head["Content-Type"] = "application/x-www-form-urlencoded" + conn.request(method="POST", url=url_path, body=postData, headers=head) + data = conn.getresponse() + if int(data.status) == 200: + ret_data = data.read() + except Exception as error: + log.error("Error sending GA data: " + str(error)) + + if self.testing and ret_data is not None: + log.info("GA: " + ret_data.encode('utf-8')) + + + \ No newline at end of file diff --git a/resources/lib/play_utils.py b/resources/lib/play_utils.py index 7ba40dc..671859f 100644 --- a/resources/lib/play_utils.py +++ b/resources/lib/play_utils.py @@ -15,6 +15,7 @@ from utils import PlayUtils, getArt from kodi_utils import HomeWindow from translation import i18n from json_rpc import json_rpc +from ga_client import GoogleAnalytics, log_error log = SimpleLogging(__name__) downloadUtils = DownloadUtils() @@ -28,8 +29,6 @@ def playFile(play_info): log.info("playFile id(%s) resume(%s) force_transcode(%s)" % (id, auto_resume, force_transcode)) - userid = downloadUtils.getUserId() - settings = xbmcaddon.Addon('plugin.video.embycon') addon_path = settings.getAddonInfo('path') playback_type = settings.getSetting("playback_type") @@ -73,8 +72,8 @@ def playFile(play_info): return_value = xbmcgui.Dialog().yesno(i18n('extra_prompt'), i18n('turn_on_auto_resume?')) if return_value: params = {"setting": "myvideos.selectaction", "value": 2} - result = json_rpc('Settings.setSettingValue').execute(params) - log.info("Save Setting (myvideos.selectaction): %s" % result) + json_rpc_result = json_rpc('Settings.setSettingValue').execute(params) + log.info("Save Setting (myvideos.selectaction): %s" % json_rpc_result) if resume_result == 1: seekTime = 0 @@ -119,6 +118,11 @@ def playFile(play_info): playlist.add(playurl, list_item) xbmc.Player().play(playlist) + item_type = result.get('Type', 'na') + ga = GoogleAnalytics() + ga.sendEventData("PlayAction", item_type, playback_type_string) + ga.sendScreenView(item_type) + if seekTime == 0: return diff --git a/resources/lib/server_info.py b/resources/lib/server_info.py new file mode 100644 index 0000000..38217ac --- /dev/null +++ b/resources/lib/server_info.py @@ -0,0 +1,39 @@ + +import json + +from kodi_utils import HomeWindow +from downloadutils import DownloadUtils +from simple_logging import SimpleLogging +log = SimpleLogging(__name__) + +def getServerId(): + + home_screen = HomeWindow() + server_id = home_screen.getProperty("server_id") + if server_id: + log.info("Server ID from stored value: " + server_id) + return server_id + + downloadUtils = DownloadUtils() + try: + url = "{server}/emby/system/info/public" + jsonData = downloadUtils.downloadUrl(url, suppress=True, authenticate=False) + result = json.loads(jsonData) + if result is not None and result.get("Id") is not None: + server_id = result.get("Id") + log.info("Server ID from server request: " + server_id) + home_screen.setProperty("server_id", server_id) + return server_id + else: + return None + except Exception as error: + log.info("Could not get Server ID: " + str(error)) + return None + + + + + + + + diff --git a/resources/settings.xml b/resources/settings.xml index 1999540..b4c173c 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -52,5 +52,6 @@ + \ No newline at end of file diff --git a/service.py b/service.py index ebe3d33..2c19943 100644 --- a/service.py +++ b/service.py @@ -6,13 +6,15 @@ import xbmcaddon import xbmcgui import time import json -from datetime import datetime +import platform from resources.lib.downloadutils import DownloadUtils +from resources.lib.server_info import getServerId from resources.lib.simple_logging import SimpleLogging from resources.lib.play_utils import playFile from resources.lib.kodi_utils import HomeWindow from resources.lib.translation import i18n +from resources.lib.ga_client import GoogleAnalytics, log_error # clear user and token when logging in home_window = HomeWindow() @@ -209,6 +211,7 @@ class Service(xbmc.Player): log.info("Starting monitor service: " + str(args)) self.played_information = {} + @log_error() def onPlayBackStarted(self): # Will be called when xbmc starts playing a file stopAll(self.played_information) @@ -247,6 +250,7 @@ class Service(xbmc.Player): log.info("ADDING_FILE : " + current_playing_file) log.info("ADDING_FILE : " + str(self.played_information)) + @log_error() def onPlayBackEnded(self): # Will be called when kodi stops playing a file log.info("EmbyCon Service -> onPlayBackEnded") @@ -254,6 +258,7 @@ class Service(xbmc.Player): home_window.clearProperty("item_id") stopAll(self.played_information) + @log_error() def onPlayBackStopped(self): # Will be called when user stops kodi playing a file log.info("onPlayBackStopped") @@ -261,6 +266,7 @@ class Service(xbmc.Player): home_window.clearProperty("item_id") stopAll(self.played_information) + @log_error() def onPlayBackPaused(self): # Will be called when kodi pauses the video log.info("onPlayBackPaused") @@ -271,6 +277,7 @@ class Service(xbmc.Player): play_data['paused'] = True sendProgress() + @log_error() def onPlayBackResumed(self): # Will be called when kodi resumes the video log.info("onPlayBackResumed") @@ -281,6 +288,7 @@ class Service(xbmc.Player): play_data['paused'] = False sendProgress() + @log_error() def onPlayBackSeek(self, time, seekOffset): # Will be called when kodi seeks in video log.info("onPlayBackSeek") @@ -288,34 +296,70 @@ class Service(xbmc.Player): monitor = Service() -last_progress_update = datetime.today() +last_progress_update = time.time() +lastMetricPing = time.time() +lastStartCheck = time.time() +startSent = False -while not xbmc.abortRequested: +ga = GoogleAnalytics() +try: + ga.sendEventData("Version", "OS", platform.platform()) + ga.sendEventData("Version", "Python", platform.python_version()) +except Exception as error: + log.error("Exception in sending client meta info: " + str(error)) - home_window = HomeWindow() +try: + while not xbmc.abortRequested: + + home_window = HomeWindow() - if xbmc.Player().isPlaying(): try: - # send update - td = datetime.today() - last_progress_update - sec_diff = td.seconds - if sec_diff > 10: - sendProgress() - last_progress_update = datetime.today() + if not startSent and (time.time() - lastStartCheck) > 30: + lastStartCheck = time.time() + server_id = getServerId() + if server_id is not None: + startSent = True + ga = GoogleAnalytics() + ga.sendEventData("Application", "Startup", server_id) - except Exception, e: - log.error("Exception in Playback Monitor : " + str(e)) - pass + except Exception as error: + log.error("Exception in sending start message: " + str(error)) + raise - else: - 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) + if xbmc.Player().isPlaying(): - xbmc.sleep(1000) - HomeWindow().setProperty("Service_Timestamp", str(int(time.time()))) + try: + if (time.time() - lastMetricPing) > 300: + lastMetricPing = time.time() + ga = GoogleAnalytics() + ga.sendEventData("PlayAction", "PlayPing") + except Exception, e: + log.error("Exception in sending play ping: " + str(e)) + + try: + if (time.time() - last_progress_update) > 10: + last_progress_update = time.time() + sendProgress() + + except Exception as error: + log.error("Exception in Playback Monitor : " + str(error)) + + else: + 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) + + xbmc.sleep(1000) + HomeWindow().setProperty("Service_Timestamp", str(int(time.time()))) + +except Exception as error: + ga = GoogleAnalytics() + err_strings = ga.formatException() + ga.sendEventData("Exception", err_strings[0], err_strings[1]) + log.error(error) + raise # clear user and token when loggin off home_window = HomeWindow()