This commit is contained in:
Pavel Suha
2025-04-25 16:31:48 +02:00
commit 7b54ba45a1
132 changed files with 6984 additions and 0 deletions

View File

@@ -0,0 +1,230 @@
# * Copyright (C) 2012 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
# *
# */
from collections import defaultdict
import util
import resolver
try:
import StorageServer
except ImportError:
print('Using dummy storage server')
import storageserverdummy as StorageServer
class ResolveException(Exception):
pass
class ContentProvider(object):
"""
ContentProvider class provides an internet content. It should NOT have any xbmc-related imports
and must be testable without XBMC runtime. This is a basic/dummy implementation.
"""
def __init__(self, name='dummy', base_url='/', username=None, password=None, filter=None, tmp_dir='.'):
"""
ContentProvider constructor
Args:
name (str): name of provider
base_url (str): base url of site being accessed
username (str): login username
password (str): login password
filter (func{item}): function to filter results returned by search or list methods
tmp_dir (str): temporary dir where provider can store/cache files
"""
self.name = name
self.username = username
self.password = password
if not base_url[-1] == '/':
base_url += '/'
self.base_url = base_url
self.filter = filter
self.tmp_dir = tmp_dir
self.cache = StorageServer.StorageServer(self.name, 24)
self.lang = 'cs' # initialize, current language could be set by XBMContentProvider
def __str__(self):
return 'ContentProvider' + self.name
def on_init(self):
"""
This function gets called by XbmcContentProvider after it initializes itself
and sets eveything up (especially 'lang' property of ContentProvider which gets detected
from kodi)
"""
pass
def capabilities(self):
"""
This way class defines which capabilities it provides ['login','search','resolve','categories']
It may also contain '!download' when provider does not support downloading
"""
return []
def video_item(self, url='', img='', quality='???'):
"""
Returns empty video item - contains all required fields
"""
return {'type': 'video', 'title': '', 'rating': 0, 'year': 0, 'size': '0MB', 'url': url, 'img': img,
'length': '', 'quality': quality, 'subs': '', 'surl': '', 'lang': ''}
def dir_item(self, title='', url='', type='dir'):
"""
Returns empty directory item
"""
return {'type': type, 'title': title, 'size': '0', 'url': url}
def login(self):
"""
A login method returns True on successfull login, False otherwise
"""
return False
def search(self, keyword):
"""
Search for a keyword on a site
Args:
keyword (str)
returns:
array of video or directory items
"""
return []
def list(self, url):
"""
Lists content on given url
Args:
url (str): either relative or absolute provider URL
Returns:
array of video or directory items
"""
return []
def categories(self):
"""
Lists categories on provided site
Returns:
array of video or directory items
"""
return []
def findstreams(self, data, regexes=None):
"""
Finds streams in given data (see resovler.findstreams for more details)
:param data: A string (piece of HTML, for example) or an array of URLs
:param regexes: An array of regexes to be used for extracting URLs from
'data' of type 'string'
:returns: An array of video items
"""
resolved = resolver.findstreams(data, regexes)
if resolved is None:
raise ResolveException(
'Nelze ziskat video link [CR]zkontrolujte jestli video nebylo odstraneno')
elif isinstance(resolved, list) and not resolved:
raise ResolveException('Video je na serveru, ktery neni podporovan')
elif not resolved:
raise ResolveException(
'Nebyl nalezen zadny video embed [CR]zkontrolujte stranku pripadne nahlaste chybu pluginu')
result = []
for j in resolved:
i = defaultdict(lambda: '', j)
item = self.video_item()
item['title'] = i['name']
item['url'] = i['url']
item['quality'] = i['quality']
item['surl'] = i['surl']
item['subs'] = i['subs']
item['headers'] = i['headers']
item['lang'] = i['lang']
item['info'] = i['info']
result.append(item)
return result
def resolve(self, item, captcha_cb=None, select_cb=None, wait_cb=None):
"""
Resolves given video item to a downloable/playable file/stream URL
Args:
url (str): relative or absolute URL to be resolved
captcha_cb(func{obj}): callback function when user input is required (captcha, one-time passwords etc).
function implementation must be Provider-specific
select_cb(func{array}): callback function for cases when given url resolves to multiple streams,
provider class may call this function and require user interaction
wait_cb(func{obj}): callback function for cases when url resolves to stream which becomes available
somewhere in future (typically in several seconds). Provider may call this and require waiting.
Returns:
None - if ``url`` was not resolved. Video item with 'url' key pointing to resolved target
"""
return None
def _url(self, url):
"""
Transforms relative to absolute url based on ``base_url`` class property
"""
if url.startswith('http'):
return url
return self.base_url + url.lstrip('./')
def _filter(self, result, item):
"""
Applies filter, if filter passes `item` is appended to `result`
Args:
result (array) : target array
item (obj) : item that is being applied filter on
"""
if self.filter:
if self.filter(item):
result.append(item)
else:
result.append(item)
def info(self, msg):
util.info('[%s] %s' % (self.name, msg))
def error(self, msg):
util.error('[%s] %s' % (self.name, msg))
class cached(object):
"""
A method decorator that can be used on any ContentProvider method
Having this decorator means that results of such method are going
to be cached for 24hours by default. You can pass number argument
to decorator, for example @cached(1) would cache for 1 hour.
"""
def __init__(self, ttl=24):
self.ttl = ttl
def __call__(self, f):
def wrap(*args):
provider = args[0]
cache = StorageServer.StorageServer(provider.name + str(self.ttl), self.ttl)
return cache.cacheFunction(f, *args)
return wrap

View File

@@ -0,0 +1,464 @@
# * Copyright (C) 2012 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
import xbmcutil
import resolver
import time
import xbmcplugin
import xbmc
import xbmcvfs
import xbmcgui
import urllib.parse
import urllib.request, urllib.parse, urllib.error
from collections import defaultdict
from provider import ResolveException
class XBMContentProvider(object):
'''
ContentProvider class provides an internet content. It should NOT have any xbmc-related imports
and must be testable without XBMC runtime. This is a basic/dummy implementation.
'''
def __init__(self, provider, settings, addon):
'''
XBMContentProvider constructor
Args:
name (str): name of provider
'''
self.provider = provider
# inject current user language
try: # not fully supported on Frodo
provider.lang = xbmc.getLanguage(xbmc.ISO_639_1)
except:
provider.lang = None
pass
self.settings = settings
# lang setting is optional for plugins
if not 'lang' in self.settings:
self.settings['lang'] = '0'
util.info('Initializing provider %s with settings %s' % (provider.name, settings))
self.addon = addon
self.addon_id = addon.getAddonInfo('id')
if '!download' not in self.provider.capabilities():
self.check_setting_keys(['downloads'])
self.cache = provider.cache
provider.on_init()
def check_setting_keys(self, keys):
for key in keys:
if not key in list(self.settings.keys()):
raise Exception('Invalid settings passed - [' + key + '] setting is required')
def params(self):
return {'cp': self.provider.name}
def run(self, params):
if params == {} or params == self.params():
return self.root()
if 'list' in list(params.keys()):
self.list(self.provider.list(params['list']))
return xbmcplugin.endOfDirectory(int(sys.argv[1]))
if 'down' in list(params.keys()):
return self.download({'url': params['down'], 'title': params['title']})
if 'play' in list(params.keys()):
return self.play({'url': params['play'], 'info': params})
if 'search-list' in list(params.keys()):
return self.search_list()
if 'search' in list(params.keys()):
return self.do_search(params['search'])
if 'search-remove' in list(params.keys()):
return self.search_remove(params['search-remove'])
if 'search-edit' in list(params.keys()):
return self.search_edit(params['search-edit'])
if self.run_custom:
return self.run_custom(params)
def search_list(self):
params = self.params()
params.update({'search': '#'})
menu1 = self.params()
menu2 = self.params()
xbmcutil.add_dir(xbmcutil.__lang__(30004), params, xbmcutil.icon('search.png'))
for what in xbmcutil.search_list(self.cache):
params['search'] = what
menu1['search-remove'] = what
menu2['search-edit'] = what
xbmcutil.add_dir(what, params, menuItems={xbmcutil.__lang__(
30016): menu2, xbmc.getLocalizedString(117): menu1})
xbmcplugin.endOfDirectory(int(sys.argv[1]))
def search_remove(self, what):
xbmcutil.search_remove(self.cache, what)
xbmc.executebuiltin('Container.Refresh')
def search_edit(self, what):
kb = xbmc.Keyboard(what, xbmcutil.__lang__(30003), False)
kb.doModal()
if kb.isConfirmed():
replacement = kb.getText()
xbmcutil.search_replace(self.cache, what, replacement)
params = self.params()
params.update({'search': replacement})
action = xbmcutil._create_plugin_url(params)
xbmc.executebuiltin('Container.Update(%s)' % action)
def do_search(self, what):
if what == '' or what == '#':
kb = xbmc.Keyboard('', xbmcutil.__lang__(30003), False)
kb.doModal()
if kb.isConfirmed():
what = kb.getText()
if not what == '':
maximum = 20
try:
maximum = int(self.settings['keep-searches'])
except:
util.error('Unable to parse convert addon setting to number')
pass
xbmcutil.search_add(self.cache, what, maximum)
self.search(what)
def root(self):
searches = xbmcutil.get_searches(self.addon, self.provider.name)
if len(searches) > 0:
self.provider.info('Upgrading to new saved search storage...')
for s in searches:
self.provider.info('Moving item %s' % s)
xbmcutil.search_add(self.cache, s, 9999999)
xbmcutil.delete_search_history(self.addon, self.provider.name)
if 'search' in self.provider.capabilities():
params = self.params()
params.update({'search-list': '#'})
xbmcutil.add_dir(xbmcutil.__lang__(30003), params, xbmcutil.icon('search.png'))
if not '!download' in self.provider.capabilities():
xbmcutil.add_local_dir(xbmcutil.__lang__(30006), self.settings[
'downloads'], xbmcutil.icon('download.png'))
self.list(self.provider.categories())
return xbmcplugin.endOfDirectory(int(sys.argv[1]))
def download(self, item):
downloads = self.settings['downloads']
if '' == downloads:
xbmcgui.Dialog().ok(self.provider.name, xbmcutil.__lang__(30009))
return
stream = self.resolve(item['url'])
if stream:
if not 'headers' in list(stream.keys()):
stream['headers'] = {}
xbmcutil.reportUsage(self.addon_id, self.addon_id + '/download')
# clean up \ and /
name = item['title'].replace('/', '_').replace('\\', '_')
if not stream['subs'] == '':
xbmcutil.save_to_file(stream['subs'], os.path.join(
downloads, name + '.srt'), stream['headers'])
dot = name.find('.')
if dot <= 0:
# name does not contain extension, append some
name += '.mp4'
xbmcutil.download(self.addon, name, self.provider._url(
stream['url']), os.path.join(downloads, name), headers=stream['headers'])
def play(self, item):
stream = self.resolve(item['url'])
if stream:
xbmcutil.reportUsage(self.addon_id, self.addon_id + '/play')
if 'headers' in list(stream.keys()):
headerStr = '|' + urllib.parse.urlencode(stream['headers'])
if len(headerStr) > 1:
stream['url'] += headerStr.encode('utf-8')
print('Sending %s to player' % stream['url'])
li = xbmcgui.ListItem(path=stream['url'])
li.setArt({'icon': 'DefaulVideo.png'})
il = self._extract_infolabels(item['info'])
if len(il) > 0: # only set when something was extracted
li.setInfo('video', il)
try:
local_subs = xbmcutil.set_subtitles(li, stream['subs'], stream.get('headers'))
except:
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, li)
xbmcutil.load_subtitles(stream['subs'], stream.get('headers'))
else:
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, li)
def _handle_exc(self, e):
msg = e.message
if msg.find('$') == 0:
try:
msg = self.addon.getLocalizedString(int(msg[1:]))
except:
try:
msg = xbmcutil.__lang__(int(msg[1:]))
except:
pass
xbmcgui.Dialog().ok(self.provider.name, msg)
def resolve(self, url):
item = self.provider.video_item()
item.update({'url': url})
try:
return self.provider.resolve(item)
except ResolveException as e:
self._handle_exc(e)
def search(self, keyword):
self.list(self.provider.search(keyword))
return xbmcplugin.endOfDirectory(int(sys.argv[1]))
def list(self, items):
params = self.params()
for item in items:
if item['type'] == 'dir':
self.render_dir(item)
elif item['type'] == 'next':
params.update({'list': item['url']})
xbmcutil.add_dir(xbmcutil.__lang__(30007), params, xbmcutil.icon('next.png'))
elif item['type'] == 'prev':
params.update({'list': item['url']})
xbmcutil.add_dir(xbmcutil.__lang__(30008), params, xbmcutil.icon('prev.png'))
elif item['type'] == 'new':
params.update({'list': item['url']})
xbmcutil.add_dir(xbmcutil.__lang__(30012), params, xbmcutil.icon('new.png'))
elif item['type'] == 'top':
params.update({'list': item['url']})
xbmcutil.add_dir(xbmcutil.__lang__(30013), params, xbmcutil.icon('top.png'))
elif item['type'] == 'video':
self.render_video(item)
else:
self.render_default(item)
def render_default(self, item):
raise Exception("Unable to render item " + str(item))
def render_dir(self, item):
params = self.params()
params.update({'list': item['url']})
title = item['title']
img = None
if 'img' in list(item.keys()):
img = item['img']
if title.find('$') == 0:
try:
title = self.addon.getLocalizedString(int(title[1:]))
except:
pass
menuItems = {}
if 'menu' in list(item.keys()):
for ctxtitle, value in item['menu'].items():
if ctxtitle.find('$') == 0:
try:
ctxtitle = self.addon.getLocalizedString(int(ctxtitle[1:]))
except:
pass
menuItems[ctxtitle] = value
xbmcutil.add_dir(title, params, img, infoLabels=self._extract_infolabels(
item), menuItems=menuItems)
def _extract_infolabels(self, item):
infoLabels = {}
for label in ['title', 'plot', 'year', 'genre', 'rating', 'director',
'votes', 'cast', 'trailer', 'tvshowtitle', 'season',
'episode', 'duration']:
if label in list(item.keys()):
infoLabels[label] = util.decode_html(item[label])
return infoLabels
def render_video(self, item):
params = self.params()
params.update({'play': item['url']})
downparams = self.params()
downparams.update({'title': item['title'], 'down': item['url']})
def_item = self.provider.video_item()
if item['size'] == def_item['size']:
item['size'] = ''
else:
item['size'] = ' (%s)' % item['size']
title = '%s%s' % (item['title'], item['size'])
menuItems = {}
if "!download" not in self.provider.capabilities():
menuItems[xbmc.getLocalizedString(33003)] = downparams
if 'menu' in list(item.keys()):
for ctxtitle, value in item['menu'].items():
if ctxtitle.find('$') == 0:
try:
ctxtitle = self.addon.getLocalizedString(int(ctxtitle[1:]))
except:
pass
menuItems[ctxtitle] = value
xbmcutil.add_video(title,
params,
item['img'],
infoLabels=self._extract_infolabels(item),
menuItems=menuItems
)
def categories(self):
self.list(self.provider.categories(keyword))
return xbmcplugin.endOfDirectory(int(sys.argv[1]))
class XBMCMultiResolverContentProvider(XBMContentProvider):
def __init__(self, provider, settings, addon):
XBMContentProvider.__init__(self, provider, settings, addon)
self.check_setting_keys(['quality'])
def resolve(self, url):
item = self.provider.video_item()
item.update({'url': url})
def select_cb(resolved):
quality = self.settings['quality'] or '0'
filtered = resolver.filter_by_quality(resolved, quality)
lang = self.settings['lang'] or '0'
filtered = resolver.filter_by_language(filtered, lang)
# if user requested something but 'ask me' or filtered result is exactly 1
if len(filtered) == 1 or (int(quality) > 0 and int(lang) == 0):
return filtered[0]
# if user requested particular language and we have it
if len(filtered) > 0 and int(lang) > 0:
return filtered[0]
dialog = xbmcgui.Dialog()
opts = []
for r in resolved:
d = defaultdict(lambda: '', r)
opts.append('%s [%s] %s' % (d['title'], d['quality'], d['lang']))
ret = dialog.select(xbmcutil.__lang__(30005), opts)
if ret >= 0:
return resolved[ret]
try:
return self.provider.resolve(item, select_cb=select_cb)
except ResolveException as e:
self._handle_exc(e)
class XBMCLoginRequiredContentProvider(XBMContentProvider):
def root(self):
if not self.provider.login():
xbmcgui.Dialog().ok(self.provider.name, xbmcutil.__lang__(30011))
else:
return XBMContentProvider.root(self)
class XBMCLoginOptionalContentProvider(XBMContentProvider):
def __init__(self, provider, settings, addon):
XBMContentProvider.__init__(self, provider, settings, addon)
self.check_setting_keys(['vip'])
def ask_for_captcha(self, params):
img = os.path.join(str(xbmcvfs.translatePath(
self.addon.getAddonInfo('profile'))), 'captcha.png')
util.save_to_file(params['img'], img)
cd = CaptchaDialog('captcha-dialog.xml',
xbmcutil.__addon__.getAddonInfo('path'), 'default', '0')
cd.image = img
xbmc.sleep(3000)
cd.doModal()
del cd
kb = xbmc.Keyboard('', self.addon.getLocalizedString(200), False)
kb.doModal()
if kb.isConfirmed():
print('got code ' + kb.getText())
return kb.getText()
def ask_for_account_type(self):
if len(self.provider.username) == 0:
util.info('Username is not set, NOT using VIP account')
return False
if self.settings['vip'] == '0':
util.info('Asking user whether to use VIP account')
ret = xbmcgui.Dialog().yesno(self.provider.name, xbmcutil.__lang__(30010))
return ret == 1
return self.settings['vip'] == '1'
def resolve(self, url):
item = self.provider.video_item()
item.update({'url': url})
if not self.ask_for_account_type():
# set user/pass to null - user does not want to use VIP at this time
self.provider.username = None
self.provider.password = None
else:
if not self.provider.login():
xbmcgui.Dialog().ok(self.provider.name, xbmcutil.__lang__(30011))
return
try:
return self.provider.resolve(item, captcha_cb=self.ask_for_captcha)
except ResolveException as e:
self._handle_exc(e)
class XBMCLoginOptionalDelayedContentProvider(XBMCLoginOptionalContentProvider):
def wait_cb(self, wait):
left = wait
msg = xbmcutil.__lang__(30014).encode('utf-8')
while left > 0:
xbmc.executebuiltin("XBMC.Notification(%s,%s,1000,%s)" %
(self.provider.name, msg % str(left), ''))
left -= 1
time.sleep(1)
def resolve(self, url):
item = self.video_item()
item.update({'url': url})
if not self.ask_for_account_type():
# set user/pass to null - user does not want to use VIP at this time
self.provider.username = None
self.provider.password = None
else:
if not self.provider.login():
xbmcgui.Dialog().ok(self.provider.name, xbmcutil.__lang__(30011))
return
try:
return self.provider.resolve(item, captcha_cb=self.ask_for_captcha, wait_cb=self.wait_cb)
except ResolveException as e:
self._handle_exc(e)
class CaptchaDialog (xbmcgui.WindowXMLDialog):
def __init__(self, *args, **kwargs):
super(xbmcgui.WindowXMLDialog, self).__init__(args, kwargs)
self.image = None
def onFocus(self, controlId):
self.controlId = controlId
def onInit(self):
self.getControl(101).setImage(self.image)
def onAction(self, action):
if action.getId() in [9, 10]:
self.close()
def onClick(self, controlId):
if controlId == 102:
self.close()