Files
stream/lib/resolver.py

290 lines
10 KiB
Python
Raw Normal View History

2025-04-25 16:31:48 +02:00
# -*- coding: UTF-8 -*-
# * Copyright (C) 2011 Libor Zoubek
# *
# *
# * 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 2, 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; see the file COPYING. If not, write to
# * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
# * http://www.gnu.org/copyleft/gpl.html
# *
# */
import sys
import os
import re
import traceback
import util
sys.path.append(os.path.join(os.path.dirname(__file__), 'server'))
RESOLVERS = []
util.debug('%s searching for modules' % __name__)
for module in os.listdir(os.path.join(os.path.dirname(__file__), 'server')):
if module == '__init__.py' or module[-3:] != '.py':
continue
module = module[:-3]
exec('import %s' % module)
resolver = eval(module)
util.debug('found %s %s' % (resolver, dir(resolver)))
if not hasattr(resolver, '__priority__'):
resolver.__priority__ = 0
RESOLVERS.append(resolver)
del module
RESOLVERS = sorted(RESOLVERS, key=lambda m: -m.__priority__)
util.debug('done')
def item():
return {'name': '', 'url': '', 'quality': '???', 'surl': '', 'subs': '', 'headers': {}}
def resolve(url):
"""
resolves given url by asking all resolvers
returns None if no resolver advised to be able to resolve this url
returns False if resolver did his job, but did not return any value (thus failed)
returns Array of resolved objects in positive usecase
"""
url = util.decode_html(url)
util.info('Resolving ' + url)
resolver = _get_resolver(url)
value = None
if resolver is None:
return None
util.info('Using resolver \'%s\'' % str(resolver.__name__));
try:
value = resolver.resolve(url)
except:
traceback.print_exc()
if value is None:
return False
default = item()
def fix_stream(i, url, resolver, default):
""" fix missing but required values """
if 'name' not in list(i.keys()):
i['name'] = resolver.__name__
if 'surl' not in list(i.keys()):
i['surl'] = url
for key in list(default.keys()):
if key not in list(i.keys()):
i[key] = default[key]
[fix_stream(i, url, resolver, default) for i in value]
return sorted(value, key=lambda i: i['quality'])
def _get_resolver(url):
util.debug('Get resolver for ' + url)
for r in RESOLVERS:
util.debug('querying %s' % r)
if r.supports(url):
return r
def can_resolve(url):
""" Returns true if we are able to resolve stream by given URL """
return _get_resolver(url) is not None
def filter_resolvable(url):
if url.find('facebook') > 0 or url.find('yield') > 0:
return
return url.strip('\'\"')
def findstreams(data, regexes=None):
"""
Finds streams in given data. Respects caller add-on settings about
quality and asks user if necessary.
:param data: A string (piece of text / HTML code), an array of URLs or an
array of dictionaries, where 'url' key stores actual URL and
all other keys not present in item() are being copied to the
resolved stream dictionary
:param regexes: An array of strings - regular expressions, each MUST define
named group called 'url', which retrieves resolvable URL
(that one is passed to resolve operation); only used
with 'data' of type 'string'
:returns: An array of resolved objects, None if at least 1 resolver failed
to resolve and nothing else was found, an empty array if no
resolvers for URLs has been found or False if none of regexes
found anything
"""
def get_url(obj):
return obj['url'] if isinstance(obj, dict) else obj
urls = []
resolvables = []
resolved = []
not_found = False
if isinstance(data, str) and regexes:
for regex in regexes:
for match in re.finditer(regex, data, re.IGNORECASE | re.DOTALL):
urls.append(match.group('url'))
elif isinstance(data, list):
urls = data
else:
raise TypeError
for url in urls:
if isinstance(url, dict):
url['url'] = filter_resolvable(url['url'])
else:
url = filter_resolvable(url)
if url and url not in resolvables:
util.info('Found resolvable ' + get_url(url))
resolvables.append(url)
if len(resolvables) == 0:
util.info('No resolvables found!')
return False
for url in resolvables:
streams = resolve(get_url(url))
if streams is None:
util.info('No resolver found for ' + get_url(url))
not_found = True
elif not streams:
util.info('There was an error resolving ' + get_url(url))
elif len(streams) > 0:
for stream in streams:
if isinstance(url, dict):
for key in list(url.keys()):
if key not in stream:
stream[key] = url[key]
elif key not in item():
if isinstance(stream[key], str) and \
isinstance(url[key], str):
stream[key] = url[key] + ' +' + stream[key]
elif isinstance(stream[key], list) and \
isinstance(url[key], list):
stream[key] = url[key] + stream[key]
elif isinstance(stream[key], dict) and \
isinstance(url[key], dict):
stream[key].update(url[key])
resolved.append(stream)
if len(resolved) == 0:
if not_found:
return []
return None
resolved = sorted(resolved, key=lambda i: i['quality'])
resolved = sorted(resolved, key=lambda i: len(i['quality']))
resolved.reverse()
return resolved
q_map = {'3': '720p', '4': '480p', '5': '360p'}
def filter_by_quality(resolved, q):
util.info('filtering by quality setting ' + q)
if q == '0':
return resolved
sources = {}
ret = []
# first group streams by source url
for item in resolved:
if item['surl'] in list(sources.keys()):
sources[item['surl']].append(item)
else:
sources[item['surl']] = [item]
if q == '1':
# always return best quality from each source
for key in list(sources.keys()):
ret.append(sources[key][0])
elif q == '2':
# always return worse quality from each source
for key in list(sources.keys()):
ret.append(sources[key][-1])
else:
# we try to select sources of desired qualities
quality = q_map[q]
# 3,4,5 are 720,480,360
for key in list(sources.keys()):
added = False
for item in sources[key]:
if quality == item['quality']:
ret.append(item)
added = True
if not added:
util.debug('Desired quality %s not found, adding best found' % quality)
ret.append(sources[key][-1])
# sort results again, so best quality streams appear first
ret = sorted(ret, key=lambda i: i['quality'])
if not q == '2':
ret.reverse()
return ret
def filter_by_language(resolved, lang):
util.info('filtering by language setting ' + lang)
if lang == '0':
return resolved
ret = []
# first group streams by source url
for item in resolved:
if 'lang' in item and item['lang'] != '':
util.info(item)
if lang == '1' and re.match('en', item['lang'], re.IGNORECASE):
ret.append(item)
if lang == '2' and re.match('cs|cz|čeština', item['lang'], re.IGNORECASE):
ret.append(item)
return ret
def findstreams_multi(data, regexes):
"""
Finds streams in given data according to given regexes
respects caller addon's setting about desired quality, asks user if needed
assumes, that all resolvables need to be returned, but in particular quality
@param data piece of text (HTML code) to search in
@param regexes - array of strings - regular expressions, each MUST define named group called 'url'
which retrieves resolvable URL (that one is passsed to resolve operation)
@return array of dictionaries with keys: name,url,quality,surl
@return None if at least 1 resoler failed to resolve and nothing else has been found
@return [] if no resolvable URLs or no resolvers for URL has been found
"""
resolved = []
# keep list of found urls to aviod having duplicates
urls = []
error = False
for regex in regexes:
for match in re.finditer(regex, data, re.IGNORECASE | re.DOTALL):
print('Found resolvable %s ' % match.group('url'))
streams = resolve(match.group('url'))
if isinstance(streams, list) and streams:
util.debug('There was an error resolving ' + match.group('url'))
error = True
if streams is not None:
if len(streams) > 0:
for stream in streams:
resolved.append(stream)
if error and len(resolved) == 0:
return None
if len(resolved) == 0:
return []
resolved = sorted(resolved, key=lambda i: i['quality'])
resolved = sorted(resolved, key=lambda i: len(i['quality']))
resolved2 = resolved
resolved2.reverse()
qualities = {}
for item in resolved2:
if item['quality'] in list(qualities.keys()):
qualities[item['quality']].append(item)
else:
qualities[item['quality']] = [item]
# now .. we must sort items to be in same order as they were found on page
for q in list(qualities.keys()):
qualities[q] = sorted(qualities[q], key=lambda i: resolved.index(i))
return qualities