Fix parenting issues with dialogs
[gpodder.git] / src / gpodder / gui.py
blob2f0cff2f7239c6334afad1928b2089df33358729
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 cgi
23 import gtk
24 import gtk.gdk
25 import gobject
26 import pango
27 import sys
28 import shutil
29 import subprocess
30 import glob
31 import time
32 import urllib
33 import urllib2
34 import tempfile
35 import collections
36 import threading
38 from xml.sax import saxutils
40 import gpodder
42 try:
43 import dbus
44 import dbus.service
45 import dbus.mainloop
46 import dbus.glib
47 except ImportError:
48 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
49 class dbus:
50 class SessionBus:
51 def __init__(self, *args, **kwargs):
52 pass
53 class glib:
54 class DBusGMainLoop:
55 pass
56 class service:
57 @staticmethod
58 def method(*args, **kwargs):
59 return lambda x: x
60 class BusName:
61 def __init__(self, *args, **kwargs):
62 pass
63 class Object:
64 def __init__(self, *args, **kwargs):
65 pass
68 from gpodder import feedcore
69 from gpodder import util
70 from gpodder import opml
71 from gpodder import download
72 from gpodder import my
73 from gpodder import youtube
74 from gpodder import player
75 from gpodder.liblogger import log
77 _ = gpodder.gettext
78 N_ = gpodder.ngettext
80 from gpodder.model import PodcastChannel
81 from gpodder.model import PodcastEpisode
82 from gpodder.dbsqlite import Database
84 from gpodder.gtkui.model import PodcastListModel
85 from gpodder.gtkui.model import EpisodeListModel
86 from gpodder.gtkui.config import UIConfig
87 from gpodder.gtkui.services import CoverDownloader
88 from gpodder.gtkui.widgets import SimpleMessageArea
89 from gpodder.gtkui.desktopfile import UserAppsReader
91 from gpodder.gtkui.draw import draw_text_box_centered
93 from gpodder.gtkui.interface.common import BuilderWidget
94 from gpodder.gtkui.interface.common import TreeViewHelper
95 from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
96 from gpodder.gtkui.mygpodder import MygPodderSettings
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 try:
110 from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
111 have_trayicon = True
112 except Exception, exc:
113 log('Warning: Could not import gpodder.trayicon.', traceback=True)
114 log('Warning: This probably means your PyGTK installation is too old!')
115 have_trayicon = False
116 elif gpodder.ui.diablo:
117 from gpodder.gtkui.download import DownloadStatusModel
119 from gpodder.gtkui.maemo.channel import gPodderChannel
120 from gpodder.gtkui.maemo.preferences import gPodderPreferences
121 from gpodder.gtkui.maemo.shownotes import gPodderShownotes
122 from gpodder.gtkui.maemo.episodeselector import gPodderEpisodeSelector
123 from gpodder.gtkui.maemo.podcastdirectory import gPodderPodcastDirectory
124 have_trayicon = False
125 elif gpodder.ui.fremantle:
126 from gpodder.gtkui.frmntl.model import DownloadStatusModel
127 from gpodder.gtkui.frmntl.model import EpisodeListModel
128 from gpodder.gtkui.frmntl.model import PodcastListModel
130 from gpodder.gtkui.maemo.channel import gPodderChannel
131 from gpodder.gtkui.frmntl.preferences import gPodderPreferences
132 from gpodder.gtkui.frmntl.shownotes import gPodderShownotes
133 from gpodder.gtkui.frmntl.episodeselector import gPodderEpisodeSelector
134 from gpodder.gtkui.frmntl.podcastdirectory import gPodderPodcastDirectory
135 from gpodder.gtkui.frmntl.episodes import gPodderEpisodes
136 from gpodder.gtkui.frmntl.downloads import gPodderDownloads
137 have_trayicon = False
139 from gpodder.gtkui.frmntl.portrait import FremantleRotation
141 from gpodder.gtkui.interface.common import Orientation
143 from gpodder.gtkui.interface.welcome import gPodderWelcome
144 from gpodder.gtkui.interface.progress import ProgressIndicator
146 if gpodder.ui.maemo:
147 import hildon
149 from gpodder.dbusproxy import DBusPodcastsProxy
151 class gPodder(BuilderWidget, dbus.service.Object):
152 finger_friendly_widgets = ['btnCleanUpDownloads', 'button_search_episodes_clear']
154 ICON_GENERAL_ADD = 'general_add'
155 ICON_GENERAL_REFRESH = 'general_refresh'
156 ICON_GENERAL_CLOSE = 'general_close'
158 def __init__(self, bus_name, config):
159 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
160 self.podcasts_proxy = DBusPodcastsProxy(lambda: self.channels, \
161 self.on_itemUpdate_activate, \
162 self.playback_episodes, \
163 self.download_episode_list, \
164 bus_name)
165 self.db = Database(gpodder.database_file)
166 self.config = config
167 BuilderWidget.__init__(self, None)
169 def new(self):
170 if gpodder.ui.diablo:
171 import hildon
172 self.app = hildon.Program()
173 self.app.add_window(self.main_window)
174 self.main_window.add_toolbar(self.toolbar)
175 menu = gtk.Menu()
176 for child in self.main_menu.get_children():
177 child.reparent(menu)
178 self.main_window.set_menu(self.set_finger_friendly(menu))
179 self.bluetooth_available = False
180 elif gpodder.ui.fremantle:
181 import hildon
182 self.app = hildon.Program()
183 self.app.add_window(self.main_window)
185 appmenu = hildon.AppMenu()
187 for filter in (self.item_view_podcasts_all, \
188 self.item_view_podcasts_downloaded, \
189 self.item_view_podcasts_unplayed):
190 button = gtk.ToggleButton()
191 filter.connect_proxy(button)
192 appmenu.add_filter(button)
194 for action in (self.itemPreferences, \
195 self.item_downloads, \
196 self.itemRemoveOldEpisodes, \
197 self.item_unsubscribe, \
198 self.itemAbout):
199 button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
200 hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
201 action.connect_proxy(button)
202 if action == self.item_downloads:
203 button.set_title(_('Downloads'))
204 button.set_value(_('Idle'))
205 self.button_downloads = button
206 appmenu.append(button)
207 appmenu.show_all()
208 self.main_window.set_app_menu(appmenu)
210 # Initialize portrait mode / rotation manager
211 self._fremantle_rotation = FremantleRotation('gPodder', \
212 self.main_window, \
213 gpodder.__version__, \
214 self.config.rotation_mode)
216 if self.config.rotation_mode == FremantleRotation.ALWAYS:
217 util.idle_add(self.on_window_orientation_changed, \
218 Orientation.PORTRAIT)
219 self._last_orientation = Orientation.PORTRAIT
220 else:
221 self._last_orientation = Orientation.LANDSCAPE
223 self.bluetooth_available = False
224 else:
225 self._last_orientation = Orientation.LANDSCAPE
226 self.bluetooth_available = util.bluetooth_available()
227 self.toolbar.set_property('visible', self.config.show_toolbar)
229 self.config.connect_gtk_window(self.gPodder, 'main_window')
230 if not gpodder.ui.fremantle:
231 self.config.connect_gtk_paned('paned_position', self.channelPaned)
232 self.main_window.show()
234 self.player_receiver = player.MediaPlayerDBusReceiver(self.on_played)
236 self.gPodder.connect('key-press-event', self.on_key_press)
238 self.preferences_dialog = None
239 self.config.add_observer(self.on_config_changed)
241 self.tray_icon = None
242 self.episode_shownotes_window = None
243 self.new_episodes_window = None
245 if gpodder.ui.desktop:
246 # Mac OS X-specific UI tweaks: Native main menu integration
247 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
248 if getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
249 try:
250 import igemacintegration as igemi
252 # Move the menu bar from the window to the Mac menu bar
253 self.mainMenu.hide()
254 igemi.ige_mac_menu_set_menu_bar(self.mainMenu)
256 # Reparent some items to the "Application" menu
257 for widget in ('/mainMenu/menuHelp/itemAbout', \
258 '/mainMenu/menuPodcasts/itemPreferences'):
259 item = self.uimanager1.get_widget(widget)
260 group = igemi.ige_mac_menu_add_app_menu_group()
261 igemi.ige_mac_menu_add_app_menu_item(group, item, None)
263 quit_widget = '/mainMenu/menuPodcasts/itemQuit'
264 quit_item = self.uimanager1.get_widget(quit_widget)
265 igemi.ige_mac_menu_set_quit_menu_item(quit_item)
266 except ImportError:
267 print >>sys.stderr, """
268 Warning: ige-mac-integration not found - no native menus.
271 self.sync_ui = gPodderSyncUI(self.config, self.notification, \
272 self.main_window, self.show_confirmation, \
273 self.update_episode_list_icons, \
274 self.update_podcast_list_model, self.toolPreferences, \
275 gPodderEpisodeSelector, \
276 self.commit_changes_to_database)
277 else:
278 self.sync_ui = None
280 self.download_status_model = DownloadStatusModel()
281 self.download_queue_manager = download.DownloadQueueManager(self.config)
283 if gpodder.ui.desktop:
284 self.show_hide_tray_icon()
285 self.itemShowAllEpisodes.set_active(self.config.podcast_list_view_all)
286 self.itemShowToolbar.set_active(self.config.show_toolbar)
287 self.itemShowDescription.set_active(self.config.episode_list_descriptions)
289 if not gpodder.ui.fremantle:
290 self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
291 self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
292 self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
293 self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
295 # When the amount of maximum downloads changes, notify the queue manager
296 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_threads()
297 self.spinMaxDownloads.connect('value-changed', changed_cb)
299 self.default_title = 'gPodder'
300 if gpodder.__version__.rfind('git') != -1:
301 self.set_title('gPodder %s' % gpodder.__version__)
302 else:
303 title = self.gPodder.get_title()
304 if title is not None:
305 self.set_title(title)
306 else:
307 self.set_title(_('gPodder'))
309 self.cover_downloader = CoverDownloader()
311 # Generate list models for podcasts and their episodes
312 self.podcast_list_model = PodcastListModel(self.cover_downloader)
314 self.cover_downloader.register('cover-available', self.cover_download_finished)
315 self.cover_downloader.register('cover-removed', self.cover_file_removed)
317 if gpodder.ui.fremantle:
318 # Work around Maemo bug #4718
319 self.button_refresh.set_name('HildonButton-finger')
320 self.button_subscribe.set_name('HildonButton-finger')
322 self.button_refresh.set_sensitive(False)
323 self.button_subscribe.set_sensitive(False)
325 self.button_subscribe.set_image(gtk.image_new_from_icon_name(\
326 self.ICON_GENERAL_ADD, gtk.ICON_SIZE_BUTTON))
327 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
328 self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
330 # Make the button scroll together with the TreeView contents
331 action_area_box = self.treeChannels.get_action_area_box()
332 for child in self.buttonbox:
333 child.reparent(action_area_box)
334 self.vbox.remove(self.buttonbox)
335 action_area_box.set_spacing(2)
336 action_area_box.set_border_width(3)
337 self.treeChannels.set_action_area_visible(True)
339 from gpodder.gtkui.frmntl import style
340 sub_font = style.get_font_desc('SmallSystemFont')
341 sub_color = style.get_color('SecondaryTextColor')
342 sub = (sub_font.to_string(), sub_color.to_string())
343 sub = '<span font_desc="%s" foreground="%s">%%s</span>' % sub
344 self.label_footer.set_markup(sub % gpodder.__copyright__)
346 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
347 while gtk.events_pending():
348 gtk.main_iteration(False)
350 try:
351 # Try to get the real package version from dpkg
352 p = subprocess.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout=subprocess.PIPE)
353 version, _stderr = p.communicate()
354 del _stderr
355 del p
356 except:
357 version = gpodder.__version__
358 self.label_footer.set_markup(sub % ('v %s' % version))
359 self.label_footer.hide()
361 self.episodes_window = gPodderEpisodes(self.main_window, \
362 on_treeview_expose_event=self.on_treeview_expose_event, \
363 show_episode_shownotes=self.show_episode_shownotes, \
364 update_podcast_list_model=self.update_podcast_list_model, \
365 on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
366 item_view_episodes_all=self.item_view_episodes_all, \
367 item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
368 item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
369 item_view_episodes_undeleted=self.item_view_episodes_undeleted, \
370 on_entry_search_episodes_changed=self.on_entry_search_episodes_changed, \
371 on_entry_search_episodes_key_press=self.on_entry_search_episodes_key_press, \
372 hide_episode_search=self.hide_episode_search, \
373 on_itemUpdateChannel_activate=self.on_itemUpdateChannel_activate, \
374 playback_episodes=self.playback_episodes, \
375 delete_episode_list=self.delete_episode_list, \
376 episode_list_status_changed=self.episode_list_status_changed, \
377 download_episode_list=self.download_episode_list, \
378 episode_is_downloading=self.episode_is_downloading, \
379 show_episode_in_download_manager=self.show_episode_in_download_manager, \
380 add_download_task_monitor=self.add_download_task_monitor, \
381 remove_download_task_monitor=self.remove_download_task_monitor, \
382 for_each_episode_set_task_status=self.for_each_episode_set_task_status, \
383 on_delete_episodes_button_clicked=self.on_itemRemoveOldEpisodes_activate, \
384 on_itemUpdate_activate=self.on_itemUpdate_activate)
386 # Expose objects for episode list type-ahead find
387 self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
388 self.entry_search_episodes = self.episodes_window.entry_search_episodes
389 self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
391 self.downloads_window = gPodderDownloads(self.main_window, \
392 on_treeview_expose_event=self.on_treeview_expose_event, \
393 on_btnCleanUpDownloads_clicked=self.on_btnCleanUpDownloads_clicked, \
394 _for_each_task_set_status=self._for_each_task_set_status, \
395 downloads_list_get_selection=self.downloads_list_get_selection, \
396 _config=self.config)
398 self.treeAvailable = self.episodes_window.treeview
399 self.treeDownloads = self.downloads_window.treeview
401 # Init the treeviews that we use
402 self.init_podcast_list_treeview()
403 self.init_episode_list_treeview()
404 self.init_download_list_treeview()
406 if self.config.podcast_list_hide_boring:
407 self.item_view_hide_boring_podcasts.set_active(True)
409 self.currently_updating = False
411 if gpodder.ui.maemo:
412 self.context_menu_mouse_button = 1
413 else:
414 self.context_menu_mouse_button = 3
416 if self.config.start_iconified:
417 self.iconify_main_window()
419 self.download_tasks_seen = set()
420 self.download_list_update_enabled = False
421 self.last_download_count = 0
422 self.download_task_monitors = set()
424 # Subscribed channels
425 self.active_channel = None
426 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
427 self.channel_list_changed = True
428 self.update_podcasts_tab()
430 # load list of user applications for audio playback
431 self.user_apps_reader = UserAppsReader(['audio', 'video'])
432 threading.Thread(target=self.user_apps_reader.read).start()
434 # Set the "Device" menu item for the first time
435 if gpodder.ui.desktop:
436 self.update_item_device()
438 # Set up the first instance of MygPoClient
439 self.mygpo_client = my.MygPoClient(self.config)
441 # Now, update the feed cache, when everything's in place
442 if not gpodder.ui.fremantle:
443 self.btnUpdateFeeds.show()
444 self.updating_feed_cache = False
445 self.feed_cache_update_cancelled = False
446 self.update_feed_cache(force_update=self.config.update_on_startup)
448 self.message_area = None
450 def find_partial_downloads():
451 # Look for partial file downloads
452 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
453 count = len(partial_files)
454 resumable_episodes = []
455 if count:
456 if not gpodder.ui.fremantle:
457 util.idle_add(self.wNotebook.set_current_page, 1)
458 indicator = ProgressIndicator(_('Loading incomplete downloads'), \
459 _('Some episodes have not finished downloading in a previous session.'), \
460 False, self.get_dialog_parent())
461 indicator.on_message(N_('%d partial file', '%d partial files', count) % count)
463 candidates = [f[:-len('.partial')] for f in partial_files]
464 found = 0
466 for c in self.channels:
467 for e in c.get_all_episodes():
468 filename = e.local_filename(create=False, check_only=True)
469 if filename in candidates:
470 log('Found episode: %s', e.title, sender=self)
471 found += 1
472 indicator.on_message(e.title)
473 indicator.on_progress(float(found)/count)
474 candidates.remove(filename)
475 partial_files.remove(filename+'.partial')
476 resumable_episodes.append(e)
478 if not candidates:
479 break
481 if not candidates:
482 break
484 for f in partial_files:
485 log('Partial file without episode: %s', f, sender=self)
486 util.delete_file(f)
488 util.idle_add(indicator.on_finished)
490 if len(resumable_episodes):
491 def offer_resuming():
492 self.download_episode_list_paused(resumable_episodes)
493 if not gpodder.ui.fremantle:
494 resume_all = gtk.Button(_('Resume all'))
495 #resume_all.set_border_width(0)
496 def on_resume_all(button):
497 selection = self.treeDownloads.get_selection()
498 selection.select_all()
499 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
500 selection.unselect_all()
501 self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
502 self.message_area.hide()
503 resume_all.connect('clicked', on_resume_all)
505 self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
506 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
507 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
508 self.message_area.show_all()
509 self.clean_up_downloads(delete_partial=False)
510 util.idle_add(offer_resuming)
511 elif not gpodder.ui.fremantle:
512 util.idle_add(self.wNotebook.set_current_page, 0)
513 else:
514 util.idle_add(self.clean_up_downloads, True)
515 threading.Thread(target=find_partial_downloads).start()
517 # Start the auto-update procedure
518 self._auto_update_timer_source_id = None
519 if self.config.auto_update_feeds:
520 self.restart_auto_update_timer()
522 # Delete old episodes if the user wishes to
523 if self.config.auto_remove_played_episodes and \
524 self.config.episode_old_age > 0:
525 old_episodes = list(self.get_expired_episodes())
526 if len(old_episodes) > 0:
527 self.delete_episode_list(old_episodes, confirm=False)
528 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
530 if gpodder.ui.fremantle:
531 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
532 self.button_refresh.set_sensitive(True)
533 self.button_subscribe.set_sensitive(True)
534 self.main_window.set_title(_('gPodder'))
535 hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
537 # Do the initial sync with the web service
538 util.idle_add(self.mygpo_client.flush, True)
540 # First-time users should be asked if they want to see the OPML
541 if not self.channels and not gpodder.ui.fremantle:
542 util.idle_add(self.on_itemUpdate_activate)
544 def on_played(self, start, end, total, file_uri):
545 """Handle the "played" signal from a media player"""
546 log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
547 filename = file_uri[len('file://'):]
548 # FIXME: Optimize this by querying the database more directly
549 for channel in self.channels:
550 for episode in channel.get_all_episodes():
551 fn = episode.local_filename(create=False, check_only=True)
552 if fn == filename:
553 file_type = episode.file_type()
554 # Automatically enable D-Bus played status mode
555 if file_type == 'audio':
556 self.config.audio_played_dbus = True
557 elif file_type == 'video':
558 self.config.video_played_dbus = True
560 now = time.time()
561 if total > 0:
562 episode.total_time = total
563 if episode.current_position_updated is None or \
564 now > episode.current_position_updated:
565 episode.current_position = end
566 episode.current_position_updated = now
567 episode.mark(is_played=True)
568 episode.save()
569 self.db.commit()
570 self.update_episode_list_icons([episode.url])
571 self.update_podcast_list_model([episode.channel.url])
573 # Submit this action to the webservice
574 self.mygpo_client.on_playback_full(episode, \
575 start, end, total)
576 return
578 def on_add_remove_podcasts_mygpo(self):
579 actions = self.mygpo_client.get_received_actions()
580 if not actions:
581 return False
583 existing_urls = [c.url for c in self.channels]
585 # Columns for the episode selector window - just one...
586 columns = (
587 ('description', None, None, _('Action')),
590 # A list of actions that have to be chosen from
591 changes = []
593 # Actions that are ignored (already carried out)
594 ignored = []
596 for action in actions:
597 if action.is_add and action.url not in existing_urls:
598 changes.append(my.Change(action))
599 elif action.is_remove and action.url in existing_urls:
600 podcast_object = None
601 for podcast in self.channels:
602 if podcast.url == action.url:
603 podcast_object = podcast
604 break
605 changes.append(my.Change(action, podcast_object))
606 else:
607 log('Ignoring action: %s', action, sender=self)
608 ignored.append(action)
610 # Confirm all ignored changes
611 self.mygpo_client.confirm_received_actions(ignored)
613 def execute_podcast_actions(selected):
614 add_list = [c.action.url for c in selected if c.action.is_add]
615 remove_list = [c.podcast for c in selected if c.action.is_remove]
617 # Apply the accepted changes locally
618 self.add_podcast_list(add_list)
619 self.remove_podcast_list(remove_list, confirm=False)
621 # All selected items are now confirmed
622 self.mygpo_client.confirm_received_actions(c.action for c in selected)
624 # Revert the changes on the server
625 rejected = [c.action for c in changes if c not in selected]
626 self.mygpo_client.reject_received_actions(rejected)
628 def ask():
629 # We're abusing the Episode Selector again ;) -- thp
630 gPodderEpisodeSelector(self.main_window, \
631 title=_('Confirm changes from gpodder.net'), \
632 instructions=_('Select the actions you want to carry out.'), \
633 episodes=changes, \
634 columns=columns, \
635 size_attribute=None, \
636 stock_ok_button=gtk.STOCK_APPLY, \
637 callback=execute_podcast_actions, \
638 _config=self.config)
640 # There are some actions that need the user's attention
641 if changes:
642 util.idle_add(ask)
643 return True
645 # We have no remaining actions - no selection happens
646 return False
648 def rewrite_urls_mygpo(self):
649 # Check if we have to rewrite URLs since the last add
650 rewritten_urls = self.mygpo_client.get_rewritten_urls()
652 for rewritten_url in rewritten_urls:
653 if not rewritten_url.new_url:
654 continue
656 for channel in self.channels:
657 if channel.url == rewritten_url.old_url:
658 log('Updating URL of %s to %s', channel, \
659 rewritten_url.new_url, sender=self)
660 channel.url = rewritten_url.new_url
661 channel.save()
662 self.channel_list_changed = True
663 util.idle_add(self.update_episode_list_model)
664 break
666 def on_send_full_subscriptions(self):
667 # Send the full subscription list to the gpodder.net client
668 # (this will overwrite the subscription list on the server)
669 indicator = ProgressIndicator(_('Uploading subscriptions'), \
670 _('Your subscriptions are being uploaded to the server.'), \
671 False, self.get_dialog_parent())
673 try:
674 self.mygpo_client.set_subscriptions([c.url for c in self.channels])
675 util.idle_add(self.show_message, _('List uploaded successfully.'))
676 except Exception, e:
677 def show_error(e):
678 message = str(e)
679 if not message:
680 message = e.__class__.__name__
681 self.show_message(message, \
682 _('Error while uploading'), \
683 important=True)
684 util.idle_add(show_error, e)
686 util.idle_add(indicator.on_finished)
688 def on_podcast_selected(self, treeview, path, column):
689 # for Maemo 5's UI
690 model = treeview.get_model()
691 channel = model.get_value(model.get_iter(path), \
692 PodcastListModel.C_CHANNEL)
693 self.active_channel = channel
694 self.update_episode_list_model()
695 self.episodes_window.channel = self.active_channel
696 self.episodes_window.show()
698 def on_button_subscribe_clicked(self, button):
699 self.on_itemImportChannels_activate(button)
701 def on_button_downloads_clicked(self, widget):
702 self.downloads_window.show()
704 def show_episode_in_download_manager(self, episode):
705 self.downloads_window.show()
706 model = self.treeDownloads.get_model()
707 selection = self.treeDownloads.get_selection()
708 selection.unselect_all()
709 it = model.get_iter_first()
710 while it is not None:
711 task = model.get_value(it, DownloadStatusModel.C_TASK)
712 if task.episode.url == episode.url:
713 selection.select_iter(it)
714 # FIXME: Scroll to selection in pannable area
715 break
716 it = model.iter_next(it)
718 def for_each_episode_set_task_status(self, episodes, status):
719 episode_urls = set(episode.url for episode in episodes)
720 model = self.treeDownloads.get_model()
721 selected_tasks = [(gtk.TreeRowReference(model, row.path), \
722 model.get_value(row.iter, \
723 DownloadStatusModel.C_TASK)) for row in model \
724 if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
725 in episode_urls]
726 self._for_each_task_set_status(selected_tasks, status)
728 def on_window_orientation_changed(self, orientation):
729 self._last_orientation = orientation
730 if self.preferences_dialog is not None:
731 self.preferences_dialog.on_window_orientation_changed(orientation)
733 treeview = self.treeChannels
734 if orientation == Orientation.PORTRAIT:
735 treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
736 # Work around Maemo bug #4718
737 self.button_subscribe.set_name('HildonButton-thumb')
738 self.button_refresh.set_name('HildonButton-thumb')
739 else:
740 treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
741 # Work around Maemo bug #4718
742 self.button_subscribe.set_name('HildonButton-finger')
743 self.button_refresh.set_name('HildonButton-finger')
745 def on_treeview_podcasts_selection_changed(self, selection):
746 model, iter = selection.get_selected()
747 if iter is None:
748 self.active_channel = None
749 self.episode_list_model.clear()
751 def on_treeview_button_pressed(self, treeview, event):
752 if event.window != treeview.get_bin_window():
753 return False
755 TreeViewHelper.save_button_press_event(treeview, event)
757 if getattr(treeview, TreeViewHelper.ROLE) == \
758 TreeViewHelper.ROLE_PODCASTS:
759 return self.currently_updating
761 return event.button == self.context_menu_mouse_button and \
762 gpodder.ui.desktop
764 def on_treeview_podcasts_button_released(self, treeview, event):
765 if event.window != treeview.get_bin_window():
766 return False
768 if gpodder.ui.maemo:
769 return self.treeview_channels_handle_gestures(treeview, event)
770 return self.treeview_channels_show_context_menu(treeview, event)
772 def on_treeview_episodes_button_released(self, treeview, event):
773 if event.window != treeview.get_bin_window():
774 return False
776 if gpodder.ui.maemo:
777 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
778 return self.treeview_available_handle_gestures(treeview, event)
780 return self.treeview_available_show_context_menu(treeview, event)
782 def on_treeview_downloads_button_released(self, treeview, event):
783 if event.window != treeview.get_bin_window():
784 return False
786 return self.treeview_downloads_show_context_menu(treeview, event)
788 def on_entry_search_podcasts_changed(self, editable):
789 if self.hbox_search_podcasts.get_property('visible'):
790 self.podcast_list_model.set_search_term(editable.get_chars(0, -1))
792 def on_entry_search_podcasts_key_press(self, editable, event):
793 if event.keyval == gtk.keysyms.Escape:
794 self.hide_podcast_search()
795 return True
797 def hide_podcast_search(self, *args):
798 self.hbox_search_podcasts.hide()
799 self.entry_search_podcasts.set_text('')
800 self.podcast_list_model.set_search_term(None)
801 self.treeChannels.grab_focus()
803 def show_podcast_search(self, input_char):
804 self.hbox_search_podcasts.show()
805 self.entry_search_podcasts.insert_text(input_char, -1)
806 self.entry_search_podcasts.grab_focus()
807 self.entry_search_podcasts.set_position(-1)
809 def init_podcast_list_treeview(self):
810 # Set up podcast channel tree view widget
811 if gpodder.ui.fremantle:
812 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
813 self.item_view_podcasts_downloaded.set_active(True)
814 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
815 self.item_view_podcasts_unplayed.set_active(True)
816 else:
817 self.item_view_podcasts_all.set_active(True)
818 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
820 iconcolumn = gtk.TreeViewColumn('')
821 iconcell = gtk.CellRendererPixbuf()
822 iconcolumn.pack_start(iconcell, False)
823 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
824 self.treeChannels.append_column(iconcolumn)
826 namecolumn = gtk.TreeViewColumn('')
827 namecell = gtk.CellRendererText()
828 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
829 namecolumn.pack_start(namecell, True)
830 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
832 iconcell = gtk.CellRendererPixbuf()
833 iconcell.set_property('xalign', 1.0)
834 namecolumn.pack_start(iconcell, False)
835 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
836 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
837 self.treeChannels.append_column(namecolumn)
839 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
841 # When no podcast is selected, clear the episode list model
842 selection = self.treeChannels.get_selection()
843 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
845 # Set up type-ahead find for the podcast list
846 def on_key_press(treeview, event):
847 if event.keyval == gtk.keysyms.Escape:
848 self.hide_podcast_search()
849 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
850 self.hide_podcast_search()
851 elif event.state & gtk.gdk.CONTROL_MASK:
852 # Don't handle type-ahead when control is pressed (so shortcuts
853 # with the Ctrl key still work, e.g. Ctrl+A, ...)
854 return True
855 else:
856 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
857 if unicode_char_id == 0:
858 return False
859 input_char = unichr(unicode_char_id)
860 self.show_podcast_search(input_char)
861 return True
862 self.treeChannels.connect('key-press-event', on_key_press)
864 # Enable separators to the podcast list to separate special podcasts
865 # from others (this is used for the "all episodes" view)
866 self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
868 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
870 def on_entry_search_episodes_changed(self, editable):
871 if self.hbox_search_episodes.get_property('visible'):
872 self.episode_list_model.set_search_term(editable.get_chars(0, -1))
874 def on_entry_search_episodes_key_press(self, editable, event):
875 if event.keyval == gtk.keysyms.Escape:
876 self.hide_episode_search()
877 return True
879 def hide_episode_search(self, *args):
880 self.hbox_search_episodes.hide()
881 self.entry_search_episodes.set_text('')
882 self.episode_list_model.set_search_term(None)
883 self.treeAvailable.grab_focus()
885 def show_episode_search(self, input_char):
886 self.hbox_search_episodes.show()
887 self.entry_search_episodes.insert_text(input_char, -1)
888 self.entry_search_episodes.grab_focus()
889 self.entry_search_episodes.set_position(-1)
891 def init_episode_list_treeview(self):
892 # For loading the list model
893 self.empty_episode_list_model = EpisodeListModel()
894 self.episode_list_model = EpisodeListModel()
896 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
897 self.item_view_episodes_undeleted.set_active(True)
898 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
899 self.item_view_episodes_downloaded.set_active(True)
900 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
901 self.item_view_episodes_unplayed.set_active(True)
902 else:
903 self.item_view_episodes_all.set_active(True)
905 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
907 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
909 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
911 iconcell = gtk.CellRendererPixbuf()
912 if gpodder.ui.maemo:
913 iconcell.set_fixed_size(50, 50)
914 status_column_label = ''
915 else:
916 status_column_label = _('Status')
917 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=EpisodeListModel.C_STATUS_ICON)
919 namecell = gtk.CellRendererText()
920 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
921 namecolumn = gtk.TreeViewColumn(_('Episode'), namecell, markup=EpisodeListModel.C_DESCRIPTION)
922 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
923 namecolumn.set_resizable(True)
924 namecolumn.set_expand(True)
926 sizecell = gtk.CellRendererText()
927 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
929 releasecell = gtk.CellRendererText()
930 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
932 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
933 itemcolumn.set_reorderable(True)
934 self.treeAvailable.append_column(itemcolumn)
936 if gpodder.ui.maemo:
937 sizecolumn.set_visible(False)
938 releasecolumn.set_visible(False)
940 # Set up type-ahead find for the episode list
941 def on_key_press(treeview, event):
942 if event.keyval == gtk.keysyms.Escape:
943 self.hide_episode_search()
944 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
945 self.hide_episode_search()
946 elif event.state & gtk.gdk.CONTROL_MASK:
947 # Don't handle type-ahead when control is pressed (so shortcuts
948 # with the Ctrl key still work, e.g. Ctrl+A, ...)
949 return False
950 else:
951 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
952 if unicode_char_id == 0:
953 return False
954 input_char = unichr(unicode_char_id)
955 self.show_episode_search(input_char)
956 return True
957 self.treeAvailable.connect('key-press-event', on_key_press)
959 if gpodder.ui.desktop:
960 self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
961 (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
962 def drag_data_get(tree, context, selection_data, info, timestamp):
963 if self.config.on_drag_mark_played:
964 for episode in self.get_selected_episodes():
965 episode.mark(is_played=True)
966 self.on_selected_episodes_status_changed()
967 uris = ['file://'+e.local_filename(create=False) \
968 for e in self.get_selected_episodes() \
969 if e.was_downloaded(and_exists=True)]
970 uris.append('') # for the trailing '\r\n'
971 selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
972 self.treeAvailable.connect('drag-data-get', drag_data_get)
974 selection = self.treeAvailable.get_selection()
975 if gpodder.ui.diablo:
976 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
977 selection.set_mode(gtk.SELECTION_SINGLE)
978 else:
979 selection.set_mode(gtk.SELECTION_MULTIPLE)
980 elif gpodder.ui.fremantle:
981 selection.set_mode(gtk.SELECTION_SINGLE)
982 else:
983 selection.set_mode(gtk.SELECTION_MULTIPLE)
984 # Update the sensitivity of the toolbar buttons on the Desktop
985 selection.connect('changed', lambda s: self.play_or_download())
987 if gpodder.ui.diablo:
988 # Set up the tap-and-hold context menu for podcasts
989 menu = gtk.Menu()
990 menu.append(self.itemUpdateChannel.create_menu_item())
991 menu.append(self.itemEditChannel.create_menu_item())
992 menu.append(gtk.SeparatorMenuItem())
993 menu.append(self.itemRemoveChannel.create_menu_item())
994 menu.append(gtk.SeparatorMenuItem())
995 item = gtk.ImageMenuItem(_('Close this menu'))
996 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
997 gtk.ICON_SIZE_MENU))
998 menu.append(item)
999 menu.show_all()
1000 menu = self.set_finger_friendly(menu)
1001 self.treeChannels.tap_and_hold_setup(menu)
1004 def init_download_list_treeview(self):
1005 # enable multiple selection support
1006 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1007 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1009 # columns and renderers for "download progress" tab
1010 # First column: [ICON] Episodename
1011 column = gtk.TreeViewColumn(_('Episode'))
1013 cell = gtk.CellRendererPixbuf()
1014 if gpodder.ui.maemo:
1015 cell.set_fixed_size(50, 50)
1016 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1017 column.pack_start(cell, expand=False)
1018 column.add_attribute(cell, 'stock-id', \
1019 DownloadStatusModel.C_ICON_NAME)
1021 cell = gtk.CellRendererText()
1022 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1023 column.pack_start(cell, expand=True)
1024 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1025 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1026 column.set_expand(True)
1027 self.treeDownloads.append_column(column)
1029 # Second column: Progress
1030 cell = gtk.CellRendererProgress()
1031 cell.set_property('yalign', .5)
1032 cell.set_property('ypad', 6)
1033 column = gtk.TreeViewColumn(_('Progress'), cell,
1034 value=DownloadStatusModel.C_PROGRESS, \
1035 text=DownloadStatusModel.C_PROGRESS_TEXT)
1036 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1037 column.set_expand(False)
1038 self.treeDownloads.append_column(column)
1039 column.set_property('min-width', 150)
1040 column.set_property('max-width', 150)
1042 self.treeDownloads.set_model(self.download_status_model)
1043 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1045 def on_treeview_expose_event(self, treeview, event):
1046 if event.window == treeview.get_bin_window():
1047 model = treeview.get_model()
1048 if (model is not None and model.get_iter_first() is not None):
1049 return False
1051 role = getattr(treeview, TreeViewHelper.ROLE)
1052 ctx = event.window.cairo_create()
1053 ctx.rectangle(event.area.x, event.area.y,
1054 event.area.width, event.area.height)
1055 ctx.clip()
1057 x, y, width, height, depth = event.window.get_geometry()
1058 progress = None
1060 if role == TreeViewHelper.ROLE_EPISODES:
1061 if self.currently_updating:
1062 text = _('Loading episodes')
1063 progress = self.episode_list_model.get_update_progress()
1064 elif self.config.episode_list_view_mode != \
1065 EpisodeListModel.VIEW_ALL:
1066 text = _('No episodes in current view')
1067 else:
1068 text = _('No episodes available')
1069 elif role == TreeViewHelper.ROLE_PODCASTS:
1070 if self.config.episode_list_view_mode != \
1071 EpisodeListModel.VIEW_ALL and \
1072 self.config.podcast_list_hide_boring and \
1073 len(self.channels) > 0:
1074 text = _('No podcasts in this view')
1075 else:
1076 text = _('No subscriptions')
1077 elif role == TreeViewHelper.ROLE_DOWNLOADS:
1078 text = _('No active downloads')
1079 else:
1080 raise Exception('on_treeview_expose_event: unknown role')
1082 if gpodder.ui.fremantle:
1083 from gpodder.gtkui.frmntl import style
1084 font_desc = style.get_font_desc('LargeSystemFont')
1085 else:
1086 font_desc = None
1088 draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1090 return False
1092 def enable_download_list_update(self):
1093 if not self.download_list_update_enabled:
1094 gobject.timeout_add(1500, self.update_downloads_list)
1095 self.download_list_update_enabled = True
1097 def on_btnCleanUpDownloads_clicked(self, button=None):
1098 model = self.download_status_model
1100 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
1101 changed_episode_urls = set()
1102 for row_reference, task in all_tasks:
1103 if task.status in (task.DONE, task.CANCELLED):
1104 model.remove(model.get_iter(row_reference.get_path()))
1105 try:
1106 # We don't "see" this task anymore - remove it;
1107 # this is needed, so update_episode_list_icons()
1108 # below gets the correct list of "seen" tasks
1109 self.download_tasks_seen.remove(task)
1110 except KeyError, key_error:
1111 log('Cannot remove task from "seen" list: %s', task, sender=self)
1112 changed_episode_urls.add(task.url)
1113 # Tell the task that it has been removed (so it can clean up)
1114 task.removed_from_list()
1116 # Tell the podcasts tab to update icons for our removed podcasts
1117 self.update_episode_list_icons(changed_episode_urls)
1119 # Tell the shownotes window that we have removed the episode
1120 if self.episode_shownotes_window is not None and \
1121 self.episode_shownotes_window.episode is not None and \
1122 self.episode_shownotes_window.episode.url in changed_episode_urls:
1123 self.episode_shownotes_window._download_status_changed(None)
1125 # Update the tab title and downloads list
1126 self.update_downloads_list()
1128 def on_tool_downloads_toggled(self, toolbutton):
1129 if toolbutton.get_active():
1130 self.wNotebook.set_current_page(1)
1131 else:
1132 self.wNotebook.set_current_page(0)
1134 def add_download_task_monitor(self, monitor):
1135 self.download_task_monitors.add(monitor)
1136 model = self.download_status_model
1137 if model is None:
1138 model = ()
1139 for row in model:
1140 task = row[self.download_status_model.C_TASK]
1141 monitor.task_updated(task)
1143 def remove_download_task_monitor(self, monitor):
1144 self.download_task_monitors.remove(monitor)
1146 def update_downloads_list(self):
1147 try:
1148 model = self.download_status_model
1150 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
1151 total_speed, total_size, done_size = 0, 0, 0
1153 # Keep a list of all download tasks that we've seen
1154 download_tasks_seen = set()
1156 # Remember the DownloadTask object for the episode that
1157 # has been opened in the episode shownotes dialog (if any)
1158 if self.episode_shownotes_window is not None:
1159 shownotes_episode = self.episode_shownotes_window.episode
1160 shownotes_task = None
1161 else:
1162 shownotes_episode = None
1163 shownotes_task = None
1165 # Do not go through the list of the model is not (yet) available
1166 if model is None:
1167 model = ()
1169 failed_downloads = []
1170 for row in model:
1171 self.download_status_model.request_update(row.iter)
1173 task = row[self.download_status_model.C_TASK]
1174 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
1176 # Let the download task monitors know of changes
1177 for monitor in self.download_task_monitors:
1178 monitor.task_updated(task)
1180 total_size += size
1181 done_size += size*progress
1183 if shownotes_episode is not None and \
1184 shownotes_episode.url == task.episode.url:
1185 shownotes_task = task
1187 download_tasks_seen.add(task)
1189 if status == download.DownloadTask.DOWNLOADING:
1190 downloading += 1
1191 total_speed += speed
1192 elif status == download.DownloadTask.FAILED:
1193 failed_downloads.append(task)
1194 failed += 1
1195 elif status == download.DownloadTask.DONE:
1196 finished += 1
1197 elif status == download.DownloadTask.QUEUED:
1198 queued += 1
1199 elif status == download.DownloadTask.PAUSED:
1200 paused += 1
1201 else:
1202 others += 1
1204 # Remember which tasks we have seen after this run
1205 self.download_tasks_seen = download_tasks_seen
1207 if gpodder.ui.desktop:
1208 text = [_('Downloads')]
1209 if downloading + failed + queued > 0:
1210 s = []
1211 if downloading > 0:
1212 s.append(N_('%d active', '%d active', downloading) % downloading)
1213 if failed > 0:
1214 s.append(N_('%d failed', '%d failed', failed) % failed)
1215 if queued > 0:
1216 s.append(N_('%d queued', '%d queued', queued) % queued)
1217 text.append(' (' + ', '.join(s)+')')
1218 self.labelDownloads.set_text(''.join(text))
1219 elif gpodder.ui.diablo:
1220 sum = downloading + failed + finished + queued + paused + others
1221 if sum:
1222 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
1223 else:
1224 self.tool_downloads.set_label(_('Downloads'))
1225 elif gpodder.ui.fremantle:
1226 if downloading + queued > 0:
1227 self.button_downloads.set_value(N_('%d active', '%d active', downloading+queued) % (downloading+queued))
1228 elif failed > 0:
1229 self.button_downloads.set_value(N_('%d failed', '%d failed', failed) % failed)
1230 elif paused > 0:
1231 self.button_downloads.set_value(N_('%d paused', '%d paused', paused) % paused)
1232 else:
1233 self.button_downloads.set_value(_('Idle'))
1235 title = [self.default_title]
1237 # We have to update all episodes/channels for which the status has
1238 # changed. Accessing task.status_changed has the side effect of
1239 # re-setting the changed flag, so we need to get the "changed" list
1240 # of tuples first and split it into two lists afterwards
1241 changed = [(task.url, task.podcast_url) for task in \
1242 self.download_tasks_seen if task.status_changed]
1243 episode_urls = [episode_url for episode_url, channel_url in changed]
1244 channel_urls = [channel_url for episode_url, channel_url in changed]
1246 count = downloading + queued
1247 if count > 0:
1248 title.append(N_('downloading %d file', 'downloading %d files', count) % count)
1250 if total_size > 0:
1251 percentage = 100.0*done_size/total_size
1252 else:
1253 percentage = 0.0
1254 total_speed = util.format_filesize(total_speed)
1255 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
1256 if self.tray_icon is not None:
1257 # Update the tray icon status and progress bar
1258 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
1259 self.tray_icon.draw_progress_bar(percentage/100.)
1260 elif self.last_download_count > 0:
1261 if self.tray_icon is not None:
1262 # Update the tray icon status
1263 self.tray_icon.set_status()
1264 if gpodder.ui.desktop:
1265 self.downloads_finished(self.download_tasks_seen)
1266 if gpodder.ui.diablo:
1267 hildon.hildon_banner_show_information(self.gPodder, '', 'gPodder: %s' % _('All downloads finished'))
1268 log('All downloads have finished.', sender=self)
1269 if self.config.cmd_all_downloads_complete:
1270 util.run_external_command(self.config.cmd_all_downloads_complete)
1272 if gpodder.ui.fremantle and failed:
1273 message = '\n'.join(['%s: %s' % (str(task), \
1274 task.error_message) for task in failed_downloads])
1275 self.show_message(message, _('Downloads failed'), important=True)
1276 self.last_download_count = count
1278 if not gpodder.ui.fremantle:
1279 self.gPodder.set_title(' - '.join(title))
1281 self.update_episode_list_icons(episode_urls)
1282 if self.episode_shownotes_window is not None:
1283 if (shownotes_task and shownotes_task.url in episode_urls) or \
1284 shownotes_task != self.episode_shownotes_window.task:
1285 self.episode_shownotes_window._download_status_changed(shownotes_task)
1286 self.episode_shownotes_window._download_status_progress()
1287 self.play_or_download()
1288 if channel_urls:
1289 self.update_podcast_list_model(channel_urls)
1291 if not self.download_queue_manager.are_queued_or_active_tasks():
1292 self.download_list_update_enabled = False
1294 return self.download_list_update_enabled
1295 except Exception, e:
1296 log('Exception happened while updating download list.', sender=self, traceback=True)
1297 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1298 # We return False here, so the update loop won't be called again,
1299 # that's why we require the restart of gPodder in the message.
1300 return False
1302 def on_config_changed(self, *args):
1303 util.idle_add(self._on_config_changed, *args)
1305 def _on_config_changed(self, name, old_value, new_value):
1306 if name == 'show_toolbar' and gpodder.ui.desktop:
1307 self.toolbar.set_property('visible', new_value)
1308 elif name == 'videoplayer':
1309 self.config.video_played_dbus = False
1310 elif name == 'player':
1311 self.config.audio_played_dbus = False
1312 elif name == 'episode_list_descriptions':
1313 self.update_episode_list_model()
1314 elif name == 'episode_list_thumbnails':
1315 self.update_episode_list_icons(all=True)
1316 elif name == 'rotation_mode':
1317 self._fremantle_rotation.set_mode(new_value)
1318 elif name in ('auto_update_feeds', 'auto_update_frequency'):
1319 self.restart_auto_update_timer()
1320 elif name == 'podcast_list_view_all':
1321 # Force a update of the podcast list model
1322 self.channel_list_changed = True
1323 if gpodder.ui.fremantle:
1324 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1325 while gtk.events_pending():
1326 gtk.main_iteration(False)
1327 self.update_podcast_list_model()
1328 if gpodder.ui.fremantle:
1329 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1331 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1332 # With get_bin_window, we get the window that contains the rows without
1333 # the header. The Y coordinate of this window will be the height of the
1334 # treeview header. This is the amount we have to subtract from the
1335 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1336 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1337 y -= x_bin
1338 y -= y_bin
1339 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1341 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or (column is not None and column != treeview.get_columns()[0]):
1342 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1343 return False
1345 if path is not None:
1346 model = treeview.get_model()
1347 iter = model.get_iter(path)
1348 role = getattr(treeview, TreeViewHelper.ROLE)
1350 if role == TreeViewHelper.ROLE_EPISODES:
1351 id = model.get_value(iter, EpisodeListModel.C_URL)
1352 elif role == TreeViewHelper.ROLE_PODCASTS:
1353 id = model.get_value(iter, PodcastListModel.C_URL)
1355 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1356 if last_tooltip is not None and last_tooltip != id:
1357 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1358 return False
1359 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1361 if role == TreeViewHelper.ROLE_EPISODES:
1362 description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1363 if description:
1364 tooltip.set_text(description)
1365 else:
1366 return False
1367 elif role == TreeViewHelper.ROLE_PODCASTS:
1368 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1369 if channel is None:
1370 return False
1371 channel.request_save_dir_size()
1372 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1373 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1374 if error_str:
1375 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1376 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1377 table = gtk.Table(rows=3, columns=3)
1378 table.set_row_spacings(5)
1379 table.set_col_spacings(5)
1380 table.set_border_width(5)
1382 heading = gtk.Label()
1383 heading.set_alignment(0, 1)
1384 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1385 table.attach(heading, 0, 1, 0, 1)
1386 size_info = gtk.Label()
1387 size_info.set_alignment(1, 1)
1388 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1389 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1390 table.attach(size_info, 2, 3, 0, 1)
1392 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1394 if len(channel.description) < 500:
1395 description = channel.description
1396 else:
1397 pos = channel.description.find('\n\n')
1398 if pos == -1 or pos > 500:
1399 description = channel.description[:498]+'[...]'
1400 else:
1401 description = channel.description[:pos]
1403 description = gtk.Label(description)
1404 if error_str:
1405 description.set_markup(error_str)
1406 description.set_alignment(0, 0)
1407 description.set_line_wrap(True)
1408 table.attach(description, 0, 3, 2, 3)
1410 table.show_all()
1411 tooltip.set_custom(table)
1413 return True
1415 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1416 return False
1418 def treeview_allow_tooltips(self, treeview, allow):
1419 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1421 def update_m3u_playlist_clicked(self, widget):
1422 if self.active_channel is not None:
1423 self.active_channel.update_m3u_playlist()
1424 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1426 def treeview_handle_context_menu_click(self, treeview, event):
1427 x, y = int(event.x), int(event.y)
1428 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1430 selection = treeview.get_selection()
1431 model, paths = selection.get_selected_rows()
1433 if path is None or (path not in paths and \
1434 event.button == self.context_menu_mouse_button):
1435 # We have right-clicked, but not into the selection,
1436 # assume we don't want to operate on the selection
1437 paths = []
1439 if path is not None and not paths and \
1440 event.button == self.context_menu_mouse_button:
1441 # No selection or clicked outside selection;
1442 # select the single item where we clicked
1443 treeview.grab_focus()
1444 treeview.set_cursor(path, column, 0)
1445 paths = [path]
1447 if not paths:
1448 # Unselect any remaining items (clicked elsewhere)
1449 if hasattr(treeview, 'is_rubber_banding_active'):
1450 if not treeview.is_rubber_banding_active():
1451 selection.unselect_all()
1452 else:
1453 selection.unselect_all()
1455 return model, paths
1457 def downloads_list_get_selection(self, model=None, paths=None):
1458 if model is None and paths is None:
1459 selection = self.treeDownloads.get_selection()
1460 model, paths = selection.get_selected_rows()
1462 can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1463 selected_tasks = [(gtk.TreeRowReference(model, path), \
1464 model.get_value(model.get_iter(path), \
1465 DownloadStatusModel.C_TASK)) for path in paths]
1467 for row_reference, task in selected_tasks:
1468 if task.status != download.DownloadTask.QUEUED:
1469 can_force = False
1470 if task.status not in (download.DownloadTask.PAUSED, \
1471 download.DownloadTask.FAILED, \
1472 download.DownloadTask.CANCELLED):
1473 can_queue = False
1474 if task.status not in (download.DownloadTask.PAUSED, \
1475 download.DownloadTask.QUEUED, \
1476 download.DownloadTask.DOWNLOADING):
1477 can_cancel = False
1478 if task.status not in (download.DownloadTask.QUEUED, \
1479 download.DownloadTask.DOWNLOADING):
1480 can_pause = False
1481 if task.status not in (download.DownloadTask.CANCELLED, \
1482 download.DownloadTask.FAILED, \
1483 download.DownloadTask.DONE):
1484 can_remove = False
1486 return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1488 def downloads_finished(self, download_tasks_seen):
1489 # FIXME: Filter all tasks that have already been reported
1490 finished_downloads = [str(task) for task in download_tasks_seen if task.status == task.DONE]
1491 failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.status == task.FAILED]
1493 if finished_downloads and failed_downloads:
1494 message = self.format_episode_list(finished_downloads, 5)
1495 message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1496 message += self.format_episode_list(failed_downloads, 5)
1497 self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1498 elif finished_downloads:
1499 message = self.format_episode_list(finished_downloads)
1500 self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1501 elif failed_downloads:
1502 message = self.format_episode_list(failed_downloads)
1503 self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1505 def format_episode_list(self, episode_list, max_episodes=10):
1507 Format a list of episode names for notifications
1509 Will truncate long episode names and limit the amount of
1510 episodes displayed (max_episodes=10).
1512 The episode_list parameter should be a list of strings.
1514 MAX_TITLE_LENGTH = 100
1516 result = []
1517 for title in episode_list[:min(len(episode_list), max_episodes)]:
1518 if len(title) > MAX_TITLE_LENGTH:
1519 middle = (MAX_TITLE_LENGTH/2)-2
1520 title = '%s...%s' % (title[0:middle], title[-middle:])
1521 result.append(saxutils.escape(title))
1522 result.append('\n')
1524 more_episodes = len(episode_list) - max_episodes
1525 if more_episodes > 0:
1526 result.append('(...')
1527 result.append(N_('%d more episode', '%d more episodes', more_episodes) % more_episodes)
1528 result.append('...)')
1530 return (''.join(result)).strip()
1532 def _for_each_task_set_status(self, tasks, status, force_start=False):
1533 episode_urls = set()
1534 model = self.treeDownloads.get_model()
1535 for row_reference, task in tasks:
1536 if status == download.DownloadTask.QUEUED:
1537 # Only queue task when its paused/failed/cancelled (or forced)
1538 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1539 self.download_queue_manager.add_task(task, force_start)
1540 self.enable_download_list_update()
1541 elif status == download.DownloadTask.CANCELLED:
1542 # Cancelling a download allowed when downloading/queued
1543 if task.status in (task.QUEUED, task.DOWNLOADING):
1544 task.status = status
1545 # Cancelling paused downloads requires a call to .run()
1546 elif task.status == task.PAUSED:
1547 task.status = status
1548 # Call run, so the partial file gets deleted
1549 task.run()
1550 elif status == download.DownloadTask.PAUSED:
1551 # Pausing a download only when queued/downloading
1552 if task.status in (task.DOWNLOADING, task.QUEUED):
1553 task.status = status
1554 elif status is None:
1555 # Remove the selected task - cancel downloading/queued tasks
1556 if task.status in (task.QUEUED, task.DOWNLOADING):
1557 task.status = task.CANCELLED
1558 model.remove(model.get_iter(row_reference.get_path()))
1559 # Remember the URL, so we can tell the UI to update
1560 try:
1561 # We don't "see" this task anymore - remove it;
1562 # this is needed, so update_episode_list_icons()
1563 # below gets the correct list of "seen" tasks
1564 self.download_tasks_seen.remove(task)
1565 except KeyError, key_error:
1566 log('Cannot remove task from "seen" list: %s', task, sender=self)
1567 episode_urls.add(task.url)
1568 # Tell the task that it has been removed (so it can clean up)
1569 task.removed_from_list()
1570 else:
1571 # We can (hopefully) simply set the task status here
1572 task.status = status
1573 # Tell the podcasts tab to update icons for our removed podcasts
1574 self.update_episode_list_icons(episode_urls)
1575 # Update the tab title and downloads list
1576 self.update_downloads_list()
1578 def treeview_downloads_show_context_menu(self, treeview, event):
1579 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1580 if not paths:
1581 if not hasattr(treeview, 'is_rubber_banding_active'):
1582 return True
1583 else:
1584 return not treeview.is_rubber_banding_active()
1586 if event.button == self.context_menu_mouse_button:
1587 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1588 self.downloads_list_get_selection(model, paths)
1590 def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1591 # This creates a menu item for selection-wide actions
1592 item = gtk.ImageMenuItem(label)
1593 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1594 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1595 item.set_sensitive(sensitive)
1596 return self.set_finger_friendly(item)
1598 menu = gtk.Menu()
1600 item = gtk.ImageMenuItem(_('Episode details'))
1601 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1602 if len(selected_tasks) == 1:
1603 row_reference, task = selected_tasks[0]
1604 episode = task.episode
1605 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1606 else:
1607 item.set_sensitive(False)
1608 menu.append(self.set_finger_friendly(item))
1609 menu.append(gtk.SeparatorMenuItem())
1610 if can_force:
1611 menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1612 else:
1613 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1614 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1615 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1616 menu.append(gtk.SeparatorMenuItem())
1617 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1619 if gpodder.ui.maemo:
1620 # Because we open the popup on left-click for Maemo,
1621 # we also include a non-action to close the menu
1622 menu.append(gtk.SeparatorMenuItem())
1623 item = gtk.ImageMenuItem(_('Close this menu'))
1624 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1626 menu.append(self.set_finger_friendly(item))
1628 menu.show_all()
1629 menu.popup(None, None, None, event.button, event.time)
1630 return True
1632 def treeview_channels_show_context_menu(self, treeview, event):
1633 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1634 if not paths:
1635 return True
1637 # Check for valid channel id, if there's no id then
1638 # assume that it is a proxy channel or equivalent
1639 # and cannot be operated with right click
1640 if self.active_channel.id is None:
1641 return True
1643 if event.button == 3:
1644 menu = gtk.Menu()
1646 ICON = lambda x: x
1648 item = gtk.ImageMenuItem( _('Open download folder'))
1649 item.set_image( gtk.image_new_from_icon_name(ICON('folder-open'), gtk.ICON_SIZE_MENU))
1650 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1651 menu.append( item)
1653 item = gtk.ImageMenuItem( _('Update Feed'))
1654 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1655 item.connect('activate', self.on_itemUpdateChannel_activate )
1656 item.set_sensitive( not self.updating_feed_cache )
1657 menu.append( item)
1659 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1660 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1661 item.connect('activate', self.update_m3u_playlist_clicked)
1662 menu.append(item)
1664 if self.active_channel.link:
1665 item = gtk.ImageMenuItem(_('Visit website'))
1666 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1667 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1668 menu.append(item)
1670 if self.active_channel.channel_is_locked:
1671 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1672 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1673 item.connect('activate', self.on_channel_toggle_lock_activate)
1674 menu.append(self.set_finger_friendly(item))
1675 else:
1676 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1677 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1678 item.connect('activate', self.on_channel_toggle_lock_activate)
1679 menu.append(self.set_finger_friendly(item))
1682 menu.append( gtk.SeparatorMenuItem())
1684 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1685 item.connect( 'activate', self.on_itemEditChannel_activate)
1686 menu.append( item)
1688 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1689 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1690 menu.append( item)
1692 menu.show_all()
1693 # Disable tooltips while we are showing the menu, so
1694 # the tooltip will not appear over the menu
1695 self.treeview_allow_tooltips(self.treeChannels, False)
1696 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1697 menu.popup( None, None, None, event.button, event.time)
1699 return True
1701 def on_itemClose_activate(self, widget):
1702 if self.tray_icon is not None:
1703 self.iconify_main_window()
1704 else:
1705 self.on_gPodder_delete_event(widget)
1707 def cover_file_removed(self, channel_url):
1709 The Cover Downloader calls this when a previously-
1710 available cover has been removed from the disk. We
1711 have to update our model to reflect this change.
1713 self.podcast_list_model.delete_cover_by_url(channel_url)
1715 def cover_download_finished(self, channel_url, pixbuf):
1717 The Cover Downloader calls this when it has finished
1718 downloading (or registering, if already downloaded)
1719 a new channel cover, which is ready for displaying.
1721 self.podcast_list_model.add_cover_by_url(channel_url, pixbuf)
1723 def save_episodes_as_file(self, episodes):
1724 for episode in episodes:
1725 self.save_episode_as_file(episode)
1727 def save_episode_as_file(self, episode):
1728 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1729 if episode.was_downloaded(and_exists=True):
1730 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1731 copy_from = episode.local_filename(create=False)
1732 assert copy_from is not None
1733 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1734 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1735 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1737 def copy_episodes_bluetooth(self, episodes):
1738 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1740 def convert_and_send_thread(episode):
1741 for episode in episodes:
1742 filename = episode.local_filename(create=False)
1743 assert filename is not None
1744 destfile = os.path.join(tempfile.gettempdir(), \
1745 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1746 (base, ext) = os.path.splitext(filename)
1747 if not destfile.endswith(ext):
1748 destfile += ext
1750 try:
1751 shutil.copyfile(filename, destfile)
1752 util.bluetooth_send_file(destfile)
1753 except:
1754 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1755 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1757 util.delete_file(destfile)
1759 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1761 def get_device_name(self):
1762 if self.config.device_type == 'ipod':
1763 return _('iPod')
1764 elif self.config.device_type in ('filesystem', 'mtp'):
1765 return _('MP3 player')
1766 else:
1767 return '(unknown device)'
1769 def _treeview_button_released(self, treeview, event):
1770 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1771 dy = int(abs(event.y-ypos))
1772 dx = int(event.x-xpos)
1774 selection = treeview.get_selection()
1775 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1776 if path is None or dy > 30:
1777 return (False, dx, dy)
1779 path, column, x, y = path
1780 selection.select_path(path)
1781 treeview.set_cursor(path)
1782 treeview.grab_focus()
1784 return (True, dx, dy)
1786 def treeview_channels_handle_gestures(self, treeview, event):
1787 if self.currently_updating:
1788 return False
1790 selected, dx, dy = self._treeview_button_released(treeview, event)
1792 if selected:
1793 if self.config.maemo_enable_gestures:
1794 if dx > 70:
1795 self.on_itemUpdateChannel_activate()
1796 elif dx < -70:
1797 self.on_itemEditChannel_activate(treeview)
1799 return False
1801 def treeview_available_handle_gestures(self, treeview, event):
1802 selected, dx, dy = self._treeview_button_released(treeview, event)
1804 if selected:
1805 if self.config.maemo_enable_gestures:
1806 if dx > 70:
1807 self.on_playback_selected_episodes(None)
1808 return True
1809 elif dx < -70:
1810 self.on_shownotes_selected_episodes(None)
1811 return True
1813 # Pass the event to the context menu handler for treeAvailable
1814 self.treeview_available_show_context_menu(treeview, event)
1816 return True
1818 def treeview_available_show_context_menu(self, treeview, event):
1819 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1820 if not paths:
1821 if not hasattr(treeview, 'is_rubber_banding_active'):
1822 return True
1823 else:
1824 return not treeview.is_rubber_banding_active()
1826 if event.button == self.context_menu_mouse_button:
1827 episodes = self.get_selected_episodes()
1828 any_locked = any(e.is_locked for e in episodes)
1829 any_played = any(e.is_played for e in episodes)
1830 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1831 downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
1832 downloading = any(self.episode_is_downloading(e) for e in episodes)
1834 menu = gtk.Menu()
1836 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1838 if open_instead_of_play:
1839 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1840 elif downloaded:
1841 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1842 else:
1843 item = gtk.ImageMenuItem(_('Stream'))
1844 item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
1846 item.set_sensitive(can_play and not downloading)
1847 item.connect('activate', self.on_playback_selected_episodes)
1848 menu.append(self.set_finger_friendly(item))
1850 if not can_cancel:
1851 item = gtk.ImageMenuItem(_('Download'))
1852 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1853 item.set_sensitive(can_download)
1854 item.connect('activate', self.on_download_selected_episodes)
1855 menu.append(self.set_finger_friendly(item))
1856 else:
1857 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1858 item.connect('activate', self.on_item_cancel_download_activate)
1859 menu.append(self.set_finger_friendly(item))
1861 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1862 item.set_sensitive(can_delete)
1863 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1864 menu.append(self.set_finger_friendly(item))
1866 ICON = lambda x: x
1868 # Ok, this probably makes sense to only display for downloaded files
1869 if downloaded:
1870 menu.append(gtk.SeparatorMenuItem())
1871 share_item = gtk.MenuItem(_('Send to'))
1872 menu.append(share_item)
1873 share_menu = gtk.Menu()
1875 item = gtk.ImageMenuItem(_('Local folder'))
1876 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
1877 item.connect('activate', lambda w, ee: self.save_episodes_as_file(ee), episodes)
1878 share_menu.append(self.set_finger_friendly(item))
1879 if self.bluetooth_available:
1880 item = gtk.ImageMenuItem(_('Bluetooth device'))
1881 item.set_image(gtk.image_new_from_icon_name(ICON('bluetooth'), gtk.ICON_SIZE_MENU))
1882 item.connect('activate', lambda w, ee: self.copy_episodes_bluetooth(ee), episodes)
1883 share_menu.append(self.set_finger_friendly(item))
1884 if can_transfer:
1885 item = gtk.ImageMenuItem(self.get_device_name())
1886 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
1887 item.connect('activate', lambda w, ee: self.on_sync_to_ipod_activate(w, ee), episodes)
1888 share_menu.append(self.set_finger_friendly(item))
1890 share_item.set_submenu(share_menu)
1892 if (downloaded or one_is_new or can_download) and not downloading:
1893 menu.append(gtk.SeparatorMenuItem())
1894 if one_is_new:
1895 item = gtk.CheckMenuItem(_('New'))
1896 item.set_active(True)
1897 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1898 menu.append(self.set_finger_friendly(item))
1899 elif can_download:
1900 item = gtk.CheckMenuItem(_('New'))
1901 item.set_active(False)
1902 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1903 menu.append(self.set_finger_friendly(item))
1905 if downloaded:
1906 item = gtk.CheckMenuItem(_('Played'))
1907 item.set_active(any_played)
1908 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
1909 menu.append(self.set_finger_friendly(item))
1911 item = gtk.CheckMenuItem(_('Keep episode'))
1912 item.set_active(any_locked)
1913 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
1914 menu.append(self.set_finger_friendly(item))
1916 menu.append(gtk.SeparatorMenuItem())
1917 # Single item, add episode information menu item
1918 item = gtk.ImageMenuItem(_('Episode details'))
1919 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1920 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
1921 menu.append(self.set_finger_friendly(item))
1923 if gpodder.ui.maemo:
1924 # Because we open the popup on left-click for Maemo,
1925 # we also include a non-action to close the menu
1926 menu.append(gtk.SeparatorMenuItem())
1927 item = gtk.ImageMenuItem(_('Close this menu'))
1928 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1929 menu.append(self.set_finger_friendly(item))
1931 menu.show_all()
1932 # Disable tooltips while we are showing the menu, so
1933 # the tooltip will not appear over the menu
1934 self.treeview_allow_tooltips(self.treeAvailable, False)
1935 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
1936 menu.popup( None, None, None, event.button, event.time)
1938 return True
1940 def set_title(self, new_title):
1941 if not gpodder.ui.fremantle:
1942 self.default_title = new_title
1943 self.gPodder.set_title(new_title)
1945 def update_episode_list_icons(self, urls=None, selected=False, all=False):
1947 Updates the status icons in the episode list.
1949 If urls is given, it should be a list of URLs
1950 of episodes that should be updated.
1952 If urls is None, set ONE OF selected, all to
1953 True (the former updates just the selected
1954 episodes and the latter updates all episodes).
1956 additional_args = (self.episode_is_downloading, \
1957 self.config.episode_list_descriptions and gpodder.ui.desktop, \
1958 self.config.episode_list_thumbnails and gpodder.ui.desktop)
1960 if urls is not None:
1961 # We have a list of URLs to walk through
1962 self.episode_list_model.update_by_urls(urls, *additional_args)
1963 elif selected and not all:
1964 # We should update all selected episodes
1965 selection = self.treeAvailable.get_selection()
1966 model, paths = selection.get_selected_rows()
1967 for path in reversed(paths):
1968 iter = model.get_iter(path)
1969 self.episode_list_model.update_by_filter_iter(iter, \
1970 *additional_args)
1971 elif all and not selected:
1972 # We update all (even the filter-hidden) episodes
1973 self.episode_list_model.update_all(*additional_args)
1974 else:
1975 # Wrong/invalid call - have to specify at least one parameter
1976 raise ValueError('Invalid call to update_episode_list_icons')
1978 def episode_list_status_changed(self, episodes):
1979 self.update_episode_list_icons(set(e.url for e in episodes))
1980 self.update_podcast_list_model(set(e.channel.url for e in episodes))
1981 self.db.commit()
1983 def clean_up_downloads(self, delete_partial=False):
1984 # Clean up temporary files left behind by old gPodder versions
1985 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
1987 if delete_partial:
1988 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
1990 for tempfile in temporary_files:
1991 util.delete_file(tempfile)
1993 # Clean up empty download folders and abandoned download folders
1994 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
1995 for ddir in download_dirs:
1996 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1997 globr = glob.glob(os.path.join(ddir, '*'))
1998 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
1999 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
2000 shutil.rmtree(ddir, ignore_errors=True)
2002 def streaming_possible(self):
2003 if gpodder.ui.desktop:
2004 # User has to have a media player set on the Desktop, or else we
2005 # would probably open the browser when giving a URL to xdg-open..
2006 return (self.config.player and self.config.player != 'default')
2007 elif gpodder.ui.maemo:
2008 # On Maemo, the default is to use the Nokia Media Player, which is
2009 # already able to deal with HTTP URLs the right way, so we
2010 # unconditionally enable streaming always on Maemo
2011 return True
2013 return False
2015 def playback_episodes_for_real(self, episodes):
2016 groups = collections.defaultdict(list)
2017 for episode in episodes:
2018 file_type = episode.file_type()
2019 if file_type == 'video' and self.config.videoplayer and \
2020 self.config.videoplayer != 'default':
2021 player = self.config.videoplayer
2022 if gpodder.ui.diablo:
2023 # Use the wrapper script if it's installed to crop 3GP YouTube
2024 # videos to fit the screen (looks much nicer than w/ black border)
2025 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2026 player = 'gpodder-mplayer'
2027 elif gpodder.ui.fremantle and player == 'mplayer':
2028 player = 'mplayer -fs %F'
2029 elif file_type == 'audio' and self.config.player and \
2030 self.config.player != 'default':
2031 player = self.config.player
2032 else:
2033 player = 'default'
2035 if file_type not in ('audio', 'video') or \
2036 (file_type == 'audio' and not self.config.audio_played_dbus) or \
2037 (file_type == 'video' and not self.config.video_played_dbus):
2038 # Mark episode as played in the database
2039 episode.mark(is_played=True)
2040 self.mygpo_client.on_playback([episode])
2042 filename = episode.local_filename(create=False)
2043 if filename is None or not os.path.exists(filename):
2044 filename = episode.url
2045 if youtube.is_video_link(filename):
2046 fmt_id = self.config.youtube_preferred_fmt_id
2047 if gpodder.ui.fremantle:
2048 fmt_id = 5
2049 filename = youtube.get_real_download_url(filename, fmt_id)
2050 groups[player].append(filename)
2052 # Open episodes with system default player
2053 if 'default' in groups:
2054 for filename in groups['default']:
2055 log('Opening with system default: %s', filename, sender=self)
2056 util.gui_open(filename)
2057 del groups['default']
2058 elif gpodder.ui.maemo:
2059 # When on Maemo and not opening with default, show a notification
2060 # (no startup notification for Panucci / MPlayer yet...)
2061 if len(episodes) == 1:
2062 text = _('Opening %s') % episodes[0].title
2063 else:
2064 count = len(episodes)
2065 text = N_('Opening %d episode', 'Opening %d episodes', count) % count
2067 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2069 def destroy_banner_later(banner):
2070 banner.destroy()
2071 return False
2072 gobject.timeout_add(5000, destroy_banner_later, banner)
2074 # For each type now, go and create play commands
2075 for group in groups:
2076 for command in util.format_desktop_command(group, groups[group]):
2077 log('Executing: %s', repr(command), sender=self)
2078 subprocess.Popen(command)
2080 # Persist episode status changes to the database
2081 self.db.commit()
2083 # Flush updated episode status
2084 self.mygpo_client.flush()
2086 def playback_episodes(self, episodes):
2087 # We need to create a list, because we run through it more than once
2088 episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2089 e.was_downloaded(and_exists=True) or self.streaming_possible()))
2091 try:
2092 self.playback_episodes_for_real(episodes)
2093 except Exception, e:
2094 log('Error in playback!', sender=self, traceback=True)
2095 if gpodder.ui.desktop:
2096 self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2097 _('Error opening player'), widget=self.toolPreferences)
2098 else:
2099 self.show_message(_('Please check your media player settings in the preferences dialog.'))
2101 channel_urls = set()
2102 episode_urls = set()
2103 for episode in episodes:
2104 channel_urls.add(episode.channel.url)
2105 episode_urls.add(episode.url)
2106 self.update_episode_list_icons(episode_urls)
2107 self.update_podcast_list_model(channel_urls)
2109 def play_or_download(self):
2110 if not gpodder.ui.fremantle:
2111 if self.wNotebook.get_current_page() > 0:
2112 if gpodder.ui.desktop:
2113 self.toolCancel.set_sensitive(True)
2114 return
2116 if self.currently_updating:
2117 return (False, False, False, False, False, False)
2119 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2120 ( is_played, is_locked ) = (False,)*2
2122 open_instead_of_play = False
2124 selection = self.treeAvailable.get_selection()
2125 if selection.count_selected_rows() > 0:
2126 (model, paths) = selection.get_selected_rows()
2128 for path in paths:
2129 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2131 if episode.file_type() not in ('audio', 'video'):
2132 open_instead_of_play = True
2134 if episode.was_downloaded():
2135 can_play = episode.was_downloaded(and_exists=True)
2136 is_played = episode.is_played
2137 is_locked = episode.is_locked
2138 if not can_play:
2139 can_download = True
2140 else:
2141 if self.episode_is_downloading(episode):
2142 can_cancel = True
2143 else:
2144 can_download = True
2146 can_download = can_download and not can_cancel
2147 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2148 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2149 can_delete = not can_cancel
2151 if gpodder.ui.desktop:
2152 if open_instead_of_play:
2153 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2154 else:
2155 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2156 self.toolPlay.set_sensitive( can_play)
2157 self.toolDownload.set_sensitive( can_download)
2158 self.toolTransfer.set_sensitive( can_transfer)
2159 self.toolCancel.set_sensitive( can_cancel)
2161 if not gpodder.ui.fremantle:
2162 self.item_cancel_download.set_sensitive(can_cancel)
2163 self.itemDownloadSelected.set_sensitive(can_download)
2164 self.itemOpenSelected.set_sensitive(can_play)
2165 self.itemPlaySelected.set_sensitive(can_play)
2166 self.itemDeleteSelected.set_sensitive(can_delete)
2167 self.item_toggle_played.set_sensitive(can_play)
2168 self.item_toggle_lock.set_sensitive(can_play)
2169 self.itemOpenSelected.set_visible(open_instead_of_play)
2170 self.itemPlaySelected.set_visible(not open_instead_of_play)
2172 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2174 def on_cbMaxDownloads_toggled(self, widget, *args):
2175 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2177 def on_cbLimitDownloads_toggled(self, widget, *args):
2178 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2180 def episode_new_status_changed(self, urls):
2181 self.update_podcast_list_model()
2182 self.update_episode_list_icons(urls)
2184 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2185 """Update the podcast list treeview model
2187 If urls is given, it should list the URLs of each
2188 podcast that has to be updated in the list.
2190 If selected is True, only update the model contents
2191 for the currently-selected podcast - nothing more.
2193 The caller can optionally specify "select_url",
2194 which is the URL of the podcast that is to be
2195 selected in the list after the update is complete.
2196 This only works if the podcast list has to be
2197 reloaded; i.e. something has been added or removed
2198 since the last update of the podcast list).
2200 selection = self.treeChannels.get_selection()
2201 model, iter = selection.get_selected()
2203 if self.config.podcast_list_view_all and not self.channel_list_changed:
2204 # Update "all episodes" view in any case (if enabled)
2205 self.podcast_list_model.update_first_row()
2207 if selected:
2208 # very cheap! only update selected channel
2209 if iter is not None:
2210 # If we have selected the "all episodes" view, we have
2211 # to update all channels for selected episodes:
2212 if self.config.podcast_list_view_all and \
2213 self.podcast_list_model.iter_is_first_row(iter):
2214 urls = self.get_podcast_urls_from_selected_episodes()
2215 self.podcast_list_model.update_by_urls(urls)
2216 else:
2217 # Otherwise just update the selected row (a podcast)
2218 self.podcast_list_model.update_by_filter_iter(iter)
2219 elif not self.channel_list_changed:
2220 # we can keep the model, but have to update some
2221 if urls is None:
2222 # still cheaper than reloading the whole list
2223 self.podcast_list_model.update_all()
2224 else:
2225 # ok, we got a bunch of urls to update
2226 self.podcast_list_model.update_by_urls(urls)
2227 else:
2228 if model and iter and select_url is None:
2229 # Get the URL of the currently-selected podcast
2230 select_url = model.get_value(iter, PodcastListModel.C_URL)
2232 # Update the podcast list model with new channels
2233 self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2235 try:
2236 selected_iter = model.get_iter_first()
2237 # Find the previously-selected URL in the new
2238 # model if we have an URL (else select first)
2239 if select_url is not None:
2240 pos = model.get_iter_first()
2241 while pos is not None:
2242 url = model.get_value(pos, PodcastListModel.C_URL)
2243 if url == select_url:
2244 selected_iter = pos
2245 break
2246 pos = model.iter_next(pos)
2248 if not gpodder.ui.fremantle:
2249 if selected_iter is not None:
2250 selection.select_iter(selected_iter)
2251 self.on_treeChannels_cursor_changed(self.treeChannels)
2252 except:
2253 log('Cannot select podcast in list', traceback=True, sender=self)
2254 self.channel_list_changed = False
2256 def episode_is_downloading(self, episode):
2257 """Returns True if the given episode is being downloaded at the moment"""
2258 if episode is None:
2259 return False
2261 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2263 def update_episode_list_model(self):
2264 if self.channels and self.active_channel is not None:
2265 if gpodder.ui.fremantle:
2266 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2268 self.currently_updating = True
2269 self.episode_list_model.clear()
2270 self.episode_list_model.reset_update_progress()
2271 self.treeAvailable.set_model(self.empty_episode_list_model)
2272 def do_update_episode_list_model():
2273 additional_args = (self.episode_is_downloading, \
2274 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2275 self.config.episode_list_thumbnails and gpodder.ui.desktop, \
2276 self.treeAvailable)
2277 self.episode_list_model.add_from_channel(self.active_channel, *additional_args)
2279 def on_episode_list_model_updated():
2280 if gpodder.ui.fremantle:
2281 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2282 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
2283 self.treeAvailable.columns_autosize()
2284 self.currently_updating = False
2285 self.play_or_download()
2286 util.idle_add(on_episode_list_model_updated)
2287 threading.Thread(target=do_update_episode_list_model).start()
2288 else:
2289 self.episode_list_model.clear()
2291 def offer_new_episodes(self, channels=None):
2292 new_episodes = self.get_new_episodes(channels)
2293 if new_episodes:
2294 self.new_episodes_show(new_episodes)
2295 return True
2296 return False
2298 def add_podcast_list(self, urls, auth_tokens=None):
2299 """Subscribe to a list of podcast given their URLs
2301 If auth_tokens is given, it should be a dictionary
2302 mapping URLs to (username, password) tuples."""
2304 if auth_tokens is None:
2305 auth_tokens = {}
2307 # Sort and split the URL list into five buckets
2308 queued, failed, existing, worked, authreq = [], [], [], [], []
2309 for input_url in urls:
2310 url = util.normalize_feed_url(input_url)
2311 if url is None:
2312 # Fail this one because the URL is not valid
2313 failed.append(input_url)
2314 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2315 # A podcast already exists in the list for this URL
2316 existing.append(url)
2317 else:
2318 # This URL has survived the first round - queue for add
2319 queued.append(url)
2320 if url != input_url and input_url in auth_tokens:
2321 auth_tokens[url] = auth_tokens[input_url]
2323 error_messages = {}
2324 redirections = {}
2326 progress = ProgressIndicator(_('Adding podcasts'), \
2327 _('Please wait while episode information is downloaded.'), \
2328 parent=self.get_dialog_parent())
2330 def on_after_update():
2331 progress.on_finished()
2332 # Report already-existing subscriptions to the user
2333 if existing:
2334 title = _('Existing subscriptions skipped')
2335 message = _('You are already subscribed to these podcasts:') \
2336 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2337 self.show_message(message, title, widget=self.treeChannels)
2339 # Report subscriptions that require authentication
2340 if authreq:
2341 retry_podcasts = {}
2342 for url in authreq:
2343 title = _('Podcast requires authentication')
2344 message = _('Please login to %s:') % (saxutils.escape(url),)
2345 success, auth_tokens = self.show_login_dialog(title, message)
2346 if success:
2347 retry_podcasts[url] = auth_tokens
2348 else:
2349 # Stop asking the user for more login data
2350 retry_podcasts = {}
2351 for url in authreq:
2352 error_messages[url] = _('Authentication failed')
2353 failed.append(url)
2354 break
2356 # If we have authentication data to retry, do so here
2357 if retry_podcasts:
2358 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2360 # Report website redirections
2361 for url in redirections:
2362 title = _('Website redirection detected')
2363 message = _('The URL %(url)s redirects to %(target)s.') \
2364 + '\n\n' + _('Do you want to visit the website now?')
2365 message = message % {'url': url, 'target': redirections[url]}
2366 if self.show_confirmation(message, title):
2367 util.open_website(url)
2368 else:
2369 break
2371 # Report failed subscriptions to the user
2372 if failed:
2373 title = _('Could not add some podcasts')
2374 message = _('Some podcasts could not be added to your list:') \
2375 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2376 error_messages.get(url, _('Unknown')))) for url in failed)
2377 self.show_message(message, title, important=True)
2379 # Upload subscription changes to gpodder.net
2380 self.mygpo_client.on_subscribe(worked)
2382 # If at least one podcast has been added, save and update all
2383 if self.channel_list_changed:
2384 # Fix URLs if mygpo has rewritten them
2385 self.rewrite_urls_mygpo()
2387 self.save_channels_opml()
2389 # If only one podcast was added, select it after the update
2390 if len(worked) == 1:
2391 url = worked[0]
2392 else:
2393 url = None
2395 # Update the list of subscribed podcasts
2396 self.update_feed_cache(force_update=False, select_url_afterwards=url)
2397 self.update_podcasts_tab()
2399 # Offer to download new episodes
2400 self.offer_new_episodes(channels=[c for c in self.channels if c.url in worked])
2402 def thread_proc():
2403 # After the initial sorting and splitting, try all queued podcasts
2404 length = len(queued)
2405 for index, url in enumerate(queued):
2406 progress.on_progress(float(index)/float(length))
2407 progress.on_message(url)
2408 log('QUEUE RUNNER: %s', url, sender=self)
2409 try:
2410 # The URL is valid and does not exist already - subscribe!
2411 channel = PodcastChannel.load(self.db, url=url, create=True, \
2412 authentication_tokens=auth_tokens.get(url, None), \
2413 max_episodes=self.config.max_episodes_per_feed, \
2414 download_dir=self.config.download_dir, \
2415 allow_empty_feeds=self.config.allow_empty_feeds)
2417 try:
2418 username, password = util.username_password_from_url(url)
2419 except ValueError, ve:
2420 username, password = (None, None)
2422 if username is not None and channel.username is None and \
2423 password is not None and channel.password is None:
2424 channel.username = username
2425 channel.password = password
2426 channel.save()
2428 self._update_cover(channel)
2429 except feedcore.AuthenticationRequired:
2430 if url in auth_tokens:
2431 # Fail for wrong authentication data
2432 error_messages[url] = _('Authentication failed')
2433 failed.append(url)
2434 else:
2435 # Queue for login dialog later
2436 authreq.append(url)
2437 continue
2438 except feedcore.WifiLogin, error:
2439 redirections[url] = error.data
2440 failed.append(url)
2441 error_messages[url] = _('Redirection detected')
2442 continue
2443 except Exception, e:
2444 log('Subscription error: %s', e, traceback=True, sender=self)
2445 error_messages[url] = str(e)
2446 failed.append(url)
2447 continue
2449 assert channel is not None
2450 worked.append(channel.url)
2451 self.channels.append(channel)
2452 self.channel_list_changed = True
2453 util.idle_add(on_after_update)
2454 threading.Thread(target=thread_proc).start()
2456 def save_channels_opml(self):
2457 exporter = opml.Exporter(gpodder.subscription_file)
2458 return exporter.write(self.channels)
2460 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2461 self.db.commit()
2462 self.updating_feed_cache = False
2464 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2465 self.channel_list_changed = True
2466 self.update_podcast_list_model(select_url=select_url_afterwards)
2468 # Only search for new episodes in podcasts that have been
2469 # updated, not in other podcasts (for single-feed updates)
2470 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2472 if gpodder.ui.fremantle:
2473 self.button_subscribe.set_sensitive(True)
2474 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
2475 self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
2476 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2477 self.update_podcasts_tab()
2478 if self.feed_cache_update_cancelled:
2479 return
2481 if episodes:
2482 if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2483 # New episodes found, but we should do nothing
2484 self.show_message(_('New episodes are available.'))
2485 elif self.config.auto_download == 'always':
2486 count = len(episodes)
2487 title = N_('Downloading %d new episode.', 'Downloading %d new episodes.', count) % count
2488 self.show_message(title)
2489 self.download_episode_list(episodes)
2490 elif self.config.auto_download == 'queue':
2491 self.show_message(_('New episodes have been added to the download list.'))
2492 self.download_episode_list_paused(episodes)
2493 else:
2494 self.new_episodes_show(episodes)
2495 elif not self.config.auto_update_feeds:
2496 self.show_message(_('No new episodes. Please check for new episodes later.'))
2497 return
2499 if self.tray_icon:
2500 self.tray_icon.set_status()
2502 if self.feed_cache_update_cancelled:
2503 # The user decided to abort the feed update
2504 self.show_update_feeds_buttons()
2505 elif not episodes:
2506 # Nothing new here - but inform the user
2507 self.pbFeedUpdate.set_fraction(1.0)
2508 self.pbFeedUpdate.set_text(_('No new episodes'))
2509 self.feed_cache_update_cancelled = True
2510 self.btnCancelFeedUpdate.show()
2511 self.btnCancelFeedUpdate.set_sensitive(True)
2512 if gpodder.ui.maemo:
2513 # btnCancelFeedUpdate is a ToolButton on Maemo
2514 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2515 else:
2516 # btnCancelFeedUpdate is a normal gtk.Button
2517 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2518 else:
2519 count = len(episodes)
2520 # New episodes are available
2521 self.pbFeedUpdate.set_fraction(1.0)
2522 # Are we minimized and should we auto download?
2523 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2524 self.download_episode_list(episodes)
2525 title = N_('Downloading %d new episode.', 'Downloading %d new episodes.', count) % count
2526 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2527 self.show_update_feeds_buttons()
2528 elif self.config.auto_download == 'queue':
2529 self.download_episode_list_paused(episodes)
2530 title = N_('%d new episode added to download list.', '%d new episodes added to download list.', count) % count
2531 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2532 self.show_update_feeds_buttons()
2533 else:
2534 self.show_update_feeds_buttons()
2535 # New episodes are available and we are not minimized
2536 if not self.config.do_not_show_new_episodes_dialog:
2537 self.new_episodes_show(episodes, notification=True)
2538 else:
2539 message = N_('%d new episode available', '%d new episodes available', count) % count
2540 self.pbFeedUpdate.set_text(message)
2542 def _update_cover(self, channel):
2543 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2544 self.cover_downloader.request_cover(channel)
2546 def update_feed_cache_proc(self, channels, select_url_afterwards):
2547 total = len(channels)
2549 for updated, channel in enumerate(channels):
2550 if not self.feed_cache_update_cancelled:
2551 try:
2552 # Update if timeout is not reached or we update a single podcast or skipping is disabled
2553 if channel.query_automatic_update() or total == 1 or not self.config.feed_update_skipping:
2554 channel.update(max_episodes=self.config.max_episodes_per_feed)
2555 else:
2556 log('Skipping update of %s (see feed_update_skipping)', channel.title, sender=self)
2557 self._update_cover(channel)
2558 except Exception, e:
2559 d = {'url': saxutils.escape(channel.url), 'message': saxutils.escape(str(e))}
2560 if d['message']:
2561 message = _('Error while updating %(url)s: %(message)s')
2562 else:
2563 message = _('The feed at %(url)s could not be updated.')
2564 self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
2565 log('Error: %s', str(e), sender=self, traceback=True)
2567 if self.feed_cache_update_cancelled:
2568 break
2570 if gpodder.ui.fremantle:
2571 util.idle_add(self.button_refresh.set_title, \
2572 _('%(position)d/%(total)d updated') % {'position': updated, 'total': total})
2573 continue
2575 # By the time we get here the update may have already been cancelled
2576 if not self.feed_cache_update_cancelled:
2577 def update_progress():
2578 d = {'podcast': channel.title, 'position': updated, 'total': total}
2579 progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2580 self.pbFeedUpdate.set_text(progression)
2581 if self.tray_icon:
2582 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2583 self.pbFeedUpdate.set_fraction(float(updated)/float(total))
2584 util.idle_add(update_progress)
2586 updated_urls = [c.url for c in channels]
2587 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2589 def show_update_feeds_buttons(self):
2590 # Make sure that the buttons for updating feeds
2591 # appear - this should happen after a feed update
2592 if gpodder.ui.maemo:
2593 self.btnUpdateSelectedFeed.show()
2594 self.toolFeedUpdateProgress.hide()
2595 self.btnCancelFeedUpdate.hide()
2596 self.btnCancelFeedUpdate.set_is_important(False)
2597 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2598 self.toolbarSpacer.set_expand(True)
2599 self.toolbarSpacer.set_draw(False)
2600 else:
2601 self.hboxUpdateFeeds.hide()
2602 self.btnUpdateFeeds.show()
2603 self.itemUpdate.set_sensitive(True)
2604 self.itemUpdateChannel.set_sensitive(True)
2606 def on_btnCancelFeedUpdate_clicked(self, widget):
2607 if not self.feed_cache_update_cancelled:
2608 self.pbFeedUpdate.set_text(_('Cancelling...'))
2609 self.feed_cache_update_cancelled = True
2610 self.btnCancelFeedUpdate.set_sensitive(False)
2611 else:
2612 self.show_update_feeds_buttons()
2614 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
2615 if self.updating_feed_cache:
2616 if gpodder.ui.fremantle:
2617 self.feed_cache_update_cancelled = True
2618 return
2620 if not force_update:
2621 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2622 self.channel_list_changed = True
2623 self.update_podcast_list_model(select_url=select_url_afterwards)
2624 return
2626 # Fix URLs if mygpo has rewritten them
2627 self.rewrite_urls_mygpo()
2629 self.updating_feed_cache = True
2631 if channels is None:
2632 channels = self.channels
2634 if gpodder.ui.fremantle:
2635 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
2636 self.button_refresh.set_title(_('Updating...'))
2637 self.button_subscribe.set_sensitive(False)
2638 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
2639 self.ICON_GENERAL_CLOSE, gtk.ICON_SIZE_BUTTON))
2640 self.feed_cache_update_cancelled = False
2641 else:
2642 self.itemUpdate.set_sensitive(False)
2643 self.itemUpdateChannel.set_sensitive(False)
2645 if self.tray_icon:
2646 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2648 if len(channels) == 1:
2649 text = _('Updating "%s"...') % channels[0].title
2650 else:
2651 count = len(channels)
2652 text = N_('Updating %d feed...', 'Updating %d feeds...', count) % count
2653 self.pbFeedUpdate.set_text(text)
2654 self.pbFeedUpdate.set_fraction(0)
2656 self.feed_cache_update_cancelled = False
2657 self.btnCancelFeedUpdate.show()
2658 self.btnCancelFeedUpdate.set_sensitive(True)
2659 if gpodder.ui.maemo:
2660 self.toolbarSpacer.set_expand(False)
2661 self.toolbarSpacer.set_draw(True)
2662 self.btnUpdateSelectedFeed.hide()
2663 self.toolFeedUpdateProgress.show_all()
2664 else:
2665 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2666 self.hboxUpdateFeeds.show_all()
2667 self.btnUpdateFeeds.hide()
2669 args = (channels, select_url_afterwards)
2670 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
2672 def on_gPodder_delete_event(self, widget, *args):
2673 """Called when the GUI wants to close the window
2674 Displays a confirmation dialog (and closes/hides gPodder)
2677 downloading = self.download_status_model.are_downloads_in_progress()
2679 # Only iconify if we are using the window's "X" button,
2680 # but not when we are using "Quit" in the menu or toolbar
2681 if self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
2682 self.iconify_main_window()
2683 elif self.config.on_quit_ask or downloading:
2684 if gpodder.ui.fremantle:
2685 self.close_gpodder()
2686 elif gpodder.ui.diablo:
2687 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2688 if result:
2689 self.close_gpodder()
2690 else:
2691 return True
2692 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2693 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2694 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2696 title = _('Quit gPodder')
2697 if downloading:
2698 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2699 else:
2700 message = _('Do you really want to quit gPodder now?')
2702 dialog.set_title(title)
2703 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2704 if not downloading:
2705 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2706 dialog.vbox.pack_start(cb_ask)
2707 cb_ask.show_all()
2709 quit_button.grab_focus()
2710 result = dialog.run()
2711 dialog.destroy()
2713 if result == gtk.RESPONSE_CLOSE:
2714 if not downloading and cb_ask.get_active() == True:
2715 self.config.on_quit_ask = False
2716 self.close_gpodder()
2717 else:
2718 self.close_gpodder()
2720 return True
2722 def close_gpodder(self):
2723 """ clean everything and exit properly
2725 if self.channels:
2726 if self.save_channels_opml():
2727 pass # FIXME: Add mygpo synchronization here
2728 else:
2729 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
2731 self.gPodder.hide()
2733 if self.tray_icon is not None:
2734 self.tray_icon.set_visible(False)
2736 # Notify all tasks to to carry out any clean-up actions
2737 self.download_status_model.tell_all_tasks_to_quit()
2739 while gtk.events_pending():
2740 gtk.main_iteration(False)
2742 self.db.close()
2744 self.quit()
2745 sys.exit(0)
2747 def get_expired_episodes(self):
2748 for channel in self.channels:
2749 for episode in channel.get_downloaded_episodes():
2750 # Never consider locked episodes as old
2751 if episode.is_locked:
2752 continue
2754 # Never consider fresh episodes as old
2755 if episode.age_in_days() < self.config.episode_old_age:
2756 continue
2758 # Do not delete played episodes (except if configured)
2759 if episode.is_played:
2760 if not self.config.auto_remove_played_episodes:
2761 continue
2763 # Do not delete unplayed episodes (except if configured)
2764 if not episode.is_played:
2765 if not self.config.auto_remove_unplayed_episodes:
2766 continue
2768 yield episode
2770 def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
2771 if not episodes:
2772 return False
2774 if skip_locked:
2775 episodes = [e for e in episodes if not e.is_locked]
2777 if not episodes:
2778 title = _('Episodes are locked')
2779 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2780 self.notification(message, title, widget=self.treeAvailable)
2781 return False
2783 count = len(episodes)
2784 title = N_('Delete %d episode?', 'Delete %d episodes?', count) % count
2785 message = _('Deleting episodes removes downloaded files.')
2787 if gpodder.ui.fremantle:
2788 message = '\n'.join([title, message])
2790 if confirm and not self.show_confirmation(message, title):
2791 return False
2793 progress = ProgressIndicator(_('Deleting episodes'), \
2794 _('Please wait while episodes are deleted'), \
2795 parent=self.get_dialog_parent())
2797 def finish_deletion(episode_urls, channel_urls):
2798 progress.on_finished()
2800 # Episodes have been deleted - persist the database
2801 self.db.commit()
2803 self.update_episode_list_icons(episode_urls)
2804 self.update_podcast_list_model(channel_urls)
2805 self.play_or_download()
2807 def thread_proc():
2808 episode_urls = set()
2809 channel_urls = set()
2811 episodes_status_update = []
2812 for idx, episode in enumerate(episodes):
2813 progress.on_progress(float(idx)/float(len(episodes)))
2814 if episode.is_locked:
2815 log('Not deleting episode (is locked): %s', episode.title)
2816 else:
2817 log('Deleting episode: %s', episode.title)
2818 progress.on_message(episode.title)
2819 episode.delete_from_disk()
2820 episode_urls.add(episode.url)
2821 channel_urls.add(episode.channel.url)
2822 episodes_status_update.append(episode)
2824 # Tell the shownotes window that we have removed the episode
2825 if self.episode_shownotes_window is not None and \
2826 self.episode_shownotes_window.episode is not None and \
2827 self.episode_shownotes_window.episode.url == episode.url:
2828 util.idle_add(self.episode_shownotes_window._download_status_changed, None)
2830 # Notify the web service about the status update + upload
2831 self.mygpo_client.on_delete(episodes_status_update)
2832 self.mygpo_client.flush()
2834 util.idle_add(finish_deletion, episode_urls, channel_urls)
2836 threading.Thread(target=thread_proc).start()
2838 return True
2840 def on_itemRemoveOldEpisodes_activate( self, widget):
2841 if gpodder.ui.maemo:
2842 columns = (
2843 ('maemo_remove_markup', None, None, _('Episode')),
2845 else:
2846 columns = (
2847 ('title_markup', None, None, _('Episode')),
2848 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2849 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2850 ('played_prop', None, None, _('Status')),
2851 ('age_prop', None, None, _('Downloaded')),
2854 msg_older_than = N_('Select older than %d day', 'Select older than %d days', self.config.episode_old_age)
2855 selection_buttons = {
2856 _('Select played'): lambda episode: episode.is_played,
2857 msg_older_than % self.config.episode_old_age: lambda episode: episode.age_in_days() > self.config.episode_old_age,
2860 instructions = _('Select the episodes you want to delete:')
2862 episodes = []
2863 selected = []
2864 for channel in self.channels:
2865 for episode in channel.get_downloaded_episodes():
2866 # Disallow deletion of locked episodes that still exist
2867 if not episode.is_locked or not episode.file_exists():
2868 episodes.append(episode)
2869 # Automatically select played and file-less episodes
2870 selected.append(episode.is_played or \
2871 not episode.file_exists())
2873 gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
2874 episodes = episodes, selected = selected, columns = columns, \
2875 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2876 selection_buttons = selection_buttons, _config=self.config, \
2877 show_episode_shownotes=self.show_episode_shownotes)
2879 def on_selected_episodes_status_changed(self):
2880 self.update_episode_list_icons(selected=True)
2881 self.update_podcast_list_model(selected=True)
2882 self.db.commit()
2884 def mark_selected_episodes_new(self):
2885 for episode in self.get_selected_episodes():
2886 episode.mark_new()
2887 self.on_selected_episodes_status_changed()
2889 def mark_selected_episodes_old(self):
2890 for episode in self.get_selected_episodes():
2891 episode.mark_old()
2892 self.on_selected_episodes_status_changed()
2894 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2895 for episode in self.get_selected_episodes():
2896 if toggle:
2897 episode.mark(is_played=not episode.is_played)
2898 else:
2899 episode.mark(is_played=new_value)
2900 self.on_selected_episodes_status_changed()
2902 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2903 for episode in self.get_selected_episodes():
2904 if toggle:
2905 episode.mark(is_locked=not episode.is_locked)
2906 else:
2907 episode.mark(is_locked=new_value)
2908 self.on_selected_episodes_status_changed()
2910 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2911 if self.active_channel is None:
2912 return
2914 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2915 self.active_channel.update_channel_lock()
2917 for episode in self.active_channel.get_all_episodes():
2918 episode.mark(is_locked=self.active_channel.channel_is_locked)
2920 self.update_podcast_list_model(selected=True)
2921 self.update_episode_list_icons(all=True)
2923 def on_itemUpdateChannel_activate(self, widget=None):
2924 if self.active_channel is None:
2925 title = _('No podcast selected')
2926 message = _('Please select a podcast in the podcasts list to update.')
2927 self.show_message( message, title, widget=self.treeChannels)
2928 return
2930 self.update_feed_cache(channels=[self.active_channel])
2932 def on_itemUpdate_activate(self, widget=None):
2933 # Check if we have outstanding subscribe/unsubscribe actions
2934 if self.on_add_remove_podcasts_mygpo():
2935 log('Update cancelled (received server changes)', sender=self)
2936 return
2938 if self.channels:
2939 self.update_feed_cache()
2940 else:
2941 gPodderWelcome(self.gPodder,
2942 center_on_widget=self.gPodder,
2943 show_example_podcasts_callback=self.on_itemImportChannels_activate,
2944 setup_my_gpodder_callback=self.on_mygpo_settings_activate)
2946 def download_episode_list_paused(self, episodes):
2947 self.download_episode_list(episodes, True)
2949 def download_episode_list(self, episodes, add_paused=False, force_start=False):
2950 for episode in episodes:
2951 log('Downloading episode: %s', episode.title, sender = self)
2952 if not episode.was_downloaded(and_exists=True):
2953 task_exists = False
2954 for task in self.download_tasks_seen:
2955 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2956 self.download_queue_manager.add_task(task, force_start)
2957 self.enable_download_list_update()
2958 task_exists = True
2959 continue
2961 if task_exists:
2962 continue
2964 try:
2965 task = download.DownloadTask(episode, self.config)
2966 except Exception, e:
2967 d = {'episode': episode.title, 'message': str(e)}
2968 message = _('Download error while downloading %(episode)s: %(message)s')
2969 self.show_message(message % d, _('Download error'), important=True)
2970 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
2971 continue
2973 if add_paused:
2974 task.status = task.PAUSED
2975 else:
2976 self.mygpo_client.on_download([task.episode])
2977 self.download_queue_manager.add_task(task, force_start)
2979 self.download_status_model.register_task(task)
2980 self.enable_download_list_update()
2982 # Flush updated episode status
2983 self.mygpo_client.flush()
2985 def cancel_task_list(self, tasks):
2986 if not tasks:
2987 return
2989 for task in tasks:
2990 if task.status in (task.QUEUED, task.DOWNLOADING):
2991 task.status = task.CANCELLED
2992 elif task.status == task.PAUSED:
2993 task.status = task.CANCELLED
2994 # Call run, so the partial file gets deleted
2995 task.run()
2997 self.update_episode_list_icons([task.url for task in tasks])
2998 self.play_or_download()
3000 # Update the tab title and downloads list
3001 self.update_downloads_list()
3003 def new_episodes_show(self, episodes, notification=False):
3004 if gpodder.ui.maemo:
3005 columns = (
3006 ('maemo_markup', None, None, _('Episode')),
3008 show_notification = notification
3009 else:
3010 columns = (
3011 ('title_markup', None, None, _('Episode')),
3012 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3013 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3015 show_notification = False
3017 instructions = _('Select the episodes you want to download:')
3019 if self.new_episodes_window is not None:
3020 self.new_episodes_window.main_window.destroy()
3021 self.new_episodes_window = None
3023 def download_episodes_callback(episodes):
3024 self.new_episodes_window = None
3025 self.download_episode_list(episodes)
3027 self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3028 title=_('New episodes available'), \
3029 instructions=instructions, \
3030 episodes=episodes, \
3031 columns=columns, \
3032 selected_default=True, \
3033 stock_ok_button = 'gpodder-download', \
3034 callback=download_episodes_callback, \
3035 remove_callback=lambda e: e.mark_old(), \
3036 remove_action=_('Mark as old'), \
3037 remove_finished=self.episode_new_status_changed, \
3038 _config=self.config, \
3039 show_notification=show_notification, \
3040 show_episode_shownotes=self.show_episode_shownotes)
3042 def on_itemDownloadAllNew_activate(self, widget, *args):
3043 if not self.offer_new_episodes():
3044 self.show_message(_('Please check for new episodes later.'), \
3045 _('No new episodes available'), widget=self.btnUpdateFeeds)
3047 def get_new_episodes(self, channels=None):
3048 if channels is None:
3049 channels = self.channels
3050 episodes = []
3051 for channel in channels:
3052 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3053 episodes.append(episode)
3055 return episodes
3057 def on_sync_to_ipod_activate(self, widget, episodes=None):
3058 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
3060 def commit_changes_to_database(self):
3061 """This will be called after the sync process is finished"""
3062 self.db.commit()
3064 def on_cleanup_ipod_activate(self, widget, *args):
3065 self.sync_ui.on_cleanup_device()
3067 def on_manage_device_playlist(self, widget):
3068 self.sync_ui.on_manage_device_playlist()
3070 def show_hide_tray_icon(self):
3071 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3072 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3073 elif not self.config.display_tray_icon and self.tray_icon is not None:
3074 self.tray_icon.set_visible(False)
3075 del self.tray_icon
3076 self.tray_icon = None
3078 if self.config.minimize_to_tray and self.tray_icon:
3079 self.tray_icon.set_visible(self.is_iconified())
3080 elif self.tray_icon:
3081 self.tray_icon.set_visible(True)
3083 def on_itemShowAllEpisodes_activate(self, widget):
3084 self.config.podcast_list_view_all = widget.get_active()
3086 def on_itemShowToolbar_activate(self, widget):
3087 self.config.show_toolbar = self.itemShowToolbar.get_active()
3089 def on_itemShowDescription_activate(self, widget):
3090 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3092 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3093 self.config.podcast_list_hide_boring = toggleaction.get_active()
3094 if self.config.podcast_list_hide_boring:
3095 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3096 else:
3097 self.podcast_list_model.set_view_mode(-1)
3099 def on_item_view_podcasts_changed(self, radioaction, current):
3100 # Only on Fremantle
3101 if current == self.item_view_podcasts_all:
3102 self.podcast_list_model.set_view_mode(-1)
3103 elif current == self.item_view_podcasts_downloaded:
3104 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3105 elif current == self.item_view_podcasts_unplayed:
3106 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3108 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3110 def on_item_view_episodes_changed(self, radioaction, current):
3111 if current == self.item_view_episodes_all:
3112 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_ALL)
3113 elif current == self.item_view_episodes_undeleted:
3114 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNDELETED)
3115 elif current == self.item_view_episodes_downloaded:
3116 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3117 elif current == self.item_view_episodes_unplayed:
3118 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3120 self.config.episode_list_view_mode = self.episode_list_model.get_view_mode()
3122 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3123 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3125 def update_item_device( self):
3126 if not gpodder.ui.fremantle:
3127 if self.config.device_type != 'none':
3128 self.itemDevice.set_visible(True)
3129 self.itemDevice.label = self.get_device_name()
3130 else:
3131 self.itemDevice.set_visible(False)
3133 def properties_closed( self):
3134 self.preferences_dialog = None
3135 self.show_hide_tray_icon()
3136 self.update_item_device()
3137 if gpodder.ui.maemo:
3138 selection = self.treeAvailable.get_selection()
3139 if self.config.maemo_enable_gestures or \
3140 self.config.enable_fingerscroll:
3141 selection.set_mode(gtk.SELECTION_SINGLE)
3142 else:
3143 selection.set_mode(gtk.SELECTION_MULTIPLE)
3145 def on_itemPreferences_activate(self, widget, *args):
3146 self.preferences_dialog = gPodderPreferences(self.main_window, \
3147 _config=self.config, \
3148 callback_finished=self.properties_closed, \
3149 user_apps_reader=self.user_apps_reader, \
3150 mygpo_login=self.on_mygpo_settings_activate, \
3151 parent_window=self.main_window, \
3152 mygpo_client=self.mygpo_client, \
3153 on_send_full_subscriptions=self.on_send_full_subscriptions)
3155 # Initial message to relayout window (in case it's opened in portrait mode
3156 self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3158 def on_itemDependencies_activate(self, widget):
3159 gPodderDependencyManager(self.gPodder)
3161 def on_goto_mygpo(self, widget):
3162 self.mygpo_client.open_website()
3164 def on_mygpo_settings_activate(self, action=None):
3165 settings = MygPodderSettings(self.main_window, \
3166 config=self.config, \
3167 mygpo_client=self.mygpo_client, \
3168 on_send_full_subscriptions=self.on_send_full_subscriptions)
3170 def on_itemAddChannel_activate(self, widget=None):
3171 gPodderAddPodcast(self.gPodder, \
3172 add_urls_callback=self.add_podcast_list)
3174 def on_itemEditChannel_activate(self, widget, *args):
3175 if self.active_channel is None:
3176 title = _('No podcast selected')
3177 message = _('Please select a podcast in the podcasts list to edit.')
3178 self.show_message( message, title, widget=self.treeChannels)
3179 return
3181 callback_closed = lambda: self.update_podcast_list_model(selected=True)
3182 gPodderChannel(self.main_window, \
3183 channel=self.active_channel, \
3184 callback_closed=callback_closed, \
3185 cover_downloader=self.cover_downloader)
3187 def on_itemMassUnsubscribe_activate(self, item=None):
3188 columns = (
3189 ('title', None, None, _('Podcast')),
3192 # We're abusing the Episode Selector for selecting Podcasts here,
3193 # but it works and looks good, so why not? -- thp
3194 gPodderEpisodeSelector(self.main_window, \
3195 title=_('Remove podcasts'), \
3196 instructions=_('Select the podcast you want to remove.'), \
3197 episodes=self.channels, \
3198 columns=columns, \
3199 size_attribute=None, \
3200 stock_ok_button=gtk.STOCK_DELETE, \
3201 callback=self.remove_podcast_list, \
3202 _config=self.config)
3204 def remove_podcast_list(self, channels, confirm=True):
3205 if not channels:
3206 log('No podcasts selected for deletion', sender=self)
3207 return
3209 if len(channels) == 1:
3210 title = _('Removing podcast')
3211 info = _('Please wait while the podcast is removed')
3212 message = _('Do you really want to remove this podcast and its episodes?')
3213 else:
3214 title = _('Removing podcasts')
3215 info = _('Please wait while the podcasts are removed')
3216 message = _('Do you really want to remove the selected podcasts and their episodes?')
3218 if confirm and not self.show_confirmation(message, title):
3219 return
3221 progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3223 def finish_deletion(select_url):
3224 # Upload subscription list changes to the web service
3225 self.mygpo_client.on_unsubscribe([c.url for c in channels])
3227 # Re-load the channels and select the desired new channel
3228 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3229 progress.on_finished()
3230 self.update_podcasts_tab()
3232 def thread_proc():
3233 select_url = None
3235 for idx, channel in enumerate(channels):
3236 # Update the UI for correct status messages
3237 progress.on_progress(float(idx)/float(len(channels)))
3238 progress.on_message(channel.title)
3240 # Delete downloaded episodes
3241 channel.remove_downloaded()
3243 # cancel any active downloads from this channel
3244 for episode in channel.get_all_episodes():
3245 util.idle_add(self.download_status_model.cancel_by_url,
3246 episode.url)
3248 if len(channels) == 1:
3249 # get the URL of the podcast we want to select next
3250 if channel in self.channels:
3251 position = self.channels.index(channel)
3252 else:
3253 position = -1
3255 if position == len(self.channels)-1:
3256 # this is the last podcast, so select the URL
3257 # of the item before this one (i.e. the "new last")
3258 select_url = self.channels[position-1].url
3259 else:
3260 # there is a podcast after the deleted one, so
3261 # we simply select the one that comes after it
3262 select_url = self.channels[position+1].url
3264 # Remove the channel and clean the database entries
3265 channel.delete()
3266 self.channels.remove(channel)
3268 # Clean up downloads and download directories
3269 self.clean_up_downloads()
3271 self.channel_list_changed = True
3272 self.save_channels_opml()
3274 # The remaining stuff is to be done in the GTK main thread
3275 util.idle_add(finish_deletion, select_url)
3277 threading.Thread(target=thread_proc).start()
3279 def on_itemRemoveChannel_activate(self, widget, *args):
3280 if self.active_channel is None:
3281 title = _('No podcast selected')
3282 message = _('Please select a podcast in the podcasts list to remove.')
3283 self.show_message( message, title, widget=self.treeChannels)
3284 return
3286 self.remove_podcast_list([self.active_channel])
3288 def get_opml_filter(self):
3289 filter = gtk.FileFilter()
3290 filter.add_pattern('*.opml')
3291 filter.add_pattern('*.xml')
3292 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3293 return filter
3295 def on_item_import_from_file_activate(self, widget, filename=None):
3296 if filename is None:
3297 if gpodder.ui.desktop or gpodder.ui.fremantle:
3298 # FIXME: Hildonization on Fremantle
3299 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3300 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3301 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3302 elif gpodder.ui.diablo:
3303 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3304 dlg.set_filter(self.get_opml_filter())
3305 response = dlg.run()
3306 filename = None
3307 if response == gtk.RESPONSE_OK:
3308 filename = dlg.get_filename()
3309 dlg.destroy()
3311 if filename is not None:
3312 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3313 custom_title=_('Import podcasts from OPML file'), \
3314 add_urls_callback=self.add_podcast_list, \
3315 hide_url_entry=True)
3316 dir.download_opml_file(filename)
3318 def on_itemExportChannels_activate(self, widget, *args):
3319 if not self.channels:
3320 title = _('Nothing to export')
3321 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3322 self.show_message(message, title, widget=self.treeChannels)
3323 return
3325 if gpodder.ui.desktop or gpodder.ui.fremantle:
3326 # FIXME: Hildonization on Fremantle
3327 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3328 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3329 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3330 elif gpodder.ui.diablo:
3331 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3332 dlg.set_filter(self.get_opml_filter())
3333 response = dlg.run()
3334 if response == gtk.RESPONSE_OK:
3335 filename = dlg.get_filename()
3336 dlg.destroy()
3337 exporter = opml.Exporter( filename)
3338 if exporter.write(self.channels):
3339 count = len(self.channels)
3340 title = N_('%d subscription exported', '%d subscriptions exported', count) % count
3341 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3342 else:
3343 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3344 else:
3345 dlg.destroy()
3347 def on_itemImportChannels_activate(self, widget, *args):
3348 if gpodder.ui.fremantle:
3349 gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3350 self.config.toplist_url, \
3351 self.config.opml_url, \
3352 self.add_podcast_list, \
3353 self.on_itemAddChannel_activate, \
3354 self.on_mygpo_settings_activate, \
3355 self.show_text_edit_dialog)
3356 else:
3357 dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3358 add_urls_callback=self.add_podcast_list)
3359 util.idle_add(dir.download_opml_file, self.config.opml_url)
3361 def on_homepage_activate(self, widget, *args):
3362 util.open_website(gpodder.__url__)
3364 def on_wiki_activate(self, widget, *args):
3365 util.open_website('http://gpodder.org/wiki/User_Manual')
3367 def on_bug_tracker_activate(self, widget, *args):
3368 if gpodder.ui.maemo:
3369 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3370 else:
3371 util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3373 def on_item_support_activate(self, widget):
3374 util.open_website('http://gpodder.org/donate')
3376 def on_itemAbout_activate(self, widget, *args):
3377 if gpodder.ui.fremantle:
3378 from gpodder.gtkui.frmntl.about import HeAboutDialog
3379 HeAboutDialog.present(self.main_window,
3380 'gPodder',
3381 'gpodder',
3382 gpodder.__version__,
3383 _('A podcast client with focus on usability'),
3384 gpodder.__copyright__,
3385 gpodder.__url__,
3386 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3387 'http://gpodder.org/donate')
3388 return
3390 dlg = gtk.AboutDialog()
3391 dlg.set_transient_for(self.main_window)
3392 dlg.set_name('gPodder')
3393 dlg.set_version(gpodder.__version__)
3394 dlg.set_copyright(gpodder.__copyright__)
3395 dlg.set_comments(_('A podcast client with focus on usability'))
3396 dlg.set_website(gpodder.__url__)
3397 dlg.set_translator_credits( _('translator-credits'))
3398 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3400 if gpodder.ui.desktop:
3401 # For the "GUI" version, we add some more
3402 # items to the about dialog (credits and logo)
3403 app_authors = [
3404 _('Maintainer:'),
3405 'Thomas Perl <thpinfo.com>',
3408 if os.path.exists(gpodder.credits_file):
3409 credits = open(gpodder.credits_file).read().strip().split('\n')
3410 app_authors += ['', _('Patches, bug reports and donations by:')]
3411 app_authors += credits
3413 dlg.set_authors(app_authors)
3414 try:
3415 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3416 except:
3417 dlg.set_logo_icon_name('gpodder')
3419 dlg.run()
3421 def on_wNotebook_switch_page(self, widget, *args):
3422 page_num = args[1]
3423 if gpodder.ui.maemo:
3424 self.tool_downloads.set_active(page_num == 1)
3425 page = self.wNotebook.get_nth_page(page_num)
3426 tab_label = self.wNotebook.get_tab_label(page).get_text()
3427 if page_num == 0 and self.active_channel is not None:
3428 self.set_title(self.active_channel.title)
3429 else:
3430 self.set_title(tab_label)
3431 if page_num == 0:
3432 self.play_or_download()
3433 self.menuChannels.set_sensitive(True)
3434 self.menuSubscriptions.set_sensitive(True)
3435 # The message area in the downloads tab should be hidden
3436 # when the user switches away from the downloads tab
3437 if self.message_area is not None:
3438 self.message_area.hide()
3439 self.message_area = None
3440 else:
3441 # Remove finished episodes
3442 if self.config.auto_cleanup_downloads:
3443 self.on_btnCleanUpDownloads_clicked()
3445 self.menuChannels.set_sensitive(False)
3446 self.menuSubscriptions.set_sensitive(False)
3447 if gpodder.ui.desktop:
3448 self.toolDownload.set_sensitive(False)
3449 self.toolPlay.set_sensitive(False)
3450 self.toolTransfer.set_sensitive(False)
3451 self.toolCancel.set_sensitive(False)
3453 def on_treeChannels_row_activated(self, widget, path, *args):
3454 # double-click action of the podcast list or enter
3455 self.treeChannels.set_cursor(path)
3457 def on_treeChannels_cursor_changed(self, widget, *args):
3458 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3460 if model is not None and iter is not None:
3461 old_active_channel = self.active_channel
3462 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3464 if self.active_channel == old_active_channel:
3465 return
3467 if gpodder.ui.maemo:
3468 self.set_title(self.active_channel.title)
3469 self.itemEditChannel.set_visible(True)
3470 self.itemRemoveChannel.set_visible(True)
3471 else:
3472 self.active_channel = None
3473 self.itemEditChannel.set_visible(False)
3474 self.itemRemoveChannel.set_visible(False)
3476 self.update_episode_list_model()
3478 def on_btnEditChannel_clicked(self, widget, *args):
3479 self.on_itemEditChannel_activate( widget, args)
3481 def get_podcast_urls_from_selected_episodes(self):
3482 """Get a set of podcast URLs based on the selected episodes"""
3483 return set(episode.channel.url for episode in \
3484 self.get_selected_episodes())
3486 def get_selected_episodes(self):
3487 """Get a list of selected episodes from treeAvailable"""
3488 selection = self.treeAvailable.get_selection()
3489 model, paths = selection.get_selected_rows()
3491 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3492 return episodes
3494 def on_transfer_selected_episodes(self, widget):
3495 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3497 def on_playback_selected_episodes(self, widget):
3498 self.playback_episodes(self.get_selected_episodes())
3500 def on_shownotes_selected_episodes(self, widget):
3501 episodes = self.get_selected_episodes()
3502 if episodes:
3503 episode = episodes.pop(0)
3504 self.show_episode_shownotes(episode)
3505 else:
3506 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3508 def on_download_selected_episodes(self, widget):
3509 episodes = self.get_selected_episodes()
3510 self.download_episode_list(episodes)
3511 self.update_episode_list_icons([episode.url for episode in episodes])
3512 self.play_or_download()
3514 def on_treeAvailable_row_activated(self, widget, path, view_column):
3515 """Double-click/enter action handler for treeAvailable"""
3516 # We should only have one one selected as it was double clicked!
3517 e = self.get_selected_episodes()[0]
3519 if (self.config.double_click_episode_action == 'download'):
3520 # If the episode has already been downloaded and exists then play it
3521 if e.was_downloaded(and_exists=True):
3522 self.playback_episodes(self.get_selected_episodes())
3523 # else download it if it is not already downloading
3524 elif not self.episode_is_downloading(e):
3525 self.download_episode_list([e])
3526 self.update_episode_list_icons([e.url])
3527 self.play_or_download()
3528 elif (self.config.double_click_episode_action == 'stream'):
3529 # If we happen to have downloaded this episode simple play it
3530 if e.was_downloaded(and_exists=True):
3531 self.playback_episodes(self.get_selected_episodes())
3532 # else if streaming is possible stream it
3533 elif self.streaming_possible():
3534 self.playback_episodes(self.get_selected_episodes())
3535 else:
3536 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3537 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3538 else:
3539 # default action is to display show notes
3540 self.on_shownotes_selected_episodes(widget)
3542 def show_episode_shownotes(self, episode):
3543 if self.episode_shownotes_window is None:
3544 log('First-time use of episode window --- creating', sender=self)
3545 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
3546 _download_episode_list=self.download_episode_list, \
3547 _playback_episodes=self.playback_episodes, \
3548 _delete_episode_list=self.delete_episode_list, \
3549 _episode_list_status_changed=self.episode_list_status_changed, \
3550 _cancel_task_list=self.cancel_task_list, \
3551 _episode_is_downloading=self.episode_is_downloading, \
3552 _streaming_possible=self.streaming_possible())
3553 self.episode_shownotes_window.show(episode)
3554 if self.episode_is_downloading(episode):
3555 self.update_downloads_list()
3557 def restart_auto_update_timer(self):
3558 if self._auto_update_timer_source_id is not None:
3559 log('Removing existing auto update timer.', sender=self)
3560 gobject.source_remove(self._auto_update_timer_source_id)
3561 self._auto_update_timer_source_id = None
3563 if self.config.auto_update_feeds and \
3564 self.config.auto_update_frequency:
3565 interval = 60*1000*self.config.auto_update_frequency
3566 log('Setting up auto update timer with interval %d.', \
3567 self.config.auto_update_frequency, sender=self)
3568 self._auto_update_timer_source_id = gobject.timeout_add(\
3569 interval, self._on_auto_update_timer)
3571 def _on_auto_update_timer(self):
3572 log('Auto update timer fired.', sender=self)
3573 self.update_feed_cache(force_update=True)
3575 # Ask web service for sub changes (if enabled)
3576 self.mygpo_client.flush()
3578 return True
3580 def on_treeDownloads_row_activated(self, widget, *args):
3581 # Use the standard way of working on the treeview
3582 selection = self.treeDownloads.get_selection()
3583 (model, paths) = selection.get_selected_rows()
3584 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
3586 for tree_row_reference, task in selected_tasks:
3587 if task.status in (task.DOWNLOADING, task.QUEUED):
3588 task.status = task.PAUSED
3589 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
3590 self.download_queue_manager.add_task(task)
3591 self.enable_download_list_update()
3592 elif task.status == task.DONE:
3593 model.remove(model.get_iter(tree_row_reference.get_path()))
3595 self.play_or_download()
3597 # Update the tab title and downloads list
3598 self.update_downloads_list()
3600 def on_item_cancel_download_activate(self, widget):
3601 if self.wNotebook.get_current_page() == 0:
3602 selection = self.treeAvailable.get_selection()
3603 (model, paths) = selection.get_selected_rows()
3604 urls = [model.get_value(model.get_iter(path), \
3605 self.episode_list_model.C_URL) for path in paths]
3606 selected_tasks = [task for task in self.download_tasks_seen \
3607 if task.url in urls]
3608 else:
3609 selection = self.treeDownloads.get_selection()
3610 (model, paths) = selection.get_selected_rows()
3611 selected_tasks = [model.get_value(model.get_iter(path), \
3612 self.download_status_model.C_TASK) for path in paths]
3613 self.cancel_task_list(selected_tasks)
3615 def on_btnCancelAll_clicked(self, widget, *args):
3616 self.cancel_task_list(self.download_tasks_seen)
3618 def on_btnDownloadedDelete_clicked(self, widget, *args):
3619 episodes = self.get_selected_episodes()
3620 if len(episodes) == 1:
3621 self.delete_episode_list(episodes, skip_locked=False)
3622 else:
3623 self.delete_episode_list(episodes)
3625 def on_key_press(self, widget, event):
3626 # Allow tab switching with Ctrl + PgUp/PgDown
3627 if event.state & gtk.gdk.CONTROL_MASK:
3628 if event.keyval == gtk.keysyms.Page_Up:
3629 self.wNotebook.prev_page()
3630 return True
3631 elif event.keyval == gtk.keysyms.Page_Down:
3632 self.wNotebook.next_page()
3633 return True
3635 # After this code we only handle Maemo hardware keys,
3636 # so if we are not a Maemo app, we don't do anything
3637 if not gpodder.ui.maemo:
3638 return False
3640 diff = 0
3641 if event.keyval == gtk.keysyms.F7: #plus
3642 diff = 1
3643 elif event.keyval == gtk.keysyms.F8: #minus
3644 diff = -1
3646 if diff != 0 and not self.currently_updating:
3647 selection = self.treeChannels.get_selection()
3648 (model, iter) = selection.get_selected()
3649 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
3650 selection.select_path(new_path)
3651 self.treeChannels.set_cursor(new_path)
3652 return True
3654 return False
3656 def on_iconify(self):
3657 if self.tray_icon:
3658 self.gPodder.set_skip_taskbar_hint(True)
3659 if self.config.minimize_to_tray:
3660 self.tray_icon.set_visible(True)
3661 else:
3662 self.gPodder.set_skip_taskbar_hint(False)
3664 def on_uniconify(self):
3665 if self.tray_icon:
3666 self.gPodder.set_skip_taskbar_hint(False)
3667 if self.config.minimize_to_tray:
3668 self.tray_icon.set_visible(False)
3669 else:
3670 self.gPodder.set_skip_taskbar_hint(False)
3672 def uniconify_main_window(self):
3673 if self.is_iconified():
3674 self.gPodder.present()
3676 def iconify_main_window(self):
3677 if not self.is_iconified():
3678 self.gPodder.iconify()
3680 def update_podcasts_tab(self):
3681 if len(self.channels):
3682 if gpodder.ui.fremantle:
3683 self.button_refresh.set_title(_('Check for new episodes'))
3684 self.button_refresh.show()
3685 else:
3686 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3687 else:
3688 if gpodder.ui.fremantle:
3689 self.button_refresh.hide()
3690 else:
3691 self.label2.set_text(_('Podcasts'))
3693 @dbus.service.method(gpodder.dbus_interface)
3694 def show_gui_window(self):
3695 self.gPodder.present()
3697 @dbus.service.method(gpodder.dbus_interface)
3698 def subscribe_to_url(self, url):
3699 gPodderAddPodcast(self.gPodder,
3700 add_urls_callback=self.add_podcast_list,
3701 preset_url=url)
3703 @dbus.service.method(gpodder.dbus_interface)
3704 def mark_episode_played(self, filename):
3705 if filename is None:
3706 return False
3708 for channel in self.channels:
3709 for episode in channel.get_all_episodes():
3710 fn = episode.local_filename(create=False, check_only=True)
3711 if fn == filename:
3712 episode.mark(is_played=True)
3713 self.db.commit()
3714 self.update_episode_list_icons([episode.url])
3715 self.update_podcast_list_model([episode.channel.url])
3716 return True
3718 return False
3721 def main(options=None):
3722 gobject.threads_init()
3723 gobject.set_application_name('gPodder')
3725 if gpodder.ui.maemo:
3726 # Try to enable the custom icon theme for gPodder on Maemo
3727 settings = gtk.settings_get_default()
3728 settings.set_string_property('gtk-icon-theme-name', \
3729 'gpodder', __file__)
3730 # Extend the search path for the optified icon theme (Maemo 5)
3731 icon_theme = gtk.icon_theme_get_default()
3732 icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
3734 gtk.window_set_default_icon_name('gpodder')
3735 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
3737 try:
3738 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
3739 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
3740 except dbus.exceptions.DBusException, dbe:
3741 log('Warning: Cannot get "on the bus".', traceback=True)
3742 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
3743 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
3744 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
3745 dlg.set_title('gPodder')
3746 dlg.run()
3747 dlg.destroy()
3748 sys.exit(0)
3750 util.make_directory(gpodder.home)
3751 gpodder.load_plugins()
3753 config = UIConfig(gpodder.config_file)
3755 if gpodder.ui.diablo:
3756 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3757 # folder exists there (allow moving "gpodder" between SD cards or USB)
3758 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3759 if not os.path.exists(config.download_dir):
3760 log('Downloads might have been moved. Trying to locate them...')
3761 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
3762 dir = os.path.join(basedir, 'gpodder')
3763 if os.path.exists(dir):
3764 log('Downloads found in: %s', dir)
3765 config.download_dir = dir
3766 break
3767 else:
3768 log('Downloads NOT FOUND in %s', dir)
3770 if config.enable_fingerscroll:
3771 BuilderWidget.use_fingerscroll = True
3772 elif gpodder.ui.fremantle:
3773 config.on_quit_ask = False
3774 config.feed_update_skipping = False
3776 config.mygpo_device_type = util.detect_device_type()
3778 gp = gPodder(bus_name, config)
3780 # Handle options
3781 if options.subscribe:
3782 util.idle_add(gp.subscribe_to_url, options.subscribe)
3784 # mac OS X stuff :
3785 # handle "subscribe to podcast" events from firefox
3786 if platform.system() == 'Darwin':
3787 from gpodder import gpodderosx
3788 gpodderosx.register_handlers(gp)
3789 # end mac OS X stuff
3791 gp.run()