class Source(BaseHandler):
    _system_monitor = None
    _account_manager = None

    kilobyte = 1024.0
    megabyte = kilobyte*kilobyte
    gigabyte = megabyte*kilobyte
    
    def __init__(self, request, client_address, server):
        self._system_monitor = KodiUtils.get_system_monitor()
        self._account_manager = AccountManager(server.service.profile_path)
        self._addonid = KodiUtils.get_addon_info('id')
        expiration = datetime.timedelta(minutes=KodiUtils.get_cache_expiration_time())
        self._page_cache = Cache(self._addonid, 'page', expiration)
        self._children_cache = Cache(self._addonid, 'children', expiration)
        self._items_cache = Cache(self._addonid, 'items', expiration)
        BaseHandler.__init__(self, request, client_address, server)
    
    def __del__(self):
        del self._system_monitor
        del self._account_manager
        Logger.debug('Request destroyed.')
    
    def _get_provider(self):
        return self.server.data(source_mode = True)
    
    def open_table(self, title):
        title = urllib.unquote(title)
        html = XHTML('html')
        html.head.title(title)
        body = html.body
        body.h1(title)
        table = body.table()
        row = table.tr
        row.th.a('Name')
        row.th.a('Last modified')
        row.th.a('Size')
        row.th.a('Description')
        row = table.tr
        row.th(colspan='4').hr()
        return html, table
    
    def add_row(self, table, file_name, date='  - ', size='  - ', description=' '):
        row = table.tr
        row.td.a(file_name, href=urllib.quote(file_name))
        row.td(date, align='right')
        row.td(size, align='right')
        row.td(description, escape=False)
    
    def close_table(self, table):
        table.tr.th(colspan='4').hr()
    
    def get_size(self, size):
        unit = ''
        if size > self.gigabyte:
            size = size / self.gigabyte
            unit = 'G'
        elif size > self.megabyte:
            size = size / self.megabyte
            unit = 'M'
        elif size > self.kilobyte:
            size = size / self.kilobyte
            unit = 'K'
        elif size < 0:
            return '-'
        return ("%.2f" % size) + unit
    
    def get_cloud_drive_addons(self):
        addons = []
        response = KodiUtils.execute_json_rpc('Addons.GetAddons', {'type':'xbmc.python.pluginsource', 'enabled': True, 'properties': ['dependencies', 'name']})
        for addon in Utils.get_safe_value(Utils.get_safe_value(response, 'result', {}), 'addons', []):
            for dependency in addon['dependencies']:
                if dependency['addonid'] == self._addonid:
                    addons.append(addon)
                    break
        return addons
    
    def get_addonid(self, addon_name):
        addons = self.get_cloud_drive_addons()
        addonid = None
        for addon in addons:
            if urllib.quote(Utils.str(addon['name'])) == addon_name:
                addonid = addon['addonid']
                break
        return addonid
    
    def show_addon_list(self):
        html, table = self.open_table('Index of /')
        addons = self.get_cloud_drive_addons()
        if addons:
            for addon in addons:
                self.add_row(table, Utils.str(addon['name']) + '/')
        else:
            self.add_row(table, KodiUtils.get_addon_info('name') + '/')
            
        self.close_table(table)
        response = Utils.get_file_buffer()
        response.write(str(html))
        return {'response_code': 200, 'content': response}
        
    def get_drive_list(self):
        drives = []
        accounts = self._account_manager.get_accounts()
        provider = self._get_provider()
        for account_id in accounts:
            account = accounts[account_id]
            for drive in account['drives']:
                drive['display_name'] = self._account_manager.get_account_display_name(account, drive, provider)
                drives.append(drive)
        return drives
    
    def get_driveid(self, drive_name):
        driveid = None
        drives = self.get_drive_list()
        for drive in drives:
            if urllib.quote(Utils.str(drive['display_name'])) == drive_name:
                driveid = drive['id']
                break
        return driveid
    
    def show_drives(self, addon_name):
        html, table = self.open_table('Index of /'+addon_name+'/')
        self.add_row(table, '../')
        drives = self.get_drive_list()
        for drive in drives:
            self.add_row(table, Utils.str(drive['display_name']) + '/')
        self.close_table(table)
        response = Utils.get_file_buffer()
        response.write(str(html))
        return {'response_code': 200, 'content': response}
    
    def process_path(self, addon_name, drive_name, path):
        headers = {}
        response = Utils.get_file_buffer()
        driveid = self.get_driveid(drive_name)
        if driveid:
            parts = self.path.split('/')
            filename = parts[len(parts)-1]
            if filename:
                response_code = 303
                if path:
                    u = urlparse(path)
                    path = u.path
                    Logger.debug('query: %s' % u.query)
                    if u.query == 'subtitles':
                        response_code = 200
                        response.write(json.dumps({'driveid': driveid, 'subtitles': self.get_subtitles(driveid, path)}))
                    else:
                        key = '%s%s:children' % (driveid, path[0:path.rfind('/')],)
                        Logger.debug('reading cache key: ' + key)
                        children = self._children_cache.get(key)
                        if not children and type(children) is NoneType:
                            self.get_folder_items(driveid, path[0:path.rfind('/')+1])
                        url = self.get_download_url(driveid, path)
                        headers['location'] = url
                        Logger.debug('redirect to: ' + url)
                else:
                    url = self.path + '/'
                    headers['location'] = url
            else:
                response_code = 200
                response.write(str(self.show_folder(driveid, path)))
        else:
            response_code = 404
            response.write('Drive "%s" does not exist for addon "%s"' % (drive_name, addon_name))
        return {'response_code': response_code, 'content': response, 'headers': headers}
    
    def get_folder_items(self, driveid, path):
        provider = self._get_provider()
        provider.configure(self._account_manager, driveid)
        cache_path = path[:len(path)-1]
        request_path = cache_path if len(path) > 1 else path
        self.is_path_possible(driveid, request_path)
        key = '%s%s:items' % (driveid, cache_path,)
        items = self._items_cache.get(key)
        if not items and type(items) is NoneType:
            items = provider.get_folder_items(path=request_path, include_download_info=True)
            self._items_cache.set(key, items)
            children_names = []
            cache_items = []
            for item in items:
                quoted_name = urllib.quote(Utils.str(item['name']))
                children_names.append(quoted_name)
                key = '%s%s%s' % (driveid, path, quoted_name,)
                Logger.debug('Adding item in cache for bulk: %s' % key)
                cache_items.append([key, item])
            self._items_cache.setmany(cache_items)
            Logger.debug('Cache in bulk saved')
            key = '%s%s:children' % (driveid, cache_path,)
            Logger.debug('saving children names for: ' + key)
            self._children_cache.set(key, children_names)
        else:
            Logger.debug('items for %s served from cache' % path)
        return items

    def show_folder(self, driveid, path):
        items = self.get_folder_items(driveid, path)
        html, table = self.open_table('Index of ' + self.path)
        self.add_row(table, '../')
        for item in items:
            file_name = Utils.str(item['name'])
            if 'folder' in item:
                file_name += '/'
            date = Utils.default(self.date_time_string(KodiUtils.to_timestamp(Utils.get_safe_value(item, 'last_modified_date'))), '  - ')
            size = self.get_size(Utils.default(Utils.get_safe_value(item, 'size'), -1))
            description = Utils.default(Utils.get_safe_value(item, 'description'), '&nbsp;')
            self.add_row(table, file_name, date, size, description)
        self.close_table(table)
        return html
    
    def is_path_possible(self, driveid, path):
        index = path.rfind('/')
        while index >= 0:
            filename = path[index+1:]
            path = path[0:index]
            key = '%s%s:children' % (driveid, path,)
            Logger.debug('testing possible path key: ' + key)
            children = self._children_cache.get(key)
            if children or type(children) is list:
                if filename and not filename in children:
                    Logger.debug('Not found. From cache.') 
                    raise RequestException('Not found. From cache.', HTTPError(self.path, 404, 'Not found.', None, None), 'Request URL: %s' % self.path, None)
                return True
            index = path.rfind('/')
        return True
    
    def get_item(self, driveid, path):
        key = '%s%s' % (driveid, path,)
        Logger.debug('Testing item from cache: %s' % key)
        item = self._items_cache.get(key)
        if not item:
            provider = self._get_provider()
            provider.configure(self._account_manager, driveid)
            self.is_path_possible(driveid, path)
            item = provider.get_item(path=path, include_download_info = True)
            Logger.debug('Saving item in cache: %s' % key)
            self._items_cache.set(key, item)
        return item
    
    def get_download_url(self, driveid, path):
        item = self.get_item(driveid, path) 
        if 'folder' in item:
            return self.path + '/'
        return item['download_info']['url']
    
    def get_subtitles(self, driveid, path):
        item = self.get_item(driveid, path) 
        key = '%s%s-subtitles' % (driveid, path,)
        Logger.debug('Testing subtitles from cache: %s' % key)
        subtitles = self._items_cache.get(key)
        if not subtitles:
            provider = self._get_provider()
            provider.configure(self._account_manager, driveid)
            self.is_path_possible(driveid, path)
            item_driveid = Utils.default(Utils.get_safe_value(item, 'drive_id'), driveid)
            subtitles = provider.get_subtitles(item['parent'], item['name'], item_driveid)
            Logger.debug('Saving subtitles in cache: %s' % key)
            self._items_cache.set(key, item)
        return subtitles
    
    def handle_resource_request(self, data):
        addon_name = data[2]
        size = len(data)
        cached_page = {}
        if size == 3:
            cached_page['response_code'] = 303
            cached_page['headers'] = {'location': self.path + '/'}
        elif size == 4 and not data[3]:
            cached_page = self.show_drives(addon_name)
        else:
            drive_name = data[3]
            path = self.path[len(self.server.service.name)+len(addon_name)+len(drive_name)+3:]
            cached_page = self.process_path(addon_name, drive_name, path)
        return cached_page
            
    def do_GET(self):
        Logger.debug(self.path + ': Requested')
        if self._system_monitor.abortRequested():
            Logger.debug(self.path + ': abort requested')
            return
        data = self.path.split('/')
        size = len(data)
        cached_page = self._page_cache.get(self.path)
        if cached_page:
            if cached_page['pending']:
                Logger.debug(self.path + ': Already requested. Waiting for original request...')
                max_waiting_time = time.time() + 30
                while not self._system_monitor.abortRequested() and max_waiting_time > time.time() and cached_page['pending']:
                    if self._system_monitor.waitForAbort(1):
                        break
                    cached_page = self._page_cache.get(self.path)

            if not self._system_monitor.abortRequested():
                if cached_page['pending']:
                    self.write_response(504)
                    Logger.debug(self.path + ': 504 - Gateway timeout')
                    self._page_cache.remove(self.path)
                else:
                    if 'content' in cached_page and cached_page['content']:
                        content = Utils.get_file_buffer()
                        content.write(cached_page['content'])
                        cached_page['content'] = content
                    self.write_response(cached_page['response_code'], content=Utils.get_safe_value(cached_page, 'content'), headers=Utils.get_safe_value(cached_page, 'headers', {}))
                    Logger.debug(self.path + ': %d - Served from cache' % cached_page['response_code'])
        else:
            cached_page = {'pending': True}
            self._page_cache.set(self.path, cached_page)
            if size > 1 and data[1] == self.server.service.name:
                try:
                    if size == 2:
                        cached_page['response_code'] = 303
                        cached_page['headers'] = {'location': self.path + '/'}
                    elif size > 2 and data[2]:
                        cached_page = self.handle_resource_request(data)
                    else:
                        cached_page = self.show_addon_list()
                except Exception as e:
                    httpex = ExceptionUtils.extract_exception(e, HTTPError)
                    if httpex:
                        cached_page['response_code'] = httpex.code
                    else:
                        cached_page['response_code'] = 500
                    
                    ErrorReport.handle_exception(e)
                    content = Utils.get_file_buffer()
                    content.write(ExceptionUtils.full_stacktrace(e))
                    
                    cached_page['content'] = content
            else:
                cached_page['response_code'] = 404
            cached_page['pending'] = False
            content_value = None
            if 'content' in cached_page:
                content_value = cached_page['content'].getvalue()
            self.write_response(cached_page['response_code'], content=Utils.get_safe_value(cached_page, 'content'), headers=Utils.get_safe_value(cached_page, 'headers', {}))
            cached_page['content'] = content_value
            if Utils.get_safe_value(cached_page, 'response_code', 0) >= 500:
                self._page_cache.remove(self.path)
            else:
                self._page_cache.set(self.path, cached_page)
            Logger.debug(self.path + ': Response code ' + Utils.str(cached_page['response_code']))
Example #2
0
class CloudDriveAddon:
    _DEFAULT_SIGNIN_TIMEOUT = 120
    _addon = None
    _addon_handle = None
    _addonid = None
    _addon_name = None
    _addon_params = None
    _addon_url = None
    _addon_version = None
    _common_addon = None
    _cancel_operation = False
    _content_type = None
    _dialog = None
    _export_manager = None
    _child_count_supported = True
    _auto_refreshed_slideshow_supported = True
    _load_target = 0
    _load_count = 0
    _profile_path = None
    _progress_dialog = None
    _progress_dialog_bg = None
    _system_monitor = None
    _video_file_extensions = [
        x for x in KodiUtils.get_supported_media("video")
        if x not in ('', 'zip')
    ]
    _audio_file_extensions = KodiUtils.get_supported_media("music")
    _image_file_extensions = KodiUtils.get_supported_media("picture")
    _account_manager = None
    _action = None
    _ip_before_pin = None

    def __init__(self):
        self._addon = KodiUtils.get_addon()
        self._addonid = self._addon.getAddonInfo('id')
        self._addon_name = self._addon.getAddonInfo('name')
        self._addon_url = sys.argv[0]
        self._addon_version = self._addon.getAddonInfo('version')
        self._common_addon_id = 'script.module.clouddrive.common'
        self._common_addon = KodiUtils.get_addon(self._common_addon_id)
        self._common_addon_version = self._common_addon.getAddonInfo('version')
        self._dialog = xbmcgui.Dialog()
        self._profile_path = Utils.unicode(
            KodiUtils.translate_path(self._addon.getAddonInfo('profile')))
        self._progress_dialog = DialogProgress(self._addon_name)
        self._progress_dialog_bg = DialogProgressBG(self._addon_name)
        self._system_monitor = KodiUtils.get_system_monitor()
        self._account_manager = AccountManager(self._profile_path)
        self._pin_dialog = None
        self.iskrypton = KodiUtils.get_home_property('iskrypton') == 'true'

        if len(sys.argv) > 1:
            self._addon_handle = int(sys.argv[1])
            self._addon_params = urllib.parse.parse_qs(sys.argv[2][1:])
            for param in self._addon_params:
                self._addon_params[param] = self._addon_params.get(param)[0]
            self._content_type = Utils.get_safe_value(self._addon_params,
                                                      'content_type')
            if not self._content_type:
                wid = xbmcgui.getCurrentWindowId()
                if wid == 10005 or wid == 10500 or wid == 10501 or wid == 10502:
                    self._content_type = 'audio'
                elif wid == 10002:
                    self._content_type = 'image'
                else:
                    self._content_type = 'video'
            xbmcplugin.addSortMethod(handle=self._addon_handle,
                                     sortMethod=xbmcplugin.SORT_METHOD_LABEL)
            xbmcplugin.addSortMethod(
                handle=self._addon_handle,
                sortMethod=xbmcplugin.SORT_METHOD_UNSORTED)
            xbmcplugin.addSortMethod(handle=self._addon_handle,
                                     sortMethod=xbmcplugin.SORT_METHOD_SIZE)
            xbmcplugin.addSortMethod(handle=self._addon_handle,
                                     sortMethod=xbmcplugin.SORT_METHOD_DATE)
            xbmcplugin.addSortMethod(
                handle=self._addon_handle,
                sortMethod=xbmcplugin.SORT_METHOD_DURATION)

    def __del__(self):
        del self._addon
        del self._common_addon
        del self._dialog
        del self._progress_dialog
        del self._progress_dialog_bg
        del self._system_monitor
        del self._account_manager

    def get_provider(self):
        raise NotImplementedError()

    def get_my_files_menu_name(self):
        return self._common_addon.getLocalizedString(32052)

    def get_custom_drive_folders(self, driveid):
        return

    def cancel_operation(self):
        return self._system_monitor.abortRequested(
        ) or self._progress_dialog.iscanceled() or self._cancel_operation or (
            self._pin_dialog and self._pin_dialog.iscanceled())

    def _get_display_name(self, account, drive=None, with_format=False):
        return self._account_manager.get_account_display_name(
            account, drive, self.get_provider(), with_format)

    def get_accounts(self, with_format=False):
        accounts = self._account_manager.get_accounts()
        for account_id in accounts:
            account = accounts[account_id]
            for drive in account['drives']:
                drive['display_name'] = self._get_display_name(
                    account, drive, with_format)
        return accounts

    def list_accounts(self):
        accounts = self.get_accounts(with_format=True)
        listing = []
        for account_id in accounts:
            account = accounts[account_id]
            size = len(account['drives'])
            for drive in account['drives']:
                context_options = []
                params = {
                    'action': '_search',
                    'content_type': self._content_type,
                    'driveid': drive['id']
                }
                cmd = 'ActivateWindow(%d,%s?%s)' % (xbmcgui.getCurrentWindowId(
                ), self._addon_url, urllib.parse.urlencode(params))
                context_options.append(
                    (self._common_addon.getLocalizedString(32039), cmd))
                params['action'] = '_remove_account'
                context_options.append(
                    (self._common_addon.getLocalizedString(32006),
                     'RunPlugin(' + self._addon_url + '?' +
                     urllib.parse.urlencode(params) + ')'))
                if size > 1:
                    params['action'] = '_remove_drive'
                    cmd = 'RunPlugin(' + self._addon_url + '?' + urllib.parse.urlencode(
                        params) + ')'
                    context_options.append(
                        (self._common_addon.getLocalizedString(32007), cmd))
                list_item = xbmcgui.ListItem(drive['display_name'])
                list_item.addContextMenuItems(context_options)
                params = {
                    'action': '_list_drive',
                    'content_type': self._content_type,
                    'driveid': drive['id']
                }
                url = self._addon_url + '?' + urllib.parse.urlencode(params)
                listing.append((url, list_item, True))
        list_item = xbmcgui.ListItem(
            self._common_addon.getLocalizedString(32005))
        params = {'action': '_add_account', 'content_type': self._content_type}
        url = self._addon_url + '?' + urllib.parse.urlencode(params)
        listing.append((url, list_item))
        xbmcplugin.addDirectoryItems(self._addon_handle, listing, len(listing))
        xbmcplugin.endOfDirectory(self._addon_handle, True)

    def _add_account(self):
        request_params = {
            'waiting_retry':
            lambda request, remaining: self._progress_dialog_bg.
            update(int((request.current_delay - remaining) / request.
                       current_delay * 100),
                   heading=self._common_addon.getLocalizedString(32043) %
                   ('' if request.current_tries == 1 else ' again'),
                   message=self._common_addon.getLocalizedString(32044) % str(
                       int(remaining)
                   ) + ' ' + self._common_addon.getLocalizedString(32045) %
                   (str(request.current_tries + 1), str(request.tries))),
            'on_complete':
            lambda request:
            (self._progress_dialog.close(), self._progress_dialog_bg.close()),
            'cancel_operation':
            self.cancel_operation,
            'wait':
            self._system_monitor.waitForAbort
        }
        provider = self.get_provider()
        self._progress_dialog.update(
            0, self._common_addon.getLocalizedString(32008))

        self._ip_before_pin = Request(KodiUtils.get_signin_server() + '/ip',
                                      None).request()
        pin_info = provider.create_pin(request_params)
        self._progress_dialog.close()
        if self.cancel_operation():
            return
        if not pin_info:
            raise Exception('Unable to retrieve a pin code')

        tokens_info = {}
        request_params[
            'on_complete'] = lambda request: self._progress_dialog_bg.close()
        self._pin_dialog = QRDialogProgress.create(
            self._addon_name,
            KodiUtils.get_signin_server() + '/signin/%s' % pin_info['pin'],
            self._common_addon.getLocalizedString(32009),
            self._common_addon.getLocalizedString(32010) %
            ('[B]%s[/B]' % KodiUtils.get_signin_server(),
             '[B][COLOR lime]%s[/COLOR][/B]' % pin_info['pin']))
        self._pin_dialog.show()
        max_waiting_time = time.time() + self._DEFAULT_SIGNIN_TIMEOUT
        while not self.cancel_operation() and max_waiting_time > time.time():
            remaining = round(max_waiting_time - time.time())
            percent = int(remaining / self._DEFAULT_SIGNIN_TIMEOUT * 100)
            self._pin_dialog.update(
                percent,
                line3='[CR]' + self._common_addon.getLocalizedString(32011) %
                str(int(remaining)) + '[CR][CR]Your source id is: %s' %
                Utils.get_source_id(Utils.unicode(self._ip_before_pin)))
            if int(remaining) % 5 == 0 or remaining == 1:
                tokens_info = provider.fetch_tokens_info(
                    pin_info, request_params=request_params)
                if self.cancel_operation() or tokens_info:
                    break
            if self._system_monitor.waitForAbort(1):
                break
        self._pin_dialog.close()

        if self.cancel_operation() or time.time() >= max_waiting_time:
            return
        if not tokens_info:
            raise Exception('Unable to retrieve the auth2 tokens')

        self._progress_dialog.update(
            25, self._common_addon.getLocalizedString(32064), ' ', ' ')
        try:
            account = provider.get_account(request_params=request_params,
                                           access_tokens=tokens_info)
        except Exception as e:
            raise UIException(32065, e)
        if self.cancel_operation():
            return

        self._progress_dialog.update(
            50, self._common_addon.getLocalizedString(32017))
        try:
            account['drives'] = provider.get_drives(
                request_params=request_params, access_tokens=tokens_info)
        except Exception as e:
            raise UIException(32018, e)
        if self.cancel_operation():
            return

        self._progress_dialog.update(
            75, self._common_addon.getLocalizedString(32020))
        try:
            account['access_tokens'] = tokens_info
            self._account_manager.save_account(account)
        except Exception as e:
            raise UIException(32021, e)
        if self.cancel_operation():
            return

        self._progress_dialog.update(90)
        try:
            accounts = self._account_manager.get_accounts()
            for drive in account['drives']:
                driveid = drive['id']
                Logger.debug('Looking for account %s...' % driveid)
                if driveid in accounts:
                    drive = accounts[driveid]['drives'][0]
                    Logger.debug(drive)
                    if drive['id'] == driveid and drive['type'] == 'migrated':
                        Logger.debug('Account %s removed.' % driveid)
                        self._account_manager.remove_account(driveid)

        except Exception as e:
            pass
        if self.cancel_operation():
            return

        self._progress_dialog.close()
        KodiUtils.executebuiltin('Container.Refresh')

    def _remove_drive(self, driveid):
        account = self._account_manager.get_by_driveid('account', driveid)
        drive = self._account_manager.get_by_driveid('drive', driveid, account)
        if self._dialog.yesno(
                self._addon_name,
                self._common_addon.getLocalizedString(32023) %
                self._get_display_name(account, drive, True)):
            self._account_manager.remove_drive(driveid, account)
            KodiUtils.executebuiltin('Container.Refresh')

    def _remove_account(self, driveid):
        account = self._account_manager.get_by_driveid('account', driveid)
        if self._dialog.yesno(
                self._addon_name,
                self._common_addon.getLocalizedString(32022) %
                self._get_display_name(account, with_format=True)):
            self._account_manager.remove_account(account['id'])
            KodiUtils.executebuiltin('Container.Refresh')

    def _list_drive(self, driveid):
        drive_folders = self.get_custom_drive_folders(driveid)
        if self.cancel_operation():
            return
        if drive_folders:
            listing = []
            url = self._addon_url + '?' + urllib.parse.urlencode(
                {
                    'action': '_list_folder',
                    'path': '/',
                    'content_type': self._content_type,
                    'driveid': driveid
                })
            listing.append(
                (url,
                 xbmcgui.ListItem('[B]%s[/B]' % self.get_my_files_menu_name()),
                 True))
            for folder in drive_folders:
                params = {
                    'action': '_list_folder',
                    'path': folder['path'],
                    'content_type': self._content_type,
                    'driveid': driveid
                }
                if 'params' in folder:
                    params.update(folder['params'])
                url = self._addon_url + '?' + urllib.parse.urlencode(params)
                list_item = xbmcgui.ListItem(Utils.unicode(folder['name']))
                if 'context_options' in folder:
                    list_item.addContextMenuItems(folder['context_options'])
                listing.append((url, list_item, True))
            if self._content_type == 'video' or self._content_type == 'audio':
                url = self._addon_url + '?' + urllib.parse.urlencode(
                    {
                        'action': '_list_exports',
                        'content_type': self._content_type,
                        'driveid': driveid
                    })
                listing.append(
                    (url,
                     xbmcgui.ListItem(
                         self._common_addon.getLocalizedString(32000)), True))
            url = self._addon_url + '?' + urllib.parse.urlencode(
                {
                    'action': '_search',
                    'content_type': self._content_type,
                    'driveid': driveid
                })
            listing.append(
                (url,
                 xbmcgui.ListItem(
                     self._common_addon.getLocalizedString(32039)), True))

            xbmcplugin.addDirectoryItems(self._addon_handle, listing,
                                         len(listing))
            xbmcplugin.endOfDirectory(self._addon_handle, True)
        else:
            self._list_folder(driveid, path='/')

    def _list_exports(self, driveid):
        self._export_manager = ExportManager(self._profile_path)
        exports = self._export_manager.get_exports()
        listing = []
        for exportid in exports:
            export = exports[exportid]
            if export['driveid'] == driveid and export[
                    'content_type'] == self._content_type:
                item_name = Utils.unicode(export['name'])
                params = {
                    'action': '_open_export',
                    'content_type': self._content_type,
                    'driveid': driveid,
                    'item_driveid': export['item_driveid'],
                    'item_id': export['id'],
                    'name': urllib.parse.quote(Utils.str(item_name))
                }
                url = self._addon_url + '?' + urllib.parse.urlencode(params)
                list_item = xbmcgui.ListItem(item_name)
                context_options = []
                params['action'] = '_run_export'
                context_options.append((KodiUtils.localize(21479),
                                        'RunPlugin(' + self._addon_url + '?' +
                                        urllib.parse.urlencode(params) + ')'))
                params['action'] = '_remove_export'
                context_options.append(
                    (KodiUtils.localize(1210), 'RunPlugin(' + self._addon_url +
                     '?' + urllib.parse.urlencode(params) + ')'))
                list_item.addContextMenuItems(context_options)
                listing.append((url, list_item, True))
        xbmcplugin.addDirectoryItems(self._addon_handle, listing, len(listing))
        xbmcplugin.endOfDirectory(self._addon_handle, True)

    def _run_export(self, driveid, item_id):
        self._export_manager = ExportManager(self._profile_path)
        export = self._export_manager.get_exports()[item_id]
        export['run_immediately'] = True
        self._export_manager.save_export(export)
        KodiUtils.show_notification(
            self._common_addon.getLocalizedString(32055) % '60')

    def _remove_export(self, driveid, item_id):
        self._export_manager = ExportManager(self._profile_path)
        exports = self._export_manager.get_exports()
        if item_id in exports:
            item = exports[item_id]
            remove_export = self._dialog.yesno(
                self._addon_name,
                self._common_addon.getLocalizedString(32001) %
                Utils.unicode(item['name']))
            if remove_export:
                keep_locals = self._dialog.yesno(
                    self._addon_name,
                    self._common_addon.getLocalizedString(32086) %
                    Utils.unicode(item['name']))
                if not keep_locals:
                    self._export_manager.remove_export(item_id, False)
                else:
                    self._export_manager.remove_export(item_id)
                KodiUtils.executebuiltin('Container.Refresh')
        else:
            KodiUtils.executebuiltin('Container.Refresh')

    def _open_export(self, driveid, item_driveid, item_id, name):
        export_dialog = ExportMainDialog.create(self._content_type, driveid,
                                                item_driveid, item_id, name,
                                                self._account_manager,
                                                self.get_provider())
        export_dialog.doModal()
        if export_dialog.run:
            KodiUtils.show_notification(
                self._common_addon.getLocalizedString(32055) % '60')

    def on_items_page_completed(self, items):
        self._load_count += len(items)
        if self._load_target > self._load_count:
            percent = int(
                round(float(self._load_count) / self._load_target * 100))
            self._progress_dialog_bg.update(
                percent, self._addon_name,
                self._common_addon.getLocalizedString(32047) %
                (Utils.str(self._load_count), Utils.str(self._load_target)))
        else:
            self._progress_dialog_bg.update(
                100, self._addon_name,
                self._common_addon.getLocalizedString(32048) %
                Utils.str(self._load_count))

    def _list_folder(self,
                     driveid,
                     item_driveid=None,
                     item_id=None,
                     path=None):
        self.get_provider().configure(self._account_manager, driveid)
        if self._child_count_supported:
            item = self.get_provider().get_item(item_driveid, item_id, path)
            if item:
                self._load_target = item['folder']['child_count']
                self._progress_dialog_bg.create(
                    self._addon_name,
                    self._common_addon.getLocalizedString(32049) %
                    Utils.str(self._load_target))

        items = self.get_provider().get_folder_items(
            item_driveid,
            item_id,
            path,
            on_items_page_completed=self.on_items_page_completed)
        if self.cancel_operation():
            return
        self._process_items(items, driveid)

    def _process_items(self, items, driveid):
        listing = []
        for item in items:
            Logger.debug(item)
            item_id = item['id']
            item_name = Utils.unicode(item['name'])
            item_name_extension = item['name_extension']
            item_driveid = Utils.default(
                Utils.get_safe_value(item, 'drive_id'), driveid)
            list_item = xbmcgui.ListItem(item_name)
            url = None
            is_folder = 'folder' in item
            params = {
                'content_type': self._content_type,
                'item_driveid': item_driveid,
                'item_id': item_id,
                'driveid': driveid
            }
            if 'extra_params' in item:
                params.update(item['extra_params'])
            context_options = []
            info = {
                'size':
                item['size'],
                'date':
                KodiUtils.to_kodi_item_date_str(
                    KodiUtils.to_datetime(
                        Utils.get_safe_value(item, 'last_modified_date')))
            }
            if is_folder:
                params['action'] = '_list_folder'
                url = self._addon_url + '?' + urllib.parse.urlencode(params)
                params['action'] = '_search'
                cmd = 'ActivateWindow(%d,%s?%s)' % (xbmcgui.getCurrentWindowId(
                ), self._addon_url, urllib.parse.urlencode(params))
                context_options.append(
                    (self._common_addon.getLocalizedString(32039), cmd))
                if self._content_type == 'audio' or self._content_type == 'video':
                    params['action'] = '_open_export'
                    params['name'] = urllib.parse.quote(Utils.str(item_name))
                    context_options.append(
                        (self._common_addon.getLocalizedString(32004),
                         'RunPlugin(' + self._addon_url + '?' +
                         urllib.parse.urlencode(params) + ')'))
                    del params['name']
                elif self._content_type == 'image' and self._auto_refreshed_slideshow_supported:
                    params['action'] = '_slideshow'
                    context_options.append(
                        (self._common_addon.getLocalizedString(32032),
                         'RunPlugin(' + self._addon_url + '?' +
                         urllib.parse.urlencode(params) + ')'))
            elif (('video' in item or
                   (item_name_extension
                    and item_name_extension in self._video_file_extensions))
                  and self._content_type == 'video') or (
                      ('audio' in item or
                       (item_name_extension and item_name_extension
                        in self._audio_file_extensions))
                      and self._content_type == 'audio'):
                list_item.setProperty('IsPlayable', 'true')
                params['action'] = 'download'
                cmd = 'RunPlugin(' + self._addon_url + '?' + urllib.parse.urlencode(
                    params) + ')'
                context_options.append(
                    (self._common_addon.getLocalizedString(32051), cmd))
                params['action'] = 'play'
                url = self._addon_url + '?' + urllib.parse.urlencode(params)
                info_type = self._content_type
                if 'audio' in item:
                    info.update(item['audio'])
                    info_type = 'music'
                elif 'video' in item:
                    list_item.addStreamInfo('video', item['video'])
                list_item.setInfo(info_type, info)
                if 'thumbnail' in item:
                    list_item.setArt({
                        'icon': item['thumbnail'],
                        'thumb': item['thumbnail']
                    })
            elif ('image' in item or
                  (item_name_extension
                   and item_name_extension in self._image_file_extensions)
                  ) and self._content_type == 'image':
                Logger.debug(
                    'image in item: %s' % (Utils.str('image' in item)), )
                Logger.debug(
                    'item_name_extension in self._image_file_extensions: %s' %
                    (Utils.str(
                        item_name_extension in self._image_file_extensions), ))
                params['action'] = 'download'
                cmd = 'RunPlugin(' + self._addon_url + '?' + urllib.parse.urlencode(
                    params) + ')'
                context_options.append(
                    (self._common_addon.getLocalizedString(32051), cmd))
                if 'url' in item:
                    url = item['url']
                else:
                    url = self._get_item_play_url(
                        urllib.parse.quote(Utils.str(item_name)), driveid,
                        item_driveid, item_id)
                if 'image' in item:
                    info.update(item['image'])
                list_item.setInfo('pictures', info)
                if 'thumbnail' in item and item['thumbnail']:
                    list_item.setArt({
                        'icon': item['thumbnail'],
                        'thumb': item['thumbnail']
                    })
            if url:
                context_options.extend(
                    self.get_context_options(list_item, params, is_folder))
                list_item.addContextMenuItems(context_options)
                mimetype = Utils.default(
                    Utils.get_mimetype_by_extension(item_name_extension),
                    Utils.get_safe_value(item, 'mimetype'))
                if mimetype:
                    list_item.setProperty('mimetype', mimetype)

                listing.append((url, list_item, is_folder))
        xbmcplugin.addDirectoryItems(self._addon_handle, listing, len(listing))
        xbmcplugin.endOfDirectory(self._addon_handle, True)

    def get_context_options(self, list_item, params, is_folder):
        return []

    def _search(self, driveid, item_driveid=None, item_id=None):
        self.get_provider().configure(self._account_manager, driveid)
        query = self._dialog.input(
            self._addon_name + ' - ' +
            self._common_addon.getLocalizedString(32042))
        if query:
            self._progress_dialog_bg.create(
                self._addon_name, self._common_addon.getLocalizedString(32041))
            items = self.get_provider().search(
                query,
                item_driveid,
                item_id,
                on_items_page_completed=self.on_items_page_completed)
            if self.cancel_operation():
                return
            self._process_items(items, driveid)

    def download(self, driveid, item_driveid=None, item_id=None):
        dest_folder = self._dialog.browse(
            0, self._common_addon.getLocalizedString(32002), 'files')
        if dest_folder:
            provider = self.get_provider()
            provider.configure(self._account_manager, driveid)
            item = provider.get_item(item_driveid,
                                     item_id,
                                     include_download_info=True)
            name = Utils.get_safe_value(item, 'name', item['id'])
            download_path = os.path.join(dest_folder, Utils.unicode(name))
            download_size = Utils.get_safe_value(item, 'size', 0)
            on_update_download = lambda request: self._progress_dialog_bg.update(
                int(1.0 * request.download_progress / download_size * 100),
                self._addon_name,
                self._common_addon.getLocalizedString(32056) % name)
            if ExportManager.download(item,
                                      download_path,
                                      provider,
                                      on_update_download=on_update_download):
                msg = self._common_addon.getLocalizedString(32057) % name
            else:
                msg = self._common_addon.getLocalizedString(32087) % name
            KodiUtils.show_notification(msg)

    def new_change_token_slideshow(self,
                                   change_token,
                                   driveid,
                                   item_driveid=None,
                                   item_id=None,
                                   path=None):
        self.get_provider().configure(self._account_manager, driveid)
        item = self.get_provider().get_item(item_driveid, item_id, path)
        if self.cancel_operation():
            return
        return item['folder']['child_count']

    def _slideshow(self,
                   driveid,
                   item_driveid=None,
                   item_id=None,
                   path=None,
                   change_token=None):
        new_change_token = self.new_change_token_slideshow(
            change_token, driveid, item_driveid, item_id, path)
        if self.cancel_operation():
            return
        wait_for_slideshow = False
        if not change_token or change_token != new_change_token:
            Logger.notice(
                'Slideshow will start. change_token: %s, new_change_token: %s'
                % (change_token, new_change_token))
            params = {
                'action': '_list_folder',
                'content_type': self._content_type,
                'item_driveid': Utils.default(item_driveid, ''),
                'item_id': Utils.default(item_id, ''),
                'driveid': driveid,
                'path': Utils.default(path, '')
            }
            extra_params = ',recursive' if self._addon.getSetting(
                'slideshow_recursive') == 'true' else ''
            KodiUtils.executebuiltin('SlideShow(' + self._addon_url + '?' +
                                     urllib.parse.urlencode(params) +
                                     extra_params + ')')
            wait_for_slideshow = True
        else:
            Logger.notice(
                'Slideshow child count is the same, nothing to refresh...')
        t = threading.Thread(target=self._refresh_slideshow,
                             args=(
                                 driveid,
                                 item_driveid,
                                 item_id,
                                 path,
                                 new_change_token,
                                 wait_for_slideshow,
                             ))
        t.setDaemon(True)
        t.start()

    def _refresh_slideshow(self, driveid, item_driveid, item_id, path,
                           change_token, wait_for_slideshow):
        if wait_for_slideshow:
            Logger.notice(
                'Waiting up to 10 minutes until the slideshow for folder %s starts...'
                % Utils.default(item_id, path))
            max_waiting_time = time.time() + 10 * 60
            while not self.cancel_operation(
            ) and not KodiUtils.get_cond_visibility(
                    'Slideshow.IsActive') and max_waiting_time > time.time():
                if self._system_monitor.waitForAbort(2):
                    break
            self._print_slideshow_info()
        interval = self._addon.getSetting('slideshow_refresh_interval')
        Logger.notice(
            'Waiting up to %s minute(s) to check if it is needed to refresh the slideshow of folder %s...'
            % (interval, Utils.default(item_id, path)))
        target_time = time.time() + int(interval) * 60
        while not self.cancel_operation() and target_time > time.time(
        ) and KodiUtils.get_cond_visibility('Slideshow.IsActive'):
            if self._system_monitor.waitForAbort(10):
                break
        self._print_slideshow_info()
        if not self.cancel_operation() and KodiUtils.get_cond_visibility(
                'Slideshow.IsActive'):
            try:
                self._slideshow(driveid, item_driveid, item_id, path,
                                change_token)
            except Exception as e:
                Logger.error(
                    'Slideshow failed to auto refresh. Will be restarted when possible. Error: '
                )
                Logger.error(ExceptionUtils.full_stacktrace(e))
                self._refresh_slideshow(driveid, item_driveid, item_id, path,
                                        None, wait_for_slideshow)
        else:
            Logger.notice(
                'Slideshow is not running anymore or abort requested.')

    def _print_slideshow_info(self):
        if KodiUtils.get_cond_visibility('Slideshow.IsActive'):
            Logger.debug('Slideshow is there...')
        elif self.cancel_operation():
            Logger.debug('Abort requested...')

    def _get_item_play_url(self,
                           file_name,
                           driveid,
                           item_driveid=None,
                           item_id=None,
                           is_subtitle=False):
        return DownloadServiceUtil.build_download_url(
            driveid, item_driveid, item_id,
            urllib.parse.quote(Utils.str(file_name)))

    def play(self, driveid, item_driveid=None, item_id=None):
        self.get_provider().configure(self._account_manager, driveid)
        find_subtitles = self._addon.getSetting(
            'set_subtitle') == 'true' and self._content_type == 'video'
        item = self.get_provider().get_item(item_driveid,
                                            item_id,
                                            find_subtitles=find_subtitles)
        file_name = Utils.unicode(item['name'])
        list_item = xbmcgui.ListItem(file_name)
        succeeded = True
        info = KodiUtils.get_current_library_info()
        if not info:
            info = KodiUtils.find_exported_video_in_library(
                item_id, file_name + ExportManager._strm_extension)
        if info and info['id']:
            Logger.debug('library info: %s' % Utils.str(info))
            KodiUtils.set_home_property('dbid', Utils.str(info['id']))
            KodiUtils.set_home_property('dbtype', info['type'])
            KodiUtils.set_home_property('addonid', self._addonid)
            details = KodiUtils.get_video_details(info['type'], info['id'])
            Logger.debug('library details: %s' % Utils.str(details))
            if details and 'resume' in details:
                KodiUtils.set_home_property('playcount',
                                            Utils.str(details['playcount']))
                resume = details['resume']
                if resume['position'] > 0:
                    play_resume = False
                    if self.iskrypton:
                        play_resume = KodiUtils.get_addon_setting(
                            'resume_playing') == 'true'
                    elif KodiUtils.get_addon_setting('ask_resume') == 'true':
                        d = datetime(1, 1,
                                     1) + timedelta(seconds=resume['position'])
                        t = '%02d:%02d:%02d' % (d.hour, d.minute, d.second)
                        Logger.debug(t)
                        option = self._dialog.contextmenu([
                            KodiUtils.localize(32054, addon=self._common_addon)
                            % t,
                            KodiUtils.localize(12021)
                        ])
                        Logger.debug('selected option: %d' % option)
                        if option == -1:
                            succeeded = False
                        elif option == 0:
                            play_resume = True
                    if play_resume:
                        list_item.setProperty('resumetime',
                                              Utils.str(resume['position']))
                        list_item.setProperty('startoffset',
                                              Utils.str(resume['position']))
                        list_item.setProperty('totaltime',
                                              Utils.str(resume['total']))
        else:
            from clouddrive.common.service.player import KodiPlayer
            KodiPlayer.cleanup()
        if 'audio' in item:
            list_item.setInfo('music', item['audio'])
        elif 'video' in item:
            list_item.addStreamInfo('video', item['video'])
        list_item.select(True)
        list_item.setPath(
            self._get_item_play_url(file_name, driveid, item_driveid, item_id))
        list_item.setProperty('mimetype',
                              Utils.get_safe_value(item, 'mimetype'))
        if find_subtitles and 'subtitles' in item:
            subtitles = []
            for subtitle in item['subtitles']:
                subtitles.append(
                    self._get_item_play_url(
                        urllib.parse.quote(Utils.str(subtitle['name'])),
                        driveid,
                        Utils.default(
                            Utils.get_safe_value(subtitle, 'drive_id'),
                            driveid), subtitle['id'], True))
            list_item.setSubtitles(subtitles)
        if not self.cancel_operation():
            xbmcplugin.setResolvedUrl(self._addon_handle, succeeded, list_item)

    def _handle_exception(self, ex, show_error_dialog=True):
        stacktrace = ExceptionUtils.full_stacktrace(ex)
        rex = ExceptionUtils.extract_exception(ex, RequestException)
        uiex = ExceptionUtils.extract_exception(ex, UIException)
        httpex = ExceptionUtils.extract_exception(ex, HTTPError)
        urlex = ExceptionUtils.extract_exception(ex, URLError)
        line1 = self._common_addon.getLocalizedString(32027)
        line2 = Utils.unicode(ex)
        line3 = self._common_addon.getLocalizedString(32016)

        if uiex:
            line1 = self._common_addon.getLocalizedString(int(Utils.str(uiex)))
            line2 = Utils.unicode(uiex.root_exception)
        elif rex and rex.response:
            line1 += ' ' + Utils.unicode(rex)
            line2 = ExceptionUtils.extract_error_message(rex.response)
        send_report = True
        add_account_cmd = 'RunPlugin(' + self._addon_url + '?' + urllib.parse.urlencode(
            {
                'action': '_add_account',
                'content_type': self._content_type
            }) + ')'
        if isinstance(ex, AccountNotFoundException) or isinstance(
                ex, DriveNotFoundException):
            show_error_dialog = False
            if self._dialog.yesno(
                    self._addon_name,
                    self._common_addon.getLocalizedString(32063) % '\n'):
                KodiUtils.executebuiltin(add_account_cmd)
        elif rex and httpex:
            if httpex.code >= 500:
                line1 = self._common_addon.getLocalizedString(32035)
                line2 = None
                line3 = self._common_addon.getLocalizedString(32038)
            elif httpex.code >= 400:
                driveid = Utils.get_safe_value(self._addon_params, 'driveid')
                if driveid:
                    account = self._account_manager.get_by_driveid(
                        'account', driveid)
                    drive = self._account_manager.get_by_driveid(
                        'drive', driveid, account)
                    if KodiUtils.get_signin_server(
                    ) in rex.request or httpex.code == 401:
                        send_report = False
                        show_error_dialog = False
                        if self._dialog.yesno(
                                self._addon_name,
                                self._common_addon.getLocalizedString(32046) %
                            (self._get_display_name(account, drive, True) +
                             '\n')):
                            KodiUtils.executebuiltin(add_account_cmd)
                    elif httpex.code == 403:
                        line1 = self._common_addon.getLocalizedString(32019)
                        line2 = line3 = None
                    elif httpex.code == 404:
                        send_report = False
                        line1 = self._common_addon.getLocalizedString(32037)
                        line2 = line2 = None
                    else:
                        line1 = self._common_addon.getLocalizedString(32036)
                        line3 = self._common_addon.getLocalizedString(32038)
                else:
                    if KodiUtils.get_signin_server(
                    ) + '/pin/' in rex.request and httpex.code == 404 and self._ip_before_pin:
                        ip_after_pin = Request(
                            KodiUtils.get_signin_server() + '/ip',
                            None).request()
                        if self._ip_before_pin != ip_after_pin:
                            send_report = False
                            line1 = self._common_addon.getLocalizedString(
                                32072)
                            line2 = self._common_addon.getLocalizedString(
                                32073) % (
                                    self._ip_before_pin,
                                    ip_after_pin,
                                )
        elif urlex:
            reason = Utils.str(urlex.reason)
            line3 = self._common_addon.getLocalizedString(32074)
            if '[Errno 101]' in reason:
                line1 = self._common_addon.getLocalizedString(32076)
            elif '[Errno 11001]' in reason:
                line1 = self._common_addon.getLocalizedString(32077)
            elif 'CERTIFICATE_VERIFY_FAILED' in reason:
                line1 = self._common_addon.getLocalizedString(32078)
            else:
                line1 = self._common_addon.getLocalizedString(32075)

        report = '[%s] [%s]/[%s]\n\n%s\n%s\n%s\n\n%s' % (
            self._addonid, self._addon_version, self._common_addon_version,
            line1, line2, line3, stacktrace)
        if rex:
            report += '\n\n%s\nResponse:\n%s' % (rex.request, rex.response)
        report += '\n\nshow_error_dialog: %s' % show_error_dialog
        Logger.error(report)
        if show_error_dialog:
            if line2:
                line1 += '\n' + line2
            if line3:
                line1 += '\n' + line3
            self._dialog.ok(self._addon_name, line1)
        if send_report:
            report_error = KodiUtils.get_addon_setting(
                'report_error', self._common_addon_id) == 'true'
            report_error_invite = KodiUtils.get_addon_setting(
                'report_error_invite', self._common_addon_id) == 'true'
            if not report_error and not report_error_invite:
                if not self._dialog.yesno(
                        self._addon_name,
                        self._common_addon.getLocalizedString(32050),
                        self._common_addon.getLocalizedString(32012),
                        self._common_addon.getLocalizedString(32013)):
                    KodiUtils.set_addon_setting('report_error', 'true',
                                                self._common_addon_id)
                KodiUtils.set_addon_setting('report_error_invite', 'true',
                                            self._common_addon_id)
            ErrorReport.send_report(report)

    def _open_common_settings(self):
        self._common_addon.openSettings()

    def _clear_cache(self):
        Cache(self._addonid, 'page', 0).clear()
        Cache(self._addonid, 'children', 0).clear()
        Cache(self._addonid, 'items', 0).clear()

    def _rename_action(self):
        pass

    def route(self):
        try:
            Logger.debug(self._addon_params)
            self._action = Utils.get_safe_value(self._addon_params, 'action')
            if self._action:
                self._rename_action()
                method = getattr(self, self._action)
                arguments = {}
                for name in inspect.getargspec(method)[0]:
                    if name in self._addon_params:
                        arguments[name] = self._addon_params[name]
                method(**arguments)
            else:
                self.list_accounts()
        except Exception as ex:
            self._handle_exception(ex)
        finally:
            self._progress_dialog.close()
            self._progress_dialog_bg.close()
            if self._pin_dialog:
                self._pin_dialog.close()