""" URLResolver Kodi module linksnappy plugin Copyright (C) 2019 twilight0 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 . """ import re #, traceback, sys from urllib import urlencode, quote import json from os.path import join, exists from os import remove import time from lib import helpers from urlresolver import common from urlresolver.common import i18n from urlresolver.resolver import UrlResolver, ResolverError logger = common.log_utils.Logger.get_logger(__name__) logger.disable() USER_AGENT = 'URLResolver for Kodi/{0}'.format(common.addon_version) base_url = 'https://linksnappy.com' api = '/'.join([base_url, 'api']) authenticate = '/'.join([api, 'AUTHENTICATE']) # filehosts = '/'.join([api, 'FILEHOSTS']) # Does not update itself as often as the one below filehosts = '/'.join([api, 'FILEHOSTSREALTIME']) regexarr = '/'.join([api, 'REGEXARR']) hostcachecheck = '/'.join([api, 'HOSTCACHECHECK?link={0}']) cachedlstatus = '/'.join([api, 'CACHEDLSTATUS?id={0}']) # user_details = '/'.join([api, 'USERDETAILS']) # Does not work well with cookies # torrents_genzip = '/'.join([torrents, 'GENZIP']) # provides a zip-file of a torrent, not used in urlresolver # toggle_download_log = '/'.join([api, 'TOGGLEDOWNLOADLOG?state={0}']) # can toggle download logs, state=on or state=off, not currently used in the plugin # regex = '/'.join([api, 'REGEX']) # single regex pattern, not particularly useful deletelink = '/'.join([api, 'DELETELINK']) linkgen = '/'.join([api, 'linkgen?genLinks={0}']) torrents = '/'.join([api, 'torrents']) torrents_addmagnet = '/'.join([torrents, 'ADDMAGNET?magnetlinks={0}']) torrents_addurl = '/'.join([torrents, 'ADDURL?url={0}']) torrents_start = '/'.join([torrents, 'START?tid={0}&fid={1}']) torrents_list = '/'.join([torrents, 'LIST']) torrents_files = '/'.join([torrents, 'FILES?id={0}']) torrents_folderlist = '/'.join([torrents, 'FOLDERLIST']) torrents_status = '/'.join([torrents, 'STATUS?tid={0}']) torrents_create = '/'.join([torrents, 'CREATEFOLDER?name={0}&dir={1}']) # add param dir for creating a subfolder torrents_rename = '/'.join([torrents, 'RENAMEFOLDER?name={0}&fid={1}']) torrents_deletefile = '/'.join([torrents, 'DELETEFILE?fid={0}']) torrents_delete = '/'.join([torrents, 'DELETETORRENT?tid={0}&delFiles={1}']) torrents_move = '/'.join([torrents, 'MOVETORRENTFILE?fid={0}&dir={1}']) torrents_links = '/'.join([torrents, 'DOWNLOADLINKS?fid={0}']) torrents_hashcheck = '/'.join([torrents, 'HASHCHECK?{0}']) folder_name = 'urlresolver' # noinspection PyBroadException class LinksnappyResolver(UrlResolver): name = "Linksnappy" domains = ["*"] media_url = None cookie_file = join(common.profile_path, '{0}.cookies'.format(name)) def __init__(self): self.hosts = None self.patterns = None self.net = common.Net() self.headers = {'User-Agent': USER_AGENT} if exists(self.cookie_file): try: expired = float(self.get_setting('expiration_timestamp')) < time.time() except ValueError: expired = True if expired: if self.authorize_resolver(): self.net.set_cookies(self.cookie_file) else: self.__update_timestamp() self.net.set_cookies(self.cookie_file) def get_media_url(self, host, media_id, cached_only=False): cached = self.__check_cache(media_id) if media_id.lower().startswith('magnet:') or '.torrent' in media_id.lower(): if cached: logger.log_debug('Linksnappy.com: "{0}" is readily available to stream'.format(media_id)) torrent_id = self.__create_transfer(media_id) else: if self.get_setting('cached_only') == 'true' or cached_only: raise ResolverError('Linksnappy.com: Cached torrents are only allowed to be initiated') else: logger.log_debug('Linksnappy.com: initiating transfer to files for "{0}"'.format(media_id)) torrent_id = self.__initiate_transfer(media_id) # self.__delete_folder() link = self.__direct_dl(media_id=torrent_id, torrent=True) # Disabled by default: # try: # # if link is not None: # # self.__clear_finished(torrent_id) # # except Exception: # # pass else: in_list = any(item in media_id for item in self.get_hosts()[1]) or any(item in host for item in self.get_hosts()[1]) if cached and in_list: logger.log_debug('Linksnappy.com: "{0}" is readily available to stream'.format(media_id)) elif (self.get_setting('cached_files_only') == 'true' or cached_only) and not cached and in_list: raise ResolverError('Linksnappy.com: Cached files from hosts are only allowed to be initiated') link = self.__direct_dl(media_id) if link is not None: logger.log_debug('Linksnappy.com: Successfully resolved url to "{0}"'.format(link)) return link + helpers.append_headers(self.headers) raise ResolverError('Link Not Found') def get_url(self, host, media_id): return media_id def get_host_and_id(self, url): return 'linksnappy.com', url @common.cache.cache_method(cache_limit=8) def get_hosts(self): _hosts = [] disabled = [] cached = [] try: if self.get_setting('torrents') == 'true': _hosts.extend([u'torrent', u'magnet']) response = self.net.http_GET(filehosts, headers=self.headers).content res = json.loads(response) if res.get('status') != 'OK': raise ResolverError('Server did not return hosts list') result = res.get('return').iteritems() for h, d in result: if d['Status'] == '0': disabled.append(h) continue elif h == 'mp3': continue elif d['iscacheable'] == 1: cached.append(h) _hosts.append(h) if 'alias' in d: for a in d['alias']: _hosts.append(a) logger.log_debug('Linksnappy.com available hosts: {0}'.format(_hosts)) logger.log_debug('Linksnappy.com hosts supporting cache: {0}'.format(cached)) if disabled: logger.log_debug('Linksnappy.com currently disabled hosts: {0}'.format(disabled)) return _hosts, cached except Exception as e: logger.log_error('Error getting Linksnappy hosts: {0}'.format(e)) return [], [] @common.cache.cache_method(cache_limit=8) def get_regexes(self): try: pattern = self.net.http_GET(regexarr, headers=self.headers).content json_object = json.loads(pattern) if json_object.get('error') is not False: raise Exception('Unexpected response received when attempting reading regex patterns') else: logger.log_debug('Linksnappy.com hosts patterns: {0}'.format(repr(json_object.get('return')))) regex_list = [re.compile(i[1:-1]) for i in json_object.get('return').values() if i] return regex_list except Exception as e: logger.log_error('Error getting Linksnappy regex pattern: {0}'.format(e)) return [] def valid_url(self, url, host): if url: if (url.lower().startswith('magnet:') or '.torrent' in url.lower()) and self.get_setting('torrents') == 'true': return True if self.patterns is None: self.patterns = self.get_regexes() for p in self.patterns: if p.search(url): return True elif host: if self.hosts is None: self.hosts = self.get_hosts()[0] if host.startswith('www.'): host = host.replace('www.', '') if any(item in host for item in self.hosts): return True return False def __check_cache(self, media_id): _url = media_id if _url.startswith('magnet:'): media_id = re.search(r'btih:(\w+)', media_id).group(1) else: media_id = quote(media_id) try: if _url.startswith('magnet:'): check_url = torrents_hashcheck.format(''.join(['hash=', media_id])) elif '.torrent' in _url: check_url = torrents_hashcheck.format(''.join(['url=', media_id])) else: check_url = hostcachecheck.format(media_id) res = self.net.http_GET(check_url, headers=self.headers).content result = json.loads(res) if result.get('status') == 'OK': return result.get('return') == 'CACHED' except Exception: logger.log_debug('Linksnappy.com failure on retrieving cache status') return False def __check_dl_status(self, hash_id): response = self.net.http_GET(cachedlstatus.format(hash_id), headers=self.headers).content result = json.loads(response) if result.get('status') != 'OK': raise ResolverError('Error occured when checking host transfer dl status') return result.get('return') def __create_transfer(self, media_id): try: if media_id.startswith('magnet:'): response = self.net.http_GET(torrents_addmagnet.format(quote(media_id)), headers=self.headers).content else: response = self.net.http_GET(torrents_addurl.format(quote(media_id)), headers=self.headers).content result = json.loads(response) if media_id.startswith('magnet:'): if result.get('status') == 'OK' and result.get('error') is False: torrent = result.get('return')[0] error = torrent.get('error') torrent_id = torrent.get('torrentid') if error: logger.log_debug('Linksnappy error at line 332: ' + error) else: raise ResolverError('Unexpected response received when attempting to add a torrent') else: if result.keys()[0].endswith('.torrent'): torrent_id = result.values()[0].get('torrentid') error = result.values()[0].get('error') if error: logger.log_debug('Linksnappy error at line 348:' + error) else: raise ResolverError('Unexpected response received when attempting to add a torrent') if torrent_id: logger.log_debug('Linksnappy.com: Added the following url for transfer {0}'.format(media_id)) folder_id = self.__create_folder() result = self.__start_transfer(torrent_id, folder_id) if result.get('error') is False: logger.log_debug('Linksnappy transfer with torrent id: "{0}" successfully started'.format(torrent_id)) else: logger.log_debug('Linksnappy transfer with torrent id "{0}" has the following error: {1}'.format(torrent_id, result.get('error'))) if result.get('error') == 'Magnet URI processing in progress. Please wait.': count = 1 while self.__start_transfer(torrent_id, folder_id).get('error') is not False: logger.log_debug('Waiting for Linksnappy transfer due to the following status: "{0}"'.format(torrent_id, result.get('error'))) common.kodi.sleep(3000) count += 1 if count == 8: raise ResolverError('Linksnappy torrens: Waited too long for transfer to start') return str(torrent_id) except Exception as e: logger.log_debug('Linksnappy error at __create_transfer: {0}'.format(e)) return '' def __start_transfer(self, torrent_id, folder_id): response = self.net.http_GET(torrents_start.format(torrent_id, folder_id), headers=self.headers).content result = json.loads(response) return result def __list_transfer(self, torrent_id): if torrent_id != '': try: response = self.net.http_GET(torrents_status.format(torrent_id), headers=self.headers).content result = json.loads(response).get('return') return result except Exception: pass return {} def __delete_transfer(self, transfer_id): if transfer_id != '': try: response = self.net.http_GET(torrents_delete.format(transfer_id, '1'), headers=self.headers).content result = json.loads(response) if result.get('status') == 'OK' and result.get('error') is False: logger.log_debug('Transfer ID "{0}" deleted from the Linksnappy files & torrents'.format(transfer_id)) return True except Exception: pass return False def __initiate_transfer(self, media_id, interval=5): torrent_id = self.__create_transfer(media_id) transfer_info = self.__list_transfer(torrent_id) if transfer_info: line1 = transfer_info.get('name') line2 = ''.join([i18n('download_rate'), ' ', str(transfer_info.get('downloadRate'))]) line3 = ''.join( [ i18n('peer_number'), ' ', str(transfer_info.get('getPeers')) ] ) with common.kodi.ProgressDialog('URLResolver Linksnappy transfer', line1, line2, line3) as pd: while transfer_info.get('status') != 'FINISHED': common.kodi.sleep(2000) transfer_info = self.__list_transfer(torrent_id) try: line1 = transfer_info.get('name') line2 = ''.join([i18n('download_rate'), ' ', str(transfer_info.get('downloadRate'))]) line3 = ''.join( [ i18n('peer_number'), ' ', str(transfer_info.get('getPeers')), ', ETA: ', str(transfer_info.get('eta')), ' ', i18n('seconds') ] ) logger.log_debug(line2) pd.update(int(transfer_info.get('percentDone')), line1=line1, line2=line2, line3=line3) except ValueError: pass if pd.is_canceled(): self.__delete_transfer(torrent_id) # self.__delete_folder() raise ResolverError('Transfer ID "{0}" canceled by user'.format(torrent_id)) else: logger.log_debug('Linksnappy.com: Transfer with id "{0}" completed!'.format(torrent_id)) common.kodi.sleep(1000 * interval) # allow api time to generate the stream_link return torrent_id def __direct_dl(self, media_id, torrent=False): try: if torrent: response = self.net.http_GET(torrents_files.format(media_id), headers=self.headers).content else: response = self.net.http_GET(linkgen.format(quote('{"link":"%s"}' % media_id)), headers=self.headers).content result = json.loads(response) if torrent: if result.get('status') == 'OK': _videos = [] def _search_tree(d): for k, v in d.items(): if isinstance(v, dict) and v.get('isVideo') != 'y': _search_tree(v) else: if isinstance(v, dict): _videos.append(v) _search_tree(result) try: link = max(_videos, key=lambda x: int(x.get('size'))).get('downloadLink', None) stream = self.net.http_GET(link, headers=self.headers).get_url() return stream except Exception: raise ResolverError('Failed to locate largest video file') else: raise ResolverError('Unexpected Response Received') else: stream = result.get('links')[0] if stream['status'] != 'OK': raise ResolverError('Link Not Found: {0}'.format(stream.get('error'))) elif stream['type'] != 'video': raise ResolverError( 'Generated link "{0}" does not contain a playable file'.format(stream.get('generated')) ) elif any(item in media_id for item in self.get_hosts()[1]): transfer_info = self.__check_dl_status(stream.get('hash')) if transfer_info.get('percent') != 100: line1 = stream.get('filename') line2 = stream.get('filehost') with common.kodi.ProgressDialog('URLResolver Linksnappy transfer', line1, line2) as pd: while self.__check_dl_status(stream.get('hash')).get('percent') != 100: common.kodi.sleep(2000) transfer_info = self.__check_dl_status(stream.get('hash')) try: logger.log_debug( 'Transfer with id "{0}" is still in progress, caching... active connections {1}, download speed {2}'.format( stream.get('hash'), transfer_info.get('connections'), transfer_info.get('downloadSpeed') ) ) except ValueError: pass try: line1 = stream.get('filename') line2 = stream.get('filehost') try: line3 = ''.join( [i18n('download_rate'), ' ', transfer_info.get('downloadSpeed')] ) pd.update(int(transfer_info.get('percent')), line1=line1, line2=line2, line3=line3) except ValueError: pd.update(int(transfer_info.get('percent')), line1=line1, line2=line2) except ValueError: pass if pd.is_canceled(): raise ResolverError('Transfer ID "{0}" canceled by user'.format(stream.get('hash'))) else: logger.log_debug('Transfer with id "{0}" completed'.format(stream.get('hash'))) pd.update(percent=100) return stream.get('generated') else: stream.get('generated') return stream.get('generated') except Exception as e: # _, __, tb = sys.exc_info() # # print traceback.print_tb(tb) logger.log_debug('Linksnappy, error at __direct_dl function: {0}'.format(e)) return None def __list_folders(self): try: response = self.net.http_GET(torrents_folderlist, headers=self.headers).content result = json.loads(response) if result.get('status') == 'OK' and not result.get('error'): for items in result.get("return")[::-1]: if items.get('type') == 'root' and (items.get('text') == folder_name or items.get('text') == 'Downloads'): return items.get('id', ''), items.get('text', '') except Exception: pass return '' def __create_folder(self): folder_id, f_name = self.__list_folders() if f_name == folder_name: return folder_id elif f_name == 'Downloads': try: response = self.net.http_GET(torrents_create.format(folder_name, folder_id), headers=self.headers).content result = json.loads(response) if result.get('status') == 'OK': logger.log_debug('Created new folder named "{0}" at Linksnappy files'.format(folder_name)) folder_id = result.get('return').get('id') return folder_id except Exception: pass return '' def __delete_folder(self): folder_id, f_name = self.__list_folders() if folder_id != '': try: response = self.net.http_GET(torrents_deletefile.format(folder_id), headers=self.headers).content result = json.loads(response) if 'status' in result: if result.get('status') == 'OK': logger.log_debug('Folder named "{0}" deleted from the Linksnappy files'.format(folder_name)) return True except Exception: pass return False def __clear_finished(self, torrent_id): try: result = self.net.http_GET(torrents_delete.format(torrent_id, '0'), headers=self.headers).content result = json.loads(result) if result.get('status') == 'OK': logger.log_debug('Successfully cleared torrent with id {0} from Linksnappy torrents'.format(torrent_id)) return True except Exception: pass return False def __update_timestamp(self): current_ts = time.time() expiration_timestamp = current_ts + 2592000.0 # cookies are valid for one month if no request is made self.set_setting('expiration_timestamp', expiration_timestamp) # SiteAuth methods def login(self): if not exists(self.cookie_file): self.authorize_resolver() def authorize_resolver(self): if exists(self.cookie_file): remove(self.cookie_file) if not self.get_setting('username') or not self.get_setting('password'): username = common.kodi.get_keyboard(i18n('username')) password = common.kodi.get_keyboard(i18n('password'), hide_input=True) if username and password: self.set_setting('username', username) self.set_setting('password', password) login_query = '?{0}'.format(urlencode({'username': username, 'password': password})) else: raise ResolverError('Linksnappy Error: {0}'.format('Did not provide both username and password')) else: login_query = '?{0}'.format( urlencode({'username': self.get_setting('username'), 'password': self.get_setting('password')}) ) response = self.net.http_GET(url=''.join([authenticate, login_query]), headers=self.headers).content res = json.loads(response) if 'OK' in res.get('status'): self.net.save_cookies(self.cookie_file) self.__update_timestamp() common.kodi.notify(msg=i18n('ls_authorized')) return True elif 'ERROR' in res.get('status'): self.set_setting('username', '') self.set_setting('password', '') remove(self.cookie_file) raise ResolverError('Linksnappy Error: {0}'.format(res.get('error'))) def reset_authorization(self): remove(self.cookie_file) self.set_setting('username', '') self.set_setting('password', '') self.set_setting('expiration_timestamp', '') @classmethod def _is_enabled(cls): return cls.get_setting('enabled') == 'true' and exists(cls.cookie_file) @classmethod def get_settings_xml(cls): xml = super(cls, cls).get_settings_xml() xml.append( ''.format( cls.__name__, i18n('username') ) ) xml.append( ''.format( cls.__name__, i18n('password') ) ) xml.append( ''.format( cls.__name__, i18n('auth_my_account') ) ) xml.append( ''.format( cls.__name__, i18n('reset_my_auth') ) ) xml.append( ''.format( cls.__name__, i18n('cached_files_only') ) ) xml.append( ''.format( cls.__name__, i18n('torrents') ) ) xml.append( ''.format( cls.__name__, i18n('cached_only') ) ) xml.append( ''.format( cls.__name__ ) ) return xml @classmethod def isUniversal(cls): return True