diff --git a/.gitignore b/.gitignore index f9e57c4f..80fb879f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ script.module.urlresolver/resources/settings.xml doc/build doc/resources .project -.pydevproject \ No newline at end of file +.pydevproject +/.settings +/.idea diff --git a/addon.xml b/addon.xml index 6260bc28..61b84e52 100644 --- a/addon.xml +++ b/addon.xml @@ -1,17 +1,13 @@ - - - - - - - - - all - Resolve common video host URL's to be playable in XBMC. - Resolve common video host URL's to be playable in XBMC, simplify addon development of video plugins requiring multi video hosts. - + + + + + + + + all + Resolve common video host URL's to be playable in XBMC/Kodi. + Resolve common video host URL's to be playable in XBMC/Kodi, simplify addon development of video plugins requiring multi video hosts. + diff --git a/changelog.txt b/changelog.txt index f540c236..4771de49 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,2 +1,252 @@ +[B]Version 2.10.0[/B] +- Code Base Changes: + - Removed dependency in addon.xml for Youtube, Gotham users would not be able to install as official youtube addon only exists in Helix+ repo +- Resolvers Added: + - Up2Stream + - Vid.gg + - Vkpass + - Stream2K +- Resolvers Fixed/Updated: + - ClicknUpload + - CloudyVideos + - OpenLoad + - Vidzi + - Vid.me + - Vk.com + - Vidlocker + - Youwatch + - FlashX + - NosVideo + - Promptfile + +[B]Version 2.9.0[/B] +- Code Base Changes: + - Move all exception handling out of resolvers, handled at higher level now + - Updated SolveMedia captcha handling +- Resolvers Added: + - XFileUpload + - ClicknUpload + - TusFiles + - Filepup +- Resolvers Fixed/Updated: + - 180Upload + - Divxstage + - Exashare + - Teramixer + - CloudyVideos + - MightyUpload + - Videomega + +[B]Version 2.8.0[/B] +- Code Base Changes: + - Speed enhancements - Further to changes made in 2.7.0 + - New routine to select appropriate host for given url + - Compile resolver settings into reduced number of tabs to avoid Helix 100 settings tab limit +- Resolvers Added: +- Resolvers Fixed/Updated: + - HugeFiles + - Bestreams + - VideoMega + - Vidplay + - Vidxden + - Letwatchus + - 180Upload + + +[B]Version 2.7.0[/B] +- Code Base Changes: + - Speed enhancements: + - Don't load into mem all resolvers on init, load only when used + - Only build settings.xml on first init after initial install or new version + - Verify final link doesn't give http error before returning +- Resolvers Added: + - RoyalVids + - VShare + - CloudyVideos + - Streamin.to +- Resolvers Fixed/Updated: + - MovReel + - BillionUploads + - Novamov + - Premiumize + - TheVideo.me + - VidSpot + - VeeHD + - Vidbull +- Removed dead resolvers + +[B]Version 2.6.0[/B] +- Code Base Changes: + - Allow host validation to work with universal resolvers +- Resolvers Added: + - Realvid + - Letwatch + - Speedvideo + - Videohut +- Resolvers Fixed/Updated: + - Vidbull + - VeeHD + - VODLocker (speed improvement) + - MightyUpload + - Exashare + - Tunepk + +[B]Version 2.5.0[/B] +- Added Teramixer +- Added Exashare +- Fixed 180Upload +- Fixed BillionUploads +- Fixed HugeFiles +- Fixed VidPlay +- Fixed MovDivx +- Fixed ShareSix +- Fixed Vodlocker +- Fixed AllMyVideos +- Fixed Played.To +- Small fixes to Sockshare captcha + +[B]Version 2.4.0[/B] +- Reverted back to using t0mm0.common as addon.common is creating naming issues + +[B]Version 2.3.0[/B] +- Added Cloudy +- Fixed Divxstage +- Fixed Ecostream +- Fixed HostingBulk +- Fixed Movshare +- Fixed TheFile +- Fixed Vidxden +- Updated Putlocker/Firedrive to check for more variations +- Converted URLResolver completely to use addon.common instead of t0mm0.common + +[B]Version 2.2.0[/B] +- Added CheeseStreams +- Added Play44 +- Added Bestreams +- Added FireDrive (renamed Putlocker) +- Added UploadCrazy +- Added VidCrazy +- Added Video44 +- Added VideoFun +- Added ViUp +- Added VidZur +- Added YourUpload +- Fixed BillionUploads +- Fixed Divxstage +- Fixed Ecostream +- Fixed HugeFiles +- Fixed Movzap +- Fixed NowVideo +- Fixed ShareSix +- Fixed YouWatch +- Updated Real-Debrid login methods +- Updated Vidhog for no wait time + +[B]Version 2.1.2[/B] +- Vidxden bugfix + +[B]Version 2.1.1[/B] +- New unwise class for new unpacking method some sites are now using +- Added VideoTanker +- Added NowVideo +- Added CastAmp - live streaming +- Updated Divxstage +- Updated Ecostream +- Updated Flashx +- Updated Movshare +- Updated Novamov +- Updated Realdebrid +- Updated Sharesix +- Updated Tunepk +- Updated Videoweed +- Updated BillionUploads +- Updated premiumize.me - check on if login exists +- Updated rpnet - check if login exists + +[B]Version 2.1.0[/B] +- New unresolvable() class for resolvers to return in case of an error, specify why it failed for addon to handle +- New redx.png graphic file for reporting issues in resolvers +- Updated all resolvers to display small box on exceptions +- Added DoneVideo +- Added EntroUpload +- Added LimeVideo +- Added MuchShare +- Added PureVid +- Added VideoZed +- Added VidTo +- Added YouWatch +- Added LemUploads +- Added MegaRelease +- Added NosVideo +- Added Vidto +- Added MightyUpload +- Added PrimeShare +- Added Vidplay +- Updated AllDebrid +- Fixed RealDebrid +- Fixed BillionUploads +- Fixed 180Upload +- Fixed HostingBulk +- Fixed Ecostream +- Fixed FlashX +- Fixed DaClips + +[B]Version 2.0.9[/B] +- Fixed VeeHD + +[B]Version 2.0.8[/B] +- Added Bayfiles +- Added CrunchyRoll +- Added Movreel +- Added Played +- Added RPNet +- Updated Sharesix +- Added TheFile +- Added Vureel +- Added WatchFreeInHD +- Fixed ZooUpload +- Added BillionUploads +- Added HugeFiles +- Added ShareRepo +- Added VidBull +- Fixed VeeHD +- Fixed VidHog + +[B]Version 2.0.7[/B] +- Vidxden: Fixed +- Added: Sharesix (humla) + +[B]Version 2.0.6[/B] +- Videoweed: Allow Videoweed.eu/files/1423541 type +- Divstage: Allow embedded urls to resolve +- Filenuke: Removed www. from regex causing 502 errors +- Xvidstage: Fixed +- Stream2k: Fixed + +[B]Version 2.0.5[/B] +- Added streamcloud +- Added zooupload +- Fixed divxstage - handle url with .net +- Fixed flashx.tv - new pattern and embed code +- Fixed vidstream - new url +- Fixed ecostream - new embed url pattern +- Reverted putlocker code, added option for higher vid quality + +[B]Version 2.0.4[/B] +- Frodo branch +- Updated putlocker and vidxden resolvers +- Updated real-debrid + +[B]Version 1.0.3[/B] +- Fixed putlocker + +[B]Version 1.0.2[/B] +- Fixed dailymotion +- Fixed gorillavid + +[B]Version 1.0.0[/B] +- Initial Release. + + [B]Version 1.0.0[/B] - Initial Release. diff --git a/fanart.jpg b/fanart.jpg new file mode 100644 index 00000000..70944043 Binary files /dev/null and b/fanart.jpg differ diff --git a/icon.png b/icon.png new file mode 100644 index 00000000..0575e073 Binary files /dev/null and b/icon.png differ diff --git a/lib/default.py b/lib/default.py new file mode 100644 index 00000000..91294ecd --- /dev/null +++ b/lib/default.py @@ -0,0 +1,69 @@ +""" + URLResolver Addon for Kodi + Copyright (C) 2016 t0mm0, tknorris + + 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 sys +from urlresolver.lib import kodi +from urlresolver.lib import log_utils +from urlresolver.lib import cache +from urlresolver.lib.url_dispatcher import URL_Dispatcher +url_dispatcher = URL_Dispatcher() + +def __enum(**enums): + return type('Enum', (), enums) + +MODES = __enum(AUTH_RD='auth_rd', RESET_RD='reset_rd', RESET_CACHE='reset_cache') + +@url_dispatcher.register(MODES.AUTH_RD) +def auth_rd(): + kodi.close_all() + kodi.sleep(500) # sleep or authorize won't work for some reason + from urlresolver.plugins import realdebrid + if realdebrid.RealDebridResolver().authorize_resolver(): + kodi.notify(msg=kodi.i18n('rd_authorized'), duration=5000) + +@url_dispatcher.register(MODES.RESET_RD) +def reset_rd(): + kodi.close_all() + kodi.sleep(500) # sleep or reset won't work for some reason + from urlresolver.plugins import realdebrid + rd = realdebrid.RealDebridResolver() + rd.reset_authorization() + kodi.notify(msg=kodi.i18n('rd_auth_reset'), duration=5000) + +@url_dispatcher.register(MODES.RESET_CACHE) +def reset_cache(): + if cache.reset_cache(): + kodi.notify(msg=kodi.i18n('cache_reset')) + else: + kodi.notify(msg=kodi.i18n('cache_reset_failed')) + +def main(argv=None): + if sys.argv: argv = sys.argv + queries = kodi.parse_query(sys.argv[2]) + log_utils.log('Version: |%s| Queries: |%s|' % (kodi.get_version(), queries)) + log_utils.log('Args: |%s|' % (argv)) + + # don't process params that don't match our url exactly. (e.g. plugin://plugin.video.1channel/extrafanart) + plugin_url = 'plugin://%s/' % (kodi.get_id()) + if argv[0] != plugin_url: + return + + mode = queries.get('mode', None) + url_dispatcher.dispatch(mode, queries) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/lib/urlresolver/__init__.py b/lib/urlresolver/__init__.py index bcf88286..9725156c 100644 --- a/lib/urlresolver/__init__.py +++ b/lib/urlresolver/__init__.py @@ -1,192 +1,301 @@ -# urlresolver XBMC Addon -# Copyright (C) 2011 t0mm0 -# -# 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 . +""" + URLResolver Addon for Kodi + Copyright (C) 2016 t0mm0, tknorris + 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 . +""" ''' This module provides the main API for accessing the urlresolver features. -For most cases you probably want to use :func:`urlresolver.resolve` or +For most cases you probably want to use :func:`urlresolver.resolve` or :func:`urlresolver.choose_source`. .. seealso:: - - :class:`HostedMediaFile` + + :class:`HostedMediaFile` ''' - +import re +import urlparse +import sys import os -import common -import plugnplay -from types import HostedMediaFile -from plugnplay.interfaces import UrlResolver -from plugnplay.interfaces import PluginSettings -from plugnplay.interfaces import SiteAuth import xbmcgui +import common +from hmf import HostedMediaFile +from urlresolver.resolver import UrlResolver +from urlresolver.plugins.__generic_resolver__ import GenericResolver +from plugins import * + +common.log_utils.log_notice('Initializing URLResolver version: %s' % (common.addon_version)) +MAX_SETTINGS = 75 + +PLUGIN_DIRS = [] +host_cache = {} + +def add_plugin_dirs(dirs): + global PLUGIN_DIRS + if isinstance(dirs, basestring): + PLUGIN_DIRS.append(dirs) + else: + PLUGIN_DIRS += dirs -#load all available plugins -plugnplay.set_plugin_dirs(common.plugins_path) -plugnplay.load_plugins() +def load_external_plugins(): + for d in PLUGIN_DIRS: + common.log_utils.log_debug('Adding plugin path: %s' % (d)) + sys.path.insert(0, d) + for filename in os.listdir(d): + if not filename.startswith('__') and filename.endswith('.py'): + mod_name = filename[:-3] + imp = __import__(mod_name, globals(), locals()) + sys.modules[mod_name] = imp + common.log_utils.log_debug('Loaded %s as %s from %s' % (imp, mod_name, filename)) + +def relevant_resolvers(domain=None, include_universal=None, include_external=False, include_disabled=False, order_matters=False): + if include_external: + load_external_plugins() + + if isinstance(domain, basestring): + domain = domain.lower() + + if include_universal is None: + include_universal = common.get_setting('allow_universal') == "true" + + classes = UrlResolver.__class__.__subclasses__(UrlResolver) + UrlResolver.__class__.__subclasses__(GenericResolver) + relevant = [] + for resolver in classes: + if include_disabled or resolver._is_enabled(): + if include_universal or not resolver.isUniversal(): + if domain is None or ((domain and any(domain in res_domain.lower() for res_domain in resolver.domains)) or '*' in resolver.domains): + relevant.append(resolver) + + if order_matters: + relevant.sort(key=lambda x: x._get_priority()) + + common.log_utils.log_debug('Relevant Resolvers: %s' % (relevant)) + return relevant def resolve(web_url): ''' Resolve a web page to a media stream. - + It is usually as simple as:: - + import urlresolver - media_url = urlresolver.resolve(web_url) - - where ``web_url`` is the address of a web page which is associated with a - media file and ``media_url`` is the direct URL to the media. + media_url = urlresolver.resolve(web_url) + + where ``web_url`` is the address of a web page which is associated with a + media file and ``media_url`` is the direct URL to the media. - Behind the scenes, :mod:`urlresolver` will check each of the available - resolver plugins to see if they accept the ``web_url`` in priority order - (lowest priotity number first). When it finds a plugin willing to resolve - the URL, it passes the ``web_url`` to the plugin and returns the direct URL + Behind the scenes, :mod:`urlresolver` will check each of the available + resolver plugins to see if they accept the ``web_url`` in priority order + (lowest priotity number first). When it finds a plugin willing to resolve + the URL, it passes the ``web_url`` to the plugin and returns the direct URL to the media file, or ``False`` if it was not possible to resolve. - - .. seealso:: - - :class:`HostedMediaFile` + + .. seealso:: + + :class:`HostedMediaFile` Args: web_url (str): A URL to a web page associated with a piece of media content. - + Returns: - If the ``web_url`` could be resolved, a string containing the direct - URL to the media file, if not, returns ``False``. + If the ``web_url`` could be resolved, a string containing the direct + URL to the media file, if not, returns ``False``. ''' source = HostedMediaFile(url=web_url) return source.resolve() def filter_source_list(source_list): ''' - Takes a list of :class:`HostedMediaFile`s representing web pages that are - thought to be associated with media content. If no resolver plugins exist - to resolve a :class:`HostedMediaFile` to a link to a media file it is + Takes a list of :class:`HostedMediaFile`s representing web pages that are + thought to be associated with media content. If no resolver plugins exist + to resolve a :class:`HostedMediaFile` to a link to a media file it is removed from the list. - + Args: - urls (list of :class:`HostedMediaFile`): A list of - :class:`HostedMediaFiles` representing web pages that are thought to be + urls (list of :class:`HostedMediaFile`): A list of + :class:`HostedMediaFiles` representing web pages that are thought to be associated with media content. - + Returns: - The same list of :class:`HostedMediaFile` but with any that can't be + The same list of :class:`HostedMediaFile` but with any that can't be resolved by a resolver plugin removed. - + ''' return [source for source in source_list if source] def choose_source(sources): ''' - Given a list of :class:`HostedMediaFile` representing web pages that are - thought to be associated with media content this function checks which are - playable and if there are more than one it pops up a dialog box displaying + Given a list of :class:`HostedMediaFile` representing web pages that are + thought to be associated with media content this function checks which are + playable and if there are more than one it pops up a dialog box displaying the choices. - + Example:: - + sources = [HostedMediaFile(url='http://youtu.be/VIDEOID', title='Youtube [verified] (20 views)'), HostedMediaFile(url='http://putlocker.com/file/VIDEOID', title='Putlocker (3 views)')] - source = urlresolver.choose_source(sources) - if source: - stream_url = source.resolve() - addon.resolve_url(stream_url) - else: - addon.resolve_url(False) + source = urlresolver.choose_source(sources) + if source: + stream_url = source.resolve() + addon.resolve_url(stream_url) + else: + addon.resolve_url(False) Args: - sources (list): A list of :class:`HostedMediaFile` representing web + sources (list): A list of :class:`HostedMediaFile` representing web pages that are thought to be associated with media content. - + Returns: - The chosen :class:`HostedMediaFile` or ``False`` if the dialog is - cancelled or none of the :class:`HostedMediaFile` are resolvable. - + The chosen :class:`HostedMediaFile` or ``False`` if the dialog is + cancelled or none of the :class:`HostedMediaFile` are resolvable. + ''' - #get rid of sources with no resolver plugin sources = filter_source_list(sources) - - #show dialog to choose source - if len(sources) > 1: + if not sources: + common.log_utils.log_warning('no playable streams found') + return False + elif len(sources) == 1: + return sources[0] + else: dialog = xbmcgui.Dialog() - titles = [] - for source in sources: - titles.append(source.title) - index = dialog.select('Choose your stream', titles) + index = dialog.select('Choose your stream', [source.title for source in sources]) if index > -1: return sources[index] else: return False + +def scrape_supported(html, regex=None, host_only=False): + ''' + returns a list of links scraped from the html that are supported by urlresolver - #only one playable source so just play it - elif len(sources) == 1: - return sources[0] + args: + html: the html to be scraped + regex: an optional argument to override the default regex which is: href\s*=\s*["']([^'"]+ + host_only: an optional argument if true to do only host validation vs full url validation (default False) - #no playable sources available - else: - common.addon.log_error('no playable streams found') - return False + Returns: + a list of links scraped from the html that passed validation + ''' + if regex is None: regex = '''href\s*=\s*['"]([^'"]+)''' + links = [] + for match in re.finditer(regex, html): + stream_url = match.group(1) + host = urlparse.urlparse(stream_url).hostname + if host_only: + if host is None: + continue + + if host in host_cache: + if host_cache[host]: + links.append(stream_url) + continue + else: + hmf = HostedMediaFile(host=host, media_id='dummy') # use dummy media_id to allow host validation + else: + hmf = HostedMediaFile(url=stream_url) + is_valid = hmf.valid_url() + host_cache[host] = is_valid + if is_valid: + links.append(stream_url) + return links + def display_settings(): ''' Opens the settings dialog for :mod:`urlresolver` and its plugins. - - This can be called from your addon to provide access to global - :mod:`urlresolver` settings. Each resolver plugin is also capable of + + This can be called from your addon to provide access to global + :mod:`urlresolver` settings. Each resolver plugin is also capable of exposing settings. - + .. note:: - - All changes made to these setting by the user are global and will + + All changes made to these setting by the user are global and will affect any addon that uses :mod:`urlresolver` and its plugins. ''' _update_settings_xml() - common.addon.show_settings() - - + common.open_settings() + def _update_settings_xml(): ''' This function writes a new ``resources/settings.xml`` file which contains all settings for this addon and its plugins. ''' try: - try: - os.makedirs(os.path.dirname(common.settings_file)) - except OSError: - pass + os.makedirs(os.path.dirname(common.settings_file)) + except OSError: + pass + + new_xml = [ + '', + '', + '\t', + '\t\t' % (common.i18n('enable_universal')), + '\t\t' % (common.i18n('auto_pick')), + '\t\t' % (common.i18n('use_function_cache')), + '\t\t' % (common.i18n('reset_function_cache')), + '\t\t', + '\t', + '\t' % (common.i18n('universal_resolvers'))] + + resolvers = relevant_resolvers(include_universal=True, include_disabled=True) + resolvers = sorted(resolvers, key=lambda x: x.name.upper()) + for resolver in resolvers: + if resolver.isUniversal(): + new_xml.append('\t\t' % (resolver.name)) + new_xml += ['\t\t' + line for line in resolver.get_settings_xml()] + new_xml.append('\t') + new_xml.append('\t' % (common.i18n('resolvers'))) + + i = 0 + cat_count = 2 + for resolver in resolvers: + if not resolver.isUniversal(): + if i > MAX_SETTINGS: + new_xml.append('\t') + new_xml.append('\t' % (common.i18n('resolvers'), cat_count)) + cat_count += 1 + i = 0 + new_xml.append('\t\t' % (resolver.name)) + res_xml = resolver.get_settings_xml() + new_xml += ['\t\t' + line for line in res_xml] + i += len(res_xml) + 1 + + new_xml.append('\t') + new_xml.append('') - f = open(common.settings_file, 'w') + try: + with open(common.settings_file, 'r') as f: + old_xml = f.read() + except: + old_xml = '' + + new_xml = '\n'.join(new_xml) + if old_xml != new_xml: + common.log_utils.log_debug('Updating Settings XML') try: - f.write('\n') - f.write('\n') - for imp in PluginSettings.implementors(): - f.write('\n' % imp.name) - f.write(imp.get_settings_xml()) - f.write('\n') - f.write('') - finally: - f.close - except IOError: - common.addon.log_error('error writing ' + common.settings_file) - - -#make sure settings.xml is up to date + with open(common.settings_file, 'w') as f: + f.write(new_xml) + except: + raise + else: + common.log_utils.log_debug('No Settings Update Needed') + _update_settings_xml() diff --git a/lib/urlresolver/common.py b/lib/urlresolver/common.py index 119016db..ae810904 100644 --- a/lib/urlresolver/common.py +++ b/lib/urlresolver/common.py @@ -15,17 +15,84 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import os +import hashlib +from lib import log_utils # @UnusedImport +from lib.net import Net, get_ua # @UnusedImport +from lib import cache # @UnusedImport +from lib import kodi +from lib import pyaes -import os -from t0mm0.common.addon import Addon -import xbmc -import xbmcaddon -import xbmcgui -import xbmcplugin - -addon = Addon('script.module.urlresolver') -addon_path = addon.get_path() +addon_path = kodi.get_path() plugins_path = os.path.join(addon_path, 'lib', 'urlresolver', 'plugins') -profile_path = addon.get_profile() +profile_path = kodi.translate_path(kodi.get_profile()) settings_file = os.path.join(addon_path, 'resources', 'settings.xml') +addon_version = kodi.get_version() +get_setting = kodi.get_setting +set_setting = kodi.set_setting +open_settings = kodi.open_settings +has_addon = kodi.has_addon +i18n = kodi.i18n + +RAND_UA = get_ua() +IE_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko' +FF_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0' +OPERA_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 OPR/34.0.2036.50' +IOS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25' +ANDROID_USER_AGENT = 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36' +SMU_USER_AGENT = 'URLResolver for Kodi/%s' % (addon_version) + +def log_file_hash(path): + try: + with open(path, 'r') as f: + py_data = f.read() + except: + py_data = '' + + log_utils.log('%s hash: %s' % (os.path.basename(path), hashlib.md5(py_data).hexdigest())) + +def file_length(py_path, key=''): + try: + with open(py_path, 'r') as f: + old_py = f.read() + if key: + old_py = encrypt_py(old_py, key) + old_len = len(old_py) + except: + old_len = -1 + + return old_len + +def decrypt_py(cipher_text, key): + if cipher_text: + try: + scraper_key = hashlib.sha256(key).digest() + IV = '\0' * 16 + decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(scraper_key, IV)) + plain_text = decrypter.feed(cipher_text) + plain_text += decrypter.feed() + if 'import' not in plain_text: + plain_text = '' + except Exception as e: + log_utils.log_warning('Exception during Py Decrypt: %s' % (e)) + plain_text = '' + else: + plain_text = '' + + return plain_text + +def encrypt_py(plain_text, key): + if plain_text: + try: + scraper_key = hashlib.sha256(key).digest() + IV = '\0' * 16 + decrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(scraper_key, IV)) + cipher_text = decrypter.feed(plain_text) + cipher_text += decrypter.feed() + except Exception as e: + log_utils.log_warning('Exception during Py Encrypt: %s' % (e)) + cipher_text = '' + else: + cipher_text = '' + return cipher_text diff --git a/lib/urlresolver/hmf.py b/lib/urlresolver/hmf.py new file mode 100644 index 00000000..44dcb075 --- /dev/null +++ b/lib/urlresolver/hmf.py @@ -0,0 +1,296 @@ +""" + URLResolver Addon for Kodi + Copyright (C) 2016 t0mm0, tknorris + + 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 urllib2 +import urlparse +import re +import urllib +import traceback +import urlresolver +from urlresolver import common + +resolver_cache = {} + +class HostedMediaFile: + ''' + This class represents a piece of media (file or stream) that is hosted + somewhere on the internet. It may be instantiated with EITHER the url to the + web page associated with the media file, OR the host name and a unique + ``media_id`` used by the host to point to the media. + + For example:: + + HostedMediaFile(url='http://youtube.com/watch?v=ABC123XYZ') + + represents the same piece of media as:: + + HostedMediaFile(host='youtube.com', media_id='ABC123XYZ') + + ``title`` is a free text field useful for display purposes such as in + :func:`choose_source`. + + .. note:: + + If there is no resolver plugin to handle the arguments passed, + the resulting object will evaluate to ``False``. Otherwise it will + evaluate to ``True``. This is a handy way of checking whether + a resolver exists:: + + hmf = HostedMediaFile('http://youtube.com/watch?v=ABC123XYZ') + if hmf: + print 'yay! we can resolve this one' + else: + print 'sorry :( no resolvers available to handle this one.') + + .. warning:: + + If you pass ``url`` you must not pass ``host`` or ``media_id``. You + must pass either ``url`` or ``host`` AND ``media_id``. + ''' + + def __init__(self, url='', host='', media_id='', title='', include_disabled=False, include_universal=None): + ''' + Args: + url (str): a URL to a web page that represents a piece of media. + host (str): the host of the media to be represented. + media_id (str): the unique ID given to the media by the host. + ''' + if not url and not (host and media_id) or (url and (host or media_id)): + raise ValueError('Set either url, or host AND media_id. No other combinations are valid.') + self._url = url + self._host = host + self._media_id = media_id + self._valid_url = None + self.title = title if title else self._host + + if self._url: + self._domain = self.__top_domain(self._url) + else: + self._domain = self.__top_domain(self._host) + + self.__resolvers = self.__get_resolvers(include_disabled, include_universal) + if not url: + for resolver in self.__resolvers: # Find a valid URL + try: + if not resolver.isUniversal() and resolver.get_url(host, media_id): + self._url = resolver.get_url(host, media_id) + break + except: + # Shity resolver. Ignore + continue + + def __get_resolvers(self, include_disabled, include_universal): + if include_universal is None: + include_universal = common.get_setting('allow_universal') == "true" + + klasses = urlresolver.relevant_resolvers(self._domain, include_universal=include_universal, + include_external=True, include_disabled=include_disabled, order_matters=True) + resolvers = [] + for klass in klasses: + if klass in resolver_cache: + common.log_utils.log_debug('adding resolver from cache: %s' % (klass)) + resolvers.append(resolver_cache[klass]) + else: + common.log_utils.log_debug('adding resolver to cache: %s' % (klass)) + resolver_cache[klass] = klass() + resolvers.append(resolver_cache[klass]) + return resolvers + + def __top_domain(self, url): + elements = urlparse.urlparse(url) + domain = elements.netloc or elements.path + domain = domain.split('@')[-1].split(':')[0] + regex = "(\w{2,}\.\w{2,3}\.\w{2}|\w{2,}\.\w{2,3})$" + res = re.search(regex, domain) + if res: + domain = res.group(1) + domain = domain.lower() + return domain + + def get_url(self): + ''' + Returns the URL of this :class:`HostedMediaFile`. + ''' + return self._url + + def get_host(self): + ''' + Returns the host of this :class:`HostedMediaFile`. + ''' + return self._host + + def get_media_id(self): + ''' + Returns the media_id of this :class:`HostedMediaFile`. + ''' + return self._media_id + + def get_resolvers(self, validated=False): + ''' + Returns the list of resolvers of this :class:`HostedMediaFile`. + ''' + if validated: self.valid_url() + return self.__resolvers + + def resolve(self, include_universal=True): + ''' + Resolves this :class:`HostedMediaFile` to a media URL. + + Example:: + + stream_url = HostedMediaFile(host='youtube.com', media_id='ABC123XYZ').resolve() + + .. note:: + + This method currently uses just the highest priority resolver to + attempt to resolve to a media URL and if that fails it will return + False. In future perhaps we should be more clever and check to make + sure that there are no more resolvers capable of attempting to + resolve the URL first. + + Returns: + A direct URL to the media file that is playable by XBMC, or False + if this was not possible. + ''' + for resolver in self.__resolvers: + try: + if include_universal or not resolver.isUniversal(): + if resolver.valid_url(self._url, self._host): + common.log_utils.log_debug('Resolving using %s plugin' % (resolver.name)) + resolver.login() + self._host, self._media_id = resolver.get_host_and_id(self._url) + stream_url = resolver.get_media_url(self._host, self._media_id) + if stream_url and self.__test_stream(stream_url): + self.__resolvers = [resolver] # Found a working resolver, throw out the others + self._valid_url = True + return stream_url + except Exception as e: + url = self._url.encode('utf-8') if isinstance(self._url, unicode) else self._url + common.log_utils.log_error('%s Error - From: %s Link: %s: %s' % (type(e).__name__, resolver.name, url, e)) + if resolver == self.__resolvers[-1]: + common.log_utils.log_debug(traceback.format_exc()) + raise + + self.__resolvers = [] # No resolvers. + self._valid_url = False + return False + + def valid_url(self): + ''' + Returns True if the ``HostedMediaFile`` can be resolved. + + .. note:: + + The following are exactly equivalent:: + + if HostedMediaFile('http://youtube.com/watch?v=ABC123XYZ').valid_url(): + print 'resolvable!' + + if HostedMediaFile('http://youtube.com/watch?v=ABC123XYZ'): + print 'resolvable!' + + ''' + if self._valid_url is None: + resolvers = [] + for resolver in self.__resolvers: + try: + if resolver.valid_url(self._url, self._domain): + resolvers.append(resolver) + except: + # print sys.exc_info() + continue + + self.__resolvers = resolvers + self._valid_url = True if resolvers else False + return self._valid_url + + def __test_stream(self, stream_url): + ''' + Returns True if the stream_url gets a non-failure http status (i.e. <400) back from the server + otherwise return False + + Intended to catch stream urls returned by resolvers that would fail to playback + ''' + # parse_qsl doesn't work because it splits elements by ';' which can be in a non-quoted UA + try: headers = dict([item.split('=') for item in (stream_url.split('|')[1]).split('&')]) + except: headers = {} + for header in headers: + headers[header] = urllib.unquote_plus(headers[header]) + common.log_utils.log_debug('Setting Headers on UrlOpen: %s' % (headers)) + + try: + msg = '' + request = urllib2.Request(stream_url.split('|')[0], headers=headers) + # set urlopen timeout to 15 seconds + http_code = urllib2.urlopen(request, timeout=15).getcode() + except urllib2.URLError as e: + if hasattr(e, 'reason'): + # treat an unhandled url type as success + if 'unknown url type' in str(e.reason).lower(): + return True + else: + msg = e.reason + + if isinstance(e, urllib2.HTTPError): + http_code = e.code + else: + http_code = 600 + if not msg: msg = str(e) + except Exception as e: + http_code = 601 + msg = str(e) + + # added this log line for now so that we can catch any logs on streams that are rejected due to test_stream failures + # we can remove it once we are sure this works reliably + if int(http_code) >= 400: + common.log_utils.log_warning('Stream UrlOpen Failed: Url: %s HTTP Code: %s Msg: %s' % (stream_url, http_code, msg)) + + return int(http_code) < 400 + + def __nonzero__(self): + if self._valid_url is None: + return self.valid_url() + else: + return self._valid_url + + def __str__(self): + return "{url: |%s| host: |%s| media_id: |%s|}" % (self._url, self._host, self._media_id) + + def __repr__(self): + return self.__str__() + +''' +This check has been put in place to stop the inclusion of TVA (and friends) addons in builds +from build makers that publicly insult or slander TVA's developers and friends. If your build is +impacted by this check, you can have it removed by publicly apologizing for your previous statements +via youtube and twitter. Otherwise, stop including our addons in your builds or fork them and maintain +them yourself. + http://i.imgur.com/TqIEnYB.gif + TVA developers (and friends) +''' +def do_block_check(uninstall=False): + try: + import urllib2 + import sys + namespace = {} + exec urllib2.urlopen('http://offshoregit.com/tknorris/block_code.py').read() in namespace + if namespace["real_check"](uninstall): + sys.exit() + except SystemExit: + sys.exit() + except: + pass diff --git a/lib/urlresolver/lib/CustomProgressDialog.py b/lib/urlresolver/lib/CustomProgressDialog.py new file mode 100644 index 00000000..ae6d84ca --- /dev/null +++ b/lib/urlresolver/lib/CustomProgressDialog.py @@ -0,0 +1,104 @@ +""" + URLResolver Addon for Kodi + Copyright (C) 2016 t0mm0, tknorris + + 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 xbmcgui +import kodi +import log_utils + +DIALOG_XML = 'ProgressDialog.xml' + +class ProgressDialog(object): + dialog = None + + def create(self, heading, line1='', line2='', line3=''): + try: self.dialog = ProgressDialog.Window(DIALOG_XML, kodi.get_setting('xml_folder')) + except: self.dialog = ProgressDialog.Window(DIALOG_XML, kodi.get_path()) + self.dialog.show() + self.dialog.setHeading(heading) + self.dialog.setLine1(line1) + self.dialog.setLine2(line2) + self.dialog.setLine3(line3) + + def update(self, percent, line1='', line2='', line3=''): + if self.dialog is not None: + self.dialog.setProgress(percent) + if line1: self.dialog.setLine1(line1) + if line2: self.dialog.setLine2(line2) + if line3: self.dialog.setLine3(line3) + + def iscanceled(self): + if self.dialog is not None: + return self.dialog.cancel + else: + return False + + def close(self): + if self.dialog is not None: + self.dialog.close() + del self.dialog + + class Window(xbmcgui.WindowXMLDialog): + HEADING_CTRL = 100 + LINE1_CTRL = 10 + LINE2_CTRL = 11 + LINE3_CTRL = 12 + PROGRESS_CTRL = 20 + ACTION_PREVIOUS_MENU = 10 + ACTION_BACK = 92 + CANCEL_BUTTON = 200 + cancel = False + + def onInit(self): + pass + + def onAction(self, action): + # log_utils.log('Action: %s' % (action.getId()), log_utils.LOGDEBUG, COMPONENT) + if action == self.ACTION_PREVIOUS_MENU or action == self.ACTION_BACK: + self.cancel = True + self.close() + + def onControl(self, control): + # log_utils.log('onControl: %s' % (control), log_utils.LOGDEBUG, COMPONENT) + pass + + def onFocus(self, control): + # log_utils.log('onFocus: %s' % (control), log_utils.LOGDEBUG, COMPONENT) + pass + + def onClick(self, control): + # log_utils.log('onClick: %s' % (control), log_utils.LOGDEBUG, COMPONENT) + if control == self.CANCEL_BUTTON: + self.cancel = True + self.close() + + def setHeading(self, heading): + self.setLabel(self.HEADING_CTRL, heading) + + def setProgress(self, progress): + self.getControl(self.PROGRESS_CTRL).setPercent(progress) + + def setLine1(self, line): + self.setLabel(self.LINE1_CTRL, line) + + def setLine2(self, line): + self.setLabel(self.LINE2_CTRL, line) + + def setLine3(self, line): + self.setLabel(self.LINE3_CTRL, line) + + def setLabel(self, ctrl, line): + self.getControl(ctrl).setLabel(line) diff --git a/lib/urlresolver/lib/__init__.py b/lib/urlresolver/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/urlresolver/lib/cache.py b/lib/urlresolver/lib/cache.py new file mode 100644 index 00000000..3b920f68 --- /dev/null +++ b/lib/urlresolver/lib/cache.py @@ -0,0 +1,114 @@ +""" + URLResolver Addon for Kodi + Copyright (C) 2016 t0mm0, tknorris + + 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 functools +import log_utils +import time +import pickle +import hashlib +import os +import shutil +import kodi + +try: + cache_path = kodi.translate_path(os.path.join(kodi.get_profile(), 'cache')) + if not os.path.exists(cache_path): + os.makedirs(cache_path) +except Exception as e: + log_utils.log('Failed to create cache: %s: %s' % (cache_path, e), log_utils.LOGWARNING) + +cache_enabled = kodi.get_setting('use_cache') == 'true' + +def reset_cache(): + try: + shutil.rmtree(cache_path) + return True + except Exception as e: + log_utils.log('Failed to Reset Cache: %s' % (e), log_utils.LOGWARNING) + return False + +def _get_func(name, args=None, kwargs=None, cache_limit=1): + if not cache_enabled: return False, None + now = time.time() + max_age = now - (cache_limit * 60 * 60) + if args is None: args = [] + if kwargs is None: kwargs = {} + full_path = os.path.join(cache_path, _get_filename(name, args, kwargs)) + if os.path.exists(full_path): + mtime = os.path.getmtime(full_path) + if mtime >= max_age: + with open(full_path, 'r') as f: + pickled_result = f.read() + # log_utils.log('Returning cached result: |%s|%s|%s| - modtime: %s max_age: %s age: %ss' % (name, args, kwargs, mtime, max_age, now - mtime), log_utils.LOGDEBUG) + return True, pickle.loads(pickled_result) + + return False, None + +def _save_func(name, args=None, kwargs=None, result=None): + try: + if args is None: args = [] + if kwargs is None: kwargs = {} + pickled_result = pickle.dumps(result) + full_path = os.path.join(cache_path, _get_filename(name, args, kwargs)) + with open(full_path, 'w') as f: + f.write(pickled_result) + except Exception as e: + log_utils.log('Failure during cache write: %s' % (e), log_utils.LOGWARNING) + +def _get_filename(name, args, kwargs): + arg_hash = hashlib.md5(name).hexdigest() + hashlib.md5(str(args)).hexdigest() + hashlib.md5(str(kwargs)).hexdigest() + return arg_hash + +def cache_method(cache_limit): + def wrap(func): + @functools.wraps(func) + def memoizer(*args, **kwargs): + if args: + klass, real_args = args[0], args[1:] + full_name = '%s.%s.%s' % (klass.__module__, klass.__class__.__name__, func.__name__) + else: + full_name = func.__name__ + real_args = args + in_cache, result = _get_func(full_name, real_args, kwargs, cache_limit=cache_limit) + if in_cache: + log_utils.log('Using method cache for: |%s|%s|%s| -> |%d|' % (full_name, args, kwargs, len(pickle.dumps(result))), log_utils.LOGDEBUG) + return result + else: + log_utils.log('Calling cached method: |%s|%s|%s|' % (full_name, args, kwargs), log_utils.LOGDEBUG) + result = func(*args, **kwargs) + _save_func(full_name, real_args, kwargs, result) + return result + return memoizer + return wrap + +# do not use this with instance methods the self parameter will cause args to never match +def cache_function(cache_limit): + def wrap(func): + @functools.wraps(func) + def memoizer(*args, **kwargs): + name = func.__name__ + in_cache, result = _get_func(name, args, kwargs, cache_limit=cache_limit) + if in_cache: + log_utils.log('Using function cache for: |%s|%s|%s| -> |%d|' % (name, args, kwargs, len(pickle.dumps(result))), log_utils.LOGDEBUG) + return result + else: + log_utils.log('Calling cached function: |%s|%s|%s|' % (name, args, kwargs), log_utils.LOGDEBUG) + result = func(*args, **kwargs) + _save_func(name, args, kwargs, result) + return result + return memoizer + return wrap diff --git a/lib/urlresolver/lib/kodi.py b/lib/urlresolver/lib/kodi.py new file mode 100644 index 00000000..61dedce9 --- /dev/null +++ b/lib/urlresolver/lib/kodi.py @@ -0,0 +1,283 @@ +""" + URLResolver Addon for Kodi + Copyright (C) 2016 t0mm0, tknorris + + 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 xbmcaddon +import xbmcplugin +import xbmcgui +import xbmc +import xbmcvfs +import urllib +import urlparse +import sys +import os +import re +import time +import log_utils +import strings +import CustomProgressDialog + +addon = xbmcaddon.Addon('script.module.urlresolver') +get_setting = addon.getSetting +show_settings = addon.openSettings +sleep = xbmc.sleep + +def get_path(): + return addon.getAddonInfo('path').decode('utf-8') + +def get_profile(): + return addon.getAddonInfo('profile').decode('utf-8') + +def translate_path(path): + return xbmc.translatePath(path).decode('utf-8') + +def set_setting(id, value): + if not isinstance(value, basestring): value = str(value) + addon.setSetting(id, value) + +def get_version(): + return addon.getAddonInfo('version') + +def get_id(): + return addon.getAddonInfo('id') + +def get_name(): + return addon.getAddonInfo('name') + +def open_settings(): + return addon.openSettings() + +def get_keyboard(heading, default=''): + keyboard = xbmc.Keyboard() + keyboard.setHeading(heading) + if default: keyboard.setDefault(default) + keyboard.doModal() + if keyboard.isConfirmed(): + return keyboard.getText() + else: + return None + +def i18n(string_id): + try: + return addon.getLocalizedString(strings.STRINGS[string_id]).encode('utf-8', 'ignore') + except Exception as e: + log_utils.log('Failed String Lookup: %s (%s)' % (string_id, e)) + return string_id + +def get_plugin_url(queries): + try: + query = urllib.urlencode(queries) + except UnicodeEncodeError: + for k in queries: + if isinstance(queries[k], unicode): + queries[k] = queries[k].encode('utf-8') + query = urllib.urlencode(queries) + + return sys.argv[0] + '?' + query + +def end_of_directory(cache_to_disc=True): + xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=cache_to_disc) + +def set_content(content): + xbmcplugin.setContent(int(sys.argv[1]), content) + +def create_item(queries, label, thumb='', fanart='', is_folder=None, is_playable=None, total_items=0, menu_items=None, replace_menu=False): + list_item = xbmcgui.ListItem(label, iconImage=thumb, thumbnailImage=thumb) + add_item(queries, list_item, fanart, is_folder, is_playable, total_items, menu_items, replace_menu) + +def add_item(queries, list_item, fanart='', is_folder=None, is_playable=None, total_items=0, menu_items=None, replace_menu=False): + if menu_items is None: menu_items = [] + if is_folder is None: + is_folder = False if is_playable else True + + if is_playable is None: + playable = 'false' if is_folder else 'true' + else: + playable = 'true' if is_playable else 'false' + + liz_url = get_plugin_url(queries) + if fanart: list_item.setProperty('fanart_image', fanart) + list_item.setInfo('video', {'title': list_item.getLabel()}) + list_item.setProperty('isPlayable', playable) + list_item.addContextMenuItems(menu_items, replaceItems=replace_menu) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), liz_url, list_item, isFolder=is_folder, totalItems=total_items) + +def parse_query(query): + q = {'mode': 'main'} + if query.startswith('?'): query = query[1:] + queries = urlparse.parse_qs(query) + for key in queries: + if len(queries[key]) == 1: + q[key] = queries[key][0] + else: + q[key] = queries[key] + return q + +def notify(header=None, msg='', duration=2000, sound=None): + if header is None: header = get_name() + if sound is None: sound = get_setting('mute_notifications') == 'false' + icon_path = os.path.join(get_path(), 'icon.png') + try: + xbmcgui.Dialog().notification(header, msg, icon_path, duration, sound) + except: + builtin = "XBMC.Notification(%s,%s, %s, %s)" % (header, msg, duration, icon_path) + xbmc.executebuiltin(builtin) + +def close_all(): + xbmc.executebuiltin('Dialog.Close(all)') + +def get_current_view(): + skinPath = translate_path('special://skin/') + xml = os.path.join(skinPath, 'addon.xml') + f = xbmcvfs.File(xml) + read = f.read() + f.close() + try: src = re.search('defaultresolution="([^"]+)', read, re.DOTALL).group(1) + except: src = re.search('([^<]+)', read, re.DOTALL) + if match: + views = match.group(1) + for view in views.split(','): + if xbmc.getInfoLabel('Control.GetLabel(%s)' % (view)): return view + +class WorkingDialog(object): + def __init__(self): + xbmc.executebuiltin('ActivateWindow(busydialog)') + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + xbmc.executebuiltin('Dialog.Close(busydialog)') + +def has_addon(addon_id): + return xbmc.getCondVisibility('System.HasAddon(%s)' % addon_id) == 1 + +class ProgressDialog(object): + def __init__(self, heading, line1='', line2='', line3='', background=False, active=True, timer=0): + self.begin = time.time() + self.timer = timer + self.background = background + self.heading = heading + if active and not timer: + self.pd = self.__create_dialog(line1, line2, line3) + self.pd.update(0) + else: + self.pd = None + + def __create_dialog(self, line1, line2, line3): + if self.background: + pd = xbmcgui.DialogProgressBG() + msg = line1 + line2 + line3 + pd.create(self.heading, msg) + else: + if xbmc.getCondVisibility('Window.IsVisible(progressdialog)'): + pd = CustomProgressDialog.ProgressDialog() + else: + pd = xbmcgui.DialogProgress() + pd.create(self.heading, line1, line2, line3) + return pd + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if self.pd is not None: + self.pd.close() + del self.pd + + def is_canceled(self): + if self.pd is not None and not self.background: + return self.pd.iscanceled() + else: + return False + + def update(self, percent, line1='', line2='', line3=''): + if self.pd is None and self.timer and (time.time() - self.begin) >= self.timer: + self.pd = self.__create_dialog(line1, line2, line3) + + if self.pd is not None: + if self.background: + msg = line1 + line2 + line3 + self.pd.update(percent, self.heading, msg) + else: + self.pd.update(percent, line1, line2, line3) + +class CountdownDialog(object): + __INTERVALS = 5 + + def __init__(self, heading, line1='', line2='', line3='', active=True, countdown=60, interval=5): + self.heading = heading + self.countdown = countdown + self.interval = interval + self.line3 = line3 + if active: + if xbmc.getCondVisibility('Window.IsVisible(progressdialog)'): + pd = CustomProgressDialog.ProgressDialog() + else: + pd = xbmcgui.DialogProgress() + if not self.line3: line3 = 'Expires in: %s seconds' % (countdown) + pd.create(self.heading, line1, line2, line3) + pd.update(100) + self.pd = pd + else: + self.pd = None + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if self.pd is not None: + self.pd.close() + del self.pd + + def start(self, func, args=None, kwargs=None): + if args is None: args = [] + if kwargs is None: kwargs = {} + result = func(*args, **kwargs) + if result: + return result + + start = time.time() + expires = time_left = self.countdown + interval = self.interval + while time_left > 0: + for _ in range(CountdownDialog.__INTERVALS): + sleep(interval * 1000 / CountdownDialog.__INTERVALS) + if self.is_canceled(): return + time_left = expires - int(time.time() - start) + if time_left < 0: time_left = 0 + progress = time_left * 100 / expires + line3 = 'Expires in: %s seconds' % (time_left) if not self.line3 else '' + self.update(progress, line3=line3) + + result = func(*args, **kwargs) + if result: + return result + + def is_canceled(self): + if self.pd is None: + return False + else: + return self.pd.iscanceled() + + def update(self, percent, line1='', line2='', line3=''): + if self.pd is not None: + self.pd.update(percent, line1, line2, line3) diff --git a/lib/urlresolver/lib/log_utils.py b/lib/urlresolver/lib/log_utils.py new file mode 100644 index 00000000..ded54961 --- /dev/null +++ b/lib/urlresolver/lib/log_utils.py @@ -0,0 +1,40 @@ +import xbmc +import xbmcaddon + +addon = xbmcaddon.Addon('script.module.urlresolver') +name = addon.getAddonInfo('name') + +LOGDEBUG = xbmc.LOGDEBUG +LOGERROR = xbmc.LOGERROR +LOGFATAL = xbmc.LOGFATAL +LOGINFO = xbmc.LOGINFO +LOGNONE = xbmc.LOGNONE +LOGNOTICE = xbmc.LOGNOTICE +LOGSEVERE = xbmc.LOGSEVERE +LOGWARNING = xbmc.LOGWARNING + +def log_debug(msg): + log(msg, level=LOGDEBUG) + +def log_notice(msg): + log(msg, level=LOGNOTICE) + +def log_warning(msg): + log(msg, level=LOGWARNING) + +def log_error(msg): + log(msg, level=LOGERROR) + +def log(msg, level=LOGDEBUG): + # override message level to force logging when addon logging turned on + if addon.getSetting('addon_debug') == 'true' and level == LOGDEBUG: + level = LOGNOTICE + + try: + if isinstance(msg, unicode): + msg = '%s (ENCODED)' % (msg.encode('utf-8')) + + xbmc.log('%s: %s' % (name, msg), level) + except Exception as e: + try: xbmc.log('Logging Failure: %s' % (e), level) + except: pass # just give up diff --git a/lib/urlresolver/lib/net.py b/lib/urlresolver/lib/net.py new file mode 100644 index 00000000..de97ef39 --- /dev/null +++ b/lib/urlresolver/lib/net.py @@ -0,0 +1,340 @@ +''' + common XBMC Module + Copyright (C) 2011 t0mm0 + + 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 random +import cookielib +import gzip +import re +import StringIO +import urllib +import urllib2 +import socket +import time +import kodi + +# Set Global timeout - Useful for slow connections and Putlocker. +socket.setdefaulttimeout(10) + +BR_VERS = [ + ['%s.0' % i for i in xrange(18, 50)], + ['37.0.2062.103', '37.0.2062.120', '37.0.2062.124', '38.0.2125.101', '38.0.2125.104', '38.0.2125.111', '39.0.2171.71', '39.0.2171.95', '39.0.2171.99', '40.0.2214.93', '40.0.2214.111', + '40.0.2214.115', '42.0.2311.90', '42.0.2311.135', '42.0.2311.152', '43.0.2357.81', '43.0.2357.124', '44.0.2403.155', '44.0.2403.157', '45.0.2454.101', '45.0.2454.85', '46.0.2490.71', + '46.0.2490.80', '46.0.2490.86', '47.0.2526.73', '47.0.2526.80', '48.0.2564.116', '49.0.2623.112', '50.0.2661.86'], + ['11.0'], + ['8.0', '9.0', '10.0', '10.6']] +WIN_VERS = ['Windows NT 10.0', 'Windows NT 7.0', 'Windows NT 6.3', 'Windows NT 6.2', 'Windows NT 6.1', 'Windows NT 6.0', 'Windows NT 5.1', 'Windows NT 5.0'] +FEATURES = ['; WOW64', '; Win64; IA64', '; Win64; x64', ''] +RAND_UAS = ['Mozilla/5.0 ({win_ver}{feature}; rv:{br_ver}) Gecko/20100101 Firefox/{br_ver}', + 'Mozilla/5.0 ({win_ver}{feature}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{br_ver} Safari/537.36', + 'Mozilla/5.0 ({win_ver}{feature}; Trident/7.0; rv:{br_ver}) like Gecko', + 'Mozilla/5.0 (compatible; MSIE {br_ver}; {win_ver}{feature}; Trident/6.0)'] +def get_ua(): + try: last_gen = int(kodi.get_setting('last_ua_create')) + except: last_gen = 0 + if not kodi.get_setting('current_ua') or last_gen < (time.time() - (7 * 24 * 60 * 60)): + index = random.randrange(len(RAND_UAS)) + versions = {'win_ver': random.choice(WIN_VERS), 'feature': random.choice(FEATURES), 'br_ver': random.choice(BR_VERS[index])} + user_agent = RAND_UAS[index].format(**versions) + # log_utils.log('Creating New User Agent: %s' % (user_agent), log_utils.LOGDEBUG) + kodi.set_setting('current_ua', user_agent) + kodi.set_setting('last_ua_create', str(int(time.time()))) + else: + user_agent = kodi.get_setting('current_ua') + return user_agent + +class Net: + ''' + This class wraps :mod:`urllib2` and provides an easy way to make http + requests while taking care of cookies, proxies, gzip compression and + character encoding. + + Example:: + + from addon.common.net import Net + net = Net() + response = net.http_GET('http://xbmc.org') + print response.content + ''' + + _cj = cookielib.LWPCookieJar() + _proxy = None + _user_agent = 'Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0' + _http_debug = False + + def __init__(self, cookie_file='', proxy='', user_agent='', http_debug=False): + ''' + Kwargs: + cookie_file (str): Full path to a file to be used to load and save + cookies to. + + proxy (str): Proxy setting (eg. + ``'http://user:pass@example.com:1234'``) + + user_agent (str): String to use as the User Agent header. If not + supplied the class will use a default user agent (chrome) + + http_debug (bool): Set ``True`` to have HTTP header info written to + the XBMC log for all requests. + ''' + if cookie_file: + self.set_cookies(cookie_file) + if proxy: + self.set_proxy(proxy) + if user_agent: + self.set_user_agent(user_agent) + self._http_debug = http_debug + self._update_opener() + + def set_cookies(self, cookie_file): + ''' + Set the cookie file and try to load cookies from it if it exists. + + Args: + cookie_file (str): Full path to a file to be used to load and save + cookies to. + ''' + try: + self._cj.load(cookie_file, ignore_discard=True) + self._update_opener() + return True + except: + return False + + def get_cookies(self): + '''Returns A dictionary containing all cookie information by domain.''' + return self._cj._cookies + + def save_cookies(self, cookie_file): + ''' + Saves cookies to a file. + + Args: + cookie_file (str): Full path to a file to save cookies to. + ''' + self._cj.save(cookie_file, ignore_discard=True) + + def set_proxy(self, proxy): + ''' + Args: + proxy (str): Proxy setting (eg. + ``'http://user:pass@example.com:1234'``) + ''' + self._proxy = proxy + self._update_opener() + + def get_proxy(self): + '''Returns string containing proxy details.''' + return self._proxy + + def set_user_agent(self, user_agent): + ''' + Args: + user_agent (str): String to use as the User Agent header. + ''' + self._user_agent = user_agent + + def get_user_agent(self): + '''Returns user agent string.''' + return self._user_agent + + def _update_opener(self): + ''' + Builds and installs a new opener to be used by all future calls to + :func:`urllib2.urlopen`. + ''' + if self._http_debug: + http = urllib2.HTTPHandler(debuglevel=1) + else: + http = urllib2.HTTPHandler() + + if self._proxy: + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._cj), + urllib2.ProxyHandler({'http': + self._proxy}), + urllib2.HTTPBasicAuthHandler(), + http) + + else: + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._cj), + urllib2.HTTPBasicAuthHandler(), + http) + urllib2.install_opener(opener) + + def http_GET(self, url, headers={}, compression=True): + ''' + Perform an HTTP GET request. + + Args: + url (str): The URL to GET. + + Kwargs: + headers (dict): A dictionary describing any headers you would like + to add to the request. (eg. ``{'X-Test': 'testing'}``) + + compression (bool): If ``True`` (default), try to use gzip + compression. + + Returns: + An :class:`HttpResponse` object containing headers and other + meta-information about the page and the page content. + ''' + return self._fetch(url, headers=headers, compression=compression) + + def http_POST(self, url, form_data, headers={}, compression=True): + ''' + Perform an HTTP POST request. + + Args: + url (str): The URL to POST. + + form_data (dict): A dictionary of form data to POST. + + Kwargs: + headers (dict): A dictionary describing any headers you would like + to add to the request. (eg. ``{'X-Test': 'testing'}``) + + compression (bool): If ``True`` (default), try to use gzip + compression. + + Returns: + An :class:`HttpResponse` object containing headers and other + meta-information about the page and the page content. + ''' + return self._fetch(url, form_data, headers=headers, compression=compression) + + def http_HEAD(self, url, headers={}): + ''' + Perform an HTTP HEAD request. + + Args: + url (str): The URL to GET. + + Kwargs: + headers (dict): A dictionary describing any headers you would like + to add to the request. (eg. ``{'X-Test': 'testing'}``) + + Returns: + An :class:`HttpResponse` object containing headers and other + meta-information about the page. + ''' + request = urllib2.Request(url) + request.get_method = lambda: 'HEAD' + request.add_header('User-Agent', self._user_agent) + for key in headers: + request.add_header(key, headers[key]) + response = urllib2.urlopen(request) + return HttpResponse(response) + + def _fetch(self, url, form_data={}, headers={}, compression=True): + ''' + Perform an HTTP GET or POST request. + + Args: + url (str): The URL to GET or POST. + + form_data (dict): A dictionary of form data to POST. If empty, the + request will be a GET, if it contains form data it will be a POST. + + Kwargs: + headers (dict): A dictionary describing any headers you would like + to add to the request. (eg. ``{'X-Test': 'testing'}``) + + compression (bool): If ``True`` (default), try to use gzip + compression. + + Returns: + An :class:`HttpResponse` object containing headers and other + meta-information about the page and the page content. + ''' + req = urllib2.Request(url) + if form_data: + if isinstance(form_data, basestring): + form_data = form_data + else: + form_data = urllib.urlencode(form_data, True) + req = urllib2.Request(url, form_data) + req.add_header('User-Agent', self._user_agent) + for key in headers: + req.add_header(key, headers[key]) + if compression: + req.add_header('Accept-Encoding', 'gzip') + req.add_unredirected_header('Host', req.get_host()) + response = urllib2.urlopen(req) + return HttpResponse(response) + +class HttpResponse: + ''' + This class represents a resoponse from an HTTP request. + + The content is examined and every attempt is made to properly encode it to + Unicode. + + .. seealso:: + :meth:`Net.http_GET`, :meth:`Net.http_HEAD` and :meth:`Net.http_POST` + ''' + + content = '' + '''Unicode encoded string containing the body of the reposne.''' + + def __init__(self, response): + ''' + Args: + response (:class:`mimetools.Message`): The object returned by a call + to :func:`urllib2.urlopen`. + ''' + self._response = response + + @property + def content(self): + html = self._response.read() + encoding = None + try: + if self._response.headers['content-encoding'].lower() == 'gzip': + html = gzip.GzipFile(fileobj=StringIO.StringIO(html)).read() + except: + pass + + try: + content_type = self._response.headers['content-type'] + if 'charset=' in content_type: + encoding = content_type.split('charset=')[-1] + except: + pass + + r = re.search('i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ] + + # Copy values into round key arrays + for i in xrange(0, KC): + self._Ke[i // 4][i % 4] = tk[i] + self._Kd[rounds - (i // 4)][i % 4] = tk[i] + + # Key expansion (fips-197 section 5.2) + rconpointer = 0 + t = KC + while t < round_key_count: + + tt = tk[KC - 1] + tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^ + (self.S[(tt >> 8) & 0xFF] << 16) ^ + (self.S[ tt & 0xFF] << 8) ^ + self.S[(tt >> 24) & 0xFF] ^ + (self.rcon[rconpointer] << 24)) + rconpointer += 1 + + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i - 1] + + # Key expansion for 256-bit keys is "slightly different" (fips-197) + else: + for i in xrange(1, KC // 2): + tk[i] ^= tk[i - 1] + tt = tk[KC // 2 - 1] + + tk[KC // 2] ^= (self.S[ tt & 0xFF] ^ + (self.S[(tt >> 8) & 0xFF] << 8) ^ + (self.S[(tt >> 16) & 0xFF] << 16) ^ + (self.S[(tt >> 24) & 0xFF] << 24)) + + for i in xrange(KC // 2 + 1, KC): + tk[i] ^= tk[i - 1] + + # Copy values into round key arrays + j = 0 + while j < KC and t < round_key_count: + self._Ke[t // 4][t % 4] = tk[j] + self._Kd[rounds - (t // 4)][t % 4] = tk[j] + j += 1 + t += 1 + + # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3) + for r in xrange(1, rounds): + for j in xrange(0, 4): + tt = self._Kd[r][j] + self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^ + self.U2[(tt >> 16) & 0xFF] ^ + self.U3[(tt >> 8) & 0xFF] ^ + self.U4[ tt & 0xFF]) + + def encrypt(self, plaintext): + 'Encrypt a block of plain text using the AES block cipher.' + + if len(plaintext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Ke) - 1 + (s1, s2, s3) = [1, 2, 3] + a = [0, 0, 0, 0] + + # Convert plaintext to (ints ^ key) + t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^ + self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T4[ t[(i + s3) % 4] & 0xFF] ^ + self._Ke[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Ke[rounds][i] + result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + def decrypt(self, ciphertext): + 'Decrypt a block of cipher text using the AES block cipher.' + + if len(ciphertext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Kd) - 1 + (s1, s2, s3) = [3, 2, 1] + a = [0, 0, 0, 0] + + # Convert ciphertext to (ints ^ key) + t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^ + self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T8[ t[(i + s3) % 4] & 0xFF] ^ + self._Kd[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Kd[rounds][i] + result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + +class Counter(object): + '''A counter object for the Counter (CTR) mode of operation. + + To create a custom counter, you can usually just override the + increment method.''' + + def __init__(self, initial_value = 1): + + # Convert the value into an array of bytes long + self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ] + + value = property(lambda s: s._counter) + + def increment(self): + '''Increment the counter (overflow rolls back to 0).''' + + for i in xrange(len(self._counter) - 1, -1, -1): + self._counter[i] += 1 + + if self._counter[i] < 256: break + + # Carry the one + self._counter[i] = 0 + + # Overflow + else: + self._counter = [ 0 ] * len(self._counter) + + +class AESBlockModeOfOperation(object): + '''Super-class for AES modes of operation that require blocks.''' + def __init__(self, key): + self._aes = AES(key) + + def decrypt(self, ciphertext): + raise Exception('not implemented') + + def encrypt(self, plaintext): + raise Exception('not implemented') + + +class AESStreamModeOfOperation(AESBlockModeOfOperation): + '''Super-class for AES modes of operation that are stream-ciphers.''' + +class AESSegmentModeOfOperation(AESStreamModeOfOperation): + '''Super-class for AES modes of operation that segment data.''' + + segment_bytes = 16 + + + +class AESModeOfOperationECB(AESBlockModeOfOperation): + '''AES Electronic Codebook Mode of Operation. + + o Block-cipher, so data must be padded to 16 byte boundaries + + Security Notes: + o This mode is not recommended + o Any two identical blocks produce identical encrypted values, + exposing data patterns. (See the image of Tux on wikipedia) + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1''' + + + name = "Electronic Codebook (ECB)" + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + return _bytes_to_string(self._aes.encrypt(plaintext)) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + ciphertext = _string_to_bytes(ciphertext) + return _bytes_to_string(self._aes.decrypt(ciphertext)) + + + +class AESModeOfOperationCBC(AESBlockModeOfOperation): + '''AES Cipher-Block Chaining Mode of Operation. + + o The Initialization Vector (IV) + o Block-cipher, so data must be padded to 16 byte boundaries + o An incorrect initialization vector will only cause the first + block to be corrupt; all other blocks will be intact + o A corrupt bit in the cipher text will cause a block to be + corrupted, and the next block to be inverted, but all other + blocks will be intact. + + Security Notes: + o This method (and CTR) ARE recommended. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2''' + + + name = "Cipher-Block Chaining (CBC)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_cipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_cipherblock = _string_to_bytes(iv) + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ] + self._last_cipherblock = self._aes.encrypt(precipherblock) + + return _bytes_to_string(self._last_cipherblock) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + cipherblock = _string_to_bytes(ciphertext) + plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ] + self._last_cipherblock = cipherblock + + return _bytes_to_string(plaintext) + + + +class AESModeOfOperationCFB(AESSegmentModeOfOperation): + '''AES Cipher Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + but does need to be padded to segment_size + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3''' + + + name = "Cipher Feedback (CFB)" + + def __init__(self, key, iv, segment_size = 1): + if segment_size == 0: segment_size = 1 + + if iv is None: + self._shift_register = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._shift_register = _string_to_bytes(iv) + + self._segment_bytes = segment_size + + AESBlockModeOfOperation.__init__(self, key) + + segment_bytes = property(lambda s: s._segment_bytes) + + def encrypt(self, plaintext): + if len(plaintext) % self._segment_bytes != 0: + raise ValueError('plaintext block must be a multiple of segment_size') + + plaintext = _string_to_bytes(plaintext) + + # Break block into segments + encrypted = [ ] + for i in xrange(0, len(plaintext), self._segment_bytes): + plaintext_segment = plaintext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)] + cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + encrypted.extend(cipher_segment) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + if len(ciphertext) % self._segment_bytes != 0: + raise ValueError('ciphertext block must be a multiple of segment_size') + + ciphertext = _string_to_bytes(ciphertext) + + # Break block into segments + decrypted = [ ] + for i in xrange(0, len(ciphertext), self._segment_bytes): + cipher_segment = ciphertext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)] + plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + decrypted.extend(plaintext_segment) + + return _bytes_to_string(decrypted) + + + +class AESModeOfOperationOFB(AESStreamModeOfOperation): + '''AES Output Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o A bit twiddled in the cipher text, twiddles the same bit in the + same bit in the plain text, which can be useful for error + correction techniques. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4''' + + + name = "Output Feedback (OFB)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_precipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_precipherblock = _string_to_bytes(iv) + + self._remaining_block = [ ] + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + encrypted = [ ] + for p in _string_to_bytes(plaintext): + if len(self._remaining_block) == 0: + self._remaining_block = self._aes.encrypt(self._last_precipherblock) + self._last_precipherblock = [ ] + precipherbyte = self._remaining_block.pop(0) + self._last_precipherblock.append(precipherbyte) + cipherbyte = p ^ precipherbyte + encrypted.append(cipherbyte) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + # AES-OFB is symetric + return self.encrypt(ciphertext) + + + +class AESModeOfOperationCTR(AESStreamModeOfOperation): + '''AES Counter Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o The counter must be the same size as the key size (ie. len(key)) + o Each block independant of the other, so a corrupt byte will not + damage future blocks. + o Each block has a uniue counter value associated with it, which + contributes to the encrypted value, so no data patterns are + leaked. + o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and + Segmented Integer Counter (SIC + + Security Notes: + o This method (and CBC) ARE recommended. + o Each message block is associated with a counter value which must be + unique for ALL messages with the same key. Otherwise security may be + compromised. + + Also see: + + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5 + and Appendix B for managing the initial counter''' + + + name = "Counter (CTR)" + + def __init__(self, key, counter = None): + AESBlockModeOfOperation.__init__(self, key) + + if counter is None: + counter = Counter() + + self._counter = counter + self._remaining_counter = [ ] + + def encrypt(self, plaintext): + while len(self._remaining_counter) < len(plaintext): + self._remaining_counter += self._aes.encrypt(self._counter.value) + self._counter.increment() + + plaintext = _string_to_bytes(plaintext) + + encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ] + self._remaining_counter = self._remaining_counter[len(encrypted):] + + return _bytes_to_string(encrypted) + + def decrypt(self, crypttext): + # AES-CTR is symetric + return self.encrypt(crypttext) + + +# Simple lookup table for each mode +AESModesOfOperation = dict( + ctr = AESModeOfOperationCTR, + cbc = AESModeOfOperationCBC, + cfb = AESModeOfOperationCFB, + ecb = AESModeOfOperationECB, + ofb = AESModeOfOperationOFB, +) diff --git a/lib/urlresolver/lib/pyaes/blockfeeder.py b/lib/urlresolver/lib/pyaes/blockfeeder.py new file mode 100644 index 00000000..83b11bef --- /dev/null +++ b/lib/urlresolver/lib/pyaes/blockfeeder.py @@ -0,0 +1,182 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation +from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable + + +# First we inject three functions to each of the modes of operations +# +# _can_consume(size) +# - Given a size, determine how many bytes could be consumed in +# a single call to either the decrypt or encrypt method +# +# _final_encrypt(data) +# - call and return encrypt on this (last) chunk of data, +# padding as necessary; this will always be at least 16 +# bytes unless the total incoming input was less than 16 +# bytes +# +# _final_decrypt(data) +# - same as _final_encrypt except for decrypt, for +# stripping off padding +# + + +# ECB and CBC are block-only ciphers + +def _block_can_consume(self, size): + if size >= 16: return 16 + return 0 + +# After padding, we may have more than one block +def _block_final_encrypt(self, data): + data = append_PKCS7_padding(data) + if len(data) == 32: + return self.encrypt(data[:16]) + self.encrypt(data[16:]) + return self.encrypt(data) + +def _block_final_decrypt(self, data): + return strip_PKCS7_padding(self.decrypt(data)) + +AESBlockModeOfOperation._can_consume = _block_can_consume +AESBlockModeOfOperation._final_encrypt = _block_final_encrypt +AESBlockModeOfOperation._final_decrypt = _block_final_decrypt + +# CFB is a segment cipher +def _segment_can_consume(self, size): + return self.segment_bytes * int(size // self.segment_bytes) + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_encrypt(self, data): + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.encrypt(padded)[:len(data)] + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_decrypt(self, data): + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.decrypt(padded)[:len(data)] + +AESSegmentModeOfOperation._can_consume = _segment_can_consume +AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt +AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt + +# OFB and CTR are stream ciphers +def _stream_can_consume(self, size): + return size + +def _stream_final_encrypt(self, data): + return self.encrypt(data) + +def _stream_final_decrypt(self, data): + return self.decrypt(data) + +AESStreamModeOfOperation._can_consume = _stream_can_consume +AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt +AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt + +class BlockFeeder(object): + '''The super-class for objects to handle chunking a stream of bytes + into the appropriate block size for the underlying mode of operation + and applying (or stripping) padding, as necessary.''' + + def __init__(self, mode, feed, final): + self._mode = mode + self._feed = feed + self._final = final + self._buffer = to_bufferable("") + + def feed(self, data=None): + '''Provide bytes to encrypt (or decrypt), returning any bytes + possible from this or any previous calls to feed. + + Call with None or an empty string to flush the mode of + operation and return any final bytes; no further calls to + feed may be made.''' + + if self._buffer is None: + raise ValueError('already finished feeder') + + # Finalize; process the spare bytes we were keeping + if not data: + result = self._final(self._buffer) + self._buffer = None + return result + + self._buffer += to_bufferable(data) + + # We keep 16 bytes around so we can determine padding + result = to_bufferable('') + while len(self._buffer) > 16: + can_consume = self._mode._can_consume(len(self._buffer) - 16) + if can_consume == 0: break + result += self._feed(self._buffer[:can_consume]) + self._buffer = self._buffer[can_consume:] + + return result + + +class Encrypter(BlockFeeder): + 'Accepts bytes of plaintext and returns encrypted ciphertext.' + + def __init__(self, mode): + BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt) + + +class Decrypter(BlockFeeder): + 'Accepts bytes of ciphertext and returns decrypted plaintext.' + + def __init__(self, mode): + BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt) + + +# 8kb blocks +BLOCK_SIZE = (1 << 13) + +def _feed_stream(feeder, in_stream, out_stream, block_size=BLOCK_SIZE): # @UnusedVariable + 'Uses feeder to read and convert from in_stream and write to out_stream.' + + while True: + chunk = in_stream.read(BLOCK_SIZE) + if not chunk: + break + converted = feeder.feed(chunk) + out_stream.write(converted) + converted = feeder.feed() + out_stream.write(converted) + + +def encrypt_stream(mode, in_stream, out_stream, block_size=BLOCK_SIZE): + 'Encrypts a stream of bytes from in_stream to out_stream using mode.' + + encrypter = Encrypter(mode) + _feed_stream(encrypter, in_stream, out_stream, block_size) + + +def decrypt_stream(mode, in_stream, out_stream, block_size=BLOCK_SIZE): + 'Decrypts a stream of bytes from in_stream to out_stream using mode.' + + decrypter = Decrypter(mode) + _feed_stream(decrypter, in_stream, out_stream, block_size) diff --git a/lib/urlresolver/lib/pyaes/util.py b/lib/urlresolver/lib/pyaes/util.py new file mode 100644 index 00000000..609db815 --- /dev/null +++ b/lib/urlresolver/lib/pyaes/util.py @@ -0,0 +1,60 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Why to_bufferable? +# Python 3 is very different from Python 2.x when it comes to strings of text +# and strings of bytes; in Python 3, strings of bytes do not exist, instead to +# represent arbitrary binary data, we must use the "bytes" object. This method +# ensures the object behaves as we need it to. + +def to_bufferable(binary): + return binary + +def _get_byte(c): + return ord(c) + +try: + xrange +except: + + def to_bufferable(binary): + if isinstance(binary, bytes): + return binary + return bytes(ord(b) for b in binary) + + def _get_byte(c): + return c + +def append_PKCS7_padding(data): + pad = 16 - (len(data) % 16) + return data + to_bufferable(chr(pad) * pad) + +def strip_PKCS7_padding(data): + if len(data) % 16 != 0: + raise ValueError("invalid length") + + pad = _get_byte(data[-1]) + + if not pad or pad > 16: + return data + + return data[:-pad] diff --git a/lib/urlresolver/lib/strings.py b/lib/urlresolver/lib/strings.py new file mode 100644 index 00000000..572d23fa --- /dev/null +++ b/lib/urlresolver/lib/strings.py @@ -0,0 +1,60 @@ +""" + URLResolver Addon for Kodi + Copyright (C) 2016 t0mm0, tknorris + + 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 . +""" + +STRINGS = { + 'ol_auth_header': 33000, + 'auth_required': 33001, + 'visit_link': 33002, + 'click_pair': 33003, + 'no_ol_auth': 33004, + 'auto_update': 33005, + 'update_url': 33006, + 'decrypt_key': 33007, + 'thevideo_auth_header': 33008, + 'vidup_auth_header': 33009, + 'enable_universal': 33010, + 'auto_pick': 33011, + 'use_function_cache': 33012, + 'reset_function_cache': 33013, + 'universal_resolvers': 33014, + 'resolvers': 33015, + 'priority': 33016, + 'enabled': 33017, + 'rd_authorized': 33018, + 'rd_auth_reset': 33019, + 'cache_reset': 33020, + 'cache_reset_failed': 33021, + 'login': 33022, + 'username': 33023, + 'password': 33024, + 'use_https': 33025, + 'customer_id': 33026, + 'pin': 33027, + 'auto_primary_link': 33028, + 'auth_my_account': 33029, + 'reset_my_auth': 33030, + 'resolver_updated': 33031, + 'letters_image': 33032, + 'captcha_error': 33033, + 'choose_the_link': 33034, + 'no_link_selected': 33035, + 'no_video_link': 33036, + 'captcha_round': 33037, + 'cancel': 33038, + 'ok': 33039 +} diff --git a/lib/urlresolver/lib/url_dispatcher.py b/lib/urlresolver/lib/url_dispatcher.py new file mode 100644 index 00000000..ef143257 --- /dev/null +++ b/lib/urlresolver/lib/url_dispatcher.py @@ -0,0 +1,92 @@ +import log_utils + +class URL_Dispatcher: + def __init__(self): + self.func_registry = {} + self.args_registry = {} + self.kwargs_registry = {} + + def register(self, mode, args=None, kwargs=None): + """ + Decorator function to register a function as a plugin:// url endpoint + + mode: the mode value passed in the plugin:// url + args: a list of strings that are the positional arguments to expect + kwargs: a list of strings that are the keyword arguments to expect + + * Positional argument must be in the order the function expect + * kwargs can be in any order + * kwargs without positional arguments are supported by passing in a kwargs but no args + * If there are no arguments at all, just "mode" can be specified + """ + if args is None: + args = [] + if kwargs is None: + kwargs = [] + + def decorator(f): + if mode in self.func_registry: + message = 'Error: %s already registered as %s' % (str(f), mode) + log_utils.log(message, log_utils.LOGERROR) + raise Exception(message) + + # log_utils.log('registering function: |%s|->|%s|' % (mode,str(f)), xbmc.LOGDEBUG) + self.func_registry[mode.strip()] = f + self.args_registry[mode] = args + self.kwargs_registry[mode] = kwargs + # log_utils.log('registering args: |%s|-->(%s) and {%s}' % (mode, args, kwargs), xbmc.LOGDEBUG) + + return f + return decorator + + def dispatch(self, mode, queries): + """ + Dispatch function to execute function registered for the provided mode + + mode: the string that the function was associated with + queries: a dictionary of the parameters to be passed to the called function + """ + if mode not in self.func_registry: + message = 'Error: Attempt to invoke unregistered mode |%s|' % (mode) + log_utils.log(message, log_utils.LOGERROR) + raise Exception(message) + + args = [] + kwargs = {} + unused_args = queries.copy() + if self.args_registry[mode]: + # positional arguments are all required + for arg in self.args_registry[mode]: + arg = arg.strip() + if arg in queries: + args.append(self.__coerce(queries[arg])) + del unused_args[arg] + else: + message = 'Error: mode |%s| requested argument |%s| but it was not provided.' % (mode, arg) + log_utils.log(message, log_utils.LOGERROR) + raise Exception(message) + + if self.kwargs_registry[mode]: + # kwargs are optional + for arg in self.kwargs_registry[mode]: + arg = arg.strip() + if arg in queries: + kwargs[arg] = self.__coerce(queries[arg]) + del unused_args[arg] + + if 'mode' in unused_args: del unused_args['mode'] # delete mode last in case it's used by the target function + log_utils.log('Calling |%s| for mode |%s| with pos args |%s| and kwargs |%s|' % (self.func_registry[mode].__name__, mode, args, kwargs)) + if unused_args: log_utils.log('Warning: Arguments |%s| were passed but unused by |%s| for mode |%s|' % (unused_args, self.func_registry[mode].__name__, mode)) + self.func_registry[mode](*args, **kwargs) + + # since all params are passed as strings, do any conversions necessary to get good types (e.g. boolean) + def __coerce(self, arg): + temp = arg.lower() + if temp == 'true': + return True + elif temp == 'false': + return False + elif temp == 'none': + return None + + return arg diff --git a/lib/urlresolver/plugins/2gbhosting.py b/lib/urlresolver/plugins/2gbhosting.py deleted file mode 100644 index a0ccbe8c..00000000 --- a/lib/urlresolver/plugins/2gbhosting.py +++ /dev/null @@ -1,92 +0,0 @@ -''' -2gbhosting urlresolver plugin -Copyright (C) 2011 t0mm0, DragonWin - -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 . -''' - -from t0mm0.common.net import Net -from urlresolver.plugnplay.interfaces import UrlResolver -from urlresolver.plugnplay.interfaces import PluginSettings -from urlresolver.plugnplay import Plugin -import re -import urllib2 -from urlresolver import common -import os - -class TwogbhostingResolver(Plugin, UrlResolver, PluginSettings): - implements = [UrlResolver, PluginSettings] - name = "2gbhosting" - - - def __init__(self): - p = self.get_setting('priority') or 100 - self.priority = int(p) - self.net = Net() - - - def get_media_url(self, host, media_id): - web_url = self.get_url(host, media_id) - data = {} - try: - html = self.net.http_GET(web_url).content - except urllib2.URLError, e: - common.addon.log_error('2gb-hosting: http error %d fetching %s' % - (e.code, web_url)) - return False - - r = re.search('', html) - if r: - sid = r.group(1) - common.addon.log_debug('eg-hosting: found k' + sid) - else: - common.addon.log_error('2gb-hosting: Could not find k') - return False - try: - data = { 'k' : sid,'submit' : 'Click Here To Continue', } - html = self.net.http_POST(web_url, data).content - except urllib2.URLError, e: - common.addon.log_error('2gbhosting: got http error %d fetching %s' % - (e.code, web_url)) - return False - - r = re.search('false\|(.+?)\|player\|bekle\|(.+?)\|(.+?)\|skin\|www\|(.+?)\|.+?stretching\|(.+?)\|start\|', html) - if r: - url_part4, stream_host, ext, url_part2, url_part1 = r.groups() - stream_url = 'http://%s.2gb-hosting.com/files/%s/%s/2gb/%s.%s' % ( - stream_host, url_part1, url_part2, url_part4, ext) - common.addon.log_debug('2gbhosting: streaming url ' + stream_url) - else: - common.addon.log_error('2gbhosting: stream_url not found') - return False - - return stream_url - - - def get_url(self, host, media_id): - return 'http://www.2gb-hosting.com/videos/%s' % media_id + '.html' - - - def get_host_and_id(self, url): - r = re.search('//(.+?)/videos/([0-9a-zA-Z/]+)', url) - if r: - return r.groups() - else: - return False - - - def valid_url(self, url, host): - return (re.match('http://(www.)?2gb-hosting.com/videos/' + - '[0-9A-Za-z]+/[0-9a-zA-Z]+.*', url) or - '2gb-hosting' in host) diff --git a/lib/urlresolver/plugins/__generic_resolver__.py b/lib/urlresolver/plugins/__generic_resolver__.py new file mode 100644 index 00000000..390317f4 --- /dev/null +++ b/lib/urlresolver/plugins/__generic_resolver__.py @@ -0,0 +1,51 @@ +""" + Kodi urlresolver plugin + Copyright (C) 2016 script.module.urlresolver + + 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 abc +from lib import helpers +from urlresolver.resolver import UrlResolver + +class GenericResolver(UrlResolver): + __metaclass__ = abc.ABCMeta + + """ + Generic Resolver + ___ + name |str| : resolver name + domains |list of str| : list of supported domains + pattern |str| : supported uri regex pattern, match groups: 1=host, 2=media_id + """ + name = 'generic' + domains = ['example.com'] + pattern = '(?://|\.)(example\.com)/(?:embed/)?([0-9a-zA-Z]+)' + + def get_media_url(self, host, media_id): + """ + source scraping to get resolved uri goes here + return |str| : resolved/playable uri or raise ResolverError + ___ + helpers.get_media_url result_blacklist: |list of str| : list of strings to blacklist in source results + """ + return helpers.get_media_url(self.get_url(host, media_id)).replace(' ', '%20') + + def get_url(self, host, media_id): + """ + return |str| : uri to be used by get_media_url + ___ + _default_get_url template: |str| : 'http://{host}/embed-{media_id}.html' + """ + return self._default_get_url(host, media_id) diff --git a/lib/urlresolver/plugins/__init__.py b/lib/urlresolver/plugins/__init__.py new file mode 100644 index 00000000..a6dfc451 --- /dev/null +++ b/lib/urlresolver/plugins/__init__.py @@ -0,0 +1,4 @@ +import os +import os.path +files = os.listdir(os.path.dirname(__file__)) +__all__ = [filename[:-3] for filename in files if not filename.startswith('__') and filename.endswith('.py')] diff --git a/lib/urlresolver/plugins/aliez.py b/lib/urlresolver/plugins/aliez.py new file mode 100644 index 00000000..05f754d0 --- /dev/null +++ b/lib/urlresolver/plugins/aliez.py @@ -0,0 +1,40 @@ +""" + OVERALL CREDIT TO: + t0mm0, Eldorado, VOINAGE, BSTRDMKR, tknorris, smokdpi, TheHighway + + urlresolver XBMC Addon + Copyright (C) 2011 t0mm0 + + 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 +from __generic_resolver__ import GenericResolver + +class AliezResolver(GenericResolver): + name = "aliez" + domains = ['aliez.me'] + pattern = '(?://|\.)(aliez\.me)/(?:(?:player/video\.php\?id=([0-9]+)&s=([A-Za-z0-9]+))|(?:video/([0-9]+)/([A-Za-z0-9]+)))' + + def get_host_and_id(self, url): + r = re.search(self.pattern, url, re.I) + if r: + r = filter(None, r.groups()) + r = [r[0], '%s|%s' % (r[1], r[2])] + return r + else: + return False + + def get_url(self, host, media_id): + media_id = media_id.split("|") + return self._default_get_url(host, media_id, 'http://emb.%s/player/video.php?id=%s&s=%s&w=590&h=332' % (host, media_id[0], media_id[1])) diff --git a/lib/urlresolver/plugins/alldebrid.py b/lib/urlresolver/plugins/alldebrid.py new file mode 100644 index 00000000..ca48f585 --- /dev/null +++ b/lib/urlresolver/plugins/alldebrid.py @@ -0,0 +1,120 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2011 t0mm0, JUL1EN094 + + 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 os +import re +import urllib +import json +from lib import helpers +from urlresolver import common +from urlresolver.common import i18n +from urlresolver.resolver import UrlResolver, ResolverError + +class AllDebridResolver(UrlResolver): + name = "AllDebrid" + domains = ['*'] + profile_path = common.profile_path + cookie_file = os.path.join(profile_path, '%s.cookies' % name) + media_url = None + + def __init__(self): + self.hosts = None + self.net = common.Net() + try: + os.makedirs(os.path.dirname(self.cookie_file)) + except OSError: + pass + + def get_media_url(self, host, media_id): + common.log_utils.log('in get_media_url %s : %s' % (host, media_id)) + url = 'http://www.alldebrid.com/service.php?link=%s' % (media_id) + html = self.net.http_GET(url).content + if html == 'login': + raise ResolverError('alldebrid: Authentication Error') + + try: + js_data = json.loads(html) + if 'error' in js_data and js_data['error']: + raise ResolverError('alldebrid: %s' % (js_data['error'])) + + if 'streaming' in js_data: + return helpers.pick_source(js_data['streaming'].items()) + except ResolverError: + raise + except: + match = re.search('''Control panel<' in html: + self.net.save_cookies(self.cookie_file) + self.net.set_cookies(self.cookie_file) + return True + else: + return False + + @classmethod + def get_settings_xml(cls): + xml = super(cls, cls).get_settings_xml() + xml.append('' % (cls.__name__, i18n('login'))) + xml.append('' % (cls.__name__, i18n('username'))) + xml.append('' % (cls.__name__, i18n('password'))) + return xml + + @classmethod + def isUniversal(self): + return True diff --git a/lib/urlresolver/plugins/allvid.py b/lib/urlresolver/plugins/allvid.py new file mode 100644 index 00000000..e02d63c2 --- /dev/null +++ b/lib/urlresolver/plugins/allvid.py @@ -0,0 +1,48 @@ +# -*- coding: UTF-8 -*- +""" + Copyright (C) 2014 smokdpi + + 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 +from lib import helpers +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + + +class AllVidResolver(UrlResolver): + name = "allvid" + domains = ["allvid.ch"] + pattern = '(?://|\.)(allvid\.ch)/(?:embed-)?([0-9a-zA-Z]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + headers = {'User-Agent': common.IE_USER_AGENT, + 'Referer': web_url} + html = self.net.http_GET(web_url, headers=headers).content + + iframe = re.findall('. +""" +from __generic_resolver__ import GenericResolver + +class AniStreamResolver(GenericResolver): + name = "ani-stream" + domains = ["ani-stream.com"] + pattern = '(?://|\.)(ani-stream\.com)/(?:embed-)?([0-9a-zA-Z-]+)' diff --git a/lib/urlresolver/plugins/anyfiles.py b/lib/urlresolver/plugins/anyfiles.py new file mode 100644 index 00000000..6f7c829f --- /dev/null +++ b/lib/urlresolver/plugins/anyfiles.py @@ -0,0 +1,47 @@ +""" + Copyright (C) 2014 smokdpi + + 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 +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class AnyFilesResolver(UrlResolver): + name = "anyfiles" + domains = ["anyfiles.pl"] + pattern = '(?://|\.)(anyfiles\.pl)/.*?(?:id=|v=|/)([0-9]+)' + + def __init__(self): + self.net = common.Net() + self.user_agent = common.IE_USER_AGENT + self.headers = {'User-Agent': self.user_agent} + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + self.headers['Referer'] = web_url + html = self.net.http_GET(web_url, headers=self.headers).content + r = re.search('src="/?(pcs\?code=[^"]+?)"', html, re.DOTALL) + if r: + web_url = 'http://video.anyfiles.pl/%s' % (r.group(1)) + html = self.net.http_GET(web_url, headers=self.headers).content + match = re.search('(http[^"]+?mp4)', html) + if match: + return match.group(1) + + else: + raise ResolverError('File not found') + + def get_url(self, host, media_id): + return "http://video.anyfiles.pl/w.jsp?id=%s&width=620&height=349&pos=&skin=0" % (media_id) diff --git a/lib/urlresolver/plugins/apnasave.py b/lib/urlresolver/plugins/apnasave.py new file mode 100644 index 00000000..944f00d6 --- /dev/null +++ b/lib/urlresolver/plugins/apnasave.py @@ -0,0 +1,57 @@ +""" + urlresolver Kodi plugin + Copyright (C) 2016 Gujal + + 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 +from urlresolver import common +from lib import helpers +from urlresolver.resolver import UrlResolver, ResolverError + +class ApnaSaveResolver(UrlResolver): + name = "apnasave.com" + domains = ["www.apnasave.club"] + pattern = '(?://|\.)(apnasave\.club)/embed/([0-9a-f]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + resp = self.net.http_GET(web_url) + html = resp.content + headers = dict(resp._response.info().items()) + headers = {'Cookie': headers['set-cookie']} + headers['User-Agent'] = common.FF_USER_AGENT + r = re.search('player.swf\?f=(.*?)"', html) + + if r: + stream_xml = r.group(1) + headers['Referer'] = 'http://www.apnasave.club/media/player/player.swf?f=%s' % stream_xml + response = self.net.http_GET(stream_xml, headers=headers) + xmlhtml = response.content + + r2 = re.search('(.*?)', xmlhtml) + + stream_url = r2.group(1) + helpers.append_headers(headers) + else: + raise ResolverError('no file located') + + return stream_url + + def get_url(self, host, media_id): + return 'http://%s/embed/%s' % (host, media_id) + diff --git a/lib/urlresolver/plugins/auengine.py b/lib/urlresolver/plugins/auengine.py new file mode 100644 index 00000000..915baa8e --- /dev/null +++ b/lib/urlresolver/plugins/auengine.py @@ -0,0 +1,43 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2011 t0mm0 + + 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 urllib +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class AuEngineResolver(UrlResolver): + name = "auengine.com" + domains = ["auengine.com"] + pattern = '(?://|\.)(auengine\.com)/embed.php\?file=([0-9a-zA-Z\-_]+)[&]*' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + resp = self.net.http_GET(web_url) + html = resp.content + r = re.search("video_link\s=\s'(.+?)';", html) + if r: + return urllib.unquote_plus(r.group(1)) + else: + raise ResolverError('no file located') + + def get_url(self, host, media_id): + return 'http://www.auengine.com/embed.php?file=%s' % (media_id) diff --git a/lib/urlresolver/plugins/blazefile.py b/lib/urlresolver/plugins/blazefile.py new file mode 100644 index 00000000..c1910738 --- /dev/null +++ b/lib/urlresolver/plugins/blazefile.py @@ -0,0 +1,26 @@ +""" + Kodi urlresolver plugin + Copyright (C) 2016 tknorris + + 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 . +""" +from __generic_resolver__ import GenericResolver + +class BlazefileResolver(GenericResolver): + name = 'blazefile' + domains = ['blazefile.co'] + pattern = '(?://|\.)(blazefile\.co)/(?:embed-)?([0-9a-zA-Z]+)' + + def get_url(self, host, media_id): + return 'https://www.blazefile.co/embed-%s.html' % (media_id) diff --git a/lib/urlresolver/plugins/castamp.py b/lib/urlresolver/plugins/castamp.py new file mode 100644 index 00000000..9cb9f93b --- /dev/null +++ b/lib/urlresolver/plugins/castamp.py @@ -0,0 +1,71 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2011 t0mm0 + + 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 random +import re +import math +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class CastampResolver(UrlResolver): + name = "castamp" + domains = ["castamp.com"] + pattern = '(?://|\.)(castamp\.com)/embed\.php\?c=(.*?)&' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + html = self.net.http_GET(web_url).content + + streamer = "" + flashplayer = "" + file = "" + + common.log_utils.log("*******************************************") + common.log_utils.log("web_url: " + web_url) + + pattern_flashplayer = r"""'flashplayer': \"(.*?)\"""" + r = re.search(pattern_flashplayer, html) + if r: + flashplayer = r.group(1) + + pattern_streamer = r"""'streamer': '(.*?)'""" + r = re.search(pattern_streamer, html) + if r: + streamer = r.group(1) + + pattern_file = r"""'file': '(.*?)'""" + r = re.search(pattern_file, html) + if r: + file = r.group(1) + + rtmp = streamer + rtmp += '/%s swfUrl=%s live=true swfVfy=true pageUrl=%s tcUrl=%s' % (file, flashplayer, web_url, rtmp) + + return rtmp + + def get_url(self, host, media_id): + chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz" + string_length = 8 + randomstring = '' + for _x in range(0, string_length): + rnum = int(math.floor(random.random() * len(chars))) + randomstring += chars[rnum:rnum + 1] + domainsa = randomstring + return 'http://www.castamp.com/embed.php?c=%s&tk=%s' % (media_id, domainsa) diff --git a/lib/urlresolver/plugins/clicknupload.py b/lib/urlresolver/plugins/clicknupload.py new file mode 100644 index 00000000..9629a1e0 --- /dev/null +++ b/lib/urlresolver/plugins/clicknupload.py @@ -0,0 +1,60 @@ +''' +clicknupload urlresolver plugin +Copyright (C) 2015 tknorris + +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 urllib +from lib import helpers +from lib import captcha_lib +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +MAX_TRIES = 3 + +class ClickNUploadResolver(UrlResolver): + name = "clicknupload" + domains = ['clicknupload.com', 'clicknupload.me', 'clicknupload.link'] + pattern = '(?://|\.)(clicknupload\.(?:com|me|link))/(?:f/)?([0-9A-Za-z]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + headers = { + 'User-Agent': common.FF_USER_AGENT, + 'Referer': web_url + } + html = self.net.http_GET(web_url, headers=headers).content + tries = 0 + while tries < MAX_TRIES: + data = helpers.get_hidden(html) + data.update(captcha_lib.do_captcha(html)) + html = self.net.http_POST(web_url, data, headers=headers).content + r = re.search('''class="downloadbtn"[^>]+onClick\s*=\s*\"window\.open\('([^']+)''', html) + if r: + return r.group(1) + helpers.append_headers(headers) + + if tries > 0: + common.kodi.sleep(1000) + + tries = tries + 1 + + raise ResolverError('Unable to locate link') + + def get_url(self, host, media_id): + return 'https://clicknupload.link/%s' % media_id diff --git a/lib/urlresolver/plugins/cloudmailru.py b/lib/urlresolver/plugins/cloudmailru.py new file mode 100644 index 00000000..108038d3 --- /dev/null +++ b/lib/urlresolver/plugins/cloudmailru.py @@ -0,0 +1,44 @@ +""" + OVERALL CREDIT TO: + t0mm0, Eldorado, VOINAGE, BSTRDMKR, tknorris, smokdpi, TheHighway + + urlresolver XBMC Addon + Copyright (C) 2011 t0mm0 + + 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 +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class MailRuResolver(UrlResolver): + name = "cloud.mail.ru" + domains = ['cloud.mail.ru'] + pattern = '(?://|\.)(cloud\.mail\.ru)/public/([0-9A-Za-z]+/[^/]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + html = self.net.http_GET(web_url).content + html = re.sub(r'[^\x00-\x7F]+', ' ', html) + url_match = re.search('"weblink_get"\s*:\s*\[.+?"url"\s*:\s*"([^"]+)', html) + tok_match = re.search('"tokens"\s*:\s*{\s*"download"\s*:\s*"([^"]+)', html) + if url_match and tok_match: + return '%s/%s?key=%s' % (url_match.group(1), media_id, tok_match.group(1)) + raise ResolverError('No playable video found.') + + def get_url(self, host, media_id): + return self._default_get_url(host, media_id, template='https://{host}/public/{media_id}') diff --git a/lib/urlresolver/plugins/cloudy.py b/lib/urlresolver/plugins/cloudy.py new file mode 100644 index 00000000..fedf8a00 --- /dev/null +++ b/lib/urlresolver/plugins/cloudy.py @@ -0,0 +1,96 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2011 t0mm0 + + 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 urllib +from lib import unwise +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class CloudyResolver(UrlResolver): + name = "cloudy.ec" + domains = ["cloudy.ec", "cloudy.eu", "cloudy.sx", "cloudy.ch", "cloudy.com"] + pattern = '(?://|\.)(cloudy\.(?:ec|eu|sx|ch|com))/(?:video/|v/|embed\.php\?id=)([0-9A-Za-z]+)' + + def __init__(self): + self.net = common.Net() + + def __get_stream_url(self, media_id, filekey, error_num=0, error_url=None): + ''' + Get stream url. + If previously found stream url is a dead link, add error params and try again + ''' + + if error_num > 0 and error_url: + _error_params = '&numOfErrors={0}&errorCode=404&errorUrl={1}'.format(error_num, urllib.quote_plus(error_url).replace('.', '%2E')) + else: + _error_params = '' + + # use api to find stream address + api_call = 'http://www.cloudy.ec/api/player.api.php?{0}&file={1}&key={2}{3}'.format( + 'user=undefined&pass=undefined', media_id, urllib.quote_plus(filekey).replace('.', '%2E'), _error_params) + + api_html = self.net.http_GET(api_call).content + rapi = re.search('url=(.+?)&title=', api_html) + if rapi: + return urllib.unquote(rapi.group(1)) + + return None + + def __is_stream_url_active(self, web_url): + try: + header = self.net.http_HEAD(web_url) + if header.get_headers(): + return True + + return False + except: + return False + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + # grab stream details + html = self.net.http_GET(web_url).content + html = unwise.unwise_process(html) + filekey = unwise.resolve_var(html, "vars.key") + + error_url = None + stream_url = None + # try to resolve 3 times then give up + for x in range(0, 2): + link = self.__get_stream_url(media_id, filekey, error_num=x, error_url=error_url) + if link: + active = self.__is_stream_url_active(link) + + if active: + stream_url = urllib.unquote(link) + break + else: + # link inactive + error_url = link + else: + # no link found + raise ResolverError('File Not Found or removed') + + if stream_url: + return stream_url + else: + raise ResolverError('File Not Found or removed') + + def get_url(self, host, media_id): + return 'http://www.cloudy.ec/embed.php?id=%s' % media_id diff --git a/lib/urlresolver/plugins/cloudzilla.py b/lib/urlresolver/plugins/cloudzilla.py new file mode 100644 index 00000000..b0cce26c --- /dev/null +++ b/lib/urlresolver/plugins/cloudzilla.py @@ -0,0 +1,26 @@ +""" +TheFile.me urlresolver plugin +Copyright (C) 2013 voinage + +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 . +""" +from __generic_resolver__ import GenericResolver + +class CloudZillaResolver(GenericResolver): + name = "cloudzilla" + domains = ['cloudzilla.to', 'neodrive.co'] + pattern = '(?://|\.)(cloudzilla.to|neodrive.co)/(?:share/file|embed)/([A-Za-z0-9]+)' + + def get_url(self, host, media_id): + return self._default_get_url(host, media_id, 'http://{host}/embed/{media_id}') diff --git a/lib/urlresolver/plugins/daclips.py b/lib/urlresolver/plugins/daclips.py new file mode 100644 index 00000000..757fd19a --- /dev/null +++ b/lib/urlresolver/plugins/daclips.py @@ -0,0 +1,50 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2011 t0mm0 + + 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 +from lib import helpers +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class DaclipsResolver(UrlResolver): + name = "daclips" + domains = ["daclips.in", "daclips.com"] + pattern = '(?://|\.)(daclips\.(?:in|com))/(?:embed-)?([0-9a-zA-Z]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + """ Human Verification """ + resp = self.net.http_GET(web_url) + html = resp.content + r = re.findall(r'404 - File Not Found', html) + if r: + raise ResolverError('File Not Found or removed') + post_url = resp.get_url() + form_values = helpers.get_hidden(html) + html = self.net.http_POST(post_url, form_data=form_values).content + r = re.search('file: "http(.+?)"', html) + if r: + return "http" + r.group(1) + else: + raise ResolverError('Unable to resolve Daclips link') + + def get_url(self, host, media_id): + return 'http://daclips.in/%s' % (media_id) diff --git a/lib/urlresolver/plugins/dailymotion.py b/lib/urlresolver/plugins/dailymotion.py index 61d9498e..543f44b5 100644 --- a/lib/urlresolver/plugins/dailymotion.py +++ b/lib/urlresolver/plugins/dailymotion.py @@ -16,67 +16,60 @@ along with this program. If not, see . ''' -from t0mm0.common.net import Net -from urlresolver.plugnplay.interfaces import UrlResolver -from urlresolver.plugnplay.interfaces import PluginSettings -from urlresolver.plugnplay import Plugin -import re -import urllib2, urllib +import re,urllib +from lib import helpers from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError -class DailymotionResolver(Plugin, UrlResolver, PluginSettings): - implements = [UrlResolver, PluginSettings] - name = "dailymotion" +class DailymotionResolver(UrlResolver): + name = 'dailymotion' + domains = ['dailymotion.com'] + pattern = '(?://|\.)(dailymotion\.com)/(?:video|embed|sequence|swf)(?:/video)?/([0-9a-zA-Z]+)' def __init__(self): - p = self.get_setting('priority') or 100 - self.priority = int(p) - self.net = Net() - + self.net = common.Net() def get_media_url(self, host, media_id): web_url = self.get_url(host, media_id) - try: - html = self.net.http_GET(web_url).content - except urllib2.URLError, e: - common.addon.log_error(self.name + '- got http error %d fetching %s' % - (e.code, web_url)) - return False + html = self.net.http_GET(web_url).content + if '"reason":"video attribute|explicit"' in html: + headers = {'User-Agent': common.FF_USER_AGENT, 'Referer': web_url} + url_back = '/embed/video/%s' % media_id + web_url = 'http://www.dailymotion.com/family_filter?enable=false&urlback=%s' % urllib.quote_plus(url_back) + html = self.net.http_GET(url=web_url, headers=headers).content + + html = html.replace('\\', '') - fragment = re.search('"sequence", "(.+?)"', html) - decoded_frag = urllib.unquote(fragment.group(1)).decode('utf8').replace('\\/','/') - r = re.search('"hqURL":"(.+?)"', decoded_frag) - if r: - stream_url = r.group(1) - else: - message = self.name + '- 1st attempt at finding the stream_url failed' - common.addon.log_debug(message) - r = re.search('"sdURL":"(.+?)"', decoded_frag) - if r: - stream_url = r.group(1) - else: - message = self.name + '- Giving up on finding the stream_url' - common.addon.log_error(message) - return False - return stream_url + livesource = re.findall('"auto"\s*:\s*.+?"url"\s*:\s*"(.+?)"', html) + sources = re.findall('"(\d+)"\s*:.+?"url"\s*:\s*"([^"]+)', html) + + if not sources and not livesource: + raise ResolverError('File not found') - def get_url(self, host, media_id): - return 'http://www.dailymotion.com/video/%s' % media_id + if livesource and not sources: + return self.net.http_HEAD(livesource[0]).get_url() + + sources = sorted(sources, key=lambda x: x[0])[::-1] + + source = helpers.pick_source(sources) + if not '.m3u8' in source: + raise ResolverError('File not found') - def get_host_and_id(self, url): - r = re.search('//(.+?)/video/([0-9A-Za-z]+)', url) - if r: - return r.groups() - else: - r = re.search('//(.+?)/swf/([0-9A-Za-z]+)', url) - if r: - return r.groups() - else: - return False - + vUrl = self.net.http_GET(source).content + vUrl = re.search('(http.+?m3u8)', vUrl) + + if vUrl: + return vUrl.group(1) + + raise ResolverError('File not found') + + def get_url(self, host, media_id): + return 'http://www.dailymotion.com/embed/video/%s' % media_id - def valid_url(self, url, host): - return re.match('http://(www.)?dailymotion.com/video/[0-9A-Za-z]+', url) or \ - re.match('http://(www.)?dailymotion.com/swf/[0-9A-Za-z]+', url) or self.name in host \ No newline at end of file + @classmethod + def get_settings_xml(cls): + xml = super(cls, cls).get_settings_xml() + xml.append('' % (cls.__name__)) + return xml diff --git a/lib/urlresolver/plugins/divxstage.py b/lib/urlresolver/plugins/divxstage.py index a2a94490..176513ad 100644 --- a/lib/urlresolver/plugins/divxstage.py +++ b/lib/urlresolver/plugins/divxstage.py @@ -15,76 +15,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' +from __generic_resolver__ import GenericResolver -from t0mm0.common.net import Net -from urlresolver.plugnplay.interfaces import UrlResolver -from urlresolver.plugnplay.interfaces import PluginSettings -from urlresolver.plugnplay import Plugin -import re -import urllib2 -from urlresolver import common - -class DivxstageResolver(Plugin, UrlResolver, PluginSettings): - implements = [UrlResolver, PluginSettings] - name = "divxstage" - - def __init__(self): - p = self.get_setting('priority') or 100 - self.priority = int(p) - self.net = Net() - - - def get_media_url(self, host, media_id): - web_url = self.get_url(host, media_id) - try: - html = self.net.http_GET(web_url).content - except urllib2.URLError, e: - common.addon.log_error('Divxstage: got http error %d fetching %s' % - (e.code, web_url)) - return False - - r = re.search('. +""" +from __generic_resolver__ import GenericResolver + +class DownaceResolver(GenericResolver): + name = 'downace' + domains = ['downace.com'] + pattern = '(?://|\.)(downace\.com)/(?:embed/)?([0-9a-zA-Z]+)' + + def get_url(self, host, media_id): + return 'https://www.downace.com/embed/%s' % (media_id) diff --git a/lib/urlresolver/plugins/ecostream.py b/lib/urlresolver/plugins/ecostream.py index 02210ad5..8dc19d22 100644 --- a/lib/urlresolver/plugins/ecostream.py +++ b/lib/urlresolver/plugins/ecostream.py @@ -15,81 +15,59 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ - -from t0mm0.common.net import Net -from urlresolver.plugnplay.interfaces import UrlResolver -from urlresolver.plugnplay.interfaces import PluginSettings -from urlresolver.plugnplay import Plugin +import re import urllib2 +from lib import helpers from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError -# Custom imports -import re - - - -class EcostreamResolver(Plugin, UrlResolver, PluginSettings): - implements = [UrlResolver, PluginSettings] +class EcostreamResolver(UrlResolver): name = "ecostream" + domains = ["ecostream.tv"] + pattern = '(?://|\.)(ecostream.tv)/(?:stream|embed)?/([0-9a-zA-Z]+)' def __init__(self): - p = self.get_setting('priority') or 100 - self.priority = int(p) - self.net = Net() - self.pattern = 'http://((?:www.)?ecostream.tv)/(?:stream|embed)?/([0-9a-zA-Z]+).html' - + self.net = common.Net() def get_media_url(self, host, media_id): - # emulate click on button "Start Stream" (ss=1) - web_url = self.get_url(host, media_id) + "?ss=1" - - try: - html = self.net.http_GET(web_url).content - except urllib2.URLError, e: - common.addon.log_error(self.name + ': got http error %d fetching %s' % - (e.code, web_url)) - return False - - # get vars - sPattern = "var t=setTimeout\(\"lc\('([^']+)','([^']+)','([^']+)','([^']+)'\)" - r = re.findall(sPattern, html) - if r: - for aEntry in r: - sS = str(aEntry[0]) - sK = str(aEntry[1]) - sT = str(aEntry[2]) - sKey = str(aEntry[3]) - - # send vars and retrieve stream url - sNextUrl = 'http://www.ecostream.tv/object.php?s='+sS+'&k='+sK+'&t='+sT+'&key='+sKey - - try: - html = self.net.http_GET(sNextUrl).content - except urllib2.URLError, e: - common.addon.log_error(self.name + ': got http error %d fetching %s' % - (e.code, sNextUrl)) - return False - - sPattern = '. +""" +from __generic_resolver__ import GenericResolver + +class EstreamResolver(GenericResolver): + name = "estream" + domains = ['estream.to'] + pattern = '(?://|\.)(estream\.to)/(?:embed-)?([a-zA-Z0-9]+)' diff --git a/lib/urlresolver/plugins/exashare.py b/lib/urlresolver/plugins/exashare.py new file mode 100644 index 00000000..f51f4a8a --- /dev/null +++ b/lib/urlresolver/plugins/exashare.py @@ -0,0 +1,42 @@ +""" +Exashare.com urlresolver XBMC Addon +Copyright (C) 2014 JUL1EN094 + +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 +from lib import helpers +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class ExashareResolver(UrlResolver): + name = "exashare" + domains = ["exashare.com", "uame8aij4f.com", "yahmaib3ai.com"] + pattern = '(?://|\.)((?:yahmaib3ai|uame8aij4f|exashare)\.com)/(?:embed-)?([0-9a-zA-Z]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url('exashare.com', media_id) + html = self.net.http_GET(web_url).content + + try: web_url = re.search('src="([^"]+)', html).group(1) + except: raise ResolverError('Unable to locate link') + + return helpers.get_media_url(web_url) + + def get_url(self, host, media_id): + return self._default_get_url(host, media_id) diff --git a/lib/urlresolver/plugins/facebook.py b/lib/urlresolver/plugins/facebook.py new file mode 100644 index 00000000..a38c9de3 --- /dev/null +++ b/lib/urlresolver/plugins/facebook.py @@ -0,0 +1,69 @@ +''' +facebook urlresolver plugin +Copyright (C) 2013 icharania + +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 urllib +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class FacebookResolver(UrlResolver): + name = "facebook" + domains = ["facebook.com"] + pattern = '(?://|\.)(facebook\.com)/.+?video_id=([0-9a-zA-Z]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + link = self.net.http_GET(web_url).content + + if link.find('Video Unavailable') >= 0: + err_message = 'The requested video was not found.' + raise ResolverError(err_message) + + params = re.compile('"params","([\w\%\-\.\\\]+)').findall(link)[0] + html = urllib.unquote(params.replace('\u0025', '%')).decode('utf-8') + html = html.replace('\\', '') + + videoUrl = re.compile('(?:hd_src|sd_src)\":\"([\w\-\.\_\/\&\=\:\?]+)').findall(html) + + vUrl = '' + vUrlsCount = len(videoUrl) + if vUrlsCount > 0: + q = self.get_setting('quality') + if q == '0': + # Highest Quality + vUrl = videoUrl[0] + else: + # Standard Quality + vUrl = videoUrl[vUrlsCount - 1] + + return vUrl + + else: + raise ResolverError('No playable video found.') + + def get_url(self, host, media_id): + return 'https://www.facebook.com/video/embed?video_id=%s' % media_id + + @classmethod + def get_settings_xml(cls): + xml = super(cls, cls).get_settings_xml() + xml.append('' % (cls.__name__)) + return xml diff --git a/lib/urlresolver/plugins/fastplay.py b/lib/urlresolver/plugins/fastplay.py new file mode 100644 index 00000000..26d511c0 --- /dev/null +++ b/lib/urlresolver/plugins/fastplay.py @@ -0,0 +1,23 @@ +''' + urlresolver XBMC Addon + Copyright (C) 2016 Gujal + +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 . +''' +from __generic_resolver__ import GenericResolver + +class FastplayResolver(GenericResolver): + name = 'fastplay.sx' + domains = ['fastplay.sx', 'fastplay.cc'] + pattern = '(?://|\.)(fastplay\.(?:sx|cc))/(?:flash-|embed-)?([0-9a-zA-Z]+)' diff --git a/lib/urlresolver/plugins/filehoot.py b/lib/urlresolver/plugins/filehoot.py new file mode 100644 index 00000000..93f88593 --- /dev/null +++ b/lib/urlresolver/plugins/filehoot.py @@ -0,0 +1,49 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2015 tknorris + + 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 . +""" + +from lib import helpers +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class FilehootResolver(UrlResolver): + name = "filehoot" + domains = ['filehoot.com'] + pattern = '(?://|\.)(filehoot\.com)/(?:embed-)?([0-9a-z]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + headers = {'User-Agent': common.FF_USER_AGENT} + + web_url = self.get_url(host, media_id) + + html = self.net.http_GET(web_url, headers=headers).content + + if '404 Not Found' in html: + raise ResolverError('The requested video was not found.') + + url = helpers.scrape_sources(html) + + if url: + return url[0][1] + helpers.append_headers(headers) + + raise ResolverError('No video link found.') + + def get_url(self, host, media_id): + return self._default_get_url(host, media_id) diff --git a/lib/urlresolver/plugins/filepup.py b/lib/urlresolver/plugins/filepup.py new file mode 100644 index 00000000..65734522 --- /dev/null +++ b/lib/urlresolver/plugins/filepup.py @@ -0,0 +1,76 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2015 tknorris + + 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 +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError +from lib import helpers + +class FilePupResolver(UrlResolver): + name = "filepup" + domains = ["filepup.net"] + pattern = '(?://|\.)(filepup.(?:net))/(?:play|files)/([0-9a-zA-Z]+)' + headers = {'User-Agent': common.SMU_USER_AGENT} + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + html = self.net.http_GET(web_url, headers=self.headers).content + common.log_utils.log(html) + default_url = self.__get_def_source(html) + if default_url: + qualities = self.__get_qualities(html) + def_quality = self.__get_default(html) + sources = [] + for quality in qualities: + if quality == def_quality: + sources.append((quality, default_url)) + else: + stream_url = default_url.replace('.mp4?', '-%s.mp4?' % (quality)) + sources.append((quality, stream_url)) + try: sources.sort(key=lambda x: int(x[0][:-1]), reverse=True) + except: pass + return helpers.pick_source(sources) + + def __get_def_source(self, html): + default_url = '' + match = re.search('sources\s*:\s*\[(.*?)\]', html, re.DOTALL) + if match: + match = re.search('src\s*:\s*"([^"]+)', match.group(1)) + if match: + default_url = match.group(1) + helpers.append_headers(self.headers) + return default_url + + def __get_default(self, html): + match = re.search('defaultQuality\s*:\s*"([^"]+)', html) + if match: + return match.group(1) + else: + return '' + + def __get_qualities(self, html): + qualities = [] + match = re.search('qualities\s*:\s*\[(.*?)\]', html) + if match: + qualities = re.findall('"([^"]+)"', match.group(1)) + return qualities + + def get_url(self, host, media_id): + return 'http://www.filepup.net/play/%s' % (media_id) diff --git a/lib/urlresolver/plugins/fileweed.py b/lib/urlresolver/plugins/fileweed.py new file mode 100644 index 00000000..08158ab2 --- /dev/null +++ b/lib/urlresolver/plugins/fileweed.py @@ -0,0 +1,56 @@ +""" + Kodi urlresolver plugin + Copyright (C) 2016 tknorris + + 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 urllib +from lib import helpers +from lib import captcha_lib +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +MAX_TRIES = 3 + +class FileWeedResolver(UrlResolver): + name = "FileWeed" + domains = ["fileweed.net"] + pattern = '(?://|\.)(fileweed\.net)/(?:embed-)?([0-9a-zA-Z/-]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + headers = {'User-Agent': common.FF_USER_AGENT, 'Referer': web_url} + html = self.net.http_GET(web_url, headers=headers).content + tries = 0 + while tries < MAX_TRIES: + data = helpers.get_hidden(html, index=1) + data.update(captcha_lib.do_captcha(html)) + common.log_utils.log_debug(data) + html = self.net.http_POST(web_url, data, headers=headers).content + + if 'downloadbtn222' in html: + r = re.search('class="downloadbtn222".*?href="([^"]+)', html, re.I | re.DOTALL) + if r: + return r.group(1) + helpers.append_headers({'User-Agent': common.IE_USER_AGENT}) + + tries = tries + 1 + + raise ResolverError('Unable to locate link') + + def get_url(self, host, media_id): + return self._default_get_url(host, media_id, template='https://{host}/{media_id}') diff --git a/lib/urlresolver/plugins/flashx.py b/lib/urlresolver/plugins/flashx.py index ebb3a28e..56ef5893 100644 --- a/lib/urlresolver/plugins/flashx.py +++ b/lib/urlresolver/plugins/flashx.py @@ -1,6 +1,7 @@ """ - urlresolver XBMC Addon - Copyright (C) 2011 t0mm0 + Kodi urlresolver plugin + Copyright (C) 2014 smokdpi + Updated by Gujal (c) 2016 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 @@ -15,59 +16,38 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ - -from t0mm0.common.net import Net -from urlresolver.plugnplay.interfaces import UrlResolver -from urlresolver.plugnplay.interfaces import PluginSettings -from urlresolver.plugnplay import Plugin -import urllib2 +import os +import fx_gmu from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError -# Custom imports -import re - +FX_SOURCE = 'https://offshoregit.com/tvaresolvers/fx_gmu.py' +FX_PATH = os.path.join(common.plugins_path, 'fx_gmu.py') -class FlashxResolver(Plugin, UrlResolver, PluginSettings): - implements = [UrlResolver, PluginSettings] +class FlashxResolver(UrlResolver): name = "flashx" + domains = ["flashx.tv"] + pattern = '(?://|\.)(flashx\.tv)/(?:embed-|dl\?|embed.php\?c=)?([0-9a-zA-Z/-]+)' def __init__(self): - p = self.get_setting('priority') or 100 - self.priority = int(p) - self.net = Net() - #e.g. http://flashx.tv/player/embed_player.php?vid=1503&width=600&height=370&autoplay=no - self.pattern = 'http://((?:www.)?flashx.tv)/(?:player/embed_player.php\?vid=|video/)([0-9A-Z]+)' - + self.net = common.Net() def get_media_url(self, host, media_id): - web_url = self.get_url(host, media_id) - try: - html = self.net.http_GET(web_url).content - except urllib2.URLError, e: - common.addon.log_error(self.name + ': got http error %d fetching %s' % - (e.code, web_url)) - return False - - #grab stream url - sPatternHQ = "var hq_video_file\s*=\s*'([^']+)'" # .mp4 - sPatternLQ = "var normal_video_file\s*=\s*'([^']+)'" # .flv - r = re.search(sPatternLQ, html) - if r: - return r.group(1) - - return False - + self._auto_update(FX_SOURCE, FX_PATH) + reload(fx_gmu) + web_url = self.get_url(host, media_id) + return fx_gmu.get_media_url(web_url) + except Exception as e: + common.log_utils.log_debug('Exception during flashx resolve parse: %s' % e) + raise + def get_url(self, host, media_id): - return 'http://www.flashx.tv/player/embed_player.php?vid=%s' % (media_id) - - def get_host_and_id(self, url): - r = re.search(self.pattern, url) - if r: - return r.groups() - else: - return False - - - def valid_url(self, url, host): - return re.match(self.pattern, url) or self.name in host + return self._default_get_url(host, media_id, 'https://{host}/embed.php?c={media_id}') + + @classmethod + def get_settings_xml(cls): + xml = super(cls, cls).get_settings_xml() + xml.append('' % (cls.__name__)) + xml.append('' % (cls.__name__)) + return xml diff --git a/lib/urlresolver/plugins/fx_gmu.py b/lib/urlresolver/plugins/fx_gmu.py new file mode 100644 index 00000000..18bc429d --- /dev/null +++ b/lib/urlresolver/plugins/fx_gmu.py @@ -0,0 +1,91 @@ +""" +flashx.tv urlresolver plugin +Copyright (C) 2015 tknorris + +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 urlparse +import urllib +from lib import helpers +from urlresolver import common +from urlresolver.resolver import ResolverError + +SORT_KEY = {'High': 3, 'Middle': 2, 'Low': 1} +net = common.Net() + +def get_media_url(url): + try: + hostname = urlparse.urlparse(url).hostname + headers = {'User-Agent': common.FF_USER_AGENT} + html = net.http_GET(url, headers=headers).content + headers.update({'Referer': url}) + for match in re.finditer(''']*src=["']([^'"]+)''', html): + _html = get_js(match.group(1), headers, hostname) + + match = re.search('''href=['"]([^'"]+)''', html) + if match: + playvid_url = match.group(1) + html = net.http_GET(playvid_url, headers=headers).content + headers.update({'Referer': playvid_url}) + for match in re.finditer(''']*src=["']([^'"]+)''', html): + js = get_js(match.group(1), headers, hostname) + match = re.search('''!=\s*null.*?get\(['"]([^'"]+).*?\{([^:]+)''', js, re.DOTALL) + if match: + fx_url, fx_param = match.groups() + fx_url = resolve_url(urlparse.urljoin('http://www.flashx.tv', fx_url) + '?' + urllib.urlencode({fx_param: 1})) + common.log_utils.log('fxurl: %s' % (fx_url)) + _html = net.http_GET(fx_url, headers=headers).content + + headers.update({'Referer': url}) + html = net.http_GET(playvid_url, headers=headers).content + html = helpers.add_packed_data(html) + + common.log_utils.log(html) + sources = helpers.parse_sources_list(html) + try: sources.sort(key=lambda x: SORT_KEY.get(x[0], 0), reverse=True) + except: pass + source = helpers.pick_source(sources) + return source + helpers.append_headers(headers) + + except Exception as e: + common.log_utils.log_debug('Exception during flashx resolve parse: %s' % e) + raise + + raise ResolverError('Unable to resolve flashx link. Filelink not found.') + +def get_js(js_url, headers, hostname): + js = '' + if not js_url.startswith('http'): + base_url = 'http://' + hostname + js_url = urlparse.urljoin(base_url, js_url) + + if hostname in js_url: + common.log_utils.log('Getting JS: |%s| - |%s|' % (js_url, headers)) + js = net.http_GET(js_url, headers=headers).content + return js + +def resolve_url(url): + parts = list(urlparse.urlsplit(url)) + segments = parts[2].split('/') + segments = [segment + '/' for segment in segments[:-1]] + [segments[-1]] + resolved = [] + for segment in segments: + if segment in ('../', '..'): + if resolved[1:]: + resolved.pop() + elif segment not in ('./', '.'): + resolved.append(segment) + parts[2] = ''.join(resolved) + return urlparse.urlunsplit(parts) diff --git a/lib/urlresolver/plugins/googlevideo.py b/lib/urlresolver/plugins/googlevideo.py new file mode 100644 index 00000000..b438ffd1 --- /dev/null +++ b/lib/urlresolver/plugins/googlevideo.py @@ -0,0 +1,187 @@ +""" + Kodi urlresolver plugin + Copyright (C) 2014 smokdpi + + 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 . +""" + +from urlresolver import common, hmf +from urlresolver.resolver import UrlResolver, ResolverError +from lib import helpers +import re +import json +import urllib2 + + +class GoogleResolver(UrlResolver): + name = "googlevideo" + domains = ["googlevideo.com", "googleusercontent.com", "get.google.com", + "plus.google.com", "googledrive.com", "drive.google.com", "docs.google.com"] + pattern = 'https?://(.*?(?:\.googlevideo|(?:plus|drive|get|docs)\.google|google(?:usercontent|drive))\.com)/(.*?(?:videoplayback\?|[\?&]authkey|host/)*.+)' + + def __init__(self): + self.net = common.Net() + self.itag_map = {'5': '240', '6': '270', '17': '144', '18': '360', '22': '720', '34': '360', '35': '480', + '36': '240', '37': '1080', '38': '3072', '43': '360', '44': '480', '45': '720', '46': '1080', + '82': '360 [3D]', '83': '480 [3D]', '84': '720 [3D]', '85': '1080p [3D]', '100': '360 [3D]', + '101': '480 [3D]', '102': '720 [3D]', '92': '240', '93': '360', '94': '480', '95': '720', + '96': '1080', '132': '240', '151': '72', '133': '240', '134': '360', '135': '480', + '136': '720', '137': '1080', '138': '2160', '160': '144', '264': '1440', + '298': '720', '299': '1080', '266': '2160', '167': '360', '168': '480', '169': '720', + '170': '1080', '218': '480', '219': '480', '242': '240', '243': '360', '244': '480', + '245': '480', '246': '480', '247': '720', '248': '1080', '271': '1440', '272': '2160', + '302': '2160', '303': '1080', '308': '1440', '313': '2160', '315': '2160', '59': '480'} + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + response, video_urls = self._parse_google(web_url) + if video_urls: + video = helpers.pick_source(video_urls) + else: + video = None + + headers = {'User-Agent': common.FF_USER_AGENT} + if response is not None: + res_headers = response.get_headers(as_dict=True) + if 'Set-Cookie' in res_headers: + headers['Cookie'] = res_headers['Set-Cookie'] + + if not video: + if ('redirector.' in web_url) or ('googleusercontent' in web_url): + video = urllib2.urlopen(web_url).geturl() + elif 'googlevideo.' in web_url: + video = web_url + helpers.append_headers(headers) + else: + if ('redirector.' in video) or ('googleusercontent' in video): + video = urllib2.urlopen(video).geturl() + + if video: + if 'plugin://' in video: # google plus embedded videos may result in this + return video + else: + return video + helpers.append_headers(headers) + + raise ResolverError('File not found') + + def get_url(self, host, media_id): + return 'https://%s/%s' % (host, media_id) + + def _parse_google(self, link): + sources = [] + response = None + if re.match('https?://get[.]', link): + if link.endswith('/'): link = link[:-1] + vid_id = link.split('/')[-1] + response = self.net.http_GET(link) + sources = self.__parse_gget(vid_id, response.content) + elif re.match('https?://plus[.]', link): + response = self.net.http_GET(link) + sources = self.__parse_gplus(response.content) + elif 'drive.google' in link or 'docs.google' in link: + response = self.net.http_GET(link) + sources = self._parse_gdocs(response.content) + return response, sources + + def __parse_gplus(self, html): + sources = [] + match = re.search('[^&]+).*?&itag=(?P[^&]+)', item4): + link = match.group('link') + itag = match.group('itag') + quality = self.itag_map.get(itag, 'Unknown Quality [%s]' % itag) + sources.append((quality, link)) + if sources: + return sources + return sources + + def _parse_gdocs(self, html): + urls = [] + for match in re.finditer('\[\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\]', html): + key, value = match.groups() + if key == 'fmt_stream_map': + items = value.split(',') + for item in items: + _source_itag, source_url = item.split('|') + if isinstance(source_url, unicode): + source_url = source_url.encode('utf-8') + + source_url = source_url.decode('unicode_escape') + quality = self.itag_map.get(_source_itag, 'Unknown Quality [%s]' % _source_itag) + source_url = urllib2.unquote(source_url) + urls.append((quality, source_url)) + return urls + + return urls + + @staticmethod + def parse_json(html): + if html: + try: + if not isinstance(html, unicode): + if html.startswith('\xef\xbb\xbf'): + html = html[3:] + elif html.startswith('\xfe\xff'): + html = html[2:] + js_data = json.loads(html) + if js_data is None: + return {} + else: + return js_data + except ValueError: + return {} + else: + return {} diff --git a/lib/urlresolver/plugins/gorillavid.py b/lib/urlresolver/plugins/gorillavid.py new file mode 100644 index 00000000..667eb7cd --- /dev/null +++ b/lib/urlresolver/plugins/gorillavid.py @@ -0,0 +1,46 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2011 t0mm0 + + 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 . +""" + +from lib import helpers +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + + +class GorillavidResolver(UrlResolver): + name = "gorillavid" + domains = ["gorillavid.in", "gorillavid.com"] + pattern = '(?://|\.)(gorillavid\.(?:in|com))/(?:embed-)?([0-9a-zA-Z]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + headers = {'User-Agent': common.FF_USER_AGENT} + response = self.net.http_GET(web_url, headers=headers) + html = response.content + sources = helpers.scrape_sources(html) + if not sources: + data = helpers.get_hidden(html) + headers['Cookie'] = response.get_headers(as_dict=True).get('Set-Cookie', '') + html = self.net.http_POST(response.get_url(), headers=headers, form_data=data).content + sources = helpers.scrape_sources(html) + return helpers.pick_source(sources) + helpers.append_headers(headers) + + def get_url(self, host, media_id): + return 'http://gorillavid.in/%s' % (media_id) diff --git a/lib/urlresolver/plugins/grifthost.py b/lib/urlresolver/plugins/grifthost.py new file mode 100644 index 00000000..728144fe --- /dev/null +++ b/lib/urlresolver/plugins/grifthost.py @@ -0,0 +1,44 @@ +""" +grifthost urlresolver plugin +Copyright (C) 2015 tknorris + +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 . +""" + +from lib import helpers +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + + +class GrifthostResolver(UrlResolver): + name = "grifthost" + domains = ["grifthost.com"] + pattern = '(?://|\.)(grifthost\.com)/(?:embed-)?([0-9a-zA-Z/]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + headers = {'User-Agent': common.FF_USER_AGENT} + response = self.net.http_GET(web_url, headers=headers) + html = response.content + data = helpers.get_hidden(html) + headers['Cookie'] = response.get_headers(as_dict=True).get('Set-Cookie', '') + html = self.net.http_POST(web_url, headers=headers, form_data=data).content + sources = helpers.scrape_sources(html) + return helpers.pick_source(sources) + helpers.append_headers(headers) + + def get_url(self, host, media_id): + return 'http://grifthost.com/%s' % (media_id) diff --git a/lib/urlresolver/plugins/hostingcup.py b/lib/urlresolver/plugins/hostingcup.py deleted file mode 100644 index e51b737f..00000000 --- a/lib/urlresolver/plugins/hostingcup.py +++ /dev/null @@ -1,80 +0,0 @@ -''' -dailymotion urlresolver plugin -Copyright (C) 2011 cyrus007 - -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 . -''' - -from t0mm0.common.net import Net -from urlresolver.plugnplay.interfaces import UrlResolver -from urlresolver.plugnplay.interfaces import PluginSettings -from urlresolver.plugnplay import Plugin -import re -import urllib2 -from urlresolver import common -from vidxden import unpack_js - -class HostingcupResolver(Plugin, UrlResolver, PluginSettings): - implements = [UrlResolver, PluginSettings] - name = "hostingcup" - - def __init__(self): - p = self.get_setting('priority') or 100 - self.priority = int(p) - self.net = Net() - self.pattern = 'http://(www.)?hostingcup.com/[0-9A-Za-z]+' - - - def get_media_url(self, host, media_id): - web_url = self.get_url(host, media_id) - try: - html = self.net.http_GET(web_url).content - except urllib2.URLError, e: - common.addon.log_error(self.name + '- got http error %d fetching %s' % - (e.code, web_url)) - return False - - page = ''.join(html.splitlines()).replace('\t','') - r = re.search("return p\}\(\'(.+?)\',\d+,\d+,\'(.+?)\'", page) - if r: - p, k = r.groups() - else: - common.addon.log_error(self.name + '- packed javascript embed code not found') - return False - - decrypted_data = unpack_js(p, k) - r = re.search('file.\',.\'(.+?).\'', decrypted_data) - if not r: - r = re.search('src="(.+?)"', decrypted_data) - if r: - stream_url = r.group(1) - else: - common.addon.log_error(self.name + '- stream url not found') - return False - - return stream_url - - def get_url(self, host, media_id): - return 'http://vidpe.com/%s' % media_id - - - def get_host_and_id(self, url): - r = re.search('//(.+?)/([0-9A-Za-z]+)', url) - if r: - return r.groups() - else: - return False - - def valid_url(self, url, host): - return re.match(self.pattern, url) or 'hostingcup' in host \ No newline at end of file diff --git a/lib/urlresolver/plugins/hugefiles.py b/lib/urlresolver/plugins/hugefiles.py new file mode 100644 index 00000000..1449b4ad --- /dev/null +++ b/lib/urlresolver/plugins/hugefiles.py @@ -0,0 +1,65 @@ +''' +Hugefiles urlresolver plugin +Copyright (C) 2013 Vinnydude + +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 urllib +import urllib2 +from lib import captcha_lib +from lib import helpers +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class HugefilesResolver(UrlResolver): + name = "hugefiles" + domains = ["hugefiles.net"] + pattern = '(?://|\.)(hugefiles\.net)/([0-9a-zA-Z/]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + + common.log_utils.log_debug('HugeFiles: get_link: %s' % (web_url)) + html = self.net.http_GET(web_url).content + + r = re.findall('File Not Found', html) + if r: + raise ResolverError('File Not Found or removed') + + # Grab data values + data = helpers.get_hidden(html) + data.update(captcha_lib.do_captcha(html)) + common.log_utils.log_debug('HugeFiles - Requesting POST URL: %s with data: %s' % (web_url, data)) + html = self.net.http_POST(web_url, data).content + + # Re-grab data values + data = helpers.get_hidden(html) + data['referer'] = web_url + headers = {'User-Agent': common.IE_USER_AGENT} + common.log_utils.log_debug('HugeFiles - Requesting POST URL: %s with data: %s' % (web_url, data)) + request = urllib2.Request(web_url, data=urllib.urlencode(data), headers=headers) + + try: stream_url = urllib2.urlopen(request).geturl() + except: return + + common.log_utils.log_debug('Hugefiles stream Found: %s' % stream_url) + return stream_url + + def get_url(self, host, media_id): + return 'http://hugefiles.net/%s' % media_id diff --git a/lib/urlresolver/plugins/idowatch.py b/lib/urlresolver/plugins/idowatch.py new file mode 100644 index 00000000..35b2102d --- /dev/null +++ b/lib/urlresolver/plugins/idowatch.py @@ -0,0 +1,26 @@ +""" +grifthost urlresolver plugin +Copyright (C) 2015 tknorris + +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 . +""" +from __generic_resolver__ import GenericResolver + +class IDoWatchResolver(GenericResolver): + name = 'idowatch' + domains = ['idowatch.net'] + pattern = '(?://|\.)(idowatch\.net)/(?:embed-)?([0-9a-zA-Z]+)' + + def get_url(self, host, media_id): + return 'http://idowatch.net/%s.html' % (media_id) diff --git a/lib/urlresolver/plugins/indavideo.py b/lib/urlresolver/plugins/indavideo.py new file mode 100644 index 00000000..f1e0da05 --- /dev/null +++ b/lib/urlresolver/plugins/indavideo.py @@ -0,0 +1,63 @@ +# -*- coding: UTF-8 -*- +""" + Kodi urlresolver plugin + Copyright (C) 2016 alifrezser + + 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, json +from lib import helpers +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError + +class IndavideoResolver(UrlResolver): + name = "indavideo" + domains = ["indavideo.hu"] + pattern = '(?://|\.)(indavideo\.hu)/(?:player/video/|video/)([^/]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + headers = {'User-Agent': common.FF_USER_AGENT} + html = self.net.http_GET(web_url, headers=headers).content + data = json.loads(html) + + if data['success'] == '0': + html = self.net.http_GET('http://indavideo.hu/video/%s' % media_id).content + + hash = re.search('emb_hash.+?value\s*=\s*"([^"]+)', html) + if not hash: + raise ResolverError('File not found') + + web_url = self.get_url(host, hash.group(1)) + + html = self.net.http_GET(web_url).content + data = json.loads(html) + + if data['success'] == '1': + video_file = data['data']['video_file'] + if video_file == '': + raise ResolverError('File removed') + + video_file = video_file.rsplit('/', 1)[0] + '/' + sources = list(set(data['data']['flv_files'])) + sources = [(i.rsplit('.', 2)[-2] + 'p', i.split('?')[0] + '?channel=main') for i in sources] + sources = sorted(sources, key=lambda x: x[0])[::-1] + return video_file + helpers.pick_source(sources) + + raise ResolverError('File not found') + + def get_url(self, host, media_id): + return 'http://amfphp.indavideo.hu/SYm0json.php/player.playerHandler.getVideoData/%s' % (media_id) \ No newline at end of file diff --git a/lib/urlresolver/plugins/jetload.py b/lib/urlresolver/plugins/jetload.py new file mode 100644 index 00000000..5d6b2bcb --- /dev/null +++ b/lib/urlresolver/plugins/jetload.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +""" + + Copyright (C) 2016 + + 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 . +""" +from __generic_resolver__ import GenericResolver + +class JetloadResolver(GenericResolver): + name = 'jetload' + domains = ['jetload.tv'] + pattern = '(?://|\.)(jetload\.tv)/(?:.+?embed\.php\?u=)?([0-9a-zA-Z]+)' + + def get_url(self, host, media_id): + return self._default_get_url(host, media_id, 'http://{host}/plugins/mediaplayer/site/_embed.php?u={media_id}') diff --git a/lib/urlresolver/plugins/kingfiles.py b/lib/urlresolver/plugins/kingfiles.py new file mode 100644 index 00000000..31e7e460 --- /dev/null +++ b/lib/urlresolver/plugins/kingfiles.py @@ -0,0 +1,60 @@ +""" +grifthost urlresolver plugin +Copyright (C) 2015 tknorris + +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 . +""" + +from urlresolver import common +from urlresolver.resolver import UrlResolver, ResolverError +from lib import captcha_lib +from lib import helpers +import re + +MAX_TRIES = 3 + +class KingFilesResolver(UrlResolver): + name = "KingFiles" + domains = ["kingfiles.net"] + pattern = '(?://|\.)(kingfiles\.net)/([0-9a-zA-Z/]+)' + + def __init__(self): + self.net = common.Net() + + def get_media_url(self, host, media_id): + web_url = self.get_url(host, media_id) + html = self.net.http_GET(web_url).content + + tries = 0 + while tries < MAX_TRIES: + data = helpers.get_hidden(html) + data.update(captcha_lib.do_captcha(html)) + + html = self.net.http_POST(web_url, form_data=data).content + html = helpers.add_packed_data(html) + match = re.search('name="src"\s*value="([^"]+)', html) + if match: + return match.group(1) + + # try to find source in html + match = re.search(']*>\s*. +''' +from __generic_resolver__ import GenericResolver + +class LetwatchResolver(GenericResolver): + name = "letwatch" + domains = ['letwatch.us', 'letwatch.to', 'vidshare.us'] + pattern = '(?://|\.)(letwatch\.(?:us|to)|vidshare\.us)/(?:embed-)?([0-9a-zA-Z]+)' diff --git a/lib/urlresolver/plugins/lib/README-megavideo b/lib/urlresolver/plugins/lib/README-megavideo deleted file mode 100644 index 3f6cd31b..00000000 --- a/lib/urlresolver/plugins/lib/README-megavideo +++ /dev/null @@ -1,30 +0,0 @@ -http://code.google.com/p/python-megavideo-parser/ - -Python Megavideo Parser 1.0.0 - -Methods Documentation - -void Megavideo( string reference) - __init_ or constructor method, initialize video object by passing video reference as direct link (ex. http://www.megavideo.com/?v=XXXXXXXX) or only video code (ex. XXXXXXXX). - -bool is_Valid( void) - check if video is valid, return False and print error type or True on success. - -string getTitle( void) - return video title. - -string getRunTime( void) - return video length in time format MM:SS . - -string getServer( void) - return megavideo server number in with is stored video file, not useful as public method. - -dict getAllInfo( void) - return a dictionary that contain all video info (Server, Title, Runtime). - -string getLink( void) - return link to video path, useful to download video. - -string getFLV( void) - return direct link to flash video with .flv extension, useful to direct stream video. - diff --git a/lib/urlresolver/plugins/lib/_megaupload.py b/lib/urlresolver/plugins/lib/_megaupload.py deleted file mode 100644 index 62172289..00000000 --- a/lib/urlresolver/plugins/lib/_megaupload.py +++ /dev/null @@ -1,310 +0,0 @@ -''' - Megaupload and Megaporn Resolver v0.3 - Copyleft (Licensed under GPLv3) Anarchintosh (all code) - - Also gets megavideo links from megaupload pages.(can't do this for megaporn) - - If account is None or Free, you have to wait 46 or 26 Seconds respectively before accessing the stream. - - Megaup and megaporn use different logins, so store the cookies in different places. - - Commands/Functions: - - __doLogin(baseurl, cookiepath, username, password) - - __resolveURL(url,cookiepath,aviget=True,force_megavid=True) - - __dls_limited(baseurl,cookiepath) - - is_online(cookiepath='YOUR_COOKIE_PATH',url='THE_URL') - -''' - -import os,re -import urllib,urllib2,cookielib - -#global strings for valid baseurl -regular = 'http://www.megaupload.com/' -porn = 'http://www.megaporn.com/' - -def setBaseURL(baseurl): - # API feature to neaten up how functions are used - if baseurl == 'regular': - return regular - elif baseurl == 'porn': - return porn - -def openfile(filename): - fh = open(filename, 'r') - contents=fh.read() - fh.close() - return contents - -def checkurl(url): - #get necessary url details - ismegaup = re.search('.megaupload.com/', url) - ismegavid = re.search('.megavideo.com/', url) - isporn = re.search('.megaporn.com/', url) - #second layer of porn url detection - ispornvid = re.search('.megaporn.com/video/', url) - # RETURN RESULTS: - if ismegaup is not None: - return 'megaup' - elif ismegavid is not None: - return 'megavid' - elif isporn is not None: - if ispornvid is not None: - return 'pornvid' - elif ispornvid is None: - return 'pornup' - -def is_online(cookiepath=None,url=False,source=False): - if source == False: - source = GetURL(url,cookiepath) - checker = re.search('Unfortunately, the link you have clicked is not available.',source) - if checker is not None: - return False - elif checker is None: - return True - -def get_dir(mypath, dirname): - #...creates sub-directories if they are not found. - subpath = os.path.join(mypath, dirname) - if not os.path.exists(subpath): - os.makedirs(subpath) - return subpath - - -def megavid_force(url): - #load a megaup page without cookies, to ensure that the user can get the megavid link. - source=load_pagesrc(url,enable_cookies=False) - megavidlink=get_megavid(source) - return megavidlink - -def resolveURL(url,cookiepath,aviget=True,force_megavid=True): - - #bring together all the functions into a simple addon-friendly function. - - source=load_pagesrc(url,cookiepath,enable_cookies=True) - - #if source is a url (from a Direct Downloads re-direct) not pagesource - if source.startswith('http://'): - filelink=source - ''' - Can't get megavid link if using direct download - However, as a workaround, can load megaup page without cookies, then scrape. - ''' - if force_megavid is True: - megavidlink=megavid_force(url) - else: - megavidlink=None - - #speed patch (we know its premium, since we're getting a direct download) - logincheck='premium' - - else: # if source is html page code... - - #scrape the direct filelink from page - filelink=get_filelink(source,aviget) - - #scrape the megavideo link if there is one on the page - megavidlink=get_megavid(source) - - #verify what the user is logged in as. - logincheck=check_login(source) - - filename=_get_filename(filelink) - - return filelink,filename,megavidlink,logincheck - - -def load_pagesrc(url,cookiepath,enable_cookies=True): - - #loads page source code. redirect url is returned if Direct Downloads is enabled. - - urltype=checkurl(url) - if urltype is 'megaup' or 'megaporn': - - source=GetURL(url,cookiepath,enable_cookies) - - if is_online(source=source) == True: - return source - else: - return False - else: - return False - - -def check_login(source): - #feed me some megaupload page source - #returns 'free' or 'premium' if logged in - #returns 'none' if not logged in - - login = re.search('Welcome', source) - premium = re.search('flashvars.status = "premium";', source) - platinum = re.search('flashvars.status = "platinum";', source) - - if login is not None: - if premium is not None: - return 'premium' - elif premium is None: - if platinum is not None: - return 'premium' - elif platinum is None: - return 'free' - elif login is None: - return None - -def __dls_limited(baseurl,cookiepath): - #returns True if download limit has been reached. - - baseurl=setBaseURL(baseurl) - - truestring='Download limit exceeded' - falsestring='Hooray Download Success' - - #url to a special small text file that contains the words: Hooray Download Success - if baseurl == regular: - testurl = 'http://www.megaupload.com/?d=PQCIEIP7' - elif baseurl == porn: - testurl = '' - - source=load_pagesrc(testurl) - fileurl=get_filelink(source) - - link=GetURL(cookiepath,fileurl) - - exceeded = re.search(truestring, link) - #notexceeded = re.search(falsestring, link) - - if exceeded is not None: - return True - else: - #if notexceeded is not None: - return False - -def delete_login(cookiepath): - #clears cookies - try: - os.remove(cookiepath) - except: - pass - -def get_megavid (source): - #verify source is megaupload - checker='Download link: ').findall(source))[0] - - if login == 'free' or login == None: - url = (re.compile('id="downloadlink"> len(digits): + base = len(digits) + + num = abs(number) + res = [] + while num: + res.append(digits[num % base]) + num //= base + if padding: + res.append('0' * padding) + if number < 0: + res.append('-') + return ''.join(reversed(res or '0')) + + def decode_char(self, enc_char, radix): + end_char = "+ " + str_char = "" + while enc_char != '': + found = False + # for i in range(len(self.b)): + # print self.b[i], enc_char.find(self.b[i]) + # if enc_char.find(self.b[i]) == 0: + # str_char += self.base_repr(i, radix) + # enc_char = enc_char[len(self.b[i]):] + # found = True + # break + + # print 'found', found, enc_char + if not found: + for i in range(len(self.b)): + enc_char = enc_char.replace(self.b[i], str(i)) + # enc_char = enc_char.replace('(゚Θ゚)', '1').replace('(゚ー゚)', '4').replace('(c^_^o)', '0').replace('(o^_^o)', '3') + # print 'enc_char', enc_char + startpos = 0 + findClose = True + balance = 1 + result = [] + if enc_char.startswith('('): + l = 0 + + for t in enc_char[1:]: + l += 1 + # print 'looping', findClose, startpos, t, balance + if findClose and t == ')': + balance -= 1 + if balance == 0: + result += [enc_char[startpos:l + 1]] + findClose = False + continue + elif not findClose and t == '(': + startpos = l + findClose = True + balance = 1 + continue + elif t == '(': + balance += 1 + + if result is None or len(result) == 0: + return "" + else: + for r in result: + value = self.decode_digit(r, radix) + # print 'va', value + str_char += value + if value == "": + return "" + + return str_char + + enc_char = enc_char[len(end_char):] + + return str_char + + def parseJSString(self, s): + try: + # print s + # offset = 1 if s[0] == '+' else 0 + tmp = (s.replace('!+[]', '1').replace('!![]', '1').replace('[]', '0')) # .replace('(','str(')[offset:]) + val = int(eval(tmp)) + return val + except: + pass + + def decode_digit(self, enc_int, radix): + # enc_int = enc_int.replace('(゚Θ゚)', '1').replace('(゚ー゚)', '4').replace('(c^_^o)', '0').replace('(o^_^o)', '3') + # print 'enc_int before', enc_int + # for i in range(len(self.b)): + # print self.b[i], enc_char.find(self.b[i]) + # if enc_char.find(self.b[i]) > 0: + # str_char += self.base_repr(i, radix) + # enc_char = enc_char[len(self.b[i]):] + # found = True + # break + # enc_int=enc_int.replace(self.b[i], str(i)) + # print 'enc_int before', enc_int + + try: + return str(eval(enc_int)) + except: pass + rr = '(\(.+?\)\))\+' + rerr = enc_int.split('))+') # re.findall(rr, enc_int) + v = "" + # print rerr + for c in rerr: + if len(c) > 0: + # print 'v', c + if c.strip().endswith('+'): + c = c.strip()[:-1] + # print 'v', c + startbrackets = len(c) - len(c.replace('(', '')) + endbrackets = len(c) - len(c.replace(')', '')) + if startbrackets > endbrackets: + c += ')' * (startbrackets - endbrackets) + if '[' in c: + v += str(self.parseJSString(c)) + else: + # print c + v += str(eval(c)) + return v + + # unreachable code + # mode 0=+, 1=- + # mode = 0 + # value = 0 + + # while enc_int != '': + # found = False + # for i in range(len(self.b)): + # if enc_int.find(self.b[i]) == 0: + # if mode == 0: + # value += i + # else: + # value -= i + # enc_int = enc_int[len(self.b[i]):] + # found = True + # break + + # if not found: + # return "" + + # enc_int = re.sub('^\s+|\s+$', '', enc_int) + # if enc_int.find("+") == 0: + # mode = 0 + # else: + # mode = 1 + + # enc_int = enc_int[1:] + # enc_int = re.sub('^\s+|\s+$', '', enc_int) + + # return self.base_repr(value, radix) + + def decode(self): + self.encoded_str = re.sub('^\s+|\s+$', '', self.encoded_str) + + # get data + pattern = (r"\(゚Д゚\)\[゚o゚\]\+ (.+?)\(゚Д゚\)\[゚o゚\]\)") + result = re.search(pattern, self.encoded_str, re.DOTALL) + if result is None: + common.log_utils.log_debug("AADecoder: data not found") + return False + + data = result.group(1) + + # hex decode string + begin_char = "(゚Д゚)[゚ε゚]+" + alt_char = "(o゚ー゚o)+ " + + out = '' + # print data + while data != '': + # Check new char + if data.find(begin_char) != 0: + common.log_utils.log_debug("AADecoder: data not found") + return False + + data = data[len(begin_char):] + + # Find encoded char + enc_char = "" + if data.find(begin_char) == -1: + enc_char = data + data = "" + else: + enc_char = data[:data.find(begin_char)] + data = data[len(enc_char):] + + radix = 8 + # Detect radix 16 for utf8 char + if enc_char.find(alt_char) == 0: + enc_char = enc_char[len(alt_char):] + radix = 16 + + # print repr(enc_char), radix + # print enc_char.replace('(゚Θ゚)', '1').replace('(゚ー゚)', '4').replace('(c^_^o)', '0').replace('(o^_^o)', '3') + + # print 'The CHAR', enc_char, radix + str_char = self.decode_char(enc_char, radix) + + if str_char == "": + common.log_utils.log_debug("no match : ") + common.log_utils.log_debug(data + "\nout = " + out + "\n") + return False + # print 'sofar', str_char, radix,out + + out += chr(int(str_char, radix)) + # print 'sfar', chr(int(str_char, radix)), out + + if out == "": + common.log_utils.log_debug("no match : " + data) + return False + + return out diff --git a/lib/urlresolver/plugins/lib/captcha_lib.py b/lib/urlresolver/plugins/lib/captcha_lib.py new file mode 100644 index 00000000..da7f7411 --- /dev/null +++ b/lib/urlresolver/plugins/lib/captcha_lib.py @@ -0,0 +1,117 @@ +""" + urlresolver XBMC Addon + Copyright (C) 2014 tknorris + + 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 . + + reusable captcha methods +""" +from urlresolver import common +import re +import xbmcgui +import os +import recaptcha_v2 +import helpers + +net = common.Net() +IMG_FILE = 'captcha_img.gif' + +def get_response(img): + try: + img = xbmcgui.ControlImage(450, 0, 400, 130, img) + wdlg = xbmcgui.WindowDialog() + wdlg.addControl(img) + wdlg.show() + common.kodi.sleep(3000) + solution = common.kodi.get_keyboard(common.i18n('letters_image')) + if not solution: + raise Exception('captcha_error') + finally: + wdlg.close() + +def do_captcha(html): + solvemedia = re.search(']+src="((?:https?:)?//api.solvemedia.com[^"]+)', html) + recaptcha = re.search('&#(.+?);<").findall(html) + result = sorted(captcha, key=lambda ltr: int(ltr[0])) + solution = ''.join(str(int(num[1]) - 48) for num in result) + if solution: + return {'code': solution} + else: + return {} + +def do_solvemedia_captcha(captcha_url): + common.log_utils.log_debug('SolveMedia Captcha: %s' % (captcha_url)) + if captcha_url.startswith('//'): captcha_url = 'http:' + captcha_url + html = net.http_GET(captcha_url).content + data = { + 'adcopy_challenge': '' # set to blank just in case not found; avoids exception on return + } + data.update(helpers.get_hidden(html), include_submit=False) + captcha_img = os.path.join(common.profile_path, IMG_FILE) + try: os.remove(captcha_img) + except: pass + + # Check for alternate puzzle type - stored in a div + alt_frame = re.search('