Fix pre-selection in "Delete episodes" (bug 1210)
[gpodder.git] / src / gpodder / gui.py
blob3bc66b89ca4155554296562f740b09f5dea38284
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 action_area_box.set_spacing(2)
365 action_area_box.set_border_width(3)
366 self.treeChannels.set_action_area_visible(True)
368 # Set up a very nice progress bar setup
369 self.fancy_progress_bar = FancyProgressBar(self.main_window, \
370 self.on_btnCancelFeedUpdate_clicked)
371 self.pbFeedUpdate = self.fancy_progress_bar.progress_bar
372 self.pbFeedUpdate.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
373 self.vbox.pack_start(self.fancy_progress_bar.event_box, False)
375 from gpodder.gtkui.frmntl import style
376 sub_font = style.get_font_desc('SmallSystemFont')
377 sub_color = style.get_color('SecondaryTextColor')
378 sub = (sub_font.to_string(), sub_color.to_string())
379 sub = '<span font_desc="%s" foreground="%s">%%s</span>' % sub
380 self.label_footer.set_markup(sub % gpodder.__copyright__)
382 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
383 while gtk.events_pending():
384 gtk.main_iteration(False)
386 try:
387 # Try to get the real package version from dpkg
388 p = subprocess.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout=subprocess.PIPE)
389 version, _stderr = p.communicate()
390 del _stderr
391 del p
392 except:
393 version = gpodder.__version__
394 self.label_footer.set_markup(sub % ('v %s' % version))
395 self.label_footer.hide()
397 self.episodes_window = gPodderEpisodes(self.main_window, \
398 on_treeview_expose_event=self.on_treeview_expose_event, \
399 show_episode_shownotes=self.show_episode_shownotes, \
400 update_podcast_list_model=self.update_podcast_list_model, \
401 on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
402 item_view_episodes_all=self.item_view_episodes_all, \
403 item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
404 item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
405 item_view_episodes_undeleted=self.item_view_episodes_undeleted, \
406 on_entry_search_episodes_changed=self.on_entry_search_episodes_changed, \
407 on_entry_search_episodes_key_press=self.on_entry_search_episodes_key_press, \
408 hide_episode_search=self.hide_episode_search, \
409 on_itemUpdateChannel_activate=self.on_itemUpdateChannel_activate, \
410 playback_episodes=self.playback_episodes, \
411 delete_episode_list=self.delete_episode_list, \
412 episode_list_status_changed=self.episode_list_status_changed, \
413 download_episode_list=self.download_episode_list, \
414 episode_is_downloading=self.episode_is_downloading, \
415 show_episode_in_download_manager=self.show_episode_in_download_manager, \
416 add_download_task_monitor=self.add_download_task_monitor, \
417 remove_download_task_monitor=self.remove_download_task_monitor, \
418 for_each_episode_set_task_status=self.for_each_episode_set_task_status, \
419 on_itemUpdate_activate=self.on_itemUpdate_activate, \
420 show_delete_episodes_window=self.show_delete_episodes_window, \
421 cover_downloader=self.cover_downloader)
423 # Expose objects for episode list type-ahead find
424 self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
425 self.entry_search_episodes = self.episodes_window.entry_search_episodes
426 self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
428 self.downloads_window = gPodderDownloads(self.main_window, \
429 on_treeview_expose_event=self.on_treeview_expose_event, \
430 cleanup_downloads=self.cleanup_downloads, \
431 _for_each_task_set_status=self._for_each_task_set_status, \
432 downloads_list_get_selection=self.downloads_list_get_selection, \
433 _config=self.config)
435 self.treeAvailable = self.episodes_window.treeview
436 self.treeDownloads = self.downloads_window.treeview
438 # Source IDs for timeouts for search-as-you-type
439 self._podcast_list_search_timeout = None
440 self._episode_list_search_timeout = None
442 # Init the treeviews that we use
443 self.init_podcast_list_treeview()
444 self.init_episode_list_treeview()
445 self.init_download_list_treeview()
447 if self.config.podcast_list_hide_boring:
448 self.item_view_hide_boring_podcasts.set_active(True)
450 self.currently_updating = False
452 if gpodder.ui.maemo or self.config.enable_fingerscroll:
453 self.context_menu_mouse_button = 1
454 else:
455 self.context_menu_mouse_button = 3
457 if self.config.start_iconified:
458 self.iconify_main_window()
460 self.download_tasks_seen = set()
461 self.download_list_update_enabled = False
462 self.download_task_monitors = set()
464 # Subscribed channels
465 self.active_channel = None
466 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
467 self.channel_list_changed = True
468 self.update_podcasts_tab()
470 # load list of user applications for audio playback
471 self.user_apps_reader = UserAppsReader(['audio', 'video'])
472 threading.Thread(target=self.user_apps_reader.read).start()
474 # Set the "Device" menu item for the first time
475 if gpodder.ui.desktop:
476 self.update_item_device()
478 # Set up the first instance of MygPoClient
479 self.mygpo_client = my.MygPoClient(self.config)
481 # Now, update the feed cache, when everything's in place
482 if not gpodder.ui.fremantle:
483 self.btnUpdateFeeds.show()
484 self.updating_feed_cache = False
485 self.feed_cache_update_cancelled = False
486 self.update_feed_cache(force_update=self.config.update_on_startup)
488 self.message_area = None
490 def find_partial_downloads():
491 # Look for partial file downloads
492 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
493 count = len(partial_files)
494 resumable_episodes = []
495 if count:
496 if not gpodder.ui.fremantle:
497 util.idle_add(self.wNotebook.set_current_page, 1)
498 indicator = ProgressIndicator(_('Loading incomplete downloads'), \
499 _('Some episodes have not finished downloading in a previous session.'), \
500 False, self.get_dialog_parent())
501 indicator.on_message(N_('%(count)d partial file', '%(count)d partial files', count) % {'count':count})
503 candidates = [f[:-len('.partial')] for f in partial_files]
504 found = 0
506 for c in self.channels:
507 for e in c.get_all_episodes():
508 filename = e.local_filename(create=False, check_only=True)
509 if filename in candidates:
510 log('Found episode: %s', e.title, sender=self)
511 found += 1
512 indicator.on_message(e.title)
513 indicator.on_progress(float(found)/count)
514 candidates.remove(filename)
515 partial_files.remove(filename+'.partial')
516 resumable_episodes.append(e)
518 if not candidates:
519 break
521 if not candidates:
522 break
524 for f in partial_files:
525 log('Partial file without episode: %s', f, sender=self)
526 util.delete_file(f)
528 util.idle_add(indicator.on_finished)
530 if len(resumable_episodes):
531 def offer_resuming():
532 self.download_episode_list_paused(resumable_episodes)
533 if not gpodder.ui.fremantle:
534 resume_all = gtk.Button(_('Resume all'))
535 #resume_all.set_border_width(0)
536 def on_resume_all(button):
537 selection = self.treeDownloads.get_selection()
538 selection.select_all()
539 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
540 selection.unselect_all()
541 self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
542 self.message_area.hide()
543 resume_all.connect('clicked', on_resume_all)
545 self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
546 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
547 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
548 self.message_area.show_all()
549 self.clean_up_downloads(delete_partial=False)
550 util.idle_add(offer_resuming)
551 elif not gpodder.ui.fremantle:
552 util.idle_add(self.wNotebook.set_current_page, 0)
553 else:
554 util.idle_add(self.clean_up_downloads, True)
555 threading.Thread(target=find_partial_downloads).start()
557 # Start the auto-update procedure
558 self._auto_update_timer_source_id = None
559 if self.config.auto_update_feeds:
560 self.restart_auto_update_timer()
562 # Delete old episodes if the user wishes to
563 if self.config.auto_remove_played_episodes and \
564 self.config.episode_old_age > 0:
565 old_episodes = list(self.get_expired_episodes())
566 if len(old_episodes) > 0:
567 self.delete_episode_list(old_episodes, confirm=False)
568 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
570 if gpodder.ui.fremantle:
571 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
572 self.button_refresh.set_sensitive(True)
573 self.button_subscribe.set_sensitive(True)
574 self.main_window.set_title(_('gPodder'))
575 hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
577 # Do the initial sync with the web service
578 util.idle_add(self.mygpo_client.flush, True)
580 # First-time users should be asked if they want to see the OPML
581 if not self.channels and not gpodder.ui.fremantle:
582 util.idle_add(self.on_itemUpdate_activate)
584 def episode_object_by_uri(self, uri):
585 """Get an episode object given a local or remote URI
587 This can be used to quickly access an episode object
588 when all we have is its download filename or episode
589 URL (e.g. from external D-Bus calls / signals, etc..)
591 if uri.startswith('/'):
592 uri = 'file://' + uri
594 prefix = 'file://' + self.config.download_dir
596 if uri.startswith(prefix):
597 # File is on the local filesystem in the download folder
598 filename = urllib.unquote(uri[len(prefix):])
599 file_parts = [x for x in filename.split(os.sep) if x]
601 if len(file_parts) == 2:
602 dir_name, filename = file_parts
603 channels = [c for c in self.channels if c.foldername == dir_name]
604 if len(channels) == 1:
605 channel = channels[0]
606 return channel.get_episode_by_filename(filename)
607 else:
608 # Possibly remote file - search the database for a podcast
609 channel_id = self.db.get_channel_id_from_episode_url(uri)
611 if channel_id is not None:
612 channels = [c for c in self.channels if c.id == channel_id]
613 if len(channels) == 1:
614 channel = channels[0]
615 return channel.get_episode_by_url(uri)
617 return None
619 def on_played(self, start, end, total, file_uri):
620 """Handle the "played" signal from a media player"""
621 if start == 0 and end == 0 and total == 0:
622 # Ignore bogus play event
623 return
624 elif end < start + 5:
625 # Ignore "less than five seconds" segments,
626 # as they can happen with seeking, etc...
627 return
629 log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
630 episode = self.episode_object_by_uri(file_uri)
632 if episode is not None:
633 file_type = episode.file_type()
634 # Automatically enable D-Bus played status mode
635 if file_type == 'audio':
636 self.config.audio_played_dbus = True
637 elif file_type == 'video':
638 self.config.video_played_dbus = True
640 now = time.time()
641 if total > 0:
642 episode.total_time = total
643 elif total == 0:
644 # Assume the episode's total time for the action
645 total = episode.total_time
646 if episode.current_position_updated is None or \
647 now > episode.current_position_updated:
648 episode.current_position = end
649 episode.current_position_updated = now
650 episode.mark(is_played=True)
651 episode.save()
652 self.db.commit()
653 self.update_episode_list_icons([episode.url])
654 self.update_podcast_list_model([episode.channel.url])
656 # Submit this action to the webservice
657 self.mygpo_client.on_playback_full(episode, \
658 start, end, total)
660 def on_add_remove_podcasts_mygpo(self):
661 actions = self.mygpo_client.get_received_actions()
662 if not actions:
663 return False
665 existing_urls = [c.url for c in self.channels]
667 # Columns for the episode selector window - just one...
668 columns = (
669 ('description', None, None, _('Action')),
672 # A list of actions that have to be chosen from
673 changes = []
675 # Actions that are ignored (already carried out)
676 ignored = []
678 for action in actions:
679 if action.is_add and action.url not in existing_urls:
680 changes.append(my.Change(action))
681 elif action.is_remove and action.url in existing_urls:
682 podcast_object = None
683 for podcast in self.channels:
684 if podcast.url == action.url:
685 podcast_object = podcast
686 break
687 changes.append(my.Change(action, podcast_object))
688 else:
689 log('Ignoring action: %s', action, sender=self)
690 ignored.append(action)
692 # Confirm all ignored changes
693 self.mygpo_client.confirm_received_actions(ignored)
695 def execute_podcast_actions(selected):
696 add_list = [c.action.url for c in selected if c.action.is_add]
697 remove_list = [c.podcast for c in selected if c.action.is_remove]
699 # Apply the accepted changes locally
700 self.add_podcast_list(add_list)
701 self.remove_podcast_list(remove_list, confirm=False)
703 # All selected items are now confirmed
704 self.mygpo_client.confirm_received_actions(c.action for c in selected)
706 # Revert the changes on the server
707 rejected = [c.action for c in changes if c not in selected]
708 self.mygpo_client.reject_received_actions(rejected)
710 def ask():
711 # We're abusing the Episode Selector again ;) -- thp
712 gPodderEpisodeSelector(self.main_window, \
713 title=_('Confirm changes from gpodder.net'), \
714 instructions=_('Select the actions you want to carry out.'), \
715 episodes=changes, \
716 columns=columns, \
717 size_attribute=None, \
718 stock_ok_button=gtk.STOCK_APPLY, \
719 callback=execute_podcast_actions, \
720 _config=self.config)
722 # There are some actions that need the user's attention
723 if changes:
724 util.idle_add(ask)
725 return True
727 # We have no remaining actions - no selection happens
728 return False
730 def rewrite_urls_mygpo(self):
731 # Check if we have to rewrite URLs since the last add
732 rewritten_urls = self.mygpo_client.get_rewritten_urls()
734 for rewritten_url in rewritten_urls:
735 if not rewritten_url.new_url:
736 continue
738 for channel in self.channels:
739 if channel.url == rewritten_url.old_url:
740 log('Updating URL of %s to %s', channel, \
741 rewritten_url.new_url, sender=self)
742 channel.url = rewritten_url.new_url
743 channel.save()
744 self.channel_list_changed = True
745 util.idle_add(self.update_episode_list_model)
746 break
748 def on_send_full_subscriptions(self):
749 # Send the full subscription list to the gpodder.net client
750 # (this will overwrite the subscription list on the server)
751 indicator = ProgressIndicator(_('Uploading subscriptions'), \
752 _('Your subscriptions are being uploaded to the server.'), \
753 False, self.get_dialog_parent())
755 try:
756 self.mygpo_client.set_subscriptions([c.url for c in self.channels])
757 util.idle_add(self.show_message, _('List uploaded successfully.'))
758 except Exception, e:
759 def show_error(e):
760 message = str(e)
761 if not message:
762 message = e.__class__.__name__
763 self.show_message(message, \
764 _('Error while uploading'), \
765 important=True)
766 util.idle_add(show_error, e)
768 util.idle_add(indicator.on_finished)
770 def on_podcast_selected(self, treeview, path, column):
771 # for Maemo 5's UI
772 model = treeview.get_model()
773 channel = model.get_value(model.get_iter(path), \
774 PodcastListModel.C_CHANNEL)
775 self.active_channel = channel
776 self.update_episode_list_model()
777 self.episodes_window.channel = self.active_channel
778 self.episodes_window.show()
780 def on_button_subscribe_clicked(self, button):
781 self.on_itemImportChannels_activate(button)
783 def on_button_downloads_clicked(self, widget):
784 self.downloads_window.show()
786 def show_episode_in_download_manager(self, episode):
787 self.downloads_window.show()
788 model = self.treeDownloads.get_model()
789 selection = self.treeDownloads.get_selection()
790 selection.unselect_all()
791 it = model.get_iter_first()
792 while it is not None:
793 task = model.get_value(it, DownloadStatusModel.C_TASK)
794 if task.episode.url == episode.url:
795 selection.select_iter(it)
796 # FIXME: Scroll to selection in pannable area
797 break
798 it = model.iter_next(it)
800 def for_each_episode_set_task_status(self, episodes, status):
801 episode_urls = set(episode.url for episode in episodes)
802 model = self.treeDownloads.get_model()
803 selected_tasks = [(gtk.TreeRowReference(model, row.path), \
804 model.get_value(row.iter, \
805 DownloadStatusModel.C_TASK)) for row in model \
806 if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
807 in episode_urls]
808 self._for_each_task_set_status(selected_tasks, status)
810 def on_window_orientation_changed(self, orientation):
811 self._last_orientation = orientation
812 if self.preferences_dialog is not None:
813 self.preferences_dialog.on_window_orientation_changed(orientation)
815 treeview = self.treeChannels
816 if orientation == Orientation.PORTRAIT:
817 treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
818 # Work around Maemo bug #4718
819 self.button_subscribe.set_name('HildonButton-thumb')
820 self.button_refresh.set_name('HildonButton-thumb')
821 else:
822 treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
823 # Work around Maemo bug #4718
824 self.button_subscribe.set_name('HildonButton-finger')
825 self.button_refresh.set_name('HildonButton-finger')
827 if gpodder.ui.fremantle:
828 self.fancy_progress_bar.relayout()
830 def on_treeview_podcasts_selection_changed(self, selection):
831 model, iter = selection.get_selected()
832 if iter is None:
833 self.active_channel = None
834 self.episode_list_model.clear()
836 def on_treeview_button_pressed(self, treeview, event):
837 if event.window != treeview.get_bin_window():
838 return False
840 TreeViewHelper.save_button_press_event(treeview, event)
842 if getattr(treeview, TreeViewHelper.ROLE) == \
843 TreeViewHelper.ROLE_PODCASTS:
844 return self.currently_updating
846 return event.button == self.context_menu_mouse_button and \
847 gpodder.ui.desktop
849 def on_treeview_podcasts_button_released(self, treeview, event):
850 if event.window != treeview.get_bin_window():
851 return False
853 if gpodder.ui.maemo:
854 return self.treeview_channels_handle_gestures(treeview, event)
855 return self.treeview_channels_show_context_menu(treeview, event)
857 def on_treeview_episodes_button_released(self, treeview, event):
858 if event.window != treeview.get_bin_window():
859 return False
861 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
862 return self.treeview_available_handle_gestures(treeview, event)
864 return self.treeview_available_show_context_menu(treeview, event)
866 def on_treeview_downloads_button_released(self, treeview, event):
867 if event.window != treeview.get_bin_window():
868 return False
870 return self.treeview_downloads_show_context_menu(treeview, event)
872 def on_entry_search_podcasts_changed(self, editable):
873 if self.hbox_search_podcasts.get_property('visible'):
874 def set_search_term(self, text):
875 self.podcast_list_model.set_search_term(text)
876 self._podcast_list_search_timeout = None
877 return False
879 if self._podcast_list_search_timeout is not None:
880 gobject.source_remove(self._podcast_list_search_timeout)
881 self._podcast_list_search_timeout = gobject.timeout_add(\
882 self.LIVE_SEARCH_DELAY, \
883 set_search_term, self, editable.get_chars(0, -1))
885 def on_entry_search_podcasts_key_press(self, editable, event):
886 if event.keyval == gtk.keysyms.Escape:
887 self.hide_podcast_search()
888 return True
890 def hide_podcast_search(self, *args):
891 if self._podcast_list_search_timeout is not None:
892 gobject.source_remove(self._podcast_list_search_timeout)
893 self._podcast_list_search_timeout = None
894 self.hbox_search_podcasts.hide()
895 self.entry_search_podcasts.set_text('')
896 self.podcast_list_model.set_search_term(None)
897 self.treeChannels.grab_focus()
899 def show_podcast_search(self, input_char):
900 self.hbox_search_podcasts.show()
901 self.entry_search_podcasts.insert_text(input_char, -1)
902 self.entry_search_podcasts.grab_focus()
903 self.entry_search_podcasts.set_position(-1)
905 def init_podcast_list_treeview(self):
906 # Set up podcast channel tree view widget
907 if gpodder.ui.fremantle:
908 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
909 self.item_view_podcasts_downloaded.set_active(True)
910 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
911 self.item_view_podcasts_unplayed.set_active(True)
912 else:
913 self.item_view_podcasts_all.set_active(True)
914 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
916 iconcolumn = gtk.TreeViewColumn('')
917 iconcell = gtk.CellRendererPixbuf()
918 iconcolumn.pack_start(iconcell, False)
919 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
920 self.treeChannels.append_column(iconcolumn)
922 namecolumn = gtk.TreeViewColumn('')
923 namecell = gtk.CellRendererText()
924 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
925 namecolumn.pack_start(namecell, True)
926 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
928 if gpodder.ui.fremantle:
929 countcell = gtk.CellRendererText()
930 from gpodder.gtkui.frmntl import style
931 countcell.set_property('font-desc', style.get_font_desc('EmpSystemFont'))
932 countcell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
933 countcell.set_property('alignment', pango.ALIGN_RIGHT)
934 countcell.set_property('xalign', 1.)
935 countcell.set_property('xpad', 5)
936 namecolumn.pack_start(countcell, False)
937 namecolumn.add_attribute(countcell, 'text', PodcastListModel.C_DOWNLOADS)
938 namecolumn.add_attribute(countcell, 'visible', PodcastListModel.C_DOWNLOADS)
939 else:
940 iconcell = gtk.CellRendererPixbuf()
941 iconcell.set_property('xalign', 1.0)
942 namecolumn.pack_start(iconcell, False)
943 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
944 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
946 self.treeChannels.append_column(namecolumn)
948 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
950 # When no podcast is selected, clear the episode list model
951 selection = self.treeChannels.get_selection()
952 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
954 # Set up type-ahead find for the podcast list
955 def on_key_press(treeview, event):
956 if event.keyval == gtk.keysyms.Escape:
957 self.hide_podcast_search()
958 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
959 self.hide_podcast_search()
960 elif event.state & gtk.gdk.CONTROL_MASK:
961 # Don't handle type-ahead when control is pressed (so shortcuts
962 # with the Ctrl key still work, e.g. Ctrl+A, ...)
963 return True
964 else:
965 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
966 if unicode_char_id == 0:
967 return False
968 input_char = unichr(unicode_char_id)
969 self.show_podcast_search(input_char)
970 return True
971 self.treeChannels.connect('key-press-event', on_key_press)
973 # Enable separators to the podcast list to separate special podcasts
974 # from others (this is used for the "all episodes" view)
975 self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
977 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
979 def on_entry_search_episodes_changed(self, editable):
980 if self.hbox_search_episodes.get_property('visible'):
981 def set_search_term(self, text):
982 self.episode_list_model.set_search_term(text)
983 self._episode_list_search_timeout = None
984 return False
986 if self._episode_list_search_timeout is not None:
987 gobject.source_remove(self._episode_list_search_timeout)
988 self._episode_list_search_timeout = gobject.timeout_add(\
989 self.LIVE_SEARCH_DELAY, \
990 set_search_term, self, editable.get_chars(0, -1))
992 def on_entry_search_episodes_key_press(self, editable, event):
993 if event.keyval == gtk.keysyms.Escape:
994 self.hide_episode_search()
995 return True
997 def hide_episode_search(self, *args):
998 if self._episode_list_search_timeout is not None:
999 gobject.source_remove(self._episode_list_search_timeout)
1000 self._episode_list_search_timeout = None
1001 self.hbox_search_episodes.hide()
1002 self.entry_search_episodes.set_text('')
1003 self.episode_list_model.set_search_term(None)
1004 self.treeAvailable.grab_focus()
1006 def show_episode_search(self, input_char):
1007 self.hbox_search_episodes.show()
1008 self.entry_search_episodes.insert_text(input_char, -1)
1009 self.entry_search_episodes.grab_focus()
1010 self.entry_search_episodes.set_position(-1)
1012 def init_episode_list_treeview(self):
1013 # For loading the list model
1014 self.episode_list_model = EpisodeListModel(self.on_episode_list_filter_changed)
1016 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
1017 self.item_view_episodes_undeleted.set_active(True)
1018 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
1019 self.item_view_episodes_downloaded.set_active(True)
1020 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
1021 self.item_view_episodes_unplayed.set_active(True)
1022 else:
1023 self.item_view_episodes_all.set_active(True)
1025 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
1027 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
1029 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
1031 iconcell = gtk.CellRendererPixbuf()
1032 iconcell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1033 if gpodder.ui.maemo:
1034 iconcell.set_fixed_size(50, 50)
1035 else:
1036 iconcell.set_fixed_size(40, -1)
1038 namecell = gtk.CellRendererText()
1039 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
1040 namecolumn = gtk.TreeViewColumn(_('Episode'))
1041 namecolumn.pack_start(iconcell, False)
1042 namecolumn.add_attribute(iconcell, 'icon-name', EpisodeListModel.C_STATUS_ICON)
1043 namecolumn.pack_start(namecell, True)
1044 namecolumn.add_attribute(namecell, 'markup', EpisodeListModel.C_DESCRIPTION)
1045 if gpodder.ui.fremantle:
1046 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1047 else:
1048 namecolumn.set_sort_column_id(EpisodeListModel.C_DESCRIPTION)
1049 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1050 namecolumn.set_resizable(True)
1051 namecolumn.set_expand(True)
1053 if gpodder.ui.fremantle:
1054 from gpodder.gtkui.frmntl import style
1055 timecell = gtk.CellRendererText()
1056 timecell.set_property('font-desc', style.get_font_desc('SmallSystemFont'))
1057 timecell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
1058 timecell.set_property('alignment', pango.ALIGN_RIGHT)
1059 timecell.set_property('xalign', 1.)
1060 timecell.set_property('xpad', 5)
1061 timecell.set_property('yalign', .85)
1062 namecolumn.pack_start(timecell, False)
1063 namecolumn.add_attribute(timecell, 'text', EpisodeListModel.C_TIME)
1064 namecolumn.add_attribute(timecell, 'visible', EpisodeListModel.C_TIME_VISIBLE)
1066 lockcell = gtk.CellRendererPixbuf()
1067 lockcell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1068 if gpodder.ui.fremantle:
1069 lockcell.set_property('icon-name', 'general_locked')
1070 else:
1071 lockcell.set_property('icon-name', 'emblem-readonly')
1073 namecolumn.pack_start(lockcell, False)
1074 namecolumn.add_attribute(lockcell, 'visible', EpisodeListModel.C_LOCKED)
1076 sizecell = gtk.CellRendererText()
1077 sizecell.set_property('xalign', 1)
1078 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
1079 sizecolumn.set_sort_column_id(EpisodeListModel.C_FILESIZE)
1081 releasecell = gtk.CellRendererText()
1082 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
1083 releasecolumn.set_sort_column_id(EpisodeListModel.C_PUBLISHED)
1085 namecolumn.set_reorderable(True)
1086 self.treeAvailable.append_column(namecolumn)
1088 if not gpodder.ui.maemo:
1089 for itemcolumn in (sizecolumn, releasecolumn):
1090 itemcolumn.set_reorderable(True)
1091 self.treeAvailable.append_column(itemcolumn)
1093 # Set up type-ahead find for the episode list
1094 def on_key_press(treeview, event):
1095 if event.keyval == gtk.keysyms.Escape:
1096 self.hide_episode_search()
1097 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
1098 self.hide_episode_search()
1099 elif event.state & gtk.gdk.CONTROL_MASK:
1100 # Don't handle type-ahead when control is pressed (so shortcuts
1101 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1102 return False
1103 else:
1104 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
1105 if unicode_char_id == 0:
1106 return False
1107 input_char = unichr(unicode_char_id)
1108 self.show_episode_search(input_char)
1109 return True
1110 self.treeAvailable.connect('key-press-event', on_key_press)
1112 if gpodder.ui.desktop and not self.config.enable_fingerscroll:
1113 self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
1114 (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
1115 def drag_data_get(tree, context, selection_data, info, timestamp):
1116 if self.config.on_drag_mark_played:
1117 for episode in self.get_selected_episodes():
1118 episode.mark(is_played=True)
1119 self.on_selected_episodes_status_changed()
1120 uris = ['file://'+e.local_filename(create=False) \
1121 for e in self.get_selected_episodes() \
1122 if e.was_downloaded(and_exists=True)]
1123 uris.append('') # for the trailing '\r\n'
1124 selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
1125 self.treeAvailable.connect('drag-data-get', drag_data_get)
1127 selection = self.treeAvailable.get_selection()
1128 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
1129 selection.set_mode(gtk.SELECTION_SINGLE)
1130 elif gpodder.ui.fremantle:
1131 selection.set_mode(gtk.SELECTION_SINGLE)
1132 else:
1133 selection.set_mode(gtk.SELECTION_MULTIPLE)
1134 # Update the sensitivity of the toolbar buttons on the Desktop
1135 selection.connect('changed', lambda s: self.play_or_download())
1137 if gpodder.ui.diablo:
1138 # Set up the tap-and-hold context menu for podcasts
1139 menu = gtk.Menu()
1140 menu.append(self.itemUpdateChannel.create_menu_item())
1141 menu.append(self.itemEditChannel.create_menu_item())
1142 menu.append(gtk.SeparatorMenuItem())
1143 menu.append(self.itemRemoveChannel.create_menu_item())
1144 menu.append(gtk.SeparatorMenuItem())
1145 item = gtk.ImageMenuItem(_('Close this menu'))
1146 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
1147 gtk.ICON_SIZE_MENU))
1148 menu.append(item)
1149 menu.show_all()
1150 menu = self.set_finger_friendly(menu)
1151 self.treeChannels.tap_and_hold_setup(menu)
1154 def init_download_list_treeview(self):
1155 # enable multiple selection support
1156 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1157 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1159 # columns and renderers for "download progress" tab
1160 # First column: [ICON] Episodename
1161 column = gtk.TreeViewColumn(_('Episode'))
1163 cell = gtk.CellRendererPixbuf()
1164 if gpodder.ui.maemo:
1165 cell.set_fixed_size(50, 50)
1166 cell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1167 column.pack_start(cell, expand=False)
1168 column.add_attribute(cell, 'icon-name', \
1169 DownloadStatusModel.C_ICON_NAME)
1171 cell = gtk.CellRendererText()
1172 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1173 column.pack_start(cell, expand=True)
1174 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1175 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1176 column.set_expand(True)
1177 self.treeDownloads.append_column(column)
1179 # Second column: Progress
1180 cell = gtk.CellRendererProgress()
1181 cell.set_property('yalign', .5)
1182 cell.set_property('ypad', 6)
1183 column = gtk.TreeViewColumn(_('Progress'), cell,
1184 value=DownloadStatusModel.C_PROGRESS, \
1185 text=DownloadStatusModel.C_PROGRESS_TEXT)
1186 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1187 column.set_expand(False)
1188 self.treeDownloads.append_column(column)
1189 if gpodder.ui.maemo:
1190 column.set_property('min-width', 200)
1191 column.set_property('max-width', 200)
1192 else:
1193 column.set_property('min-width', 150)
1194 column.set_property('max-width', 150)
1196 self.treeDownloads.set_model(self.download_status_model)
1197 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1199 def on_treeview_expose_event(self, treeview, event):
1200 if event.window == treeview.get_bin_window():
1201 model = treeview.get_model()
1202 if (model is not None and model.get_iter_first() is not None):
1203 return False
1205 role = getattr(treeview, TreeViewHelper.ROLE, None)
1206 if role is None:
1207 return False
1209 ctx = event.window.cairo_create()
1210 ctx.rectangle(event.area.x, event.area.y,
1211 event.area.width, event.area.height)
1212 ctx.clip()
1214 x, y, width, height, depth = event.window.get_geometry()
1215 progress = None
1217 if role == TreeViewHelper.ROLE_EPISODES:
1218 if self.currently_updating:
1219 text = _('Loading episodes')
1220 elif self.config.episode_list_view_mode != \
1221 EpisodeListModel.VIEW_ALL:
1222 text = _('No episodes in current view')
1223 else:
1224 text = _('No episodes available')
1225 elif role == TreeViewHelper.ROLE_PODCASTS:
1226 if self.config.episode_list_view_mode != \
1227 EpisodeListModel.VIEW_ALL and \
1228 self.config.podcast_list_hide_boring and \
1229 len(self.channels) > 0:
1230 text = _('No podcasts in this view')
1231 else:
1232 text = _('No subscriptions')
1233 elif role == TreeViewHelper.ROLE_DOWNLOADS:
1234 text = _('No active downloads')
1235 else:
1236 raise Exception('on_treeview_expose_event: unknown role')
1238 if gpodder.ui.fremantle:
1239 from gpodder.gtkui.frmntl import style
1240 font_desc = style.get_font_desc('LargeSystemFont')
1241 else:
1242 font_desc = None
1244 draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1246 return False
1248 def enable_download_list_update(self):
1249 if not self.download_list_update_enabled:
1250 self.update_downloads_list()
1251 gobject.timeout_add(1500, self.update_downloads_list)
1252 self.download_list_update_enabled = True
1254 def cleanup_downloads(self):
1255 model = self.download_status_model
1257 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
1258 changed_episode_urls = set()
1259 for row_reference, task in all_tasks:
1260 if task.status in (task.DONE, task.CANCELLED):
1261 model.remove(model.get_iter(row_reference.get_path()))
1262 try:
1263 # We don't "see" this task anymore - remove it;
1264 # this is needed, so update_episode_list_icons()
1265 # below gets the correct list of "seen" tasks
1266 self.download_tasks_seen.remove(task)
1267 except KeyError, key_error:
1268 log('Cannot remove task from "seen" list: %s', task, sender=self)
1269 changed_episode_urls.add(task.url)
1270 # Tell the task that it has been removed (so it can clean up)
1271 task.removed_from_list()
1273 # Tell the podcasts tab to update icons for our removed podcasts
1274 self.update_episode_list_icons(changed_episode_urls)
1276 # Tell the shownotes window that we have removed the episode
1277 if self.episode_shownotes_window is not None and \
1278 self.episode_shownotes_window.episode is not None and \
1279 self.episode_shownotes_window.episode.url in changed_episode_urls:
1280 self.episode_shownotes_window._download_status_changed(None)
1282 # Update the downloads list one more time
1283 self.update_downloads_list(can_call_cleanup=False)
1285 def on_tool_downloads_toggled(self, toolbutton):
1286 if toolbutton.get_active():
1287 self.wNotebook.set_current_page(1)
1288 else:
1289 self.wNotebook.set_current_page(0)
1291 def add_download_task_monitor(self, monitor):
1292 self.download_task_monitors.add(monitor)
1293 model = self.download_status_model
1294 if model is None:
1295 model = ()
1296 for row in model:
1297 task = row[self.download_status_model.C_TASK]
1298 monitor.task_updated(task)
1300 def remove_download_task_monitor(self, monitor):
1301 self.download_task_monitors.remove(monitor)
1303 def update_downloads_list(self, can_call_cleanup=True):
1304 try:
1305 model = self.download_status_model
1307 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
1308 total_speed, total_size, done_size = 0, 0, 0
1310 # Keep a list of all download tasks that we've seen
1311 download_tasks_seen = set()
1313 # Remember the DownloadTask object for the episode that
1314 # has been opened in the episode shownotes dialog (if any)
1315 if self.episode_shownotes_window is not None:
1316 shownotes_episode = self.episode_shownotes_window.episode
1317 shownotes_task = None
1318 else:
1319 shownotes_episode = None
1320 shownotes_task = None
1322 # Do not go through the list of the model is not (yet) available
1323 if model is None:
1324 model = ()
1326 failed_downloads = []
1327 for row in model:
1328 self.download_status_model.request_update(row.iter)
1330 task = row[self.download_status_model.C_TASK]
1331 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
1333 # Let the download task monitors know of changes
1334 for monitor in self.download_task_monitors:
1335 monitor.task_updated(task)
1337 total_size += size
1338 done_size += size*progress
1340 if shownotes_episode is not None and \
1341 shownotes_episode.url == task.episode.url:
1342 shownotes_task = task
1344 download_tasks_seen.add(task)
1346 if status == download.DownloadTask.DOWNLOADING:
1347 downloading += 1
1348 total_speed += speed
1349 elif status == download.DownloadTask.FAILED:
1350 failed_downloads.append(task)
1351 failed += 1
1352 elif status == download.DownloadTask.DONE:
1353 finished += 1
1354 elif status == download.DownloadTask.QUEUED:
1355 queued += 1
1356 elif status == download.DownloadTask.PAUSED:
1357 paused += 1
1358 else:
1359 others += 1
1361 # Remember which tasks we have seen after this run
1362 self.download_tasks_seen = download_tasks_seen
1364 if gpodder.ui.desktop:
1365 text = [_('Downloads')]
1366 if downloading + failed + queued > 0:
1367 s = []
1368 if downloading > 0:
1369 s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count':downloading})
1370 if failed > 0:
1371 s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1372 if queued > 0:
1373 s.append(N_('%(count)d queued', '%(count)d queued', queued) % {'count':queued})
1374 text.append(' (' + ', '.join(s)+')')
1375 self.labelDownloads.set_text(''.join(text))
1376 elif gpodder.ui.diablo:
1377 sum = downloading + failed + finished + queued + paused + others
1378 if sum:
1379 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
1380 else:
1381 self.tool_downloads.set_label(_('Downloads'))
1382 elif gpodder.ui.fremantle:
1383 if downloading + queued > 0:
1384 self.button_downloads.set_value(N_('%(count)d active', '%(count)d active', downloading+queued) % {'count':(downloading+queued)})
1385 elif failed > 0:
1386 self.button_downloads.set_value(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1387 elif paused > 0:
1388 self.button_downloads.set_value(N_('%(count)d paused', '%(count)d paused', paused) % {'count':paused})
1389 else:
1390 self.button_downloads.set_value(_('Idle'))
1392 title = [self.default_title]
1394 # We have to update all episodes/channels for which the status has
1395 # changed. Accessing task.status_changed has the side effect of
1396 # re-setting the changed flag, so we need to get the "changed" list
1397 # of tuples first and split it into two lists afterwards
1398 changed = [(task.url, task.podcast_url) for task in \
1399 self.download_tasks_seen if task.status_changed]
1400 episode_urls = [episode_url for episode_url, channel_url in changed]
1401 channel_urls = [channel_url for episode_url, channel_url in changed]
1403 count = downloading + queued
1404 if count > 0:
1405 title.append(N_('downloading %(count)d file', 'downloading %(count)d files', count) % {'count':count})
1407 if total_size > 0:
1408 percentage = 100.0*done_size/total_size
1409 else:
1410 percentage = 0.0
1411 total_speed = util.format_filesize(total_speed)
1412 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
1413 if self.tray_icon is not None:
1414 # Update the tray icon status and progress bar
1415 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
1416 self.tray_icon.draw_progress_bar(percentage/100.)
1417 else:
1418 if self.tray_icon is not None:
1419 # Update the tray icon status
1420 self.tray_icon.set_status()
1421 if gpodder.ui.desktop:
1422 self.downloads_finished(self.download_tasks_seen)
1423 if gpodder.ui.diablo:
1424 hildon.hildon_banner_show_information(self.gPodder, '', 'gPodder: %s' % _('All downloads finished'))
1425 log('All downloads have finished.', sender=self)
1426 if self.config.cmd_all_downloads_complete:
1427 util.run_external_command(self.config.cmd_all_downloads_complete)
1429 if gpodder.ui.fremantle and failed:
1430 message = '\n'.join(['%s: %s' % (str(task), \
1431 task.error_message) for task in failed_downloads])
1432 self.show_message(message, _('Downloads failed'), important=True)
1434 # Remove finished episodes
1435 if self.config.auto_cleanup_downloads and can_call_cleanup:
1436 self.cleanup_downloads()
1438 # Stop updating the download list here
1439 self.download_list_update_enabled = False
1441 if not gpodder.ui.fremantle:
1442 self.gPodder.set_title(' - '.join(title))
1444 self.update_episode_list_icons(episode_urls)
1445 if self.episode_shownotes_window is not None:
1446 if (shownotes_task and shownotes_task.url in episode_urls) or \
1447 shownotes_task != self.episode_shownotes_window.task:
1448 self.episode_shownotes_window._download_status_changed(shownotes_task)
1449 self.episode_shownotes_window._download_status_progress()
1450 self.play_or_download()
1451 if channel_urls:
1452 self.update_podcast_list_model(channel_urls)
1454 return self.download_list_update_enabled
1455 except Exception, e:
1456 log('Exception happened while updating download list.', sender=self, traceback=True)
1457 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1458 # We return False here, so the update loop won't be called again,
1459 # that's why we require the restart of gPodder in the message.
1460 return False
1462 def on_config_changed(self, *args):
1463 util.idle_add(self._on_config_changed, *args)
1465 def _on_config_changed(self, name, old_value, new_value):
1466 if name == 'show_toolbar' and gpodder.ui.desktop:
1467 self.toolbar.set_property('visible', new_value)
1468 elif name == 'videoplayer':
1469 self.config.video_played_dbus = False
1470 elif name == 'player':
1471 self.config.audio_played_dbus = False
1472 elif name == 'episode_list_descriptions':
1473 self.update_episode_list_model()
1474 elif name == 'episode_list_thumbnails':
1475 self.update_episode_list_icons(all=True)
1476 elif name == 'rotation_mode':
1477 self._fremantle_rotation.set_mode(new_value)
1478 elif name in ('auto_update_feeds', 'auto_update_frequency'):
1479 self.restart_auto_update_timer()
1480 elif name == 'podcast_list_view_all':
1481 # Force a update of the podcast list model
1482 self.channel_list_changed = True
1483 if gpodder.ui.fremantle:
1484 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1485 while gtk.events_pending():
1486 gtk.main_iteration(False)
1487 self.update_podcast_list_model()
1488 if gpodder.ui.fremantle:
1489 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1491 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1492 # With get_bin_window, we get the window that contains the rows without
1493 # the header. The Y coordinate of this window will be the height of the
1494 # treeview header. This is the amount we have to subtract from the
1495 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1496 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1497 y -= x_bin
1498 y -= y_bin
1499 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1501 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or x > 50 or (column is not None and column != treeview.get_columns()[0]):
1502 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1503 return False
1505 if path is not None:
1506 model = treeview.get_model()
1507 iter = model.get_iter(path)
1508 role = getattr(treeview, TreeViewHelper.ROLE)
1510 if role == TreeViewHelper.ROLE_EPISODES:
1511 id = model.get_value(iter, EpisodeListModel.C_URL)
1512 elif role == TreeViewHelper.ROLE_PODCASTS:
1513 id = model.get_value(iter, PodcastListModel.C_URL)
1515 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1516 if last_tooltip is not None and last_tooltip != id:
1517 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1518 return False
1519 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1521 if role == TreeViewHelper.ROLE_EPISODES:
1522 description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1523 if description:
1524 tooltip.set_text(description)
1525 else:
1526 return False
1527 elif role == TreeViewHelper.ROLE_PODCASTS:
1528 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1529 if channel is None:
1530 return False
1531 channel.request_save_dir_size()
1532 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1533 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1534 if error_str:
1535 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1536 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1537 table = gtk.Table(rows=3, columns=3)
1538 table.set_row_spacings(5)
1539 table.set_col_spacings(5)
1540 table.set_border_width(5)
1542 heading = gtk.Label()
1543 heading.set_alignment(0, 1)
1544 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1545 table.attach(heading, 0, 1, 0, 1)
1546 size_info = gtk.Label()
1547 size_info.set_alignment(1, 1)
1548 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1549 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1550 table.attach(size_info, 2, 3, 0, 1)
1552 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1554 if len(channel.description) < 500:
1555 description = channel.description
1556 else:
1557 pos = channel.description.find('\n\n')
1558 if pos == -1 or pos > 500:
1559 description = channel.description[:498]+'[...]'
1560 else:
1561 description = channel.description[:pos]
1563 description = gtk.Label(description)
1564 if error_str:
1565 description.set_markup(error_str)
1566 description.set_alignment(0, 0)
1567 description.set_line_wrap(True)
1568 table.attach(description, 0, 3, 2, 3)
1570 table.show_all()
1571 tooltip.set_custom(table)
1573 return True
1575 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1576 return False
1578 def treeview_allow_tooltips(self, treeview, allow):
1579 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1581 def update_m3u_playlist_clicked(self, widget):
1582 if self.active_channel is not None:
1583 self.active_channel.update_m3u_playlist()
1584 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1586 def treeview_handle_context_menu_click(self, treeview, event):
1587 x, y = int(event.x), int(event.y)
1588 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1590 selection = treeview.get_selection()
1591 model, paths = selection.get_selected_rows()
1593 if path is None or (path not in paths and \
1594 event.button == self.context_menu_mouse_button):
1595 # We have right-clicked, but not into the selection,
1596 # assume we don't want to operate on the selection
1597 paths = []
1599 if path is not None and not paths and \
1600 event.button == self.context_menu_mouse_button:
1601 # No selection or clicked outside selection;
1602 # select the single item where we clicked
1603 treeview.grab_focus()
1604 treeview.set_cursor(path, column, 0)
1605 paths = [path]
1607 if not paths:
1608 # Unselect any remaining items (clicked elsewhere)
1609 if hasattr(treeview, 'is_rubber_banding_active'):
1610 if not treeview.is_rubber_banding_active():
1611 selection.unselect_all()
1612 else:
1613 selection.unselect_all()
1615 return model, paths
1617 def downloads_list_get_selection(self, model=None, paths=None):
1618 if model is None and paths is None:
1619 selection = self.treeDownloads.get_selection()
1620 model, paths = selection.get_selected_rows()
1622 can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1623 selected_tasks = [(gtk.TreeRowReference(model, path), \
1624 model.get_value(model.get_iter(path), \
1625 DownloadStatusModel.C_TASK)) for path in paths]
1627 for row_reference, task in selected_tasks:
1628 if task.status != download.DownloadTask.QUEUED:
1629 can_force = False
1630 if task.status not in (download.DownloadTask.PAUSED, \
1631 download.DownloadTask.FAILED, \
1632 download.DownloadTask.CANCELLED):
1633 can_queue = False
1634 if task.status not in (download.DownloadTask.PAUSED, \
1635 download.DownloadTask.QUEUED, \
1636 download.DownloadTask.DOWNLOADING, \
1637 download.DownloadTask.FAILED):
1638 can_cancel = False
1639 if task.status not in (download.DownloadTask.QUEUED, \
1640 download.DownloadTask.DOWNLOADING):
1641 can_pause = False
1642 if task.status not in (download.DownloadTask.CANCELLED, \
1643 download.DownloadTask.FAILED, \
1644 download.DownloadTask.DONE):
1645 can_remove = False
1647 return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1649 def downloads_finished(self, download_tasks_seen):
1650 # FIXME: Filter all tasks that have already been reported
1651 finished_downloads = [str(task) for task in download_tasks_seen if task.status == task.DONE]
1652 failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.status == task.FAILED]
1654 if finished_downloads and failed_downloads:
1655 message = self.format_episode_list(finished_downloads, 5)
1656 message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1657 message += self.format_episode_list(failed_downloads, 5)
1658 self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1659 elif finished_downloads:
1660 message = self.format_episode_list(finished_downloads)
1661 self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1662 elif failed_downloads:
1663 message = self.format_episode_list(failed_downloads)
1664 self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1666 # Open torrent files right after download (bug 1029)
1667 if self.config.open_torrent_after_download:
1668 for task in download_tasks_seen:
1669 if task.status != task.DONE:
1670 continue
1672 episode = task.episode
1673 if episode.mimetype != 'application/x-bittorrent':
1674 continue
1676 self.playback_episodes([episode])
1679 def format_episode_list(self, episode_list, max_episodes=10):
1681 Format a list of episode names for notifications
1683 Will truncate long episode names and limit the amount of
1684 episodes displayed (max_episodes=10).
1686 The episode_list parameter should be a list of strings.
1688 MAX_TITLE_LENGTH = 100
1690 result = []
1691 for title in episode_list[:min(len(episode_list), max_episodes)]:
1692 if len(title) > MAX_TITLE_LENGTH:
1693 middle = (MAX_TITLE_LENGTH/2)-2
1694 title = '%s...%s' % (title[0:middle], title[-middle:])
1695 result.append(saxutils.escape(title))
1696 result.append('\n')
1698 more_episodes = len(episode_list) - max_episodes
1699 if more_episodes > 0:
1700 result.append('(...')
1701 result.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes) % {'count':more_episodes})
1702 result.append('...)')
1704 return (''.join(result)).strip()
1706 def _for_each_task_set_status(self, tasks, status, force_start=False):
1707 episode_urls = set()
1708 model = self.treeDownloads.get_model()
1709 for row_reference, task in tasks:
1710 if status == download.DownloadTask.QUEUED:
1711 # Only queue task when its paused/failed/cancelled (or forced)
1712 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1713 self.download_queue_manager.add_task(task, force_start)
1714 self.enable_download_list_update()
1715 elif status == download.DownloadTask.CANCELLED:
1716 # Cancelling a download allowed when downloading/queued
1717 if task.status in (task.QUEUED, task.DOWNLOADING):
1718 task.status = status
1719 # Cancelling paused/failed downloads requires a call to .run()
1720 elif task.status in (task.PAUSED, task.FAILED):
1721 task.status = status
1722 # Call run, so the partial file gets deleted
1723 task.run()
1724 elif status == download.DownloadTask.PAUSED:
1725 # Pausing a download only when queued/downloading
1726 if task.status in (task.DOWNLOADING, task.QUEUED):
1727 task.status = status
1728 elif status is None:
1729 # Remove the selected task - cancel downloading/queued tasks
1730 if task.status in (task.QUEUED, task.DOWNLOADING):
1731 task.status = task.CANCELLED
1732 model.remove(model.get_iter(row_reference.get_path()))
1733 # Remember the URL, so we can tell the UI to update
1734 try:
1735 # We don't "see" this task anymore - remove it;
1736 # this is needed, so update_episode_list_icons()
1737 # below gets the correct list of "seen" tasks
1738 self.download_tasks_seen.remove(task)
1739 except KeyError, key_error:
1740 log('Cannot remove task from "seen" list: %s', task, sender=self)
1741 episode_urls.add(task.url)
1742 # Tell the task that it has been removed (so it can clean up)
1743 task.removed_from_list()
1744 else:
1745 # We can (hopefully) simply set the task status here
1746 task.status = status
1747 # Tell the podcasts tab to update icons for our removed podcasts
1748 self.update_episode_list_icons(episode_urls)
1749 # Update the tab title and downloads list
1750 self.update_downloads_list()
1752 def treeview_downloads_show_context_menu(self, treeview, event):
1753 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1754 if not paths:
1755 if not hasattr(treeview, 'is_rubber_banding_active'):
1756 return True
1757 else:
1758 return not treeview.is_rubber_banding_active()
1760 if event.button == self.context_menu_mouse_button:
1761 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1762 self.downloads_list_get_selection(model, paths)
1764 def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1765 # This creates a menu item for selection-wide actions
1766 item = gtk.ImageMenuItem(label)
1767 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1768 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1769 item.set_sensitive(sensitive)
1770 return self.set_finger_friendly(item)
1772 menu = gtk.Menu()
1774 item = gtk.ImageMenuItem(_('Episode details'))
1775 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1776 if len(selected_tasks) == 1:
1777 row_reference, task = selected_tasks[0]
1778 episode = task.episode
1779 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1780 else:
1781 item.set_sensitive(False)
1782 menu.append(self.set_finger_friendly(item))
1783 menu.append(gtk.SeparatorMenuItem())
1784 if can_force:
1785 menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1786 else:
1787 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1788 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1789 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1790 menu.append(gtk.SeparatorMenuItem())
1791 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1793 if gpodder.ui.maemo or self.config.enable_fingerscroll:
1794 # Because we open the popup on left-click for Maemo,
1795 # we also include a non-action to close the menu
1796 menu.append(gtk.SeparatorMenuItem())
1797 item = gtk.ImageMenuItem(_('Close this menu'))
1798 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1800 menu.append(self.set_finger_friendly(item))
1802 menu.show_all()
1803 menu.popup(None, None, None, event.button, event.time)
1804 return True
1806 def treeview_channels_show_context_menu(self, treeview, event):
1807 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1808 if not paths:
1809 return True
1811 # Check for valid channel id, if there's no id then
1812 # assume that it is a proxy channel or equivalent
1813 # and cannot be operated with right click
1814 if self.active_channel.id is None:
1815 return True
1817 if event.button == 3:
1818 menu = gtk.Menu()
1820 ICON = lambda x: x
1822 item = gtk.ImageMenuItem( _('Update podcast'))
1823 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1824 item.connect('activate', self.on_itemUpdateChannel_activate)
1825 item.set_sensitive(not self.updating_feed_cache)
1826 menu.append(item)
1828 menu.append(gtk.SeparatorMenuItem())
1830 item = gtk.CheckMenuItem(_('Keep episodes'))
1831 item.set_active(self.active_channel.channel_is_locked)
1832 item.connect('activate', self.on_channel_toggle_lock_activate)
1833 menu.append(self.set_finger_friendly(item))
1835 item = gtk.ImageMenuItem(_('Remove podcast'))
1836 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1837 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1838 menu.append( item)
1840 if self.config.device_type != 'none':
1841 item = gtk.MenuItem(_('Synchronize to device'))
1842 item.connect('activate', lambda item: self.on_sync_to_ipod_activate(item, self.active_channel.get_downloaded_episodes()))
1843 menu.append(item)
1845 menu.append( gtk.SeparatorMenuItem())
1847 item = gtk.ImageMenuItem(_('Podcast details'))
1848 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1849 item.connect('activate', self.on_itemEditChannel_activate)
1850 menu.append(item)
1852 menu.show_all()
1853 # Disable tooltips while we are showing the menu, so
1854 # the tooltip will not appear over the menu
1855 self.treeview_allow_tooltips(self.treeChannels, False)
1856 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1857 menu.popup( None, None, None, event.button, event.time)
1859 return True
1861 def on_itemClose_activate(self, widget):
1862 if self.tray_icon is not None:
1863 self.iconify_main_window()
1864 else:
1865 self.on_gPodder_delete_event(widget)
1867 def cover_file_removed(self, channel_url):
1869 The Cover Downloader calls this when a previously-
1870 available cover has been removed from the disk. We
1871 have to update our model to reflect this change.
1873 self.podcast_list_model.delete_cover_by_url(channel_url)
1875 def cover_download_finished(self, channel, pixbuf):
1877 The Cover Downloader calls this when it has finished
1878 downloading (or registering, if already downloaded)
1879 a new channel cover, which is ready for displaying.
1881 self.podcast_list_model.add_cover_by_channel(channel, pixbuf)
1883 def save_episodes_as_file(self, episodes):
1884 for episode in episodes:
1885 self.save_episode_as_file(episode)
1887 def save_episode_as_file(self, episode):
1888 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1889 if episode.was_downloaded(and_exists=True):
1890 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1891 copy_from = episode.local_filename(create=False)
1892 assert copy_from is not None
1893 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1894 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1895 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1897 def copy_episodes_bluetooth(self, episodes):
1898 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1900 if gpodder.ui.maemo:
1901 util.bluetooth_send_files_maemo([e.local_filename(create=False) \
1902 for e in episodes_to_copy])
1903 return True
1905 def convert_and_send_thread(episode):
1906 for episode in episodes:
1907 filename = episode.local_filename(create=False)
1908 assert filename is not None
1909 destfile = os.path.join(tempfile.gettempdir(), \
1910 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1911 (base, ext) = os.path.splitext(filename)
1912 if not destfile.endswith(ext):
1913 destfile += ext
1915 try:
1916 shutil.copyfile(filename, destfile)
1917 util.bluetooth_send_file(destfile)
1918 except:
1919 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1920 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1922 util.delete_file(destfile)
1924 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1926 def get_device_name(self):
1927 if self.config.device_type == 'ipod':
1928 return _('iPod')
1929 elif self.config.device_type in ('filesystem', 'mtp'):
1930 return _('MP3 player')
1931 else:
1932 return '(unknown device)'
1934 def _treeview_button_released(self, treeview, event):
1935 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1936 dy = int(abs(event.y-ypos))
1937 dx = int(event.x-xpos)
1939 selection = treeview.get_selection()
1940 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1941 if path is None or dy > 30:
1942 return (False, dx, dy)
1944 path, column, x, y = path
1945 selection.select_path(path)
1946 treeview.set_cursor(path)
1947 treeview.grab_focus()
1949 return (True, dx, dy)
1951 def treeview_channels_handle_gestures(self, treeview, event):
1952 if self.currently_updating:
1953 return False
1955 selected, dx, dy = self._treeview_button_released(treeview, event)
1957 if selected:
1958 if self.config.maemo_enable_gestures:
1959 if dx > 70:
1960 self.on_itemUpdateChannel_activate()
1961 elif dx < -70:
1962 self.on_itemEditChannel_activate(treeview)
1964 return False
1966 def treeview_available_handle_gestures(self, treeview, event):
1967 selected, dx, dy = self._treeview_button_released(treeview, event)
1969 if selected:
1970 if self.config.maemo_enable_gestures:
1971 if dx > 70:
1972 self.on_playback_selected_episodes(None)
1973 return True
1974 elif dx < -70:
1975 self.on_shownotes_selected_episodes(None)
1976 return True
1978 # Pass the event to the context menu handler for treeAvailable
1979 self.treeview_available_show_context_menu(treeview, event)
1981 return True
1983 def treeview_available_show_context_menu(self, treeview, event):
1984 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1985 if not paths:
1986 if not hasattr(treeview, 'is_rubber_banding_active'):
1987 return True
1988 else:
1989 return not treeview.is_rubber_banding_active()
1991 if event.button == self.context_menu_mouse_button:
1992 episodes = self.get_selected_episodes()
1993 any_locked = any(e.is_locked for e in episodes)
1994 any_played = any(e.is_played for e in episodes)
1995 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1996 downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
1997 downloading = any(self.episode_is_downloading(e) for e in episodes)
1999 menu = gtk.Menu()
2001 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
2003 if open_instead_of_play:
2004 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
2005 elif downloaded:
2006 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
2007 else:
2008 item = gtk.ImageMenuItem(_('Stream'))
2009 item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
2011 item.set_sensitive(can_play and not downloading)
2012 item.connect('activate', self.on_playback_selected_episodes)
2013 menu.append(self.set_finger_friendly(item))
2015 if not can_cancel:
2016 item = gtk.ImageMenuItem(_('Download'))
2017 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
2018 item.set_sensitive(can_download)
2019 item.connect('activate', self.on_download_selected_episodes)
2020 menu.append(self.set_finger_friendly(item))
2021 else:
2022 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
2023 item.connect('activate', self.on_item_cancel_download_activate)
2024 menu.append(self.set_finger_friendly(item))
2026 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
2027 item.set_sensitive(can_delete)
2028 item.connect('activate', self.on_btnDownloadedDelete_clicked)
2029 menu.append(self.set_finger_friendly(item))
2031 ICON = lambda x: x
2033 # Ok, this probably makes sense to only display for downloaded files
2034 if downloaded:
2035 menu.append(gtk.SeparatorMenuItem())
2036 share_item = gtk.MenuItem(_('Send to'))
2037 menu.append(self.set_finger_friendly(share_item))
2038 share_menu = gtk.Menu()
2040 item = gtk.ImageMenuItem(_('Local folder'))
2041 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
2042 item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
2043 share_menu.append(self.set_finger_friendly(item))
2044 if self.bluetooth_available:
2045 item = gtk.ImageMenuItem(_('Bluetooth device'))
2046 if gpodder.ui.maemo:
2047 icon_name = ICON('qgn_list_filesys_bluetooth')
2048 else:
2049 icon_name = ICON('bluetooth')
2050 item.set_image(gtk.image_new_from_icon_name(icon_name, gtk.ICON_SIZE_MENU))
2051 item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
2052 share_menu.append(self.set_finger_friendly(item))
2053 if can_transfer:
2054 item = gtk.ImageMenuItem(self.get_device_name())
2055 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
2056 item.connect('button-press-event', lambda w, ee: self.on_sync_to_ipod_activate(w, episodes))
2057 share_menu.append(self.set_finger_friendly(item))
2059 share_item.set_submenu(share_menu)
2061 if (downloaded or one_is_new or can_download) and not downloading:
2062 menu.append(gtk.SeparatorMenuItem())
2063 if one_is_new:
2064 item = gtk.CheckMenuItem(_('New'))
2065 item.set_active(True)
2066 item.connect('activate', lambda w: self.mark_selected_episodes_old())
2067 menu.append(self.set_finger_friendly(item))
2068 elif can_download:
2069 item = gtk.CheckMenuItem(_('New'))
2070 item.set_active(False)
2071 item.connect('activate', lambda w: self.mark_selected_episodes_new())
2072 menu.append(self.set_finger_friendly(item))
2074 if downloaded:
2075 item = gtk.CheckMenuItem(_('Played'))
2076 item.set_active(any_played)
2077 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
2078 menu.append(self.set_finger_friendly(item))
2080 item = gtk.CheckMenuItem(_('Keep episode'))
2081 item.set_active(any_locked)
2082 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
2083 menu.append(self.set_finger_friendly(item))
2085 menu.append(gtk.SeparatorMenuItem())
2086 # Single item, add episode information menu item
2087 item = gtk.ImageMenuItem(_('Episode details'))
2088 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
2089 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
2090 menu.append(self.set_finger_friendly(item))
2092 if gpodder.ui.maemo or self.config.enable_fingerscroll:
2093 # Because we open the popup on left-click for Maemo,
2094 # we also include a non-action to close the menu
2095 menu.append(gtk.SeparatorMenuItem())
2096 item = gtk.ImageMenuItem(_('Close this menu'))
2097 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
2098 menu.append(self.set_finger_friendly(item))
2100 menu.show_all()
2101 # Disable tooltips while we are showing the menu, so
2102 # the tooltip will not appear over the menu
2103 self.treeview_allow_tooltips(self.treeAvailable, False)
2104 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
2105 menu.popup( None, None, None, event.button, event.time)
2107 return True
2109 def set_title(self, new_title):
2110 if not gpodder.ui.fremantle:
2111 self.default_title = new_title
2112 self.gPodder.set_title(new_title)
2114 def update_episode_list_icons(self, urls=None, selected=False, all=False):
2116 Updates the status icons in the episode list.
2118 If urls is given, it should be a list of URLs
2119 of episodes that should be updated.
2121 If urls is None, set ONE OF selected, all to
2122 True (the former updates just the selected
2123 episodes and the latter updates all episodes).
2125 additional_args = (self.episode_is_downloading, \
2126 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2127 self.config.episode_list_thumbnails and gpodder.ui.desktop)
2129 if urls is not None:
2130 # We have a list of URLs to walk through
2131 self.episode_list_model.update_by_urls(urls, *additional_args)
2132 elif selected and not all:
2133 # We should update all selected episodes
2134 selection = self.treeAvailable.get_selection()
2135 model, paths = selection.get_selected_rows()
2136 for path in reversed(paths):
2137 iter = model.get_iter(path)
2138 self.episode_list_model.update_by_filter_iter(iter, \
2139 *additional_args)
2140 elif all and not selected:
2141 # We update all (even the filter-hidden) episodes
2142 self.episode_list_model.update_all(*additional_args)
2143 else:
2144 # Wrong/invalid call - have to specify at least one parameter
2145 raise ValueError('Invalid call to update_episode_list_icons')
2147 def episode_list_status_changed(self, episodes):
2148 self.update_episode_list_icons(set(e.url for e in episodes))
2149 self.update_podcast_list_model(set(e.channel.url for e in episodes))
2150 self.db.commit()
2152 def clean_up_downloads(self, delete_partial=False):
2153 # Clean up temporary files left behind by old gPodder versions
2154 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
2156 if delete_partial:
2157 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
2159 for tempfile in temporary_files:
2160 util.delete_file(tempfile)
2162 # Clean up empty download folders and abandoned download folders
2163 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
2164 for ddir in download_dirs:
2165 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2166 globr = glob.glob(os.path.join(ddir, '*'))
2167 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
2168 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
2169 shutil.rmtree(ddir, ignore_errors=True)
2171 def streaming_possible(self):
2172 if gpodder.ui.desktop:
2173 # User has to have a media player set on the Desktop, or else we
2174 # would probably open the browser when giving a URL to xdg-open..
2175 return (self.config.player and self.config.player != 'default')
2176 elif gpodder.ui.maemo:
2177 # On Maemo, the default is to use the Nokia Media Player, which is
2178 # already able to deal with HTTP URLs the right way, so we
2179 # unconditionally enable streaming always on Maemo
2180 return True
2182 return False
2184 def playback_episodes_for_real(self, episodes):
2185 groups = collections.defaultdict(list)
2186 for episode in episodes:
2187 file_type = episode.file_type()
2188 if file_type == 'video' and self.config.videoplayer and \
2189 self.config.videoplayer != 'default':
2190 player = self.config.videoplayer
2191 if gpodder.ui.diablo:
2192 # Use the wrapper script if it's installed to crop 3GP YouTube
2193 # videos to fit the screen (looks much nicer than w/ black border)
2194 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2195 player = 'gpodder-mplayer'
2196 elif gpodder.ui.fremantle and player == 'mplayer':
2197 player = 'mplayer -fs %F'
2198 elif file_type == 'audio' and self.config.player and \
2199 self.config.player != 'default':
2200 player = self.config.player
2201 else:
2202 player = 'default'
2204 if file_type not in ('audio', 'video') or \
2205 (file_type == 'audio' and not self.config.audio_played_dbus) or \
2206 (file_type == 'video' and not self.config.video_played_dbus):
2207 # Mark episode as played in the database
2208 episode.mark(is_played=True)
2209 self.mygpo_client.on_playback([episode])
2211 filename = episode.local_filename(create=False)
2212 if filename is None or not os.path.exists(filename):
2213 filename = episode.url
2214 if youtube.is_video_link(filename):
2215 fmt_id = self.config.youtube_preferred_fmt_id
2216 if gpodder.ui.fremantle:
2217 fmt_id = 5
2218 filename = youtube.get_real_download_url(filename, fmt_id)
2220 # Determine the playback resume position - if the file
2221 # was played 100%, we simply start from the beginning
2222 resume_position = episode.current_position
2223 if resume_position == episode.total_time:
2224 resume_position = 0
2226 if gpodder.ui.fremantle:
2227 self.mafw_monitor.set_resume_point(filename, resume_position)
2229 # If Panucci is configured, use D-Bus on Maemo to call it
2230 if player == 'panucci':
2231 try:
2232 PANUCCI_NAME = 'org.panucci.panucciInterface'
2233 PANUCCI_PATH = '/panucciInterface'
2234 PANUCCI_INTF = 'org.panucci.panucciInterface'
2235 o = gpodder.dbus_session_bus.get_object(PANUCCI_NAME, PANUCCI_PATH)
2236 i = dbus.Interface(o, PANUCCI_INTF)
2238 def on_reply(*args):
2239 pass
2241 def error_handler(filename, err):
2242 log('Exception in D-Bus call: %s', str(err), \
2243 sender=self)
2245 # Fallback: use the command line client
2246 for command in util.format_desktop_command('panucci', \
2247 [filename]):
2248 log('Executing: %s', repr(command), sender=self)
2249 subprocess.Popen(command)
2251 on_error = lambda err: error_handler(filename, err)
2253 # This method only exists in Panucci > 0.9 ('new Panucci')
2254 i.playback_from(filename, resume_position, \
2255 reply_handler=on_reply, error_handler=on_error)
2257 continue # This file was handled by the D-Bus call
2258 except Exception, e:
2259 log('Error calling Panucci using D-Bus', sender=self, traceback=True)
2260 elif player == 'MediaBox' and gpodder.ui.maemo:
2261 try:
2262 MEDIABOX_NAME = 'de.pycage.mediabox'
2263 MEDIABOX_PATH = '/de/pycage/mediabox/control'
2264 MEDIABOX_INTF = 'de.pycage.mediabox.control'
2265 o = gpodder.dbus_session_bus.get_object(MEDIABOX_NAME, MEDIABOX_PATH)
2266 i = dbus.Interface(o, MEDIABOX_INTF)
2268 def on_reply(*args):
2269 pass
2271 def on_error(err):
2272 log('Exception in D-Bus call: %s', str(err), \
2273 sender=self)
2275 i.load(filename, '%s/x-unknown' % file_type, \
2276 reply_handler=on_reply, error_handler=on_error)
2278 continue # This file was handled by the D-Bus call
2279 except Exception, e:
2280 log('Error calling MediaBox using D-Bus', sender=self, traceback=True)
2282 groups[player].append(filename)
2284 # Open episodes with system default player
2285 if 'default' in groups:
2286 # Special-casing for a single episode when the object is a PDF
2287 # file - this is needed on Maemo 5, so we only use gui_open()
2288 # for single PDF files, but still use the built-in media player
2289 # with an M3U file for single audio/video files. (The Maemo 5
2290 # media player behaves differently when opening a single-file
2291 # M3U playlist compared to opening the single file directly.)
2292 if len(groups['default']) == 1:
2293 fn = groups['default'][0]
2294 # The list of extensions is taken from gui_open in util.py
2295 # where all special-cases of Maemo apps are listed
2296 for extension in ('.pdf', '.jpg', '.jpeg', '.png'):
2297 if fn.lower().endswith(extension):
2298 util.gui_open(fn)
2299 groups['default'] = []
2300 break
2302 if gpodder.ui.maemo and groups['default']:
2303 # The Nokia Media Player app does not support receiving multiple
2304 # file names via D-Bus, so we simply place all file names into a
2305 # temporary M3U playlist and open that with the Media Player.
2306 m3u_filename = os.path.join(gpodder.home, 'gpodder_open_with.m3u')
2308 def to_url(x):
2309 if '://' not in x:
2310 return 'file://' + urllib.quote(os.path.abspath(x))
2311 return x
2313 util.write_m3u_playlist(m3u_filename, \
2314 map(to_url, groups['default']), \
2315 extm3u=False)
2316 util.gui_open(m3u_filename)
2317 else:
2318 for filename in groups['default']:
2319 log('Opening with system default: %s', filename, sender=self)
2320 util.gui_open(filename)
2321 del groups['default']
2322 elif gpodder.ui.maemo and groups:
2323 # When on Maemo and not opening with default, show a notification
2324 # (no startup notification for Panucci / MPlayer yet...)
2325 if len(episodes) == 1:
2326 text = _('Opening %s') % episodes[0].title
2327 else:
2328 count = len(episodes)
2329 text = N_('Opening %(count)d episode', 'Opening %(count)d episodes', count) % {'count':count}
2331 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2333 def destroy_banner_later(banner):
2334 banner.destroy()
2335 return False
2336 gobject.timeout_add(5000, destroy_banner_later, banner)
2338 # For each type now, go and create play commands
2339 for group in groups:
2340 for command in util.format_desktop_command(group, groups[group]):
2341 log('Executing: %s', repr(command), sender=self)
2342 subprocess.Popen(command)
2344 # Persist episode status changes to the database
2345 self.db.commit()
2347 # Flush updated episode status
2348 self.mygpo_client.flush()
2350 def playback_episodes(self, episodes):
2351 # We need to create a list, because we run through it more than once
2352 episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2353 e.was_downloaded(and_exists=True) or self.streaming_possible()))
2355 try:
2356 self.playback_episodes_for_real(episodes)
2357 except Exception, e:
2358 log('Error in playback!', sender=self, traceback=True)
2359 if gpodder.ui.desktop:
2360 self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2361 _('Error opening player'), widget=self.toolPreferences)
2362 else:
2363 self.show_message(_('Please check your media player settings in the preferences dialog.'))
2365 channel_urls = set()
2366 episode_urls = set()
2367 for episode in episodes:
2368 channel_urls.add(episode.channel.url)
2369 episode_urls.add(episode.url)
2370 self.update_episode_list_icons(episode_urls)
2371 self.update_podcast_list_model(channel_urls)
2373 def play_or_download(self):
2374 if not gpodder.ui.fremantle:
2375 if self.wNotebook.get_current_page() > 0:
2376 if gpodder.ui.desktop:
2377 self.toolCancel.set_sensitive(True)
2378 return
2380 if self.currently_updating:
2381 return (False, False, False, False, False, False)
2383 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2384 ( is_played, is_locked ) = (False,)*2
2386 open_instead_of_play = False
2388 selection = self.treeAvailable.get_selection()
2389 if selection.count_selected_rows() > 0:
2390 (model, paths) = selection.get_selected_rows()
2392 for path in paths:
2393 try:
2394 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2395 except TypeError, te:
2396 log('Invalid episode at path %s', str(path), sender=self)
2397 continue
2399 if episode.file_type() not in ('audio', 'video'):
2400 open_instead_of_play = True
2402 if episode.was_downloaded():
2403 can_play = episode.was_downloaded(and_exists=True)
2404 is_played = episode.is_played
2405 is_locked = episode.is_locked
2406 if not can_play:
2407 can_download = True
2408 else:
2409 if self.episode_is_downloading(episode):
2410 can_cancel = True
2411 else:
2412 can_download = True
2414 can_download = can_download and not can_cancel
2415 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2416 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2417 can_delete = not can_cancel
2419 if gpodder.ui.desktop:
2420 if open_instead_of_play:
2421 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2422 else:
2423 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2424 self.toolPlay.set_sensitive( can_play)
2425 self.toolDownload.set_sensitive( can_download)
2426 self.toolTransfer.set_sensitive( can_transfer)
2427 self.toolCancel.set_sensitive( can_cancel)
2429 if not gpodder.ui.fremantle:
2430 self.item_cancel_download.set_sensitive(can_cancel)
2431 self.itemDownloadSelected.set_sensitive(can_download)
2432 self.itemOpenSelected.set_sensitive(can_play)
2433 self.itemPlaySelected.set_sensitive(can_play)
2434 self.itemDeleteSelected.set_sensitive(can_delete)
2435 self.item_toggle_played.set_sensitive(can_play)
2436 self.item_toggle_lock.set_sensitive(can_play)
2437 self.itemOpenSelected.set_visible(open_instead_of_play)
2438 self.itemPlaySelected.set_visible(not open_instead_of_play)
2440 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2442 def on_cbMaxDownloads_toggled(self, widget, *args):
2443 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2445 def on_cbLimitDownloads_toggled(self, widget, *args):
2446 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2448 def episode_new_status_changed(self, urls):
2449 self.update_podcast_list_model()
2450 self.update_episode_list_icons(urls)
2452 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2453 """Update the podcast list treeview model
2455 If urls is given, it should list the URLs of each
2456 podcast that has to be updated in the list.
2458 If selected is True, only update the model contents
2459 for the currently-selected podcast - nothing more.
2461 The caller can optionally specify "select_url",
2462 which is the URL of the podcast that is to be
2463 selected in the list after the update is complete.
2464 This only works if the podcast list has to be
2465 reloaded; i.e. something has been added or removed
2466 since the last update of the podcast list).
2468 selection = self.treeChannels.get_selection()
2469 model, iter = selection.get_selected()
2471 if self.config.podcast_list_view_all and not self.channel_list_changed:
2472 # Update "all episodes" view in any case (if enabled)
2473 self.podcast_list_model.update_first_row()
2475 if selected:
2476 # very cheap! only update selected channel
2477 if iter is not None:
2478 # If we have selected the "all episodes" view, we have
2479 # to update all channels for selected episodes:
2480 if self.config.podcast_list_view_all and \
2481 self.podcast_list_model.iter_is_first_row(iter):
2482 urls = self.get_podcast_urls_from_selected_episodes()
2483 self.podcast_list_model.update_by_urls(urls)
2484 else:
2485 # Otherwise just update the selected row (a podcast)
2486 self.podcast_list_model.update_by_filter_iter(iter)
2487 elif not self.channel_list_changed:
2488 # we can keep the model, but have to update some
2489 if urls is None:
2490 # still cheaper than reloading the whole list
2491 self.podcast_list_model.update_all()
2492 else:
2493 # ok, we got a bunch of urls to update
2494 self.podcast_list_model.update_by_urls(urls)
2495 else:
2496 if model and iter and select_url is None:
2497 # Get the URL of the currently-selected podcast
2498 select_url = model.get_value(iter, PodcastListModel.C_URL)
2500 # Update the podcast list model with new channels
2501 self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2503 try:
2504 selected_iter = model.get_iter_first()
2505 # Find the previously-selected URL in the new
2506 # model if we have an URL (else select first)
2507 if select_url is not None:
2508 pos = model.get_iter_first()
2509 while pos is not None:
2510 url = model.get_value(pos, PodcastListModel.C_URL)
2511 if url == select_url:
2512 selected_iter = pos
2513 break
2514 pos = model.iter_next(pos)
2516 if not gpodder.ui.maemo:
2517 if selected_iter is not None:
2518 selection.select_iter(selected_iter)
2519 self.on_treeChannels_cursor_changed(self.treeChannels)
2520 except:
2521 log('Cannot select podcast in list', traceback=True, sender=self)
2522 self.channel_list_changed = False
2524 def episode_is_downloading(self, episode):
2525 """Returns True if the given episode is being downloaded at the moment"""
2526 if episode is None:
2527 return False
2529 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2531 def on_episode_list_filter_changed(self, has_episodes):
2532 if gpodder.ui.fremantle:
2533 if has_episodes:
2534 self.episodes_window.empty_label.hide()
2535 self.episodes_window.pannablearea.show()
2536 else:
2537 if self.config.episode_list_view_mode != \
2538 EpisodeListModel.VIEW_ALL:
2539 text = _('No episodes in current view')
2540 else:
2541 text = _('No episodes available')
2542 self.episodes_window.empty_label.set_text(text)
2543 self.episodes_window.pannablearea.hide()
2544 self.episodes_window.empty_label.show()
2546 def update_episode_list_model(self):
2547 if self.channels and self.active_channel is not None:
2548 if gpodder.ui.fremantle:
2549 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2551 self.currently_updating = True
2552 self.episode_list_model.clear()
2553 if gpodder.ui.fremantle:
2554 self.episodes_window.pannablearea.hide()
2555 self.episodes_window.empty_label.set_text(_('Loading episodes'))
2556 self.episodes_window.empty_label.show()
2558 def update():
2559 additional_args = (self.episode_is_downloading, \
2560 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2561 self.config.episode_list_thumbnails and gpodder.ui.desktop)
2562 self.episode_list_model.replace_from_channel(self.active_channel, *additional_args)
2564 self.treeAvailable.get_selection().unselect_all()
2565 self.treeAvailable.scroll_to_point(0, 0)
2567 self.currently_updating = False
2568 self.play_or_download()
2570 if gpodder.ui.fremantle:
2571 hildon.hildon_gtk_window_set_progress_indicator(\
2572 self.episodes_window.main_window, False)
2574 util.idle_add(update)
2575 else:
2576 self.episode_list_model.clear()
2578 @dbus.service.method(gpodder.dbus_interface)
2579 def offer_new_episodes(self, channels=None):
2580 if gpodder.ui.fremantle:
2581 # Assume that when this function is called that the
2582 # notification is not shown anymore (Maemo bug 11345)
2583 self._fremantle_notification_visible = False
2585 new_episodes = self.get_new_episodes(channels)
2586 if new_episodes:
2587 self.new_episodes_show(new_episodes)
2588 return True
2589 return False
2591 def add_podcast_list(self, urls, auth_tokens=None):
2592 """Subscribe to a list of podcast given their URLs
2594 If auth_tokens is given, it should be a dictionary
2595 mapping URLs to (username, password) tuples."""
2597 if auth_tokens is None:
2598 auth_tokens = {}
2600 # Sort and split the URL list into five buckets
2601 queued, failed, existing, worked, authreq = [], [], [], [], []
2602 for input_url in urls:
2603 url = util.normalize_feed_url(input_url)
2604 if url is None:
2605 # Fail this one because the URL is not valid
2606 failed.append(input_url)
2607 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2608 # A podcast already exists in the list for this URL
2609 existing.append(url)
2610 else:
2611 # This URL has survived the first round - queue for add
2612 queued.append(url)
2613 if url != input_url and input_url in auth_tokens:
2614 auth_tokens[url] = auth_tokens[input_url]
2616 error_messages = {}
2617 redirections = {}
2619 progress = ProgressIndicator(_('Adding podcasts'), \
2620 _('Please wait while episode information is downloaded.'), \
2621 parent=self.get_dialog_parent())
2623 def on_after_update():
2624 progress.on_finished()
2625 # Report already-existing subscriptions to the user
2626 if existing:
2627 title = _('Existing subscriptions skipped')
2628 message = _('You are already subscribed to these podcasts:') \
2629 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2630 self.show_message(message, title, widget=self.treeChannels)
2632 # Report subscriptions that require authentication
2633 if authreq:
2634 retry_podcasts = {}
2635 for url in authreq:
2636 title = _('Podcast requires authentication')
2637 message = _('Please login to %s:') % (saxutils.escape(url),)
2638 success, auth_tokens = self.show_login_dialog(title, message)
2639 if success:
2640 retry_podcasts[url] = auth_tokens
2641 else:
2642 # Stop asking the user for more login data
2643 retry_podcasts = {}
2644 for url in authreq:
2645 error_messages[url] = _('Authentication failed')
2646 failed.append(url)
2647 break
2649 # If we have authentication data to retry, do so here
2650 if retry_podcasts:
2651 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2653 # Report website redirections
2654 for url in redirections:
2655 title = _('Website redirection detected')
2656 message = _('The URL %(url)s redirects to %(target)s.') \
2657 + '\n\n' + _('Do you want to visit the website now?')
2658 message = message % {'url': url, 'target': redirections[url]}
2659 if self.show_confirmation(message, title):
2660 util.open_website(url)
2661 else:
2662 break
2664 # Report failed subscriptions to the user
2665 if failed:
2666 title = _('Could not add some podcasts')
2667 message = _('Some podcasts could not be added to your list:') \
2668 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2669 error_messages.get(url, _('Unknown')))) for url in failed)
2670 self.show_message(message, title, important=True)
2672 # Upload subscription changes to gpodder.net
2673 self.mygpo_client.on_subscribe(worked)
2675 # If at least one podcast has been added, save and update all
2676 if self.channel_list_changed:
2677 # Fix URLs if mygpo has rewritten them
2678 self.rewrite_urls_mygpo()
2680 self.save_channels_opml()
2682 # If only one podcast was added, select it after the update
2683 if len(worked) == 1:
2684 url = worked[0]
2685 else:
2686 url = None
2688 # Update the list of subscribed podcasts
2689 self.update_feed_cache(force_update=False, select_url_afterwards=url)
2690 self.update_podcasts_tab()
2692 # Offer to download new episodes
2693 episodes = []
2694 for podcast in self.channels:
2695 if podcast.url in worked:
2696 episodes.extend(podcast.get_all_episodes())
2698 if episodes:
2699 episodes = list(PodcastEpisode.sort_by_pubdate(episodes, \
2700 reverse=True))
2701 self.new_episodes_show(episodes, \
2702 selected=[e.check_is_new() for e in episodes])
2705 def thread_proc():
2706 # After the initial sorting and splitting, try all queued podcasts
2707 length = len(queued)
2708 for index, url in enumerate(queued):
2709 progress.on_progress(float(index)/float(length))
2710 progress.on_message(url)
2711 log('QUEUE RUNNER: %s', url, sender=self)
2712 try:
2713 # The URL is valid and does not exist already - subscribe!
2714 channel = PodcastChannel.load(self.db, url=url, create=True, \
2715 authentication_tokens=auth_tokens.get(url, None), \
2716 max_episodes=self.config.max_episodes_per_feed, \
2717 download_dir=self.config.download_dir, \
2718 allow_empty_feeds=self.config.allow_empty_feeds, \
2719 mimetype_prefs=self.config.mimetype_prefs)
2721 try:
2722 username, password = util.username_password_from_url(url)
2723 except ValueError, ve:
2724 username, password = (None, None)
2726 if username is not None and channel.username is None and \
2727 password is not None and channel.password is None:
2728 channel.username = username
2729 channel.password = password
2730 channel.save()
2732 self._update_cover(channel)
2733 except feedcore.AuthenticationRequired:
2734 if url in auth_tokens:
2735 # Fail for wrong authentication data
2736 error_messages[url] = _('Authentication failed')
2737 failed.append(url)
2738 else:
2739 # Queue for login dialog later
2740 authreq.append(url)
2741 continue
2742 except feedcore.WifiLogin, error:
2743 redirections[url] = error.data
2744 failed.append(url)
2745 error_messages[url] = _('Redirection detected')
2746 continue
2747 except Exception, e:
2748 log('Subscription error: %s', e, traceback=True, sender=self)
2749 error_messages[url] = str(e)
2750 failed.append(url)
2751 continue
2753 assert channel is not None
2754 worked.append(channel.url)
2755 self.channels.append(channel)
2756 self.channel_list_changed = True
2757 util.idle_add(on_after_update)
2758 threading.Thread(target=thread_proc).start()
2760 def save_channels_opml(self):
2761 exporter = opml.Exporter(gpodder.subscription_file)
2762 return exporter.write(self.channels)
2764 def find_episode(self, podcast_url, episode_url):
2765 """Find an episode given its podcast and episode URL
2767 The function will return a PodcastEpisode object if
2768 the episode is found, or None if it's not found.
2770 for podcast in self.channels:
2771 if podcast_url == podcast.url:
2772 for episode in podcast.get_all_episodes():
2773 if episode_url == episode.url:
2774 return episode
2776 return None
2778 def process_received_episode_actions(self, updated_urls):
2779 """Process/merge episode actions from gpodder.net
2781 This function will merge all changes received from
2782 the server to the local database and update the
2783 status of the affected episodes as necessary.
2785 indicator = ProgressIndicator(_('Merging episode actions'), \
2786 _('Episode actions from gpodder.net are merged.'), \
2787 False, self.get_dialog_parent())
2789 for idx, action in enumerate(self.mygpo_client.get_episode_actions(updated_urls)):
2790 if action.action == 'play':
2791 episode = self.find_episode(action.podcast_url, \
2792 action.episode_url)
2794 if episode is not None:
2795 log('Play action for %s', episode.url, sender=self)
2796 episode.mark(is_played=True)
2798 if action.timestamp > episode.current_position_updated and \
2799 action.position is not None:
2800 log('Updating position for %s', episode.url, sender=self)
2801 episode.current_position = action.position
2802 episode.current_position_updated = action.timestamp
2804 if action.total:
2805 log('Updating total time for %s', episode.url, sender=self)
2806 episode.total_time = action.total
2808 episode.save()
2809 elif action.action == 'delete':
2810 episode = self.find_episode(action.podcast_url, \
2811 action.episode_url)
2813 if episode is not None:
2814 if not episode.was_downloaded(and_exists=True):
2815 # Set the episode to a "deleted" state
2816 log('Marking as deleted: %s', episode.url, sender=self)
2817 episode.delete_from_disk()
2818 episode.save()
2820 indicator.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx) % {'count':idx})
2821 gtk.main_iteration(False)
2823 indicator.on_finished()
2824 self.db.commit()
2827 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2828 self.db.commit()
2829 self.updating_feed_cache = False
2831 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2833 # Process received episode actions for all updated URLs
2834 self.process_received_episode_actions(updated_urls)
2836 self.channel_list_changed = True
2837 self.update_podcast_list_model(select_url=select_url_afterwards)
2839 # Only search for new episodes in podcasts that have been
2840 # updated, not in other podcasts (for single-feed updates)
2841 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2843 if gpodder.ui.fremantle:
2844 self.fancy_progress_bar.hide()
2845 self.button_subscribe.set_sensitive(True)
2846 self.button_refresh.set_sensitive(True)
2847 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2848 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2849 self.update_podcasts_tab()
2850 self.update_episode_list_model()
2851 if self.feed_cache_update_cancelled:
2852 return
2854 def application_in_foreground():
2855 try:
2856 return any(w.get_property('is-topmost') for w in hildon.WindowStack.get_default().get_windows())
2857 except Exception, e:
2858 log('Could not determine is-topmost', traceback=True)
2859 # When in doubt, assume not in foreground
2860 return False
2862 if episodes:
2863 if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2864 # New episodes found, but we should do nothing
2865 self.show_message(_('New episodes are available.'))
2866 elif self.config.auto_download == 'always':
2867 count = len(episodes)
2868 title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2869 self.show_message(title)
2870 self.download_episode_list(episodes)
2871 elif self.config.auto_download == 'queue':
2872 self.show_message(_('New episodes have been added to the download list.'))
2873 self.download_episode_list_paused(episodes)
2874 elif application_in_foreground():
2875 if not self._fremantle_notification_visible:
2876 self.new_episodes_show(episodes)
2877 elif not self._fremantle_notification_visible:
2878 try:
2879 import pynotify
2880 pynotify.init('gPodder')
2881 n = pynotify.Notification('gPodder', _('New episodes available'), 'gpodder')
2882 n.set_urgency(pynotify.URGENCY_CRITICAL)
2883 n.set_hint('dbus-callback-default', ' '.join([
2884 gpodder.dbus_bus_name,
2885 gpodder.dbus_gui_object_path,
2886 gpodder.dbus_interface,
2887 'offer_new_episodes',
2889 n.set_category('gpodder-new-episodes')
2890 n.show()
2891 self._fremantle_notification_visible = True
2892 except Exception, e:
2893 log('Error: %s', str(e), sender=self, traceback=True)
2894 self.new_episodes_show(episodes)
2895 self._fremantle_notification_visible = False
2896 elif not self.config.auto_update_feeds:
2897 self.show_message(_('No new episodes. Please check for new episodes later.'))
2898 return
2900 if self.tray_icon:
2901 self.tray_icon.set_status()
2903 if self.feed_cache_update_cancelled:
2904 # The user decided to abort the feed update
2905 self.show_update_feeds_buttons()
2906 elif not episodes:
2907 # Nothing new here - but inform the user
2908 self.pbFeedUpdate.set_fraction(1.0)
2909 self.pbFeedUpdate.set_text(_('No new episodes'))
2910 self.feed_cache_update_cancelled = True
2911 self.btnCancelFeedUpdate.show()
2912 self.btnCancelFeedUpdate.set_sensitive(True)
2913 self.itemUpdate.set_sensitive(True)
2914 if gpodder.ui.maemo:
2915 # btnCancelFeedUpdate is a ToolButton on Maemo
2916 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2917 else:
2918 # btnCancelFeedUpdate is a normal gtk.Button
2919 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2920 else:
2921 count = len(episodes)
2922 # New episodes are available
2923 self.pbFeedUpdate.set_fraction(1.0)
2924 # Are we minimized and should we auto download?
2925 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2926 self.download_episode_list(episodes)
2927 title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2928 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2929 self.show_update_feeds_buttons()
2930 elif self.config.auto_download == 'queue':
2931 self.download_episode_list_paused(episodes)
2932 title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
2933 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2934 self.show_update_feeds_buttons()
2935 else:
2936 self.show_update_feeds_buttons()
2937 # New episodes are available and we are not minimized
2938 if not self.config.do_not_show_new_episodes_dialog:
2939 self.new_episodes_show(episodes, notification=True)
2940 else:
2941 message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
2942 self.pbFeedUpdate.set_text(message)
2944 def _update_cover(self, channel):
2945 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2946 self.cover_downloader.request_cover(channel)
2948 def update_feed_cache_proc(self, channels, select_url_afterwards):
2949 total = len(channels)
2951 for updated, channel in enumerate(channels):
2952 if not self.feed_cache_update_cancelled:
2953 try:
2954 channel.update(max_episodes=self.config.max_episodes_per_feed, \
2955 mimetype_prefs=self.config.mimetype_prefs)
2956 self._update_cover(channel)
2957 except Exception, e:
2958 d = {'url': saxutils.escape(channel.url), 'message': saxutils.escape(str(e))}
2959 if d['message']:
2960 message = _('Error while updating %(url)s: %(message)s')
2961 else:
2962 message = _('The feed at %(url)s could not be updated.')
2963 self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
2964 log('Error: %s', str(e), sender=self, traceback=True)
2966 if self.feed_cache_update_cancelled:
2967 break
2969 # By the time we get here the update may have already been cancelled
2970 if not self.feed_cache_update_cancelled:
2971 def update_progress():
2972 d = {'podcast': channel.title, 'position': updated+1, 'total': total}
2973 progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2974 self.pbFeedUpdate.set_text(progression)
2975 if self.tray_icon:
2976 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2977 self.pbFeedUpdate.set_fraction(float(updated+1)/float(total))
2978 util.idle_add(update_progress)
2980 updated_urls = [c.url for c in channels]
2981 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2983 def show_update_feeds_buttons(self):
2984 # Make sure that the buttons for updating feeds
2985 # appear - this should happen after a feed update
2986 if gpodder.ui.maemo:
2987 self.btnUpdateSelectedFeed.show()
2988 self.toolFeedUpdateProgress.hide()
2989 self.btnCancelFeedUpdate.hide()
2990 self.btnCancelFeedUpdate.set_is_important(False)
2991 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2992 self.toolbarSpacer.set_expand(True)
2993 self.toolbarSpacer.set_draw(False)
2994 else:
2995 self.hboxUpdateFeeds.hide()
2996 self.btnUpdateFeeds.show()
2997 self.itemUpdate.set_sensitive(True)
2998 self.itemUpdateChannel.set_sensitive(True)
3000 def on_btnCancelFeedUpdate_clicked(self, widget):
3001 if not self.feed_cache_update_cancelled:
3002 self.pbFeedUpdate.set_text(_('Cancelling...'))
3003 self.feed_cache_update_cancelled = True
3004 if not gpodder.ui.fremantle:
3005 self.btnCancelFeedUpdate.set_sensitive(False)
3006 elif not gpodder.ui.fremantle:
3007 self.show_update_feeds_buttons()
3009 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
3010 if self.updating_feed_cache:
3011 if gpodder.ui.fremantle:
3012 self.feed_cache_update_cancelled = True
3013 return
3015 if not force_update:
3016 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
3017 self.channel_list_changed = True
3018 self.update_podcast_list_model(select_url=select_url_afterwards)
3019 return
3021 # Fix URLs if mygpo has rewritten them
3022 self.rewrite_urls_mygpo()
3024 self.updating_feed_cache = True
3026 if channels is None:
3027 # Only update podcasts for which updates are enabled
3028 channels = [c for c in self.channels if c.feed_update_enabled]
3030 if gpodder.ui.fremantle:
3031 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
3032 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
3033 self.fancy_progress_bar.show()
3034 self.button_subscribe.set_sensitive(False)
3035 self.button_refresh.set_sensitive(False)
3036 self.feed_cache_update_cancelled = False
3037 else:
3038 self.itemUpdate.set_sensitive(False)
3039 self.itemUpdateChannel.set_sensitive(False)
3041 if self.tray_icon:
3042 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
3044 self.feed_cache_update_cancelled = False
3045 self.btnCancelFeedUpdate.show()
3046 self.btnCancelFeedUpdate.set_sensitive(True)
3047 if gpodder.ui.maemo:
3048 self.toolbarSpacer.set_expand(False)
3049 self.toolbarSpacer.set_draw(True)
3050 self.btnUpdateSelectedFeed.hide()
3051 self.toolFeedUpdateProgress.show_all()
3052 else:
3053 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
3054 self.hboxUpdateFeeds.show_all()
3055 self.btnUpdateFeeds.hide()
3057 if len(channels) == 1:
3058 text = _('Updating "%s"...') % channels[0].title
3059 else:
3060 count = len(channels)
3061 text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count) % {'count':count}
3062 self.pbFeedUpdate.set_text(text)
3063 self.pbFeedUpdate.set_fraction(0)
3065 args = (channels, select_url_afterwards)
3066 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
3068 def on_gPodder_delete_event(self, widget, *args):
3069 """Called when the GUI wants to close the window
3070 Displays a confirmation dialog (and closes/hides gPodder)
3073 downloading = self.download_status_model.are_downloads_in_progress()
3075 # Only iconify if we are using the window's "X" button,
3076 # but not when we are using "Quit" in the menu or toolbar
3077 if self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
3078 self.iconify_main_window()
3079 elif downloading:
3080 if gpodder.ui.fremantle:
3081 self.close_gpodder()
3082 elif gpodder.ui.diablo:
3083 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
3084 if result:
3085 self.close_gpodder()
3086 else:
3087 return True
3088 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
3089 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3090 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
3092 title = _('Quit gPodder')
3093 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3095 dialog.set_title(title)
3096 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
3098 quit_button.grab_focus()
3099 result = dialog.run()
3100 dialog.destroy()
3102 if result == gtk.RESPONSE_CLOSE:
3103 self.close_gpodder()
3104 else:
3105 self.close_gpodder()
3107 return True
3109 def close_gpodder(self):
3110 """ clean everything and exit properly
3112 if self.channels:
3113 if self.save_channels_opml():
3114 pass # FIXME: Add mygpo synchronization here
3115 else:
3116 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
3118 self.gPodder.hide()
3120 if self.tray_icon is not None:
3121 self.tray_icon.set_visible(False)
3123 # Notify all tasks to to carry out any clean-up actions
3124 self.download_status_model.tell_all_tasks_to_quit()
3126 while gtk.events_pending():
3127 gtk.main_iteration(False)
3129 self.db.close()
3131 self.quit()
3132 sys.exit(0)
3134 def get_expired_episodes(self):
3135 for channel in self.channels:
3136 for episode in channel.get_downloaded_episodes():
3137 # Never consider locked episodes as old
3138 if episode.is_locked:
3139 continue
3141 # Never consider fresh episodes as old
3142 if episode.age_in_days() < self.config.episode_old_age:
3143 continue
3145 # Do not delete played episodes (except if configured)
3146 if episode.is_played:
3147 if not self.config.auto_remove_played_episodes:
3148 continue
3150 # Do not delete unplayed episodes (except if configured)
3151 if not episode.is_played:
3152 if not self.config.auto_remove_unplayed_episodes:
3153 continue
3155 yield episode
3157 def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
3158 if not episodes:
3159 return False
3161 if skip_locked:
3162 episodes = [e for e in episodes if not e.is_locked]
3164 if not episodes:
3165 title = _('Episodes are locked')
3166 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3167 self.notification(message, title, widget=self.treeAvailable)
3168 return False
3170 count = len(episodes)
3171 title = N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count) % {'count':count}
3172 message = _('Deleting episodes removes downloaded files.')
3174 if gpodder.ui.fremantle:
3175 message = '\n'.join([title, message])
3177 if confirm and not self.show_confirmation(message, title):
3178 return False
3180 progress = ProgressIndicator(_('Deleting episodes'), \
3181 _('Please wait while episodes are deleted'), \
3182 parent=self.get_dialog_parent())
3184 def finish_deletion(episode_urls, channel_urls):
3185 progress.on_finished()
3187 # Episodes have been deleted - persist the database
3188 self.db.commit()
3190 self.update_episode_list_icons(episode_urls)
3191 self.update_podcast_list_model(channel_urls)
3192 self.play_or_download()
3194 def thread_proc():
3195 episode_urls = set()
3196 channel_urls = set()
3198 episodes_status_update = []
3199 for idx, episode in enumerate(episodes):
3200 progress.on_progress(float(idx)/float(len(episodes)))
3201 if episode.is_locked and skip_locked:
3202 log('Not deleting episode (is locked): %s', episode.title)
3203 else:
3204 log('Deleting episode: %s', episode.title)
3205 progress.on_message(episode.title)
3206 episode.delete_from_disk()
3207 episode_urls.add(episode.url)
3208 channel_urls.add(episode.channel.url)
3209 episodes_status_update.append(episode)
3211 # Tell the shownotes window that we have removed the episode
3212 if self.episode_shownotes_window is not None and \
3213 self.episode_shownotes_window.episode is not None and \
3214 self.episode_shownotes_window.episode.url == episode.url:
3215 util.idle_add(self.episode_shownotes_window._download_status_changed, None)
3217 # Notify the web service about the status update + upload
3218 self.mygpo_client.on_delete(episodes_status_update)
3219 self.mygpo_client.flush()
3221 util.idle_add(finish_deletion, episode_urls, channel_urls)
3223 threading.Thread(target=thread_proc).start()
3225 return True
3227 def on_itemRemoveOldEpisodes_activate(self, widget):
3228 self.show_delete_episodes_window()
3230 def show_delete_episodes_window(self, channel=None):
3231 """Offer deletion of episodes
3233 If channel is None, offer deletion of all episodes.
3234 Otherwise only offer deletion of episodes in the channel.
3236 if gpodder.ui.maemo:
3237 columns = (
3238 ('maemo_remove_markup', None, None, _('Episode')),
3240 else:
3241 columns = (
3242 ('title_markup', None, None, _('Episode')),
3243 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3244 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3245 ('played_prop', None, None, _('Status')),
3246 ('age_prop', 'age_int_prop', gobject.TYPE_INT, _('Downloaded')),
3249 msg_older_than = N_('Select older than %(count)d day', 'Select older than %(count)d days', self.config.episode_old_age)
3250 selection_buttons = {
3251 _('Select played'): lambda episode: episode.is_played,
3252 _('Select finished'): lambda episode: episode.is_finished(),
3253 msg_older_than % {'count':self.config.episode_old_age}: lambda episode: episode.age_in_days() > self.config.episode_old_age,
3256 instructions = _('Select the episodes you want to delete:')
3258 if channel is None:
3259 channels = self.channels
3260 else:
3261 channels = [channel]
3263 episodes = []
3264 for channel in channels:
3265 for episode in channel.get_downloaded_episodes():
3266 # Disallow deletion of locked episodes that still exist
3267 if not episode.is_locked or not episode.file_exists():
3268 episodes.append(episode)
3270 selected = [e.is_played or not e.file_exists() for e in episodes]
3272 gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
3273 episodes = episodes, selected = selected, columns = columns, \
3274 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
3275 selection_buttons = selection_buttons, _config=self.config, \
3276 show_episode_shownotes=self.show_episode_shownotes)
3278 def on_selected_episodes_status_changed(self):
3279 # The order of the updates here is important! When "All episodes" is
3280 # selected, the update of the podcast list model depends on the episode
3281 # list selection to determine which podcasts are affected. Updating
3282 # the episode list could remove the selection if a filter is active.
3283 self.update_podcast_list_model(selected=True)
3284 self.update_episode_list_icons(selected=True)
3285 self.db.commit()
3287 def mark_selected_episodes_new(self):
3288 for episode in self.get_selected_episodes():
3289 episode.mark_new()
3290 self.on_selected_episodes_status_changed()
3292 def mark_selected_episodes_old(self):
3293 for episode in self.get_selected_episodes():
3294 episode.mark_old()
3295 self.on_selected_episodes_status_changed()
3297 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
3298 for episode in self.get_selected_episodes():
3299 if toggle:
3300 episode.mark(is_played=not episode.is_played)
3301 else:
3302 episode.mark(is_played=new_value)
3303 self.on_selected_episodes_status_changed()
3305 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3306 for episode in self.get_selected_episodes():
3307 if toggle:
3308 episode.mark(is_locked=not episode.is_locked)
3309 else:
3310 episode.mark(is_locked=new_value)
3311 self.on_selected_episodes_status_changed()
3313 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3314 if self.active_channel is None:
3315 return
3317 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
3318 self.active_channel.update_channel_lock()
3320 for episode in self.active_channel.get_all_episodes():
3321 episode.mark(is_locked=self.active_channel.channel_is_locked)
3323 self.update_podcast_list_model(selected=True)
3324 self.update_episode_list_icons(all=True)
3326 def on_itemUpdateChannel_activate(self, widget=None):
3327 if self.active_channel is None:
3328 title = _('No podcast selected')
3329 message = _('Please select a podcast in the podcasts list to update.')
3330 self.show_message( message, title, widget=self.treeChannels)
3331 return
3333 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3334 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3335 self.update_feed_cache()
3336 else:
3337 self.update_feed_cache(channels=[self.active_channel])
3339 def on_itemUpdate_activate(self, widget=None):
3340 # Check if we have outstanding subscribe/unsubscribe actions
3341 if self.on_add_remove_podcasts_mygpo():
3342 log('Update cancelled (received server changes)', sender=self)
3343 return
3345 if self.channels:
3346 self.update_feed_cache()
3347 else:
3348 gPodderWelcome(self.gPodder,
3349 center_on_widget=self.gPodder,
3350 show_example_podcasts_callback=self.on_itemImportChannels_activate,
3351 setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
3353 def download_episode_list_paused(self, episodes):
3354 self.download_episode_list(episodes, True)
3356 def download_episode_list(self, episodes, add_paused=False, force_start=False):
3357 enable_update = False
3359 for episode in episodes:
3360 log('Downloading episode: %s', episode.title, sender = self)
3361 if not episode.was_downloaded(and_exists=True):
3362 task_exists = False
3363 for task in self.download_tasks_seen:
3364 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
3365 self.download_queue_manager.add_task(task, force_start)
3366 enable_update = True
3367 task_exists = True
3368 continue
3370 if task_exists:
3371 continue
3373 try:
3374 task = download.DownloadTask(episode, self.config)
3375 except Exception, e:
3376 d = {'episode': episode.title, 'message': str(e)}
3377 message = _('Download error while downloading %(episode)s: %(message)s')
3378 self.show_message(message % d, _('Download error'), important=True)
3379 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
3380 continue
3382 if add_paused:
3383 task.status = task.PAUSED
3384 else:
3385 self.mygpo_client.on_download([task.episode])
3386 self.download_queue_manager.add_task(task, force_start)
3388 self.download_status_model.register_task(task)
3389 enable_update = True
3391 if enable_update:
3392 self.enable_download_list_update()
3394 # Flush updated episode status
3395 self.mygpo_client.flush()
3397 def cancel_task_list(self, tasks):
3398 if not tasks:
3399 return
3401 for task in tasks:
3402 if task.status in (task.QUEUED, task.DOWNLOADING):
3403 task.status = task.CANCELLED
3404 elif task.status == task.PAUSED:
3405 task.status = task.CANCELLED
3406 # Call run, so the partial file gets deleted
3407 task.run()
3409 self.update_episode_list_icons([task.url for task in tasks])
3410 self.play_or_download()
3412 # Update the tab title and downloads list
3413 self.update_downloads_list()
3415 def new_episodes_show(self, episodes, notification=False, selected=None):
3416 if gpodder.ui.maemo:
3417 columns = (
3418 ('maemo_markup', None, None, _('Episode')),
3420 show_notification = notification
3421 else:
3422 columns = (
3423 ('title_markup', None, None, _('Episode')),
3424 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3425 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3427 show_notification = False
3429 instructions = _('Select the episodes you want to download:')
3431 if self.new_episodes_window is not None:
3432 self.new_episodes_window.main_window.destroy()
3433 self.new_episodes_window = None
3435 def download_episodes_callback(episodes):
3436 self.new_episodes_window = None
3437 self.download_episode_list(episodes)
3439 if selected is None:
3440 # Select all by default
3441 selected = [True]*len(episodes)
3443 self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3444 title=_('New episodes available'), \
3445 instructions=instructions, \
3446 episodes=episodes, \
3447 columns=columns, \
3448 selected=selected, \
3449 stock_ok_button = 'gpodder-download', \
3450 callback=download_episodes_callback, \
3451 remove_callback=lambda e: e.mark_old(), \
3452 remove_action=_('Mark as old'), \
3453 remove_finished=self.episode_new_status_changed, \
3454 _config=self.config, \
3455 show_notification=show_notification, \
3456 show_episode_shownotes=self.show_episode_shownotes)
3458 def on_itemDownloadAllNew_activate(self, widget, *args):
3459 if not self.offer_new_episodes():
3460 self.show_message(_('Please check for new episodes later.'), \
3461 _('No new episodes available'), widget=self.btnUpdateFeeds)
3463 def get_new_episodes(self, channels=None):
3464 if channels is None:
3465 channels = self.channels
3466 episodes = []
3467 for channel in channels:
3468 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3469 episodes.append(episode)
3471 return episodes
3473 @dbus.service.method(gpodder.dbus_interface)
3474 def start_device_synchronization(self):
3475 """Public D-Bus API for starting Device sync (Desktop only)
3477 This method can be called to initiate a synchronization with
3478 a configured protable media player. This only works for the
3479 Desktop version of gPodder and does nothing on Maemo.
3481 if gpodder.ui.desktop:
3482 self.on_sync_to_ipod_activate(None)
3483 return True
3485 return False
3487 def on_sync_to_ipod_activate(self, widget, episodes=None):
3488 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
3490 def commit_changes_to_database(self):
3491 """This will be called after the sync process is finished"""
3492 self.db.commit()
3494 def on_cleanup_ipod_activate(self, widget, *args):
3495 self.sync_ui.on_cleanup_device()
3497 def on_manage_device_playlist(self, widget):
3498 self.sync_ui.on_manage_device_playlist()
3500 def show_hide_tray_icon(self):
3501 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3502 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3503 elif not self.config.display_tray_icon and self.tray_icon is not None:
3504 self.tray_icon.set_visible(False)
3505 del self.tray_icon
3506 self.tray_icon = None
3508 if self.config.minimize_to_tray and self.tray_icon:
3509 self.tray_icon.set_visible(self.is_iconified())
3510 elif self.tray_icon:
3511 self.tray_icon.set_visible(True)
3513 def on_itemShowAllEpisodes_activate(self, widget):
3514 self.config.podcast_list_view_all = widget.get_active()
3516 def on_itemShowToolbar_activate(self, widget):
3517 self.config.show_toolbar = self.itemShowToolbar.get_active()
3519 def on_itemShowDescription_activate(self, widget):
3520 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3522 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3523 self.config.podcast_list_hide_boring = toggleaction.get_active()
3524 if self.config.podcast_list_hide_boring:
3525 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3526 else:
3527 self.podcast_list_model.set_view_mode(-1)
3529 def on_item_view_podcasts_changed(self, radioaction, current):
3530 # Only on Fremantle
3531 if current == self.item_view_podcasts_all:
3532 self.podcast_list_model.set_view_mode(-1)
3533 elif current == self.item_view_podcasts_downloaded:
3534 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3535 elif current == self.item_view_podcasts_unplayed:
3536 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3538 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3540 def on_item_view_episodes_changed(self, radioaction, current):
3541 if current == self.item_view_episodes_all:
3542 self.config.episode_list_view_mode = EpisodeListModel.VIEW_ALL
3543 elif current == self.item_view_episodes_undeleted:
3544 self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNDELETED
3545 elif current == self.item_view_episodes_downloaded:
3546 self.config.episode_list_view_mode = EpisodeListModel.VIEW_DOWNLOADED
3547 elif current == self.item_view_episodes_unplayed:
3548 self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNPLAYED
3550 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
3552 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3553 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3555 def update_item_device( self):
3556 if not gpodder.ui.fremantle:
3557 if self.config.device_type != 'none':
3558 self.itemDevice.set_visible(True)
3559 self.itemDevice.label = self.get_device_name()
3560 else:
3561 self.itemDevice.set_visible(False)
3563 def properties_closed( self):
3564 self.preferences_dialog = None
3565 self.show_hide_tray_icon()
3566 self.update_item_device()
3567 if gpodder.ui.maemo:
3568 selection = self.treeAvailable.get_selection()
3569 if self.config.maemo_enable_gestures or \
3570 self.config.enable_fingerscroll:
3571 selection.set_mode(gtk.SELECTION_SINGLE)
3572 else:
3573 selection.set_mode(gtk.SELECTION_MULTIPLE)
3575 def on_itemPreferences_activate(self, widget, *args):
3576 self.preferences_dialog = gPodderPreferences(self.main_window, \
3577 _config=self.config, \
3578 callback_finished=self.properties_closed, \
3579 user_apps_reader=self.user_apps_reader, \
3580 parent_window=self.main_window, \
3581 mygpo_client=self.mygpo_client, \
3582 on_send_full_subscriptions=self.on_send_full_subscriptions)
3584 # Initial message to relayout window (in case it's opened in portrait mode
3585 self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3587 def on_itemDependencies_activate(self, widget):
3588 gPodderDependencyManager(self.gPodder)
3590 def on_goto_mygpo(self, widget):
3591 self.mygpo_client.open_website()
3593 def on_download_subscriptions_from_mygpo(self, action=None):
3594 title = _('Login to gpodder.net')
3595 message = _('Please login to download your subscriptions.')
3596 success, (username, password) = self.show_login_dialog(title, message, \
3597 self.config.mygpo_username, self.config.mygpo_password)
3598 if not success:
3599 return
3601 self.config.mygpo_username = username
3602 self.config.mygpo_password = password
3604 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3605 custom_title=_('Subscriptions on gpodder.net'), \
3606 add_urls_callback=self.add_podcast_list, \
3607 hide_url_entry=True)
3609 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3610 # we do not have to hardcode the URL here
3611 OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
3612 url = util.url_add_authentication(OPML_URL, \
3613 self.config.mygpo_username, \
3614 self.config.mygpo_password)
3615 dir.download_opml_file(url)
3617 def on_mygpo_settings_activate(self, action=None):
3618 # This dialog is only used for Maemo 4
3619 if not gpodder.ui.diablo:
3620 return
3622 settings = MygPodderSettings(self.main_window, \
3623 config=self.config, \
3624 mygpo_client=self.mygpo_client, \
3625 on_send_full_subscriptions=self.on_send_full_subscriptions)
3627 def on_itemAddChannel_activate(self, widget=None):
3628 gPodderAddPodcast(self.gPodder, \
3629 add_urls_callback=self.add_podcast_list)
3631 def on_itemEditChannel_activate(self, widget, *args):
3632 if self.active_channel is None:
3633 title = _('No podcast selected')
3634 message = _('Please select a podcast in the podcasts list to edit.')
3635 self.show_message( message, title, widget=self.treeChannels)
3636 return
3638 callback_closed = lambda: self.update_podcast_list_model(selected=True)
3639 gPodderChannel(self.main_window, \
3640 channel=self.active_channel, \
3641 callback_closed=callback_closed, \
3642 cover_downloader=self.cover_downloader)
3644 def on_itemMassUnsubscribe_activate(self, item=None):
3645 columns = (
3646 ('title', None, None, _('Podcast')),
3649 # We're abusing the Episode Selector for selecting Podcasts here,
3650 # but it works and looks good, so why not? -- thp
3651 gPodderEpisodeSelector(self.main_window, \
3652 title=_('Remove podcasts'), \
3653 instructions=_('Select the podcast you want to remove.'), \
3654 episodes=self.channels, \
3655 columns=columns, \
3656 size_attribute=None, \
3657 stock_ok_button=_('Remove'), \
3658 callback=self.remove_podcast_list, \
3659 _config=self.config)
3661 def remove_podcast_list(self, channels, confirm=True):
3662 if not channels:
3663 log('No podcasts selected for deletion', sender=self)
3664 return
3666 if len(channels) == 1:
3667 title = _('Removing podcast')
3668 info = _('Please wait while the podcast is removed')
3669 message = _('Do you really want to remove this podcast and its episodes?')
3670 else:
3671 title = _('Removing podcasts')
3672 info = _('Please wait while the podcasts are removed')
3673 message = _('Do you really want to remove the selected podcasts and their episodes?')
3675 if confirm and not self.show_confirmation(message, title):
3676 return
3678 progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3680 def finish_deletion(select_url):
3681 # Upload subscription list changes to the web service
3682 self.mygpo_client.on_unsubscribe([c.url for c in channels])
3684 # Re-load the channels and select the desired new channel
3685 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3686 progress.on_finished()
3687 self.update_podcasts_tab()
3689 def thread_proc():
3690 select_url = None
3692 for idx, channel in enumerate(channels):
3693 # Update the UI for correct status messages
3694 progress.on_progress(float(idx)/float(len(channels)))
3695 progress.on_message(channel.title)
3697 # Delete downloaded episodes
3698 channel.remove_downloaded()
3700 # cancel any active downloads from this channel
3701 for episode in channel.get_all_episodes():
3702 util.idle_add(self.download_status_model.cancel_by_url,
3703 episode.url)
3705 if len(channels) == 1:
3706 # get the URL of the podcast we want to select next
3707 if channel in self.channels:
3708 position = self.channels.index(channel)
3709 else:
3710 position = -1
3712 if position == len(self.channels)-1:
3713 # this is the last podcast, so select the URL
3714 # of the item before this one (i.e. the "new last")
3715 select_url = self.channels[position-1].url
3716 else:
3717 # there is a podcast after the deleted one, so
3718 # we simply select the one that comes after it
3719 select_url = self.channels[position+1].url
3721 # Remove the channel and clean the database entries
3722 channel.delete()
3723 self.channels.remove(channel)
3725 # Clean up downloads and download directories
3726 self.clean_up_downloads()
3728 self.channel_list_changed = True
3729 self.save_channels_opml()
3731 # The remaining stuff is to be done in the GTK main thread
3732 util.idle_add(finish_deletion, select_url)
3734 threading.Thread(target=thread_proc).start()
3736 def on_itemRemoveChannel_activate(self, widget, *args):
3737 if self.active_channel is None:
3738 title = _('No podcast selected')
3739 message = _('Please select a podcast in the podcasts list to remove.')
3740 self.show_message( message, title, widget=self.treeChannels)
3741 return
3743 self.remove_podcast_list([self.active_channel])
3745 def get_opml_filter(self):
3746 filter = gtk.FileFilter()
3747 filter.add_pattern('*.opml')
3748 filter.add_pattern('*.xml')
3749 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3750 return filter
3752 def on_item_import_from_file_activate(self, widget, filename=None):
3753 if filename is None:
3754 if gpodder.ui.desktop or gpodder.ui.fremantle:
3755 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), \
3756 parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3757 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3758 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3759 elif gpodder.ui.diablo:
3760 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3761 dlg.set_filter(self.get_opml_filter())
3762 response = dlg.run()
3763 filename = None
3764 if response == gtk.RESPONSE_OK:
3765 filename = dlg.get_filename()
3766 dlg.destroy()
3768 if filename is not None:
3769 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3770 custom_title=_('Import podcasts from OPML file'), \
3771 add_urls_callback=self.add_podcast_list, \
3772 hide_url_entry=True)
3773 dir.download_opml_file(filename)
3775 def on_itemExportChannels_activate(self, widget, *args):
3776 if not self.channels:
3777 title = _('Nothing to export')
3778 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3779 self.show_message(message, title, widget=self.treeChannels)
3780 return
3782 if gpodder.ui.desktop or gpodder.ui.fremantle:
3783 # FIXME: Hildonization on Fremantle
3784 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3785 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3786 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3787 elif gpodder.ui.diablo:
3788 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3789 dlg.set_filter(self.get_opml_filter())
3790 response = dlg.run()
3791 if response == gtk.RESPONSE_OK:
3792 filename = dlg.get_filename()
3793 dlg.destroy()
3794 exporter = opml.Exporter( filename)
3795 if exporter.write(self.channels):
3796 count = len(self.channels)
3797 title = N_('%(count)d subscription exported', '%(count)d subscriptions exported', count) % {'count':count}
3798 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3799 else:
3800 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3801 else:
3802 dlg.destroy()
3804 def on_itemImportChannels_activate(self, widget, *args):
3805 if gpodder.ui.fremantle:
3806 gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3807 self.config.toplist_url, \
3808 self.config.opml_url, \
3809 self.add_podcast_list, \
3810 self.on_itemAddChannel_activate, \
3811 self.on_download_subscriptions_from_mygpo, \
3812 self.show_text_edit_dialog)
3813 else:
3814 dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3815 add_urls_callback=self.add_podcast_list)
3816 util.idle_add(dir.download_opml_file, self.config.opml_url)
3818 def on_homepage_activate(self, widget, *args):
3819 util.open_website(gpodder.__url__)
3821 def on_wiki_activate(self, widget, *args):
3822 util.open_website('http://gpodder.org/wiki/User_Manual')
3824 def on_bug_tracker_activate(self, widget, *args):
3825 if gpodder.ui.maemo:
3826 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3827 else:
3828 util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3830 def on_item_support_activate(self, widget):
3831 util.open_website('http://gpodder.org/donate')
3833 def on_itemAbout_activate(self, widget, *args):
3834 if gpodder.ui.fremantle:
3835 from gpodder.gtkui.frmntl.about import HeAboutDialog
3836 HeAboutDialog.present(self.main_window,
3837 'gPodder',
3838 'gpodder',
3839 gpodder.__version__,
3840 _('A podcast client with focus on usability'),
3841 gpodder.__copyright__,
3842 gpodder.__url__,
3843 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3844 'http://gpodder.org/donate')
3845 return
3847 dlg = gtk.AboutDialog()
3848 dlg.set_transient_for(self.main_window)
3849 dlg.set_name('gPodder')
3850 dlg.set_version(gpodder.__version__)
3851 dlg.set_copyright(gpodder.__copyright__)
3852 dlg.set_comments(_('A podcast client with focus on usability'))
3853 dlg.set_website(gpodder.__url__)
3854 dlg.set_translator_credits( _('translator-credits'))
3855 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3857 if gpodder.ui.desktop:
3858 # For the "GUI" version, we add some more
3859 # items to the about dialog (credits and logo)
3860 app_authors = [
3861 _('Maintainer:'),
3862 'Thomas Perl <thp.io>',
3865 if os.path.exists(gpodder.credits_file):
3866 credits = open(gpodder.credits_file).read().strip().split('\n')
3867 app_authors += ['', _('Patches, bug reports and donations by:')]
3868 app_authors += credits
3870 dlg.set_authors(app_authors)
3871 try:
3872 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3873 except:
3874 dlg.set_logo_icon_name('gpodder')
3876 dlg.run()
3878 def on_wNotebook_switch_page(self, widget, *args):
3879 page_num = args[1]
3880 if gpodder.ui.maemo:
3881 self.tool_downloads.set_active(page_num == 1)
3882 page = self.wNotebook.get_nth_page(page_num)
3883 tab_label = self.wNotebook.get_tab_label(page).get_text()
3884 if page_num == 0 and self.active_channel is not None:
3885 self.set_title(self.active_channel.title)
3886 else:
3887 self.set_title(tab_label)
3888 if page_num == 0:
3889 self.play_or_download()
3890 self.menuChannels.set_sensitive(True)
3891 self.menuSubscriptions.set_sensitive(True)
3892 # The message area in the downloads tab should be hidden
3893 # when the user switches away from the downloads tab
3894 if self.message_area is not None:
3895 self.message_area.hide()
3896 self.message_area = None
3897 else:
3898 self.menuChannels.set_sensitive(False)
3899 self.menuSubscriptions.set_sensitive(False)
3900 if gpodder.ui.desktop:
3901 self.toolDownload.set_sensitive(False)
3902 self.toolPlay.set_sensitive(False)
3903 self.toolTransfer.set_sensitive(False)
3904 self.toolCancel.set_sensitive(False)
3906 def on_treeChannels_row_activated(self, widget, path, *args):
3907 # double-click action of the podcast list or enter
3908 self.treeChannels.set_cursor(path)
3910 def on_treeChannels_cursor_changed(self, widget, *args):
3911 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3913 if model is not None and iter is not None:
3914 old_active_channel = self.active_channel
3915 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3917 if self.active_channel == old_active_channel:
3918 return
3920 if gpodder.ui.maemo:
3921 self.set_title(self.active_channel.title)
3923 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3924 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3925 self.itemEditChannel.set_visible(False)
3926 self.itemRemoveChannel.set_visible(False)
3927 else:
3928 self.itemEditChannel.set_visible(True)
3929 self.itemRemoveChannel.set_visible(True)
3930 else:
3931 self.active_channel = None
3932 self.itemEditChannel.set_visible(False)
3933 self.itemRemoveChannel.set_visible(False)
3935 self.update_episode_list_model()
3937 def on_btnEditChannel_clicked(self, widget, *args):
3938 self.on_itemEditChannel_activate( widget, args)
3940 def get_podcast_urls_from_selected_episodes(self):
3941 """Get a set of podcast URLs based on the selected episodes"""
3942 return set(episode.channel.url for episode in \
3943 self.get_selected_episodes())
3945 def get_selected_episodes(self):
3946 """Get a list of selected episodes from treeAvailable"""
3947 selection = self.treeAvailable.get_selection()
3948 model, paths = selection.get_selected_rows()
3950 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3951 return episodes
3953 def on_transfer_selected_episodes(self, widget):
3954 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3956 def on_playback_selected_episodes(self, widget):
3957 self.playback_episodes(self.get_selected_episodes())
3959 def on_shownotes_selected_episodes(self, widget):
3960 episodes = self.get_selected_episodes()
3961 if episodes:
3962 episode = episodes.pop(0)
3963 self.show_episode_shownotes(episode)
3964 else:
3965 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3967 def on_download_selected_episodes(self, widget):
3968 episodes = self.get_selected_episodes()
3969 self.download_episode_list(episodes)
3970 self.update_episode_list_icons([episode.url for episode in episodes])
3971 self.play_or_download()
3973 def on_treeAvailable_row_activated(self, widget, path, view_column):
3974 """Double-click/enter action handler for treeAvailable"""
3975 # We should only have one one selected as it was double clicked!
3976 e = self.get_selected_episodes()[0]
3978 if (self.config.double_click_episode_action == 'download'):
3979 # If the episode has already been downloaded and exists then play it
3980 if e.was_downloaded(and_exists=True):
3981 self.playback_episodes(self.get_selected_episodes())
3982 # else download it if it is not already downloading
3983 elif not self.episode_is_downloading(e):
3984 self.download_episode_list([e])
3985 self.update_episode_list_icons([e.url])
3986 self.play_or_download()
3987 elif (self.config.double_click_episode_action == 'stream'):
3988 # If we happen to have downloaded this episode simple play it
3989 if e.was_downloaded(and_exists=True):
3990 self.playback_episodes(self.get_selected_episodes())
3991 # else if streaming is possible stream it
3992 elif self.streaming_possible():
3993 self.playback_episodes(self.get_selected_episodes())
3994 else:
3995 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3996 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3997 else:
3998 # default action is to display show notes
3999 self.on_shownotes_selected_episodes(widget)
4001 def show_episode_shownotes(self, episode):
4002 if self.episode_shownotes_window is None:
4003 log('First-time use of episode window --- creating', sender=self)
4004 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
4005 _download_episode_list=self.download_episode_list, \
4006 _playback_episodes=self.playback_episodes, \
4007 _delete_episode_list=self.delete_episode_list, \
4008 _episode_list_status_changed=self.episode_list_status_changed, \
4009 _cancel_task_list=self.cancel_task_list, \
4010 _episode_is_downloading=self.episode_is_downloading, \
4011 _streaming_possible=self.streaming_possible())
4012 self.episode_shownotes_window.show(episode)
4013 if self.episode_is_downloading(episode):
4014 self.update_downloads_list()
4016 def restart_auto_update_timer(self):
4017 if self._auto_update_timer_source_id is not None:
4018 log('Removing existing auto update timer.', sender=self)
4019 gobject.source_remove(self._auto_update_timer_source_id)
4020 self._auto_update_timer_source_id = None
4022 if self.config.auto_update_feeds and \
4023 self.config.auto_update_frequency:
4024 interval = 60*1000*self.config.auto_update_frequency
4025 log('Setting up auto update timer with interval %d.', \
4026 self.config.auto_update_frequency, sender=self)
4027 self._auto_update_timer_source_id = gobject.timeout_add(\
4028 interval, self._on_auto_update_timer)
4030 def _on_auto_update_timer(self):
4031 log('Auto update timer fired.', sender=self)
4032 self.update_feed_cache(force_update=True)
4034 # Ask web service for sub changes (if enabled)
4035 self.mygpo_client.flush()
4037 return True
4039 def on_treeDownloads_row_activated(self, widget, *args):
4040 # Use the standard way of working on the treeview
4041 selection = self.treeDownloads.get_selection()
4042 (model, paths) = selection.get_selected_rows()
4043 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
4045 for tree_row_reference, task in selected_tasks:
4046 if task.status in (task.DOWNLOADING, task.QUEUED):
4047 task.status = task.PAUSED
4048 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
4049 self.download_queue_manager.add_task(task)
4050 self.enable_download_list_update()
4051 elif task.status == task.DONE:
4052 model.remove(model.get_iter(tree_row_reference.get_path()))
4054 self.play_or_download()
4056 # Update the tab title and downloads list
4057 self.update_downloads_list()
4059 def on_item_cancel_download_activate(self, widget):
4060 if self.wNotebook.get_current_page() == 0:
4061 selection = self.treeAvailable.get_selection()
4062 (model, paths) = selection.get_selected_rows()
4063 urls = [model.get_value(model.get_iter(path), \
4064 self.episode_list_model.C_URL) for path in paths]
4065 selected_tasks = [task for task in self.download_tasks_seen \
4066 if task.url in urls]
4067 else:
4068 selection = self.treeDownloads.get_selection()
4069 (model, paths) = selection.get_selected_rows()
4070 selected_tasks = [model.get_value(model.get_iter(path), \
4071 self.download_status_model.C_TASK) for path in paths]
4072 self.cancel_task_list(selected_tasks)
4074 def on_btnCancelAll_clicked(self, widget, *args):
4075 self.cancel_task_list(self.download_tasks_seen)
4077 def on_btnDownloadedDelete_clicked(self, widget, *args):
4078 episodes = self.get_selected_episodes()
4079 if len(episodes) == 1:
4080 self.delete_episode_list(episodes, skip_locked=False)
4081 else:
4082 self.delete_episode_list(episodes)
4084 def on_key_press(self, widget, event):
4085 # Allow tab switching with Ctrl + PgUp/PgDown
4086 if event.state & gtk.gdk.CONTROL_MASK:
4087 if event.keyval == gtk.keysyms.Page_Up:
4088 self.wNotebook.prev_page()
4089 return True
4090 elif event.keyval == gtk.keysyms.Page_Down:
4091 self.wNotebook.next_page()
4092 return True
4094 # After this code we only handle Maemo hardware keys,
4095 # so if we are not a Maemo app, we don't do anything
4096 if not gpodder.ui.maemo:
4097 return False
4099 diff = 0
4100 if event.keyval == gtk.keysyms.F7: #plus
4101 diff = 1
4102 elif event.keyval == gtk.keysyms.F8: #minus
4103 diff = -1
4105 if diff != 0 and not self.currently_updating:
4106 selection = self.treeChannels.get_selection()
4107 (model, iter) = selection.get_selected()
4108 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
4109 selection.select_path(new_path)
4110 self.treeChannels.set_cursor(new_path)
4111 return True
4113 return False
4115 def on_iconify(self):
4116 if self.tray_icon:
4117 self.gPodder.set_skip_taskbar_hint(True)
4118 if self.config.minimize_to_tray:
4119 self.tray_icon.set_visible(True)
4120 else:
4121 self.gPodder.set_skip_taskbar_hint(False)
4123 def on_uniconify(self):
4124 if self.tray_icon:
4125 self.gPodder.set_skip_taskbar_hint(False)
4126 if self.config.minimize_to_tray:
4127 self.tray_icon.set_visible(False)
4128 else:
4129 self.gPodder.set_skip_taskbar_hint(False)
4131 def uniconify_main_window(self):
4132 if self.is_iconified():
4133 # We need to hide and then show the window in WMs like Metacity
4134 # or KWin4 to move the window to the active workspace
4135 # (see http://gpodder.org/bug/1125)
4136 self.gPodder.hide()
4137 self.gPodder.show()
4138 self.gPodder.present()
4140 def iconify_main_window(self):
4141 if not self.is_iconified():
4142 self.gPodder.iconify()
4144 def update_podcasts_tab(self):
4145 if len(self.channels):
4146 if gpodder.ui.fremantle:
4147 self.button_refresh.set_title(_('Check for new episodes'))
4148 self.button_refresh.show()
4149 else:
4150 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
4151 else:
4152 if gpodder.ui.fremantle:
4153 self.button_refresh.hide()
4154 else:
4155 self.label2.set_text(_('Podcasts'))
4157 @dbus.service.method(gpodder.dbus_interface)
4158 def show_gui_window(self):
4159 parent = self.get_dialog_parent()
4160 parent.present()
4162 @dbus.service.method(gpodder.dbus_interface)
4163 def subscribe_to_url(self, url):
4164 gPodderAddPodcast(self.gPodder,
4165 add_urls_callback=self.add_podcast_list,
4166 preset_url=url)
4168 @dbus.service.method(gpodder.dbus_interface)
4169 def mark_episode_played(self, filename):
4170 if filename is None:
4171 return False
4173 for channel in self.channels:
4174 for episode in channel.get_all_episodes():
4175 fn = episode.local_filename(create=False, check_only=True)
4176 if fn == filename:
4177 episode.mark(is_played=True)
4178 self.db.commit()
4179 self.update_episode_list_icons([episode.url])
4180 self.update_podcast_list_model([episode.channel.url])
4181 return True
4183 return False
4186 def main(options=None):
4187 gobject.threads_init()
4188 gobject.set_application_name('gPodder')
4190 if gpodder.ui.maemo:
4191 # Try to enable the custom icon theme for gPodder on Maemo
4192 settings = gtk.settings_get_default()
4193 settings.set_string_property('gtk-icon-theme-name', \
4194 'gpodder', __file__)
4195 # Extend the search path for the optified icon theme (Maemo 5)
4196 icon_theme = gtk.icon_theme_get_default()
4197 icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
4199 gtk.window_set_default_icon_name('gpodder')
4200 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
4202 try:
4203 dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
4204 gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
4206 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
4207 except dbus.exceptions.DBusException, dbe:
4208 log('Warning: Cannot get "on the bus".', traceback=True)
4209 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
4210 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
4211 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
4212 dlg.set_title('gPodder')
4213 dlg.run()
4214 dlg.destroy()
4215 sys.exit(0)
4217 util.make_directory(gpodder.home)
4218 gpodder.load_plugins()
4220 config = UIConfig(gpodder.config_file)
4222 # Load hook modules and install the hook manager globally
4223 # if modules have been found an instantiated by the manager
4224 user_hooks = hooks.HookManager()
4225 if user_hooks.has_modules():
4226 gpodder.user_hooks = user_hooks
4228 if gpodder.ui.diablo:
4229 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4230 # folder exists there (allow moving "gpodder" between SD cards or USB)
4231 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4232 if not os.path.exists(config.download_dir):
4233 log('Downloads might have been moved. Trying to locate them...')
4234 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
4235 dir = os.path.join(basedir, 'gpodder')
4236 if os.path.exists(dir):
4237 log('Downloads found in: %s', dir)
4238 config.download_dir = dir
4239 break
4240 else:
4241 log('Downloads NOT FOUND in %s', dir)
4243 if config.enable_fingerscroll:
4244 BuilderWidget.use_fingerscroll = True
4246 config.mygpo_device_type = util.detect_device_type()
4248 gp = gPodder(bus_name, config)
4250 # Handle options
4251 if options.subscribe:
4252 util.idle_add(gp.subscribe_to_url, options.subscribe)
4254 # mac OS X stuff :
4255 # handle "subscribe to podcast" events from firefox
4256 if platform.system() == 'Darwin':
4257 from gpodder import gpodderosx
4258 gpodderosx.register_handlers(gp)
4259 # end mac OS X stuff
4261 gp.run()