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
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
51 logger
= logging
.getLogger("qmlui")
54 EPISODE_LIST_FILTERS
= [
55 # (UI label, EQL expression)
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
)
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
)
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
):
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
}
140 def getVersion(self
):
141 return gpodder
.__version
__
144 def getReleased(self
):
145 return gpodder
.__date
__
147 @Slot(result
=unicode)
148 def getCredits(self
):
149 credits_file
= os
.path
.join(gpodder
.prefix
, 'share', 'gpodder', 'credits.txt')
150 return util
.convert_bytes(open(credits_file
).read())
152 @Slot(result
=unicode)
153 def getCopyright(self
):
154 return util
.convert_bytes(gpodder
.__copyright
__)
157 def getLicense(self
):
158 return gpodder
.__license
__
162 return gpodder
.__url
__
165 def loadLastEpisode(self
):
166 self
.root
.load_last_episode()
168 @Slot(QObject
, int, int)
169 def storePlaybackAction(self
, episode
, start
, end
):
171 logger
.info('Ignoring too short playback action.')
173 total
= episode
.qduration
174 self
.root
.mygpo_client
.on_playback_full(episode
, start
, end
, total
)
175 self
.root
.mygpo_client
.flush()
178 def playVideo(self
, episode
):
179 """Video Playback on MeeGo 1.2 Harmattan"""
182 self
.update_subset_stats()
184 url
= episode
.get_playback_url()
185 if gpodder
.ui
.harmattan
:
186 subprocess
.Popen(['video-suite', url
])
190 self
.root
.mygpo_client
.on_playback([episode
])
191 self
.root
.mygpo_client
.flush()
194 def podcastSelected(self
, podcast
):
195 self
.setEpisodeListTitle(podcast
.qtitle
)
196 self
.root
.select_podcast(podcast
)
198 windowTitleChanged
= Signal()
200 def getWindowTitle(self
):
201 return self
.root
.view
.windowTitle()
203 def setWindowTitle(self
, windowTitle
):
204 if gpodder
.ui
.fremantle
:
205 self
.root
.view
.setWindowTitle(windowTitle
)
207 windowTitle
= Property(unicode, getWindowTitle
,
208 setWindowTitle
, notify
=windowTitleChanged
)
211 def myGpoUploadList(self
):
212 def upload_proc(self
):
213 self
.root
.start_progress(_('Uploading subscriptions...'))
217 self
.root
.mygpo_client
.set_subscriptions([podcast
.url
218 for podcast
in self
.root
.podcast_model
.get_podcasts()])
220 self
.root
.show_message('\n'.join((_('Error on upload:'), unicode(e
))))
222 self
.root
.end_progress()
224 t
= threading
.Thread(target
=upload_proc
, args
=[self
])
228 def saveMyGpoSettings(self
):
229 # Update the device settings and upload changes
230 self
.root
.mygpo_client
.create_device()
231 self
.root
.mygpo_client
.flush(now
=True)
233 myGpoEnabledChanged
= Signal()
235 def getMyGpoEnabled(self
):
236 return self
.root
.config
.mygpo
.enabled
238 def setMyGpoEnabled(self
, enabled
):
239 self
.root
.config
.mygpo
.enabled
= enabled
241 myGpoEnabled
= Property(bool, getMyGpoEnabled
,
242 setMyGpoEnabled
, notify
=myGpoEnabledChanged
)
244 myGpoUsernameChanged
= Signal()
246 def getMyGpoUsername(self
):
247 return model
.convert(self
.root
.config
.mygpo
.username
)
249 def setMyGpoUsername(self
, username
):
250 self
.root
.config
.mygpo
.username
= username
252 myGpoUsername
= Property(unicode, getMyGpoUsername
,
253 setMyGpoUsername
, notify
=myGpoUsernameChanged
)
255 myGpoPasswordChanged
= Signal()
257 def getMyGpoPassword(self
):
258 return model
.convert(self
.root
.config
.mygpo
.password
)
260 def setMyGpoPassword(self
, password
):
261 self
.root
.config
.mygpo
.password
= password
263 myGpoPassword
= Property(unicode, getMyGpoPassword
,
264 setMyGpoPassword
, notify
=myGpoPasswordChanged
)
266 myGpoDeviceCaptionChanged
= Signal()
268 def getMyGpoDeviceCaption(self
):
269 return model
.convert(self
.root
.config
.mygpo
.device
.caption
)
271 def setMyGpoDeviceCaption(self
, caption
):
272 self
.root
.config
.mygpo
.device
.caption
= caption
274 myGpoDeviceCaption
= Property(unicode, getMyGpoDeviceCaption
,
275 setMyGpoDeviceCaption
, notify
=myGpoDeviceCaptionChanged
)
278 def podcastContextMenu(self
, podcast
):
281 if isinstance(podcast
, model
.EpisodeSubsetView
):
282 menu
.append(helper
.Action(_('Update all'), 'update-all', podcast
))
284 menu
.append(helper
.Action(_('Update'), 'update', podcast
))
285 menu
.append(helper
.Action(_('Mark episodes as old'), 'mark-as-read', podcast
))
286 menu
.append(helper
.Action(_('Rename'), 'rename-podcast', podcast
))
287 menu
.append(helper
.Action(_('Change section'), 'change-section', podcast
))
288 menu
.append(helper
.Action(_('Unsubscribe'), 'unsubscribe', podcast
))
290 #menu.append(helper.Action('Force update all', 'force-update-all', podcast))
291 #menu.append(helper.Action('Force update', 'force-update', podcast))
293 self
.show_context_menu(menu
)
295 def show_context_menu(self
, actions
):
296 if gpodder
.ui
.harmattan
:
297 actions
= filter(lambda a
: a
.caption
!= '', actions
)
298 self
.context_menu_actions
= actions
299 self
.root
.open_context_menu(self
.context_menu_actions
)
301 def update_subset_stats(self
):
302 # This should be called when an episode changes state,
303 # so that all subset views (e.g. "All episodes") can
304 # update its status (i.e. download/new counts, etc..)
305 for podcast
in self
.root
.podcast_model
.get_objects():
306 if isinstance(podcast
, model
.EpisodeSubsetView
):
309 def find_episode(self
, podcast_url
, episode_url
):
310 for podcast
in self
.root
.podcast_model
.get_podcasts():
311 if podcast
.url
== podcast_url
:
312 for episode
in podcast
.get_all_episodes():
313 if episode
.url
== episode_url
:
318 def updateAllPodcasts(self
):
319 # Process episode actions received from gpodder.net
320 def merge_proc(self
):
321 self
.root
.start_progress(_('Merging episode actions...'))
323 def find_episode(podcast_url
, episode_url
, counter
):
325 self
.root
.start_progress(_('Merging episode actions (%d)')
327 return self
.find_episode(podcast_url
, episode_url
)
330 d
= {'x': 0} # Used to "remember" the counter inside find_episode
331 self
.root
.mygpo_client
.process_episode_actions(lambda x
, y
:
332 find_episode(x
, y
, d
))
334 self
.root
.end_progress()
336 t
= threading
.Thread(target
=merge_proc
, args
=[self
])
339 for podcast
in self
.root
.podcast_model
.get_objects():
340 podcast
.qupdate(finished_callback
=self
.update_subset_stats
)
343 def contextMenuResponse(self
, index
):
344 assert index
< len(self
.context_menu_actions
)
345 action
= self
.context_menu_actions
[index
]
346 if action
.action
== 'update':
347 action
.target
.qupdate(finished_callback
=self
.update_subset_stats
)
348 elif action
.action
== 'force-update':
349 action
.target
.qupdate(force
=True, \
350 finished_callback
=self
.update_subset_stats
)
351 elif action
.action
== 'update-all':
352 self
.updateAllPodcasts()
353 elif action
.action
== 'force-update-all':
354 for podcast
in self
.root
.podcast_model
.get_objects():
355 podcast
.qupdate(force
=True, finished_callback
=self
.update_subset_stats
)
356 if action
.action
== 'unsubscribe':
358 action
.target
.remove_downloaded()
359 action
.target
.delete()
360 self
.root
.remove_podcast(action
.target
)
362 self
.confirm_action(_('Remove this podcast and episodes?'),
363 _('Unsubscribe'), unsubscribe
)
364 elif action
.action
== 'episode-toggle-new':
365 action
.target
.toggle_new()
366 self
.update_subset_stats()
367 elif action
.action
== 'episode-toggle-archive':
368 action
.target
.toggle_archive()
369 self
.update_subset_stats()
370 elif action
.action
== 'episode-delete':
371 self
.deleteEpisode(action
.target
)
372 elif action
.action
== 'episode-enqueue':
373 self
.root
.enqueue_episode(action
.target
)
374 elif action
.action
== 'mark-as-read':
375 for episode
in action
.target
.get_all_episodes():
376 if not episode
.was_downloaded(and_exists
=True):
377 episode
.mark(is_played
=True)
378 action
.target
.changed
.emit()
379 self
.update_subset_stats()
380 elif action
.action
== 'change-section':
381 def section_changer(podcast
):
382 section
= yield (_('New section name:'), podcast
.section
,
384 if section
and section
!= podcast
.section
:
385 podcast
.set_section(section
)
386 self
.root
.resort_podcast_list()
388 self
.start_input_dialog(section_changer(action
.target
))
389 elif action
.action
== 'rename-podcast':
390 def title_changer(podcast
):
391 title
= yield (_('New name:'), podcast
.title
,
393 if title
and title
!= podcast
.title
:
394 podcast
.rename(title
)
395 self
.root
.resort_podcast_list()
397 self
.start_input_dialog(title_changer(action
.target
))
399 def confirm_action(self
, message
, affirmative
, callback
):
400 def confirm(message
, affirmative
, callback
):
401 args
= (message
, '', affirmative
, _('Cancel'), False)
405 self
.start_input_dialog(confirm(message
, affirmative
, callback
))
407 def start_input_dialog(self
, generator
):
408 """Carry out an input dialog with the UI
410 This function takes a generator function as argument
411 which should yield a tuple of arguments for the
412 "show_input_dialog" function (i.e. message, default
413 value, accept and reject message - only the message
414 is mandatory, the other arguments have default values).
416 The generator will receive the user's response as a
417 result of the yield expression. If the user accepted
418 the dialog, a string is returned (the value that has
419 been input), otherwise None is returned.
424 result = yield ('A simple message', 'default value')
426 # user has rejected the dialog
428 # user has accepted, new value in "result"
430 start_input_dialog(some_function())
432 assert self
.current_input_dialog
is None
433 self
.current_input_dialog
= generator
434 args
= generator
.next()
435 self
.root
.show_input_dialog(*args
)
437 @Slot(bool, str, bool)
438 def inputDialogResponse(self
, accepted
, value
, is_text
):
445 self
.current_input_dialog
.send(value
)
446 except StopIteration:
447 # This is expected, as the generator
448 # should only have one yield statement
451 self
.current_input_dialog
= None
454 def downloadEpisode(self
, episode
):
455 episode
.qdownload(self
.root
.config
, self
.update_subset_stats
)
456 self
.root
.mygpo_client
.on_download([episode
])
457 self
.root
.mygpo_client
.flush()
460 def cancelDownload(self
, episode
):
461 episode
.download_task
.cancel()
462 episode
.download_task
.removed_from_list()
465 def deleteEpisode(self
, episode
):
467 episode
.delete_episode()
468 self
.update_subset_stats()
469 self
.root
.mygpo_client
.on_delete([episode
])
470 self
.root
.mygpo_client
.flush()
471 self
.root
.on_episode_deleted(episode
)
472 self
.root
.episode_model
.sort()
474 self
.confirm_action(_('Delete this episode?'), _('Delete'), delete
)
477 def acquireEpisode(self
, episode
):
478 self
.root
.add_active_episode(episode
)
481 def releaseEpisode(self
, episode
):
482 self
.root
.remove_active_episode(episode
)
485 def contextMenuClosed(self
):
486 self
.context_menu_actions
= []
489 def episodeContextMenu(self
, episode
):
492 toggle_new
= _('Mark as old') if episode
.is_new
else _('Mark as new')
493 menu
.append(helper
.Action(toggle_new
, 'episode-toggle-new', episode
))
495 toggle_archive
= _('Allow deletion') if episode
.archive
else _('Archive')
496 menu
.append(helper
.Action(toggle_archive
, 'episode-toggle-archive', episode
))
498 if episode
.state
!= gpodder
.STATE_DELETED
:
499 menu
.append(helper
.Action(_('Delete'), 'episode-delete', episode
))
501 menu
.append(helper
.Action(_('Add to play queue'), 'episode-enqueue', episode
))
503 self
.show_context_menu(menu
)
506 def addSubscriptions(self
, urls
):
507 def not_yet_subscribed(url
):
508 for podcast
in self
.root
.podcast_model
.get_objects():
509 if isinstance(podcast
, model
.EpisodeSubsetView
):
512 if podcast
.url
== url
:
513 logger
.info('Already subscribed: %s', url
)
518 urls
= map(util
.normalize_feed_url
, urls
)
519 urls
= filter(not_yet_subscribed
, urls
)
521 def subscribe_proc(self
, urls
):
522 self
.root
.start_progress(_('Adding podcasts...'))
524 for idx
, url
in enumerate(urls
):
526 self
.root
.start_progress(_('Adding podcasts...') + ' (%d/%d)' % (idx
, len(urls
)))
528 podcast
= self
.root
.model
.load_podcast(url
=url
, create
=True,
529 max_episodes
=self
.root
.config
.max_episodes_per_feed
)
531 self
.root
.insert_podcast(model
.QPodcast(podcast
))
533 logger
.warn('Cannot add pocast: %s', e
)
534 # XXX: Visual feedback in the QML UI
536 self
.root
.end_progress()
538 t
= threading
.Thread(target
=subscribe_proc
, args
=[self
, urls
])
542 def currentEpisodeChanging(self
):
543 self
.root
.save_pending_data()
547 self
.root
.quit
.emit()
551 if gpodder
.ui
.harmattan
:
552 self
.root
.view
.showMinimized()
553 elif gpodder
.ui
.fremantle
:
554 os
.system('dbus-send /com/nokia/hildon_desktop '+
555 'com.nokia.hildon_desktop.exit_app_view')
557 self
.root
.view
.showMinimized()
560 class gPodderListModel(QAbstractListModel
):
561 def __init__(self
, objects
=None):
562 QAbstractListModel
.__init
__(self
)
565 self
._objects
= objects
566 self
.setRoleNames({0: 'modelData', 1: 'section'})
569 # Unimplemented for the generic list model
572 def insert_object(self
, o
):
573 self
._objects
.append(o
)
576 def remove_object(self
, o
):
577 self
._objects
.remove(o
)
580 def set_objects(self
, objects
):
581 self
._objects
= objects
584 def get_objects(self
):
587 def get_object(self
, index
):
588 return self
._objects
[index
.row()]
590 def rowCount(self
, parent
=QModelIndex()):
591 return len(self
.get_objects())
593 def data(self
, index
, role
):
596 return self
.get_object(index
)
598 return self
.get_object(index
).qsection
601 class gPodderPodcastListModel(gPodderListModel
):
602 def set_podcasts(self
, db
, podcasts
):
604 model
.EpisodeSubsetView(db
, self
, _('All episodes'), ''),
606 self
.set_objects(views
+ podcasts
)
608 def get_podcasts(self
):
609 return filter(lambda podcast
: isinstance(podcast
, model
.QPodcast
),
613 self
._objects
= sorted(self
._objects
, key
=model
.QPodcast
.sort_key
)
616 class gPodderEpisodeListModel(gPodderListModel
):
617 def __init__(self
, config
):
618 gPodderListModel
.__init
__(self
)
619 self
._filter
= config
.ui
.qml
.state
.episode_list_filter
622 self
._config
= config
623 config
.add_observer(self
._on
_config
_changed
)
625 def _on_config_changed(self
, name
, old_value
, new_value
):
626 if name
== 'ui.qml.state.episode_list_filter':
627 self
._filter
= new_value
631 caption
, eql
= EPISODE_LIST_FILTERS
[self
._filter
]
634 self
._filtered
= self
._objects
637 match
= lambda episode
: eql
.match(episode
._episode
)
638 self
._filtered
= filter(match
, self
._objects
)
642 def get_objects(self
):
643 return self
._filtered
645 def get_object(self
, index
):
646 return self
._filtered
[index
.row()]
653 def setFilter(self
, filter_index
):
654 self
._config
.ui
.qml
.state
.episode_list_filter
= filter_index
658 for folder
in gpodder
.ui_folders
:
659 filename
= os
.path
.join(folder
, filename
)
660 if os
.path
.exists(filename
):
663 class DeclarativeView(QDeclarativeView
):
665 QDeclarativeView
.__init
__(self
)
666 self
.setAttribute(Qt
.WA_OpaquePaintEvent
)
667 self
.setAttribute(Qt
.WA_NoSystemBackground
)
668 self
.viewport().setAttribute(Qt
.WA_OpaquePaintEvent
)
669 self
.viewport().setAttribute(Qt
.WA_NoSystemBackground
)
673 def closeEvent(self
, event
):
677 class qtPodder(QObject
):
678 def __init__(self
, args
, gpodder_core
):
679 QObject
.__init
__(self
)
681 # Enable OpenGL rendering without requiring QtOpenGL
682 # On Harmattan we let the system choose the best graphicssystem
683 if '-graphicssystem' not in args
and not gpodder
.ui
.harmattan
:
684 if gpodder
.ui
.fremantle
:
685 args
+= ['-graphicssystem', 'opengl']
686 elif not gpodder
.win32
:
687 args
+= ['-graphicssystem', 'raster']
689 self
.app
= QApplication(args
)
690 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
691 self
.quit
.connect(self
.on_quit
)
693 self
.core
= gpodder_core
694 self
.config
= self
.core
.config
695 self
.db
= self
.core
.db
696 self
.model
= self
.core
.model
698 self
.config_proxy
= ConfigProxy(self
.config
)
700 # Initialize the gpodder.net client
701 self
.mygpo_client
= my
.MygPoClient(self
.config
)
703 gpodder
.user_extensions
.on_ui_initialized(self
.model
,
704 self
.extensions_podcast_update_cb
,
705 self
.extensions_episode_download_cb
)
707 self
.view
= DeclarativeView()
708 self
.view
.closing
.connect(self
.on_quit
)
709 self
.view
.setResizeMode(QDeclarativeView
.SizeRootObjectToView
)
711 self
.controller
= Controller(self
)
712 self
.media_buttons_handler
= helper
.MediaButtonsHandler()
713 self
.podcast_model
= gPodderPodcastListModel()
714 self
.episode_model
= gPodderEpisodeListModel(self
.config
)
715 self
.last_episode
= None
717 # A dictionary of episodes that are currently active
718 # in some way (i.e. playing back or downloading)
719 self
.active_episode_wrappers
= {}
721 engine
= self
.view
.engine()
723 # Maemo 5: Experimental Qt Mobility packages are installed in /opt
724 if gpodder
.ui
.fremantle
:
725 for path
in ('/opt/qtm11/imports', '/opt/qtm12/imports'):
726 engine
.addImportPath(path
)
728 for path
in (r
'C:\QtSDK\Desktop\Qt\4.7.4\msvc2008\imports',):
729 engine
.addImportPath(path
)
731 # Add the cover art image provider
732 self
.cover_provider
= images
.LocalCachedImageProvider()
733 engine
.addImageProvider('cover', self
.cover_provider
)
735 root_context
= self
.view
.rootContext()
736 root_context
.setContextProperty('controller', self
.controller
)
737 root_context
.setContextProperty('configProxy', self
.config_proxy
)
738 root_context
.setContextProperty('mediaButtonsHandler',
739 self
.media_buttons_handler
)
741 # Load the QML UI (this could take a while...)
742 self
.view
.setSource(QUrl
.fromLocalFile(QML('main_default.qml')))
744 # Proxy to the "main" QML object for direct access to Qt Properties
745 self
.main
= helper
.QObjectProxy(self
.view
.rootObject().property('main'))
747 self
.main
.podcastModel
= self
.podcast_model
748 self
.main
.episodeModel
= self
.episode_model
750 self
.view
.setWindowTitle('gPodder')
752 if gpodder
.ui
.harmattan
:
753 self
.view
.showFullScreen()
754 elif gpodder
.ui
.fremantle
:
755 self
.view
.setAttribute(Qt
.WA_Maemo5AutoOrientation
, True)
756 self
.view
.showFullScreen()
758 # On the Desktop, scale to fit my small laptop screen..
759 desktop
= self
.app
.desktop()
760 if desktop
.height() < 1000:
762 self
.view
.scale(FACTOR
, FACTOR
)
763 size
= self
.view
.size()
765 self
.view
.resize(size
)
768 self
.do_start_progress
.connect(self
.on_start_progress
)
769 self
.do_end_progress
.connect(self
.on_end_progress
)
770 self
.do_show_message
.connect(self
.on_show_message
)
774 def add_active_episode(self
, episode
):
775 self
.active_episode_wrappers
[episode
.id] = episode
776 episode
.episode_wrapper_refcount
+= 1
778 def remove_active_episode(self
, episode
):
779 episode
.episode_wrapper_refcount
-= 1
780 if episode
.episode_wrapper_refcount
== 0:
781 del self
.active_episode_wrappers
[episode
.id]
783 def load_last_episode(self
):
786 for podcast
in self
.podcast_model
.get_podcasts():
787 for episode
in podcast
.get_all_episodes():
788 if not episode
.last_playback
:
790 if last_episode
is None or \
791 episode
.last_playback
> last_episode
.last_playback
:
792 last_episode
= episode
793 last_podcast
= podcast
795 if last_episode
is not None:
796 self
.last_episode
= self
.wrap_episode(last_podcast
, last_episode
)
797 # FIXME: Send last episode to player
798 #self.select_episode(self.last_episode)
800 def on_episode_deleted(self
, episode
):
801 # Remove episode from play queue (if it's in there)
802 self
.main
.removeQueuedEpisode(episode
)
804 # If the episode that has been deleted is currently
805 # being played back (or paused), stop playback now.
806 if self
.main
.currentEpisode
== episode
:
807 self
.main
.togglePlayback(None)
809 def enqueue_episode(self
, episode
):
810 self
.main
.enqueueEpisode(episode
)
813 return self
.app
.exec_()
818 # Make sure the audio playback is stopped immediately
819 self
.main
.togglePlayback(None)
820 self
.save_pending_data()
825 do_show_message
= Signal(unicode)
828 def on_show_message(self
, message
):
829 self
.main
.showMessage(message
)
831 def show_message(self
, message
):
832 self
.do_show_message
.emit(message
)
834 def show_input_dialog(self
, message
, value
='', accept
=_('OK'),
835 reject
=_('Cancel'), is_text
=True):
836 self
.main
.showInputDialog(message
, value
, accept
, reject
, is_text
)
838 def open_context_menu(self
, items
):
839 self
.main
.openContextMenu(items
)
841 do_start_progress
= Signal(str)
844 def on_start_progress(self
, text
):
845 self
.main
.startProgress(text
)
847 def start_progress(self
, text
=_('Please wait...')):
848 self
.do_start_progress
.emit(text
)
850 do_end_progress
= Signal()
853 def on_end_progress(self
):
854 self
.main
.endProgress()
856 def end_progress(self
):
857 self
.do_end_progress
.emit()
859 def resort_podcast_list(self
):
860 self
.podcast_model
.sort()
862 def insert_podcast(self
, podcast
):
863 self
.podcast_model
.insert_object(podcast
)
864 self
.mygpo_client
.on_subscribe([podcast
.url
])
865 self
.mygpo_client
.flush()
867 def remove_podcast(self
, podcast
):
868 # Remove queued episodes for this specific podcast
869 self
.main
.removeQueuedEpisodesForPodcast(podcast
)
871 if self
.main
.currentEpisode
is not None:
872 # If the currently-playing episode is in the podcast
873 # that is to be deleted, stop playback immediately.
874 if self
.main
.currentEpisode
.qpodcast
== podcast
:
875 self
.main
.togglePlayback(None)
876 self
.podcast_model
.remove_object(podcast
)
877 self
.mygpo_client
.on_unsubscribe([podcast
.url
])
878 self
.mygpo_client
.flush()
880 def load_podcasts(self
):
881 podcasts
= map(model
.QPodcast
, self
.model
.get_podcasts())
882 self
.podcast_model
.set_podcasts(self
.db
, podcasts
)
884 def wrap_episode(self
, podcast
, episode
):
886 return self
.active_episode_wrappers
[episode
.id]
888 return model
.QEpisode(self
, podcast
, episode
)
890 def select_podcast(self
, podcast
):
891 if isinstance(podcast
, model
.QPodcast
):
892 # Normal QPodcast instance
893 wrap
= functools
.partial(self
.wrap_episode
, podcast
)
894 objects
= podcast
.get_all_episodes()
897 wrap
= lambda args
: self
.wrap_episode(*args
)
898 objects
= podcast
.get_all_episodes_with_podcast()
900 self
.episode_model
.set_objects(map(wrap
, objects
))
901 self
.main
.state
= 'episodes'
903 def save_pending_data(self
):
904 current_ep
= self
.main
.currentEpisode
905 if isinstance(current_ep
, model
.QEpisode
):
908 def podcast_to_qpodcast(self
, podcast
):
909 podcasts
= filter(lambda p
: p
._podcast
== podcast
,
910 self
.podcast_model
.get_podcasts())
911 assert len(podcasts
) <= 1
916 def extensions_podcast_update_cb(self
, podcast
):
917 logger
.debug('extensions_podcast_update_cb(%s)', podcast
)
919 qpodcast
= self
.podcast_to_qpodcast(podcast
)
920 if qpodcast
is not None:
922 finished_callback
=self
.controller
.update_subset_stats
)
924 logger
.exception('extensions_podcast_update_cb(%s): %s', podcast
, e
)
926 def extensions_episode_download_cb(self
, episode
):
927 logger
.debug('extensions_episode_download_cb(%s)', episode
)
929 qpodcast
= self
.podcast_to_qpodcast(episode
.channel
)
930 qepisode
= self
.wrap_episode(qpodcast
, episode
)
931 self
.controller
.downloadEpisode(qepisode
)
933 logger
.exception('extensions_episode_download_cb(%s): %s', episode
, e
)
936 gui
= qtPodder(args
, core
.Core())