Maemo 5: Don't resume finished episodes
[gpodder.git] / src / gpodder / gui.py
blob729bedd70f2bf6e2c2d68b05ceb1c7c0f42689e1
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 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 import os
21 import platform
22 import gtk
23 import gtk.gdk
24 import gobject
25 import pango
26 import random
27 import sys
28 import shutil
29 import subprocess
30 import glob
31 import time
32 import tempfile
33 import collections
34 import threading
35 import urllib
37 from xml.sax import saxutils
39 import gpodder
41 try:
42 import dbus
43 import dbus.service
44 import dbus.mainloop
45 import dbus.glib
46 except ImportError:
47 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
48 class dbus:
49 class SessionBus:
50 def __init__(self, *args, **kwargs):
51 pass
52 def add_signal_receiver(self, *args, **kwargs):
53 pass
54 class glib:
55 class DBusGMainLoop:
56 def __init__(self, *args, **kwargs):
57 pass
58 class service:
59 @staticmethod
60 def method(*args, **kwargs):
61 return lambda x: x
62 class BusName:
63 def __init__(self, *args, **kwargs):
64 pass
65 class Object:
66 def __init__(self, *args, **kwargs):
67 pass
70 from gpodder import feedcore
71 from gpodder import util
72 from gpodder import opml
73 from gpodder import download
74 from gpodder import my
75 from gpodder import youtube
76 from gpodder import player
77 from gpodder.liblogger import log
79 _ = gpodder.gettext
80 N_ = gpodder.ngettext
82 from gpodder.model import PodcastChannel
83 from gpodder.model import PodcastEpisode
84 from gpodder.dbsqlite import Database
86 from gpodder.gtkui.model import PodcastListModel
87 from gpodder.gtkui.model import EpisodeListModel
88 from gpodder.gtkui.config import UIConfig
89 from gpodder.gtkui.services import CoverDownloader
90 from gpodder.gtkui.widgets import SimpleMessageArea
91 from gpodder.gtkui.desktopfile import UserAppsReader
93 from gpodder.gtkui.draw import draw_text_box_centered
95 from gpodder.gtkui.interface.common import BuilderWidget
96 from gpodder.gtkui.interface.common import TreeViewHelper
97 from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
99 if gpodder.ui.desktop:
100 from gpodder.gtkui.download import DownloadStatusModel
102 from gpodder.gtkui.desktop.sync import gPodderSyncUI
104 from gpodder.gtkui.desktop.channel import gPodderChannel
105 from gpodder.gtkui.desktop.preferences import gPodderPreferences
106 from gpodder.gtkui.desktop.shownotes import gPodderShownotes
107 from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
108 from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
109 from gpodder.gtkui.desktop.dependencymanager import gPodderDependencyManager
110 from gpodder.gtkui.interface.progress import ProgressIndicator
111 try:
112 from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
113 have_trayicon = True
114 except Exception, exc:
115 log('Warning: Could not import gpodder.trayicon.', traceback=True)
116 log('Warning: This probably means your PyGTK installation is too old!')
117 have_trayicon = False
118 elif gpodder.ui.diablo:
119 from gpodder.gtkui.download import DownloadStatusModel
121 from gpodder.gtkui.maemo.channel import gPodderChannel
122 from gpodder.gtkui.maemo.preferences import gPodderPreferences
123 from gpodder.gtkui.maemo.shownotes import gPodderShownotes
124 from gpodder.gtkui.maemo.episodeselector import gPodderEpisodeSelector
125 from gpodder.gtkui.maemo.podcastdirectory import gPodderPodcastDirectory
126 from gpodder.gtkui.maemo.mygpodder import MygPodderSettings
127 from gpodder.gtkui.interface.progress import ProgressIndicator
128 have_trayicon = False
129 elif gpodder.ui.fremantle:
130 from gpodder.gtkui.frmntl.model import DownloadStatusModel
131 from gpodder.gtkui.frmntl.model import EpisodeListModel
132 from gpodder.gtkui.frmntl.model import PodcastListModel
134 from gpodder.gtkui.maemo.channel import gPodderChannel
135 from gpodder.gtkui.frmntl.preferences import gPodderPreferences
136 from gpodder.gtkui.frmntl.shownotes import gPodderShownotes
137 from gpodder.gtkui.frmntl.episodeselector import gPodderEpisodeSelector
138 from gpodder.gtkui.frmntl.podcastdirectory import gPodderPodcastDirectory
139 from gpodder.gtkui.frmntl.episodes import gPodderEpisodes
140 from gpodder.gtkui.frmntl.downloads import gPodderDownloads
141 from gpodder.gtkui.frmntl.progress import ProgressIndicator
142 from gpodder.gtkui.frmntl.widgets import FancyProgressBar
143 have_trayicon = False
145 from gpodder.gtkui.frmntl.portrait import FremantleRotation
146 from gpodder.gtkui.frmntl.mafw import MafwPlaybackMonitor
147 from gpodder.gtkui.frmntl.hints import HINT_STRINGS
149 from gpodder.gtkui.interface.common import Orientation
151 from gpodder.gtkui.interface.welcome import gPodderWelcome
153 if gpodder.ui.maemo:
154 import hildon
156 from gpodder.dbusproxy import DBusPodcastsProxy
157 from gpodder import hooks
159 class gPodder(BuilderWidget, dbus.service.Object):
160 finger_friendly_widgets = ['btnCleanUpDownloads', 'button_search_episodes_clear', 'label2', 'labelDownloads', 'btnUpdateFeeds']
162 ICON_GENERAL_ADD = 'general_add'
163 ICON_GENERAL_REFRESH = 'general_refresh'
165 # Delay until live search is started after typing stop
166 LIVE_SEARCH_DELAY = 200
168 def __init__(self, bus_name, config):
169 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
170 self.podcasts_proxy = DBusPodcastsProxy(lambda: self.channels, \
171 self.on_itemUpdate_activate, \
172 self.playback_episodes, \
173 self.download_episode_list, \
174 self.episode_object_by_uri, \
175 bus_name)
176 self.db = Database(gpodder.database_file)
177 self.config = config
178 BuilderWidget.__init__(self, None)
180 def new(self):
181 if gpodder.ui.diablo:
182 import hildon
183 self.app = hildon.Program()
184 self.app.add_window(self.main_window)
185 self.main_window.add_toolbar(self.toolbar)
186 menu = gtk.Menu()
187 for child in self.main_menu.get_children():
188 child.reparent(menu)
189 self.main_window.set_menu(self.set_finger_friendly(menu))
190 self._last_orientation = Orientation.LANDSCAPE
191 elif gpodder.ui.fremantle:
192 import hildon
193 self.app = hildon.Program()
194 self.app.add_window(self.main_window)
196 appmenu = hildon.AppMenu()
198 for filter in (self.item_view_podcasts_all, \
199 self.item_view_podcasts_downloaded, \
200 self.item_view_podcasts_unplayed):
201 button = gtk.ToggleButton()
202 filter.connect_proxy(button)
203 appmenu.add_filter(button)
205 for action in (self.itemPreferences, \
206 self.item_downloads, \
207 self.itemRemoveOldEpisodes, \
208 self.item_unsubscribe, \
209 self.itemAbout):
210 button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
211 hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
212 action.connect_proxy(button)
213 if action == self.item_downloads:
214 button.set_title(_('Downloads'))
215 button.set_value(_('Idle'))
216 self.button_downloads = button
217 appmenu.append(button)
219 def show_hint(button):
220 self.show_message(random.choice(HINT_STRINGS), important=True)
222 button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
223 hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
224 button.set_title(_('Hint of the day'))
225 button.connect('clicked', show_hint)
226 appmenu.append(button)
228 appmenu.show_all()
229 self.main_window.set_app_menu(appmenu)
231 # Initialize portrait mode / rotation manager
232 self._fremantle_rotation = FremantleRotation('gPodder', \
233 self.main_window, \
234 gpodder.__version__, \
235 self.config.rotation_mode)
237 if self.config.rotation_mode == FremantleRotation.ALWAYS:
238 util.idle_add(self.on_window_orientation_changed, \
239 Orientation.PORTRAIT)
240 self._last_orientation = Orientation.PORTRAIT
241 else:
242 self._last_orientation = Orientation.LANDSCAPE
244 # Flag set when a notification is being shown (Maemo bug 11235)
245 self._fremantle_notification_visible = False
246 else:
247 self._last_orientation = Orientation.LANDSCAPE
248 self.toolbar.set_property('visible', self.config.show_toolbar)
250 self.bluetooth_available = util.bluetooth_available()
252 self.config.connect_gtk_window(self.gPodder, 'main_window')
253 if not gpodder.ui.fremantle:
254 self.config.connect_gtk_paned('paned_position', self.channelPaned)
255 self.main_window.show()
257 self.player_receiver = player.MediaPlayerDBusReceiver(self.on_played)
259 if gpodder.ui.fremantle:
260 # Create a D-Bus monitoring object that takes care of
261 # tracking MAFW (Nokia Media Player) playback events
262 # and sends episode playback status events via D-Bus
263 self.mafw_monitor = MafwPlaybackMonitor(gpodder.dbus_session_bus)
265 self.gPodder.connect('key-press-event', self.on_key_press)
267 self.preferences_dialog = None
268 self.config.add_observer(self.on_config_changed)
270 self.tray_icon = None
271 self.episode_shownotes_window = None
272 self.new_episodes_window = None
274 if gpodder.ui.desktop:
275 # Mac OS X-specific UI tweaks: Native main menu integration
276 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
277 if getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
278 try:
279 import igemacintegration as igemi
281 # Move the menu bar from the window to the Mac menu bar
282 self.mainMenu.hide()
283 igemi.ige_mac_menu_set_menu_bar(self.mainMenu)
285 # Reparent some items to the "Application" menu
286 for widget in ('/mainMenu/menuHelp/itemAbout', \
287 '/mainMenu/menuPodcasts/itemPreferences'):
288 item = self.uimanager1.get_widget(widget)
289 group = igemi.ige_mac_menu_add_app_menu_group()
290 igemi.ige_mac_menu_add_app_menu_item(group, item, None)
292 quit_widget = '/mainMenu/menuPodcasts/itemQuit'
293 quit_item = self.uimanager1.get_widget(quit_widget)
294 igemi.ige_mac_menu_set_quit_menu_item(quit_item)
295 except ImportError:
296 print >>sys.stderr, """
297 Warning: ige-mac-integration not found - no native menus.
300 self.sync_ui = gPodderSyncUI(self.config, self.notification, \
301 self.main_window, self.show_confirmation, \
302 self.update_episode_list_icons, \
303 self.update_podcast_list_model, self.toolPreferences, \
304 gPodderEpisodeSelector, \
305 self.commit_changes_to_database)
306 else:
307 self.sync_ui = None
309 self.download_status_model = DownloadStatusModel()
310 self.download_queue_manager = download.DownloadQueueManager(self.config)
312 if gpodder.ui.desktop:
313 self.show_hide_tray_icon()
314 self.itemShowAllEpisodes.set_active(self.config.podcast_list_view_all)
315 self.itemShowToolbar.set_active(self.config.show_toolbar)
316 self.itemShowDescription.set_active(self.config.episode_list_descriptions)
318 if not gpodder.ui.fremantle:
319 self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
320 self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
321 self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
322 self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
324 # When the amount of maximum downloads changes, notify the queue manager
325 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_threads()
326 self.spinMaxDownloads.connect('value-changed', changed_cb)
328 self.default_title = 'gPodder'
329 if gpodder.__version__.rfind('git') != -1:
330 self.set_title('gPodder %s' % gpodder.__version__)
331 else:
332 title = self.gPodder.get_title()
333 if title is not None:
334 self.set_title(title)
335 else:
336 self.set_title(_('gPodder'))
338 self.cover_downloader = CoverDownloader()
340 # Generate list models for podcasts and their episodes
341 self.podcast_list_model = PodcastListModel(self.cover_downloader)
343 self.cover_downloader.register('cover-available', self.cover_download_finished)
344 self.cover_downloader.register('cover-removed', self.cover_file_removed)
346 if gpodder.ui.fremantle:
347 # Work around Maemo bug #4718
348 self.button_refresh.set_name('HildonButton-finger')
349 self.button_subscribe.set_name('HildonButton-finger')
351 self.button_refresh.set_sensitive(False)
352 self.button_subscribe.set_sensitive(False)
354 self.button_subscribe.set_image(gtk.image_new_from_icon_name(\
355 self.ICON_GENERAL_ADD, gtk.ICON_SIZE_BUTTON))
356 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
357 self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
359 # Make the button scroll together with the TreeView contents
360 action_area_box = self.treeChannels.get_action_area_box()
361 for child in self.buttonbox:
362 child.reparent(action_area_box)
363 self.vbox.remove(self.buttonbox)
364 self.treeChannels.set_action_area_visible(True)
366 # Set up a very nice progress bar setup
367 self.fancy_progress_bar = FancyProgressBar(self.main_window, \
368 self.on_btnCancelFeedUpdate_clicked)
369 self.pbFeedUpdate = self.fancy_progress_bar.progress_bar
370 self.pbFeedUpdate.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
371 self.vbox.pack_start(self.fancy_progress_bar.event_box, False)
373 from gpodder.gtkui.frmntl import style
374 sub_font = style.get_font_desc('SmallSystemFont')
375 sub_color = style.get_color('SecondaryTextColor')
376 sub = (sub_font.to_string(), sub_color.to_string())
377 sub = '<span font_desc="%s" foreground="%s">%%s</span>' % sub
378 self.label_footer.set_markup(sub % gpodder.__copyright__)
380 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
381 while gtk.events_pending():
382 gtk.main_iteration(False)
384 try:
385 # Try to get the real package version from dpkg
386 p = subprocess.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout=subprocess.PIPE)
387 version, _stderr = p.communicate()
388 del _stderr
389 del p
390 except:
391 version = gpodder.__version__
392 self.label_footer.set_markup(sub % ('v %s' % version))
393 self.label_footer.hide()
395 self.episodes_window = gPodderEpisodes(self.main_window, \
396 on_treeview_expose_event=self.on_treeview_expose_event, \
397 show_episode_shownotes=self.show_episode_shownotes, \
398 update_podcast_list_model=self.update_podcast_list_model, \
399 on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
400 item_view_episodes_all=self.item_view_episodes_all, \
401 item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
402 item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
403 item_view_episodes_undeleted=self.item_view_episodes_undeleted, \
404 on_entry_search_episodes_changed=self.on_entry_search_episodes_changed, \
405 on_entry_search_episodes_key_press=self.on_entry_search_episodes_key_press, \
406 hide_episode_search=self.hide_episode_search, \
407 on_itemUpdateChannel_activate=self.on_itemUpdateChannel_activate, \
408 playback_episodes=self.playback_episodes, \
409 delete_episode_list=self.delete_episode_list, \
410 episode_list_status_changed=self.episode_list_status_changed, \
411 download_episode_list=self.download_episode_list, \
412 episode_is_downloading=self.episode_is_downloading, \
413 show_episode_in_download_manager=self.show_episode_in_download_manager, \
414 add_download_task_monitor=self.add_download_task_monitor, \
415 remove_download_task_monitor=self.remove_download_task_monitor, \
416 for_each_episode_set_task_status=self.for_each_episode_set_task_status, \
417 on_itemUpdate_activate=self.on_itemUpdate_activate, \
418 show_delete_episodes_window=self.show_delete_episodes_window, \
419 cover_downloader=self.cover_downloader)
421 # Expose objects for episode list type-ahead find
422 self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
423 self.entry_search_episodes = self.episodes_window.entry_search_episodes
424 self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
426 self.downloads_window = gPodderDownloads(self.main_window, \
427 on_treeview_expose_event=self.on_treeview_expose_event, \
428 cleanup_downloads=self.cleanup_downloads, \
429 _for_each_task_set_status=self._for_each_task_set_status, \
430 downloads_list_get_selection=self.downloads_list_get_selection, \
431 _config=self.config)
433 self.treeAvailable = self.episodes_window.treeview
434 self.treeDownloads = self.downloads_window.treeview
436 # Source IDs for timeouts for search-as-you-type
437 self._podcast_list_search_timeout = None
438 self._episode_list_search_timeout = None
440 # Init the treeviews that we use
441 self.init_podcast_list_treeview()
442 self.init_episode_list_treeview()
443 self.init_download_list_treeview()
445 if self.config.podcast_list_hide_boring:
446 self.item_view_hide_boring_podcasts.set_active(True)
448 self.currently_updating = False
450 if gpodder.ui.maemo or self.config.enable_fingerscroll:
451 self.context_menu_mouse_button = 1
452 else:
453 self.context_menu_mouse_button = 3
455 if self.config.start_iconified:
456 self.iconify_main_window()
458 self.download_tasks_seen = set()
459 self.download_list_update_enabled = False
460 self.download_task_monitors = set()
462 # Subscribed channels
463 self.active_channel = None
464 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
465 self.channel_list_changed = True
466 self.update_podcasts_tab()
468 # load list of user applications for audio playback
469 self.user_apps_reader = UserAppsReader(['audio', 'video'])
470 threading.Thread(target=self.user_apps_reader.read).start()
472 # Set the "Device" menu item for the first time
473 if gpodder.ui.desktop:
474 self.update_item_device()
476 # Set up the first instance of MygPoClient
477 self.mygpo_client = my.MygPoClient(self.config)
479 # Now, update the feed cache, when everything's in place
480 if not gpodder.ui.fremantle:
481 self.btnUpdateFeeds.show()
482 self.updating_feed_cache = False
483 self.feed_cache_update_cancelled = False
484 self.update_feed_cache(force_update=self.config.update_on_startup)
486 self.message_area = None
488 def find_partial_downloads():
489 # Look for partial file downloads
490 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
491 count = len(partial_files)
492 resumable_episodes = []
493 if count:
494 if not gpodder.ui.fremantle:
495 util.idle_add(self.wNotebook.set_current_page, 1)
496 indicator = ProgressIndicator(_('Loading incomplete downloads'), \
497 _('Some episodes have not finished downloading in a previous session.'), \
498 False, self.get_dialog_parent())
499 indicator.on_message(N_('%(count)d partial file', '%(count)d partial files', count) % {'count':count})
501 candidates = [f[:-len('.partial')] for f in partial_files]
502 found = 0
504 for c in self.channels:
505 for e in c.get_all_episodes():
506 filename = e.local_filename(create=False, check_only=True)
507 if filename in candidates:
508 log('Found episode: %s', e.title, sender=self)
509 found += 1
510 indicator.on_message(e.title)
511 indicator.on_progress(float(found)/count)
512 candidates.remove(filename)
513 partial_files.remove(filename+'.partial')
514 resumable_episodes.append(e)
516 if not candidates:
517 break
519 if not candidates:
520 break
522 for f in partial_files:
523 log('Partial file without episode: %s', f, sender=self)
524 util.delete_file(f)
526 util.idle_add(indicator.on_finished)
528 if len(resumable_episodes):
529 def offer_resuming():
530 self.download_episode_list_paused(resumable_episodes)
531 if not gpodder.ui.fremantle:
532 resume_all = gtk.Button(_('Resume all'))
533 #resume_all.set_border_width(0)
534 def on_resume_all(button):
535 selection = self.treeDownloads.get_selection()
536 selection.select_all()
537 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
538 selection.unselect_all()
539 self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
540 self.message_area.hide()
541 resume_all.connect('clicked', on_resume_all)
543 self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
544 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
545 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
546 self.message_area.show_all()
547 self.clean_up_downloads(delete_partial=False)
548 util.idle_add(offer_resuming)
549 elif not gpodder.ui.fremantle:
550 util.idle_add(self.wNotebook.set_current_page, 0)
551 else:
552 util.idle_add(self.clean_up_downloads, True)
553 threading.Thread(target=find_partial_downloads).start()
555 # Start the auto-update procedure
556 self._auto_update_timer_source_id = None
557 if self.config.auto_update_feeds:
558 self.restart_auto_update_timer()
560 # Delete old episodes if the user wishes to
561 if self.config.auto_remove_played_episodes and \
562 self.config.episode_old_age > 0:
563 old_episodes = list(self.get_expired_episodes())
564 if len(old_episodes) > 0:
565 self.delete_episode_list(old_episodes, confirm=False)
566 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
568 if gpodder.ui.fremantle:
569 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
570 self.button_refresh.set_sensitive(True)
571 self.button_subscribe.set_sensitive(True)
572 self.main_window.set_title(_('gPodder'))
573 hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
575 # Do the initial sync with the web service
576 util.idle_add(self.mygpo_client.flush, True)
578 # First-time users should be asked if they want to see the OPML
579 if not self.channels and not gpodder.ui.fremantle:
580 util.idle_add(self.on_itemUpdate_activate)
582 def episode_object_by_uri(self, uri):
583 """Get an episode object given a local or remote URI
585 This can be used to quickly access an episode object
586 when all we have is its download filename or episode
587 URL (e.g. from external D-Bus calls / signals, etc..)
589 if uri.startswith('/'):
590 uri = 'file://' + uri
592 prefix = 'file://' + self.config.download_dir
594 if uri.startswith(prefix):
595 # File is on the local filesystem in the download folder
596 filename = urllib.unquote(uri[len(prefix):])
597 file_parts = [x for x in filename.split(os.sep) if x]
599 if len(file_parts) == 2:
600 dir_name, filename = file_parts
601 channels = [c for c in self.channels if c.foldername == dir_name]
602 if len(channels) == 1:
603 channel = channels[0]
604 return channel.get_episode_by_filename(filename)
605 else:
606 # Possibly remote file - search the database for a podcast
607 channel_id = self.db.get_channel_id_from_episode_url(uri)
609 if channel_id is not None:
610 channels = [c for c in self.channels if c.id == channel_id]
611 if len(channels) == 1:
612 channel = channels[0]
613 return channel.get_episode_by_url(uri)
615 return None
617 def on_played(self, start, end, total, file_uri):
618 """Handle the "played" signal from a media player"""
619 if start == 0 and end == 0 and total == 0:
620 # Ignore bogus play event
621 return
622 elif end < start + 5:
623 # Ignore "less than five seconds" segments,
624 # as they can happen with seeking, etc...
625 return
627 log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
628 episode = self.episode_object_by_uri(file_uri)
630 if episode is not None:
631 file_type = episode.file_type()
632 # Automatically enable D-Bus played status mode
633 if file_type == 'audio':
634 self.config.audio_played_dbus = True
635 elif file_type == 'video':
636 self.config.video_played_dbus = True
638 now = time.time()
639 if total > 0:
640 episode.total_time = total
641 elif total == 0:
642 # Assume the episode's total time for the action
643 total = episode.total_time
644 if episode.current_position_updated is None or \
645 now > episode.current_position_updated:
646 episode.current_position = end
647 episode.current_position_updated = now
648 episode.mark(is_played=True)
649 episode.save()
650 self.db.commit()
651 self.update_episode_list_icons([episode.url])
652 self.update_podcast_list_model([episode.channel.url])
654 # Submit this action to the webservice
655 self.mygpo_client.on_playback_full(episode, \
656 start, end, total)
658 def on_add_remove_podcasts_mygpo(self):
659 actions = self.mygpo_client.get_received_actions()
660 if not actions:
661 return False
663 existing_urls = [c.url for c in self.channels]
665 # Columns for the episode selector window - just one...
666 columns = (
667 ('description', None, None, _('Action')),
670 # A list of actions that have to be chosen from
671 changes = []
673 # Actions that are ignored (already carried out)
674 ignored = []
676 for action in actions:
677 if action.is_add and action.url not in existing_urls:
678 changes.append(my.Change(action))
679 elif action.is_remove and action.url in existing_urls:
680 podcast_object = None
681 for podcast in self.channels:
682 if podcast.url == action.url:
683 podcast_object = podcast
684 break
685 changes.append(my.Change(action, podcast_object))
686 else:
687 log('Ignoring action: %s', action, sender=self)
688 ignored.append(action)
690 # Confirm all ignored changes
691 self.mygpo_client.confirm_received_actions(ignored)
693 def execute_podcast_actions(selected):
694 add_list = [c.action.url for c in selected if c.action.is_add]
695 remove_list = [c.podcast for c in selected if c.action.is_remove]
697 # Apply the accepted changes locally
698 self.add_podcast_list(add_list)
699 self.remove_podcast_list(remove_list, confirm=False)
701 # All selected items are now confirmed
702 self.mygpo_client.confirm_received_actions(c.action for c in selected)
704 # Revert the changes on the server
705 rejected = [c.action for c in changes if c not in selected]
706 self.mygpo_client.reject_received_actions(rejected)
708 def ask():
709 # We're abusing the Episode Selector again ;) -- thp
710 gPodderEpisodeSelector(self.main_window, \
711 title=_('Confirm changes from gpodder.net'), \
712 instructions=_('Select the actions you want to carry out.'), \
713 episodes=changes, \
714 columns=columns, \
715 size_attribute=None, \
716 stock_ok_button=gtk.STOCK_APPLY, \
717 callback=execute_podcast_actions, \
718 _config=self.config)
720 # There are some actions that need the user's attention
721 if changes:
722 util.idle_add(ask)
723 return True
725 # We have no remaining actions - no selection happens
726 return False
728 def rewrite_urls_mygpo(self):
729 # Check if we have to rewrite URLs since the last add
730 rewritten_urls = self.mygpo_client.get_rewritten_urls()
732 for rewritten_url in rewritten_urls:
733 if not rewritten_url.new_url:
734 continue
736 for channel in self.channels:
737 if channel.url == rewritten_url.old_url:
738 log('Updating URL of %s to %s', channel, \
739 rewritten_url.new_url, sender=self)
740 channel.url = rewritten_url.new_url
741 channel.save()
742 self.channel_list_changed = True
743 util.idle_add(self.update_episode_list_model)
744 break
746 def on_send_full_subscriptions(self):
747 # Send the full subscription list to the gpodder.net client
748 # (this will overwrite the subscription list on the server)
749 indicator = ProgressIndicator(_('Uploading subscriptions'), \
750 _('Your subscriptions are being uploaded to the server.'), \
751 False, self.get_dialog_parent())
753 try:
754 self.mygpo_client.set_subscriptions([c.url for c in self.channels])
755 util.idle_add(self.show_message, _('List uploaded successfully.'))
756 except Exception, e:
757 def show_error(e):
758 message = str(e)
759 if not message:
760 message = e.__class__.__name__
761 self.show_message(message, \
762 _('Error while uploading'), \
763 important=True)
764 util.idle_add(show_error, e)
766 util.idle_add(indicator.on_finished)
768 def on_podcast_selected(self, treeview, path, column):
769 # for Maemo 5's UI
770 model = treeview.get_model()
771 channel = model.get_value(model.get_iter(path), \
772 PodcastListModel.C_CHANNEL)
773 self.active_channel = channel
774 self.update_episode_list_model()
775 self.episodes_window.channel = self.active_channel
776 self.episodes_window.show()
778 def on_button_subscribe_clicked(self, button):
779 self.on_itemImportChannels_activate(button)
781 def on_button_downloads_clicked(self, widget):
782 self.downloads_window.show()
784 def show_episode_in_download_manager(self, episode):
785 self.downloads_window.show()
786 model = self.treeDownloads.get_model()
787 selection = self.treeDownloads.get_selection()
788 selection.unselect_all()
789 it = model.get_iter_first()
790 while it is not None:
791 task = model.get_value(it, DownloadStatusModel.C_TASK)
792 if task.episode.url == episode.url:
793 selection.select_iter(it)
794 # FIXME: Scroll to selection in pannable area
795 break
796 it = model.iter_next(it)
798 def for_each_episode_set_task_status(self, episodes, status):
799 episode_urls = set(episode.url for episode in episodes)
800 model = self.treeDownloads.get_model()
801 selected_tasks = [(gtk.TreeRowReference(model, row.path), \
802 model.get_value(row.iter, \
803 DownloadStatusModel.C_TASK)) for row in model \
804 if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
805 in episode_urls]
806 self._for_each_task_set_status(selected_tasks, status)
808 def on_window_orientation_changed(self, orientation):
809 self._last_orientation = orientation
810 if self.preferences_dialog is not None:
811 self.preferences_dialog.on_window_orientation_changed(orientation)
813 treeview = self.treeChannels
814 if orientation == Orientation.PORTRAIT:
815 treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
816 # Work around Maemo bug #4718
817 self.button_subscribe.set_name('HildonButton-thumb')
818 self.button_refresh.set_name('HildonButton-thumb')
819 else:
820 treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
821 # Work around Maemo bug #4718
822 self.button_subscribe.set_name('HildonButton-finger')
823 self.button_refresh.set_name('HildonButton-finger')
825 if gpodder.ui.fremantle:
826 self.fancy_progress_bar.relayout()
828 def on_treeview_podcasts_selection_changed(self, selection):
829 model, iter = selection.get_selected()
830 if iter is None:
831 self.active_channel = None
832 self.episode_list_model.clear()
834 def on_treeview_button_pressed(self, treeview, event):
835 if event.window != treeview.get_bin_window():
836 return False
838 TreeViewHelper.save_button_press_event(treeview, event)
840 if getattr(treeview, TreeViewHelper.ROLE) == \
841 TreeViewHelper.ROLE_PODCASTS:
842 return self.currently_updating
844 return event.button == self.context_menu_mouse_button and \
845 gpodder.ui.desktop
847 def on_treeview_podcasts_button_released(self, treeview, event):
848 if event.window != treeview.get_bin_window():
849 return False
851 if gpodder.ui.maemo:
852 return self.treeview_channels_handle_gestures(treeview, event)
853 return self.treeview_channels_show_context_menu(treeview, event)
855 def on_treeview_episodes_button_released(self, treeview, event):
856 if event.window != treeview.get_bin_window():
857 return False
859 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
860 return self.treeview_available_handle_gestures(treeview, event)
862 return self.treeview_available_show_context_menu(treeview, event)
864 def on_treeview_downloads_button_released(self, treeview, event):
865 if event.window != treeview.get_bin_window():
866 return False
868 return self.treeview_downloads_show_context_menu(treeview, event)
870 def on_entry_search_podcasts_changed(self, editable):
871 if self.hbox_search_podcasts.get_property('visible'):
872 def set_search_term(self, text):
873 self.podcast_list_model.set_search_term(text)
874 self._podcast_list_search_timeout = None
875 return False
877 if self._podcast_list_search_timeout is not None:
878 gobject.source_remove(self._podcast_list_search_timeout)
879 self._podcast_list_search_timeout = gobject.timeout_add(\
880 self.LIVE_SEARCH_DELAY, \
881 set_search_term, self, editable.get_chars(0, -1))
883 def on_entry_search_podcasts_key_press(self, editable, event):
884 if event.keyval == gtk.keysyms.Escape:
885 self.hide_podcast_search()
886 return True
888 def hide_podcast_search(self, *args):
889 if self._podcast_list_search_timeout is not None:
890 gobject.source_remove(self._podcast_list_search_timeout)
891 self._podcast_list_search_timeout = None
892 self.hbox_search_podcasts.hide()
893 self.entry_search_podcasts.set_text('')
894 self.podcast_list_model.set_search_term(None)
895 self.treeChannels.grab_focus()
897 def show_podcast_search(self, input_char):
898 self.hbox_search_podcasts.show()
899 self.entry_search_podcasts.insert_text(input_char, -1)
900 self.entry_search_podcasts.grab_focus()
901 self.entry_search_podcasts.set_position(-1)
903 def init_podcast_list_treeview(self):
904 # Set up podcast channel tree view widget
905 if gpodder.ui.fremantle:
906 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
907 self.item_view_podcasts_downloaded.set_active(True)
908 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
909 self.item_view_podcasts_unplayed.set_active(True)
910 else:
911 self.item_view_podcasts_all.set_active(True)
912 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
914 iconcolumn = gtk.TreeViewColumn('')
915 iconcell = gtk.CellRendererPixbuf()
916 iconcolumn.pack_start(iconcell, False)
917 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
918 self.treeChannels.append_column(iconcolumn)
920 namecolumn = gtk.TreeViewColumn('')
921 namecell = gtk.CellRendererText()
922 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
923 namecolumn.pack_start(namecell, True)
924 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
926 if gpodder.ui.fremantle:
927 countcell = gtk.CellRendererText()
928 from gpodder.gtkui.frmntl import style
929 countcell.set_property('font-desc', style.get_font_desc('EmpSystemFont'))
930 countcell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
931 countcell.set_property('alignment', pango.ALIGN_RIGHT)
932 countcell.set_property('xalign', 1.)
933 countcell.set_property('xpad', 5)
934 namecolumn.pack_start(countcell, False)
935 namecolumn.add_attribute(countcell, 'text', PodcastListModel.C_DOWNLOADS)
936 namecolumn.add_attribute(countcell, 'visible', PodcastListModel.C_DOWNLOADS)
937 else:
938 iconcell = gtk.CellRendererPixbuf()
939 iconcell.set_property('xalign', 1.0)
940 namecolumn.pack_start(iconcell, False)
941 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
942 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
944 self.treeChannels.append_column(namecolumn)
946 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
948 # When no podcast is selected, clear the episode list model
949 selection = self.treeChannels.get_selection()
950 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
952 # Set up type-ahead find for the podcast list
953 def on_key_press(treeview, event):
954 if event.keyval == gtk.keysyms.Escape:
955 self.hide_podcast_search()
956 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
957 self.hide_podcast_search()
958 elif event.state & gtk.gdk.CONTROL_MASK:
959 # Don't handle type-ahead when control is pressed (so shortcuts
960 # with the Ctrl key still work, e.g. Ctrl+A, ...)
961 return True
962 else:
963 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
964 if unicode_char_id == 0:
965 return False
966 input_char = unichr(unicode_char_id)
967 self.show_podcast_search(input_char)
968 return True
969 self.treeChannels.connect('key-press-event', on_key_press)
971 # Enable separators to the podcast list to separate special podcasts
972 # from others (this is used for the "all episodes" view)
973 self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
975 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
977 def on_entry_search_episodes_changed(self, editable):
978 if self.hbox_search_episodes.get_property('visible'):
979 def set_search_term(self, text):
980 self.episode_list_model.set_search_term(text)
981 self._episode_list_search_timeout = None
982 return False
984 if self._episode_list_search_timeout is not None:
985 gobject.source_remove(self._episode_list_search_timeout)
986 self._episode_list_search_timeout = gobject.timeout_add(\
987 self.LIVE_SEARCH_DELAY, \
988 set_search_term, self, editable.get_chars(0, -1))
990 def on_entry_search_episodes_key_press(self, editable, event):
991 if event.keyval == gtk.keysyms.Escape:
992 self.hide_episode_search()
993 return True
995 def hide_episode_search(self, *args):
996 if self._episode_list_search_timeout is not None:
997 gobject.source_remove(self._episode_list_search_timeout)
998 self._episode_list_search_timeout = None
999 self.hbox_search_episodes.hide()
1000 self.entry_search_episodes.set_text('')
1001 self.episode_list_model.set_search_term(None)
1002 self.treeAvailable.grab_focus()
1004 def show_episode_search(self, input_char):
1005 self.hbox_search_episodes.show()
1006 self.entry_search_episodes.insert_text(input_char, -1)
1007 self.entry_search_episodes.grab_focus()
1008 self.entry_search_episodes.set_position(-1)
1010 def init_episode_list_treeview(self):
1011 # For loading the list model
1012 self.episode_list_model = EpisodeListModel(self.on_episode_list_filter_changed)
1014 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
1015 self.item_view_episodes_undeleted.set_active(True)
1016 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
1017 self.item_view_episodes_downloaded.set_active(True)
1018 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
1019 self.item_view_episodes_unplayed.set_active(True)
1020 else:
1021 self.item_view_episodes_all.set_active(True)
1023 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
1025 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
1027 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
1029 iconcell = gtk.CellRendererPixbuf()
1030 iconcell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1031 if gpodder.ui.maemo:
1032 iconcell.set_fixed_size(50, 50)
1033 else:
1034 iconcell.set_fixed_size(40, -1)
1036 namecell = gtk.CellRendererText()
1037 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
1038 namecolumn = gtk.TreeViewColumn(_('Episode'))
1039 namecolumn.pack_start(iconcell, False)
1040 namecolumn.add_attribute(iconcell, 'icon-name', EpisodeListModel.C_STATUS_ICON)
1041 namecolumn.pack_start(namecell, True)
1042 namecolumn.add_attribute(namecell, 'markup', EpisodeListModel.C_DESCRIPTION)
1043 if gpodder.ui.fremantle:
1044 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1045 else:
1046 namecolumn.set_sort_column_id(EpisodeListModel.C_DESCRIPTION)
1047 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1048 namecolumn.set_resizable(True)
1049 namecolumn.set_expand(True)
1051 if gpodder.ui.fremantle:
1052 from gpodder.gtkui.frmntl import style
1053 timecell = gtk.CellRendererText()
1054 timecell.set_property('font-desc', style.get_font_desc('SmallSystemFont'))
1055 timecell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
1056 timecell.set_property('alignment', pango.ALIGN_RIGHT)
1057 timecell.set_property('xalign', 1.)
1058 timecell.set_property('xpad', 5)
1059 timecell.set_property('yalign', .85)
1060 namecolumn.pack_start(timecell, False)
1061 namecolumn.add_attribute(timecell, 'text', EpisodeListModel.C_TIME)
1062 namecolumn.add_attribute(timecell, 'visible', EpisodeListModel.C_TIME_VISIBLE)
1064 lockcell = gtk.CellRendererPixbuf()
1065 lockcell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1066 if gpodder.ui.fremantle:
1067 lockcell.set_property('icon-name', 'general_locked')
1068 else:
1069 lockcell.set_property('icon-name', 'emblem-readonly')
1071 namecolumn.pack_start(lockcell, False)
1072 namecolumn.add_attribute(lockcell, 'visible', EpisodeListModel.C_LOCKED)
1074 sizecell = gtk.CellRendererText()
1075 sizecell.set_property('xalign', 1)
1076 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
1077 sizecolumn.set_sort_column_id(EpisodeListModel.C_FILESIZE)
1079 releasecell = gtk.CellRendererText()
1080 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
1081 releasecolumn.set_sort_column_id(EpisodeListModel.C_PUBLISHED)
1083 namecolumn.set_reorderable(True)
1084 self.treeAvailable.append_column(namecolumn)
1086 if not gpodder.ui.maemo:
1087 for itemcolumn in (sizecolumn, releasecolumn):
1088 itemcolumn.set_reorderable(True)
1089 self.treeAvailable.append_column(itemcolumn)
1091 # Set up type-ahead find for the episode list
1092 def on_key_press(treeview, event):
1093 if event.keyval == gtk.keysyms.Escape:
1094 self.hide_episode_search()
1095 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
1096 self.hide_episode_search()
1097 elif event.state & gtk.gdk.CONTROL_MASK:
1098 # Don't handle type-ahead when control is pressed (so shortcuts
1099 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1100 return False
1101 else:
1102 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
1103 if unicode_char_id == 0:
1104 return False
1105 input_char = unichr(unicode_char_id)
1106 self.show_episode_search(input_char)
1107 return True
1108 self.treeAvailable.connect('key-press-event', on_key_press)
1110 if gpodder.ui.desktop and not self.config.enable_fingerscroll:
1111 self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
1112 (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
1113 def drag_data_get(tree, context, selection_data, info, timestamp):
1114 if self.config.on_drag_mark_played:
1115 for episode in self.get_selected_episodes():
1116 episode.mark(is_played=True)
1117 self.on_selected_episodes_status_changed()
1118 uris = ['file://'+e.local_filename(create=False) \
1119 for e in self.get_selected_episodes() \
1120 if e.was_downloaded(and_exists=True)]
1121 uris.append('') # for the trailing '\r\n'
1122 selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
1123 self.treeAvailable.connect('drag-data-get', drag_data_get)
1125 selection = self.treeAvailable.get_selection()
1126 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
1127 selection.set_mode(gtk.SELECTION_SINGLE)
1128 elif gpodder.ui.fremantle:
1129 selection.set_mode(gtk.SELECTION_SINGLE)
1130 else:
1131 selection.set_mode(gtk.SELECTION_MULTIPLE)
1132 # Update the sensitivity of the toolbar buttons on the Desktop
1133 selection.connect('changed', lambda s: self.play_or_download())
1135 if gpodder.ui.diablo:
1136 # Set up the tap-and-hold context menu for podcasts
1137 menu = gtk.Menu()
1138 menu.append(self.itemUpdateChannel.create_menu_item())
1139 menu.append(self.itemEditChannel.create_menu_item())
1140 menu.append(gtk.SeparatorMenuItem())
1141 menu.append(self.itemRemoveChannel.create_menu_item())
1142 menu.append(gtk.SeparatorMenuItem())
1143 item = gtk.ImageMenuItem(_('Close this menu'))
1144 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
1145 gtk.ICON_SIZE_MENU))
1146 menu.append(item)
1147 menu.show_all()
1148 menu = self.set_finger_friendly(menu)
1149 self.treeChannels.tap_and_hold_setup(menu)
1152 def init_download_list_treeview(self):
1153 # enable multiple selection support
1154 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1155 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1157 # columns and renderers for "download progress" tab
1158 # First column: [ICON] Episodename
1159 column = gtk.TreeViewColumn(_('Episode'))
1161 cell = gtk.CellRendererPixbuf()
1162 if gpodder.ui.maemo:
1163 cell.set_fixed_size(50, 50)
1164 cell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1165 column.pack_start(cell, expand=False)
1166 column.add_attribute(cell, 'icon-name', \
1167 DownloadStatusModel.C_ICON_NAME)
1169 cell = gtk.CellRendererText()
1170 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1171 column.pack_start(cell, expand=True)
1172 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1173 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1174 column.set_expand(True)
1175 self.treeDownloads.append_column(column)
1177 # Second column: Progress
1178 cell = gtk.CellRendererProgress()
1179 cell.set_property('yalign', .5)
1180 cell.set_property('ypad', 6)
1181 column = gtk.TreeViewColumn(_('Progress'), cell,
1182 value=DownloadStatusModel.C_PROGRESS, \
1183 text=DownloadStatusModel.C_PROGRESS_TEXT)
1184 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1185 column.set_expand(False)
1186 self.treeDownloads.append_column(column)
1187 if gpodder.ui.maemo:
1188 column.set_property('min-width', 200)
1189 column.set_property('max-width', 200)
1190 else:
1191 column.set_property('min-width', 150)
1192 column.set_property('max-width', 150)
1194 self.treeDownloads.set_model(self.download_status_model)
1195 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1197 def on_treeview_expose_event(self, treeview, event):
1198 if event.window == treeview.get_bin_window():
1199 model = treeview.get_model()
1200 if (model is not None and model.get_iter_first() is not None):
1201 return False
1203 role = getattr(treeview, TreeViewHelper.ROLE, None)
1204 if role is None:
1205 return False
1207 ctx = event.window.cairo_create()
1208 ctx.rectangle(event.area.x, event.area.y,
1209 event.area.width, event.area.height)
1210 ctx.clip()
1212 x, y, width, height, depth = event.window.get_geometry()
1213 progress = None
1215 if role == TreeViewHelper.ROLE_EPISODES:
1216 if self.currently_updating:
1217 text = _('Loading episodes')
1218 elif self.config.episode_list_view_mode != \
1219 EpisodeListModel.VIEW_ALL:
1220 text = _('No episodes in current view')
1221 else:
1222 text = _('No episodes available')
1223 elif role == TreeViewHelper.ROLE_PODCASTS:
1224 if self.config.episode_list_view_mode != \
1225 EpisodeListModel.VIEW_ALL and \
1226 self.config.podcast_list_hide_boring and \
1227 len(self.channels) > 0:
1228 text = _('No podcasts in this view')
1229 else:
1230 text = _('No subscriptions')
1231 elif role == TreeViewHelper.ROLE_DOWNLOADS:
1232 text = _('No active downloads')
1233 else:
1234 raise Exception('on_treeview_expose_event: unknown role')
1236 if gpodder.ui.fremantle:
1237 from gpodder.gtkui.frmntl import style
1238 font_desc = style.get_font_desc('LargeSystemFont')
1239 else:
1240 font_desc = None
1242 draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1244 return False
1246 def enable_download_list_update(self):
1247 if not self.download_list_update_enabled:
1248 self.update_downloads_list()
1249 gobject.timeout_add(1500, self.update_downloads_list)
1250 self.download_list_update_enabled = True
1252 def cleanup_downloads(self):
1253 model = self.download_status_model
1255 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
1256 changed_episode_urls = set()
1257 for row_reference, task in all_tasks:
1258 if task.status in (task.DONE, task.CANCELLED):
1259 model.remove(model.get_iter(row_reference.get_path()))
1260 try:
1261 # We don't "see" this task anymore - remove it;
1262 # this is needed, so update_episode_list_icons()
1263 # below gets the correct list of "seen" tasks
1264 self.download_tasks_seen.remove(task)
1265 except KeyError, key_error:
1266 log('Cannot remove task from "seen" list: %s', task, sender=self)
1267 changed_episode_urls.add(task.url)
1268 # Tell the task that it has been removed (so it can clean up)
1269 task.removed_from_list()
1271 # Tell the podcasts tab to update icons for our removed podcasts
1272 self.update_episode_list_icons(changed_episode_urls)
1274 # Tell the shownotes window that we have removed the episode
1275 if self.episode_shownotes_window is not None and \
1276 self.episode_shownotes_window.episode is not None and \
1277 self.episode_shownotes_window.episode.url in changed_episode_urls:
1278 self.episode_shownotes_window._download_status_changed(None)
1280 # Update the downloads list one more time
1281 self.update_downloads_list(can_call_cleanup=False)
1283 def on_tool_downloads_toggled(self, toolbutton):
1284 if toolbutton.get_active():
1285 self.wNotebook.set_current_page(1)
1286 else:
1287 self.wNotebook.set_current_page(0)
1289 def add_download_task_monitor(self, monitor):
1290 self.download_task_monitors.add(monitor)
1291 model = self.download_status_model
1292 if model is None:
1293 model = ()
1294 for row in model:
1295 task = row[self.download_status_model.C_TASK]
1296 monitor.task_updated(task)
1298 def remove_download_task_monitor(self, monitor):
1299 self.download_task_monitors.remove(monitor)
1301 def update_downloads_list(self, can_call_cleanup=True):
1302 try:
1303 model = self.download_status_model
1305 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
1306 total_speed, total_size, done_size = 0, 0, 0
1308 # Keep a list of all download tasks that we've seen
1309 download_tasks_seen = set()
1311 # Remember the DownloadTask object for the episode that
1312 # has been opened in the episode shownotes dialog (if any)
1313 if self.episode_shownotes_window is not None:
1314 shownotes_episode = self.episode_shownotes_window.episode
1315 shownotes_task = None
1316 else:
1317 shownotes_episode = None
1318 shownotes_task = None
1320 # Do not go through the list of the model is not (yet) available
1321 if model is None:
1322 model = ()
1324 failed_downloads = []
1325 for row in model:
1326 self.download_status_model.request_update(row.iter)
1328 task = row[self.download_status_model.C_TASK]
1329 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
1331 # Let the download task monitors know of changes
1332 for monitor in self.download_task_monitors:
1333 monitor.task_updated(task)
1335 total_size += size
1336 done_size += size*progress
1338 if shownotes_episode is not None and \
1339 shownotes_episode.url == task.episode.url:
1340 shownotes_task = task
1342 download_tasks_seen.add(task)
1344 if status == download.DownloadTask.DOWNLOADING:
1345 downloading += 1
1346 total_speed += speed
1347 elif status == download.DownloadTask.FAILED:
1348 failed_downloads.append(task)
1349 failed += 1
1350 elif status == download.DownloadTask.DONE:
1351 finished += 1
1352 elif status == download.DownloadTask.QUEUED:
1353 queued += 1
1354 elif status == download.DownloadTask.PAUSED:
1355 paused += 1
1356 else:
1357 others += 1
1359 # Remember which tasks we have seen after this run
1360 self.download_tasks_seen = download_tasks_seen
1362 if gpodder.ui.desktop:
1363 text = [_('Downloads')]
1364 if downloading + failed + queued > 0:
1365 s = []
1366 if downloading > 0:
1367 s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count':downloading})
1368 if failed > 0:
1369 s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1370 if queued > 0:
1371 s.append(N_('%(count)d queued', '%(count)d queued', queued) % {'count':queued})
1372 text.append(' (' + ', '.join(s)+')')
1373 self.labelDownloads.set_text(''.join(text))
1374 elif gpodder.ui.diablo:
1375 sum = downloading + failed + finished + queued + paused + others
1376 if sum:
1377 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
1378 else:
1379 self.tool_downloads.set_label(_('Downloads'))
1380 elif gpodder.ui.fremantle:
1381 if downloading + queued > 0:
1382 self.button_downloads.set_value(N_('%(count)d active', '%(count)d active', downloading+queued) % {'count':(downloading+queued)})
1383 elif failed > 0:
1384 self.button_downloads.set_value(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1385 elif paused > 0:
1386 self.button_downloads.set_value(N_('%(count)d paused', '%(count)d paused', paused) % {'count':paused})
1387 else:
1388 self.button_downloads.set_value(_('Idle'))
1390 title = [self.default_title]
1392 # We have to update all episodes/channels for which the status has
1393 # changed. Accessing task.status_changed has the side effect of
1394 # re-setting the changed flag, so we need to get the "changed" list
1395 # of tuples first and split it into two lists afterwards
1396 changed = [(task.url, task.podcast_url) for task in \
1397 self.download_tasks_seen if task.status_changed]
1398 episode_urls = [episode_url for episode_url, channel_url in changed]
1399 channel_urls = [channel_url for episode_url, channel_url in changed]
1401 count = downloading + queued
1402 if count > 0:
1403 title.append(N_('downloading %(count)d file', 'downloading %(count)d files', count) % {'count':count})
1405 if total_size > 0:
1406 percentage = 100.0*done_size/total_size
1407 else:
1408 percentage = 0.0
1409 total_speed = util.format_filesize(total_speed)
1410 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
1411 if self.tray_icon is not None:
1412 # Update the tray icon status and progress bar
1413 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
1414 self.tray_icon.draw_progress_bar(percentage/100.)
1415 else:
1416 if self.tray_icon is not None:
1417 # Update the tray icon status
1418 self.tray_icon.set_status()
1419 if gpodder.ui.desktop:
1420 self.downloads_finished(self.download_tasks_seen)
1421 if gpodder.ui.diablo:
1422 hildon.hildon_banner_show_information(self.gPodder, '', 'gPodder: %s' % _('All downloads finished'))
1423 log('All downloads have finished.', sender=self)
1424 if self.config.cmd_all_downloads_complete:
1425 util.run_external_command(self.config.cmd_all_downloads_complete)
1427 if gpodder.ui.fremantle and failed:
1428 message = '\n'.join(['%s: %s' % (str(task), \
1429 task.error_message) for task in failed_downloads])
1430 self.show_message(message, _('Downloads failed'), important=True)
1432 # Remove finished episodes
1433 if self.config.auto_cleanup_downloads and can_call_cleanup:
1434 self.cleanup_downloads()
1436 # Stop updating the download list here
1437 self.download_list_update_enabled = False
1439 if not gpodder.ui.fremantle:
1440 self.gPodder.set_title(' - '.join(title))
1442 self.update_episode_list_icons(episode_urls)
1443 if self.episode_shownotes_window is not None:
1444 if (shownotes_task and shownotes_task.url in episode_urls) or \
1445 shownotes_task != self.episode_shownotes_window.task:
1446 self.episode_shownotes_window._download_status_changed(shownotes_task)
1447 self.episode_shownotes_window._download_status_progress()
1448 self.play_or_download()
1449 if channel_urls:
1450 self.update_podcast_list_model(channel_urls)
1452 return self.download_list_update_enabled
1453 except Exception, e:
1454 log('Exception happened while updating download list.', sender=self, traceback=True)
1455 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1456 # We return False here, so the update loop won't be called again,
1457 # that's why we require the restart of gPodder in the message.
1458 return False
1460 def on_config_changed(self, *args):
1461 util.idle_add(self._on_config_changed, *args)
1463 def _on_config_changed(self, name, old_value, new_value):
1464 if name == 'show_toolbar' and gpodder.ui.desktop:
1465 self.toolbar.set_property('visible', new_value)
1466 elif name == 'videoplayer':
1467 self.config.video_played_dbus = False
1468 elif name == 'player':
1469 self.config.audio_played_dbus = False
1470 elif name == 'episode_list_descriptions':
1471 self.update_episode_list_model()
1472 elif name == 'episode_list_thumbnails':
1473 self.update_episode_list_icons(all=True)
1474 elif name == 'rotation_mode':
1475 self._fremantle_rotation.set_mode(new_value)
1476 elif name in ('auto_update_feeds', 'auto_update_frequency'):
1477 self.restart_auto_update_timer()
1478 elif name == 'podcast_list_view_all':
1479 # Force a update of the podcast list model
1480 self.channel_list_changed = True
1481 if gpodder.ui.fremantle:
1482 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1483 while gtk.events_pending():
1484 gtk.main_iteration(False)
1485 self.update_podcast_list_model()
1486 if gpodder.ui.fremantle:
1487 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1489 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1490 # With get_bin_window, we get the window that contains the rows without
1491 # the header. The Y coordinate of this window will be the height of the
1492 # treeview header. This is the amount we have to subtract from the
1493 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1494 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1495 y -= x_bin
1496 y -= y_bin
1497 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1499 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or x > 50 or (column is not None and column != treeview.get_columns()[0]):
1500 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1501 return False
1503 if path is not None:
1504 model = treeview.get_model()
1505 iter = model.get_iter(path)
1506 role = getattr(treeview, TreeViewHelper.ROLE)
1508 if role == TreeViewHelper.ROLE_EPISODES:
1509 id = model.get_value(iter, EpisodeListModel.C_URL)
1510 elif role == TreeViewHelper.ROLE_PODCASTS:
1511 id = model.get_value(iter, PodcastListModel.C_URL)
1513 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1514 if last_tooltip is not None and last_tooltip != id:
1515 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1516 return False
1517 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1519 if role == TreeViewHelper.ROLE_EPISODES:
1520 description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1521 if description:
1522 tooltip.set_text(description)
1523 else:
1524 return False
1525 elif role == TreeViewHelper.ROLE_PODCASTS:
1526 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1527 if channel is None:
1528 return False
1529 channel.request_save_dir_size()
1530 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1531 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1532 if error_str:
1533 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1534 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1535 table = gtk.Table(rows=3, columns=3)
1536 table.set_row_spacings(5)
1537 table.set_col_spacings(5)
1538 table.set_border_width(5)
1540 heading = gtk.Label()
1541 heading.set_alignment(0, 1)
1542 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1543 table.attach(heading, 0, 1, 0, 1)
1544 size_info = gtk.Label()
1545 size_info.set_alignment(1, 1)
1546 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1547 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1548 table.attach(size_info, 2, 3, 0, 1)
1550 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1552 if len(channel.description) < 500:
1553 description = channel.description
1554 else:
1555 pos = channel.description.find('\n\n')
1556 if pos == -1 or pos > 500:
1557 description = channel.description[:498]+'[...]'
1558 else:
1559 description = channel.description[:pos]
1561 description = gtk.Label(description)
1562 if error_str:
1563 description.set_markup(error_str)
1564 description.set_alignment(0, 0)
1565 description.set_line_wrap(True)
1566 table.attach(description, 0, 3, 2, 3)
1568 table.show_all()
1569 tooltip.set_custom(table)
1571 return True
1573 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1574 return False
1576 def treeview_allow_tooltips(self, treeview, allow):
1577 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1579 def update_m3u_playlist_clicked(self, widget):
1580 if self.active_channel is not None:
1581 self.active_channel.update_m3u_playlist()
1582 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1584 def treeview_handle_context_menu_click(self, treeview, event):
1585 x, y = int(event.x), int(event.y)
1586 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1588 selection = treeview.get_selection()
1589 model, paths = selection.get_selected_rows()
1591 if path is None or (path not in paths and \
1592 event.button == self.context_menu_mouse_button):
1593 # We have right-clicked, but not into the selection,
1594 # assume we don't want to operate on the selection
1595 paths = []
1597 if path is not None and not paths and \
1598 event.button == self.context_menu_mouse_button:
1599 # No selection or clicked outside selection;
1600 # select the single item where we clicked
1601 treeview.grab_focus()
1602 treeview.set_cursor(path, column, 0)
1603 paths = [path]
1605 if not paths:
1606 # Unselect any remaining items (clicked elsewhere)
1607 if hasattr(treeview, 'is_rubber_banding_active'):
1608 if not treeview.is_rubber_banding_active():
1609 selection.unselect_all()
1610 else:
1611 selection.unselect_all()
1613 return model, paths
1615 def downloads_list_get_selection(self, model=None, paths=None):
1616 if model is None and paths is None:
1617 selection = self.treeDownloads.get_selection()
1618 model, paths = selection.get_selected_rows()
1620 can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1621 selected_tasks = [(gtk.TreeRowReference(model, path), \
1622 model.get_value(model.get_iter(path), \
1623 DownloadStatusModel.C_TASK)) for path in paths]
1625 for row_reference, task in selected_tasks:
1626 if task.status != download.DownloadTask.QUEUED:
1627 can_force = False
1628 if task.status not in (download.DownloadTask.PAUSED, \
1629 download.DownloadTask.FAILED, \
1630 download.DownloadTask.CANCELLED):
1631 can_queue = False
1632 if task.status not in (download.DownloadTask.PAUSED, \
1633 download.DownloadTask.QUEUED, \
1634 download.DownloadTask.DOWNLOADING, \
1635 download.DownloadTask.FAILED):
1636 can_cancel = False
1637 if task.status not in (download.DownloadTask.QUEUED, \
1638 download.DownloadTask.DOWNLOADING):
1639 can_pause = False
1640 if task.status not in (download.DownloadTask.CANCELLED, \
1641 download.DownloadTask.FAILED, \
1642 download.DownloadTask.DONE):
1643 can_remove = False
1645 return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1647 def downloads_finished(self, download_tasks_seen):
1648 # FIXME: Filter all tasks that have already been reported
1649 finished_downloads = [str(task) for task in download_tasks_seen if task.status == task.DONE]
1650 failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.status == task.FAILED]
1652 if finished_downloads and failed_downloads:
1653 message = self.format_episode_list(finished_downloads, 5)
1654 message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1655 message += self.format_episode_list(failed_downloads, 5)
1656 self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1657 elif finished_downloads:
1658 message = self.format_episode_list(finished_downloads)
1659 self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1660 elif failed_downloads:
1661 message = self.format_episode_list(failed_downloads)
1662 self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1664 # Open torrent files right after download (bug 1029)
1665 if self.config.open_torrent_after_download:
1666 for task in download_tasks_seen:
1667 if task.status != task.DONE:
1668 continue
1670 episode = task.episode
1671 if episode.mimetype != 'application/x-bittorrent':
1672 continue
1674 self.playback_episodes([episode])
1677 def format_episode_list(self, episode_list, max_episodes=10):
1679 Format a list of episode names for notifications
1681 Will truncate long episode names and limit the amount of
1682 episodes displayed (max_episodes=10).
1684 The episode_list parameter should be a list of strings.
1686 MAX_TITLE_LENGTH = 100
1688 result = []
1689 for title in episode_list[:min(len(episode_list), max_episodes)]:
1690 if len(title) > MAX_TITLE_LENGTH:
1691 middle = (MAX_TITLE_LENGTH/2)-2
1692 title = '%s...%s' % (title[0:middle], title[-middle:])
1693 result.append(saxutils.escape(title))
1694 result.append('\n')
1696 more_episodes = len(episode_list) - max_episodes
1697 if more_episodes > 0:
1698 result.append('(...')
1699 result.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes) % {'count':more_episodes})
1700 result.append('...)')
1702 return (''.join(result)).strip()
1704 def _for_each_task_set_status(self, tasks, status, force_start=False):
1705 episode_urls = set()
1706 model = self.treeDownloads.get_model()
1707 for row_reference, task in tasks:
1708 if status == download.DownloadTask.QUEUED:
1709 # Only queue task when its paused/failed/cancelled (or forced)
1710 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1711 self.download_queue_manager.add_task(task, force_start)
1712 self.enable_download_list_update()
1713 elif status == download.DownloadTask.CANCELLED:
1714 # Cancelling a download allowed when downloading/queued
1715 if task.status in (task.QUEUED, task.DOWNLOADING):
1716 task.status = status
1717 # Cancelling paused/failed downloads requires a call to .run()
1718 elif task.status in (task.PAUSED, task.FAILED):
1719 task.status = status
1720 # Call run, so the partial file gets deleted
1721 task.run()
1722 elif status == download.DownloadTask.PAUSED:
1723 # Pausing a download only when queued/downloading
1724 if task.status in (task.DOWNLOADING, task.QUEUED):
1725 task.status = status
1726 elif status is None:
1727 # Remove the selected task - cancel downloading/queued tasks
1728 if task.status in (task.QUEUED, task.DOWNLOADING):
1729 task.status = task.CANCELLED
1730 model.remove(model.get_iter(row_reference.get_path()))
1731 # Remember the URL, so we can tell the UI to update
1732 try:
1733 # We don't "see" this task anymore - remove it;
1734 # this is needed, so update_episode_list_icons()
1735 # below gets the correct list of "seen" tasks
1736 self.download_tasks_seen.remove(task)
1737 except KeyError, key_error:
1738 log('Cannot remove task from "seen" list: %s', task, sender=self)
1739 episode_urls.add(task.url)
1740 # Tell the task that it has been removed (so it can clean up)
1741 task.removed_from_list()
1742 else:
1743 # We can (hopefully) simply set the task status here
1744 task.status = status
1745 # Tell the podcasts tab to update icons for our removed podcasts
1746 self.update_episode_list_icons(episode_urls)
1747 # Update the tab title and downloads list
1748 self.update_downloads_list()
1750 def treeview_downloads_show_context_menu(self, treeview, event):
1751 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1752 if not paths:
1753 if not hasattr(treeview, 'is_rubber_banding_active'):
1754 return True
1755 else:
1756 return not treeview.is_rubber_banding_active()
1758 if event.button == self.context_menu_mouse_button:
1759 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1760 self.downloads_list_get_selection(model, paths)
1762 def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1763 # This creates a menu item for selection-wide actions
1764 item = gtk.ImageMenuItem(label)
1765 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1766 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1767 item.set_sensitive(sensitive)
1768 return self.set_finger_friendly(item)
1770 menu = gtk.Menu()
1772 item = gtk.ImageMenuItem(_('Episode details'))
1773 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1774 if len(selected_tasks) == 1:
1775 row_reference, task = selected_tasks[0]
1776 episode = task.episode
1777 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1778 else:
1779 item.set_sensitive(False)
1780 menu.append(self.set_finger_friendly(item))
1781 menu.append(gtk.SeparatorMenuItem())
1782 if can_force:
1783 menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1784 else:
1785 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1786 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1787 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1788 menu.append(gtk.SeparatorMenuItem())
1789 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1791 if gpodder.ui.maemo or self.config.enable_fingerscroll:
1792 # Because we open the popup on left-click for Maemo,
1793 # we also include a non-action to close the menu
1794 menu.append(gtk.SeparatorMenuItem())
1795 item = gtk.ImageMenuItem(_('Close this menu'))
1796 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1798 menu.append(self.set_finger_friendly(item))
1800 menu.show_all()
1801 menu.popup(None, None, None, event.button, event.time)
1802 return True
1804 def treeview_channels_show_context_menu(self, treeview, event):
1805 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1806 if not paths:
1807 return True
1809 # Check for valid channel id, if there's no id then
1810 # assume that it is a proxy channel or equivalent
1811 # and cannot be operated with right click
1812 if self.active_channel.id is None:
1813 return True
1815 if event.button == 3:
1816 menu = gtk.Menu()
1818 ICON = lambda x: x
1820 item = gtk.ImageMenuItem( _('Update podcast'))
1821 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1822 item.connect('activate', self.on_itemUpdateChannel_activate)
1823 item.set_sensitive(not self.updating_feed_cache)
1824 menu.append(item)
1826 menu.append(gtk.SeparatorMenuItem())
1828 item = gtk.CheckMenuItem(_('Keep episodes'))
1829 item.set_active(self.active_channel.channel_is_locked)
1830 item.connect('activate', self.on_channel_toggle_lock_activate)
1831 menu.append(self.set_finger_friendly(item))
1833 item = gtk.ImageMenuItem(_('Remove podcast'))
1834 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1835 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1836 menu.append( item)
1838 if self.config.device_type != 'none':
1839 item = gtk.MenuItem(_('Synchronize to device'))
1840 item.connect('activate', lambda item: self.on_sync_to_ipod_activate(item, self.active_channel.get_downloaded_episodes()))
1841 menu.append(item)
1843 menu.append( gtk.SeparatorMenuItem())
1845 item = gtk.ImageMenuItem(_('Podcast details'))
1846 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1847 item.connect('activate', self.on_itemEditChannel_activate)
1848 menu.append(item)
1850 menu.show_all()
1851 # Disable tooltips while we are showing the menu, so
1852 # the tooltip will not appear over the menu
1853 self.treeview_allow_tooltips(self.treeChannels, False)
1854 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1855 menu.popup( None, None, None, event.button, event.time)
1857 return True
1859 def on_itemClose_activate(self, widget):
1860 if self.tray_icon is not None:
1861 self.iconify_main_window()
1862 else:
1863 self.on_gPodder_delete_event(widget)
1865 def cover_file_removed(self, channel_url):
1867 The Cover Downloader calls this when a previously-
1868 available cover has been removed from the disk. We
1869 have to update our model to reflect this change.
1871 self.podcast_list_model.delete_cover_by_url(channel_url)
1873 def cover_download_finished(self, channel, pixbuf):
1875 The Cover Downloader calls this when it has finished
1876 downloading (or registering, if already downloaded)
1877 a new channel cover, which is ready for displaying.
1879 self.podcast_list_model.add_cover_by_channel(channel, pixbuf)
1881 def save_episodes_as_file(self, episodes):
1882 for episode in episodes:
1883 self.save_episode_as_file(episode)
1885 def save_episode_as_file(self, episode):
1886 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1887 if episode.was_downloaded(and_exists=True):
1888 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1889 copy_from = episode.local_filename(create=False)
1890 assert copy_from is not None
1891 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1892 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1893 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1895 def copy_episodes_bluetooth(self, episodes):
1896 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1898 if gpodder.ui.maemo:
1899 util.bluetooth_send_files_maemo([e.local_filename(create=False) \
1900 for e in episodes_to_copy])
1901 return True
1903 def convert_and_send_thread(episode):
1904 for episode in episodes:
1905 filename = episode.local_filename(create=False)
1906 assert filename is not None
1907 destfile = os.path.join(tempfile.gettempdir(), \
1908 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1909 (base, ext) = os.path.splitext(filename)
1910 if not destfile.endswith(ext):
1911 destfile += ext
1913 try:
1914 shutil.copyfile(filename, destfile)
1915 util.bluetooth_send_file(destfile)
1916 except:
1917 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1918 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1920 util.delete_file(destfile)
1922 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1924 def get_device_name(self):
1925 if self.config.device_type == 'ipod':
1926 return _('iPod')
1927 elif self.config.device_type in ('filesystem', 'mtp'):
1928 return _('MP3 player')
1929 else:
1930 return '(unknown device)'
1932 def _treeview_button_released(self, treeview, event):
1933 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1934 dy = int(abs(event.y-ypos))
1935 dx = int(event.x-xpos)
1937 selection = treeview.get_selection()
1938 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1939 if path is None or dy > 30:
1940 return (False, dx, dy)
1942 path, column, x, y = path
1943 selection.select_path(path)
1944 treeview.set_cursor(path)
1945 treeview.grab_focus()
1947 return (True, dx, dy)
1949 def treeview_channels_handle_gestures(self, treeview, event):
1950 if self.currently_updating:
1951 return False
1953 selected, dx, dy = self._treeview_button_released(treeview, event)
1955 if selected:
1956 if self.config.maemo_enable_gestures:
1957 if dx > 70:
1958 self.on_itemUpdateChannel_activate()
1959 elif dx < -70:
1960 self.on_itemEditChannel_activate(treeview)
1962 return False
1964 def treeview_available_handle_gestures(self, treeview, event):
1965 selected, dx, dy = self._treeview_button_released(treeview, event)
1967 if selected:
1968 if self.config.maemo_enable_gestures:
1969 if dx > 70:
1970 self.on_playback_selected_episodes(None)
1971 return True
1972 elif dx < -70:
1973 self.on_shownotes_selected_episodes(None)
1974 return True
1976 # Pass the event to the context menu handler for treeAvailable
1977 self.treeview_available_show_context_menu(treeview, event)
1979 return True
1981 def treeview_available_show_context_menu(self, treeview, event):
1982 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1983 if not paths:
1984 if not hasattr(treeview, 'is_rubber_banding_active'):
1985 return True
1986 else:
1987 return not treeview.is_rubber_banding_active()
1989 if event.button == self.context_menu_mouse_button:
1990 episodes = self.get_selected_episodes()
1991 any_locked = any(e.is_locked for e in episodes)
1992 any_played = any(e.is_played for e in episodes)
1993 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1994 downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
1995 downloading = any(self.episode_is_downloading(e) for e in episodes)
1997 menu = gtk.Menu()
1999 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
2001 if open_instead_of_play:
2002 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
2003 elif downloaded:
2004 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
2005 else:
2006 item = gtk.ImageMenuItem(_('Stream'))
2007 item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
2009 item.set_sensitive(can_play and not downloading)
2010 item.connect('activate', self.on_playback_selected_episodes)
2011 menu.append(self.set_finger_friendly(item))
2013 if not can_cancel:
2014 item = gtk.ImageMenuItem(_('Download'))
2015 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
2016 item.set_sensitive(can_download)
2017 item.connect('activate', self.on_download_selected_episodes)
2018 menu.append(self.set_finger_friendly(item))
2019 else:
2020 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
2021 item.connect('activate', self.on_item_cancel_download_activate)
2022 menu.append(self.set_finger_friendly(item))
2024 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
2025 item.set_sensitive(can_delete)
2026 item.connect('activate', self.on_btnDownloadedDelete_clicked)
2027 menu.append(self.set_finger_friendly(item))
2029 ICON = lambda x: x
2031 # Ok, this probably makes sense to only display for downloaded files
2032 if downloaded:
2033 menu.append(gtk.SeparatorMenuItem())
2034 share_item = gtk.MenuItem(_('Send to'))
2035 menu.append(self.set_finger_friendly(share_item))
2036 share_menu = gtk.Menu()
2038 item = gtk.ImageMenuItem(_('Local folder'))
2039 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
2040 item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
2041 share_menu.append(self.set_finger_friendly(item))
2042 if self.bluetooth_available:
2043 item = gtk.ImageMenuItem(_('Bluetooth device'))
2044 if gpodder.ui.maemo:
2045 icon_name = ICON('qgn_list_filesys_bluetooth')
2046 else:
2047 icon_name = ICON('bluetooth')
2048 item.set_image(gtk.image_new_from_icon_name(icon_name, gtk.ICON_SIZE_MENU))
2049 item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
2050 share_menu.append(self.set_finger_friendly(item))
2051 if can_transfer:
2052 item = gtk.ImageMenuItem(self.get_device_name())
2053 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
2054 item.connect('button-press-event', lambda w, ee: self.on_sync_to_ipod_activate(w, episodes))
2055 share_menu.append(self.set_finger_friendly(item))
2057 share_item.set_submenu(share_menu)
2059 if (downloaded or one_is_new or can_download) and not downloading:
2060 menu.append(gtk.SeparatorMenuItem())
2061 if one_is_new:
2062 item = gtk.CheckMenuItem(_('New'))
2063 item.set_active(True)
2064 item.connect('activate', lambda w: self.mark_selected_episodes_old())
2065 menu.append(self.set_finger_friendly(item))
2066 elif can_download:
2067 item = gtk.CheckMenuItem(_('New'))
2068 item.set_active(False)
2069 item.connect('activate', lambda w: self.mark_selected_episodes_new())
2070 menu.append(self.set_finger_friendly(item))
2072 if downloaded:
2073 item = gtk.CheckMenuItem(_('Played'))
2074 item.set_active(any_played)
2075 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
2076 menu.append(self.set_finger_friendly(item))
2078 item = gtk.CheckMenuItem(_('Keep episode'))
2079 item.set_active(any_locked)
2080 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
2081 menu.append(self.set_finger_friendly(item))
2083 menu.append(gtk.SeparatorMenuItem())
2084 # Single item, add episode information menu item
2085 item = gtk.ImageMenuItem(_('Episode details'))
2086 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
2087 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
2088 menu.append(self.set_finger_friendly(item))
2090 if gpodder.ui.maemo or self.config.enable_fingerscroll:
2091 # Because we open the popup on left-click for Maemo,
2092 # we also include a non-action to close the menu
2093 menu.append(gtk.SeparatorMenuItem())
2094 item = gtk.ImageMenuItem(_('Close this menu'))
2095 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
2096 menu.append(self.set_finger_friendly(item))
2098 menu.show_all()
2099 # Disable tooltips while we are showing the menu, so
2100 # the tooltip will not appear over the menu
2101 self.treeview_allow_tooltips(self.treeAvailable, False)
2102 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
2103 menu.popup( None, None, None, event.button, event.time)
2105 return True
2107 def set_title(self, new_title):
2108 if not gpodder.ui.fremantle:
2109 self.default_title = new_title
2110 self.gPodder.set_title(new_title)
2112 def update_episode_list_icons(self, urls=None, selected=False, all=False):
2114 Updates the status icons in the episode list.
2116 If urls is given, it should be a list of URLs
2117 of episodes that should be updated.
2119 If urls is None, set ONE OF selected, all to
2120 True (the former updates just the selected
2121 episodes and the latter updates all episodes).
2123 additional_args = (self.episode_is_downloading, \
2124 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2125 self.config.episode_list_thumbnails and gpodder.ui.desktop)
2127 if urls is not None:
2128 # We have a list of URLs to walk through
2129 self.episode_list_model.update_by_urls(urls, *additional_args)
2130 elif selected and not all:
2131 # We should update all selected episodes
2132 selection = self.treeAvailable.get_selection()
2133 model, paths = selection.get_selected_rows()
2134 for path in reversed(paths):
2135 iter = model.get_iter(path)
2136 self.episode_list_model.update_by_filter_iter(iter, \
2137 *additional_args)
2138 elif all and not selected:
2139 # We update all (even the filter-hidden) episodes
2140 self.episode_list_model.update_all(*additional_args)
2141 else:
2142 # Wrong/invalid call - have to specify at least one parameter
2143 raise ValueError('Invalid call to update_episode_list_icons')
2145 def episode_list_status_changed(self, episodes):
2146 self.update_episode_list_icons(set(e.url for e in episodes))
2147 self.update_podcast_list_model(set(e.channel.url for e in episodes))
2148 self.db.commit()
2150 def clean_up_downloads(self, delete_partial=False):
2151 # Clean up temporary files left behind by old gPodder versions
2152 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
2154 if delete_partial:
2155 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
2157 for tempfile in temporary_files:
2158 util.delete_file(tempfile)
2160 # Clean up empty download folders and abandoned download folders
2161 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
2162 for ddir in download_dirs:
2163 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2164 globr = glob.glob(os.path.join(ddir, '*'))
2165 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
2166 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
2167 shutil.rmtree(ddir, ignore_errors=True)
2169 def streaming_possible(self):
2170 if gpodder.ui.desktop:
2171 # User has to have a media player set on the Desktop, or else we
2172 # would probably open the browser when giving a URL to xdg-open..
2173 return (self.config.player and self.config.player != 'default')
2174 elif gpodder.ui.maemo:
2175 # On Maemo, the default is to use the Nokia Media Player, which is
2176 # already able to deal with HTTP URLs the right way, so we
2177 # unconditionally enable streaming always on Maemo
2178 return True
2180 return False
2182 def playback_episodes_for_real(self, episodes):
2183 groups = collections.defaultdict(list)
2184 for episode in episodes:
2185 file_type = episode.file_type()
2186 if file_type == 'video' and self.config.videoplayer and \
2187 self.config.videoplayer != 'default':
2188 player = self.config.videoplayer
2189 if gpodder.ui.diablo:
2190 # Use the wrapper script if it's installed to crop 3GP YouTube
2191 # videos to fit the screen (looks much nicer than w/ black border)
2192 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2193 player = 'gpodder-mplayer'
2194 elif gpodder.ui.fremantle and player == 'mplayer':
2195 player = 'mplayer -fs %F'
2196 elif file_type == 'audio' and self.config.player and \
2197 self.config.player != 'default':
2198 player = self.config.player
2199 else:
2200 player = 'default'
2202 if file_type not in ('audio', 'video') or \
2203 (file_type == 'audio' and not self.config.audio_played_dbus) or \
2204 (file_type == 'video' and not self.config.video_played_dbus):
2205 # Mark episode as played in the database
2206 episode.mark(is_played=True)
2207 self.mygpo_client.on_playback([episode])
2209 filename = episode.local_filename(create=False)
2210 if filename is None or not os.path.exists(filename):
2211 filename = episode.url
2212 if youtube.is_video_link(filename):
2213 fmt_id = self.config.youtube_preferred_fmt_id
2214 if gpodder.ui.fremantle:
2215 fmt_id = 5
2216 filename = youtube.get_real_download_url(filename, fmt_id)
2218 # Determine the playback resume position - if the file
2219 # was played 100%, we simply start from the beginning
2220 resume_position = episode.current_position
2221 if resume_position == episode.total_time:
2222 resume_position = 0
2224 # Only on Maemo 5, and only if the episode isn't finished yet
2225 if gpodder.ui.fremantle and not episode.is_finished():
2226 self.mafw_monitor.set_resume_point(filename, resume_position)
2228 # If Panucci is configured, use D-Bus on Maemo to call it
2229 if player == 'panucci':
2230 try:
2231 PANUCCI_NAME = 'org.panucci.panucciInterface'
2232 PANUCCI_PATH = '/panucciInterface'
2233 PANUCCI_INTF = 'org.panucci.panucciInterface'
2234 o = gpodder.dbus_session_bus.get_object(PANUCCI_NAME, PANUCCI_PATH)
2235 i = dbus.Interface(o, PANUCCI_INTF)
2237 def on_reply(*args):
2238 pass
2240 def error_handler(filename, err):
2241 log('Exception in D-Bus call: %s', str(err), \
2242 sender=self)
2244 # Fallback: use the command line client
2245 for command in util.format_desktop_command('panucci', \
2246 [filename]):
2247 log('Executing: %s', repr(command), sender=self)
2248 subprocess.Popen(command)
2250 on_error = lambda err: error_handler(filename, err)
2252 # This method only exists in Panucci > 0.9 ('new Panucci')
2253 i.playback_from(filename, resume_position, \
2254 reply_handler=on_reply, error_handler=on_error)
2256 continue # This file was handled by the D-Bus call
2257 except Exception, e:
2258 log('Error calling Panucci using D-Bus', sender=self, traceback=True)
2259 elif player == 'MediaBox' and gpodder.ui.maemo:
2260 try:
2261 MEDIABOX_NAME = 'de.pycage.mediabox'
2262 MEDIABOX_PATH = '/de/pycage/mediabox/control'
2263 MEDIABOX_INTF = 'de.pycage.mediabox.control'
2264 o = gpodder.dbus_session_bus.get_object(MEDIABOX_NAME, MEDIABOX_PATH)
2265 i = dbus.Interface(o, MEDIABOX_INTF)
2267 def on_reply(*args):
2268 pass
2270 def on_error(err):
2271 log('Exception in D-Bus call: %s', str(err), \
2272 sender=self)
2274 i.load(filename, '%s/x-unknown' % file_type, \
2275 reply_handler=on_reply, error_handler=on_error)
2277 continue # This file was handled by the D-Bus call
2278 except Exception, e:
2279 log('Error calling MediaBox using D-Bus', sender=self, traceback=True)
2281 groups[player].append(filename)
2283 # Open episodes with system default player
2284 if 'default' in groups:
2285 # Special-casing for a single episode when the object is a PDF
2286 # file - this is needed on Maemo 5, so we only use gui_open()
2287 # for single PDF files, but still use the built-in media player
2288 # with an M3U file for single audio/video files. (The Maemo 5
2289 # media player behaves differently when opening a single-file
2290 # M3U playlist compared to opening the single file directly.)
2291 if len(groups['default']) == 1:
2292 fn = groups['default'][0]
2293 # The list of extensions is taken from gui_open in util.py
2294 # where all special-cases of Maemo apps are listed
2295 for extension in ('.pdf', '.jpg', '.jpeg', '.png'):
2296 if fn.lower().endswith(extension):
2297 util.gui_open(fn)
2298 groups['default'] = []
2299 break
2301 if gpodder.ui.maemo and groups['default']:
2302 # The Nokia Media Player app does not support receiving multiple
2303 # file names via D-Bus, so we simply place all file names into a
2304 # temporary M3U playlist and open that with the Media Player.
2305 m3u_filename = os.path.join(gpodder.home, 'gpodder_open_with.m3u')
2307 def to_url(x):
2308 if '://' not in x:
2309 return 'file://' + urllib.quote(os.path.abspath(x))
2310 return x
2312 util.write_m3u_playlist(m3u_filename, \
2313 map(to_url, groups['default']), \
2314 extm3u=False)
2315 util.gui_open(m3u_filename)
2316 else:
2317 for filename in groups['default']:
2318 log('Opening with system default: %s', filename, sender=self)
2319 util.gui_open(filename)
2320 del groups['default']
2321 elif gpodder.ui.maemo and groups:
2322 # When on Maemo and not opening with default, show a notification
2323 # (no startup notification for Panucci / MPlayer yet...)
2324 if len(episodes) == 1:
2325 text = _('Opening %s') % episodes[0].title
2326 else:
2327 count = len(episodes)
2328 text = N_('Opening %(count)d episode', 'Opening %(count)d episodes', count) % {'count':count}
2330 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2332 def destroy_banner_later(banner):
2333 banner.destroy()
2334 return False
2335 gobject.timeout_add(5000, destroy_banner_later, banner)
2337 # For each type now, go and create play commands
2338 for group in groups:
2339 for command in util.format_desktop_command(group, groups[group]):
2340 log('Executing: %s', repr(command), sender=self)
2341 subprocess.Popen(command)
2343 # Persist episode status changes to the database
2344 self.db.commit()
2346 # Flush updated episode status
2347 self.mygpo_client.flush()
2349 def playback_episodes(self, episodes):
2350 # We need to create a list, because we run through it more than once
2351 episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2352 e.was_downloaded(and_exists=True) or self.streaming_possible()))
2354 try:
2355 self.playback_episodes_for_real(episodes)
2356 except Exception, e:
2357 log('Error in playback!', sender=self, traceback=True)
2358 if gpodder.ui.desktop:
2359 self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2360 _('Error opening player'), widget=self.toolPreferences)
2361 else:
2362 self.show_message(_('Please check your media player settings in the preferences dialog.'))
2364 channel_urls = set()
2365 episode_urls = set()
2366 for episode in episodes:
2367 channel_urls.add(episode.channel.url)
2368 episode_urls.add(episode.url)
2369 self.update_episode_list_icons(episode_urls)
2370 self.update_podcast_list_model(channel_urls)
2372 def play_or_download(self):
2373 if not gpodder.ui.fremantle:
2374 if self.wNotebook.get_current_page() > 0:
2375 if gpodder.ui.desktop:
2376 self.toolCancel.set_sensitive(True)
2377 return
2379 if self.currently_updating:
2380 return (False, False, False, False, False, False)
2382 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2383 ( is_played, is_locked ) = (False,)*2
2385 open_instead_of_play = False
2387 selection = self.treeAvailable.get_selection()
2388 if selection.count_selected_rows() > 0:
2389 (model, paths) = selection.get_selected_rows()
2391 for path in paths:
2392 try:
2393 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2394 except TypeError, te:
2395 log('Invalid episode at path %s', str(path), sender=self)
2396 continue
2398 if episode.file_type() not in ('audio', 'video'):
2399 open_instead_of_play = True
2401 if episode.was_downloaded():
2402 can_play = episode.was_downloaded(and_exists=True)
2403 is_played = episode.is_played
2404 is_locked = episode.is_locked
2405 if not can_play:
2406 can_download = True
2407 else:
2408 if self.episode_is_downloading(episode):
2409 can_cancel = True
2410 else:
2411 can_download = True
2413 can_download = can_download and not can_cancel
2414 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2415 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2416 can_delete = not can_cancel
2418 if gpodder.ui.desktop:
2419 if open_instead_of_play:
2420 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2421 else:
2422 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2423 self.toolPlay.set_sensitive( can_play)
2424 self.toolDownload.set_sensitive( can_download)
2425 self.toolTransfer.set_sensitive( can_transfer)
2426 self.toolCancel.set_sensitive( can_cancel)
2428 if not gpodder.ui.fremantle:
2429 self.item_cancel_download.set_sensitive(can_cancel)
2430 self.itemDownloadSelected.set_sensitive(can_download)
2431 self.itemOpenSelected.set_sensitive(can_play)
2432 self.itemPlaySelected.set_sensitive(can_play)
2433 self.itemDeleteSelected.set_sensitive(can_delete)
2434 self.item_toggle_played.set_sensitive(can_play)
2435 self.item_toggle_lock.set_sensitive(can_play)
2436 self.itemOpenSelected.set_visible(open_instead_of_play)
2437 self.itemPlaySelected.set_visible(not open_instead_of_play)
2439 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2441 def on_cbMaxDownloads_toggled(self, widget, *args):
2442 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2444 def on_cbLimitDownloads_toggled(self, widget, *args):
2445 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2447 def episode_new_status_changed(self, urls):
2448 self.update_podcast_list_model()
2449 self.update_episode_list_icons(urls)
2451 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2452 """Update the podcast list treeview model
2454 If urls is given, it should list the URLs of each
2455 podcast that has to be updated in the list.
2457 If selected is True, only update the model contents
2458 for the currently-selected podcast - nothing more.
2460 The caller can optionally specify "select_url",
2461 which is the URL of the podcast that is to be
2462 selected in the list after the update is complete.
2463 This only works if the podcast list has to be
2464 reloaded; i.e. something has been added or removed
2465 since the last update of the podcast list).
2467 selection = self.treeChannels.get_selection()
2468 model, iter = selection.get_selected()
2470 if self.config.podcast_list_view_all and not self.channel_list_changed:
2471 # Update "all episodes" view in any case (if enabled)
2472 self.podcast_list_model.update_first_row()
2474 if selected:
2475 # very cheap! only update selected channel
2476 if iter is not None:
2477 # If we have selected the "all episodes" view, we have
2478 # to update all channels for selected episodes:
2479 if self.config.podcast_list_view_all and \
2480 self.podcast_list_model.iter_is_first_row(iter):
2481 urls = self.get_podcast_urls_from_selected_episodes()
2482 self.podcast_list_model.update_by_urls(urls)
2483 else:
2484 # Otherwise just update the selected row (a podcast)
2485 self.podcast_list_model.update_by_filter_iter(iter)
2486 elif not self.channel_list_changed:
2487 # we can keep the model, but have to update some
2488 if urls is None:
2489 # still cheaper than reloading the whole list
2490 self.podcast_list_model.update_all()
2491 else:
2492 # ok, we got a bunch of urls to update
2493 self.podcast_list_model.update_by_urls(urls)
2494 else:
2495 if model and iter and select_url is None:
2496 # Get the URL of the currently-selected podcast
2497 select_url = model.get_value(iter, PodcastListModel.C_URL)
2499 # Update the podcast list model with new channels
2500 self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2502 try:
2503 selected_iter = model.get_iter_first()
2504 # Find the previously-selected URL in the new
2505 # model if we have an URL (else select first)
2506 if select_url is not None:
2507 pos = model.get_iter_first()
2508 while pos is not None:
2509 url = model.get_value(pos, PodcastListModel.C_URL)
2510 if url == select_url:
2511 selected_iter = pos
2512 break
2513 pos = model.iter_next(pos)
2515 if not gpodder.ui.maemo:
2516 if selected_iter is not None:
2517 selection.select_iter(selected_iter)
2518 self.on_treeChannels_cursor_changed(self.treeChannels)
2519 except:
2520 log('Cannot select podcast in list', traceback=True, sender=self)
2521 self.channel_list_changed = False
2523 def episode_is_downloading(self, episode):
2524 """Returns True if the given episode is being downloaded at the moment"""
2525 if episode is None:
2526 return False
2528 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2530 def on_episode_list_filter_changed(self, has_episodes):
2531 if gpodder.ui.fremantle:
2532 if has_episodes:
2533 self.episodes_window.empty_label.hide()
2534 self.episodes_window.pannablearea.show()
2535 else:
2536 if self.config.episode_list_view_mode != \
2537 EpisodeListModel.VIEW_ALL:
2538 text = _('No episodes in current view')
2539 else:
2540 text = _('No episodes available')
2541 self.episodes_window.empty_label.set_text(text)
2542 self.episodes_window.pannablearea.hide()
2543 self.episodes_window.empty_label.show()
2545 def update_episode_list_model(self):
2546 if self.channels and self.active_channel is not None:
2547 if gpodder.ui.fremantle:
2548 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2550 self.currently_updating = True
2551 self.episode_list_model.clear()
2552 if gpodder.ui.fremantle:
2553 self.episodes_window.pannablearea.hide()
2554 self.episodes_window.empty_label.set_text(_('Loading episodes'))
2555 self.episodes_window.empty_label.show()
2557 def update():
2558 additional_args = (self.episode_is_downloading, \
2559 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2560 self.config.episode_list_thumbnails and gpodder.ui.desktop)
2561 self.episode_list_model.replace_from_channel(self.active_channel, *additional_args)
2563 self.treeAvailable.get_selection().unselect_all()
2564 self.treeAvailable.scroll_to_point(0, 0)
2566 self.currently_updating = False
2567 self.play_or_download()
2569 if gpodder.ui.fremantle:
2570 hildon.hildon_gtk_window_set_progress_indicator(\
2571 self.episodes_window.main_window, False)
2573 util.idle_add(update)
2574 else:
2575 self.episode_list_model.clear()
2577 @dbus.service.method(gpodder.dbus_interface)
2578 def offer_new_episodes(self, channels=None):
2579 if gpodder.ui.fremantle:
2580 # Assume that when this function is called that the
2581 # notification is not shown anymore (Maemo bug 11345)
2582 self._fremantle_notification_visible = False
2584 new_episodes = self.get_new_episodes(channels)
2585 if new_episodes:
2586 self.new_episodes_show(new_episodes)
2587 return True
2588 return False
2590 def add_podcast_list(self, urls, auth_tokens=None):
2591 """Subscribe to a list of podcast given their URLs
2593 If auth_tokens is given, it should be a dictionary
2594 mapping URLs to (username, password) tuples."""
2596 if auth_tokens is None:
2597 auth_tokens = {}
2599 # Sort and split the URL list into five buckets
2600 queued, failed, existing, worked, authreq = [], [], [], [], []
2601 for input_url in urls:
2602 url = util.normalize_feed_url(input_url)
2603 if url is None:
2604 # Fail this one because the URL is not valid
2605 failed.append(input_url)
2606 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2607 # A podcast already exists in the list for this URL
2608 existing.append(url)
2609 else:
2610 # This URL has survived the first round - queue for add
2611 queued.append(url)
2612 if url != input_url and input_url in auth_tokens:
2613 auth_tokens[url] = auth_tokens[input_url]
2615 error_messages = {}
2616 redirections = {}
2618 progress = ProgressIndicator(_('Adding podcasts'), \
2619 _('Please wait while episode information is downloaded.'), \
2620 parent=self.get_dialog_parent())
2622 def on_after_update():
2623 progress.on_finished()
2624 # Report already-existing subscriptions to the user
2625 if existing:
2626 title = _('Existing subscriptions skipped')
2627 message = _('You are already subscribed to these podcasts:') \
2628 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2629 self.show_message(message, title, widget=self.treeChannels)
2631 # Report subscriptions that require authentication
2632 if authreq:
2633 retry_podcasts = {}
2634 for url in authreq:
2635 title = _('Podcast requires authentication')
2636 message = _('Please login to %s:') % (saxutils.escape(url),)
2637 success, auth_tokens = self.show_login_dialog(title, message)
2638 if success:
2639 retry_podcasts[url] = auth_tokens
2640 else:
2641 # Stop asking the user for more login data
2642 retry_podcasts = {}
2643 for url in authreq:
2644 error_messages[url] = _('Authentication failed')
2645 failed.append(url)
2646 break
2648 # If we have authentication data to retry, do so here
2649 if retry_podcasts:
2650 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2652 # Report website redirections
2653 for url in redirections:
2654 title = _('Website redirection detected')
2655 message = _('The URL %(url)s redirects to %(target)s.') \
2656 + '\n\n' + _('Do you want to visit the website now?')
2657 message = message % {'url': url, 'target': redirections[url]}
2658 if self.show_confirmation(message, title):
2659 util.open_website(url)
2660 else:
2661 break
2663 # Report failed subscriptions to the user
2664 if failed:
2665 title = _('Could not add some podcasts')
2666 message = _('Some podcasts could not be added to your list:') \
2667 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2668 error_messages.get(url, _('Unknown')))) for url in failed)
2669 self.show_message(message, title, important=True)
2671 # Upload subscription changes to gpodder.net
2672 self.mygpo_client.on_subscribe(worked)
2674 # If at least one podcast has been added, save and update all
2675 if self.channel_list_changed:
2676 # Fix URLs if mygpo has rewritten them
2677 self.rewrite_urls_mygpo()
2679 self.save_channels_opml()
2681 # If only one podcast was added, select it after the update
2682 if len(worked) == 1:
2683 url = worked[0]
2684 else:
2685 url = None
2687 # Update the list of subscribed podcasts
2688 self.update_feed_cache(force_update=False, select_url_afterwards=url)
2689 self.update_podcasts_tab()
2691 # Offer to download new episodes
2692 episodes = []
2693 for podcast in self.channels:
2694 if podcast.url in worked:
2695 episodes.extend(podcast.get_all_episodes())
2697 if episodes:
2698 episodes = list(PodcastEpisode.sort_by_pubdate(episodes, \
2699 reverse=True))
2700 self.new_episodes_show(episodes, \
2701 selected=[e.check_is_new() for e in episodes])
2704 def thread_proc():
2705 # After the initial sorting and splitting, try all queued podcasts
2706 length = len(queued)
2707 for index, url in enumerate(queued):
2708 progress.on_progress(float(index)/float(length))
2709 progress.on_message(url)
2710 log('QUEUE RUNNER: %s', url, sender=self)
2711 try:
2712 # The URL is valid and does not exist already - subscribe!
2713 channel = PodcastChannel.load(self.db, url=url, create=True, \
2714 authentication_tokens=auth_tokens.get(url, None), \
2715 max_episodes=self.config.max_episodes_per_feed, \
2716 download_dir=self.config.download_dir, \
2717 allow_empty_feeds=self.config.allow_empty_feeds, \
2718 mimetype_prefs=self.config.mimetype_prefs)
2720 try:
2721 username, password = util.username_password_from_url(url)
2722 except ValueError, ve:
2723 username, password = (None, None)
2725 if username is not None and channel.username is None and \
2726 password is not None and channel.password is None:
2727 channel.username = username
2728 channel.password = password
2729 channel.save()
2731 self._update_cover(channel)
2732 except feedcore.AuthenticationRequired:
2733 if url in auth_tokens:
2734 # Fail for wrong authentication data
2735 error_messages[url] = _('Authentication failed')
2736 failed.append(url)
2737 else:
2738 # Queue for login dialog later
2739 authreq.append(url)
2740 continue
2741 except feedcore.WifiLogin, error:
2742 redirections[url] = error.data
2743 failed.append(url)
2744 error_messages[url] = _('Redirection detected')
2745 continue
2746 except Exception, e:
2747 log('Subscription error: %s', e, traceback=True, sender=self)
2748 error_messages[url] = str(e)
2749 failed.append(url)
2750 continue
2752 assert channel is not None
2753 worked.append(channel.url)
2754 self.channels.append(channel)
2755 self.channel_list_changed = True
2756 util.idle_add(on_after_update)
2757 threading.Thread(target=thread_proc).start()
2759 def save_channels_opml(self):
2760 exporter = opml.Exporter(gpodder.subscription_file)
2761 return exporter.write(self.channels)
2763 def find_episode(self, podcast_url, episode_url):
2764 """Find an episode given its podcast and episode URL
2766 The function will return a PodcastEpisode object if
2767 the episode is found, or None if it's not found.
2769 for podcast in self.channels:
2770 if podcast_url == podcast.url:
2771 for episode in podcast.get_all_episodes():
2772 if episode_url == episode.url:
2773 return episode
2775 return None
2777 def process_received_episode_actions(self, updated_urls):
2778 """Process/merge episode actions from gpodder.net
2780 This function will merge all changes received from
2781 the server to the local database and update the
2782 status of the affected episodes as necessary.
2784 indicator = ProgressIndicator(_('Merging episode actions'), \
2785 _('Episode actions from gpodder.net are merged.'), \
2786 False, self.get_dialog_parent())
2788 for idx, action in enumerate(self.mygpo_client.get_episode_actions(updated_urls)):
2789 if action.action == 'play':
2790 episode = self.find_episode(action.podcast_url, \
2791 action.episode_url)
2793 if episode is not None:
2794 log('Play action for %s', episode.url, sender=self)
2795 episode.mark(is_played=True)
2797 if action.timestamp > episode.current_position_updated and \
2798 action.position is not None:
2799 log('Updating position for %s', episode.url, sender=self)
2800 episode.current_position = action.position
2801 episode.current_position_updated = action.timestamp
2803 if action.total:
2804 log('Updating total time for %s', episode.url, sender=self)
2805 episode.total_time = action.total
2807 episode.save()
2808 elif action.action == 'delete':
2809 episode = self.find_episode(action.podcast_url, \
2810 action.episode_url)
2812 if episode is not None:
2813 if not episode.was_downloaded(and_exists=True):
2814 # Set the episode to a "deleted" state
2815 log('Marking as deleted: %s', episode.url, sender=self)
2816 episode.delete_from_disk()
2817 episode.save()
2819 indicator.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx) % {'count':idx})
2820 gtk.main_iteration(False)
2822 indicator.on_finished()
2823 self.db.commit()
2826 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2827 self.db.commit()
2828 self.updating_feed_cache = False
2830 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2832 # Process received episode actions for all updated URLs
2833 self.process_received_episode_actions(updated_urls)
2835 self.channel_list_changed = True
2836 self.update_podcast_list_model(select_url=select_url_afterwards)
2838 # Only search for new episodes in podcasts that have been
2839 # updated, not in other podcasts (for single-feed updates)
2840 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2842 if gpodder.ui.fremantle:
2843 self.fancy_progress_bar.hide()
2844 self.button_subscribe.set_sensitive(True)
2845 self.button_refresh.set_sensitive(True)
2846 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2847 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2848 self.update_podcasts_tab()
2849 self.update_episode_list_model()
2850 if self.feed_cache_update_cancelled:
2851 return
2853 def application_in_foreground():
2854 try:
2855 return any(w.get_property('is-topmost') for w in hildon.WindowStack.get_default().get_windows())
2856 except Exception, e:
2857 log('Could not determine is-topmost', traceback=True)
2858 # When in doubt, assume not in foreground
2859 return False
2861 if episodes:
2862 if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2863 # New episodes found, but we should do nothing
2864 self.show_message(_('New episodes are available.'))
2865 elif self.config.auto_download == 'always':
2866 count = len(episodes)
2867 title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2868 self.show_message(title)
2869 self.download_episode_list(episodes)
2870 elif self.config.auto_download == 'queue':
2871 self.show_message(_('New episodes have been added to the download list.'))
2872 self.download_episode_list_paused(episodes)
2873 elif application_in_foreground():
2874 if not self._fremantle_notification_visible:
2875 self.new_episodes_show(episodes)
2876 elif not self._fremantle_notification_visible:
2877 try:
2878 import pynotify
2879 pynotify.init('gPodder')
2880 n = pynotify.Notification('gPodder', _('New episodes available'), 'gpodder')
2881 n.set_urgency(pynotify.URGENCY_CRITICAL)
2882 n.set_hint('dbus-callback-default', ' '.join([
2883 gpodder.dbus_bus_name,
2884 gpodder.dbus_gui_object_path,
2885 gpodder.dbus_interface,
2886 'offer_new_episodes',
2888 n.set_category('gpodder-new-episodes')
2889 n.show()
2890 self._fremantle_notification_visible = True
2891 except Exception, e:
2892 log('Error: %s', str(e), sender=self, traceback=True)
2893 self.new_episodes_show(episodes)
2894 self._fremantle_notification_visible = False
2895 elif not self.config.auto_update_feeds:
2896 self.show_message(_('No new episodes. Please check for new episodes later.'))
2897 return
2899 if self.tray_icon:
2900 self.tray_icon.set_status()
2902 if self.feed_cache_update_cancelled:
2903 # The user decided to abort the feed update
2904 self.show_update_feeds_buttons()
2905 elif not episodes:
2906 # Nothing new here - but inform the user
2907 self.pbFeedUpdate.set_fraction(1.0)
2908 self.pbFeedUpdate.set_text(_('No new episodes'))
2909 self.feed_cache_update_cancelled = True
2910 self.btnCancelFeedUpdate.show()
2911 self.btnCancelFeedUpdate.set_sensitive(True)
2912 self.itemUpdate.set_sensitive(True)
2913 if gpodder.ui.maemo:
2914 # btnCancelFeedUpdate is a ToolButton on Maemo
2915 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2916 else:
2917 # btnCancelFeedUpdate is a normal gtk.Button
2918 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2919 else:
2920 count = len(episodes)
2921 # New episodes are available
2922 self.pbFeedUpdate.set_fraction(1.0)
2923 # Are we minimized and should we auto download?
2924 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2925 self.download_episode_list(episodes)
2926 title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2927 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2928 self.show_update_feeds_buttons()
2929 elif self.config.auto_download == 'queue':
2930 self.download_episode_list_paused(episodes)
2931 title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
2932 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2933 self.show_update_feeds_buttons()
2934 else:
2935 self.show_update_feeds_buttons()
2936 # New episodes are available and we are not minimized
2937 if not self.config.do_not_show_new_episodes_dialog:
2938 self.new_episodes_show(episodes, notification=True)
2939 else:
2940 message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
2941 self.pbFeedUpdate.set_text(message)
2943 def _update_cover(self, channel):
2944 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2945 self.cover_downloader.request_cover(channel)
2947 def update_feed_cache_proc(self, channels, select_url_afterwards):
2948 total = len(channels)
2950 for updated, channel in enumerate(channels):
2951 if not self.feed_cache_update_cancelled:
2952 try:
2953 channel.update(max_episodes=self.config.max_episodes_per_feed, \
2954 mimetype_prefs=self.config.mimetype_prefs)
2955 self._update_cover(channel)
2956 except Exception, e:
2957 d = {'url': saxutils.escape(channel.url), 'message': saxutils.escape(str(e))}
2958 if d['message']:
2959 message = _('Error while updating %(url)s: %(message)s')
2960 else:
2961 message = _('The feed at %(url)s could not be updated.')
2962 self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
2963 log('Error: %s', str(e), sender=self, traceback=True)
2965 if self.feed_cache_update_cancelled:
2966 break
2968 # By the time we get here the update may have already been cancelled
2969 if not self.feed_cache_update_cancelled:
2970 def update_progress():
2971 d = {'podcast': channel.title, 'position': updated+1, 'total': total}
2972 progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2973 self.pbFeedUpdate.set_text(progression)
2974 if self.tray_icon:
2975 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2976 self.pbFeedUpdate.set_fraction(float(updated+1)/float(total))
2977 util.idle_add(update_progress)
2979 updated_urls = [c.url for c in channels]
2980 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2982 def show_update_feeds_buttons(self):
2983 # Make sure that the buttons for updating feeds
2984 # appear - this should happen after a feed update
2985 if gpodder.ui.maemo:
2986 self.btnUpdateSelectedFeed.show()
2987 self.toolFeedUpdateProgress.hide()
2988 self.btnCancelFeedUpdate.hide()
2989 self.btnCancelFeedUpdate.set_is_important(False)
2990 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2991 self.toolbarSpacer.set_expand(True)
2992 self.toolbarSpacer.set_draw(False)
2993 else:
2994 self.hboxUpdateFeeds.hide()
2995 self.btnUpdateFeeds.show()
2996 self.itemUpdate.set_sensitive(True)
2997 self.itemUpdateChannel.set_sensitive(True)
2999 def on_btnCancelFeedUpdate_clicked(self, widget):
3000 if not self.feed_cache_update_cancelled:
3001 self.pbFeedUpdate.set_text(_('Cancelling...'))
3002 self.feed_cache_update_cancelled = True
3003 if not gpodder.ui.fremantle:
3004 self.btnCancelFeedUpdate.set_sensitive(False)
3005 elif not gpodder.ui.fremantle:
3006 self.show_update_feeds_buttons()
3008 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
3009 if self.updating_feed_cache:
3010 if gpodder.ui.fremantle:
3011 self.feed_cache_update_cancelled = True
3012 return
3014 if not force_update:
3015 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
3016 self.channel_list_changed = True
3017 self.update_podcast_list_model(select_url=select_url_afterwards)
3018 return
3020 # Fix URLs if mygpo has rewritten them
3021 self.rewrite_urls_mygpo()
3023 self.updating_feed_cache = True
3025 if channels is None:
3026 # Only update podcasts for which updates are enabled
3027 channels = [c for c in self.channels if c.feed_update_enabled]
3029 if gpodder.ui.fremantle:
3030 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
3031 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
3032 self.fancy_progress_bar.show()
3033 self.button_subscribe.set_sensitive(False)
3034 self.button_refresh.set_sensitive(False)
3035 self.feed_cache_update_cancelled = False
3036 else:
3037 self.itemUpdate.set_sensitive(False)
3038 self.itemUpdateChannel.set_sensitive(False)
3040 if self.tray_icon:
3041 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
3043 self.feed_cache_update_cancelled = False
3044 self.btnCancelFeedUpdate.show()
3045 self.btnCancelFeedUpdate.set_sensitive(True)
3046 if gpodder.ui.maemo:
3047 self.toolbarSpacer.set_expand(False)
3048 self.toolbarSpacer.set_draw(True)
3049 self.btnUpdateSelectedFeed.hide()
3050 self.toolFeedUpdateProgress.show_all()
3051 else:
3052 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
3053 self.hboxUpdateFeeds.show_all()
3054 self.btnUpdateFeeds.hide()
3056 if len(channels) == 1:
3057 text = _('Updating "%s"...') % channels[0].title
3058 else:
3059 count = len(channels)
3060 text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count) % {'count':count}
3061 self.pbFeedUpdate.set_text(text)
3062 self.pbFeedUpdate.set_fraction(0)
3064 args = (channels, select_url_afterwards)
3065 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
3067 def on_gPodder_delete_event(self, widget, *args):
3068 """Called when the GUI wants to close the window
3069 Displays a confirmation dialog (and closes/hides gPodder)
3072 downloading = self.download_status_model.are_downloads_in_progress()
3074 # Only iconify if we are using the window's "X" button,
3075 # but not when we are using "Quit" in the menu or toolbar
3076 if self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
3077 self.iconify_main_window()
3078 elif downloading:
3079 if gpodder.ui.fremantle:
3080 self.close_gpodder()
3081 elif gpodder.ui.diablo:
3082 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
3083 if result:
3084 self.close_gpodder()
3085 else:
3086 return True
3087 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
3088 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3089 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
3091 title = _('Quit gPodder')
3092 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3094 dialog.set_title(title)
3095 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
3097 quit_button.grab_focus()
3098 result = dialog.run()
3099 dialog.destroy()
3101 if result == gtk.RESPONSE_CLOSE:
3102 self.close_gpodder()
3103 else:
3104 self.close_gpodder()
3106 return True
3108 def close_gpodder(self):
3109 """ clean everything and exit properly
3111 if self.channels:
3112 if self.save_channels_opml():
3113 pass # FIXME: Add mygpo synchronization here
3114 else:
3115 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
3117 self.gPodder.hide()
3119 if self.tray_icon is not None:
3120 self.tray_icon.set_visible(False)
3122 # Notify all tasks to to carry out any clean-up actions
3123 self.download_status_model.tell_all_tasks_to_quit()
3125 while gtk.events_pending():
3126 gtk.main_iteration(False)
3128 self.db.close()
3130 self.quit()
3131 sys.exit(0)
3133 def get_expired_episodes(self):
3134 for channel in self.channels:
3135 for episode in channel.get_downloaded_episodes():
3136 # Never consider locked episodes as old
3137 if episode.is_locked:
3138 continue
3140 # Never consider fresh episodes as old
3141 if episode.age_in_days() < self.config.episode_old_age:
3142 continue
3144 # Do not delete played episodes (except if configured)
3145 if episode.is_played:
3146 if not self.config.auto_remove_played_episodes:
3147 continue
3149 # Do not delete unplayed episodes (except if configured)
3150 if not episode.is_played:
3151 if not self.config.auto_remove_unplayed_episodes:
3152 continue
3154 yield episode
3156 def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
3157 if not episodes:
3158 return False
3160 if skip_locked:
3161 episodes = [e for e in episodes if not e.is_locked]
3163 if not episodes:
3164 title = _('Episodes are locked')
3165 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3166 self.notification(message, title, widget=self.treeAvailable)
3167 return False
3169 count = len(episodes)
3170 title = N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count) % {'count':count}
3171 message = _('Deleting episodes removes downloaded files.')
3173 if gpodder.ui.fremantle:
3174 message = '\n'.join([title, message])
3176 if confirm and not self.show_confirmation(message, title):
3177 return False
3179 progress = ProgressIndicator(_('Deleting episodes'), \
3180 _('Please wait while episodes are deleted'), \
3181 parent=self.get_dialog_parent())
3183 def finish_deletion(episode_urls, channel_urls):
3184 progress.on_finished()
3186 # Episodes have been deleted - persist the database
3187 self.db.commit()
3189 self.update_episode_list_icons(episode_urls)
3190 self.update_podcast_list_model(channel_urls)
3191 self.play_or_download()
3193 def thread_proc():
3194 episode_urls = set()
3195 channel_urls = set()
3197 episodes_status_update = []
3198 for idx, episode in enumerate(episodes):
3199 progress.on_progress(float(idx)/float(len(episodes)))
3200 if episode.is_locked and skip_locked:
3201 log('Not deleting episode (is locked): %s', episode.title)
3202 else:
3203 log('Deleting episode: %s', episode.title)
3204 progress.on_message(episode.title)
3205 episode.delete_from_disk()
3206 episode_urls.add(episode.url)
3207 channel_urls.add(episode.channel.url)
3208 episodes_status_update.append(episode)
3210 # Tell the shownotes window that we have removed the episode
3211 if self.episode_shownotes_window is not None and \
3212 self.episode_shownotes_window.episode is not None and \
3213 self.episode_shownotes_window.episode.url == episode.url:
3214 util.idle_add(self.episode_shownotes_window._download_status_changed, None)
3216 # Notify the web service about the status update + upload
3217 self.mygpo_client.on_delete(episodes_status_update)
3218 self.mygpo_client.flush()
3220 util.idle_add(finish_deletion, episode_urls, channel_urls)
3222 threading.Thread(target=thread_proc).start()
3224 return True
3226 def on_itemRemoveOldEpisodes_activate(self, widget):
3227 self.show_delete_episodes_window()
3229 def show_delete_episodes_window(self, channel=None):
3230 """Offer deletion of episodes
3232 If channel is None, offer deletion of all episodes.
3233 Otherwise only offer deletion of episodes in the channel.
3235 if gpodder.ui.maemo:
3236 columns = (
3237 ('maemo_remove_markup', None, None, _('Episode')),
3239 else:
3240 columns = (
3241 ('title_markup', None, None, _('Episode')),
3242 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3243 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3244 ('played_prop', None, None, _('Status')),
3245 ('age_prop', 'age_int_prop', gobject.TYPE_INT, _('Downloaded')),
3248 msg_older_than = N_('Select older than %(count)d day', 'Select older than %(count)d days', self.config.episode_old_age)
3249 selection_buttons = {
3250 _('Select played'): lambda episode: episode.is_played,
3251 _('Select finished'): lambda episode: episode.is_finished(),
3252 msg_older_than % {'count':self.config.episode_old_age}: lambda episode: episode.age_in_days() > self.config.episode_old_age,
3255 instructions = _('Select the episodes you want to delete:')
3257 if channel is None:
3258 channels = self.channels
3259 else:
3260 channels = [channel]
3262 episodes = []
3263 for channel in channels:
3264 for episode in channel.get_downloaded_episodes():
3265 # Disallow deletion of locked episodes that still exist
3266 if not episode.is_locked or not episode.file_exists():
3267 episodes.append(episode)
3269 selected = [e.is_played or not e.file_exists() for e in episodes]
3271 gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
3272 episodes = episodes, selected = selected, columns = columns, \
3273 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
3274 selection_buttons = selection_buttons, _config=self.config, \
3275 show_episode_shownotes=self.show_episode_shownotes)
3277 def on_selected_episodes_status_changed(self):
3278 # The order of the updates here is important! When "All episodes" is
3279 # selected, the update of the podcast list model depends on the episode
3280 # list selection to determine which podcasts are affected. Updating
3281 # the episode list could remove the selection if a filter is active.
3282 self.update_podcast_list_model(selected=True)
3283 self.update_episode_list_icons(selected=True)
3284 self.db.commit()
3286 def mark_selected_episodes_new(self):
3287 for episode in self.get_selected_episodes():
3288 episode.mark_new()
3289 self.on_selected_episodes_status_changed()
3291 def mark_selected_episodes_old(self):
3292 for episode in self.get_selected_episodes():
3293 episode.mark_old()
3294 self.on_selected_episodes_status_changed()
3296 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
3297 for episode in self.get_selected_episodes():
3298 if toggle:
3299 episode.mark(is_played=not episode.is_played)
3300 else:
3301 episode.mark(is_played=new_value)
3302 self.on_selected_episodes_status_changed()
3304 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3305 for episode in self.get_selected_episodes():
3306 if toggle:
3307 episode.mark(is_locked=not episode.is_locked)
3308 else:
3309 episode.mark(is_locked=new_value)
3310 self.on_selected_episodes_status_changed()
3312 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3313 if self.active_channel is None:
3314 return
3316 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
3317 self.active_channel.update_channel_lock()
3319 for episode in self.active_channel.get_all_episodes():
3320 episode.mark(is_locked=self.active_channel.channel_is_locked)
3322 self.update_podcast_list_model(selected=True)
3323 self.update_episode_list_icons(all=True)
3325 def on_itemUpdateChannel_activate(self, widget=None):
3326 if self.active_channel is None:
3327 title = _('No podcast selected')
3328 message = _('Please select a podcast in the podcasts list to update.')
3329 self.show_message( message, title, widget=self.treeChannels)
3330 return
3332 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3333 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3334 self.update_feed_cache()
3335 else:
3336 self.update_feed_cache(channels=[self.active_channel])
3338 def on_itemUpdate_activate(self, widget=None):
3339 # Check if we have outstanding subscribe/unsubscribe actions
3340 if self.on_add_remove_podcasts_mygpo():
3341 log('Update cancelled (received server changes)', sender=self)
3342 return
3344 if self.channels:
3345 self.update_feed_cache()
3346 else:
3347 gPodderWelcome(self.gPodder,
3348 center_on_widget=self.gPodder,
3349 show_example_podcasts_callback=self.on_itemImportChannels_activate,
3350 setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
3352 def download_episode_list_paused(self, episodes):
3353 self.download_episode_list(episodes, True)
3355 def download_episode_list(self, episodes, add_paused=False, force_start=False):
3356 enable_update = False
3358 for episode in episodes:
3359 log('Downloading episode: %s', episode.title, sender = self)
3360 if not episode.was_downloaded(and_exists=True):
3361 task_exists = False
3362 for task in self.download_tasks_seen:
3363 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
3364 self.download_queue_manager.add_task(task, force_start)
3365 enable_update = True
3366 task_exists = True
3367 continue
3369 if task_exists:
3370 continue
3372 try:
3373 task = download.DownloadTask(episode, self.config)
3374 except Exception, e:
3375 d = {'episode': episode.title, 'message': str(e)}
3376 message = _('Download error while downloading %(episode)s: %(message)s')
3377 self.show_message(message % d, _('Download error'), important=True)
3378 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
3379 continue
3381 if add_paused:
3382 task.status = task.PAUSED
3383 else:
3384 self.mygpo_client.on_download([task.episode])
3385 self.download_queue_manager.add_task(task, force_start)
3387 self.download_status_model.register_task(task)
3388 enable_update = True
3390 if enable_update:
3391 self.enable_download_list_update()
3393 # Flush updated episode status
3394 self.mygpo_client.flush()
3396 def cancel_task_list(self, tasks):
3397 if not tasks:
3398 return
3400 for task in tasks:
3401 if task.status in (task.QUEUED, task.DOWNLOADING):
3402 task.status = task.CANCELLED
3403 elif task.status == task.PAUSED:
3404 task.status = task.CANCELLED
3405 # Call run, so the partial file gets deleted
3406 task.run()
3408 self.update_episode_list_icons([task.url for task in tasks])
3409 self.play_or_download()
3411 # Update the tab title and downloads list
3412 self.update_downloads_list()
3414 def new_episodes_show(self, episodes, notification=False, selected=None):
3415 if gpodder.ui.maemo:
3416 columns = (
3417 ('maemo_markup', None, None, _('Episode')),
3419 show_notification = notification
3420 else:
3421 columns = (
3422 ('title_markup', None, None, _('Episode')),
3423 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3424 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3426 show_notification = False
3428 instructions = _('Select the episodes you want to download:')
3430 if self.new_episodes_window is not None:
3431 self.new_episodes_window.main_window.destroy()
3432 self.new_episodes_window = None
3434 def download_episodes_callback(episodes):
3435 self.new_episodes_window = None
3436 self.download_episode_list(episodes)
3438 if selected is None:
3439 # Select all by default
3440 selected = [True]*len(episodes)
3442 self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3443 title=_('New episodes available'), \
3444 instructions=instructions, \
3445 episodes=episodes, \
3446 columns=columns, \
3447 selected=selected, \
3448 stock_ok_button = 'gpodder-download', \
3449 callback=download_episodes_callback, \
3450 remove_callback=lambda e: e.mark_old(), \
3451 remove_action=_('Mark as old'), \
3452 remove_finished=self.episode_new_status_changed, \
3453 _config=self.config, \
3454 show_notification=show_notification, \
3455 show_episode_shownotes=self.show_episode_shownotes)
3457 def on_itemDownloadAllNew_activate(self, widget, *args):
3458 if not self.offer_new_episodes():
3459 self.show_message(_('Please check for new episodes later.'), \
3460 _('No new episodes available'), widget=self.btnUpdateFeeds)
3462 def get_new_episodes(self, channels=None):
3463 if channels is None:
3464 channels = self.channels
3465 episodes = []
3466 for channel in channels:
3467 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3468 episodes.append(episode)
3470 return episodes
3472 @dbus.service.method(gpodder.dbus_interface)
3473 def start_device_synchronization(self):
3474 """Public D-Bus API for starting Device sync (Desktop only)
3476 This method can be called to initiate a synchronization with
3477 a configured protable media player. This only works for the
3478 Desktop version of gPodder and does nothing on Maemo.
3480 if gpodder.ui.desktop:
3481 self.on_sync_to_ipod_activate(None)
3482 return True
3484 return False
3486 def on_sync_to_ipod_activate(self, widget, episodes=None):
3487 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
3489 def commit_changes_to_database(self):
3490 """This will be called after the sync process is finished"""
3491 self.db.commit()
3493 def on_cleanup_ipod_activate(self, widget, *args):
3494 self.sync_ui.on_cleanup_device()
3496 def on_manage_device_playlist(self, widget):
3497 self.sync_ui.on_manage_device_playlist()
3499 def show_hide_tray_icon(self):
3500 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3501 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3502 elif not self.config.display_tray_icon and self.tray_icon is not None:
3503 self.tray_icon.set_visible(False)
3504 del self.tray_icon
3505 self.tray_icon = None
3507 if self.config.minimize_to_tray and self.tray_icon:
3508 self.tray_icon.set_visible(self.is_iconified())
3509 elif self.tray_icon:
3510 self.tray_icon.set_visible(True)
3512 def on_itemShowAllEpisodes_activate(self, widget):
3513 self.config.podcast_list_view_all = widget.get_active()
3515 def on_itemShowToolbar_activate(self, widget):
3516 self.config.show_toolbar = self.itemShowToolbar.get_active()
3518 def on_itemShowDescription_activate(self, widget):
3519 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3521 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3522 self.config.podcast_list_hide_boring = toggleaction.get_active()
3523 if self.config.podcast_list_hide_boring:
3524 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3525 else:
3526 self.podcast_list_model.set_view_mode(-1)
3528 def on_item_view_podcasts_changed(self, radioaction, current):
3529 # Only on Fremantle
3530 if current == self.item_view_podcasts_all:
3531 self.podcast_list_model.set_view_mode(-1)
3532 elif current == self.item_view_podcasts_downloaded:
3533 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3534 elif current == self.item_view_podcasts_unplayed:
3535 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3537 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3539 def on_item_view_episodes_changed(self, radioaction, current):
3540 if current == self.item_view_episodes_all:
3541 self.config.episode_list_view_mode = EpisodeListModel.VIEW_ALL
3542 elif current == self.item_view_episodes_undeleted:
3543 self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNDELETED
3544 elif current == self.item_view_episodes_downloaded:
3545 self.config.episode_list_view_mode = EpisodeListModel.VIEW_DOWNLOADED
3546 elif current == self.item_view_episodes_unplayed:
3547 self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNPLAYED
3549 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
3551 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3552 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3554 def update_item_device( self):
3555 if not gpodder.ui.fremantle:
3556 if self.config.device_type != 'none':
3557 self.itemDevice.set_visible(True)
3558 self.itemDevice.label = self.get_device_name()
3559 else:
3560 self.itemDevice.set_visible(False)
3562 def properties_closed( self):
3563 self.preferences_dialog = None
3564 self.show_hide_tray_icon()
3565 self.update_item_device()
3566 if gpodder.ui.maemo:
3567 selection = self.treeAvailable.get_selection()
3568 if self.config.maemo_enable_gestures or \
3569 self.config.enable_fingerscroll:
3570 selection.set_mode(gtk.SELECTION_SINGLE)
3571 else:
3572 selection.set_mode(gtk.SELECTION_MULTIPLE)
3574 def on_itemPreferences_activate(self, widget, *args):
3575 self.preferences_dialog = gPodderPreferences(self.main_window, \
3576 _config=self.config, \
3577 callback_finished=self.properties_closed, \
3578 user_apps_reader=self.user_apps_reader, \
3579 parent_window=self.main_window, \
3580 mygpo_client=self.mygpo_client, \
3581 on_send_full_subscriptions=self.on_send_full_subscriptions)
3583 # Initial message to relayout window (in case it's opened in portrait mode
3584 self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3586 def on_itemDependencies_activate(self, widget):
3587 gPodderDependencyManager(self.gPodder)
3589 def on_goto_mygpo(self, widget):
3590 self.mygpo_client.open_website()
3592 def on_download_subscriptions_from_mygpo(self, action=None):
3593 title = _('Login to gpodder.net')
3594 message = _('Please login to download your subscriptions.')
3595 success, (username, password) = self.show_login_dialog(title, message, \
3596 self.config.mygpo_username, self.config.mygpo_password)
3597 if not success:
3598 return
3600 self.config.mygpo_username = username
3601 self.config.mygpo_password = password
3603 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3604 custom_title=_('Subscriptions on gpodder.net'), \
3605 add_urls_callback=self.add_podcast_list, \
3606 hide_url_entry=True)
3608 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3609 # we do not have to hardcode the URL here
3610 OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
3611 url = util.url_add_authentication(OPML_URL, \
3612 self.config.mygpo_username, \
3613 self.config.mygpo_password)
3614 dir.download_opml_file(url)
3616 def on_mygpo_settings_activate(self, action=None):
3617 # This dialog is only used for Maemo 4
3618 if not gpodder.ui.diablo:
3619 return
3621 settings = MygPodderSettings(self.main_window, \
3622 config=self.config, \
3623 mygpo_client=self.mygpo_client, \
3624 on_send_full_subscriptions=self.on_send_full_subscriptions)
3626 def on_itemAddChannel_activate(self, widget=None):
3627 gPodderAddPodcast(self.gPodder, \
3628 add_urls_callback=self.add_podcast_list)
3630 def on_itemEditChannel_activate(self, widget, *args):
3631 if self.active_channel is None:
3632 title = _('No podcast selected')
3633 message = _('Please select a podcast in the podcasts list to edit.')
3634 self.show_message( message, title, widget=self.treeChannels)
3635 return
3637 callback_closed = lambda: self.update_podcast_list_model(selected=True)
3638 gPodderChannel(self.main_window, \
3639 channel=self.active_channel, \
3640 callback_closed=callback_closed, \
3641 cover_downloader=self.cover_downloader)
3643 def on_itemMassUnsubscribe_activate(self, item=None):
3644 columns = (
3645 ('title', None, None, _('Podcast')),
3648 # We're abusing the Episode Selector for selecting Podcasts here,
3649 # but it works and looks good, so why not? -- thp
3650 gPodderEpisodeSelector(self.main_window, \
3651 title=_('Remove podcasts'), \
3652 instructions=_('Select the podcast you want to remove.'), \
3653 episodes=self.channels, \
3654 columns=columns, \
3655 size_attribute=None, \
3656 stock_ok_button=_('Remove'), \
3657 callback=self.remove_podcast_list, \
3658 _config=self.config)
3660 def remove_podcast_list(self, channels, confirm=True):
3661 if not channels:
3662 log('No podcasts selected for deletion', sender=self)
3663 return
3665 if len(channels) == 1:
3666 title = _('Removing podcast')
3667 info = _('Please wait while the podcast is removed')
3668 message = _('Do you really want to remove this podcast and its episodes?')
3669 else:
3670 title = _('Removing podcasts')
3671 info = _('Please wait while the podcasts are removed')
3672 message = _('Do you really want to remove the selected podcasts and their episodes?')
3674 if confirm and not self.show_confirmation(message, title):
3675 return
3677 progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3679 def finish_deletion(select_url):
3680 # Upload subscription list changes to the web service
3681 self.mygpo_client.on_unsubscribe([c.url for c in channels])
3683 # Re-load the channels and select the desired new channel
3684 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3685 progress.on_finished()
3686 self.update_podcasts_tab()
3688 def thread_proc():
3689 select_url = None
3691 for idx, channel in enumerate(channels):
3692 # Update the UI for correct status messages
3693 progress.on_progress(float(idx)/float(len(channels)))
3694 progress.on_message(channel.title)
3696 # Delete downloaded episodes
3697 channel.remove_downloaded()
3699 # cancel any active downloads from this channel
3700 for episode in channel.get_all_episodes():
3701 util.idle_add(self.download_status_model.cancel_by_url,
3702 episode.url)
3704 if len(channels) == 1:
3705 # get the URL of the podcast we want to select next
3706 if channel in self.channels:
3707 position = self.channels.index(channel)
3708 else:
3709 position = -1
3711 if position == len(self.channels)-1:
3712 # this is the last podcast, so select the URL
3713 # of the item before this one (i.e. the "new last")
3714 select_url = self.channels[position-1].url
3715 else:
3716 # there is a podcast after the deleted one, so
3717 # we simply select the one that comes after it
3718 select_url = self.channels[position+1].url
3720 # Remove the channel and clean the database entries
3721 channel.delete()
3722 self.channels.remove(channel)
3724 # Clean up downloads and download directories
3725 self.clean_up_downloads()
3727 self.channel_list_changed = True
3728 self.save_channels_opml()
3730 # The remaining stuff is to be done in the GTK main thread
3731 util.idle_add(finish_deletion, select_url)
3733 threading.Thread(target=thread_proc).start()
3735 def on_itemRemoveChannel_activate(self, widget, *args):
3736 if self.active_channel is None:
3737 title = _('No podcast selected')
3738 message = _('Please select a podcast in the podcasts list to remove.')
3739 self.show_message( message, title, widget=self.treeChannels)
3740 return
3742 self.remove_podcast_list([self.active_channel])
3744 def get_opml_filter(self):
3745 filter = gtk.FileFilter()
3746 filter.add_pattern('*.opml')
3747 filter.add_pattern('*.xml')
3748 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3749 return filter
3751 def on_item_import_from_file_activate(self, widget, filename=None):
3752 if filename is None:
3753 if gpodder.ui.desktop or gpodder.ui.fremantle:
3754 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), \
3755 parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3756 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3757 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3758 elif gpodder.ui.diablo:
3759 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3760 dlg.set_filter(self.get_opml_filter())
3761 response = dlg.run()
3762 filename = None
3763 if response == gtk.RESPONSE_OK:
3764 filename = dlg.get_filename()
3765 dlg.destroy()
3767 if filename is not None:
3768 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3769 custom_title=_('Import podcasts from OPML file'), \
3770 add_urls_callback=self.add_podcast_list, \
3771 hide_url_entry=True)
3772 dir.download_opml_file(filename)
3774 def on_itemExportChannels_activate(self, widget, *args):
3775 if not self.channels:
3776 title = _('Nothing to export')
3777 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3778 self.show_message(message, title, widget=self.treeChannels)
3779 return
3781 if gpodder.ui.desktop or gpodder.ui.fremantle:
3782 # FIXME: Hildonization on Fremantle
3783 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3784 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3785 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3786 elif gpodder.ui.diablo:
3787 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3788 dlg.set_filter(self.get_opml_filter())
3789 response = dlg.run()
3790 if response == gtk.RESPONSE_OK:
3791 filename = dlg.get_filename()
3792 dlg.destroy()
3793 exporter = opml.Exporter( filename)
3794 if exporter.write(self.channels):
3795 count = len(self.channels)
3796 title = N_('%(count)d subscription exported', '%(count)d subscriptions exported', count) % {'count':count}
3797 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3798 else:
3799 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3800 else:
3801 dlg.destroy()
3803 def on_itemImportChannels_activate(self, widget, *args):
3804 if gpodder.ui.fremantle:
3805 gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3806 self.config.toplist_url, \
3807 self.config.opml_url, \
3808 self.add_podcast_list, \
3809 self.on_itemAddChannel_activate, \
3810 self.on_download_subscriptions_from_mygpo, \
3811 self.show_text_edit_dialog)
3812 else:
3813 dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3814 add_urls_callback=self.add_podcast_list)
3815 util.idle_add(dir.download_opml_file, self.config.opml_url)
3817 def on_homepage_activate(self, widget, *args):
3818 util.open_website(gpodder.__url__)
3820 def on_wiki_activate(self, widget, *args):
3821 util.open_website('http://gpodder.org/wiki/User_Manual')
3823 def on_bug_tracker_activate(self, widget, *args):
3824 if gpodder.ui.maemo:
3825 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3826 else:
3827 util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3829 def on_item_support_activate(self, widget):
3830 util.open_website('http://gpodder.org/donate')
3832 def on_itemAbout_activate(self, widget, *args):
3833 if gpodder.ui.fremantle:
3834 from gpodder.gtkui.frmntl.about import HeAboutDialog
3835 HeAboutDialog.present(self.main_window,
3836 'gPodder',
3837 'gpodder',
3838 gpodder.__version__,
3839 _('A podcast client with focus on usability'),
3840 gpodder.__copyright__,
3841 gpodder.__url__,
3842 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3843 'http://gpodder.org/donate')
3844 return
3846 dlg = gtk.AboutDialog()
3847 dlg.set_transient_for(self.main_window)
3848 dlg.set_name('gPodder')
3849 dlg.set_version(gpodder.__version__)
3850 dlg.set_copyright(gpodder.__copyright__)
3851 dlg.set_comments(_('A podcast client with focus on usability'))
3852 dlg.set_website(gpodder.__url__)
3853 dlg.set_translator_credits( _('translator-credits'))
3854 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3856 if gpodder.ui.desktop:
3857 # For the "GUI" version, we add some more
3858 # items to the about dialog (credits and logo)
3859 app_authors = [
3860 _('Maintainer:'),
3861 'Thomas Perl <thp.io>',
3864 if os.path.exists(gpodder.credits_file):
3865 credits = open(gpodder.credits_file).read().strip().split('\n')
3866 app_authors += ['', _('Patches, bug reports and donations by:')]
3867 app_authors += credits
3869 dlg.set_authors(app_authors)
3870 try:
3871 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3872 except:
3873 dlg.set_logo_icon_name('gpodder')
3875 dlg.run()
3877 def on_wNotebook_switch_page(self, widget, *args):
3878 page_num = args[1]
3879 if gpodder.ui.maemo:
3880 self.tool_downloads.set_active(page_num == 1)
3881 page = self.wNotebook.get_nth_page(page_num)
3882 tab_label = self.wNotebook.get_tab_label(page).get_text()
3883 if page_num == 0 and self.active_channel is not None:
3884 self.set_title(self.active_channel.title)
3885 else:
3886 self.set_title(tab_label)
3887 if page_num == 0:
3888 self.play_or_download()
3889 self.menuChannels.set_sensitive(True)
3890 self.menuSubscriptions.set_sensitive(True)
3891 # The message area in the downloads tab should be hidden
3892 # when the user switches away from the downloads tab
3893 if self.message_area is not None:
3894 self.message_area.hide()
3895 self.message_area = None
3896 else:
3897 self.menuChannels.set_sensitive(False)
3898 self.menuSubscriptions.set_sensitive(False)
3899 if gpodder.ui.desktop:
3900 self.toolDownload.set_sensitive(False)
3901 self.toolPlay.set_sensitive(False)
3902 self.toolTransfer.set_sensitive(False)
3903 self.toolCancel.set_sensitive(False)
3905 def on_treeChannels_row_activated(self, widget, path, *args):
3906 # double-click action of the podcast list or enter
3907 self.treeChannels.set_cursor(path)
3909 def on_treeChannels_cursor_changed(self, widget, *args):
3910 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3912 if model is not None and iter is not None:
3913 old_active_channel = self.active_channel
3914 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3916 if self.active_channel == old_active_channel:
3917 return
3919 if gpodder.ui.maemo:
3920 self.set_title(self.active_channel.title)
3922 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3923 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3924 self.itemEditChannel.set_visible(False)
3925 self.itemRemoveChannel.set_visible(False)
3926 else:
3927 self.itemEditChannel.set_visible(True)
3928 self.itemRemoveChannel.set_visible(True)
3929 else:
3930 self.active_channel = None
3931 self.itemEditChannel.set_visible(False)
3932 self.itemRemoveChannel.set_visible(False)
3934 self.update_episode_list_model()
3936 def on_btnEditChannel_clicked(self, widget, *args):
3937 self.on_itemEditChannel_activate( widget, args)
3939 def get_podcast_urls_from_selected_episodes(self):
3940 """Get a set of podcast URLs based on the selected episodes"""
3941 return set(episode.channel.url for episode in \
3942 self.get_selected_episodes())
3944 def get_selected_episodes(self):
3945 """Get a list of selected episodes from treeAvailable"""
3946 selection = self.treeAvailable.get_selection()
3947 model, paths = selection.get_selected_rows()
3949 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3950 return episodes
3952 def on_transfer_selected_episodes(self, widget):
3953 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3955 def on_playback_selected_episodes(self, widget):
3956 self.playback_episodes(self.get_selected_episodes())
3958 def on_shownotes_selected_episodes(self, widget):
3959 episodes = self.get_selected_episodes()
3960 if episodes:
3961 episode = episodes.pop(0)
3962 self.show_episode_shownotes(episode)
3963 else:
3964 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3966 def on_download_selected_episodes(self, widget):
3967 episodes = self.get_selected_episodes()
3968 self.download_episode_list(episodes)
3969 self.update_episode_list_icons([episode.url for episode in episodes])
3970 self.play_or_download()
3972 def on_treeAvailable_row_activated(self, widget, path, view_column):
3973 """Double-click/enter action handler for treeAvailable"""
3974 # We should only have one one selected as it was double clicked!
3975 e = self.get_selected_episodes()[0]
3977 if (self.config.double_click_episode_action == 'download'):
3978 # If the episode has already been downloaded and exists then play it
3979 if e.was_downloaded(and_exists=True):
3980 self.playback_episodes(self.get_selected_episodes())
3981 # else download it if it is not already downloading
3982 elif not self.episode_is_downloading(e):
3983 self.download_episode_list([e])
3984 self.update_episode_list_icons([e.url])
3985 self.play_or_download()
3986 elif (self.config.double_click_episode_action == 'stream'):
3987 # If we happen to have downloaded this episode simple play it
3988 if e.was_downloaded(and_exists=True):
3989 self.playback_episodes(self.get_selected_episodes())
3990 # else if streaming is possible stream it
3991 elif self.streaming_possible():
3992 self.playback_episodes(self.get_selected_episodes())
3993 else:
3994 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3995 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3996 else:
3997 # default action is to display show notes
3998 self.on_shownotes_selected_episodes(widget)
4000 def show_episode_shownotes(self, episode):
4001 if self.episode_shownotes_window is None:
4002 log('First-time use of episode window --- creating', sender=self)
4003 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
4004 _download_episode_list=self.download_episode_list, \
4005 _playback_episodes=self.playback_episodes, \
4006 _delete_episode_list=self.delete_episode_list, \
4007 _episode_list_status_changed=self.episode_list_status_changed, \
4008 _cancel_task_list=self.cancel_task_list, \
4009 _episode_is_downloading=self.episode_is_downloading, \
4010 _streaming_possible=self.streaming_possible())
4011 self.episode_shownotes_window.show(episode)
4012 if self.episode_is_downloading(episode):
4013 self.update_downloads_list()
4015 def restart_auto_update_timer(self):
4016 if self._auto_update_timer_source_id is not None:
4017 log('Removing existing auto update timer.', sender=self)
4018 gobject.source_remove(self._auto_update_timer_source_id)
4019 self._auto_update_timer_source_id = None
4021 if self.config.auto_update_feeds and \
4022 self.config.auto_update_frequency:
4023 interval = 60*1000*self.config.auto_update_frequency
4024 log('Setting up auto update timer with interval %d.', \
4025 self.config.auto_update_frequency, sender=self)
4026 self._auto_update_timer_source_id = gobject.timeout_add(\
4027 interval, self._on_auto_update_timer)
4029 def _on_auto_update_timer(self):
4030 log('Auto update timer fired.', sender=self)
4031 self.update_feed_cache(force_update=True)
4033 # Ask web service for sub changes (if enabled)
4034 self.mygpo_client.flush()
4036 return True
4038 def on_treeDownloads_row_activated(self, widget, *args):
4039 # Use the standard way of working on the treeview
4040 selection = self.treeDownloads.get_selection()
4041 (model, paths) = selection.get_selected_rows()
4042 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
4044 for tree_row_reference, task in selected_tasks:
4045 if task.status in (task.DOWNLOADING, task.QUEUED):
4046 task.status = task.PAUSED
4047 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
4048 self.download_queue_manager.add_task(task)
4049 self.enable_download_list_update()
4050 elif task.status == task.DONE:
4051 model.remove(model.get_iter(tree_row_reference.get_path()))
4053 self.play_or_download()
4055 # Update the tab title and downloads list
4056 self.update_downloads_list()
4058 def on_item_cancel_download_activate(self, widget):
4059 if self.wNotebook.get_current_page() == 0:
4060 selection = self.treeAvailable.get_selection()
4061 (model, paths) = selection.get_selected_rows()
4062 urls = [model.get_value(model.get_iter(path), \
4063 self.episode_list_model.C_URL) for path in paths]
4064 selected_tasks = [task for task in self.download_tasks_seen \
4065 if task.url in urls]
4066 else:
4067 selection = self.treeDownloads.get_selection()
4068 (model, paths) = selection.get_selected_rows()
4069 selected_tasks = [model.get_value(model.get_iter(path), \
4070 self.download_status_model.C_TASK) for path in paths]
4071 self.cancel_task_list(selected_tasks)
4073 def on_btnCancelAll_clicked(self, widget, *args):
4074 self.cancel_task_list(self.download_tasks_seen)
4076 def on_btnDownloadedDelete_clicked(self, widget, *args):
4077 episodes = self.get_selected_episodes()
4078 if len(episodes) == 1:
4079 self.delete_episode_list(episodes, skip_locked=False)
4080 else:
4081 self.delete_episode_list(episodes)
4083 def on_key_press(self, widget, event):
4084 # Allow tab switching with Ctrl + PgUp/PgDown
4085 if event.state & gtk.gdk.CONTROL_MASK:
4086 if event.keyval == gtk.keysyms.Page_Up:
4087 self.wNotebook.prev_page()
4088 return True
4089 elif event.keyval == gtk.keysyms.Page_Down:
4090 self.wNotebook.next_page()
4091 return True
4093 # After this code we only handle Maemo hardware keys,
4094 # so if we are not a Maemo app, we don't do anything
4095 if not gpodder.ui.maemo:
4096 return False
4098 diff = 0
4099 if event.keyval == gtk.keysyms.F7: #plus
4100 diff = 1
4101 elif event.keyval == gtk.keysyms.F8: #minus
4102 diff = -1
4104 if diff != 0 and not self.currently_updating:
4105 selection = self.treeChannels.get_selection()
4106 (model, iter) = selection.get_selected()
4107 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
4108 selection.select_path(new_path)
4109 self.treeChannels.set_cursor(new_path)
4110 return True
4112 return False
4114 def on_iconify(self):
4115 if self.tray_icon:
4116 self.gPodder.set_skip_taskbar_hint(True)
4117 if self.config.minimize_to_tray:
4118 self.tray_icon.set_visible(True)
4119 else:
4120 self.gPodder.set_skip_taskbar_hint(False)
4122 def on_uniconify(self):
4123 if self.tray_icon:
4124 self.gPodder.set_skip_taskbar_hint(False)
4125 if self.config.minimize_to_tray:
4126 self.tray_icon.set_visible(False)
4127 else:
4128 self.gPodder.set_skip_taskbar_hint(False)
4130 def uniconify_main_window(self):
4131 if self.is_iconified():
4132 # We need to hide and then show the window in WMs like Metacity
4133 # or KWin4 to move the window to the active workspace
4134 # (see http://gpodder.org/bug/1125)
4135 self.gPodder.hide()
4136 self.gPodder.show()
4137 self.gPodder.present()
4139 def iconify_main_window(self):
4140 if not self.is_iconified():
4141 self.gPodder.iconify()
4143 def update_podcasts_tab(self):
4144 if len(self.channels):
4145 if gpodder.ui.fremantle:
4146 self.button_refresh.set_title(_('Check for new episodes'))
4147 self.button_refresh.show()
4148 else:
4149 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
4150 else:
4151 if gpodder.ui.fremantle:
4152 self.button_refresh.hide()
4153 else:
4154 self.label2.set_text(_('Podcasts'))
4156 @dbus.service.method(gpodder.dbus_interface)
4157 def show_gui_window(self):
4158 parent = self.get_dialog_parent()
4159 parent.present()
4161 @dbus.service.method(gpodder.dbus_interface)
4162 def subscribe_to_url(self, url):
4163 gPodderAddPodcast(self.gPodder,
4164 add_urls_callback=self.add_podcast_list,
4165 preset_url=url)
4167 @dbus.service.method(gpodder.dbus_interface)
4168 def mark_episode_played(self, filename):
4169 if filename is None:
4170 return False
4172 for channel in self.channels:
4173 for episode in channel.get_all_episodes():
4174 fn = episode.local_filename(create=False, check_only=True)
4175 if fn == filename:
4176 episode.mark(is_played=True)
4177 self.db.commit()
4178 self.update_episode_list_icons([episode.url])
4179 self.update_podcast_list_model([episode.channel.url])
4180 return True
4182 return False
4185 def main(options=None):
4186 gobject.threads_init()
4187 gobject.set_application_name('gPodder')
4189 if gpodder.ui.maemo:
4190 # Try to enable the custom icon theme for gPodder on Maemo
4191 settings = gtk.settings_get_default()
4192 settings.set_string_property('gtk-icon-theme-name', \
4193 'gpodder', __file__)
4194 # Extend the search path for the optified icon theme (Maemo 5)
4195 icon_theme = gtk.icon_theme_get_default()
4196 icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
4198 gtk.window_set_default_icon_name('gpodder')
4199 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
4201 try:
4202 dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
4203 gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
4205 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
4206 except dbus.exceptions.DBusException, dbe:
4207 log('Warning: Cannot get "on the bus".', traceback=True)
4208 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
4209 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
4210 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
4211 dlg.set_title('gPodder')
4212 dlg.run()
4213 dlg.destroy()
4214 sys.exit(0)
4216 util.make_directory(gpodder.home)
4217 gpodder.load_plugins()
4219 config = UIConfig(gpodder.config_file)
4221 # Load hook modules and install the hook manager globally
4222 # if modules have been found an instantiated by the manager
4223 user_hooks = hooks.HookManager()
4224 if user_hooks.has_modules():
4225 gpodder.user_hooks = user_hooks
4227 if gpodder.ui.diablo:
4228 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4229 # folder exists there (allow moving "gpodder" between SD cards or USB)
4230 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4231 if not os.path.exists(config.download_dir):
4232 log('Downloads might have been moved. Trying to locate them...')
4233 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
4234 dir = os.path.join(basedir, 'gpodder')
4235 if os.path.exists(dir):
4236 log('Downloads found in: %s', dir)
4237 config.download_dir = dir
4238 break
4239 else:
4240 log('Downloads NOT FOUND in %s', dir)
4242 if config.enable_fingerscroll:
4243 BuilderWidget.use_fingerscroll = True
4245 config.mygpo_device_type = util.detect_device_type()
4247 gp = gPodder(bus_name, config)
4249 # Handle options
4250 if options.subscribe:
4251 util.idle_add(gp.subscribe_to_url, options.subscribe)
4253 # mac OS X stuff :
4254 # handle "subscribe to podcast" events from firefox
4255 if platform.system() == 'Darwin':
4256 from gpodder import gpodderosx
4257 gpodderosx.register_handlers(gp)
4258 # end mac OS X stuff
4260 gp.run()