"""
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
:func:`urlresolver.choose_source`.
.. seealso::
:class:`HostedMediaFile`
'''
import re
import urlparse
import sys
import os
import xbmc
import xbmcvfs
import xbmcgui
import common
from hmf import HostedMediaFile
from urlresolver.resolver import UrlResolver
from urlresolver.plugins.__generic_resolver__ import GenericResolver
from plugins import *
common.logger.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
def load_external_plugins():
for d in PLUGIN_DIRS:
common.logger.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.logger.log_debug('Loaded %s as %s from %s' % (imp, mod_name, filename))
def relevant_resolvers(domain=None, include_universal=None, include_popups=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"
if include_popups is None:
include_popups = common.get_setting('allow_popups') == "true"
if include_popups is False:
common.logger.log_debug('Resolvers that require popups have been disabled')
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()) and (include_popups or not resolver.isPopup()):
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.logger.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.
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`
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``.
"""
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
removed from the list.
Args:
source_list (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
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
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)
Args:
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.
"""
sources = filter_source_list(sources)
if not sources:
common.logger.log_warning('no playable streams found')
return False
elif len(sources) == 1:
return sources[0]
else:
dialog = xbmcgui.Dialog()
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
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)
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
exposing settings.
.. note::
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.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:
os.makedirs(os.path.dirname(common.settings_file))
except OSError:
pass
new_xml = [
'',
'',
'\t',
'\t\t' % (common.i18n('enable_universal')),
'\t\t' % (common.i18n('enable_popups')),
'\t\t' % (common.i18n('auto_pick')),
'\t\t' % (common.i18n('use_function_cache')),
'\t\t' % (common.i18n('reset_function_cache')),
'\t\t',
'\t\t',
'\t\t',
'\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('')
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.logger.log_debug('Updating Settings XML')
try:
with open(common.settings_file, 'w') as f:
f.write(new_xml)
except:
raise
else:
common.logger.log_debug('No Settings Update Needed')
_update_settings_xml()