Fix main menu items for "All episodes" view
[gpodder.git] / src / gpodder / gui.py
blob80ea264617f63803cc624edd0f572d1fe6aafa2c
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import os
21 import platform
22 import gtk
23 import gtk.gdk
24 import gobject
25 import pango
26 import sys
27 import shutil
28 import subprocess
29 import glob
30 import time
31 import tempfile
32 import collections
33 import threading
35 from xml.sax import saxutils
37 import gpodder
39 try:
40 import dbus
41 import dbus.service
42 import dbus.mainloop
43 import dbus.glib
44 except ImportError:
45 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
46 class dbus:
47 class SessionBus:
48 def __init__(self, *args, **kwargs):
49 pass
50 class glib:
51 class DBusGMainLoop:
52 pass
53 class service:
54 @staticmethod
55 def method(*args, **kwargs):
56 return lambda x: x
57 class BusName:
58 def __init__(self, *args, **kwargs):
59 pass
60 class Object:
61 def __init__(self, *args, **kwargs):
62 pass
65 from gpodder import feedcore
66 from gpodder import util
67 from gpodder import opml
68 from gpodder import download
69 from gpodder import my
70 from gpodder import youtube
71 from gpodder import player
72 from gpodder.liblogger import log
74 _ = gpodder.gettext
75 N_ = gpodder.ngettext
77 from gpodder.model import PodcastChannel
78 from gpodder.model import PodcastEpisode
79 from gpodder.dbsqlite import Database
81 from gpodder.gtkui.model import PodcastListModel
82 from gpodder.gtkui.model import EpisodeListModel
83 from gpodder.gtkui.config import UIConfig
84 from gpodder.gtkui.services import CoverDownloader
85 from gpodder.gtkui.widgets import SimpleMessageArea
86 from gpodder.gtkui.desktopfile import UserAppsReader
88 from gpodder.gtkui.draw import draw_text_box_centered
90 from gpodder.gtkui.interface.common import BuilderWidget
91 from gpodder.gtkui.interface.common import TreeViewHelper
92 from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
94 if gpodder.ui.desktop:
95 from gpodder.gtkui.download import DownloadStatusModel
97 from gpodder.gtkui.desktop.sync import gPodderSyncUI
99 from gpodder.gtkui.desktop.channel import gPodderChannel
100 from gpodder.gtkui.desktop.preferences import gPodderPreferences
101 from gpodder.gtkui.desktop.shownotes import gPodderShownotes
102 from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
103 from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
104 from gpodder.gtkui.desktop.dependencymanager import gPodderDependencyManager
105 try:
106 from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
107 have_trayicon = True
108 except Exception, exc:
109 log('Warning: Could not import gpodder.trayicon.', traceback=True)
110 log('Warning: This probably means your PyGTK installation is too old!')
111 have_trayicon = False
112 elif gpodder.ui.diablo:
113 from gpodder.gtkui.download import DownloadStatusModel
115 from gpodder.gtkui.maemo.channel import gPodderChannel
116 from gpodder.gtkui.maemo.preferences import gPodderPreferences
117 from gpodder.gtkui.maemo.shownotes import gPodderShownotes
118 from gpodder.gtkui.maemo.episodeselector import gPodderEpisodeSelector
119 from gpodder.gtkui.maemo.podcastdirectory import gPodderPodcastDirectory
120 from gpodder.gtkui.maemo.mygpodder import MygPodderSettings
121 have_trayicon = False
122 elif gpodder.ui.fremantle:
123 from gpodder.gtkui.frmntl.model import DownloadStatusModel
124 from gpodder.gtkui.frmntl.model import EpisodeListModel
125 from gpodder.gtkui.frmntl.model import PodcastListModel
127 from gpodder.gtkui.maemo.channel import gPodderChannel
128 from gpodder.gtkui.frmntl.preferences import gPodderPreferences
129 from gpodder.gtkui.frmntl.shownotes import gPodderShownotes
130 from gpodder.gtkui.frmntl.episodeselector import gPodderEpisodeSelector
131 from gpodder.gtkui.frmntl.podcastdirectory import gPodderPodcastDirectory
132 from gpodder.gtkui.frmntl.episodes import gPodderEpisodes
133 from gpodder.gtkui.frmntl.downloads import gPodderDownloads
134 have_trayicon = False
136 from gpodder.gtkui.frmntl.portrait import FremantleRotation
138 from gpodder.gtkui.interface.common import Orientation
140 from gpodder.gtkui.interface.welcome import gPodderWelcome
141 from gpodder.gtkui.interface.progress import ProgressIndicator
143 if gpodder.ui.maemo:
144 import hildon
146 from gpodder.dbusproxy import DBusPodcastsProxy
148 class gPodder(BuilderWidget, dbus.service.Object):
149 finger_friendly_widgets = ['btnCleanUpDownloads', 'button_search_episodes_clear']
151 ICON_GENERAL_ADD = 'general_add'
152 ICON_GENERAL_REFRESH = 'general_refresh'
153 ICON_GENERAL_CLOSE = 'general_close'
155 def __init__(self, bus_name, config):
156 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
157 self.podcasts_proxy = DBusPodcastsProxy(lambda: self.channels, \
158 self.on_itemUpdate_activate, \
159 self.playback_episodes, \
160 self.download_episode_list, \
161 bus_name)
162 self.db = Database(gpodder.database_file)
163 self.config = config
164 BuilderWidget.__init__(self, None)
166 def new(self):
167 if gpodder.ui.diablo:
168 import hildon
169 self.app = hildon.Program()
170 self.app.add_window(self.main_window)
171 self.main_window.add_toolbar(self.toolbar)
172 menu = gtk.Menu()
173 for child in self.main_menu.get_children():
174 child.reparent(menu)
175 self.main_window.set_menu(self.set_finger_friendly(menu))
176 self.bluetooth_available = False
177 self._last_orientation = Orientation.LANDSCAPE
178 elif gpodder.ui.fremantle:
179 import hildon
180 self.app = hildon.Program()
181 self.app.add_window(self.main_window)
183 appmenu = hildon.AppMenu()
185 for filter in (self.item_view_podcasts_all, \
186 self.item_view_podcasts_downloaded, \
187 self.item_view_podcasts_unplayed):
188 button = gtk.ToggleButton()
189 filter.connect_proxy(button)
190 appmenu.add_filter(button)
192 for action in (self.itemPreferences, \
193 self.item_downloads, \
194 self.itemRemoveOldEpisodes, \
195 self.item_unsubscribe, \
196 self.itemAbout):
197 button = hildon.Button(gtk.HILDON_SIZE_AUTO,\
198 hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
199 action.connect_proxy(button)
200 if action == self.item_downloads:
201 button.set_title(_('Downloads'))
202 button.set_value(_('Idle'))
203 self.button_downloads = button
204 appmenu.append(button)
205 appmenu.show_all()
206 self.main_window.set_app_menu(appmenu)
208 # Initialize portrait mode / rotation manager
209 self._fremantle_rotation = FremantleRotation('gPodder', \
210 self.main_window, \
211 gpodder.__version__, \
212 self.config.rotation_mode)
214 if self.config.rotation_mode == FremantleRotation.ALWAYS:
215 util.idle_add(self.on_window_orientation_changed, \
216 Orientation.PORTRAIT)
217 self._last_orientation = Orientation.PORTRAIT
218 else:
219 self._last_orientation = Orientation.LANDSCAPE
221 self.bluetooth_available = False
222 else:
223 self._last_orientation = Orientation.LANDSCAPE
224 self.bluetooth_available = util.bluetooth_available()
225 self.toolbar.set_property('visible', self.config.show_toolbar)
227 self.config.connect_gtk_window(self.gPodder, 'main_window')
228 if not gpodder.ui.fremantle:
229 self.config.connect_gtk_paned('paned_position', self.channelPaned)
230 self.main_window.show()
232 self.player_receiver = player.MediaPlayerDBusReceiver(self.on_played)
234 self.gPodder.connect('key-press-event', self.on_key_press)
236 self.preferences_dialog = None
237 self.config.add_observer(self.on_config_changed)
239 self.tray_icon = None
240 self.episode_shownotes_window = None
241 self.new_episodes_window = None
243 if gpodder.ui.desktop:
244 # Mac OS X-specific UI tweaks: Native main menu integration
245 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
246 if getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
247 try:
248 import igemacintegration as igemi
250 # Move the menu bar from the window to the Mac menu bar
251 self.mainMenu.hide()
252 igemi.ige_mac_menu_set_menu_bar(self.mainMenu)
254 # Reparent some items to the "Application" menu
255 for widget in ('/mainMenu/menuHelp/itemAbout', \
256 '/mainMenu/menuPodcasts/itemPreferences'):
257 item = self.uimanager1.get_widget(widget)
258 group = igemi.ige_mac_menu_add_app_menu_group()
259 igemi.ige_mac_menu_add_app_menu_item(group, item, None)
261 quit_widget = '/mainMenu/menuPodcasts/itemQuit'
262 quit_item = self.uimanager1.get_widget(quit_widget)
263 igemi.ige_mac_menu_set_quit_menu_item(quit_item)
264 except ImportError:
265 print >>sys.stderr, """
266 Warning: ige-mac-integration not found - no native menus.
269 self.sync_ui = gPodderSyncUI(self.config, self.notification, \
270 self.main_window, self.show_confirmation, \
271 self.update_episode_list_icons, \
272 self.update_podcast_list_model, self.toolPreferences, \
273 gPodderEpisodeSelector, \
274 self.commit_changes_to_database)
275 else:
276 self.sync_ui = None
278 self.download_status_model = DownloadStatusModel()
279 self.download_queue_manager = download.DownloadQueueManager(self.config)
281 if gpodder.ui.desktop:
282 self.show_hide_tray_icon()
283 self.itemShowAllEpisodes.set_active(self.config.podcast_list_view_all)
284 self.itemShowToolbar.set_active(self.config.show_toolbar)
285 self.itemShowDescription.set_active(self.config.episode_list_descriptions)
287 if not gpodder.ui.fremantle:
288 self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
289 self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
290 self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
291 self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
293 # When the amount of maximum downloads changes, notify the queue manager
294 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_threads()
295 self.spinMaxDownloads.connect('value-changed', changed_cb)
297 self.default_title = 'gPodder'
298 if gpodder.__version__.rfind('git') != -1:
299 self.set_title('gPodder %s' % gpodder.__version__)
300 else:
301 title = self.gPodder.get_title()
302 if title is not None:
303 self.set_title(title)
304 else:
305 self.set_title(_('gPodder'))
307 self.cover_downloader = CoverDownloader()
309 # Generate list models for podcasts and their episodes
310 self.podcast_list_model = PodcastListModel(self.cover_downloader)
312 self.cover_downloader.register('cover-available', self.cover_download_finished)
313 self.cover_downloader.register('cover-removed', self.cover_file_removed)
315 if gpodder.ui.fremantle:
316 # Work around Maemo bug #4718
317 self.button_refresh.set_name('HildonButton-finger')
318 self.button_subscribe.set_name('HildonButton-finger')
320 self.button_refresh.set_sensitive(False)
321 self.button_subscribe.set_sensitive(False)
323 self.button_subscribe.set_image(gtk.image_new_from_icon_name(\
324 self.ICON_GENERAL_ADD, gtk.ICON_SIZE_BUTTON))
325 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
326 self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
328 # Make the button scroll together with the TreeView contents
329 action_area_box = self.treeChannels.get_action_area_box()
330 for child in self.buttonbox:
331 child.reparent(action_area_box)
332 self.vbox.remove(self.buttonbox)
333 action_area_box.set_spacing(2)
334 action_area_box.set_border_width(3)
335 self.treeChannels.set_action_area_visible(True)
337 from gpodder.gtkui.frmntl import style
338 sub_font = style.get_font_desc('SmallSystemFont')
339 sub_color = style.get_color('SecondaryTextColor')
340 sub = (sub_font.to_string(), sub_color.to_string())
341 sub = '<span font_desc="%s" foreground="%s">%%s</span>' % sub
342 self.label_footer.set_markup(sub % gpodder.__copyright__)
344 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
345 while gtk.events_pending():
346 gtk.main_iteration(False)
348 try:
349 # Try to get the real package version from dpkg
350 p = subprocess.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout=subprocess.PIPE)
351 version, _stderr = p.communicate()
352 del _stderr
353 del p
354 except:
355 version = gpodder.__version__
356 self.label_footer.set_markup(sub % ('v %s' % version))
357 self.label_footer.hide()
359 self.episodes_window = gPodderEpisodes(self.main_window, \
360 on_treeview_expose_event=self.on_treeview_expose_event, \
361 show_episode_shownotes=self.show_episode_shownotes, \
362 update_podcast_list_model=self.update_podcast_list_model, \
363 on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
364 item_view_episodes_all=self.item_view_episodes_all, \
365 item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
366 item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
367 item_view_episodes_undeleted=self.item_view_episodes_undeleted, \
368 on_entry_search_episodes_changed=self.on_entry_search_episodes_changed, \
369 on_entry_search_episodes_key_press=self.on_entry_search_episodes_key_press, \
370 hide_episode_search=self.hide_episode_search, \
371 on_itemUpdateChannel_activate=self.on_itemUpdateChannel_activate, \
372 playback_episodes=self.playback_episodes, \
373 delete_episode_list=self.delete_episode_list, \
374 episode_list_status_changed=self.episode_list_status_changed, \
375 download_episode_list=self.download_episode_list, \
376 episode_is_downloading=self.episode_is_downloading, \
377 show_episode_in_download_manager=self.show_episode_in_download_manager, \
378 add_download_task_monitor=self.add_download_task_monitor, \
379 remove_download_task_monitor=self.remove_download_task_monitor, \
380 for_each_episode_set_task_status=self.for_each_episode_set_task_status, \
381 on_delete_episodes_button_clicked=self.on_itemRemoveOldEpisodes_activate, \
382 on_itemUpdate_activate=self.on_itemUpdate_activate)
384 # Expose objects for episode list type-ahead find
385 self.hbox_search_episodes = self.episodes_window.hbox_search_episodes
386 self.entry_search_episodes = self.episodes_window.entry_search_episodes
387 self.button_search_episodes_clear = self.episodes_window.button_search_episodes_clear
389 self.downloads_window = gPodderDownloads(self.main_window, \
390 on_treeview_expose_event=self.on_treeview_expose_event, \
391 on_btnCleanUpDownloads_clicked=self.on_btnCleanUpDownloads_clicked, \
392 _for_each_task_set_status=self._for_each_task_set_status, \
393 downloads_list_get_selection=self.downloads_list_get_selection, \
394 _config=self.config)
396 self.treeAvailable = self.episodes_window.treeview
397 self.treeDownloads = self.downloads_window.treeview
399 # Init the treeviews that we use
400 self.init_podcast_list_treeview()
401 self.init_episode_list_treeview()
402 self.init_download_list_treeview()
404 if self.config.podcast_list_hide_boring:
405 self.item_view_hide_boring_podcasts.set_active(True)
407 self.currently_updating = False
409 if gpodder.ui.maemo:
410 self.context_menu_mouse_button = 1
411 else:
412 self.context_menu_mouse_button = 3
414 if self.config.start_iconified:
415 self.iconify_main_window()
417 self.download_tasks_seen = set()
418 self.download_list_update_enabled = False
419 self.last_download_count = 0
420 self.download_task_monitors = set()
422 # Subscribed channels
423 self.active_channel = None
424 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
425 self.channel_list_changed = True
426 self.update_podcasts_tab()
428 # load list of user applications for audio playback
429 self.user_apps_reader = UserAppsReader(['audio', 'video'])
430 threading.Thread(target=self.user_apps_reader.read).start()
432 # Set the "Device" menu item for the first time
433 if gpodder.ui.desktop:
434 self.update_item_device()
436 # Set up the first instance of MygPoClient
437 self.mygpo_client = my.MygPoClient(self.config)
439 # Now, update the feed cache, when everything's in place
440 if not gpodder.ui.fremantle:
441 self.btnUpdateFeeds.show()
442 self.updating_feed_cache = False
443 self.feed_cache_update_cancelled = False
444 self.update_feed_cache(force_update=self.config.update_on_startup)
446 self.message_area = None
448 def find_partial_downloads():
449 # Look for partial file downloads
450 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
451 count = len(partial_files)
452 resumable_episodes = []
453 if count:
454 if not gpodder.ui.fremantle:
455 util.idle_add(self.wNotebook.set_current_page, 1)
456 indicator = ProgressIndicator(_('Loading incomplete downloads'), \
457 _('Some episodes have not finished downloading in a previous session.'), \
458 False, self.get_dialog_parent())
459 indicator.on_message(N_('%d partial file', '%d partial files', count) % count)
461 candidates = [f[:-len('.partial')] for f in partial_files]
462 found = 0
464 for c in self.channels:
465 for e in c.get_all_episodes():
466 filename = e.local_filename(create=False, check_only=True)
467 if filename in candidates:
468 log('Found episode: %s', e.title, sender=self)
469 found += 1
470 indicator.on_message(e.title)
471 indicator.on_progress(float(found)/count)
472 candidates.remove(filename)
473 partial_files.remove(filename+'.partial')
474 resumable_episodes.append(e)
476 if not candidates:
477 break
479 if not candidates:
480 break
482 for f in partial_files:
483 log('Partial file without episode: %s', f, sender=self)
484 util.delete_file(f)
486 util.idle_add(indicator.on_finished)
488 if len(resumable_episodes):
489 def offer_resuming():
490 self.download_episode_list_paused(resumable_episodes)
491 if not gpodder.ui.fremantle:
492 resume_all = gtk.Button(_('Resume all'))
493 #resume_all.set_border_width(0)
494 def on_resume_all(button):
495 selection = self.treeDownloads.get_selection()
496 selection.select_all()
497 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
498 selection.unselect_all()
499 self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
500 self.message_area.hide()
501 resume_all.connect('clicked', on_resume_all)
503 self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
504 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
505 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
506 self.message_area.show_all()
507 self.clean_up_downloads(delete_partial=False)
508 util.idle_add(offer_resuming)
509 elif not gpodder.ui.fremantle:
510 util.idle_add(self.wNotebook.set_current_page, 0)
511 else:
512 util.idle_add(self.clean_up_downloads, True)
513 threading.Thread(target=find_partial_downloads).start()
515 # Start the auto-update procedure
516 self._auto_update_timer_source_id = None
517 if self.config.auto_update_feeds:
518 self.restart_auto_update_timer()
520 # Delete old episodes if the user wishes to
521 if self.config.auto_remove_played_episodes and \
522 self.config.episode_old_age > 0:
523 old_episodes = list(self.get_expired_episodes())
524 if len(old_episodes) > 0:
525 self.delete_episode_list(old_episodes, confirm=False)
526 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
528 if gpodder.ui.fremantle:
529 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
530 self.button_refresh.set_sensitive(True)
531 self.button_subscribe.set_sensitive(True)
532 self.main_window.set_title(_('gPodder'))
533 hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
535 # Do the initial sync with the web service
536 util.idle_add(self.mygpo_client.flush, True)
538 # First-time users should be asked if they want to see the OPML
539 if not self.channels and not gpodder.ui.fremantle:
540 util.idle_add(self.on_itemUpdate_activate)
542 def on_played(self, start, end, total, file_uri):
543 """Handle the "played" signal from a media player"""
544 log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
545 filename = file_uri[len('file://'):]
546 # FIXME: Optimize this by querying the database more directly
547 for channel in self.channels:
548 for episode in channel.get_all_episodes():
549 fn = episode.local_filename(create=False, check_only=True)
550 if fn == filename:
551 file_type = episode.file_type()
552 # Automatically enable D-Bus played status mode
553 if file_type == 'audio':
554 self.config.audio_played_dbus = True
555 elif file_type == 'video':
556 self.config.video_played_dbus = True
558 now = time.time()
559 if total > 0:
560 episode.total_time = total
561 if episode.current_position_updated is None or \
562 now > episode.current_position_updated:
563 episode.current_position = end
564 episode.current_position_updated = now
565 episode.mark(is_played=True)
566 episode.save()
567 self.db.commit()
568 self.update_episode_list_icons([episode.url])
569 self.update_podcast_list_model([episode.channel.url])
571 # Submit this action to the webservice
572 self.mygpo_client.on_playback_full(episode, \
573 start, end, total)
574 return
576 def on_add_remove_podcasts_mygpo(self):
577 actions = self.mygpo_client.get_received_actions()
578 if not actions:
579 return False
581 existing_urls = [c.url for c in self.channels]
583 # Columns for the episode selector window - just one...
584 columns = (
585 ('description', None, None, _('Action')),
588 # A list of actions that have to be chosen from
589 changes = []
591 # Actions that are ignored (already carried out)
592 ignored = []
594 for action in actions:
595 if action.is_add and action.url not in existing_urls:
596 changes.append(my.Change(action))
597 elif action.is_remove and action.url in existing_urls:
598 podcast_object = None
599 for podcast in self.channels:
600 if podcast.url == action.url:
601 podcast_object = podcast
602 break
603 changes.append(my.Change(action, podcast_object))
604 else:
605 log('Ignoring action: %s', action, sender=self)
606 ignored.append(action)
608 # Confirm all ignored changes
609 self.mygpo_client.confirm_received_actions(ignored)
611 def execute_podcast_actions(selected):
612 add_list = [c.action.url for c in selected if c.action.is_add]
613 remove_list = [c.podcast for c in selected if c.action.is_remove]
615 # Apply the accepted changes locally
616 self.add_podcast_list(add_list)
617 self.remove_podcast_list(remove_list, confirm=False)
619 # All selected items are now confirmed
620 self.mygpo_client.confirm_received_actions(c.action for c in selected)
622 # Revert the changes on the server
623 rejected = [c.action for c in changes if c not in selected]
624 self.mygpo_client.reject_received_actions(rejected)
626 def ask():
627 # We're abusing the Episode Selector again ;) -- thp
628 gPodderEpisodeSelector(self.main_window, \
629 title=_('Confirm changes from gpodder.net'), \
630 instructions=_('Select the actions you want to carry out.'), \
631 episodes=changes, \
632 columns=columns, \
633 size_attribute=None, \
634 stock_ok_button=gtk.STOCK_APPLY, \
635 callback=execute_podcast_actions, \
636 _config=self.config)
638 # There are some actions that need the user's attention
639 if changes:
640 util.idle_add(ask)
641 return True
643 # We have no remaining actions - no selection happens
644 return False
646 def rewrite_urls_mygpo(self):
647 # Check if we have to rewrite URLs since the last add
648 rewritten_urls = self.mygpo_client.get_rewritten_urls()
650 for rewritten_url in rewritten_urls:
651 if not rewritten_url.new_url:
652 continue
654 for channel in self.channels:
655 if channel.url == rewritten_url.old_url:
656 log('Updating URL of %s to %s', channel, \
657 rewritten_url.new_url, sender=self)
658 channel.url = rewritten_url.new_url
659 channel.save()
660 self.channel_list_changed = True
661 util.idle_add(self.update_episode_list_model)
662 break
664 def on_send_full_subscriptions(self):
665 # Send the full subscription list to the gpodder.net client
666 # (this will overwrite the subscription list on the server)
667 indicator = ProgressIndicator(_('Uploading subscriptions'), \
668 _('Your subscriptions are being uploaded to the server.'), \
669 False, self.get_dialog_parent())
671 try:
672 self.mygpo_client.set_subscriptions([c.url for c in self.channels])
673 util.idle_add(self.show_message, _('List uploaded successfully.'))
674 except Exception, e:
675 def show_error(e):
676 message = str(e)
677 if not message:
678 message = e.__class__.__name__
679 self.show_message(message, \
680 _('Error while uploading'), \
681 important=True)
682 util.idle_add(show_error, e)
684 util.idle_add(indicator.on_finished)
686 def on_podcast_selected(self, treeview, path, column):
687 # for Maemo 5's UI
688 model = treeview.get_model()
689 channel = model.get_value(model.get_iter(path), \
690 PodcastListModel.C_CHANNEL)
691 self.active_channel = channel
692 self.update_episode_list_model()
693 self.episodes_window.channel = self.active_channel
694 self.episodes_window.show()
696 def on_button_subscribe_clicked(self, button):
697 self.on_itemImportChannels_activate(button)
699 def on_button_downloads_clicked(self, widget):
700 self.downloads_window.show()
702 def show_episode_in_download_manager(self, episode):
703 self.downloads_window.show()
704 model = self.treeDownloads.get_model()
705 selection = self.treeDownloads.get_selection()
706 selection.unselect_all()
707 it = model.get_iter_first()
708 while it is not None:
709 task = model.get_value(it, DownloadStatusModel.C_TASK)
710 if task.episode.url == episode.url:
711 selection.select_iter(it)
712 # FIXME: Scroll to selection in pannable area
713 break
714 it = model.iter_next(it)
716 def for_each_episode_set_task_status(self, episodes, status):
717 episode_urls = set(episode.url for episode in episodes)
718 model = self.treeDownloads.get_model()
719 selected_tasks = [(gtk.TreeRowReference(model, row.path), \
720 model.get_value(row.iter, \
721 DownloadStatusModel.C_TASK)) for row in model \
722 if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
723 in episode_urls]
724 self._for_each_task_set_status(selected_tasks, status)
726 def on_window_orientation_changed(self, orientation):
727 self._last_orientation = orientation
728 if self.preferences_dialog is not None:
729 self.preferences_dialog.on_window_orientation_changed(orientation)
731 treeview = self.treeChannels
732 if orientation == Orientation.PORTRAIT:
733 treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
734 # Work around Maemo bug #4718
735 self.button_subscribe.set_name('HildonButton-thumb')
736 self.button_refresh.set_name('HildonButton-thumb')
737 else:
738 treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
739 # Work around Maemo bug #4718
740 self.button_subscribe.set_name('HildonButton-finger')
741 self.button_refresh.set_name('HildonButton-finger')
743 def on_treeview_podcasts_selection_changed(self, selection):
744 model, iter = selection.get_selected()
745 if iter is None:
746 self.active_channel = None
747 self.episode_list_model.clear()
749 def on_treeview_button_pressed(self, treeview, event):
750 if event.window != treeview.get_bin_window():
751 return False
753 TreeViewHelper.save_button_press_event(treeview, event)
755 if getattr(treeview, TreeViewHelper.ROLE) == \
756 TreeViewHelper.ROLE_PODCASTS:
757 return self.currently_updating
759 return event.button == self.context_menu_mouse_button and \
760 gpodder.ui.desktop
762 def on_treeview_podcasts_button_released(self, treeview, event):
763 if event.window != treeview.get_bin_window():
764 return False
766 if gpodder.ui.maemo:
767 return self.treeview_channels_handle_gestures(treeview, event)
768 return self.treeview_channels_show_context_menu(treeview, event)
770 def on_treeview_episodes_button_released(self, treeview, event):
771 if event.window != treeview.get_bin_window():
772 return False
774 if gpodder.ui.maemo:
775 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
776 return self.treeview_available_handle_gestures(treeview, event)
778 return self.treeview_available_show_context_menu(treeview, event)
780 def on_treeview_downloads_button_released(self, treeview, event):
781 if event.window != treeview.get_bin_window():
782 return False
784 return self.treeview_downloads_show_context_menu(treeview, event)
786 def on_entry_search_podcasts_changed(self, editable):
787 if self.hbox_search_podcasts.get_property('visible'):
788 self.podcast_list_model.set_search_term(editable.get_chars(0, -1))
790 def on_entry_search_podcasts_key_press(self, editable, event):
791 if event.keyval == gtk.keysyms.Escape:
792 self.hide_podcast_search()
793 return True
795 def hide_podcast_search(self, *args):
796 self.hbox_search_podcasts.hide()
797 self.entry_search_podcasts.set_text('')
798 self.podcast_list_model.set_search_term(None)
799 self.treeChannels.grab_focus()
801 def show_podcast_search(self, input_char):
802 self.hbox_search_podcasts.show()
803 self.entry_search_podcasts.insert_text(input_char, -1)
804 self.entry_search_podcasts.grab_focus()
805 self.entry_search_podcasts.set_position(-1)
807 def init_podcast_list_treeview(self):
808 # Set up podcast channel tree view widget
809 if gpodder.ui.fremantle:
810 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
811 self.item_view_podcasts_downloaded.set_active(True)
812 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
813 self.item_view_podcasts_unplayed.set_active(True)
814 else:
815 self.item_view_podcasts_all.set_active(True)
816 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
818 iconcolumn = gtk.TreeViewColumn('')
819 iconcell = gtk.CellRendererPixbuf()
820 iconcolumn.pack_start(iconcell, False)
821 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
822 self.treeChannels.append_column(iconcolumn)
824 namecolumn = gtk.TreeViewColumn('')
825 namecell = gtk.CellRendererText()
826 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
827 namecolumn.pack_start(namecell, True)
828 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
830 iconcell = gtk.CellRendererPixbuf()
831 iconcell.set_property('xalign', 1.0)
832 namecolumn.pack_start(iconcell, False)
833 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
834 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
835 self.treeChannels.append_column(namecolumn)
837 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
839 # When no podcast is selected, clear the episode list model
840 selection = self.treeChannels.get_selection()
841 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
843 # Set up type-ahead find for the podcast list
844 def on_key_press(treeview, event):
845 if event.keyval == gtk.keysyms.Escape:
846 self.hide_podcast_search()
847 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
848 self.hide_podcast_search()
849 elif event.state & gtk.gdk.CONTROL_MASK:
850 # Don't handle type-ahead when control is pressed (so shortcuts
851 # with the Ctrl key still work, e.g. Ctrl+A, ...)
852 return True
853 else:
854 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
855 if unicode_char_id == 0:
856 return False
857 input_char = unichr(unicode_char_id)
858 self.show_podcast_search(input_char)
859 return True
860 self.treeChannels.connect('key-press-event', on_key_press)
862 # Enable separators to the podcast list to separate special podcasts
863 # from others (this is used for the "all episodes" view)
864 self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
866 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
868 def on_entry_search_episodes_changed(self, editable):
869 if self.hbox_search_episodes.get_property('visible'):
870 self.episode_list_model.set_search_term(editable.get_chars(0, -1))
872 def on_entry_search_episodes_key_press(self, editable, event):
873 if event.keyval == gtk.keysyms.Escape:
874 self.hide_episode_search()
875 return True
877 def hide_episode_search(self, *args):
878 self.hbox_search_episodes.hide()
879 self.entry_search_episodes.set_text('')
880 self.episode_list_model.set_search_term(None)
881 self.treeAvailable.grab_focus()
883 def show_episode_search(self, input_char):
884 self.hbox_search_episodes.show()
885 self.entry_search_episodes.insert_text(input_char, -1)
886 self.entry_search_episodes.grab_focus()
887 self.entry_search_episodes.set_position(-1)
889 def init_episode_list_treeview(self):
890 # For loading the list model
891 self.empty_episode_list_model = EpisodeListModel()
892 self.episode_list_model = EpisodeListModel()
894 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
895 self.item_view_episodes_undeleted.set_active(True)
896 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
897 self.item_view_episodes_downloaded.set_active(True)
898 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
899 self.item_view_episodes_unplayed.set_active(True)
900 else:
901 self.item_view_episodes_all.set_active(True)
903 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
905 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
907 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
909 iconcell = gtk.CellRendererPixbuf()
910 if gpodder.ui.maemo:
911 iconcell.set_fixed_size(50, 50)
912 status_column_label = ''
913 else:
914 status_column_label = _('Status')
915 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=EpisodeListModel.C_STATUS_ICON)
917 namecell = gtk.CellRendererText()
918 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
919 namecolumn = gtk.TreeViewColumn(_('Episode'), namecell, markup=EpisodeListModel.C_DESCRIPTION)
920 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
921 namecolumn.set_resizable(True)
922 namecolumn.set_expand(True)
924 sizecell = gtk.CellRendererText()
925 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
927 releasecell = gtk.CellRendererText()
928 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
930 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
931 itemcolumn.set_reorderable(True)
932 self.treeAvailable.append_column(itemcolumn)
934 if gpodder.ui.maemo:
935 sizecolumn.set_visible(False)
936 releasecolumn.set_visible(False)
938 # Set up type-ahead find for the episode list
939 def on_key_press(treeview, event):
940 if event.keyval == gtk.keysyms.Escape:
941 self.hide_episode_search()
942 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
943 self.hide_episode_search()
944 elif event.state & gtk.gdk.CONTROL_MASK:
945 # Don't handle type-ahead when control is pressed (so shortcuts
946 # with the Ctrl key still work, e.g. Ctrl+A, ...)
947 return False
948 else:
949 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
950 if unicode_char_id == 0:
951 return False
952 input_char = unichr(unicode_char_id)
953 self.show_episode_search(input_char)
954 return True
955 self.treeAvailable.connect('key-press-event', on_key_press)
957 if gpodder.ui.desktop:
958 self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
959 (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
960 def drag_data_get(tree, context, selection_data, info, timestamp):
961 if self.config.on_drag_mark_played:
962 for episode in self.get_selected_episodes():
963 episode.mark(is_played=True)
964 self.on_selected_episodes_status_changed()
965 uris = ['file://'+e.local_filename(create=False) \
966 for e in self.get_selected_episodes() \
967 if e.was_downloaded(and_exists=True)]
968 uris.append('') # for the trailing '\r\n'
969 selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
970 self.treeAvailable.connect('drag-data-get', drag_data_get)
972 selection = self.treeAvailable.get_selection()
973 if gpodder.ui.diablo:
974 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
975 selection.set_mode(gtk.SELECTION_SINGLE)
976 else:
977 selection.set_mode(gtk.SELECTION_MULTIPLE)
978 elif gpodder.ui.fremantle:
979 selection.set_mode(gtk.SELECTION_SINGLE)
980 else:
981 selection.set_mode(gtk.SELECTION_MULTIPLE)
982 # Update the sensitivity of the toolbar buttons on the Desktop
983 selection.connect('changed', lambda s: self.play_or_download())
985 if gpodder.ui.diablo:
986 # Set up the tap-and-hold context menu for podcasts
987 menu = gtk.Menu()
988 menu.append(self.itemUpdateChannel.create_menu_item())
989 menu.append(self.itemEditChannel.create_menu_item())
990 menu.append(gtk.SeparatorMenuItem())
991 menu.append(self.itemRemoveChannel.create_menu_item())
992 menu.append(gtk.SeparatorMenuItem())
993 item = gtk.ImageMenuItem(_('Close this menu'))
994 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
995 gtk.ICON_SIZE_MENU))
996 menu.append(item)
997 menu.show_all()
998 menu = self.set_finger_friendly(menu)
999 self.treeChannels.tap_and_hold_setup(menu)
1002 def init_download_list_treeview(self):
1003 # enable multiple selection support
1004 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1005 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1007 # columns and renderers for "download progress" tab
1008 # First column: [ICON] Episodename
1009 column = gtk.TreeViewColumn(_('Episode'))
1011 cell = gtk.CellRendererPixbuf()
1012 if gpodder.ui.maemo:
1013 cell.set_fixed_size(50, 50)
1014 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1015 column.pack_start(cell, expand=False)
1016 column.add_attribute(cell, 'stock-id', \
1017 DownloadStatusModel.C_ICON_NAME)
1019 cell = gtk.CellRendererText()
1020 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1021 column.pack_start(cell, expand=True)
1022 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1023 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1024 column.set_expand(True)
1025 self.treeDownloads.append_column(column)
1027 # Second column: Progress
1028 cell = gtk.CellRendererProgress()
1029 cell.set_property('yalign', .5)
1030 cell.set_property('ypad', 6)
1031 column = gtk.TreeViewColumn(_('Progress'), cell,
1032 value=DownloadStatusModel.C_PROGRESS, \
1033 text=DownloadStatusModel.C_PROGRESS_TEXT)
1034 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1035 column.set_expand(False)
1036 self.treeDownloads.append_column(column)
1037 column.set_property('min-width', 150)
1038 column.set_property('max-width', 150)
1040 self.treeDownloads.set_model(self.download_status_model)
1041 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1043 def on_treeview_expose_event(self, treeview, event):
1044 if event.window == treeview.get_bin_window():
1045 model = treeview.get_model()
1046 if (model is not None and model.get_iter_first() is not None):
1047 return False
1049 role = getattr(treeview, TreeViewHelper.ROLE)
1050 ctx = event.window.cairo_create()
1051 ctx.rectangle(event.area.x, event.area.y,
1052 event.area.width, event.area.height)
1053 ctx.clip()
1055 x, y, width, height, depth = event.window.get_geometry()
1056 progress = None
1058 if role == TreeViewHelper.ROLE_EPISODES:
1059 if self.currently_updating:
1060 text = _('Loading episodes')
1061 progress = self.episode_list_model.get_update_progress()
1062 elif self.config.episode_list_view_mode != \
1063 EpisodeListModel.VIEW_ALL:
1064 text = _('No episodes in current view')
1065 else:
1066 text = _('No episodes available')
1067 elif role == TreeViewHelper.ROLE_PODCASTS:
1068 if self.config.episode_list_view_mode != \
1069 EpisodeListModel.VIEW_ALL and \
1070 self.config.podcast_list_hide_boring and \
1071 len(self.channels) > 0:
1072 text = _('No podcasts in this view')
1073 else:
1074 text = _('No subscriptions')
1075 elif role == TreeViewHelper.ROLE_DOWNLOADS:
1076 text = _('No active downloads')
1077 else:
1078 raise Exception('on_treeview_expose_event: unknown role')
1080 if gpodder.ui.fremantle:
1081 from gpodder.gtkui.frmntl import style
1082 font_desc = style.get_font_desc('LargeSystemFont')
1083 else:
1084 font_desc = None
1086 draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1088 return False
1090 def enable_download_list_update(self):
1091 if not self.download_list_update_enabled:
1092 gobject.timeout_add(1500, self.update_downloads_list)
1093 self.download_list_update_enabled = True
1095 def on_btnCleanUpDownloads_clicked(self, button=None):
1096 model = self.download_status_model
1098 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
1099 changed_episode_urls = set()
1100 for row_reference, task in all_tasks:
1101 if task.status in (task.DONE, task.CANCELLED):
1102 model.remove(model.get_iter(row_reference.get_path()))
1103 try:
1104 # We don't "see" this task anymore - remove it;
1105 # this is needed, so update_episode_list_icons()
1106 # below gets the correct list of "seen" tasks
1107 self.download_tasks_seen.remove(task)
1108 except KeyError, key_error:
1109 log('Cannot remove task from "seen" list: %s', task, sender=self)
1110 changed_episode_urls.add(task.url)
1111 # Tell the task that it has been removed (so it can clean up)
1112 task.removed_from_list()
1114 # Tell the podcasts tab to update icons for our removed podcasts
1115 self.update_episode_list_icons(changed_episode_urls)
1117 # Tell the shownotes window that we have removed the episode
1118 if self.episode_shownotes_window is not None and \
1119 self.episode_shownotes_window.episode is not None and \
1120 self.episode_shownotes_window.episode.url in changed_episode_urls:
1121 self.episode_shownotes_window._download_status_changed(None)
1123 # Update the tab title and downloads list
1124 self.update_downloads_list()
1126 def on_tool_downloads_toggled(self, toolbutton):
1127 if toolbutton.get_active():
1128 self.wNotebook.set_current_page(1)
1129 else:
1130 self.wNotebook.set_current_page(0)
1132 def add_download_task_monitor(self, monitor):
1133 self.download_task_monitors.add(monitor)
1134 model = self.download_status_model
1135 if model is None:
1136 model = ()
1137 for row in model:
1138 task = row[self.download_status_model.C_TASK]
1139 monitor.task_updated(task)
1141 def remove_download_task_monitor(self, monitor):
1142 self.download_task_monitors.remove(monitor)
1144 def update_downloads_list(self):
1145 try:
1146 model = self.download_status_model
1148 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
1149 total_speed, total_size, done_size = 0, 0, 0
1151 # Keep a list of all download tasks that we've seen
1152 download_tasks_seen = set()
1154 # Remember the DownloadTask object for the episode that
1155 # has been opened in the episode shownotes dialog (if any)
1156 if self.episode_shownotes_window is not None:
1157 shownotes_episode = self.episode_shownotes_window.episode
1158 shownotes_task = None
1159 else:
1160 shownotes_episode = None
1161 shownotes_task = None
1163 # Do not go through the list of the model is not (yet) available
1164 if model is None:
1165 model = ()
1167 failed_downloads = []
1168 for row in model:
1169 self.download_status_model.request_update(row.iter)
1171 task = row[self.download_status_model.C_TASK]
1172 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
1174 # Let the download task monitors know of changes
1175 for monitor in self.download_task_monitors:
1176 monitor.task_updated(task)
1178 total_size += size
1179 done_size += size*progress
1181 if shownotes_episode is not None and \
1182 shownotes_episode.url == task.episode.url:
1183 shownotes_task = task
1185 download_tasks_seen.add(task)
1187 if status == download.DownloadTask.DOWNLOADING:
1188 downloading += 1
1189 total_speed += speed
1190 elif status == download.DownloadTask.FAILED:
1191 failed_downloads.append(task)
1192 failed += 1
1193 elif status == download.DownloadTask.DONE:
1194 finished += 1
1195 elif status == download.DownloadTask.QUEUED:
1196 queued += 1
1197 elif status == download.DownloadTask.PAUSED:
1198 paused += 1
1199 else:
1200 others += 1
1202 # Remember which tasks we have seen after this run
1203 self.download_tasks_seen = download_tasks_seen
1205 if gpodder.ui.desktop:
1206 text = [_('Downloads')]
1207 if downloading + failed + queued > 0:
1208 s = []
1209 if downloading > 0:
1210 s.append(N_('%d active', '%d active', downloading) % downloading)
1211 if failed > 0:
1212 s.append(N_('%d failed', '%d failed', failed) % failed)
1213 if queued > 0:
1214 s.append(N_('%d queued', '%d queued', queued) % queued)
1215 text.append(' (' + ', '.join(s)+')')
1216 self.labelDownloads.set_text(''.join(text))
1217 elif gpodder.ui.diablo:
1218 sum = downloading + failed + finished + queued + paused + others
1219 if sum:
1220 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
1221 else:
1222 self.tool_downloads.set_label(_('Downloads'))
1223 elif gpodder.ui.fremantle:
1224 if downloading + queued > 0:
1225 self.button_downloads.set_value(N_('%d active', '%d active', downloading+queued) % (downloading+queued))
1226 elif failed > 0:
1227 self.button_downloads.set_value(N_('%d failed', '%d failed', failed) % failed)
1228 elif paused > 0:
1229 self.button_downloads.set_value(N_('%d paused', '%d paused', paused) % paused)
1230 else:
1231 self.button_downloads.set_value(_('Idle'))
1233 title = [self.default_title]
1235 # We have to update all episodes/channels for which the status has
1236 # changed. Accessing task.status_changed has the side effect of
1237 # re-setting the changed flag, so we need to get the "changed" list
1238 # of tuples first and split it into two lists afterwards
1239 changed = [(task.url, task.podcast_url) for task in \
1240 self.download_tasks_seen if task.status_changed]
1241 episode_urls = [episode_url for episode_url, channel_url in changed]
1242 channel_urls = [channel_url for episode_url, channel_url in changed]
1244 count = downloading + queued
1245 if count > 0:
1246 title.append(N_('downloading %d file', 'downloading %d files', count) % count)
1248 if total_size > 0:
1249 percentage = 100.0*done_size/total_size
1250 else:
1251 percentage = 0.0
1252 total_speed = util.format_filesize(total_speed)
1253 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
1254 if self.tray_icon is not None:
1255 # Update the tray icon status and progress bar
1256 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
1257 self.tray_icon.draw_progress_bar(percentage/100.)
1258 elif self.last_download_count > 0:
1259 if self.tray_icon is not None:
1260 # Update the tray icon status
1261 self.tray_icon.set_status()
1262 if gpodder.ui.desktop:
1263 self.downloads_finished(self.download_tasks_seen)
1264 if gpodder.ui.diablo:
1265 hildon.hildon_banner_show_information(self.gPodder, '', 'gPodder: %s' % _('All downloads finished'))
1266 log('All downloads have finished.', sender=self)
1267 if self.config.cmd_all_downloads_complete:
1268 util.run_external_command(self.config.cmd_all_downloads_complete)
1270 if gpodder.ui.fremantle and failed:
1271 message = '\n'.join(['%s: %s' % (str(task), \
1272 task.error_message) for task in failed_downloads])
1273 self.show_message(message, _('Downloads failed'), important=True)
1274 self.last_download_count = count
1276 if not gpodder.ui.fremantle:
1277 self.gPodder.set_title(' - '.join(title))
1279 self.update_episode_list_icons(episode_urls)
1280 if self.episode_shownotes_window is not None:
1281 if (shownotes_task and shownotes_task.url in episode_urls) or \
1282 shownotes_task != self.episode_shownotes_window.task:
1283 self.episode_shownotes_window._download_status_changed(shownotes_task)
1284 self.episode_shownotes_window._download_status_progress()
1285 self.play_or_download()
1286 if channel_urls:
1287 self.update_podcast_list_model(channel_urls)
1289 if not self.download_queue_manager.are_queued_or_active_tasks():
1290 self.download_list_update_enabled = False
1292 return self.download_list_update_enabled
1293 except Exception, e:
1294 log('Exception happened while updating download list.', sender=self, traceback=True)
1295 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1296 # We return False here, so the update loop won't be called again,
1297 # that's why we require the restart of gPodder in the message.
1298 return False
1300 def on_config_changed(self, *args):
1301 util.idle_add(self._on_config_changed, *args)
1303 def _on_config_changed(self, name, old_value, new_value):
1304 if name == 'show_toolbar' and gpodder.ui.desktop:
1305 self.toolbar.set_property('visible', new_value)
1306 elif name == 'videoplayer':
1307 self.config.video_played_dbus = False
1308 elif name == 'player':
1309 self.config.audio_played_dbus = False
1310 elif name == 'episode_list_descriptions':
1311 self.update_episode_list_model()
1312 elif name == 'episode_list_thumbnails':
1313 self.update_episode_list_icons(all=True)
1314 elif name == 'rotation_mode':
1315 self._fremantle_rotation.set_mode(new_value)
1316 elif name in ('auto_update_feeds', 'auto_update_frequency'):
1317 self.restart_auto_update_timer()
1318 elif name == 'podcast_list_view_all':
1319 # Force a update of the podcast list model
1320 self.channel_list_changed = True
1321 if gpodder.ui.fremantle:
1322 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1323 while gtk.events_pending():
1324 gtk.main_iteration(False)
1325 self.update_podcast_list_model()
1326 if gpodder.ui.fremantle:
1327 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1329 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1330 # With get_bin_window, we get the window that contains the rows without
1331 # the header. The Y coordinate of this window will be the height of the
1332 # treeview header. This is the amount we have to subtract from the
1333 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1334 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1335 y -= x_bin
1336 y -= y_bin
1337 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1339 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or (column is not None and column != treeview.get_columns()[0]):
1340 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1341 return False
1343 if path is not None:
1344 model = treeview.get_model()
1345 iter = model.get_iter(path)
1346 role = getattr(treeview, TreeViewHelper.ROLE)
1348 if role == TreeViewHelper.ROLE_EPISODES:
1349 id = model.get_value(iter, EpisodeListModel.C_URL)
1350 elif role == TreeViewHelper.ROLE_PODCASTS:
1351 id = model.get_value(iter, PodcastListModel.C_URL)
1353 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1354 if last_tooltip is not None and last_tooltip != id:
1355 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1356 return False
1357 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1359 if role == TreeViewHelper.ROLE_EPISODES:
1360 description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1361 if description:
1362 tooltip.set_text(description)
1363 else:
1364 return False
1365 elif role == TreeViewHelper.ROLE_PODCASTS:
1366 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1367 if channel is None:
1368 return False
1369 channel.request_save_dir_size()
1370 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1371 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1372 if error_str:
1373 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1374 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1375 table = gtk.Table(rows=3, columns=3)
1376 table.set_row_spacings(5)
1377 table.set_col_spacings(5)
1378 table.set_border_width(5)
1380 heading = gtk.Label()
1381 heading.set_alignment(0, 1)
1382 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1383 table.attach(heading, 0, 1, 0, 1)
1384 size_info = gtk.Label()
1385 size_info.set_alignment(1, 1)
1386 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1387 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1388 table.attach(size_info, 2, 3, 0, 1)
1390 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1392 if len(channel.description) < 500:
1393 description = channel.description
1394 else:
1395 pos = channel.description.find('\n\n')
1396 if pos == -1 or pos > 500:
1397 description = channel.description[:498]+'[...]'
1398 else:
1399 description = channel.description[:pos]
1401 description = gtk.Label(description)
1402 if error_str:
1403 description.set_markup(error_str)
1404 description.set_alignment(0, 0)
1405 description.set_line_wrap(True)
1406 table.attach(description, 0, 3, 2, 3)
1408 table.show_all()
1409 tooltip.set_custom(table)
1411 return True
1413 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1414 return False
1416 def treeview_allow_tooltips(self, treeview, allow):
1417 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1419 def update_m3u_playlist_clicked(self, widget):
1420 if self.active_channel is not None:
1421 self.active_channel.update_m3u_playlist()
1422 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1424 def treeview_handle_context_menu_click(self, treeview, event):
1425 x, y = int(event.x), int(event.y)
1426 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1428 selection = treeview.get_selection()
1429 model, paths = selection.get_selected_rows()
1431 if path is None or (path not in paths and \
1432 event.button == self.context_menu_mouse_button):
1433 # We have right-clicked, but not into the selection,
1434 # assume we don't want to operate on the selection
1435 paths = []
1437 if path is not None and not paths and \
1438 event.button == self.context_menu_mouse_button:
1439 # No selection or clicked outside selection;
1440 # select the single item where we clicked
1441 treeview.grab_focus()
1442 treeview.set_cursor(path, column, 0)
1443 paths = [path]
1445 if not paths:
1446 # Unselect any remaining items (clicked elsewhere)
1447 if hasattr(treeview, 'is_rubber_banding_active'):
1448 if not treeview.is_rubber_banding_active():
1449 selection.unselect_all()
1450 else:
1451 selection.unselect_all()
1453 return model, paths
1455 def downloads_list_get_selection(self, model=None, paths=None):
1456 if model is None and paths is None:
1457 selection = self.treeDownloads.get_selection()
1458 model, paths = selection.get_selected_rows()
1460 can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1461 selected_tasks = [(gtk.TreeRowReference(model, path), \
1462 model.get_value(model.get_iter(path), \
1463 DownloadStatusModel.C_TASK)) for path in paths]
1465 for row_reference, task in selected_tasks:
1466 if task.status != download.DownloadTask.QUEUED:
1467 can_force = False
1468 if task.status not in (download.DownloadTask.PAUSED, \
1469 download.DownloadTask.FAILED, \
1470 download.DownloadTask.CANCELLED):
1471 can_queue = False
1472 if task.status not in (download.DownloadTask.PAUSED, \
1473 download.DownloadTask.QUEUED, \
1474 download.DownloadTask.DOWNLOADING):
1475 can_cancel = False
1476 if task.status not in (download.DownloadTask.QUEUED, \
1477 download.DownloadTask.DOWNLOADING):
1478 can_pause = False
1479 if task.status not in (download.DownloadTask.CANCELLED, \
1480 download.DownloadTask.FAILED, \
1481 download.DownloadTask.DONE):
1482 can_remove = False
1484 return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1486 def downloads_finished(self, download_tasks_seen):
1487 # FIXME: Filter all tasks that have already been reported
1488 finished_downloads = [str(task) for task in download_tasks_seen if task.status == task.DONE]
1489 failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.status == task.FAILED]
1491 if finished_downloads and failed_downloads:
1492 message = self.format_episode_list(finished_downloads, 5)
1493 message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1494 message += self.format_episode_list(failed_downloads, 5)
1495 self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1496 elif finished_downloads:
1497 message = self.format_episode_list(finished_downloads)
1498 self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1499 elif failed_downloads:
1500 message = self.format_episode_list(failed_downloads)
1501 self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1503 def format_episode_list(self, episode_list, max_episodes=10):
1505 Format a list of episode names for notifications
1507 Will truncate long episode names and limit the amount of
1508 episodes displayed (max_episodes=10).
1510 The episode_list parameter should be a list of strings.
1512 MAX_TITLE_LENGTH = 100
1514 result = []
1515 for title in episode_list[:min(len(episode_list), max_episodes)]:
1516 if len(title) > MAX_TITLE_LENGTH:
1517 middle = (MAX_TITLE_LENGTH/2)-2
1518 title = '%s...%s' % (title[0:middle], title[-middle:])
1519 result.append(saxutils.escape(title))
1520 result.append('\n')
1522 more_episodes = len(episode_list) - max_episodes
1523 if more_episodes > 0:
1524 result.append('(...')
1525 result.append(N_('%d more episode', '%d more episodes', more_episodes) % more_episodes)
1526 result.append('...)')
1528 return (''.join(result)).strip()
1530 def _for_each_task_set_status(self, tasks, status, force_start=False):
1531 episode_urls = set()
1532 model = self.treeDownloads.get_model()
1533 for row_reference, task in tasks:
1534 if status == download.DownloadTask.QUEUED:
1535 # Only queue task when its paused/failed/cancelled (or forced)
1536 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1537 self.download_queue_manager.add_task(task, force_start)
1538 self.enable_download_list_update()
1539 elif status == download.DownloadTask.CANCELLED:
1540 # Cancelling a download allowed when downloading/queued
1541 if task.status in (task.QUEUED, task.DOWNLOADING):
1542 task.status = status
1543 # Cancelling paused downloads requires a call to .run()
1544 elif task.status == task.PAUSED:
1545 task.status = status
1546 # Call run, so the partial file gets deleted
1547 task.run()
1548 elif status == download.DownloadTask.PAUSED:
1549 # Pausing a download only when queued/downloading
1550 if task.status in (task.DOWNLOADING, task.QUEUED):
1551 task.status = status
1552 elif status is None:
1553 # Remove the selected task - cancel downloading/queued tasks
1554 if task.status in (task.QUEUED, task.DOWNLOADING):
1555 task.status = task.CANCELLED
1556 model.remove(model.get_iter(row_reference.get_path()))
1557 # Remember the URL, so we can tell the UI to update
1558 try:
1559 # We don't "see" this task anymore - remove it;
1560 # this is needed, so update_episode_list_icons()
1561 # below gets the correct list of "seen" tasks
1562 self.download_tasks_seen.remove(task)
1563 except KeyError, key_error:
1564 log('Cannot remove task from "seen" list: %s', task, sender=self)
1565 episode_urls.add(task.url)
1566 # Tell the task that it has been removed (so it can clean up)
1567 task.removed_from_list()
1568 else:
1569 # We can (hopefully) simply set the task status here
1570 task.status = status
1571 # Tell the podcasts tab to update icons for our removed podcasts
1572 self.update_episode_list_icons(episode_urls)
1573 # Update the tab title and downloads list
1574 self.update_downloads_list()
1576 def treeview_downloads_show_context_menu(self, treeview, event):
1577 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1578 if not paths:
1579 if not hasattr(treeview, 'is_rubber_banding_active'):
1580 return True
1581 else:
1582 return not treeview.is_rubber_banding_active()
1584 if event.button == self.context_menu_mouse_button:
1585 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1586 self.downloads_list_get_selection(model, paths)
1588 def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1589 # This creates a menu item for selection-wide actions
1590 item = gtk.ImageMenuItem(label)
1591 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1592 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1593 item.set_sensitive(sensitive)
1594 return self.set_finger_friendly(item)
1596 menu = gtk.Menu()
1598 item = gtk.ImageMenuItem(_('Episode details'))
1599 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1600 if len(selected_tasks) == 1:
1601 row_reference, task = selected_tasks[0]
1602 episode = task.episode
1603 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1604 else:
1605 item.set_sensitive(False)
1606 menu.append(self.set_finger_friendly(item))
1607 menu.append(gtk.SeparatorMenuItem())
1608 if can_force:
1609 menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1610 else:
1611 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1612 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1613 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1614 menu.append(gtk.SeparatorMenuItem())
1615 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1617 if gpodder.ui.maemo:
1618 # Because we open the popup on left-click for Maemo,
1619 # we also include a non-action to close the menu
1620 menu.append(gtk.SeparatorMenuItem())
1621 item = gtk.ImageMenuItem(_('Close this menu'))
1622 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1624 menu.append(self.set_finger_friendly(item))
1626 menu.show_all()
1627 menu.popup(None, None, None, event.button, event.time)
1628 return True
1630 def treeview_channels_show_context_menu(self, treeview, event):
1631 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1632 if not paths:
1633 return True
1635 # Check for valid channel id, if there's no id then
1636 # assume that it is a proxy channel or equivalent
1637 # and cannot be operated with right click
1638 if self.active_channel.id is None:
1639 return True
1641 if event.button == 3:
1642 menu = gtk.Menu()
1644 ICON = lambda x: x
1646 item = gtk.ImageMenuItem( _('Open download folder'))
1647 item.set_image( gtk.image_new_from_icon_name(ICON('folder-open'), gtk.ICON_SIZE_MENU))
1648 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1649 menu.append( item)
1651 item = gtk.ImageMenuItem( _('Update Feed'))
1652 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1653 item.connect('activate', self.on_itemUpdateChannel_activate )
1654 item.set_sensitive( not self.updating_feed_cache )
1655 menu.append( item)
1657 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1658 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1659 item.connect('activate', self.update_m3u_playlist_clicked)
1660 menu.append(item)
1662 if self.active_channel.link:
1663 item = gtk.ImageMenuItem(_('Visit website'))
1664 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1665 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1666 menu.append(item)
1668 if self.active_channel.channel_is_locked:
1669 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1670 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1671 item.connect('activate', self.on_channel_toggle_lock_activate)
1672 menu.append(self.set_finger_friendly(item))
1673 else:
1674 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1675 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1676 item.connect('activate', self.on_channel_toggle_lock_activate)
1677 menu.append(self.set_finger_friendly(item))
1680 menu.append( gtk.SeparatorMenuItem())
1682 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1683 item.connect( 'activate', self.on_itemEditChannel_activate)
1684 menu.append( item)
1686 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1687 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1688 menu.append( item)
1690 menu.show_all()
1691 # Disable tooltips while we are showing the menu, so
1692 # the tooltip will not appear over the menu
1693 self.treeview_allow_tooltips(self.treeChannels, False)
1694 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1695 menu.popup( None, None, None, event.button, event.time)
1697 return True
1699 def on_itemClose_activate(self, widget):
1700 if self.tray_icon is not None:
1701 self.iconify_main_window()
1702 else:
1703 self.on_gPodder_delete_event(widget)
1705 def cover_file_removed(self, channel_url):
1707 The Cover Downloader calls this when a previously-
1708 available cover has been removed from the disk. We
1709 have to update our model to reflect this change.
1711 self.podcast_list_model.delete_cover_by_url(channel_url)
1713 def cover_download_finished(self, channel_url, pixbuf):
1715 The Cover Downloader calls this when it has finished
1716 downloading (or registering, if already downloaded)
1717 a new channel cover, which is ready for displaying.
1719 self.podcast_list_model.add_cover_by_url(channel_url, pixbuf)
1721 def save_episodes_as_file(self, episodes):
1722 for episode in episodes:
1723 self.save_episode_as_file(episode)
1725 def save_episode_as_file(self, episode):
1726 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1727 if episode.was_downloaded(and_exists=True):
1728 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1729 copy_from = episode.local_filename(create=False)
1730 assert copy_from is not None
1731 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1732 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1733 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1735 def copy_episodes_bluetooth(self, episodes):
1736 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1738 def convert_and_send_thread(episode):
1739 for episode in episodes:
1740 filename = episode.local_filename(create=False)
1741 assert filename is not None
1742 destfile = os.path.join(tempfile.gettempdir(), \
1743 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1744 (base, ext) = os.path.splitext(filename)
1745 if not destfile.endswith(ext):
1746 destfile += ext
1748 try:
1749 shutil.copyfile(filename, destfile)
1750 util.bluetooth_send_file(destfile)
1751 except:
1752 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1753 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1755 util.delete_file(destfile)
1757 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1759 def get_device_name(self):
1760 if self.config.device_type == 'ipod':
1761 return _('iPod')
1762 elif self.config.device_type in ('filesystem', 'mtp'):
1763 return _('MP3 player')
1764 else:
1765 return '(unknown device)'
1767 def _treeview_button_released(self, treeview, event):
1768 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1769 dy = int(abs(event.y-ypos))
1770 dx = int(event.x-xpos)
1772 selection = treeview.get_selection()
1773 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1774 if path is None or dy > 30:
1775 return (False, dx, dy)
1777 path, column, x, y = path
1778 selection.select_path(path)
1779 treeview.set_cursor(path)
1780 treeview.grab_focus()
1782 return (True, dx, dy)
1784 def treeview_channels_handle_gestures(self, treeview, event):
1785 if self.currently_updating:
1786 return False
1788 selected, dx, dy = self._treeview_button_released(treeview, event)
1790 if selected:
1791 if self.config.maemo_enable_gestures:
1792 if dx > 70:
1793 self.on_itemUpdateChannel_activate()
1794 elif dx < -70:
1795 self.on_itemEditChannel_activate(treeview)
1797 return False
1799 def treeview_available_handle_gestures(self, treeview, event):
1800 selected, dx, dy = self._treeview_button_released(treeview, event)
1802 if selected:
1803 if self.config.maemo_enable_gestures:
1804 if dx > 70:
1805 self.on_playback_selected_episodes(None)
1806 return True
1807 elif dx < -70:
1808 self.on_shownotes_selected_episodes(None)
1809 return True
1811 # Pass the event to the context menu handler for treeAvailable
1812 self.treeview_available_show_context_menu(treeview, event)
1814 return True
1816 def treeview_available_show_context_menu(self, treeview, event):
1817 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1818 if not paths:
1819 if not hasattr(treeview, 'is_rubber_banding_active'):
1820 return True
1821 else:
1822 return not treeview.is_rubber_banding_active()
1824 if event.button == self.context_menu_mouse_button:
1825 episodes = self.get_selected_episodes()
1826 any_locked = any(e.is_locked for e in episodes)
1827 any_played = any(e.is_played for e in episodes)
1828 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1829 downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
1830 downloading = any(self.episode_is_downloading(e) for e in episodes)
1832 menu = gtk.Menu()
1834 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1836 if open_instead_of_play:
1837 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1838 elif downloaded:
1839 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1840 else:
1841 item = gtk.ImageMenuItem(_('Stream'))
1842 item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
1844 item.set_sensitive(can_play and not downloading)
1845 item.connect('activate', self.on_playback_selected_episodes)
1846 menu.append(self.set_finger_friendly(item))
1848 if not can_cancel:
1849 item = gtk.ImageMenuItem(_('Download'))
1850 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1851 item.set_sensitive(can_download)
1852 item.connect('activate', self.on_download_selected_episodes)
1853 menu.append(self.set_finger_friendly(item))
1854 else:
1855 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1856 item.connect('activate', self.on_item_cancel_download_activate)
1857 menu.append(self.set_finger_friendly(item))
1859 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1860 item.set_sensitive(can_delete)
1861 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1862 menu.append(self.set_finger_friendly(item))
1864 ICON = lambda x: x
1866 # Ok, this probably makes sense to only display for downloaded files
1867 if downloaded:
1868 menu.append(gtk.SeparatorMenuItem())
1869 share_item = gtk.MenuItem(_('Send to'))
1870 menu.append(share_item)
1871 share_menu = gtk.Menu()
1873 item = gtk.ImageMenuItem(_('Local folder'))
1874 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
1875 item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
1876 share_menu.append(self.set_finger_friendly(item))
1877 if self.bluetooth_available:
1878 item = gtk.ImageMenuItem(_('Bluetooth device'))
1879 item.set_image(gtk.image_new_from_icon_name(ICON('bluetooth'), gtk.ICON_SIZE_MENU))
1880 item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
1881 share_menu.append(self.set_finger_friendly(item))
1882 if can_transfer:
1883 item = gtk.ImageMenuItem(self.get_device_name())
1884 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
1885 item.connect('button-press-event', lambda w, ee: self.on_sync_to_ipod_activate(w, episodes))
1886 share_menu.append(self.set_finger_friendly(item))
1888 share_item.set_submenu(share_menu)
1890 if (downloaded or one_is_new or can_download) and not downloading:
1891 menu.append(gtk.SeparatorMenuItem())
1892 if one_is_new:
1893 item = gtk.CheckMenuItem(_('New'))
1894 item.set_active(True)
1895 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1896 menu.append(self.set_finger_friendly(item))
1897 elif can_download:
1898 item = gtk.CheckMenuItem(_('New'))
1899 item.set_active(False)
1900 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1901 menu.append(self.set_finger_friendly(item))
1903 if downloaded:
1904 item = gtk.CheckMenuItem(_('Played'))
1905 item.set_active(any_played)
1906 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
1907 menu.append(self.set_finger_friendly(item))
1909 item = gtk.CheckMenuItem(_('Keep episode'))
1910 item.set_active(any_locked)
1911 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
1912 menu.append(self.set_finger_friendly(item))
1914 menu.append(gtk.SeparatorMenuItem())
1915 # Single item, add episode information menu item
1916 item = gtk.ImageMenuItem(_('Episode details'))
1917 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1918 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
1919 menu.append(self.set_finger_friendly(item))
1921 if gpodder.ui.maemo:
1922 # Because we open the popup on left-click for Maemo,
1923 # we also include a non-action to close the menu
1924 menu.append(gtk.SeparatorMenuItem())
1925 item = gtk.ImageMenuItem(_('Close this menu'))
1926 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1927 menu.append(self.set_finger_friendly(item))
1929 menu.show_all()
1930 # Disable tooltips while we are showing the menu, so
1931 # the tooltip will not appear over the menu
1932 self.treeview_allow_tooltips(self.treeAvailable, False)
1933 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
1934 menu.popup( None, None, None, event.button, event.time)
1936 return True
1938 def set_title(self, new_title):
1939 if not gpodder.ui.fremantle:
1940 self.default_title = new_title
1941 self.gPodder.set_title(new_title)
1943 def update_episode_list_icons(self, urls=None, selected=False, all=False):
1945 Updates the status icons in the episode list.
1947 If urls is given, it should be a list of URLs
1948 of episodes that should be updated.
1950 If urls is None, set ONE OF selected, all to
1951 True (the former updates just the selected
1952 episodes and the latter updates all episodes).
1954 additional_args = (self.episode_is_downloading, \
1955 self.config.episode_list_descriptions and gpodder.ui.desktop, \
1956 self.config.episode_list_thumbnails and gpodder.ui.desktop)
1958 if urls is not None:
1959 # We have a list of URLs to walk through
1960 self.episode_list_model.update_by_urls(urls, *additional_args)
1961 elif selected and not all:
1962 # We should update all selected episodes
1963 selection = self.treeAvailable.get_selection()
1964 model, paths = selection.get_selected_rows()
1965 for path in reversed(paths):
1966 iter = model.get_iter(path)
1967 self.episode_list_model.update_by_filter_iter(iter, \
1968 *additional_args)
1969 elif all and not selected:
1970 # We update all (even the filter-hidden) episodes
1971 self.episode_list_model.update_all(*additional_args)
1972 else:
1973 # Wrong/invalid call - have to specify at least one parameter
1974 raise ValueError('Invalid call to update_episode_list_icons')
1976 def episode_list_status_changed(self, episodes):
1977 self.update_episode_list_icons(set(e.url for e in episodes))
1978 self.update_podcast_list_model(set(e.channel.url for e in episodes))
1979 self.db.commit()
1981 def clean_up_downloads(self, delete_partial=False):
1982 # Clean up temporary files left behind by old gPodder versions
1983 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
1985 if delete_partial:
1986 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
1988 for tempfile in temporary_files:
1989 util.delete_file(tempfile)
1991 # Clean up empty download folders and abandoned download folders
1992 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
1993 for ddir in download_dirs:
1994 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1995 globr = glob.glob(os.path.join(ddir, '*'))
1996 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
1997 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
1998 shutil.rmtree(ddir, ignore_errors=True)
2000 def streaming_possible(self):
2001 if gpodder.ui.desktop:
2002 # User has to have a media player set on the Desktop, or else we
2003 # would probably open the browser when giving a URL to xdg-open..
2004 return (self.config.player and self.config.player != 'default')
2005 elif gpodder.ui.maemo:
2006 # On Maemo, the default is to use the Nokia Media Player, which is
2007 # already able to deal with HTTP URLs the right way, so we
2008 # unconditionally enable streaming always on Maemo
2009 return True
2011 return False
2013 def playback_episodes_for_real(self, episodes):
2014 groups = collections.defaultdict(list)
2015 for episode in episodes:
2016 file_type = episode.file_type()
2017 if file_type == 'video' and self.config.videoplayer and \
2018 self.config.videoplayer != 'default':
2019 player = self.config.videoplayer
2020 if gpodder.ui.diablo:
2021 # Use the wrapper script if it's installed to crop 3GP YouTube
2022 # videos to fit the screen (looks much nicer than w/ black border)
2023 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2024 player = 'gpodder-mplayer'
2025 elif gpodder.ui.fremantle and player == 'mplayer':
2026 player = 'mplayer -fs %F'
2027 elif file_type == 'audio' and self.config.player and \
2028 self.config.player != 'default':
2029 player = self.config.player
2030 else:
2031 player = 'default'
2033 if file_type not in ('audio', 'video') or \
2034 (file_type == 'audio' and not self.config.audio_played_dbus) or \
2035 (file_type == 'video' and not self.config.video_played_dbus):
2036 # Mark episode as played in the database
2037 episode.mark(is_played=True)
2038 self.mygpo_client.on_playback([episode])
2040 filename = episode.local_filename(create=False)
2041 if filename is None or not os.path.exists(filename):
2042 filename = episode.url
2043 if youtube.is_video_link(filename):
2044 fmt_id = self.config.youtube_preferred_fmt_id
2045 if gpodder.ui.fremantle:
2046 fmt_id = 5
2047 filename = youtube.get_real_download_url(filename, fmt_id)
2048 groups[player].append(filename)
2050 # Open episodes with system default player
2051 if 'default' in groups:
2052 for filename in groups['default']:
2053 log('Opening with system default: %s', filename, sender=self)
2054 util.gui_open(filename)
2055 del groups['default']
2056 elif gpodder.ui.maemo:
2057 # When on Maemo and not opening with default, show a notification
2058 # (no startup notification for Panucci / MPlayer yet...)
2059 if len(episodes) == 1:
2060 text = _('Opening %s') % episodes[0].title
2061 else:
2062 count = len(episodes)
2063 text = N_('Opening %d episode', 'Opening %d episodes', count) % count
2065 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2067 def destroy_banner_later(banner):
2068 banner.destroy()
2069 return False
2070 gobject.timeout_add(5000, destroy_banner_later, banner)
2072 # For each type now, go and create play commands
2073 for group in groups:
2074 for command in util.format_desktop_command(group, groups[group]):
2075 log('Executing: %s', repr(command), sender=self)
2076 subprocess.Popen(command)
2078 # Persist episode status changes to the database
2079 self.db.commit()
2081 # Flush updated episode status
2082 self.mygpo_client.flush()
2084 def playback_episodes(self, episodes):
2085 # We need to create a list, because we run through it more than once
2086 episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2087 e.was_downloaded(and_exists=True) or self.streaming_possible()))
2089 try:
2090 self.playback_episodes_for_real(episodes)
2091 except Exception, e:
2092 log('Error in playback!', sender=self, traceback=True)
2093 if gpodder.ui.desktop:
2094 self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2095 _('Error opening player'), widget=self.toolPreferences)
2096 else:
2097 self.show_message(_('Please check your media player settings in the preferences dialog.'))
2099 channel_urls = set()
2100 episode_urls = set()
2101 for episode in episodes:
2102 channel_urls.add(episode.channel.url)
2103 episode_urls.add(episode.url)
2104 self.update_episode_list_icons(episode_urls)
2105 self.update_podcast_list_model(channel_urls)
2107 def play_or_download(self):
2108 if not gpodder.ui.fremantle:
2109 if self.wNotebook.get_current_page() > 0:
2110 if gpodder.ui.desktop:
2111 self.toolCancel.set_sensitive(True)
2112 return
2114 if self.currently_updating:
2115 return (False, False, False, False, False, False)
2117 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2118 ( is_played, is_locked ) = (False,)*2
2120 open_instead_of_play = False
2122 selection = self.treeAvailable.get_selection()
2123 if selection.count_selected_rows() > 0:
2124 (model, paths) = selection.get_selected_rows()
2126 for path in paths:
2127 try:
2128 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2129 except TypeError, te:
2130 log('Invalid episode at path %s', str(path), sender=self)
2131 continue
2133 if episode.file_type() not in ('audio', 'video'):
2134 open_instead_of_play = True
2136 if episode.was_downloaded():
2137 can_play = episode.was_downloaded(and_exists=True)
2138 is_played = episode.is_played
2139 is_locked = episode.is_locked
2140 if not can_play:
2141 can_download = True
2142 else:
2143 if self.episode_is_downloading(episode):
2144 can_cancel = True
2145 else:
2146 can_download = True
2148 can_download = can_download and not can_cancel
2149 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2150 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2151 can_delete = not can_cancel
2153 if gpodder.ui.desktop:
2154 if open_instead_of_play:
2155 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2156 else:
2157 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2158 self.toolPlay.set_sensitive( can_play)
2159 self.toolDownload.set_sensitive( can_download)
2160 self.toolTransfer.set_sensitive( can_transfer)
2161 self.toolCancel.set_sensitive( can_cancel)
2163 if not gpodder.ui.fremantle:
2164 self.item_cancel_download.set_sensitive(can_cancel)
2165 self.itemDownloadSelected.set_sensitive(can_download)
2166 self.itemOpenSelected.set_sensitive(can_play)
2167 self.itemPlaySelected.set_sensitive(can_play)
2168 self.itemDeleteSelected.set_sensitive(can_delete)
2169 self.item_toggle_played.set_sensitive(can_play)
2170 self.item_toggle_lock.set_sensitive(can_play)
2171 self.itemOpenSelected.set_visible(open_instead_of_play)
2172 self.itemPlaySelected.set_visible(not open_instead_of_play)
2174 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2176 def on_cbMaxDownloads_toggled(self, widget, *args):
2177 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2179 def on_cbLimitDownloads_toggled(self, widget, *args):
2180 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2182 def episode_new_status_changed(self, urls):
2183 self.update_podcast_list_model()
2184 self.update_episode_list_icons(urls)
2186 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2187 """Update the podcast list treeview model
2189 If urls is given, it should list the URLs of each
2190 podcast that has to be updated in the list.
2192 If selected is True, only update the model contents
2193 for the currently-selected podcast - nothing more.
2195 The caller can optionally specify "select_url",
2196 which is the URL of the podcast that is to be
2197 selected in the list after the update is complete.
2198 This only works if the podcast list has to be
2199 reloaded; i.e. something has been added or removed
2200 since the last update of the podcast list).
2202 selection = self.treeChannels.get_selection()
2203 model, iter = selection.get_selected()
2205 if self.config.podcast_list_view_all and not self.channel_list_changed:
2206 # Update "all episodes" view in any case (if enabled)
2207 self.podcast_list_model.update_first_row()
2209 if selected:
2210 # very cheap! only update selected channel
2211 if iter is not None:
2212 # If we have selected the "all episodes" view, we have
2213 # to update all channels for selected episodes:
2214 if self.config.podcast_list_view_all and \
2215 self.podcast_list_model.iter_is_first_row(iter):
2216 urls = self.get_podcast_urls_from_selected_episodes()
2217 self.podcast_list_model.update_by_urls(urls)
2218 else:
2219 # Otherwise just update the selected row (a podcast)
2220 self.podcast_list_model.update_by_filter_iter(iter)
2221 elif not self.channel_list_changed:
2222 # we can keep the model, but have to update some
2223 if urls is None:
2224 # still cheaper than reloading the whole list
2225 self.podcast_list_model.update_all()
2226 else:
2227 # ok, we got a bunch of urls to update
2228 self.podcast_list_model.update_by_urls(urls)
2229 else:
2230 if model and iter and select_url is None:
2231 # Get the URL of the currently-selected podcast
2232 select_url = model.get_value(iter, PodcastListModel.C_URL)
2234 # Update the podcast list model with new channels
2235 self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2237 try:
2238 selected_iter = model.get_iter_first()
2239 # Find the previously-selected URL in the new
2240 # model if we have an URL (else select first)
2241 if select_url is not None:
2242 pos = model.get_iter_first()
2243 while pos is not None:
2244 url = model.get_value(pos, PodcastListModel.C_URL)
2245 if url == select_url:
2246 selected_iter = pos
2247 break
2248 pos = model.iter_next(pos)
2250 if not gpodder.ui.fremantle:
2251 if selected_iter is not None:
2252 selection.select_iter(selected_iter)
2253 self.on_treeChannels_cursor_changed(self.treeChannels)
2254 except:
2255 log('Cannot select podcast in list', traceback=True, sender=self)
2256 self.channel_list_changed = False
2258 def episode_is_downloading(self, episode):
2259 """Returns True if the given episode is being downloaded at the moment"""
2260 if episode is None:
2261 return False
2263 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2265 def update_episode_list_model(self):
2266 if self.channels and self.active_channel is not None:
2267 if gpodder.ui.fremantle:
2268 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2270 self.currently_updating = True
2271 self.episode_list_model.clear()
2272 self.episode_list_model.reset_update_progress()
2273 self.treeAvailable.set_model(self.empty_episode_list_model)
2274 def do_update_episode_list_model():
2275 additional_args = (self.episode_is_downloading, \
2276 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2277 self.config.episode_list_thumbnails and gpodder.ui.desktop, \
2278 self.treeAvailable)
2279 self.episode_list_model.add_from_channel(self.active_channel, *additional_args)
2281 def on_episode_list_model_updated():
2282 if gpodder.ui.fremantle:
2283 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2284 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
2285 self.treeAvailable.columns_autosize()
2286 self.currently_updating = False
2287 self.play_or_download()
2288 util.idle_add(on_episode_list_model_updated)
2289 threading.Thread(target=do_update_episode_list_model).start()
2290 else:
2291 self.episode_list_model.clear()
2293 def offer_new_episodes(self, channels=None):
2294 new_episodes = self.get_new_episodes(channels)
2295 if new_episodes:
2296 self.new_episodes_show(new_episodes)
2297 return True
2298 return False
2300 def add_podcast_list(self, urls, auth_tokens=None):
2301 """Subscribe to a list of podcast given their URLs
2303 If auth_tokens is given, it should be a dictionary
2304 mapping URLs to (username, password) tuples."""
2306 if auth_tokens is None:
2307 auth_tokens = {}
2309 # Sort and split the URL list into five buckets
2310 queued, failed, existing, worked, authreq = [], [], [], [], []
2311 for input_url in urls:
2312 url = util.normalize_feed_url(input_url)
2313 if url is None:
2314 # Fail this one because the URL is not valid
2315 failed.append(input_url)
2316 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2317 # A podcast already exists in the list for this URL
2318 existing.append(url)
2319 else:
2320 # This URL has survived the first round - queue for add
2321 queued.append(url)
2322 if url != input_url and input_url in auth_tokens:
2323 auth_tokens[url] = auth_tokens[input_url]
2325 error_messages = {}
2326 redirections = {}
2328 progress = ProgressIndicator(_('Adding podcasts'), \
2329 _('Please wait while episode information is downloaded.'), \
2330 parent=self.get_dialog_parent())
2332 def on_after_update():
2333 progress.on_finished()
2334 # Report already-existing subscriptions to the user
2335 if existing:
2336 title = _('Existing subscriptions skipped')
2337 message = _('You are already subscribed to these podcasts:') \
2338 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2339 self.show_message(message, title, widget=self.treeChannels)
2341 # Report subscriptions that require authentication
2342 if authreq:
2343 retry_podcasts = {}
2344 for url in authreq:
2345 title = _('Podcast requires authentication')
2346 message = _('Please login to %s:') % (saxutils.escape(url),)
2347 success, auth_tokens = self.show_login_dialog(title, message)
2348 if success:
2349 retry_podcasts[url] = auth_tokens
2350 else:
2351 # Stop asking the user for more login data
2352 retry_podcasts = {}
2353 for url in authreq:
2354 error_messages[url] = _('Authentication failed')
2355 failed.append(url)
2356 break
2358 # If we have authentication data to retry, do so here
2359 if retry_podcasts:
2360 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2362 # Report website redirections
2363 for url in redirections:
2364 title = _('Website redirection detected')
2365 message = _('The URL %(url)s redirects to %(target)s.') \
2366 + '\n\n' + _('Do you want to visit the website now?')
2367 message = message % {'url': url, 'target': redirections[url]}
2368 if self.show_confirmation(message, title):
2369 util.open_website(url)
2370 else:
2371 break
2373 # Report failed subscriptions to the user
2374 if failed:
2375 title = _('Could not add some podcasts')
2376 message = _('Some podcasts could not be added to your list:') \
2377 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2378 error_messages.get(url, _('Unknown')))) for url in failed)
2379 self.show_message(message, title, important=True)
2381 # Upload subscription changes to gpodder.net
2382 self.mygpo_client.on_subscribe(worked)
2384 # If at least one podcast has been added, save and update all
2385 if self.channel_list_changed:
2386 # Fix URLs if mygpo has rewritten them
2387 self.rewrite_urls_mygpo()
2389 self.save_channels_opml()
2391 # If only one podcast was added, select it after the update
2392 if len(worked) == 1:
2393 url = worked[0]
2394 else:
2395 url = None
2397 # Update the list of subscribed podcasts
2398 self.update_feed_cache(force_update=False, select_url_afterwards=url)
2399 self.update_podcasts_tab()
2401 # Offer to download new episodes
2402 self.offer_new_episodes(channels=[c for c in self.channels if c.url in worked])
2404 def thread_proc():
2405 # After the initial sorting and splitting, try all queued podcasts
2406 length = len(queued)
2407 for index, url in enumerate(queued):
2408 progress.on_progress(float(index)/float(length))
2409 progress.on_message(url)
2410 log('QUEUE RUNNER: %s', url, sender=self)
2411 try:
2412 # The URL is valid and does not exist already - subscribe!
2413 channel = PodcastChannel.load(self.db, url=url, create=True, \
2414 authentication_tokens=auth_tokens.get(url, None), \
2415 max_episodes=self.config.max_episodes_per_feed, \
2416 download_dir=self.config.download_dir, \
2417 allow_empty_feeds=self.config.allow_empty_feeds)
2419 try:
2420 username, password = util.username_password_from_url(url)
2421 except ValueError, ve:
2422 username, password = (None, None)
2424 if username is not None and channel.username is None and \
2425 password is not None and channel.password is None:
2426 channel.username = username
2427 channel.password = password
2428 channel.save()
2430 self._update_cover(channel)
2431 except feedcore.AuthenticationRequired:
2432 if url in auth_tokens:
2433 # Fail for wrong authentication data
2434 error_messages[url] = _('Authentication failed')
2435 failed.append(url)
2436 else:
2437 # Queue for login dialog later
2438 authreq.append(url)
2439 continue
2440 except feedcore.WifiLogin, error:
2441 redirections[url] = error.data
2442 failed.append(url)
2443 error_messages[url] = _('Redirection detected')
2444 continue
2445 except Exception, e:
2446 log('Subscription error: %s', e, traceback=True, sender=self)
2447 error_messages[url] = str(e)
2448 failed.append(url)
2449 continue
2451 assert channel is not None
2452 worked.append(channel.url)
2453 self.channels.append(channel)
2454 self.channel_list_changed = True
2455 util.idle_add(on_after_update)
2456 threading.Thread(target=thread_proc).start()
2458 def save_channels_opml(self):
2459 exporter = opml.Exporter(gpodder.subscription_file)
2460 return exporter.write(self.channels)
2462 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2463 self.db.commit()
2464 self.updating_feed_cache = False
2466 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2467 self.channel_list_changed = True
2468 self.update_podcast_list_model(select_url=select_url_afterwards)
2470 # Only search for new episodes in podcasts that have been
2471 # updated, not in other podcasts (for single-feed updates)
2472 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2474 if gpodder.ui.fremantle:
2475 self.button_subscribe.set_sensitive(True)
2476 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
2477 self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
2478 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2479 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2480 self.update_podcasts_tab()
2481 self.update_episode_list_model()
2482 if self.feed_cache_update_cancelled:
2483 return
2485 if episodes:
2486 if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2487 # New episodes found, but we should do nothing
2488 self.show_message(_('New episodes are available.'))
2489 elif self.config.auto_download == 'always':
2490 count = len(episodes)
2491 title = N_('Downloading %d new episode.', 'Downloading %d new episodes.', count) % count
2492 self.show_message(title)
2493 self.download_episode_list(episodes)
2494 elif self.config.auto_download == 'queue':
2495 self.show_message(_('New episodes have been added to the download list.'))
2496 self.download_episode_list_paused(episodes)
2497 else:
2498 self.new_episodes_show(episodes)
2499 elif not self.config.auto_update_feeds:
2500 self.show_message(_('No new episodes. Please check for new episodes later.'))
2501 return
2503 if self.tray_icon:
2504 self.tray_icon.set_status()
2506 if self.feed_cache_update_cancelled:
2507 # The user decided to abort the feed update
2508 self.show_update_feeds_buttons()
2509 elif not episodes:
2510 # Nothing new here - but inform the user
2511 self.pbFeedUpdate.set_fraction(1.0)
2512 self.pbFeedUpdate.set_text(_('No new episodes'))
2513 self.feed_cache_update_cancelled = True
2514 self.btnCancelFeedUpdate.show()
2515 self.btnCancelFeedUpdate.set_sensitive(True)
2516 if gpodder.ui.maemo:
2517 # btnCancelFeedUpdate is a ToolButton on Maemo
2518 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2519 else:
2520 # btnCancelFeedUpdate is a normal gtk.Button
2521 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2522 else:
2523 count = len(episodes)
2524 # New episodes are available
2525 self.pbFeedUpdate.set_fraction(1.0)
2526 # Are we minimized and should we auto download?
2527 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2528 self.download_episode_list(episodes)
2529 title = N_('Downloading %d new episode.', 'Downloading %d new episodes.', count) % count
2530 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2531 self.show_update_feeds_buttons()
2532 elif self.config.auto_download == 'queue':
2533 self.download_episode_list_paused(episodes)
2534 title = N_('%d new episode added to download list.', '%d new episodes added to download list.', count) % count
2535 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2536 self.show_update_feeds_buttons()
2537 else:
2538 self.show_update_feeds_buttons()
2539 # New episodes are available and we are not minimized
2540 if not self.config.do_not_show_new_episodes_dialog:
2541 self.new_episodes_show(episodes, notification=True)
2542 else:
2543 message = N_('%d new episode available', '%d new episodes available', count) % count
2544 self.pbFeedUpdate.set_text(message)
2546 def _update_cover(self, channel):
2547 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2548 self.cover_downloader.request_cover(channel)
2550 def update_feed_cache_proc(self, channels, select_url_afterwards):
2551 total = len(channels)
2553 for updated, channel in enumerate(channels):
2554 if not self.feed_cache_update_cancelled:
2555 try:
2556 channel.update(max_episodes=self.config.max_episodes_per_feed)
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 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2637 self.button_refresh.set_title(_('Updating...'))
2638 self.button_subscribe.set_sensitive(False)
2639 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
2640 self.ICON_GENERAL_CLOSE, gtk.ICON_SIZE_BUTTON))
2641 self.feed_cache_update_cancelled = False
2642 else:
2643 self.itemUpdate.set_sensitive(False)
2644 self.itemUpdateChannel.set_sensitive(False)
2646 if self.tray_icon:
2647 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2649 if len(channels) == 1:
2650 text = _('Updating "%s"...') % channels[0].title
2651 else:
2652 count = len(channels)
2653 text = N_('Updating %d feed...', 'Updating %d feeds...', count) % count
2654 self.pbFeedUpdate.set_text(text)
2655 self.pbFeedUpdate.set_fraction(0)
2657 self.feed_cache_update_cancelled = False
2658 self.btnCancelFeedUpdate.show()
2659 self.btnCancelFeedUpdate.set_sensitive(True)
2660 if gpodder.ui.maemo:
2661 self.toolbarSpacer.set_expand(False)
2662 self.toolbarSpacer.set_draw(True)
2663 self.btnUpdateSelectedFeed.hide()
2664 self.toolFeedUpdateProgress.show_all()
2665 else:
2666 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2667 self.hboxUpdateFeeds.show_all()
2668 self.btnUpdateFeeds.hide()
2670 args = (channels, select_url_afterwards)
2671 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
2673 def on_gPodder_delete_event(self, widget, *args):
2674 """Called when the GUI wants to close the window
2675 Displays a confirmation dialog (and closes/hides gPodder)
2678 downloading = self.download_status_model.are_downloads_in_progress()
2680 # Only iconify if we are using the window's "X" button,
2681 # but not when we are using "Quit" in the menu or toolbar
2682 if self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
2683 self.iconify_main_window()
2684 elif self.config.on_quit_ask or downloading:
2685 if gpodder.ui.fremantle:
2686 self.close_gpodder()
2687 elif gpodder.ui.diablo:
2688 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2689 if result:
2690 self.close_gpodder()
2691 else:
2692 return True
2693 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2694 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2695 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2697 title = _('Quit gPodder')
2698 if downloading:
2699 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2700 else:
2701 message = _('Do you really want to quit gPodder now?')
2703 dialog.set_title(title)
2704 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2705 if not downloading:
2706 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2707 dialog.vbox.pack_start(cb_ask)
2708 cb_ask.show_all()
2710 quit_button.grab_focus()
2711 result = dialog.run()
2712 dialog.destroy()
2714 if result == gtk.RESPONSE_CLOSE:
2715 if not downloading and cb_ask.get_active() == True:
2716 self.config.on_quit_ask = False
2717 self.close_gpodder()
2718 else:
2719 self.close_gpodder()
2721 return True
2723 def close_gpodder(self):
2724 """ clean everything and exit properly
2726 if self.channels:
2727 if self.save_channels_opml():
2728 pass # FIXME: Add mygpo synchronization here
2729 else:
2730 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
2732 self.gPodder.hide()
2734 if self.tray_icon is not None:
2735 self.tray_icon.set_visible(False)
2737 # Notify all tasks to to carry out any clean-up actions
2738 self.download_status_model.tell_all_tasks_to_quit()
2740 while gtk.events_pending():
2741 gtk.main_iteration(False)
2743 self.db.close()
2745 self.quit()
2746 sys.exit(0)
2748 def get_expired_episodes(self):
2749 for channel in self.channels:
2750 for episode in channel.get_downloaded_episodes():
2751 # Never consider locked episodes as old
2752 if episode.is_locked:
2753 continue
2755 # Never consider fresh episodes as old
2756 if episode.age_in_days() < self.config.episode_old_age:
2757 continue
2759 # Do not delete played episodes (except if configured)
2760 if episode.is_played:
2761 if not self.config.auto_remove_played_episodes:
2762 continue
2764 # Do not delete unplayed episodes (except if configured)
2765 if not episode.is_played:
2766 if not self.config.auto_remove_unplayed_episodes:
2767 continue
2769 yield episode
2771 def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
2772 if not episodes:
2773 return False
2775 if skip_locked:
2776 episodes = [e for e in episodes if not e.is_locked]
2778 if not episodes:
2779 title = _('Episodes are locked')
2780 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2781 self.notification(message, title, widget=self.treeAvailable)
2782 return False
2784 count = len(episodes)
2785 title = N_('Delete %d episode?', 'Delete %d episodes?', count) % count
2786 message = _('Deleting episodes removes downloaded files.')
2788 if gpodder.ui.fremantle:
2789 message = '\n'.join([title, message])
2791 if confirm and not self.show_confirmation(message, title):
2792 return False
2794 progress = ProgressIndicator(_('Deleting episodes'), \
2795 _('Please wait while episodes are deleted'), \
2796 parent=self.get_dialog_parent())
2798 def finish_deletion(episode_urls, channel_urls):
2799 progress.on_finished()
2801 # Episodes have been deleted - persist the database
2802 self.db.commit()
2804 self.update_episode_list_icons(episode_urls)
2805 self.update_podcast_list_model(channel_urls)
2806 self.play_or_download()
2808 def thread_proc():
2809 episode_urls = set()
2810 channel_urls = set()
2812 episodes_status_update = []
2813 for idx, episode in enumerate(episodes):
2814 progress.on_progress(float(idx)/float(len(episodes)))
2815 if episode.is_locked and skip_locked:
2816 log('Not deleting episode (is locked): %s', episode.title)
2817 else:
2818 log('Deleting episode: %s', episode.title)
2819 progress.on_message(episode.title)
2820 episode.delete_from_disk()
2821 episode_urls.add(episode.url)
2822 channel_urls.add(episode.channel.url)
2823 episodes_status_update.append(episode)
2825 # Tell the shownotes window that we have removed the episode
2826 if self.episode_shownotes_window is not None and \
2827 self.episode_shownotes_window.episode is not None and \
2828 self.episode_shownotes_window.episode.url == episode.url:
2829 util.idle_add(self.episode_shownotes_window._download_status_changed, None)
2831 # Notify the web service about the status update + upload
2832 self.mygpo_client.on_delete(episodes_status_update)
2833 self.mygpo_client.flush()
2835 util.idle_add(finish_deletion, episode_urls, channel_urls)
2837 threading.Thread(target=thread_proc).start()
2839 return True
2841 def on_itemRemoveOldEpisodes_activate( self, widget):
2842 if gpodder.ui.maemo:
2843 columns = (
2844 ('maemo_remove_markup', None, None, _('Episode')),
2846 else:
2847 columns = (
2848 ('title_markup', None, None, _('Episode')),
2849 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2850 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2851 ('played_prop', None, None, _('Status')),
2852 ('age_prop', None, None, _('Downloaded')),
2855 msg_older_than = N_('Select older than %d day', 'Select older than %d days', self.config.episode_old_age)
2856 selection_buttons = {
2857 _('Select played'): lambda episode: episode.is_played,
2858 msg_older_than % self.config.episode_old_age: lambda episode: episode.age_in_days() > self.config.episode_old_age,
2861 instructions = _('Select the episodes you want to delete:')
2863 episodes = []
2864 selected = []
2865 for channel in self.channels:
2866 for episode in channel.get_downloaded_episodes():
2867 # Disallow deletion of locked episodes that still exist
2868 if not episode.is_locked or not episode.file_exists():
2869 episodes.append(episode)
2870 # Automatically select played and file-less episodes
2871 selected.append(episode.is_played or \
2872 not episode.file_exists())
2874 gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
2875 episodes = episodes, selected = selected, columns = columns, \
2876 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2877 selection_buttons = selection_buttons, _config=self.config, \
2878 show_episode_shownotes=self.show_episode_shownotes)
2880 def on_selected_episodes_status_changed(self):
2881 self.update_episode_list_icons(selected=True)
2882 self.update_podcast_list_model(selected=True)
2883 self.db.commit()
2885 def mark_selected_episodes_new(self):
2886 for episode in self.get_selected_episodes():
2887 episode.mark_new()
2888 self.on_selected_episodes_status_changed()
2890 def mark_selected_episodes_old(self):
2891 for episode in self.get_selected_episodes():
2892 episode.mark_old()
2893 self.on_selected_episodes_status_changed()
2895 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2896 for episode in self.get_selected_episodes():
2897 if toggle:
2898 episode.mark(is_played=not episode.is_played)
2899 else:
2900 episode.mark(is_played=new_value)
2901 self.on_selected_episodes_status_changed()
2903 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2904 for episode in self.get_selected_episodes():
2905 if toggle:
2906 episode.mark(is_locked=not episode.is_locked)
2907 else:
2908 episode.mark(is_locked=new_value)
2909 self.on_selected_episodes_status_changed()
2911 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2912 if self.active_channel is None:
2913 return
2915 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2916 self.active_channel.update_channel_lock()
2918 for episode in self.active_channel.get_all_episodes():
2919 episode.mark(is_locked=self.active_channel.channel_is_locked)
2921 self.update_podcast_list_model(selected=True)
2922 self.update_episode_list_icons(all=True)
2924 def on_itemUpdateChannel_activate(self, widget=None):
2925 if self.active_channel is None:
2926 title = _('No podcast selected')
2927 message = _('Please select a podcast in the podcasts list to update.')
2928 self.show_message( message, title, widget=self.treeChannels)
2929 return
2931 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
2932 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
2933 self.update_feed_cache()
2934 else:
2935 self.update_feed_cache(channels=[self.active_channel])
2937 def on_itemUpdate_activate(self, widget=None):
2938 # Check if we have outstanding subscribe/unsubscribe actions
2939 if self.on_add_remove_podcasts_mygpo():
2940 log('Update cancelled (received server changes)', sender=self)
2941 return
2943 if self.channels:
2944 self.update_feed_cache()
2945 else:
2946 gPodderWelcome(self.gPodder,
2947 center_on_widget=self.gPodder,
2948 show_example_podcasts_callback=self.on_itemImportChannels_activate,
2949 setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
2951 def download_episode_list_paused(self, episodes):
2952 self.download_episode_list(episodes, True)
2954 def download_episode_list(self, episodes, add_paused=False, force_start=False):
2955 for episode in episodes:
2956 log('Downloading episode: %s', episode.title, sender = self)
2957 if not episode.was_downloaded(and_exists=True):
2958 task_exists = False
2959 for task in self.download_tasks_seen:
2960 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2961 self.download_queue_manager.add_task(task, force_start)
2962 self.enable_download_list_update()
2963 task_exists = True
2964 continue
2966 if task_exists:
2967 continue
2969 try:
2970 task = download.DownloadTask(episode, self.config)
2971 except Exception, e:
2972 d = {'episode': episode.title, 'message': str(e)}
2973 message = _('Download error while downloading %(episode)s: %(message)s')
2974 self.show_message(message % d, _('Download error'), important=True)
2975 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
2976 continue
2978 if add_paused:
2979 task.status = task.PAUSED
2980 else:
2981 self.mygpo_client.on_download([task.episode])
2982 self.download_queue_manager.add_task(task, force_start)
2984 self.download_status_model.register_task(task)
2985 self.enable_download_list_update()
2987 # Flush updated episode status
2988 self.mygpo_client.flush()
2990 def cancel_task_list(self, tasks):
2991 if not tasks:
2992 return
2994 for task in tasks:
2995 if task.status in (task.QUEUED, task.DOWNLOADING):
2996 task.status = task.CANCELLED
2997 elif task.status == task.PAUSED:
2998 task.status = task.CANCELLED
2999 # Call run, so the partial file gets deleted
3000 task.run()
3002 self.update_episode_list_icons([task.url for task in tasks])
3003 self.play_or_download()
3005 # Update the tab title and downloads list
3006 self.update_downloads_list()
3008 def new_episodes_show(self, episodes, notification=False):
3009 if gpodder.ui.maemo:
3010 columns = (
3011 ('maemo_markup', None, None, _('Episode')),
3013 show_notification = notification
3014 else:
3015 columns = (
3016 ('title_markup', None, None, _('Episode')),
3017 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3018 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3020 show_notification = False
3022 instructions = _('Select the episodes you want to download:')
3024 if self.new_episodes_window is not None:
3025 self.new_episodes_window.main_window.destroy()
3026 self.new_episodes_window = None
3028 def download_episodes_callback(episodes):
3029 self.new_episodes_window = None
3030 self.download_episode_list(episodes)
3032 self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3033 title=_('New episodes available'), \
3034 instructions=instructions, \
3035 episodes=episodes, \
3036 columns=columns, \
3037 selected_default=True, \
3038 stock_ok_button = 'gpodder-download', \
3039 callback=download_episodes_callback, \
3040 remove_callback=lambda e: e.mark_old(), \
3041 remove_action=_('Mark as old'), \
3042 remove_finished=self.episode_new_status_changed, \
3043 _config=self.config, \
3044 show_notification=show_notification, \
3045 show_episode_shownotes=self.show_episode_shownotes)
3047 def on_itemDownloadAllNew_activate(self, widget, *args):
3048 if not self.offer_new_episodes():
3049 self.show_message(_('Please check for new episodes later.'), \
3050 _('No new episodes available'), widget=self.btnUpdateFeeds)
3052 def get_new_episodes(self, channels=None):
3053 if channels is None:
3054 channels = self.channels
3055 episodes = []
3056 for channel in channels:
3057 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3058 episodes.append(episode)
3060 return episodes
3062 def on_sync_to_ipod_activate(self, widget, episodes=None):
3063 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
3065 def commit_changes_to_database(self):
3066 """This will be called after the sync process is finished"""
3067 self.db.commit()
3069 def on_cleanup_ipod_activate(self, widget, *args):
3070 self.sync_ui.on_cleanup_device()
3072 def on_manage_device_playlist(self, widget):
3073 self.sync_ui.on_manage_device_playlist()
3075 def show_hide_tray_icon(self):
3076 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3077 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3078 elif not self.config.display_tray_icon and self.tray_icon is not None:
3079 self.tray_icon.set_visible(False)
3080 del self.tray_icon
3081 self.tray_icon = None
3083 if self.config.minimize_to_tray and self.tray_icon:
3084 self.tray_icon.set_visible(self.is_iconified())
3085 elif self.tray_icon:
3086 self.tray_icon.set_visible(True)
3088 def on_itemShowAllEpisodes_activate(self, widget):
3089 self.config.podcast_list_view_all = widget.get_active()
3091 def on_itemShowToolbar_activate(self, widget):
3092 self.config.show_toolbar = self.itemShowToolbar.get_active()
3094 def on_itemShowDescription_activate(self, widget):
3095 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3097 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3098 self.config.podcast_list_hide_boring = toggleaction.get_active()
3099 if self.config.podcast_list_hide_boring:
3100 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3101 else:
3102 self.podcast_list_model.set_view_mode(-1)
3104 def on_item_view_podcasts_changed(self, radioaction, current):
3105 # Only on Fremantle
3106 if current == self.item_view_podcasts_all:
3107 self.podcast_list_model.set_view_mode(-1)
3108 elif current == self.item_view_podcasts_downloaded:
3109 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3110 elif current == self.item_view_podcasts_unplayed:
3111 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3113 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3115 def on_item_view_episodes_changed(self, radioaction, current):
3116 if current == self.item_view_episodes_all:
3117 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_ALL)
3118 elif current == self.item_view_episodes_undeleted:
3119 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNDELETED)
3120 elif current == self.item_view_episodes_downloaded:
3121 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3122 elif current == self.item_view_episodes_unplayed:
3123 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3125 self.config.episode_list_view_mode = self.episode_list_model.get_view_mode()
3127 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3128 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3130 def update_item_device( self):
3131 if not gpodder.ui.fremantle:
3132 if self.config.device_type != 'none':
3133 self.itemDevice.set_visible(True)
3134 self.itemDevice.label = self.get_device_name()
3135 else:
3136 self.itemDevice.set_visible(False)
3138 def properties_closed( self):
3139 self.preferences_dialog = None
3140 self.show_hide_tray_icon()
3141 self.update_item_device()
3142 if gpodder.ui.maemo:
3143 selection = self.treeAvailable.get_selection()
3144 if self.config.maemo_enable_gestures or \
3145 self.config.enable_fingerscroll:
3146 selection.set_mode(gtk.SELECTION_SINGLE)
3147 else:
3148 selection.set_mode(gtk.SELECTION_MULTIPLE)
3150 def on_itemPreferences_activate(self, widget, *args):
3151 self.preferences_dialog = gPodderPreferences(self.main_window, \
3152 _config=self.config, \
3153 callback_finished=self.properties_closed, \
3154 user_apps_reader=self.user_apps_reader, \
3155 parent_window=self.main_window, \
3156 mygpo_client=self.mygpo_client, \
3157 on_send_full_subscriptions=self.on_send_full_subscriptions)
3159 # Initial message to relayout window (in case it's opened in portrait mode
3160 self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3162 def on_itemDependencies_activate(self, widget):
3163 gPodderDependencyManager(self.gPodder)
3165 def on_goto_mygpo(self, widget):
3166 self.mygpo_client.open_website()
3168 def on_download_subscriptions_from_mygpo(self, action=None):
3169 title = _('Login to gpodder.net')
3170 message = _('Please login to download your subscriptions.')
3171 success, (username, password) = self.show_login_dialog(title, message, \
3172 self.config.mygpo_username, self.config.mygpo_password)
3173 if not success:
3174 return
3176 self.config.mygpo_username = username
3177 self.config.mygpo_password = password
3179 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3180 custom_title=_('Subscriptions on gpodder.net'), \
3181 add_urls_callback=self.add_podcast_list, \
3182 hide_url_entry=True)
3184 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3185 # we do not have to hardcode the URL here
3186 OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
3187 url = util.url_add_authentication(OPML_URL, \
3188 self.config.mygpo_username, \
3189 self.config.mygpo_password)
3190 dir.download_opml_file(url)
3192 def on_mygpo_settings_activate(self, action=None):
3193 # This dialog is only used for Maemo 4
3194 if not gpodder.ui.diablo:
3195 return
3197 settings = MygPodderSettings(self.main_window, \
3198 config=self.config, \
3199 mygpo_client=self.mygpo_client, \
3200 on_send_full_subscriptions=self.on_send_full_subscriptions)
3202 def on_itemAddChannel_activate(self, widget=None):
3203 gPodderAddPodcast(self.gPodder, \
3204 add_urls_callback=self.add_podcast_list)
3206 def on_itemEditChannel_activate(self, widget, *args):
3207 if self.active_channel is None:
3208 title = _('No podcast selected')
3209 message = _('Please select a podcast in the podcasts list to edit.')
3210 self.show_message( message, title, widget=self.treeChannels)
3211 return
3213 callback_closed = lambda: self.update_podcast_list_model(selected=True)
3214 gPodderChannel(self.main_window, \
3215 channel=self.active_channel, \
3216 callback_closed=callback_closed, \
3217 cover_downloader=self.cover_downloader)
3219 def on_itemMassUnsubscribe_activate(self, item=None):
3220 columns = (
3221 ('title', None, None, _('Podcast')),
3224 # We're abusing the Episode Selector for selecting Podcasts here,
3225 # but it works and looks good, so why not? -- thp
3226 gPodderEpisodeSelector(self.main_window, \
3227 title=_('Remove podcasts'), \
3228 instructions=_('Select the podcast you want to remove.'), \
3229 episodes=self.channels, \
3230 columns=columns, \
3231 size_attribute=None, \
3232 stock_ok_button=gtk.STOCK_DELETE, \
3233 callback=self.remove_podcast_list, \
3234 _config=self.config)
3236 def remove_podcast_list(self, channels, confirm=True):
3237 if not channels:
3238 log('No podcasts selected for deletion', sender=self)
3239 return
3241 if len(channels) == 1:
3242 title = _('Removing podcast')
3243 info = _('Please wait while the podcast is removed')
3244 message = _('Do you really want to remove this podcast and its episodes?')
3245 else:
3246 title = _('Removing podcasts')
3247 info = _('Please wait while the podcasts are removed')
3248 message = _('Do you really want to remove the selected podcasts and their episodes?')
3250 if confirm and not self.show_confirmation(message, title):
3251 return
3253 progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3255 def finish_deletion(select_url):
3256 # Upload subscription list changes to the web service
3257 self.mygpo_client.on_unsubscribe([c.url for c in channels])
3259 # Re-load the channels and select the desired new channel
3260 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3261 progress.on_finished()
3262 self.update_podcasts_tab()
3264 def thread_proc():
3265 select_url = None
3267 for idx, channel in enumerate(channels):
3268 # Update the UI for correct status messages
3269 progress.on_progress(float(idx)/float(len(channels)))
3270 progress.on_message(channel.title)
3272 # Delete downloaded episodes
3273 channel.remove_downloaded()
3275 # cancel any active downloads from this channel
3276 for episode in channel.get_all_episodes():
3277 util.idle_add(self.download_status_model.cancel_by_url,
3278 episode.url)
3280 if len(channels) == 1:
3281 # get the URL of the podcast we want to select next
3282 if channel in self.channels:
3283 position = self.channels.index(channel)
3284 else:
3285 position = -1
3287 if position == len(self.channels)-1:
3288 # this is the last podcast, so select the URL
3289 # of the item before this one (i.e. the "new last")
3290 select_url = self.channels[position-1].url
3291 else:
3292 # there is a podcast after the deleted one, so
3293 # we simply select the one that comes after it
3294 select_url = self.channels[position+1].url
3296 # Remove the channel and clean the database entries
3297 channel.delete()
3298 self.channels.remove(channel)
3300 # Clean up downloads and download directories
3301 self.clean_up_downloads()
3303 self.channel_list_changed = True
3304 self.save_channels_opml()
3306 # The remaining stuff is to be done in the GTK main thread
3307 util.idle_add(finish_deletion, select_url)
3309 threading.Thread(target=thread_proc).start()
3311 def on_itemRemoveChannel_activate(self, widget, *args):
3312 if self.active_channel is None:
3313 title = _('No podcast selected')
3314 message = _('Please select a podcast in the podcasts list to remove.')
3315 self.show_message( message, title, widget=self.treeChannels)
3316 return
3318 self.remove_podcast_list([self.active_channel])
3320 def get_opml_filter(self):
3321 filter = gtk.FileFilter()
3322 filter.add_pattern('*.opml')
3323 filter.add_pattern('*.xml')
3324 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3325 return filter
3327 def on_item_import_from_file_activate(self, widget, filename=None):
3328 if filename is None:
3329 if gpodder.ui.desktop or gpodder.ui.fremantle:
3330 # FIXME: Hildonization on Fremantle
3331 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3332 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3333 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3334 elif gpodder.ui.diablo:
3335 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3336 dlg.set_filter(self.get_opml_filter())
3337 response = dlg.run()
3338 filename = None
3339 if response == gtk.RESPONSE_OK:
3340 filename = dlg.get_filename()
3341 dlg.destroy()
3343 if filename is not None:
3344 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3345 custom_title=_('Import podcasts from OPML file'), \
3346 add_urls_callback=self.add_podcast_list, \
3347 hide_url_entry=True)
3348 dir.download_opml_file(filename)
3350 def on_itemExportChannels_activate(self, widget, *args):
3351 if not self.channels:
3352 title = _('Nothing to export')
3353 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3354 self.show_message(message, title, widget=self.treeChannels)
3355 return
3357 if gpodder.ui.desktop or gpodder.ui.fremantle:
3358 # FIXME: Hildonization on Fremantle
3359 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3360 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3361 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3362 elif gpodder.ui.diablo:
3363 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3364 dlg.set_filter(self.get_opml_filter())
3365 response = dlg.run()
3366 if response == gtk.RESPONSE_OK:
3367 filename = dlg.get_filename()
3368 dlg.destroy()
3369 exporter = opml.Exporter( filename)
3370 if exporter.write(self.channels):
3371 count = len(self.channels)
3372 title = N_('%d subscription exported', '%d subscriptions exported', count) % count
3373 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3374 else:
3375 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3376 else:
3377 dlg.destroy()
3379 def on_itemImportChannels_activate(self, widget, *args):
3380 if gpodder.ui.fremantle:
3381 gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3382 self.config.toplist_url, \
3383 self.config.opml_url, \
3384 self.add_podcast_list, \
3385 self.on_itemAddChannel_activate, \
3386 self.on_download_subscriptions_from_mygpo, \
3387 self.show_text_edit_dialog)
3388 else:
3389 dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3390 add_urls_callback=self.add_podcast_list)
3391 util.idle_add(dir.download_opml_file, self.config.opml_url)
3393 def on_homepage_activate(self, widget, *args):
3394 util.open_website(gpodder.__url__)
3396 def on_wiki_activate(self, widget, *args):
3397 util.open_website('http://gpodder.org/wiki/User_Manual')
3399 def on_bug_tracker_activate(self, widget, *args):
3400 if gpodder.ui.maemo:
3401 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3402 else:
3403 util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3405 def on_item_support_activate(self, widget):
3406 util.open_website('http://gpodder.org/donate')
3408 def on_itemAbout_activate(self, widget, *args):
3409 if gpodder.ui.fremantle:
3410 from gpodder.gtkui.frmntl.about import HeAboutDialog
3411 HeAboutDialog.present(self.main_window,
3412 'gPodder',
3413 'gpodder',
3414 gpodder.__version__,
3415 _('A podcast client with focus on usability'),
3416 gpodder.__copyright__,
3417 gpodder.__url__,
3418 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3419 'http://gpodder.org/donate')
3420 return
3422 dlg = gtk.AboutDialog()
3423 dlg.set_transient_for(self.main_window)
3424 dlg.set_name('gPodder')
3425 dlg.set_version(gpodder.__version__)
3426 dlg.set_copyright(gpodder.__copyright__)
3427 dlg.set_comments(_('A podcast client with focus on usability'))
3428 dlg.set_website(gpodder.__url__)
3429 dlg.set_translator_credits( _('translator-credits'))
3430 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3432 if gpodder.ui.desktop:
3433 # For the "GUI" version, we add some more
3434 # items to the about dialog (credits and logo)
3435 app_authors = [
3436 _('Maintainer:'),
3437 'Thomas Perl <thpinfo.com>',
3440 if os.path.exists(gpodder.credits_file):
3441 credits = open(gpodder.credits_file).read().strip().split('\n')
3442 app_authors += ['', _('Patches, bug reports and donations by:')]
3443 app_authors += credits
3445 dlg.set_authors(app_authors)
3446 try:
3447 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3448 except:
3449 dlg.set_logo_icon_name('gpodder')
3451 dlg.run()
3453 def on_wNotebook_switch_page(self, widget, *args):
3454 page_num = args[1]
3455 if gpodder.ui.maemo:
3456 self.tool_downloads.set_active(page_num == 1)
3457 page = self.wNotebook.get_nth_page(page_num)
3458 tab_label = self.wNotebook.get_tab_label(page).get_text()
3459 if page_num == 0 and self.active_channel is not None:
3460 self.set_title(self.active_channel.title)
3461 else:
3462 self.set_title(tab_label)
3463 if page_num == 0:
3464 self.play_or_download()
3465 self.menuChannels.set_sensitive(True)
3466 self.menuSubscriptions.set_sensitive(True)
3467 # The message area in the downloads tab should be hidden
3468 # when the user switches away from the downloads tab
3469 if self.message_area is not None:
3470 self.message_area.hide()
3471 self.message_area = None
3472 else:
3473 # Remove finished episodes
3474 if self.config.auto_cleanup_downloads:
3475 self.on_btnCleanUpDownloads_clicked()
3477 self.menuChannels.set_sensitive(False)
3478 self.menuSubscriptions.set_sensitive(False)
3479 if gpodder.ui.desktop:
3480 self.toolDownload.set_sensitive(False)
3481 self.toolPlay.set_sensitive(False)
3482 self.toolTransfer.set_sensitive(False)
3483 self.toolCancel.set_sensitive(False)
3485 def on_treeChannels_row_activated(self, widget, path, *args):
3486 # double-click action of the podcast list or enter
3487 self.treeChannels.set_cursor(path)
3489 def on_treeChannels_cursor_changed(self, widget, *args):
3490 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3492 if model is not None and iter is not None:
3493 old_active_channel = self.active_channel
3494 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3496 if self.active_channel == old_active_channel:
3497 return
3499 if gpodder.ui.maemo:
3500 self.set_title(self.active_channel.title)
3502 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3503 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3504 self.itemEditChannel.set_visible(False)
3505 self.itemRemoveChannel.set_visible(False)
3506 else:
3507 self.itemEditChannel.set_visible(True)
3508 self.itemRemoveChannel.set_visible(True)
3509 else:
3510 self.active_channel = None
3511 self.itemEditChannel.set_visible(False)
3512 self.itemRemoveChannel.set_visible(False)
3514 self.update_episode_list_model()
3516 def on_btnEditChannel_clicked(self, widget, *args):
3517 self.on_itemEditChannel_activate( widget, args)
3519 def get_podcast_urls_from_selected_episodes(self):
3520 """Get a set of podcast URLs based on the selected episodes"""
3521 return set(episode.channel.url for episode in \
3522 self.get_selected_episodes())
3524 def get_selected_episodes(self):
3525 """Get a list of selected episodes from treeAvailable"""
3526 selection = self.treeAvailable.get_selection()
3527 model, paths = selection.get_selected_rows()
3529 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3530 return episodes
3532 def on_transfer_selected_episodes(self, widget):
3533 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3535 def on_playback_selected_episodes(self, widget):
3536 self.playback_episodes(self.get_selected_episodes())
3538 def on_shownotes_selected_episodes(self, widget):
3539 episodes = self.get_selected_episodes()
3540 if episodes:
3541 episode = episodes.pop(0)
3542 self.show_episode_shownotes(episode)
3543 else:
3544 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3546 def on_download_selected_episodes(self, widget):
3547 episodes = self.get_selected_episodes()
3548 self.download_episode_list(episodes)
3549 self.update_episode_list_icons([episode.url for episode in episodes])
3550 self.play_or_download()
3552 def on_treeAvailable_row_activated(self, widget, path, view_column):
3553 """Double-click/enter action handler for treeAvailable"""
3554 # We should only have one one selected as it was double clicked!
3555 e = self.get_selected_episodes()[0]
3557 if (self.config.double_click_episode_action == 'download'):
3558 # If the episode has already been downloaded and exists then play it
3559 if e.was_downloaded(and_exists=True):
3560 self.playback_episodes(self.get_selected_episodes())
3561 # else download it if it is not already downloading
3562 elif not self.episode_is_downloading(e):
3563 self.download_episode_list([e])
3564 self.update_episode_list_icons([e.url])
3565 self.play_or_download()
3566 elif (self.config.double_click_episode_action == 'stream'):
3567 # If we happen to have downloaded this episode simple play it
3568 if e.was_downloaded(and_exists=True):
3569 self.playback_episodes(self.get_selected_episodes())
3570 # else if streaming is possible stream it
3571 elif self.streaming_possible():
3572 self.playback_episodes(self.get_selected_episodes())
3573 else:
3574 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3575 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3576 else:
3577 # default action is to display show notes
3578 self.on_shownotes_selected_episodes(widget)
3580 def show_episode_shownotes(self, episode):
3581 if self.episode_shownotes_window is None:
3582 log('First-time use of episode window --- creating', sender=self)
3583 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
3584 _download_episode_list=self.download_episode_list, \
3585 _playback_episodes=self.playback_episodes, \
3586 _delete_episode_list=self.delete_episode_list, \
3587 _episode_list_status_changed=self.episode_list_status_changed, \
3588 _cancel_task_list=self.cancel_task_list, \
3589 _episode_is_downloading=self.episode_is_downloading, \
3590 _streaming_possible=self.streaming_possible())
3591 self.episode_shownotes_window.show(episode)
3592 if self.episode_is_downloading(episode):
3593 self.update_downloads_list()
3595 def restart_auto_update_timer(self):
3596 if self._auto_update_timer_source_id is not None:
3597 log('Removing existing auto update timer.', sender=self)
3598 gobject.source_remove(self._auto_update_timer_source_id)
3599 self._auto_update_timer_source_id = None
3601 if self.config.auto_update_feeds and \
3602 self.config.auto_update_frequency:
3603 interval = 60*1000*self.config.auto_update_frequency
3604 log('Setting up auto update timer with interval %d.', \
3605 self.config.auto_update_frequency, sender=self)
3606 self._auto_update_timer_source_id = gobject.timeout_add(\
3607 interval, self._on_auto_update_timer)
3609 def _on_auto_update_timer(self):
3610 log('Auto update timer fired.', sender=self)
3611 self.update_feed_cache(force_update=True)
3613 # Ask web service for sub changes (if enabled)
3614 self.mygpo_client.flush()
3616 return True
3618 def on_treeDownloads_row_activated(self, widget, *args):
3619 # Use the standard way of working on the treeview
3620 selection = self.treeDownloads.get_selection()
3621 (model, paths) = selection.get_selected_rows()
3622 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
3624 for tree_row_reference, task in selected_tasks:
3625 if task.status in (task.DOWNLOADING, task.QUEUED):
3626 task.status = task.PAUSED
3627 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
3628 self.download_queue_manager.add_task(task)
3629 self.enable_download_list_update()
3630 elif task.status == task.DONE:
3631 model.remove(model.get_iter(tree_row_reference.get_path()))
3633 self.play_or_download()
3635 # Update the tab title and downloads list
3636 self.update_downloads_list()
3638 def on_item_cancel_download_activate(self, widget):
3639 if self.wNotebook.get_current_page() == 0:
3640 selection = self.treeAvailable.get_selection()
3641 (model, paths) = selection.get_selected_rows()
3642 urls = [model.get_value(model.get_iter(path), \
3643 self.episode_list_model.C_URL) for path in paths]
3644 selected_tasks = [task for task in self.download_tasks_seen \
3645 if task.url in urls]
3646 else:
3647 selection = self.treeDownloads.get_selection()
3648 (model, paths) = selection.get_selected_rows()
3649 selected_tasks = [model.get_value(model.get_iter(path), \
3650 self.download_status_model.C_TASK) for path in paths]
3651 self.cancel_task_list(selected_tasks)
3653 def on_btnCancelAll_clicked(self, widget, *args):
3654 self.cancel_task_list(self.download_tasks_seen)
3656 def on_btnDownloadedDelete_clicked(self, widget, *args):
3657 episodes = self.get_selected_episodes()
3658 if len(episodes) == 1:
3659 self.delete_episode_list(episodes, skip_locked=False)
3660 else:
3661 self.delete_episode_list(episodes)
3663 def on_key_press(self, widget, event):
3664 # Allow tab switching with Ctrl + PgUp/PgDown
3665 if event.state & gtk.gdk.CONTROL_MASK:
3666 if event.keyval == gtk.keysyms.Page_Up:
3667 self.wNotebook.prev_page()
3668 return True
3669 elif event.keyval == gtk.keysyms.Page_Down:
3670 self.wNotebook.next_page()
3671 return True
3673 # After this code we only handle Maemo hardware keys,
3674 # so if we are not a Maemo app, we don't do anything
3675 if not gpodder.ui.maemo:
3676 return False
3678 diff = 0
3679 if event.keyval == gtk.keysyms.F7: #plus
3680 diff = 1
3681 elif event.keyval == gtk.keysyms.F8: #minus
3682 diff = -1
3684 if diff != 0 and not self.currently_updating:
3685 selection = self.treeChannels.get_selection()
3686 (model, iter) = selection.get_selected()
3687 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
3688 selection.select_path(new_path)
3689 self.treeChannels.set_cursor(new_path)
3690 return True
3692 return False
3694 def on_iconify(self):
3695 if self.tray_icon:
3696 self.gPodder.set_skip_taskbar_hint(True)
3697 if self.config.minimize_to_tray:
3698 self.tray_icon.set_visible(True)
3699 else:
3700 self.gPodder.set_skip_taskbar_hint(False)
3702 def on_uniconify(self):
3703 if self.tray_icon:
3704 self.gPodder.set_skip_taskbar_hint(False)
3705 if self.config.minimize_to_tray:
3706 self.tray_icon.set_visible(False)
3707 else:
3708 self.gPodder.set_skip_taskbar_hint(False)
3710 def uniconify_main_window(self):
3711 if self.is_iconified():
3712 self.gPodder.present()
3714 def iconify_main_window(self):
3715 if not self.is_iconified():
3716 self.gPodder.iconify()
3718 def update_podcasts_tab(self):
3719 if len(self.channels):
3720 if gpodder.ui.fremantle:
3721 self.button_refresh.set_title(_('Check for new episodes'))
3722 self.button_refresh.show()
3723 else:
3724 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3725 else:
3726 if gpodder.ui.fremantle:
3727 self.button_refresh.hide()
3728 else:
3729 self.label2.set_text(_('Podcasts'))
3731 @dbus.service.method(gpodder.dbus_interface)
3732 def show_gui_window(self):
3733 self.gPodder.present()
3735 @dbus.service.method(gpodder.dbus_interface)
3736 def subscribe_to_url(self, url):
3737 gPodderAddPodcast(self.gPodder,
3738 add_urls_callback=self.add_podcast_list,
3739 preset_url=url)
3741 @dbus.service.method(gpodder.dbus_interface)
3742 def mark_episode_played(self, filename):
3743 if filename is None:
3744 return False
3746 for channel in self.channels:
3747 for episode in channel.get_all_episodes():
3748 fn = episode.local_filename(create=False, check_only=True)
3749 if fn == filename:
3750 episode.mark(is_played=True)
3751 self.db.commit()
3752 self.update_episode_list_icons([episode.url])
3753 self.update_podcast_list_model([episode.channel.url])
3754 return True
3756 return False
3759 def main(options=None):
3760 gobject.threads_init()
3761 gobject.set_application_name('gPodder')
3763 if gpodder.ui.maemo:
3764 # Try to enable the custom icon theme for gPodder on Maemo
3765 settings = gtk.settings_get_default()
3766 settings.set_string_property('gtk-icon-theme-name', \
3767 'gpodder', __file__)
3768 # Extend the search path for the optified icon theme (Maemo 5)
3769 icon_theme = gtk.icon_theme_get_default()
3770 icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
3772 gtk.window_set_default_icon_name('gpodder')
3773 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
3775 try:
3776 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
3777 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
3778 except dbus.exceptions.DBusException, dbe:
3779 log('Warning: Cannot get "on the bus".', traceback=True)
3780 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
3781 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
3782 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
3783 dlg.set_title('gPodder')
3784 dlg.run()
3785 dlg.destroy()
3786 sys.exit(0)
3788 util.make_directory(gpodder.home)
3789 gpodder.load_plugins()
3791 config = UIConfig(gpodder.config_file)
3793 if gpodder.ui.diablo:
3794 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3795 # folder exists there (allow moving "gpodder" between SD cards or USB)
3796 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3797 if not os.path.exists(config.download_dir):
3798 log('Downloads might have been moved. Trying to locate them...')
3799 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
3800 dir = os.path.join(basedir, 'gpodder')
3801 if os.path.exists(dir):
3802 log('Downloads found in: %s', dir)
3803 config.download_dir = dir
3804 break
3805 else:
3806 log('Downloads NOT FOUND in %s', dir)
3808 if config.enable_fingerscroll:
3809 BuilderWidget.use_fingerscroll = True
3810 elif gpodder.ui.fremantle:
3811 config.on_quit_ask = False
3813 config.mygpo_device_type = util.detect_device_type()
3815 gp = gPodder(bus_name, config)
3817 # Handle options
3818 if options.subscribe:
3819 util.idle_add(gp.subscribe_to_url, options.subscribe)
3821 # mac OS X stuff :
3822 # handle "subscribe to podcast" events from firefox
3823 if platform.system() == 'Darwin':
3824 from gpodder import gpodderosx
3825 gpodderosx.register_handlers(gp)
3826 # end mac OS X stuff
3828 gp.run()