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