class EmcModule(object):
    name = ''
    label = ''
    icon = ''
    info = ''

    def __init__(self):
        pass

    def __shutdown__(self):
        pass

    def __restart__(self):
        self.__shutdown__()
        self.__init__() class EmcBrowser(object):
    """
    This is the browser object, it is used to show various page each containing
    a list. Usually you need a single instance of this class for all your needs.
    In order you have to:
     1. implement at least one class that inherit from EmcItemClass
     2. create an instance of EmcBrowser
     3. add a page to the browser using the page_add() method
     4. add items to the current page using item_add(MyItemClass(), url, user_data)
    Later you can create new pages or use back(), clear(), show(), hide()
    """

    def __init__(self, name, default_style='List', icon='icon/home'):
        DBG('EmcBrowser __init__')
        _instances.append(self)
        self.name = name
        self.default_style = default_style
        self.icon = icon

        self.pages = []
        self.current_view = None
        self.autoselect_url = None
        self._freezed = False

    def __str__(self):
        text = '=' * 70 + '\n'
        text += '===  %s  %s\n' % (self.name, '=' * (63 - len(self.name)))
        text += '== name: %s  pages: %d curview: %s\n' % \
                (self.name, len(self.pages), self.current_view)
        for p in self.pages:
            text += '== page: ' + str(p) + '\n'
        text += '=' * 70 + '\n'
        # for s in _style_memory:
        #    text += 'style mem: %s  style: %s\n' % (s, _style_memory[s])
        # text += '*' * 70 + '\n'
        return text

    def delete(self):
        _instances.remove(self)
        del self

    def _search_style_in_parent(self):
        for p in reversed(self.pages):
            if _memorydb.id_exists(p['url']):
                return _memorydb.get_data(p['url'])
        return None

    def page_add(self, url, title, styles, populate_cb, *args, **kwargs):
        """
        When you create a page you need to give at least the url, the title
        and the populate callback. Every other arguments will be passed back
        in the callback. style can be None to use the default page style,
        usually the plain list.

        Args:
           url: A unique string id for the page
           title: Readable text for the user
           styles: A tuple with all the style that the page can show.
                   Available styles: 'List', 'PosterGrid', 'CoverGrid'
                   If set to None it default to the default style given at
                   the Browser instance creation.
                   The first item is the default one.
           populate_cb: Function to call when the page need to be populated.
                        Signature: func(browser, url, *args, **kwargs)
        """

        # choose the style of the new page
        if styles is None:
            styles = (self.default_style,)
        if _memorydb.id_exists(url):
            style = _memorydb.get_data(url)
        else:
            style = self._search_style_in_parent()
        if not style:
            style = styles[0]
        if ini.get_bool('general', 'ignore_views_restrictions') is False:
            if style not in styles:
                style = styles[0]

        # get the correct view instance
        view = self._create_or_get_view(style)

        # append the new page in the pages list
        page = {'view': view, 'url': url, 'title': title, 'styles': styles,
                'cb': populate_cb, 'args': args, 'kwargs': kwargs}
        self.pages.append(page)

        # first time, we don't have a current_view, set it
        if not self.current_view:
            self.current_view = view

        # switch to the new page
        self._populate_page(page)

    def item_add(self, item_class, url, user_data=None):
        """
        Use this method to add an item in the current (last added) page
        Url should be (but its not mandatory) a full correct url in the form:
        file:///home/user/some/dir, it MUST be unique in all the browser
        and MUST not contain any strange chars.
        """
        self.current_view.item_add(item_class, url, user_data,
                                   True if (url and url == self.autoselect_url) else False)

    def group_add(self, label, icon=None):
        """ TODO DOC """
        self.current_view.group_add(label, icon)

    def back(self):
        """ TODO Function doc """
        # discard current page
        old_page = self.pages.pop()
        self.autoselect_url = old_page['url']
        del old_page

        # no more page to go back, hide the view and return to main menu
        if len(self.pages) == 0:
            self.hide()
            mainmenu.show()
            return

        # switch to the previous page
        page_data = self.pages[-1]
        self._populate_page(page_data, is_back=True)

    def refresh(self, hard=False):
        if self.pages and self.current_view:
            if hard:
                # try to keep the selected item selected
                self.autoselect_url = self.current_view.selected_url_get()
                # re-create the page
                self._populate_page(self.pages[-1], is_refresh=True)
            else:
                self.current_view.refresh()

    def change_style(self, style):
        # the current page is always the last one
        page = self.pages[-1]

        # check if the style is valid (unless not explicitly ignored)
        if ini.get_bool('general', 'ignore_views_restrictions') is False:
            if style not in page['styles']:
                DBG('Style %s not available for this page' % style)
                EmcDialog(style='info', title=_('View restriction'),
                          text=_('<br><br>The requested view is not enabled for the current '
                                 'page.<br><br><small><info>NOTE: You can ovveride this '
                                 'restriction in the <i>Configuration → Views</i> '
                                 'section.</info></small>'))
                return

        # change only if needed
        view = self._create_or_get_view(style)
        if view == self.current_view:
            return

        # remember the selected item
        self.autoselect_url = self.current_view.selected_url_get()

        # clear & hide the current view
        self.current_view.clear()
        self.current_view.hide()

        # set the new view in the current (always the last) page
        page['view'] = view

        # remember the user choice
        global _memorydb
        page_url = self.pages[-1]['url']
        _memorydb.set_data(page_url, style)

        # recreate the page
        self.refresh(hard=True)

    def clear(self):
        """ TODO Function doc """
        self.current_view = None
        self.autoselect_url = None
        self._freezed = False
        self.pages = []

    def show(self):
        """ TODO Function doc """
        gui.signal_emit('topbar,show')
        gui.swallow_set('topbar.icon', EmcImage(self.icon))
        self.current_view.show()
        input_events.listener_add('browser-' + self.name, self._input_event_cb)
        for b in _topbar_btns:
            b.focus_allow = True

    def hide(self):
        """ TODO Function doc """
        gui.signal_emit('topbar,hide')
        input_events.listener_del('browser-' + self.name)
        self.current_view.hide()
        for b in _topbar_btns:
            b.focus_allow = False

    @property
    def freezed(self):
        """ TODO Function doc """
        return self._freezed

    def freeze(self):
        """ TODO Function doc """
        self.autoselect_url = self.current_view.selected_url_get()
        self.hide()
        mainmenu.show()
        self._freezed = True

    def unfreeze(self):
        """ TODO Function doc """
        mainmenu.hide()
        self._populate_page(self.pages[-1], is_unfreeze=True)
        self.show()
        self._freezed = False

    def item_bring_in(self, pos='top', animated=True):
        """ Move the view so that the currently selected item will go on 'pos'
        pos can be: 'in', ', top', 'mid'
        """
        self.current_view.item_bring_in(pos, animated)

    # private stuff
    def _populate_page(self, page, is_back=False, is_refresh=False, is_unfreeze=False):
        full = ' > '.join([p['title'] for p in self.pages])
        gui.text_set('topbar.title', full)

        view = page['view']
        if view == self.current_view:
            # same style for the 2 pages, ask the view to perform the correct anim
            if is_refresh or is_unfreeze:
                view.page_show(page['title'], ANIM_NONE)
            elif is_back:
                view.page_show(page['title'], ANIM_BACK)
            elif len(self.pages) < 2:
                view.page_show(page['title'], ANIM_NONE)
            else:
                view.page_show(page['title'], ANIM_FORWARD)
        else:
            # different style...hide one view and show the other
            self.current_view.clear()
            self.current_view.hide()
            view.page_show(page['title'], ANIM_NONE)
            view.show()

        # update state
        self.current_view = view

        # back item (optional)
        if ini.get_bool('general', 'back_in_lists') is True:
            self.item_add(BackItemClass(), 'emc://back', self)
            view.items_count -= 1

        # use this for extra debug
        # print(self)

        # and finally populate the page
        url = page['url']
        cb = page['cb']
        args = page['args']
        kwargs = page['kwargs']
        cb(self, url, *args, **kwargs)

    def _input_event_cb(self, event):
        # pass the event to the view
        view_ret = self.current_view.input_event_cb(event)
        if view_ret == input_events.EVENT_BLOCK:
            return input_events.EVENT_BLOCK

        # always
        if event == 'BACK':
            self.back()
            return input_events.EVENT_BLOCK
        if event == 'EXIT':
            self.freeze()
            return input_events.EVENT_BLOCK
        elif event == 'VIEW_LIST':
            self.change_style('List')
            return input_events.EVENT_BLOCK
        elif event == 'VIEW_POSTERGRID':
            self.change_style('PosterGrid')
            return input_events.EVENT_BLOCK
        elif event == 'VIEW_COVERGRID':
            self.change_style('CoverGrid')
            return input_events.EVENT_BLOCK

        return input_events.EVENT_CONTINUE

    @staticmethod
    def _create_or_get_view(view_name):
        if view_name in _views:
            DBG('View exists: ' + view_name)
            return _views[view_name]
        # call the constructor of the given class style ex: ViewList()
        DBG('Create view: ' + view_name)
        _views[view_name] = eval('View' + view_name + '()')
        return _views[view_name] class EmcItemClass(Singleton):
    """ TODO Class doc """

    def item_selected(self, url, user_data):
        """ Called when an item is selected """
        # DBG(('item_selected(%s)' % url))
        pass

    def label_get(self, url, user_data):
        """ Called when a view need to show the label of your item.
            Must return the string to show. """
        # DBG(('label_get(%s)' % url))
        return 'Unknow'

    def label_end_get(self, url, user_data):
        """ Called when a view need to show the secondary label of your item.
            Must return the string to show, or None """
        return None

    def icon_get(self, url, user_data):
        """ Called when a view need to show the icon of your item.
            Must return the name of the icon to use for the given url
            see gui.load_icon() for detail on what you can pass as the name"""
        # DBG(('icon_get(%s)' % url))
        return None

    def icon_end_get(self, url, user_data):
        # DBG(('icon_end_get(%s)' % url))
        return None

    def info_get(self, url, user_data):
        """ Called when a view need to show the info of your item.
            Must return a string with the murkupped text that describe the item """
        # DBG(('info_get(%s)' % url))
        return None

    def poster_get(self, url, user_data):
        """ Called when a view need to show the poster of your
            item, must return the full path of a valid image file.
            The poster MUST have a 1:2 aspect.
            You can also return a valid url (http://) to automatically
            download the image to a random temp file.
            In addition you can also set the destination path for the given url,
            to set an url AND a destination just return a tuple, as:
            (url, local_path)
            """
        # DBG(('poster_get(%s)' % url))
        return None

    def cover_get(self, url, user_data):
        """ Like poster_get but th image should be squared (or a bit larger) """
        # DBG(('poster_get(%s)' % url))
        return None

    def fanart_get(self, url, user_data):
        """ Called when a view need to show the fanart of your item.
            Must return the full path of a valid image file """
        # DBG(('fanart_get(%s)' % url))
        return None class FolderItemClass(EmcItemClass):
    """ Base item class to be subclassed for ALL folder items """
    def item_selected(self, url, user_data):
        raise NotImplementedError

    def label_get(self, url, user_data):
        return elm.utf8_to_markup(os.path.basename(url))

    def icon_get(self, url, user_data):
        return 'icon/folder'

    def poster_get(self, url, user_data):
        return 'special/folder/' + elm.utf8_to_markup(os.path.basename(url)) class EmcDatabase(object):
    """ TODO doc this """

    def __init__(self, name, version=None):
        self._name = name
        self._vers = version
        self._vkey = '__database__version__'
        self._sync_timer = None

        # build the db name (different db for py2 and py3)
        dbname = os.path.join(utils.user_conf_dir,
                              'db_py%d_%s' % (sys.version_info[0], name))
        DBG('Open db: ' + name + ' from file: ' + dbname)

        # check if the db exist (or is the first time we use it)
        first_run = False if glob.glob(dbname + '*') else True

        # open the shelve
        self._sh = shelve.open(dbname)

        if (not first_run) and (version is not None) and (self.get_version() != version):
            # the db is outdated
            text = _(
                '<b>The database %s is outdated!</b><br><br>'
                'The old file has been renamed with a .backup extension and a new (empty) '
                'one has been created.<br><br>'
                'Sorry for the incovenience.') % name
            EmcDialog(style='warning', title=_('EpyMC Database'), text=text)

            # close the shelve
            self._sh.close()

            # rename db files to .backup
            for fname in glob.glob(dbname + '*'):
                os.rename(fname, fname + '.backup')

            # reopen a new (empty) shelve
            self._sh = shelve.open(dbname)

        if version is not None:
            # store the version inside the db
            self._sh[self._vkey] = version

        _instances.append(self)

    def _close(self):
        DBG('Closing database %s' % self._name)
        if self._sync_timer is not None:
            self._sync_timer.delete()
            self._sync_timer = None
        self._sh.close()

    def __len__(self):
        if self._vers:
            return len(self._sh) - 1
        else:
            return len(self._sh)

    def __contains__(self, key):
        return key in self._sh

    def __iter__(self):
        return self.items()

    def items(self):
        for k, v in self._sh.items():
            if k != self._vkey:
                yield k, v

    def keys(self):
        if self._vers:
            return [k for k in self._sh.keys() if k != self._vkey]
        else:
            return self._sh.keys()

    def get_data(self, key):
        DBG('Get Data for db: %s, key: %s' % (self._name, key))
        return self._sh[key]

    def set_data(self, key, data, thread_safe=False):
        DBG('Set data for db: %s, id: %s' % (self._name, key))
        if thread_safe:
            # just put in the queue
            _queue.put((self, key, data))
        else:
            # update the db now
            self._sh[key] = data
            self._delayed_sync()

    def del_data(self, key):
        if key in self._sh:
            del self._sh[key]
            self._delayed_sync()

    def id_exists(self, key):
        return key in self._sh

    def get_version(self):
        if self._vkey in self._sh:
            return self._sh[self._vkey]

    def dump(self):
        import pprint
        print('=' * 60)
        print('DB NAME: "{}" - VERSION: {}'.format(self._name, self._vers))
        print('=' * 60)
        for key in self._sh.keys():
            print('\nDB KEY: "{}"'.format(key))
            pprint.pprint(self._sh[key])
        print('=' * 60)

    def _delayed_sync(self):
        if self._sync_timer is None:
            self._sync_timer = ecore.Timer(5.0, self._sync_timer_cb)
        else:
            self._sync_timer.reset()

    def _sync_timer_cb(self):
        DBG("Syncing database %s" % self._name)
        self._sh.sync()
        self._sync_timer = None
        return ecore.ECORE_CALLBACK_CANCEL class EmcDialog(EmcLayout):
    """ TODO doc this
    style can be 'panel' or 'minimal'

    you can also apply special style that perform specific task:
       'info', 'error', 'warning', 'yesno', 'cancel', 'progress',
       'list', 'image_list_horiz', 'image_list_vert',
       'buffering'
    """

    minimal_styles = ['info', 'error', 'warning', 'yesno', 'cancel', 'progress']
    dialogs_counter = 0

    def __init__(self, title=None, text=None, content=None, spinner=False,
                 style='panel', done_cb=None, canc_cb=None, user_data=None):

        # load the right edje object
        if style in EmcDialog.minimal_styles or style == 'minimal':
            group = 'emc/dialog/minimal'
        elif style == 'buffering':
            group = 'emc/dialog/buffering'
        else:
            group = 'emc/dialog/panel'
        EmcLayout.__init__(self, layout, file=(theme_file, group),
                           focus_allow=True, is_focus_manager=True,
                           size_hint_align=FILL_BOTH,
                           size_hint_weight=EXPAND_BOTH)
        self.signal_callback_add('emc,dialog,close', '', self._close_pressed)
        self.signal_callback_add('emc,dialog,hide,done', '',
                                 (lambda a, s, d: self._delete_real()))
        self.signal_callback_add('emc,dialog,show,done', '',
                                 (lambda a, s, d: None))

        # put the dialog in the dialogs box of the main edje obj,
        # this way we only manage one edje and don't have stacking problems.
        # otherwise dialogs will stole the mouse events.
        box_append('dialogs.box.stack', self)

        EmcDialog.dialogs_counter += 1
        self._name = 'Dialog-' + str(EmcDialog.dialogs_counter)
        self._content = content
        self._done_cb = done_cb
        self._canc_cb = canc_cb
        self._user_data = user_data
        self._list = None
        self._textentry = None
        self._buttons = []

        # give focus to the dialog itself (for dialogs without any focusable)
        if style != 'buffering':
            self.focus = True

        # title
        if title is None:
            self.signal_emit('emc,dialog,title,hide', 'emc')
        else:
            self.part_text_set('emc.text.title', title)
            self.signal_emit('emc,dialog,title,show', 'emc')

        # vbox
        if style != 'buffering':
            self._vbox = elm.Box(self, horizontal=False, size_hint_align=FILL_HORIZ,
                                 size_hint_weight=EXPAND_HORIZ)
            self._vbox.show()
            self.content_set('emc.swallow.content', self._vbox)

        # if both text and content given then put them side by side
        if text and content:
            hbox = elm.Box(self, horizontal=True, size_hint_align=FILL_BOTH,
                           size_hint_weight=EXPAND_BOTH)
            hbox.show()
            self._vbox.pack_end(hbox)

        # text entry
        if text is not None:
            self._textentry = EmcScrolledEntry(parent=self, text=text,
                                               size_hint_weight=EXPAND_BOTH,
                                               size_hint_align=FILL_BOTH)
            self._textentry.show()

            if content:
                hbox.pack_end(self._textentry)
            else:
                self._vbox.pack_end(self._textentry)

        # user content
        if content is not None:
            frame = elm.Frame(self, style='pad_small', size_hint_align=FILL_BOTH,
                              size_hint_weight=EXPAND_BOTH, content=content)
            frame.show()
            if text is not None:
                hbox.pack_start(frame)
            else:
                self._vbox.pack_end(frame)

        # automatic list
        if style in ['list', 'image_list_horiz', 'image_list_vert']:
            self._list = EmcList(self, focus_allow=False,
                                 size_hint_align=FILL_BOTH, size_hint_weight=EXPAND_BOTH,
                                 horizontal=True if style == 'image_list_horiz' else False,
                                 style='dialog' if style == 'list' else 'image_list')
            self._list.callback_activated_add(self._list_item_activated_cb)
            self._list.show()
            self._vbox.pack_end(self._list)

        # spinner
        if spinner:
            self._spinner = elm.Progressbar(self, style='wheel', pulse_mode=True)
            self._spinner.pulse(True)
            self._spinner.show()
            self._vbox.pack_end(self._spinner)

        # set minimal styles + automatic title
        if style in EmcDialog.minimal_styles:
            self.signal_emit('emc,dialog,%s,set' % style, 'emc')
            if title is None:
                self.part_text_set('emc.text.title', style)
                self.signal_emit('emc,dialog,title,show', 'emc')

        # buttons
        if style in ('info', 'error', 'warning'):
            self.button_add(_('Ok'), lambda btn: self.delete())

        if style == 'yesno':
            if self._canc_cb:
                self.button_add(_('No'), lambda btn: self._canc_cb(self))
            else:
                self.button_add(_('No'), lambda btn: self.delete())

            if self._done_cb:
                self.button_add(_('Yes'), lambda btn: self._done_cb(self))
            else:
                self.button_add(_('Yes'), lambda btn: self.delete())

        if style == 'cancel':
            if canc_cb:
                self.button_add(_('Cancel'), lambda btn: self._canc_cb(self))
            else:
                self.button_add(_('Cancel'), lambda btn: self.delete())

        # listen for input events (not for the buffering dialog)
        if style != 'buffering':
            input_events.listener_add(self._name, self._input_event_cb)

        # show
        self.show()
        self.signal_emit('emc,dialog,show', 'emc')

    @staticmethod
    def activate():
        print('DEPRECATED EmcDialog.activate()')

    def delete(self):
        input_events.listener_del(self._name)
        self.signal_emit('emc,dialog,hide', 'emc')
        self.tree_focus_allow = False
        self.focus_allow = False
        for b in self._buttons:
            b.disabled = True

    def _delete_real(self):
        if self._textentry:
            self._textentry.delete()
        for b in self._buttons:
            b.delete()
        content = self.content_unset('emc.swallow.content')
        if content:
            content.delete()
        box_remove('dialogs.box.stack', self)
        elm.Layout.delete(self)
        del self

    def _close_pressed(self, a, s, d):
        if self._canc_cb:
            self._canc_cb(self)
        else:
            self.delete()

    def content_get(self):
        return self._content

    def data_get(self):
        return self._user_data

    def button_add(self, label, selected_cb=None, cb_data=None, icon=None, default=False):
        b = EmcButton(self, label, icon, selected_cb, cb_data)
        self.box_prepend('emc.box.buttons', b)
        self._buttons.append(b)
        if len(self._buttons) == 1:
            self.signal_emit('emc,dialog,buttons,show', 'emc')
            b.focus = True
        if default is True:
            b.focus = True
        return b

    def buttons_clear(self):
        for b in self._buttons:
            b.delete()
        del self._buttons
        self._buttons = []

    def title_set(self, text):
        if text is not None:
            self.part_text_set('emc.text.title', text)
            self.signal_emit('emc,dialog,title,show', 'emc')
        else:
            self.signal_emit('emc,dialog,title,hide', 'emc')

    def title_get(self):
        return self.part_text_get('emc.text.title')

    def text_set(self, text):
        if self._textentry:
            self._textentry.text_set(text)

    def text_get(self):
        return self._textentry.text_get()

    def text_append(self, text):
        self._textentry.text_set(self._textentry.text_get() + text)

    def list_item_append(self, label, icon=None, end=None, *args, **kwargs):
        if self._list:
            if isinstance(end, str) and end.startswith('text/'):
                end = elm.Label(self, style='dia_list', text=end[5:])
            it = self._list.item_append(label, load_icon(icon), load_icon(end), None)
            it.data['_user_item_data_'] = (args, kwargs)
            if not self._list.selected_item_get():
                it.selected = True
            return it

    def list_go(self):
        self._list.go()

    def list_clear(self):
        self._list.clear()

    def list_item_selected_get(self):
        if self._list:
            return self._list.selected_item_get()

    @staticmethod
    def list_item_icon_set(it, icon, end=False):
        it.part_content_set('end' if end else 'icon', load_icon(icon))

    def _list_item_activated_cb(self, li, it):
        if self._done_cb:
            args, kwargs = it.data['_user_item_data_']
            self._done_cb(self, *args, **kwargs)
        else:
            self.delete()

    def spinner_start(self):
        self._spinner.show()
        self._spinner.pulse(True)

    def spinner_stop(self):
        self._spinner.pulse(False)
        self._spinner.hide()

    def progress_set(self, val):
        self.edje.part_external_object_get('emc.dialog.progress').value_set(val)

    def autoscroll_enable(self, speed_scale=1.0, start_delay=3.0):
        self._textentry.autoscroll_start_delay = start_delay
        self._textentry.autoscroll_speed_scale = speed_scale
        self._textentry.autoscroll = True

    def focus_move_internal(self, direction):
        # try to move between list items
        if self._list or isinstance(self._content, (EmcList, EmcGenlist)):
            li = self._list or self._content
            if li.focus_move_internal(direction):
                return True

        # try to scroll the text entry
        if self._textentry:
            if direction == 'UP':
                self._textentry.scroll_by(0, -100)
            if direction == 'DOWN':
                self._textentry.scroll_by(0, +100)

        # try to move between dialog widgets
        self.focus_move(direction)

        # never let the focus goes out of the dialog
        return True

    def _input_event_cb(self, event):

        if not self.visible:
            return input_events.EVENT_CONTINUE

        if event in ['BACK', 'EXIT']:
            if self._canc_cb:
                self._canc_cb(self)
            else:
                self.delete()
            return input_events.EVENT_BLOCK

        if event == 'OK':
            if isinstance(self.focused_object, EmcButton):
                self.focused_object.activate()

            elif self._done_cb:
                if self._list:
                    it = self._list.selected_item
                    self._list_item_activated_cb(self._list, it)
                else:
                    self._done_cb(self)

            else:
                self.delete()

            return input_events.EVENT_BLOCK

        return input_events.EVENT_CONTINUE class EmcImage(elm.Image):
    """ An image object with support for remote url, with optional
        saving of the downloaded image to a local destination and a simple
        cache-to-file mechanism to avoid re-downloading the image again.

       Params:
          url: The url to load the image from, can be one of:
                * a local file fullpath
                * a real remote url
                * a tuple containing (url, dest) (deprecated method ??)
                * 'icon/*' to load an icon (aspect 1:1) from the theme
                * 'image/*' to load an image from the theme
                * 'special/style/text' to create a "special" image, supported
                  styles are: 'folder', 'bd', 'cd', 'icon'
                  The text will be inserted in the image.
                * 'special/vthumb/video_url' to create a thumb of a video file
                * None to "unset" the image
          dest: Local path to save the image to. If the dest path already exists
                the image will not be downloaded, but directly loaded from dest.
                If dest is None the downloaded file will be saved in cache.
          icon: For the special style 'icon', you can here specify the icon
                to swallow inside the special image.
          label2: For the special style 'icon', you can here specify the
                  secondary label text.
    """
    _in_progress_downloads = {}  # key:url  val:[instance1, instance2, ...]

    def __init__(self, url=None, dest=None, icon=None, label2=None,
                 aspect_fixed=True, fill_outside=False, thumb=False):
        self._icon_obj = None
        self._thumb_request_id = None
        elm.Image.__init__(self, layout, aspect_fixed=aspect_fixed,
                           fill_outside=fill_outside, size_hint_expand=EXPAND_BOTH,
                           size_hint_fill=FILL_BOTH)
        self.on_del_add(self._del_cb)
        if url is not None:
            self.url_set(url, dest, icon, label2, thumb)

    def url_set(self, url, dest=None, icon=None, label2=None, thumb=False):
        # None to "unset" the image
        if url is None:
            self.file_set(theme_file, 'emc/image/null')
            return

        # url can also include dest
        if isinstance(url, tuple):
            url, dest = url

        # cancel in progress thumb
        if self._thumb_request_id is not None:
            emc_thumbnailer.cancel_request(self._thumb_request_id)
            self._thumb_request_id = None

        # a remote url ?
        if url.startswith(('http://', 'https://')):
            if dest is None:
                dest = self.cache_path_get(url)
            if os.path.exists(dest):
                self.file_set(dest)
            else:
                self._start_download(url, dest)
            return

        # do we want to use/generate a thumbnail?
        if thumb and not url.startswith(('special/', 'icon/', 'image/')):
            ret = emc_thumbnailer.generate(url, self._thumb_complete_cb)
            if isinstance(ret, str):  # thumb already exists (ret is thumb path)
                self.file_set(ret)
            elif isinstance(ret, int):  # generation started (ret is req_id)
                self._thumb_request_id = ret
                self.file_set(theme_file, 'emc/image/thumbnailing')
            else:  # failed ... this cannot really happend atm
                self.file_set(theme_file, 'emc/image/error')
            return

        # an icon from the theme ?
        if url.startswith(('icon/', 'image/')):
            self.file_set(theme_file, url)
            return

        # a video thumbnail ?
        if url.startswith('special/vthumb/'):
            ret = emc_thumbnailer.generate(url[15:], self._thumb_complete_cb,
                                           frame='vthumb')
            if isinstance(ret, str):  # thumb already exists (ret is thumb path)
                self.file_set(ret)
            elif isinstance(ret, int):  # generation started (ret is req_id)
                self._thumb_request_id = ret
                self.file_set(theme_file, 'emc/image/thumbnailing')
            else:  # failed ... this cannot really happend atm
                self.file_set(theme_file, 'emc/image/error')
            return

        # a special image ?
        if url.startswith('special/'):
            _, style, text = url.split('/', maxsplit=2)
            self.file_set(theme_file, 'emc/image/' + style)
            obj = self.object
            obj.part_text_set('emc.text', text)
            if icon:
                self._icon_obj = EmcImage(icon)
                obj.part_swallow('emc.icon', self._icon_obj)
            if label2:
                obj.part_text_set('emc.text2', label2)
            return

        # a local path ?
        if os.path.exists(url):
            self.file_set(url)
            if self.animated_available:
                self.animated = True
                self.animated_play = True
            return

    def cache_path_get(self, url):
        fname = utils.md5(url) + '.jpg'  # TODO fix extension !
        return os.path.join(utils.user_cache_dir, 'remotes', fname[:2], fname)

    def _thumb_complete_cb(self, status, file, thumb):
        self._thumb_request_id = None
        if self.is_deleted():
            return
        if status is True:
            self.file_set(thumb)
        else:
            self.file_set(theme_file, 'emc/image/error')

    def _start_download(self, url, dest):
        # already downloading the same url ?
        if dest in EmcImage._in_progress_downloads:
            # append ourself in the list of in-progress for this url
            EmcImage._in_progress_downloads[dest].append(self)
            self.file_set(theme_file, 'emc/image/downloading')
            return

        # or start a real download
        dest_tmp = '{}.temp{}'.format(dest, random.randint(100000, 999999))
        try:
            utils.download_url_async(url, dest_tmp, urlencode=False,
                                     complete_cb=self._download_complete_cb)
        except:
            self.file_set(theme_file, 'emc/image/error')
        else:
            # create a new list of in-progress for this url
            EmcImage._in_progress_downloads[dest] = [self, ]
            self.file_set(theme_file, 'emc/image/downloading')

    @staticmethod
    def _download_complete_cb(dest_tmp, status):
        dest = dest_tmp[:-11]  # strip '.tempXXXXXX'
        if status == 200:
            os.rename(dest_tmp, dest)

        if dest in EmcImage._in_progress_downloads:
            for instance in EmcImage._in_progress_downloads[dest]:
                if instance.is_deleted():
                    continue
                if status == 200:
                    instance.file_set(dest)
                else:
                    instance.file_set(theme_file, 'emc/image/error')

            # remove the whole in-progress list for this url
            del EmcImage._in_progress_downloads[dest]

    def _del_cb(self, obj):
        if self._icon_obj:
            self._icon_obj.delete()
            self._icon_obj = None

        if self._thumb_request_id is not None:
            emc_thumbnailer.cancel_request(self._thumb_request_id)
            self._thumb_request_id = None class EmcSourcesManager(EmcDialog):
    """ Open a dialog that allow the user to manage (add/remove) source
    folders. The manager automatically get the folders list from config file,
    using the group passed in the contructor and the 'folders' config item.
    The config item is also automatically updated when finished.

    Args:
       conf_group:
          The name of the config section to read the folders list from.
       title:
          Optional title for the dialog.
       done_cb:
          Function called when the user press the 'done' button.
          Signature: cb(new_folders_list)
    """

    def __init__(self, conf_group, title=_('Sources Manager'), done_cb=None):
        EmcDialog.__init__(self, title, style='list')
        self.button_add(_('Done'), icon='icon/ok',
                        selected_cb=self._cb_btn_done)
        self.button_add(_('Add'), icon='icon/plus',
                        selected_cb=self._cb_btn_add)
        self.button_add(_('Remove'), icon='icon/minus',
                        selected_cb=self._cb_btn_remove)
        self._sources = ini.get_string_list(conf_group, 'folders', ';')
        self._conf_group = conf_group
        self._done_cb = done_cb
        self._populate()

    def _populate(self):
        self.list_clear()
        for src in self._sources:
            self.list_item_append(src, 'icon/folder')
        self.list_go()

    def _cb_btn_add(self, btn):
        EmcFolderSelector(title=_('Choose a new source'), done_cb=self._cb_selected)

    def _cb_btn_remove(self, btn):
        it = self.list_item_selected_get()
        if it and it.text in self._sources:
            self._sources.remove(it.text)
            self._populate()

    def _cb_selected(self, path):
        if path not in self._sources:
            self._sources.append(path)
            self._populate()

    def _cb_btn_done(self, btn):
        ini.set_string_list(self._conf_group, 'folders', self._sources, ';')
        if callable(self._done_cb):
            self._done_cb(self._sources)
        self.delete() class EmcVKeyboard(EmcDialog):
    """ TODOC """

    def __init__(self, accept_cb=None, dismiss_cb=None,
                 title=None, text=None, user_data=None):
        """ TODOC """

        self.accept_cb = accept_cb
        self.dismiss_cb = dismiss_cb
        self.user_data = user_data
        self.buttons = list()

        # choose the correct layout
        self._available_layouts = ini.get_string_list('general', 'keyb_layouts')
        self._current_layout = self._available_layouts[0]  # TODO remember the last one used

        # table
        tb = elm.Table(layout, homogeneous=True)
        tb.show()
        self._table = tb

        # init the parent EmcDialog class
        EmcDialog.__init__(self, title=title, style='minimal', content=tb)

        # set dialog title
        self.part_text_set('emc.text.title', title or _('Insert text'))

        # entry
        self.entry = EmcTextEntry(self, style='vkeyboard', scrollable=True,
                                  single_line=True, editable=True,
                                  context_menu_disabled=True, focus_allow=True,
                                  size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH)
        self.entry.callback_activated_add(self._accept_cb)
        self.entry.callback_aborted_add(self._dismiss_cb)
        if text: self.text_set(text)
        tb.pack(self.entry, 0, 0, 12, 1)
        self.entry.show()

        # numbers + backspace
        for i, c in enumerate('1234567890'):
            self._pack_btn(i, 1, 1, 1, c, cb=self._default_btn_cb)
        self._pack_btn(10, 1, 2, 1, _('ERASE'), cb=self._erase_cb)

        # characters
        self._apply_layout(self._current_layout)

        # last 2 rows
        self._pack_btn(0, 5, 4, 1, keyboard_layouts[self._current_layout][0],
                       cb=self._change_layout_cb)
        self._pack_btn(4, 5, 4, 1, _('SPACE'), cb=self._space_cb)
        self._pack_btn(8, 5, 4, 1, _('UPPERCASE'), cb=self._uppercase_cb)

        self._pack_btn(0, 6, 5, 1, _('Dismiss'), 'icon/cancel', self._dismiss_cb)
        self._pack_btn(5, 6, 1, 1, None, 'icon/arrowL',
                       lambda b: self.entry.cursor_prev())
        self._pack_btn(6, 6, 1, 1, None, 'icon/arrowR',
                       lambda b: self.entry.cursor_next())
        self._pack_btn(7, 6, 5, 1, _('Accept'), 'icon/ok', self._accept_cb)

        # catch input events
        input_events.listener_add('vkbd', self.input_event_cb)
        self.entry.focus = True

    def _pack_btn(self, x, y, w, h, label, icon=None, cb=None):
        b = EmcButton(self, label, icon, cb=cb, size_hint_align=FILL_HORIZ)
        self._table.pack(b, x, y, w, h)
        self.buttons.append(b)
        return b

    def delete(self):
        input_events.listener_del('vkbd')
        EmcDialog.delete(self)

    def text_set(self, text):
        self.entry.text = text
        self.entry.cursor_end_set()

    def _dismiss_cb(self, button):
        if self.dismiss_cb and callable(self.dismiss_cb):
            self.dismiss_cb(self)
        self.delete()

    def _accept_cb(self, button):
        if self.accept_cb and callable(self.accept_cb):
            if self.user_data is not None:
                ret = self.accept_cb(self, self.entry.entry_get(), self.user_data)
            else:
                ret = self.accept_cb(self, self.entry.entry_get())
            if ret is False:
                return
        self.delete()

    def _default_btn_cb(self, button):
        self.entry.entry_insert(button.text)

    def _erase_cb(self, button):
        pos = self.entry.cursor_pos_get()
        if pos > 0:
            text = self.entry.text
            self.entry.text = text[:pos - 1] + text[pos:]
            self.entry.cursor_pos_set(pos - 1)

    def _space_cb(self, button):
        self.entry.entry_insert(' ')

    def _uppercase_cb(self, button):
        if self._current_layout == 'symbols':
            return

        for btn in self.buttons:
            c = btn.text_get()
            if c and len(c) == 1 and c.isalpha():
                if c.islower():
                    btn.text = c.upper()
                    button.text = _('LOWERCASE')
                else:
                    btn.text = c.lower()
                    button.text = _('UPPERCASE')
            elif c and len(c) == 1:
                if c == '(':
                    btn.text = '['
                elif c == ')':
                    btn.text = ']'
                elif c == '[':
                    btn.text = '('
                elif c == ']':
                    btn.text = ')'
                elif c == '.':
                    btn.text = ':'
                elif c == ':':
                    btn.text = '.'
                elif c == ',':
                    btn.text = ';'
                elif c == ';':
                    btn.text = ','
                elif c == '-':
                    btn.text = '_'
                elif c == '_':
                    btn.text = '-'
                elif c == '?':
                    btn.text = '!'
                elif c == '!':
                    btn.text = '?'
                elif c == '+':
                    btn.text = '*'
                elif c == '*':
                    btn.text = '+'
                elif c == '@':
                    btn.text = '/'
                elif c == '/':
                    btn.text = '@'

    def _apply_layout(self, layout):
        for j in range(2, 5):
            for i in range(12):
                old = self._table.child_get(i, j)
                if old:
                    old.delete()

        self._current_layout = layout
        for j in range(3):
            for i, c in enumerate(keyboard_layouts[self._current_layout][j + 1]):
                if c != ' ':
                    self._pack_btn(i, j + 2, 1, 1, c, cb=self._default_btn_cb)

    def _change_layout_cb(self, button):
        new_idx = self._available_layouts.index(self._current_layout) + 1
        if new_idx >= len(self._available_layouts):
            new_idx = 0
        self._apply_layout(self._available_layouts[new_idx])
        button.text = keyboard_layouts[self._current_layout][0]

    def input_event_cb(self, event):
        if event == 'EXIT':
            self._dismiss_cb(None)
            return input_events.EVENT_BLOCK

        return input_events.EVENT_CONTINUE class EmcNotify(edje.Edje):
    """ TODO doc this"""

    def __init__(self, text, icon='icon/star', hidein=5.0, close_cb=None):
        self.timer = None
        self.close_cb = close_cb

        edje.Edje.__init__(self, layout.evas, file=theme_file,
                           group='emc/notify/default')
        self.part_text_set('emc.text.caption', text)
        self._icon = load_image(icon)
        self.part_swallow('emc.swallow.icon', self._icon)
        box_append('notify.box.stack', self)

        if hidein > 0.0:
            self.hidein(hidein)

        self.show()

    def hidein(self, hidein):
        if self.timer:
            self.timer.delete()
        self.timer = ecore.Timer(hidein, self._hide_timer_cb)

    def _hide_timer_cb(self):
        box_remove('notify.box.stack', self)
        self._icon.delete()
        self.delete()
        if callable(self.close_cb):
            self.close_cb()
        return ecore.ECORE_CALLBACK_CANCEL

    def close(self):
        if self.timer:
            self.timer.delete()
        self._hide_timer_cb()

    def text_set(self, text):
        self.part_text_set('emc.text.caption', text) class EmcTagsManager(EmcDialog):
    def __init__(self, tags_db, done_cb=None):
        EmcDialog.__init__(self, _('Tags manager'), style='list',
                           done_cb=self._btn_rename_cb, canc_cb=self._close_cb)
        self.button_add(_('Done'), icon='icon/ok',
                        selected_cb=self._close_cb)
        self.button_add(_('Add'), icon='icon/plus',
                        selected_cb=self._btn_add_cb)
        self.button_add(_('Remove'), icon='icon/minus',
                        selected_cb=self._btn_remove_cb)
        self.button_add(_('Rename'), icon=None,  # TODO find a decent icon
                        selected_cb=self._btn_rename_cb)
        self._tags_db = tags_db
        self._tags_done_cb = done_cb
        self._populate()

    def _populate(self):
        self.list_clear()
        for tag in sorted(self._tags_db.keys()):
            count = len(self._tags_db.get_data(tag))
            txt = ngettext('{} item', '{} items', count).format(count)
            self.list_item_append(tag, 'icon/tag', 'text/' + txt)
        self.list_go()

    def _close_cb(self, *args):
        if callable(self._tags_done_cb):
            self._tags_done_cb()
        self.delete()

    def _btn_add_cb(self, btn):
        EmcVKeyboard(title=_('Type a name for the new tag'),
                     accept_cb=self._add_vkeyb_cb)

    def _add_vkeyb_cb(self, vkeyb, text):
        if not text or '/' in text:
            EmcDialog(style='error', text=_('Invalid tag name'))
            return False  # do not close the keyb

        if not text or text in self._tags_db.keys():
            EmcDialog(style='error', text=_('Tag already exists'))
            return False  # do not close the keyb
        self._tags_db.set_data(text, [])
        self._populate()

    def _btn_remove_cb(self, btn):
        try:
            tag_name = self.list_item_selected_get().text
        except AttributeError:  # no item selected
            return
        txt = _('Are you sure you want to delete the tag?')
        txt += '<br><b>{}</b>'.format(tag_name)
        EmcDialog(style='yesno', title=_('Warning'), text=txt,
                  done_cb=self._remove_confirmed_cb)

    def _remove_confirmed_cb(self, dia):
        tag_name = self.list_item_selected_get().text
        self._tags_db.del_data(tag_name)
        self._populate()
        dia.delete()

    def _btn_rename_cb(self, obj):
        try:
            tag_name = self.list_item_selected_get().text
        except AttributeError:  # no item selected
            return
        EmcVKeyboard(title=_('Type the new name for the tag'), text=tag_name,
                     accept_cb=self._rename_vkeyb_cb)

    def _rename_vkeyb_cb(self, vkeyb, text):
        if not text or '/' in text:
            EmcDialog(style='error', text=_('Invalid tag name'))
            return False  # do not close the keyb

        tag_name = self.list_item_selected_get().text
        if tag_name == text:
            return True  # close the keyb

        if text in self._tags_db.keys():
            EmcDialog(style='error', text=_('Tag already exists'))
            return False  # do not close the keyb

        # create a new tag with the data of the old name and delete the old tag
        data = self._tags_db.get_data(tag_name)
        self._tags_db.set_data(text, data)
        self._tags_db.del_data(tag_name)

        self._populate() class EmcMenu(_EmcFocusable, elm.Ctxpopup):
    """ Dont forget to call show() AFTER all items added """

    def __init__(self, relto=None, dismiss_on_select=True):
        self.dismiss_on_select = dismiss_on_select

        # base popup class
        elm.Ctxpopup.__init__(self, layout, style='emc_menu', direction_priority=(
            elm.ELM_CTXPOPUP_DIRECTION_DOWN, elm.ELM_CTXPOPUP_DIRECTION_UP,
            elm.ELM_CTXPOPUP_DIRECTION_RIGHT, elm.ELM_CTXPOPUP_DIRECTION_LEFT)
                              )
        _EmcFocusable.__init__(self, layout, is_focus_manager=True,
                               focus_allow=False)
        self.callback_dismissed_add(self._dismissed_cb)

        # Scrollable list as content
        self.li = EmcList(self, style='emc_menu', name='MenuList',
                          mode=elm.ELM_LIST_EXPAND)
        self.li.content_min_limit(True, False)
        self.li.callback_activated_add(self._item_activated_cb)
        self.li.on_mouse_move_add(self._mouse_move_cb)
        self.li.on_mouse_down_add(self._mouse_click_cb)
        self.content = self.li

        # adjust popup position
        if relto:
            x, y = relto.center
            self.move(x, y)

        input_events.listener_add('EmcMenu', self._input_event_cb)

    def show(self):
        # adjust list size
        win_w, win_h = win.size
        self.li.size_hint_max = (-1, win_h / 1.5)
        self.li.go()

        # show the popup
        elm.Ctxpopup.show(self)

        # auto select first (not disabled or separator) item
        if self.li.selected_item is None:
            item = self.li.first_item
            while item and item.disabled:
                item = item.next
            if item:
                item.selected = True

        # give focus to the inner list
        self.li.focus = True

    def close(self):
        self.li.focus_allow = False
        self.dismiss()

    def item_add(self, label=None, icon=None, end=None, callback=None, *args, **kargs):
        item = self.li.item_append(label, load_icon(icon), load_icon(end))
        item.data['_user_cb_data_'] = (callback, args, kargs)
        return item

    def item_separator_add(self):
        item = self.li.item_append()
        item.separator = True
        item.disabled = True
        return item

    @staticmethod
    def item_icon_set(item, icon):
        item.content_set(load_icon(icon))

    @staticmethod
    def item_icon_end_set(item, icon):
        item.part_content_set('end', load_icon(icon))

    def _dismissed_cb(self, obj):
        input_events.listener_del('EmcMenu')
        self.delete()
        del self

    def _item_activated_cb(self, obj, item):
        cb, args, kwargs = item.data['_user_cb_data_']
        if callable(cb):
            cb(self, item, *args, **kwargs)
        if self.dismiss_on_select:
            self.close()

    def _mouse_move_cb(self, obj, event):
        item, pos = self.li.at_xy_item_get(*event.position.canvas)
        if item and not item.disabled:
            item.selected = True

    def _mouse_click_cb(self, obj, event):
        self._item_activated_cb(self.li, self.li.selected_item)

    def focus_move_internal(self, direction):
        if direction in ('LEFT', 'RIGHT'):
            return True
        d = self.direction
        if (d == elm.ELM_CTXPOPUP_DIRECTION_DOWN and direction == 'UP') or \
                (d == elm.ELM_CTXPOPUP_DIRECTION_UP and direction == 'DOWN'):
            self.close()
        return True

    def _input_event_cb(self, event):
        if event == 'OK':
            self._item_activated_cb(self.li, self.li.selected_item)
            return input_events.EVENT_BLOCK
        elif event in ('BACK', 'EXIT'):
            self.close()
            return input_events.EVENT_BLOCK
        return input_events.EVENT_CONTINUE class TMDBv3(object):
    """ TMDB API v3

    tmdb = TMDBv3()

    # search for a given movie name + year:
    tmdb.movie_search('Alien', 1979, search_done_cb)
    def search_done_cb(tmdb_obj, results):

    # get movie info + poster + backdrop:
    tmdb.get_movie_info(tmdb_id, info_done_cb, info_progress_cb)
    def info_progress_cb(tmdb, progress):
    def info_done_cb(tmdb, movie_info):

    # get list of all available posters:
    tmdb.get_posters(tmdb_id, list_complete_cb)
    def list_complete_cb(tmdb, posters):

    # get list of all available backdrop:
    tmdb.get_backdrops(tmdb_id, list_complete_cb)
    def list_complete_cb(tmdb, backdrops):

    # get info for a list of casts
    tmdb.get_cast_info(cast_ids, cast_done_cb):
    def cast_done_cb(tmdb, result):

    # abort the current operation:
    tmdb.abort()

    """

    def __init__(self, api_key=TMDB_API_KEY, lang=DEFAULT_INFO_LANG):
        self.key = api_key
        self.lang = lang
        self.base_url = 'http://api.themoviedb.org/3'
        self.base_url_img = 'http://image.tmdb.org/t/p'  # should be queried with /3/configuration
        self.api_handler = None
        self.dwl_handler = None
        self.done_cb = None
        self.progress_cb = None
        self.download_total = 0

    # abort the current operation
    def abort(self):
        if self.dwl_handler:
            utils.download_abort(self.dwl_handler)
            self.dwl_handler = None
        if self.api_handler:
            self.api_handler.delete()
            self.api_handler = None

    # ### api helpers  #########################################################
    def _api_url(self, entry_point, **kargs):
        if 'api_key' not in kargs:
            kargs['api_key'] = self.key
        if 'language' not in kargs:
            kargs['language'] = self.lang
        return self.base_url + entry_point + '?' + urlencode(kargs)

    def _api_call(self, callback, cb_data, entry_point, **kargs):
        url = self._api_url(entry_point, **kargs)
        DBG('TMDB API CALL: ' + url)
        self.api_handler = utils.EmcUrl(url, done_cb=self._api_call_done_cb,
                                        user_callback=callback, user_data=cb_data)

    def _api_call_done_cb(self, url, status, data, user_callback, user_data):
        self.api_handler = None
        api_data = json.loads(data)
        if user_data:
            user_callback(api_data, user_data)
        else:
            user_callback(api_data)

    def _img_url(self, final_part, size):
        if final_part:
            return self.base_url_img + '/' + size + final_part

    # ### movie search #########################################################
    def movie_search(self, name, year, done_cb):
        if year:
            self._api_call(self._movie_search_done, done_cb,
                           '/search/movie', query=name, year=year)
        else:
            self._api_call(self._movie_search_done, done_cb,
                           '/search/movie', query=name)

    def _movie_search_done(self, api_data, done_cb):
        results = []
        for result in api_data['results']:
            try:
                results.append({
                    'tmdb_id': result['id'],
                    'title': result['title'],
                    'year': result['release_date'][:4],
                    'poster_url': self._img_url(result['poster_path'], 'w154')
                })
            except:
                pass
        done_cb(self, results)

    # ### get movie info #######################################################
    def get_movie_info(self, tid, done_cb, progress_cb=None):
        self.done_cb = done_cb
        self.progress_cb = progress_cb

        self._api_call(self._movie_info_done, None,
                       '/movie/%s' % tid, append_to_response='credits')

    def _movie_info_done(self, api_data):

        # create the movie info dict
        try:
            director = [d['name'] for d in api_data['credits']['crew'] if d['job'] == 'Director'][0]
        except:
            director = 'missing'

        try:
            country = api_data['production_countries'][0]['iso_3166_1']
        except:
            country = ''

        try:
            countries = ', '.join([c['iso_3166_1'] for c in api_data['production_countries']])
        except:
            countries = ''

        for person in api_data['credits']['cast']:
            person['profile_path'] = self._img_url(person['profile_path'], 'w154')

        for person in api_data['credits']['crew']:
            person['profile_path'] = self._img_url(person['profile_path'], 'w154')

        self.movie_info = {
            'id': api_data['id'],
            'tmdb_id': api_data['id'],
            'imdb_id': api_data['imdb_id'],
            'title': api_data['title'],
            'adult': api_data['adult'],
            'original_title': api_data['original_title'],
            'release_date': api_data['release_date'],
            'budget': api_data['budget'],
            'overview': api_data['overview'],
            'tagline': api_data['tagline'],
            'rating': api_data['vote_average'],
            'country': country,
            'countries': countries,
            'director': director,
            'cast': api_data['credits']['cast'],
            'crew': api_data['credits']['crew'],
        }

        # queue backdrop, poster and icon
        self.dqueue_images = []

        if api_data['backdrop_path'] is not None:
            self.dqueue_images.append(
                (self._img_url(api_data['backdrop_path'], 'w1280'),
                 get_backdrop_filename(api_data['id'])))
        if api_data['poster_path'] is not None:
            self.dqueue_images.append(
                (self._img_url(api_data['poster_path'], 'w500'),
                 get_poster_filename(api_data['id'])))
            self.dqueue_images.append(
                (self._img_url(api_data['poster_path'], 'w92'),
                 get_icon_filename(api_data['id'])))

        # start the multi download loop
        self.download_total = len(self.dqueue_images) + 1
        self._movie_multi_img_download(None, None)

    def _movie_multi_img_download(self, dest, status):
        self.dwl_handler = None

        # call the progress callback
        if callable(self.progress_cb):
            prog = 1 - (float(len(self.dqueue_images)) / self.download_total)
            self.progress_cb(self, prog)

        # download the next image or end the loop
        if len(self.dqueue_images) > 0:
            url, dest = self.dqueue_images.pop(0)
            DBG("getting img: " + url)
            self.dwl_handler = utils.download_url_async(url, dest, urlencode=False,
                                                        complete_cb=self._movie_multi_img_download)
        else:
            self._movie_info_all_done()

    def _movie_info_all_done(self):
        if callable(self.progress_cb):
            self.progress_cb(self, 1.0)
        self.done_cb(self, self.movie_info)

    # ### tv search ############################################################
    def tv_search(self, name, done_cb):
        self._api_call(self._tv_search_done, done_cb,
                       '/search/tv', query=name)

    def _tv_search_done(self, api_data, done_cb):
        results = []
        for result in api_data['results']:
            try:
                results.append({
                    'tmdb_id': result['id'],
                    'name': result['name'],
                    'year': result['first_air_date'][:4] if result['first_air_date'] else None,
                    'poster_url': self._img_url(result['poster_path'], 'w154')
                })
            except:
                pass
        done_cb(self, results)

    # ### get tv info  #########################################################
    def get_tv_info(self, tid, done_cb, progress_cb=None):
        self.done_cb = done_cb
        self.progress_cb = progress_cb
        self.dqueue_images = []
        self.dqueue_seasons = []
        self.dqueue_seasons_fb = []
        self.dqueue_total = 0
        self._api_call(self._tv_info_done, tid,
                       '/tv/%s' % tid, append_to_response='credits')

    def _tv_info_progress_report(self):
        if callable(self.progress_cb):
            if self.dqueue_total > 0:
                remaining = len(self.dqueue_images) + len(self.dqueue_seasons)
                prog = 1.0 - (remaining / float(self.dqueue_total))
                self.progress_cb(self, prog)
            else:
                self.progress_cb(self, 0.0)

    def _tv_info_done(self, data, tid=None):
        self._tv_info_progress_report()

        # fallback to english if needed
        if not data['overview'] and self.lang != 'en' and tid is not None:
            self._api_call(self._tv_info_done, None, '/tv/%s' % tid,
                           language='en', append_to_response='credits')
            return

        for person in data['credits']['cast']:
            person['profile_path'] = self._img_url(person['profile_path'], 'w154')

        for person in data['credits']['crew']:
            person['profile_path'] = self._img_url(person['profile_path'], 'w154')

        # build main tvshow info dict
        self.tv_info = {
            'id': data['id'],
            'name': data['original_name'],
            'overview': data['overview'],
            'created_by': ', '.join([c['name'] for c in data['created_by']]),
            'episode_run_time': ', '.join(map(str, data['episode_run_time'])),
            'first_air_date': data['first_air_date'],
            'last_air_date': data['last_air_date'],
            'genres': ', '.join([g['name'] for g in data['genres']]),
            'networks': ', '.join([n['name'] for n in data['networks']]),
            'number_of_episodes': data['number_of_episodes'],
            'number_of_seasons': data['number_of_seasons'],
            'country': data['origin_country'],
            'status': data['status'],
            'vote_average': data['vote_average'],
            'vote_count': data['vote_count'],
            'cast': data['credits']['cast'],
            'crew': data['credits']['crew'],
            'seasons': {}
        }

        # queue backdrop, poster and icon
        if data['backdrop_path']:
            self.dqueue_images.append(
                (self._img_url(data['backdrop_path'], 'w1280'),
                 get_tv_backdrop_filename(data['id'])))

        if data['poster_path']:
            self.dqueue_images.append(
                (self._img_url(data['poster_path'], 'w500'),
                 get_tv_poster_filename(data['id'])))
            self.dqueue_images.append(
                (self._img_url(data['poster_path'], 'w92'),
                 get_tv_icon_filename(data['id'])))

        for s in data['seasons']:
            # queue posters and icons for each season
            if s['poster_path']:
                self.dqueue_images.append(
                    (self._img_url(s['poster_path'], 'w92'),
                     get_tv_icon_filename(data['id'], s['season_number'])))

                self.dqueue_images.append(
                    (self._img_url(s['poster_path'], 'w500'),
                     get_tv_poster_filename(data['id'], s['season_number'])))

            # queue the season to retrive data
            self.dqueue_seasons.append(s['season_number'])

        # start the download of all the images and all the seasons info
        self.dqueue_total = len(self.dqueue_images) + len(self.dqueue_seasons)
        self._tv_multi_img_download(None, None)
        self._tv_multi_season_api(None)

    def _tv_multi_season_api(self, data):
        self._tv_info_progress_report()

        if (data is not None) and (len(data['episodes']) > 0):
            missing_translation = False

            # store the info for this season
            season = {
                'id': data['id'],
                'season_num': data['season_number'],
                'first_air_date': data['air_date'],
                'overview': data['overview'],
                'episodes': {},
            }

            if not data['overview'] and self.lang != 'en':
                missing_translation = True

            # store info for each episode
            for e in data['episodes']:
                season['episodes'][e['episode_number']] = {
                    'id': e['id'],
                    'series_id': self.tv_info['id'],
                    'title': e['name'],
                    'overview': e['overview'],
                    'first_aired': e['air_date'],
                    'episode_num': e['episode_number'],
                    'season_num': season['season_num'],
                    'director': ['TODO'],  # TODO
                    'writer': ['TODO'],  # TODO
                    'thumb_url': self._img_url(e['still_path'], 'w300')
                }

                if (not e['overview'] or not e['name']) and self.lang != 'en':
                    missing_translation = True

            self.tv_info['seasons'][data['season_number']] = season

            if missing_translation:
                self.dqueue_seasons_fb.append(data['season_number'])

        # get the next season if available or end the loop
        if len(self.dqueue_seasons) > 0:
            season = self.dqueue_seasons.pop(0)
            self._api_call(self._tv_multi_season_api, None,
                           '/tv/%d/season/%d' % (self.tv_info['id'], season))
        else:
            self._tv_multi_season_fallback_api(None)

    def _tv_multi_season_fallback_api(self, data):
        self._tv_info_progress_report()
        if (data is not None) and (len(data['episodes']) > 0):
            season = self.tv_info['seasons'][data['season_number']]

            if not season['overview']:
                season['overview'] = data['overview']

            for e in data['episodes']:
                ep = season['episodes'][e['episode_number']]
                if not ep['title']: ep['title'] = e['name']
                if not ep['overview']: ep['overview'] = e['overview']

        # get the next fallback season if available or end the loop
        if len(self.dqueue_seasons_fb) > 0:
            season = self.dqueue_seasons_fb.pop(0)
            self._api_call(self._tv_multi_season_fallback_api, None,
                           '/tv/%d/season/%d' % (self.tv_info['id'], season),
                           language='en')
        else:
            self._tv_info_all_done()

    def _tv_multi_img_download(self, dest, status):
        self._tv_info_progress_report()
        self.dwl_handler = None
        if len(self.dqueue_images) > 0:
            url, dest = self.dqueue_images.pop(0)
            DBG("getting img: " + url)
            self.dwl_handler = utils.download_url_async(url, dest, urlencode=False,
                                                        complete_cb=self._tv_multi_img_download)
        else:
            self._tv_info_all_done()

    def _tv_info_all_done(self):
        if (len(self.dqueue_images) + len(self.dqueue_seasons) + len(self.dqueue_seasons_fb)) > 0:
            return

        self._tv_info_progress_report()
        self.done_cb(self, self.tv_info)

    # ### posters list #########################################################
    def get_posters(self, tmdb_id, done_cb, tv=False):
        langs = '%s,en,null' % self.lang
        entry_point = 'tv' if tv else 'movie'
        self._api_call(self._poster_list_done, done_cb,
                       '/%s/%s/images' % (entry_point, tmdb_id),
                       include_image_language=langs)

    def _poster_list_done(self, api_data, done_cb):
        results = []
        for poster in api_data['posters']:
            results.append({
                'movie_id': api_data['id'],
                'icon_url': self._img_url(poster['file_path'], 'w92'),
                'thumb_url': self._img_url(poster['file_path'], 'w342'),
                'url': self._img_url(poster['file_path'], 'w500'),
                'lsort': 1 if poster['iso_639_1'] == self.lang else 2
            })
        results.sort(key=itemgetter('lsort'))
        done_cb(self, results)

    # ### backdrops list #######################################################
    def get_backdrops(self, tmdb_id, done_cb, tv=False):
        langs = '%s,en,null' % self.lang
        entry_point = 'tv' if tv else 'movie'
        self._api_call(self._backdrops_list_done, done_cb,
                       '/%s/%s/images' % (entry_point, tmdb_id),
                       include_image_language=langs)

    def _backdrops_list_done(self, api_data, done_cb):
        results = []
        for backdrop in api_data['backdrops']:
            results.append({
                'movie_id': api_data['id'],
                'thumb_url': self._img_url(backdrop['file_path'], 'w300'),
                'url': self._img_url(backdrop['file_path'], 'w1280'),
                'lsort': 1 if backdrop['iso_639_1'] == self.lang else 2
            })
        results.sort(key=itemgetter('lsort'))
        done_cb(self, results)

    # ### get cast info ########################################################
    def person_search(self, name, done_cb):
        self._api_call(self._person_search_done, done_cb,
                       '/search/person', query=name)

    def _person_search_done(self, api_data, done_cb):
        # TODO adjust image urls
        done_cb(self, api_data)

    def get_cast_info(self, cast_id, done_cb):
        self._api_call(self._cast_info_done, done_cb,
                       '/person/%s' % cast_id, append_to_response='credits,images')

    def _cast_info_done(self, api_data, done_cb):
        # adjust image urls
        api_data['profile_path'] = self._img_url(api_data['profile_path'], 'h632')
        for img in api_data['images']['profiles']:
            img['file_path'] = self._img_url(img['file_path'], 'h632')
        for movie in api_data['credits']['cast']:
            movie['poster_path'] = self._img_url(movie['poster_path'], 'w154')
        for movie in api_data['credits']['crew']:
            movie['poster_path'] = self._img_url(movie['poster_path'], 'w154')

        done_cb(self, api_data) class CastPanel(EmcDialog):
    def __init__(self, pid=None, name=None, lang=DEFAULT_INFO_LANG):
        self.pid = pid
        self.info = None

        tmdb = TMDBv3(lang=lang)
        if name:
            tmdb.person_search(name, self._search_done_cb)
        elif pid:
            tmdb.get_cast_info(self.pid, self._fetch_done_cb)
        self._dia = EmcDialog(style='minimal', title=_('Fetching info'),
                              content=EmcImage('image/tmdb_logo.png'),
                              spinner=True)

    def _search_done_cb(self, tmdb, result):
        # TODO manage errors
        self.pid = result['results'][0]['id']
        tmdb.get_cast_info(self.pid, self._fetch_done_cb)

    def _fetch_done_cb(self, tmdb, result):
        self.info = result
        self._dia.delete()

        text = '<title>%s</><br>' % self.info['name']
        if self.info.get('biography'):
            text += '%s<br><br>' % self.info['biography'].replace('\n', '<br>')
        if self.info.get('birthday'):
            text += '<name>%s:</> %s<br>' % (_('Birthday'), self.info['birthday'])
        if self.info.get('deathday'):
            text += '<name>%s:</> %s<br>' % (_('Deathday'), self.info['deathday'])
        if self.info.get('place_of_birth'):
            text += '<name>%s:</> %s<br>' % (_('Place of birth'), self.info['place_of_birth'])

        image = EmcImage(self.info['profile_path'])
        EmcDialog.__init__(self, title=self.info['name'], style='panel',
                           content=image, text=text)

        # merge cast and crew in a single movies list
        movies = {}  # key: tmdbid val: tmdb-data-dict + 'jobs'
        for movie in self.info['credits']['crew']:
            tid = movie['id']
            if tid in movies:
                movies[tid]['jobs'].append(movie.get('job'))
            else:
                movie['jobs'] = [movie.get('job')]
                movies[tid] = movie

        for movie in self.info['credits']['cast']:
            tid = movie['id']
            if tid in movies:
                movies[tid]['character'] = movie.get('character')
            else:
                movies[tid] = movie

        self.button_add(_('Movies (%s)') % len(movies),
                        lambda b: self.movies_dialog(movies))
        self.button_add(_('Photos (%s)') % len(self.info['images']['profiles']),
                        lambda b: self.photos_dialog())

    def photos_dialog(self):
        dia = EmcDialog(style='image_list_horiz', title=self.info['name'])
        for image in self.info['images']['profiles']:
            img = EmcImage(image['file_path'])
            dia.list_item_append(None, img)
        dia.list_go()

    def movies_dialog(self, movies):
        dia = EmcDialog(style='list', title=self.info['name'])
        for movie in sorted(movies.values(), key=itemgetter('title')):
            label = '<big>{}</big>'.format(movie['title'])
            if 'character' in movie:
                label += ' <i>{} <big>{}</big></i>'.format(_('as'), movie['character'])
            if 'jobs' in movie:
                label += ' <i>({})</i>'.format(', '.join(movie['jobs']))
            if movie.get('poster_path'):
                icon = EmcImage(movie.get('poster_path'))
                icon.size_hint_min_set(100, 100)  # TODO FIXME
            else:
                icon = None
            dia.list_item_append(label, icon)
        dia.list_go() def get_poster_filename(movie_id):
    return os.path.join(utils.user_conf_dir, 'movies', str(movie_id), 'poster.jpg') def get_backdrop_filename(movie_id):
    return os.path.join(utils.user_conf_dir, 'movies', str(movie_id), 'backdrop.jpg') def get_icon_filename(movie_id):
    return os.path.join(utils.user_conf_dir, 'movies', str(movie_id), 'icon.jpg')
import os, re, time
import epymc.mainmenu as mainmenu
import epymc.mediaplayer as mediaplayer
import epymc.ini as ini
import epymc.utils as utils
import epymc.gui as gui
import epymc.events as events
import epymc.config_gui as config_gui
from operator import itemgetter
from efl import ecore, evas, elementary, emotion
from efl.elementary.image import Image
from efl.elementary.entry import utf8_to_markup
from epymc.modules import EmcModule
from epymc.browser import EmcBrowser, EmcItemClass, FolderItemClass
from epymc.sdb import EmcDatabase
from epymc.gui import EmcDialog, EmcImage, EmcSourcesManager, \
    EmcVKeyboard, EmcNotify, EmcTagsManager, EmcMenu
from epymc.themoviedb import TMDBv3, CastPanel, get_poster_filename, \
    get_backdrop_filename, get_icon_filename
            dia.list_go()

    @staticmethod
    def _cast_info_done_cb(list_dia, pid):
        CastPanel(pid, lang=ini.get('movies', 'info_lang'))

    # ####### Choose poster
    def _cb_panel_3(self, button):
        if self._movie_db.id_exists(self._current_url):
            movie_info = self._movie_db.get_data(self._current_url)
            tmdb = TMDBv3(lang=ini.get('movies', 'info_lang'))
            tmdb.get_posters(movie_info['tmdb_id'], self._cb_posters_list_complete)

    def _cb_posters_list_complete(self, tmdb, posters):
        title = _('%d posters available') % (len(posters))
        dialog = EmcDialog(style='image_list_horiz', title=title,
                           done_cb=self._cb_posters_list_ok)
        for poster in posters:
            icon = EmcImage(poster['thumb_url'])
            dialog.list_item_append(None, icon, poster=poster)
        dialog.list_go()

    def _cb_posters_list_ok(self, dialog, poster):
        dest = get_poster_filename(poster['movie_id'])
        utils.download_url_async(poster['url'], dest,
                                 complete_cb=self._cb_image_done,
                                 progress_cb=self._cb_image_progress)

        # kill the list dialog
        dialog.delete()
        del dialog

        # show a progress dialog
        self._poster_dialog = EmcDialog(title=_('Downloading image'),
                                        style='progress',
                                        content=gui.load_image('tmdb_logo.png'))

    def _cb_image_progress(self, dest, tot, done):
        if tot > 0: self._poster_dialog.progress_set(float(done) / float(tot))

    def _cb_image_done(self, dest, status):
        # kill the dialog
        self._poster_dialog.delete()
        del self._poster_dialog
        # update browser and info panel
        self.update_movie_info(self._current_url)
        self._browser.refresh()

    # ####### Choose fanart
    def _cb_panel_4(self, button):
        if self._movie_db.id_exists(self._current_url):
            movie_info = self._movie_db.get_data(self._current_url)
            tmdb = TMDBv3()
            tmdb.get_backdrops(movie_info['tmdb_id'], self._cb_backdrops_list_complete)

    def _cb_backdrops_list_complete(self, tmdb, backdrops):

        title = _('%d backdrops available') % (len(backdrops))
        dialog = EmcDialog(style='image_list_vert', title=title,
                           done_cb=self._cb_backdrops_list_ok)
        for backdrop in backdrops:
            img = EmcImage(backdrop['thumb_url'])
            dialog.list_item_append(None, img, backdrop=backdrop)
        dialog.list_go()

    def _cb_backdrops_list_ok(self, dialog, backdrop):
        # download the selected backdrop
        dest = get_backdrop_filename(backdrop['movie_id'])
        utils.download_url_async(backdrop['url'], dest,
                                 complete_cb=self._cb_image_done,
                                 progress_cb=self._cb_image_progress)

        # kill the list dialog
        dialog.delete()
        del dialog

        # show a progress dialog
        self._poster_dialog = EmcDialog(title=_('Downloading image'),
                                        style='progress',
                                        content=gui.load_image('tmdb_logo.png'))

    # ####### Get movie info from themoviedb.org
    def _cb_panel_5(self, button):
        name, year = get_movie_name_from_url(self._current_url)
        self._do_movie_search(name, year)

    def _do_movie_search(self, name, year):
        tmdb = TMDBv3(lang=ini.get('movies', 'info_lang'))
        tmdb.movie_search(name, year, self._cb_search_done)
        search = _('Searching for')
        if year:
            text = '<b>%s:</b><br>%s (%s)<br>' % (search, name, year)
        else:
            text = '<b>%s:</b><br>%s<br>' % (search, name)

        self.tmdb_dialog = EmcDialog(title='themoviedb.org',
                                     style='progress', text=text,
                                     user_data=tmdb)
        self.tmdb_dialog.button_add(_('Change name'),
                                    lambda b: EmcVKeyboard(text=name, accept_cb=self._vkbd_cb))

    def _vkbd_cb(self, vkbd, txt):
        if self.tmdb_dialog: self.tmdb_dialog.delete()
        self._do_movie_search(txt, None)

    def _cb_search_done(self, tmdb, results):
        if len(results) == 0:
            self.tmdb_dialog.text_append(
                '<br>' + _('nothing found, please try with a better name'))  # TODO explain better the format
        elif len(results) == 1:
            tmdb.get_movie_info(results[0]['tmdb_id'],
                                self._cb_info_done, self._cb_info_progress)
            self.tmdb_dialog.text_append('<b>%s</b>' % _('Downloading movie info...'))
        else:
            self.tmdb_dialog.text_append(_('<b>Found %d results</b><br>') % (len(results)))
            title = _('Found %d results, which one?') % (len(results))
            dialog2 = EmcDialog(title=title, style='list',
                                done_cb=self._cb_list_ok,
                                canc_cb=self._cb_list_cancel)
            for res in results:

                icon = EmcImage(res['poster_url'])