# -*- coding: utf-8 -*- '''* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. *''' import re import math import json import base64 import random import requests import traceback from commoncore import kodi from commoncore import dom_parser from commoncore.baseapi import DB_CACHABLE_API as CACHABLE_API, EXPIRE_TIMES from distutils.version import LooseVersion try: from urllib.parse import urlencode except ImportError: from urllib import urlencode import github from github import DB class githubException(Exception): pass default_branch = 'master' base_url = "https://api.github.com" content_url = "https://raw.githubusercontent.com/%s/%s/%s" page_limit = 100 def get_token(): dts= "" \ "NzUxOTg0NjE5YWY3MTEzNmY2ZDM2ZGM4NzFkNzcxY2FjYzZlZG" \ "ExNiBlNDQ5Zjc0MjM4ZTY1Mjc1ODIwYjk0Zjc1ZTAxOTE1Njhh" \ "NDI3M2Q2IDdiMjFkMTJkMzgzOGFiODYzYzlhMzNhNzZkN2MxZj" \ "k0N2M3NDFjMTcgNDhjMjlkN2FjMzUwMWRlM2JhYzU2ODk4NmI2" \ "NjJkZWM1MDYyZDA5YyA0NTY3NWVkMzBhMmNkNmQ5MTM4YzQwOT" \ "g3NGVhNzRlY2RjYWQ5ZDUzIDBmMmE1MzI0NWM5N2VmNjQ0MmFl" \ "NzA0ZjQwYTMzYzUxODdkNzhiNzkgMDNlMDUwOGQxYzE0YjQwZG" \ "ZmZmIwNDcwM2FlM2I3MzllNDZmMTQxNCAzM2RmZTk1OWRiNTYy" \ "OWIzMTlkY2U3NzhmYjQwNjkwM2FkYTA4ZjMyIDg1ZDBmMDhhND" \ "U5ZGM3YmQyNzg1YWJmMTU4M2NkNGVkYzBhM2YxNjMgM2U0NDY2" \ "ZTkwYTZhM2Y0NmU1OWU5ZjI4ZDAzNjE5ZmNiMWE4YTMyMiA4OT" \ "M4NWYyODg2OTA3NzA1ZWZmZGM4MjY1NmYxNmFhOTI4NDAzNjc1" \ "IDJiMmIzN2VkNmU5YjQ3MGNmMGVlMjljNzI2YzFjZjRhZGFiYT" \ "FlMmM=" return random.choice(base64.b64decode(dts).split()) SORT_ORDER = kodi.enum(REPO=0, FEED=1, INSTALLER=2, PLUGIN=3, PROGRAM=4, SKIN=5, SERVICE=6, SCRIPT=7, OTHER=100) #class GitHubWeb(CACHABLE_API): # default_return_type = 'text' # base_url = "https://github.com/search" class GitHubAPI(CACHABLE_API): default_return_type = 'json' base_url = "https://api.github.com" def prepair_request(self): kodi.sleep(random.randint(100, 250)) # random delay 50-250 ms def build_url(self, uri, query, append_base): if append_base: url = self.base_url + uri token = kodi.get_setting('access_token') if token: if query is None: query = {"access_token": token} else: query["access_token"] = token if query is not None: query = urlencode(query) for r in [('%3A', ":"), ("%2B", "+")]: f,t = r query = query.replace(f,t) url += '?' + query return url def handel_error(self, error, response, request_args, request_kwargs): if response.status_code == 401: traceback.print_stack() kodi.close_busy_dialog() raise githubException("Unauthorized: %s" % error) elif response.status_code == 403 and 'X-RateLimit-Reset' in response.headers: import time retry = int(response.headers['X-RateLimit-Reset']) - int(time.time()) for delay in range(retry, 0, -1): kodi.notify("API Rate limit exceeded", "Retry in %s seconds(s)" % delay, timeout=1000) kodi.sleep(1000) return self.request(*request_args, **request_kwargs) elif response.status_code == 422 and 'Only the first 1000' in response.text: kodi.handel_error('Result count exceeds API limit.', 'Try different search or result filter.') kodi.close_busy_dialog() traceback.print_stack() else: kodi.close_busy_dialog() traceback.print_stack() raise githubException("Status %s: %s" % (response.status_code, response.text)) def process_response(self, url, response, cache_limit, request_args, request_kwargs): if 'page' in request_kwargs['query']: page = request_kwargs['query']['page'] + 1 else: page = 1 results = response.json() total_count = float(results['total_count']) page_count = int(math.ceil(total_count / page_limit)) if page_count > 1 and page == 1: results = response.json() for p in range(page+1, int(page_count+1)): kodi.sleep(500) request_kwargs['query']['page'] = p temp = self.request(*request_args, **request_kwargs) results['items'] += temp['items'] self.cache_response(url, json.dumps(results), cache_limit) return results self.cache_response(url, response.text, cache_limit) return self.get_content(self.get_response(response)) GH = GitHubAPI() re_plugin = re.compile("^plugin\.", re.IGNORECASE) re_service = re.compile("^service\.", re.IGNORECASE) re_script = re.compile("^script\.", re.IGNORECASE) re_repository = re.compile("[\.\-_]?repo(sitory)?[\.\-_]?", re.IGNORECASE) re_feed = re.compile("(\.|-)*gitbrowser\.feed-", re.IGNORECASE) re_installer = re.compile("(\.|-)*gitbrowser\.installer-", re.IGNORECASE) re_program = re.compile("^(program\.)|(plugin\.program)", re.IGNORECASE) re_skin = re.compile("^skin\.", re.IGNORECASE) re_version = re.compile("-([^zip]+)\.zip$", re.IGNORECASE) re_split_version = re.compile("^(.+?)-([^zip]+)\.zip$") def is_zip(filename): return filename.lower().endswith('.zip') def split_version(name): try: match = re_split_version.search(name) addon_id, version = match.groups() return addon_id, version except: return False, False def get_download_url(full_name, path): url = content_url % (full_name, default_branch, path) if github.test_url(url): return url # didn't work, need to get the branch name response = requests.get("https://api.github.com/repos/%s/branches" % full_name).json() for attempt in response: branch = attempt['name'] url = content_url % (full_name, branch, path) if github.test_url(url): return url raise githubException('No available download link') def get_version_by_name(name): version = re_version.search(name) if version: return version.group(1) else: return '0.0.0' def get_version_by_xml(xml): try: addon = xml.find('addon') version = addon['version'] except: return False def version_sort(name): v = re_version.search(name) if v: return LooseVersion(v.group(1)) else: return LooseVersion('0.0.0') def sort_results(results, limit=False): def highest_versions(results): last = "" final = [] for a in results: addon_id, version = split_version(a['name']) if addon_id == last: continue last = addon_id final.append(a) return final def sort_results(name): index = SORT_ORDER.OTHER version = get_version_by_name(name) version_index = LooseVersion(version) if re_program.search(name): index = SORT_ORDER.PROGRAM elif re_plugin.search(name): index = SORT_ORDER.PLUGIN elif re_repository.search(name): index = SORT_ORDER.REPO elif re_service.search(name): index = SORT_ORDER.SERVICE elif re_script.search(name): index = SORT_ORDER.SCRIPT elif re_feed.search(name): index = SORT_ORDER.FEED elif re_installer.search(name): index = SORT_ORDER.INSTALLER return index, name.lower(), version_index if limit: return highest_versions(sorted(results, key=lambda x:sort_results(x['name']), reverse=True)) else: return sorted(results, key=lambda x:sort_results(x['name']), reverse=False) def limit_versions(results): final = [] sorted_results = sort_results(results['items'], True) for a in sorted_results: if not is_zip(a['name']): continue a['is_feed'] = True if re_feed.search(a['name']) else False a['is_installer'] = True if re_installer.search(a['name']) else False a['is_repository'] = True if re_repository.search(a['name']) else False final.append(a) results['items'] = final return results def search(q, method=False): if method=='user': return GH.request("/search/repositories", query={"per_page": page_limit, "q": "user:%s" % q}, cache_limit=EXPIRE_TIMES.HOUR) elif method=='title': return GH.request("/search/repositories", query={"per_page": page_limit, "q": "in:name %s" % q}, cache_limit=EXPIRE_TIMES.HOUR) elif method == 'id': results = [] temp = GH.request("/search/code", query={"per_page": page_limit, "q": "in:path %s.zip" % q, "access_token": get_token()}, cache_limit=EXPIRE_TIMES.HOUR) for t in temp['items']: if re_version.search(t['name']): results.append(t) return results else: return GH.request("/search/repositories", query={"per_page": page_limit, "q": q}, cache_limit=EXPIRE_TIMES.HOUR) #def find_xml(full_name): # return GitHubWeb().request(content_url % (full_name, default_branch, 'addon.xml'), append_base=False) def find_zips(user, repo=None): filters = {'Repository': '*repository*.zip', 'Feed': '*gitbrowser.feed*.zip', 'Installer': '*gitbrowser.installer*.zip', 'Music Plugin': '*plugin.audio*.zip', 'Video Plugin': '*plugin.video*.zip', 'Script': '*script*.zip'} filter = kodi.get_property('search.filter') if filter in filters: q = filters[filter] else: q = '*.zip' if repo is None: results = limit_versions(GH.request("/search/code", query={"per_page": page_limit, "q":"user:%s filename:%s" % (user, q)}, cache_limit=EXPIRE_TIMES.HOUR)) else: results = limit_versions(GH.request("/search/code", query={"per_page": page_limit, "q":"user:%s repo:%s filename:%s" % (user, repo, q)}, cache_limit=EXPIRE_TIMES.HOUR)) return results def find_zip(user, addon_id): results = [] response = GH.request("/search/code", query={"q": "user:%s filename:%s*.zip" % (user, addon_id)}, cache_limit=EXPIRE_TIMES.HOUR) if response is None: return False, False, False if response['total_count'] > 0: test = re.compile("%s(-.+\.zip|\.zip)$" % addon_id, re.IGNORECASE) def sort_results(name): version = get_version_by_name(name) return LooseVersion(version) response['items'].sort(key=lambda k: sort_results(k['name']), reverse=True) for r in response['items']: if test.match(r['name']): url = get_download_url(r['repository']['full_name'], r['path']) version = get_version_by_name(r['path']) return url, r['name'], r['repository']['full_name'], version return False, False, False, False def browse_repository(url): import requests from commoncore import zipfile from commoncore.beautifulsoup import BeautifulSoup r = requests.get(url, stream=True) if kodi.strings.PY2: import StringIO zip_ref = zipfile.ZipFile(StringIO.StringIO(r.content)) else: from io import BytesIO zip_ref = zipfile.ZipFile(BytesIO(r.content)) for f in zip_ref.namelist(): if f.endswith('addon.xml'): xml = BeautifulSoup(zip_ref.read(f)) url = xml.find('info').text xml=BeautifulSoup(requests.get(url).text) return xml return False def install_feed(url, local=False): import requests from commoncore import zipfile if kodi.strings.PY2: from StringIO import StringIO as byte_reader else: from io import BytesIO as byte_reader from commoncore.beautifulsoup import BeautifulSoup if local: r = kodi.vfs.open(url, "r") if kodi.strings.PY2: zip_ref = zipfile.ZipFile(byte_reader(r.read())) else: zip_ref = zipfile.ZipFile(byte_reader(r.readBytes())) else: r = requests.get(url, stream=True) zip_ref = zipfile.ZipFile(byte_reader(r.content)) for f in zip_ref.namelist(): if f.endswith('.xml'): xml = BeautifulSoup(zip_ref.read(f)) return xml return False def batch_installer(url, local=False): import requests from commoncore import zipfile if kodi.strings.PY2: from StringIO import StringIO as byte_reader else: from io import BytesIO as byte_reader from commoncore.beautifulsoup import BeautifulSoup if local: r = kodi.vfs.open(url, "r") if kodi.strings.PY2: zip_ref = zipfile.ZipFile(byte_reader(r.read())) else: zip_ref = zipfile.ZipFile(byte_reader(r.readBytes())) else: r = requests.get(url, stream=True) zip_ref = zipfile.ZipFile(byte_reader(r.content)) xml = BeautifulSoup(zip_ref.read('manifest.xml')) return xml, zip_ref #def get_download(url): # r = GH.request(url, append_base=False) # return r['download_url']