# * 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()