Use dict-based format strings for numbers (bug 1165)
[gpodder.git] / src / gpodder / gui.py
blob1b27db276b266c37a6ee395f20a5ad212ace9906
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
36 from xml.sax import saxutils
38 import gpodder
40 try:
41 import dbus
42 import dbus.service
43 import dbus.mainloop
44 import dbus.glib
45 except ImportError:
46 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
47 class dbus:
48 class SessionBus:
49 def __init__(self, *args, **kwargs):
50 pass
51 def add_signal_receiver(self, *args, **kwargs):
52 pass
53 class glib:
54 class DBusGMainLoop:
55 def __init__(self, *args, **kwargs):
56 pass
57 class service:
58 @staticmethod
59 def method(*args, **kwargs):
60 return lambda x: x
61 class BusName:
62 def __init__(self, *args, **kwargs):
63 pass
64 class Object:
65 def __init__(self, *args, **kwargs):
66 pass
69 from gpodder import feedcore
70 from gpodder import util
71 from gpodder import opml
72 from gpodder import download
73 from gpodder import my
74 from gpodder import youtube
75 from gpodder import player
76 from gpodder.liblogger import log
78 _ = gpodder.gettext
79 N_ = gpodder.ngettext
81 from gpodder.model import PodcastChannel
82 from gpodder.model import PodcastEpisode
83 from gpodder.dbsqlite import Database
85 from gpodder.gtkui.model import PodcastListModel
86 from gpodder.gtkui.model import EpisodeListModel
87 from gpodder.gtkui.config import UIConfig
88 from gpodder.gtkui.services import CoverDownloader
89 from gpodder.gtkui.widgets import SimpleMessageArea
90 from gpodder.gtkui.desktopfile import UserAppsReader
92 from gpodder.gtkui.draw import draw_text_box_centered
94 from gpodder.gtkui.interface.common import BuilderWidget
95 from gpodder.gtkui.interface.common import TreeViewHelper
96 from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
98 if gpodder.ui.desktop:
99 from gpodder.gtkui.download import DownloadStatusModel
101 from gpodder.gtkui.desktop.sync import gPodderSyncUI
103 from gpodder.gtkui.desktop.channel import gPodderChannel
104 from gpodder.gtkui.desktop.preferences import gPodderPreferences
105 from gpodder.gtkui.desktop.shownotes import gPodderShownotes
106 from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
107 from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
108 from gpodder.gtkui.desktop.dependencymanager import gPodderDependencyManager
109 from gpodder.gtkui.interface.progress import ProgressIndicator
110 try:
111 from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
112 have_trayicon = True
113 except Exception, exc:
114 log('Warning: Could not import gpodder.trayicon.', traceback=True)
115 log('Warning: This probably means your PyGTK installation is too old!')
116 have_trayicon = False
117 elif gpodder.ui.diablo:
118 from gpodder.gtkui.download import DownloadStatusModel
120 from gpodder.gtkui.maemo.channel import gPodderChannel
121 from gpodder.gtkui.maemo.preferences import gPodderPreferences
122 from gpodder.gtkui.maemo.shownotes import gPodderShownotes
123 from gpodder.gtkui.maemo.episodeselector import gPodderEpisodeSelector
124 from gpodder.gtkui.maemo.podcastdirectory import gPodderPodcastDirectory
125 from gpodder.gtkui.maemo.mygpodder import MygPodderSettings
126 from gpodder.gtkui.interface.progress import ProgressIndicator
127 have_trayicon = False
128 elif gpodder.ui.fremantle:
129 from gpodder.gtkui.frmntl.model import DownloadStatusModel
130 from gpodder.gtkui.frmntl.model import EpisodeListModel
131 from gpodder.gtkui.frmntl.model import PodcastListModel
133 from gpodder.gtkui.maemo.channel import gPodderChannel
134 from gpodder.gtkui.frmntl.preferences import gPodderPreferences
135 from gpodder.gtkui.frmntl.shownotes import gPodderShownotes
136 from gpodder.gtkui.frmntl.episodeselector import gPodderEpisodeSelector
137 from gpodder.gtkui.frmntl.podcastdirectory import gPodderPodcastDirectory
138 from gpodder.gtkui.frmntl.episodes import gPodderEpisodes
139 from gpodder.gtkui.frmntl.downloads import gPodderDownloads
140 from gpodder.gtkui.frmntl.progress import ProgressIndicator
141 from gpodder.gtkui.frmntl.widgets import FancyProgressBar
142 have_trayicon = False
144 from gpodder.gtkui.frmntl.portrait import FremantleRotation
145 from gpodder.gtkui.frmntl.mafw import MafwPlaybackMonitor
146 from gpodder.gtkui.frmntl.hints import HINT_STRINGS
148 from gpodder.gtkui.interface.common import Orientation
150 from gpodder.gtkui.interface.welcome import gPodderWelcome
152 if gpodder.ui.maemo:
153 import hildon
155 from gpodder.dbusproxy import DBusPodcastsProxy
156 from gpodder import hooks
158 class gPodder(BuilderWidget, dbus.service.Object):
159 finger_friendly_widgets = ['btnCleanUpDownloads', 'button_search_episodes_clear', 'label2', 'labelDownloads', 'btnUpdateFeeds']
161 ICON_GENERAL_ADD = 'general_add'
162 ICON_GENERAL_REFRESH = 'general_refresh'
164 # Delay until live search is started after typing stop
165 LIVE_SEARCH_DELAY = 200
167 def __init__(self, bus_name, config):
168 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
169 self.podcasts_proxy = DBusPodcastsProxy(lambda: self.channels, \
170 self.on_itemUpdate_activate, \
171 self.playback_episodes, \
172 self.download_episode_list, \
173 self.episode_object_by_uri, \
174 bus_name)
175 self.db = Database(gpodder.database_file)
176 self.config = config
177 BuilderWidget.__init__(self, None)
179 def new(self):
180 if gpodder.ui.diablo:
181 import hildon
182 self.app = hildon.Program()
183 self.app.add_window(self.main_window)
184 self.main_window.add_toolbar(self.toolbar)
185 menu = gtk.Menu()
186 for child in self.main_menu.get_children():
187 child.reparent(menu)
188 self.main_window.set_menu(self.set_finger_friendly(menu))
189 self._last_orientation = Orientation.LANDSCAPE
190 elif gpodder.ui.fremantle:
191 import hildon
192 self.app = hildon.Program()
193 self.app.add_window(self.main_window)
195 appmenu = hildon.AppMenu()
197 for filter in (self.item_view_podcasts_all, \
198 self.item_view_podcasts_downloaded, \
199 self.item_view_podcasts_unplayed):
200 button = gtk.ToggleButton()
201 filter.connect_proxy(button)
202 appmenu.add_filter(button)
204 for action in (self.itemPreferences, \
205 self.item_downloads, \
206 self.itemRemoveOldEpisodes, \
207 self.item_unsubscribe, \
208 self.itemAbout):
209 button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
210 hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
211 action.connect_proxy(button)
212 if action == self.item_downloads:
213 button.set_title(_('Downloads'))
214 button.set_value(_('Idle'))
215 self.button_downloads = button
216 appmenu.append(button)
218 def show_hint(button):
219 self.show_message(random.choice(HINT_STRINGS), important=True)
221 button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
222 hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
223 button.set_title(_('Hint of the day'))
224 button.connect('clicked', show_hint)
225 appmenu.append(button)
227 appmenu.show_all()
228 self.main_window.set_app_menu(appmenu)
230 # Initialize portrait mode / rotation manager
231 self._fremantle_rotation = FremantleRotation('gPodder', \
232 self.main_window, \
233 gpodder.__version__, \
234 self.config.rotation_mode)
236 if self.config.rotation_mode == FremantleRotation.ALWAYS:
237 util.idle_add(self.on_window_orientation_changed, \
238 Orientation.PORTRAIT)
239 self._last_orientation = Orientation.PORTRAIT
240 else:
241 self._last_orientation = Orientation.LANDSCAPE
243 # Flag set when a notification is being shown (Maemo bug 11235)
244 self._fremantle_notification_visible = False
245 else:
246 self._last_orientation = Orientation.LANDSCAPE
247 self.toolbar.set_property('visible', self.config.show_toolbar)
249 self.bluetooth_available = util.bluetooth_available()
251 self.config.connect_gtk_window(self.gPodder, 'main_window')
252 if not gpodder.ui.fremantle:
253 self.config.connect_gtk_paned('paned_position', self.channelPaned)
254 self.main_window.show()
256 self.player_receiver = player.MediaPlayerDBusReceiver(self.on_played)
258 if gpodder.ui.fremantle:
259 # Create a D-Bus monitoring object that takes care of
260 # tracking MAFW (Nokia Media Player) playback events
261 # and sends episode playback status events via D-Bus
262 self.mafw_monitor = MafwPlaybackMonitor(gpodder.dbus_session_bus)
264 self.gPodder.connect('key-press-event', self.on_key_press)
266 self.preferences_dialog = None
267 self.config.add_observer(self.on_config_changed)
269 self.tray_icon = None
270 self.episode_shownotes_window = None
271 self.new_episodes_window = None
273 if gpodder.ui.desktop:
274 # Mac OS X-specific UI tweaks: Native main menu integration
275 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
276 if getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
277 try:
278 import igemacintegration as igemi
280 # Move the menu bar from the window to the Mac menu bar
281 self.mainMenu.hide()
282 igemi.ige_mac_menu_set_menu_bar(self.mainMenu)
284 # Reparent some items to the "Application" menu
285 for widget in ('/mainMenu/menuHelp/itemAbout', \
286 '/mainMenu/menuPodcasts/itemPreferences'):
287 item = self.uimanager1.get_widget(widget)
288 group = igemi.ige_mac_menu_add_app_menu_group()
289 igemi.ige_mac_menu_add_app_menu_item(group, item, None)
291 quit_widget = '/mainMenu/menuPodcasts/itemQuit'
292 quit_item = self.uimanager1.get_widget(quit_widget)
293 igemi.ige_mac_menu_set_quit_menu_item(quit_item)
294 except ImportError:
295 print >>sys.stderr, """
296 Warning: ige-mac-integration not found - no native menus.
299 self.sync_ui = gPodderSyncUI(self.config, self.notification, \
300 self.main_window, self.show_confirmation, \
301 self.update_episode_list_icons, \
302 self.update_podcast_list_model, self.toolPreferences, \
303 gPodderEpisodeSelector, \
304 self.commit_changes_to_database)
305 else:
306 self.sync_ui = None
308 self.download_status_model = DownloadStatusModel()
309 self.download_queue_manager = download.DownloadQueueManager(self.config)
311 if gpodder.ui.desktop:
312 self.show_hide_tray_icon()
313 self.itemShowAllEpisodes.set_active(self.config.podcast_list_view_all)
314 self.itemShowToolbar.set_active(self.config.show_toolbar)
315 self.itemShowDescription.set_active(self.config.episode_list_descriptions)
317 if not gpodder.ui.fremantle:
318 self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
319 self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
320 self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
321 self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
323 # When the amount of maximum downloads changes, notify the queue manager
324 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_threads()
325 self.spinMaxDownloads.connect('value-changed', changed_cb)
327 self.default_title = 'gPodder'
328 if gpodder.__version__.rfind('git') != -1:
329 self.set_title('gPodder %s' % gpodder.__version__)
330 else:
331 title = self.gPodder.get_title()
332 if title is not None:
333 self.set_title(title)
334 else:
335 self.set_title(_('gPodder'))
337 self.cover_downloader = CoverDownloader()
339 # Generate list models for podcasts and their episodes
340 self.podcast_list_model = PodcastListModel(self.cover_downloader)
342 self.cover_downloader.register('cover-available', self.cover_download_finished)
343 self.cover_downloader.register('cover-removed', self.cover_file_removed)
345 if gpodder.ui.fremantle:
346 # Work around Maemo bug #4718
347 self.button_refresh.set_name('HildonButton-finger')
348 self.button_subscribe.set_name('HildonButton-finger')
350 self.button_refresh.set_sensitive(False)
351 self.button_subscribe.set_sensitive(False)
353 self.button_subscribe.set_image(gtk.image_new_from_icon_name(\
354 self.ICON_GENERAL_ADD, gtk.ICON_SIZE_BUTTON))
355 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
356 self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
358 # Make the button scroll together with the TreeView contents
359 action_area_box = self.treeChannels.get_action_area_box()
360 for child in self.buttonbox:
361 child.reparent(action_area_box)
362 self.vbox.remove(self.buttonbox)
363 action_area_box.set_spacing(2)
364 action_area_box.set_border_width(3)
365 self.treeChannels.set_action_area_visible(True)
367 # Set up a very nice progress bar setup
368 self.fancy_progress_bar = FancyProgressBar(self.main_window, \
369 self.on_btnCancelFeedUpdate_clicked)
370 self.pbFeedUpdate = self.fancy_progress_bar.progress_bar
371 self.pbFeedUpdate.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
372 self.vbox.pack_start(self.fancy_progress_bar.event_box, False)
374 from gpodder.gtkui.frmntl import style
375 sub_font = style.get_font_desc('SmallSystemFont')
376 sub_color = style.get_color('SecondaryTextColor')
377 sub = (sub_font.to_string(), sub_color.to_string())
378 sub = '<span font_desc="%s" foreground="%s">%%s</span>' % sub
379 self.label_footer.set_markup(sub % gpodder.__copyright__)
381 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
382 while gtk.events_pending():
383 gtk.main_iteration(False)
385 try:
386 # Try to get the real package version from dpkg
387 p = subprocess.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout=subprocess.PIPE)
388 version, _stderr = p.communicate()
389 del _stderr
390 del p
391 except:
392 version = gpodder.__version__
393 self.label_footer.set_markup(sub % ('v %s' % version))
394 self.label_footer.hide()
396 self.episodes_window = gPodderEpisodes(self.main_window, \
397 on_treeview_expose_event=self.on_treeview_expose_event, \
398 show_episode_shownotes=self.show_episode_shownotes, \
399 update_podcast_list_model=self.update_podcast_list_model, \
400 on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
401 item_view_episodes_all=self.item_view_episodes_all, \
402 item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
403 item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
404 item_view_episodes_undeleted=self.item_view_episodes_undeleted, \
405 on_entry_search_episodes_changed=self.on_entry_search_episodes_changed, \
406 on_entry_search_episodes_key_press=self.on_entry_search_episodes_key_press, \
407 hide_episode_search=self.hide_episode_search, \
408 on_itemUpdateChannel_activate=self.on_itemUpdateChannel_activate, \
409 playback_episodes=self.playback_episodes, \
410 delete_episode_list=self.delete_episode_list, \
411 episode_list_status_changed=self.episode_list_status_changed, \
412 download_episode_list=self.download_episode_list, \
413 episode_is_downloading=self.episode_is_downloading, \
414 show_episode_in_download_manager=self.show_episode_in_download_manager, \
415 add_download_task_monitor=self.add_download_task_monitor, \
416 remove_download_task_monitor=self.remove_download_task_monitor, \
417 for_each_episode_set_task_status=self.for_each_episode_set_task_status, \
418 on_itemUpdate_activate=self.on_itemUpdate_activate, \
419 show_delete_episodes_window=self.show_delete_episodes_window, \
420 cover_downloader=self.cover_downloader)
422 # Expose objects for episode list type-ahead find
423 self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
424 self.entry_search_episodes = self.episodes_window.entry_search_episodes
425 self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
427 self.downloads_window = gPodderDownloads(self.main_window, \
428 on_treeview_expose_event=self.on_treeview_expose_event, \
429 cleanup_downloads=self.cleanup_downloads, \
430 _for_each_task_set_status=self._for_each_task_set_status, \
431 downloads_list_get_selection=self.downloads_list_get_selection, \
432 _config=self.config)
434 self.treeAvailable = self.episodes_window.treeview
435 self.treeDownloads = self.downloads_window.treeview
437 # Source IDs for timeouts for search-as-you-type
438 self._podcast_list_search_timeout = None
439 self._episode_list_search_timeout = None
441 # Init the treeviews that we use
442 self.init_podcast_list_treeview()
443 self.init_episode_list_treeview()
444 self.init_download_list_treeview()
446 if self.config.podcast_list_hide_boring:
447 self.item_view_hide_boring_podcasts.set_active(True)
449 self.currently_updating = False
451 if gpodder.ui.maemo or self.config.enable_fingerscroll:
452 self.context_menu_mouse_button = 1
453 else:
454 self.context_menu_mouse_button = 3
456 if self.config.start_iconified:
457 self.iconify_main_window()
459 self.download_tasks_seen = set()
460 self.download_list_update_enabled = False
461 self.download_task_monitors = set()
463 # Subscribed channels
464 self.active_channel = None
465 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
466 self.channel_list_changed = True
467 self.update_podcasts_tab()
469 # load list of user applications for audio playback
470 self.user_apps_reader = UserAppsReader(['audio', 'video'])
471 threading.Thread(target=self.user_apps_reader.read).start()
473 # Set the "Device" menu item for the first time
474 if gpodder.ui.desktop:
475 self.update_item_device()
477 # Set up the first instance of MygPoClient
478 self.mygpo_client = my.MygPoClient(self.config)
480 # Now, update the feed cache, when everything's in place
481 if not gpodder.ui.fremantle:
482 self.btnUpdateFeeds.show()
483 self.updating_feed_cache = False
484 self.feed_cache_update_cancelled = False
485 self.update_feed_cache(force_update=self.config.update_on_startup)
487 self.message_area = None
489 def find_partial_downloads():
490 # Look for partial file downloads
491 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
492 count = len(partial_files)
493 resumable_episodes = []
494 if count:
495 if not gpodder.ui.fremantle:
496 util.idle_add(self.wNotebook.set_current_page, 1)
497 indicator = ProgressIndicator(_('Loading incomplete downloads'), \
498 _('Some episodes have not finished downloading in a previous session.'), \
499 False, self.get_dialog_parent())
500 indicator.on_message(N_('%(count)d partial file', '%(count)d partial files', count) % {'count':count})
502 candidates = [f[:-len('.partial')] for f in partial_files]
503 found = 0
505 for c in self.channels:
506 for e in c.get_all_episodes():
507 filename = e.local_filename(create=False, check_only=True)
508 if filename in candidates:
509 log('Found episode: %s', e.title, sender=self)
510 found += 1
511 indicator.on_message(e.title)
512 indicator.on_progress(float(found)/count)
513 candidates.remove(filename)
514 partial_files.remove(filename+'.partial')
515 resumable_episodes.append(e)
517 if not candidates:
518 break
520 if not candidates:
521 break
523 for f in partial_files:
524 log('Partial file without episode: %s', f, sender=self)
525 util.delete_file(f)
527 util.idle_add(indicator.on_finished)
529 if len(resumable_episodes):
530 def offer_resuming():
531 self.download_episode_list_paused(resumable_episodes)
532 if not gpodder.ui.fremantle:
533 resume_all = gtk.Button(_('Resume all'))
534 #resume_all.set_border_width(0)
535 def on_resume_all(button):
536 selection = self.treeDownloads.get_selection()
537 selection.select_all()
538 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
539 selection.unselect_all()
540 self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
541 self.message_area.hide()
542 resume_all.connect('clicked', on_resume_all)
544 self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
545 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
546 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
547 self.message_area.show_all()
548 self.clean_up_downloads(delete_partial=False)
549 util.idle_add(offer_resuming)
550 elif not gpodder.ui.fremantle:
551 util.idle_add(self.wNotebook.set_current_page, 0)
552 else:
553 util.idle_add(self.clean_up_downloads, True)
554 threading.Thread(target=find_partial_downloads).start()
556 # Start the auto-update procedure
557 self._auto_update_timer_source_id = None
558 if self.config.auto_update_feeds:
559 self.restart_auto_update_timer()
561 # Delete old episodes if the user wishes to
562 if self.config.auto_remove_played_episodes and \
563 self.config.episode_old_age > 0:
564 old_episodes = list(self.get_expired_episodes())
565 if len(old_episodes) > 0:
566 self.delete_episode_list(old_episodes, confirm=False)
567 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
569 if gpodder.ui.fremantle:
570 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
571 self.button_refresh.set_sensitive(True)
572 self.button_subscribe.set_sensitive(True)
573 self.main_window.set_title(_('gPodder'))
574 hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
576 # Do the initial sync with the web service
577 util.idle_add(self.mygpo_client.flush, True)
579 # First-time users should be asked if they want to see the OPML
580 if not self.channels and not gpodder.ui.fremantle:
581 util.idle_add(self.on_itemUpdate_activate)
583 def episode_object_by_uri(self, uri):
584 """Get an episode object given a local or remote URI
586 This can be used to quickly access an episode object
587 when all we have is its download filename or episode
588 URL (e.g. from external D-Bus calls / signals, etc..)
590 if uri.startswith('/'):
591 uri = 'file://' + uri
593 prefix = 'file://' + self.config.download_dir
595 if uri.startswith(prefix):
596 # File is on the local filesystem in the download folder
597 filename = uri[len(prefix):]
598 file_parts = [x for x in filename.split(os.sep) if x]
600 if len(file_parts) == 2:
601 dir_name, filename = file_parts
602 channels = [c for c in self.channels if c.foldername == dir_name]
603 if len(channels) == 1:
604 channel = channels[0]
605 return channel.get_episode_by_filename(filename)
606 else:
607 # Possibly remote file - search the database for a podcast
608 channel_id = self.db.get_channel_id_from_episode_url(uri)
610 if channel_id is not None:
611 channels = [c for c in self.channels if c.id == channel_id]
612 if len(channels) == 1:
613 channel = channels[0]
614 return channel.get_episode_by_url(uri)
616 return None
618 def on_played(self, start, end, total, file_uri):
619 """Handle the "played" signal from a media player"""
620 if start == 0 and end == 0 and total == 0:
621 # Ignore bogus play event
622 return
623 elif end < start + 5:
624 # Ignore "less than five seconds" segments,
625 # as they can happen with seeking, etc...
626 return
628 log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
629 episode = self.episode_object_by_uri(file_uri)
631 if episode is not None:
632 file_type = episode.file_type()
633 # Automatically enable D-Bus played status mode
634 if file_type == 'audio':
635 self.config.audio_played_dbus = True
636 elif file_type == 'video':
637 self.config.video_played_dbus = True
639 now = time.time()
640 if total > 0:
641 episode.total_time = total
642 elif total == 0:
643 # Assume the episode's total time for the action
644 total = episode.total_time
645 if episode.current_position_updated is None or \
646 now > episode.current_position_updated:
647 episode.current_position = end
648 episode.current_position_updated = now
649 episode.mark(is_played=True)
650 episode.save()
651 self.db.commit()
652 self.update_episode_list_icons([episode.url])
653 self.update_podcast_list_model([episode.channel.url])
655 # Submit this action to the webservice
656 self.mygpo_client.on_playback_full(episode, \
657 start, end, total)
659 def on_add_remove_podcasts_mygpo(self):
660 actions = self.mygpo_client.get_received_actions()
661 if not actions:
662 return False
664 existing_urls = [c.url for c in self.channels]
666 # Columns for the episode selector window - just one...
667 columns = (
668 ('description', None, None, _('Action')),
671 # A list of actions that have to be chosen from
672 changes = []
674 # Actions that are ignored (already carried out)
675 ignored = []
677 for action in actions:
678 if action.is_add and action.url not in existing_urls:
679 changes.append(my.Change(action))
680 elif action.is_remove and action.url in existing_urls:
681 podcast_object = None
682 for podcast in self.channels:
683 if podcast.url == action.url:
684 podcast_object = podcast
685 break
686 changes.append(my.Change(action, podcast_object))
687 else:
688 log('Ignoring action: %s', action, sender=self)
689 ignored.append(action)
691 # Confirm all ignored changes
692 self.mygpo_client.confirm_received_actions(ignored)
694 def execute_podcast_actions(selected):
695 add_list = [c.action.url for c in selected if c.action.is_add]
696 remove_list = [c.podcast for c in selected if c.action.is_remove]
698 # Apply the accepted changes locally
699 self.add_podcast_list(add_list)
700 self.remove_podcast_list(remove_list, confirm=False)
702 # All selected items are now confirmed
703 self.mygpo_client.confirm_received_actions(c.action for c in selected)
705 # Revert the changes on the server
706 rejected = [c.action for c in changes if c not in selected]
707 self.mygpo_client.reject_received_actions(rejected)
709 def ask():
710 # We're abusing the Episode Selector again ;) -- thp
711 gPodderEpisodeSelector(self.main_window, \
712 title=_('Confirm changes from gpodder.net'), \
713 instructions=_('Select the actions you want to carry out.'), \
714 episodes=changes, \
715 columns=columns, \
716 size_attribute=None, \
717 stock_ok_button=gtk.STOCK_APPLY, \
718 callback=execute_podcast_actions, \
719 _config=self.config)
721 # There are some actions that need the user's attention
722 if changes:
723 util.idle_add(ask)
724 return True
726 # We have no remaining actions - no selection happens
727 return False
729 def rewrite_urls_mygpo(self):
730 # Check if we have to rewrite URLs since the last add
731 rewritten_urls = self.mygpo_client.get_rewritten_urls()
733 for rewritten_url in rewritten_urls:
734 if not rewritten_url.new_url:
735 continue
737 for channel in self.channels:
738 if channel.url == rewritten_url.old_url:
739 log('Updating URL of %s to %s', channel, \
740 rewritten_url.new_url, sender=self)
741 channel.url = rewritten_url.new_url
742 channel.save()
743 self.channel_list_changed = True
744 util.idle_add(self.update_episode_list_model)
745 break
747 def on_send_full_subscriptions(self):
748 # Send the full subscription list to the gpodder.net client
749 # (this will overwrite the subscription list on the server)
750 indicator = ProgressIndicator(_('Uploading subscriptions'), \
751 _('Your subscriptions are being uploaded to the server.'), \
752 False, self.get_dialog_parent())
754 try:
755 self.mygpo_client.set_subscriptions([c.url for c in self.channels])
756 util.idle_add(self.show_message, _('List uploaded successfully.'))
757 except Exception, e:
758 def show_error(e):
759 message = str(e)
760 if not message:
761 message = e.__class__.__name__
762 self.show_message(message, \
763 _('Error while uploading'), \
764 important=True)
765 util.idle_add(show_error, e)
767 util.idle_add(indicator.on_finished)
769 def on_podcast_selected(self, treeview, path, column):
770 # for Maemo 5's UI
771 model = treeview.get_model()
772 channel = model.get_value(model.get_iter(path), \
773 PodcastListModel.C_CHANNEL)
774 self.active_channel = channel
775 self.update_episode_list_model()
776 self.episodes_window.channel = self.active_channel
777 self.episodes_window.show()
779 def on_button_subscribe_clicked(self, button):
780 self.on_itemImportChannels_activate(button)
782 def on_button_downloads_clicked(self, widget):
783 self.downloads_window.show()
785 def show_episode_in_download_manager(self, episode):
786 self.downloads_window.show()
787 model = self.treeDownloads.get_model()
788 selection = self.treeDownloads.get_selection()
789 selection.unselect_all()
790 it = model.get_iter_first()
791 while it is not None:
792 task = model.get_value(it, DownloadStatusModel.C_TASK)
793 if task.episode.url == episode.url:
794 selection.select_iter(it)
795 # FIXME: Scroll to selection in pannable area
796 break
797 it = model.iter_next(it)
799 def for_each_episode_set_task_status(self, episodes, status):
800 episode_urls = set(episode.url for episode in episodes)
801 model = self.treeDownloads.get_model()
802 selected_tasks = [(gtk.TreeRowReference(model, row.path), \
803 model.get_value(row.iter, \
804 DownloadStatusModel.C_TASK)) for row in model \
805 if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
806 in episode_urls]
807 self._for_each_task_set_status(selected_tasks, status)
809 def on_window_orientation_changed(self, orientation):
810 self._last_orientation = orientation
811 if self.preferences_dialog is not None:
812 self.preferences_dialog.on_window_orientation_changed(orientation)
814 treeview = self.treeChannels
815 if orientation == Orientation.PORTRAIT:
816 treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
817 # Work around Maemo bug #4718
818 self.button_subscribe.set_name('HildonButton-thumb')
819 self.button_refresh.set_name('HildonButton-thumb')
820 else:
821 treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
822 # Work around Maemo bug #4718
823 self.button_subscribe.set_name('HildonButton-finger')
824 self.button_refresh.set_name('HildonButton-finger')
826 if gpodder.ui.fremantle:
827 self.fancy_progress_bar.relayout()
829 def on_treeview_podcasts_selection_changed(self, selection):
830 model, iter = selection.get_selected()
831 if iter is None:
832 self.active_channel = None
833 self.episode_list_model.clear()
835 def on_treeview_button_pressed(self, treeview, event):
836 if event.window != treeview.get_bin_window():
837 return False
839 TreeViewHelper.save_button_press_event(treeview, event)
841 if getattr(treeview, TreeViewHelper.ROLE) == \
842 TreeViewHelper.ROLE_PODCASTS:
843 return self.currently_updating
845 return event.button == self.context_menu_mouse_button and \
846 gpodder.ui.desktop
848 def on_treeview_podcasts_button_released(self, treeview, event):
849 if event.window != treeview.get_bin_window():
850 return False
852 if gpodder.ui.maemo:
853 return self.treeview_channels_handle_gestures(treeview, event)
854 return self.treeview_channels_show_context_menu(treeview, event)
856 def on_treeview_episodes_button_released(self, treeview, event):
857 if event.window != treeview.get_bin_window():
858 return False
860 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
861 return self.treeview_available_handle_gestures(treeview, event)
863 return self.treeview_available_show_context_menu(treeview, event)
865 def on_treeview_downloads_button_released(self, treeview, event):
866 if event.window != treeview.get_bin_window():
867 return False
869 return self.treeview_downloads_show_context_menu(treeview, event)
871 def on_entry_search_podcasts_changed(self, editable):
872 if self.hbox_search_podcasts.get_property('visible'):
873 def set_search_term(self, text):
874 self.podcast_list_model.set_search_term(text)
875 self._podcast_list_search_timeout = None
876 return False
878 if self._podcast_list_search_timeout is not None:
879 gobject.source_remove(self._podcast_list_search_timeout)
880 self._podcast_list_search_timeout = gobject.timeout_add(\
881 self.LIVE_SEARCH_DELAY, \
882 set_search_term, self, editable.get_chars(0, -1))
884 def on_entry_search_podcasts_key_press(self, editable, event):
885 if event.keyval == gtk.keysyms.Escape:
886 self.hide_podcast_search()
887 return True
889 def hide_podcast_search(self, *args):
890 if self._podcast_list_search_timeout is not None:
891 gobject.source_remove(self._podcast_list_search_timeout)
892 self._podcast_list_search_timeout = None
893 self.hbox_search_podcasts.hide()
894 self.entry_search_podcasts.set_text('')
895 self.podcast_list_model.set_search_term(None)
896 self.treeChannels.grab_focus()
898 def show_podcast_search(self, input_char):
899 self.hbox_search_podcasts.show()
900 self.entry_search_podcasts.insert_text(input_char, -1)
901 self.entry_search_podcasts.grab_focus()
902 self.entry_search_podcasts.set_position(-1)
904 def init_podcast_list_treeview(self):
905 # Set up podcast channel tree view widget
906 if gpodder.ui.fremantle:
907 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
908 self.item_view_podcasts_downloaded.set_active(True)
909 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
910 self.item_view_podcasts_unplayed.set_active(True)
911 else:
912 self.item_view_podcasts_all.set_active(True)
913 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
915 iconcolumn = gtk.TreeViewColumn('')
916 iconcell = gtk.CellRendererPixbuf()
917 iconcolumn.pack_start(iconcell, False)
918 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
919 self.treeChannels.append_column(iconcolumn)
921 namecolumn = gtk.TreeViewColumn('')
922 namecell = gtk.CellRendererText()
923 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
924 namecolumn.pack_start(namecell, True)
925 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
927 if gpodder.ui.fremantle:
928 countcell = gtk.CellRendererText()
929 from gpodder.gtkui.frmntl import style
930 countcell.set_property('font-desc', style.get_font_desc('EmpSystemFont'))
931 countcell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
932 countcell.set_property('alignment', pango.ALIGN_RIGHT)
933 countcell.set_property('xalign', 1.)
934 countcell.set_property('xpad', 5)
935 namecolumn.pack_start(countcell, False)
936 namecolumn.add_attribute(countcell, 'text', PodcastListModel.C_DOWNLOADS)
937 namecolumn.add_attribute(countcell, 'visible', PodcastListModel.C_DOWNLOADS)
938 else:
939 iconcell = gtk.CellRendererPixbuf()
940 iconcell.set_property('xalign', 1.0)
941 namecolumn.pack_start(iconcell, False)
942 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
943 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
945 self.treeChannels.append_column(namecolumn)
947 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
949 # When no podcast is selected, clear the episode list model
950 selection = self.treeChannels.get_selection()
951 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
953 # Set up type-ahead find for the podcast list
954 def on_key_press(treeview, event):
955 if event.keyval == gtk.keysyms.Escape:
956 self.hide_podcast_search()
957 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
958 self.hide_podcast_search()
959 elif event.state & gtk.gdk.CONTROL_MASK:
960 # Don't handle type-ahead when control is pressed (so shortcuts
961 # with the Ctrl key still work, e.g. Ctrl+A, ...)
962 return True
963 else:
964 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
965 if unicode_char_id == 0:
966 return False
967 input_char = unichr(unicode_char_id)
968 self.show_podcast_search(input_char)
969 return True
970 self.treeChannels.connect('key-press-event', on_key_press)
972 # Enable separators to the podcast list to separate special podcasts
973 # from others (this is used for the "all episodes" view)
974 self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
976 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
978 def on_entry_search_episodes_changed(self, editable):
979 if self.hbox_search_episodes.get_property('visible'):
980 def set_search_term(self, text):
981 self.episode_list_model.set_search_term(text)
982 self._episode_list_search_timeout = None
983 return False
985 if self._episode_list_search_timeout is not None:
986 gobject.source_remove(self._episode_list_search_timeout)
987 self._episode_list_search_timeout = gobject.timeout_add(\
988 self.LIVE_SEARCH_DELAY, \
989 set_search_term, self, editable.get_chars(0, -1))
991 def on_entry_search_episodes_key_press(self, editable, event):
992 if event.keyval == gtk.keysyms.Escape:
993 self.hide_episode_search()
994 return True
996 def hide_episode_search(self, *args):
997 if self._episode_list_search_timeout is not None:
998 gobject.source_remove(self._episode_list_search_timeout)
999 self._episode_list_search_timeout = None
1000 self.hbox_search_episodes.hide()
1001 self.entry_search_episodes.set_text('')
1002 self.episode_list_model.set_search_term(None)
1003 self.treeAvailable.grab_focus()
1005 def show_episode_search(self, input_char):
1006 self.hbox_search_episodes.show()
1007 self.entry_search_episodes.insert_text(input_char, -1)
1008 self.entry_search_episodes.grab_focus()
1009 self.entry_search_episodes.set_position(-1)
1011 def init_episode_list_treeview(self):
1012 # For loading the list model
1013 self.episode_list_model = EpisodeListModel(self.on_episode_list_filter_changed)
1015 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
1016 self.item_view_episodes_undeleted.set_active(True)
1017 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
1018 self.item_view_episodes_downloaded.set_active(True)
1019 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
1020 self.item_view_episodes_unplayed.set_active(True)
1021 else:
1022 self.item_view_episodes_all.set_active(True)
1024 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
1026 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
1028 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
1030 iconcell = gtk.CellRendererPixbuf()
1031 iconcell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1032 if gpodder.ui.maemo:
1033 iconcell.set_fixed_size(50, 50)
1034 else:
1035 iconcell.set_fixed_size(40, -1)
1037 namecell = gtk.CellRendererText()
1038 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
1039 namecolumn = gtk.TreeViewColumn(_('Episode'))
1040 namecolumn.pack_start(iconcell, False)
1041 namecolumn.add_attribute(iconcell, 'icon-name', EpisodeListModel.C_STATUS_ICON)
1042 namecolumn.pack_start(namecell, True)
1043 namecolumn.add_attribute(namecell, 'markup', EpisodeListModel.C_DESCRIPTION)
1044 if gpodder.ui.fremantle:
1045 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1046 else:
1047 namecolumn.set_sort_column_id(EpisodeListModel.C_DESCRIPTION)
1048 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1049 namecolumn.set_resizable(True)
1050 namecolumn.set_expand(True)
1052 if gpodder.ui.fremantle:
1053 from gpodder.gtkui.frmntl import style
1054 timecell = gtk.CellRendererText()
1055 timecell.set_property('font-desc', style.get_font_desc('SystemFont'))
1056 timecell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
1057 timecell.set_property('alignment', pango.ALIGN_RIGHT)
1058 timecell.set_property('xalign', 1.)
1059 timecell.set_property('xpad', 5)
1060 namecolumn.pack_start(timecell, False)
1061 namecolumn.add_attribute(timecell, 'text', EpisodeListModel.C_TIME)
1062 namecolumn.add_attribute(timecell, 'visible', EpisodeListModel.C_TIME1_VISIBLE)
1064 # Add another cell renderer to fix a sizing issue (one renderer
1065 # only renders short text and the other one longer text to avoid
1066 # having titles of episodes unnecessarily cut off)
1067 timecell = gtk.CellRendererText()
1068 timecell.set_property('font-desc', style.get_font_desc('SystemFont'))
1069 timecell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
1070 timecell.set_property('alignment', pango.ALIGN_RIGHT)
1071 timecell.set_property('xalign', 1.)
1072 timecell.set_property('xpad', 5)
1073 namecolumn.pack_start(timecell, False)
1074 namecolumn.add_attribute(timecell, 'text', EpisodeListModel.C_TIME)
1075 namecolumn.add_attribute(timecell, 'visible', EpisodeListModel.C_TIME2_VISIBLE)
1077 lockcell = gtk.CellRendererPixbuf()
1078 lockcell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1079 if gpodder.ui.fremantle:
1080 lockcell.set_property('icon-name', 'general_locked')
1081 else:
1082 lockcell.set_property('icon-name', 'emblem-readonly')
1084 namecolumn.pack_start(lockcell, False)
1085 namecolumn.add_attribute(lockcell, 'visible', EpisodeListModel.C_LOCKED)
1087 sizecell = gtk.CellRendererText()
1088 sizecell.set_property('xalign', 1)
1089 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
1090 sizecolumn.set_sort_column_id(EpisodeListModel.C_FILESIZE)
1092 releasecell = gtk.CellRendererText()
1093 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
1094 releasecolumn.set_sort_column_id(EpisodeListModel.C_PUBLISHED)
1096 namecolumn.set_reorderable(True)
1097 self.treeAvailable.append_column(namecolumn)
1099 if not gpodder.ui.maemo:
1100 for itemcolumn in (sizecolumn, releasecolumn):
1101 itemcolumn.set_reorderable(True)
1102 self.treeAvailable.append_column(itemcolumn)
1104 # Set up type-ahead find for the episode list
1105 def on_key_press(treeview, event):
1106 if event.keyval == gtk.keysyms.Escape:
1107 self.hide_episode_search()
1108 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
1109 self.hide_episode_search()
1110 elif event.state & gtk.gdk.CONTROL_MASK:
1111 # Don't handle type-ahead when control is pressed (so shortcuts
1112 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1113 return False
1114 else:
1115 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
1116 if unicode_char_id == 0:
1117 return False
1118 input_char = unichr(unicode_char_id)
1119 self.show_episode_search(input_char)
1120 return True
1121 self.treeAvailable.connect('key-press-event', on_key_press)
1123 if gpodder.ui.desktop and not self.config.enable_fingerscroll:
1124 self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
1125 (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
1126 def drag_data_get(tree, context, selection_data, info, timestamp):
1127 if self.config.on_drag_mark_played:
1128 for episode in self.get_selected_episodes():
1129 episode.mark(is_played=True)
1130 self.on_selected_episodes_status_changed()
1131 uris = ['file://'+e.local_filename(create=False) \
1132 for e in self.get_selected_episodes() \
1133 if e.was_downloaded(and_exists=True)]
1134 uris.append('') # for the trailing '\r\n'
1135 selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
1136 self.treeAvailable.connect('drag-data-get', drag_data_get)
1138 selection = self.treeAvailable.get_selection()
1139 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
1140 selection.set_mode(gtk.SELECTION_SINGLE)
1141 elif gpodder.ui.fremantle:
1142 selection.set_mode(gtk.SELECTION_SINGLE)
1143 else:
1144 selection.set_mode(gtk.SELECTION_MULTIPLE)
1145 # Update the sensitivity of the toolbar buttons on the Desktop
1146 selection.connect('changed', lambda s: self.play_or_download())
1148 if gpodder.ui.diablo:
1149 # Set up the tap-and-hold context menu for podcasts
1150 menu = gtk.Menu()
1151 menu.append(self.itemUpdateChannel.create_menu_item())
1152 menu.append(self.itemEditChannel.create_menu_item())
1153 menu.append(gtk.SeparatorMenuItem())
1154 menu.append(self.itemRemoveChannel.create_menu_item())
1155 menu.append(gtk.SeparatorMenuItem())
1156 item = gtk.ImageMenuItem(_('Close this menu'))
1157 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
1158 gtk.ICON_SIZE_MENU))
1159 menu.append(item)
1160 menu.show_all()
1161 menu = self.set_finger_friendly(menu)
1162 self.treeChannels.tap_and_hold_setup(menu)
1165 def init_download_list_treeview(self):
1166 # enable multiple selection support
1167 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1168 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1170 # columns and renderers for "download progress" tab
1171 # First column: [ICON] Episodename
1172 column = gtk.TreeViewColumn(_('Episode'))
1174 cell = gtk.CellRendererPixbuf()
1175 if gpodder.ui.maemo:
1176 cell.set_fixed_size(50, 50)
1177 cell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1178 column.pack_start(cell, expand=False)
1179 column.add_attribute(cell, 'icon-name', \
1180 DownloadStatusModel.C_ICON_NAME)
1182 cell = gtk.CellRendererText()
1183 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1184 column.pack_start(cell, expand=True)
1185 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1186 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1187 column.set_expand(True)
1188 self.treeDownloads.append_column(column)
1190 # Second column: Progress
1191 cell = gtk.CellRendererProgress()
1192 cell.set_property('yalign', .5)
1193 cell.set_property('ypad', 6)
1194 column = gtk.TreeViewColumn(_('Progress'), cell,
1195 value=DownloadStatusModel.C_PROGRESS, \
1196 text=DownloadStatusModel.C_PROGRESS_TEXT)
1197 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1198 column.set_expand(False)
1199 self.treeDownloads.append_column(column)
1200 if gpodder.ui.maemo:
1201 column.set_property('min-width', 200)
1202 column.set_property('max-width', 200)
1203 else:
1204 column.set_property('min-width', 150)
1205 column.set_property('max-width', 150)
1207 self.treeDownloads.set_model(self.download_status_model)
1208 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1210 def on_treeview_expose_event(self, treeview, event):
1211 if event.window == treeview.get_bin_window():
1212 model = treeview.get_model()
1213 if (model is not None and model.get_iter_first() is not None):
1214 return False
1216 role = getattr(treeview, TreeViewHelper.ROLE, None)
1217 if role is None:
1218 return False
1220 ctx = event.window.cairo_create()
1221 ctx.rectangle(event.area.x, event.area.y,
1222 event.area.width, event.area.height)
1223 ctx.clip()
1225 x, y, width, height, depth = event.window.get_geometry()
1226 progress = None
1228 if role == TreeViewHelper.ROLE_EPISODES:
1229 if self.currently_updating:
1230 text = _('Loading episodes')
1231 elif self.config.episode_list_view_mode != \
1232 EpisodeListModel.VIEW_ALL:
1233 text = _('No episodes in current view')
1234 else:
1235 text = _('No episodes available')
1236 elif role == TreeViewHelper.ROLE_PODCASTS:
1237 if self.config.episode_list_view_mode != \
1238 EpisodeListModel.VIEW_ALL and \
1239 self.config.podcast_list_hide_boring and \
1240 len(self.channels) > 0:
1241 text = _('No podcasts in this view')
1242 else:
1243 text = _('No subscriptions')
1244 elif role == TreeViewHelper.ROLE_DOWNLOADS:
1245 text = _('No active downloads')
1246 else:
1247 raise Exception('on_treeview_expose_event: unknown role')
1249 if gpodder.ui.fremantle:
1250 from gpodder.gtkui.frmntl import style
1251 font_desc = style.get_font_desc('LargeSystemFont')
1252 else:
1253 font_desc = None
1255 draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1257 return False
1259 def enable_download_list_update(self):
1260 if not self.download_list_update_enabled:
1261 self.update_downloads_list()
1262 gobject.timeout_add(1500, self.update_downloads_list)
1263 self.download_list_update_enabled = True
1265 def cleanup_downloads(self):
1266 model = self.download_status_model
1268 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
1269 changed_episode_urls = set()
1270 for row_reference, task in all_tasks:
1271 if task.status in (task.DONE, task.CANCELLED):
1272 model.remove(model.get_iter(row_reference.get_path()))
1273 try:
1274 # We don't "see" this task anymore - remove it;
1275 # this is needed, so update_episode_list_icons()
1276 # below gets the correct list of "seen" tasks
1277 self.download_tasks_seen.remove(task)
1278 except KeyError, key_error:
1279 log('Cannot remove task from "seen" list: %s', task, sender=self)
1280 changed_episode_urls.add(task.url)
1281 # Tell the task that it has been removed (so it can clean up)
1282 task.removed_from_list()
1284 # Tell the podcasts tab to update icons for our removed podcasts
1285 self.update_episode_list_icons(changed_episode_urls)
1287 # Tell the shownotes window that we have removed the episode
1288 if self.episode_shownotes_window is not None and \
1289 self.episode_shownotes_window.episode is not None and \
1290 self.episode_shownotes_window.episode.url in changed_episode_urls:
1291 self.episode_shownotes_window._download_status_changed(None)
1293 # Update the downloads list one more time
1294 self.update_downloads_list(can_call_cleanup=False)
1296 def on_tool_downloads_toggled(self, toolbutton):
1297 if toolbutton.get_active():
1298 self.wNotebook.set_current_page(1)
1299 else:
1300 self.wNotebook.set_current_page(0)
1302 def add_download_task_monitor(self, monitor):
1303 self.download_task_monitors.add(monitor)
1304 model = self.download_status_model
1305 if model is None:
1306 model = ()
1307 for row in model:
1308 task = row[self.download_status_model.C_TASK]
1309 monitor.task_updated(task)
1311 def remove_download_task_monitor(self, monitor):
1312 self.download_task_monitors.remove(monitor)
1314 def update_downloads_list(self, can_call_cleanup=True):
1315 try:
1316 model = self.download_status_model
1318 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
1319 total_speed, total_size, done_size = 0, 0, 0
1321 # Keep a list of all download tasks that we've seen
1322 download_tasks_seen = set()
1324 # Remember the DownloadTask object for the episode that
1325 # has been opened in the episode shownotes dialog (if any)
1326 if self.episode_shownotes_window is not None:
1327 shownotes_episode = self.episode_shownotes_window.episode
1328 shownotes_task = None
1329 else:
1330 shownotes_episode = None
1331 shownotes_task = None
1333 # Do not go through the list of the model is not (yet) available
1334 if model is None:
1335 model = ()
1337 failed_downloads = []
1338 for row in model:
1339 self.download_status_model.request_update(row.iter)
1341 task = row[self.download_status_model.C_TASK]
1342 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
1344 # Let the download task monitors know of changes
1345 for monitor in self.download_task_monitors:
1346 monitor.task_updated(task)
1348 total_size += size
1349 done_size += size*progress
1351 if shownotes_episode is not None and \
1352 shownotes_episode.url == task.episode.url:
1353 shownotes_task = task
1355 download_tasks_seen.add(task)
1357 if status == download.DownloadTask.DOWNLOADING:
1358 downloading += 1
1359 total_speed += speed
1360 elif status == download.DownloadTask.FAILED:
1361 failed_downloads.append(task)
1362 failed += 1
1363 elif status == download.DownloadTask.DONE:
1364 finished += 1
1365 elif status == download.DownloadTask.QUEUED:
1366 queued += 1
1367 elif status == download.DownloadTask.PAUSED:
1368 paused += 1
1369 else:
1370 others += 1
1372 # Remember which tasks we have seen after this run
1373 self.download_tasks_seen = download_tasks_seen
1375 if gpodder.ui.desktop:
1376 text = [_('Downloads')]
1377 if downloading + failed + queued > 0:
1378 s = []
1379 if downloading > 0:
1380 s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count':downloading})
1381 if failed > 0:
1382 s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1383 if queued > 0:
1384 s.append(N_('%(count)d queued', '%(count)d queued', queued) % {'count':queued})
1385 text.append(' (' + ', '.join(s)+')')
1386 self.labelDownloads.set_text(''.join(text))
1387 elif gpodder.ui.diablo:
1388 sum = downloading + failed + finished + queued + paused + others
1389 if sum:
1390 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
1391 else:
1392 self.tool_downloads.set_label(_('Downloads'))
1393 elif gpodder.ui.fremantle:
1394 if downloading + queued > 0:
1395 self.button_downloads.set_value(N_('%(count)d active', '%(count)d active', downloading+queued) % {'count':(downloading+queued)})
1396 elif failed > 0:
1397 self.button_downloads.set_value(N_('%(count)d failed', '%(count)d failed', failed) % {'count':failed})
1398 elif paused > 0:
1399 self.button_downloads.set_value(N_('%(count)d paused', '%(count)d paused', paused) % {'count':paused})
1400 else:
1401 self.button_downloads.set_value(_('Idle'))
1403 title = [self.default_title]
1405 # We have to update all episodes/channels for which the status has
1406 # changed. Accessing task.status_changed has the side effect of
1407 # re-setting the changed flag, so we need to get the "changed" list
1408 # of tuples first and split it into two lists afterwards
1409 changed = [(task.url, task.podcast_url) for task in \
1410 self.download_tasks_seen if task.status_changed]
1411 episode_urls = [episode_url for episode_url, channel_url in changed]
1412 channel_urls = [channel_url for episode_url, channel_url in changed]
1414 count = downloading + queued
1415 if count > 0:
1416 title.append(N_('downloading %(count)d file', 'downloading %(count)d files', count) % {'count':count})
1418 if total_size > 0:
1419 percentage = 100.0*done_size/total_size
1420 else:
1421 percentage = 0.0
1422 total_speed = util.format_filesize(total_speed)
1423 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
1424 if self.tray_icon is not None:
1425 # Update the tray icon status and progress bar
1426 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
1427 self.tray_icon.draw_progress_bar(percentage/100.)
1428 else:
1429 if self.tray_icon is not None:
1430 # Update the tray icon status
1431 self.tray_icon.set_status()
1432 if gpodder.ui.desktop:
1433 self.downloads_finished(self.download_tasks_seen)
1434 if gpodder.ui.diablo:
1435 hildon.hildon_banner_show_information(self.gPodder, '', 'gPodder: %s' % _('All downloads finished'))
1436 log('All downloads have finished.', sender=self)
1437 if self.config.cmd_all_downloads_complete:
1438 util.run_external_command(self.config.cmd_all_downloads_complete)
1440 if gpodder.ui.fremantle and failed:
1441 message = '\n'.join(['%s: %s' % (str(task), \
1442 task.error_message) for task in failed_downloads])
1443 self.show_message(message, _('Downloads failed'), important=True)
1445 # Remove finished episodes
1446 if self.config.auto_cleanup_downloads and can_call_cleanup:
1447 self.cleanup_downloads()
1449 # Stop updating the download list here
1450 self.download_list_update_enabled = False
1452 if not gpodder.ui.fremantle:
1453 self.gPodder.set_title(' - '.join(title))
1455 self.update_episode_list_icons(episode_urls)
1456 if self.episode_shownotes_window is not None:
1457 if (shownotes_task and shownotes_task.url in episode_urls) or \
1458 shownotes_task != self.episode_shownotes_window.task:
1459 self.episode_shownotes_window._download_status_changed(shownotes_task)
1460 self.episode_shownotes_window._download_status_progress()
1461 self.play_or_download()
1462 if channel_urls:
1463 self.update_podcast_list_model(channel_urls)
1465 return self.download_list_update_enabled
1466 except Exception, e:
1467 log('Exception happened while updating download list.', sender=self, traceback=True)
1468 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1469 # We return False here, so the update loop won't be called again,
1470 # that's why we require the restart of gPodder in the message.
1471 return False
1473 def on_config_changed(self, *args):
1474 util.idle_add(self._on_config_changed, *args)
1476 def _on_config_changed(self, name, old_value, new_value):
1477 if name == 'show_toolbar' and gpodder.ui.desktop:
1478 self.toolbar.set_property('visible', new_value)
1479 elif name == 'videoplayer':
1480 self.config.video_played_dbus = False
1481 elif name == 'player':
1482 self.config.audio_played_dbus = False
1483 elif name == 'episode_list_descriptions':
1484 self.update_episode_list_model()
1485 elif name == 'episode_list_thumbnails':
1486 self.update_episode_list_icons(all=True)
1487 elif name == 'rotation_mode':
1488 self._fremantle_rotation.set_mode(new_value)
1489 elif name in ('auto_update_feeds', 'auto_update_frequency'):
1490 self.restart_auto_update_timer()
1491 elif name == 'podcast_list_view_all':
1492 # Force a update of the podcast list model
1493 self.channel_list_changed = True
1494 if gpodder.ui.fremantle:
1495 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1496 while gtk.events_pending():
1497 gtk.main_iteration(False)
1498 self.update_podcast_list_model()
1499 if gpodder.ui.fremantle:
1500 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1502 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1503 # With get_bin_window, we get the window that contains the rows without
1504 # the header. The Y coordinate of this window will be the height of the
1505 # treeview header. This is the amount we have to subtract from the
1506 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1507 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1508 y -= x_bin
1509 y -= y_bin
1510 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1512 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or x > 50 or (column is not None and column != treeview.get_columns()[0]):
1513 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1514 return False
1516 if path is not None:
1517 model = treeview.get_model()
1518 iter = model.get_iter(path)
1519 role = getattr(treeview, TreeViewHelper.ROLE)
1521 if role == TreeViewHelper.ROLE_EPISODES:
1522 id = model.get_value(iter, EpisodeListModel.C_URL)
1523 elif role == TreeViewHelper.ROLE_PODCASTS:
1524 id = model.get_value(iter, PodcastListModel.C_URL)
1526 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1527 if last_tooltip is not None and last_tooltip != id:
1528 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1529 return False
1530 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1532 if role == TreeViewHelper.ROLE_EPISODES:
1533 description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1534 if description:
1535 tooltip.set_text(description)
1536 else:
1537 return False
1538 elif role == TreeViewHelper.ROLE_PODCASTS:
1539 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1540 if channel is None:
1541 return False
1542 channel.request_save_dir_size()
1543 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1544 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1545 if error_str:
1546 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1547 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1548 table = gtk.Table(rows=3, columns=3)
1549 table.set_row_spacings(5)
1550 table.set_col_spacings(5)
1551 table.set_border_width(5)
1553 heading = gtk.Label()
1554 heading.set_alignment(0, 1)
1555 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1556 table.attach(heading, 0, 1, 0, 1)
1557 size_info = gtk.Label()
1558 size_info.set_alignment(1, 1)
1559 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1560 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1561 table.attach(size_info, 2, 3, 0, 1)
1563 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1565 if len(channel.description) < 500:
1566 description = channel.description
1567 else:
1568 pos = channel.description.find('\n\n')
1569 if pos == -1 or pos > 500:
1570 description = channel.description[:498]+'[...]'
1571 else:
1572 description = channel.description[:pos]
1574 description = gtk.Label(description)
1575 if error_str:
1576 description.set_markup(error_str)
1577 description.set_alignment(0, 0)
1578 description.set_line_wrap(True)
1579 table.attach(description, 0, 3, 2, 3)
1581 table.show_all()
1582 tooltip.set_custom(table)
1584 return True
1586 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1587 return False
1589 def treeview_allow_tooltips(self, treeview, allow):
1590 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1592 def update_m3u_playlist_clicked(self, widget):
1593 if self.active_channel is not None:
1594 self.active_channel.update_m3u_playlist()
1595 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1597 def treeview_handle_context_menu_click(self, treeview, event):
1598 x, y = int(event.x), int(event.y)
1599 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1601 selection = treeview.get_selection()
1602 model, paths = selection.get_selected_rows()
1604 if path is None or (path not in paths and \
1605 event.button == self.context_menu_mouse_button):
1606 # We have right-clicked, but not into the selection,
1607 # assume we don't want to operate on the selection
1608 paths = []
1610 if path is not None and not paths and \
1611 event.button == self.context_menu_mouse_button:
1612 # No selection or clicked outside selection;
1613 # select the single item where we clicked
1614 treeview.grab_focus()
1615 treeview.set_cursor(path, column, 0)
1616 paths = [path]
1618 if not paths:
1619 # Unselect any remaining items (clicked elsewhere)
1620 if hasattr(treeview, 'is_rubber_banding_active'):
1621 if not treeview.is_rubber_banding_active():
1622 selection.unselect_all()
1623 else:
1624 selection.unselect_all()
1626 return model, paths
1628 def downloads_list_get_selection(self, model=None, paths=None):
1629 if model is None and paths is None:
1630 selection = self.treeDownloads.get_selection()
1631 model, paths = selection.get_selected_rows()
1633 can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1634 selected_tasks = [(gtk.TreeRowReference(model, path), \
1635 model.get_value(model.get_iter(path), \
1636 DownloadStatusModel.C_TASK)) for path in paths]
1638 for row_reference, task in selected_tasks:
1639 if task.status != download.DownloadTask.QUEUED:
1640 can_force = False
1641 if task.status not in (download.DownloadTask.PAUSED, \
1642 download.DownloadTask.FAILED, \
1643 download.DownloadTask.CANCELLED):
1644 can_queue = False
1645 if task.status not in (download.DownloadTask.PAUSED, \
1646 download.DownloadTask.QUEUED, \
1647 download.DownloadTask.DOWNLOADING, \
1648 download.DownloadTask.FAILED):
1649 can_cancel = False
1650 if task.status not in (download.DownloadTask.QUEUED, \
1651 download.DownloadTask.DOWNLOADING):
1652 can_pause = False
1653 if task.status not in (download.DownloadTask.CANCELLED, \
1654 download.DownloadTask.FAILED, \
1655 download.DownloadTask.DONE):
1656 can_remove = False
1658 return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1660 def downloads_finished(self, download_tasks_seen):
1661 # FIXME: Filter all tasks that have already been reported
1662 finished_downloads = [str(task) for task in download_tasks_seen if task.status == task.DONE]
1663 failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.status == task.FAILED]
1665 if finished_downloads and failed_downloads:
1666 message = self.format_episode_list(finished_downloads, 5)
1667 message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1668 message += self.format_episode_list(failed_downloads, 5)
1669 self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1670 elif finished_downloads:
1671 message = self.format_episode_list(finished_downloads)
1672 self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1673 elif failed_downloads:
1674 message = self.format_episode_list(failed_downloads)
1675 self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1677 # Open torrent files right after download (bug 1029)
1678 if self.config.open_torrent_after_download:
1679 for task in download_tasks_seen:
1680 if task.status != task.DONE:
1681 continue
1683 episode = task.episode
1684 if episode.mimetype != 'application/x-bittorrent':
1685 continue
1687 self.playback_episodes([episode])
1690 def format_episode_list(self, episode_list, max_episodes=10):
1692 Format a list of episode names for notifications
1694 Will truncate long episode names and limit the amount of
1695 episodes displayed (max_episodes=10).
1697 The episode_list parameter should be a list of strings.
1699 MAX_TITLE_LENGTH = 100
1701 result = []
1702 for title in episode_list[:min(len(episode_list), max_episodes)]:
1703 if len(title) > MAX_TITLE_LENGTH:
1704 middle = (MAX_TITLE_LENGTH/2)-2
1705 title = '%s...%s' % (title[0:middle], title[-middle:])
1706 result.append(saxutils.escape(title))
1707 result.append('\n')
1709 more_episodes = len(episode_list) - max_episodes
1710 if more_episodes > 0:
1711 result.append('(...')
1712 result.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes) % {'count':more_episodes})
1713 result.append('...)')
1715 return (''.join(result)).strip()
1717 def _for_each_task_set_status(self, tasks, status, force_start=False):
1718 episode_urls = set()
1719 model = self.treeDownloads.get_model()
1720 for row_reference, task in tasks:
1721 if status == download.DownloadTask.QUEUED:
1722 # Only queue task when its paused/failed/cancelled (or forced)
1723 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1724 self.download_queue_manager.add_task(task, force_start)
1725 self.enable_download_list_update()
1726 elif status == download.DownloadTask.CANCELLED:
1727 # Cancelling a download allowed when downloading/queued
1728 if task.status in (task.QUEUED, task.DOWNLOADING):
1729 task.status = status
1730 # Cancelling paused/failed downloads requires a call to .run()
1731 elif task.status in (task.PAUSED, task.FAILED):
1732 task.status = status
1733 # Call run, so the partial file gets deleted
1734 task.run()
1735 elif status == download.DownloadTask.PAUSED:
1736 # Pausing a download only when queued/downloading
1737 if task.status in (task.DOWNLOADING, task.QUEUED):
1738 task.status = status
1739 elif status is None:
1740 # Remove the selected task - cancel downloading/queued tasks
1741 if task.status in (task.QUEUED, task.DOWNLOADING):
1742 task.status = task.CANCELLED
1743 model.remove(model.get_iter(row_reference.get_path()))
1744 # Remember the URL, so we can tell the UI to update
1745 try:
1746 # We don't "see" this task anymore - remove it;
1747 # this is needed, so update_episode_list_icons()
1748 # below gets the correct list of "seen" tasks
1749 self.download_tasks_seen.remove(task)
1750 except KeyError, key_error:
1751 log('Cannot remove task from "seen" list: %s', task, sender=self)
1752 episode_urls.add(task.url)
1753 # Tell the task that it has been removed (so it can clean up)
1754 task.removed_from_list()
1755 else:
1756 # We can (hopefully) simply set the task status here
1757 task.status = status
1758 # Tell the podcasts tab to update icons for our removed podcasts
1759 self.update_episode_list_icons(episode_urls)
1760 # Update the tab title and downloads list
1761 self.update_downloads_list()
1763 def treeview_downloads_show_context_menu(self, treeview, event):
1764 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1765 if not paths:
1766 if not hasattr(treeview, 'is_rubber_banding_active'):
1767 return True
1768 else:
1769 return not treeview.is_rubber_banding_active()
1771 if event.button == self.context_menu_mouse_button:
1772 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1773 self.downloads_list_get_selection(model, paths)
1775 def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1776 # This creates a menu item for selection-wide actions
1777 item = gtk.ImageMenuItem(label)
1778 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1779 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1780 item.set_sensitive(sensitive)
1781 return self.set_finger_friendly(item)
1783 menu = gtk.Menu()
1785 item = gtk.ImageMenuItem(_('Episode details'))
1786 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1787 if len(selected_tasks) == 1:
1788 row_reference, task = selected_tasks[0]
1789 episode = task.episode
1790 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1791 else:
1792 item.set_sensitive(False)
1793 menu.append(self.set_finger_friendly(item))
1794 menu.append(gtk.SeparatorMenuItem())
1795 if can_force:
1796 menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1797 else:
1798 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1799 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1800 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1801 menu.append(gtk.SeparatorMenuItem())
1802 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1804 if gpodder.ui.maemo or self.config.enable_fingerscroll:
1805 # Because we open the popup on left-click for Maemo,
1806 # we also include a non-action to close the menu
1807 menu.append(gtk.SeparatorMenuItem())
1808 item = gtk.ImageMenuItem(_('Close this menu'))
1809 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1811 menu.append(self.set_finger_friendly(item))
1813 menu.show_all()
1814 menu.popup(None, None, None, event.button, event.time)
1815 return True
1817 def treeview_channels_show_context_menu(self, treeview, event):
1818 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1819 if not paths:
1820 return True
1822 # Check for valid channel id, if there's no id then
1823 # assume that it is a proxy channel or equivalent
1824 # and cannot be operated with right click
1825 if self.active_channel.id is None:
1826 return True
1828 if event.button == 3:
1829 menu = gtk.Menu()
1831 ICON = lambda x: x
1833 item = gtk.ImageMenuItem( _('Update podcast'))
1834 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1835 item.connect('activate', self.on_itemUpdateChannel_activate)
1836 item.set_sensitive(not self.updating_feed_cache)
1837 menu.append(item)
1839 menu.append(gtk.SeparatorMenuItem())
1841 item = gtk.CheckMenuItem(_('Keep episodes'))
1842 item.set_active(self.active_channel.channel_is_locked)
1843 item.connect('activate', self.on_channel_toggle_lock_activate)
1844 menu.append(self.set_finger_friendly(item))
1846 item = gtk.ImageMenuItem(_('Remove podcast'))
1847 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1848 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1849 menu.append( item)
1851 if self.config.device_type != 'none':
1852 item = gtk.MenuItem(_('Synchronize to device'))
1853 item.connect('activate', lambda item: self.on_sync_to_ipod_activate(item, self.active_channel.get_downloaded_episodes()))
1854 menu.append(item)
1856 menu.append( gtk.SeparatorMenuItem())
1858 item = gtk.ImageMenuItem(_('Podcast details'))
1859 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1860 item.connect('activate', self.on_itemEditChannel_activate)
1861 menu.append(item)
1863 menu.show_all()
1864 # Disable tooltips while we are showing the menu, so
1865 # the tooltip will not appear over the menu
1866 self.treeview_allow_tooltips(self.treeChannels, False)
1867 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1868 menu.popup( None, None, None, event.button, event.time)
1870 return True
1872 def on_itemClose_activate(self, widget):
1873 if self.tray_icon is not None:
1874 self.iconify_main_window()
1875 else:
1876 self.on_gPodder_delete_event(widget)
1878 def cover_file_removed(self, channel_url):
1880 The Cover Downloader calls this when a previously-
1881 available cover has been removed from the disk. We
1882 have to update our model to reflect this change.
1884 self.podcast_list_model.delete_cover_by_url(channel_url)
1886 def cover_download_finished(self, channel, pixbuf):
1888 The Cover Downloader calls this when it has finished
1889 downloading (or registering, if already downloaded)
1890 a new channel cover, which is ready for displaying.
1892 self.podcast_list_model.add_cover_by_channel(channel, pixbuf)
1894 def save_episodes_as_file(self, episodes):
1895 for episode in episodes:
1896 self.save_episode_as_file(episode)
1898 def save_episode_as_file(self, episode):
1899 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1900 if episode.was_downloaded(and_exists=True):
1901 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1902 copy_from = episode.local_filename(create=False)
1903 assert copy_from is not None
1904 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1905 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1906 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1908 def copy_episodes_bluetooth(self, episodes):
1909 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1911 if gpodder.ui.maemo:
1912 util.bluetooth_send_files_maemo([e.local_filename(create=False) \
1913 for e in episodes_to_copy])
1914 return True
1916 def convert_and_send_thread(episode):
1917 for episode in episodes:
1918 filename = episode.local_filename(create=False)
1919 assert filename is not None
1920 destfile = os.path.join(tempfile.gettempdir(), \
1921 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1922 (base, ext) = os.path.splitext(filename)
1923 if not destfile.endswith(ext):
1924 destfile += ext
1926 try:
1927 shutil.copyfile(filename, destfile)
1928 util.bluetooth_send_file(destfile)
1929 except:
1930 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1931 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1933 util.delete_file(destfile)
1935 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1937 def get_device_name(self):
1938 if self.config.device_type == 'ipod':
1939 return _('iPod')
1940 elif self.config.device_type in ('filesystem', 'mtp'):
1941 return _('MP3 player')
1942 else:
1943 return '(unknown device)'
1945 def _treeview_button_released(self, treeview, event):
1946 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1947 dy = int(abs(event.y-ypos))
1948 dx = int(event.x-xpos)
1950 selection = treeview.get_selection()
1951 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1952 if path is None or dy > 30:
1953 return (False, dx, dy)
1955 path, column, x, y = path
1956 selection.select_path(path)
1957 treeview.set_cursor(path)
1958 treeview.grab_focus()
1960 return (True, dx, dy)
1962 def treeview_channels_handle_gestures(self, treeview, event):
1963 if self.currently_updating:
1964 return False
1966 selected, dx, dy = self._treeview_button_released(treeview, event)
1968 if selected:
1969 if self.config.maemo_enable_gestures:
1970 if dx > 70:
1971 self.on_itemUpdateChannel_activate()
1972 elif dx < -70:
1973 self.on_itemEditChannel_activate(treeview)
1975 return False
1977 def treeview_available_handle_gestures(self, treeview, event):
1978 selected, dx, dy = self._treeview_button_released(treeview, event)
1980 if selected:
1981 if self.config.maemo_enable_gestures:
1982 if dx > 70:
1983 self.on_playback_selected_episodes(None)
1984 return True
1985 elif dx < -70:
1986 self.on_shownotes_selected_episodes(None)
1987 return True
1989 # Pass the event to the context menu handler for treeAvailable
1990 self.treeview_available_show_context_menu(treeview, event)
1992 return True
1994 def treeview_available_show_context_menu(self, treeview, event):
1995 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1996 if not paths:
1997 if not hasattr(treeview, 'is_rubber_banding_active'):
1998 return True
1999 else:
2000 return not treeview.is_rubber_banding_active()
2002 if event.button == self.context_menu_mouse_button:
2003 episodes = self.get_selected_episodes()
2004 any_locked = any(e.is_locked for e in episodes)
2005 any_played = any(e.is_played for e in episodes)
2006 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
2007 downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
2008 downloading = any(self.episode_is_downloading(e) for e in episodes)
2010 menu = gtk.Menu()
2012 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
2014 if open_instead_of_play:
2015 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
2016 elif downloaded:
2017 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
2018 else:
2019 item = gtk.ImageMenuItem(_('Stream'))
2020 item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
2022 item.set_sensitive(can_play and not downloading)
2023 item.connect('activate', self.on_playback_selected_episodes)
2024 menu.append(self.set_finger_friendly(item))
2026 if not can_cancel:
2027 item = gtk.ImageMenuItem(_('Download'))
2028 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
2029 item.set_sensitive(can_download)
2030 item.connect('activate', self.on_download_selected_episodes)
2031 menu.append(self.set_finger_friendly(item))
2032 else:
2033 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
2034 item.connect('activate', self.on_item_cancel_download_activate)
2035 menu.append(self.set_finger_friendly(item))
2037 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
2038 item.set_sensitive(can_delete)
2039 item.connect('activate', self.on_btnDownloadedDelete_clicked)
2040 menu.append(self.set_finger_friendly(item))
2042 ICON = lambda x: x
2044 # Ok, this probably makes sense to only display for downloaded files
2045 if downloaded:
2046 menu.append(gtk.SeparatorMenuItem())
2047 share_item = gtk.MenuItem(_('Send to'))
2048 menu.append(self.set_finger_friendly(share_item))
2049 share_menu = gtk.Menu()
2051 item = gtk.ImageMenuItem(_('Local folder'))
2052 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
2053 item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
2054 share_menu.append(self.set_finger_friendly(item))
2055 if self.bluetooth_available:
2056 item = gtk.ImageMenuItem(_('Bluetooth device'))
2057 if gpodder.ui.maemo:
2058 icon_name = ICON('qgn_list_filesys_bluetooth')
2059 else:
2060 icon_name = ICON('bluetooth')
2061 item.set_image(gtk.image_new_from_icon_name(icon_name, gtk.ICON_SIZE_MENU))
2062 item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
2063 share_menu.append(self.set_finger_friendly(item))
2064 if can_transfer:
2065 item = gtk.ImageMenuItem(self.get_device_name())
2066 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
2067 item.connect('button-press-event', lambda w, ee: self.on_sync_to_ipod_activate(w, episodes))
2068 share_menu.append(self.set_finger_friendly(item))
2070 share_item.set_submenu(share_menu)
2072 if (downloaded or one_is_new or can_download) and not downloading:
2073 menu.append(gtk.SeparatorMenuItem())
2074 if one_is_new:
2075 item = gtk.CheckMenuItem(_('New'))
2076 item.set_active(True)
2077 item.connect('activate', lambda w: self.mark_selected_episodes_old())
2078 menu.append(self.set_finger_friendly(item))
2079 elif can_download:
2080 item = gtk.CheckMenuItem(_('New'))
2081 item.set_active(False)
2082 item.connect('activate', lambda w: self.mark_selected_episodes_new())
2083 menu.append(self.set_finger_friendly(item))
2085 if downloaded:
2086 item = gtk.CheckMenuItem(_('Played'))
2087 item.set_active(any_played)
2088 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
2089 menu.append(self.set_finger_friendly(item))
2091 item = gtk.CheckMenuItem(_('Keep episode'))
2092 item.set_active(any_locked)
2093 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
2094 menu.append(self.set_finger_friendly(item))
2096 menu.append(gtk.SeparatorMenuItem())
2097 # Single item, add episode information menu item
2098 item = gtk.ImageMenuItem(_('Episode details'))
2099 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
2100 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
2101 menu.append(self.set_finger_friendly(item))
2103 if gpodder.ui.maemo or self.config.enable_fingerscroll:
2104 # Because we open the popup on left-click for Maemo,
2105 # we also include a non-action to close the menu
2106 menu.append(gtk.SeparatorMenuItem())
2107 item = gtk.ImageMenuItem(_('Close this menu'))
2108 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
2109 menu.append(self.set_finger_friendly(item))
2111 menu.show_all()
2112 # Disable tooltips while we are showing the menu, so
2113 # the tooltip will not appear over the menu
2114 self.treeview_allow_tooltips(self.treeAvailable, False)
2115 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
2116 menu.popup( None, None, None, event.button, event.time)
2118 return True
2120 def set_title(self, new_title):
2121 if not gpodder.ui.fremantle:
2122 self.default_title = new_title
2123 self.gPodder.set_title(new_title)
2125 def update_episode_list_icons(self, urls=None, selected=False, all=False):
2127 Updates the status icons in the episode list.
2129 If urls is given, it should be a list of URLs
2130 of episodes that should be updated.
2132 If urls is None, set ONE OF selected, all to
2133 True (the former updates just the selected
2134 episodes and the latter updates all episodes).
2136 additional_args = (self.episode_is_downloading, \
2137 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2138 self.config.episode_list_thumbnails and gpodder.ui.desktop)
2140 if urls is not None:
2141 # We have a list of URLs to walk through
2142 self.episode_list_model.update_by_urls(urls, *additional_args)
2143 elif selected and not all:
2144 # We should update all selected episodes
2145 selection = self.treeAvailable.get_selection()
2146 model, paths = selection.get_selected_rows()
2147 for path in reversed(paths):
2148 iter = model.get_iter(path)
2149 self.episode_list_model.update_by_filter_iter(iter, \
2150 *additional_args)
2151 elif all and not selected:
2152 # We update all (even the filter-hidden) episodes
2153 self.episode_list_model.update_all(*additional_args)
2154 else:
2155 # Wrong/invalid call - have to specify at least one parameter
2156 raise ValueError('Invalid call to update_episode_list_icons')
2158 def episode_list_status_changed(self, episodes):
2159 self.update_episode_list_icons(set(e.url for e in episodes))
2160 self.update_podcast_list_model(set(e.channel.url for e in episodes))
2161 self.db.commit()
2163 def clean_up_downloads(self, delete_partial=False):
2164 # Clean up temporary files left behind by old gPodder versions
2165 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
2167 if delete_partial:
2168 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
2170 for tempfile in temporary_files:
2171 util.delete_file(tempfile)
2173 # Clean up empty download folders and abandoned download folders
2174 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
2175 for ddir in download_dirs:
2176 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2177 globr = glob.glob(os.path.join(ddir, '*'))
2178 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
2179 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
2180 shutil.rmtree(ddir, ignore_errors=True)
2182 def streaming_possible(self):
2183 if gpodder.ui.desktop:
2184 # User has to have a media player set on the Desktop, or else we
2185 # would probably open the browser when giving a URL to xdg-open..
2186 return (self.config.player and self.config.player != 'default')
2187 elif gpodder.ui.maemo:
2188 # On Maemo, the default is to use the Nokia Media Player, which is
2189 # already able to deal with HTTP URLs the right way, so we
2190 # unconditionally enable streaming always on Maemo
2191 return True
2193 return False
2195 def playback_episodes_for_real(self, episodes):
2196 groups = collections.defaultdict(list)
2197 for episode in episodes:
2198 file_type = episode.file_type()
2199 if file_type == 'video' and self.config.videoplayer and \
2200 self.config.videoplayer != 'default':
2201 player = self.config.videoplayer
2202 if gpodder.ui.diablo:
2203 # Use the wrapper script if it's installed to crop 3GP YouTube
2204 # videos to fit the screen (looks much nicer than w/ black border)
2205 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2206 player = 'gpodder-mplayer'
2207 elif gpodder.ui.fremantle and player == 'mplayer':
2208 player = 'mplayer -fs %F'
2209 elif file_type == 'audio' and self.config.player and \
2210 self.config.player != 'default':
2211 player = self.config.player
2212 else:
2213 player = 'default'
2215 if file_type not in ('audio', 'video') or \
2216 (file_type == 'audio' and not self.config.audio_played_dbus) or \
2217 (file_type == 'video' and not self.config.video_played_dbus):
2218 # Mark episode as played in the database
2219 episode.mark(is_played=True)
2220 self.mygpo_client.on_playback([episode])
2222 filename = episode.local_filename(create=False)
2223 if filename is None or not os.path.exists(filename):
2224 filename = episode.url
2225 if youtube.is_video_link(filename):
2226 fmt_id = self.config.youtube_preferred_fmt_id
2227 if gpodder.ui.fremantle:
2228 fmt_id = 5
2229 filename = youtube.get_real_download_url(filename, fmt_id)
2231 # Determine the playback resume position - if the file
2232 # was played 100%, we simply start from the beginning
2233 resume_position = episode.current_position
2234 if resume_position == episode.total_time:
2235 resume_position = 0
2237 if gpodder.ui.fremantle:
2238 self.mafw_monitor.set_resume_point(filename, resume_position)
2240 # If Panucci is configured, use D-Bus on Maemo to call it
2241 if player == 'panucci':
2242 try:
2243 PANUCCI_NAME = 'org.panucci.panucciInterface'
2244 PANUCCI_PATH = '/panucciInterface'
2245 PANUCCI_INTF = 'org.panucci.panucciInterface'
2246 o = gpodder.dbus_session_bus.get_object(PANUCCI_NAME, PANUCCI_PATH)
2247 i = dbus.Interface(o, PANUCCI_INTF)
2249 def on_reply(*args):
2250 pass
2252 def error_handler(filename, err):
2253 log('Exception in D-Bus call: %s', str(err), \
2254 sender=self)
2256 # Fallback: use the command line client
2257 for command in util.format_desktop_command('panucci', \
2258 [filename]):
2259 log('Executing: %s', repr(command), sender=self)
2260 subprocess.Popen(command)
2262 on_error = lambda err: error_handler(filename, err)
2264 # This method only exists in Panucci > 0.9 ('new Panucci')
2265 i.playback_from(filename, resume_position, \
2266 reply_handler=on_reply, error_handler=on_error)
2268 continue # This file was handled by the D-Bus call
2269 except Exception, e:
2270 log('Error calling Panucci using D-Bus', sender=self, traceback=True)
2271 elif player == 'MediaBox' and gpodder.ui.maemo:
2272 try:
2273 MEDIABOX_NAME = 'de.pycage.mediabox'
2274 MEDIABOX_PATH = '/de/pycage/mediabox/control'
2275 MEDIABOX_INTF = 'de.pycage.mediabox.control'
2276 o = gpodder.dbus_session_bus.get_object(MEDIABOX_NAME, MEDIABOX_PATH)
2277 i = dbus.Interface(o, MEDIABOX_INTF)
2279 def on_reply(*args):
2280 pass
2282 def on_error(err):
2283 log('Exception in D-Bus call: %s', str(err), \
2284 sender=self)
2286 i.load(filename, '%s/x-unknown' % file_type, \
2287 reply_handler=on_reply, error_handler=on_error)
2289 continue # This file was handled by the D-Bus call
2290 except Exception, e:
2291 log('Error calling MediaBox using D-Bus', sender=self, traceback=True)
2293 groups[player].append(filename)
2295 # Open episodes with system default player
2296 if 'default' in groups:
2297 if gpodder.ui.maemo and len(groups['default']) > 1:
2298 # The Nokia Media Player app does not support receiving multiple
2299 # file names via D-Bus, so we simply place all file names into a
2300 # temporary M3U playlist and open that with the Media Player.
2301 m3u_filename = os.path.join(gpodder.home, 'gpodder_open_with.m3u')
2302 util.write_m3u_playlist(m3u_filename, groups['default'], extm3u=False)
2303 util.gui_open(m3u_filename)
2304 else:
2305 for filename in groups['default']:
2306 log('Opening with system default: %s', filename, sender=self)
2307 util.gui_open(filename)
2308 del groups['default']
2309 elif gpodder.ui.maemo and groups:
2310 # When on Maemo and not opening with default, show a notification
2311 # (no startup notification for Panucci / MPlayer yet...)
2312 if len(episodes) == 1:
2313 text = _('Opening %s') % episodes[0].title
2314 else:
2315 count = len(episodes)
2316 text = N_('Opening %(count)d episode', 'Opening %(count)d episodes', count) % {'count':count}
2318 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2320 def destroy_banner_later(banner):
2321 banner.destroy()
2322 return False
2323 gobject.timeout_add(5000, destroy_banner_later, banner)
2325 # For each type now, go and create play commands
2326 for group in groups:
2327 for command in util.format_desktop_command(group, groups[group]):
2328 log('Executing: %s', repr(command), sender=self)
2329 subprocess.Popen(command)
2331 # Persist episode status changes to the database
2332 self.db.commit()
2334 # Flush updated episode status
2335 self.mygpo_client.flush()
2337 def playback_episodes(self, episodes):
2338 # We need to create a list, because we run through it more than once
2339 episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2340 e.was_downloaded(and_exists=True) or self.streaming_possible()))
2342 try:
2343 self.playback_episodes_for_real(episodes)
2344 except Exception, e:
2345 log('Error in playback!', sender=self, traceback=True)
2346 if gpodder.ui.desktop:
2347 self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2348 _('Error opening player'), widget=self.toolPreferences)
2349 else:
2350 self.show_message(_('Please check your media player settings in the preferences dialog.'))
2352 channel_urls = set()
2353 episode_urls = set()
2354 for episode in episodes:
2355 channel_urls.add(episode.channel.url)
2356 episode_urls.add(episode.url)
2357 self.update_episode_list_icons(episode_urls)
2358 self.update_podcast_list_model(channel_urls)
2360 def play_or_download(self):
2361 if not gpodder.ui.fremantle:
2362 if self.wNotebook.get_current_page() > 0:
2363 if gpodder.ui.desktop:
2364 self.toolCancel.set_sensitive(True)
2365 return
2367 if self.currently_updating:
2368 return (False, False, False, False, False, False)
2370 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2371 ( is_played, is_locked ) = (False,)*2
2373 open_instead_of_play = False
2375 selection = self.treeAvailable.get_selection()
2376 if selection.count_selected_rows() > 0:
2377 (model, paths) = selection.get_selected_rows()
2379 for path in paths:
2380 try:
2381 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2382 except TypeError, te:
2383 log('Invalid episode at path %s', str(path), sender=self)
2384 continue
2386 if episode.file_type() not in ('audio', 'video'):
2387 open_instead_of_play = True
2389 if episode.was_downloaded():
2390 can_play = episode.was_downloaded(and_exists=True)
2391 is_played = episode.is_played
2392 is_locked = episode.is_locked
2393 if not can_play:
2394 can_download = True
2395 else:
2396 if self.episode_is_downloading(episode):
2397 can_cancel = True
2398 else:
2399 can_download = True
2401 can_download = can_download and not can_cancel
2402 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2403 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2404 can_delete = not can_cancel
2406 if gpodder.ui.desktop:
2407 if open_instead_of_play:
2408 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2409 else:
2410 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2411 self.toolPlay.set_sensitive( can_play)
2412 self.toolDownload.set_sensitive( can_download)
2413 self.toolTransfer.set_sensitive( can_transfer)
2414 self.toolCancel.set_sensitive( can_cancel)
2416 if not gpodder.ui.fremantle:
2417 self.item_cancel_download.set_sensitive(can_cancel)
2418 self.itemDownloadSelected.set_sensitive(can_download)
2419 self.itemOpenSelected.set_sensitive(can_play)
2420 self.itemPlaySelected.set_sensitive(can_play)
2421 self.itemDeleteSelected.set_sensitive(can_delete)
2422 self.item_toggle_played.set_sensitive(can_play)
2423 self.item_toggle_lock.set_sensitive(can_play)
2424 self.itemOpenSelected.set_visible(open_instead_of_play)
2425 self.itemPlaySelected.set_visible(not open_instead_of_play)
2427 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2429 def on_cbMaxDownloads_toggled(self, widget, *args):
2430 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2432 def on_cbLimitDownloads_toggled(self, widget, *args):
2433 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2435 def episode_new_status_changed(self, urls):
2436 self.update_podcast_list_model()
2437 self.update_episode_list_icons(urls)
2439 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2440 """Update the podcast list treeview model
2442 If urls is given, it should list the URLs of each
2443 podcast that has to be updated in the list.
2445 If selected is True, only update the model contents
2446 for the currently-selected podcast - nothing more.
2448 The caller can optionally specify "select_url",
2449 which is the URL of the podcast that is to be
2450 selected in the list after the update is complete.
2451 This only works if the podcast list has to be
2452 reloaded; i.e. something has been added or removed
2453 since the last update of the podcast list).
2455 selection = self.treeChannels.get_selection()
2456 model, iter = selection.get_selected()
2458 if self.config.podcast_list_view_all and not self.channel_list_changed:
2459 # Update "all episodes" view in any case (if enabled)
2460 self.podcast_list_model.update_first_row()
2462 if selected:
2463 # very cheap! only update selected channel
2464 if iter is not None:
2465 # If we have selected the "all episodes" view, we have
2466 # to update all channels for selected episodes:
2467 if self.config.podcast_list_view_all and \
2468 self.podcast_list_model.iter_is_first_row(iter):
2469 urls = self.get_podcast_urls_from_selected_episodes()
2470 self.podcast_list_model.update_by_urls(urls)
2471 else:
2472 # Otherwise just update the selected row (a podcast)
2473 self.podcast_list_model.update_by_filter_iter(iter)
2474 elif not self.channel_list_changed:
2475 # we can keep the model, but have to update some
2476 if urls is None:
2477 # still cheaper than reloading the whole list
2478 self.podcast_list_model.update_all()
2479 else:
2480 # ok, we got a bunch of urls to update
2481 self.podcast_list_model.update_by_urls(urls)
2482 else:
2483 if model and iter and select_url is None:
2484 # Get the URL of the currently-selected podcast
2485 select_url = model.get_value(iter, PodcastListModel.C_URL)
2487 # Update the podcast list model with new channels
2488 self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2490 try:
2491 selected_iter = model.get_iter_first()
2492 # Find the previously-selected URL in the new
2493 # model if we have an URL (else select first)
2494 if select_url is not None:
2495 pos = model.get_iter_first()
2496 while pos is not None:
2497 url = model.get_value(pos, PodcastListModel.C_URL)
2498 if url == select_url:
2499 selected_iter = pos
2500 break
2501 pos = model.iter_next(pos)
2503 if not gpodder.ui.maemo:
2504 if selected_iter is not None:
2505 selection.select_iter(selected_iter)
2506 self.on_treeChannels_cursor_changed(self.treeChannels)
2507 except:
2508 log('Cannot select podcast in list', traceback=True, sender=self)
2509 self.channel_list_changed = False
2511 def episode_is_downloading(self, episode):
2512 """Returns True if the given episode is being downloaded at the moment"""
2513 if episode is None:
2514 return False
2516 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2518 def on_episode_list_filter_changed(self, has_episodes):
2519 if gpodder.ui.fremantle:
2520 if has_episodes:
2521 self.episodes_window.empty_label.hide()
2522 self.episodes_window.pannablearea.show()
2523 else:
2524 if self.config.episode_list_view_mode != \
2525 EpisodeListModel.VIEW_ALL:
2526 text = _('No episodes in current view')
2527 else:
2528 text = _('No episodes available')
2529 self.episodes_window.empty_label.set_text(text)
2530 self.episodes_window.pannablearea.hide()
2531 self.episodes_window.empty_label.show()
2533 def update_episode_list_model(self):
2534 if self.channels and self.active_channel is not None:
2535 if gpodder.ui.fremantle:
2536 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2538 self.currently_updating = True
2539 self.episode_list_model.clear()
2540 if gpodder.ui.fremantle:
2541 self.episodes_window.pannablearea.hide()
2542 self.episodes_window.empty_label.set_text(_('Loading episodes'))
2543 self.episodes_window.empty_label.show()
2545 def update():
2546 additional_args = (self.episode_is_downloading, \
2547 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2548 self.config.episode_list_thumbnails and gpodder.ui.desktop)
2549 self.episode_list_model.replace_from_channel(self.active_channel, *additional_args)
2551 self.treeAvailable.get_selection().unselect_all()
2552 self.treeAvailable.scroll_to_point(0, 0)
2554 self.currently_updating = False
2555 self.play_or_download()
2557 if gpodder.ui.fremantle:
2558 hildon.hildon_gtk_window_set_progress_indicator(\
2559 self.episodes_window.main_window, False)
2561 util.idle_add(update)
2562 else:
2563 self.episode_list_model.clear()
2565 @dbus.service.method(gpodder.dbus_interface)
2566 def offer_new_episodes(self, channels=None):
2567 if gpodder.ui.fremantle:
2568 # Assume that when this function is called that the
2569 # notification is not shown anymore (Maemo bug 11345)
2570 self._fremantle_notification_visible = False
2572 new_episodes = self.get_new_episodes(channels)
2573 if new_episodes:
2574 self.new_episodes_show(new_episodes)
2575 return True
2576 return False
2578 def add_podcast_list(self, urls, auth_tokens=None):
2579 """Subscribe to a list of podcast given their URLs
2581 If auth_tokens is given, it should be a dictionary
2582 mapping URLs to (username, password) tuples."""
2584 if auth_tokens is None:
2585 auth_tokens = {}
2587 # Sort and split the URL list into five buckets
2588 queued, failed, existing, worked, authreq = [], [], [], [], []
2589 for input_url in urls:
2590 url = util.normalize_feed_url(input_url)
2591 if url is None:
2592 # Fail this one because the URL is not valid
2593 failed.append(input_url)
2594 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2595 # A podcast already exists in the list for this URL
2596 existing.append(url)
2597 else:
2598 # This URL has survived the first round - queue for add
2599 queued.append(url)
2600 if url != input_url and input_url in auth_tokens:
2601 auth_tokens[url] = auth_tokens[input_url]
2603 error_messages = {}
2604 redirections = {}
2606 progress = ProgressIndicator(_('Adding podcasts'), \
2607 _('Please wait while episode information is downloaded.'), \
2608 parent=self.get_dialog_parent())
2610 def on_after_update():
2611 progress.on_finished()
2612 # Report already-existing subscriptions to the user
2613 if existing:
2614 title = _('Existing subscriptions skipped')
2615 message = _('You are already subscribed to these podcasts:') \
2616 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2617 self.show_message(message, title, widget=self.treeChannels)
2619 # Report subscriptions that require authentication
2620 if authreq:
2621 retry_podcasts = {}
2622 for url in authreq:
2623 title = _('Podcast requires authentication')
2624 message = _('Please login to %s:') % (saxutils.escape(url),)
2625 success, auth_tokens = self.show_login_dialog(title, message)
2626 if success:
2627 retry_podcasts[url] = auth_tokens
2628 else:
2629 # Stop asking the user for more login data
2630 retry_podcasts = {}
2631 for url in authreq:
2632 error_messages[url] = _('Authentication failed')
2633 failed.append(url)
2634 break
2636 # If we have authentication data to retry, do so here
2637 if retry_podcasts:
2638 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2640 # Report website redirections
2641 for url in redirections:
2642 title = _('Website redirection detected')
2643 message = _('The URL %(url)s redirects to %(target)s.') \
2644 + '\n\n' + _('Do you want to visit the website now?')
2645 message = message % {'url': url, 'target': redirections[url]}
2646 if self.show_confirmation(message, title):
2647 util.open_website(url)
2648 else:
2649 break
2651 # Report failed subscriptions to the user
2652 if failed:
2653 title = _('Could not add some podcasts')
2654 message = _('Some podcasts could not be added to your list:') \
2655 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2656 error_messages.get(url, _('Unknown')))) for url in failed)
2657 self.show_message(message, title, important=True)
2659 # Upload subscription changes to gpodder.net
2660 self.mygpo_client.on_subscribe(worked)
2662 # If at least one podcast has been added, save and update all
2663 if self.channel_list_changed:
2664 # Fix URLs if mygpo has rewritten them
2665 self.rewrite_urls_mygpo()
2667 self.save_channels_opml()
2669 # If only one podcast was added, select it after the update
2670 if len(worked) == 1:
2671 url = worked[0]
2672 else:
2673 url = None
2675 # Update the list of subscribed podcasts
2676 self.update_feed_cache(force_update=False, select_url_afterwards=url)
2677 self.update_podcasts_tab()
2679 # Offer to download new episodes
2680 episodes = []
2681 for podcast in self.channels:
2682 if podcast.url in worked:
2683 episodes.extend(podcast.get_all_episodes())
2685 if episodes:
2686 episodes = list(PodcastEpisode.sort_by_pubdate(episodes, \
2687 reverse=True))
2688 self.new_episodes_show(episodes, \
2689 selected=[e.check_is_new() for e in episodes])
2692 def thread_proc():
2693 # After the initial sorting and splitting, try all queued podcasts
2694 length = len(queued)
2695 for index, url in enumerate(queued):
2696 progress.on_progress(float(index)/float(length))
2697 progress.on_message(url)
2698 log('QUEUE RUNNER: %s', url, sender=self)
2699 try:
2700 # The URL is valid and does not exist already - subscribe!
2701 channel = PodcastChannel.load(self.db, url=url, create=True, \
2702 authentication_tokens=auth_tokens.get(url, None), \
2703 max_episodes=self.config.max_episodes_per_feed, \
2704 download_dir=self.config.download_dir, \
2705 allow_empty_feeds=self.config.allow_empty_feeds, \
2706 mimetype_prefs=self.config.mimetype_prefs)
2708 try:
2709 username, password = util.username_password_from_url(url)
2710 except ValueError, ve:
2711 username, password = (None, None)
2713 if username is not None and channel.username is None and \
2714 password is not None and channel.password is None:
2715 channel.username = username
2716 channel.password = password
2717 channel.save()
2719 self._update_cover(channel)
2720 except feedcore.AuthenticationRequired:
2721 if url in auth_tokens:
2722 # Fail for wrong authentication data
2723 error_messages[url] = _('Authentication failed')
2724 failed.append(url)
2725 else:
2726 # Queue for login dialog later
2727 authreq.append(url)
2728 continue
2729 except feedcore.WifiLogin, error:
2730 redirections[url] = error.data
2731 failed.append(url)
2732 error_messages[url] = _('Redirection detected')
2733 continue
2734 except Exception, e:
2735 log('Subscription error: %s', e, traceback=True, sender=self)
2736 error_messages[url] = str(e)
2737 failed.append(url)
2738 continue
2740 assert channel is not None
2741 worked.append(channel.url)
2742 self.channels.append(channel)
2743 self.channel_list_changed = True
2744 util.idle_add(on_after_update)
2745 threading.Thread(target=thread_proc).start()
2747 def save_channels_opml(self):
2748 exporter = opml.Exporter(gpodder.subscription_file)
2749 return exporter.write(self.channels)
2751 def find_episode(self, podcast_url, episode_url):
2752 """Find an episode given its podcast and episode URL
2754 The function will return a PodcastEpisode object if
2755 the episode is found, or None if it's not found.
2757 for podcast in self.channels:
2758 if podcast_url == podcast.url:
2759 for episode in podcast.get_all_episodes():
2760 if episode_url == episode.url:
2761 return episode
2763 return None
2765 def process_received_episode_actions(self, updated_urls):
2766 """Process/merge episode actions from gpodder.net
2768 This function will merge all changes received from
2769 the server to the local database and update the
2770 status of the affected episodes as necessary.
2772 indicator = ProgressIndicator(_('Merging episode actions'), \
2773 _('Episode actions from gpodder.net are merged.'), \
2774 False, self.get_dialog_parent())
2776 for idx, action in enumerate(self.mygpo_client.get_episode_actions(updated_urls)):
2777 if action.action == 'play':
2778 episode = self.find_episode(action.podcast_url, \
2779 action.episode_url)
2781 if episode is not None:
2782 log('Play action for %s', episode.url, sender=self)
2783 episode.mark(is_played=True)
2785 if action.timestamp > episode.current_position_updated and \
2786 action.position is not None:
2787 log('Updating position for %s', episode.url, sender=self)
2788 episode.current_position = action.position
2789 episode.current_position_updated = action.timestamp
2791 if action.total:
2792 log('Updating total time for %s', episode.url, sender=self)
2793 episode.total_time = action.total
2795 episode.save()
2796 elif action.action == 'delete':
2797 episode = self.find_episode(action.podcast_url, \
2798 action.episode_url)
2800 if episode is not None:
2801 if not episode.was_downloaded(and_exists=True):
2802 # Set the episode to a "deleted" state
2803 log('Marking as deleted: %s', episode.url, sender=self)
2804 episode.delete_from_disk()
2805 episode.save()
2807 indicator.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx) % {'count':idx})
2808 gtk.main_iteration(False)
2810 indicator.on_finished()
2811 self.db.commit()
2814 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2815 self.db.commit()
2816 self.updating_feed_cache = False
2818 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2820 # Process received episode actions for all updated URLs
2821 self.process_received_episode_actions(updated_urls)
2823 self.channel_list_changed = True
2824 self.update_podcast_list_model(select_url=select_url_afterwards)
2826 # Only search for new episodes in podcasts that have been
2827 # updated, not in other podcasts (for single-feed updates)
2828 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2830 if gpodder.ui.fremantle:
2831 self.fancy_progress_bar.hide()
2832 self.button_subscribe.set_sensitive(True)
2833 self.button_refresh.set_sensitive(True)
2834 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2835 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2836 self.update_podcasts_tab()
2837 self.update_episode_list_model()
2838 if self.feed_cache_update_cancelled:
2839 return
2841 def application_in_foreground():
2842 try:
2843 return any(w.get_property('is-topmost') for w in hildon.WindowStack.get_default().get_windows())
2844 except Exception, e:
2845 log('Could not determine is-topmost', traceback=True)
2846 # When in doubt, assume not in foreground
2847 return False
2849 if episodes:
2850 if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2851 # New episodes found, but we should do nothing
2852 self.show_message(_('New episodes are available.'))
2853 elif self.config.auto_download == 'always':
2854 count = len(episodes)
2855 title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2856 self.show_message(title)
2857 self.download_episode_list(episodes)
2858 elif self.config.auto_download == 'queue':
2859 self.show_message(_('New episodes have been added to the download list.'))
2860 self.download_episode_list_paused(episodes)
2861 elif application_in_foreground():
2862 if not self._fremantle_notification_visible:
2863 self.new_episodes_show(episodes)
2864 elif not self._fremantle_notification_visible:
2865 try:
2866 import pynotify
2867 pynotify.init('gPodder')
2868 n = pynotify.Notification('gPodder', _('New episodes available'), 'gpodder')
2869 n.set_urgency(pynotify.URGENCY_CRITICAL)
2870 n.set_hint('dbus-callback-default', ' '.join([
2871 gpodder.dbus_bus_name,
2872 gpodder.dbus_gui_object_path,
2873 gpodder.dbus_interface,
2874 'offer_new_episodes',
2876 n.set_category('gpodder-new-episodes')
2877 n.show()
2878 self._fremantle_notification_visible = True
2879 except Exception, e:
2880 log('Error: %s', str(e), sender=self, traceback=True)
2881 self.new_episodes_show(episodes)
2882 self._fremantle_notification_visible = False
2883 elif not self.config.auto_update_feeds:
2884 self.show_message(_('No new episodes. Please check for new episodes later.'))
2885 return
2887 if self.tray_icon:
2888 self.tray_icon.set_status()
2890 if self.feed_cache_update_cancelled:
2891 # The user decided to abort the feed update
2892 self.show_update_feeds_buttons()
2893 elif not episodes:
2894 # Nothing new here - but inform the user
2895 self.pbFeedUpdate.set_fraction(1.0)
2896 self.pbFeedUpdate.set_text(_('No new episodes'))
2897 self.feed_cache_update_cancelled = True
2898 self.btnCancelFeedUpdate.show()
2899 self.btnCancelFeedUpdate.set_sensitive(True)
2900 self.itemUpdate.set_sensitive(True)
2901 if gpodder.ui.maemo:
2902 # btnCancelFeedUpdate is a ToolButton on Maemo
2903 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2904 else:
2905 # btnCancelFeedUpdate is a normal gtk.Button
2906 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2907 else:
2908 count = len(episodes)
2909 # New episodes are available
2910 self.pbFeedUpdate.set_fraction(1.0)
2911 # Are we minimized and should we auto download?
2912 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2913 self.download_episode_list(episodes)
2914 title = N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count) % {'count':count}
2915 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2916 self.show_update_feeds_buttons()
2917 elif self.config.auto_download == 'queue':
2918 self.download_episode_list_paused(episodes)
2919 title = N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count) % {'count':count}
2920 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2921 self.show_update_feeds_buttons()
2922 else:
2923 self.show_update_feeds_buttons()
2924 # New episodes are available and we are not minimized
2925 if not self.config.do_not_show_new_episodes_dialog:
2926 self.new_episodes_show(episodes, notification=True)
2927 else:
2928 message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
2929 self.pbFeedUpdate.set_text(message)
2931 def _update_cover(self, channel):
2932 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2933 self.cover_downloader.request_cover(channel)
2935 def update_feed_cache_proc(self, channels, select_url_afterwards):
2936 total = len(channels)
2938 for updated, channel in enumerate(channels):
2939 if not self.feed_cache_update_cancelled:
2940 try:
2941 channel.update(max_episodes=self.config.max_episodes_per_feed, \
2942 mimetype_prefs=self.config.mimetype_prefs)
2943 self._update_cover(channel)
2944 except Exception, e:
2945 d = {'url': saxutils.escape(channel.url), 'message': saxutils.escape(str(e))}
2946 if d['message']:
2947 message = _('Error while updating %(url)s: %(message)s')
2948 else:
2949 message = _('The feed at %(url)s could not be updated.')
2950 self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
2951 log('Error: %s', str(e), sender=self, traceback=True)
2953 if self.feed_cache_update_cancelled:
2954 break
2956 # By the time we get here the update may have already been cancelled
2957 if not self.feed_cache_update_cancelled:
2958 def update_progress():
2959 d = {'podcast': channel.title, 'position': updated+1, 'total': total}
2960 progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2961 self.pbFeedUpdate.set_text(progression)
2962 if self.tray_icon:
2963 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2964 self.pbFeedUpdate.set_fraction(float(updated+1)/float(total))
2965 util.idle_add(update_progress)
2967 updated_urls = [c.url for c in channels]
2968 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2970 def show_update_feeds_buttons(self):
2971 # Make sure that the buttons for updating feeds
2972 # appear - this should happen after a feed update
2973 if gpodder.ui.maemo:
2974 self.btnUpdateSelectedFeed.show()
2975 self.toolFeedUpdateProgress.hide()
2976 self.btnCancelFeedUpdate.hide()
2977 self.btnCancelFeedUpdate.set_is_important(False)
2978 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2979 self.toolbarSpacer.set_expand(True)
2980 self.toolbarSpacer.set_draw(False)
2981 else:
2982 self.hboxUpdateFeeds.hide()
2983 self.btnUpdateFeeds.show()
2984 self.itemUpdate.set_sensitive(True)
2985 self.itemUpdateChannel.set_sensitive(True)
2987 def on_btnCancelFeedUpdate_clicked(self, widget):
2988 if not self.feed_cache_update_cancelled:
2989 self.pbFeedUpdate.set_text(_('Cancelling...'))
2990 self.feed_cache_update_cancelled = True
2991 if not gpodder.ui.fremantle:
2992 self.btnCancelFeedUpdate.set_sensitive(False)
2993 elif not gpodder.ui.fremantle:
2994 self.show_update_feeds_buttons()
2996 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
2997 if self.updating_feed_cache:
2998 if gpodder.ui.fremantle:
2999 self.feed_cache_update_cancelled = True
3000 return
3002 if not force_update:
3003 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
3004 self.channel_list_changed = True
3005 self.update_podcast_list_model(select_url=select_url_afterwards)
3006 return
3008 # Fix URLs if mygpo has rewritten them
3009 self.rewrite_urls_mygpo()
3011 self.updating_feed_cache = True
3013 if channels is None:
3014 # Only update podcasts for which updates are enabled
3015 channels = [c for c in self.channels if c.feed_update_enabled]
3017 if gpodder.ui.fremantle:
3018 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
3019 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
3020 self.fancy_progress_bar.show()
3021 self.button_subscribe.set_sensitive(False)
3022 self.button_refresh.set_sensitive(False)
3023 self.feed_cache_update_cancelled = False
3024 else:
3025 self.itemUpdate.set_sensitive(False)
3026 self.itemUpdateChannel.set_sensitive(False)
3028 if self.tray_icon:
3029 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
3031 self.feed_cache_update_cancelled = False
3032 self.btnCancelFeedUpdate.show()
3033 self.btnCancelFeedUpdate.set_sensitive(True)
3034 if gpodder.ui.maemo:
3035 self.toolbarSpacer.set_expand(False)
3036 self.toolbarSpacer.set_draw(True)
3037 self.btnUpdateSelectedFeed.hide()
3038 self.toolFeedUpdateProgress.show_all()
3039 else:
3040 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
3041 self.hboxUpdateFeeds.show_all()
3042 self.btnUpdateFeeds.hide()
3044 if len(channels) == 1:
3045 text = _('Updating "%s"...') % channels[0].title
3046 else:
3047 count = len(channels)
3048 text = N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count) % {'count':count}
3049 self.pbFeedUpdate.set_text(text)
3050 self.pbFeedUpdate.set_fraction(0)
3052 args = (channels, select_url_afterwards)
3053 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
3055 def on_gPodder_delete_event(self, widget, *args):
3056 """Called when the GUI wants to close the window
3057 Displays a confirmation dialog (and closes/hides gPodder)
3060 downloading = self.download_status_model.are_downloads_in_progress()
3062 # Only iconify if we are using the window's "X" button,
3063 # but not when we are using "Quit" in the menu or toolbar
3064 if self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
3065 self.iconify_main_window()
3066 elif downloading:
3067 if gpodder.ui.fremantle:
3068 self.close_gpodder()
3069 elif gpodder.ui.diablo:
3070 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
3071 if result:
3072 self.close_gpodder()
3073 else:
3074 return True
3075 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
3076 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3077 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
3079 title = _('Quit gPodder')
3080 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3082 dialog.set_title(title)
3083 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
3085 quit_button.grab_focus()
3086 result = dialog.run()
3087 dialog.destroy()
3089 if result == gtk.RESPONSE_CLOSE:
3090 self.close_gpodder()
3091 else:
3092 self.close_gpodder()
3094 return True
3096 def close_gpodder(self):
3097 """ clean everything and exit properly
3099 if self.channels:
3100 if self.save_channels_opml():
3101 pass # FIXME: Add mygpo synchronization here
3102 else:
3103 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
3105 self.gPodder.hide()
3107 if self.tray_icon is not None:
3108 self.tray_icon.set_visible(False)
3110 # Notify all tasks to to carry out any clean-up actions
3111 self.download_status_model.tell_all_tasks_to_quit()
3113 while gtk.events_pending():
3114 gtk.main_iteration(False)
3116 self.db.close()
3118 self.quit()
3119 sys.exit(0)
3121 def get_expired_episodes(self):
3122 for channel in self.channels:
3123 for episode in channel.get_downloaded_episodes():
3124 # Never consider locked episodes as old
3125 if episode.is_locked:
3126 continue
3128 # Never consider fresh episodes as old
3129 if episode.age_in_days() < self.config.episode_old_age:
3130 continue
3132 # Do not delete played episodes (except if configured)
3133 if episode.is_played:
3134 if not self.config.auto_remove_played_episodes:
3135 continue
3137 # Do not delete unplayed episodes (except if configured)
3138 if not episode.is_played:
3139 if not self.config.auto_remove_unplayed_episodes:
3140 continue
3142 yield episode
3144 def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
3145 if not episodes:
3146 return False
3148 if skip_locked:
3149 episodes = [e for e in episodes if not e.is_locked]
3151 if not episodes:
3152 title = _('Episodes are locked')
3153 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3154 self.notification(message, title, widget=self.treeAvailable)
3155 return False
3157 count = len(episodes)
3158 title = N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count) % {'count':count}
3159 message = _('Deleting episodes removes downloaded files.')
3161 if gpodder.ui.fremantle:
3162 message = '\n'.join([title, message])
3164 if confirm and not self.show_confirmation(message, title):
3165 return False
3167 progress = ProgressIndicator(_('Deleting episodes'), \
3168 _('Please wait while episodes are deleted'), \
3169 parent=self.get_dialog_parent())
3171 def finish_deletion(episode_urls, channel_urls):
3172 progress.on_finished()
3174 # Episodes have been deleted - persist the database
3175 self.db.commit()
3177 self.update_episode_list_icons(episode_urls)
3178 self.update_podcast_list_model(channel_urls)
3179 self.play_or_download()
3181 def thread_proc():
3182 episode_urls = set()
3183 channel_urls = set()
3185 episodes_status_update = []
3186 for idx, episode in enumerate(episodes):
3187 progress.on_progress(float(idx)/float(len(episodes)))
3188 if episode.is_locked and skip_locked:
3189 log('Not deleting episode (is locked): %s', episode.title)
3190 else:
3191 log('Deleting episode: %s', episode.title)
3192 progress.on_message(episode.title)
3193 episode.delete_from_disk()
3194 episode_urls.add(episode.url)
3195 channel_urls.add(episode.channel.url)
3196 episodes_status_update.append(episode)
3198 # Tell the shownotes window that we have removed the episode
3199 if self.episode_shownotes_window is not None and \
3200 self.episode_shownotes_window.episode is not None and \
3201 self.episode_shownotes_window.episode.url == episode.url:
3202 util.idle_add(self.episode_shownotes_window._download_status_changed, None)
3204 # Notify the web service about the status update + upload
3205 self.mygpo_client.on_delete(episodes_status_update)
3206 self.mygpo_client.flush()
3208 util.idle_add(finish_deletion, episode_urls, channel_urls)
3210 threading.Thread(target=thread_proc).start()
3212 return True
3214 def on_itemRemoveOldEpisodes_activate(self, widget):
3215 self.show_delete_episodes_window()
3217 def show_delete_episodes_window(self, channel=None):
3218 """Offer deletion of episodes
3220 If channel is None, offer deletion of all episodes.
3221 Otherwise only offer deletion of episodes in the channel.
3223 if gpodder.ui.maemo:
3224 columns = (
3225 ('maemo_remove_markup', None, None, _('Episode')),
3227 else:
3228 columns = (
3229 ('title_markup', None, None, _('Episode')),
3230 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3231 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3232 ('played_prop', None, None, _('Status')),
3233 ('age_prop', 'age_int_prop', gobject.TYPE_INT, _('Downloaded')),
3236 msg_older_than = N_('Select older than %(count)d day', 'Select older than %(count)d days', self.config.episode_old_age)
3237 selection_buttons = {
3238 _('Select played'): lambda episode: episode.is_played,
3239 _('Select finished'): lambda episode: episode.is_finished(),
3240 msg_older_than % {'count':self.config.episode_old_age}: lambda episode: episode.age_in_days() > self.config.episode_old_age,
3243 instructions = _('Select the episodes you want to delete:')
3245 if channel is None:
3246 channels = self.channels
3247 else:
3248 channels = [channel]
3250 episodes = []
3251 for channel in channels:
3252 for episode in channel.get_downloaded_episodes():
3253 # Disallow deletion of locked episodes that still exist
3254 if not episode.is_locked or not episode.file_exists():
3255 episodes.append(episode)
3257 selected = [e for e in episodes if episode.is_played or not episode.file_exists()]
3259 gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
3260 episodes = episodes, selected = selected, columns = columns, \
3261 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
3262 selection_buttons = selection_buttons, _config=self.config, \
3263 show_episode_shownotes=self.show_episode_shownotes)
3265 def on_selected_episodes_status_changed(self):
3266 # The order of the updates here is important! When "All episodes" is
3267 # selected, the update of the podcast list model depends on the episode
3268 # list selection to determine which podcasts are affected. Updating
3269 # the episode list could remove the selection if a filter is active.
3270 self.update_podcast_list_model(selected=True)
3271 self.update_episode_list_icons(selected=True)
3272 self.db.commit()
3274 def mark_selected_episodes_new(self):
3275 for episode in self.get_selected_episodes():
3276 episode.mark_new()
3277 self.on_selected_episodes_status_changed()
3279 def mark_selected_episodes_old(self):
3280 for episode in self.get_selected_episodes():
3281 episode.mark_old()
3282 self.on_selected_episodes_status_changed()
3284 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
3285 for episode in self.get_selected_episodes():
3286 if toggle:
3287 episode.mark(is_played=not episode.is_played)
3288 else:
3289 episode.mark(is_played=new_value)
3290 self.on_selected_episodes_status_changed()
3292 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3293 for episode in self.get_selected_episodes():
3294 if toggle:
3295 episode.mark(is_locked=not episode.is_locked)
3296 else:
3297 episode.mark(is_locked=new_value)
3298 self.on_selected_episodes_status_changed()
3300 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3301 if self.active_channel is None:
3302 return
3304 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
3305 self.active_channel.update_channel_lock()
3307 for episode in self.active_channel.get_all_episodes():
3308 episode.mark(is_locked=self.active_channel.channel_is_locked)
3310 self.update_podcast_list_model(selected=True)
3311 self.update_episode_list_icons(all=True)
3313 def on_itemUpdateChannel_activate(self, widget=None):
3314 if self.active_channel is None:
3315 title = _('No podcast selected')
3316 message = _('Please select a podcast in the podcasts list to update.')
3317 self.show_message( message, title, widget=self.treeChannels)
3318 return
3320 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3321 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3322 self.update_feed_cache()
3323 else:
3324 self.update_feed_cache(channels=[self.active_channel])
3326 def on_itemUpdate_activate(self, widget=None):
3327 # Check if we have outstanding subscribe/unsubscribe actions
3328 if self.on_add_remove_podcasts_mygpo():
3329 log('Update cancelled (received server changes)', sender=self)
3330 return
3332 if self.channels:
3333 self.update_feed_cache()
3334 else:
3335 gPodderWelcome(self.gPodder,
3336 center_on_widget=self.gPodder,
3337 show_example_podcasts_callback=self.on_itemImportChannels_activate,
3338 setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
3340 def download_episode_list_paused(self, episodes):
3341 self.download_episode_list(episodes, True)
3343 def download_episode_list(self, episodes, add_paused=False, force_start=False):
3344 enable_update = False
3346 for episode in episodes:
3347 log('Downloading episode: %s', episode.title, sender = self)
3348 if not episode.was_downloaded(and_exists=True):
3349 task_exists = False
3350 for task in self.download_tasks_seen:
3351 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
3352 self.download_queue_manager.add_task(task, force_start)
3353 enable_update = True
3354 task_exists = True
3355 continue
3357 if task_exists:
3358 continue
3360 try:
3361 task = download.DownloadTask(episode, self.config)
3362 except Exception, e:
3363 d = {'episode': episode.title, 'message': str(e)}
3364 message = _('Download error while downloading %(episode)s: %(message)s')
3365 self.show_message(message % d, _('Download error'), important=True)
3366 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
3367 continue
3369 if add_paused:
3370 task.status = task.PAUSED
3371 else:
3372 self.mygpo_client.on_download([task.episode])
3373 self.download_queue_manager.add_task(task, force_start)
3375 self.download_status_model.register_task(task)
3376 enable_update = True
3378 if enable_update:
3379 self.enable_download_list_update()
3381 # Flush updated episode status
3382 self.mygpo_client.flush()
3384 def cancel_task_list(self, tasks):
3385 if not tasks:
3386 return
3388 for task in tasks:
3389 if task.status in (task.QUEUED, task.DOWNLOADING):
3390 task.status = task.CANCELLED
3391 elif task.status == task.PAUSED:
3392 task.status = task.CANCELLED
3393 # Call run, so the partial file gets deleted
3394 task.run()
3396 self.update_episode_list_icons([task.url for task in tasks])
3397 self.play_or_download()
3399 # Update the tab title and downloads list
3400 self.update_downloads_list()
3402 def new_episodes_show(self, episodes, notification=False, selected=None):
3403 if gpodder.ui.maemo:
3404 columns = (
3405 ('maemo_markup', None, None, _('Episode')),
3407 show_notification = notification
3408 else:
3409 columns = (
3410 ('title_markup', None, None, _('Episode')),
3411 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3412 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3414 show_notification = False
3416 instructions = _('Select the episodes you want to download:')
3418 if self.new_episodes_window is not None:
3419 self.new_episodes_window.main_window.destroy()
3420 self.new_episodes_window = None
3422 def download_episodes_callback(episodes):
3423 self.new_episodes_window = None
3424 self.download_episode_list(episodes)
3426 if selected is None:
3427 # Select all by default
3428 selected = [True]*len(episodes)
3430 self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3431 title=_('New episodes available'), \
3432 instructions=instructions, \
3433 episodes=episodes, \
3434 columns=columns, \
3435 selected=selected, \
3436 stock_ok_button = 'gpodder-download', \
3437 callback=download_episodes_callback, \
3438 remove_callback=lambda e: e.mark_old(), \
3439 remove_action=_('Mark as old'), \
3440 remove_finished=self.episode_new_status_changed, \
3441 _config=self.config, \
3442 show_notification=show_notification, \
3443 show_episode_shownotes=self.show_episode_shownotes)
3445 def on_itemDownloadAllNew_activate(self, widget, *args):
3446 if not self.offer_new_episodes():
3447 self.show_message(_('Please check for new episodes later.'), \
3448 _('No new episodes available'), widget=self.btnUpdateFeeds)
3450 def get_new_episodes(self, channels=None):
3451 if channels is None:
3452 channels = self.channels
3453 episodes = []
3454 for channel in channels:
3455 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3456 episodes.append(episode)
3458 return episodes
3460 @dbus.service.method(gpodder.dbus_interface)
3461 def start_device_synchronization(self):
3462 """Public D-Bus API for starting Device sync (Desktop only)
3464 This method can be called to initiate a synchronization with
3465 a configured protable media player. This only works for the
3466 Desktop version of gPodder and does nothing on Maemo.
3468 if gpodder.ui.desktop:
3469 self.on_sync_to_ipod_activate(None)
3470 return True
3472 return False
3474 def on_sync_to_ipod_activate(self, widget, episodes=None):
3475 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
3477 def commit_changes_to_database(self):
3478 """This will be called after the sync process is finished"""
3479 self.db.commit()
3481 def on_cleanup_ipod_activate(self, widget, *args):
3482 self.sync_ui.on_cleanup_device()
3484 def on_manage_device_playlist(self, widget):
3485 self.sync_ui.on_manage_device_playlist()
3487 def show_hide_tray_icon(self):
3488 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3489 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3490 elif not self.config.display_tray_icon and self.tray_icon is not None:
3491 self.tray_icon.set_visible(False)
3492 del self.tray_icon
3493 self.tray_icon = None
3495 if self.config.minimize_to_tray and self.tray_icon:
3496 self.tray_icon.set_visible(self.is_iconified())
3497 elif self.tray_icon:
3498 self.tray_icon.set_visible(True)
3500 def on_itemShowAllEpisodes_activate(self, widget):
3501 self.config.podcast_list_view_all = widget.get_active()
3503 def on_itemShowToolbar_activate(self, widget):
3504 self.config.show_toolbar = self.itemShowToolbar.get_active()
3506 def on_itemShowDescription_activate(self, widget):
3507 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3509 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3510 self.config.podcast_list_hide_boring = toggleaction.get_active()
3511 if self.config.podcast_list_hide_boring:
3512 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3513 else:
3514 self.podcast_list_model.set_view_mode(-1)
3516 def on_item_view_podcasts_changed(self, radioaction, current):
3517 # Only on Fremantle
3518 if current == self.item_view_podcasts_all:
3519 self.podcast_list_model.set_view_mode(-1)
3520 elif current == self.item_view_podcasts_downloaded:
3521 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3522 elif current == self.item_view_podcasts_unplayed:
3523 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3525 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3527 def on_item_view_episodes_changed(self, radioaction, current):
3528 if current == self.item_view_episodes_all:
3529 self.config.episode_list_view_mode = EpisodeListModel.VIEW_ALL
3530 elif current == self.item_view_episodes_undeleted:
3531 self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNDELETED
3532 elif current == self.item_view_episodes_downloaded:
3533 self.config.episode_list_view_mode = EpisodeListModel.VIEW_DOWNLOADED
3534 elif current == self.item_view_episodes_unplayed:
3535 self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNPLAYED
3537 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
3539 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3540 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3542 def update_item_device( self):
3543 if not gpodder.ui.fremantle:
3544 if self.config.device_type != 'none':
3545 self.itemDevice.set_visible(True)
3546 self.itemDevice.label = self.get_device_name()
3547 else:
3548 self.itemDevice.set_visible(False)
3550 def properties_closed( self):
3551 self.preferences_dialog = None
3552 self.show_hide_tray_icon()
3553 self.update_item_device()
3554 if gpodder.ui.maemo:
3555 selection = self.treeAvailable.get_selection()
3556 if self.config.maemo_enable_gestures or \
3557 self.config.enable_fingerscroll:
3558 selection.set_mode(gtk.SELECTION_SINGLE)
3559 else:
3560 selection.set_mode(gtk.SELECTION_MULTIPLE)
3562 def on_itemPreferences_activate(self, widget, *args):
3563 self.preferences_dialog = gPodderPreferences(self.main_window, \
3564 _config=self.config, \
3565 callback_finished=self.properties_closed, \
3566 user_apps_reader=self.user_apps_reader, \
3567 parent_window=self.main_window, \
3568 mygpo_client=self.mygpo_client, \
3569 on_send_full_subscriptions=self.on_send_full_subscriptions)
3571 # Initial message to relayout window (in case it's opened in portrait mode
3572 self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3574 def on_itemDependencies_activate(self, widget):
3575 gPodderDependencyManager(self.gPodder)
3577 def on_goto_mygpo(self, widget):
3578 self.mygpo_client.open_website()
3580 def on_download_subscriptions_from_mygpo(self, action=None):
3581 title = _('Login to gpodder.net')
3582 message = _('Please login to download your subscriptions.')
3583 success, (username, password) = self.show_login_dialog(title, message, \
3584 self.config.mygpo_username, self.config.mygpo_password)
3585 if not success:
3586 return
3588 self.config.mygpo_username = username
3589 self.config.mygpo_password = password
3591 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3592 custom_title=_('Subscriptions on gpodder.net'), \
3593 add_urls_callback=self.add_podcast_list, \
3594 hide_url_entry=True)
3596 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3597 # we do not have to hardcode the URL here
3598 OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
3599 url = util.url_add_authentication(OPML_URL, \
3600 self.config.mygpo_username, \
3601 self.config.mygpo_password)
3602 dir.download_opml_file(url)
3604 def on_mygpo_settings_activate(self, action=None):
3605 # This dialog is only used for Maemo 4
3606 if not gpodder.ui.diablo:
3607 return
3609 settings = MygPodderSettings(self.main_window, \
3610 config=self.config, \
3611 mygpo_client=self.mygpo_client, \
3612 on_send_full_subscriptions=self.on_send_full_subscriptions)
3614 def on_itemAddChannel_activate(self, widget=None):
3615 gPodderAddPodcast(self.gPodder, \
3616 add_urls_callback=self.add_podcast_list)
3618 def on_itemEditChannel_activate(self, widget, *args):
3619 if self.active_channel is None:
3620 title = _('No podcast selected')
3621 message = _('Please select a podcast in the podcasts list to edit.')
3622 self.show_message( message, title, widget=self.treeChannels)
3623 return
3625 callback_closed = lambda: self.update_podcast_list_model(selected=True)
3626 gPodderChannel(self.main_window, \
3627 channel=self.active_channel, \
3628 callback_closed=callback_closed, \
3629 cover_downloader=self.cover_downloader)
3631 def on_itemMassUnsubscribe_activate(self, item=None):
3632 columns = (
3633 ('title', None, None, _('Podcast')),
3636 # We're abusing the Episode Selector for selecting Podcasts here,
3637 # but it works and looks good, so why not? -- thp
3638 gPodderEpisodeSelector(self.main_window, \
3639 title=_('Remove podcasts'), \
3640 instructions=_('Select the podcast you want to remove.'), \
3641 episodes=self.channels, \
3642 columns=columns, \
3643 size_attribute=None, \
3644 stock_ok_button=_('Remove'), \
3645 callback=self.remove_podcast_list, \
3646 _config=self.config)
3648 def remove_podcast_list(self, channels, confirm=True):
3649 if not channels:
3650 log('No podcasts selected for deletion', sender=self)
3651 return
3653 if len(channels) == 1:
3654 title = _('Removing podcast')
3655 info = _('Please wait while the podcast is removed')
3656 message = _('Do you really want to remove this podcast and its episodes?')
3657 else:
3658 title = _('Removing podcasts')
3659 info = _('Please wait while the podcasts are removed')
3660 message = _('Do you really want to remove the selected podcasts and their episodes?')
3662 if confirm and not self.show_confirmation(message, title):
3663 return
3665 progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3667 def finish_deletion(select_url):
3668 # Upload subscription list changes to the web service
3669 self.mygpo_client.on_unsubscribe([c.url for c in channels])
3671 # Re-load the channels and select the desired new channel
3672 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3673 progress.on_finished()
3674 self.update_podcasts_tab()
3676 def thread_proc():
3677 select_url = None
3679 for idx, channel in enumerate(channels):
3680 # Update the UI for correct status messages
3681 progress.on_progress(float(idx)/float(len(channels)))
3682 progress.on_message(channel.title)
3684 # Delete downloaded episodes
3685 channel.remove_downloaded()
3687 # cancel any active downloads from this channel
3688 for episode in channel.get_all_episodes():
3689 util.idle_add(self.download_status_model.cancel_by_url,
3690 episode.url)
3692 if len(channels) == 1:
3693 # get the URL of the podcast we want to select next
3694 if channel in self.channels:
3695 position = self.channels.index(channel)
3696 else:
3697 position = -1
3699 if position == len(self.channels)-1:
3700 # this is the last podcast, so select the URL
3701 # of the item before this one (i.e. the "new last")
3702 select_url = self.channels[position-1].url
3703 else:
3704 # there is a podcast after the deleted one, so
3705 # we simply select the one that comes after it
3706 select_url = self.channels[position+1].url
3708 # Remove the channel and clean the database entries
3709 channel.delete()
3710 self.channels.remove(channel)
3712 # Clean up downloads and download directories
3713 self.clean_up_downloads()
3715 self.channel_list_changed = True
3716 self.save_channels_opml()
3718 # The remaining stuff is to be done in the GTK main thread
3719 util.idle_add(finish_deletion, select_url)
3721 threading.Thread(target=thread_proc).start()
3723 def on_itemRemoveChannel_activate(self, widget, *args):
3724 if self.active_channel is None:
3725 title = _('No podcast selected')
3726 message = _('Please select a podcast in the podcasts list to remove.')
3727 self.show_message( message, title, widget=self.treeChannels)
3728 return
3730 self.remove_podcast_list([self.active_channel])
3732 def get_opml_filter(self):
3733 filter = gtk.FileFilter()
3734 filter.add_pattern('*.opml')
3735 filter.add_pattern('*.xml')
3736 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3737 return filter
3739 def on_item_import_from_file_activate(self, widget, filename=None):
3740 if filename is None:
3741 if gpodder.ui.desktop or gpodder.ui.fremantle:
3742 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), \
3743 parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3744 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3745 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3746 elif gpodder.ui.diablo:
3747 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3748 dlg.set_filter(self.get_opml_filter())
3749 response = dlg.run()
3750 filename = None
3751 if response == gtk.RESPONSE_OK:
3752 filename = dlg.get_filename()
3753 dlg.destroy()
3755 if filename is not None:
3756 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3757 custom_title=_('Import podcasts from OPML file'), \
3758 add_urls_callback=self.add_podcast_list, \
3759 hide_url_entry=True)
3760 dir.download_opml_file(filename)
3762 def on_itemExportChannels_activate(self, widget, *args):
3763 if not self.channels:
3764 title = _('Nothing to export')
3765 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3766 self.show_message(message, title, widget=self.treeChannels)
3767 return
3769 if gpodder.ui.desktop or gpodder.ui.fremantle:
3770 # FIXME: Hildonization on Fremantle
3771 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3772 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3773 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3774 elif gpodder.ui.diablo:
3775 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3776 dlg.set_filter(self.get_opml_filter())
3777 response = dlg.run()
3778 if response == gtk.RESPONSE_OK:
3779 filename = dlg.get_filename()
3780 dlg.destroy()
3781 exporter = opml.Exporter( filename)
3782 if exporter.write(self.channels):
3783 count = len(self.channels)
3784 title = N_('%(count)d subscription exported', '%(count)d subscriptions exported', count) % {'count':count}
3785 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3786 else:
3787 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3788 else:
3789 dlg.destroy()
3791 def on_itemImportChannels_activate(self, widget, *args):
3792 if gpodder.ui.fremantle:
3793 gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3794 self.config.toplist_url, \
3795 self.config.opml_url, \
3796 self.add_podcast_list, \
3797 self.on_itemAddChannel_activate, \
3798 self.on_download_subscriptions_from_mygpo, \
3799 self.show_text_edit_dialog)
3800 else:
3801 dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3802 add_urls_callback=self.add_podcast_list)
3803 util.idle_add(dir.download_opml_file, self.config.opml_url)
3805 def on_homepage_activate(self, widget, *args):
3806 util.open_website(gpodder.__url__)
3808 def on_wiki_activate(self, widget, *args):
3809 util.open_website('http://gpodder.org/wiki/User_Manual')
3811 def on_bug_tracker_activate(self, widget, *args):
3812 if gpodder.ui.maemo:
3813 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3814 else:
3815 util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3817 def on_item_support_activate(self, widget):
3818 util.open_website('http://gpodder.org/donate')
3820 def on_itemAbout_activate(self, widget, *args):
3821 if gpodder.ui.fremantle:
3822 from gpodder.gtkui.frmntl.about import HeAboutDialog
3823 HeAboutDialog.present(self.main_window,
3824 'gPodder',
3825 'gpodder',
3826 gpodder.__version__,
3827 _('A podcast client with focus on usability'),
3828 gpodder.__copyright__,
3829 gpodder.__url__,
3830 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3831 'http://gpodder.org/donate')
3832 return
3834 dlg = gtk.AboutDialog()
3835 dlg.set_transient_for(self.main_window)
3836 dlg.set_name('gPodder')
3837 dlg.set_version(gpodder.__version__)
3838 dlg.set_copyright(gpodder.__copyright__)
3839 dlg.set_comments(_('A podcast client with focus on usability'))
3840 dlg.set_website(gpodder.__url__)
3841 dlg.set_translator_credits( _('translator-credits'))
3842 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3844 if gpodder.ui.desktop:
3845 # For the "GUI" version, we add some more
3846 # items to the about dialog (credits and logo)
3847 app_authors = [
3848 _('Maintainer:'),
3849 'Thomas Perl <thp.io>',
3852 if os.path.exists(gpodder.credits_file):
3853 credits = open(gpodder.credits_file).read().strip().split('\n')
3854 app_authors += ['', _('Patches, bug reports and donations by:')]
3855 app_authors += credits
3857 dlg.set_authors(app_authors)
3858 try:
3859 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3860 except:
3861 dlg.set_logo_icon_name('gpodder')
3863 dlg.run()
3865 def on_wNotebook_switch_page(self, widget, *args):
3866 page_num = args[1]
3867 if gpodder.ui.maemo:
3868 self.tool_downloads.set_active(page_num == 1)
3869 page = self.wNotebook.get_nth_page(page_num)
3870 tab_label = self.wNotebook.get_tab_label(page).get_text()
3871 if page_num == 0 and self.active_channel is not None:
3872 self.set_title(self.active_channel.title)
3873 else:
3874 self.set_title(tab_label)
3875 if page_num == 0:
3876 self.play_or_download()
3877 self.menuChannels.set_sensitive(True)
3878 self.menuSubscriptions.set_sensitive(True)
3879 # The message area in the downloads tab should be hidden
3880 # when the user switches away from the downloads tab
3881 if self.message_area is not None:
3882 self.message_area.hide()
3883 self.message_area = None
3884 else:
3885 self.menuChannels.set_sensitive(False)
3886 self.menuSubscriptions.set_sensitive(False)
3887 if gpodder.ui.desktop:
3888 self.toolDownload.set_sensitive(False)
3889 self.toolPlay.set_sensitive(False)
3890 self.toolTransfer.set_sensitive(False)
3891 self.toolCancel.set_sensitive(False)
3893 def on_treeChannels_row_activated(self, widget, path, *args):
3894 # double-click action of the podcast list or enter
3895 self.treeChannels.set_cursor(path)
3897 def on_treeChannels_cursor_changed(self, widget, *args):
3898 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3900 if model is not None and iter is not None:
3901 old_active_channel = self.active_channel
3902 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3904 if self.active_channel == old_active_channel:
3905 return
3907 if gpodder.ui.maemo:
3908 self.set_title(self.active_channel.title)
3910 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3911 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3912 self.itemEditChannel.set_visible(False)
3913 self.itemRemoveChannel.set_visible(False)
3914 else:
3915 self.itemEditChannel.set_visible(True)
3916 self.itemRemoveChannel.set_visible(True)
3917 else:
3918 self.active_channel = None
3919 self.itemEditChannel.set_visible(False)
3920 self.itemRemoveChannel.set_visible(False)
3922 self.update_episode_list_model()
3924 def on_btnEditChannel_clicked(self, widget, *args):
3925 self.on_itemEditChannel_activate( widget, args)
3927 def get_podcast_urls_from_selected_episodes(self):
3928 """Get a set of podcast URLs based on the selected episodes"""
3929 return set(episode.channel.url for episode in \
3930 self.get_selected_episodes())
3932 def get_selected_episodes(self):
3933 """Get a list of selected episodes from treeAvailable"""
3934 selection = self.treeAvailable.get_selection()
3935 model, paths = selection.get_selected_rows()
3937 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3938 return episodes
3940 def on_transfer_selected_episodes(self, widget):
3941 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3943 def on_playback_selected_episodes(self, widget):
3944 self.playback_episodes(self.get_selected_episodes())
3946 def on_shownotes_selected_episodes(self, widget):
3947 episodes = self.get_selected_episodes()
3948 if episodes:
3949 episode = episodes.pop(0)
3950 self.show_episode_shownotes(episode)
3951 else:
3952 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3954 def on_download_selected_episodes(self, widget):
3955 episodes = self.get_selected_episodes()
3956 self.download_episode_list(episodes)
3957 self.update_episode_list_icons([episode.url for episode in episodes])
3958 self.play_or_download()
3960 def on_treeAvailable_row_activated(self, widget, path, view_column):
3961 """Double-click/enter action handler for treeAvailable"""
3962 # We should only have one one selected as it was double clicked!
3963 e = self.get_selected_episodes()[0]
3965 if (self.config.double_click_episode_action == 'download'):
3966 # If the episode has already been downloaded and exists then play it
3967 if e.was_downloaded(and_exists=True):
3968 self.playback_episodes(self.get_selected_episodes())
3969 # else download it if it is not already downloading
3970 elif not self.episode_is_downloading(e):
3971 self.download_episode_list([e])
3972 self.update_episode_list_icons([e.url])
3973 self.play_or_download()
3974 elif (self.config.double_click_episode_action == 'stream'):
3975 # If we happen to have downloaded this episode simple play it
3976 if e.was_downloaded(and_exists=True):
3977 self.playback_episodes(self.get_selected_episodes())
3978 # else if streaming is possible stream it
3979 elif self.streaming_possible():
3980 self.playback_episodes(self.get_selected_episodes())
3981 else:
3982 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3983 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3984 else:
3985 # default action is to display show notes
3986 self.on_shownotes_selected_episodes(widget)
3988 def show_episode_shownotes(self, episode):
3989 if self.episode_shownotes_window is None:
3990 log('First-time use of episode window --- creating', sender=self)
3991 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
3992 _download_episode_list=self.download_episode_list, \
3993 _playback_episodes=self.playback_episodes, \
3994 _delete_episode_list=self.delete_episode_list, \
3995 _episode_list_status_changed=self.episode_list_status_changed, \
3996 _cancel_task_list=self.cancel_task_list, \
3997 _episode_is_downloading=self.episode_is_downloading, \
3998 _streaming_possible=self.streaming_possible())
3999 self.episode_shownotes_window.show(episode)
4000 if self.episode_is_downloading(episode):
4001 self.update_downloads_list()
4003 def restart_auto_update_timer(self):
4004 if self._auto_update_timer_source_id is not None:
4005 log('Removing existing auto update timer.', sender=self)
4006 gobject.source_remove(self._auto_update_timer_source_id)
4007 self._auto_update_timer_source_id = None
4009 if self.config.auto_update_feeds and \
4010 self.config.auto_update_frequency:
4011 interval = 60*1000*self.config.auto_update_frequency
4012 log('Setting up auto update timer with interval %d.', \
4013 self.config.auto_update_frequency, sender=self)
4014 self._auto_update_timer_source_id = gobject.timeout_add(\
4015 interval, self._on_auto_update_timer)
4017 def _on_auto_update_timer(self):
4018 log('Auto update timer fired.', sender=self)
4019 self.update_feed_cache(force_update=True)
4021 # Ask web service for sub changes (if enabled)
4022 self.mygpo_client.flush()
4024 return True
4026 def on_treeDownloads_row_activated(self, widget, *args):
4027 # Use the standard way of working on the treeview
4028 selection = self.treeDownloads.get_selection()
4029 (model, paths) = selection.get_selected_rows()
4030 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
4032 for tree_row_reference, task in selected_tasks:
4033 if task.status in (task.DOWNLOADING, task.QUEUED):
4034 task.status = task.PAUSED
4035 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
4036 self.download_queue_manager.add_task(task)
4037 self.enable_download_list_update()
4038 elif task.status == task.DONE:
4039 model.remove(model.get_iter(tree_row_reference.get_path()))
4041 self.play_or_download()
4043 # Update the tab title and downloads list
4044 self.update_downloads_list()
4046 def on_item_cancel_download_activate(self, widget):
4047 if self.wNotebook.get_current_page() == 0:
4048 selection = self.treeAvailable.get_selection()
4049 (model, paths) = selection.get_selected_rows()
4050 urls = [model.get_value(model.get_iter(path), \
4051 self.episode_list_model.C_URL) for path in paths]
4052 selected_tasks = [task for task in self.download_tasks_seen \
4053 if task.url in urls]
4054 else:
4055 selection = self.treeDownloads.get_selection()
4056 (model, paths) = selection.get_selected_rows()
4057 selected_tasks = [model.get_value(model.get_iter(path), \
4058 self.download_status_model.C_TASK) for path in paths]
4059 self.cancel_task_list(selected_tasks)
4061 def on_btnCancelAll_clicked(self, widget, *args):
4062 self.cancel_task_list(self.download_tasks_seen)
4064 def on_btnDownloadedDelete_clicked(self, widget, *args):
4065 episodes = self.get_selected_episodes()
4066 if len(episodes) == 1:
4067 self.delete_episode_list(episodes, skip_locked=False)
4068 else:
4069 self.delete_episode_list(episodes)
4071 def on_key_press(self, widget, event):
4072 # Allow tab switching with Ctrl + PgUp/PgDown
4073 if event.state & gtk.gdk.CONTROL_MASK:
4074 if event.keyval == gtk.keysyms.Page_Up:
4075 self.wNotebook.prev_page()
4076 return True
4077 elif event.keyval == gtk.keysyms.Page_Down:
4078 self.wNotebook.next_page()
4079 return True
4081 # After this code we only handle Maemo hardware keys,
4082 # so if we are not a Maemo app, we don't do anything
4083 if not gpodder.ui.maemo:
4084 return False
4086 diff = 0
4087 if event.keyval == gtk.keysyms.F7: #plus
4088 diff = 1
4089 elif event.keyval == gtk.keysyms.F8: #minus
4090 diff = -1
4092 if diff != 0 and not self.currently_updating:
4093 selection = self.treeChannels.get_selection()
4094 (model, iter) = selection.get_selected()
4095 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
4096 selection.select_path(new_path)
4097 self.treeChannels.set_cursor(new_path)
4098 return True
4100 return False
4102 def on_iconify(self):
4103 if self.tray_icon:
4104 self.gPodder.set_skip_taskbar_hint(True)
4105 if self.config.minimize_to_tray:
4106 self.tray_icon.set_visible(True)
4107 else:
4108 self.gPodder.set_skip_taskbar_hint(False)
4110 def on_uniconify(self):
4111 if self.tray_icon:
4112 self.gPodder.set_skip_taskbar_hint(False)
4113 if self.config.minimize_to_tray:
4114 self.tray_icon.set_visible(False)
4115 else:
4116 self.gPodder.set_skip_taskbar_hint(False)
4118 def uniconify_main_window(self):
4119 if self.is_iconified():
4120 # We need to hide and then show the window in WMs like Metacity
4121 # or KWin4 to move the window to the active workspace
4122 # (see http://gpodder.org/bug/1125)
4123 self.gPodder.hide()
4124 self.gPodder.show()
4125 self.gPodder.present()
4127 def iconify_main_window(self):
4128 if not self.is_iconified():
4129 self.gPodder.iconify()
4131 def update_podcasts_tab(self):
4132 if len(self.channels):
4133 if gpodder.ui.fremantle:
4134 self.button_refresh.set_title(_('Check for new episodes'))
4135 self.button_refresh.show()
4136 else:
4137 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
4138 else:
4139 if gpodder.ui.fremantle:
4140 self.button_refresh.hide()
4141 else:
4142 self.label2.set_text(_('Podcasts'))
4144 @dbus.service.method(gpodder.dbus_interface)
4145 def show_gui_window(self):
4146 parent = self.get_dialog_parent()
4147 parent.present()
4149 @dbus.service.method(gpodder.dbus_interface)
4150 def subscribe_to_url(self, url):
4151 gPodderAddPodcast(self.gPodder,
4152 add_urls_callback=self.add_podcast_list,
4153 preset_url=url)
4155 @dbus.service.method(gpodder.dbus_interface)
4156 def mark_episode_played(self, filename):
4157 if filename is None:
4158 return False
4160 for channel in self.channels:
4161 for episode in channel.get_all_episodes():
4162 fn = episode.local_filename(create=False, check_only=True)
4163 if fn == filename:
4164 episode.mark(is_played=True)
4165 self.db.commit()
4166 self.update_episode_list_icons([episode.url])
4167 self.update_podcast_list_model([episode.channel.url])
4168 return True
4170 return False
4173 def main(options=None):
4174 gobject.threads_init()
4175 gobject.set_application_name('gPodder')
4177 if gpodder.ui.maemo:
4178 # Try to enable the custom icon theme for gPodder on Maemo
4179 settings = gtk.settings_get_default()
4180 settings.set_string_property('gtk-icon-theme-name', \
4181 'gpodder', __file__)
4182 # Extend the search path for the optified icon theme (Maemo 5)
4183 icon_theme = gtk.icon_theme_get_default()
4184 icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
4186 gtk.window_set_default_icon_name('gpodder')
4187 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
4189 try:
4190 dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
4191 gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
4193 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
4194 except dbus.exceptions.DBusException, dbe:
4195 log('Warning: Cannot get "on the bus".', traceback=True)
4196 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
4197 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
4198 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
4199 dlg.set_title('gPodder')
4200 dlg.run()
4201 dlg.destroy()
4202 sys.exit(0)
4204 util.make_directory(gpodder.home)
4205 gpodder.load_plugins()
4207 config = UIConfig(gpodder.config_file)
4209 # Load hook modules and install the hook manager globally
4210 # if modules have been found an instantiated by the manager
4211 user_hooks = hooks.HookManager()
4212 if user_hooks.has_modules():
4213 gpodder.user_hooks = user_hooks
4215 if gpodder.ui.diablo:
4216 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4217 # folder exists there (allow moving "gpodder" between SD cards or USB)
4218 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4219 if not os.path.exists(config.download_dir):
4220 log('Downloads might have been moved. Trying to locate them...')
4221 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
4222 dir = os.path.join(basedir, 'gpodder')
4223 if os.path.exists(dir):
4224 log('Downloads found in: %s', dir)
4225 config.download_dir = dir
4226 break
4227 else:
4228 log('Downloads NOT FOUND in %s', dir)
4230 if config.enable_fingerscroll:
4231 BuilderWidget.use_fingerscroll = True
4233 config.mygpo_device_type = util.detect_device_type()
4235 gp = gPodder(bus_name, config)
4237 # Handle options
4238 if options.subscribe:
4239 util.idle_add(gp.subscribe_to_url, options.subscribe)
4241 # mac OS X stuff :
4242 # handle "subscribe to podcast" events from firefox
4243 if platform.system() == 'Darwin':
4244 from gpodder import gpodderosx
4245 gpodderosx.register_handlers(gp)
4246 # end mac OS X stuff
4248 gp.run()