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