# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2014 Thomas Perl and the gPodder Team
#
# gPodder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
 
# gpodder.qmlui - gPodder's QML interface
# Thomas Perl <thp@gpodder.org>; 2011-02-06
 
 
from PySide.QtGui import QApplication
from PySide.QtCore import Qt, QObject, Signal, Slot, Property, QUrl
from PySide.QtCore import QAbstractListModel, QModelIndex
from PySide.QtDeclarative import QDeclarativeView
 
import os
import signal
import functools
import itertools
import subprocess
 
import dbus
import dbus.service
 
from dbus.mainloop.glib import DBusGMainLoop
 
 
import gpodder
 
_ = gpodder.gettext
N_ = gpodder.ngettext
 
from gpodder import core
from gpodder import util
from gpodder import my
from gpodder import query
from gpodder import common
 
from gpodder.model import Model
 
from gpodder.qmlui import model
from gpodder.qmlui import helper
from gpodder.qmlui import images
 
import logging
logger = logging.getLogger("qmlui")
 
 
EPISODE_LIST_FILTERS = [
    # (UI label, EQL expression)
    (_('All'), None),
    (_('Hide deleted'), 'not deleted'),
    (_('New'), 'new or downloading'),
    (_('Downloaded'), 'downloaded or downloading'),
    (_('Deleted'), 'deleted'),
    (_('Finished'), 'finished'),
    (_('Archived'), 'downloaded and archive'),
    (_('Videos'), 'video'),
    (_('Partially played'), 'downloaded and played and not finished'),
    (_('Unplayed downloads'), 'downloaded and not played'),
]
 
EPISODE_LIST_LIMIT = 200
 
def grouped(iterable, n):
    return itertools.izip(*[iter(iterable)]*n)
 
class ConfigProxy(QObject):
    def __init__(self, config):
        QObject.__init__(self)
        self._config = config
 
        config.add_observer(self._on_config_changed)
 
    def _on_config_changed(self, name, old_value, new_value):
        if name == 'ui.qml.autorotate':
            self.autorotateChanged.emit()
        elif name == 'flattr.token':
            self.flattrTokenChanged.emit()
        elif name == 'flattr.flattr_on_play':
            self.flattrOnPlayChanged.emit()
 
    def get_autorotate(self):
        return self._config.ui.qml.autorotate
 
    def set_autorotate(self, autorotate):
        self._config.ui.qml.autorotate = autorotate
 
    autorotateChanged = Signal()
 
    autorotate = Property(bool, get_autorotate, set_autorotate,
            notify=autorotateChanged)
 
    def get_flattr_token(self):
        return self._config.flattr.token
 
    def set_flattr_token(self, flattr_token):
        self._config.flattr.token = flattr_token
 
    flattrTokenChanged = Signal()
 
    flattrToken = Property(unicode, get_flattr_token, set_flattr_token,
            notify=flattrTokenChanged)
 
    def get_flattr_on_play(self):
        return self._config.flattr.flattr_on_play
 
    def set_flattr_on_play(self, flattr_on_play):
        self._config.flattr.flattr_on_play = flattr_on_play
 
    flattrOnPlayChanged = Signal()
 
    flattrOnPlay = Property(bool, get_flattr_on_play, set_flattr_on_play,
            notify=flattrOnPlayChanged)
 
 
class Controller(QObject):
    def __init__(self, root):
        QObject.__init__(self)
        self.root = root
        self.context_menu_actions = []
        self.episode_list_title = u''
        self.current_input_dialog = None
        self.root.config.add_observer(self.on_config_changed)
        self._flattr = self.root.core.flattr
        self.flattr_button_text = u''
        self._busy = False
        self.updating_podcasts = 0
        self.current_episode = None
        self._subscribe_in_progress = False
 
 
    # Upcalls to the GUI
    episodeUpdated = Signal(int)
 
    showMessage = Signal(unicode)
    showInputDialog = Signal(unicode, unicode, unicode, unicode, bool)
    openContextMenu = Signal('QVariant')
 
    startProgress = Signal(unicode)
    endProgress = Signal()
 
    clearEpisodeListModel = Signal()
    setEpisodeListModel = Signal()
 
    enqueueEpisode = Signal(QObject)
    removeQueuedEpisode = Signal(QObject)
    removeQueuedEpisodesForPodcast = Signal(QObject)
 
    shutdown = Signal()
 
 
    def on_config_changed(self, name, old_value, new_value):
        logger.info('Config changed: %s (%s -> %s)', name,
                old_value, new_value)
        if name == 'mygpo.enabled':
            self.myGpoEnabledChanged.emit()
        elif name == 'mygpo.username':
            self.myGpoUsernameChanged.emit()
        elif name == 'mygpo.password':
            self.myGpoPasswordChanged.emit()
        elif name == 'mygpo.device.caption':
            self.myGpoDeviceCaptionChanged.emit()
 
    busyChanged = Signal()
 
    def getBusy(self):
        return self._busy
 
    def setBusy(self, busy):
        if self._busy != busy:
            self._busy = busy
            self.busyChanged.emit()
 
    busy = Property(bool, getBusy, setBusy, notify=busyChanged)
 
    episodeListTitleChanged = Signal()
 
    def setEpisodeListTitle(self, title):
        if self.episode_list_title != title:
            self.episode_list_title = title
            self.episodeListTitleChanged.emit()
 
    def getEpisodeListTitle(self):
        return self.episode_list_title
 
    episodeListTitle = Property(unicode, getEpisodeListTitle, \
            setEpisodeListTitle, notify=episodeListTitleChanged)
 
    flattrButtonTextChanged = Signal()
 
    def setFlattrButtonText(self, flattr_button_text):
        if self.flattr_button_text != flattr_button_text:
            self.flattr_button_text = flattr_button_text
            self.flattrButtonTextChanged.emit()
 
    def getFlattrButtonText(self):
        return self.flattr_button_text
 
    flattrButtonText = Property(unicode, getFlattrButtonText,
            setFlattrButtonText, notify=flattrButtonTextChanged)
 
    @Slot(QObject)
    def onPlayback(self, qepisode):
        if (qepisode.payment_url and self.root.config.flattr.token and
                self.root.config.flattr.flattr_on_play):
            success, message = self._flattr.flattr_url(qepisode.payment_url)
            if not success:
                logger.warn('Flattr message on playback action: %s', message)
 
    @Slot(QObject)
    def updateFlattrButtonText(self, qepisode):
        self.setFlattrButtonText('')
 
        if qepisode is None:
            return
 
        episode = qepisode._episode
 
        if not episode.payment_url:
            return
        if not self._flattr.has_token():
            self.setFlattrButtonText(_('Sign in'))
            return
 
        @util.run_in_background
        def get_flattr_info():
            flattrs, flattred = self._flattr.get_thing_info(episode.payment_url)
 
            if flattred:
                self.setFlattrButtonText(_('Flattred (%(count)d)') % {
                    'count': flattrs
                })
            else:
                self.setFlattrButtonText(_('Flattr this (%(count)d)') % {
                    'count': flattrs
                })
 
    @Slot(QObject)
    def flattrEpisode(self, qepisode):
        if not qepisode:
            return
 
        episode = qepisode._episode
 
        if not episode.payment_url:
            return
        if not self._flattr.has_token():
            self.showMessage.emit(_('Sign in to Flattr in the settings.'))
            return
 
        self.startProgress.emit(_('Flattring episode...'))
 
        @util.run_in_background
        def flattr_episode():
            try:
                success, message = self._flattr.flattr_url(episode.payment_url)
                if success:
                    self.updateFlattrButtonText(qepisode)
                else:
                    self.showMessage.emit(message)
            finally:
                self.endProgress.emit()
 
    @Slot(result=str)
    def getFlattrLoginURL(self):
        return self._flattr.get_auth_url()
 
    @Slot(result=str)
    def getFlattrCallbackURL(self):
        return self._flattr.CALLBACK
 
    @Slot(str)
    def processFlattrCode(self, url):
        if not self._flattr.process_retrieved_code(url):
            self.showMessage.emit(_('Could not log in to Flattr.'))
 
    @Slot(result='QStringList')
    def getEpisodeListFilterNames(self):
        return [caption for caption, eql in EPISODE_LIST_FILTERS]
 
    @Slot('QVariant', str)
    def multiEpisodeAction(self, selected, action):
        if not selected:
            return
 
        count = len(selected)
        # JS/QML/PySide might convert the array of indices to an array of floats,
        # so we need to convert these floats back to integers (bug 1802)
        selected = map(int, selected)
        episodes = map(lambda idx: self.root.episode_model._filtered[idx], selected)
 
        def delete():
            for episode in episodes:
                if not episode.qarchive:
                    episode.delete_episode()
                    self.episodeUpdated.emit(episode.id)
            self.update_subset_stats()
            self.root.mygpo_client.on_delete(episodes)
            self.root.mygpo_client.flush()
            for episode in episodes:
                self.removeQueuedEpisode.emit(episode)
 
            util.run_in_background(self.root.episode_model.sort)
 
        if action == 'delete':
            msg = N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count) % {'count':count}
            self.confirm_action(msg, _('Delete'), delete)
        elif action == 'download':
            for episode in episodes:
                if episode.qdownloaded:
                    print '    XXX     already downloaded'
                    continue
                self.downloadEpisode(episode)
            self.root.mygpo_client.on_download(episodes)
            self.root.mygpo_client.flush()
        elif action == 'play':
            for episode in episodes:
                self.enqueueEpisode.emit(episode)
                self.episodeUpdated.emit(episode.id)
 
    @Slot(str, result=str)
    def translate(self, x):
        return _(x)
 
    @Slot(str, str, int, result=str)
    def ntranslate(self, singular, plural, count):
        return N_(singular, plural, count)
 
    @Slot(str, int, result=str)
    def formatCount(self, template, count):
        return template % {'count': count}
 
    @Slot(result=str)
    def getVersion(self):
        return gpodder.__version__
 
    @Slot(result=str)
    def getReleased(self):
        return gpodder.__date__
 
    @Slot(result=unicode)
    def getCredits(self):
        credits_file = os.path.join(gpodder.prefix, 'share', 'gpodder', 'credits.txt')
        return util.convert_bytes(open(credits_file).read())
 
    @Slot(result=unicode)
    def getCopyright(self):
        return util.convert_bytes(gpodder.__copyright__)
 
    @Slot(result=str)
    def getLicense(self):
        return gpodder.__license__
 
    @Slot(result=str)
    def getURL(self):
        return gpodder.__url__
 
    @Slot()
    def loadLastEpisode(self):
        self.root.load_last_episode()
 
    @Slot(QObject, int, int)
    def storePlaybackAction(self, episode, start, end):
        self.episodeUpdated.emit(episode.id)
        if end - 5 < start:
            logger.info('Ignoring too short playback action.')
            return
        total = episode.qduration
        self.root.mygpo_client.on_playback_full(episode, start, end, total)
        self.root.mygpo_client.flush()
 
    @Slot(QObject)
    def playVideo(self, episode):
        """Video Playback on MeeGo 1.2 Harmattan"""
        if episode.qnew:
            episode.toggle_new()
            self.update_subset_stats()
 
        url = episode.get_playback_url()
        if gpodder.ui.harmattan:
            subprocess.Popen(['video-suite', url])
        else:
            util.gui_open(url)
 
        self.root.mygpo_client.on_playback([episode])
        self.root.mygpo_client.flush()
 
    @Slot(QObject)
    def podcastSelected(self, podcast):
        self.setEpisodeListTitle(podcast.qtitle)
        util.run_in_background(lambda: self.root.select_podcast(podcast))
 
    windowTitleChanged = Signal()
 
    def getWindowTitle(self):
        return self.root.view.windowTitle()
 
    def setWindowTitle(self, windowTitle):
        self.root.view.setWindowTitle(windowTitle)
 
    windowTitle = Property(unicode, getWindowTitle,
            setWindowTitle, notify=windowTitleChanged)
 
    @Slot()
    def myGpoUploadList(self):
        def upload_proc(self):
            self.startProgress.emit(_('Uploading subscriptions...'))
 
            try:
                try:
                    self.root.mygpo_client.set_subscriptions([podcast.url
                        for podcast in self.root.podcast_model.get_podcasts()])
                except Exception, e:
                    self.showMessage.emit('\n'.join((_('Error on upload:'), unicode(e))))
            finally:
                self.endProgress.emit()
 
        util.run_in_background(lambda: upload_proc(self))
 
    @Slot()
    def saveMyGpoSettings(self):
        # Update the device settings and upload changes
        self.root.mygpo_client.create_device()
        self.root.mygpo_client.flush(now=True)
 
    myGpoEnabledChanged = Signal()
 
    def getMyGpoEnabled(self):
        return self.root.config.mygpo.enabled
 
    def setMyGpoEnabled(self, enabled):
        self.root.config.mygpo.enabled = enabled
 
    myGpoEnabled = Property(bool, getMyGpoEnabled,
            setMyGpoEnabled, notify=myGpoEnabledChanged)
 
    myGpoUsernameChanged = Signal()
 
    def getMyGpoUsername(self):
        return model.convert(self.root.config.mygpo.username)
 
    def setMyGpoUsername(self, username):
        self.root.config.mygpo.username = username
 
    myGpoUsername = Property(unicode, getMyGpoUsername,
            setMyGpoUsername, notify=myGpoUsernameChanged)
 
    myGpoPasswordChanged = Signal()
 
    def getMyGpoPassword(self):
        return model.convert(self.root.config.mygpo.password)
 
    def setMyGpoPassword(self, password):
        self.root.config.mygpo.password = password
 
    myGpoPassword = Property(unicode, getMyGpoPassword,
            setMyGpoPassword, notify=myGpoPasswordChanged)
 
    myGpoDeviceCaptionChanged = Signal()
 
    def getMyGpoDeviceCaption(self):
        return model.convert(self.root.config.mygpo.device.caption)
 
    def setMyGpoDeviceCaption(self, caption):
        self.root.config.mygpo.device.caption = caption
 
    myGpoDeviceCaption = Property(unicode, getMyGpoDeviceCaption,
            setMyGpoDeviceCaption, notify=myGpoDeviceCaptionChanged)
 
    @Slot(QObject)
    def podcastContextMenu(self, podcast):
        menu = []
 
        if isinstance(podcast, model.EpisodeSubsetView):
            menu.append(helper.Action(_('Update all'), 'update-all', podcast))
        else:
            menu.append(helper.Action(_('Update'), 'update', podcast))
            menu.append(helper.Action(_('Mark episodes as old'), 'mark-as-read', podcast))
            menu.append(helper.Action(_('Rename'), 'rename-podcast', podcast))
            menu.append(helper.Action(_('Change section'), 'change-section', podcast))
            menu.append(helper.Action(_('Unsubscribe'), 'unsubscribe', podcast))
 
        #menu.append(helper.Action('Force update all', 'force-update-all', podcast))
        #menu.append(helper.Action('Force update', 'force-update', podcast))
 
        self.show_context_menu(menu)
 
    def show_context_menu(self, actions):
        actions = filter(lambda a: a.caption != '', actions)
        self.context_menu_actions = actions
        self.openContextMenu.emit(self.context_menu_actions)
 
    def finished_update(self):
        if self.updating_podcasts > 0:
            self.updating_podcasts -= 1
            self.setBusy(self.updating_podcasts > 0)
        self.update_subset_stats()
 
    def update_subset_stats(self):
        # This should be called when an episode changes state,
        # so that all subset views (e.g. "All episodes") can
        # update its status (i.e. download/new counts, etc..)
        for podcast in self.root.podcast_model.get_objects():
            if isinstance(podcast, model.EpisodeSubsetView):
                podcast.qupdate()
 
    def find_episode(self, podcast_url, episode_url):
        for podcast in self.root.podcast_model.get_podcasts():
            if podcast.url == podcast_url:
                for episode in podcast.get_all_episodes():
                    if episode.url == episode_url:
                        return episode
        return None
 
    @Slot(QObject)
    def updatePodcast(self, podcast):
        if not self.request_connection():
            return
        if not podcast.pause_subscription:
            podcast.qupdate(finished_callback=self.update_subset_stats)
 
    @Slot()
    def updateAllPodcasts(self):
        if not self.request_connection():
            self.setBusy(False)
            return
 
        self.setBusy(True)
 
        # Process episode actions received from gpodder.net
        def merge_proc(self):
            self.startProgress.emit(_('Merging episode actions...'))
 
            def find_episode(podcast_url, episode_url, counter):
                counter['x'] += 1
                self.startProgress.emit(_('Merging episode actions (%d)')
                        % counter['x'])
                return self.find_episode(podcast_url, episode_url)
 
            try:
                d = {'x': 0} # Used to "remember" the counter inside find_episode
                self.root.mygpo_client.process_episode_actions(lambda x, y:
                        find_episode(x, y, d))
            finally:
                self.endProgress.emit()
 
        util.run_in_background(lambda: merge_proc(self))
 
        for podcast in self.root.podcast_model.get_objects():
            if not podcast.pause_subscription and not podcast.qupdating:
                if podcast.qtitle != 'All episodes':
                    self.updating_podcasts += 1
                podcast.qupdate(finished_callback=self.finished_update)
 
        self.setBusy(self.updating_podcasts > 0)
 
    def request_connection(self):
        """Request an internet connection
 
        Returns True if a connection is available, False otherwise
        """
        if not util.connection_available():
            # TODO: Try to request the network connection dialog, and wait
            # for a connection - if a connection is available, return True
 
            self.showMessage.emit('\n\n'.join((_('No network connection'),
                _('Please connect to a network, then try again.'))))
            return False
 
        return True
 
    @Slot(int)
    def contextMenuResponse(self, index):
        assert index < len(self.context_menu_actions)
        action = self.context_menu_actions[index]
        if action.action == 'update':
            podcast = action.target
            self.updatePodcast(podcast)
        elif action.action == 'force-update':
            action.target.qupdate(force=True, \
                    finished_callback=self.update_subset_stats)
        elif action.action == 'update-all':
            self.updateAllPodcasts()
        elif action.action == 'force-update-all':
            for podcast in self.root.podcast_model.get_objects():
                podcast.qupdate(force=True, finished_callback=self.update_subset_stats)
        if action.action == 'unsubscribe':
            def unsubscribe():
                action.target.remove_downloaded()
                action.target.delete()
 
                # Remove queued episodes for this specific podcast
                self.removeQueuedEpisodesForPodcast.emit(action.target)
 
                # Update statistics in "All episodes"
                self.update_subset_stats()
 
                self.root.podcast_model.remove_object(action.target)
                self.root.mygpo_client.on_unsubscribe([action.target.url])
                self.root.mygpo_client.flush()
 
            self.confirm_action(_('Remove this podcast and episodes?'),
                    _('Unsubscribe'), unsubscribe)
        elif action.action == 'episode-toggle-new':
            action.target.toggle_new()
            self.episodeUpdated.emit(action.target.id)
            self.update_subset_stats()
        elif action.action == 'episode-toggle-archive':
            action.target.toggle_archive()
            self.episodeUpdated.emit(action.target.id)
            self.update_subset_stats()
        elif action.action == 'episode-delete':
            self.deleteEpisode(action.target)
        elif action.action == 'episode-enqueue':
            self.enqueueEpisode.emit(action.target)
        elif action.action == 'mark-as-read':
            for episode in action.target.get_all_episodes():
                if not episode.was_downloaded(and_exists=True):
                    episode.mark(is_played=True)
            action.target.changed.emit()
            self.update_subset_stats()
        elif action.action == 'change-section':
            def section_changer(podcast):
                section = yield (_('New section name:'), podcast.section,
                        _('Rename'), _('Cancel'), True)
                if section and section != podcast.section:
                    podcast.set_section(section)
                    self.root.resort_podcast_list()
 
            self.start_input_dialog(section_changer(action.target))
        elif action.action == 'rename-podcast':
            def title_changer(podcast):
                title = yield (_('New name:'), podcast.title,
                        _('Rename'), _('Cancel'), True)
                if title and title != podcast.title:
                    podcast.rename(title)
                    self.root.resort_podcast_list()
 
            self.start_input_dialog(title_changer(action.target))
 
    def confirm_action(self, message, affirmative, callback,
            negative_callback=None):
        def confirm(message, affirmative, callback, negative_callback):
            args = (message, '', affirmative, _('Cancel'), False)
            if (yield args):
                callback()
            elif negative_callback is not None:
                negative_callback()
 
        self.start_input_dialog(confirm(message, affirmative, callback,
            negative_callback))
 
    def start_input_dialog(self, generator):
        """Carry out an input dialog with the UI
 
        This function takes a generator function as argument
        which should yield a tuple of arguments for the
        showInputDialog signal.
 
        The generator will receive the user's response as a
        result of the yield expression. If the user accepted
        the dialog, a string is returned (the value that has
        been input), otherwise None is returned.
 
        Example usage:
 
        def some_function():
            result = yield ('A simple message', 'default value')
            if result is None:
                # user has rejected the dialog
            else:
                # user has accepted, new value in "result"
 
        start_input_dialog(some_function())
        """
        assert self.current_input_dialog is None
        self.current_input_dialog = generator
        args = generator.next()
        self.showInputDialog.emit(*args)
 
    @Slot(bool, str, bool)
    def inputDialogResponse(self, accepted, value, is_text):
        if not is_text:
            value = accepted
        elif not accepted:
            value = None
 
        try:
            self.current_input_dialog.send(value)
        except StopIteration:
            # This is expected, as the generator
            # should only have one yield statement
            pass
 
        self.current_input_dialog = None
 
    @Slot(QObject)
    def downloadEpisode(self, episode):
        episode.qdownload(self.root.config, self.update_subset_stats,
                self.episodeUpdated.emit)
        self.root.mygpo_client.on_download([episode])
        self.root.mygpo_client.flush()
 
    @Slot(QObject)
    def cancelDownload(self, episode):
        episode.download_task.cancel()
        episode.download_task.removed_from_list()
        self.episodeUpdated.emit(episode.id)
 
    @Slot(QObject)
    def deleteEpisode(self, episode):
        def delete():
            episode.delete_episode()
            self.episodeUpdated.emit(episode.id)
            self.update_subset_stats()
            self.root.mygpo_client.on_delete([episode])
            self.root.mygpo_client.flush()
            self.removeQueuedEpisode.emit(episode)
            util.run_in_background(self.root.episode_model.sort)
 
        self.confirm_action(_('Delete this episode?'), _('Delete'), delete)
 
    @Slot(QObject)
    def acquireEpisode(self, episode):
        self.root.add_active_episode(episode)
 
    @Slot(QObject)
    def releaseEpisode(self, episode):
        self.root.remove_active_episode(episode)
 
    @Slot()
    def contextMenuClosed(self):
        self.context_menu_actions = []
 
    @Slot(QObject)
    def episodeContextMenu(self, episode):
        menu = []
 
        toggle_new = _('Mark as old') if episode.is_new else _('Mark as new')
        menu.append(helper.Action(toggle_new, 'episode-toggle-new', episode))
 
        toggle_archive = _('Allow deletion') if episode.archive else _('Archive')
        menu.append(helper.Action(toggle_archive, 'episode-toggle-archive', episode))
 
        if episode.state != gpodder.STATE_DELETED:
            menu.append(helper.Action(_('Delete'), 'episode-delete', episode))
 
        menu.append(helper.Action(_('Add to play queue'), 'episode-enqueue', episode))
 
        self.show_context_menu(menu)
 
    @Slot('QVariant')
    def addSubscriptions(self, urls):
        if self._subscribe_in_progress:
            logger.warn('addSubscriptions call ignored (already subscribing)')
            return
 
        self._subscribe_in_progress = True
 
        def not_yet_subscribed(url):
            if not url:
                return False
 
            for podcast in self.root.podcast_model.get_objects():
                if isinstance(podcast, model.EpisodeSubsetView):
                    continue
 
                if podcast.url == url:
                    logger.info('Already subscribed: %s', url)
                    return False
 
            return True
 
        urls = filter(not_yet_subscribed, map(util.normalize_feed_url, urls))
 
        @util.run_in_background
        def subscribe_proc():
            self.startProgress.emit(_('Adding podcasts...'))
            failed = []
            try:
                for idx, url in enumerate(urls):
                    self.startProgress.emit(_('Adding podcasts...') + ' (%d/%d)' % (idx, len(urls)))
                    try:
                        podcast = self.root.model.load_podcast(url=url, create=True,
                                max_episodes=self.root.config.max_episodes_per_feed)
                        podcast.save()
                        self.root.insert_podcast(model.QPodcast(podcast))
                    except Exception, e:
                        logger.warn('Cannot add podcast: %s', url, exc_info=True)
                        failed.append((url, str(e)))
            finally:
                self.endProgress.emit()
                self._subscribe_in_progress = False
                self.update_subset_stats()
 
            def format_error(failed):
                yield _('Could not add some podcasts:')
                yield ''
                for url, error in failed:
                    yield ': '.join((url, error)) if error else url
 
            if failed:
                self.showMessage.emit('\n'.join(format_error(failed)))
 
    @Slot(QObject)
    def currentEpisodeChanging(self, episode):
        if self.current_episode is not None:
            self.current_episode.save()
        self.current_episode = episode
 
    def on_quit(self):
        # Make sure the audio playback is stopped immediately
        self.shutdown.emit()
 
        # Save current episode
        if self.current_episode is not None:
            self.current_episode.save()
 
 
class gPodderListModel(QAbstractListModel):
    def __init__(self, objects=None):
        QAbstractListModel.__init__(self)
        if objects is None:
            objects = []
        self._objects = objects
        self.setRoleNames({0: 'modelData', 1: 'section'})
 
    def sort(self):
        # Unimplemented for the generic list model
        self.reset()
 
    def insert_object(self, o):
        self._objects.append(o)
        self.sort()
 
    def remove_object(self, o):
        self._objects.remove(o)
        self.reset()
 
    def set_objects(self, objects):
        self._objects = objects
        self.sort()
 
    def get_objects(self):
        return self._objects
 
    def get_object(self, index):
        return self._objects[index.row()]
 
    def rowCount(self, parent=QModelIndex()):
        return len(self.get_objects())
 
    def data(self, index, role):
        if index.isValid():
            if role == 0:
                return self.get_object(index)
            elif role == 1:
                return self.get_object(index).qsection
        return None
 
class gPodderPodcastListModel(gPodderListModel):
    def set_podcasts(self, db, podcasts):
        views = [
            model.EpisodeSubsetView(db, self, _('All episodes'), ''),
        ]
        self.set_objects(views + podcasts)
 
    def get_podcasts(self):
        return filter(lambda podcast: isinstance(podcast, model.QPodcast),
                self.get_objects())
 
    def sort(self):
        self._objects = sorted(self._objects, key=model.QPodcast.sort_key)
        self.reset()
 
class gPodderEpisodeListModel(gPodderListModel):
    def __init__(self, config, root):
        gPodderListModel.__init__(self)
        self._filter = config.ui.qml.state.episode_list_filter
        self._filtered = []
        self._processed = []
        self._is_subset_view = False
 
        self._config = config
        self._root = root
        config.add_observer(self._on_config_changed)
 
    is_subset_view_changed = Signal()
 
    def get_is_subset_view(self):
        return self._is_subset_view
 
    def set_is_subset_view(self, is_subset_view):
        if is_subset_view != self.is_subset_view:
            self._is_subset_view = is_subset_view
            self.is_subset_view_changed.emit()
 
    is_subset_view = Property(bool, get_is_subset_view,
            set_is_subset_view, notify=is_subset_view_changed)
 
    def _on_config_changed(self, name, old_value, new_value):
        if name == 'ui.qml.state.episode_list_filter':
            self._filter = new_value
            util.run_in_background(self.sort)
 
    def sort(self):
        self._root.controller.clearEpisodeListModel.emit()
 
        caption, eql = EPISODE_LIST_FILTERS[self._filter]
 
        if eql is None:
            self._filtered = self._objects
        else:
            eql = query.EQL(eql)
            match = lambda episode: eql.match(episode._episode)
            self._filtered = filter(match, self._objects)
 
        def to_dict(episode):
            return {
                'episode_id': episode._episode.id,
 
                'episode': episode,
 
                'title': episode.qtitle,
                'podcast': episode.qpodcast,
                'cover_url': episode.qpodcast.qcoverart,
                'filetype': episode.qfiletype,
 
                'duration': episode.qduration,
                'downloading': episode.qdownloading,
                'position': episode.qposition,
                'progress': episode.qprogress,
                'downloaded': episode.qdownloaded,
                'isnew': episode.qnew,
                'archive': episode.qarchive,
            }
 
        self._processed = map(to_dict, self._filtered[:EPISODE_LIST_LIMIT])
        self.reset()
 
        self._root.controller.setEpisodeListModel.emit()
 
    def get_objects(self):
        return self._processed
 
    def get_object(self, index):
        return self._processed[index.row()]
 
    @Slot(int, result='QVariant')
    def get_object_by_index(self, index):
        return self._processed[int(index)]
 
    @Slot(int, result='QVariant')
    def get_object_by_index(self, index):
        return self._processed[int(index)]
 
    @Slot(result=int)
    def getCount(self):
        return len(self._processed)
 
    @Slot(result=int)
    def getFilter(self):
        return self._filter
 
    @Slot(int)
    def setFilter(self, filter_index):
        self._config.ui.qml.state.episode_list_filter = filter_index
 
 
def QML(filename):
    for folder in gpodder.ui_folders:
        filename = os.path.join(folder, filename)
        if os.path.exists(filename):
            return filename
 
import time
 
class DeclarativeView(QDeclarativeView):
    def __init__(self):
        QDeclarativeView.__init__(self)
        self.setAttribute(Qt.WA_OpaquePaintEvent)
        self.setAttribute(Qt.WA_NoSystemBackground)
        self.viewport().setAttribute(Qt.WA_OpaquePaintEvent)
        self.viewport().setAttribute(Qt.WA_NoSystemBackground)
 
#    def paintEvent(self, event):
#        old = time.time()
#        QDeclarativeView.paintEvent(self, event)
#        fps = 1. / (time.time() - old)
#        if fps < 60:
#            print 'FPS: %2.0f' % fps
 
    closing = Signal()
 
    def closeEvent(self, event):
        self.closing.emit()
        event.ignore()
 
class qtPodder(QObject):
    def __init__(self, args, gpodder_core, dbus_bus_name):
        QObject.__init__(self)
 
        self.dbus_bus_name = dbus_bus_name
        # TODO: Expose the same D-Bus API as the Gtk UI D-Bus object (/gui)
        # TODO: Create a gpodder.dbusproxy.DBusPodcastsProxy object (/podcasts)
 
        self.app = QApplication(args)
        signal.signal(signal.SIGINT, signal.SIG_DFL)
        self.quit.connect(self.on_quit)
 
        self.core = gpodder_core
        self.config = self.core.config
        self.db = self.core.db
        self.model = self.core.model
 
        self.config_proxy = ConfigProxy(self.config)
 
        # Initialize the gpodder.net client
        self.mygpo_client = my.MygPoClient(self.config)
 
        gpodder.user_extensions.on_ui_initialized(self.model,
                self.extensions_podcast_update_cb,
                self.extensions_episode_download_cb)
 
        self.view = DeclarativeView()
        self.view.closing.connect(self.on_quit)
        self.view.setResizeMode(QDeclarativeView.SizeRootObjectToView)
 
        self.controller = Controller(self)
        self.media_buttons_handler = helper.MediaButtonsHandler()
        self.tracker_miner_config = helper.TrackerMinerConfig()
        self.podcast_model = gPodderPodcastListModel()
        self.episode_model = gPodderEpisodeListModel(self.config, self)
        self.last_episode = None
 
        # A dictionary of episodes that are currently active
        # in some way (i.e. playing back or downloading)
        self.active_episode_wrappers = {}
 
        engine = self.view.engine()
 
        # Add the cover art image provider
        self.cover_provider = images.LocalCachedImageProvider()
        engine.addImageProvider('cover', self.cover_provider)
 
        root_context = self.view.rootContext()
        root_context.setContextProperty('controller', self.controller)
        root_context.setContextProperty('configProxy', self.config_proxy)
        root_context.setContextProperty('mediaButtonsHandler',
                self.media_buttons_handler)
        root_context.setContextProperty('trackerMinerConfig',
                self.tracker_miner_config)
        root_context.setContextProperty('podcastModel', self.podcast_model)
        root_context.setContextProperty('episodeModel', self.episode_model)
        root_context.setContextProperty('isSailfish', gpodder.ui.sailfish)
 
        for folder in gpodder.ui_folders:
            if gpodder.ui.sailfish:
                path = os.path.join(folder, 'sailfish')
            else:
                path = os.path.join(folder, 'harmattan')
 
            if os.path.exists(path):
                logger.info('Adding QML Import Path: %s', path)
                engine.addImportPath(path)
 
        # Load the QML UI (this could take a while...)
        self.view.setSource(QUrl.fromLocalFile(QML('main_default.qml')))
 
        self.view.setWindowTitle('gPodder')
 
        if gpodder.ui.harmattan or gpodder.ui.sailfish:
            self.view.showFullScreen()
        else:
            # On the Desktop, scale to fit my small laptop screen..
            desktop = self.app.desktop()
            if desktop.height() < 1000:
                FACTOR = .8
                self.view.scale(FACTOR, FACTOR)
                size = self.view.size()
                size *= FACTOR
                self.view.resize(size)
            self.view.show()
 
        podcasts = self.load_podcasts()
 
        self.resumable_episodes = None
        self.do_offer_download_resume.connect(self.on_offer_download_resume)
        util.run_in_background(self.find_partial_downloads(podcasts))
 
    def find_partial_downloads(self, podcasts):
        def start_progress_callback(count):
            self.controller.startProgress.emit(_('Loading incomplete downloads'))
 
        def progress_callback(title, progress):
            self.controller.startProgress.emit('%s (%d%%)' % (
                _('Loading incomplete downloads'),
                progress*100))
 
        def finish_progress_callback(resumable_episodes):
            self.controller.endProgress.emit()
            self.resumable_episodes = resumable_episodes
            self.do_offer_download_resume.emit()
 
        common.find_partial_downloads(podcasts,
                start_progress_callback,
                progress_callback,
                finish_progress_callback)
 
    do_offer_download_resume = Signal()
 
    def on_offer_download_resume(self):
        if self.resumable_episodes:
            def download_episodes():
                for episode in self.resumable_episodes:
                    qepisode = self.wrap_simple_episode(episode)
                    self.controller.downloadEpisode(qepisode)
 
            def delete_episodes():
                logger.debug('Deleting incomplete downloads.')
                common.clean_up_downloads(delete_partial=True)
 
            message = _('Incomplete downloads from a previous session were found.')
            title = _('Resume')
 
            self.controller.confirm_action(message, title, download_episodes, delete_episodes)
 
    def add_active_episode(self, episode):
        self.active_episode_wrappers[episode.id] = episode
        episode.episode_wrapper_refcount += 1
 
    def remove_active_episode(self, episode):
        episode.episode_wrapper_refcount -= 1
        if episode.episode_wrapper_refcount == 0:
            del self.active_episode_wrappers[episode.id]
 
    def load_last_episode(self):
        last_episode = None
        last_podcast = None
        for podcast in self.podcast_model.get_podcasts():
            for episode in podcast.get_all_episodes():
                if not episode.last_playback:
                    continue
                if last_episode is None or \
                        episode.last_playback > last_episode.last_playback:
                    last_episode = episode
                    last_podcast = podcast
 
        if last_episode is not None:
            self.last_episode = self.wrap_episode(last_podcast, last_episode)
            # FIXME: Send last episode to player
            #self.select_episode(self.last_episode)
 
    def run(self):
        return self.app.exec_()
 
    quit = Signal()
 
    def on_quit(self):
        self.controller.on_quit()
        self.view.hide()
        self.core.shutdown()
        self.app.quit()
 
    def resort_podcast_list(self):
        self.podcast_model.sort()
 
    def insert_podcast(self, podcast):
        self.podcast_model.insert_object(podcast)
        self.mygpo_client.on_subscribe([podcast.url])
        self.mygpo_client.flush()
 
    def load_podcasts(self):
        podcasts = map(model.QPodcast, self.model.get_podcasts())
        self.podcast_model.set_podcasts(self.db, podcasts)
        return podcasts
 
    def wrap_episode(self, podcast, episode):
        try:
            return self.active_episode_wrappers[episode.id]
        except KeyError:
            return model.QEpisode(self, podcast, episode)
 
    def wrap_simple_episode(self, episode):
        for podcast in self.podcast_model.get_podcasts():
            if podcast.id == episode.podcast_id:
                return self.wrap_episode(podcast, episode)
 
        return None
 
    def select_podcast(self, podcast):
        if isinstance(podcast, model.QPodcast):
            # Normal QPodcast instance
            wrap = functools.partial(self.wrap_episode, podcast)
            objects = podcast.get_all_episodes()
            self.episode_model.set_is_subset_view(False)
        else:
            # EpisodeSubsetView
            wrap = lambda args: self.wrap_episode(*args)
            objects = podcast.get_all_episodes_with_podcast()
            self.episode_model.set_is_subset_view(True)
 
        self.episode_model.set_objects(map(wrap, objects))
 
    def podcast_to_qpodcast(self, podcast):
        podcasts = filter(lambda p: p._podcast == podcast,
                          self.podcast_model.get_podcasts())
        assert len(podcasts) <= 1
        if podcasts:
            return podcasts[0]
        return None
 
    def extensions_podcast_update_cb(self, podcast):
        logger.debug('extensions_podcast_update_cb(%s)', podcast)
        try:
            qpodcast = self.podcast_to_qpodcast(podcast)
            if qpodcast is not None and not qpodcast.pause_subscription:
                qpodcast.qupdate(
                    finished_callback=self.controller.update_subset_stats)
        except Exception, e:
            logger.exception('extensions_podcast_update_cb(%s): %s', podcast, e)
 
    def extensions_episode_download_cb(self, episode):
        logger.debug('extensions_episode_download_cb(%s)', episode)
        try:
            qpodcast = self.podcast_to_qpodcast(episode.channel)
            qepisode = self.wrap_episode(qpodcast, episode)
            self.controller.downloadEpisode(qepisode)
        except Exception, e:
            logger.exception('extensions_episode_download_cb(%s): %s', episode, e)
 
def main(args):
    try:
        dbus_main_loop = DBusGMainLoop(set_as_default=True)
        gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
 
        bus_name = dbus.service.BusName(gpodder.dbus_bus_name,
                bus=gpodder.dbus_session_bus)
    except dbus.exceptions.DBusException, dbe:
        logger.warn('Cannot get "on the bus".', exc_info=True)
        bus_name = None
 
    gui = qtPodder(args, core.Core(), bus_name)
    return gui.run()