""" URLResolver Addon for Kodi Copyright (C) 2016 t0mm0, tknorris, jsergio 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 import urllib2 import json 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() CLIENT_ID = 'X245A4XAIBGVM' USER_AGENT = 'URLResolver for Kodi/%s' % common.addon_version INTERVALS = 5 # seconds FORMATS = common.VIDEO_FORMATS STALLED = ['magnet_error', 'error', 'virus', 'dead'] rest_base_url = 'https://api.real-debrid.com/rest/1.0' oauth_base_url = 'https://api.real-debrid.com/oauth/v2' unrestrict_link_path = 'unrestrict/link' device_endpoint_path = 'device/code' token_endpoint_path = 'token' authorize_endpoint_path = 'auth' credentials_endpoint_path = 'device/credentials' hosts_regexes_path = 'hosts/regex' hosts_domains_path = 'hosts/domains' add_magnet_path = 'torrents/addMagnet' torrents_info_path = 'torrents/info' select_files_path = 'torrents/selectFiles' torrents_delete_path = 'torrents/delete' check_cache_path = 'torrents/instantAvailability' class RealDebridResolver(UrlResolver): name = "Real-Debrid" domains = ["*"] def __init__(self): self.net = common.Net() self.hosters = None self.hosts = None self.headers = {'User-Agent': USER_AGENT} def get_media_url(self, host, media_id, retry=False, cached_only=False): try: self.headers.update({'Authorization': 'Bearer %s' % self.get_setting('token')}) if media_id.lower().startswith('magnet:'): cached = self.__check_cache(media_id) if not cached and (self.get_setting('cached_only') == 'true' or cached_only): raise ResolverError('Real-Debrid: Cached torrents only allowed to be initiated') torrent_id = self.__add_magnet(media_id) if not torrent_id == "": torrent_info = self.__torrent_info(torrent_id) heading = 'URL Resolver Real-Debrid Transfer' line1 = torrent_info.get('filename') status = torrent_info.get('status') if status == 'magnet_conversion': line2 = 'Converting MAGNET...' line3 = '%s seeders' % torrent_info.get('seeders') _TIMEOUT = 100 # seconds with common.kodi.ProgressDialog(heading, line1, line2, line3) as cd: while status == 'magnet_conversion' and _TIMEOUT > 0: cd.update(_TIMEOUT, line1=line1, line3=line3) if cd.is_canceled(): self.__delete_torrent(torrent_id) raise ResolverError('Real-Debrid: Torrent ID %s canceled by user' % torrent_id) elif any(x in status for x in STALLED): self.__delete_torrent(torrent_id) raise ResolverError('Real-Debrid: Torrent ID %s has stalled | REASON: %s' % (torrent_id, status)) _TIMEOUT -= INTERVALS common.kodi.sleep(1000 * INTERVALS) torrent_info = self.__torrent_info(torrent_id) status = torrent_info.get('status') line1 = torrent_info.get('filename') line3 = '%s seeders' % torrent_info.get('seeders') if status == 'magnet_conversion': self.__delete_torrent(torrent_id) raise ResolverError('Real-Debrid Error: MAGNET Conversion exceeded time limit') if status == 'waiting_files_selection': _videos = [] for _file in torrent_info.get('files'): if any(_file.get('path').lower().endswith(x) for x in FORMATS): _videos.append(_file) try: _video = max(_videos, key=lambda x: x.get('bytes')) file_id = _video.get('id', 0) except ValueError: self.__delete_torrent(torrent_id) raise ResolverError('Real-Debrid Error: Failed to locate largest video file') file_selected = self.__select_file(torrent_id, file_id) if not file_selected: self.__delete_torrent(torrent_id) raise ResolverError('Real-Debrid Error: Failed to select file') else: torrent_info = self.__torrent_info(torrent_id) status = torrent_info.get('status') if not status == 'downloaded': file_size = round(float(_video.get('bytes')) / (1000 ** 3), 2) if cached: line2 = 'Getting torrent from the Real-Debrid Cloud' else: line2 = 'Saving torrent to the Real-Debrid Cloud' line3 = status with common.kodi.ProgressDialog(heading, line1, line2, line3) as pd: while not status == 'downloaded': common.kodi.sleep(1000 * INTERVALS) torrent_info = self.__torrent_info(torrent_id) line1 = torrent_info.get('filename') status = torrent_info.get('status') if status == 'downloading': line3 = 'Downloading %s GB @ %s mbps from %s peers, %s %% completed' % (file_size, round(float(torrent_info.get('speed')) / (1000**2), 2), torrent_info.get("seeders"), torrent_info.get('progress')) else: line3 = status logger.log_debug(line3) pd.update(int(float(torrent_info.get('progress'))), line1=line1, line3=line3) if pd.is_canceled(): self.__delete_torrent(torrent_id) raise ResolverError('Real-Debrid: Torrent ID %s canceled by user' % torrent_id) elif any(x in status for x in STALLED): self.__delete_torrent(torrent_id) raise ResolverError('Real-Debrid: Torrent ID %s has stalled | REASON: %s' % (torrent_id, status)) # xbmc.sleep(1000 * INTERVALS) # allow api time to generate the stream_link media_id = torrent_info.get('links')[0] self.__delete_torrent(torrent_id) if media_id.lower().startswith('magnet:'): self.__delete_torrent(torrent_id) # clean up just incase raise ResolverError('Real-Debrid Error: Failed to transfer torrent to/from the cloud') url = '%s/%s' % (rest_base_url, unrestrict_link_path) data = {'link': media_id} result = self.net.http_POST(url, form_data=data, headers=self.headers).content except urllib2.HTTPError as e: if not retry and e.code == 401: if self.get_setting('refresh'): self.refresh_token() return self.get_media_url(host, media_id, retry=True) else: self.reset_authorization() raise ResolverError('Real Debrid Auth Failed & No Refresh Token') else: try: js_result = json.loads(e.read()) if 'error' in js_result: msg = js_result['error'] else: msg = 'Unknown Error (1)' except: msg = 'Unknown Error (2)' raise ResolverError('Real Debrid Error: %s (%s)' % (msg, e.code)) except Exception as e: raise ResolverError('Unexpected Exception during RD Unrestrict: %s' % e) else: js_result = json.loads(result) links = [] link = self.__get_link(js_result) if link is not None: links.append(link) if 'alternative' in js_result: for alt in js_result['alternative']: link = self.__get_link(alt) if link is not None: links.append(link) return helpers.pick_source(links) def __check_cache(self, media_id): r = re.search('''magnet:.+?urn:([a-zA-Z0-9]+):([a-zA-Z0-9]+)''', media_id, re.I) if r: _hash, _format = r.group(2).lower(), r.group(1) try: url = '%s/%s/%s' % (rest_base_url, check_cache_path, _hash) result = self.net.http_GET(url, headers=self.headers).content js_result = json.loads(result) _hash_info = js_result.get(_hash, {}) if isinstance(_hash_info, dict): if len(_hash_info.get('rd')) > 0: logger.log_debug('Real-Debrid: %s is readily available to stream' % _hash) return _hash_info except Exception as e: common.logger.log_warning("Real-Debrid Error: CHECK CACHE | %s" % e) raise return {} def __torrent_info(self, torrent_id): try: url = '%s/%s/%s' % (rest_base_url, torrents_info_path, torrent_id) result = self.net.http_GET(url, headers=self.headers).content js_result = json.loads(result) return js_result except Exception as e: common.logger.log_warning("Real-Debrid Error: TORRENT INFO | %s" % e) raise def __add_magnet(self, media_id): try: url = '%s/%s' % (rest_base_url, add_magnet_path) data = {'magnet': media_id} result = self.net.http_POST(url, form_data=data, headers=self.headers).content js_result = json.loads(result) logger.log_debug('Real-Debrid: Sending MAGNET URL to the real-debrid cloud') return js_result.get('id', "") except Exception as e: common.logger.log_warning("Real-Debrid Error: ADD MAGNET | %s" % e) raise def __select_file(self, torrent_id, file_id): try: url = '%s/%s/%s' % (rest_base_url, select_files_path, torrent_id) data = {'files': file_id} self.net.http_POST(url, form_data=data, headers=self.headers) logger.log_debug('Real-Debrid: Selected file ID %s from Torrent ID %s to transfer' % (file_id, torrent_id)) return True except Exception as e: common.logger.log_warning("Real-Debrid Error: SELECT FILE | %s" % e) return False def __delete_torrent(self, torrent_id): try: url = '%s/%s/%s' % (rest_base_url, torrents_delete_path, torrent_id) self.net.http_DELETE(url, headers=self.headers) logger.log_debug('Real-Debrid: Torrent ID %s was removed from your active torrents' % torrent_id) return True except Exception as e: common.logger.log_warning("Real-Debrid Error: DELETE TORRENT | %s" % e) raise def __get_link(self, link): if 'download' in link: if 'quality' in link: label = '[%s] %s' % (link['quality'], link['download']) else: label = link['download'] return label, link['download'] # SiteAuth methods def login(self): if not self.get_setting('token'): self.authorize_resolver() def refresh_token(self): client_id = self.get_setting('client_id') client_secret = self.get_setting('client_secret') refresh_token = self.get_setting('refresh') logger.log_debug('Refreshing Expired Real Debrid Token: |%s|%s|' % (client_id, refresh_token)) if not self.__get_token(client_id, client_secret, refresh_token): # empty all auth settings to force a re-auth on next use self.reset_authorization() raise ResolverError('Unable to Refresh Real Debrid Token') def authorize_resolver(self): url = '%s/%s?client_id=%s&new_credentials=yes' % (oauth_base_url, device_endpoint_path, CLIENT_ID) js_result = json.loads(self.net.http_GET(url, headers=self.headers).content) line1 = 'Go to URL: %s' % (js_result['verification_url']) line2 = 'When prompted enter: %s' % (js_result['user_code']) with common.kodi.CountdownDialog('URL Resolver Real Debrid Authorization', line1, line2, countdown=120, interval=js_result['interval']) as cd: result = cd.start(self.__check_auth, [js_result['device_code']]) # cancelled if result is None: return return self.__get_token(result['client_id'], result['client_secret'], js_result['device_code']) def __get_token(self, client_id, client_secret, code): try: url = '%s/%s' % (oauth_base_url, token_endpoint_path) data = {'client_id': client_id, 'client_secret': client_secret, 'code': code, 'grant_type': 'http://oauth.net/grant_type/device/1.0'} self.set_setting('client_id', client_id) self.set_setting('client_secret', client_secret) logger.log_debug('Authorizing Real Debrid: %s' % client_id) js_result = json.loads(self.net.http_POST(url, data, headers=self.headers).content) logger.log_debug('Authorizing Real Debrid Result: |%s|' % js_result) self.set_setting('token', js_result['access_token']) self.set_setting('refresh', js_result['refresh_token']) return True except Exception as e: logger.log_debug('Real Debrid Authorization Failed: %s' % e) return False def __check_auth(self, device_code): try: url = '%s/%s?client_id=%s&code=%s' % (oauth_base_url, credentials_endpoint_path, CLIENT_ID, device_code) js_result = json.loads(self.net.http_GET(url, headers=self.headers).content) except Exception as e: logger.log_debug('Exception during RD auth: %s' % e) else: return js_result def reset_authorization(self): self.set_setting('client_id', '') self.set_setting('client_secret', '') self.set_setting('token', '') self.set_setting('refresh', '') def get_url(self, host, media_id): return media_id def get_host_and_id(self, url): return 'www.real-debrid.com', url @common.cache.cache_method(cache_limit=8) def get_all_hosters(self): hosters = [] try: url = '%s/%s' % (rest_base_url, hosts_regexes_path) js_result = json.loads(self.net.http_GET(url, headers=self.headers).content) regexes = [regex[1:-1].replace('\/', '/').rstrip('\\') for regex in js_result] logger.log_debug('RealDebrid hosters : %s' % regexes) hosters = [re.compile(regex, re.I) for regex in regexes] except Exception as e: logger.log_error('Error getting RD regexes: %s' % e) return hosters @common.cache.cache_method(cache_limit=8) def get_hosts(self): hosts = [] try: url = '%s/%s' % (rest_base_url, hosts_domains_path) hosts = json.loads(self.net.http_GET(url, headers=self.headers).content) if self.get_setting('torrents') == 'true': hosts.extend([u'torrent', u'magnet']) except Exception as e: logger.log_error('Error getting RD hosts: %s' % e) logger.log_debug('RealDebrid hosts : %s' % hosts) return hosts @classmethod def _is_enabled(cls): return cls.get_setting('enabled') == 'true' and cls.get_setting('token') def valid_url(self, url, host): logger.log_debug('in valid_url %s : %s' % (url, host)) if url: if url.lower().startswith('magnet:') and self.get_setting('torrents') == 'true': return True if self.hosters is None: self.hosters = self.get_all_hosters() for host in self.hosters: # logger.log_debug('RealDebrid checking host : %s' %str(host)) if re.search(host, url): logger.log_debug('RealDebrid Match found') return True elif host: if self.hosts is None: self.hosts = self.get_hosts() if host.startswith('www.'): host = host.replace('www.', '') if any(host in item for item in self.hosts): return True return False @classmethod def get_settings_xml(cls): xml = super(cls, cls).get_settings_xml() xml.append('' % (cls.__name__, i18n('torrents'))) xml.append('' % (cls.__name__, i18n('cached_only'))) xml.append('' % (cls.__name__, i18n('auto_primary_link'))) xml.append('' % (cls.__name__, i18n('auth_my_account'))) xml.append('' % (cls.__name__, i18n('reset_my_auth'))) xml.append('' % cls.__name__) xml.append('' % cls.__name__) xml.append('' % cls.__name__) xml.append('' % cls.__name__) return xml @classmethod def isUniversal(cls): return True