Support always and never rotation on Fremantle
[gpodder.git] / src / gpodder / gui.py
blobbf0c67bf36e417b690f599e1ff5eeb615188d485
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2009 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 gtk
22 import gtk.gdk
23 import gobject
24 import pango
25 import sys
26 import shutil
27 import subprocess
28 import glob
29 import time
30 import urllib
31 import urllib2
32 import tempfile
33 import collections
34 import threading
36 from xml.sax import saxutils
38 import gpodder
40 try:
41 import dbus
42 import dbus.service
43 import dbus.mainloop
44 import dbus.glib
45 except ImportError:
46 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
47 class dbus:
48 class SessionBus:
49 def __init__(self, *args, **kwargs):
50 pass
51 class glib:
52 class DBusGMainLoop:
53 pass
54 class service:
55 @staticmethod
56 def method(interface):
57 return lambda x: x
58 class BusName:
59 def __init__(self, *args, **kwargs):
60 pass
61 class Object:
62 def __init__(self, *args, **kwargs):
63 pass
66 from gpodder import feedcore
67 from gpodder import util
68 from gpodder import opml
69 from gpodder import download
70 from gpodder import my
71 from gpodder.liblogger import log
73 _ = gpodder.gettext
75 from gpodder.model import PodcastChannel
76 from gpodder.dbsqlite import Database
78 from gpodder.gtkui.model import PodcastListModel
79 from gpodder.gtkui.model import EpisodeListModel
80 from gpodder.gtkui.config import UIConfig
81 from gpodder.gtkui.download import DownloadStatusModel
82 from gpodder.gtkui.services import CoverDownloader
83 from gpodder.gtkui.widgets import SimpleMessageArea
84 from gpodder.gtkui.desktopfile import UserAppsReader
86 from gpodder.gtkui.draw import draw_text_box_centered
88 from gpodder.gtkui.interface.common import BuilderWidget
89 from gpodder.gtkui.interface.common import TreeViewHelper
90 from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
92 if gpodder.ui.desktop:
93 from gpodder.gtkui.desktop.sync import gPodderSyncUI
95 from gpodder.gtkui.desktop.channel import gPodderChannel
96 from gpodder.gtkui.desktop.preferences import gPodderPreferences
97 from gpodder.gtkui.desktop.shownotes import gPodderShownotes
98 from gpodder.gtkui.desktop.episodeselector import gPodderEpisodeSelector
99 from gpodder.gtkui.desktop.podcastdirectory import gPodderPodcastDirectory
100 from gpodder.gtkui.desktop.dependencymanager import gPodderDependencyManager
101 try:
102 from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
103 have_trayicon = True
104 except Exception, exc:
105 log('Warning: Could not import gpodder.trayicon.', traceback=True)
106 log('Warning: This probably means your PyGTK installation is too old!')
107 have_trayicon = False
108 elif gpodder.ui.diablo:
109 from gpodder.gtkui.maemo.channel import gPodderChannel
110 from gpodder.gtkui.maemo.preferences import gPodderPreferences
111 from gpodder.gtkui.maemo.shownotes import gPodderShownotes
112 from gpodder.gtkui.maemo.episodeselector import gPodderEpisodeSelector
113 from gpodder.gtkui.maemo.podcastdirectory import gPodderPodcastDirectory
114 have_trayicon = False
115 elif gpodder.ui.fremantle:
116 from gpodder.gtkui.maemo.channel import gPodderChannel
117 from gpodder.gtkui.maemo.preferences import gPodderPreferences
118 from gpodder.gtkui.frmntl.shownotes import gPodderShownotes
119 from gpodder.gtkui.frmntl.episodeselector import gPodderEpisodeSelector
120 from gpodder.gtkui.frmntl.podcastdirectory import gPodderPodcastDirectory
121 from gpodder.gtkui.frmntl.podcasts import gPodderPodcasts
122 from gpodder.gtkui.frmntl.episodes import gPodderEpisodes
123 from gpodder.gtkui.frmntl.downloads import gPodderDownloads
124 from gpodder.gtkui.interface.common import Orientation
125 have_trayicon = False
127 from gpodder.gtkui.frmntl.portrait import FremantleRotation
129 from gpodder.gtkui.interface.welcome import gPodderWelcome
130 from gpodder.gtkui.interface.progress import ProgressIndicator
132 if gpodder.ui.maemo:
133 import hildon
135 class gPodder(BuilderWidget, dbus.service.Object):
136 finger_friendly_widgets = ['btnCleanUpDownloads']
138 def __init__(self, bus_name, config):
139 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
140 self.db = Database(gpodder.database_file)
141 self.config = config
142 BuilderWidget.__init__(self, None)
144 def new(self):
145 if gpodder.ui.diablo:
146 import hildon
147 self.app = hildon.Program()
148 self.app.add_window(self.main_window)
149 self.main_window.add_toolbar(self.toolbar)
150 menu = gtk.Menu()
151 for child in self.main_menu.get_children():
152 child.reparent(menu)
153 self.main_window.set_menu(self.set_finger_friendly(menu))
154 self.bluetooth_available = False
155 elif gpodder.ui.fremantle:
156 import hildon
157 self.app = hildon.Program()
158 self.app.add_window(self.main_window)
160 appmenu = hildon.AppMenu()
161 for action in (self.itemUpdate, \
162 self.itemAddChannel, \
163 self.itemRemoveOldEpisodes, \
164 self.itemAbout):
165 button = gtk.Button()
166 action.connect_proxy(button)
167 appmenu.append(button)
168 appmenu.show_all()
169 self.main_window.set_app_menu(appmenu)
170 self._fremantle_update_banner = None
172 # Initialize portrait mode / rotation manager
173 self._fremantle_rotation = FremantleRotation(self.main_window)
175 self.bluetooth_available = False
176 else:
177 if gpodder.win32:
178 # FIXME: Implement e-mail sending of list in win32
179 self.item_email_subscriptions.set_sensitive(False)
180 self.bluetooth_available = util.bluetooth_available()
181 self.toolbar.set_property('visible', self.config.show_toolbar)
183 self.config.connect_gtk_window(self.gPodder, 'main_window')
184 if not gpodder.ui.fremantle:
185 self.config.connect_gtk_paned('paned_position', self.channelPaned)
186 self.main_window.show()
188 self.gPodder.connect('key-press-event', self.on_key_press)
190 self.config.add_observer(self.on_config_changed)
192 self.tray_icon = None
193 self.episode_shownotes_window = None
195 if gpodder.ui.desktop:
196 self.sync_ui = gPodderSyncUI(self.config, self.notification, \
197 self.main_window, self.show_confirmation, \
198 self.update_episode_list_icons, \
199 self.update_podcast_list_model, self.toolPreferences, \
200 gPodderEpisodeSelector)
201 else:
202 self.sync_ui = None
204 self.download_status_model = DownloadStatusModel()
205 self.download_queue_manager = download.DownloadQueueManager(self.config)
207 if gpodder.ui.desktop:
208 self.show_hide_tray_icon()
209 self.itemShowToolbar.set_active(self.config.show_toolbar)
210 self.itemShowDescription.set_active(self.config.episode_list_descriptions)
212 if not gpodder.ui.fremantle:
213 self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
214 self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
215 self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
216 self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
218 # When the amount of maximum downloads changes, notify the queue manager
219 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_and_retire_threads()
220 self.spinMaxDownloads.connect('value-changed', changed_cb)
222 self.default_title = 'gPodder'
223 if gpodder.__version__.rfind('git') != -1:
224 self.set_title('gPodder %s' % gpodder.__version__)
225 else:
226 title = self.gPodder.get_title()
227 if title is not None:
228 self.set_title(title)
229 else:
230 self.set_title(_('gPodder'))
232 self.cover_downloader = CoverDownloader()
234 # Generate list models for podcasts and their episodes
235 self.podcast_list_model = PodcastListModel(self.config.podcast_list_icon_size, self.cover_downloader)
237 self.cover_downloader.register('cover-available', self.cover_download_finished)
238 self.cover_downloader.register('cover-removed', self.cover_file_removed)
240 if gpodder.ui.fremantle:
241 self.button_subscribe.set_name('HildonButton-thumb')
242 self.button_podcasts.set_name('HildonButton-thumb')
243 self.button_downloads.set_name('HildonButton-thumb')
245 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
246 while gtk.events_pending():
247 gtk.main_iteration(False)
249 self.episodes_window = gPodderEpisodes(self.main_window, \
250 on_treeview_expose_event=self.on_treeview_expose_event, \
251 show_episode_shownotes=self.show_episode_shownotes, \
252 update_podcast_list_model=self.update_podcast_list_model, \
253 on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
254 item_view_episodes_all=self.item_view_episodes_all, \
255 item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
256 item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
257 item_view_episodes_undeleted=self.item_view_episodes_undeleted)
259 def on_podcast_selected(channel):
260 self.active_channel = channel
261 self.update_episode_list_model()
262 self.episodes_window.channel = self.active_channel
263 self.episodes_window.show()
265 self.podcasts_window = gPodderPodcasts(self.main_window, \
266 show_podcast_episodes=on_podcast_selected, \
267 on_treeview_expose_event=self.on_treeview_expose_event, \
268 on_itemAddChannel_activate=self.on_itemAddChannel_activate, \
269 on_itemUpdate_activate=self.on_itemUpdate_activate, \
270 item_view_podcasts_all=self.item_view_podcasts_all, \
271 item_view_podcasts_downloaded=self.item_view_podcasts_downloaded, \
272 item_view_podcasts_unplayed=self.item_view_podcasts_unplayed)
274 self.downloads_window = gPodderDownloads(self.main_window, \
275 on_treeview_expose_event=self.on_treeview_expose_event, \
276 on_btnCleanUpDownloads_clicked=self.on_btnCleanUpDownloads_clicked, \
277 _for_each_task_set_status=self._for_each_task_set_status, \
278 downloads_list_get_selection=self.downloads_list_get_selection)
279 self.treeChannels = self.podcasts_window.treeview
280 self.treeAvailable = self.episodes_window.treeview
281 self.treeDownloads = self.downloads_window.treeview
283 # Init the treeviews that we use
284 self.init_podcast_list_treeview()
285 self.init_episode_list_treeview()
286 self.init_download_list_treeview()
288 if self.config.podcast_list_hide_boring:
289 self.item_view_hide_boring_podcasts.set_active(True)
291 self.currently_updating = False
293 if gpodder.ui.maemo:
294 self.context_menu_mouse_button = 1
295 else:
296 self.context_menu_mouse_button = 3
298 if self.config.start_iconified:
299 self.iconify_main_window()
301 self.download_tasks_seen = set()
302 self.download_list_update_enabled = False
303 self.last_download_count = 0
305 # Subscribed channels
306 self.active_channel = None
307 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
308 self.channel_list_changed = True
309 self.update_podcasts_tab()
311 # load list of user applications for audio playback
312 self.user_apps_reader = UserAppsReader(['audio', 'video'])
313 def read_apps():
314 time.sleep(3) # give other parts of gpodder a chance to start up
315 self.user_apps_reader.read()
316 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
317 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
318 threading.Thread(target=read_apps).start()
320 # Set the "Device" menu item for the first time
321 if gpodder.ui.desktop:
322 self.update_item_device()
324 # Now, update the feed cache, when everything's in place
325 if not gpodder.ui.fremantle:
326 self.btnUpdateFeeds.show()
327 self.updating_feed_cache = False
328 self.feed_cache_update_cancelled = False
329 self.update_feed_cache(force_update=self.config.update_on_startup)
331 # Look for partial file downloads
332 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
334 # Message area
335 self.message_area = None
337 resumable_episodes = []
338 if len(partial_files) > 0:
339 for f in partial_files:
340 correct_name = f[:-len('.partial')] # strip ".partial"
341 log('Searching episode for file: %s', correct_name, sender=self)
342 found_episode = False
343 for c in self.channels:
344 for e in c.get_all_episodes():
345 if e.local_filename(create=False, check_only=True) == correct_name:
346 log('Found episode: %s', e.title, sender=self)
347 resumable_episodes.append(e)
348 found_episode = True
349 if found_episode:
350 break
351 if found_episode:
352 break
353 if not found_episode:
354 log('Partial file without episode: %s', f, sender=self)
355 util.delete_file(f)
357 if len(resumable_episodes):
358 self.download_episode_list_paused(resumable_episodes)
359 if not gpodder.ui.fremantle:
360 self.message_area = SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
361 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
362 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
363 self.message_area.show_all()
364 self.wNotebook.set_current_page(1)
366 self.clean_up_downloads(delete_partial=False)
367 else:
368 self.clean_up_downloads(delete_partial=True)
370 # Start the auto-update procedure
371 self.auto_update_procedure(first_run=True)
373 # Delete old episodes if the user wishes to
374 if self.config.auto_remove_old_episodes:
375 old_episodes = self.get_old_episodes()
376 if len(old_episodes) > 0:
377 self.delete_episode_list(old_episodes, confirm=False)
378 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
380 if gpodder.ui.fremantle:
381 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
382 self.button_subscribe.set_sensitive(True)
383 self.button_podcasts.set_sensitive(True)
384 self.button_downloads.set_sensitive(True)
386 # First-time users should be asked if they want to see the OPML
387 if not self.channels and not gpodder.ui.fremantle:
388 util.idle_add(self.on_itemUpdate_activate)
390 def on_button_subscribe_clicked(self, button):
391 self.on_itemImportChannels_activate(button)
393 def on_button_podcasts_clicked(self, widget):
394 if self.channels:
395 self.podcasts_window.show()
396 else:
397 gPodderWelcome(self.gPodder, \
398 show_example_podcasts_callback=self.on_itemImportChannels_activate, \
399 setup_my_gpodder_callback=self.on_download_from_mygpo)
401 def on_button_downloads_clicked(self, widget):
402 self.downloads_window.show()
404 def on_window_orientation_changed(self, orientation):
405 old_container = self.main_window.get_child()
406 if orientation == Orientation.PORTRAIT:
407 container = gtk.VButtonBox()
408 else:
409 container = gtk.HButtonBox()
410 container.set_layout(old_container.get_layout())
411 for child in old_container.get_children():
412 if orientation == Orientation.LANDSCAPE:
413 child.set_alignment(0.5, 0.5, 0., 0.)
414 child.set_property('width-request', 200)
415 else:
416 child.set_alignment(0.5, 0.5, .9, 0.)
417 child.set_property('width-request', 350)
418 child.reparent(container)
419 container.show_all()
420 self.buttonbox = container
421 self.main_window.remove(old_container)
422 self.main_window.add(container)
424 def on_treeview_podcasts_selection_changed(self, selection):
425 model, iter = selection.get_selected()
426 if iter is None:
427 self.active_channel = None
428 self.episode_list_model.clear()
430 def on_treeview_button_pressed(self, treeview, event):
431 TreeViewHelper.save_button_press_event(treeview, event)
433 if getattr(treeview, TreeViewHelper.ROLE) == \
434 TreeViewHelper.ROLE_PODCASTS:
435 return self.currently_updating
437 return event.button == self.context_menu_mouse_button and \
438 gpodder.ui.desktop
440 def on_treeview_podcasts_button_released(self, treeview, event):
441 if gpodder.ui.maemo:
442 return self.treeview_channels_handle_gestures(treeview, event)
444 return self.treeview_channels_show_context_menu(treeview, event)
446 def on_treeview_episodes_button_released(self, treeview, event):
447 if gpodder.ui.maemo:
448 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
449 return self.treeview_available_handle_gestures(treeview, event)
451 return self.treeview_available_show_context_menu(treeview, event)
453 def on_treeview_downloads_button_released(self, treeview, event):
454 return self.treeview_downloads_show_context_menu(treeview, event)
456 def init_podcast_list_treeview(self):
457 # Set up podcast channel tree view widget
458 self.treeChannels.set_search_equal_func(TreeViewHelper.make_search_equal_func(PodcastListModel))
460 if gpodder.ui.fremantle:
461 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
462 self.item_view_podcasts_downloaded.set_active(True)
463 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
464 self.item_view_podcasts_unplayed.set_active(True)
465 else:
466 self.item_view_podcasts_all.set_active(True)
467 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
469 iconcolumn = gtk.TreeViewColumn('')
470 iconcell = gtk.CellRendererPixbuf()
471 iconcolumn.pack_start(iconcell, False)
472 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
473 self.treeChannels.append_column(iconcolumn)
475 namecolumn = gtk.TreeViewColumn('')
476 namecell = gtk.CellRendererText()
477 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
478 namecolumn.pack_start(namecell, True)
479 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
481 iconcell = gtk.CellRendererPixbuf()
482 iconcell.set_property('xalign', 1.0)
483 namecolumn.pack_start(iconcell, False)
484 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
485 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
486 self.treeChannels.append_column(namecolumn)
488 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
490 # When no podcast is selected, clear the episode list model
491 selection = self.treeChannels.get_selection()
492 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
494 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
496 def init_episode_list_treeview(self):
497 self.episode_list_model = EpisodeListModel()
499 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
500 self.item_view_episodes_undeleted.set_active(True)
501 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
502 self.item_view_episodes_downloaded.set_active(True)
503 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
504 self.item_view_episodes_unplayed.set_active(True)
505 else:
506 self.item_view_episodes_all.set_active(True)
508 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
510 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
512 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
514 iconcell = gtk.CellRendererPixbuf()
515 if gpodder.ui.maemo:
516 iconcell.set_fixed_size(50, 50)
517 status_column_label = ''
518 else:
519 status_column_label = _('Status')
520 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=EpisodeListModel.C_STATUS_ICON)
522 namecell = gtk.CellRendererText()
523 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
524 namecolumn = gtk.TreeViewColumn(_('Episode'), namecell, markup=EpisodeListModel.C_DESCRIPTION)
525 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
526 namecolumn.set_resizable(True)
527 namecolumn.set_expand(True)
529 sizecell = gtk.CellRendererText()
530 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
532 releasecell = gtk.CellRendererText()
533 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
535 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
536 itemcolumn.set_reorderable(True)
537 self.treeAvailable.append_column(itemcolumn)
539 if gpodder.ui.maemo:
540 sizecolumn.set_visible(False)
541 releasecolumn.set_visible(False)
543 self.treeAvailable.set_search_equal_func(TreeViewHelper.make_search_equal_func(EpisodeListModel))
545 selection = self.treeAvailable.get_selection()
546 if gpodder.ui.diablo:
547 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
548 selection.set_mode(gtk.SELECTION_SINGLE)
549 else:
550 selection.set_mode(gtk.SELECTION_MULTIPLE)
551 elif gpodder.ui.fremantle:
552 selection.set_mode(gtk.SELECTION_SINGLE)
553 else:
554 selection.set_mode(gtk.SELECTION_MULTIPLE)
555 # Update the sensitivity of the toolbar buttons on the Desktop
556 selection.connect('changed', lambda s: self.play_or_download())
558 if gpodder.ui.diablo:
559 # Set up the tap-and-hold context menu for podcasts
560 menu = gtk.Menu()
561 menu.append(self.itemUpdateChannel.create_menu_item())
562 menu.append(self.itemEditChannel.create_menu_item())
563 menu.append(gtk.SeparatorMenuItem())
564 menu.append(self.itemRemoveChannel.create_menu_item())
565 menu.append(gtk.SeparatorMenuItem())
566 item = gtk.ImageMenuItem(_('Close this menu'))
567 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
568 gtk.ICON_SIZE_MENU))
569 menu.append(item)
570 menu.show_all()
571 menu = self.set_finger_friendly(menu)
572 self.treeChannels.tap_and_hold_setup(menu)
575 def init_download_list_treeview(self):
576 # enable multiple selection support
577 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
578 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
580 # columns and renderers for "download progress" tab
581 # First column: [ICON] Episodename
582 column = gtk.TreeViewColumn(_('Episode'))
584 cell = gtk.CellRendererPixbuf()
585 if gpodder.ui.maemo:
586 cell.set_fixed_size(50, 50)
587 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
588 column.pack_start(cell, expand=False)
589 column.add_attribute(cell, 'stock-id', \
590 DownloadStatusModel.C_ICON_NAME)
592 cell = gtk.CellRendererText()
593 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
594 column.pack_start(cell, expand=True)
595 column.add_attribute(cell, 'markup', DownloadStatusModel.C_NAME)
596 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
597 column.set_expand(True)
598 self.treeDownloads.append_column(column)
600 # Second column: Progress
601 cell = gtk.CellRendererProgress()
602 cell.set_property('yalign', .5)
603 cell.set_property('ypad', 6)
604 column = gtk.TreeViewColumn(_('Progress'), cell,
605 value=DownloadStatusModel.C_PROGRESS, \
606 text=DownloadStatusModel.C_PROGRESS_TEXT)
607 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
608 column.set_expand(False)
609 column.set_property('min-width', 150)
610 column.set_property('max-width', 150)
611 self.treeDownloads.append_column(column)
613 self.treeDownloads.set_model(self.download_status_model)
614 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
616 def on_treeview_expose_event(self, treeview, event):
617 if event.window == treeview.get_bin_window():
618 model = treeview.get_model()
619 if (model is not None and model.get_iter_first() is not None):
620 return False
622 role = getattr(treeview, TreeViewHelper.ROLE)
623 ctx = event.window.cairo_create()
624 ctx.rectangle(event.area.x, event.area.y,
625 event.area.width, event.area.height)
626 ctx.clip()
628 x, y, width, height, depth = event.window.get_geometry()
630 if role == TreeViewHelper.ROLE_EPISODES:
631 if self.currently_updating:
632 text = _('Loading episodes') + '...'
633 elif self.config.episode_list_view_mode != \
634 EpisodeListModel.VIEW_ALL:
635 text = _('No episodes in current view')
636 else:
637 text = _('No episodes available')
638 elif role == TreeViewHelper.ROLE_PODCASTS:
639 if self.config.episode_list_view_mode != \
640 EpisodeListModel.VIEW_ALL and \
641 self.config.podcast_list_hide_boring and \
642 len(self.channels) > 0:
643 text = _('No podcasts in this view')
644 else:
645 text = _('No subscriptions')
646 elif role == TreeViewHelper.ROLE_DOWNLOADS:
647 text = _('No active downloads')
648 else:
649 raise Exception('on_treeview_expose_event: unknown role')
651 if gpodder.ui.fremantle:
652 from gpodder.gtkui.frmntl import style
653 font_desc = style.get_font_desc('LargeSystemFont')
654 else:
655 font_desc = None
657 draw_text_box_centered(ctx, treeview, width, height, text, font_desc)
659 return False
661 def enable_download_list_update(self):
662 if not self.download_list_update_enabled:
663 gobject.timeout_add(1500, self.update_downloads_list)
664 self.download_list_update_enabled = True
666 def on_btnCleanUpDownloads_clicked(self, button):
667 model = self.download_status_model
669 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
670 changed_episode_urls = []
671 for row_reference, task in all_tasks:
672 if task.status in (task.DONE, task.CANCELLED, task.FAILED):
673 model.remove(model.get_iter(row_reference.get_path()))
674 try:
675 # We don't "see" this task anymore - remove it;
676 # this is needed, so update_episode_list_icons()
677 # below gets the correct list of "seen" tasks
678 self.download_tasks_seen.remove(task)
679 except KeyError, key_error:
680 log('Cannot remove task from "seen" list: %s', task, sender=self)
681 changed_episode_urls.append(task.url)
682 # Tell the task that it has been removed (so it can clean up)
683 task.removed_from_list()
685 # Tell the podcasts tab to update icons for our removed podcasts
686 self.update_episode_list_icons(changed_episode_urls)
688 # Tell the shownotes window that we have removed the episode
689 if self.episode_shownotes_window is not None and \
690 self.episode_shownotes_window.episode is not None and \
691 self.episode_shownotes_window.episode.url in changed_episode_urls:
692 self.episode_shownotes_window._download_status_changed(None)
694 # Update the tab title and downloads list
695 self.update_downloads_list()
697 def on_tool_downloads_toggled(self, toolbutton):
698 if toolbutton.get_active():
699 self.wNotebook.set_current_page(1)
700 else:
701 self.wNotebook.set_current_page(0)
703 def update_downloads_list(self):
704 try:
705 model = self.download_status_model
707 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
708 total_speed, total_size, done_size = 0, 0, 0
710 # Keep a list of all download tasks that we've seen
711 download_tasks_seen = set()
713 # Remember the DownloadTask object for the episode that
714 # has been opened in the episode shownotes dialog (if any)
715 if self.episode_shownotes_window is not None:
716 shownotes_episode = self.episode_shownotes_window.episode
717 shownotes_task = None
718 else:
719 shownotes_episode = None
720 shownotes_task = None
722 # Do not go through the list of the model is not (yet) available
723 if model is None:
724 model = ()
726 for row in model:
727 self.download_status_model.request_update(row.iter)
729 task = row[self.download_status_model.C_TASK]
730 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
732 total_size += size
733 done_size += size*progress
735 if shownotes_episode is not None and \
736 shownotes_episode.url == task.episode.url:
737 shownotes_task = task
739 download_tasks_seen.add(task)
741 if status == download.DownloadTask.DOWNLOADING:
742 downloading += 1
743 total_speed += speed
744 elif status == download.DownloadTask.FAILED:
745 failed += 1
746 elif status == download.DownloadTask.DONE:
747 finished += 1
748 elif status == download.DownloadTask.QUEUED:
749 queued += 1
750 elif status == download.DownloadTask.PAUSED:
751 paused += 1
752 else:
753 others += 1
755 # Remember which tasks we have seen after this run
756 self.download_tasks_seen = download_tasks_seen
758 if gpodder.ui.desktop:
759 text = [_('Downloads')]
760 if downloading + failed + finished + queued > 0:
761 s = []
762 if downloading > 0:
763 s.append(_('%d active') % downloading)
764 if failed > 0:
765 s.append(_('%d failed') % failed)
766 if finished > 0:
767 s.append(_('%d done') % finished)
768 if queued > 0:
769 s.append(_('%d queued') % queued)
770 text.append(' (' + ', '.join(s)+')')
771 self.labelDownloads.set_text(''.join(text))
772 elif gpodder.ui.diablo:
773 sum = downloading + failed + finished + queued + paused + others
774 if sum:
775 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
776 else:
777 self.tool_downloads.set_label(_('Downloads'))
778 elif gpodder.ui.fremantle:
779 if downloading + queued > 0:
780 self.button_downloads.set_value(_('%d active') % (downloading+queued))
781 elif failed > 0:
782 self.button_downloads.set_value(_('%d failed') % failed)
783 elif paused > 0:
784 self.button_downloads.set_value(_('%d paused') % paused)
785 else:
786 self.button_downloads.set_value(_('None active'))
788 title = [self.default_title]
790 # We have to update all episodes/channels for which the status has
791 # changed. Accessing task.status_changed has the side effect of
792 # re-setting the changed flag, so we need to get the "changed" list
793 # of tuples first and split it into two lists afterwards
794 changed = [(task.url, task.podcast_url) for task in \
795 self.download_tasks_seen if task.status_changed]
796 episode_urls = [episode_url for episode_url, channel_url in changed]
797 channel_urls = [channel_url for episode_url, channel_url in changed]
799 count = downloading + queued
800 if count > 0:
801 if count == 1:
802 title.append( _('downloading one file'))
803 elif count > 1:
804 title.append( _('downloading %d files') % count)
806 if total_size > 0:
807 percentage = 100.0*done_size/total_size
808 else:
809 percentage = 0.0
810 total_speed = util.format_filesize(total_speed)
811 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
812 if self.tray_icon is not None:
813 # Update the tray icon status and progress bar
814 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
815 self.tray_icon.draw_progress_bar(percentage/100.)
816 elif self.last_download_count > 0:
817 if self.tray_icon is not None:
818 # Update the tray icon status
819 self.tray_icon.set_status()
820 self.tray_icon.downloads_finished(self.download_tasks_seen)
821 if gpodder.ui.diablo:
822 hildon.hildon_banner_show_information(self.gPodder, None, 'gPodder: %s' % _('All downloads finished'))
823 log('All downloads have finished.', sender=self)
824 if self.config.cmd_all_downloads_complete:
825 util.run_external_command(self.config.cmd_all_downloads_complete)
826 self.last_download_count = count
828 self.gPodder.set_title(' - '.join(title))
830 self.update_episode_list_icons(episode_urls)
831 if self.episode_shownotes_window is not None:
832 if (shownotes_task and shownotes_task.url in episode_urls) or \
833 shownotes_task != self.episode_shownotes_window.task:
834 self.episode_shownotes_window._download_status_changed(shownotes_task)
835 self.episode_shownotes_window._download_status_progress()
836 self.play_or_download()
837 if channel_urls:
838 self.update_podcast_list_model(channel_urls)
840 if not self.download_queue_manager.are_queued_or_active_tasks():
841 self.download_list_update_enabled = False
843 return self.download_list_update_enabled
844 except Exception, e:
845 log('Exception happened while updating download list.', sender=self, traceback=True)
846 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
847 # We return False here, so the update loop won't be called again,
848 # that's why we require the restart of gPodder in the message.
849 return False
851 def on_config_changed(self, name, old_value, new_value):
852 if name == 'show_toolbar' and gpodder.ui.desktop:
853 self.toolbar.set_property('visible', new_value)
854 elif name == 'episode_list_descriptions':
855 self.update_episode_list_model()
857 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
858 # With get_bin_window, we get the window that contains the rows without
859 # the header. The Y coordinate of this window will be the height of the
860 # treeview header. This is the amount we have to subtract from the
861 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
862 (x_bin, y_bin) = treeview.get_bin_window().get_position()
863 y -= x_bin
864 y -= y_bin
865 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
867 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or (column is not None and column != treeview.get_columns()[0]):
868 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
869 return False
871 if path is not None:
872 model = treeview.get_model()
873 iter = model.get_iter(path)
874 role = getattr(treeview, TreeViewHelper.ROLE)
876 if role == TreeViewHelper.ROLE_EPISODES:
877 id = model.get_value(iter, EpisodeListModel.C_URL)
878 elif role == TreeViewHelper.ROLE_PODCASTS:
879 id = model.get_value(iter, PodcastListModel.C_URL)
881 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
882 if last_tooltip is not None and last_tooltip != id:
883 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
884 return False
885 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
887 if role == TreeViewHelper.ROLE_EPISODES:
888 description = model.get_value(iter, EpisodeListModel.C_DESCRIPTION_STRIPPED)
889 if len(description) > 400:
890 description = description[:398]+'[...]'
892 tooltip.set_text(description)
893 elif role == TreeViewHelper.ROLE_PODCASTS:
894 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
895 channel.request_save_dir_size()
896 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
897 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
898 if error_str:
899 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
900 error_str = '<span foreground="#ff0000">%s</span>' % error_str
901 table = gtk.Table(rows=3, columns=3)
902 table.set_row_spacings(5)
903 table.set_col_spacings(5)
904 table.set_border_width(5)
906 heading = gtk.Label()
907 heading.set_alignment(0, 1)
908 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
909 table.attach(heading, 0, 1, 0, 1)
910 size_info = gtk.Label()
911 size_info.set_alignment(1, 1)
912 size_info.set_justify(gtk.JUSTIFY_RIGHT)
913 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
914 table.attach(size_info, 2, 3, 0, 1)
916 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
918 if len(channel.description) < 500:
919 description = channel.description
920 else:
921 pos = channel.description.find('\n\n')
922 if pos == -1 or pos > 500:
923 description = channel.description[:498]+'[...]'
924 else:
925 description = channel.description[:pos]
927 description = gtk.Label(description)
928 if error_str:
929 description.set_markup(error_str)
930 description.set_alignment(0, 0)
931 description.set_line_wrap(True)
932 table.attach(description, 0, 3, 2, 3)
934 table.show_all()
935 tooltip.set_custom(table)
937 return True
939 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
940 return False
942 def treeview_allow_tooltips(self, treeview, allow):
943 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
945 def update_m3u_playlist_clicked(self, widget):
946 if self.active_channel is not None:
947 self.active_channel.update_m3u_playlist()
948 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
950 def treeview_handle_context_menu_click(self, treeview, event):
951 x, y = int(event.x), int(event.y)
952 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
954 selection = treeview.get_selection()
955 model, paths = selection.get_selected_rows()
957 if path is None or (path not in paths and \
958 event.button == self.context_menu_mouse_button):
959 # We have right-clicked, but not into the selection,
960 # assume we don't want to operate on the selection
961 paths = []
963 if path is not None and not paths and \
964 event.button == self.context_menu_mouse_button:
965 # No selection or clicked outside selection;
966 # select the single item where we clicked
967 treeview.grab_focus()
968 treeview.set_cursor(path, column, 0)
969 paths = [path]
971 if not paths:
972 # Unselect any remaining items (clicked elsewhere)
973 if hasattr(treeview, 'is_rubber_banding_active'):
974 if not treeview.is_rubber_banding_active():
975 selection.unselect_all()
976 else:
977 selection.unselect_all()
979 return model, paths
981 def downloads_list_get_selection(self, model=None, paths=None):
982 if model is None and paths is None:
983 selection = self.treeDownloads.get_selection()
984 model, paths = selection.get_selected_rows()
986 can_queue, can_cancel, can_pause, can_remove = (True,)*4
987 selected_tasks = [(gtk.TreeRowReference(model, path), \
988 model.get_value(model.get_iter(path), \
989 DownloadStatusModel.C_TASK)) for path in paths]
991 for row_reference, task in selected_tasks:
992 if task.status not in (download.DownloadTask.PAUSED, \
993 download.DownloadTask.FAILED, \
994 download.DownloadTask.CANCELLED):
995 can_queue = False
996 if task.status not in (download.DownloadTask.PAUSED, \
997 download.DownloadTask.QUEUED, \
998 download.DownloadTask.DOWNLOADING):
999 can_cancel = False
1000 if task.status not in (download.DownloadTask.QUEUED, \
1001 download.DownloadTask.DOWNLOADING):
1002 can_pause = False
1003 if task.status not in (download.DownloadTask.CANCELLED, \
1004 download.DownloadTask.FAILED, \
1005 download.DownloadTask.DONE):
1006 can_remove = False
1008 return selected_tasks, can_queue, can_cancel, can_pause, can_remove
1010 def _for_each_task_set_status(self, tasks, status):
1011 episode_urls = set()
1012 model = self.treeDownloads.get_model()
1013 for row_reference, task in tasks:
1014 if status == download.DownloadTask.QUEUED:
1015 # Only queue task when its paused/failed/cancelled
1016 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED):
1017 self.download_queue_manager.add_task(task)
1018 self.enable_download_list_update()
1019 elif status == download.DownloadTask.CANCELLED:
1020 # Cancelling a download allowed when downloading/queued
1021 if task.status in (task.QUEUED, task.DOWNLOADING):
1022 task.status = status
1023 # Cancelling paused downloads requires a call to .run()
1024 elif task.status == task.PAUSED:
1025 task.status = status
1026 # Call run, so the partial file gets deleted
1027 task.run()
1028 elif status == download.DownloadTask.PAUSED:
1029 # Pausing a download only when queued/downloading
1030 if task.status in (task.DOWNLOADING, task.QUEUED):
1031 task.status = status
1032 elif status is None:
1033 # Remove the selected task - cancel downloading/queued tasks
1034 if task.status in (task.QUEUED, task.DOWNLOADING):
1035 task.status = task.CANCELLED
1036 model.remove(model.get_iter(row_reference.get_path()))
1037 # Remember the URL, so we can tell the UI to update
1038 try:
1039 # We don't "see" this task anymore - remove it;
1040 # this is needed, so update_episode_list_icons()
1041 # below gets the correct list of "seen" tasks
1042 self.download_tasks_seen.remove(task)
1043 except KeyError, key_error:
1044 log('Cannot remove task from "seen" list: %s', task, sender=self)
1045 episode_urls.add(task.url)
1046 # Tell the task that it has been removed (so it can clean up)
1047 task.removed_from_list()
1048 else:
1049 # We can (hopefully) simply set the task status here
1050 task.status = status
1051 # Tell the podcasts tab to update icons for our removed podcasts
1052 self.update_episode_list_icons(episode_urls)
1053 # Update the tab title and downloads list
1054 self.update_downloads_list()
1056 def treeview_downloads_show_context_menu(self, treeview, event):
1057 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1058 if not paths:
1059 if not hasattr(treeview, 'is_rubber_banding_active'):
1060 return True
1061 else:
1062 return not treeview.is_rubber_banding_active()
1064 if event.button == self.context_menu_mouse_button:
1065 selected_tasks, can_queue, can_cancel, can_pause, can_remove = \
1066 self.downloads_list_get_selection(model, paths)
1068 def make_menu_item(label, stock_id, tasks, status, sensitive):
1069 # This creates a menu item for selection-wide actions
1070 item = gtk.ImageMenuItem(label)
1071 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1072 item.connect('activate', lambda item: self._for_each_task_set_status(tasks, status))
1073 item.set_sensitive(sensitive)
1074 return self.set_finger_friendly(item)
1076 menu = gtk.Menu()
1078 item = gtk.ImageMenuItem(_('Episode details'))
1079 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1080 if len(selected_tasks) == 1:
1081 row_reference, task = selected_tasks[0]
1082 episode = task.episode
1083 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1084 else:
1085 item.set_sensitive(False)
1086 menu.append(self.set_finger_friendly(item))
1087 menu.append(gtk.SeparatorMenuItem())
1088 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED, can_queue))
1089 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED, can_cancel))
1090 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED, can_pause))
1091 menu.append(gtk.SeparatorMenuItem())
1092 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None, can_remove))
1094 if gpodder.ui.maemo:
1095 # Because we open the popup on left-click for Maemo,
1096 # we also include a non-action to close the menu
1097 menu.append(gtk.SeparatorMenuItem())
1098 item = gtk.ImageMenuItem(_('Close this menu'))
1099 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1101 menu.append(self.set_finger_friendly(item))
1103 menu.show_all()
1104 menu.popup(None, None, None, event.button, event.time)
1105 return True
1107 def treeview_channels_show_context_menu(self, treeview, event):
1108 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1109 if not paths:
1110 return True
1112 if event.button == 3:
1113 menu = gtk.Menu()
1115 ICON = lambda x: x
1117 item = gtk.ImageMenuItem( _('Open download folder'))
1118 item.set_image( gtk.image_new_from_icon_name(ICON('folder-open'), gtk.ICON_SIZE_MENU))
1119 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1120 menu.append( item)
1122 item = gtk.ImageMenuItem( _('Update Feed'))
1123 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1124 item.connect('activate', self.on_itemUpdateChannel_activate )
1125 item.set_sensitive( not self.updating_feed_cache )
1126 menu.append( item)
1128 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1129 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1130 item.connect('activate', self.update_m3u_playlist_clicked)
1131 menu.append(item)
1133 if self.active_channel.link:
1134 item = gtk.ImageMenuItem(_('Visit website'))
1135 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1136 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1137 menu.append(item)
1139 if self.active_channel.channel_is_locked:
1140 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1141 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1142 item.connect('activate', self.on_channel_toggle_lock_activate)
1143 menu.append(self.set_finger_friendly(item))
1144 else:
1145 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1146 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1147 item.connect('activate', self.on_channel_toggle_lock_activate)
1148 menu.append(self.set_finger_friendly(item))
1151 menu.append( gtk.SeparatorMenuItem())
1153 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1154 item.connect( 'activate', self.on_itemEditChannel_activate)
1155 menu.append( item)
1157 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1158 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1159 menu.append( item)
1161 menu.show_all()
1162 # Disable tooltips while we are showing the menu, so
1163 # the tooltip will not appear over the menu
1164 self.treeview_allow_tooltips(self.treeChannels, False)
1165 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1166 menu.popup( None, None, None, event.button, event.time)
1168 return True
1170 def on_itemClose_activate(self, widget):
1171 if self.tray_icon is not None:
1172 self.iconify_main_window()
1173 else:
1174 self.on_gPodder_delete_event(widget)
1176 def cover_file_removed(self, channel_url):
1178 The Cover Downloader calls this when a previously-
1179 available cover has been removed from the disk. We
1180 have to update our model to reflect this change.
1182 self.podcast_list_model.delete_cover_by_url(channel_url)
1184 def cover_download_finished(self, channel_url, pixbuf):
1186 The Cover Downloader calls this when it has finished
1187 downloading (or registering, if already downloaded)
1188 a new channel cover, which is ready for displaying.
1190 self.podcast_list_model.add_cover_by_url(channel_url, pixbuf)
1192 def save_episode_as_file(self, episode):
1193 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1194 if episode.was_downloaded(and_exists=True):
1195 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1196 copy_from = episode.local_filename(create=False)
1197 assert copy_from is not None
1198 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1199 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1200 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1202 def copy_episodes_bluetooth(self, episodes):
1203 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1205 def convert_and_send_thread(episode):
1206 for episode in episodes:
1207 filename = episode.local_filename(create=False)
1208 assert filename is not None
1209 destfile = os.path.join(tempfile.gettempdir(), \
1210 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1211 (base, ext) = os.path.splitext(filename)
1212 if not destfile.endswith(ext):
1213 destfile += ext
1215 try:
1216 shutil.copyfile(filename, destfile)
1217 util.bluetooth_send_file(destfile)
1218 except:
1219 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1220 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1222 util.delete_file(destfile)
1224 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1226 def get_device_name(self):
1227 if self.config.device_type == 'ipod':
1228 return _('iPod')
1229 elif self.config.device_type in ('filesystem', 'mtp'):
1230 return _('MP3 player')
1231 else:
1232 return '(unknown device)'
1234 def _treeview_button_released(self, treeview, event):
1235 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1236 dy = int(abs(event.y-ypos))
1237 dx = int(event.x-xpos)
1239 selection = treeview.get_selection()
1240 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1241 if path is None or dy > 30:
1242 return (False, dx, dy)
1244 path, column, x, y = path
1245 selection.select_path(path)
1246 treeview.set_cursor(path)
1247 treeview.grab_focus()
1249 return (True, dx, dy)
1251 def treeview_channels_handle_gestures(self, treeview, event):
1252 if self.currently_updating:
1253 return False
1255 selected, dx, dy = self._treeview_button_released(treeview, event)
1257 if selected:
1258 if self.config.maemo_enable_gestures:
1259 if dx > 70:
1260 self.on_itemUpdateChannel_activate()
1261 elif dx < -70:
1262 self.on_itemEditChannel_activate(treeview)
1264 return False
1266 def treeview_available_handle_gestures(self, treeview, event):
1267 selected, dx, dy = self._treeview_button_released(treeview, event)
1269 if selected:
1270 if self.config.maemo_enable_gestures:
1271 if dx > 70:
1272 self.on_playback_selected_episodes(None)
1273 return True
1274 elif dx < -70:
1275 self.on_shownotes_selected_episodes(None)
1276 return True
1278 # Pass the event to the context menu handler for treeAvailable
1279 self.treeview_available_show_context_menu(treeview, event)
1281 return True
1283 def treeview_available_show_context_menu(self, treeview, event):
1284 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1285 if not paths:
1286 if not hasattr(treeview, 'is_rubber_banding_active'):
1287 return True
1288 else:
1289 return not treeview.is_rubber_banding_active()
1291 if event.button == self.context_menu_mouse_button:
1292 episodes = self.get_selected_episodes()
1293 any_locked = any(e.is_locked for e in episodes)
1294 any_played = any(e.is_played for e in episodes)
1295 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1297 menu = gtk.Menu()
1299 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1301 if open_instead_of_play:
1302 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1303 else:
1304 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1306 item.set_sensitive(can_play)
1307 item.connect('activate', self.on_playback_selected_episodes)
1308 menu.append(self.set_finger_friendly(item))
1310 if not can_cancel:
1311 item = gtk.ImageMenuItem(_('Download'))
1312 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1313 item.set_sensitive(can_download)
1314 item.connect('activate', self.on_download_selected_episodes)
1315 menu.append(self.set_finger_friendly(item))
1316 else:
1317 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1318 item.connect('activate', self.on_item_cancel_download_activate)
1319 menu.append(self.set_finger_friendly(item))
1321 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1322 item.set_sensitive(can_delete)
1323 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1324 menu.append(self.set_finger_friendly(item))
1326 if one_is_new:
1327 item = gtk.ImageMenuItem(_('Do not download'))
1328 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1329 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1330 menu.append(self.set_finger_friendly(item))
1331 elif can_download:
1332 item = gtk.ImageMenuItem(_('Mark as new'))
1333 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1334 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1335 menu.append(self.set_finger_friendly(item))
1337 ICON = lambda x: x
1339 # Ok, this probably makes sense to only display for downloaded files
1340 if can_play and not can_download:
1341 menu.append( gtk.SeparatorMenuItem())
1342 item = gtk.ImageMenuItem(_('Save to disk'))
1343 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1344 item.connect('activate', lambda w: [self.save_episode_as_file(e) for e in episodes])
1345 menu.append(self.set_finger_friendly(item))
1346 if self.bluetooth_available:
1347 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1348 item.set_image(gtk.image_new_from_icon_name(ICON('bluetooth'), gtk.ICON_SIZE_MENU))
1349 item.connect('activate', lambda w: self.copy_episodes_bluetooth(episodes))
1350 menu.append(self.set_finger_friendly(item))
1351 if can_transfer:
1352 item = gtk.ImageMenuItem(_('Transfer to %s') % self.get_device_name())
1353 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
1354 item.connect('activate', lambda w: self.on_sync_to_ipod_activate(w, episodes))
1355 menu.append(self.set_finger_friendly(item))
1357 if can_play:
1358 menu.append( gtk.SeparatorMenuItem())
1359 if any_played:
1360 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1361 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1362 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1363 menu.append(self.set_finger_friendly(item))
1364 else:
1365 item = gtk.ImageMenuItem(_('Mark as played'))
1366 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1367 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1368 menu.append(self.set_finger_friendly(item))
1370 if any_locked:
1371 item = gtk.ImageMenuItem(_('Allow deletion'))
1372 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1373 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, False))
1374 menu.append(self.set_finger_friendly(item))
1375 else:
1376 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1377 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1378 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, True))
1379 menu.append(self.set_finger_friendly(item))
1381 menu.append(gtk.SeparatorMenuItem())
1382 # Single item, add episode information menu item
1383 item = gtk.ImageMenuItem(_('Episode details'))
1384 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1385 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
1386 menu.append(self.set_finger_friendly(item))
1388 # If we have it, also add episode website link
1389 if episodes[0].link and episodes[0].link != episodes[0].url:
1390 item = gtk.ImageMenuItem(_('Visit website'))
1391 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1392 item.connect('activate', lambda w: util.open_website(episodes[0].link))
1393 menu.append(self.set_finger_friendly(item))
1395 if gpodder.ui.maemo:
1396 # Because we open the popup on left-click for Maemo,
1397 # we also include a non-action to close the menu
1398 menu.append(gtk.SeparatorMenuItem())
1399 item = gtk.ImageMenuItem(_('Close this menu'))
1400 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1401 menu.append(self.set_finger_friendly(item))
1403 menu.show_all()
1404 # Disable tooltips while we are showing the menu, so
1405 # the tooltip will not appear over the menu
1406 self.treeview_allow_tooltips(self.treeAvailable, False)
1407 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
1408 menu.popup( None, None, None, event.button, event.time)
1410 return True
1412 def set_title(self, new_title):
1413 if not gpodder.ui.fremantle:
1414 self.default_title = new_title
1415 self.gPodder.set_title(new_title)
1417 def update_episode_list_icons(self, urls=None, selected=False, all=False):
1419 Updates the status icons in the episode list.
1421 If urls is given, it should be a list of URLs
1422 of episodes that should be updated.
1424 If urls is None, set ONE OF selected, all to
1425 True (the former updates just the selected
1426 episodes and the latter updates all episodes).
1428 if urls is not None:
1429 # We have a list of URLs to walk through
1430 self.episode_list_model.update_by_urls(urls, \
1431 self.episode_is_downloading, \
1432 self.config.episode_list_descriptions and \
1433 gpodder.ui.desktop)
1434 elif selected and not all:
1435 # We should update all selected episodes
1436 selection = self.treeAvailable.get_selection()
1437 model, paths = selection.get_selected_rows()
1438 for path in reversed(paths):
1439 iter = model.get_iter(path)
1440 self.episode_list_model.update_by_filter_iter(iter, \
1441 self.episode_is_downloading, \
1442 self.config.episode_list_descriptions and \
1443 gpodder.ui.desktop)
1444 elif all and not selected:
1445 # We update all (even the filter-hidden) episodes
1446 self.episode_list_model.update_all(\
1447 self.episode_is_downloading, \
1448 self.config.episode_list_descriptions and \
1449 gpodder.ui.desktop)
1450 else:
1451 # Wrong/invalid call - have to specify at least one parameter
1452 raise ValueError('Invalid call to update_episode_list_icons')
1454 def episode_list_status_changed(self, episodes):
1455 self.update_episode_list_icons(set(e.url for e in episodes))
1456 self.update_podcast_list_model(set(e.channel.url for e in episodes))
1457 self.db.commit()
1459 def clean_up_downloads(self, delete_partial=False):
1460 # Clean up temporary files left behind by old gPodder versions
1461 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
1463 if delete_partial:
1464 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
1466 for tempfile in temporary_files:
1467 util.delete_file(tempfile)
1469 # Clean up empty download folders and abandoned download folders
1470 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
1471 for ddir in download_dirs:
1472 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1473 globr = glob.glob(os.path.join(ddir, '*'))
1474 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
1475 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
1476 shutil.rmtree(ddir, ignore_errors=True)
1478 def streaming_possible(self):
1479 return self.config.player and \
1480 self.config.player != 'default' and \
1481 gpodder.ui.desktop
1483 def playback_episodes_for_real(self, episodes):
1484 groups = collections.defaultdict(list)
1485 for episode in episodes:
1486 file_type = episode.file_type()
1487 if file_type == 'video' and self.config.videoplayer and \
1488 self.config.videoplayer != 'default':
1489 player = self.config.videoplayer
1490 if gpodder.ui.diablo:
1491 # Use the wrapper script if it's installed to crop 3GP YouTube
1492 # videos to fit the screen (looks much nicer than w/ black border)
1493 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
1494 player = 'gpodder-mplayer'
1495 elif file_type == 'audio' and self.config.player and \
1496 self.config.player != 'default':
1497 player = self.config.player
1498 else:
1499 player = 'default'
1501 if file_type not in ('audio', 'video') or \
1502 (file_type == 'audio' and not self.config.audio_played_dbus) or \
1503 (file_type == 'video' and not self.config.video_played_dbus):
1504 # Mark episode as played in the database
1505 episode.mark(is_played=True)
1507 filename = episode.local_filename(create=False)
1508 if filename is None or not os.path.exists(filename):
1509 filename = episode.url
1510 groups[player].append(filename)
1512 # Open episodes with system default player
1513 if 'default' in groups:
1514 for filename in groups['default']:
1515 log('Opening with system default: %s', filename, sender=self)
1516 util.gui_open(filename)
1517 del groups['default']
1518 elif gpodder.ui.maemo:
1519 # When on Maemo and not opening with default, show a notification
1520 # (no startup notification for Panucci / MPlayer yet...)
1521 if len(episodes) == 1:
1522 text = _('Opening %s') % episodes[0].title
1523 else:
1524 text = _('Opening %d episodes') % len(episodes)
1526 banner = hildon.hildon_banner_show_animation(self.gPodder, None, text)
1528 def destroy_banner_later(banner):
1529 banner.destroy()
1530 return False
1531 gobject.timeout_add(5000, destroy_banner_later, banner)
1533 # For each type now, go and create play commands
1534 for group in groups:
1535 for command in util.format_desktop_command(group, groups[group]):
1536 log('Executing: %s', repr(command), sender=self)
1537 subprocess.Popen(command)
1539 def playback_episodes(self, episodes):
1540 episodes = [e for e in episodes if \
1541 e.was_downloaded(and_exists=True) or self.streaming_possible()]
1543 try:
1544 self.playback_episodes_for_real(episodes)
1545 except Exception, e:
1546 log('Error in playback!', sender=self, traceback=True)
1547 self.show_message( _('Please check your media player settings in the preferences dialog.'), _('Error opening player'), widget=self.toolPreferences)
1549 channel_urls = set()
1550 episode_urls = set()
1551 for episode in episodes:
1552 channel_urls.add(episode.channel.url)
1553 episode_urls.add(episode.url)
1554 self.update_episode_list_icons(episode_urls)
1555 self.update_podcast_list_model(channel_urls)
1557 def play_or_download(self):
1558 if not gpodder.ui.fremantle:
1559 if self.wNotebook.get_current_page() > 0:
1560 if gpodder.ui.desktop:
1561 self.toolCancel.set_sensitive(True)
1562 return
1564 if self.currently_updating:
1565 return (False, False, False, False, False, False)
1567 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1568 ( is_played, is_locked ) = (False,)*2
1570 open_instead_of_play = False
1572 selection = self.treeAvailable.get_selection()
1573 if selection.count_selected_rows() > 0:
1574 (model, paths) = selection.get_selected_rows()
1576 for path in paths:
1577 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
1579 if episode.file_type() not in ('audio', 'video'):
1580 open_instead_of_play = True
1582 if episode.was_downloaded():
1583 can_play = episode.was_downloaded(and_exists=True)
1584 can_delete = True
1585 is_played = episode.is_played
1586 is_locked = episode.is_locked
1587 if not can_play:
1588 can_download = True
1589 else:
1590 if self.episode_is_downloading(episode):
1591 can_cancel = True
1592 else:
1593 can_download = True
1595 can_download = can_download and not can_cancel
1596 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
1597 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
1599 if gpodder.ui.desktop:
1600 if open_instead_of_play:
1601 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1602 else:
1603 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1604 self.toolPlay.set_sensitive( can_play)
1605 self.toolDownload.set_sensitive( can_download)
1606 self.toolTransfer.set_sensitive( can_transfer)
1607 self.toolCancel.set_sensitive( can_cancel)
1609 if not gpodder.ui.fremantle:
1610 self.item_cancel_download.set_sensitive(can_cancel)
1611 self.itemDownloadSelected.set_sensitive(can_download)
1612 self.itemOpenSelected.set_sensitive(can_play)
1613 self.itemPlaySelected.set_sensitive(can_play)
1614 self.itemDeleteSelected.set_sensitive(can_play and not can_download)
1615 self.item_toggle_played.set_sensitive(can_play)
1616 self.item_toggle_lock.set_sensitive(can_play)
1617 self.itemOpenSelected.set_visible(open_instead_of_play)
1618 self.itemPlaySelected.set_visible(not open_instead_of_play)
1620 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1622 def on_cbMaxDownloads_toggled(self, widget, *args):
1623 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1625 def on_cbLimitDownloads_toggled(self, widget, *args):
1626 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1628 def episode_new_status_changed(self, urls):
1629 self.update_podcast_list_model()
1630 self.update_episode_list_icons(urls)
1632 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
1633 """Update the podcast list treeview model
1635 If urls is given, it should list the URLs of each
1636 podcast that has to be updated in the list.
1638 If selected is True, only update the model contents
1639 for the currently-selected podcast - nothing more.
1641 The caller can optionally specify "select_url",
1642 which is the URL of the podcast that is to be
1643 selected in the list after the update is complete.
1644 This only works if the podcast list has to be
1645 reloaded; i.e. something has been added or removed
1646 since the last update of the podcast list).
1648 selection = self.treeChannels.get_selection()
1649 model, iter = selection.get_selected()
1651 if selected:
1652 # very cheap! only update selected channel
1653 if iter is not None:
1654 self.podcast_list_model.update_by_filter_iter(iter)
1655 elif not self.channel_list_changed:
1656 # we can keep the model, but have to update some
1657 if urls is None:
1658 # still cheaper than reloading the whole list
1659 self.podcast_list_model.update_all()
1660 else:
1661 # ok, we got a bunch of urls to update
1662 self.podcast_list_model.update_by_urls(urls)
1663 else:
1664 if model and iter and select_url is None:
1665 # Get the URL of the currently-selected podcast
1666 select_url = model.get_value(iter, PodcastListModel.C_URL)
1668 # Update the podcast list model with new channels
1669 self.podcast_list_model.set_channels(self.channels)
1671 try:
1672 selected_iter = model.get_iter_first()
1673 # Find the previously-selected URL in the new
1674 # model if we have an URL (else select first)
1675 if select_url is not None:
1676 pos = model.get_iter_first()
1677 while pos is not None:
1678 url = model.get_value(pos, PodcastListModel.C_URL)
1679 if url == select_url:
1680 selected_iter = pos
1681 break
1682 pos = model.iter_next(pos)
1684 if not gpodder.ui.fremantle:
1685 if selected_iter is not None:
1686 selection.select_iter(selected_iter)
1687 self.on_treeChannels_cursor_changed(self.treeChannels)
1688 except:
1689 log('Cannot select podcast in list', traceback=True, sender=self)
1690 self.channel_list_changed = False
1692 def episode_is_downloading(self, episode):
1693 """Returns True if the given episode is being downloaded at the moment"""
1694 if episode is None:
1695 return False
1697 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
1699 def update_episode_list_model(self):
1700 if self.channels and self.active_channel is not None:
1701 if gpodder.ui.diablo:
1702 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Loading episodes'))
1703 else:
1704 banner = None
1706 if gpodder.ui.fremantle:
1707 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
1709 self.currently_updating = True
1710 self.episode_list_model.clear()
1711 def do_update_episode_list_model():
1712 self.episode_list_model.add_from_channel(\
1713 self.active_channel, \
1714 self.episode_is_downloading, \
1715 self.config.episode_list_descriptions \
1716 and gpodder.ui.desktop)
1718 def on_episode_list_model_updated():
1719 if banner is not None:
1720 banner.destroy()
1721 if gpodder.ui.fremantle:
1722 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
1723 self.treeAvailable.columns_autosize()
1724 self.currently_updating = False
1725 self.play_or_download()
1726 util.idle_add(on_episode_list_model_updated)
1727 threading.Thread(target=do_update_episode_list_model).start()
1728 else:
1729 self.episode_list_model.clear()
1731 def offer_new_episodes(self, channels=None):
1732 new_episodes = self.get_new_episodes(channels)
1733 if new_episodes:
1734 self.new_episodes_show(new_episodes)
1735 return True
1736 return False
1738 def add_podcast_list(self, urls, auth_tokens=None):
1739 """Subscribe to a list of podcast given their URLs
1741 If auth_tokens is given, it should be a dictionary
1742 mapping URLs to (username, password) tuples."""
1744 if auth_tokens is None:
1745 auth_tokens = {}
1747 # Sort and split the URL list into five buckets
1748 queued, failed, existing, worked, authreq = [], [], [], [], []
1749 for input_url in urls:
1750 url = util.normalize_feed_url(input_url)
1751 if url is None:
1752 # Fail this one because the URL is not valid
1753 failed.append(input_url)
1754 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
1755 # A podcast already exists in the list for this URL
1756 existing.append(url)
1757 else:
1758 # This URL has survived the first round - queue for add
1759 queued.append(url)
1760 if url != input_url and input_url in auth_tokens:
1761 auth_tokens[url] = auth_tokens[input_url]
1763 error_messages = {}
1764 redirections = {}
1766 progress = ProgressIndicator(_('Adding podcasts'), \
1767 _('Please wait while episode information is downloaded.'), \
1768 parent=self.main_window)
1770 def on_after_update():
1771 progress.on_finished()
1772 # Report already-existing subscriptions to the user
1773 if existing:
1774 title = _('Existing subscriptions skipped')
1775 message = _('You are already subscribed to these podcasts:') \
1776 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
1777 self.show_message(message, title, widget=self.treeChannels)
1779 # Report subscriptions that require authentication
1780 if authreq:
1781 retry_podcasts = {}
1782 for url in authreq:
1783 title = _('Podcast requires authentication')
1784 message = _('Please login to %s:') % (saxutils.escape(url),)
1785 success, auth_tokens = self.show_login_dialog(title, message)
1786 if success:
1787 retry_podcasts[url] = auth_tokens
1788 else:
1789 # Stop asking the user for more login data
1790 retry_podcasts = {}
1791 for url in authreq:
1792 error_messages[url] = _('Authentication failed')
1793 failed.append(url)
1794 break
1796 # If we have authentication data to retry, do so here
1797 if retry_podcasts:
1798 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
1800 # Report website redirections
1801 for url in redirections:
1802 title = _('Website redirection detected')
1803 message = _('The URL %s redirects to %s.') \
1804 + '\n\n' + _('Do you want to visit the website now?')
1805 message = message % (url, redirections[url])
1806 if self.show_confirmation(message, title):
1807 util.open_website(url)
1808 else:
1809 break
1811 # Report failed subscriptions to the user
1812 if failed:
1813 title = _('Could not add some podcasts')
1814 message = _('Some podcasts could not be added to your list:') \
1815 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
1816 error_messages.get(url, _('Unknown')))) for url in failed)
1817 self.show_message(message, title, important=True)
1819 # If at least one podcast has been added, save and update all
1820 if self.channel_list_changed:
1821 self.save_channels_opml()
1823 # If only one podcast was added, select it after the update
1824 if len(worked) == 1:
1825 url = worked[0]
1826 else:
1827 url = None
1829 # Update the list of subscribed podcasts
1830 self.update_feed_cache(force_update=False, select_url_afterwards=url)
1831 self.update_podcasts_tab()
1833 # Offer to download new episodes
1834 self.offer_new_episodes(channels=[c for c in self.channels if c.url in worked])
1836 def thread_proc():
1837 # After the initial sorting and splitting, try all queued podcasts
1838 length = len(queued)
1839 for index, url in enumerate(queued):
1840 progress.on_progress(float(index)/float(length))
1841 progress.on_message(url)
1842 log('QUEUE RUNNER: %s', url, sender=self)
1843 try:
1844 # The URL is valid and does not exist already - subscribe!
1845 channel = PodcastChannel.load(self.db, url=url, create=True, \
1846 authentication_tokens=auth_tokens.get(url, None), \
1847 max_episodes=self.config.max_episodes_per_feed, \
1848 download_dir=self.config.download_dir)
1850 try:
1851 username, password = util.username_password_from_url(url)
1852 except ValueError, ve:
1853 username, password = (None, None)
1855 if username is not None and channel.username is None and \
1856 password is not None and channel.password is None:
1857 channel.username = username
1858 channel.password = password
1859 channel.save()
1861 self._update_cover(channel)
1862 except feedcore.AuthenticationRequired:
1863 if url in auth_tokens:
1864 # Fail for wrong authentication data
1865 error_messages[url] = _('Authentication failed')
1866 failed.append(url)
1867 else:
1868 # Queue for login dialog later
1869 authreq.append(url)
1870 continue
1871 except feedcore.WifiLogin, error:
1872 redirections[url] = error.data
1873 failed.append(url)
1874 error_messages[url] = _('Redirection detected')
1875 continue
1876 except Exception, e:
1877 log('Subscription error: %s', e, traceback=True, sender=self)
1878 error_messages[url] = str(e)
1879 failed.append(url)
1880 continue
1882 assert channel is not None
1883 worked.append(channel.url)
1884 self.channels.append(channel)
1885 self.channel_list_changed = True
1886 util.idle_add(on_after_update)
1887 threading.Thread(target=thread_proc).start()
1889 def save_channels_opml(self):
1890 exporter = opml.Exporter(gpodder.subscription_file)
1891 return exporter.write(self.channels)
1893 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
1894 self.db.commit()
1895 self.updating_feed_cache = False
1897 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
1898 self.channel_list_changed = True
1899 self.update_podcast_list_model(select_url=select_url_afterwards)
1901 # Only search for new episodes in podcasts that have been
1902 # updated, not in other podcasts (for single-feed updates)
1903 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
1905 if gpodder.ui.fremantle:
1906 if self._fremantle_update_banner is not None:
1907 self._fremantle_update_banner.destroy()
1908 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1909 self.update_podcasts_tab()
1910 if episodes:
1911 self.new_episodes_show(episodes)
1912 else:
1913 self.show_message(_('No new episodes. Please check for new episodes later.'), important=True)
1914 return
1916 if self.tray_icon:
1917 self.tray_icon.set_status()
1919 if self.feed_cache_update_cancelled:
1920 # The user decided to abort the feed update
1921 self.show_update_feeds_buttons()
1922 elif not episodes:
1923 # Nothing new here - but inform the user
1924 self.pbFeedUpdate.set_fraction(1.0)
1925 self.pbFeedUpdate.set_text(_('No new episodes'))
1926 self.feed_cache_update_cancelled = True
1927 self.btnCancelFeedUpdate.show()
1928 self.btnCancelFeedUpdate.set_sensitive(True)
1929 if gpodder.ui.maemo:
1930 # btnCancelFeedUpdate is a ToolButton on Maemo
1931 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
1932 else:
1933 # btnCancelFeedUpdate is a normal gtk.Button
1934 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
1935 else:
1936 # New episodes are available
1937 self.pbFeedUpdate.set_fraction(1.0)
1938 # Are we minimized and should we auto download?
1939 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
1940 self.download_episode_list(episodes)
1941 if len(episodes) == 1:
1942 title = _('Downloading one new episode.')
1943 else:
1944 title = _('Downloading %d new episodes.') % len(episodes)
1946 if not gpodder.ui.fremantle:
1947 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
1948 self.show_update_feeds_buttons()
1949 else:
1950 self.show_update_feeds_buttons()
1951 # New episodes are available and we are not minimized
1952 if not self.config.do_not_show_new_episodes_dialog:
1953 self.new_episodes_show(episodes, notification=True)
1954 else:
1955 if len(episodes) == 1:
1956 message = _('One new episode is available for download')
1957 else:
1958 message = _('%i new episodes are available for download' % len(episodes))
1960 self.pbFeedUpdate.set_text(message)
1962 def _update_cover(self, channel):
1963 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
1964 self.cover_downloader.request_cover(channel)
1966 def update_feed_cache_proc(self, channels, select_url_afterwards):
1967 total = len(channels)
1969 for updated, channel in enumerate(channels):
1970 if not self.feed_cache_update_cancelled:
1971 try:
1972 # Update if timeout is not reached or we update a single podcast or skipping is disabled
1973 if channel.query_automatic_update() or total == 1 or not self.config.feed_update_skipping:
1974 channel.update(max_episodes=self.config.max_episodes_per_feed)
1975 else:
1976 log('Skipping update of %s (see feed_update_skipping)', channel.title, sender=self)
1977 self._update_cover(channel)
1978 except Exception, e:
1979 self.notification(_('There has been an error updating %s: %s') % (saxutils.escape(channel.url), saxutils.escape(str(e))), _('Error while updating feed'), widget=self.treeChannels)
1980 log('Error: %s', str(e), sender=self, traceback=True)
1982 if self.feed_cache_update_cancelled:
1983 break
1985 if gpodder.ui.fremantle:
1986 self.button_podcasts.set_value(_('%d/%d updated') % (updated, total))
1987 continue
1989 # By the time we get here the update may have already been cancelled
1990 if not self.feed_cache_update_cancelled:
1991 def update_progress():
1992 progression = _('Updated %s (%d/%d)') % (channel.title, updated, total)
1993 self.pbFeedUpdate.set_text(progression)
1994 if self.tray_icon:
1995 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
1996 self.pbFeedUpdate.set_fraction(float(updated)/float(total))
1997 util.idle_add(update_progress)
1999 updated_urls = [c.url for c in channels]
2000 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2002 def show_update_feeds_buttons(self):
2003 # Make sure that the buttons for updating feeds
2004 # appear - this should happen after a feed update
2005 if gpodder.ui.maemo:
2006 self.btnUpdateSelectedFeed.show()
2007 self.toolFeedUpdateProgress.hide()
2008 self.btnCancelFeedUpdate.hide()
2009 self.btnCancelFeedUpdate.set_is_important(False)
2010 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2011 self.toolbarSpacer.set_expand(True)
2012 self.toolbarSpacer.set_draw(False)
2013 else:
2014 self.hboxUpdateFeeds.hide()
2015 self.btnUpdateFeeds.show()
2016 self.itemUpdate.set_sensitive(True)
2017 self.itemUpdateChannel.set_sensitive(True)
2019 def on_btnCancelFeedUpdate_clicked(self, widget):
2020 if not self.feed_cache_update_cancelled:
2021 self.pbFeedUpdate.set_text(_('Cancelling...'))
2022 self.feed_cache_update_cancelled = True
2023 self.btnCancelFeedUpdate.set_sensitive(False)
2024 else:
2025 self.show_update_feeds_buttons()
2027 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
2028 if self.updating_feed_cache:
2029 return
2031 if not force_update:
2032 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2033 self.channel_list_changed = True
2034 self.update_podcast_list_model(select_url=select_url_afterwards)
2035 return
2037 self.updating_feed_cache = True
2039 if channels is None:
2040 channels = self.channels
2042 if gpodder.ui.fremantle:
2043 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
2044 self._fremantle_update_banner = hildon.hildon_banner_show_animation(self.main_window, \
2045 '', _('Updating podcast feeds'))
2046 else:
2047 self.itemUpdate.set_sensitive(False)
2048 self.itemUpdateChannel.set_sensitive(False)
2050 if self.tray_icon:
2051 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2053 if len(channels) == 1:
2054 text = _('Updating "%s"...') % channels[0].title
2055 else:
2056 text = _('Updating %d feeds...') % len(channels)
2057 self.pbFeedUpdate.set_text(text)
2058 self.pbFeedUpdate.set_fraction(0)
2060 self.feed_cache_update_cancelled = False
2061 self.btnCancelFeedUpdate.show()
2062 self.btnCancelFeedUpdate.set_sensitive(True)
2063 if gpodder.ui.maemo:
2064 self.toolbarSpacer.set_expand(False)
2065 self.toolbarSpacer.set_draw(True)
2066 self.btnUpdateSelectedFeed.hide()
2067 self.toolFeedUpdateProgress.show_all()
2068 else:
2069 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2070 self.hboxUpdateFeeds.show_all()
2071 self.btnUpdateFeeds.hide()
2073 args = (channels, select_url_afterwards)
2074 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
2076 def on_gPodder_delete_event(self, widget, *args):
2077 """Called when the GUI wants to close the window
2078 Displays a confirmation dialog (and closes/hides gPodder)
2081 downloading = self.download_status_model.are_downloads_in_progress()
2083 # Only iconify if we are using the window's "X" button,
2084 # but not when we are using "Quit" in the menu or toolbar
2085 if not self.config.on_quit_ask and self.config.on_quit_systray and self.tray_icon and widget.get_name() not in ('toolQuit', 'itemQuit'):
2086 self.iconify_main_window()
2087 elif self.config.on_quit_ask or downloading:
2088 if gpodder.ui.maemo:
2089 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2090 if result:
2091 self.close_gpodder()
2092 else:
2093 return True
2094 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2095 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2096 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2098 title = _('Quit gPodder')
2099 if downloading:
2100 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2101 else:
2102 message = _('Do you really want to quit gPodder now?')
2104 dialog.set_title(title)
2105 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2106 if not downloading:
2107 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2108 dialog.vbox.pack_start(cb_ask)
2109 cb_ask.show_all()
2111 quit_button.grab_focus()
2112 result = dialog.run()
2113 dialog.destroy()
2115 if result == gtk.RESPONSE_CLOSE:
2116 if not downloading and cb_ask.get_active() == True:
2117 self.config.on_quit_ask = False
2118 self.close_gpodder()
2119 else:
2120 self.close_gpodder()
2122 return True
2124 def close_gpodder(self):
2125 """ clean everything and exit properly
2127 if self.channels:
2128 if self.save_channels_opml():
2129 if self.config.my_gpodder_autoupload:
2130 log('Uploading to my.gpodder.org on close', sender=self)
2131 util.idle_add(self.on_upload_to_mygpo, None)
2132 else:
2133 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
2135 self.gPodder.hide()
2137 if self.tray_icon is not None:
2138 self.tray_icon.set_visible(False)
2140 # Notify all tasks to to carry out any clean-up actions
2141 self.download_status_model.tell_all_tasks_to_quit()
2143 while gtk.events_pending():
2144 gtk.main_iteration(False)
2146 self.db.close()
2148 self.quit()
2149 sys.exit(0)
2151 def get_old_episodes(self):
2152 episodes = []
2153 for channel in self.channels:
2154 for episode in channel.get_downloaded_episodes():
2155 if episode.age_in_days() > self.config.episode_old_age and \
2156 not episode.is_locked and episode.is_played:
2157 episodes.append(episode)
2158 return episodes
2160 def delete_episode_list(self, episodes, confirm=True):
2161 if not episodes:
2162 return
2164 count = len(episodes)
2166 if count == 1:
2167 episode = episodes[0]
2168 if episode.is_locked:
2169 title = _('%s is locked') % saxutils.escape(episode.title)
2170 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2171 self.notification(message, title, widget=self.treeAvailable)
2172 return
2174 title = _('Remove %s?') % saxutils.escape(episode.title)
2175 message = _("If you remove this episode, it will be deleted from your computer. If you want to listen to this episode again, you will have to re-download it.")
2176 else:
2177 title = _('Remove %d episodes?') % count
2178 message = _('If you remove these episodes, they will be deleted from your computer. If you want to listen to any of these episodes again, you will have to re-download the episodes in question.')
2180 locked_count = sum(int(e.is_locked) for e in episodes if e.is_locked is not None)
2182 if count == locked_count:
2183 title = _('Episodes are locked')
2184 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2185 self.notification(message, title, widget=self.treeAvailable)
2186 return
2187 elif locked_count > 0:
2188 title = _('Remove %d out of %d episodes?') % (count-locked_count, count)
2189 message = _('The selection contains locked episodes that will not be deleted. If you want to listen to the deleted episodes, you will have to re-download them.')
2191 if confirm and not self.show_confirmation(message, title):
2192 return
2194 episode_urls = set()
2195 channel_urls = set()
2196 for episode in episodes:
2197 if episode.is_locked:
2198 log('Not deleting episode (is locked): %s', episode.title)
2199 else:
2200 log('Deleting episode: %s', episode.title)
2201 episode.delete_from_disk()
2202 episode_urls.add(episode.url)
2203 channel_urls.add(episode.channel.url)
2205 # Tell the shownotes window that we have removed the episode
2206 if self.episode_shownotes_window is not None and \
2207 self.episode_shownotes_window.episode is not None and \
2208 self.episode_shownotes_window.episode.url == episode.url:
2209 self.episode_shownotes_window._download_status_changed(None)
2211 # Episodes have been deleted - persist the database
2212 self.db.commit()
2214 self.update_episode_list_icons(episode_urls)
2215 self.update_podcast_list_model(channel_urls)
2216 self.play_or_download()
2218 def on_itemRemoveOldEpisodes_activate( self, widget):
2219 if gpodder.ui.maemo:
2220 columns = (
2221 ('maemo_remove_markup', None, None, _('Episode')),
2223 else:
2224 columns = (
2225 ('title_markup', None, None, _('Episode')),
2226 ('channel_prop', None, None, _('Podcast')),
2227 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2228 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2229 ('played_prop', None, None, _('Status')),
2230 ('age_prop', None, None, _('Downloaded')),
2233 selection_buttons = {
2234 _('Select played'): lambda episode: episode.is_played,
2235 _('Select older than %d days') % self.config.episode_old_age: lambda episode: episode.age_in_days() > self.config.episode_old_age,
2238 instructions = _('Select the episodes you want to delete:')
2240 episodes = []
2241 selected = []
2242 for channel in self.channels:
2243 for episode in channel.get_downloaded_episodes():
2244 if not episode.is_locked:
2245 episodes.append(episode)
2246 selected.append(episode.is_played)
2248 gPodderEpisodeSelector(self.gPodder, title = _('Remove old episodes'), instructions = instructions, \
2249 episodes = episodes, selected = selected, columns = columns, \
2250 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2251 selection_buttons = selection_buttons, _config=self.config)
2253 def on_selected_episodes_status_changed(self):
2254 self.update_episode_list_icons(selected=True)
2255 self.update_podcast_list_model(selected=True)
2256 self.db.commit()
2258 def mark_selected_episodes_new(self):
2259 for episode in self.get_selected_episodes():
2260 episode.mark_new()
2261 self.on_selected_episodes_status_changed()
2263 def mark_selected_episodes_old(self):
2264 for episode in self.get_selected_episodes():
2265 episode.mark_old()
2266 self.on_selected_episodes_status_changed()
2268 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2269 for episode in self.get_selected_episodes():
2270 if toggle:
2271 episode.mark(is_played=not episode.is_played)
2272 else:
2273 episode.mark(is_played=new_value)
2274 self.on_selected_episodes_status_changed()
2276 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2277 for episode in self.get_selected_episodes():
2278 if toggle:
2279 episode.mark(is_locked=not episode.is_locked)
2280 else:
2281 episode.mark(is_locked=new_value)
2282 self.on_selected_episodes_status_changed()
2284 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2285 if self.active_channel is None:
2286 return
2288 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2289 self.active_channel.update_channel_lock()
2291 for episode in self.active_channel.get_all_episodes():
2292 episode.mark(is_locked=self.active_channel.channel_is_locked)
2294 self.update_podcast_list_model(selected=True)
2295 self.update_episode_list_icons(all=True)
2297 def send_subscriptions(self):
2298 try:
2299 subprocess.Popen(['xdg-email', '--subject', _('My podcast subscriptions'),
2300 '--attach', gpodder.subscription_file])
2301 except:
2302 return False
2304 return True
2306 def on_item_email_subscriptions_activate(self, widget):
2307 if not self.channels:
2308 self.show_message(_('Your subscription list is empty. Add some podcasts first.'), _('Could not send list'), widget=self.treeChannels)
2309 elif not self.send_subscriptions():
2310 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'), important=True)
2312 def on_itemUpdateChannel_activate(self, widget=None):
2313 if self.active_channel is None:
2314 title = _('No podcast selected')
2315 message = _('Please select a podcast in the podcasts list to update.')
2316 self.show_message( message, title, widget=self.treeChannels)
2317 return
2319 self.update_feed_cache(channels=[self.active_channel])
2321 def on_itemUpdate_activate(self, widget=None):
2322 if self.channels:
2323 self.update_feed_cache()
2324 else:
2325 gPodderWelcome(self.gPodder, center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
2327 def download_episode_list_paused(self, episodes):
2328 self.download_episode_list(episodes, True)
2330 def download_episode_list(self, episodes, add_paused=False):
2331 for episode in episodes:
2332 log('Downloading episode: %s', episode.title, sender = self)
2333 if not episode.was_downloaded(and_exists=True):
2334 task_exists = False
2335 for task in self.download_tasks_seen:
2336 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2337 self.download_queue_manager.add_task(task)
2338 self.enable_download_list_update()
2339 task_exists = True
2340 continue
2342 if task_exists:
2343 continue
2345 try:
2346 task = download.DownloadTask(episode, self.config)
2347 except Exception, e:
2348 self.show_message(_('Download error while downloading %s:\n\n%s') % (episode.title, str(e)), _('Download error'), important=True)
2349 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
2350 continue
2352 if add_paused:
2353 task.status = task.PAUSED
2354 else:
2355 self.download_queue_manager.add_task(task)
2357 self.download_status_model.register_task(task)
2358 self.enable_download_list_update()
2360 def cancel_task_list(self, tasks):
2361 if not tasks:
2362 return
2364 for task in tasks:
2365 if task.status in (task.QUEUED, task.DOWNLOADING):
2366 task.status = task.CANCELLED
2367 elif task.status == task.PAUSED:
2368 task.status = task.CANCELLED
2369 # Call run, so the partial file gets deleted
2370 task.run()
2372 self.update_episode_list_icons([task.url for task in tasks])
2373 self.play_or_download()
2375 # Update the tab title and downloads list
2376 self.update_downloads_list()
2378 def new_episodes_show(self, episodes, notification=False):
2379 if gpodder.ui.maemo:
2380 columns = (
2381 ('maemo_markup', None, None, _('Episode')),
2383 show_notification = notification
2384 else:
2385 columns = (
2386 ('title_markup', None, None, _('Episode')),
2387 ('channel_prop', None, None, _('Podcast')),
2388 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2389 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2391 show_notification = False
2393 instructions = _('Select the episodes you want to download:')
2395 gPodderEpisodeSelector(self.gPodder, title=_('New episodes available'), instructions=instructions, \
2396 episodes=episodes, columns=columns, selected_default=True, \
2397 stock_ok_button = 'gpodder-download', \
2398 callback=self.download_episode_list, \
2399 remove_callback=lambda e: e.mark_old(), \
2400 remove_action=_('Mark as old'), \
2401 remove_finished=self.episode_new_status_changed, \
2402 _config=self.config, \
2403 show_notification=show_notification)
2405 def on_itemDownloadAllNew_activate(self, widget, *args):
2406 if not self.offer_new_episodes():
2407 self.show_message(_('Please check for new episodes later.'), \
2408 _('No new episodes available'), widget=self.btnUpdateFeeds)
2410 def get_new_episodes(self, channels=None):
2411 if channels is None:
2412 channels = self.channels
2413 episodes = []
2414 for channel in channels:
2415 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
2416 episodes.append(episode)
2418 return episodes
2420 def on_sync_to_ipod_activate(self, widget, episodes=None):
2421 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
2422 # The sync process might have updated the status of episodes,
2423 # therefore persist the database here to avoid losing data
2424 self.db.commit()
2426 def on_cleanup_ipod_activate(self, widget, *args):
2427 self.sync_ui.on_cleanup_device()
2429 def on_manage_device_playlist(self, widget):
2430 self.sync_ui.on_manage_device_playlist()
2432 def show_hide_tray_icon(self):
2433 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2434 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
2435 elif not self.config.display_tray_icon and self.tray_icon is not None:
2436 self.tray_icon.set_visible(False)
2437 del self.tray_icon
2438 self.tray_icon = None
2440 if self.config.minimize_to_tray and self.tray_icon:
2441 self.tray_icon.set_visible(self.is_iconified())
2442 elif self.tray_icon:
2443 self.tray_icon.set_visible(True)
2445 def on_itemShowToolbar_activate(self, widget):
2446 self.config.show_toolbar = self.itemShowToolbar.get_active()
2448 def on_itemShowDescription_activate(self, widget):
2449 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
2451 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
2452 self.config.podcast_list_hide_boring = toggleaction.get_active()
2453 if self.config.podcast_list_hide_boring:
2454 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
2455 else:
2456 self.podcast_list_model.set_view_mode(-1)
2458 def on_item_view_podcasts_changed(self, radioaction, current):
2459 # Only on Fremantle
2460 if current == self.item_view_podcasts_all:
2461 self.podcast_list_model.set_view_mode(-1)
2462 elif current == self.item_view_podcasts_downloaded:
2463 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
2464 elif current == self.item_view_podcasts_unplayed:
2465 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
2467 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
2469 def on_item_view_episodes_changed(self, radioaction, current):
2470 if current == self.item_view_episodes_all:
2471 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_ALL)
2472 elif current == self.item_view_episodes_undeleted:
2473 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNDELETED)
2474 elif current == self.item_view_episodes_downloaded:
2475 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
2476 elif current == self.item_view_episodes_unplayed:
2477 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
2479 self.config.episode_list_view_mode = self.episode_list_model.get_view_mode()
2481 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
2482 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
2484 def update_item_device( self):
2485 if not gpodder.ui.fremantle:
2486 if self.config.device_type != 'none':
2487 self.itemDevice.set_visible(True)
2488 self.itemDevice.label = self.get_device_name()
2489 else:
2490 self.itemDevice.set_visible(False)
2492 def properties_closed( self):
2493 self.show_hide_tray_icon()
2494 self.update_item_device()
2495 if gpodder.ui.maemo:
2496 selection = self.treeAvailable.get_selection()
2497 if self.config.maemo_enable_gestures or \
2498 self.config.enable_fingerscroll:
2499 selection.set_mode(gtk.SELECTION_SINGLE)
2500 else:
2501 selection.set_mode(gtk.SELECTION_MULTIPLE)
2503 def on_itemPreferences_activate(self, widget, *args):
2504 gPodderPreferences(self.gPodder, _config=self.config, \
2505 callback_finished=self.properties_closed, \
2506 user_apps_reader=self.user_apps_reader)
2508 def on_itemDependencies_activate(self, widget):
2509 gPodderDependencyManager(self.gPodder)
2511 def require_my_gpodder_authentication(self):
2512 if not self.config.my_gpodder_username or not self.config.my_gpodder_password:
2513 success, authentication = self.show_login_dialog(_('Login to my.gpodder.org'), _('Please enter your e-mail address and your password.'), username=self.config.my_gpodder_username, password=self.config.my_gpodder_password, username_prompt=_('E-Mail Address'), register_callback=lambda: util.open_website('http://my.gpodder.org/register'))
2514 if success and authentication[0] and authentication[1]:
2515 self.config.my_gpodder_username, self.config.my_gpodder_password = authentication
2516 return True
2517 else:
2518 return False
2520 return True
2522 def my_gpodder_offer_autoupload(self):
2523 if not self.config.my_gpodder_autoupload:
2524 if self.show_confirmation(_('gPodder can automatically upload your subscription list to my.gpodder.org when you close it. Do you want to enable this feature?'), _('Upload subscriptions on quit')):
2525 self.config.my_gpodder_autoupload = True
2527 def on_download_from_mygpo(self, widget):
2528 if self.require_my_gpodder_authentication():
2529 client = my.MygPodderClient(self.config.my_gpodder_username, self.config.my_gpodder_password)
2530 opml_data = client.download_subscriptions()
2531 if len(opml_data) > 0:
2532 fp = open(gpodder.subscription_file, 'w')
2533 fp.write(opml_data)
2534 fp.close()
2535 (added, skipped) = (0, 0)
2536 i = opml.Importer(gpodder.subscription_file)
2538 existing = [c.url for c in self.channels]
2539 urls = [item['url'] for item in i.items if item['url'] not in existing]
2541 skipped = len(i.items) - len(urls)
2542 added = len(urls)
2544 self.add_podcast_list(urls)
2546 self.my_gpodder_offer_autoupload()
2547 if added > 0:
2548 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'), widget=self.treeChannels)
2549 elif widget is not None:
2550 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget=self.treeChannels)
2551 else:
2552 self.config.my_gpodder_password = ''
2553 self.on_download_from_mygpo(widget)
2554 else:
2555 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
2557 def on_upload_to_mygpo(self, widget):
2558 if self.require_my_gpodder_authentication():
2559 client = my.MygPodderClient(self.config.my_gpodder_username, self.config.my_gpodder_password)
2560 self.save_channels_opml()
2561 success, messages = client.upload_subscriptions(gpodder.subscription_file)
2562 if widget is not None:
2563 if not success:
2564 self.show_message('\n'.join(messages), _('Results of upload'), important=True)
2565 self.config.my_gpodder_password = ''
2566 self.on_upload_to_mygpo(widget)
2567 else:
2568 self.my_gpodder_offer_autoupload()
2569 self.show_message('\n'.join(messages), _('Results of upload'), widget=self.treeChannels)
2570 elif not success:
2571 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2572 elif widget is not None:
2573 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
2575 def on_itemAddChannel_activate(self, widget, *args):
2576 gPodderAddPodcast(self.gPodder, \
2577 add_urls_callback=self.add_podcast_list)
2579 def on_itemEditChannel_activate(self, widget, *args):
2580 if self.active_channel is None:
2581 title = _('No podcast selected')
2582 message = _('Please select a podcast in the podcasts list to edit.')
2583 self.show_message( message, title, widget=self.treeChannels)
2584 return
2586 callback_closed = lambda: self.update_podcast_list_model(selected=True)
2587 gPodderChannel(self.main_window, \
2588 channel=self.active_channel, \
2589 callback_closed=callback_closed, \
2590 cover_downloader=self.cover_downloader)
2592 def on_itemRemoveChannel_activate(self, widget, *args):
2593 if self.active_channel is None:
2594 title = _('No podcast selected')
2595 message = _('Please select a podcast in the podcasts list to remove.')
2596 self.show_message( message, title, widget=self.treeChannels)
2597 return
2599 try:
2600 if gpodder.ui.desktop:
2601 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2602 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2603 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2605 title = _('Remove podcast and episodes?')
2606 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2608 dialog.set_title(title)
2609 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2611 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2612 dialog.vbox.pack_start(cb_ask)
2613 cb_ask.show_all()
2614 result = (dialog.run() == gtk.RESPONSE_YES)
2615 keep_episodes = cb_ask.get_active()
2616 dialog.destroy()
2617 elif gpodder.ui.diablo:
2618 result = self.show_confirmation(_('Do you really want to remove this podcast and all downloaded episodes?'))
2619 keep_episodes = False
2620 elif gpodder.ui.fremantle:
2621 result = True
2622 keep_episodes = False
2624 if result:
2625 # delete downloaded episodes only if checkbox is unchecked
2626 if keep_episodes:
2627 log('Not removing downloaded episodes', sender=self)
2628 else:
2629 self.active_channel.remove_downloaded()
2631 # Clean up downloads and download directories
2632 self.clean_up_downloads()
2634 # cancel any active downloads from this channel
2635 for episode in self.active_channel.get_all_episodes():
2636 self.download_status_model.cancel_by_url(episode.url)
2638 # get the URL of the podcast we want to select next
2639 position = self.channels.index(self.active_channel)
2640 if position == len(self.channels)-1:
2641 # this is the last podcast, so select the URL
2642 # of the item before this one (i.e. the "new last")
2643 select_url = self.channels[position-1].url
2644 else:
2645 # there is a podcast after the deleted one, so
2646 # we simply select the one that comes after it
2647 select_url = self.channels[position+1].url
2649 title = self.active_channel.title
2651 # Remove the channel
2652 self.active_channel.delete(purge=not keep_episodes)
2653 self.channels.remove(self.active_channel)
2654 self.channel_list_changed = True
2655 self.save_channels_opml()
2657 if gpodder.ui.fremantle:
2658 self.show_message(_('Podcast removed: %s') % title)
2660 # Re-load the channels and select the desired new channel
2661 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2662 except:
2663 log('There has been an error removing the channel.', traceback=True, sender=self)
2664 self.update_podcasts_tab()
2666 def get_opml_filter(self):
2667 filter = gtk.FileFilter()
2668 filter.add_pattern('*.opml')
2669 filter.add_pattern('*.xml')
2670 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2671 return filter
2673 def on_item_import_from_file_activate(self, widget, filename=None):
2674 if filename is None:
2675 if gpodder.ui.desktop or gpodder.ui.fremantle:
2676 # FIXME: Hildonization on Fremantle
2677 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2678 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2679 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2680 elif gpodder.ui.diablo:
2681 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2682 dlg.set_filter(self.get_opml_filter())
2683 response = dlg.run()
2684 filename = None
2685 if response == gtk.RESPONSE_OK:
2686 filename = dlg.get_filename()
2687 dlg.destroy()
2689 if filename is not None:
2690 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
2691 custom_title=_('Import podcasts from OPML file'), \
2692 add_urls_callback=self.add_podcast_list, \
2693 hide_url_entry=True)
2694 dir.download_opml_file(filename)
2696 def on_itemExportChannels_activate(self, widget, *args):
2697 if not self.channels:
2698 title = _('Nothing to export')
2699 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2700 self.show_message(message, title, widget=self.treeChannels)
2701 return
2703 if gpodder.ui.desktop or gpodder.ui.fremantle:
2704 # FIXME: Hildonization on Fremantle
2705 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2706 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2707 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2708 elif gpodder.ui.diablo:
2709 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2710 dlg.set_filter(self.get_opml_filter())
2711 response = dlg.run()
2712 if response == gtk.RESPONSE_OK:
2713 filename = dlg.get_filename()
2714 dlg.destroy()
2715 exporter = opml.Exporter( filename)
2716 if exporter.write(self.channels):
2717 if len(self.channels) == 1:
2718 title = _('One subscription exported')
2719 else:
2720 title = _('%d subscriptions exported') % len(self.channels)
2721 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
2722 else:
2723 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
2724 else:
2725 dlg.destroy()
2727 def on_itemImportChannels_activate(self, widget, *args):
2728 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
2729 add_urls_callback=self.add_podcast_list)
2730 if not gpodder.ui.fremantle:
2731 util.idle_add(dir.download_opml_file, self.config.opml_url)
2733 def on_homepage_activate(self, widget, *args):
2734 util.open_website(gpodder.__url__)
2736 def on_wiki_activate(self, widget, *args):
2737 util.open_website('http://wiki.gpodder.org/')
2739 def on_bug_tracker_activate(self, widget, *args):
2740 if gpodder.ui.maemo:
2741 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
2742 else:
2743 util.open_website('http://bugs.gpodder.org/')
2745 def on_shop_activate(self, widget, *args):
2746 util.open_website('http://gpodder.org/shop')
2748 def on_wishlist_activate(self, widget, *args):
2749 util.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
2751 def on_itemAbout_activate(self, widget, *args):
2752 dlg = gtk.AboutDialog()
2753 dlg.set_name('gPodder')
2754 dlg.set_version(gpodder.__version__)
2755 dlg.set_copyright(gpodder.__copyright__)
2756 dlg.set_website(gpodder.__url__)
2757 dlg.set_translator_credits( _('translator-credits'))
2758 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2760 if gpodder.ui.desktop:
2761 # For the "GUI" version, we add some more
2762 # items to the about dialog (credits and logo)
2763 app_authors = [
2764 _('Maintainer:'),
2765 'Thomas Perl <thpinfo.com>',
2768 if os.path.exists(gpodder.credits_file):
2769 credits = open(gpodder.credits_file).read().strip().split('\n')
2770 app_authors += ['', _('Patches, bug reports and donations by:')]
2771 app_authors += credits
2773 dlg.set_authors(app_authors)
2774 try:
2775 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
2776 except:
2777 dlg.set_logo_icon_name('gpodder')
2778 elif gpodder.ui.fremantle:
2779 for parent in dlg.vbox.get_children():
2780 for child in parent.get_children():
2781 if isinstance(child, gtk.Label):
2782 child.set_selectable(False)
2784 dlg.run()
2786 def on_wNotebook_switch_page(self, widget, *args):
2787 page_num = args[1]
2788 if gpodder.ui.maemo:
2789 self.tool_downloads.set_active(page_num == 1)
2790 page = self.wNotebook.get_nth_page(page_num)
2791 tab_label = self.wNotebook.get_tab_label(page).get_text()
2792 if page_num == 0 and self.active_channel is not None:
2793 self.set_title(self.active_channel.title)
2794 else:
2795 self.set_title(tab_label)
2796 if page_num == 0:
2797 self.play_or_download()
2798 self.menuChannels.set_sensitive(True)
2799 self.menuSubscriptions.set_sensitive(True)
2800 # The message area in the downloads tab should be hidden
2801 # when the user switches away from the downloads tab
2802 if self.message_area is not None:
2803 self.message_area.hide()
2804 self.message_area = None
2805 else:
2806 self.menuChannels.set_sensitive(False)
2807 self.menuSubscriptions.set_sensitive(False)
2808 if gpodder.ui.desktop:
2809 self.toolDownload.set_sensitive(False)
2810 self.toolPlay.set_sensitive(False)
2811 self.toolTransfer.set_sensitive(False)
2812 self.toolCancel.set_sensitive(False)
2814 def on_treeChannels_row_activated(self, widget, path, *args):
2815 # double-click action of the podcast list or enter
2816 self.treeChannels.set_cursor(path)
2818 def on_treeChannels_cursor_changed(self, widget, *args):
2819 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2821 if model is not None and iter is not None:
2822 old_active_channel = self.active_channel
2823 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
2825 if self.active_channel == old_active_channel:
2826 return
2828 if gpodder.ui.maemo:
2829 self.set_title(self.active_channel.title)
2830 self.itemEditChannel.set_visible(True)
2831 self.itemRemoveChannel.set_visible(True)
2832 else:
2833 self.active_channel = None
2834 self.itemEditChannel.set_visible(False)
2835 self.itemRemoveChannel.set_visible(False)
2837 self.update_episode_list_model()
2839 def on_btnEditChannel_clicked(self, widget, *args):
2840 self.on_itemEditChannel_activate( widget, args)
2842 def get_selected_episodes(self):
2843 """Get a list of selected episodes from treeAvailable"""
2844 selection = self.treeAvailable.get_selection()
2845 model, paths = selection.get_selected_rows()
2847 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
2848 return episodes
2850 def on_transfer_selected_episodes(self, widget):
2851 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
2853 def on_playback_selected_episodes(self, widget):
2854 self.playback_episodes(self.get_selected_episodes())
2856 def on_shownotes_selected_episodes(self, widget):
2857 episodes = self.get_selected_episodes()
2858 if episodes:
2859 episode = episodes.pop(0)
2860 self.show_episode_shownotes(episode)
2861 else:
2862 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
2864 def on_download_selected_episodes(self, widget):
2865 episodes = self.get_selected_episodes()
2866 self.download_episode_list(episodes)
2867 self.update_episode_list_icons([episode.url for episode in episodes])
2868 self.play_or_download()
2870 def on_treeAvailable_row_activated(self, widget, path, view_column):
2871 """Double-click/enter action handler for treeAvailable"""
2872 # We should only have one one selected as it was double clicked!
2873 e = self.get_selected_episodes()[0]
2875 if (self.config.double_click_episode_action == 'download'):
2876 # If the episode has already been downloaded and exists then play it
2877 if e.was_downloaded(and_exists=True):
2878 self.playback_episodes(self.get_selected_episodes())
2879 # else download it if it is not already downloading
2880 elif not self.episode_is_downloading(e):
2881 self.download_episode_list([e])
2882 self.update_episode_list_icons([e.url])
2883 self.play_or_download()
2884 elif (self.config.double_click_episode_action == 'stream'):
2885 # If we happen to have downloaded this episode simple play it
2886 if e.was_downloaded(and_exists=True):
2887 self.playback_episodes(self.get_selected_episodes())
2888 # else if streaming is possible stream it
2889 elif self.streaming_possible():
2890 self.playback_episodes(self.get_selected_episodes())
2891 else:
2892 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
2893 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
2894 else:
2895 # default action is to display show notes
2896 self.on_shownotes_selected_episodes(widget)
2898 def show_episode_shownotes(self, episode):
2899 if self.episode_shownotes_window is None:
2900 log('First-time use of episode window --- creating', sender=self)
2901 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
2902 _download_episode_list=self.download_episode_list, \
2903 _playback_episodes=self.playback_episodes, \
2904 _delete_episode_list=self.delete_episode_list, \
2905 _episode_list_status_changed=self.episode_list_status_changed, \
2906 _cancel_task_list=self.cancel_task_list, \
2907 _episode_is_downloading=self.episode_is_downloading)
2908 self.episode_shownotes_window.show(episode)
2909 if self.episode_is_downloading(episode):
2910 self.update_downloads_list()
2912 def auto_update_procedure(self, first_run=False):
2913 log('auto_update_procedure() got called', sender=self)
2914 if not first_run and self.config.auto_update_feeds and self.is_iconified():
2915 self.update_feed_cache(force_update=True)
2917 next_update = 60*1000*self.config.auto_update_frequency
2918 gobject.timeout_add(next_update, self.auto_update_procedure)
2919 return False
2921 def on_treeDownloads_row_activated(self, widget, *args):
2922 # Use the standard way of working on the treeview
2923 selection = self.treeDownloads.get_selection()
2924 (model, paths) = selection.get_selected_rows()
2925 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
2927 for tree_row_reference, task in selected_tasks:
2928 if task.status in (task.DOWNLOADING, task.QUEUED):
2929 task.status = task.PAUSED
2930 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
2931 self.download_queue_manager.add_task(task)
2932 self.enable_download_list_update()
2933 elif task.status == task.DONE:
2934 model.remove(model.get_iter(tree_row_reference.get_path()))
2936 self.play_or_download()
2938 # Update the tab title and downloads list
2939 self.update_downloads_list()
2941 def on_item_cancel_download_activate(self, widget):
2942 if self.wNotebook.get_current_page() == 0:
2943 selection = self.treeAvailable.get_selection()
2944 (model, paths) = selection.get_selected_rows()
2945 urls = [model.get_value(model.get_iter(path), \
2946 self.episode_list_model.C_URL) for path in paths]
2947 selected_tasks = [task for task in self.download_tasks_seen \
2948 if task.url in urls]
2949 else:
2950 selection = self.treeDownloads.get_selection()
2951 (model, paths) = selection.get_selected_rows()
2952 selected_tasks = [model.get_value(model.get_iter(path), \
2953 self.download_status_model.C_TASK) for path in paths]
2954 self.cancel_task_list(selected_tasks)
2956 def on_btnCancelAll_clicked(self, widget, *args):
2957 self.cancel_task_list(self.download_tasks_seen)
2959 def on_btnDownloadedDelete_clicked(self, widget, *args):
2960 if self.wNotebook.get_current_page() == 1:
2961 # Downloads tab visibile - skip (for now)
2962 return
2964 episodes = self.get_selected_episodes()
2965 self.delete_episode_list(episodes)
2967 def on_key_press(self, widget, event):
2968 # Allow tab switching with Ctrl + PgUp/PgDown
2969 if event.state & gtk.gdk.CONTROL_MASK:
2970 if event.keyval == gtk.keysyms.Page_Up:
2971 self.wNotebook.prev_page()
2972 return True
2973 elif event.keyval == gtk.keysyms.Page_Down:
2974 self.wNotebook.next_page()
2975 return True
2977 # After this code we only handle Maemo hardware keys,
2978 # so if we are not a Maemo app, we don't do anything
2979 if not gpodder.ui.maemo:
2980 return False
2982 diff = 0
2983 if event.keyval == gtk.keysyms.F7: #plus
2984 diff = 1
2985 elif event.keyval == gtk.keysyms.F8: #minus
2986 diff = -1
2988 if diff != 0 and not self.currently_updating:
2989 selection = self.treeChannels.get_selection()
2990 (model, iter) = selection.get_selected()
2991 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
2992 selection.select_path(new_path)
2993 self.treeChannels.set_cursor(new_path)
2994 return True
2996 return False
2998 def on_iconify(self):
2999 if self.tray_icon:
3000 self.gPodder.set_skip_taskbar_hint(True)
3001 if self.config.minimize_to_tray:
3002 self.tray_icon.set_visible(True)
3003 else:
3004 self.gPodder.set_skip_taskbar_hint(False)
3006 def on_uniconify(self):
3007 if self.tray_icon:
3008 self.gPodder.set_skip_taskbar_hint(False)
3009 if self.config.minimize_to_tray:
3010 self.tray_icon.set_visible(False)
3011 else:
3012 self.gPodder.set_skip_taskbar_hint(False)
3014 def uniconify_main_window(self):
3015 if self.is_iconified():
3016 self.gPodder.present()
3018 def iconify_main_window(self):
3019 if not self.is_iconified():
3020 self.gPodder.iconify()
3022 def update_podcasts_tab(self):
3023 if len(self.channels):
3024 if gpodder.ui.fremantle:
3025 self.button_podcasts.set_value(_('%d subscriptions') % len(self.channels))
3026 else:
3027 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3028 else:
3029 if gpodder.ui.fremantle:
3030 self.button_podcasts.set_value(_('No subscriptions'))
3031 else:
3032 self.label2.set_text(_('Podcasts'))
3034 @dbus.service.method(gpodder.dbus_interface)
3035 def show_gui_window(self):
3036 self.gPodder.present()
3038 @dbus.service.method(gpodder.dbus_interface)
3039 def subscribe_to_url(self, url):
3040 gPodderAddPodcast(self.gPodder,
3041 add_urls_callback=self.add_podcast_list,
3042 preset_url=url)
3044 @dbus.service.method(gpodder.dbus_interface)
3045 def mark_episode_played(self, filename):
3046 if filename is None:
3047 return False
3049 for channel in self.channels:
3050 for episode in channel.get_all_episodes():
3051 fn = episode.local_filename(create=False, check_only=True)
3052 if fn == filename:
3053 episode.mark(is_played=True)
3054 self.db.commit()
3055 self.update_episode_list_icons([episode.url])
3056 self.update_podcast_list_model([episode.channel.url])
3057 return True
3059 return False
3062 def main(options=None):
3063 gobject.threads_init()
3064 gobject.set_application_name('gPodder')
3066 if gpodder.ui.diablo:
3067 # Try to enable the custom icon theme for gPodder on Maemo
3068 settings = gtk.settings_get_default()
3069 settings.set_string_property('gtk-icon-theme-name', \
3070 'gpodder', __file__)
3072 gtk.window_set_default_icon_name('gpodder')
3073 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
3075 try:
3076 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
3077 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
3078 except dbus.exceptions.DBusException, dbe:
3079 log('Warning: Cannot get "on the bus".', traceback=True)
3080 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
3081 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
3082 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
3083 dlg.set_title('gPodder')
3084 dlg.run()
3085 dlg.destroy()
3086 sys.exit(0)
3088 util.make_directory(gpodder.home)
3089 config = UIConfig(gpodder.config_file)
3091 if gpodder.ui.diablo:
3092 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3093 # folder exists there (allow moving "gpodder" between SD cards or USB)
3094 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3095 if not os.path.exists(config.download_dir):
3096 log('Downloads might have been moved. Trying to locate them...')
3097 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
3098 dir = os.path.join(basedir, 'gpodder')
3099 if os.path.exists(dir):
3100 log('Downloads found in: %s', dir)
3101 config.download_dir = dir
3102 break
3103 else:
3104 log('Downloads NOT FOUND in %s', dir)
3106 if config.enable_fingerscroll:
3107 BuilderWidget.use_fingerscroll = True
3108 elif gpodder.ui.fremantle:
3109 # FIXME: Move download_dir from ~/gPodder-Podcasts to default setting
3110 pass
3112 gp = gPodder(bus_name, config)
3114 # Handle options
3115 if options.subscribe:
3116 util.idle_add(gp.subscribe_to_url, options.subscribe)
3118 gp.run()