From bd498529830e600f03dde91f9ed8d06c5a7d6835 Mon Sep 17 00:00:00 2001 From: cttynul Date: Tue, 11 Jun 2019 16:55:53 +0200 Subject: [PATCH 01/10] + compatibility with python 3 --- README | 4 + addon.xml | 8 +- default.py | 15 +- icon.png | Bin 5053 -> 26634 bytes resources/lib/StorageServer.py | 770 ++++++++++++++++++++++++++++ resources/lib/raiplay.py | 5 +- resources/lib/raiplayradio.py | 5 +- resources/lib/relinker.py | 10 +- resources/lib/search.py | 5 +- resources/lib/storageserverdummy.py | 30 ++ resources/lib/tgr.py | 5 +- 11 files changed, 843 insertions(+), 14 deletions(-) create mode 100644 resources/lib/StorageServer.py create mode 100644 resources/lib/storageserverdummy.py diff --git a/README b/README index e69de29..e06aeba 100644 --- a/README +++ b/README @@ -0,0 +1,4 @@ +# Rai Play +-- desc -- + +Storage Server used is a fork of Common Plugin Cache, edited to make it works with Python 3 diff --git a/addon.xml b/addon.xml index 4d70ac3..ff1e41f 100644 --- a/addon.xml +++ b/addon.xml @@ -1,11 +1,11 @@ + name="Rai Play" + version="3.0.0" + provider-name="Nightflyer, cttynul"> - + diff --git a/default.py b/default.py index ebde2f6..bfa8cce 100644 --- a/default.py +++ b/default.py @@ -6,9 +6,16 @@ import xbmcplugin import xbmcaddon import urllib -import urlparse +try: + import urllib.parse as urlparse +except ImportError: + import urlparse +try: + from urllib.parse import urlencode +except: + from urllib import urlencode import datetime -import StorageServer +from resources.lib import StorageServer from resources.lib.tgr import TGR from resources.lib.search import Search from resources.lib.raiplay import RaiPlay @@ -37,13 +44,13 @@ def parameters_string_to_dict(parameters): return paramDict def addDirectoryItem(parameters, li): - url = sys.argv[0] + '?' + urllib.urlencode(parameters) + url = sys.argv[0] + '?' + urlencode(parameters) return xbmcplugin.addDirectoryItem(handle=handle, url=url, listitem=li, isFolder=True) def addLinkItem(parameters, li, url=""): if url == "": - url = sys.argv[0] + '?' + urllib.urlencode(parameters) + url = sys.argv[0] + '?' + urlencode(parameters) li.setProperty('IsPlayable', 'true') return xbmcplugin.addDirectoryItem(handle=handle, url=url, listitem=li, isFolder=False) diff --git a/icon.png b/icon.png index 20864a9bf147f85551391e5738116a24b2285ea9..0387002d29d4a76d00188fc7f7d3d1c8d52b7ff3 100644 GIT binary patch literal 26634 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 I$t3W<04Cct1ONa4 literal 5053 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. + Version 0.8 +''' + +import os +import sys +import socket +import time +import hashlib +import inspect +import string +import xbmc + +try: import sqlite +except: pass +try: import sqlite3 +except: pass + + +class StorageServer(): + def __init__(self, table=None, timeout=24, instance=False): + self.version = u"2.5.4" + self.plugin = u"StorageClient-" + self.version + self.instance = instance + self.die = False + + if hasattr(sys.modules["__main__"], "dbg"): + self.dbg = sys.modules["__main__"].dbg + else: + self.dbg = False + + if hasattr(sys.modules["__main__"], "dbglevel"): + self.dbglevel = sys.modules["__main__"].dbglevel + else: + self.dbglevel = 3 + + if hasattr(sys.modules["__main__"], "xbmc"): + self.xbmc = sys.modules["__main__"].xbmc + else: + import xbmc + self.xbmc = xbmc + + if hasattr(sys.modules["__main__"], "xbmcvfs"): + self.xbmcvfs = sys.modules["__main__"].xbmcvfs + else: + import xbmcvfs + self.xbmcvfs = xbmcvfs + + if hasattr(sys.modules["__main__"], "xbmcaddon"): + self.xbmcaddon = sys.modules["__main__"].xbmcaddon + else: + import xbmcaddon + self.xbmcaddon = xbmcaddon + + self.settings = self.xbmcaddon.Addon(id='script.common.plugin.cache') + self.language = self.settings.getLocalizedString + + self.path = self.xbmc.translatePath('special://temp/') + try: + if not self.xbmcvfs.exists(self.path.decode('utf8', 'ignore')): + self._log(u"Making path structure: " + self.path) + self.xbmcvfs.mkdir(self.path) + except: + if not self.xbmcvfs.exists(self.path): + self._log(u"Making path structure: " + self.path) + self.xbmcvfs.mkdir(self.path) + self.path = os.path.join(self.path, 'commoncache.db') + + self.socket = "" + self.clientsocket = False + self.sql2 = False + self.sql3 = False + self.abortRequested = False + self.daemon_start_time = time.time() + if self.instance: + self.idle = int(self.settings.getSetting("timeout")) + else: + self.idle = 3 + + self.platform = sys.platform + self.modules = sys.modules + self.network_buffer_size = 4096 + + if isinstance(table, str) and len(table) > 0: + self.table = ''.join(c for c in table if c in "%s%s" % (string.ascii_letters, string.digits)) + self._log("Setting table to : %s" % self.table) + elif table != False: + self._log("No table defined") + + self.setCacheTimeout(timeout) + + def _startDB(self): + try: + if "sqlite3" in self.modules: + self.sql3 = True + self._log("sql3 - " + self.path, 2) + self.conn = sqlite3.connect(self.path, check_same_thread=False) + elif "sqlite" in self.modules: + self.sql2 = True + self._log("sql2 - " + self.path, 2) + self.conn = sqlite.connect(self.path) + else: + self._log("Error, no sql found") + return False + + self.curs = self.conn.cursor() + return True + except Exception as e: + self._log("Exception: " + repr(e)) + self.xbmcvfs.delete(self.path) + return False + + def _aborting(self): + if self.instance: + if self.die: + return True + else: + return self.xbmc.abortRequested + return False + + def _usePosixSockets(self): + if self.platform in ["win32", 'win10'] or xbmc.getCondVisibility('system.platform.android') or xbmc.getCondVisibility('system.platform.ios') or xbmc.getCondVisibility('system.platform.tvos'): + return False + else: + return True + + def _sock_init(self, check_stale=False): + self._log("", 2) + if not self.socket or check_stale: + self._log("Checking", 4) + + if self._usePosixSockets(): + self._log("POSIX", 4) + try: self.socket = os.path.join(self.xbmc.translatePath('special://temp/').decode("utf-8"), 'commoncache.socket') + except: self.socket = os.path.join(self.xbmc.translatePath('special://temp/'), 'commoncache.socket') + #self.socket = os.path.join(self.xbmc.translatePath(self.settings.getAddonInfo("profile")).decode("utf-8"), 'commoncache.socket') + if self.xbmcvfs.exists(self.socket) and check_stale: + self._log("Deleting stale socket file : " + self.socket) + self.xbmcvfs.delete(self.socket) + else: + self._log("Non-POSIX", 4) + port = self.settings.getSetting("port") + self.socket = ("127.0.0.1", int(port)) + + self._log("Done: " + repr(self.socket), 2) + + def _recieveData(self): + self._log("", 3) + data = self._recv(self.clientsocket) + self._log("received data: " + data, 4) + + try: + data = eval(data) + except: + self._log("Couldn't evaluate message : " + repr(data)) + data = {"action": "stop"} + + self._log("Done, got data: " + str(len(data)) + " - " + str(repr(data))[0:50], 3) + return data + + def _runCommand(self, data): + self._log("", 3) + res = "" + if data["action"] == "get": + res = self._sqlGet(data["table"], data["name"]) + elif data["action"] == "get_multi": + res = self._sqlGetMulti(data["table"], data["name"], data["items"]) + elif data["action"] == "set_multi": + res = self._sqlSetMulti(data["table"], data["name"], data["data"]) + elif data["action"] == "set": + res = self._sqlSet(data["table"], data["name"], data["data"]) + elif data["action"] == "del": + res = self._sqlDel(data["table"], data["name"]) + elif data["action"] == "lock": + res = self._lock(data["table"], data["name"]) + elif data["action"] == "unlock": + res = self._unlock(data["table"], data["name"]) + + if len(res) > 0: + self._log("Got response: " + str(len(res)) + " - " + str(repr(res))[0:50], 3) + self._send(self.clientsocket, repr(res)) + + self._log("Done", 3) + + def _showMessage(self, heading, message): + self._log(repr(type(heading)) + " - " + repr(type(message))) + duration = 10 * 1000 + self.xbmc.executebuiltin((u'XBMC.Notification("%s", "%s", %s)' % (heading, message, duration)).encode("utf-8")) + + def run(self): + self.plugin = "StorageServer-" + self.version + #self.xbmc.log(self.plugin + " Storage Server starting " + self.path) + self._sock_init(True) + + if not self._startDB(): + self._startDB() + + if self._usePosixSockets(): + sock = socket.socket(socket.AF_UNIX) + else: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + try: + sock.bind(self.socket) + except Exception as e: + self._log("Exception: " + repr(e)) + self._showMessage(self.language(100), self.language(200)) + + return False + + sock.listen(1) + sock.setblocking(0) + + idle_since = time.time() + waiting = 0 + while not self._aborting(): + if waiting == 0: + self._log("accepting", 3) + waiting = 1 + try: + (self.clientsocket, address) = sock.accept() + if waiting == 2: + self._log("Waking up, slept for %s seconds." % int(time.time() - idle_since)) + waiting = 0 + except socket.error as e: + if e.errno == 11 or e.errno == 10035 or e.errno == 35: + # There has to be a better way to accomplish this. + if idle_since + self.idle < time.time(): + if self.instance: + self.die = True + if waiting == 1: + self._log("Idle for %s seconds. Going to sleep. zzzzzzzz " % self.idle) + time.sleep(0.5) + waiting = 2 + continue + self._log("EXCEPTION : " + repr(e)) + except: + pass + + if waiting: + self._log("Continue : " + repr(waiting), 3) + continue + + data = self._recieveData() + self._runCommand(data) + idle_since = time.time() + + self._log("Done") + + self._log("Closing down") + sock.close() + # self.conn.close() + if self._usePosixSockets(): + if self.xbmcvfs.exists(self.socket): + self._log("Deleting socket file") + self.xbmcvfs.delete(self.socket) + self.xbmc.log(self.plugin + " Closed down") + + def _recv(self, sock): + data = " " + idle = True + + self._log(u"", 3) + i = 0 + start = time.time() + while data[len(data) - 2:] != "\r\n" or not idle: + try: + if idle: + recv_buffer = sock.recv(self.network_buffer_size) + idle = False + i += 1 + self._log(u"got data : " + str(i) + u" - " + repr(idle) + u" - " + str(len(data)) + u" + " + str(len(recv_buffer)) + u" | " + repr(recv_buffer)[len(recv_buffer) - 5:], 4) + data += recv_buffer + start = time.time() + elif not idle: + if data[len(data) - 2:] == "\r\n": + sock.send("COMPLETE\r\n" + (" " * (15 - len("COMPLETE\r\n")))) + idle = True + self._log(u"sent COMPLETE " + str(i), 4) + elif len(recv_buffer) > 0: + sock.send("ACK\r\n" + (" " * (15 - len("ACK\r\n")))) + idle = True + self._log(u"sent ACK " + str(i), 4) + recv_buffer = "" + self._log(u"status " + repr(not idle) + u" - " + repr(data[len(data) - 2:] != u"\r\n"), 3) + + except socket.error as e: + if not e.errno in [10035, 35]: + self._log(u"Except error " + repr(e)) + + if e.errno in [22]: # We can't fix this. + return "" + + if start + 10 < time.time(): + self._log(u"over time", 2) + break + + self._log(u"done", 3) + return data.strip() + + def _send(self, sock, data): + idle = True + status = "" + self._log(str(len(data)) + u" - " + repr(data)[0:20], 3) + i = 0 + start = time.time() + while len(data) > 0 or not idle: + send_buffer = " " + try: + if idle: + if len(data) > self.network_buffer_size: + send_buffer = data[:self.network_buffer_size] + else: + send_buffer = data + "\r\n" + + result = sock.send(send_buffer) + i += 1 + idle = False + start = time.time() + elif not idle: + status = "" + while status.find("COMPLETE\r\n") == -1 and status.find("ACK\r\n") == -1: + status = sock.recv(15) + i -= 1 + + idle = True + if len(data) > self.network_buffer_size: + data = data[self.network_buffer_size:] + else: + data = "" + + self._log(u"Got response " + str(i) + u" - " + str(result) + u" == " + str(len(send_buffer)) + u" | " + str(len(data)) + u" - " + repr(send_buffer)[len(send_buffer) - 5:], 3) + + except socket.error as e: + self._log(u"Except error " + repr(e)) + if e.errno != 10035 and e.errno != 35 and e.errno != 107 and e.errno != 32: + self._log(u"Except error " + repr(e)) + if start + 10 < time.time(): + self._log(u"Over time", 2) + break + self._log(u"Done", 3) + return status.find(u"COMPLETE\r\n") > -1 + + def _lock(self, table, name): # This is NOT atomic + self._log(name, 1) + locked = True + curlock = self._sqlGet(table, name) + if curlock.strip(): + if float(curlock) < self.daemon_start_time: + self._log(u"removing stale lock.") + self._sqlExecute("DELETE FROM " + table + " WHERE name = %s", (name,)) + self.conn.commit() + locked = False + else: + locked = False + + if not locked: + self._sqlExecute("INSERT INTO " + table + " VALUES ( %s , %s )", (name, time.time())) + self.conn.commit() + try: self._log(u"locked: " + name.decode('utf8', 'ignore')) + except: self._log(u"locked: " + name) + return "true" + + try: self._log(u"failed for : " + name.decode('utf8', 'ignore'), 1) + except: self._log(u"failed for : " + name, 1) + return "false" + + def _unlock(self, table, name): + self._log(name, 1) + + self._checkTable(table) + self._sqlExecute("DELETE FROM " + table + " WHERE name = %s", (name,)) + + self.conn.commit() + self._log(u"done", 1) + return "true" + + def _sqlSetMulti(self, table, pre, inp_data): + self._log(pre, 1) + self._checkTable(table) + for name in inp_data: + if self._sqlGet(table, pre + name).strip(): + try: self._log(u"Update : " + pre + name.decode('utf8', 'ignore'), 3) + except: self._log(u"Update : " + pre + name, 3) + self._sqlExecute("UPDATE " + table + " SET data = %s WHERE name = %s", (inp_data[name], pre + name)) + else: + try: self._log(u"Insert : " + pre + name.decode('utf8', 'ignore'), 3) + except: self._log(u"Insert : " + pre + name, 3) + self._sqlExecute("INSERT INTO " + table + " VALUES ( %s , %s )", (pre + name, inp_data[name])) + + self.conn.commit() + self._log(u"Done", 3) + return "" + + def _sqlGetMulti(self, table, pre, items): + self._log(pre, 1) + + self._checkTable(table) + ret_val = [] + for name in items: + self._log(pre + name, 3) + self._sqlExecute("SELECT data FROM " + table + " WHERE name = %s", (pre + name)) + + result = "" + for row in self.curs: + self._log(u"Adding : " + str(repr(row[0]))[0:20], 3) + result = row[0] + ret_val += [result] + + self._log(u"Returning : " + repr(ret_val), 2) + return ret_val + + def _sqlSet(self, table, name, data): + self._log(name + str(repr(data))[0:20], 2) + + self._checkTable(table) + if self._sqlGet(table, name).strip(): + self._sqlExecute("UPDATE " + table + " SET data = %s WHERE name = %s", (data, name)) + else: + self._sqlExecute("INSERT INTO " + table + " VALUES ( %s , %s )", (name, data)) + + self.conn.commit() + self._log(u"Done", 2) + return "" + + def _sqlDel(self, table, name): + self._log(name + u" - " + table, 1) + + self._checkTable(table) + + self._sqlExecute("DELETE FROM " + table + " WHERE name LIKE %s", name) + self.conn.commit() + self._log(u"done", 1) + return "true" + + def _sqlGet(self, table, name): + self._log(name + u" - " + table, 2) + + self._checkTable(table) + self._sqlExecute("SELECT data FROM " + table + " WHERE name = %s", name) + + for row in self.curs: + self._log(u"Returning : " + str(repr(row[0]))[0:20], 3) + return row[0] + + self._log(u"Returning empty", 3) + return " " + + def _sqlExecute(self, sql, data): + try: + self._log(repr(sql) + u" - " + repr(data), 5) + if self.sql2: + self.curs.execute(sql, data) + elif self.sql3: + sql = sql.replace("%s", "?") + if isinstance(data, tuple): + self.curs.execute(sql, data) + else: + self.curs.execute(sql, (data,)) + except sqlite3.DatabaseError as e: + if self.xbmcvfs.exists(self.path) and (str(e).find("file is encrypted") > -1 or str(e).find("not a database") > -1): + self._log(u"Deleting broken database file") + self.xbmcvfs.delete(self.path) + self._startDB() + else: + self._log(u"Database error, but database NOT deleted: " + repr(e)) + except: + self._log(u"Uncaught exception") + + def _checkTable(self, table): + try: + self.curs.execute("create table " + table + " (name text unique, data text)") + self.conn.commit() + self._log(u"Created new table") + except: + self._log(u"Passed", 5) + pass + + def _evaluate(self, data): + try: + data = eval(data) # Test json.loads vs eval + return data + except: + self._log(u"Couldn't evaluate message : " + repr(data)) + return "" + + def _generateKey(self, funct, *args): + self._log(u"", 5) + name = repr(funct) + if name.find(" of ") > -1: + name = name[name.find("method") + 7:name.find(" of ")] + elif name.find(" at ") > -1: + name = name[name.find("function") + 9:name.find(" at ")] + + keyhash = hashlib.md5() + for params in args: + if isinstance(params, dict): + for key in sorted(params.keys()): + if key not in ["new_results_function"]: + keyhash.update("'%s'='%s'" % (key, params[key])) + elif isinstance(params, list): + keyhash.update(",".join(["%s" % el for el in params])) + else: + try: + keyhash.update(params) + except: + keyhash.update(str(params)) + + name += "|" + keyhash.hexdigest() + "|" + + self._log(u"Done: " + repr(name), 5) + return name + + def _getCache(self, name, cache): + self._log(u"") + if name in cache: + if "timeout" not in cache[name]: + cache[name]["timeout"] = 3600 + + if cache[name]["timestamp"] > time.time() - (cache[name]["timeout"]): + return cache[name]["res"] + else: + del(cache[name]) + + self._log(u"Done") + return False + + def _setCache(self, cache, name, ret_val): + self._log(u"") + if len(ret_val) > 0: + if not isinstance(cache, dict): + cache = {} + cache[name] = {"timestamp": time.time(), + "timeout": self.timeout, + "res": ret_val} + self._log(u"Saving cache: " + name + str(repr(cache[name]["res"]))[0:50], 1) + self.set("cache" + name, repr(cache)) + self._log(u"Done") + return ret_val + +### EXTERNAL FUNCTIONS ### + soccon = False + table = False + + def cacheFunction(self, funct=False, *args): + self._log(u"function : " + repr(funct) + u" - table_name: " + repr(self.table)) + if funct and self.table: + name = self._generateKey(funct, *args) + cache = self.get("cache" + name) + + if cache.strip() == "": + cache = {} + else: + cache = self._evaluate(cache) + + ret_val = self._getCache(name, cache) + + if not ret_val: + try: self._log(u"Running: " + name.decode('utf8', 'ignore')) + except: self._log(u"Running: " + name) + ret_val = funct(*args) + self._setCache(cache, name, ret_val) + + if ret_val: + self._log(u"Returning result: " + str(len(ret_val))) + self._log(ret_val, 4) + return ret_val + else: + self._log(u"Returning []. Got result: " + repr(ret_val)) + return [] + + self._log(u"Error") + return [] + + def cacheDelete(self, name): + self._log(name, 1) + if self._connect() and self.table: + temp = repr({"action": "del", "table": self.table, "name": "cache" + name}) + self._send(self.soccon, temp) + res = self._recv(self.soccon) + self._log(u"GOT " + repr(res), 3) + + def cacheClean(self, empty=False): + self._log(u"") + if self.table: + cache = self.get("cache" + self.table) + + try: + cache = self._evaluate(cache) + except: + self._log(u"Couldn't evaluate message : " + repr(cache)) + + self._log(u"Cache : " + repr(cache), 5) + if cache: + new_cache = {} + for item in cache: + if (cache[item]["timestamp"] > time.time() - (3600)) and not empty: + new_cache[item] = cache[item] + else: + try: self._log(u"Deleting: " + item.decode('utf8', 'ignore')) + except: self._log(u"Deleting: " + item) + self.set("cache", repr(new_cache)) + return True + + return False + + def lock(self, name): + self._log(name, 1) + self._log(self.table, 1) + + if self._connect() and self.table: + data = repr({"action": "lock", "table": self.table, "name": name}) + self._send(self.soccon, data) + res = self._recv(self.soccon) + if res: + res = self._evaluate(res) + + if res == "true": + self._log(u"Done : " + res.strip(), 1) + return True + + self._log(u"Failed", 1) + return False + + def unlock(self, name): + self._log(name, 1) + + if self._connect() and self.table: + data = repr({"action": "unlock", "table": self.table, "name": name}) + self._send(self.soccon, data) + res = self._recv(self.soccon) + if res: + res = self._evaluate(res) + + if res == "true": + self._log(u"Done: " + res.strip(), 1) + return True + + self._log(u"Failed", 1) + return False + + def _connect(self): + self._log("", 3) + self._sock_init() + + if self._usePosixSockets(): + self.soccon = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + else: + self.soccon = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + connected = False + try: + self.soccon.connect(self.socket) + connected = True + except socket.error as e: + if e.errno in [111]: + self._log(u"StorageServer isn't running") + else: + self._log(u"Exception: " + repr(e)) + self._log(u"Exception: " + repr(self.socket)) + + return connected + + def setMulti(self, name, data): + self._log(name, 1) + if self._connect() and self.table: + temp = repr({"action": "set_multi", "table": self.table, "name": name, "data": data}) + res = self._send(self.soccon, temp) + self._log(u"GOT " + repr(res), 3) + + def getMulti(self, name, items): + self._log(name, 1) + if self._connect() and self.table: + self._send(self.soccon, repr({"action": "get_multi", "table": self.table, "name": name, "items": items})) + self._log(u"Receive", 3) + res = self._recv(self.soccon) + + self._log(u"res : " + str(len(res)), 3) + if res: + res = self._evaluate(res) + + if res == " ": # We return " " as nothing. + return "" + else: + return res + + return "" + + def delete(self, name): + self._log(name, 1) + if self._connect() and self.table: + temp = repr({"action": "del", "table": self.table, "name": name}) + self._send(self.soccon, temp) + res = self._recv(self.soccon) + self._log(u"GOT " + repr(res), 3) + + def set(self, name, data): + self._log(name, 1) + if self._connect() and self.table: + temp = repr({"action": "set", "table": self.table, "name": name, "data": data}) + res = self._send(self.soccon, temp) + self._log(u"GOT " + repr(res), 3) + + def get(self, name): + self._log(name, 1) + if self._connect() and self.table: + self._send(self.soccon, repr({"action": "get", "table": self.table, "name": name})) + self._log(u"Receive", 3) + res = self._recv(self.soccon) + + self._log(u"res : " + str(len(res)), 3) + if res: + res = self._evaluate(res) + return res.strip() # We return " " as nothing. Strip it out. + + return "" + + def setCacheTimeout(self, timeout): + self.timeout = float(timeout) * 3600 + + def _log(self, description, level=0): + if self.dbg and self.dbglevel > level: + try: + self.xbmc.log(u"[%s] %s : '%s'" % (self.plugin, repr(inspect.stack()[1][3]), description), self.xbmc.LOGNOTICE) + except: + self.xbmc.log(u"[%s] %s : '%s'" % (self.plugin, repr(inspect.stack()[1][3]), repr(description)), self.xbmc.LOGNOTICE) + +# Check if this module should be run in instance mode or not. +__workersByName = {} +def run_async(func, *args, **kwargs): + from threading import Thread + worker = Thread(target=func, args=args, kwargs=kwargs) + __workersByName[worker.getName()] = worker + worker.start() + return worker + +def checkInstanceMode(): + if hasattr(sys.modules["__main__"], "xbmcaddon"): + xbmcaddon = sys.modules["__main__"].xbmcaddon + else: + import xbmcaddon + + settings = xbmcaddon.Addon(id='script.common.plugin.cache') + if settings.getSetting("autostart") == "false": + s = StorageServer(table=False, instance=True) + xbmc.log(u" StorageServer Module loaded RUN(instance only)") + + xbmc.log(s.plugin + u" Starting server") + + run_async(s.run) + return True + else: + return False + +checkInstanceMode() diff --git a/resources/lib/raiplay.py b/resources/lib/raiplay.py index e7f8454..7092154 100644 --- a/resources/lib/raiplay.py +++ b/resources/lib/raiplay.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- -import urllib2 +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 import json class RaiPlay: diff --git a/resources/lib/raiplayradio.py b/resources/lib/raiplayradio.py index 5ebd753..e47db9e 100644 --- a/resources/lib/raiplayradio.py +++ b/resources/lib/raiplayradio.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- -import urllib2 +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 import json import unicodedata diff --git a/resources/lib/relinker.py b/resources/lib/relinker.py index 31f7fc0..e9f4423 100644 --- a/resources/lib/relinker.py +++ b/resources/lib/relinker.py @@ -1,6 +1,12 @@ import urllib -import urllib2 -import urlparse +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 +try: + import urllib.parse as urlparse +except ImportError: + import urlparse class Relinker: # Firefox 52 on Android diff --git a/resources/lib/search.py b/resources/lib/search.py index 8c7ac35..ba2f630 100644 --- a/resources/lib/search.py +++ b/resources/lib/search.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- import json import urllib -import urllib2 +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 class Search: baseUrl = "http://www.rai.it" diff --git a/resources/lib/storageserverdummy.py b/resources/lib/storageserverdummy.py new file mode 100644 index 0000000..df2fc85 --- /dev/null +++ b/resources/lib/storageserverdummy.py @@ -0,0 +1,30 @@ +''' + StorageServer override. + Version: 1.0 +''' + + +class StorageServer: + def __init__(self, table, timeout=24): + return None + + def cacheFunction(self, funct=False, *args): + return funct(*args) + + def set(self, name, data): + return "" + + def get(self, name): + return "" + + def setMulti(self, name, data): + return "" + + def getMulti(self, name, items): + return "" + + def lock(self, name): + return False + + def unlock(self, name): + return False diff --git a/resources/lib/tgr.py b/resources/lib/tgr.py index fb47c2e..c7807dd 100644 --- a/resources/lib/tgr.py +++ b/resources/lib/tgr.py @@ -1,4 +1,7 @@ -import urllib2 +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 from xml.dom import minidom class TGR: From c045cf9503cd644cef898eb277d030cf7dc0b711 Mon Sep 17 00:00:00 2001 From: cttynul Date: Tue, 11 Jun 2019 18:45:47 +0200 Subject: [PATCH 02/10] fixing error in stream live channel --- default.py | 3 ++- resources/lib/relinker.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/default.py b/default.py index bfa8cce..aa509c0 100644 --- a/default.py +++ b/default.py @@ -155,7 +155,8 @@ def play(url, pathId="", srt=[]): xbmc.log("Media URL: " + url) # Play the item - item=xbmcgui.ListItem(path=url + '|User-Agent=' + urllib.quote_plus(Relinker.UserAgent)) + try: item=xbmcgui.ListItem(path=url + '|User-Agent=' + urllib.quote_plus(Relinker.UserAgent)) + except: item=xbmcgui.ListItem(path=url + '|User-Agent=' + urllib.parse.quote_plus(Relinker.UserAgent)) if len(srt) > 0: item.setSubtitles(srt) xbmcplugin.setResolvedUrl(handle=handle, succeeded=True, listitem=item) diff --git a/resources/lib/relinker.py b/resources/lib/relinker.py index e9f4423..64d6114 100644 --- a/resources/lib/relinker.py +++ b/resources/lib/relinker.py @@ -7,6 +7,10 @@ import urllib.parse as urlparse except ImportError: import urlparse +try: + from urllib.parse import urlencode +except: + from urllib import urlencode class Relinker: # Firefox 52 on Android @@ -42,14 +46,14 @@ def getURL(self, url): del(qs['output']) qs['output'] = "20" - query = urllib.urlencode(qs, True) + query = urlencode(qs, True) url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) response = urllib2.urlopen(url) mediaUrl = response.read().strip() # Workaround to normalize URL if the relinker doesn't - mediaUrl = urllib.quote(mediaUrl, safe="%/:=&?~#+!$,;'@()*[]") - + try: mediaUrl = urllib.quote(mediaUrl, safe="%/:=&?~#+!$,;'@()*[]") + except: mediaUrl = urllib.parse.quote(mediaUrl, safe="%/:=&?~#+!$,;'@()*[]") return mediaUrl From b13f7be6718b8d95b5acfa9623a565ac9b86234b Mon Sep 17 00:00:00 2001 From: cttynul Date: Tue, 11 Jun 2019 23:29:07 +0200 Subject: [PATCH 03/10] fixing strict python2 code --- default.py | 28 ++++++++++++++++++++-------- resources/lib/search.py | 6 ++++-- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/default.py b/default.py index aa509c0..032f4c0 100644 --- a/default.py +++ b/default.py @@ -78,10 +78,16 @@ def show_root_menu(): def show_tg_root(): search = Search() - for k, v in search.newsArchives.iteritems(): - liStyle = xbmcgui.ListItem(k) - addDirectoryItem({"mode": "get_last_content_by_tag", - "tags": search.newsArchives[k]}, liStyle) + try: + for k, v in search.newsArchives.iteritems(): + liStyle = xbmcgui.ListItem(k) + addDirectoryItem({"mode": "get_last_content_by_tag", + "tags": search.newsArchives[k]}, liStyle) + except: + for k, v in search.newsArchives.items(): + liStyle = xbmcgui.ListItem(k) + addDirectoryItem({"mode": "get_last_content_by_tag", + "tags": search.newsArchives[k]}, liStyle) liStyle = xbmcgui.ListItem("TGR", thumbnailImage="http://www.tgr.rai.it/dl/tgr/mhp/immagini/splash.png") addDirectoryItem({"mode": "tgr"}, liStyle) @@ -390,10 +396,16 @@ def search_ondemand_programmes(): def show_news_providers(): search = Search() - for k, v in search.newsProviders.iteritems(): - liStyle = xbmcgui.ListItem(k) - addDirectoryItem({"mode": "get_last_content_by_tag", - "tags": search.newsProviders[k]}, liStyle) + try: + for k, v in search.newsProviders.iteritems(): + liStyle = xbmcgui.ListItem(k) + addDirectoryItem({"mode": "get_last_content_by_tag", + "tags": search.newsProviders[k]}, liStyle) + except: + for k, v in search.newsProviders.items(): + liStyle = xbmcgui.ListItem(k) + addDirectoryItem({"mode": "get_last_content_by_tag", + "tags": search.newsProviders[k]}, liStyle) xbmcplugin.addSortMethod(handle, xbmcplugin.SORT_METHOD_LABEL) xbmcplugin.endOfDirectory(handle=handle, succeeded=True) diff --git a/resources/lib/search.py b/resources/lib/search.py index ba2f630..dce7e18 100644 --- a/resources/lib/search.py +++ b/resources/lib/search.py @@ -25,7 +25,8 @@ class Search: "Salute", "Satira", "Scienza", "Società", "Spettacolo", "Sport", "Storia", "Telefilm", "Tempo libero", "Viaggi"] def getLastContentByTag(self, tags="", numContents=16): - tags = urllib.quote(tags) + try: tags = urllib.quote(tags) + except: tags = urllib.parse.quote(tags) domain = "RaiTv" xsl = "rai_tv-statistiche-raiplay-json" @@ -35,7 +36,8 @@ def getLastContentByTag(self, tags="", numContents=16): return response["list"] def getMostVisited(self, tags, days=7, numContents=16): - tags = urllib.quote(tags) + try: tags = urllib.quote(tags) + except: tags = urllib.parse.quote(tags) domain = "RaiTv" xsl = "rai_tv-statistiche-raiplay-json" From c60e5ef677e8e11b4bd410703b5e7f2fea8e9f85 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 10 Jul 2019 21:45:16 +0200 Subject: [PATCH 04/10] Risolvi "Programmi on demand" vuoto --- default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default.py b/default.py index ebde2f6..203df0b 100644 --- a/default.py +++ b/default.py @@ -284,7 +284,7 @@ def show_ondemand_root(): raiplay = RaiPlay() items = raiplay.getMainMenu() for item in items: - if item["sub-type"] in ("RaiPlay Tipologia Page", "RaiPlay Genere Page"): + if item["sub-type"] in ("RaiPlay Tipologia Page", "RaiPlay Genere Page", "RaiPlay Tipologia Editoriale Page" ): liStyle = xbmcgui.ListItem(item["name"]) addDirectoryItem({"mode": "ondemand", "path_id": item["PathID"], "sub_type": item["sub-type"]}, liStyle) liStyle = xbmcgui.ListItem("Cerca") @@ -469,7 +469,7 @@ def log_country(): elif mode == "ondemand": if subType == "": show_ondemand_root() - elif subType in ("RaiPlay Tipologia Page", "RaiPlay Genere Page"): + elif subType in ("RaiPlay Tipologia Page", "RaiPlay Genere Page", "RaiPlay Tipologia Editoriale Page"): show_ondemand_programmes(pathId) elif subType == "Raiplay Tipologia Item": show_ondemand_list(pathId) From abfe7a1640a41c1c2ced988b4aa586359fdc7de1 Mon Sep 17 00:00:00 2001 From: cttynul Date: Wed, 24 Jul 2019 04:23:21 +0200 Subject: [PATCH 05/10] fix error in ondemand search --- default.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/default.py b/default.py index 032f4c0..d479c33 100644 --- a/default.py +++ b/default.py @@ -382,7 +382,8 @@ def search_ondemand_programmes(): kb.setHeading("Cerca un programma") kb.doModal() if kb.isConfirmed(): - name = kb.getText().decode('utf8') + try: name = kb.getText().decode('utf8').lower() + except: name = kb.getText().lower() xbmc.log("Searching for programme: " + name) raiplay = RaiPlay() dir = raiplay.getProgrammeList(raiplay.AzTvShowPath) From 53cd803e70398793d2299a5d4f7b33081ea6f234 Mon Sep 17 00:00:00 2001 From: cttynul Date: Wed, 2 Oct 2019 21:14:22 +0200 Subject: [PATCH 06/10] Fix new palinsesto URL --- resources/lib/raiplay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/raiplay.py b/resources/lib/raiplay.py index 7092154..c2ecb42 100644 --- a/resources/lib/raiplay.py +++ b/resources/lib/raiplay.py @@ -17,7 +17,7 @@ class RaiPlay: channelsUrl = "http://www.rai.it/dl/RaiPlay/2016/PublishingBlock-9a2ff311-fcf0-4539-8f8f-c4fee2a71d58.html?json" localizeUrl = "http://mediapolisgs.rai.it/relinker/relinkerServlet.htm?cont=201342" menuUrl = "http://www.rai.it/dl/RaiPlay/2016/menu/PublishingBlock-20b274b1-23ae-414f-b3bf-4bdc13b86af2.html?homejson" - palinsestoUrl = "https://www.raiplay.it/dl/palinsesti/Page-e120a813-1b92-4057-a214-15943d95aa68-json.html?canale=[nomeCanale]&giorno=[dd-mm-yyyy]" + palinsestoUrl = "https://www.raiplay.it/palinsesto/app/old/[nomeCanale]/[dd-mm-yyyy].json" AzTvShowPath = "/dl/RaiTV/RaiPlayMobile/Prod/Config/programmiAZ-elenco.json" def __init__(self): @@ -95,4 +95,4 @@ def getThumbnailUrl(self, pathId): url = self.getUrl(pathId) url = url.replace("[RESOLUTION]", "256x-") return url - \ No newline at end of file + From 28e100075876c0243a1de2dbd0fc4cf7af4c0275 Mon Sep 17 00:00:00 2001 From: cttynul Date: Wed, 2 Oct 2019 21:15:28 +0200 Subject: [PATCH 07/10] Update addon.xml --- addon.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index ff1e41f..9421201 100644 --- a/addon.xml +++ b/addon.xml @@ -1,8 +1,8 @@ + version="3.0.1" + provider-name="Nightflyer, cttynul, maxbambi"> From d3ad285ab942006fb44de9a900f49e28376b7b29 Mon Sep 17 00:00:00 2001 From: fabpolli Date: Thu, 3 Oct 2019 15:31:57 +0200 Subject: [PATCH 08/10] Recupero palinsesto da html Aggiunto indirizzo e funzione per recupero palinsesto giornaliero da pagina html --- default.py | 106 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 32 deletions(-) diff --git a/default.py b/default.py index 81525e2..ff9d243 100644 --- a/default.py +++ b/default.py @@ -22,6 +22,7 @@ from resources.lib.raiplayradio import RaiPlayRadio from resources.lib.relinker import Relinker import resources.lib.utils as utils +import re # plugin constants __plugin__ = "plugin.video.raitv" @@ -222,38 +223,79 @@ def show_replay_tv_epg(date, channelId): xbmc.log("Showing EPG for " + channelId + " on " + date) raiplay = RaiPlay() programmes = raiplay.getProgrammes(channelId, date) - - for programme in programmes: - if not programme: - continue - - startTime = programme["timePublished"] - title = programme["name"] - - if programme["images"]["landscape"] != "": - thumb = raiplay.getThumbnailUrl(programme["images"]["landscape"]) - elif programme["isPartOf"] and programme["isPartOf"]["images"]["landscape"] != "": - thumb = raiplay.getThumbnailUrl(programme["isPartOf"]["images"]["landscape"]) - else: - thumb = raiplay.noThumbUrl - - if programme["hasVideo"]: - videoUrl = programme["pathID"] - else: - videoUrl = None - - if videoUrl is None: - # programme is not available - liStyle = xbmcgui.ListItem(startTime + " [I]" + title + "[/I]", - thumbnailImage=thumb) - liStyle.setInfo("video", {}) - addLinkItem({"mode": "nop"}, liStyle) - else: - liStyle = xbmcgui.ListItem(startTime + " " + title, - thumbnailImage=thumb) - liStyle.setInfo("video", {}) - addLinkItem({"mode": "play", - "path_id": videoUrl}, liStyle) + if(programmes): + for programme in programmes: + if not programme: + continue + + startTime = programme["timePublished"] + title = programme["name"] + + if programme["images"]["landscape"] != "": + thumb = raiplay.getThumbnailUrl(programme["images"]["landscape"]) + elif programme["isPartOf"] and programme["isPartOf"]["images"]["landscape"] != "": + thumb = raiplay.getThumbnailUrl(programme["isPartOf"]["images"]["landscape"]) + else: + thumb = raiplay.noThumbUrl + + if programme["hasVideo"]: + videoUrl = programme["pathID"] + else: + videoUrl = None + + if videoUrl is None: + # programme is not available + liStyle = xbmcgui.ListItem(startTime + " [I]" + title + "[/I]", + thumbnailImage=thumb) + liStyle.setInfo("video", {}) + addLinkItem({"mode": "nop"}, liStyle) + else: + liStyle = xbmcgui.ListItem(startTime + " " + title, + thumbnailImage=thumb) + liStyle.setInfo("video", {}) + addLinkItem({"mode": "play", + "path_id": videoUrl}, liStyle) + else: + response = raiplay.getProgrammesHtml(channelId, date) + programmes = re.findall('()', response) + for i in programmes: + icon = re.findall('''data-img=['"]([^'^"]+?)['"]''', i) + if icon: + icon = raiplay.getUrl(icon[0]) + else: + icon ='' + + title = re.findall("

([^<]+?)

", i) + if title: + title = title[0] + else: + title = '' + + startTime = re.findall("

([^<]+?)

", i) + if startTime: + title = startTime[0] + " " + title + + desc = re.findall("

([^<]+?)

", i, re.S) + if desc: + desc= desc[0] + else: + desc="" + + videoUrl = re.findall('''data-href=['"]([^'^"]+?)['"]''', i) + + if not videoUrl: + # programme is not available + liStyle = xbmcgui.ListItem(" [I]" + title + "[/I]", thumbnailImage = icon) + liStyle.setInfo("video", {}) + addLinkItem({"mode": "nop"}, liStyle) + else: + videoUrl = videoUrl[0] + if not videoUrl.endswith('json'): + videoUrl = videoUrl + "?json" + + liStyle = xbmcgui.ListItem(title, thumbnailImage = icon ) + liStyle.setInfo("video", {}) + addLinkItem({"mode": "play", "path_id": videoUrl}, liStyle) xbmcplugin.endOfDirectory(handle=handle, succeeded=True) def show_replay_radio_epg(date, channelId): From 9f6a7ed117b7c537d4a7e05f874455163d99548c Mon Sep 17 00:00:00 2001 From: fabpolli Date: Thu, 3 Oct 2019 15:33:19 +0200 Subject: [PATCH 09/10] Recupero palinsesto da pagina html Aggiunta cattura eccezione in caso di JSON vuoto e tentativo di recupero delle informazioni del palinsesto a partire dalla pagina html --- resources/lib/raiplay.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/resources/lib/raiplay.py b/resources/lib/raiplay.py index c2ecb42..0d97588 100644 --- a/resources/lib/raiplay.py +++ b/resources/lib/raiplay.py @@ -4,6 +4,7 @@ except ImportError: import urllib2 import json +import re class RaiPlay: # Raiplay android app @@ -18,6 +19,7 @@ class RaiPlay: localizeUrl = "http://mediapolisgs.rai.it/relinker/relinkerServlet.htm?cont=201342" menuUrl = "http://www.rai.it/dl/RaiPlay/2016/menu/PublishingBlock-20b274b1-23ae-414f-b3bf-4bdc13b86af2.html?homejson" palinsestoUrl = "https://www.raiplay.it/palinsesto/app/old/[nomeCanale]/[dd-mm-yyyy].json" + palinsestoUrlHtml = "https://www.raiplay.it/palinsesto/guidatv/lista/[idCanale]/[dd-mm-yyyy].html" AzTvShowPath = "/dl/RaiTV/RaiPlayMobile/Prod/Config/programmiAZ-elenco.json" def __init__(self): @@ -43,8 +45,19 @@ def getProgrammes(self, channelName, epgDate): url = url.replace("[nomeCanale]", channelTag) url = url.replace("[dd-mm-yyyy]", epgDate) response = json.load(urllib2.urlopen(url)) - return response[channelName][0]["palinsesto"][0]["programmi"] - + try: + oRetVal = response[channelName][0]["palinsesto"][0]["programmi"] + except: + oRetVal = None + return oRetVal + + def getProgrammesHtml(self, channelName, epgDate): + channelTag = channelName.replace(" ", "-").lower() + url = self.palinsestoUrlHtml + url = url.replace("[idCanale]", channelTag) + url = url.replace("[dd-mm-yyyy]", epgDate) + return urllib2.urlopen(url).read() + def getMainMenu(self): response = json.load(urllib2.urlopen(self.menuUrl)) return response["menu"] From 443d7fb68470d5a128e3d49af5b4399cdd400e90 Mon Sep 17 00:00:00 2001 From: Federico Di Sante Date: Mon, 4 Nov 2019 16:41:40 +0100 Subject: [PATCH 10/10] Fix errore download dopo aggiornamento app RaiPlay. Fix errore download dopo aggiornamento app RaiPlay su Android, alla versione 3.0.2, del 2 novembre 2019. --- addon.xml | 2 +- default.py | 2 +- resources/lib/raiplay.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/addon.xml b/addon.xml index 9421201..f400112 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ diff --git a/default.py b/default.py index ff9d243..2890b8a 100644 --- a/default.py +++ b/default.py @@ -141,7 +141,7 @@ def play(url, pathId="", srt=[]): else: raiplay = RaiPlay() metadata = raiplay.getVideoMetadata(pathId) - url = metadata["contentUrl"] + url = metadata["content_url"] srtUrl = metadata["subtitles"] if srtUrl != "": diff --git a/resources/lib/raiplay.py b/resources/lib/raiplay.py index 0d97588..4494161 100644 --- a/resources/lib/raiplay.py +++ b/resources/lib/raiplay.py @@ -92,6 +92,7 @@ def getVideoMetadata(self, pathId): return response["video"] def getUrl(self, pathId): + pathId = pathId[:-9] + pathId[-9:].replace("html?json", "json") pathId = pathId.replace(" ", "%20") if pathId[0:2] == "//": url = "http:" + pathId