Delay live search to be more responsive
[gpodder.git] / src / gpodder / gui.py
blob4659bafc26cda2314cab133e42e886e5fdd1f652
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_delete_episodes_button_clicked=self.on_itemRemoveOldEpisodes_activate, \
419 on_itemUpdate_activate=self.on_itemUpdate_activate)
421 # Expose objects for episode list type-ahead find
422 self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
423 self.entry_search_episodes = self.episodes_window.entry_search_episodes
424 self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
426 self.downloads_window = gPodderDownloads(self.main_window, \
427 on_treeview_expose_event=self.on_treeview_expose_event, \
428 cleanup_downloads=self.cleanup_downloads, \
429 _for_each_task_set_status=self._for_each_task_set_status, \
430 downloads_list_get_selection=self.downloads_list_get_selection, \
431 _config=self.config)
433 self.treeAvailable = self.episodes_window.treeview
434 self.treeDownloads = self.downloads_window.treeview
436 # Source IDs for timeouts for search-as-you-type
437 self._podcast_list_search_timeout = None
438 self._episode_list_search_timeout = None
440 # Init the treeviews that we use
441 self.init_podcast_list_treeview()
442 self.init_episode_list_treeview()
443 self.init_download_list_treeview()
445 if self.config.podcast_list_hide_boring:
446 self.item_view_hide_boring_podcasts.set_active(True)
448 self.currently_updating = False
450 if gpodder.ui.maemo or self.config.enable_fingerscroll:
451 self.context_menu_mouse_button = 1
452 else:
453 self.context_menu_mouse_button = 3
455 if self.config.start_iconified:
456 self.iconify_main_window()
458 self.download_tasks_seen = set()
459 self.download_list_update_enabled = False
460 self.download_task_monitors = set()
462 # Subscribed channels
463 self.active_channel = None
464 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
465 self.channel_list_changed = True
466 self.update_podcasts_tab()
468 # load list of user applications for audio playback
469 self.user_apps_reader = UserAppsReader(['audio', 'video'])
470 threading.Thread(target=self.user_apps_reader.read).start()
472 # Set the "Device" menu item for the first time
473 if gpodder.ui.desktop:
474 self.update_item_device()
476 # Set up the first instance of MygPoClient
477 self.mygpo_client = my.MygPoClient(self.config)
479 # Now, update the feed cache, when everything's in place
480 if not gpodder.ui.fremantle:
481 self.btnUpdateFeeds.show()
482 self.updating_feed_cache = False
483 self.feed_cache_update_cancelled = False
484 self.update_feed_cache(force_update=self.config.update_on_startup)
486 self.message_area = None
488 def find_partial_downloads():
489 # Look for partial file downloads
490 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
491 count = len(partial_files)
492 resumable_episodes = []
493 if count:
494 if not gpodder.ui.fremantle:
495 util.idle_add(self.wNotebook.set_current_page, 1)
496 indicator = ProgressIndicator(_('Loading incomplete downloads'), \
497 _('Some episodes have not finished downloading in a previous session.'), \
498 False, self.get_dialog_parent())
499 indicator.on_message(N_('%d partial file', '%d partial files', count) % count)
501 candidates = [f[:-len('.partial')] for f in partial_files]
502 found = 0
504 for c in self.channels:
505 for e in c.get_all_episodes():
506 filename = e.local_filename(create=False, check_only=True)
507 if filename in candidates:
508 log('Found episode: %s', e.title, sender=self)
509 found += 1
510 indicator.on_message(e.title)
511 indicator.on_progress(float(found)/count)
512 candidates.remove(filename)
513 partial_files.remove(filename+'.partial')
514 resumable_episodes.append(e)
516 if not candidates:
517 break
519 if not candidates:
520 break
522 for f in partial_files:
523 log('Partial file without episode: %s', f, sender=self)
524 util.delete_file(f)
526 util.idle_add(indicator.on_finished)
528 if len(resumable_episodes):
529 def offer_resuming():
530 self.download_episode_list_paused(resumable_episodes)
531 if not gpodder.ui.fremantle:
532 resume_all = gtk.Button(_('Resume all'))
533 #resume_all.set_border_width(0)
534 def on_resume_all(button):
535 selection = self.treeDownloads.get_selection()
536 selection.select_all()
537 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
538 selection.unselect_all()
539 self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
540 self.message_area.hide()
541 resume_all.connect('clicked', on_resume_all)
543 self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
544 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
545 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
546 self.message_area.show_all()
547 self.clean_up_downloads(delete_partial=False)
548 util.idle_add(offer_resuming)
549 elif not gpodder.ui.fremantle:
550 util.idle_add(self.wNotebook.set_current_page, 0)
551 else:
552 util.idle_add(self.clean_up_downloads, True)
553 threading.Thread(target=find_partial_downloads).start()
555 # Start the auto-update procedure
556 self._auto_update_timer_source_id = None
557 if self.config.auto_update_feeds:
558 self.restart_auto_update_timer()
560 # Delete old episodes if the user wishes to
561 if self.config.auto_remove_played_episodes and \
562 self.config.episode_old_age > 0:
563 old_episodes = list(self.get_expired_episodes())
564 if len(old_episodes) > 0:
565 self.delete_episode_list(old_episodes, confirm=False)
566 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
568 if gpodder.ui.fremantle:
569 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
570 self.button_refresh.set_sensitive(True)
571 self.button_subscribe.set_sensitive(True)
572 self.main_window.set_title(_('gPodder'))
573 hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
575 # Do the initial sync with the web service
576 util.idle_add(self.mygpo_client.flush, True)
578 # First-time users should be asked if they want to see the OPML
579 if not self.channels and not gpodder.ui.fremantle:
580 util.idle_add(self.on_itemUpdate_activate)
582 def episode_object_by_uri(self, uri):
583 """Get an episode object given a local or remote URI
585 This can be used to quickly access an episode object
586 when all we have is its download filename or episode
587 URL (e.g. from external D-Bus calls / signals, etc..)
589 if uri.startswith('/'):
590 uri = 'file://' + uri
592 prefix = 'file://' + self.config.download_dir
594 if uri.startswith(prefix):
595 # File is on the local filesystem in the download folder
596 filename = uri[len(prefix):]
597 file_parts = [x for x in filename.split(os.sep) if x]
599 if len(file_parts) == 2:
600 dir_name, filename = file_parts
601 channels = [c for c in self.channels if c.foldername == dir_name]
602 if len(channels) == 1:
603 channel = channels[0]
604 return channel.get_episode_by_filename(filename)
605 else:
606 # Possibly remote file - search the database for a podcast
607 channel_id = self.db.get_channel_id_from_episode_url(uri)
609 if channel_id is not None:
610 channels = [c for c in self.channels if c.id == channel_id]
611 if len(channels) == 1:
612 channel = channels[0]
613 return channel.get_episode_by_url(uri)
615 return None
617 def on_played(self, start, end, total, file_uri):
618 """Handle the "played" signal from a media player"""
619 if start == 0 and end == 0 and total == 0:
620 # Ignore bogus play event
621 return
622 elif end < start + 5:
623 # Ignore "less than five seconds" segments,
624 # as they can happen with seeking, etc...
625 return
627 log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
628 episode = self.episode_object_by_uri(file_uri)
630 if episode is not None:
631 file_type = episode.file_type()
632 # Automatically enable D-Bus played status mode
633 if file_type == 'audio':
634 self.config.audio_played_dbus = True
635 elif file_type == 'video':
636 self.config.video_played_dbus = True
638 now = time.time()
639 if total > 0:
640 episode.total_time = total
641 elif total == 0:
642 # Assume the episode's total time for the action
643 total = episode.total_time
644 if episode.current_position_updated is None or \
645 now > episode.current_position_updated:
646 episode.current_position = end
647 episode.current_position_updated = now
648 episode.mark(is_played=True)
649 episode.save()
650 self.db.commit()
651 self.update_episode_list_icons([episode.url])
652 self.update_podcast_list_model([episode.channel.url])
654 # Submit this action to the webservice
655 self.mygpo_client.on_playback_full(episode, \
656 start, end, total)
658 def on_add_remove_podcasts_mygpo(self):
659 actions = self.mygpo_client.get_received_actions()
660 if not actions:
661 return False
663 existing_urls = [c.url for c in self.channels]
665 # Columns for the episode selector window - just one...
666 columns = (
667 ('description', None, None, _('Action')),
670 # A list of actions that have to be chosen from
671 changes = []
673 # Actions that are ignored (already carried out)
674 ignored = []
676 for action in actions:
677 if action.is_add and action.url not in existing_urls:
678 changes.append(my.Change(action))
679 elif action.is_remove and action.url in existing_urls:
680 podcast_object = None
681 for podcast in self.channels:
682 if podcast.url == action.url:
683 podcast_object = podcast
684 break
685 changes.append(my.Change(action, podcast_object))
686 else:
687 log('Ignoring action: %s', action, sender=self)
688 ignored.append(action)
690 # Confirm all ignored changes
691 self.mygpo_client.confirm_received_actions(ignored)
693 def execute_podcast_actions(selected):
694 add_list = [c.action.url for c in selected if c.action.is_add]
695 remove_list = [c.podcast for c in selected if c.action.is_remove]
697 # Apply the accepted changes locally
698 self.add_podcast_list(add_list)
699 self.remove_podcast_list(remove_list, confirm=False)
701 # All selected items are now confirmed
702 self.mygpo_client.confirm_received_actions(c.action for c in selected)
704 # Revert the changes on the server
705 rejected = [c.action for c in changes if c not in selected]
706 self.mygpo_client.reject_received_actions(rejected)
708 def ask():
709 # We're abusing the Episode Selector again ;) -- thp
710 gPodderEpisodeSelector(self.main_window, \
711 title=_('Confirm changes from gpodder.net'), \
712 instructions=_('Select the actions you want to carry out.'), \
713 episodes=changes, \
714 columns=columns, \
715 size_attribute=None, \
716 stock_ok_button=gtk.STOCK_APPLY, \
717 callback=execute_podcast_actions, \
718 _config=self.config)
720 # There are some actions that need the user's attention
721 if changes:
722 util.idle_add(ask)
723 return True
725 # We have no remaining actions - no selection happens
726 return False
728 def rewrite_urls_mygpo(self):
729 # Check if we have to rewrite URLs since the last add
730 rewritten_urls = self.mygpo_client.get_rewritten_urls()
732 for rewritten_url in rewritten_urls:
733 if not rewritten_url.new_url:
734 continue
736 for channel in self.channels:
737 if channel.url == rewritten_url.old_url:
738 log('Updating URL of %s to %s', channel, \
739 rewritten_url.new_url, sender=self)
740 channel.url = rewritten_url.new_url
741 channel.save()
742 self.channel_list_changed = True
743 util.idle_add(self.update_episode_list_model)
744 break
746 def on_send_full_subscriptions(self):
747 # Send the full subscription list to the gpodder.net client
748 # (this will overwrite the subscription list on the server)
749 indicator = ProgressIndicator(_('Uploading subscriptions'), \
750 _('Your subscriptions are being uploaded to the server.'), \
751 False, self.get_dialog_parent())
753 try:
754 self.mygpo_client.set_subscriptions([c.url for c in self.channels])
755 util.idle_add(self.show_message, _('List uploaded successfully.'))
756 except Exception, e:
757 def show_error(e):
758 message = str(e)
759 if not message:
760 message = e.__class__.__name__
761 self.show_message(message, \
762 _('Error while uploading'), \
763 important=True)
764 util.idle_add(show_error, e)
766 util.idle_add(indicator.on_finished)
768 def on_podcast_selected(self, treeview, path, column):
769 # for Maemo 5's UI
770 model = treeview.get_model()
771 channel = model.get_value(model.get_iter(path), \
772 PodcastListModel.C_CHANNEL)
773 self.active_channel = channel
774 self.update_episode_list_model()
775 self.episodes_window.channel = self.active_channel
776 self.episodes_window.show()
778 def on_button_subscribe_clicked(self, button):
779 self.on_itemImportChannels_activate(button)
781 def on_button_downloads_clicked(self, widget):
782 self.downloads_window.show()
784 def show_episode_in_download_manager(self, episode):
785 self.downloads_window.show()
786 model = self.treeDownloads.get_model()
787 selection = self.treeDownloads.get_selection()
788 selection.unselect_all()
789 it = model.get_iter_first()
790 while it is not None:
791 task = model.get_value(it, DownloadStatusModel.C_TASK)
792 if task.episode.url == episode.url:
793 selection.select_iter(it)
794 # FIXME: Scroll to selection in pannable area
795 break
796 it = model.iter_next(it)
798 def for_each_episode_set_task_status(self, episodes, status):
799 episode_urls = set(episode.url for episode in episodes)
800 model = self.treeDownloads.get_model()
801 selected_tasks = [(gtk.TreeRowReference(model, row.path), \
802 model.get_value(row.iter, \
803 DownloadStatusModel.C_TASK)) for row in model \
804 if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
805 in episode_urls]
806 self._for_each_task_set_status(selected_tasks, status)
808 def on_window_orientation_changed(self, orientation):
809 self._last_orientation = orientation
810 if self.preferences_dialog is not None:
811 self.preferences_dialog.on_window_orientation_changed(orientation)
813 treeview = self.treeChannels
814 if orientation == Orientation.PORTRAIT:
815 treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
816 # Work around Maemo bug #4718
817 self.button_subscribe.set_name('HildonButton-thumb')
818 self.button_refresh.set_name('HildonButton-thumb')
819 else:
820 treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
821 # Work around Maemo bug #4718
822 self.button_subscribe.set_name('HildonButton-finger')
823 self.button_refresh.set_name('HildonButton-finger')
825 if gpodder.ui.fremantle:
826 self.fancy_progress_bar.relayout()
828 def on_treeview_podcasts_selection_changed(self, selection):
829 model, iter = selection.get_selected()
830 if iter is None:
831 self.active_channel = None
832 self.episode_list_model.clear()
834 def on_treeview_button_pressed(self, treeview, event):
835 if event.window != treeview.get_bin_window():
836 return False
838 TreeViewHelper.save_button_press_event(treeview, event)
840 if getattr(treeview, TreeViewHelper.ROLE) == \
841 TreeViewHelper.ROLE_PODCASTS:
842 return self.currently_updating
844 return event.button == self.context_menu_mouse_button and \
845 gpodder.ui.desktop
847 def on_treeview_podcasts_button_released(self, treeview, event):
848 if event.window != treeview.get_bin_window():
849 return False
851 if gpodder.ui.maemo:
852 return self.treeview_channels_handle_gestures(treeview, event)
853 return self.treeview_channels_show_context_menu(treeview, event)
855 def on_treeview_episodes_button_released(self, treeview, event):
856 if event.window != treeview.get_bin_window():
857 return False
859 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
860 return self.treeview_available_handle_gestures(treeview, event)
862 return self.treeview_available_show_context_menu(treeview, event)
864 def on_treeview_downloads_button_released(self, treeview, event):
865 if event.window != treeview.get_bin_window():
866 return False
868 return self.treeview_downloads_show_context_menu(treeview, event)
870 def on_entry_search_podcasts_changed(self, editable):
871 if self.hbox_search_podcasts.get_property('visible'):
872 def set_search_term(self, text):
873 self.podcast_list_model.set_search_term(text)
874 self._podcast_list_search_timeout = None
875 return False
877 if self._podcast_list_search_timeout is not None:
878 gobject.source_remove(self._podcast_list_search_timeout)
879 self._podcast_list_search_timeout = gobject.timeout_add(\
880 self.LIVE_SEARCH_DELAY, \
881 set_search_term, self, editable.get_chars(0, -1))
883 def on_entry_search_podcasts_key_press(self, editable, event):
884 if event.keyval == gtk.keysyms.Escape:
885 self.hide_podcast_search()
886 return True
888 def hide_podcast_search(self, *args):
889 if self._podcast_list_search_timeout is not None:
890 gobject.source_remove(self._podcast_list_search_timeout)
891 self._podcast_list_search_timeout = None
892 self.hbox_search_podcasts.hide()
893 self.entry_search_podcasts.set_text('')
894 self.podcast_list_model.set_search_term(None)
895 self.treeChannels.grab_focus()
897 def show_podcast_search(self, input_char):
898 self.hbox_search_podcasts.show()
899 self.entry_search_podcasts.insert_text(input_char, -1)
900 self.entry_search_podcasts.grab_focus()
901 self.entry_search_podcasts.set_position(-1)
903 def init_podcast_list_treeview(self):
904 # Set up podcast channel tree view widget
905 if gpodder.ui.fremantle:
906 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
907 self.item_view_podcasts_downloaded.set_active(True)
908 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
909 self.item_view_podcasts_unplayed.set_active(True)
910 else:
911 self.item_view_podcasts_all.set_active(True)
912 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
914 iconcolumn = gtk.TreeViewColumn('')
915 iconcell = gtk.CellRendererPixbuf()
916 iconcolumn.pack_start(iconcell, False)
917 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
918 self.treeChannels.append_column(iconcolumn)
920 namecolumn = gtk.TreeViewColumn('')
921 namecell = gtk.CellRendererText()
922 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
923 namecolumn.pack_start(namecell, True)
924 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
926 if gpodder.ui.fremantle:
927 countcell = gtk.CellRendererText()
928 from gpodder.gtkui.frmntl import style
929 countcell.set_property('font-desc', style.get_font_desc('EmpSystemFont'))
930 countcell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
931 countcell.set_property('alignment', pango.ALIGN_RIGHT)
932 countcell.set_property('xalign', 1.)
933 countcell.set_property('xpad', 5)
934 namecolumn.pack_start(countcell, False)
935 namecolumn.add_attribute(countcell, 'text', PodcastListModel.C_DOWNLOADS)
936 namecolumn.add_attribute(countcell, 'visible', PodcastListModel.C_DOWNLOADS)
937 else:
938 iconcell = gtk.CellRendererPixbuf()
939 iconcell.set_property('xalign', 1.0)
940 namecolumn.pack_start(iconcell, False)
941 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
942 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
944 self.treeChannels.append_column(namecolumn)
946 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
948 # When no podcast is selected, clear the episode list model
949 selection = self.treeChannels.get_selection()
950 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
952 # Set up type-ahead find for the podcast list
953 def on_key_press(treeview, event):
954 if event.keyval == gtk.keysyms.Escape:
955 self.hide_podcast_search()
956 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
957 self.hide_podcast_search()
958 elif event.state & gtk.gdk.CONTROL_MASK:
959 # Don't handle type-ahead when control is pressed (so shortcuts
960 # with the Ctrl key still work, e.g. Ctrl+A, ...)
961 return True
962 else:
963 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
964 if unicode_char_id == 0:
965 return False
966 input_char = unichr(unicode_char_id)
967 self.show_podcast_search(input_char)
968 return True
969 self.treeChannels.connect('key-press-event', on_key_press)
971 # Enable separators to the podcast list to separate special podcasts
972 # from others (this is used for the "all episodes" view)
973 self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
975 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
977 def on_entry_search_episodes_changed(self, editable):
978 if self.hbox_search_episodes.get_property('visible'):
979 def set_search_term(self, text):
980 self.episode_list_model.set_search_term(text)
981 self._episode_list_search_timeout = None
982 return False
984 if self._episode_list_search_timeout is not None:
985 gobject.source_remove(self._episode_list_search_timeout)
986 self._episode_list_search_timeout = gobject.timeout_add(\
987 self.LIVE_SEARCH_DELAY, \
988 set_search_term, self, editable.get_chars(0, -1))
990 def on_entry_search_episodes_key_press(self, editable, event):
991 if event.keyval == gtk.keysyms.Escape:
992 self.hide_episode_search()
993 return True
995 def hide_episode_search(self, *args):
996 if self._episode_list_search_timeout is not None:
997 gobject.source_remove(self._episode_list_search_timeout)
998 self._episode_list_search_timeout = None
999 self.hbox_search_episodes.hide()
1000 self.entry_search_episodes.set_text('')
1001 self.episode_list_model.set_search_term(None)
1002 self.treeAvailable.grab_focus()
1004 def show_episode_search(self, input_char):
1005 self.hbox_search_episodes.show()
1006 self.entry_search_episodes.insert_text(input_char, -1)
1007 self.entry_search_episodes.grab_focus()
1008 self.entry_search_episodes.set_position(-1)
1010 def init_episode_list_treeview(self):
1011 # For loading the list model
1012 self.episode_list_model = EpisodeListModel(self.on_episode_list_filter_changed)
1014 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
1015 self.item_view_episodes_undeleted.set_active(True)
1016 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
1017 self.item_view_episodes_downloaded.set_active(True)
1018 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
1019 self.item_view_episodes_unplayed.set_active(True)
1020 else:
1021 self.item_view_episodes_all.set_active(True)
1023 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
1025 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
1027 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
1029 iconcell = gtk.CellRendererPixbuf()
1030 iconcell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1031 if gpodder.ui.maemo:
1032 iconcell.set_fixed_size(50, 50)
1033 else:
1034 iconcell.set_fixed_size(40, -1)
1036 namecell = gtk.CellRendererText()
1037 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
1038 namecolumn = gtk.TreeViewColumn(_('Episode'))
1039 namecolumn.pack_start(iconcell, False)
1040 namecolumn.add_attribute(iconcell, 'icon-name', EpisodeListModel.C_STATUS_ICON)
1041 namecolumn.pack_start(namecell, True)
1042 namecolumn.add_attribute(namecell, 'markup', EpisodeListModel.C_DESCRIPTION)
1043 if gpodder.ui.fremantle:
1044 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1045 else:
1046 namecolumn.set_sort_column_id(EpisodeListModel.C_DESCRIPTION)
1047 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1048 namecolumn.set_resizable(True)
1049 namecolumn.set_expand(True)
1051 if gpodder.ui.fremantle:
1052 from gpodder.gtkui.frmntl import style
1053 timecell = gtk.CellRendererText()
1054 timecell.set_property('font-desc', style.get_font_desc('SystemFont'))
1055 timecell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
1056 timecell.set_property('alignment', pango.ALIGN_RIGHT)
1057 timecell.set_property('xalign', 1.)
1058 timecell.set_property('xpad', 5)
1059 namecolumn.pack_start(timecell, False)
1060 namecolumn.add_attribute(timecell, 'text', EpisodeListModel.C_TIME)
1061 namecolumn.add_attribute(timecell, 'visible', EpisodeListModel.C_TIME1_VISIBLE)
1063 # Add another cell renderer to fix a sizing issue (one renderer
1064 # only renders short text and the other one longer text to avoid
1065 # having titles of episodes unnecessarily cut off)
1066 timecell = gtk.CellRendererText()
1067 timecell.set_property('font-desc', style.get_font_desc('SystemFont'))
1068 timecell.set_property('foreground-gdk', style.get_color('SecondaryTextColor'))
1069 timecell.set_property('alignment', pango.ALIGN_RIGHT)
1070 timecell.set_property('xalign', 1.)
1071 timecell.set_property('xpad', 5)
1072 namecolumn.pack_start(timecell, False)
1073 namecolumn.add_attribute(timecell, 'text', EpisodeListModel.C_TIME)
1074 namecolumn.add_attribute(timecell, 'visible', EpisodeListModel.C_TIME2_VISIBLE)
1076 lockcell = gtk.CellRendererPixbuf()
1077 lockcell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1078 if gpodder.ui.fremantle:
1079 lockcell.set_property('icon-name', 'general_locked')
1080 else:
1081 lockcell.set_property('icon-name', 'emblem-readonly')
1083 namecolumn.pack_start(lockcell, False)
1084 namecolumn.add_attribute(lockcell, 'visible', EpisodeListModel.C_LOCKED)
1086 sizecell = gtk.CellRendererText()
1087 sizecell.set_property('xalign', 1)
1088 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
1089 sizecolumn.set_sort_column_id(EpisodeListModel.C_FILESIZE)
1091 releasecell = gtk.CellRendererText()
1092 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
1093 releasecolumn.set_sort_column_id(EpisodeListModel.C_PUBLISHED)
1095 namecolumn.set_reorderable(True)
1096 self.treeAvailable.append_column(namecolumn)
1098 if not gpodder.ui.maemo:
1099 for itemcolumn in (sizecolumn, releasecolumn):
1100 itemcolumn.set_reorderable(True)
1101 self.treeAvailable.append_column(itemcolumn)
1103 # Set up type-ahead find for the episode list
1104 def on_key_press(treeview, event):
1105 if event.keyval == gtk.keysyms.Escape:
1106 self.hide_episode_search()
1107 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
1108 self.hide_episode_search()
1109 elif event.state & gtk.gdk.CONTROL_MASK:
1110 # Don't handle type-ahead when control is pressed (so shortcuts
1111 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1112 return False
1113 else:
1114 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
1115 if unicode_char_id == 0:
1116 return False
1117 input_char = unichr(unicode_char_id)
1118 self.show_episode_search(input_char)
1119 return True
1120 self.treeAvailable.connect('key-press-event', on_key_press)
1122 if gpodder.ui.desktop and not self.config.enable_fingerscroll:
1123 self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
1124 (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
1125 def drag_data_get(tree, context, selection_data, info, timestamp):
1126 if self.config.on_drag_mark_played:
1127 for episode in self.get_selected_episodes():
1128 episode.mark(is_played=True)
1129 self.on_selected_episodes_status_changed()
1130 uris = ['file://'+e.local_filename(create=False) \
1131 for e in self.get_selected_episodes() \
1132 if e.was_downloaded(and_exists=True)]
1133 uris.append('') # for the trailing '\r\n'
1134 selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
1135 self.treeAvailable.connect('drag-data-get', drag_data_get)
1137 selection = self.treeAvailable.get_selection()
1138 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
1139 selection.set_mode(gtk.SELECTION_SINGLE)
1140 elif gpodder.ui.fremantle:
1141 selection.set_mode(gtk.SELECTION_SINGLE)
1142 else:
1143 selection.set_mode(gtk.SELECTION_MULTIPLE)
1144 # Update the sensitivity of the toolbar buttons on the Desktop
1145 selection.connect('changed', lambda s: self.play_or_download())
1147 if gpodder.ui.diablo:
1148 # Set up the tap-and-hold context menu for podcasts
1149 menu = gtk.Menu()
1150 menu.append(self.itemUpdateChannel.create_menu_item())
1151 menu.append(self.itemEditChannel.create_menu_item())
1152 menu.append(gtk.SeparatorMenuItem())
1153 menu.append(self.itemRemoveChannel.create_menu_item())
1154 menu.append(gtk.SeparatorMenuItem())
1155 item = gtk.ImageMenuItem(_('Close this menu'))
1156 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
1157 gtk.ICON_SIZE_MENU))
1158 menu.append(item)
1159 menu.show_all()
1160 menu = self.set_finger_friendly(menu)
1161 self.treeChannels.tap_and_hold_setup(menu)
1164 def init_download_list_treeview(self):
1165 # enable multiple selection support
1166 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1167 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1169 # columns and renderers for "download progress" tab
1170 # First column: [ICON] Episodename
1171 column = gtk.TreeViewColumn(_('Episode'))
1173 cell = gtk.CellRendererPixbuf()
1174 if gpodder.ui.maemo:
1175 cell.set_fixed_size(50, 50)
1176 cell.set_property('stock-size', gtk.ICON_SIZE_BUTTON)
1177 column.pack_start(cell, expand=False)
1178 column.add_attribute(cell, 'icon-name', \
1179 DownloadStatusModel.C_ICON_NAME)
1181 cell = gtk.CellRendererText()
1182 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1183 column.pack_start(cell, expand=True)
1184 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1185 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1186 column.set_expand(True)
1187 self.treeDownloads.append_column(column)
1189 # Second column: Progress
1190 cell = gtk.CellRendererProgress()
1191 cell.set_property('yalign', .5)
1192 cell.set_property('ypad', 6)
1193 column = gtk.TreeViewColumn(_('Progress'), cell,
1194 value=DownloadStatusModel.C_PROGRESS, \
1195 text=DownloadStatusModel.C_PROGRESS_TEXT)
1196 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1197 column.set_expand(False)
1198 self.treeDownloads.append_column(column)
1199 if gpodder.ui.maemo:
1200 column.set_property('min-width', 200)
1201 column.set_property('max-width', 200)
1202 else:
1203 column.set_property('min-width', 150)
1204 column.set_property('max-width', 150)
1206 self.treeDownloads.set_model(self.download_status_model)
1207 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1209 def on_treeview_expose_event(self, treeview, event):
1210 if event.window == treeview.get_bin_window():
1211 model = treeview.get_model()
1212 if (model is not None and model.get_iter_first() is not None):
1213 return False
1215 role = getattr(treeview, TreeViewHelper.ROLE, None)
1216 if role is None:
1217 return False
1219 ctx = event.window.cairo_create()
1220 ctx.rectangle(event.area.x, event.area.y,
1221 event.area.width, event.area.height)
1222 ctx.clip()
1224 x, y, width, height, depth = event.window.get_geometry()
1225 progress = None
1227 if role == TreeViewHelper.ROLE_EPISODES:
1228 if self.currently_updating:
1229 text = _('Loading episodes')
1230 elif self.config.episode_list_view_mode != \
1231 EpisodeListModel.VIEW_ALL:
1232 text = _('No episodes in current view')
1233 else:
1234 text = _('No episodes available')
1235 elif role == TreeViewHelper.ROLE_PODCASTS:
1236 if self.config.episode_list_view_mode != \
1237 EpisodeListModel.VIEW_ALL and \
1238 self.config.podcast_list_hide_boring and \
1239 len(self.channels) > 0:
1240 text = _('No podcasts in this view')
1241 else:
1242 text = _('No subscriptions')
1243 elif role == TreeViewHelper.ROLE_DOWNLOADS:
1244 text = _('No active downloads')
1245 else:
1246 raise Exception('on_treeview_expose_event: unknown role')
1248 if gpodder.ui.fremantle:
1249 from gpodder.gtkui.frmntl import style
1250 font_desc = style.get_font_desc('LargeSystemFont')
1251 else:
1252 font_desc = None
1254 draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1256 return False
1258 def enable_download_list_update(self):
1259 if not self.download_list_update_enabled:
1260 self.update_downloads_list()
1261 gobject.timeout_add(1500, self.update_downloads_list)
1262 self.download_list_update_enabled = True
1264 def cleanup_downloads(self):
1265 model = self.download_status_model
1267 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
1268 changed_episode_urls = set()
1269 for row_reference, task in all_tasks:
1270 if task.status in (task.DONE, task.CANCELLED):
1271 model.remove(model.get_iter(row_reference.get_path()))
1272 try:
1273 # We don't "see" this task anymore - remove it;
1274 # this is needed, so update_episode_list_icons()
1275 # below gets the correct list of "seen" tasks
1276 self.download_tasks_seen.remove(task)
1277 except KeyError, key_error:
1278 log('Cannot remove task from "seen" list: %s', task, sender=self)
1279 changed_episode_urls.add(task.url)
1280 # Tell the task that it has been removed (so it can clean up)
1281 task.removed_from_list()
1283 # Tell the podcasts tab to update icons for our removed podcasts
1284 self.update_episode_list_icons(changed_episode_urls)
1286 # Tell the shownotes window that we have removed the episode
1287 if self.episode_shownotes_window is not None and \
1288 self.episode_shownotes_window.episode is not None and \
1289 self.episode_shownotes_window.episode.url in changed_episode_urls:
1290 self.episode_shownotes_window._download_status_changed(None)
1292 # Update the downloads list one more time
1293 self.update_downloads_list(can_call_cleanup=False)
1295 def on_tool_downloads_toggled(self, toolbutton):
1296 if toolbutton.get_active():
1297 self.wNotebook.set_current_page(1)
1298 else:
1299 self.wNotebook.set_current_page(0)
1301 def add_download_task_monitor(self, monitor):
1302 self.download_task_monitors.add(monitor)
1303 model = self.download_status_model
1304 if model is None:
1305 model = ()
1306 for row in model:
1307 task = row[self.download_status_model.C_TASK]
1308 monitor.task_updated(task)
1310 def remove_download_task_monitor(self, monitor):
1311 self.download_task_monitors.remove(monitor)
1313 def update_downloads_list(self, can_call_cleanup=True):
1314 try:
1315 model = self.download_status_model
1317 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
1318 total_speed, total_size, done_size = 0, 0, 0
1320 # Keep a list of all download tasks that we've seen
1321 download_tasks_seen = set()
1323 # Remember the DownloadTask object for the episode that
1324 # has been opened in the episode shownotes dialog (if any)
1325 if self.episode_shownotes_window is not None:
1326 shownotes_episode = self.episode_shownotes_window.episode
1327 shownotes_task = None
1328 else:
1329 shownotes_episode = None
1330 shownotes_task = None
1332 # Do not go through the list of the model is not (yet) available
1333 if model is None:
1334 model = ()
1336 failed_downloads = []
1337 for row in model:
1338 self.download_status_model.request_update(row.iter)
1340 task = row[self.download_status_model.C_TASK]
1341 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
1343 # Let the download task monitors know of changes
1344 for monitor in self.download_task_monitors:
1345 monitor.task_updated(task)
1347 total_size += size
1348 done_size += size*progress
1350 if shownotes_episode is not None and \
1351 shownotes_episode.url == task.episode.url:
1352 shownotes_task = task
1354 download_tasks_seen.add(task)
1356 if status == download.DownloadTask.DOWNLOADING:
1357 downloading += 1
1358 total_speed += speed
1359 elif status == download.DownloadTask.FAILED:
1360 failed_downloads.append(task)
1361 failed += 1
1362 elif status == download.DownloadTask.DONE:
1363 finished += 1
1364 elif status == download.DownloadTask.QUEUED:
1365 queued += 1
1366 elif status == download.DownloadTask.PAUSED:
1367 paused += 1
1368 else:
1369 others += 1
1371 # Remember which tasks we have seen after this run
1372 self.download_tasks_seen = download_tasks_seen
1374 if gpodder.ui.desktop:
1375 text = [_('Downloads')]
1376 if downloading + failed + queued > 0:
1377 s = []
1378 if downloading > 0:
1379 s.append(N_('%d active', '%d active', downloading) % downloading)
1380 if failed > 0:
1381 s.append(N_('%d failed', '%d failed', failed) % failed)
1382 if queued > 0:
1383 s.append(N_('%d queued', '%d queued', queued) % queued)
1384 text.append(' (' + ', '.join(s)+')')
1385 self.labelDownloads.set_text(''.join(text))
1386 elif gpodder.ui.diablo:
1387 sum = downloading + failed + finished + queued + paused + others
1388 if sum:
1389 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
1390 else:
1391 self.tool_downloads.set_label(_('Downloads'))
1392 elif gpodder.ui.fremantle:
1393 if downloading + queued > 0:
1394 self.button_downloads.set_value(N_('%d active', '%d active', downloading+queued) % (downloading+queued))
1395 elif failed > 0:
1396 self.button_downloads.set_value(N_('%d failed', '%d failed', failed) % failed)
1397 elif paused > 0:
1398 self.button_downloads.set_value(N_('%d paused', '%d paused', paused) % paused)
1399 else:
1400 self.button_downloads.set_value(_('Idle'))
1402 title = [self.default_title]
1404 # We have to update all episodes/channels for which the status has
1405 # changed. Accessing task.status_changed has the side effect of
1406 # re-setting the changed flag, so we need to get the "changed" list
1407 # of tuples first and split it into two lists afterwards
1408 changed = [(task.url, task.podcast_url) for task in \
1409 self.download_tasks_seen if task.status_changed]
1410 episode_urls = [episode_url for episode_url, channel_url in changed]
1411 channel_urls = [channel_url for episode_url, channel_url in changed]
1413 count = downloading + queued
1414 if count > 0:
1415 title.append(N_('downloading %d file', 'downloading %d files', count) % count)
1417 if total_size > 0:
1418 percentage = 100.0*done_size/total_size
1419 else:
1420 percentage = 0.0
1421 total_speed = util.format_filesize(total_speed)
1422 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
1423 if self.tray_icon is not None:
1424 # Update the tray icon status and progress bar
1425 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
1426 self.tray_icon.draw_progress_bar(percentage/100.)
1427 else:
1428 if self.tray_icon is not None:
1429 # Update the tray icon status
1430 self.tray_icon.set_status()
1431 if gpodder.ui.desktop:
1432 self.downloads_finished(self.download_tasks_seen)
1433 if gpodder.ui.diablo:
1434 hildon.hildon_banner_show_information(self.gPodder, '', 'gPodder: %s' % _('All downloads finished'))
1435 log('All downloads have finished.', sender=self)
1436 if self.config.cmd_all_downloads_complete:
1437 util.run_external_command(self.config.cmd_all_downloads_complete)
1439 if gpodder.ui.fremantle and failed:
1440 message = '\n'.join(['%s: %s' % (str(task), \
1441 task.error_message) for task in failed_downloads])
1442 self.show_message(message, _('Downloads failed'), important=True)
1444 # Remove finished episodes
1445 if self.config.auto_cleanup_downloads and can_call_cleanup:
1446 self.cleanup_downloads()
1448 # Stop updating the download list here
1449 self.download_list_update_enabled = False
1451 if not gpodder.ui.fremantle:
1452 self.gPodder.set_title(' - '.join(title))
1454 self.update_episode_list_icons(episode_urls)
1455 if self.episode_shownotes_window is not None:
1456 if (shownotes_task and shownotes_task.url in episode_urls) or \
1457 shownotes_task != self.episode_shownotes_window.task:
1458 self.episode_shownotes_window._download_status_changed(shownotes_task)
1459 self.episode_shownotes_window._download_status_progress()
1460 self.play_or_download()
1461 if channel_urls:
1462 self.update_podcast_list_model(channel_urls)
1464 return self.download_list_update_enabled
1465 except Exception, e:
1466 log('Exception happened while updating download list.', sender=self, traceback=True)
1467 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1468 # We return False here, so the update loop won't be called again,
1469 # that's why we require the restart of gPodder in the message.
1470 return False
1472 def on_config_changed(self, *args):
1473 util.idle_add(self._on_config_changed, *args)
1475 def _on_config_changed(self, name, old_value, new_value):
1476 if name == 'show_toolbar' and gpodder.ui.desktop:
1477 self.toolbar.set_property('visible', new_value)
1478 elif name == 'videoplayer':
1479 self.config.video_played_dbus = False
1480 elif name == 'player':
1481 self.config.audio_played_dbus = False
1482 elif name == 'episode_list_descriptions':
1483 self.update_episode_list_model()
1484 elif name == 'episode_list_thumbnails':
1485 self.update_episode_list_icons(all=True)
1486 elif name == 'rotation_mode':
1487 self._fremantle_rotation.set_mode(new_value)
1488 elif name in ('auto_update_feeds', 'auto_update_frequency'):
1489 self.restart_auto_update_timer()
1490 elif name == 'podcast_list_view_all':
1491 # Force a update of the podcast list model
1492 self.channel_list_changed = True
1493 if gpodder.ui.fremantle:
1494 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1495 while gtk.events_pending():
1496 gtk.main_iteration(False)
1497 self.update_podcast_list_model()
1498 if gpodder.ui.fremantle:
1499 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1501 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1502 # With get_bin_window, we get the window that contains the rows without
1503 # the header. The Y coordinate of this window will be the height of the
1504 # treeview header. This is the amount we have to subtract from the
1505 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1506 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1507 y -= x_bin
1508 y -= y_bin
1509 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1511 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or x > 50 or (column is not None and column != treeview.get_columns()[0]):
1512 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1513 return False
1515 if path is not None:
1516 model = treeview.get_model()
1517 iter = model.get_iter(path)
1518 role = getattr(treeview, TreeViewHelper.ROLE)
1520 if role == TreeViewHelper.ROLE_EPISODES:
1521 id = model.get_value(iter, EpisodeListModel.C_URL)
1522 elif role == TreeViewHelper.ROLE_PODCASTS:
1523 id = model.get_value(iter, PodcastListModel.C_URL)
1525 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1526 if last_tooltip is not None and last_tooltip != id:
1527 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1528 return False
1529 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1531 if role == TreeViewHelper.ROLE_EPISODES:
1532 description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1533 if description:
1534 tooltip.set_text(description)
1535 else:
1536 return False
1537 elif role == TreeViewHelper.ROLE_PODCASTS:
1538 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1539 if channel is None:
1540 return False
1541 channel.request_save_dir_size()
1542 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1543 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1544 if error_str:
1545 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1546 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1547 table = gtk.Table(rows=3, columns=3)
1548 table.set_row_spacings(5)
1549 table.set_col_spacings(5)
1550 table.set_border_width(5)
1552 heading = gtk.Label()
1553 heading.set_alignment(0, 1)
1554 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1555 table.attach(heading, 0, 1, 0, 1)
1556 size_info = gtk.Label()
1557 size_info.set_alignment(1, 1)
1558 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1559 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1560 table.attach(size_info, 2, 3, 0, 1)
1562 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1564 if len(channel.description) < 500:
1565 description = channel.description
1566 else:
1567 pos = channel.description.find('\n\n')
1568 if pos == -1 or pos > 500:
1569 description = channel.description[:498]+'[...]'
1570 else:
1571 description = channel.description[:pos]
1573 description = gtk.Label(description)
1574 if error_str:
1575 description.set_markup(error_str)
1576 description.set_alignment(0, 0)
1577 description.set_line_wrap(True)
1578 table.attach(description, 0, 3, 2, 3)
1580 table.show_all()
1581 tooltip.set_custom(table)
1583 return True
1585 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1586 return False
1588 def treeview_allow_tooltips(self, treeview, allow):
1589 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1591 def update_m3u_playlist_clicked(self, widget):
1592 if self.active_channel is not None:
1593 self.active_channel.update_m3u_playlist()
1594 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1596 def treeview_handle_context_menu_click(self, treeview, event):
1597 x, y = int(event.x), int(event.y)
1598 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1600 selection = treeview.get_selection()
1601 model, paths = selection.get_selected_rows()
1603 if path is None or (path not in paths and \
1604 event.button == self.context_menu_mouse_button):
1605 # We have right-clicked, but not into the selection,
1606 # assume we don't want to operate on the selection
1607 paths = []
1609 if path is not None and not paths and \
1610 event.button == self.context_menu_mouse_button:
1611 # No selection or clicked outside selection;
1612 # select the single item where we clicked
1613 treeview.grab_focus()
1614 treeview.set_cursor(path, column, 0)
1615 paths = [path]
1617 if not paths:
1618 # Unselect any remaining items (clicked elsewhere)
1619 if hasattr(treeview, 'is_rubber_banding_active'):
1620 if not treeview.is_rubber_banding_active():
1621 selection.unselect_all()
1622 else:
1623 selection.unselect_all()
1625 return model, paths
1627 def downloads_list_get_selection(self, model=None, paths=None):
1628 if model is None and paths is None:
1629 selection = self.treeDownloads.get_selection()
1630 model, paths = selection.get_selected_rows()
1632 can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1633 selected_tasks = [(gtk.TreeRowReference(model, path), \
1634 model.get_value(model.get_iter(path), \
1635 DownloadStatusModel.C_TASK)) for path in paths]
1637 for row_reference, task in selected_tasks:
1638 if task.status != download.DownloadTask.QUEUED:
1639 can_force = False
1640 if task.status not in (download.DownloadTask.PAUSED, \
1641 download.DownloadTask.FAILED, \
1642 download.DownloadTask.CANCELLED):
1643 can_queue = False
1644 if task.status not in (download.DownloadTask.PAUSED, \
1645 download.DownloadTask.QUEUED, \
1646 download.DownloadTask.DOWNLOADING, \
1647 download.DownloadTask.FAILED):
1648 can_cancel = False
1649 if task.status not in (download.DownloadTask.QUEUED, \
1650 download.DownloadTask.DOWNLOADING):
1651 can_pause = False
1652 if task.status not in (download.DownloadTask.CANCELLED, \
1653 download.DownloadTask.FAILED, \
1654 download.DownloadTask.DONE):
1655 can_remove = False
1657 return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1659 def downloads_finished(self, download_tasks_seen):
1660 # FIXME: Filter all tasks that have already been reported
1661 finished_downloads = [str(task) for task in download_tasks_seen if task.status == task.DONE]
1662 failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.status == task.FAILED]
1664 if finished_downloads and failed_downloads:
1665 message = self.format_episode_list(finished_downloads, 5)
1666 message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1667 message += self.format_episode_list(failed_downloads, 5)
1668 self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1669 elif finished_downloads:
1670 message = self.format_episode_list(finished_downloads)
1671 self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1672 elif failed_downloads:
1673 message = self.format_episode_list(failed_downloads)
1674 self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1676 # Open torrent files right after download (bug 1029)
1677 if self.config.open_torrent_after_download:
1678 for task in download_tasks_seen:
1679 if task.status != task.DONE:
1680 continue
1682 episode = task.episode
1683 if episode.mimetype != 'application/x-bittorrent':
1684 continue
1686 self.playback_episodes([episode])
1689 def format_episode_list(self, episode_list, max_episodes=10):
1691 Format a list of episode names for notifications
1693 Will truncate long episode names and limit the amount of
1694 episodes displayed (max_episodes=10).
1696 The episode_list parameter should be a list of strings.
1698 MAX_TITLE_LENGTH = 100
1700 result = []
1701 for title in episode_list[:min(len(episode_list), max_episodes)]:
1702 if len(title) > MAX_TITLE_LENGTH:
1703 middle = (MAX_TITLE_LENGTH/2)-2
1704 title = '%s...%s' % (title[0:middle], title[-middle:])
1705 result.append(saxutils.escape(title))
1706 result.append('\n')
1708 more_episodes = len(episode_list) - max_episodes
1709 if more_episodes > 0:
1710 result.append('(...')
1711 result.append(N_('%d more episode', '%d more episodes', more_episodes) % more_episodes)
1712 result.append('...)')
1714 return (''.join(result)).strip()
1716 def _for_each_task_set_status(self, tasks, status, force_start=False):
1717 episode_urls = set()
1718 model = self.treeDownloads.get_model()
1719 for row_reference, task in tasks:
1720 if status == download.DownloadTask.QUEUED:
1721 # Only queue task when its paused/failed/cancelled (or forced)
1722 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1723 self.download_queue_manager.add_task(task, force_start)
1724 self.enable_download_list_update()
1725 elif status == download.DownloadTask.CANCELLED:
1726 # Cancelling a download allowed when downloading/queued
1727 if task.status in (task.QUEUED, task.DOWNLOADING):
1728 task.status = status
1729 # Cancelling paused/failed downloads requires a call to .run()
1730 elif task.status in (task.PAUSED, task.FAILED):
1731 task.status = status
1732 # Call run, so the partial file gets deleted
1733 task.run()
1734 elif status == download.DownloadTask.PAUSED:
1735 # Pausing a download only when queued/downloading
1736 if task.status in (task.DOWNLOADING, task.QUEUED):
1737 task.status = status
1738 elif status is None:
1739 # Remove the selected task - cancel downloading/queued tasks
1740 if task.status in (task.QUEUED, task.DOWNLOADING):
1741 task.status = task.CANCELLED
1742 model.remove(model.get_iter(row_reference.get_path()))
1743 # Remember the URL, so we can tell the UI to update
1744 try:
1745 # We don't "see" this task anymore - remove it;
1746 # this is needed, so update_episode_list_icons()
1747 # below gets the correct list of "seen" tasks
1748 self.download_tasks_seen.remove(task)
1749 except KeyError, key_error:
1750 log('Cannot remove task from "seen" list: %s', task, sender=self)
1751 episode_urls.add(task.url)
1752 # Tell the task that it has been removed (so it can clean up)
1753 task.removed_from_list()
1754 else:
1755 # We can (hopefully) simply set the task status here
1756 task.status = status
1757 # Tell the podcasts tab to update icons for our removed podcasts
1758 self.update_episode_list_icons(episode_urls)
1759 # Update the tab title and downloads list
1760 self.update_downloads_list()
1762 def treeview_downloads_show_context_menu(self, treeview, event):
1763 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1764 if not paths:
1765 if not hasattr(treeview, 'is_rubber_banding_active'):
1766 return True
1767 else:
1768 return not treeview.is_rubber_banding_active()
1770 if event.button == self.context_menu_mouse_button:
1771 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1772 self.downloads_list_get_selection(model, paths)
1774 def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1775 # This creates a menu item for selection-wide actions
1776 item = gtk.ImageMenuItem(label)
1777 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1778 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1779 item.set_sensitive(sensitive)
1780 return self.set_finger_friendly(item)
1782 menu = gtk.Menu()
1784 item = gtk.ImageMenuItem(_('Episode details'))
1785 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1786 if len(selected_tasks) == 1:
1787 row_reference, task = selected_tasks[0]
1788 episode = task.episode
1789 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1790 else:
1791 item.set_sensitive(False)
1792 menu.append(self.set_finger_friendly(item))
1793 menu.append(gtk.SeparatorMenuItem())
1794 if can_force:
1795 menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1796 else:
1797 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1798 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1799 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1800 menu.append(gtk.SeparatorMenuItem())
1801 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1803 if gpodder.ui.maemo or self.config.enable_fingerscroll:
1804 # Because we open the popup on left-click for Maemo,
1805 # we also include a non-action to close the menu
1806 menu.append(gtk.SeparatorMenuItem())
1807 item = gtk.ImageMenuItem(_('Close this menu'))
1808 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1810 menu.append(self.set_finger_friendly(item))
1812 menu.show_all()
1813 menu.popup(None, None, None, event.button, event.time)
1814 return True
1816 def treeview_channels_show_context_menu(self, treeview, event):
1817 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1818 if not paths:
1819 return True
1821 # Check for valid channel id, if there's no id then
1822 # assume that it is a proxy channel or equivalent
1823 # and cannot be operated with right click
1824 if self.active_channel.id is None:
1825 return True
1827 if event.button == 3:
1828 menu = gtk.Menu()
1830 ICON = lambda x: x
1832 item = gtk.ImageMenuItem( _('Update podcast'))
1833 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1834 item.connect('activate', self.on_itemUpdateChannel_activate)
1835 item.set_sensitive(not self.updating_feed_cache)
1836 menu.append(item)
1838 menu.append(gtk.SeparatorMenuItem())
1840 item = gtk.CheckMenuItem(_('Keep episodes'))
1841 item.set_active(self.active_channel.channel_is_locked)
1842 item.connect('activate', self.on_channel_toggle_lock_activate)
1843 menu.append(self.set_finger_friendly(item))
1845 item = gtk.ImageMenuItem(_('Remove podcast'))
1846 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1847 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1848 menu.append( item)
1850 if self.config.device_type != 'none':
1851 item = gtk.MenuItem(_('Synchronize to device'))
1852 item.connect('activate', lambda item: self.on_sync_to_ipod_activate(item, self.active_channel.get_downloaded_episodes()))
1853 menu.append(item)
1855 menu.append( gtk.SeparatorMenuItem())
1857 item = gtk.ImageMenuItem(_('Podcast details'))
1858 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1859 item.connect('activate', self.on_itemEditChannel_activate)
1860 menu.append(item)
1862 menu.show_all()
1863 # Disable tooltips while we are showing the menu, so
1864 # the tooltip will not appear over the menu
1865 self.treeview_allow_tooltips(self.treeChannels, False)
1866 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1867 menu.popup( None, None, None, event.button, event.time)
1869 return True
1871 def on_itemClose_activate(self, widget):
1872 if self.tray_icon is not None:
1873 self.iconify_main_window()
1874 else:
1875 self.on_gPodder_delete_event(widget)
1877 def cover_file_removed(self, channel_url):
1879 The Cover Downloader calls this when a previously-
1880 available cover has been removed from the disk. We
1881 have to update our model to reflect this change.
1883 self.podcast_list_model.delete_cover_by_url(channel_url)
1885 def cover_download_finished(self, channel, pixbuf):
1887 The Cover Downloader calls this when it has finished
1888 downloading (or registering, if already downloaded)
1889 a new channel cover, which is ready for displaying.
1891 self.podcast_list_model.add_cover_by_channel(channel, pixbuf)
1893 def save_episodes_as_file(self, episodes):
1894 for episode in episodes:
1895 self.save_episode_as_file(episode)
1897 def save_episode_as_file(self, episode):
1898 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1899 if episode.was_downloaded(and_exists=True):
1900 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1901 copy_from = episode.local_filename(create=False)
1902 assert copy_from is not None
1903 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1904 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1905 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1907 def copy_episodes_bluetooth(self, episodes):
1908 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1910 if gpodder.ui.maemo:
1911 util.bluetooth_send_files_maemo([e.local_filename(create=False) \
1912 for e in episodes_to_copy])
1913 return True
1915 def convert_and_send_thread(episode):
1916 for episode in episodes:
1917 filename = episode.local_filename(create=False)
1918 assert filename is not None
1919 destfile = os.path.join(tempfile.gettempdir(), \
1920 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1921 (base, ext) = os.path.splitext(filename)
1922 if not destfile.endswith(ext):
1923 destfile += ext
1925 try:
1926 shutil.copyfile(filename, destfile)
1927 util.bluetooth_send_file(destfile)
1928 except:
1929 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1930 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1932 util.delete_file(destfile)
1934 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1936 def get_device_name(self):
1937 if self.config.device_type == 'ipod':
1938 return _('iPod')
1939 elif self.config.device_type in ('filesystem', 'mtp'):
1940 return _('MP3 player')
1941 else:
1942 return '(unknown device)'
1944 def _treeview_button_released(self, treeview, event):
1945 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1946 dy = int(abs(event.y-ypos))
1947 dx = int(event.x-xpos)
1949 selection = treeview.get_selection()
1950 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1951 if path is None or dy > 30:
1952 return (False, dx, dy)
1954 path, column, x, y = path
1955 selection.select_path(path)
1956 treeview.set_cursor(path)
1957 treeview.grab_focus()
1959 return (True, dx, dy)
1961 def treeview_channels_handle_gestures(self, treeview, event):
1962 if self.currently_updating:
1963 return False
1965 selected, dx, dy = self._treeview_button_released(treeview, event)
1967 if selected:
1968 if self.config.maemo_enable_gestures:
1969 if dx > 70:
1970 self.on_itemUpdateChannel_activate()
1971 elif dx < -70:
1972 self.on_itemEditChannel_activate(treeview)
1974 return False
1976 def treeview_available_handle_gestures(self, treeview, event):
1977 selected, dx, dy = self._treeview_button_released(treeview, event)
1979 if selected:
1980 if self.config.maemo_enable_gestures:
1981 if dx > 70:
1982 self.on_playback_selected_episodes(None)
1983 return True
1984 elif dx < -70:
1985 self.on_shownotes_selected_episodes(None)
1986 return True
1988 # Pass the event to the context menu handler for treeAvailable
1989 self.treeview_available_show_context_menu(treeview, event)
1991 return True
1993 def treeview_available_show_context_menu(self, treeview, event):
1994 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1995 if not paths:
1996 if not hasattr(treeview, 'is_rubber_banding_active'):
1997 return True
1998 else:
1999 return not treeview.is_rubber_banding_active()
2001 if event.button == self.context_menu_mouse_button:
2002 episodes = self.get_selected_episodes()
2003 any_locked = any(e.is_locked for e in episodes)
2004 any_played = any(e.is_played for e in episodes)
2005 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
2006 downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
2007 downloading = any(self.episode_is_downloading(e) for e in episodes)
2009 menu = gtk.Menu()
2011 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
2013 if open_instead_of_play:
2014 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
2015 elif downloaded:
2016 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
2017 else:
2018 item = gtk.ImageMenuItem(_('Stream'))
2019 item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
2021 item.set_sensitive(can_play and not downloading)
2022 item.connect('activate', self.on_playback_selected_episodes)
2023 menu.append(self.set_finger_friendly(item))
2025 if not can_cancel:
2026 item = gtk.ImageMenuItem(_('Download'))
2027 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
2028 item.set_sensitive(can_download)
2029 item.connect('activate', self.on_download_selected_episodes)
2030 menu.append(self.set_finger_friendly(item))
2031 else:
2032 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
2033 item.connect('activate', self.on_item_cancel_download_activate)
2034 menu.append(self.set_finger_friendly(item))
2036 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
2037 item.set_sensitive(can_delete)
2038 item.connect('activate', self.on_btnDownloadedDelete_clicked)
2039 menu.append(self.set_finger_friendly(item))
2041 ICON = lambda x: x
2043 # Ok, this probably makes sense to only display for downloaded files
2044 if downloaded:
2045 menu.append(gtk.SeparatorMenuItem())
2046 share_item = gtk.MenuItem(_('Send to'))
2047 menu.append(self.set_finger_friendly(share_item))
2048 share_menu = gtk.Menu()
2050 item = gtk.ImageMenuItem(_('Local folder'))
2051 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
2052 item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
2053 share_menu.append(self.set_finger_friendly(item))
2054 if self.bluetooth_available:
2055 item = gtk.ImageMenuItem(_('Bluetooth device'))
2056 if gpodder.ui.maemo:
2057 icon_name = ICON('qgn_list_filesys_bluetooth')
2058 else:
2059 icon_name = ICON('bluetooth')
2060 item.set_image(gtk.image_new_from_icon_name(icon_name, gtk.ICON_SIZE_MENU))
2061 item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
2062 share_menu.append(self.set_finger_friendly(item))
2063 if can_transfer:
2064 item = gtk.ImageMenuItem(self.get_device_name())
2065 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
2066 item.connect('button-press-event', lambda w, ee: self.on_sync_to_ipod_activate(w, episodes))
2067 share_menu.append(self.set_finger_friendly(item))
2069 share_item.set_submenu(share_menu)
2071 if (downloaded or one_is_new or can_download) and not downloading:
2072 menu.append(gtk.SeparatorMenuItem())
2073 if one_is_new:
2074 item = gtk.CheckMenuItem(_('New'))
2075 item.set_active(True)
2076 item.connect('activate', lambda w: self.mark_selected_episodes_old())
2077 menu.append(self.set_finger_friendly(item))
2078 elif can_download:
2079 item = gtk.CheckMenuItem(_('New'))
2080 item.set_active(False)
2081 item.connect('activate', lambda w: self.mark_selected_episodes_new())
2082 menu.append(self.set_finger_friendly(item))
2084 if downloaded:
2085 item = gtk.CheckMenuItem(_('Played'))
2086 item.set_active(any_played)
2087 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
2088 menu.append(self.set_finger_friendly(item))
2090 item = gtk.CheckMenuItem(_('Keep episode'))
2091 item.set_active(any_locked)
2092 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
2093 menu.append(self.set_finger_friendly(item))
2095 menu.append(gtk.SeparatorMenuItem())
2096 # Single item, add episode information menu item
2097 item = gtk.ImageMenuItem(_('Episode details'))
2098 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
2099 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
2100 menu.append(self.set_finger_friendly(item))
2102 if gpodder.ui.maemo or self.config.enable_fingerscroll:
2103 # Because we open the popup on left-click for Maemo,
2104 # we also include a non-action to close the menu
2105 menu.append(gtk.SeparatorMenuItem())
2106 item = gtk.ImageMenuItem(_('Close this menu'))
2107 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
2108 menu.append(self.set_finger_friendly(item))
2110 menu.show_all()
2111 # Disable tooltips while we are showing the menu, so
2112 # the tooltip will not appear over the menu
2113 self.treeview_allow_tooltips(self.treeAvailable, False)
2114 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
2115 menu.popup( None, None, None, event.button, event.time)
2117 return True
2119 def set_title(self, new_title):
2120 if not gpodder.ui.fremantle:
2121 self.default_title = new_title
2122 self.gPodder.set_title(new_title)
2124 def update_episode_list_icons(self, urls=None, selected=False, all=False):
2126 Updates the status icons in the episode list.
2128 If urls is given, it should be a list of URLs
2129 of episodes that should be updated.
2131 If urls is None, set ONE OF selected, all to
2132 True (the former updates just the selected
2133 episodes and the latter updates all episodes).
2135 additional_args = (self.episode_is_downloading, \
2136 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2137 self.config.episode_list_thumbnails and gpodder.ui.desktop)
2139 if urls is not None:
2140 # We have a list of URLs to walk through
2141 self.episode_list_model.update_by_urls(urls, *additional_args)
2142 elif selected and not all:
2143 # We should update all selected episodes
2144 selection = self.treeAvailable.get_selection()
2145 model, paths = selection.get_selected_rows()
2146 for path in reversed(paths):
2147 iter = model.get_iter(path)
2148 self.episode_list_model.update_by_filter_iter(iter, \
2149 *additional_args)
2150 elif all and not selected:
2151 # We update all (even the filter-hidden) episodes
2152 self.episode_list_model.update_all(*additional_args)
2153 else:
2154 # Wrong/invalid call - have to specify at least one parameter
2155 raise ValueError('Invalid call to update_episode_list_icons')
2157 def episode_list_status_changed(self, episodes):
2158 self.update_episode_list_icons(set(e.url for e in episodes))
2159 self.update_podcast_list_model(set(e.channel.url for e in episodes))
2160 self.db.commit()
2162 def clean_up_downloads(self, delete_partial=False):
2163 # Clean up temporary files left behind by old gPodder versions
2164 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
2166 if delete_partial:
2167 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
2169 for tempfile in temporary_files:
2170 util.delete_file(tempfile)
2172 # Clean up empty download folders and abandoned download folders
2173 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
2174 for ddir in download_dirs:
2175 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2176 globr = glob.glob(os.path.join(ddir, '*'))
2177 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
2178 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
2179 shutil.rmtree(ddir, ignore_errors=True)
2181 def streaming_possible(self):
2182 if gpodder.ui.desktop:
2183 # User has to have a media player set on the Desktop, or else we
2184 # would probably open the browser when giving a URL to xdg-open..
2185 return (self.config.player and self.config.player != 'default')
2186 elif gpodder.ui.maemo:
2187 # On Maemo, the default is to use the Nokia Media Player, which is
2188 # already able to deal with HTTP URLs the right way, so we
2189 # unconditionally enable streaming always on Maemo
2190 return True
2192 return False
2194 def playback_episodes_for_real(self, episodes):
2195 groups = collections.defaultdict(list)
2196 for episode in episodes:
2197 file_type = episode.file_type()
2198 if file_type == 'video' and self.config.videoplayer and \
2199 self.config.videoplayer != 'default':
2200 player = self.config.videoplayer
2201 if gpodder.ui.diablo:
2202 # Use the wrapper script if it's installed to crop 3GP YouTube
2203 # videos to fit the screen (looks much nicer than w/ black border)
2204 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2205 player = 'gpodder-mplayer'
2206 elif gpodder.ui.fremantle and player == 'mplayer':
2207 player = 'mplayer -fs %F'
2208 elif file_type == 'audio' and self.config.player and \
2209 self.config.player != 'default':
2210 player = self.config.player
2211 else:
2212 player = 'default'
2214 if file_type not in ('audio', 'video') or \
2215 (file_type == 'audio' and not self.config.audio_played_dbus) or \
2216 (file_type == 'video' and not self.config.video_played_dbus):
2217 # Mark episode as played in the database
2218 episode.mark(is_played=True)
2219 self.mygpo_client.on_playback([episode])
2221 filename = episode.local_filename(create=False)
2222 if filename is None or not os.path.exists(filename):
2223 filename = episode.url
2224 if youtube.is_video_link(filename):
2225 fmt_id = self.config.youtube_preferred_fmt_id
2226 if gpodder.ui.fremantle:
2227 fmt_id = 5
2228 filename = youtube.get_real_download_url(filename, fmt_id)
2230 # Determine the playback resume position - if the file
2231 # was played 100%, we simply start from the beginning
2232 resume_position = episode.current_position
2233 if resume_position == episode.total_time:
2234 resume_position = 0
2236 if gpodder.ui.fremantle:
2237 self.mafw_monitor.set_resume_point(filename, resume_position)
2239 # If Panucci is configured, use D-Bus on Maemo to call it
2240 if player == 'panucci':
2241 try:
2242 PANUCCI_NAME = 'org.panucci.panucciInterface'
2243 PANUCCI_PATH = '/panucciInterface'
2244 PANUCCI_INTF = 'org.panucci.panucciInterface'
2245 o = gpodder.dbus_session_bus.get_object(PANUCCI_NAME, PANUCCI_PATH)
2246 i = dbus.Interface(o, PANUCCI_INTF)
2248 def on_reply(*args):
2249 pass
2251 def error_handler(filename, err):
2252 log('Exception in D-Bus call: %s', str(err), \
2253 sender=self)
2255 # Fallback: use the command line client
2256 for command in util.format_desktop_command('panucci', \
2257 [filename]):
2258 log('Executing: %s', repr(command), sender=self)
2259 subprocess.Popen(command)
2261 on_error = lambda err: error_handler(filename, err)
2263 # This method only exists in Panucci > 0.9 ('new Panucci')
2264 i.playback_from(filename, resume_position, \
2265 reply_handler=on_reply, error_handler=on_error)
2267 continue # This file was handled by the D-Bus call
2268 except Exception, e:
2269 log('Error calling Panucci using D-Bus', sender=self, traceback=True)
2270 elif player == 'MediaBox' and gpodder.ui.maemo:
2271 try:
2272 MEDIABOX_NAME = 'de.pycage.mediabox'
2273 MEDIABOX_PATH = '/de/pycage/mediabox/control'
2274 MEDIABOX_INTF = 'de.pycage.mediabox.control'
2275 o = gpodder.dbus_session_bus.get_object(MEDIABOX_NAME, MEDIABOX_PATH)
2276 i = dbus.Interface(o, MEDIABOX_INTF)
2278 def on_reply(*args):
2279 pass
2281 def on_error(err):
2282 log('Exception in D-Bus call: %s', str(err), \
2283 sender=self)
2285 i.load(filename, '%s/x-unknown' % file_type, \
2286 reply_handler=on_reply, error_handler=on_error)
2288 continue # This file was handled by the D-Bus call
2289 except Exception, e:
2290 log('Error calling MediaBox using D-Bus', sender=self, traceback=True)
2292 groups[player].append(filename)
2294 # Open episodes with system default player
2295 if 'default' in groups:
2296 if gpodder.ui.maemo and len(groups['default']) > 1:
2297 # The Nokia Media Player app does not support receiving multiple
2298 # file names via D-Bus, so we simply place all file names into a
2299 # temporary M3U playlist and open that with the Media Player.
2300 m3u_filename = os.path.join(gpodder.home, 'gpodder_open_with.m3u')
2301 util.write_m3u_playlist(m3u_filename, groups['default'], extm3u=False)
2302 util.gui_open(m3u_filename)
2303 else:
2304 for filename in groups['default']:
2305 log('Opening with system default: %s', filename, sender=self)
2306 util.gui_open(filename)
2307 del groups['default']
2308 elif gpodder.ui.maemo and groups:
2309 # When on Maemo and not opening with default, show a notification
2310 # (no startup notification for Panucci / MPlayer yet...)
2311 if len(episodes) == 1:
2312 text = _('Opening %s') % episodes[0].title
2313 else:
2314 count = len(episodes)
2315 text = N_('Opening %d episode', 'Opening %d episodes', count) % count
2317 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2319 def destroy_banner_later(banner):
2320 banner.destroy()
2321 return False
2322 gobject.timeout_add(5000, destroy_banner_later, banner)
2324 # For each type now, go and create play commands
2325 for group in groups:
2326 for command in util.format_desktop_command(group, groups[group]):
2327 log('Executing: %s', repr(command), sender=self)
2328 subprocess.Popen(command)
2330 # Persist episode status changes to the database
2331 self.db.commit()
2333 # Flush updated episode status
2334 self.mygpo_client.flush()
2336 def playback_episodes(self, episodes):
2337 # We need to create a list, because we run through it more than once
2338 episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2339 e.was_downloaded(and_exists=True) or self.streaming_possible()))
2341 try:
2342 self.playback_episodes_for_real(episodes)
2343 except Exception, e:
2344 log('Error in playback!', sender=self, traceback=True)
2345 if gpodder.ui.desktop:
2346 self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2347 _('Error opening player'), widget=self.toolPreferences)
2348 else:
2349 self.show_message(_('Please check your media player settings in the preferences dialog.'))
2351 channel_urls = set()
2352 episode_urls = set()
2353 for episode in episodes:
2354 channel_urls.add(episode.channel.url)
2355 episode_urls.add(episode.url)
2356 self.update_episode_list_icons(episode_urls)
2357 self.update_podcast_list_model(channel_urls)
2359 def play_or_download(self):
2360 if not gpodder.ui.fremantle:
2361 if self.wNotebook.get_current_page() > 0:
2362 if gpodder.ui.desktop:
2363 self.toolCancel.set_sensitive(True)
2364 return
2366 if self.currently_updating:
2367 return (False, False, False, False, False, False)
2369 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2370 ( is_played, is_locked ) = (False,)*2
2372 open_instead_of_play = False
2374 selection = self.treeAvailable.get_selection()
2375 if selection.count_selected_rows() > 0:
2376 (model, paths) = selection.get_selected_rows()
2378 for path in paths:
2379 try:
2380 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2381 except TypeError, te:
2382 log('Invalid episode at path %s', str(path), sender=self)
2383 continue
2385 if episode.file_type() not in ('audio', 'video'):
2386 open_instead_of_play = True
2388 if episode.was_downloaded():
2389 can_play = episode.was_downloaded(and_exists=True)
2390 is_played = episode.is_played
2391 is_locked = episode.is_locked
2392 if not can_play:
2393 can_download = True
2394 else:
2395 if self.episode_is_downloading(episode):
2396 can_cancel = True
2397 else:
2398 can_download = True
2400 can_download = can_download and not can_cancel
2401 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2402 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2403 can_delete = not can_cancel
2405 if gpodder.ui.desktop:
2406 if open_instead_of_play:
2407 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2408 else:
2409 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2410 self.toolPlay.set_sensitive( can_play)
2411 self.toolDownload.set_sensitive( can_download)
2412 self.toolTransfer.set_sensitive( can_transfer)
2413 self.toolCancel.set_sensitive( can_cancel)
2415 if not gpodder.ui.fremantle:
2416 self.item_cancel_download.set_sensitive(can_cancel)
2417 self.itemDownloadSelected.set_sensitive(can_download)
2418 self.itemOpenSelected.set_sensitive(can_play)
2419 self.itemPlaySelected.set_sensitive(can_play)
2420 self.itemDeleteSelected.set_sensitive(can_delete)
2421 self.item_toggle_played.set_sensitive(can_play)
2422 self.item_toggle_lock.set_sensitive(can_play)
2423 self.itemOpenSelected.set_visible(open_instead_of_play)
2424 self.itemPlaySelected.set_visible(not open_instead_of_play)
2426 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2428 def on_cbMaxDownloads_toggled(self, widget, *args):
2429 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2431 def on_cbLimitDownloads_toggled(self, widget, *args):
2432 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2434 def episode_new_status_changed(self, urls):
2435 self.update_podcast_list_model()
2436 self.update_episode_list_icons(urls)
2438 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2439 """Update the podcast list treeview model
2441 If urls is given, it should list the URLs of each
2442 podcast that has to be updated in the list.
2444 If selected is True, only update the model contents
2445 for the currently-selected podcast - nothing more.
2447 The caller can optionally specify "select_url",
2448 which is the URL of the podcast that is to be
2449 selected in the list after the update is complete.
2450 This only works if the podcast list has to be
2451 reloaded; i.e. something has been added or removed
2452 since the last update of the podcast list).
2454 selection = self.treeChannels.get_selection()
2455 model, iter = selection.get_selected()
2457 if self.config.podcast_list_view_all and not self.channel_list_changed:
2458 # Update "all episodes" view in any case (if enabled)
2459 self.podcast_list_model.update_first_row()
2461 if selected:
2462 # very cheap! only update selected channel
2463 if iter is not None:
2464 # If we have selected the "all episodes" view, we have
2465 # to update all channels for selected episodes:
2466 if self.config.podcast_list_view_all and \
2467 self.podcast_list_model.iter_is_first_row(iter):
2468 urls = self.get_podcast_urls_from_selected_episodes()
2469 self.podcast_list_model.update_by_urls(urls)
2470 else:
2471 # Otherwise just update the selected row (a podcast)
2472 self.podcast_list_model.update_by_filter_iter(iter)
2473 elif not self.channel_list_changed:
2474 # we can keep the model, but have to update some
2475 if urls is None:
2476 # still cheaper than reloading the whole list
2477 self.podcast_list_model.update_all()
2478 else:
2479 # ok, we got a bunch of urls to update
2480 self.podcast_list_model.update_by_urls(urls)
2481 else:
2482 if model and iter and select_url is None:
2483 # Get the URL of the currently-selected podcast
2484 select_url = model.get_value(iter, PodcastListModel.C_URL)
2486 # Update the podcast list model with new channels
2487 self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2489 try:
2490 selected_iter = model.get_iter_first()
2491 # Find the previously-selected URL in the new
2492 # model if we have an URL (else select first)
2493 if select_url is not None:
2494 pos = model.get_iter_first()
2495 while pos is not None:
2496 url = model.get_value(pos, PodcastListModel.C_URL)
2497 if url == select_url:
2498 selected_iter = pos
2499 break
2500 pos = model.iter_next(pos)
2502 if not gpodder.ui.fremantle:
2503 if selected_iter is not None:
2504 selection.select_iter(selected_iter)
2505 self.on_treeChannels_cursor_changed(self.treeChannels)
2506 except:
2507 log('Cannot select podcast in list', traceback=True, sender=self)
2508 self.channel_list_changed = False
2510 def episode_is_downloading(self, episode):
2511 """Returns True if the given episode is being downloaded at the moment"""
2512 if episode is None:
2513 return False
2515 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2517 def on_episode_list_filter_changed(self, has_episodes):
2518 if gpodder.ui.fremantle:
2519 if has_episodes:
2520 self.episodes_window.empty_label.hide()
2521 self.episodes_window.pannablearea.show()
2522 else:
2523 if self.config.episode_list_view_mode != \
2524 EpisodeListModel.VIEW_ALL:
2525 text = _('No episodes in current view')
2526 else:
2527 text = _('No episodes available')
2528 self.episodes_window.empty_label.set_text(text)
2529 self.episodes_window.pannablearea.hide()
2530 self.episodes_window.empty_label.show()
2532 def update_episode_list_model(self):
2533 if self.channels and self.active_channel is not None:
2534 if gpodder.ui.fremantle:
2535 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2537 self.currently_updating = True
2538 self.episode_list_model.clear()
2539 if gpodder.ui.fremantle:
2540 self.episodes_window.pannablearea.hide()
2541 self.episodes_window.empty_label.set_text(_('Loading episodes'))
2542 self.episodes_window.empty_label.show()
2544 def update():
2545 additional_args = (self.episode_is_downloading, \
2546 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2547 self.config.episode_list_thumbnails and gpodder.ui.desktop)
2548 self.episode_list_model.replace_from_channel(self.active_channel, *additional_args)
2550 self.treeAvailable.get_selection().unselect_all()
2551 self.treeAvailable.scroll_to_point(0, 0)
2553 self.currently_updating = False
2554 self.play_or_download()
2556 if gpodder.ui.fremantle:
2557 hildon.hildon_gtk_window_set_progress_indicator(\
2558 self.episodes_window.main_window, False)
2560 util.idle_add(update)
2561 else:
2562 self.episode_list_model.clear()
2564 @dbus.service.method(gpodder.dbus_interface)
2565 def offer_new_episodes(self, channels=None):
2566 if gpodder.ui.fremantle:
2567 # Assume that when this function is called that the
2568 # notification is not shown anymore (Maemo bug 11345)
2569 self._fremantle_notification_visible = False
2571 new_episodes = self.get_new_episodes(channels)
2572 if new_episodes:
2573 self.new_episodes_show(new_episodes)
2574 return True
2575 return False
2577 def add_podcast_list(self, urls, auth_tokens=None):
2578 """Subscribe to a list of podcast given their URLs
2580 If auth_tokens is given, it should be a dictionary
2581 mapping URLs to (username, password) tuples."""
2583 if auth_tokens is None:
2584 auth_tokens = {}
2586 # Sort and split the URL list into five buckets
2587 queued, failed, existing, worked, authreq = [], [], [], [], []
2588 for input_url in urls:
2589 url = util.normalize_feed_url(input_url)
2590 if url is None:
2591 # Fail this one because the URL is not valid
2592 failed.append(input_url)
2593 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2594 # A podcast already exists in the list for this URL
2595 existing.append(url)
2596 else:
2597 # This URL has survived the first round - queue for add
2598 queued.append(url)
2599 if url != input_url and input_url in auth_tokens:
2600 auth_tokens[url] = auth_tokens[input_url]
2602 error_messages = {}
2603 redirections = {}
2605 progress = ProgressIndicator(_('Adding podcasts'), \
2606 _('Please wait while episode information is downloaded.'), \
2607 parent=self.get_dialog_parent())
2609 def on_after_update():
2610 progress.on_finished()
2611 # Report already-existing subscriptions to the user
2612 if existing:
2613 title = _('Existing subscriptions skipped')
2614 message = _('You are already subscribed to these podcasts:') \
2615 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2616 self.show_message(message, title, widget=self.treeChannels)
2618 # Report subscriptions that require authentication
2619 if authreq:
2620 retry_podcasts = {}
2621 for url in authreq:
2622 title = _('Podcast requires authentication')
2623 message = _('Please login to %s:') % (saxutils.escape(url),)
2624 success, auth_tokens = self.show_login_dialog(title, message)
2625 if success:
2626 retry_podcasts[url] = auth_tokens
2627 else:
2628 # Stop asking the user for more login data
2629 retry_podcasts = {}
2630 for url in authreq:
2631 error_messages[url] = _('Authentication failed')
2632 failed.append(url)
2633 break
2635 # If we have authentication data to retry, do so here
2636 if retry_podcasts:
2637 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2639 # Report website redirections
2640 for url in redirections:
2641 title = _('Website redirection detected')
2642 message = _('The URL %(url)s redirects to %(target)s.') \
2643 + '\n\n' + _('Do you want to visit the website now?')
2644 message = message % {'url': url, 'target': redirections[url]}
2645 if self.show_confirmation(message, title):
2646 util.open_website(url)
2647 else:
2648 break
2650 # Report failed subscriptions to the user
2651 if failed:
2652 title = _('Could not add some podcasts')
2653 message = _('Some podcasts could not be added to your list:') \
2654 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2655 error_messages.get(url, _('Unknown')))) for url in failed)
2656 self.show_message(message, title, important=True)
2658 # Upload subscription changes to gpodder.net
2659 self.mygpo_client.on_subscribe(worked)
2661 # If at least one podcast has been added, save and update all
2662 if self.channel_list_changed:
2663 # Fix URLs if mygpo has rewritten them
2664 self.rewrite_urls_mygpo()
2666 self.save_channels_opml()
2668 # If only one podcast was added, select it after the update
2669 if len(worked) == 1:
2670 url = worked[0]
2671 else:
2672 url = None
2674 # Update the list of subscribed podcasts
2675 self.update_feed_cache(force_update=False, select_url_afterwards=url)
2676 self.update_podcasts_tab()
2678 # Offer to download new episodes
2679 episodes = []
2680 for podcast in self.channels:
2681 if podcast.url in worked:
2682 episodes.extend(podcast.get_all_episodes())
2684 if episodes:
2685 episodes = list(PodcastEpisode.sort_by_pubdate(episodes, \
2686 reverse=True))
2687 self.new_episodes_show(episodes, \
2688 selected=[e.check_is_new() for e in episodes])
2691 def thread_proc():
2692 # After the initial sorting and splitting, try all queued podcasts
2693 length = len(queued)
2694 for index, url in enumerate(queued):
2695 progress.on_progress(float(index)/float(length))
2696 progress.on_message(url)
2697 log('QUEUE RUNNER: %s', url, sender=self)
2698 try:
2699 # The URL is valid and does not exist already - subscribe!
2700 channel = PodcastChannel.load(self.db, url=url, create=True, \
2701 authentication_tokens=auth_tokens.get(url, None), \
2702 max_episodes=self.config.max_episodes_per_feed, \
2703 download_dir=self.config.download_dir, \
2704 allow_empty_feeds=self.config.allow_empty_feeds, \
2705 mimetype_prefs=self.config.mimetype_prefs)
2707 try:
2708 username, password = util.username_password_from_url(url)
2709 except ValueError, ve:
2710 username, password = (None, None)
2712 if username is not None and channel.username is None and \
2713 password is not None and channel.password is None:
2714 channel.username = username
2715 channel.password = password
2716 channel.save()
2718 self._update_cover(channel)
2719 except feedcore.AuthenticationRequired:
2720 if url in auth_tokens:
2721 # Fail for wrong authentication data
2722 error_messages[url] = _('Authentication failed')
2723 failed.append(url)
2724 else:
2725 # Queue for login dialog later
2726 authreq.append(url)
2727 continue
2728 except feedcore.WifiLogin, error:
2729 redirections[url] = error.data
2730 failed.append(url)
2731 error_messages[url] = _('Redirection detected')
2732 continue
2733 except Exception, e:
2734 log('Subscription error: %s', e, traceback=True, sender=self)
2735 error_messages[url] = str(e)
2736 failed.append(url)
2737 continue
2739 assert channel is not None
2740 worked.append(channel.url)
2741 self.channels.append(channel)
2742 self.channel_list_changed = True
2743 util.idle_add(on_after_update)
2744 threading.Thread(target=thread_proc).start()
2746 def save_channels_opml(self):
2747 exporter = opml.Exporter(gpodder.subscription_file)
2748 return exporter.write(self.channels)
2750 def find_episode(self, podcast_url, episode_url):
2751 """Find an episode given its podcast and episode URL
2753 The function will return a PodcastEpisode object if
2754 the episode is found, or None if it's not found.
2756 for podcast in self.channels:
2757 if podcast_url == podcast.url:
2758 for episode in podcast.get_all_episodes():
2759 if episode_url == episode.url:
2760 return episode
2762 return None
2764 def process_received_episode_actions(self, updated_urls):
2765 """Process/merge episode actions from gpodder.net
2767 This function will merge all changes received from
2768 the server to the local database and update the
2769 status of the affected episodes as necessary.
2771 indicator = ProgressIndicator(_('Merging episode actions'), \
2772 _('Episode actions from gpodder.net are merged.'), \
2773 False, self.get_dialog_parent())
2775 for idx, action in enumerate(self.mygpo_client.get_episode_actions(updated_urls)):
2776 if action.action == 'play':
2777 episode = self.find_episode(action.podcast_url, \
2778 action.episode_url)
2780 if episode is not None:
2781 log('Play action for %s', episode.url, sender=self)
2782 episode.mark(is_played=True)
2784 if action.timestamp > episode.current_position_updated and \
2785 action.position is not None:
2786 log('Updating position for %s', episode.url, sender=self)
2787 episode.current_position = action.position
2788 episode.current_position_updated = action.timestamp
2790 if action.total:
2791 log('Updating total time for %s', episode.url, sender=self)
2792 episode.total_time = action.total
2794 episode.save()
2795 elif action.action == 'delete':
2796 episode = self.find_episode(action.podcast_url, \
2797 action.episode_url)
2799 if episode is not None:
2800 if not episode.was_downloaded(and_exists=True):
2801 # Set the episode to a "deleted" state
2802 log('Marking as deleted: %s', episode.url, sender=self)
2803 episode.delete_from_disk()
2804 episode.save()
2806 indicator.on_message(N_('%d action processed', '%d actions processed', idx) % idx)
2807 gtk.main_iteration(False)
2809 indicator.on_finished()
2810 self.db.commit()
2813 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2814 self.db.commit()
2815 self.updating_feed_cache = False
2817 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2819 # Process received episode actions for all updated URLs
2820 self.process_received_episode_actions(updated_urls)
2822 self.channel_list_changed = True
2823 self.update_podcast_list_model(select_url=select_url_afterwards)
2825 # Only search for new episodes in podcasts that have been
2826 # updated, not in other podcasts (for single-feed updates)
2827 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2829 if gpodder.ui.fremantle:
2830 self.fancy_progress_bar.hide()
2831 self.button_subscribe.set_sensitive(True)
2832 self.button_refresh.set_sensitive(True)
2833 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2834 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2835 self.update_podcasts_tab()
2836 self.update_episode_list_model()
2837 if self.feed_cache_update_cancelled:
2838 return
2840 def application_in_foreground():
2841 try:
2842 return any(w.get_property('is-topmost') for w in hildon.WindowStack.get_default().get_windows())
2843 except Exception, e:
2844 log('Could not determine is-topmost', traceback=True)
2845 # When in doubt, assume not in foreground
2846 return False
2848 if episodes:
2849 if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2850 # New episodes found, but we should do nothing
2851 self.show_message(_('New episodes are available.'))
2852 elif self.config.auto_download == 'always':
2853 count = len(episodes)
2854 title = N_('Downloading %d new episode.', 'Downloading %d new episodes.', count) % count
2855 self.show_message(title)
2856 self.download_episode_list(episodes)
2857 elif self.config.auto_download == 'queue':
2858 self.show_message(_('New episodes have been added to the download list.'))
2859 self.download_episode_list_paused(episodes)
2860 elif application_in_foreground():
2861 if not self._fremantle_notification_visible:
2862 self.new_episodes_show(episodes)
2863 elif not self._fremantle_notification_visible:
2864 try:
2865 import pynotify
2866 pynotify.init('gPodder')
2867 n = pynotify.Notification('gPodder', _('New episodes available'), 'gpodder')
2868 n.set_urgency(pynotify.URGENCY_CRITICAL)
2869 n.set_hint('dbus-callback-default', ' '.join([
2870 gpodder.dbus_bus_name,
2871 gpodder.dbus_gui_object_path,
2872 gpodder.dbus_interface,
2873 'offer_new_episodes',
2875 n.set_category('gpodder-new-episodes')
2876 n.show()
2877 self._fremantle_notification_visible = True
2878 except Exception, e:
2879 log('Error: %s', str(e), sender=self, traceback=True)
2880 self.new_episodes_show(episodes)
2881 self._fremantle_notification_visible = False
2882 elif not self.config.auto_update_feeds:
2883 self.show_message(_('No new episodes. Please check for new episodes later.'))
2884 return
2886 if self.tray_icon:
2887 self.tray_icon.set_status()
2889 if self.feed_cache_update_cancelled:
2890 # The user decided to abort the feed update
2891 self.show_update_feeds_buttons()
2892 elif not episodes:
2893 # Nothing new here - but inform the user
2894 self.pbFeedUpdate.set_fraction(1.0)
2895 self.pbFeedUpdate.set_text(_('No new episodes'))
2896 self.feed_cache_update_cancelled = True
2897 self.btnCancelFeedUpdate.show()
2898 self.btnCancelFeedUpdate.set_sensitive(True)
2899 self.itemUpdate.set_sensitive(True)
2900 if gpodder.ui.maemo:
2901 # btnCancelFeedUpdate is a ToolButton on Maemo
2902 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2903 else:
2904 # btnCancelFeedUpdate is a normal gtk.Button
2905 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2906 else:
2907 count = len(episodes)
2908 # New episodes are available
2909 self.pbFeedUpdate.set_fraction(1.0)
2910 # Are we minimized and should we auto download?
2911 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2912 self.download_episode_list(episodes)
2913 title = N_('Downloading %d new episode.', 'Downloading %d new episodes.', count) % count
2914 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2915 self.show_update_feeds_buttons()
2916 elif self.config.auto_download == 'queue':
2917 self.download_episode_list_paused(episodes)
2918 title = N_('%d new episode added to download list.', '%d new episodes added to download list.', count) % count
2919 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2920 self.show_update_feeds_buttons()
2921 else:
2922 self.show_update_feeds_buttons()
2923 # New episodes are available and we are not minimized
2924 if not self.config.do_not_show_new_episodes_dialog:
2925 self.new_episodes_show(episodes, notification=True)
2926 else:
2927 message = N_('%d new episode available', '%d new episodes available', count) % count
2928 self.pbFeedUpdate.set_text(message)
2930 def _update_cover(self, channel):
2931 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2932 self.cover_downloader.request_cover(channel)
2934 def update_feed_cache_proc(self, channels, select_url_afterwards):
2935 total = len(channels)
2937 for updated, channel in enumerate(channels):
2938 if not self.feed_cache_update_cancelled:
2939 try:
2940 channel.update(max_episodes=self.config.max_episodes_per_feed, \
2941 mimetype_prefs=self.config.mimetype_prefs)
2942 self._update_cover(channel)
2943 except Exception, e:
2944 d = {'url': saxutils.escape(channel.url), 'message': saxutils.escape(str(e))}
2945 if d['message']:
2946 message = _('Error while updating %(url)s: %(message)s')
2947 else:
2948 message = _('The feed at %(url)s could not be updated.')
2949 self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
2950 log('Error: %s', str(e), sender=self, traceback=True)
2952 if self.feed_cache_update_cancelled:
2953 break
2955 # By the time we get here the update may have already been cancelled
2956 if not self.feed_cache_update_cancelled:
2957 def update_progress():
2958 d = {'podcast': channel.title, 'position': updated+1, 'total': total}
2959 progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2960 self.pbFeedUpdate.set_text(progression)
2961 if self.tray_icon:
2962 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2963 self.pbFeedUpdate.set_fraction(float(updated+1)/float(total))
2964 util.idle_add(update_progress)
2966 updated_urls = [c.url for c in channels]
2967 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2969 def show_update_feeds_buttons(self):
2970 # Make sure that the buttons for updating feeds
2971 # appear - this should happen after a feed update
2972 if gpodder.ui.maemo:
2973 self.btnUpdateSelectedFeed.show()
2974 self.toolFeedUpdateProgress.hide()
2975 self.btnCancelFeedUpdate.hide()
2976 self.btnCancelFeedUpdate.set_is_important(False)
2977 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2978 self.toolbarSpacer.set_expand(True)
2979 self.toolbarSpacer.set_draw(False)
2980 else:
2981 self.hboxUpdateFeeds.hide()
2982 self.btnUpdateFeeds.show()
2983 self.itemUpdate.set_sensitive(True)
2984 self.itemUpdateChannel.set_sensitive(True)
2986 def on_btnCancelFeedUpdate_clicked(self, widget):
2987 if not self.feed_cache_update_cancelled:
2988 self.pbFeedUpdate.set_text(_('Cancelling...'))
2989 self.feed_cache_update_cancelled = True
2990 if not gpodder.ui.fremantle:
2991 self.btnCancelFeedUpdate.set_sensitive(False)
2992 elif not gpodder.ui.fremantle:
2993 self.show_update_feeds_buttons()
2995 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
2996 if self.updating_feed_cache:
2997 if gpodder.ui.fremantle:
2998 self.feed_cache_update_cancelled = True
2999 return
3001 if not force_update:
3002 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
3003 self.channel_list_changed = True
3004 self.update_podcast_list_model(select_url=select_url_afterwards)
3005 return
3007 # Fix URLs if mygpo has rewritten them
3008 self.rewrite_urls_mygpo()
3010 self.updating_feed_cache = True
3012 if channels is None:
3013 # Only update podcasts for which updates are enabled
3014 channels = [c for c in self.channels if c.feed_update_enabled]
3016 if gpodder.ui.fremantle:
3017 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
3018 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
3019 self.fancy_progress_bar.show()
3020 self.button_subscribe.set_sensitive(False)
3021 self.button_refresh.set_sensitive(False)
3022 self.feed_cache_update_cancelled = False
3023 else:
3024 self.itemUpdate.set_sensitive(False)
3025 self.itemUpdateChannel.set_sensitive(False)
3027 if self.tray_icon:
3028 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
3030 self.feed_cache_update_cancelled = False
3031 self.btnCancelFeedUpdate.show()
3032 self.btnCancelFeedUpdate.set_sensitive(True)
3033 if gpodder.ui.maemo:
3034 self.toolbarSpacer.set_expand(False)
3035 self.toolbarSpacer.set_draw(True)
3036 self.btnUpdateSelectedFeed.hide()
3037 self.toolFeedUpdateProgress.show_all()
3038 else:
3039 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
3040 self.hboxUpdateFeeds.show_all()
3041 self.btnUpdateFeeds.hide()
3043 if len(channels) == 1:
3044 text = _('Updating "%s"...') % channels[0].title
3045 else:
3046 count = len(channels)
3047 text = N_('Updating %d feed...', 'Updating %d feeds...', count) % count
3048 self.pbFeedUpdate.set_text(text)
3049 self.pbFeedUpdate.set_fraction(0)
3051 args = (channels, select_url_afterwards)
3052 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
3054 def on_gPodder_delete_event(self, widget, *args):
3055 """Called when the GUI wants to close the window
3056 Displays a confirmation dialog (and closes/hides gPodder)
3059 downloading = self.download_status_model.are_downloads_in_progress()
3061 # Only iconify if we are using the window's "X" button,
3062 # but not when we are using "Quit" in the menu or toolbar
3063 if self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
3064 self.iconify_main_window()
3065 elif self.config.on_quit_ask or downloading:
3066 if gpodder.ui.fremantle:
3067 self.close_gpodder()
3068 elif gpodder.ui.diablo:
3069 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
3070 if result:
3071 self.close_gpodder()
3072 else:
3073 return True
3074 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
3075 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3076 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
3078 title = _('Quit gPodder')
3079 if downloading:
3080 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3081 else:
3082 message = _('Do you really want to quit gPodder now?')
3084 dialog.set_title(title)
3085 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
3086 if not downloading:
3087 cb_ask = gtk.CheckButton(_("Don't ask me again"))
3088 dialog.vbox.pack_start(cb_ask)
3089 cb_ask.show_all()
3091 quit_button.grab_focus()
3092 result = dialog.run()
3093 dialog.destroy()
3095 if result == gtk.RESPONSE_CLOSE:
3096 if not downloading and cb_ask.get_active() == True:
3097 self.config.on_quit_ask = False
3098 self.close_gpodder()
3099 else:
3100 self.close_gpodder()
3102 return True
3104 def close_gpodder(self):
3105 """ clean everything and exit properly
3107 if self.channels:
3108 if self.save_channels_opml():
3109 pass # FIXME: Add mygpo synchronization here
3110 else:
3111 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
3113 self.gPodder.hide()
3115 if self.tray_icon is not None:
3116 self.tray_icon.set_visible(False)
3118 # Notify all tasks to to carry out any clean-up actions
3119 self.download_status_model.tell_all_tasks_to_quit()
3121 while gtk.events_pending():
3122 gtk.main_iteration(False)
3124 self.db.close()
3126 self.quit()
3127 sys.exit(0)
3129 def get_expired_episodes(self):
3130 for channel in self.channels:
3131 for episode in channel.get_downloaded_episodes():
3132 # Never consider locked episodes as old
3133 if episode.is_locked:
3134 continue
3136 # Never consider fresh episodes as old
3137 if episode.age_in_days() < self.config.episode_old_age:
3138 continue
3140 # Do not delete played episodes (except if configured)
3141 if episode.is_played:
3142 if not self.config.auto_remove_played_episodes:
3143 continue
3145 # Do not delete unplayed episodes (except if configured)
3146 if not episode.is_played:
3147 if not self.config.auto_remove_unplayed_episodes:
3148 continue
3150 yield episode
3152 def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
3153 if not episodes:
3154 return False
3156 if skip_locked:
3157 episodes = [e for e in episodes if not e.is_locked]
3159 if not episodes:
3160 title = _('Episodes are locked')
3161 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3162 self.notification(message, title, widget=self.treeAvailable)
3163 return False
3165 count = len(episodes)
3166 title = N_('Delete %d episode?', 'Delete %d episodes?', count) % count
3167 message = _('Deleting episodes removes downloaded files.')
3169 if gpodder.ui.fremantle:
3170 message = '\n'.join([title, message])
3172 if confirm and not self.show_confirmation(message, title):
3173 return False
3175 progress = ProgressIndicator(_('Deleting episodes'), \
3176 _('Please wait while episodes are deleted'), \
3177 parent=self.get_dialog_parent())
3179 def finish_deletion(episode_urls, channel_urls):
3180 progress.on_finished()
3182 # Episodes have been deleted - persist the database
3183 self.db.commit()
3185 self.update_episode_list_icons(episode_urls)
3186 self.update_podcast_list_model(channel_urls)
3187 self.play_or_download()
3189 def thread_proc():
3190 episode_urls = set()
3191 channel_urls = set()
3193 episodes_status_update = []
3194 for idx, episode in enumerate(episodes):
3195 progress.on_progress(float(idx)/float(len(episodes)))
3196 if episode.is_locked and skip_locked:
3197 log('Not deleting episode (is locked): %s', episode.title)
3198 else:
3199 log('Deleting episode: %s', episode.title)
3200 progress.on_message(episode.title)
3201 episode.delete_from_disk()
3202 episode_urls.add(episode.url)
3203 channel_urls.add(episode.channel.url)
3204 episodes_status_update.append(episode)
3206 # Tell the shownotes window that we have removed the episode
3207 if self.episode_shownotes_window is not None and \
3208 self.episode_shownotes_window.episode is not None and \
3209 self.episode_shownotes_window.episode.url == episode.url:
3210 util.idle_add(self.episode_shownotes_window._download_status_changed, None)
3212 # Notify the web service about the status update + upload
3213 self.mygpo_client.on_delete(episodes_status_update)
3214 self.mygpo_client.flush()
3216 util.idle_add(finish_deletion, episode_urls, channel_urls)
3218 threading.Thread(target=thread_proc).start()
3220 return True
3222 def on_itemRemoveOldEpisodes_activate( self, widget):
3223 if gpodder.ui.maemo:
3224 columns = (
3225 ('maemo_remove_markup', None, None, _('Episode')),
3227 else:
3228 columns = (
3229 ('title_markup', None, None, _('Episode')),
3230 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3231 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3232 ('played_prop', None, None, _('Status')),
3233 ('age_prop', 'age_int_prop', gobject.TYPE_INT, _('Downloaded')),
3236 msg_older_than = N_('Select older than %d day', 'Select older than %d days', self.config.episode_old_age)
3237 selection_buttons = {
3238 _('Select played'): lambda episode: episode.is_played,
3239 _('Select finished'): lambda episode: episode.is_finished(),
3240 msg_older_than % self.config.episode_old_age: lambda episode: episode.age_in_days() > self.config.episode_old_age,
3243 instructions = _('Select the episodes you want to delete:')
3245 episodes = []
3246 selected = []
3247 for channel in self.channels:
3248 for episode in channel.get_downloaded_episodes():
3249 # Disallow deletion of locked episodes that still exist
3250 if not episode.is_locked or not episode.file_exists():
3251 episodes.append(episode)
3252 # Automatically select played and file-less episodes
3253 selected.append(episode.is_played or \
3254 not episode.file_exists())
3256 gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
3257 episodes = episodes, selected = selected, columns = columns, \
3258 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
3259 selection_buttons = selection_buttons, _config=self.config, \
3260 show_episode_shownotes=self.show_episode_shownotes)
3262 def on_selected_episodes_status_changed(self):
3263 # The order of the updates here is important! When "All episodes" is
3264 # selected, the update of the podcast list model depends on the episode
3265 # list selection to determine which podcasts are affected. Updating
3266 # the episode list could remove the selection if a filter is active.
3267 self.update_podcast_list_model(selected=True)
3268 self.update_episode_list_icons(selected=True)
3269 self.db.commit()
3271 def mark_selected_episodes_new(self):
3272 for episode in self.get_selected_episodes():
3273 episode.mark_new()
3274 self.on_selected_episodes_status_changed()
3276 def mark_selected_episodes_old(self):
3277 for episode in self.get_selected_episodes():
3278 episode.mark_old()
3279 self.on_selected_episodes_status_changed()
3281 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
3282 for episode in self.get_selected_episodes():
3283 if toggle:
3284 episode.mark(is_played=not episode.is_played)
3285 else:
3286 episode.mark(is_played=new_value)
3287 self.on_selected_episodes_status_changed()
3289 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3290 for episode in self.get_selected_episodes():
3291 if toggle:
3292 episode.mark(is_locked=not episode.is_locked)
3293 else:
3294 episode.mark(is_locked=new_value)
3295 self.on_selected_episodes_status_changed()
3297 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
3298 if self.active_channel is None:
3299 return
3301 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
3302 self.active_channel.update_channel_lock()
3304 for episode in self.active_channel.get_all_episodes():
3305 episode.mark(is_locked=self.active_channel.channel_is_locked)
3307 self.update_podcast_list_model(selected=True)
3308 self.update_episode_list_icons(all=True)
3310 def on_itemUpdateChannel_activate(self, widget=None):
3311 if self.active_channel is None:
3312 title = _('No podcast selected')
3313 message = _('Please select a podcast in the podcasts list to update.')
3314 self.show_message( message, title, widget=self.treeChannels)
3315 return
3317 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3318 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3319 self.update_feed_cache()
3320 else:
3321 self.update_feed_cache(channels=[self.active_channel])
3323 def on_itemUpdate_activate(self, widget=None):
3324 # Check if we have outstanding subscribe/unsubscribe actions
3325 if self.on_add_remove_podcasts_mygpo():
3326 log('Update cancelled (received server changes)', sender=self)
3327 return
3329 if self.channels:
3330 self.update_feed_cache()
3331 else:
3332 gPodderWelcome(self.gPodder,
3333 center_on_widget=self.gPodder,
3334 show_example_podcasts_callback=self.on_itemImportChannels_activate,
3335 setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
3337 def download_episode_list_paused(self, episodes):
3338 self.download_episode_list(episodes, True)
3340 def download_episode_list(self, episodes, add_paused=False, force_start=False):
3341 enable_update = False
3343 for episode in episodes:
3344 log('Downloading episode: %s', episode.title, sender = self)
3345 if not episode.was_downloaded(and_exists=True):
3346 task_exists = False
3347 for task in self.download_tasks_seen:
3348 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
3349 self.download_queue_manager.add_task(task, force_start)
3350 enable_update = True
3351 task_exists = True
3352 continue
3354 if task_exists:
3355 continue
3357 try:
3358 task = download.DownloadTask(episode, self.config)
3359 except Exception, e:
3360 d = {'episode': episode.title, 'message': str(e)}
3361 message = _('Download error while downloading %(episode)s: %(message)s')
3362 self.show_message(message % d, _('Download error'), important=True)
3363 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
3364 continue
3366 if add_paused:
3367 task.status = task.PAUSED
3368 else:
3369 self.mygpo_client.on_download([task.episode])
3370 self.download_queue_manager.add_task(task, force_start)
3372 self.download_status_model.register_task(task)
3373 enable_update = True
3375 if enable_update:
3376 self.enable_download_list_update()
3378 # Flush updated episode status
3379 self.mygpo_client.flush()
3381 def cancel_task_list(self, tasks):
3382 if not tasks:
3383 return
3385 for task in tasks:
3386 if task.status in (task.QUEUED, task.DOWNLOADING):
3387 task.status = task.CANCELLED
3388 elif task.status == task.PAUSED:
3389 task.status = task.CANCELLED
3390 # Call run, so the partial file gets deleted
3391 task.run()
3393 self.update_episode_list_icons([task.url for task in tasks])
3394 self.play_or_download()
3396 # Update the tab title and downloads list
3397 self.update_downloads_list()
3399 def new_episodes_show(self, episodes, notification=False, selected=None):
3400 if gpodder.ui.maemo:
3401 columns = (
3402 ('maemo_markup', None, None, _('Episode')),
3404 show_notification = notification
3405 else:
3406 columns = (
3407 ('title_markup', None, None, _('Episode')),
3408 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3409 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3411 show_notification = False
3413 instructions = _('Select the episodes you want to download:')
3415 if self.new_episodes_window is not None:
3416 self.new_episodes_window.main_window.destroy()
3417 self.new_episodes_window = None
3419 def download_episodes_callback(episodes):
3420 self.new_episodes_window = None
3421 self.download_episode_list(episodes)
3423 if selected is None:
3424 # Select all by default
3425 selected = [True]*len(episodes)
3427 self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3428 title=_('New episodes available'), \
3429 instructions=instructions, \
3430 episodes=episodes, \
3431 columns=columns, \
3432 selected=selected, \
3433 stock_ok_button = 'gpodder-download', \
3434 callback=download_episodes_callback, \
3435 remove_callback=lambda e: e.mark_old(), \
3436 remove_action=_('Mark as old'), \
3437 remove_finished=self.episode_new_status_changed, \
3438 _config=self.config, \
3439 show_notification=show_notification, \
3440 show_episode_shownotes=self.show_episode_shownotes)
3442 def on_itemDownloadAllNew_activate(self, widget, *args):
3443 if not self.offer_new_episodes():
3444 self.show_message(_('Please check for new episodes later.'), \
3445 _('No new episodes available'), widget=self.btnUpdateFeeds)
3447 def get_new_episodes(self, channels=None):
3448 if channels is None:
3449 channels = self.channels
3450 episodes = []
3451 for channel in channels:
3452 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3453 episodes.append(episode)
3455 return episodes
3457 @dbus.service.method(gpodder.dbus_interface)
3458 def start_device_synchronization(self):
3459 """Public D-Bus API for starting Device sync (Desktop only)
3461 This method can be called to initiate a synchronization with
3462 a configured protable media player. This only works for the
3463 Desktop version of gPodder and does nothing on Maemo.
3465 if gpodder.ui.desktop:
3466 self.on_sync_to_ipod_activate(None)
3467 return True
3469 return False
3471 def on_sync_to_ipod_activate(self, widget, episodes=None):
3472 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
3474 def commit_changes_to_database(self):
3475 """This will be called after the sync process is finished"""
3476 self.db.commit()
3478 def on_cleanup_ipod_activate(self, widget, *args):
3479 self.sync_ui.on_cleanup_device()
3481 def on_manage_device_playlist(self, widget):
3482 self.sync_ui.on_manage_device_playlist()
3484 def show_hide_tray_icon(self):
3485 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3486 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3487 elif not self.config.display_tray_icon and self.tray_icon is not None:
3488 self.tray_icon.set_visible(False)
3489 del self.tray_icon
3490 self.tray_icon = None
3492 if self.config.minimize_to_tray and self.tray_icon:
3493 self.tray_icon.set_visible(self.is_iconified())
3494 elif self.tray_icon:
3495 self.tray_icon.set_visible(True)
3497 def on_itemShowAllEpisodes_activate(self, widget):
3498 self.config.podcast_list_view_all = widget.get_active()
3500 def on_itemShowToolbar_activate(self, widget):
3501 self.config.show_toolbar = self.itemShowToolbar.get_active()
3503 def on_itemShowDescription_activate(self, widget):
3504 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3506 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3507 self.config.podcast_list_hide_boring = toggleaction.get_active()
3508 if self.config.podcast_list_hide_boring:
3509 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3510 else:
3511 self.podcast_list_model.set_view_mode(-1)
3513 def on_item_view_podcasts_changed(self, radioaction, current):
3514 # Only on Fremantle
3515 if current == self.item_view_podcasts_all:
3516 self.podcast_list_model.set_view_mode(-1)
3517 elif current == self.item_view_podcasts_downloaded:
3518 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3519 elif current == self.item_view_podcasts_unplayed:
3520 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3522 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3524 def on_item_view_episodes_changed(self, radioaction, current):
3525 if current == self.item_view_episodes_all:
3526 self.config.episode_list_view_mode = EpisodeListModel.VIEW_ALL
3527 elif current == self.item_view_episodes_undeleted:
3528 self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNDELETED
3529 elif current == self.item_view_episodes_downloaded:
3530 self.config.episode_list_view_mode = EpisodeListModel.VIEW_DOWNLOADED
3531 elif current == self.item_view_episodes_unplayed:
3532 self.config.episode_list_view_mode = EpisodeListModel.VIEW_UNPLAYED
3534 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
3536 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3537 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3539 def update_item_device( self):
3540 if not gpodder.ui.fremantle:
3541 if self.config.device_type != 'none':
3542 self.itemDevice.set_visible(True)
3543 self.itemDevice.label = self.get_device_name()
3544 else:
3545 self.itemDevice.set_visible(False)
3547 def properties_closed( self):
3548 self.preferences_dialog = None
3549 self.show_hide_tray_icon()
3550 self.update_item_device()
3551 if gpodder.ui.maemo:
3552 selection = self.treeAvailable.get_selection()
3553 if self.config.maemo_enable_gestures or \
3554 self.config.enable_fingerscroll:
3555 selection.set_mode(gtk.SELECTION_SINGLE)
3556 else:
3557 selection.set_mode(gtk.SELECTION_MULTIPLE)
3559 def on_itemPreferences_activate(self, widget, *args):
3560 self.preferences_dialog = gPodderPreferences(self.main_window, \
3561 _config=self.config, \
3562 callback_finished=self.properties_closed, \
3563 user_apps_reader=self.user_apps_reader, \
3564 parent_window=self.main_window, \
3565 mygpo_client=self.mygpo_client, \
3566 on_send_full_subscriptions=self.on_send_full_subscriptions)
3568 # Initial message to relayout window (in case it's opened in portrait mode
3569 self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3571 def on_itemDependencies_activate(self, widget):
3572 gPodderDependencyManager(self.gPodder)
3574 def on_goto_mygpo(self, widget):
3575 self.mygpo_client.open_website()
3577 def on_download_subscriptions_from_mygpo(self, action=None):
3578 title = _('Login to gpodder.net')
3579 message = _('Please login to download your subscriptions.')
3580 success, (username, password) = self.show_login_dialog(title, message, \
3581 self.config.mygpo_username, self.config.mygpo_password)
3582 if not success:
3583 return
3585 self.config.mygpo_username = username
3586 self.config.mygpo_password = password
3588 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3589 custom_title=_('Subscriptions on gpodder.net'), \
3590 add_urls_callback=self.add_podcast_list, \
3591 hide_url_entry=True)
3593 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3594 # we do not have to hardcode the URL here
3595 OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
3596 url = util.url_add_authentication(OPML_URL, \
3597 self.config.mygpo_username, \
3598 self.config.mygpo_password)
3599 dir.download_opml_file(url)
3601 def on_mygpo_settings_activate(self, action=None):
3602 # This dialog is only used for Maemo 4
3603 if not gpodder.ui.diablo:
3604 return
3606 settings = MygPodderSettings(self.main_window, \
3607 config=self.config, \
3608 mygpo_client=self.mygpo_client, \
3609 on_send_full_subscriptions=self.on_send_full_subscriptions)
3611 def on_itemAddChannel_activate(self, widget=None):
3612 gPodderAddPodcast(self.gPodder, \
3613 add_urls_callback=self.add_podcast_list)
3615 def on_itemEditChannel_activate(self, widget, *args):
3616 if self.active_channel is None:
3617 title = _('No podcast selected')
3618 message = _('Please select a podcast in the podcasts list to edit.')
3619 self.show_message( message, title, widget=self.treeChannels)
3620 return
3622 callback_closed = lambda: self.update_podcast_list_model(selected=True)
3623 gPodderChannel(self.main_window, \
3624 channel=self.active_channel, \
3625 callback_closed=callback_closed, \
3626 cover_downloader=self.cover_downloader)
3628 def on_itemMassUnsubscribe_activate(self, item=None):
3629 columns = (
3630 ('title', None, None, _('Podcast')),
3633 # We're abusing the Episode Selector for selecting Podcasts here,
3634 # but it works and looks good, so why not? -- thp
3635 gPodderEpisodeSelector(self.main_window, \
3636 title=_('Remove podcasts'), \
3637 instructions=_('Select the podcast you want to remove.'), \
3638 episodes=self.channels, \
3639 columns=columns, \
3640 size_attribute=None, \
3641 stock_ok_button=_('Remove'), \
3642 callback=self.remove_podcast_list, \
3643 _config=self.config)
3645 def remove_podcast_list(self, channels, confirm=True):
3646 if not channels:
3647 log('No podcasts selected for deletion', sender=self)
3648 return
3650 if len(channels) == 1:
3651 title = _('Removing podcast')
3652 info = _('Please wait while the podcast is removed')
3653 message = _('Do you really want to remove this podcast and its episodes?')
3654 else:
3655 title = _('Removing podcasts')
3656 info = _('Please wait while the podcasts are removed')
3657 message = _('Do you really want to remove the selected podcasts and their episodes?')
3659 if confirm and not self.show_confirmation(message, title):
3660 return
3662 progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3664 def finish_deletion(select_url):
3665 # Upload subscription list changes to the web service
3666 self.mygpo_client.on_unsubscribe([c.url for c in channels])
3668 # Re-load the channels and select the desired new channel
3669 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3670 progress.on_finished()
3671 self.update_podcasts_tab()
3673 def thread_proc():
3674 select_url = None
3676 for idx, channel in enumerate(channels):
3677 # Update the UI for correct status messages
3678 progress.on_progress(float(idx)/float(len(channels)))
3679 progress.on_message(channel.title)
3681 # Delete downloaded episodes
3682 channel.remove_downloaded()
3684 # cancel any active downloads from this channel
3685 for episode in channel.get_all_episodes():
3686 util.idle_add(self.download_status_model.cancel_by_url,
3687 episode.url)
3689 if len(channels) == 1:
3690 # get the URL of the podcast we want to select next
3691 if channel in self.channels:
3692 position = self.channels.index(channel)
3693 else:
3694 position = -1
3696 if position == len(self.channels)-1:
3697 # this is the last podcast, so select the URL
3698 # of the item before this one (i.e. the "new last")
3699 select_url = self.channels[position-1].url
3700 else:
3701 # there is a podcast after the deleted one, so
3702 # we simply select the one that comes after it
3703 select_url = self.channels[position+1].url
3705 # Remove the channel and clean the database entries
3706 channel.delete()
3707 self.channels.remove(channel)
3709 # Clean up downloads and download directories
3710 self.clean_up_downloads()
3712 self.channel_list_changed = True
3713 self.save_channels_opml()
3715 # The remaining stuff is to be done in the GTK main thread
3716 util.idle_add(finish_deletion, select_url)
3718 threading.Thread(target=thread_proc).start()
3720 def on_itemRemoveChannel_activate(self, widget, *args):
3721 if self.active_channel is None:
3722 title = _('No podcast selected')
3723 message = _('Please select a podcast in the podcasts list to remove.')
3724 self.show_message( message, title, widget=self.treeChannels)
3725 return
3727 self.remove_podcast_list([self.active_channel])
3729 def get_opml_filter(self):
3730 filter = gtk.FileFilter()
3731 filter.add_pattern('*.opml')
3732 filter.add_pattern('*.xml')
3733 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3734 return filter
3736 def on_item_import_from_file_activate(self, widget, filename=None):
3737 if filename is None:
3738 if gpodder.ui.desktop or gpodder.ui.fremantle:
3739 # FIXME: Hildonization on Fremantle
3740 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3741 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3742 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3743 elif gpodder.ui.diablo:
3744 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3745 dlg.set_filter(self.get_opml_filter())
3746 response = dlg.run()
3747 filename = None
3748 if response == gtk.RESPONSE_OK:
3749 filename = dlg.get_filename()
3750 dlg.destroy()
3752 if filename is not None:
3753 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3754 custom_title=_('Import podcasts from OPML file'), \
3755 add_urls_callback=self.add_podcast_list, \
3756 hide_url_entry=True)
3757 dir.download_opml_file(filename)
3759 def on_itemExportChannels_activate(self, widget, *args):
3760 if not self.channels:
3761 title = _('Nothing to export')
3762 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3763 self.show_message(message, title, widget=self.treeChannels)
3764 return
3766 if gpodder.ui.desktop or gpodder.ui.fremantle:
3767 # FIXME: Hildonization on Fremantle
3768 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3769 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3770 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3771 elif gpodder.ui.diablo:
3772 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3773 dlg.set_filter(self.get_opml_filter())
3774 response = dlg.run()
3775 if response == gtk.RESPONSE_OK:
3776 filename = dlg.get_filename()
3777 dlg.destroy()
3778 exporter = opml.Exporter( filename)
3779 if exporter.write(self.channels):
3780 count = len(self.channels)
3781 title = N_('%d subscription exported', '%d subscriptions exported', count) % count
3782 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3783 else:
3784 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3785 else:
3786 dlg.destroy()
3788 def on_itemImportChannels_activate(self, widget, *args):
3789 if gpodder.ui.fremantle:
3790 gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3791 self.config.toplist_url, \
3792 self.config.opml_url, \
3793 self.add_podcast_list, \
3794 self.on_itemAddChannel_activate, \
3795 self.on_download_subscriptions_from_mygpo, \
3796 self.show_text_edit_dialog)
3797 else:
3798 dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3799 add_urls_callback=self.add_podcast_list)
3800 util.idle_add(dir.download_opml_file, self.config.opml_url)
3802 def on_homepage_activate(self, widget, *args):
3803 util.open_website(gpodder.__url__)
3805 def on_wiki_activate(self, widget, *args):
3806 util.open_website('http://gpodder.org/wiki/User_Manual')
3808 def on_bug_tracker_activate(self, widget, *args):
3809 if gpodder.ui.maemo:
3810 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3811 else:
3812 util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3814 def on_item_support_activate(self, widget):
3815 util.open_website('http://gpodder.org/donate')
3817 def on_itemAbout_activate(self, widget, *args):
3818 if gpodder.ui.fremantle:
3819 from gpodder.gtkui.frmntl.about import HeAboutDialog
3820 HeAboutDialog.present(self.main_window,
3821 'gPodder',
3822 'gpodder',
3823 gpodder.__version__,
3824 _('A podcast client with focus on usability'),
3825 gpodder.__copyright__,
3826 gpodder.__url__,
3827 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3828 'http://gpodder.org/donate')
3829 return
3831 dlg = gtk.AboutDialog()
3832 dlg.set_transient_for(self.main_window)
3833 dlg.set_name('gPodder')
3834 dlg.set_version(gpodder.__version__)
3835 dlg.set_copyright(gpodder.__copyright__)
3836 dlg.set_comments(_('A podcast client with focus on usability'))
3837 dlg.set_website(gpodder.__url__)
3838 dlg.set_translator_credits( _('translator-credits'))
3839 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3841 if gpodder.ui.desktop:
3842 # For the "GUI" version, we add some more
3843 # items to the about dialog (credits and logo)
3844 app_authors = [
3845 _('Maintainer:'),
3846 'Thomas Perl <thpinfo.com>',
3849 if os.path.exists(gpodder.credits_file):
3850 credits = open(gpodder.credits_file).read().strip().split('\n')
3851 app_authors += ['', _('Patches, bug reports and donations by:')]
3852 app_authors += credits
3854 dlg.set_authors(app_authors)
3855 try:
3856 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3857 except:
3858 dlg.set_logo_icon_name('gpodder')
3860 dlg.run()
3862 def on_wNotebook_switch_page(self, widget, *args):
3863 page_num = args[1]
3864 if gpodder.ui.maemo:
3865 self.tool_downloads.set_active(page_num == 1)
3866 page = self.wNotebook.get_nth_page(page_num)
3867 tab_label = self.wNotebook.get_tab_label(page).get_text()
3868 if page_num == 0 and self.active_channel is not None:
3869 self.set_title(self.active_channel.title)
3870 else:
3871 self.set_title(tab_label)
3872 if page_num == 0:
3873 self.play_or_download()
3874 self.menuChannels.set_sensitive(True)
3875 self.menuSubscriptions.set_sensitive(True)
3876 # The message area in the downloads tab should be hidden
3877 # when the user switches away from the downloads tab
3878 if self.message_area is not None:
3879 self.message_area.hide()
3880 self.message_area = None
3881 else:
3882 self.menuChannels.set_sensitive(False)
3883 self.menuSubscriptions.set_sensitive(False)
3884 if gpodder.ui.desktop:
3885 self.toolDownload.set_sensitive(False)
3886 self.toolPlay.set_sensitive(False)
3887 self.toolTransfer.set_sensitive(False)
3888 self.toolCancel.set_sensitive(False)
3890 def on_treeChannels_row_activated(self, widget, path, *args):
3891 # double-click action of the podcast list or enter
3892 self.treeChannels.set_cursor(path)
3894 def on_treeChannels_cursor_changed(self, widget, *args):
3895 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3897 if model is not None and iter is not None:
3898 old_active_channel = self.active_channel
3899 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3901 if self.active_channel == old_active_channel:
3902 return
3904 if gpodder.ui.maemo:
3905 self.set_title(self.active_channel.title)
3907 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3908 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3909 self.itemEditChannel.set_visible(False)
3910 self.itemRemoveChannel.set_visible(False)
3911 else:
3912 self.itemEditChannel.set_visible(True)
3913 self.itemRemoveChannel.set_visible(True)
3914 else:
3915 self.active_channel = None
3916 self.itemEditChannel.set_visible(False)
3917 self.itemRemoveChannel.set_visible(False)
3919 self.update_episode_list_model()
3921 def on_btnEditChannel_clicked(self, widget, *args):
3922 self.on_itemEditChannel_activate( widget, args)
3924 def get_podcast_urls_from_selected_episodes(self):
3925 """Get a set of podcast URLs based on the selected episodes"""
3926 return set(episode.channel.url for episode in \
3927 self.get_selected_episodes())
3929 def get_selected_episodes(self):
3930 """Get a list of selected episodes from treeAvailable"""
3931 selection = self.treeAvailable.get_selection()
3932 model, paths = selection.get_selected_rows()
3934 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3935 return episodes
3937 def on_transfer_selected_episodes(self, widget):
3938 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3940 def on_playback_selected_episodes(self, widget):
3941 self.playback_episodes(self.get_selected_episodes())
3943 def on_shownotes_selected_episodes(self, widget):
3944 episodes = self.get_selected_episodes()
3945 if episodes:
3946 episode = episodes.pop(0)
3947 self.show_episode_shownotes(episode)
3948 else:
3949 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3951 def on_download_selected_episodes(self, widget):
3952 episodes = self.get_selected_episodes()
3953 self.download_episode_list(episodes)
3954 self.update_episode_list_icons([episode.url for episode in episodes])
3955 self.play_or_download()
3957 def on_treeAvailable_row_activated(self, widget, path, view_column):
3958 """Double-click/enter action handler for treeAvailable"""
3959 # We should only have one one selected as it was double clicked!
3960 e = self.get_selected_episodes()[0]
3962 if (self.config.double_click_episode_action == 'download'):
3963 # If the episode has already been downloaded and exists then play it
3964 if e.was_downloaded(and_exists=True):
3965 self.playback_episodes(self.get_selected_episodes())
3966 # else download it if it is not already downloading
3967 elif not self.episode_is_downloading(e):
3968 self.download_episode_list([e])
3969 self.update_episode_list_icons([e.url])
3970 self.play_or_download()
3971 elif (self.config.double_click_episode_action == 'stream'):
3972 # If we happen to have downloaded this episode simple play it
3973 if e.was_downloaded(and_exists=True):
3974 self.playback_episodes(self.get_selected_episodes())
3975 # else if streaming is possible stream it
3976 elif self.streaming_possible():
3977 self.playback_episodes(self.get_selected_episodes())
3978 else:
3979 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3980 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3981 else:
3982 # default action is to display show notes
3983 self.on_shownotes_selected_episodes(widget)
3985 def show_episode_shownotes(self, episode):
3986 if self.episode_shownotes_window is None:
3987 log('First-time use of episode window --- creating', sender=self)
3988 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
3989 _download_episode_list=self.download_episode_list, \
3990 _playback_episodes=self.playback_episodes, \
3991 _delete_episode_list=self.delete_episode_list, \
3992 _episode_list_status_changed=self.episode_list_status_changed, \
3993 _cancel_task_list=self.cancel_task_list, \
3994 _episode_is_downloading=self.episode_is_downloading, \
3995 _streaming_possible=self.streaming_possible())
3996 self.episode_shownotes_window.show(episode)
3997 if self.episode_is_downloading(episode):
3998 self.update_downloads_list()
4000 def restart_auto_update_timer(self):
4001 if self._auto_update_timer_source_id is not None:
4002 log('Removing existing auto update timer.', sender=self)
4003 gobject.source_remove(self._auto_update_timer_source_id)
4004 self._auto_update_timer_source_id = None
4006 if self.config.auto_update_feeds and \
4007 self.config.auto_update_frequency:
4008 interval = 60*1000*self.config.auto_update_frequency
4009 log('Setting up auto update timer with interval %d.', \
4010 self.config.auto_update_frequency, sender=self)
4011 self._auto_update_timer_source_id = gobject.timeout_add(\
4012 interval, self._on_auto_update_timer)
4014 def _on_auto_update_timer(self):
4015 log('Auto update timer fired.', sender=self)
4016 self.update_feed_cache(force_update=True)
4018 # Ask web service for sub changes (if enabled)
4019 self.mygpo_client.flush()
4021 return True
4023 def on_treeDownloads_row_activated(self, widget, *args):
4024 # Use the standard way of working on the treeview
4025 selection = self.treeDownloads.get_selection()
4026 (model, paths) = selection.get_selected_rows()
4027 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
4029 for tree_row_reference, task in selected_tasks:
4030 if task.status in (task.DOWNLOADING, task.QUEUED):
4031 task.status = task.PAUSED
4032 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
4033 self.download_queue_manager.add_task(task)
4034 self.enable_download_list_update()
4035 elif task.status == task.DONE:
4036 model.remove(model.get_iter(tree_row_reference.get_path()))
4038 self.play_or_download()
4040 # Update the tab title and downloads list
4041 self.update_downloads_list()
4043 def on_item_cancel_download_activate(self, widget):
4044 if self.wNotebook.get_current_page() == 0:
4045 selection = self.treeAvailable.get_selection()
4046 (model, paths) = selection.get_selected_rows()
4047 urls = [model.get_value(model.get_iter(path), \
4048 self.episode_list_model.C_URL) for path in paths]
4049 selected_tasks = [task for task in self.download_tasks_seen \
4050 if task.url in urls]
4051 else:
4052 selection = self.treeDownloads.get_selection()
4053 (model, paths) = selection.get_selected_rows()
4054 selected_tasks = [model.get_value(model.get_iter(path), \
4055 self.download_status_model.C_TASK) for path in paths]
4056 self.cancel_task_list(selected_tasks)
4058 def on_btnCancelAll_clicked(self, widget, *args):
4059 self.cancel_task_list(self.download_tasks_seen)
4061 def on_btnDownloadedDelete_clicked(self, widget, *args):
4062 episodes = self.get_selected_episodes()
4063 if len(episodes) == 1:
4064 self.delete_episode_list(episodes, skip_locked=False)
4065 else:
4066 self.delete_episode_list(episodes)
4068 def on_key_press(self, widget, event):
4069 # Allow tab switching with Ctrl + PgUp/PgDown
4070 if event.state & gtk.gdk.CONTROL_MASK:
4071 if event.keyval == gtk.keysyms.Page_Up:
4072 self.wNotebook.prev_page()
4073 return True
4074 elif event.keyval == gtk.keysyms.Page_Down:
4075 self.wNotebook.next_page()
4076 return True
4078 # After this code we only handle Maemo hardware keys,
4079 # so if we are not a Maemo app, we don't do anything
4080 if not gpodder.ui.maemo:
4081 return False
4083 diff = 0
4084 if event.keyval == gtk.keysyms.F7: #plus
4085 diff = 1
4086 elif event.keyval == gtk.keysyms.F8: #minus
4087 diff = -1
4089 if diff != 0 and not self.currently_updating:
4090 selection = self.treeChannels.get_selection()
4091 (model, iter) = selection.get_selected()
4092 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
4093 selection.select_path(new_path)
4094 self.treeChannels.set_cursor(new_path)
4095 return True
4097 return False
4099 def on_iconify(self):
4100 if self.tray_icon:
4101 self.gPodder.set_skip_taskbar_hint(True)
4102 if self.config.minimize_to_tray:
4103 self.tray_icon.set_visible(True)
4104 else:
4105 self.gPodder.set_skip_taskbar_hint(False)
4107 def on_uniconify(self):
4108 if self.tray_icon:
4109 self.gPodder.set_skip_taskbar_hint(False)
4110 if self.config.minimize_to_tray:
4111 self.tray_icon.set_visible(False)
4112 else:
4113 self.gPodder.set_skip_taskbar_hint(False)
4115 def uniconify_main_window(self):
4116 if self.is_iconified():
4117 # We need to hide and then show the window in WMs like Metacity
4118 # or KWin4 to move the window to the active workspace
4119 # (see http://gpodder.org/bug/1125)
4120 self.gPodder.hide()
4121 self.gPodder.show()
4122 self.gPodder.present()
4124 def iconify_main_window(self):
4125 if not self.is_iconified():
4126 self.gPodder.iconify()
4128 def update_podcasts_tab(self):
4129 if len(self.channels):
4130 if gpodder.ui.fremantle:
4131 self.button_refresh.set_title(_('Check for new episodes'))
4132 self.button_refresh.show()
4133 else:
4134 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
4135 else:
4136 if gpodder.ui.fremantle:
4137 self.button_refresh.hide()
4138 else:
4139 self.label2.set_text(_('Podcasts'))
4141 @dbus.service.method(gpodder.dbus_interface)
4142 def show_gui_window(self):
4143 parent = self.get_dialog_parent()
4144 parent.present()
4146 @dbus.service.method(gpodder.dbus_interface)
4147 def subscribe_to_url(self, url):
4148 gPodderAddPodcast(self.gPodder,
4149 add_urls_callback=self.add_podcast_list,
4150 preset_url=url)
4152 @dbus.service.method(gpodder.dbus_interface)
4153 def mark_episode_played(self, filename):
4154 if filename is None:
4155 return False
4157 for channel in self.channels:
4158 for episode in channel.get_all_episodes():
4159 fn = episode.local_filename(create=False, check_only=True)
4160 if fn == filename:
4161 episode.mark(is_played=True)
4162 self.db.commit()
4163 self.update_episode_list_icons([episode.url])
4164 self.update_podcast_list_model([episode.channel.url])
4165 return True
4167 return False
4170 def main(options=None):
4171 gobject.threads_init()
4172 gobject.set_application_name('gPodder')
4174 if gpodder.ui.maemo:
4175 # Try to enable the custom icon theme for gPodder on Maemo
4176 settings = gtk.settings_get_default()
4177 settings.set_string_property('gtk-icon-theme-name', \
4178 'gpodder', __file__)
4179 # Extend the search path for the optified icon theme (Maemo 5)
4180 icon_theme = gtk.icon_theme_get_default()
4181 icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
4183 gtk.window_set_default_icon_name('gpodder')
4184 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
4186 try:
4187 dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
4188 gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
4190 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
4191 except dbus.exceptions.DBusException, dbe:
4192 log('Warning: Cannot get "on the bus".', traceback=True)
4193 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
4194 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
4195 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
4196 dlg.set_title('gPodder')
4197 dlg.run()
4198 dlg.destroy()
4199 sys.exit(0)
4201 util.make_directory(gpodder.home)
4202 gpodder.load_plugins()
4204 config = UIConfig(gpodder.config_file)
4206 # Load hook modules and install the hook manager globally
4207 # if modules have been found an instantiated by the manager
4208 user_hooks = hooks.HookManager()
4209 if user_hooks.has_modules():
4210 gpodder.user_hooks = user_hooks
4212 if gpodder.ui.diablo:
4213 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4214 # folder exists there (allow moving "gpodder" between SD cards or USB)
4215 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4216 if not os.path.exists(config.download_dir):
4217 log('Downloads might have been moved. Trying to locate them...')
4218 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
4219 dir = os.path.join(basedir, 'gpodder')
4220 if os.path.exists(dir):
4221 log('Downloads found in: %s', dir)
4222 config.download_dir = dir
4223 break
4224 else:
4225 log('Downloads NOT FOUND in %s', dir)
4226 elif gpodder.ui.fremantle:
4227 config.on_quit_ask = False
4229 if config.enable_fingerscroll:
4230 BuilderWidget.use_fingerscroll = True
4232 config.mygpo_device_type = util.detect_device_type()
4234 gp = gPodder(bus_name, config)
4236 # Handle options
4237 if options.subscribe:
4238 util.idle_add(gp.subscribe_to_url, options.subscribe)
4240 # mac OS X stuff :
4241 # handle "subscribe to podcast" events from firefox
4242 if platform.system() == 'Darwin':
4243 from gpodder import gpodderosx
4244 gpodderosx.register_handlers(gp)
4245 # end mac OS X stuff
4247 gp.run()