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