QML UI: Add "Delete" context menu for episodes
[gpodder.git] / src / gpodder / qmlui / __init__.py
blob1ae973cd642c4c20cc739bbad221bb067f1d157c
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2012 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # gpodder.qmlui - gPodder's QML interface
21 # Thomas Perl <thp@gpodder.org>; 2011-02-06
24 from PySide.QtGui import QApplication
25 from PySide.QtCore import Qt, QObject, Signal, Slot, Property, QUrl
26 from PySide.QtCore import QAbstractListModel, QModelIndex
27 from PySide.QtDeclarative import QDeclarativeView
29 import os
30 import threading
31 import signal
32 import functools
33 import gpodder
34 import subprocess
36 _ = gpodder.gettext
37 N_ = gpodder.ngettext
39 from gpodder import core
40 from gpodder import util
41 from gpodder import my
42 from gpodder import query
44 from gpodder.model import Model
46 from gpodder.qmlui import model
47 from gpodder.qmlui import helper
48 from gpodder.qmlui import images
50 import logging
51 logger = logging.getLogger("qmlui")
54 EPISODE_LIST_FILTERS = [
55 # (UI label, EQL expression)
56 (_('All'), None),
57 (_('Hide deleted'), 'not deleted'),
58 (_('New'), 'new or downloading'),
59 (_('Downloaded'), 'downloaded or downloading'),
60 (_('Deleted'), 'deleted'),
61 (_('Finished'), 'finished'),
62 (_('Archived'), 'downloaded and archive'),
63 (_('Videos'), 'video'),
67 class ConfigProxy(QObject):
68 def __init__(self, config):
69 QObject.__init__(self)
70 self._config = config
72 config.add_observer(self._on_config_changed)
74 def _on_config_changed(self, name, old_value, new_value):
75 if name == 'ui.qml.autorotate':
76 self.autorotateChanged.emit()
78 def get_autorotate(self):
79 return self._config.ui.qml.autorotate
81 def set_autorotate(self, autorotate):
82 self._config.ui.qml.autorotate = autorotate
84 autorotateChanged = Signal()
86 autorotate = Property(bool, get_autorotate, set_autorotate,
87 notify=autorotateChanged)
89 class Controller(QObject):
90 def __init__(self, root):
91 QObject.__init__(self)
92 self.root = root
93 self.context_menu_actions = []
94 self.episode_list_title = u''
95 self.current_input_dialog = None
96 self.root.config.add_observer(self.on_config_changed)
98 def on_config_changed(self, name, old_value, new_value):
99 logger.info('Config changed: %s (%s -> %s)', name,
100 old_value, new_value)
101 if name == 'mygpo.enabled':
102 self.myGpoEnabledChanged.emit()
103 elif name == 'mygpo.username':
104 self.myGpoUsernameChanged.emit()
105 elif name == 'mygpo.password':
106 self.myGpoPasswordChanged.emit()
107 elif name == 'mygpo.device.caption':
108 self.myGpoDeviceCaptionChanged.emit()
110 episodeListTitleChanged = Signal()
112 def setEpisodeListTitle(self, title):
113 if self.episode_list_title != title:
114 self.episode_list_title = title
115 self.episodeListTitleChanged.emit()
117 def getEpisodeListTitle(self):
118 return self.episode_list_title
120 episodeListTitle = Property(unicode, getEpisodeListTitle, \
121 setEpisodeListTitle, notify=episodeListTitleChanged)
123 @Slot(result='QStringList')
124 def getEpisodeListFilterNames(self):
125 return [caption for caption, eql in EPISODE_LIST_FILTERS]
127 @Slot(str, result=str)
128 def translate(self, x):
129 return _(x)
131 @Slot(str, str, int, result=str)
132 def ntranslate(self, singular, plural, count):
133 return N_(singular, plural, count)
135 @Slot(str, int, result=str)
136 def formatCount(self, template, count):
137 return template % {'count': count}
139 @Slot(result=str)
140 def getVersion(self):
141 return gpodder.__version__
143 @Slot(result=unicode)
144 def getCopyright(self):
145 return util.convert_bytes(gpodder.__copyright__)
147 @Slot(result=str)
148 def getLicense(self):
149 return gpodder.__license__
151 @Slot(result=str)
152 def getURL(self):
153 return gpodder.__url__
155 @Slot()
156 def loadLastEpisode(self):
157 self.root.load_last_episode()
159 @Slot(QObject, int, int)
160 def storePlaybackAction(self, episode, start, end):
161 if end - 5 < start:
162 logger.info('Ignoring too short playback action.')
163 return
164 total = episode.qduration
165 self.root.mygpo_client.on_playback_full(episode, start, end, total)
166 self.root.mygpo_client.flush()
168 @Slot(QObject)
169 def playVideo(self, episode):
170 """Video Playback on MeeGo 1.2 Harmattan"""
171 if episode.qnew:
172 episode.toggle_new()
173 self.update_subset_stats()
175 url = episode.get_playback_url()
176 if gpodder.ui.harmattan:
177 subprocess.Popen(['video-suite', url])
178 else:
179 util.gui_open(url)
181 self.root.mygpo_client.on_playback([episode])
182 self.root.mygpo_client.flush()
184 @Slot(QObject)
185 def podcastSelected(self, podcast):
186 self.setEpisodeListTitle(podcast.qtitle)
187 self.root.select_podcast(podcast)
189 windowTitleChanged = Signal()
191 def getWindowTitle(self):
192 return self.root.view.windowTitle()
194 def setWindowTitle(self, windowTitle):
195 if gpodder.ui.fremantle:
196 self.root.view.setWindowTitle(windowTitle)
198 windowTitle = Property(unicode, getWindowTitle,
199 setWindowTitle, notify=windowTitleChanged)
201 @Slot()
202 def myGpoUploadList(self):
203 def upload_proc(self):
204 self.root.start_progress(_('Uploading subscriptions...'))
206 try:
207 try:
208 self.root.mygpo_client.set_subscriptions([podcast.url
209 for podcast in self.root.podcast_model.get_podcasts()])
210 except Exception, e:
211 self.root.show_message('\n'.join((_('Error on upload:'), unicode(e))))
212 finally:
213 self.root.end_progress()
215 t = threading.Thread(target=upload_proc, args=[self])
216 t.start()
218 @Slot()
219 def saveMyGpoSettings(self):
220 # Update the device settings and upload changes
221 self.root.mygpo_client.create_device()
222 self.root.mygpo_client.flush(now=True)
224 myGpoEnabledChanged = Signal()
226 def getMyGpoEnabled(self):
227 return self.root.config.mygpo.enabled
229 def setMyGpoEnabled(self, enabled):
230 self.root.config.mygpo.enabled = enabled
232 myGpoEnabled = Property(bool, getMyGpoEnabled,
233 setMyGpoEnabled, notify=myGpoEnabledChanged)
235 myGpoUsernameChanged = Signal()
237 def getMyGpoUsername(self):
238 return model.convert(self.root.config.mygpo.username)
240 def setMyGpoUsername(self, username):
241 self.root.config.mygpo.username = username
243 myGpoUsername = Property(unicode, getMyGpoUsername,
244 setMyGpoUsername, notify=myGpoUsernameChanged)
246 myGpoPasswordChanged = Signal()
248 def getMyGpoPassword(self):
249 return model.convert(self.root.config.mygpo.password)
251 def setMyGpoPassword(self, password):
252 self.root.config.mygpo.password = password
254 myGpoPassword = Property(unicode, getMyGpoPassword,
255 setMyGpoPassword, notify=myGpoPasswordChanged)
257 myGpoDeviceCaptionChanged = Signal()
259 def getMyGpoDeviceCaption(self):
260 return model.convert(self.root.config.mygpo.device.caption)
262 def setMyGpoDeviceCaption(self, caption):
263 self.root.config.mygpo.device.caption = caption
265 myGpoDeviceCaption = Property(unicode, getMyGpoDeviceCaption,
266 setMyGpoDeviceCaption, notify=myGpoDeviceCaptionChanged)
268 @Slot(QObject)
269 def podcastContextMenu(self, podcast):
270 menu = []
272 if isinstance(podcast, model.EpisodeSubsetView):
273 menu.append(helper.Action(_('Update all'), 'update-all', podcast))
274 else:
275 menu.append(helper.Action(_('Update'), 'update', podcast))
276 menu.append(helper.Action(_('Mark episodes as old'), 'mark-as-read', podcast))
277 menu.append(helper.Action(_('Rename'), 'rename-podcast', podcast))
278 menu.append(helper.Action(_('Change section'), 'change-section', podcast))
279 menu.append(helper.Action(_('Unsubscribe'), 'unsubscribe', podcast))
281 #menu.append(helper.Action('Force update all', 'force-update-all', podcast))
282 #menu.append(helper.Action('Force update', 'force-update', podcast))
284 self.show_context_menu(menu)
286 def show_context_menu(self, actions):
287 if gpodder.ui.harmattan:
288 actions = filter(lambda a: a.caption != '', actions)
289 self.context_menu_actions = actions
290 self.root.open_context_menu(self.context_menu_actions)
292 def update_subset_stats(self):
293 # This should be called when an episode changes state,
294 # so that all subset views (e.g. "All episodes") can
295 # update its status (i.e. download/new counts, etc..)
296 for podcast in self.root.podcast_model.get_objects():
297 if isinstance(podcast, model.EpisodeSubsetView):
298 podcast.qupdate()
300 def find_episode(self, podcast_url, episode_url):
301 for podcast in self.root.podcast_model.get_podcasts():
302 if podcast.url == podcast_url:
303 for episode in podcast.get_all_episodes():
304 if episode.url == episode_url:
305 return episode
306 return None
308 @Slot()
309 def updateAllPodcasts(self):
310 # Process episode actions received from gpodder.net
311 def merge_proc(self):
312 self.root.start_progress(_('Merging episode actions...'))
314 def find_episode(podcast_url, episode_url, counter):
315 counter['x'] += 1
316 self.root.start_progress(_('Merging episode actions (%d)')
317 % counter['x'])
318 return self.find_episode(podcast_url, episode_url)
320 try:
321 d = {'x': 0} # Used to "remember" the counter inside find_episode
322 self.root.mygpo_client.process_episode_actions(lambda x, y:
323 find_episode(x, y, d))
324 finally:
325 self.root.end_progress()
327 t = threading.Thread(target=merge_proc, args=[self])
328 t.start()
330 for podcast in self.root.podcast_model.get_objects():
331 podcast.qupdate(finished_callback=self.update_subset_stats)
333 @Slot(int)
334 def contextMenuResponse(self, index):
335 assert index < len(self.context_menu_actions)
336 action = self.context_menu_actions[index]
337 if action.action == 'update':
338 action.target.qupdate(finished_callback=self.update_subset_stats)
339 elif action.action == 'force-update':
340 action.target.qupdate(force=True, \
341 finished_callback=self.update_subset_stats)
342 elif action.action == 'update-all':
343 self.updateAllPodcasts()
344 elif action.action == 'force-update-all':
345 for podcast in self.root.podcast_model.get_objects():
346 podcast.qupdate(force=True, finished_callback=self.update_subset_stats)
347 if action.action == 'unsubscribe':
348 def unsubscribe():
349 action.target.remove_downloaded()
350 action.target.delete()
351 self.root.remove_podcast(action.target)
353 self.confirm_action(_('Remove this podcast and episodes?'),
354 _('Unsubscribe'), unsubscribe)
355 elif action.action == 'episode-toggle-new':
356 action.target.toggle_new()
357 self.update_subset_stats()
358 elif action.action == 'episode-toggle-archive':
359 action.target.toggle_archive()
360 self.update_subset_stats()
361 elif action.action == 'episode-delete':
362 self.deleteEpisode(action.target)
363 elif action.action == 'episode-enqueue':
364 self.root.enqueue_episode(action.target)
365 elif action.action == 'mark-as-read':
366 for episode in action.target.get_all_episodes():
367 if not episode.was_downloaded(and_exists=True):
368 episode.mark(is_played=True)
369 action.target.changed.emit()
370 self.update_subset_stats()
371 elif action.action == 'change-section':
372 def section_changer(podcast):
373 section = yield (_('New section name:'), podcast.section,
374 _('Rename'))
375 if section and section != podcast.section:
376 podcast.set_section(section)
377 self.root.resort_podcast_list()
379 self.start_input_dialog(section_changer(action.target))
380 elif action.action == 'rename-podcast':
381 def title_changer(podcast):
382 title = yield (_('New name:'), podcast.title,
383 _('Rename'))
384 if title and title != podcast.title:
385 podcast.rename(title)
386 self.root.resort_podcast_list()
388 self.start_input_dialog(title_changer(action.target))
390 def confirm_action(self, message, affirmative, callback):
391 def confirm(message, affirmative, callback):
392 args = (message, '', affirmative, _('Cancel'), False)
393 if (yield args):
394 callback()
396 self.start_input_dialog(confirm(message, affirmative, callback))
398 def start_input_dialog(self, generator):
399 """Carry out an input dialog with the UI
401 This function takes a generator function as argument
402 which should yield a tuple of arguments for the
403 "show_input_dialog" function (i.e. message, default
404 value, accept and reject message - only the message
405 is mandatory, the other arguments have default values).
407 The generator will receive the user's response as a
408 result of the yield expression. If the user accepted
409 the dialog, a string is returned (the value that has
410 been input), otherwise None is returned.
412 Example usage:
414 def some_function():
415 result = yield ('A simple message', 'default value')
416 if result is None:
417 # user has rejected the dialog
418 else:
419 # user has accepted, new value in "result"
421 start_input_dialog(some_function())
423 assert self.current_input_dialog is None
424 self.current_input_dialog = generator
425 args = generator.next()
426 self.root.show_input_dialog(*args)
428 @Slot(bool, str, bool)
429 def inputDialogResponse(self, accepted, value, is_text):
430 if not is_text:
431 value = accepted
432 elif not accepted:
433 value = None
435 try:
436 self.current_input_dialog.send(value)
437 except StopIteration:
438 # This is expected, as the generator
439 # should only have one yield statement
440 pass
442 self.current_input_dialog = None
444 @Slot(QObject)
445 def downloadEpisode(self, episode):
446 episode.qdownload(self.root.config, self.update_subset_stats)
447 self.root.mygpo_client.on_download([episode])
448 self.root.mygpo_client.flush()
450 @Slot(QObject)
451 def cancelDownload(self, episode):
452 episode.download_task.cancel()
453 episode.download_task.removed_from_list()
455 @Slot(QObject)
456 def deleteEpisode(self, episode):
457 def delete():
458 episode.delete_episode()
459 self.update_subset_stats()
460 self.root.mygpo_client.on_delete([episode])
461 self.root.mygpo_client.flush()
462 self.root.on_episode_deleted(episode)
463 self.root.episode_model.sort()
465 self.confirm_action(_('Delete this episode?'), _('Delete'), delete)
467 @Slot(QObject)
468 def acquireEpisode(self, episode):
469 self.root.add_active_episode(episode)
471 @Slot(QObject)
472 def releaseEpisode(self, episode):
473 self.root.remove_active_episode(episode)
475 @Slot()
476 def contextMenuClosed(self):
477 self.context_menu_actions = []
479 @Slot(QObject)
480 def episodeContextMenu(self, episode):
481 menu = []
483 toggle_new = _('Mark as old') if episode.is_new else _('Mark as new')
484 menu.append(helper.Action(toggle_new, 'episode-toggle-new', episode))
486 toggle_archive = _('Allow deletion') if episode.archive else _('Archive')
487 menu.append(helper.Action(toggle_archive, 'episode-toggle-archive', episode))
489 if episode.state != gpodder.STATE_DELETED:
490 menu.append(helper.Action(_('Delete'), 'episode-delete', episode))
492 menu.append(helper.Action(_('Add to play queue'), 'episode-enqueue', episode))
494 self.show_context_menu(menu)
496 @Slot('QVariant')
497 def addSubscriptions(self, urls):
498 def not_yet_subscribed(url):
499 for podcast in self.root.podcast_model.get_objects():
500 if isinstance(podcast, model.EpisodeSubsetView):
501 continue
503 if podcast.url == url:
504 logger.info('Already subscribed: %s', url)
505 return False
507 return True
509 urls = map(util.normalize_feed_url, urls)
510 urls = filter(not_yet_subscribed, urls)
512 def subscribe_proc(self, urls):
513 self.root.start_progress(_('Adding podcasts...'))
514 try:
515 for idx, url in enumerate(urls):
516 print idx, url
517 self.root.start_progress(_('Adding podcasts...') + ' (%d/%d)' % (idx, len(urls)))
518 try:
519 podcast = self.root.model.load_podcast(url=url, create=True,
520 max_episodes=self.root.config.max_episodes_per_feed)
521 podcast.save()
522 self.root.insert_podcast(model.QPodcast(podcast))
523 except Exception, e:
524 logger.warn('Cannot add pocast: %s', e)
525 # XXX: Visual feedback in the QML UI
526 finally:
527 self.root.end_progress()
529 t = threading.Thread(target=subscribe_proc, args=[self, urls])
530 t.start()
532 @Slot()
533 def currentEpisodeChanging(self):
534 self.root.save_pending_data()
536 @Slot()
537 def quit(self):
538 self.root.quit.emit()
540 @Slot()
541 def switcher(self):
542 if gpodder.ui.harmattan:
543 self.root.view.showMinimized()
544 elif gpodder.ui.fremantle:
545 os.system('dbus-send /com/nokia/hildon_desktop '+
546 'com.nokia.hildon_desktop.exit_app_view')
547 else:
548 self.root.view.showMinimized()
551 class gPodderListModel(QAbstractListModel):
552 def __init__(self, objects=None):
553 QAbstractListModel.__init__(self)
554 if objects is None:
555 objects = []
556 self._objects = objects
557 self.setRoleNames({0: 'modelData', 1: 'section'})
559 def sort(self):
560 # Unimplemented for the generic list model
561 self.reset()
563 def insert_object(self, o):
564 self._objects.append(o)
565 self.sort()
567 def remove_object(self, o):
568 self._objects.remove(o)
569 self.reset()
571 def set_objects(self, objects):
572 self._objects = objects
573 self.sort()
575 def get_objects(self):
576 return self._objects
578 def get_object(self, index):
579 return self._objects[index.row()]
581 def rowCount(self, parent=QModelIndex()):
582 return len(self.get_objects())
584 def data(self, index, role):
585 if index.isValid():
586 if role == 0:
587 return self.get_object(index)
588 elif role == 1:
589 return self.get_object(index).qsection
590 return None
592 class gPodderPodcastListModel(gPodderListModel):
593 def set_podcasts(self, db, podcasts):
594 views = [
595 model.EpisodeSubsetView(db, self, _('All episodes'), ''),
597 self.set_objects(views + podcasts)
599 def get_podcasts(self):
600 return filter(lambda podcast: isinstance(podcast, model.QPodcast),
601 self.get_objects())
603 def sort(self):
604 self._objects = sorted(self._objects, key=model.QPodcast.sort_key)
605 self.reset()
607 class gPodderEpisodeListModel(gPodderListModel):
608 def __init__(self, config):
609 gPodderListModel.__init__(self)
610 self._filter = config.ui.qml.state.episode_list_filter
611 self._filtered = []
613 self._config = config
614 config.add_observer(self._on_config_changed)
616 def _on_config_changed(self, name, old_value, new_value):
617 if name == 'ui.qml.state.episode_list_filter':
618 self._filter = new_value
619 self.sort()
621 def sort(self):
622 caption, eql = EPISODE_LIST_FILTERS[self._filter]
624 if eql is None:
625 self._filtered = self._objects
626 else:
627 eql = query.EQL(eql)
628 match = lambda episode: eql.match(episode._episode)
629 self._filtered = filter(match, self._objects)
631 self.reset()
633 def get_objects(self):
634 return self._filtered
636 def get_object(self, index):
637 return self._filtered[index.row()]
639 @Slot(result=int)
640 def getFilter(self):
641 return self._filter
643 @Slot(int)
644 def setFilter(self, filter_index):
645 self._config.ui.qml.state.episode_list_filter = filter_index
648 def QML(filename):
649 for folder in gpodder.ui_folders:
650 filename = os.path.join(folder, filename)
651 if os.path.exists(filename):
652 return filename
654 class DeclarativeView(QDeclarativeView):
655 def __init__(self):
656 QDeclarativeView.__init__(self)
657 self.setAttribute(Qt.WA_OpaquePaintEvent)
658 self.setAttribute(Qt.WA_NoSystemBackground)
659 self.viewport().setAttribute(Qt.WA_OpaquePaintEvent)
660 self.viewport().setAttribute(Qt.WA_NoSystemBackground)
662 closing = Signal()
664 def closeEvent(self, event):
665 self.closing.emit()
666 event.ignore()
668 class qtPodder(QObject):
669 def __init__(self, args, gpodder_core):
670 QObject.__init__(self)
672 # Enable OpenGL rendering without requiring QtOpenGL
673 # On Harmattan we let the system choose the best graphicssystem
674 if '-graphicssystem' not in args and not gpodder.ui.harmattan:
675 if gpodder.ui.fremantle:
676 args += ['-graphicssystem', 'opengl']
677 elif not gpodder.win32:
678 args += ['-graphicssystem', 'raster']
680 self.app = QApplication(args)
681 signal.signal(signal.SIGINT, signal.SIG_DFL)
682 self.quit.connect(self.on_quit)
684 self.core = gpodder_core
685 self.config = self.core.config
686 self.db = self.core.db
687 self.model = self.core.model
689 self.config_proxy = ConfigProxy(self.config)
691 # Initialize the gpodder.net client
692 self.mygpo_client = my.MygPoClient(self.config)
694 gpodder.user_extensions.on_ui_initialized(self.model,
695 self.extensions_podcast_update_cb,
696 self.extensions_episode_download_cb)
698 self.view = DeclarativeView()
699 self.view.closing.connect(self.on_quit)
700 self.view.setResizeMode(QDeclarativeView.SizeRootObjectToView)
702 self.controller = Controller(self)
703 self.media_buttons_handler = helper.MediaButtonsHandler()
704 self.podcast_model = gPodderPodcastListModel()
705 self.episode_model = gPodderEpisodeListModel(self.config)
706 self.last_episode = None
708 # A dictionary of episodes that are currently active
709 # in some way (i.e. playing back or downloading)
710 self.active_episode_wrappers = {}
712 engine = self.view.engine()
714 # Maemo 5: Experimental Qt Mobility packages are installed in /opt
715 if gpodder.ui.fremantle:
716 for path in ('/opt/qtm11/imports', '/opt/qtm12/imports'):
717 engine.addImportPath(path)
718 elif gpodder.win32:
719 for path in (r'C:\QtSDK\Desktop\Qt\4.7.4\msvc2008\imports',):
720 engine.addImportPath(path)
722 # Add the cover art image provider
723 self.cover_provider = images.LocalCachedImageProvider()
724 engine.addImageProvider('cover', self.cover_provider)
726 root_context = self.view.rootContext()
727 root_context.setContextProperty('controller', self.controller)
728 root_context.setContextProperty('configProxy', self.config_proxy)
729 root_context.setContextProperty('mediaButtonsHandler',
730 self.media_buttons_handler)
732 # Load the QML UI (this could take a while...)
733 self.view.setSource(QUrl.fromLocalFile(QML('main_default.qml')))
735 # Proxy to the "main" QML object for direct access to Qt Properties
736 self.main = helper.QObjectProxy(self.view.rootObject().property('main'))
738 self.main.podcastModel = self.podcast_model
739 self.main.episodeModel = self.episode_model
741 self.view.setWindowTitle('gPodder')
743 if gpodder.ui.harmattan:
744 self.view.showFullScreen()
745 elif gpodder.ui.fremantle:
746 self.view.setAttribute(Qt.WA_Maemo5AutoOrientation, True)
747 self.view.showFullScreen()
748 else:
749 # On the Desktop, scale to fit my small laptop screen..
750 FACTOR = .8
751 self.view.scale(FACTOR, FACTOR)
752 size = self.view.size()
753 size *= FACTOR
754 self.view.resize(size)
755 self.view.show()
757 self.do_start_progress.connect(self.on_start_progress)
758 self.do_end_progress.connect(self.on_end_progress)
759 self.do_show_message.connect(self.on_show_message)
761 self.load_podcasts()
763 def add_active_episode(self, episode):
764 self.active_episode_wrappers[episode.id] = episode
765 episode.episode_wrapper_refcount += 1
767 def remove_active_episode(self, episode):
768 episode.episode_wrapper_refcount -= 1
769 if episode.episode_wrapper_refcount == 0:
770 del self.active_episode_wrappers[episode.id]
772 def load_last_episode(self):
773 last_episode = None
774 last_podcast = None
775 for podcast in self.podcast_model.get_podcasts():
776 for episode in podcast.get_all_episodes():
777 if not episode.last_playback:
778 continue
779 if last_episode is None or \
780 episode.last_playback > last_episode.last_playback:
781 last_episode = episode
782 last_podcast = podcast
784 if last_episode is not None:
785 self.last_episode = self.wrap_episode(last_podcast, last_episode)
786 # FIXME: Send last episode to player
787 #self.select_episode(self.last_episode)
789 def on_episode_deleted(self, episode):
790 # Remove episode from play queue (if it's in there)
791 self.main.removeQueuedEpisode(episode)
793 # If the episode that has been deleted is currently
794 # being played back (or paused), stop playback now.
795 if self.main.currentEpisode == episode:
796 self.main.togglePlayback(None)
798 def enqueue_episode(self, episode):
799 self.main.enqueueEpisode(episode)
801 def run(self):
802 return self.app.exec_()
804 quit = Signal()
806 def on_quit(self):
807 # Make sure the audio playback is stopped immediately
808 self.main.togglePlayback(None)
809 self.save_pending_data()
810 self.view.hide()
811 self.core.shutdown()
812 self.app.quit()
814 do_show_message = Signal(unicode)
816 @Slot(unicode)
817 def on_show_message(self, message):
818 self.main.showMessage(message)
820 def show_message(self, message):
821 self.do_show_message.emit(message)
823 def show_input_dialog(self, message, value='', accept=_('OK'),
824 reject=_('Cancel'), is_text=True):
825 self.main.showInputDialog(message, value, accept, reject, is_text)
827 def open_context_menu(self, items):
828 self.main.openContextMenu(items)
830 do_start_progress = Signal(str)
832 @Slot(str)
833 def on_start_progress(self, text):
834 self.main.startProgress(text)
836 def start_progress(self, text=_('Please wait...')):
837 self.do_start_progress.emit(text)
839 do_end_progress = Signal()
841 @Slot()
842 def on_end_progress(self):
843 self.main.endProgress()
845 def end_progress(self):
846 self.do_end_progress.emit()
848 def resort_podcast_list(self):
849 self.podcast_model.sort()
851 def insert_podcast(self, podcast):
852 self.podcast_model.insert_object(podcast)
853 self.mygpo_client.on_subscribe([podcast.url])
854 self.mygpo_client.flush()
856 def remove_podcast(self, podcast):
857 # Remove queued episodes for this specific podcast
858 self.main.removeQueuedEpisodesForPodcast(podcast)
860 if self.main.currentEpisode is not None:
861 # If the currently-playing episode is in the podcast
862 # that is to be deleted, stop playback immediately.
863 if self.main.currentEpisode.qpodcast == podcast:
864 self.main.togglePlayback(None)
865 self.podcast_model.remove_object(podcast)
866 self.mygpo_client.on_unsubscribe([podcast.url])
867 self.mygpo_client.flush()
869 def load_podcasts(self):
870 podcasts = map(model.QPodcast, self.model.get_podcasts())
871 self.podcast_model.set_podcasts(self.db, podcasts)
873 def wrap_episode(self, podcast, episode):
874 try:
875 return self.active_episode_wrappers[episode.id]
876 except KeyError:
877 return model.QEpisode(self, podcast, episode)
879 def select_podcast(self, podcast):
880 if isinstance(podcast, model.QPodcast):
881 # Normal QPodcast instance
882 wrap = functools.partial(self.wrap_episode, podcast)
883 objects = podcast.get_all_episodes()
884 else:
885 # EpisodeSubsetView
886 wrap = lambda args: self.wrap_episode(*args)
887 objects = podcast.get_all_episodes_with_podcast()
889 self.episode_model.set_objects(map(wrap, objects))
890 self.main.state = 'episodes'
892 def save_pending_data(self):
893 current_ep = self.main.currentEpisode
894 if isinstance(current_ep, model.QEpisode):
895 current_ep.save()
897 def podcast_to_qpodcast(self, podcast):
898 podcasts = filter(lambda p: p._podcast == podcast,
899 self.podcast_model.get_podcasts())
900 assert len(podcasts) <= 1
901 if podcasts:
902 return podcasts[0]
903 return None
905 def extensions_podcast_update_cb(self, podcast):
906 logger.debug('extensions_podcast_update_cb(%s)', podcast)
907 try:
908 qpodcast = self.podcast_to_qpodcast(podcast)
909 if qpodcast is not None:
910 qpodcast.qupdate(
911 finished_callback=self.controller.update_subset_stats)
912 except Exception, e:
913 logger.exception('extensions_podcast_update_cb(%s): %s', podcast, e)
915 def extensions_episode_download_cb(self, episode):
916 logger.debug('extensions_episode_download_cb(%s)', episode)
917 try:
918 qpodcast = self.podcast_to_qpodcast(episode.channel)
919 qepisode = self.wrap_episode(qpodcast, episode)
920 self.controller.downloadEpisode(qepisode)
921 except Exception, e:
922 logger.exception('extensions_episode_download_cb(%s): %s', episode, e)
924 def main(args):
925 gui = qtPodder(args, core.Core())
926 return gui.run()