Extend Panucci D-Bus interface for streaming
[gpodder.git] / src / gpodder / gui.py
blob9eecef24e334836ab9e82d33b82fc5fe75129f10
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 cleanup_downloads=self.cleanup_downloads, \
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.download_task_monitors = set()
421 # Subscribed channels
422 self.active_channel = None
423 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
424 self.channel_list_changed = True
425 self.update_podcasts_tab()
427 # load list of user applications for audio playback
428 self.user_apps_reader = UserAppsReader(['audio', 'video'])
429 threading.Thread(target=self.user_apps_reader.read).start()
431 # Set the "Device" menu item for the first time
432 if gpodder.ui.desktop:
433 self.update_item_device()
435 # Set up the first instance of MygPoClient
436 self.mygpo_client = my.MygPoClient(self.config)
438 # Now, update the feed cache, when everything's in place
439 if not gpodder.ui.fremantle:
440 self.btnUpdateFeeds.show()
441 self.updating_feed_cache = False
442 self.feed_cache_update_cancelled = False
443 self.update_feed_cache(force_update=self.config.update_on_startup)
445 self.message_area = None
447 def find_partial_downloads():
448 # Look for partial file downloads
449 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
450 count = len(partial_files)
451 resumable_episodes = []
452 if count:
453 if not gpodder.ui.fremantle:
454 util.idle_add(self.wNotebook.set_current_page, 1)
455 indicator = ProgressIndicator(_('Loading incomplete downloads'), \
456 _('Some episodes have not finished downloading in a previous session.'), \
457 False, self.get_dialog_parent())
458 indicator.on_message(N_('%d partial file', '%d partial files', count) % count)
460 candidates = [f[:-len('.partial')] for f in partial_files]
461 found = 0
463 for c in self.channels:
464 for e in c.get_all_episodes():
465 filename = e.local_filename(create=False, check_only=True)
466 if filename in candidates:
467 log('Found episode: %s', e.title, sender=self)
468 found += 1
469 indicator.on_message(e.title)
470 indicator.on_progress(float(found)/count)
471 candidates.remove(filename)
472 partial_files.remove(filename+'.partial')
473 resumable_episodes.append(e)
475 if not candidates:
476 break
478 if not candidates:
479 break
481 for f in partial_files:
482 log('Partial file without episode: %s', f, sender=self)
483 util.delete_file(f)
485 util.idle_add(indicator.on_finished)
487 if len(resumable_episodes):
488 def offer_resuming():
489 self.download_episode_list_paused(resumable_episodes)
490 if not gpodder.ui.fremantle:
491 resume_all = gtk.Button(_('Resume all'))
492 #resume_all.set_border_width(0)
493 def on_resume_all(button):
494 selection = self.treeDownloads.get_selection()
495 selection.select_all()
496 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = self.downloads_list_get_selection()
497 selection.unselect_all()
498 self._for_each_task_set_status(selected_tasks, download.DownloadTask.QUEUED)
499 self.message_area.hide()
500 resume_all.connect('clicked', on_resume_all)
502 self.message_area = SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all,))
503 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
504 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
505 self.message_area.show_all()
506 self.clean_up_downloads(delete_partial=False)
507 util.idle_add(offer_resuming)
508 elif not gpodder.ui.fremantle:
509 util.idle_add(self.wNotebook.set_current_page, 0)
510 else:
511 util.idle_add(self.clean_up_downloads, True)
512 threading.Thread(target=find_partial_downloads).start()
514 # Start the auto-update procedure
515 self._auto_update_timer_source_id = None
516 if self.config.auto_update_feeds:
517 self.restart_auto_update_timer()
519 # Delete old episodes if the user wishes to
520 if self.config.auto_remove_played_episodes and \
521 self.config.episode_old_age > 0:
522 old_episodes = list(self.get_expired_episodes())
523 if len(old_episodes) > 0:
524 self.delete_episode_list(old_episodes, confirm=False)
525 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
527 if gpodder.ui.fremantle:
528 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
529 self.button_refresh.set_sensitive(True)
530 self.button_subscribe.set_sensitive(True)
531 self.main_window.set_title(_('gPodder'))
532 hildon.hildon_gtk_window_take_screenshot(self.main_window, True)
534 # Do the initial sync with the web service
535 util.idle_add(self.mygpo_client.flush, True)
537 # First-time users should be asked if they want to see the OPML
538 if not self.channels and not gpodder.ui.fremantle:
539 util.idle_add(self.on_itemUpdate_activate)
541 def on_played(self, start, end, total, file_uri):
542 """Handle the "played" signal from a media player"""
543 log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
544 filename = file_uri[len('file://'):]
545 # FIXME: Optimize this by querying the database more directly
546 for channel in self.channels:
547 for episode in channel.get_all_episodes():
548 fn = episode.local_filename(create=False, check_only=True)
549 if fn == filename or episode.url == file_uri:
550 file_type = episode.file_type()
551 # Automatically enable D-Bus played status mode
552 if file_type == 'audio':
553 self.config.audio_played_dbus = True
554 elif file_type == 'video':
555 self.config.video_played_dbus = True
557 now = time.time()
558 if total > 0:
559 episode.total_time = total
560 if episode.current_position_updated is None or \
561 now > episode.current_position_updated:
562 episode.current_position = end
563 episode.current_position_updated = now
564 episode.mark(is_played=True)
565 episode.save()
566 self.db.commit()
567 self.update_episode_list_icons([episode.url])
568 self.update_podcast_list_model([episode.channel.url])
570 # Submit this action to the webservice
571 self.mygpo_client.on_playback_full(episode, \
572 start, end, total)
573 return
575 def on_add_remove_podcasts_mygpo(self):
576 actions = self.mygpo_client.get_received_actions()
577 if not actions:
578 return False
580 existing_urls = [c.url for c in self.channels]
582 # Columns for the episode selector window - just one...
583 columns = (
584 ('description', None, None, _('Action')),
587 # A list of actions that have to be chosen from
588 changes = []
590 # Actions that are ignored (already carried out)
591 ignored = []
593 for action in actions:
594 if action.is_add and action.url not in existing_urls:
595 changes.append(my.Change(action))
596 elif action.is_remove and action.url in existing_urls:
597 podcast_object = None
598 for podcast in self.channels:
599 if podcast.url == action.url:
600 podcast_object = podcast
601 break
602 changes.append(my.Change(action, podcast_object))
603 else:
604 log('Ignoring action: %s', action, sender=self)
605 ignored.append(action)
607 # Confirm all ignored changes
608 self.mygpo_client.confirm_received_actions(ignored)
610 def execute_podcast_actions(selected):
611 add_list = [c.action.url for c in selected if c.action.is_add]
612 remove_list = [c.podcast for c in selected if c.action.is_remove]
614 # Apply the accepted changes locally
615 self.add_podcast_list(add_list)
616 self.remove_podcast_list(remove_list, confirm=False)
618 # All selected items are now confirmed
619 self.mygpo_client.confirm_received_actions(c.action for c in selected)
621 # Revert the changes on the server
622 rejected = [c.action for c in changes if c not in selected]
623 self.mygpo_client.reject_received_actions(rejected)
625 def ask():
626 # We're abusing the Episode Selector again ;) -- thp
627 gPodderEpisodeSelector(self.main_window, \
628 title=_('Confirm changes from gpodder.net'), \
629 instructions=_('Select the actions you want to carry out.'), \
630 episodes=changes, \
631 columns=columns, \
632 size_attribute=None, \
633 stock_ok_button=gtk.STOCK_APPLY, \
634 callback=execute_podcast_actions, \
635 _config=self.config)
637 # There are some actions that need the user's attention
638 if changes:
639 util.idle_add(ask)
640 return True
642 # We have no remaining actions - no selection happens
643 return False
645 def rewrite_urls_mygpo(self):
646 # Check if we have to rewrite URLs since the last add
647 rewritten_urls = self.mygpo_client.get_rewritten_urls()
649 for rewritten_url in rewritten_urls:
650 if not rewritten_url.new_url:
651 continue
653 for channel in self.channels:
654 if channel.url == rewritten_url.old_url:
655 log('Updating URL of %s to %s', channel, \
656 rewritten_url.new_url, sender=self)
657 channel.url = rewritten_url.new_url
658 channel.save()
659 self.channel_list_changed = True
660 util.idle_add(self.update_episode_list_model)
661 break
663 def on_send_full_subscriptions(self):
664 # Send the full subscription list to the gpodder.net client
665 # (this will overwrite the subscription list on the server)
666 indicator = ProgressIndicator(_('Uploading subscriptions'), \
667 _('Your subscriptions are being uploaded to the server.'), \
668 False, self.get_dialog_parent())
670 try:
671 self.mygpo_client.set_subscriptions([c.url for c in self.channels])
672 util.idle_add(self.show_message, _('List uploaded successfully.'))
673 except Exception, e:
674 def show_error(e):
675 message = str(e)
676 if not message:
677 message = e.__class__.__name__
678 self.show_message(message, \
679 _('Error while uploading'), \
680 important=True)
681 util.idle_add(show_error, e)
683 util.idle_add(indicator.on_finished)
685 def on_podcast_selected(self, treeview, path, column):
686 # for Maemo 5's UI
687 model = treeview.get_model()
688 channel = model.get_value(model.get_iter(path), \
689 PodcastListModel.C_CHANNEL)
690 self.active_channel = channel
691 self.update_episode_list_model()
692 self.episodes_window.channel = self.active_channel
693 self.episodes_window.show()
695 def on_button_subscribe_clicked(self, button):
696 self.on_itemImportChannels_activate(button)
698 def on_button_downloads_clicked(self, widget):
699 self.downloads_window.show()
701 def show_episode_in_download_manager(self, episode):
702 self.downloads_window.show()
703 model = self.treeDownloads.get_model()
704 selection = self.treeDownloads.get_selection()
705 selection.unselect_all()
706 it = model.get_iter_first()
707 while it is not None:
708 task = model.get_value(it, DownloadStatusModel.C_TASK)
709 if task.episode.url == episode.url:
710 selection.select_iter(it)
711 # FIXME: Scroll to selection in pannable area
712 break
713 it = model.iter_next(it)
715 def for_each_episode_set_task_status(self, episodes, status):
716 episode_urls = set(episode.url for episode in episodes)
717 model = self.treeDownloads.get_model()
718 selected_tasks = [(gtk.TreeRowReference(model, row.path), \
719 model.get_value(row.iter, \
720 DownloadStatusModel.C_TASK)) for row in model \
721 if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
722 in episode_urls]
723 self._for_each_task_set_status(selected_tasks, status)
725 def on_window_orientation_changed(self, orientation):
726 self._last_orientation = orientation
727 if self.preferences_dialog is not None:
728 self.preferences_dialog.on_window_orientation_changed(orientation)
730 treeview = self.treeChannels
731 if orientation == Orientation.PORTRAIT:
732 treeview.set_action_area_orientation(gtk.ORIENTATION_VERTICAL)
733 # Work around Maemo bug #4718
734 self.button_subscribe.set_name('HildonButton-thumb')
735 self.button_refresh.set_name('HildonButton-thumb')
736 else:
737 treeview.set_action_area_orientation(gtk.ORIENTATION_HORIZONTAL)
738 # Work around Maemo bug #4718
739 self.button_subscribe.set_name('HildonButton-finger')
740 self.button_refresh.set_name('HildonButton-finger')
742 def on_treeview_podcasts_selection_changed(self, selection):
743 model, iter = selection.get_selected()
744 if iter is None:
745 self.active_channel = None
746 self.episode_list_model.clear()
748 def on_treeview_button_pressed(self, treeview, event):
749 if event.window != treeview.get_bin_window():
750 return False
752 TreeViewHelper.save_button_press_event(treeview, event)
754 if getattr(treeview, TreeViewHelper.ROLE) == \
755 TreeViewHelper.ROLE_PODCASTS:
756 return self.currently_updating
758 return event.button == self.context_menu_mouse_button and \
759 gpodder.ui.desktop
761 def on_treeview_podcasts_button_released(self, treeview, event):
762 if event.window != treeview.get_bin_window():
763 return False
765 if gpodder.ui.maemo:
766 return self.treeview_channels_handle_gestures(treeview, event)
767 return self.treeview_channels_show_context_menu(treeview, event)
769 def on_treeview_episodes_button_released(self, treeview, event):
770 if event.window != treeview.get_bin_window():
771 return False
773 if gpodder.ui.maemo:
774 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
775 return self.treeview_available_handle_gestures(treeview, event)
777 return self.treeview_available_show_context_menu(treeview, event)
779 def on_treeview_downloads_button_released(self, treeview, event):
780 if event.window != treeview.get_bin_window():
781 return False
783 return self.treeview_downloads_show_context_menu(treeview, event)
785 def on_entry_search_podcasts_changed(self, editable):
786 if self.hbox_search_podcasts.get_property('visible'):
787 self.podcast_list_model.set_search_term(editable.get_chars(0, -1))
789 def on_entry_search_podcasts_key_press(self, editable, event):
790 if event.keyval == gtk.keysyms.Escape:
791 self.hide_podcast_search()
792 return True
794 def hide_podcast_search(self, *args):
795 self.hbox_search_podcasts.hide()
796 self.entry_search_podcasts.set_text('')
797 self.podcast_list_model.set_search_term(None)
798 self.treeChannels.grab_focus()
800 def show_podcast_search(self, input_char):
801 self.hbox_search_podcasts.show()
802 self.entry_search_podcasts.insert_text(input_char, -1)
803 self.entry_search_podcasts.grab_focus()
804 self.entry_search_podcasts.set_position(-1)
806 def init_podcast_list_treeview(self):
807 # Set up podcast channel tree view widget
808 if gpodder.ui.fremantle:
809 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
810 self.item_view_podcasts_downloaded.set_active(True)
811 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
812 self.item_view_podcasts_unplayed.set_active(True)
813 else:
814 self.item_view_podcasts_all.set_active(True)
815 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
817 iconcolumn = gtk.TreeViewColumn('')
818 iconcell = gtk.CellRendererPixbuf()
819 iconcolumn.pack_start(iconcell, False)
820 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
821 self.treeChannels.append_column(iconcolumn)
823 namecolumn = gtk.TreeViewColumn('')
824 namecell = gtk.CellRendererText()
825 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
826 namecolumn.pack_start(namecell, True)
827 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
829 iconcell = gtk.CellRendererPixbuf()
830 iconcell.set_property('xalign', 1.0)
831 namecolumn.pack_start(iconcell, False)
832 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
833 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
834 self.treeChannels.append_column(namecolumn)
836 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
838 # When no podcast is selected, clear the episode list model
839 selection = self.treeChannels.get_selection()
840 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
842 # Set up type-ahead find for the podcast list
843 def on_key_press(treeview, event):
844 if event.keyval == gtk.keysyms.Escape:
845 self.hide_podcast_search()
846 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
847 self.hide_podcast_search()
848 elif event.state & gtk.gdk.CONTROL_MASK:
849 # Don't handle type-ahead when control is pressed (so shortcuts
850 # with the Ctrl key still work, e.g. Ctrl+A, ...)
851 return True
852 else:
853 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
854 if unicode_char_id == 0:
855 return False
856 input_char = unichr(unicode_char_id)
857 self.show_podcast_search(input_char)
858 return True
859 self.treeChannels.connect('key-press-event', on_key_press)
861 # Enable separators to the podcast list to separate special podcasts
862 # from others (this is used for the "all episodes" view)
863 self.treeChannels.set_row_separator_func(PodcastListModel.row_separator_func)
865 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
867 def on_entry_search_episodes_changed(self, editable):
868 if self.hbox_search_episodes.get_property('visible'):
869 self.episode_list_model.set_search_term(editable.get_chars(0, -1))
871 def on_entry_search_episodes_key_press(self, editable, event):
872 if event.keyval == gtk.keysyms.Escape:
873 self.hide_episode_search()
874 return True
876 def hide_episode_search(self, *args):
877 self.hbox_search_episodes.hide()
878 self.entry_search_episodes.set_text('')
879 self.episode_list_model.set_search_term(None)
880 self.treeAvailable.grab_focus()
882 def show_episode_search(self, input_char):
883 self.hbox_search_episodes.show()
884 self.entry_search_episodes.insert_text(input_char, -1)
885 self.entry_search_episodes.grab_focus()
886 self.entry_search_episodes.set_position(-1)
888 def init_episode_list_treeview(self):
889 # For loading the list model
890 self.empty_episode_list_model = EpisodeListModel()
891 self.episode_list_model = EpisodeListModel()
893 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
894 self.item_view_episodes_undeleted.set_active(True)
895 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
896 self.item_view_episodes_downloaded.set_active(True)
897 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
898 self.item_view_episodes_unplayed.set_active(True)
899 else:
900 self.item_view_episodes_all.set_active(True)
902 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
904 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
906 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
908 iconcell = gtk.CellRendererPixbuf()
909 if gpodder.ui.maemo:
910 iconcell.set_fixed_size(50, 50)
911 status_column_label = ''
912 else:
913 status_column_label = _('Status')
914 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=EpisodeListModel.C_STATUS_ICON)
916 namecell = gtk.CellRendererText()
917 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
918 namecolumn = gtk.TreeViewColumn(_('Episode'), namecell, markup=EpisodeListModel.C_DESCRIPTION)
919 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
920 namecolumn.set_resizable(True)
921 namecolumn.set_expand(True)
923 sizecell = gtk.CellRendererText()
924 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
926 releasecell = gtk.CellRendererText()
927 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
929 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
930 itemcolumn.set_reorderable(True)
931 self.treeAvailable.append_column(itemcolumn)
933 if gpodder.ui.maemo:
934 sizecolumn.set_visible(False)
935 releasecolumn.set_visible(False)
937 # Set up type-ahead find for the episode list
938 def on_key_press(treeview, event):
939 if event.keyval == gtk.keysyms.Escape:
940 self.hide_episode_search()
941 elif gpodder.ui.fremantle and event.keyval == gtk.keysyms.BackSpace:
942 self.hide_episode_search()
943 elif event.state & gtk.gdk.CONTROL_MASK:
944 # Don't handle type-ahead when control is pressed (so shortcuts
945 # with the Ctrl key still work, e.g. Ctrl+A, ...)
946 return False
947 else:
948 unicode_char_id = gtk.gdk.keyval_to_unicode(event.keyval)
949 if unicode_char_id == 0:
950 return False
951 input_char = unichr(unicode_char_id)
952 self.show_episode_search(input_char)
953 return True
954 self.treeAvailable.connect('key-press-event', on_key_press)
956 if gpodder.ui.desktop:
957 self.treeAvailable.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
958 (('text/uri-list', 0, 0),), gtk.gdk.ACTION_COPY)
959 def drag_data_get(tree, context, selection_data, info, timestamp):
960 if self.config.on_drag_mark_played:
961 for episode in self.get_selected_episodes():
962 episode.mark(is_played=True)
963 self.on_selected_episodes_status_changed()
964 uris = ['file://'+e.local_filename(create=False) \
965 for e in self.get_selected_episodes() \
966 if e.was_downloaded(and_exists=True)]
967 uris.append('') # for the trailing '\r\n'
968 selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
969 self.treeAvailable.connect('drag-data-get', drag_data_get)
971 selection = self.treeAvailable.get_selection()
972 if gpodder.ui.diablo:
973 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
974 selection.set_mode(gtk.SELECTION_SINGLE)
975 else:
976 selection.set_mode(gtk.SELECTION_MULTIPLE)
977 elif gpodder.ui.fremantle:
978 selection.set_mode(gtk.SELECTION_SINGLE)
979 else:
980 selection.set_mode(gtk.SELECTION_MULTIPLE)
981 # Update the sensitivity of the toolbar buttons on the Desktop
982 selection.connect('changed', lambda s: self.play_or_download())
984 if gpodder.ui.diablo:
985 # Set up the tap-and-hold context menu for podcasts
986 menu = gtk.Menu()
987 menu.append(self.itemUpdateChannel.create_menu_item())
988 menu.append(self.itemEditChannel.create_menu_item())
989 menu.append(gtk.SeparatorMenuItem())
990 menu.append(self.itemRemoveChannel.create_menu_item())
991 menu.append(gtk.SeparatorMenuItem())
992 item = gtk.ImageMenuItem(_('Close this menu'))
993 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
994 gtk.ICON_SIZE_MENU))
995 menu.append(item)
996 menu.show_all()
997 menu = self.set_finger_friendly(menu)
998 self.treeChannels.tap_and_hold_setup(menu)
1001 def init_download_list_treeview(self):
1002 # enable multiple selection support
1003 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
1004 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
1006 # columns and renderers for "download progress" tab
1007 # First column: [ICON] Episodename
1008 column = gtk.TreeViewColumn(_('Episode'))
1010 cell = gtk.CellRendererPixbuf()
1011 if gpodder.ui.maemo:
1012 cell.set_fixed_size(50, 50)
1013 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
1014 column.pack_start(cell, expand=False)
1015 column.add_attribute(cell, 'stock-id', \
1016 DownloadStatusModel.C_ICON_NAME)
1018 cell = gtk.CellRendererText()
1019 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
1020 column.pack_start(cell, expand=True)
1021 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
1022 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1023 column.set_expand(True)
1024 self.treeDownloads.append_column(column)
1026 # Second column: Progress
1027 cell = gtk.CellRendererProgress()
1028 cell.set_property('yalign', .5)
1029 cell.set_property('ypad', 6)
1030 column = gtk.TreeViewColumn(_('Progress'), cell,
1031 value=DownloadStatusModel.C_PROGRESS, \
1032 text=DownloadStatusModel.C_PROGRESS_TEXT)
1033 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
1034 column.set_expand(False)
1035 self.treeDownloads.append_column(column)
1036 column.set_property('min-width', 150)
1037 column.set_property('max-width', 150)
1039 self.treeDownloads.set_model(self.download_status_model)
1040 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
1042 def on_treeview_expose_event(self, treeview, event):
1043 if event.window == treeview.get_bin_window():
1044 model = treeview.get_model()
1045 if (model is not None and model.get_iter_first() is not None):
1046 return False
1048 role = getattr(treeview, TreeViewHelper.ROLE)
1049 ctx = event.window.cairo_create()
1050 ctx.rectangle(event.area.x, event.area.y,
1051 event.area.width, event.area.height)
1052 ctx.clip()
1054 x, y, width, height, depth = event.window.get_geometry()
1055 progress = None
1057 if role == TreeViewHelper.ROLE_EPISODES:
1058 if self.currently_updating:
1059 text = _('Loading episodes')
1060 progress = self.episode_list_model.get_update_progress()
1061 elif self.config.episode_list_view_mode != \
1062 EpisodeListModel.VIEW_ALL:
1063 text = _('No episodes in current view')
1064 else:
1065 text = _('No episodes available')
1066 elif role == TreeViewHelper.ROLE_PODCASTS:
1067 if self.config.episode_list_view_mode != \
1068 EpisodeListModel.VIEW_ALL and \
1069 self.config.podcast_list_hide_boring and \
1070 len(self.channels) > 0:
1071 text = _('No podcasts in this view')
1072 else:
1073 text = _('No subscriptions')
1074 elif role == TreeViewHelper.ROLE_DOWNLOADS:
1075 text = _('No active downloads')
1076 else:
1077 raise Exception('on_treeview_expose_event: unknown role')
1079 if gpodder.ui.fremantle:
1080 from gpodder.gtkui.frmntl import style
1081 font_desc = style.get_font_desc('LargeSystemFont')
1082 else:
1083 font_desc = None
1085 draw_text_box_centered(ctx, treeview, width, height, text, font_desc, progress)
1087 return False
1089 def enable_download_list_update(self):
1090 if not self.download_list_update_enabled:
1091 self.update_downloads_list()
1092 gobject.timeout_add(1500, self.update_downloads_list)
1093 self.download_list_update_enabled = True
1095 def cleanup_downloads(self):
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 downloads list one more time
1124 self.update_downloads_list(can_call_cleanup=False)
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, can_call_cleanup=True):
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 else:
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)
1275 # Remove finished episodes
1276 if self.config.auto_cleanup_downloads and can_call_cleanup:
1277 self.cleanup_downloads()
1279 # Stop updating the download list here
1280 self.download_list_update_enabled = False
1282 if not gpodder.ui.fremantle:
1283 self.gPodder.set_title(' - '.join(title))
1285 self.update_episode_list_icons(episode_urls)
1286 if self.episode_shownotes_window is not None:
1287 if (shownotes_task and shownotes_task.url in episode_urls) or \
1288 shownotes_task != self.episode_shownotes_window.task:
1289 self.episode_shownotes_window._download_status_changed(shownotes_task)
1290 self.episode_shownotes_window._download_status_progress()
1291 self.play_or_download()
1292 if channel_urls:
1293 self.update_podcast_list_model(channel_urls)
1295 return self.download_list_update_enabled
1296 except Exception, e:
1297 log('Exception happened while updating download list.', sender=self, traceback=True)
1298 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
1299 # We return False here, so the update loop won't be called again,
1300 # that's why we require the restart of gPodder in the message.
1301 return False
1303 def on_config_changed(self, *args):
1304 util.idle_add(self._on_config_changed, *args)
1306 def _on_config_changed(self, name, old_value, new_value):
1307 if name == 'show_toolbar' and gpodder.ui.desktop:
1308 self.toolbar.set_property('visible', new_value)
1309 elif name == 'videoplayer':
1310 self.config.video_played_dbus = False
1311 elif name == 'player':
1312 self.config.audio_played_dbus = False
1313 elif name == 'episode_list_descriptions':
1314 self.update_episode_list_model()
1315 elif name == 'episode_list_thumbnails':
1316 self.update_episode_list_icons(all=True)
1317 elif name == 'rotation_mode':
1318 self._fremantle_rotation.set_mode(new_value)
1319 elif name in ('auto_update_feeds', 'auto_update_frequency'):
1320 self.restart_auto_update_timer()
1321 elif name == 'podcast_list_view_all':
1322 # Force a update of the podcast list model
1323 self.channel_list_changed = True
1324 if gpodder.ui.fremantle:
1325 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
1326 while gtk.events_pending():
1327 gtk.main_iteration(False)
1328 self.update_podcast_list_model()
1329 if gpodder.ui.fremantle:
1330 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1332 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
1333 # With get_bin_window, we get the window that contains the rows without
1334 # the header. The Y coordinate of this window will be the height of the
1335 # treeview header. This is the amount we have to subtract from the
1336 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1337 (x_bin, y_bin) = treeview.get_bin_window().get_position()
1338 y -= x_bin
1339 y -= y_bin
1340 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
1342 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or (column is not None and column != treeview.get_columns()[0]):
1343 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1344 return False
1346 if path is not None:
1347 model = treeview.get_model()
1348 iter = model.get_iter(path)
1349 role = getattr(treeview, TreeViewHelper.ROLE)
1351 if role == TreeViewHelper.ROLE_EPISODES:
1352 id = model.get_value(iter, EpisodeListModel.C_URL)
1353 elif role == TreeViewHelper.ROLE_PODCASTS:
1354 id = model.get_value(iter, PodcastListModel.C_URL)
1356 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
1357 if last_tooltip is not None and last_tooltip != id:
1358 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1359 return False
1360 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
1362 if role == TreeViewHelper.ROLE_EPISODES:
1363 description = model.get_value(iter, EpisodeListModel.C_TOOLTIP)
1364 if description:
1365 tooltip.set_text(description)
1366 else:
1367 return False
1368 elif role == TreeViewHelper.ROLE_PODCASTS:
1369 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
1370 if channel is None:
1371 return False
1372 channel.request_save_dir_size()
1373 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
1374 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
1375 if error_str:
1376 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
1377 error_str = '<span foreground="#ff0000">%s</span>' % error_str
1378 table = gtk.Table(rows=3, columns=3)
1379 table.set_row_spacings(5)
1380 table.set_col_spacings(5)
1381 table.set_border_width(5)
1383 heading = gtk.Label()
1384 heading.set_alignment(0, 1)
1385 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
1386 table.attach(heading, 0, 1, 0, 1)
1387 size_info = gtk.Label()
1388 size_info.set_alignment(1, 1)
1389 size_info.set_justify(gtk.JUSTIFY_RIGHT)
1390 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
1391 table.attach(size_info, 2, 3, 0, 1)
1393 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
1395 if len(channel.description) < 500:
1396 description = channel.description
1397 else:
1398 pos = channel.description.find('\n\n')
1399 if pos == -1 or pos > 500:
1400 description = channel.description[:498]+'[...]'
1401 else:
1402 description = channel.description[:pos]
1404 description = gtk.Label(description)
1405 if error_str:
1406 description.set_markup(error_str)
1407 description.set_alignment(0, 0)
1408 description.set_line_wrap(True)
1409 table.attach(description, 0, 3, 2, 3)
1411 table.show_all()
1412 tooltip.set_custom(table)
1414 return True
1416 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
1417 return False
1419 def treeview_allow_tooltips(self, treeview, allow):
1420 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
1422 def update_m3u_playlist_clicked(self, widget):
1423 if self.active_channel is not None:
1424 self.active_channel.update_m3u_playlist()
1425 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
1427 def treeview_handle_context_menu_click(self, treeview, event):
1428 x, y = int(event.x), int(event.y)
1429 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
1431 selection = treeview.get_selection()
1432 model, paths = selection.get_selected_rows()
1434 if path is None or (path not in paths and \
1435 event.button == self.context_menu_mouse_button):
1436 # We have right-clicked, but not into the selection,
1437 # assume we don't want to operate on the selection
1438 paths = []
1440 if path is not None and not paths and \
1441 event.button == self.context_menu_mouse_button:
1442 # No selection or clicked outside selection;
1443 # select the single item where we clicked
1444 treeview.grab_focus()
1445 treeview.set_cursor(path, column, 0)
1446 paths = [path]
1448 if not paths:
1449 # Unselect any remaining items (clicked elsewhere)
1450 if hasattr(treeview, 'is_rubber_banding_active'):
1451 if not treeview.is_rubber_banding_active():
1452 selection.unselect_all()
1453 else:
1454 selection.unselect_all()
1456 return model, paths
1458 def downloads_list_get_selection(self, model=None, paths=None):
1459 if model is None and paths is None:
1460 selection = self.treeDownloads.get_selection()
1461 model, paths = selection.get_selected_rows()
1463 can_queue, can_cancel, can_pause, can_remove, can_force = (True,)*5
1464 selected_tasks = [(gtk.TreeRowReference(model, path), \
1465 model.get_value(model.get_iter(path), \
1466 DownloadStatusModel.C_TASK)) for path in paths]
1468 for row_reference, task in selected_tasks:
1469 if task.status != download.DownloadTask.QUEUED:
1470 can_force = False
1471 if task.status not in (download.DownloadTask.PAUSED, \
1472 download.DownloadTask.FAILED, \
1473 download.DownloadTask.CANCELLED):
1474 can_queue = False
1475 if task.status not in (download.DownloadTask.PAUSED, \
1476 download.DownloadTask.QUEUED, \
1477 download.DownloadTask.DOWNLOADING):
1478 can_cancel = False
1479 if task.status not in (download.DownloadTask.QUEUED, \
1480 download.DownloadTask.DOWNLOADING):
1481 can_pause = False
1482 if task.status not in (download.DownloadTask.CANCELLED, \
1483 download.DownloadTask.FAILED, \
1484 download.DownloadTask.DONE):
1485 can_remove = False
1487 return selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force
1489 def downloads_finished(self, download_tasks_seen):
1490 # FIXME: Filter all tasks that have already been reported
1491 finished_downloads = [str(task) for task in download_tasks_seen if task.status == task.DONE]
1492 failed_downloads = [str(task)+' ('+task.error_message+')' for task in download_tasks_seen if task.status == task.FAILED]
1494 if finished_downloads and failed_downloads:
1495 message = self.format_episode_list(finished_downloads, 5)
1496 message += '\n\n<i>%s</i>\n' % _('These downloads failed:')
1497 message += self.format_episode_list(failed_downloads, 5)
1498 self.show_message(message, _('Downloads finished'), True, widget=self.labelDownloads)
1499 elif finished_downloads:
1500 message = self.format_episode_list(finished_downloads)
1501 self.show_message(message, _('Downloads finished'), widget=self.labelDownloads)
1502 elif failed_downloads:
1503 message = self.format_episode_list(failed_downloads)
1504 self.show_message(message, _('Downloads failed'), True, widget=self.labelDownloads)
1506 # Open torrent files right after download (bug 1029)
1507 if self.config.open_torrent_after_download:
1508 for task in download_tasks_seen:
1509 if task.status != task.DONE:
1510 continue
1512 episode = task.episode
1513 if episode.mimetype != 'application/x-bittorrent':
1514 continue
1516 self.playback_episodes([episode])
1519 def format_episode_list(self, episode_list, max_episodes=10):
1521 Format a list of episode names for notifications
1523 Will truncate long episode names and limit the amount of
1524 episodes displayed (max_episodes=10).
1526 The episode_list parameter should be a list of strings.
1528 MAX_TITLE_LENGTH = 100
1530 result = []
1531 for title in episode_list[:min(len(episode_list), max_episodes)]:
1532 if len(title) > MAX_TITLE_LENGTH:
1533 middle = (MAX_TITLE_LENGTH/2)-2
1534 title = '%s...%s' % (title[0:middle], title[-middle:])
1535 result.append(saxutils.escape(title))
1536 result.append('\n')
1538 more_episodes = len(episode_list) - max_episodes
1539 if more_episodes > 0:
1540 result.append('(...')
1541 result.append(N_('%d more episode', '%d more episodes', more_episodes) % more_episodes)
1542 result.append('...)')
1544 return (''.join(result)).strip()
1546 def _for_each_task_set_status(self, tasks, status, force_start=False):
1547 episode_urls = set()
1548 model = self.treeDownloads.get_model()
1549 for row_reference, task in tasks:
1550 if status == download.DownloadTask.QUEUED:
1551 # Only queue task when its paused/failed/cancelled (or forced)
1552 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED) or force_start:
1553 self.download_queue_manager.add_task(task, force_start)
1554 self.enable_download_list_update()
1555 elif status == download.DownloadTask.CANCELLED:
1556 # Cancelling a download allowed when downloading/queued
1557 if task.status in (task.QUEUED, task.DOWNLOADING):
1558 task.status = status
1559 # Cancelling paused downloads requires a call to .run()
1560 elif task.status == task.PAUSED:
1561 task.status = status
1562 # Call run, so the partial file gets deleted
1563 task.run()
1564 elif status == download.DownloadTask.PAUSED:
1565 # Pausing a download only when queued/downloading
1566 if task.status in (task.DOWNLOADING, task.QUEUED):
1567 task.status = status
1568 elif status is None:
1569 # Remove the selected task - cancel downloading/queued tasks
1570 if task.status in (task.QUEUED, task.DOWNLOADING):
1571 task.status = task.CANCELLED
1572 model.remove(model.get_iter(row_reference.get_path()))
1573 # Remember the URL, so we can tell the UI to update
1574 try:
1575 # We don't "see" this task anymore - remove it;
1576 # this is needed, so update_episode_list_icons()
1577 # below gets the correct list of "seen" tasks
1578 self.download_tasks_seen.remove(task)
1579 except KeyError, key_error:
1580 log('Cannot remove task from "seen" list: %s', task, sender=self)
1581 episode_urls.add(task.url)
1582 # Tell the task that it has been removed (so it can clean up)
1583 task.removed_from_list()
1584 else:
1585 # We can (hopefully) simply set the task status here
1586 task.status = status
1587 # Tell the podcasts tab to update icons for our removed podcasts
1588 self.update_episode_list_icons(episode_urls)
1589 # Update the tab title and downloads list
1590 self.update_downloads_list()
1592 def treeview_downloads_show_context_menu(self, treeview, event):
1593 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1594 if not paths:
1595 if not hasattr(treeview, 'is_rubber_banding_active'):
1596 return True
1597 else:
1598 return not treeview.is_rubber_banding_active()
1600 if event.button == self.context_menu_mouse_button:
1601 selected_tasks, can_queue, can_cancel, can_pause, can_remove, can_force = \
1602 self.downloads_list_get_selection(model, paths)
1604 def make_menu_item(label, stock_id, tasks, status, sensitive, force_start=False):
1605 # This creates a menu item for selection-wide actions
1606 item = gtk.ImageMenuItem(label)
1607 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1608 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status, force_start))
1609 item.set_sensitive(sensitive)
1610 return self.set_finger_friendly(item)
1612 menu = gtk.Menu()
1614 item = gtk.ImageMenuItem(_('Episode details'))
1615 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1616 if len(selected_tasks) == 1:
1617 row_reference, task = selected_tasks[0]
1618 episode = task.episode
1619 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1620 else:
1621 item.set_sensitive(False)
1622 menu.append(self.set_finger_friendly(item))
1623 menu.append(gtk.SeparatorMenuItem())
1624 if can_force:
1625 menu.append(make_menu_item(_('Start download now'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, True, True))
1626 else:
1627 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue, False))
1628 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1629 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1630 menu.append(gtk.SeparatorMenuItem())
1631 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1633 if gpodder.ui.maemo:
1634 # Because we open the popup on left-click for Maemo,
1635 # we also include a non-action to close the menu
1636 menu.append(gtk.SeparatorMenuItem())
1637 item = gtk.ImageMenuItem(_('Close this menu'))
1638 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1640 menu.append(self.set_finger_friendly(item))
1642 menu.show_all()
1643 menu.popup(None, None, None, event.button, event.time)
1644 return True
1646 def treeview_channels_show_context_menu(self, treeview, event):
1647 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1648 if not paths:
1649 return True
1651 # Check for valid channel id, if there's no id then
1652 # assume that it is a proxy channel or equivalent
1653 # and cannot be operated with right click
1654 if self.active_channel.id is None:
1655 return True
1657 if event.button == 3:
1658 menu = gtk.Menu()
1660 ICON = lambda x: x
1662 item = gtk.ImageMenuItem( _('Open download folder'))
1663 item.set_image( gtk.image_new_from_icon_name(ICON('folder-open'), gtk.ICON_SIZE_MENU))
1664 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1665 menu.append( item)
1667 item = gtk.ImageMenuItem( _('Update Feed'))
1668 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1669 item.connect('activate', self.on_itemUpdateChannel_activate )
1670 item.set_sensitive( not self.updating_feed_cache )
1671 menu.append( item)
1673 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1674 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1675 item.connect('activate', self.update_m3u_playlist_clicked)
1676 menu.append(item)
1678 if self.active_channel.link:
1679 item = gtk.ImageMenuItem(_('Visit website'))
1680 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1681 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1682 menu.append(item)
1684 if self.active_channel.channel_is_locked:
1685 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1686 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1687 item.connect('activate', self.on_channel_toggle_lock_activate)
1688 menu.append(self.set_finger_friendly(item))
1689 else:
1690 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1691 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1692 item.connect('activate', self.on_channel_toggle_lock_activate)
1693 menu.append(self.set_finger_friendly(item))
1696 menu.append( gtk.SeparatorMenuItem())
1698 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1699 item.connect( 'activate', self.on_itemEditChannel_activate)
1700 menu.append( item)
1702 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1703 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1704 menu.append( item)
1706 menu.show_all()
1707 # Disable tooltips while we are showing the menu, so
1708 # the tooltip will not appear over the menu
1709 self.treeview_allow_tooltips(self.treeChannels, False)
1710 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1711 menu.popup( None, None, None, event.button, event.time)
1713 return True
1715 def on_itemClose_activate(self, widget):
1716 if self.tray_icon is not None:
1717 self.iconify_main_window()
1718 else:
1719 self.on_gPodder_delete_event(widget)
1721 def cover_file_removed(self, channel_url):
1723 The Cover Downloader calls this when a previously-
1724 available cover has been removed from the disk. We
1725 have to update our model to reflect this change.
1727 self.podcast_list_model.delete_cover_by_url(channel_url)
1729 def cover_download_finished(self, channel_url, pixbuf):
1731 The Cover Downloader calls this when it has finished
1732 downloading (or registering, if already downloaded)
1733 a new channel cover, which is ready for displaying.
1735 self.podcast_list_model.add_cover_by_url(channel_url, pixbuf)
1737 def save_episodes_as_file(self, episodes):
1738 for episode in episodes:
1739 self.save_episode_as_file(episode)
1741 def save_episode_as_file(self, episode):
1742 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1743 if episode.was_downloaded(and_exists=True):
1744 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1745 copy_from = episode.local_filename(create=False)
1746 assert copy_from is not None
1747 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1748 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1749 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1751 def copy_episodes_bluetooth(self, episodes):
1752 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1754 def convert_and_send_thread(episode):
1755 for episode in episodes:
1756 filename = episode.local_filename(create=False)
1757 assert filename is not None
1758 destfile = os.path.join(tempfile.gettempdir(), \
1759 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1760 (base, ext) = os.path.splitext(filename)
1761 if not destfile.endswith(ext):
1762 destfile += ext
1764 try:
1765 shutil.copyfile(filename, destfile)
1766 util.bluetooth_send_file(destfile)
1767 except:
1768 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1769 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1771 util.delete_file(destfile)
1773 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1775 def get_device_name(self):
1776 if self.config.device_type == 'ipod':
1777 return _('iPod')
1778 elif self.config.device_type in ('filesystem', 'mtp'):
1779 return _('MP3 player')
1780 else:
1781 return '(unknown device)'
1783 def _treeview_button_released(self, treeview, event):
1784 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1785 dy = int(abs(event.y-ypos))
1786 dx = int(event.x-xpos)
1788 selection = treeview.get_selection()
1789 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1790 if path is None or dy > 30:
1791 return (False, dx, dy)
1793 path, column, x, y = path
1794 selection.select_path(path)
1795 treeview.set_cursor(path)
1796 treeview.grab_focus()
1798 return (True, dx, dy)
1800 def treeview_channels_handle_gestures(self, treeview, event):
1801 if self.currently_updating:
1802 return False
1804 selected, dx, dy = self._treeview_button_released(treeview, event)
1806 if selected:
1807 if self.config.maemo_enable_gestures:
1808 if dx > 70:
1809 self.on_itemUpdateChannel_activate()
1810 elif dx < -70:
1811 self.on_itemEditChannel_activate(treeview)
1813 return False
1815 def treeview_available_handle_gestures(self, treeview, event):
1816 selected, dx, dy = self._treeview_button_released(treeview, event)
1818 if selected:
1819 if self.config.maemo_enable_gestures:
1820 if dx > 70:
1821 self.on_playback_selected_episodes(None)
1822 return True
1823 elif dx < -70:
1824 self.on_shownotes_selected_episodes(None)
1825 return True
1827 # Pass the event to the context menu handler for treeAvailable
1828 self.treeview_available_show_context_menu(treeview, event)
1830 return True
1832 def treeview_available_show_context_menu(self, treeview, event):
1833 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1834 if not paths:
1835 if not hasattr(treeview, 'is_rubber_banding_active'):
1836 return True
1837 else:
1838 return not treeview.is_rubber_banding_active()
1840 if event.button == self.context_menu_mouse_button:
1841 episodes = self.get_selected_episodes()
1842 any_locked = any(e.is_locked for e in episodes)
1843 any_played = any(e.is_played for e in episodes)
1844 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1845 downloaded = all(e.was_downloaded(and_exists=True) for e in episodes)
1846 downloading = any(self.episode_is_downloading(e) for e in episodes)
1848 menu = gtk.Menu()
1850 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1852 if open_instead_of_play:
1853 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1854 elif downloaded:
1855 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1856 else:
1857 item = gtk.ImageMenuItem(_('Stream'))
1858 item.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
1860 item.set_sensitive(can_play and not downloading)
1861 item.connect('activate', self.on_playback_selected_episodes)
1862 menu.append(self.set_finger_friendly(item))
1864 if not can_cancel:
1865 item = gtk.ImageMenuItem(_('Download'))
1866 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1867 item.set_sensitive(can_download)
1868 item.connect('activate', self.on_download_selected_episodes)
1869 menu.append(self.set_finger_friendly(item))
1870 else:
1871 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1872 item.connect('activate', self.on_item_cancel_download_activate)
1873 menu.append(self.set_finger_friendly(item))
1875 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1876 item.set_sensitive(can_delete)
1877 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1878 menu.append(self.set_finger_friendly(item))
1880 ICON = lambda x: x
1882 # Ok, this probably makes sense to only display for downloaded files
1883 if downloaded:
1884 menu.append(gtk.SeparatorMenuItem())
1885 share_item = gtk.MenuItem(_('Send to'))
1886 menu.append(share_item)
1887 share_menu = gtk.Menu()
1889 item = gtk.ImageMenuItem(_('Local folder'))
1890 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
1891 item.connect('button-press-event', lambda w, ee: self.save_episodes_as_file(episodes))
1892 share_menu.append(self.set_finger_friendly(item))
1893 if self.bluetooth_available:
1894 item = gtk.ImageMenuItem(_('Bluetooth device'))
1895 item.set_image(gtk.image_new_from_icon_name(ICON('bluetooth'), gtk.ICON_SIZE_MENU))
1896 item.connect('button-press-event', lambda w, ee: self.copy_episodes_bluetooth(episodes))
1897 share_menu.append(self.set_finger_friendly(item))
1898 if can_transfer:
1899 item = gtk.ImageMenuItem(self.get_device_name())
1900 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
1901 item.connect('button-press-event', lambda w, ee: self.on_sync_to_ipod_activate(w, episodes))
1902 share_menu.append(self.set_finger_friendly(item))
1904 share_item.set_submenu(share_menu)
1906 if (downloaded or one_is_new or can_download) and not downloading:
1907 menu.append(gtk.SeparatorMenuItem())
1908 if one_is_new:
1909 item = gtk.CheckMenuItem(_('New'))
1910 item.set_active(True)
1911 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1912 menu.append(self.set_finger_friendly(item))
1913 elif can_download:
1914 item = gtk.CheckMenuItem(_('New'))
1915 item.set_active(False)
1916 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1917 menu.append(self.set_finger_friendly(item))
1919 if downloaded:
1920 item = gtk.CheckMenuItem(_('Played'))
1921 item.set_active(any_played)
1922 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, not any_played))
1923 menu.append(self.set_finger_friendly(item))
1925 item = gtk.CheckMenuItem(_('Keep episode'))
1926 item.set_active(any_locked)
1927 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, not any_locked))
1928 menu.append(self.set_finger_friendly(item))
1930 menu.append(gtk.SeparatorMenuItem())
1931 # Single item, add episode information menu item
1932 item = gtk.ImageMenuItem(_('Episode details'))
1933 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1934 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
1935 menu.append(self.set_finger_friendly(item))
1937 if gpodder.ui.maemo:
1938 # Because we open the popup on left-click for Maemo,
1939 # we also include a non-action to close the menu
1940 menu.append(gtk.SeparatorMenuItem())
1941 item = gtk.ImageMenuItem(_('Close this menu'))
1942 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1943 menu.append(self.set_finger_friendly(item))
1945 menu.show_all()
1946 # Disable tooltips while we are showing the menu, so
1947 # the tooltip will not appear over the menu
1948 self.treeview_allow_tooltips(self.treeAvailable, False)
1949 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
1950 menu.popup( None, None, None, event.button, event.time)
1952 return True
1954 def set_title(self, new_title):
1955 if not gpodder.ui.fremantle:
1956 self.default_title = new_title
1957 self.gPodder.set_title(new_title)
1959 def update_episode_list_icons(self, urls=None, selected=False, all=False):
1961 Updates the status icons in the episode list.
1963 If urls is given, it should be a list of URLs
1964 of episodes that should be updated.
1966 If urls is None, set ONE OF selected, all to
1967 True (the former updates just the selected
1968 episodes and the latter updates all episodes).
1970 additional_args = (self.episode_is_downloading, \
1971 self.config.episode_list_descriptions and gpodder.ui.desktop, \
1972 self.config.episode_list_thumbnails and gpodder.ui.desktop)
1974 if urls is not None:
1975 # We have a list of URLs to walk through
1976 self.episode_list_model.update_by_urls(urls, *additional_args)
1977 elif selected and not all:
1978 # We should update all selected episodes
1979 selection = self.treeAvailable.get_selection()
1980 model, paths = selection.get_selected_rows()
1981 for path in reversed(paths):
1982 iter = model.get_iter(path)
1983 self.episode_list_model.update_by_filter_iter(iter, \
1984 *additional_args)
1985 elif all and not selected:
1986 # We update all (even the filter-hidden) episodes
1987 self.episode_list_model.update_all(*additional_args)
1988 else:
1989 # Wrong/invalid call - have to specify at least one parameter
1990 raise ValueError('Invalid call to update_episode_list_icons')
1992 def episode_list_status_changed(self, episodes):
1993 self.update_episode_list_icons(set(e.url for e in episodes))
1994 self.update_podcast_list_model(set(e.channel.url for e in episodes))
1995 self.db.commit()
1997 def clean_up_downloads(self, delete_partial=False):
1998 # Clean up temporary files left behind by old gPodder versions
1999 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
2001 if delete_partial:
2002 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
2004 for tempfile in temporary_files:
2005 util.delete_file(tempfile)
2007 # Clean up empty download folders and abandoned download folders
2008 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
2009 for ddir in download_dirs:
2010 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2011 globr = glob.glob(os.path.join(ddir, '*'))
2012 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
2013 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
2014 shutil.rmtree(ddir, ignore_errors=True)
2016 def streaming_possible(self):
2017 if gpodder.ui.desktop:
2018 # User has to have a media player set on the Desktop, or else we
2019 # would probably open the browser when giving a URL to xdg-open..
2020 return (self.config.player and self.config.player != 'default')
2021 elif gpodder.ui.maemo:
2022 # On Maemo, the default is to use the Nokia Media Player, which is
2023 # already able to deal with HTTP URLs the right way, so we
2024 # unconditionally enable streaming always on Maemo
2025 return True
2027 return False
2029 def playback_episodes_for_real(self, episodes):
2030 groups = collections.defaultdict(list)
2031 for episode in episodes:
2032 file_type = episode.file_type()
2033 if file_type == 'video' and self.config.videoplayer and \
2034 self.config.videoplayer != 'default':
2035 player = self.config.videoplayer
2036 if gpodder.ui.diablo:
2037 # Use the wrapper script if it's installed to crop 3GP YouTube
2038 # videos to fit the screen (looks much nicer than w/ black border)
2039 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
2040 player = 'gpodder-mplayer'
2041 elif gpodder.ui.fremantle and player == 'mplayer':
2042 player = 'mplayer -fs %F'
2043 elif file_type == 'audio' and self.config.player and \
2044 self.config.player != 'default':
2045 player = self.config.player
2046 else:
2047 player = 'default'
2049 if file_type not in ('audio', 'video') or \
2050 (file_type == 'audio' and not self.config.audio_played_dbus) or \
2051 (file_type == 'video' and not self.config.video_played_dbus):
2052 # Mark episode as played in the database
2053 episode.mark(is_played=True)
2054 self.mygpo_client.on_playback([episode])
2056 filename = episode.local_filename(create=False)
2057 if filename is None or not os.path.exists(filename):
2058 filename = episode.url
2059 if youtube.is_video_link(filename):
2060 fmt_id = self.config.youtube_preferred_fmt_id
2061 if gpodder.ui.fremantle:
2062 fmt_id = 5
2063 filename = youtube.get_real_download_url(filename, fmt_id)
2064 groups[player].append(filename)
2066 # Open episodes with system default player
2067 if 'default' in groups:
2068 for filename in groups['default']:
2069 log('Opening with system default: %s', filename, sender=self)
2070 util.gui_open(filename)
2071 del groups['default']
2072 elif gpodder.ui.maemo:
2073 # When on Maemo and not opening with default, show a notification
2074 # (no startup notification for Panucci / MPlayer yet...)
2075 if len(episodes) == 1:
2076 text = _('Opening %s') % episodes[0].title
2077 else:
2078 count = len(episodes)
2079 text = N_('Opening %d episode', 'Opening %d episodes', count) % count
2081 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
2083 def destroy_banner_later(banner):
2084 banner.destroy()
2085 return False
2086 gobject.timeout_add(5000, destroy_banner_later, banner)
2088 # For each type now, go and create play commands
2089 for group in groups:
2090 for command in util.format_desktop_command(group, groups[group]):
2091 log('Executing: %s', repr(command), sender=self)
2092 subprocess.Popen(command)
2094 # Persist episode status changes to the database
2095 self.db.commit()
2097 # Flush updated episode status
2098 self.mygpo_client.flush()
2100 def playback_episodes(self, episodes):
2101 # We need to create a list, because we run through it more than once
2102 episodes = list(PodcastEpisode.sort_by_pubdate(e for e in episodes if \
2103 e.was_downloaded(and_exists=True) or self.streaming_possible()))
2105 try:
2106 self.playback_episodes_for_real(episodes)
2107 except Exception, e:
2108 log('Error in playback!', sender=self, traceback=True)
2109 if gpodder.ui.desktop:
2110 self.show_message(_('Please check your media player settings in the preferences dialog.'), \
2111 _('Error opening player'), widget=self.toolPreferences)
2112 else:
2113 self.show_message(_('Please check your media player settings in the preferences dialog.'))
2115 channel_urls = set()
2116 episode_urls = set()
2117 for episode in episodes:
2118 channel_urls.add(episode.channel.url)
2119 episode_urls.add(episode.url)
2120 self.update_episode_list_icons(episode_urls)
2121 self.update_podcast_list_model(channel_urls)
2123 def play_or_download(self):
2124 if not gpodder.ui.fremantle:
2125 if self.wNotebook.get_current_page() > 0:
2126 if gpodder.ui.desktop:
2127 self.toolCancel.set_sensitive(True)
2128 return
2130 if self.currently_updating:
2131 return (False, False, False, False, False, False)
2133 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
2134 ( is_played, is_locked ) = (False,)*2
2136 open_instead_of_play = False
2138 selection = self.treeAvailable.get_selection()
2139 if selection.count_selected_rows() > 0:
2140 (model, paths) = selection.get_selected_rows()
2142 for path in paths:
2143 try:
2144 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
2145 except TypeError, te:
2146 log('Invalid episode at path %s', str(path), sender=self)
2147 continue
2149 if episode.file_type() not in ('audio', 'video'):
2150 open_instead_of_play = True
2152 if episode.was_downloaded():
2153 can_play = episode.was_downloaded(and_exists=True)
2154 is_played = episode.is_played
2155 is_locked = episode.is_locked
2156 if not can_play:
2157 can_download = True
2158 else:
2159 if self.episode_is_downloading(episode):
2160 can_cancel = True
2161 else:
2162 can_download = True
2164 can_download = can_download and not can_cancel
2165 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
2166 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
2167 can_delete = not can_cancel
2169 if gpodder.ui.desktop:
2170 if open_instead_of_play:
2171 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
2172 else:
2173 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
2174 self.toolPlay.set_sensitive( can_play)
2175 self.toolDownload.set_sensitive( can_download)
2176 self.toolTransfer.set_sensitive( can_transfer)
2177 self.toolCancel.set_sensitive( can_cancel)
2179 if not gpodder.ui.fremantle:
2180 self.item_cancel_download.set_sensitive(can_cancel)
2181 self.itemDownloadSelected.set_sensitive(can_download)
2182 self.itemOpenSelected.set_sensitive(can_play)
2183 self.itemPlaySelected.set_sensitive(can_play)
2184 self.itemDeleteSelected.set_sensitive(can_delete)
2185 self.item_toggle_played.set_sensitive(can_play)
2186 self.item_toggle_lock.set_sensitive(can_play)
2187 self.itemOpenSelected.set_visible(open_instead_of_play)
2188 self.itemPlaySelected.set_visible(not open_instead_of_play)
2190 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
2192 def on_cbMaxDownloads_toggled(self, widget, *args):
2193 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
2195 def on_cbLimitDownloads_toggled(self, widget, *args):
2196 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
2198 def episode_new_status_changed(self, urls):
2199 self.update_podcast_list_model()
2200 self.update_episode_list_icons(urls)
2202 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
2203 """Update the podcast list treeview model
2205 If urls is given, it should list the URLs of each
2206 podcast that has to be updated in the list.
2208 If selected is True, only update the model contents
2209 for the currently-selected podcast - nothing more.
2211 The caller can optionally specify "select_url",
2212 which is the URL of the podcast that is to be
2213 selected in the list after the update is complete.
2214 This only works if the podcast list has to be
2215 reloaded; i.e. something has been added or removed
2216 since the last update of the podcast list).
2218 selection = self.treeChannels.get_selection()
2219 model, iter = selection.get_selected()
2221 if self.config.podcast_list_view_all and not self.channel_list_changed:
2222 # Update "all episodes" view in any case (if enabled)
2223 self.podcast_list_model.update_first_row()
2225 if selected:
2226 # very cheap! only update selected channel
2227 if iter is not None:
2228 # If we have selected the "all episodes" view, we have
2229 # to update all channels for selected episodes:
2230 if self.config.podcast_list_view_all and \
2231 self.podcast_list_model.iter_is_first_row(iter):
2232 urls = self.get_podcast_urls_from_selected_episodes()
2233 self.podcast_list_model.update_by_urls(urls)
2234 else:
2235 # Otherwise just update the selected row (a podcast)
2236 self.podcast_list_model.update_by_filter_iter(iter)
2237 elif not self.channel_list_changed:
2238 # we can keep the model, but have to update some
2239 if urls is None:
2240 # still cheaper than reloading the whole list
2241 self.podcast_list_model.update_all()
2242 else:
2243 # ok, we got a bunch of urls to update
2244 self.podcast_list_model.update_by_urls(urls)
2245 else:
2246 if model and iter and select_url is None:
2247 # Get the URL of the currently-selected podcast
2248 select_url = model.get_value(iter, PodcastListModel.C_URL)
2250 # Update the podcast list model with new channels
2251 self.podcast_list_model.set_channels(self.db, self.config, self.channels)
2253 try:
2254 selected_iter = model.get_iter_first()
2255 # Find the previously-selected URL in the new
2256 # model if we have an URL (else select first)
2257 if select_url is not None:
2258 pos = model.get_iter_first()
2259 while pos is not None:
2260 url = model.get_value(pos, PodcastListModel.C_URL)
2261 if url == select_url:
2262 selected_iter = pos
2263 break
2264 pos = model.iter_next(pos)
2266 if not gpodder.ui.fremantle:
2267 if selected_iter is not None:
2268 selection.select_iter(selected_iter)
2269 self.on_treeChannels_cursor_changed(self.treeChannels)
2270 except:
2271 log('Cannot select podcast in list', traceback=True, sender=self)
2272 self.channel_list_changed = False
2274 def episode_is_downloading(self, episode):
2275 """Returns True if the given episode is being downloaded at the moment"""
2276 if episode is None:
2277 return False
2279 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
2281 def update_episode_list_model(self):
2282 if self.channels and self.active_channel is not None:
2283 if gpodder.ui.fremantle:
2284 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2286 self.currently_updating = True
2287 self.episode_list_model.clear()
2288 self.episode_list_model.reset_update_progress()
2289 self.treeAvailable.set_model(self.empty_episode_list_model)
2290 def do_update_episode_list_model():
2291 additional_args = (self.episode_is_downloading, \
2292 self.config.episode_list_descriptions and gpodder.ui.desktop, \
2293 self.config.episode_list_thumbnails and gpodder.ui.desktop, \
2294 self.treeAvailable)
2295 self.episode_list_model.add_from_channel(self.active_channel, *additional_args)
2297 def on_episode_list_model_updated():
2298 if gpodder.ui.fremantle:
2299 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2300 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
2301 self.treeAvailable.columns_autosize()
2302 self.currently_updating = False
2303 self.play_or_download()
2304 util.idle_add(on_episode_list_model_updated)
2305 threading.Thread(target=do_update_episode_list_model).start()
2306 else:
2307 self.episode_list_model.clear()
2309 def offer_new_episodes(self, channels=None):
2310 new_episodes = self.get_new_episodes(channels)
2311 if new_episodes:
2312 self.new_episodes_show(new_episodes)
2313 return True
2314 return False
2316 def add_podcast_list(self, urls, auth_tokens=None):
2317 """Subscribe to a list of podcast given their URLs
2319 If auth_tokens is given, it should be a dictionary
2320 mapping URLs to (username, password) tuples."""
2322 if auth_tokens is None:
2323 auth_tokens = {}
2325 # Sort and split the URL list into five buckets
2326 queued, failed, existing, worked, authreq = [], [], [], [], []
2327 for input_url in urls:
2328 url = util.normalize_feed_url(input_url)
2329 if url is None:
2330 # Fail this one because the URL is not valid
2331 failed.append(input_url)
2332 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
2333 # A podcast already exists in the list for this URL
2334 existing.append(url)
2335 else:
2336 # This URL has survived the first round - queue for add
2337 queued.append(url)
2338 if url != input_url and input_url in auth_tokens:
2339 auth_tokens[url] = auth_tokens[input_url]
2341 error_messages = {}
2342 redirections = {}
2344 progress = ProgressIndicator(_('Adding podcasts'), \
2345 _('Please wait while episode information is downloaded.'), \
2346 parent=self.get_dialog_parent())
2348 def on_after_update():
2349 progress.on_finished()
2350 # Report already-existing subscriptions to the user
2351 if existing:
2352 title = _('Existing subscriptions skipped')
2353 message = _('You are already subscribed to these podcasts:') \
2354 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
2355 self.show_message(message, title, widget=self.treeChannels)
2357 # Report subscriptions that require authentication
2358 if authreq:
2359 retry_podcasts = {}
2360 for url in authreq:
2361 title = _('Podcast requires authentication')
2362 message = _('Please login to %s:') % (saxutils.escape(url),)
2363 success, auth_tokens = self.show_login_dialog(title, message)
2364 if success:
2365 retry_podcasts[url] = auth_tokens
2366 else:
2367 # Stop asking the user for more login data
2368 retry_podcasts = {}
2369 for url in authreq:
2370 error_messages[url] = _('Authentication failed')
2371 failed.append(url)
2372 break
2374 # If we have authentication data to retry, do so here
2375 if retry_podcasts:
2376 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
2378 # Report website redirections
2379 for url in redirections:
2380 title = _('Website redirection detected')
2381 message = _('The URL %(url)s redirects to %(target)s.') \
2382 + '\n\n' + _('Do you want to visit the website now?')
2383 message = message % {'url': url, 'target': redirections[url]}
2384 if self.show_confirmation(message, title):
2385 util.open_website(url)
2386 else:
2387 break
2389 # Report failed subscriptions to the user
2390 if failed:
2391 title = _('Could not add some podcasts')
2392 message = _('Some podcasts could not be added to your list:') \
2393 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
2394 error_messages.get(url, _('Unknown')))) for url in failed)
2395 self.show_message(message, title, important=True)
2397 # Upload subscription changes to gpodder.net
2398 self.mygpo_client.on_subscribe(worked)
2400 # If at least one podcast has been added, save and update all
2401 if self.channel_list_changed:
2402 # Fix URLs if mygpo has rewritten them
2403 self.rewrite_urls_mygpo()
2405 self.save_channels_opml()
2407 # If only one podcast was added, select it after the update
2408 if len(worked) == 1:
2409 url = worked[0]
2410 else:
2411 url = None
2413 # Update the list of subscribed podcasts
2414 self.update_feed_cache(force_update=False, select_url_afterwards=url)
2415 self.update_podcasts_tab()
2417 # Offer to download new episodes
2418 self.offer_new_episodes(channels=[c for c in self.channels if c.url in worked])
2420 def thread_proc():
2421 # After the initial sorting and splitting, try all queued podcasts
2422 length = len(queued)
2423 for index, url in enumerate(queued):
2424 progress.on_progress(float(index)/float(length))
2425 progress.on_message(url)
2426 log('QUEUE RUNNER: %s', url, sender=self)
2427 try:
2428 # The URL is valid and does not exist already - subscribe!
2429 channel = PodcastChannel.load(self.db, url=url, create=True, \
2430 authentication_tokens=auth_tokens.get(url, None), \
2431 max_episodes=self.config.max_episodes_per_feed, \
2432 download_dir=self.config.download_dir, \
2433 allow_empty_feeds=self.config.allow_empty_feeds)
2435 try:
2436 username, password = util.username_password_from_url(url)
2437 except ValueError, ve:
2438 username, password = (None, None)
2440 if username is not None and channel.username is None and \
2441 password is not None and channel.password is None:
2442 channel.username = username
2443 channel.password = password
2444 channel.save()
2446 self._update_cover(channel)
2447 except feedcore.AuthenticationRequired:
2448 if url in auth_tokens:
2449 # Fail for wrong authentication data
2450 error_messages[url] = _('Authentication failed')
2451 failed.append(url)
2452 else:
2453 # Queue for login dialog later
2454 authreq.append(url)
2455 continue
2456 except feedcore.WifiLogin, error:
2457 redirections[url] = error.data
2458 failed.append(url)
2459 error_messages[url] = _('Redirection detected')
2460 continue
2461 except Exception, e:
2462 log('Subscription error: %s', e, traceback=True, sender=self)
2463 error_messages[url] = str(e)
2464 failed.append(url)
2465 continue
2467 assert channel is not None
2468 worked.append(channel.url)
2469 self.channels.append(channel)
2470 self.channel_list_changed = True
2471 util.idle_add(on_after_update)
2472 threading.Thread(target=thread_proc).start()
2474 def save_channels_opml(self):
2475 exporter = opml.Exporter(gpodder.subscription_file)
2476 return exporter.write(self.channels)
2478 def find_episode(self, podcast_url, episode_url):
2479 """Find an episode given its podcast and episode URL
2481 The function will return a PodcastEpisode object if
2482 the episode is found, or None if it's not found.
2484 for podcast in self.channels:
2485 if podcast_url == podcast.url:
2486 for episode in podcast.get_all_episodes():
2487 if episode_url == episode.url:
2488 return episode
2490 return None
2492 def process_received_episode_actions(self, updated_urls):
2493 """Process/merge episode actions from gpodder.net
2495 This function will merge all changes received from
2496 the server to the local database and update the
2497 status of the affected episodes as necessary.
2499 indicator = ProgressIndicator(_('Merging episode actions'), \
2500 _('Episode actions from gpodder.net are merged.'), \
2501 False, self.get_dialog_parent())
2503 for idx, action in enumerate(self.mygpo_client.get_episode_actions(updated_urls)):
2504 if action.action == 'play':
2505 episode = self.find_episode(action.podcast_url, \
2506 action.episode_url)
2508 if episode is not None:
2509 log('Play action for %s', episode.url, sender=self)
2510 episode.mark(is_played=True)
2512 if action.timestamp > episode.current_position_updated:
2513 log('Updating position for %s', episode.url, sender=self)
2514 episode.current_position = action.position
2515 episode.current_position_updated = action.timestamp
2517 if action.total:
2518 log('Updating total time for %s', episode.url, sender=self)
2519 episode.total_time = action.total
2521 episode.save()
2522 elif action.action == 'delete':
2523 episode = self.find_episode(action.podcast_url, \
2524 action.episode_url)
2526 if episode is not None:
2527 if not episode.was_downloaded(and_exists=True):
2528 # Set the episode to a "deleted" state
2529 log('Marking as deleted: %s', episode.url, sender=self)
2530 episode.delete_from_disk()
2531 episode.save()
2533 indicator.on_message(N_('%d action processed', '%d actions processed', idx) % idx)
2534 gtk.main_iteration(False)
2536 indicator.on_finished()
2537 self.db.commit()
2540 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
2541 self.db.commit()
2542 self.updating_feed_cache = False
2544 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2546 # Process received episode actions for all updated URLs
2547 self.process_received_episode_actions(updated_urls)
2549 self.channel_list_changed = True
2550 self.update_podcast_list_model(select_url=select_url_afterwards)
2552 # Only search for new episodes in podcasts that have been
2553 # updated, not in other podcasts (for single-feed updates)
2554 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
2556 if gpodder.ui.fremantle:
2557 self.button_subscribe.set_sensitive(True)
2558 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
2559 self.ICON_GENERAL_REFRESH, gtk.ICON_SIZE_BUTTON))
2560 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
2561 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
2562 self.update_podcasts_tab()
2563 self.update_episode_list_model()
2564 if self.feed_cache_update_cancelled:
2565 return
2567 if episodes:
2568 if self.config.auto_download == 'quiet' and not self.config.auto_update_feeds:
2569 # New episodes found, but we should do nothing
2570 self.show_message(_('New episodes are available.'))
2571 elif self.config.auto_download == 'always':
2572 count = len(episodes)
2573 title = N_('Downloading %d new episode.', 'Downloading %d new episodes.', count) % count
2574 self.show_message(title)
2575 self.download_episode_list(episodes)
2576 elif self.config.auto_download == 'queue':
2577 self.show_message(_('New episodes have been added to the download list.'))
2578 self.download_episode_list_paused(episodes)
2579 else:
2580 self.new_episodes_show(episodes)
2581 elif not self.config.auto_update_feeds:
2582 self.show_message(_('No new episodes. Please check for new episodes later.'))
2583 return
2585 if self.tray_icon:
2586 self.tray_icon.set_status()
2588 if self.feed_cache_update_cancelled:
2589 # The user decided to abort the feed update
2590 self.show_update_feeds_buttons()
2591 elif not episodes:
2592 # Nothing new here - but inform the user
2593 self.pbFeedUpdate.set_fraction(1.0)
2594 self.pbFeedUpdate.set_text(_('No new episodes'))
2595 self.feed_cache_update_cancelled = True
2596 self.btnCancelFeedUpdate.show()
2597 self.btnCancelFeedUpdate.set_sensitive(True)
2598 if gpodder.ui.maemo:
2599 # btnCancelFeedUpdate is a ToolButton on Maemo
2600 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
2601 else:
2602 # btnCancelFeedUpdate is a normal gtk.Button
2603 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
2604 else:
2605 count = len(episodes)
2606 # New episodes are available
2607 self.pbFeedUpdate.set_fraction(1.0)
2608 # Are we minimized and should we auto download?
2609 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
2610 self.download_episode_list(episodes)
2611 title = N_('Downloading %d new episode.', 'Downloading %d new episodes.', count) % count
2612 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2613 self.show_update_feeds_buttons()
2614 elif self.config.auto_download == 'queue':
2615 self.download_episode_list_paused(episodes)
2616 title = N_('%d new episode added to download list.', '%d new episodes added to download list.', count) % count
2617 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
2618 self.show_update_feeds_buttons()
2619 else:
2620 self.show_update_feeds_buttons()
2621 # New episodes are available and we are not minimized
2622 if not self.config.do_not_show_new_episodes_dialog:
2623 self.new_episodes_show(episodes, notification=True)
2624 else:
2625 message = N_('%d new episode available', '%d new episodes available', count) % count
2626 self.pbFeedUpdate.set_text(message)
2628 def _update_cover(self, channel):
2629 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
2630 self.cover_downloader.request_cover(channel)
2632 def update_feed_cache_proc(self, channels, select_url_afterwards):
2633 total = len(channels)
2635 for updated, channel in enumerate(channels):
2636 if not self.feed_cache_update_cancelled:
2637 try:
2638 channel.update(max_episodes=self.config.max_episodes_per_feed)
2639 self._update_cover(channel)
2640 except Exception, e:
2641 d = {'url': saxutils.escape(channel.url), 'message': saxutils.escape(str(e))}
2642 if d['message']:
2643 message = _('Error while updating %(url)s: %(message)s')
2644 else:
2645 message = _('The feed at %(url)s could not be updated.')
2646 self.notification(message % d, _('Error while updating feed'), widget=self.treeChannels)
2647 log('Error: %s', str(e), sender=self, traceback=True)
2649 if self.feed_cache_update_cancelled:
2650 break
2652 if gpodder.ui.fremantle:
2653 util.idle_add(self.button_refresh.set_title, \
2654 _('%(position)d/%(total)d updated') % {'position': updated, 'total': total})
2655 continue
2657 # By the time we get here the update may have already been cancelled
2658 if not self.feed_cache_update_cancelled:
2659 def update_progress():
2660 d = {'podcast': channel.title, 'position': updated, 'total': total}
2661 progression = _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2662 self.pbFeedUpdate.set_text(progression)
2663 if self.tray_icon:
2664 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2665 self.pbFeedUpdate.set_fraction(float(updated)/float(total))
2666 util.idle_add(update_progress)
2668 updated_urls = [c.url for c in channels]
2669 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2671 def show_update_feeds_buttons(self):
2672 # Make sure that the buttons for updating feeds
2673 # appear - this should happen after a feed update
2674 if gpodder.ui.maemo:
2675 self.btnUpdateSelectedFeed.show()
2676 self.toolFeedUpdateProgress.hide()
2677 self.btnCancelFeedUpdate.hide()
2678 self.btnCancelFeedUpdate.set_is_important(False)
2679 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2680 self.toolbarSpacer.set_expand(True)
2681 self.toolbarSpacer.set_draw(False)
2682 else:
2683 self.hboxUpdateFeeds.hide()
2684 self.btnUpdateFeeds.show()
2685 self.itemUpdate.set_sensitive(True)
2686 self.itemUpdateChannel.set_sensitive(True)
2688 def on_btnCancelFeedUpdate_clicked(self, widget):
2689 if not self.feed_cache_update_cancelled:
2690 self.pbFeedUpdate.set_text(_('Cancelling...'))
2691 self.feed_cache_update_cancelled = True
2692 self.btnCancelFeedUpdate.set_sensitive(False)
2693 else:
2694 self.show_update_feeds_buttons()
2696 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
2697 if self.updating_feed_cache:
2698 if gpodder.ui.fremantle:
2699 self.feed_cache_update_cancelled = True
2700 return
2702 if not force_update:
2703 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2704 self.channel_list_changed = True
2705 self.update_podcast_list_model(select_url=select_url_afterwards)
2706 return
2708 # Fix URLs if mygpo has rewritten them
2709 self.rewrite_urls_mygpo()
2711 self.updating_feed_cache = True
2713 if channels is None:
2714 channels = self.channels
2716 if gpodder.ui.fremantle:
2717 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
2718 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
2719 self.button_refresh.set_title(_('Updating...'))
2720 self.button_subscribe.set_sensitive(False)
2721 self.button_refresh.set_image(gtk.image_new_from_icon_name(\
2722 self.ICON_GENERAL_CLOSE, gtk.ICON_SIZE_BUTTON))
2723 self.feed_cache_update_cancelled = False
2724 else:
2725 self.itemUpdate.set_sensitive(False)
2726 self.itemUpdateChannel.set_sensitive(False)
2728 if self.tray_icon:
2729 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2731 if len(channels) == 1:
2732 text = _('Updating "%s"...') % channels[0].title
2733 else:
2734 count = len(channels)
2735 text = N_('Updating %d feed...', 'Updating %d feeds...', count) % count
2736 self.pbFeedUpdate.set_text(text)
2737 self.pbFeedUpdate.set_fraction(0)
2739 self.feed_cache_update_cancelled = False
2740 self.btnCancelFeedUpdate.show()
2741 self.btnCancelFeedUpdate.set_sensitive(True)
2742 if gpodder.ui.maemo:
2743 self.toolbarSpacer.set_expand(False)
2744 self.toolbarSpacer.set_draw(True)
2745 self.btnUpdateSelectedFeed.hide()
2746 self.toolFeedUpdateProgress.show_all()
2747 else:
2748 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2749 self.hboxUpdateFeeds.show_all()
2750 self.btnUpdateFeeds.hide()
2752 args = (channels, select_url_afterwards)
2753 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
2755 def on_gPodder_delete_event(self, widget, *args):
2756 """Called when the GUI wants to close the window
2757 Displays a confirmation dialog (and closes/hides gPodder)
2760 downloading = self.download_status_model.are_downloads_in_progress()
2762 # Only iconify if we are using the window's "X" button,
2763 # but not when we are using "Quit" in the menu or toolbar
2764 if self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
2765 self.iconify_main_window()
2766 elif self.config.on_quit_ask or downloading:
2767 if gpodder.ui.fremantle:
2768 self.close_gpodder()
2769 elif gpodder.ui.diablo:
2770 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2771 if result:
2772 self.close_gpodder()
2773 else:
2774 return True
2775 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2776 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2777 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2779 title = _('Quit gPodder')
2780 if downloading:
2781 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2782 else:
2783 message = _('Do you really want to quit gPodder now?')
2785 dialog.set_title(title)
2786 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2787 if not downloading:
2788 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2789 dialog.vbox.pack_start(cb_ask)
2790 cb_ask.show_all()
2792 quit_button.grab_focus()
2793 result = dialog.run()
2794 dialog.destroy()
2796 if result == gtk.RESPONSE_CLOSE:
2797 if not downloading and cb_ask.get_active() == True:
2798 self.config.on_quit_ask = False
2799 self.close_gpodder()
2800 else:
2801 self.close_gpodder()
2803 return True
2805 def close_gpodder(self):
2806 """ clean everything and exit properly
2808 if self.channels:
2809 if self.save_channels_opml():
2810 pass # FIXME: Add mygpo synchronization here
2811 else:
2812 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
2814 self.gPodder.hide()
2816 if self.tray_icon is not None:
2817 self.tray_icon.set_visible(False)
2819 # Notify all tasks to to carry out any clean-up actions
2820 self.download_status_model.tell_all_tasks_to_quit()
2822 while gtk.events_pending():
2823 gtk.main_iteration(False)
2825 self.db.close()
2827 self.quit()
2828 sys.exit(0)
2830 def get_expired_episodes(self):
2831 for channel in self.channels:
2832 for episode in channel.get_downloaded_episodes():
2833 # Never consider locked episodes as old
2834 if episode.is_locked:
2835 continue
2837 # Never consider fresh episodes as old
2838 if episode.age_in_days() < self.config.episode_old_age:
2839 continue
2841 # Do not delete played episodes (except if configured)
2842 if episode.is_played:
2843 if not self.config.auto_remove_played_episodes:
2844 continue
2846 # Do not delete unplayed episodes (except if configured)
2847 if not episode.is_played:
2848 if not self.config.auto_remove_unplayed_episodes:
2849 continue
2851 yield episode
2853 def delete_episode_list(self, episodes, confirm=True, skip_locked=True):
2854 if not episodes:
2855 return False
2857 if skip_locked:
2858 episodes = [e for e in episodes if not e.is_locked]
2860 if not episodes:
2861 title = _('Episodes are locked')
2862 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2863 self.notification(message, title, widget=self.treeAvailable)
2864 return False
2866 count = len(episodes)
2867 title = N_('Delete %d episode?', 'Delete %d episodes?', count) % count
2868 message = _('Deleting episodes removes downloaded files.')
2870 if gpodder.ui.fremantle:
2871 message = '\n'.join([title, message])
2873 if confirm and not self.show_confirmation(message, title):
2874 return False
2876 progress = ProgressIndicator(_('Deleting episodes'), \
2877 _('Please wait while episodes are deleted'), \
2878 parent=self.get_dialog_parent())
2880 def finish_deletion(episode_urls, channel_urls):
2881 progress.on_finished()
2883 # Episodes have been deleted - persist the database
2884 self.db.commit()
2886 self.update_episode_list_icons(episode_urls)
2887 self.update_podcast_list_model(channel_urls)
2888 self.play_or_download()
2890 def thread_proc():
2891 episode_urls = set()
2892 channel_urls = set()
2894 episodes_status_update = []
2895 for idx, episode in enumerate(episodes):
2896 progress.on_progress(float(idx)/float(len(episodes)))
2897 if episode.is_locked and skip_locked:
2898 log('Not deleting episode (is locked): %s', episode.title)
2899 else:
2900 log('Deleting episode: %s', episode.title)
2901 progress.on_message(episode.title)
2902 episode.delete_from_disk()
2903 episode_urls.add(episode.url)
2904 channel_urls.add(episode.channel.url)
2905 episodes_status_update.append(episode)
2907 # Tell the shownotes window that we have removed the episode
2908 if self.episode_shownotes_window is not None and \
2909 self.episode_shownotes_window.episode is not None and \
2910 self.episode_shownotes_window.episode.url == episode.url:
2911 util.idle_add(self.episode_shownotes_window._download_status_changed, None)
2913 # Notify the web service about the status update + upload
2914 self.mygpo_client.on_delete(episodes_status_update)
2915 self.mygpo_client.flush()
2917 util.idle_add(finish_deletion, episode_urls, channel_urls)
2919 threading.Thread(target=thread_proc).start()
2921 return True
2923 def on_itemRemoveOldEpisodes_activate( self, widget):
2924 if gpodder.ui.maemo:
2925 columns = (
2926 ('maemo_remove_markup', None, None, _('Episode')),
2928 else:
2929 columns = (
2930 ('title_markup', None, None, _('Episode')),
2931 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2932 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2933 ('played_prop', None, None, _('Status')),
2934 ('age_prop', None, None, _('Downloaded')),
2937 msg_older_than = N_('Select older than %d day', 'Select older than %d days', self.config.episode_old_age)
2938 selection_buttons = {
2939 _('Select played'): lambda episode: episode.is_played,
2940 msg_older_than % self.config.episode_old_age: lambda episode: episode.age_in_days() > self.config.episode_old_age,
2943 instructions = _('Select the episodes you want to delete:')
2945 episodes = []
2946 selected = []
2947 for channel in self.channels:
2948 for episode in channel.get_downloaded_episodes():
2949 # Disallow deletion of locked episodes that still exist
2950 if not episode.is_locked or not episode.file_exists():
2951 episodes.append(episode)
2952 # Automatically select played and file-less episodes
2953 selected.append(episode.is_played or \
2954 not episode.file_exists())
2956 gPodderEpisodeSelector(self.gPodder, title = _('Delete episodes'), instructions = instructions, \
2957 episodes = episodes, selected = selected, columns = columns, \
2958 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2959 selection_buttons = selection_buttons, _config=self.config, \
2960 show_episode_shownotes=self.show_episode_shownotes)
2962 def on_selected_episodes_status_changed(self):
2963 self.update_episode_list_icons(selected=True)
2964 self.update_podcast_list_model(selected=True)
2965 self.db.commit()
2967 def mark_selected_episodes_new(self):
2968 for episode in self.get_selected_episodes():
2969 episode.mark_new()
2970 self.on_selected_episodes_status_changed()
2972 def mark_selected_episodes_old(self):
2973 for episode in self.get_selected_episodes():
2974 episode.mark_old()
2975 self.on_selected_episodes_status_changed()
2977 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2978 for episode in self.get_selected_episodes():
2979 if toggle:
2980 episode.mark(is_played=not episode.is_played)
2981 else:
2982 episode.mark(is_played=new_value)
2983 self.on_selected_episodes_status_changed()
2985 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2986 for episode in self.get_selected_episodes():
2987 if toggle:
2988 episode.mark(is_locked=not episode.is_locked)
2989 else:
2990 episode.mark(is_locked=new_value)
2991 self.on_selected_episodes_status_changed()
2993 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2994 if self.active_channel is None:
2995 return
2997 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2998 self.active_channel.update_channel_lock()
3000 for episode in self.active_channel.get_all_episodes():
3001 episode.mark(is_locked=self.active_channel.channel_is_locked)
3003 self.update_podcast_list_model(selected=True)
3004 self.update_episode_list_icons(all=True)
3006 def on_itemUpdateChannel_activate(self, widget=None):
3007 if self.active_channel is None:
3008 title = _('No podcast selected')
3009 message = _('Please select a podcast in the podcasts list to update.')
3010 self.show_message( message, title, widget=self.treeChannels)
3011 return
3013 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3014 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3015 self.update_feed_cache()
3016 else:
3017 self.update_feed_cache(channels=[self.active_channel])
3019 def on_itemUpdate_activate(self, widget=None):
3020 # Check if we have outstanding subscribe/unsubscribe actions
3021 if self.on_add_remove_podcasts_mygpo():
3022 log('Update cancelled (received server changes)', sender=self)
3023 return
3025 if self.channels:
3026 self.update_feed_cache()
3027 else:
3028 gPodderWelcome(self.gPodder,
3029 center_on_widget=self.gPodder,
3030 show_example_podcasts_callback=self.on_itemImportChannels_activate,
3031 setup_my_gpodder_callback=self.on_download_subscriptions_from_mygpo)
3033 def download_episode_list_paused(self, episodes):
3034 self.download_episode_list(episodes, True)
3036 def download_episode_list(self, episodes, add_paused=False, force_start=False):
3037 enable_update = False
3039 for episode in episodes:
3040 log('Downloading episode: %s', episode.title, sender = self)
3041 if not episode.was_downloaded(and_exists=True):
3042 task_exists = False
3043 for task in self.download_tasks_seen:
3044 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
3045 self.download_queue_manager.add_task(task, force_start)
3046 enable_update = True
3047 task_exists = True
3048 continue
3050 if task_exists:
3051 continue
3053 try:
3054 task = download.DownloadTask(episode, self.config)
3055 except Exception, e:
3056 d = {'episode': episode.title, 'message': str(e)}
3057 message = _('Download error while downloading %(episode)s: %(message)s')
3058 self.show_message(message % d, _('Download error'), important=True)
3059 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
3060 continue
3062 if add_paused:
3063 task.status = task.PAUSED
3064 else:
3065 self.mygpo_client.on_download([task.episode])
3066 self.download_queue_manager.add_task(task, force_start)
3068 self.download_status_model.register_task(task)
3069 enable_update = True
3071 if enable_update:
3072 self.enable_download_list_update()
3074 # Flush updated episode status
3075 self.mygpo_client.flush()
3077 def cancel_task_list(self, tasks):
3078 if not tasks:
3079 return
3081 for task in tasks:
3082 if task.status in (task.QUEUED, task.DOWNLOADING):
3083 task.status = task.CANCELLED
3084 elif task.status == task.PAUSED:
3085 task.status = task.CANCELLED
3086 # Call run, so the partial file gets deleted
3087 task.run()
3089 self.update_episode_list_icons([task.url for task in tasks])
3090 self.play_or_download()
3092 # Update the tab title and downloads list
3093 self.update_downloads_list()
3095 def new_episodes_show(self, episodes, notification=False):
3096 if gpodder.ui.maemo:
3097 columns = (
3098 ('maemo_markup', None, None, _('Episode')),
3100 show_notification = notification
3101 else:
3102 columns = (
3103 ('title_markup', None, None, _('Episode')),
3104 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
3105 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
3107 show_notification = False
3109 instructions = _('Select the episodes you want to download:')
3111 if self.new_episodes_window is not None:
3112 self.new_episodes_window.main_window.destroy()
3113 self.new_episodes_window = None
3115 def download_episodes_callback(episodes):
3116 self.new_episodes_window = None
3117 self.download_episode_list(episodes)
3119 self.new_episodes_window = gPodderEpisodeSelector(self.gPodder, \
3120 title=_('New episodes available'), \
3121 instructions=instructions, \
3122 episodes=episodes, \
3123 columns=columns, \
3124 selected_default=True, \
3125 stock_ok_button = 'gpodder-download', \
3126 callback=download_episodes_callback, \
3127 remove_callback=lambda e: e.mark_old(), \
3128 remove_action=_('Mark as old'), \
3129 remove_finished=self.episode_new_status_changed, \
3130 _config=self.config, \
3131 show_notification=show_notification, \
3132 show_episode_shownotes=self.show_episode_shownotes)
3134 def on_itemDownloadAllNew_activate(self, widget, *args):
3135 if not self.offer_new_episodes():
3136 self.show_message(_('Please check for new episodes later.'), \
3137 _('No new episodes available'), widget=self.btnUpdateFeeds)
3139 def get_new_episodes(self, channels=None):
3140 if channels is None:
3141 channels = self.channels
3142 episodes = []
3143 for channel in channels:
3144 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
3145 episodes.append(episode)
3147 return episodes
3149 def on_sync_to_ipod_activate(self, widget, episodes=None):
3150 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
3152 def commit_changes_to_database(self):
3153 """This will be called after the sync process is finished"""
3154 self.db.commit()
3156 def on_cleanup_ipod_activate(self, widget, *args):
3157 self.sync_ui.on_cleanup_device()
3159 def on_manage_device_playlist(self, widget):
3160 self.sync_ui.on_manage_device_playlist()
3162 def show_hide_tray_icon(self):
3163 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
3164 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
3165 elif not self.config.display_tray_icon and self.tray_icon is not None:
3166 self.tray_icon.set_visible(False)
3167 del self.tray_icon
3168 self.tray_icon = None
3170 if self.config.minimize_to_tray and self.tray_icon:
3171 self.tray_icon.set_visible(self.is_iconified())
3172 elif self.tray_icon:
3173 self.tray_icon.set_visible(True)
3175 def on_itemShowAllEpisodes_activate(self, widget):
3176 self.config.podcast_list_view_all = widget.get_active()
3178 def on_itemShowToolbar_activate(self, widget):
3179 self.config.show_toolbar = self.itemShowToolbar.get_active()
3181 def on_itemShowDescription_activate(self, widget):
3182 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
3184 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
3185 self.config.podcast_list_hide_boring = toggleaction.get_active()
3186 if self.config.podcast_list_hide_boring:
3187 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3188 else:
3189 self.podcast_list_model.set_view_mode(-1)
3191 def on_item_view_podcasts_changed(self, radioaction, current):
3192 # Only on Fremantle
3193 if current == self.item_view_podcasts_all:
3194 self.podcast_list_model.set_view_mode(-1)
3195 elif current == self.item_view_podcasts_downloaded:
3196 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3197 elif current == self.item_view_podcasts_unplayed:
3198 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3200 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
3202 def on_item_view_episodes_changed(self, radioaction, current):
3203 if current == self.item_view_episodes_all:
3204 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_ALL)
3205 elif current == self.item_view_episodes_undeleted:
3206 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNDELETED)
3207 elif current == self.item_view_episodes_downloaded:
3208 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
3209 elif current == self.item_view_episodes_unplayed:
3210 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
3212 self.config.episode_list_view_mode = self.episode_list_model.get_view_mode()
3214 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
3215 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
3217 def update_item_device( self):
3218 if not gpodder.ui.fremantle:
3219 if self.config.device_type != 'none':
3220 self.itemDevice.set_visible(True)
3221 self.itemDevice.label = self.get_device_name()
3222 else:
3223 self.itemDevice.set_visible(False)
3225 def properties_closed( self):
3226 self.preferences_dialog = None
3227 self.show_hide_tray_icon()
3228 self.update_item_device()
3229 if gpodder.ui.maemo:
3230 selection = self.treeAvailable.get_selection()
3231 if self.config.maemo_enable_gestures or \
3232 self.config.enable_fingerscroll:
3233 selection.set_mode(gtk.SELECTION_SINGLE)
3234 else:
3235 selection.set_mode(gtk.SELECTION_MULTIPLE)
3237 def on_itemPreferences_activate(self, widget, *args):
3238 self.preferences_dialog = gPodderPreferences(self.main_window, \
3239 _config=self.config, \
3240 callback_finished=self.properties_closed, \
3241 user_apps_reader=self.user_apps_reader, \
3242 parent_window=self.main_window, \
3243 mygpo_client=self.mygpo_client, \
3244 on_send_full_subscriptions=self.on_send_full_subscriptions)
3246 # Initial message to relayout window (in case it's opened in portrait mode
3247 self.preferences_dialog.on_window_orientation_changed(self._last_orientation)
3249 def on_itemDependencies_activate(self, widget):
3250 gPodderDependencyManager(self.gPodder)
3252 def on_goto_mygpo(self, widget):
3253 self.mygpo_client.open_website()
3255 def on_download_subscriptions_from_mygpo(self, action=None):
3256 title = _('Login to gpodder.net')
3257 message = _('Please login to download your subscriptions.')
3258 success, (username, password) = self.show_login_dialog(title, message, \
3259 self.config.mygpo_username, self.config.mygpo_password)
3260 if not success:
3261 return
3263 self.config.mygpo_username = username
3264 self.config.mygpo_password = password
3266 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3267 custom_title=_('Subscriptions on gpodder.net'), \
3268 add_urls_callback=self.add_podcast_list, \
3269 hide_url_entry=True)
3271 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3272 # we do not have to hardcode the URL here
3273 OPML_URL = 'http://gpodder.net/subscriptions/%s.opml' % self.config.mygpo_username
3274 url = util.url_add_authentication(OPML_URL, \
3275 self.config.mygpo_username, \
3276 self.config.mygpo_password)
3277 dir.download_opml_file(url)
3279 def on_mygpo_settings_activate(self, action=None):
3280 # This dialog is only used for Maemo 4
3281 if not gpodder.ui.diablo:
3282 return
3284 settings = MygPodderSettings(self.main_window, \
3285 config=self.config, \
3286 mygpo_client=self.mygpo_client, \
3287 on_send_full_subscriptions=self.on_send_full_subscriptions)
3289 def on_itemAddChannel_activate(self, widget=None):
3290 gPodderAddPodcast(self.gPodder, \
3291 add_urls_callback=self.add_podcast_list)
3293 def on_itemEditChannel_activate(self, widget, *args):
3294 if self.active_channel is None:
3295 title = _('No podcast selected')
3296 message = _('Please select a podcast in the podcasts list to edit.')
3297 self.show_message( message, title, widget=self.treeChannels)
3298 return
3300 callback_closed = lambda: self.update_podcast_list_model(selected=True)
3301 gPodderChannel(self.main_window, \
3302 channel=self.active_channel, \
3303 callback_closed=callback_closed, \
3304 cover_downloader=self.cover_downloader)
3306 def on_itemMassUnsubscribe_activate(self, item=None):
3307 columns = (
3308 ('title', None, None, _('Podcast')),
3311 # We're abusing the Episode Selector for selecting Podcasts here,
3312 # but it works and looks good, so why not? -- thp
3313 gPodderEpisodeSelector(self.main_window, \
3314 title=_('Remove podcasts'), \
3315 instructions=_('Select the podcast you want to remove.'), \
3316 episodes=self.channels, \
3317 columns=columns, \
3318 size_attribute=None, \
3319 stock_ok_button=gtk.STOCK_DELETE, \
3320 callback=self.remove_podcast_list, \
3321 _config=self.config)
3323 def remove_podcast_list(self, channels, confirm=True):
3324 if not channels:
3325 log('No podcasts selected for deletion', sender=self)
3326 return
3328 if len(channels) == 1:
3329 title = _('Removing podcast')
3330 info = _('Please wait while the podcast is removed')
3331 message = _('Do you really want to remove this podcast and its episodes?')
3332 else:
3333 title = _('Removing podcasts')
3334 info = _('Please wait while the podcasts are removed')
3335 message = _('Do you really want to remove the selected podcasts and their episodes?')
3337 if confirm and not self.show_confirmation(message, title):
3338 return
3340 progress = ProgressIndicator(title, info, parent=self.get_dialog_parent())
3342 def finish_deletion(select_url):
3343 # Upload subscription list changes to the web service
3344 self.mygpo_client.on_unsubscribe([c.url for c in channels])
3346 # Re-load the channels and select the desired new channel
3347 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
3348 progress.on_finished()
3349 self.update_podcasts_tab()
3351 def thread_proc():
3352 select_url = None
3354 for idx, channel in enumerate(channels):
3355 # Update the UI for correct status messages
3356 progress.on_progress(float(idx)/float(len(channels)))
3357 progress.on_message(channel.title)
3359 # Delete downloaded episodes
3360 channel.remove_downloaded()
3362 # cancel any active downloads from this channel
3363 for episode in channel.get_all_episodes():
3364 util.idle_add(self.download_status_model.cancel_by_url,
3365 episode.url)
3367 if len(channels) == 1:
3368 # get the URL of the podcast we want to select next
3369 if channel in self.channels:
3370 position = self.channels.index(channel)
3371 else:
3372 position = -1
3374 if position == len(self.channels)-1:
3375 # this is the last podcast, so select the URL
3376 # of the item before this one (i.e. the "new last")
3377 select_url = self.channels[position-1].url
3378 else:
3379 # there is a podcast after the deleted one, so
3380 # we simply select the one that comes after it
3381 select_url = self.channels[position+1].url
3383 # Remove the channel and clean the database entries
3384 channel.delete()
3385 self.channels.remove(channel)
3387 # Clean up downloads and download directories
3388 self.clean_up_downloads()
3390 self.channel_list_changed = True
3391 self.save_channels_opml()
3393 # The remaining stuff is to be done in the GTK main thread
3394 util.idle_add(finish_deletion, select_url)
3396 threading.Thread(target=thread_proc).start()
3398 def on_itemRemoveChannel_activate(self, widget, *args):
3399 if self.active_channel is None:
3400 title = _('No podcast selected')
3401 message = _('Please select a podcast in the podcasts list to remove.')
3402 self.show_message( message, title, widget=self.treeChannels)
3403 return
3405 self.remove_podcast_list([self.active_channel])
3407 def get_opml_filter(self):
3408 filter = gtk.FileFilter()
3409 filter.add_pattern('*.opml')
3410 filter.add_pattern('*.xml')
3411 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3412 return filter
3414 def on_item_import_from_file_activate(self, widget, filename=None):
3415 if filename is None:
3416 if gpodder.ui.desktop or gpodder.ui.fremantle:
3417 # FIXME: Hildonization on Fremantle
3418 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
3419 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3420 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3421 elif gpodder.ui.diablo:
3422 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
3423 dlg.set_filter(self.get_opml_filter())
3424 response = dlg.run()
3425 filename = None
3426 if response == gtk.RESPONSE_OK:
3427 filename = dlg.get_filename()
3428 dlg.destroy()
3430 if filename is not None:
3431 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
3432 custom_title=_('Import podcasts from OPML file'), \
3433 add_urls_callback=self.add_podcast_list, \
3434 hide_url_entry=True)
3435 dir.download_opml_file(filename)
3437 def on_itemExportChannels_activate(self, widget, *args):
3438 if not self.channels:
3439 title = _('Nothing to export')
3440 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3441 self.show_message(message, title, widget=self.treeChannels)
3442 return
3444 if gpodder.ui.desktop or gpodder.ui.fremantle:
3445 # FIXME: Hildonization on Fremantle
3446 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
3447 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3448 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
3449 elif gpodder.ui.diablo:
3450 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
3451 dlg.set_filter(self.get_opml_filter())
3452 response = dlg.run()
3453 if response == gtk.RESPONSE_OK:
3454 filename = dlg.get_filename()
3455 dlg.destroy()
3456 exporter = opml.Exporter( filename)
3457 if exporter.write(self.channels):
3458 count = len(self.channels)
3459 title = N_('%d subscription exported', '%d subscriptions exported', count) % count
3460 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
3461 else:
3462 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
3463 else:
3464 dlg.destroy()
3466 def on_itemImportChannels_activate(self, widget, *args):
3467 if gpodder.ui.fremantle:
3468 gPodderPodcastDirectory.show_add_podcast_picker(self.main_window, \
3469 self.config.toplist_url, \
3470 self.config.opml_url, \
3471 self.add_podcast_list, \
3472 self.on_itemAddChannel_activate, \
3473 self.on_download_subscriptions_from_mygpo, \
3474 self.show_text_edit_dialog)
3475 else:
3476 dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
3477 add_urls_callback=self.add_podcast_list)
3478 util.idle_add(dir.download_opml_file, self.config.opml_url)
3480 def on_homepage_activate(self, widget, *args):
3481 util.open_website(gpodder.__url__)
3483 def on_wiki_activate(self, widget, *args):
3484 util.open_website('http://gpodder.org/wiki/User_Manual')
3486 def on_bug_tracker_activate(self, widget, *args):
3487 if gpodder.ui.maemo:
3488 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3489 else:
3490 util.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3492 def on_item_support_activate(self, widget):
3493 util.open_website('http://gpodder.org/donate')
3495 def on_itemAbout_activate(self, widget, *args):
3496 if gpodder.ui.fremantle:
3497 from gpodder.gtkui.frmntl.about import HeAboutDialog
3498 HeAboutDialog.present(self.main_window,
3499 'gPodder',
3500 'gpodder',
3501 gpodder.__version__,
3502 _('A podcast client with focus on usability'),
3503 gpodder.__copyright__,
3504 gpodder.__url__,
3505 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3506 'http://gpodder.org/donate')
3507 return
3509 dlg = gtk.AboutDialog()
3510 dlg.set_transient_for(self.main_window)
3511 dlg.set_name('gPodder')
3512 dlg.set_version(gpodder.__version__)
3513 dlg.set_copyright(gpodder.__copyright__)
3514 dlg.set_comments(_('A podcast client with focus on usability'))
3515 dlg.set_website(gpodder.__url__)
3516 dlg.set_translator_credits( _('translator-credits'))
3517 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
3519 if gpodder.ui.desktop:
3520 # For the "GUI" version, we add some more
3521 # items to the about dialog (credits and logo)
3522 app_authors = [
3523 _('Maintainer:'),
3524 'Thomas Perl <thpinfo.com>',
3527 if os.path.exists(gpodder.credits_file):
3528 credits = open(gpodder.credits_file).read().strip().split('\n')
3529 app_authors += ['', _('Patches, bug reports and donations by:')]
3530 app_authors += credits
3532 dlg.set_authors(app_authors)
3533 try:
3534 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
3535 except:
3536 dlg.set_logo_icon_name('gpodder')
3538 dlg.run()
3540 def on_wNotebook_switch_page(self, widget, *args):
3541 page_num = args[1]
3542 if gpodder.ui.maemo:
3543 self.tool_downloads.set_active(page_num == 1)
3544 page = self.wNotebook.get_nth_page(page_num)
3545 tab_label = self.wNotebook.get_tab_label(page).get_text()
3546 if page_num == 0 and self.active_channel is not None:
3547 self.set_title(self.active_channel.title)
3548 else:
3549 self.set_title(tab_label)
3550 if page_num == 0:
3551 self.play_or_download()
3552 self.menuChannels.set_sensitive(True)
3553 self.menuSubscriptions.set_sensitive(True)
3554 # The message area in the downloads tab should be hidden
3555 # when the user switches away from the downloads tab
3556 if self.message_area is not None:
3557 self.message_area.hide()
3558 self.message_area = None
3559 else:
3560 self.menuChannels.set_sensitive(False)
3561 self.menuSubscriptions.set_sensitive(False)
3562 if gpodder.ui.desktop:
3563 self.toolDownload.set_sensitive(False)
3564 self.toolPlay.set_sensitive(False)
3565 self.toolTransfer.set_sensitive(False)
3566 self.toolCancel.set_sensitive(False)
3568 def on_treeChannels_row_activated(self, widget, path, *args):
3569 # double-click action of the podcast list or enter
3570 self.treeChannels.set_cursor(path)
3572 def on_treeChannels_cursor_changed(self, widget, *args):
3573 ( model, iter ) = self.treeChannels.get_selection().get_selected()
3575 if model is not None and iter is not None:
3576 old_active_channel = self.active_channel
3577 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
3579 if self.active_channel == old_active_channel:
3580 return
3582 if gpodder.ui.maemo:
3583 self.set_title(self.active_channel.title)
3585 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3586 if getattr(self.active_channel, 'ALL_EPISODES_PROXY', False):
3587 self.itemEditChannel.set_visible(False)
3588 self.itemRemoveChannel.set_visible(False)
3589 else:
3590 self.itemEditChannel.set_visible(True)
3591 self.itemRemoveChannel.set_visible(True)
3592 else:
3593 self.active_channel = None
3594 self.itemEditChannel.set_visible(False)
3595 self.itemRemoveChannel.set_visible(False)
3597 self.update_episode_list_model()
3599 def on_btnEditChannel_clicked(self, widget, *args):
3600 self.on_itemEditChannel_activate( widget, args)
3602 def get_podcast_urls_from_selected_episodes(self):
3603 """Get a set of podcast URLs based on the selected episodes"""
3604 return set(episode.channel.url for episode in \
3605 self.get_selected_episodes())
3607 def get_selected_episodes(self):
3608 """Get a list of selected episodes from treeAvailable"""
3609 selection = self.treeAvailable.get_selection()
3610 model, paths = selection.get_selected_rows()
3612 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
3613 return episodes
3615 def on_transfer_selected_episodes(self, widget):
3616 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
3618 def on_playback_selected_episodes(self, widget):
3619 self.playback_episodes(self.get_selected_episodes())
3621 def on_shownotes_selected_episodes(self, widget):
3622 episodes = self.get_selected_episodes()
3623 if episodes:
3624 episode = episodes.pop(0)
3625 self.show_episode_shownotes(episode)
3626 else:
3627 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
3629 def on_download_selected_episodes(self, widget):
3630 episodes = self.get_selected_episodes()
3631 self.download_episode_list(episodes)
3632 self.update_episode_list_icons([episode.url for episode in episodes])
3633 self.play_or_download()
3635 def on_treeAvailable_row_activated(self, widget, path, view_column):
3636 """Double-click/enter action handler for treeAvailable"""
3637 # We should only have one one selected as it was double clicked!
3638 e = self.get_selected_episodes()[0]
3640 if (self.config.double_click_episode_action == 'download'):
3641 # If the episode has already been downloaded and exists then play it
3642 if e.was_downloaded(and_exists=True):
3643 self.playback_episodes(self.get_selected_episodes())
3644 # else download it if it is not already downloading
3645 elif not self.episode_is_downloading(e):
3646 self.download_episode_list([e])
3647 self.update_episode_list_icons([e.url])
3648 self.play_or_download()
3649 elif (self.config.double_click_episode_action == 'stream'):
3650 # If we happen to have downloaded this episode simple play it
3651 if e.was_downloaded(and_exists=True):
3652 self.playback_episodes(self.get_selected_episodes())
3653 # else if streaming is possible stream it
3654 elif self.streaming_possible():
3655 self.playback_episodes(self.get_selected_episodes())
3656 else:
3657 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
3658 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
3659 else:
3660 # default action is to display show notes
3661 self.on_shownotes_selected_episodes(widget)
3663 def show_episode_shownotes(self, episode):
3664 if self.episode_shownotes_window is None:
3665 log('First-time use of episode window --- creating', sender=self)
3666 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
3667 _download_episode_list=self.download_episode_list, \
3668 _playback_episodes=self.playback_episodes, \
3669 _delete_episode_list=self.delete_episode_list, \
3670 _episode_list_status_changed=self.episode_list_status_changed, \
3671 _cancel_task_list=self.cancel_task_list, \
3672 _episode_is_downloading=self.episode_is_downloading, \
3673 _streaming_possible=self.streaming_possible())
3674 self.episode_shownotes_window.show(episode)
3675 if self.episode_is_downloading(episode):
3676 self.update_downloads_list()
3678 def restart_auto_update_timer(self):
3679 if self._auto_update_timer_source_id is not None:
3680 log('Removing existing auto update timer.', sender=self)
3681 gobject.source_remove(self._auto_update_timer_source_id)
3682 self._auto_update_timer_source_id = None
3684 if self.config.auto_update_feeds and \
3685 self.config.auto_update_frequency:
3686 interval = 60*1000*self.config.auto_update_frequency
3687 log('Setting up auto update timer with interval %d.', \
3688 self.config.auto_update_frequency, sender=self)
3689 self._auto_update_timer_source_id = gobject.timeout_add(\
3690 interval, self._on_auto_update_timer)
3692 def _on_auto_update_timer(self):
3693 log('Auto update timer fired.', sender=self)
3694 self.update_feed_cache(force_update=True)
3696 # Ask web service for sub changes (if enabled)
3697 self.mygpo_client.flush()
3699 return True
3701 def on_treeDownloads_row_activated(self, widget, *args):
3702 # Use the standard way of working on the treeview
3703 selection = self.treeDownloads.get_selection()
3704 (model, paths) = selection.get_selected_rows()
3705 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
3707 for tree_row_reference, task in selected_tasks:
3708 if task.status in (task.DOWNLOADING, task.QUEUED):
3709 task.status = task.PAUSED
3710 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
3711 self.download_queue_manager.add_task(task)
3712 self.enable_download_list_update()
3713 elif task.status == task.DONE:
3714 model.remove(model.get_iter(tree_row_reference.get_path()))
3716 self.play_or_download()
3718 # Update the tab title and downloads list
3719 self.update_downloads_list()
3721 def on_item_cancel_download_activate(self, widget):
3722 if self.wNotebook.get_current_page() == 0:
3723 selection = self.treeAvailable.get_selection()
3724 (model, paths) = selection.get_selected_rows()
3725 urls = [model.get_value(model.get_iter(path), \
3726 self.episode_list_model.C_URL) for path in paths]
3727 selected_tasks = [task for task in self.download_tasks_seen \
3728 if task.url in urls]
3729 else:
3730 selection = self.treeDownloads.get_selection()
3731 (model, paths) = selection.get_selected_rows()
3732 selected_tasks = [model.get_value(model.get_iter(path), \
3733 self.download_status_model.C_TASK) for path in paths]
3734 self.cancel_task_list(selected_tasks)
3736 def on_btnCancelAll_clicked(self, widget, *args):
3737 self.cancel_task_list(self.download_tasks_seen)
3739 def on_btnDownloadedDelete_clicked(self, widget, *args):
3740 episodes = self.get_selected_episodes()
3741 if len(episodes) == 1:
3742 self.delete_episode_list(episodes, skip_locked=False)
3743 else:
3744 self.delete_episode_list(episodes)
3746 def on_key_press(self, widget, event):
3747 # Allow tab switching with Ctrl + PgUp/PgDown
3748 if event.state & gtk.gdk.CONTROL_MASK:
3749 if event.keyval == gtk.keysyms.Page_Up:
3750 self.wNotebook.prev_page()
3751 return True
3752 elif event.keyval == gtk.keysyms.Page_Down:
3753 self.wNotebook.next_page()
3754 return True
3756 # After this code we only handle Maemo hardware keys,
3757 # so if we are not a Maemo app, we don't do anything
3758 if not gpodder.ui.maemo:
3759 return False
3761 diff = 0
3762 if event.keyval == gtk.keysyms.F7: #plus
3763 diff = 1
3764 elif event.keyval == gtk.keysyms.F8: #minus
3765 diff = -1
3767 if diff != 0 and not self.currently_updating:
3768 selection = self.treeChannels.get_selection()
3769 (model, iter) = selection.get_selected()
3770 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
3771 selection.select_path(new_path)
3772 self.treeChannels.set_cursor(new_path)
3773 return True
3775 return False
3777 def on_iconify(self):
3778 if self.tray_icon:
3779 self.gPodder.set_skip_taskbar_hint(True)
3780 if self.config.minimize_to_tray:
3781 self.tray_icon.set_visible(True)
3782 else:
3783 self.gPodder.set_skip_taskbar_hint(False)
3785 def on_uniconify(self):
3786 if self.tray_icon:
3787 self.gPodder.set_skip_taskbar_hint(False)
3788 if self.config.minimize_to_tray:
3789 self.tray_icon.set_visible(False)
3790 else:
3791 self.gPodder.set_skip_taskbar_hint(False)
3793 def uniconify_main_window(self):
3794 if self.is_iconified():
3795 self.gPodder.present()
3797 def iconify_main_window(self):
3798 if not self.is_iconified():
3799 self.gPodder.iconify()
3801 def update_podcasts_tab(self):
3802 if len(self.channels):
3803 if gpodder.ui.fremantle:
3804 self.button_refresh.set_title(_('Check for new episodes'))
3805 self.button_refresh.show()
3806 else:
3807 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3808 else:
3809 if gpodder.ui.fremantle:
3810 self.button_refresh.hide()
3811 else:
3812 self.label2.set_text(_('Podcasts'))
3814 @dbus.service.method(gpodder.dbus_interface)
3815 def show_gui_window(self):
3816 self.gPodder.present()
3818 @dbus.service.method(gpodder.dbus_interface)
3819 def subscribe_to_url(self, url):
3820 gPodderAddPodcast(self.gPodder,
3821 add_urls_callback=self.add_podcast_list,
3822 preset_url=url)
3824 @dbus.service.method(gpodder.dbus_interface)
3825 def mark_episode_played(self, filename):
3826 if filename is None:
3827 return False
3829 for channel in self.channels:
3830 for episode in channel.get_all_episodes():
3831 fn = episode.local_filename(create=False, check_only=True)
3832 if fn == filename:
3833 episode.mark(is_played=True)
3834 self.db.commit()
3835 self.update_episode_list_icons([episode.url])
3836 self.update_podcast_list_model([episode.channel.url])
3837 return True
3839 return False
3842 def main(options=None):
3843 gobject.threads_init()
3844 gobject.set_application_name('gPodder')
3846 if gpodder.ui.maemo:
3847 # Try to enable the custom icon theme for gPodder on Maemo
3848 settings = gtk.settings_get_default()
3849 settings.set_string_property('gtk-icon-theme-name', \
3850 'gpodder', __file__)
3851 # Extend the search path for the optified icon theme (Maemo 5)
3852 icon_theme = gtk.icon_theme_get_default()
3853 icon_theme.prepend_search_path('/opt/gpodder-icon-theme/')
3855 gtk.window_set_default_icon_name('gpodder')
3856 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
3858 try:
3859 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
3860 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
3861 except dbus.exceptions.DBusException, dbe:
3862 log('Warning: Cannot get "on the bus".', traceback=True)
3863 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
3864 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
3865 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
3866 dlg.set_title('gPodder')
3867 dlg.run()
3868 dlg.destroy()
3869 sys.exit(0)
3871 util.make_directory(gpodder.home)
3872 gpodder.load_plugins()
3874 config = UIConfig(gpodder.config_file)
3876 if gpodder.ui.diablo:
3877 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3878 # folder exists there (allow moving "gpodder" between SD cards or USB)
3879 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3880 if not os.path.exists(config.download_dir):
3881 log('Downloads might have been moved. Trying to locate them...')
3882 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
3883 dir = os.path.join(basedir, 'gpodder')
3884 if os.path.exists(dir):
3885 log('Downloads found in: %s', dir)
3886 config.download_dir = dir
3887 break
3888 else:
3889 log('Downloads NOT FOUND in %s', dir)
3891 if config.enable_fingerscroll:
3892 BuilderWidget.use_fingerscroll = True
3893 elif gpodder.ui.fremantle:
3894 config.on_quit_ask = False
3896 config.mygpo_device_type = util.detect_device_type()
3898 gp = gPodder(bus_name, config)
3900 # Handle options
3901 if options.subscribe:
3902 util.idle_add(gp.subscribe_to_url, options.subscribe)
3904 # mac OS X stuff :
3905 # handle "subscribe to podcast" events from firefox
3906 if platform.system() == 'Darwin':
3907 from gpodder import gpodderosx
3908 gpodderosx.register_handlers(gp)
3909 # end mac OS X stuff
3911 gp.run()