Portrait mode support for gPodder on Maemo 5
[gpodder.git] / src / gpodder / gui.py
blob5c19f12b01e6c083d478b48b48b82a650861e1ee
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 try:
101 from gpodder.gtkui.desktop.trayicon import GPodderStatusIcon
102 have_trayicon = True
103 except Exception, exc:
104 log('Warning: Could not import gpodder.trayicon.', traceback=True)
105 log('Warning: This probably means your PyGTK installation is too old!')
106 have_trayicon = False
107 from gpodder.gtkui.interface.dependencymanager import gPodderDependencyManager
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 FremantleAutoRotation
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.itemRemoveOldEpisodes, \
163 self.itemPreferences):
164 button = gtk.Button()
165 action.connect_proxy(button)
166 appmenu.append(button)
167 appmenu.show_all()
168 self.main_window.set_app_menu(appmenu)
169 self._fremantle_update_banner = None
171 # Activate application-wide automatic portrait orientation
172 FremantleAutoRotation()
174 self.bluetooth_available = False
175 else:
176 if gpodder.win32:
177 # FIXME: Implement e-mail sending of list in win32
178 self.item_email_subscriptions.set_sensitive(False)
179 self.bluetooth_available = util.bluetooth_available()
180 self.toolbar.set_property('visible', self.config.show_toolbar)
182 self.config.connect_gtk_window(self.gPodder, 'main_window')
183 if not gpodder.ui.fremantle:
184 self.config.connect_gtk_paned('paned_position', self.channelPaned)
185 self.main_window.show()
187 self.gPodder.connect('key-press-event', self.on_key_press)
189 self.config.add_observer(self.on_config_changed)
191 self.tray_icon = None
192 self.episode_shownotes_window = None
194 if gpodder.ui.desktop:
195 self.sync_ui = gPodderSyncUI(self.config, self.notification, \
196 self.main_window, self.show_confirmation, \
197 self.update_episode_list_icons, \
198 self.update_podcast_list_model, self.toolPreferences, \
199 gPodderEpisodeSelector)
200 else:
201 self.sync_ui = None
203 self.download_status_model = DownloadStatusModel()
204 self.download_queue_manager = download.DownloadQueueManager(self.config)
206 if gpodder.ui.desktop:
207 self.show_hide_tray_icon()
208 self.itemShowToolbar.set_active(self.config.show_toolbar)
209 self.itemShowDescription.set_active(self.config.episode_list_descriptions)
211 if not gpodder.ui.fremantle:
212 self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
213 self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
214 self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
215 self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
217 # When the amount of maximum downloads changes, notify the queue manager
218 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_and_retire_threads()
219 self.spinMaxDownloads.connect('value-changed', changed_cb)
221 self.default_title = 'gPodder'
222 if gpodder.__version__.rfind('git') != -1:
223 self.set_title('gPodder %s' % gpodder.__version__)
224 else:
225 title = self.gPodder.get_title()
226 if title is not None:
227 self.set_title(title)
228 else:
229 self.set_title(_('gPodder'))
231 self.cover_downloader = CoverDownloader()
233 # Generate list models for podcasts and their episodes
234 self.podcast_list_model = PodcastListModel(self.config.podcast_list_icon_size, self.cover_downloader)
236 self.cover_downloader.register('cover-available', self.cover_download_finished)
237 self.cover_downloader.register('cover-removed', self.cover_file_removed)
239 if gpodder.ui.fremantle:
240 self.button_subscribe.set_name('HildonButton-thumb')
241 self.button_podcasts.set_name('HildonButton-thumb')
242 self.button_downloads.set_name('HildonButton-thumb')
244 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
245 while gtk.events_pending():
246 gtk.main_iteration(False)
248 self.episodes_window = gPodderEpisodes(self.main_window, \
249 on_treeview_expose_event=self.on_treeview_expose_event, \
250 show_episode_shownotes=self.show_episode_shownotes, \
251 update_podcast_list_model=self.update_podcast_list_model, \
252 on_itemRemoveChannel_activate=self.on_itemRemoveChannel_activate, \
253 item_view_episodes_all=self.item_view_episodes_all, \
254 item_view_episodes_unplayed=self.item_view_episodes_unplayed, \
255 item_view_episodes_downloaded=self.item_view_episodes_downloaded, \
256 item_view_episodes_undeleted=self.item_view_episodes_undeleted)
258 def on_podcast_selected(channel):
259 self.active_channel = channel
260 self.update_episode_list_model()
261 self.episodes_window.channel = self.active_channel
262 self.episodes_window.show()
264 self.podcasts_window = gPodderPodcasts(self.main_window, \
265 show_podcast_episodes=on_podcast_selected, \
266 on_treeview_expose_event=self.on_treeview_expose_event, \
267 on_itemAddChannel_activate=self.on_itemAddChannel_activate, \
268 on_itemUpdate_activate=self.on_itemUpdate_activate, \
269 item_view_podcasts_all=self.item_view_podcasts_all, \
270 item_view_podcasts_downloaded=self.item_view_podcasts_downloaded, \
271 item_view_podcasts_unplayed=self.item_view_podcasts_unplayed)
273 self.downloads_window = gPodderDownloads(self.main_window, \
274 on_treeview_expose_event=self.on_treeview_expose_event, \
275 on_btnCleanUpDownloads_clicked=self.on_btnCleanUpDownloads_clicked)
276 self.treeChannels = self.podcasts_window.treeview
277 self.treeAvailable = self.episodes_window.treeview
278 self.treeDownloads = self.downloads_window.treeview
280 # Init the treeviews that we use
281 self.init_podcast_list_treeview()
282 self.init_episode_list_treeview()
283 self.init_download_list_treeview()
285 if self.config.podcast_list_hide_boring:
286 self.item_view_hide_boring_podcasts.set_active(True)
288 self.currently_updating = False
290 if gpodder.ui.maemo:
291 self.context_menu_mouse_button = 1
292 else:
293 self.context_menu_mouse_button = 3
295 if self.config.start_iconified:
296 self.iconify_main_window()
298 self.download_tasks_seen = set()
299 self.download_list_update_enabled = False
300 self.last_download_count = 0
302 # Subscribed channels
303 self.active_channel = None
304 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
305 self.channel_list_changed = True
306 self.update_podcasts_tab()
308 # load list of user applications for audio playback
309 self.user_apps_reader = UserAppsReader(['audio', 'video'])
310 def read_apps():
311 time.sleep(3) # give other parts of gpodder a chance to start up
312 self.user_apps_reader.read()
313 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
314 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
315 threading.Thread(target=read_apps).start()
317 # Set the "Device" menu item for the first time
318 if gpodder.ui.desktop:
319 self.update_item_device()
321 # Now, update the feed cache, when everything's in place
322 if not gpodder.ui.fremantle:
323 self.btnUpdateFeeds.show()
324 self.updating_feed_cache = False
325 self.feed_cache_update_cancelled = False
326 self.update_feed_cache(force_update=self.config.update_on_startup)
328 # Look for partial file downloads
329 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
331 # Message area
332 self.message_area = None
334 resumable_episodes = []
335 if len(partial_files) > 0:
336 for f in partial_files:
337 correct_name = f[:-len('.partial')] # strip ".partial"
338 log('Searching episode for file: %s', correct_name, sender=self)
339 found_episode = False
340 for c in self.channels:
341 for e in c.get_all_episodes():
342 if e.local_filename(create=False, check_only=True) == correct_name:
343 log('Found episode: %s', e.title, sender=self)
344 resumable_episodes.append(e)
345 found_episode = True
346 if found_episode:
347 break
348 if found_episode:
349 break
350 if not found_episode:
351 log('Partial file without episode: %s', f, sender=self)
352 util.delete_file(f)
354 if len(resumable_episodes):
355 self.download_episode_list_paused(resumable_episodes)
356 if not gpodder.ui.fremantle:
357 self.message_area = SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
358 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
359 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
360 self.message_area.show_all()
361 self.wNotebook.set_current_page(1)
363 self.clean_up_downloads(delete_partial=False)
364 else:
365 self.clean_up_downloads(delete_partial=True)
367 # Start the auto-update procedure
368 self.auto_update_procedure(first_run=True)
370 # Delete old episodes if the user wishes to
371 if self.config.auto_remove_old_episodes:
372 old_episodes = self.get_old_episodes()
373 if len(old_episodes) > 0:
374 self.delete_episode_list(old_episodes, confirm=False)
375 self.update_podcast_list_model(set(e.channel.url for e in old_episodes))
377 if gpodder.ui.fremantle:
378 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
379 self.button_subscribe.set_sensitive(True)
380 self.button_podcasts.set_sensitive(True)
381 self.button_downloads.set_sensitive(True)
383 # First-time users should be asked if they want to see the OPML
384 if not self.channels and not gpodder.ui.fremantle:
385 util.idle_add(self.on_itemUpdate_activate)
387 def on_button_subscribe_clicked(self, button):
388 self.on_itemImportChannels_activate(button)
390 def on_button_podcasts_clicked(self, widget):
391 if self.channels:
392 self.podcasts_window.show()
393 else:
394 gPodderWelcome(self.gPodder, \
395 show_example_podcasts_callback=self.on_itemImportChannels_activate, \
396 setup_my_gpodder_callback=self.on_download_from_mygpo)
398 def on_button_downloads_clicked(self, widget):
399 self.downloads_window.show()
401 def on_window_orientation_changed(self, orientation):
402 old_container = self.main_window.get_child()
403 if orientation == Orientation.PORTRAIT:
404 container = gtk.VButtonBox()
405 else:
406 container = gtk.HButtonBox()
407 container.set_layout(old_container.get_layout())
408 for child in old_container.get_children():
409 if orientation == Orientation.LANDSCAPE:
410 child.set_alignment(0.5, 0.5, 0., 0.)
411 child.set_property('width-request', 200)
412 else:
413 child.set_alignment(0.5, 0.5, .9, 0.)
414 child.set_property('width-request', 350)
415 child.reparent(container)
416 container.show_all()
417 self.buttonbox = container
418 self.main_window.remove(old_container)
419 self.main_window.add(container)
421 def on_treeview_podcasts_selection_changed(self, selection):
422 model, iter = selection.get_selected()
423 if iter is None:
424 self.active_channel = None
425 self.episode_list_model.clear()
427 def on_treeview_button_pressed(self, treeview, event):
428 TreeViewHelper.save_button_press_event(treeview, event)
430 if getattr(treeview, TreeViewHelper.ROLE) == \
431 TreeViewHelper.ROLE_PODCASTS:
432 return self.currently_updating
434 return event.button == self.context_menu_mouse_button and \
435 gpodder.ui.desktop
437 def on_treeview_podcasts_button_released(self, treeview, event):
438 if gpodder.ui.maemo:
439 return self.treeview_channels_handle_gestures(treeview, event)
441 return self.treeview_channels_show_context_menu(treeview, event)
443 def on_treeview_episodes_button_released(self, treeview, event):
444 if gpodder.ui.maemo:
445 if self.config.enable_fingerscroll or self.config.maemo_enable_gestures:
446 return self.treeview_available_handle_gestures(treeview, event)
448 return self.treeview_available_show_context_menu(treeview, event)
450 def on_treeview_downloads_button_released(self, treeview, event):
451 return self.treeview_downloads_show_context_menu(treeview, event)
453 def init_podcast_list_treeview(self):
454 # Set up podcast channel tree view widget
455 self.treeChannels.set_search_equal_func(TreeViewHelper.make_search_equal_func(PodcastListModel))
457 if gpodder.ui.fremantle:
458 if self.config.podcast_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
459 self.item_view_podcasts_downloaded.set_active(True)
460 elif self.config.podcast_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
461 self.item_view_podcasts_unplayed.set_active(True)
462 else:
463 self.item_view_podcasts_all.set_active(True)
464 self.podcast_list_model.set_view_mode(self.config.podcast_list_view_mode)
466 iconcolumn = gtk.TreeViewColumn('')
467 iconcell = gtk.CellRendererPixbuf()
468 iconcolumn.pack_start(iconcell, False)
469 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
470 self.treeChannels.append_column(iconcolumn)
472 namecolumn = gtk.TreeViewColumn('')
473 namecell = gtk.CellRendererText()
474 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
475 namecolumn.pack_start(namecell, True)
476 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
478 iconcell = gtk.CellRendererPixbuf()
479 iconcell.set_property('xalign', 1.0)
480 namecolumn.pack_start(iconcell, False)
481 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
482 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
483 self.treeChannels.append_column(namecolumn)
485 self.treeChannels.set_model(self.podcast_list_model.get_filtered_model())
487 # When no podcast is selected, clear the episode list model
488 selection = self.treeChannels.get_selection()
489 selection.connect('changed', self.on_treeview_podcasts_selection_changed)
491 TreeViewHelper.set(self.treeChannels, TreeViewHelper.ROLE_PODCASTS)
493 def init_episode_list_treeview(self):
494 self.episode_list_model = EpisodeListModel()
496 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
497 self.item_view_episodes_undeleted.set_active(True)
498 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
499 self.item_view_episodes_downloaded.set_active(True)
500 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNPLAYED:
501 self.item_view_episodes_unplayed.set_active(True)
502 else:
503 self.item_view_episodes_all.set_active(True)
505 self.episode_list_model.set_view_mode(self.config.episode_list_view_mode)
507 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
509 TreeViewHelper.set(self.treeAvailable, TreeViewHelper.ROLE_EPISODES)
511 iconcell = gtk.CellRendererPixbuf()
512 if gpodder.ui.maemo:
513 iconcell.set_fixed_size(50, 50)
514 status_column_label = ''
515 else:
516 status_column_label = _('Status')
517 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=EpisodeListModel.C_STATUS_ICON)
519 namecell = gtk.CellRendererText()
520 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
521 namecolumn = gtk.TreeViewColumn(_('Episode'), namecell, markup=EpisodeListModel.C_DESCRIPTION)
522 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
523 namecolumn.set_resizable(True)
524 namecolumn.set_expand(True)
526 sizecell = gtk.CellRendererText()
527 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
529 releasecell = gtk.CellRendererText()
530 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
532 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
533 itemcolumn.set_reorderable(True)
534 self.treeAvailable.append_column(itemcolumn)
536 if gpodder.ui.maemo:
537 sizecolumn.set_visible(False)
538 releasecolumn.set_visible(False)
540 self.treeAvailable.set_search_equal_func(TreeViewHelper.make_search_equal_func(EpisodeListModel))
542 selection = self.treeAvailable.get_selection()
543 if gpodder.ui.diablo:
544 if self.config.maemo_enable_gestures or self.config.enable_fingerscroll:
545 selection.set_mode(gtk.SELECTION_SINGLE)
546 else:
547 selection.set_mode(gtk.SELECTION_MULTIPLE)
548 elif gpodder.ui.fremantle:
549 selection.set_mode(gtk.SELECTION_SINGLE)
550 else:
551 selection.set_mode(gtk.SELECTION_MULTIPLE)
552 # Update the sensitivity of the toolbar buttons on the Desktop
553 selection.connect('changed', lambda s: self.play_or_download())
555 if gpodder.ui.diablo:
556 # Set up the tap-and-hold context menu for podcasts
557 menu = gtk.Menu()
558 menu.append(self.itemUpdateChannel.create_menu_item())
559 menu.append(self.itemEditChannel.create_menu_item())
560 menu.append(gtk.SeparatorMenuItem())
561 menu.append(self.itemRemoveChannel.create_menu_item())
562 menu.append(gtk.SeparatorMenuItem())
563 item = gtk.ImageMenuItem(_('Close this menu'))
564 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, \
565 gtk.ICON_SIZE_MENU))
566 menu.append(item)
567 menu.show_all()
568 menu = self.set_finger_friendly(menu)
569 self.treeChannels.tap_and_hold_setup(menu)
572 def init_download_list_treeview(self):
573 # enable multiple selection support
574 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
575 self.treeDownloads.set_search_equal_func(TreeViewHelper.make_search_equal_func(DownloadStatusModel))
577 # columns and renderers for "download progress" tab
578 # First column: [ICON] Episodename
579 column = gtk.TreeViewColumn(_('Episode'))
581 cell = gtk.CellRendererPixbuf()
582 if gpodder.ui.maemo:
583 cell.set_fixed_size(50, 50)
584 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
585 column.pack_start(cell, expand=False)
586 column.add_attribute(cell, 'stock-id', \
587 DownloadStatusModel.C_ICON_NAME)
589 cell = gtk.CellRendererText()
590 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
591 column.pack_start(cell, expand=True)
592 column.add_attribute(cell, 'text', DownloadStatusModel.C_NAME)
594 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
595 column.set_resizable(True)
596 column.set_expand(True)
597 self.treeDownloads.append_column(column)
599 # Second column: Progress
600 column = gtk.TreeViewColumn(_('Progress'), gtk.CellRendererProgress(),
601 value=DownloadStatusModel.C_PROGRESS, \
602 text=DownloadStatusModel.C_PROGRESS_TEXT)
603 self.treeDownloads.append_column(column)
605 # Third column: Size
606 if gpodder.ui.desktop:
607 column = gtk.TreeViewColumn(_('Size'), gtk.CellRendererText(),
608 text=DownloadStatusModel.C_SIZE_TEXT)
609 self.treeDownloads.append_column(column)
611 # Fourth column: Speed
612 column = gtk.TreeViewColumn(_('Speed'), gtk.CellRendererText(),
613 text=DownloadStatusModel.C_SPEED_TEXT)
614 self.treeDownloads.append_column(column)
616 if not gpodder.ui.fremantle:
617 # Fifth column: Status
618 column = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(),
619 text=DownloadStatusModel.C_STATUS_TEXT)
620 self.treeDownloads.append_column(column)
622 self.treeDownloads.set_model(self.download_status_model)
623 TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
625 def on_treeview_expose_event(self, treeview, event):
626 if event.window == treeview.get_bin_window():
627 model = treeview.get_model()
628 if (model is not None and model.get_iter_first() is not None):
629 return False
631 role = getattr(treeview, TreeViewHelper.ROLE)
632 ctx = event.window.cairo_create()
633 ctx.rectangle(event.area.x, event.area.y,
634 event.area.width, event.area.height)
635 ctx.clip()
637 x, y, width, height, depth = event.window.get_geometry()
639 if role == TreeViewHelper.ROLE_EPISODES:
640 if self.currently_updating:
641 text = _('Loading episodes') + '...'
642 elif self.config.episode_list_view_mode != \
643 EpisodeListModel.VIEW_ALL:
644 text = _('No episodes in current view')
645 else:
646 text = _('No episodes available')
647 elif role == TreeViewHelper.ROLE_PODCASTS:
648 if self.config.episode_list_view_mode != \
649 EpisodeListModel.VIEW_ALL and \
650 self.config.podcast_list_hide_boring and \
651 len(self.channels) > 0:
652 text = _('No podcasts in this view')
653 else:
654 text = _('No subscriptions')
655 elif role == TreeViewHelper.ROLE_DOWNLOADS:
656 text = _('No active downloads')
657 else:
658 raise Exception('on_treeview_expose_event: unknown role')
660 if gpodder.ui.fremantle:
661 from gpodder.gtkui.frmntl import style
662 font_desc = style.get_font_desc('LargeSystemFont')
663 else:
664 font_desc = None
666 draw_text_box_centered(ctx, treeview, width, height, text, font_desc)
668 return False
670 def enable_download_list_update(self):
671 if not self.download_list_update_enabled:
672 gobject.timeout_add(1500, self.update_downloads_list)
673 self.download_list_update_enabled = True
675 def on_btnCleanUpDownloads_clicked(self, button):
676 model = self.download_status_model
678 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
679 changed_episode_urls = []
680 for row_reference, task in all_tasks:
681 if task.status in (task.DONE, task.CANCELLED, task.FAILED):
682 model.remove(model.get_iter(row_reference.get_path()))
683 try:
684 # We don't "see" this task anymore - remove it;
685 # this is needed, so update_episode_list_icons()
686 # below gets the correct list of "seen" tasks
687 self.download_tasks_seen.remove(task)
688 except KeyError, key_error:
689 log('Cannot remove task from "seen" list: %s', task, sender=self)
690 changed_episode_urls.append(task.url)
691 # Tell the task that it has been removed (so it can clean up)
692 task.removed_from_list()
694 # Tell the podcasts tab to update icons for our removed podcasts
695 self.update_episode_list_icons(changed_episode_urls)
697 # Tell the shownotes window that we have removed the episode
698 if self.episode_shownotes_window is not None and \
699 self.episode_shownotes_window.episode is not None and \
700 self.episode_shownotes_window.episode.url in changed_episode_urls:
701 self.episode_shownotes_window._download_status_changed(None)
703 # Update the tab title and downloads list
704 self.update_downloads_list()
706 def on_tool_downloads_toggled(self, toolbutton):
707 if toolbutton.get_active():
708 self.wNotebook.set_current_page(1)
709 else:
710 self.wNotebook.set_current_page(0)
712 def update_downloads_list(self):
713 try:
714 model = self.download_status_model
716 downloading, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0
717 total_speed, total_size, done_size = 0, 0, 0
719 # Keep a list of all download tasks that we've seen
720 download_tasks_seen = set()
722 # Remember the DownloadTask object for the episode that
723 # has been opened in the episode shownotes dialog (if any)
724 if self.episode_shownotes_window is not None:
725 shownotes_episode = self.episode_shownotes_window.episode
726 shownotes_task = None
727 else:
728 shownotes_episode = None
729 shownotes_task = None
731 # Do not go through the list of the model is not (yet) available
732 if model is None:
733 model = ()
735 for row in model:
736 self.download_status_model.request_update(row.iter)
738 task = row[self.download_status_model.C_TASK]
739 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
741 total_size += size
742 done_size += size*progress
744 if shownotes_episode is not None and \
745 shownotes_episode.url == task.episode.url:
746 shownotes_task = task
748 download_tasks_seen.add(task)
750 if status == download.DownloadTask.DOWNLOADING:
751 downloading += 1
752 total_speed += speed
753 elif status == download.DownloadTask.FAILED:
754 failed += 1
755 elif status == download.DownloadTask.DONE:
756 finished += 1
757 elif status == download.DownloadTask.QUEUED:
758 queued += 1
759 elif status == download.DownloadTask.PAUSED:
760 paused += 1
761 else:
762 others += 1
764 # Remember which tasks we have seen after this run
765 self.download_tasks_seen = download_tasks_seen
767 if gpodder.ui.desktop:
768 text = [_('Downloads')]
769 if downloading + failed + finished + queued > 0:
770 s = []
771 if downloading > 0:
772 s.append(_('%d active') % downloading)
773 if failed > 0:
774 s.append(_('%d failed') % failed)
775 if finished > 0:
776 s.append(_('%d done') % finished)
777 if queued > 0:
778 s.append(_('%d queued') % queued)
779 text.append(' (' + ', '.join(s)+')')
780 self.labelDownloads.set_text(''.join(text))
781 elif gpodder.ui.diablo:
782 sum = downloading + failed + finished + queued + paused + others
783 if sum:
784 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
785 else:
786 self.tool_downloads.set_label(_('Downloads'))
787 elif gpodder.ui.fremantle:
788 if downloading + queued > 0:
789 self.button_downloads.set_value(_('%d active') % (downloading+queued))
790 elif failed > 0:
791 self.button_downloads.set_value(_('%d failed') % failed)
792 elif paused > 0:
793 self.button_downloads.set_value(_('%d paused') % paused)
794 else:
795 self.button_downloads.set_value(_('None active'))
797 title = [self.default_title]
799 # We have to update all episodes/channels for which the status has
800 # changed. Accessing task.status_changed has the side effect of
801 # re-setting the changed flag, so we need to get the "changed" list
802 # of tuples first and split it into two lists afterwards
803 changed = [(task.url, task.podcast_url) for task in \
804 self.download_tasks_seen if task.status_changed]
805 episode_urls = [episode_url for episode_url, channel_url in changed]
806 channel_urls = [channel_url for episode_url, channel_url in changed]
808 count = downloading + queued
809 if count > 0:
810 if count == 1:
811 title.append( _('downloading one file'))
812 elif count > 1:
813 title.append( _('downloading %d files') % count)
815 if total_size > 0:
816 percentage = 100.0*done_size/total_size
817 else:
818 percentage = 0.0
819 total_speed = util.format_filesize(total_speed)
820 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
821 if self.tray_icon is not None:
822 # Update the tray icon status and progress bar
823 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
824 self.tray_icon.draw_progress_bar(percentage/100.)
825 elif self.last_download_count > 0:
826 if self.tray_icon is not None:
827 # Update the tray icon status
828 self.tray_icon.set_status()
829 self.tray_icon.downloads_finished(self.download_tasks_seen)
830 if gpodder.ui.diablo:
831 hildon.hildon_banner_show_information(self.gPodder, None, 'gPodder: %s' % _('All downloads finished'))
832 log('All downloads have finished.', sender=self)
833 if self.config.cmd_all_downloads_complete:
834 util.run_external_command(self.config.cmd_all_downloads_complete)
835 self.last_download_count = count
837 self.gPodder.set_title(' - '.join(title))
839 self.update_episode_list_icons(episode_urls)
840 if self.episode_shownotes_window is not None:
841 if (shownotes_task and shownotes_task.url in episode_urls) or \
842 shownotes_task != self.episode_shownotes_window.task:
843 self.episode_shownotes_window._download_status_changed(shownotes_task)
844 self.episode_shownotes_window._download_status_progress()
845 self.play_or_download()
846 if channel_urls:
847 self.update_podcast_list_model(channel_urls)
849 if not self.download_queue_manager.are_queued_or_active_tasks():
850 self.download_list_update_enabled = False
852 return self.download_list_update_enabled
853 except Exception, e:
854 log('Exception happened while updating download list.', sender=self, traceback=True)
855 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
856 # We return False here, so the update loop won't be called again,
857 # that's why we require the restart of gPodder in the message.
858 return False
860 def on_config_changed(self, name, old_value, new_value):
861 if name == 'show_toolbar' and gpodder.ui.desktop:
862 self.toolbar.set_property('visible', new_value)
863 elif name == 'episode_list_descriptions':
864 self.update_episode_list_model()
866 def on_treeview_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
867 # With get_bin_window, we get the window that contains the rows without
868 # the header. The Y coordinate of this window will be the height of the
869 # treeview header. This is the amount we have to subtract from the
870 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
871 (x_bin, y_bin) = treeview.get_bin_window().get_position()
872 y -= x_bin
873 y -= y_bin
874 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
876 if not getattr(treeview, TreeViewHelper.CAN_TOOLTIP) or (column is not None and column != treeview.get_columns()[0]):
877 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
878 return False
880 if path is not None:
881 model = treeview.get_model()
882 iter = model.get_iter(path)
883 role = getattr(treeview, TreeViewHelper.ROLE)
885 if role == TreeViewHelper.ROLE_EPISODES:
886 id = model.get_value(iter, EpisodeListModel.C_URL)
887 elif role == TreeViewHelper.ROLE_PODCASTS:
888 id = model.get_value(iter, PodcastListModel.C_URL)
890 last_tooltip = getattr(treeview, TreeViewHelper.LAST_TOOLTIP)
891 if last_tooltip is not None and last_tooltip != id:
892 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
893 return False
894 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, id)
896 if role == TreeViewHelper.ROLE_EPISODES:
897 description = model.get_value(iter, EpisodeListModel.C_DESCRIPTION_STRIPPED)
898 if len(description) > 400:
899 description = description[:398]+'[...]'
901 tooltip.set_text(description)
902 elif role == TreeViewHelper.ROLE_PODCASTS:
903 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
904 channel.request_save_dir_size()
905 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
906 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
907 if error_str:
908 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
909 error_str = '<span foreground="#ff0000">%s</span>' % error_str
910 table = gtk.Table(rows=3, columns=3)
911 table.set_row_spacings(5)
912 table.set_col_spacings(5)
913 table.set_border_width(5)
915 heading = gtk.Label()
916 heading.set_alignment(0, 1)
917 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
918 table.attach(heading, 0, 1, 0, 1)
919 size_info = gtk.Label()
920 size_info.set_alignment(1, 1)
921 size_info.set_justify(gtk.JUSTIFY_RIGHT)
922 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
923 table.attach(size_info, 2, 3, 0, 1)
925 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
927 if len(channel.description) < 500:
928 description = channel.description
929 else:
930 pos = channel.description.find('\n\n')
931 if pos == -1 or pos > 500:
932 description = channel.description[:498]+'[...]'
933 else:
934 description = channel.description[:pos]
936 description = gtk.Label(description)
937 if error_str:
938 description.set_markup(error_str)
939 description.set_alignment(0, 0)
940 description.set_line_wrap(True)
941 table.attach(description, 0, 3, 2, 3)
943 table.show_all()
944 tooltip.set_custom(table)
946 return True
948 setattr(treeview, TreeViewHelper.LAST_TOOLTIP, None)
949 return False
951 def treeview_allow_tooltips(self, treeview, allow):
952 setattr(treeview, TreeViewHelper.CAN_TOOLTIP, allow)
954 def update_m3u_playlist_clicked(self, widget):
955 if self.active_channel is not None:
956 self.active_channel.update_m3u_playlist()
957 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
959 def treeview_handle_context_menu_click(self, treeview, event):
960 x, y = int(event.x), int(event.y)
961 path, column, rx, ry = treeview.get_path_at_pos(x, y) or (None,)*4
963 selection = treeview.get_selection()
964 model, paths = selection.get_selected_rows()
966 if path is None or (path not in paths and \
967 event.button == self.context_menu_mouse_button):
968 # We have right-clicked, but not into the selection,
969 # assume we don't want to operate on the selection
970 paths = []
972 if path is not None and not paths and \
973 event.button == self.context_menu_mouse_button:
974 # No selection or clicked outside selection;
975 # select the single item where we clicked
976 treeview.grab_focus()
977 treeview.set_cursor(path, column, 0)
978 paths = [path]
980 if not paths:
981 # Unselect any remaining items (clicked elsewhere)
982 if hasattr(treeview, 'is_rubber_banding_active'):
983 if not treeview.is_rubber_banding_active():
984 selection.unselect_all()
985 else:
986 selection.unselect_all()
988 return model, paths
990 def treeview_downloads_show_context_menu(self, treeview, event):
991 model, paths = self.treeview_handle_context_menu_click(treeview, event)
992 if not paths:
993 if not hasattr(treeview, 'is_rubber_banding_active'):
994 return True
995 else:
996 return not treeview.is_rubber_banding_active()
998 if event.button == self.context_menu_mouse_button:
999 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
1001 def make_menu_item(label, stock_id, tasks, status):
1002 # This creates a menu item for selection-wide actions
1003 def for_each_task_set_status(tasks, status):
1004 changed_episode_urls = []
1005 for row_reference, task in tasks:
1006 if status is not None:
1007 if status == download.DownloadTask.QUEUED:
1008 # Only queue task when its paused/failed/cancelled
1009 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED):
1010 self.download_queue_manager.add_task(task)
1011 self.enable_download_list_update()
1012 elif status == download.DownloadTask.CANCELLED:
1013 # Cancelling a download allowed when downloading/queued
1014 if task.status in (task.QUEUED, task.DOWNLOADING):
1015 task.status = status
1016 # Cancelling paused downloads requires a call to .run()
1017 elif task.status == task.PAUSED:
1018 task.status = status
1019 # Call run, so the partial file gets deleted
1020 task.run()
1021 elif status == download.DownloadTask.PAUSED:
1022 # Pausing a download only when queued/downloading
1023 if task.status in (task.DOWNLOADING, task.QUEUED):
1024 task.status = status
1025 else:
1026 # We (hopefully) can simply set the task status here
1027 task.status = status
1028 else:
1029 # Remove the selected task - cancel downloading/queued tasks
1030 if task.status in (task.QUEUED, task.DOWNLOADING):
1031 task.status = task.CANCELLED
1032 model.remove(model.get_iter(row_reference.get_path()))
1033 # Remember the URL, so we can tell the UI to update
1034 try:
1035 # We don't "see" this task anymore - remove it;
1036 # this is needed, so update_episode_list_icons()
1037 # below gets the correct list of "seen" tasks
1038 self.download_tasks_seen.remove(task)
1039 except KeyError, key_error:
1040 log('Cannot remove task from "seen" list: %s', task, sender=self)
1041 changed_episode_urls.append(task.url)
1042 # Tell the task that it has been removed (so it can clean up)
1043 task.removed_from_list()
1044 # Tell the podcasts tab to update icons for our removed podcasts
1045 self.update_episode_list_icons(changed_episode_urls)
1046 # Update the tab title and downloads list
1047 self.update_downloads_list()
1048 return True
1049 item = gtk.ImageMenuItem(label)
1050 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
1051 item.connect('activate', lambda item: for_each_task_set_status(tasks, status))
1053 # Determine if we should disable this menu item
1054 for row_reference, task in tasks:
1055 if status == download.DownloadTask.QUEUED:
1056 if task.status not in (download.DownloadTask.PAUSED, \
1057 download.DownloadTask.FAILED, \
1058 download.DownloadTask.CANCELLED):
1059 item.set_sensitive(False)
1060 break
1061 elif status == download.DownloadTask.CANCELLED:
1062 if task.status not in (download.DownloadTask.PAUSED, \
1063 download.DownloadTask.QUEUED, \
1064 download.DownloadTask.DOWNLOADING):
1065 item.set_sensitive(False)
1066 break
1067 elif status == download.DownloadTask.PAUSED:
1068 if task.status not in (download.DownloadTask.QUEUED, \
1069 download.DownloadTask.DOWNLOADING):
1070 item.set_sensitive(False)
1071 break
1072 elif status is None:
1073 if task.status not in (download.DownloadTask.CANCELLED, \
1074 download.DownloadTask.FAILED, \
1075 download.DownloadTask.DONE):
1076 item.set_sensitive(False)
1077 break
1079 return self.set_finger_friendly(item)
1081 menu = gtk.Menu()
1083 item = gtk.ImageMenuItem(_('Episode details'))
1084 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1085 if len(selected_tasks) == 1:
1086 row_reference, task = selected_tasks[0]
1087 episode = task.episode
1088 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
1089 else:
1090 item.set_sensitive(False)
1091 menu.append(self.set_finger_friendly(item))
1092 menu.append(gtk.SeparatorMenuItem())
1093 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED))
1094 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED))
1095 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED))
1096 menu.append(gtk.SeparatorMenuItem())
1097 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None))
1099 if gpodder.ui.maemo:
1100 # Because we open the popup on left-click for Maemo,
1101 # we also include a non-action to close the menu
1102 menu.append(gtk.SeparatorMenuItem())
1103 item = gtk.ImageMenuItem(_('Close this menu'))
1104 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1106 menu.append(self.set_finger_friendly(item))
1108 menu.show_all()
1109 menu.popup(None, None, None, event.button, event.time)
1110 return True
1112 def treeview_channels_show_context_menu(self, treeview, event):
1113 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1114 if not paths:
1115 return True
1117 if event.button == 3:
1118 menu = gtk.Menu()
1120 ICON = lambda x: x
1122 item = gtk.ImageMenuItem( _('Open download folder'))
1123 item.set_image( gtk.image_new_from_icon_name(ICON('folder-open'), gtk.ICON_SIZE_MENU))
1124 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1125 menu.append( item)
1127 item = gtk.ImageMenuItem( _('Update Feed'))
1128 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1129 item.connect('activate', self.on_itemUpdateChannel_activate )
1130 item.set_sensitive( not self.updating_feed_cache )
1131 menu.append( item)
1133 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1134 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1135 item.connect('activate', self.update_m3u_playlist_clicked)
1136 menu.append(item)
1138 if self.active_channel.link:
1139 item = gtk.ImageMenuItem(_('Visit website'))
1140 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1141 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1142 menu.append(item)
1144 if self.active_channel.channel_is_locked:
1145 item = gtk.ImageMenuItem(_('Allow 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))
1149 else:
1150 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1151 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1152 item.connect('activate', self.on_channel_toggle_lock_activate)
1153 menu.append(self.set_finger_friendly(item))
1156 menu.append( gtk.SeparatorMenuItem())
1158 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1159 item.connect( 'activate', self.on_itemEditChannel_activate)
1160 menu.append( item)
1162 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1163 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1164 menu.append( item)
1166 menu.show_all()
1167 # Disable tooltips while we are showing the menu, so
1168 # the tooltip will not appear over the menu
1169 self.treeview_allow_tooltips(self.treeChannels, False)
1170 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeChannels, True))
1171 menu.popup( None, None, None, event.button, event.time)
1173 return True
1175 def on_itemClose_activate(self, widget):
1176 if self.tray_icon is not None:
1177 self.iconify_main_window()
1178 else:
1179 self.on_gPodder_delete_event(widget)
1181 def cover_file_removed(self, channel_url):
1183 The Cover Downloader calls this when a previously-
1184 available cover has been removed from the disk. We
1185 have to update our model to reflect this change.
1187 self.podcast_list_model.delete_cover_by_url(channel_url)
1189 def cover_download_finished(self, channel_url, pixbuf):
1191 The Cover Downloader calls this when it has finished
1192 downloading (or registering, if already downloaded)
1193 a new channel cover, which is ready for displaying.
1195 self.podcast_list_model.add_cover_by_url(channel_url, pixbuf)
1197 def save_episode_as_file(self, episode):
1198 PRIVATE_FOLDER_ATTRIBUTE = '_save_episodes_as_file_folder'
1199 if episode.was_downloaded(and_exists=True):
1200 folder = getattr(self, PRIVATE_FOLDER_ATTRIBUTE, None)
1201 copy_from = episode.local_filename(create=False)
1202 assert copy_from is not None
1203 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1204 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1205 setattr(self, PRIVATE_FOLDER_ATTRIBUTE, folder)
1207 def copy_episodes_bluetooth(self, episodes):
1208 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1210 def convert_and_send_thread(episode):
1211 for episode in episodes:
1212 filename = episode.local_filename(create=False)
1213 assert filename is not None
1214 destfile = os.path.join(tempfile.gettempdir(), \
1215 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1216 (base, ext) = os.path.splitext(filename)
1217 if not destfile.endswith(ext):
1218 destfile += ext
1220 try:
1221 shutil.copyfile(filename, destfile)
1222 util.bluetooth_send_file(destfile)
1223 except:
1224 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1225 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1227 util.delete_file(destfile)
1229 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1231 def get_device_name(self):
1232 if self.config.device_type == 'ipod':
1233 return _('iPod')
1234 elif self.config.device_type in ('filesystem', 'mtp'):
1235 return _('MP3 player')
1236 else:
1237 return '(unknown device)'
1239 def _treeview_button_released(self, treeview, event):
1240 xpos, ypos = TreeViewHelper.get_button_press_event(treeview)
1241 dy = int(abs(event.y-ypos))
1242 dx = int(event.x-xpos)
1244 selection = treeview.get_selection()
1245 path = treeview.get_path_at_pos(int(event.x), int(event.y))
1246 if path is None or dy > 30:
1247 return (False, dx, dy)
1249 path, column, x, y = path
1250 selection.select_path(path)
1251 treeview.set_cursor(path)
1252 treeview.grab_focus()
1254 return (True, dx, dy)
1256 def treeview_channels_handle_gestures(self, treeview, event):
1257 if self.currently_updating:
1258 return False
1260 selected, dx, dy = self._treeview_button_released(treeview, event)
1262 if selected:
1263 if self.config.maemo_enable_gestures:
1264 if dx > 70:
1265 self.on_itemUpdateChannel_activate()
1266 elif dx < -70:
1267 self.on_itemEditChannel_activate(treeview)
1269 return False
1271 def treeview_available_handle_gestures(self, treeview, event):
1272 selected, dx, dy = self._treeview_button_released(treeview, event)
1274 if selected:
1275 if self.config.maemo_enable_gestures:
1276 if dx > 70:
1277 self.on_playback_selected_episodes(None)
1278 return True
1279 elif dx < -70:
1280 self.on_shownotes_selected_episodes(None)
1281 return True
1283 # Pass the event to the context menu handler for treeAvailable
1284 self.treeview_available_show_context_menu(treeview, event)
1286 return True
1288 def treeview_available_show_context_menu(self, treeview, event):
1289 model, paths = self.treeview_handle_context_menu_click(treeview, event)
1290 if not paths:
1291 if not hasattr(treeview, 'is_rubber_banding_active'):
1292 return True
1293 else:
1294 return not treeview.is_rubber_banding_active()
1296 if event.button == self.context_menu_mouse_button:
1297 episodes = self.get_selected_episodes()
1298 any_locked = any(e.is_locked for e in episodes)
1299 any_played = any(e.is_played for e in episodes)
1300 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1302 menu = gtk.Menu()
1304 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1306 if open_instead_of_play:
1307 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1308 else:
1309 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1311 item.set_sensitive(can_play)
1312 item.connect('activate', self.on_playback_selected_episodes)
1313 menu.append(self.set_finger_friendly(item))
1315 if not can_cancel:
1316 item = gtk.ImageMenuItem(_('Download'))
1317 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1318 item.set_sensitive(can_download)
1319 item.connect('activate', self.on_download_selected_episodes)
1320 menu.append(self.set_finger_friendly(item))
1321 else:
1322 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1323 item.connect('activate', self.on_item_cancel_download_activate)
1324 menu.append(self.set_finger_friendly(item))
1326 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1327 item.set_sensitive(can_delete)
1328 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1329 menu.append(self.set_finger_friendly(item))
1331 if one_is_new:
1332 item = gtk.ImageMenuItem(_('Do not download'))
1333 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1334 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1335 menu.append(self.set_finger_friendly(item))
1336 elif can_download:
1337 item = gtk.ImageMenuItem(_('Mark as new'))
1338 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1339 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1340 menu.append(self.set_finger_friendly(item))
1342 ICON = lambda x: x
1344 # Ok, this probably makes sense to only display for downloaded files
1345 if can_play and not can_download:
1346 menu.append( gtk.SeparatorMenuItem())
1347 item = gtk.ImageMenuItem(_('Save to disk'))
1348 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1349 item.connect('activate', lambda w: [self.save_episode_as_file(e) for e in episodes])
1350 menu.append(self.set_finger_friendly(item))
1351 if self.bluetooth_available:
1352 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1353 item.set_image(gtk.image_new_from_icon_name(ICON('bluetooth'), gtk.ICON_SIZE_MENU))
1354 item.connect('activate', lambda w: self.copy_episodes_bluetooth(episodes))
1355 menu.append(self.set_finger_friendly(item))
1356 if can_transfer:
1357 item = gtk.ImageMenuItem(_('Transfer to %s') % self.get_device_name())
1358 item.set_image(gtk.image_new_from_icon_name(ICON('multimedia-player'), gtk.ICON_SIZE_MENU))
1359 item.connect('activate', lambda w: self.on_sync_to_ipod_activate(w, episodes))
1360 menu.append(self.set_finger_friendly(item))
1362 if can_play:
1363 menu.append( gtk.SeparatorMenuItem())
1364 if any_played:
1365 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1366 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1367 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1368 menu.append(self.set_finger_friendly(item))
1369 else:
1370 item = gtk.ImageMenuItem(_('Mark as played'))
1371 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1372 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1373 menu.append(self.set_finger_friendly(item))
1375 if any_locked:
1376 item = gtk.ImageMenuItem(_('Allow 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, False))
1379 menu.append(self.set_finger_friendly(item))
1380 else:
1381 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1382 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1383 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, True))
1384 menu.append(self.set_finger_friendly(item))
1386 menu.append(gtk.SeparatorMenuItem())
1387 # Single item, add episode information menu item
1388 item = gtk.ImageMenuItem(_('Episode details'))
1389 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1390 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
1391 menu.append(self.set_finger_friendly(item))
1393 # If we have it, also add episode website link
1394 if episodes[0].link and episodes[0].link != episodes[0].url:
1395 item = gtk.ImageMenuItem(_('Visit website'))
1396 item.set_image(gtk.image_new_from_icon_name(ICON('web-browser'), gtk.ICON_SIZE_MENU))
1397 item.connect('activate', lambda w: util.open_website(episodes[0].link))
1398 menu.append(self.set_finger_friendly(item))
1400 if gpodder.ui.maemo:
1401 # Because we open the popup on left-click for Maemo,
1402 # we also include a non-action to close the menu
1403 menu.append(gtk.SeparatorMenuItem())
1404 item = gtk.ImageMenuItem(_('Close this menu'))
1405 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1406 menu.append(self.set_finger_friendly(item))
1408 menu.show_all()
1409 # Disable tooltips while we are showing the menu, so
1410 # the tooltip will not appear over the menu
1411 self.treeview_allow_tooltips(self.treeAvailable, False)
1412 menu.connect('deactivate', lambda menushell: self.treeview_allow_tooltips(self.treeAvailable, True))
1413 menu.popup( None, None, None, event.button, event.time)
1415 return True
1417 def set_title(self, new_title):
1418 if not gpodder.ui.fremantle:
1419 self.default_title = new_title
1420 self.gPodder.set_title(new_title)
1422 def update_episode_list_icons(self, urls=None, selected=False, all=False):
1424 Updates the status icons in the episode list.
1426 If urls is given, it should be a list of URLs
1427 of episodes that should be updated.
1429 If urls is None, set ONE OF selected, all to
1430 True (the former updates just the selected
1431 episodes and the latter updates all episodes).
1433 if urls is not None:
1434 # We have a list of URLs to walk through
1435 self.episode_list_model.update_by_urls(urls, \
1436 self.episode_is_downloading, \
1437 self.config.episode_list_descriptions and \
1438 gpodder.ui.desktop)
1439 elif selected and not all:
1440 # We should update all selected episodes
1441 selection = self.treeAvailable.get_selection()
1442 model, paths = selection.get_selected_rows()
1443 for path in reversed(paths):
1444 iter = model.get_iter(path)
1445 self.episode_list_model.update_by_filter_iter(iter, \
1446 self.episode_is_downloading, \
1447 self.config.episode_list_descriptions and \
1448 gpodder.ui.desktop)
1449 elif all and not selected:
1450 # We update all (even the filter-hidden) episodes
1451 self.episode_list_model.update_all(\
1452 self.episode_is_downloading, \
1453 self.config.episode_list_descriptions and \
1454 gpodder.ui.desktop)
1455 else:
1456 # Wrong/invalid call - have to specify at least one parameter
1457 raise ValueError('Invalid call to update_episode_list_icons')
1459 def episode_list_status_changed(self, episodes):
1460 self.update_episode_list_icons(set(e.url for e in episodes))
1461 self.update_podcast_list_model(set(e.channel.url for e in episodes))
1462 self.db.commit()
1464 def clean_up_downloads(self, delete_partial=False):
1465 # Clean up temporary files left behind by old gPodder versions
1466 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
1468 if delete_partial:
1469 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
1471 for tempfile in temporary_files:
1472 util.delete_file(tempfile)
1474 # Clean up empty download folders and abandoned download folders
1475 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
1476 for ddir in download_dirs:
1477 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1478 globr = glob.glob(os.path.join(ddir, '*'))
1479 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
1480 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
1481 shutil.rmtree(ddir, ignore_errors=True)
1483 def streaming_possible(self):
1484 return self.config.player and \
1485 self.config.player != 'default' and \
1486 gpodder.ui.desktop
1488 def playback_episodes_for_real(self, episodes):
1489 groups = collections.defaultdict(list)
1490 for episode in episodes:
1491 file_type = episode.file_type()
1492 if file_type == 'video' and self.config.videoplayer and \
1493 self.config.videoplayer != 'default':
1494 player = self.config.videoplayer
1495 if gpodder.ui.diablo:
1496 # Use the wrapper script if it's installed to crop 3GP YouTube
1497 # videos to fit the screen (looks much nicer than w/ black border)
1498 if player == 'mplayer' and util.find_command('gpodder-mplayer'):
1499 player = 'gpodder-mplayer'
1500 elif file_type == 'audio' and self.config.player and \
1501 self.config.player != 'default':
1502 player = self.config.player
1503 else:
1504 player = 'default'
1506 if file_type not in ('audio', 'video') or \
1507 (file_type == 'audio' and not self.config.audio_played_dbus) or \
1508 (file_type == 'video' and not self.config.video_played_dbus):
1509 # Mark episode as played in the database
1510 episode.mark(is_played=True)
1512 filename = episode.local_filename(create=False)
1513 if filename is None or not os.path.exists(filename):
1514 filename = episode.url
1515 groups[player].append(filename)
1517 # Open episodes with system default player
1518 if 'default' in groups:
1519 for filename in groups['default']:
1520 log('Opening with system default: %s', filename, sender=self)
1521 util.gui_open(filename)
1522 del groups['default']
1524 # For each type now, go and create play commands
1525 for group in groups:
1526 for command in util.format_desktop_command(group, groups[group]):
1527 log('Executing: %s', repr(command), sender=self)
1528 subprocess.Popen(command)
1530 def playback_episodes(self, episodes):
1531 if gpodder.ui.maemo:
1532 if len(episodes) == 1:
1533 text = _('Opening %s') % episodes[0].title
1534 else:
1535 text = _('Opening %d episodes') % len(episodes)
1537 if gpodder.ui.diablo:
1538 banner = hildon.hildon_banner_show_animation(self.gPodder, None, text)
1539 elif gpodder.ui.fremantle:
1540 banner = hildon.hildon_banner_show_animation(self.gPodder, '', text)
1542 def destroy_banner_later(banner):
1543 banner.destroy()
1544 return False
1545 gobject.timeout_add(5000, destroy_banner_later, banner)
1547 episodes = [e for e in episodes if \
1548 e.was_downloaded(and_exists=True) or self.streaming_possible()]
1550 try:
1551 self.playback_episodes_for_real(episodes)
1552 except Exception, e:
1553 log('Error in playback!', sender=self, traceback=True)
1554 self.show_message( _('Please check your media player settings in the preferences dialog.'), _('Error opening player'), widget=self.toolPreferences)
1556 channel_urls = set()
1557 episode_urls = set()
1558 for episode in episodes:
1559 channel_urls.add(episode.channel.url)
1560 episode_urls.add(episode.url)
1561 self.update_episode_list_icons(episode_urls)
1562 self.update_podcast_list_model(channel_urls)
1564 def play_or_download(self):
1565 if not gpodder.ui.fremantle:
1566 if self.wNotebook.get_current_page() > 0:
1567 if gpodder.ui.desktop:
1568 self.toolCancel.set_sensitive(True)
1569 return
1571 if self.currently_updating:
1572 return (False, False, False, False, False, False)
1574 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1575 ( is_played, is_locked ) = (False,)*2
1577 open_instead_of_play = False
1579 selection = self.treeAvailable.get_selection()
1580 if selection.count_selected_rows() > 0:
1581 (model, paths) = selection.get_selected_rows()
1583 for path in paths:
1584 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
1586 if episode.file_type() not in ('audio', 'video'):
1587 open_instead_of_play = True
1589 if episode.was_downloaded():
1590 can_play = episode.was_downloaded(and_exists=True)
1591 can_delete = True
1592 is_played = episode.is_played
1593 is_locked = episode.is_locked
1594 if not can_play:
1595 can_download = True
1596 else:
1597 if self.episode_is_downloading(episode):
1598 can_cancel = True
1599 else:
1600 can_download = True
1602 can_download = can_download and not can_cancel
1603 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
1604 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download and not open_instead_of_play
1606 if gpodder.ui.desktop:
1607 if open_instead_of_play:
1608 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1609 else:
1610 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1611 self.toolPlay.set_sensitive( can_play)
1612 self.toolDownload.set_sensitive( can_download)
1613 self.toolTransfer.set_sensitive( can_transfer)
1614 self.toolCancel.set_sensitive( can_cancel)
1616 if not gpodder.ui.fremantle:
1617 self.item_cancel_download.set_sensitive(can_cancel)
1618 self.itemDownloadSelected.set_sensitive(can_download)
1619 self.itemOpenSelected.set_sensitive(can_play)
1620 self.itemPlaySelected.set_sensitive(can_play)
1621 self.itemDeleteSelected.set_sensitive(can_play and not can_download)
1622 self.item_toggle_played.set_sensitive(can_play)
1623 self.item_toggle_lock.set_sensitive(can_play)
1624 self.itemOpenSelected.set_visible(open_instead_of_play)
1625 self.itemPlaySelected.set_visible(not open_instead_of_play)
1627 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1629 def on_cbMaxDownloads_toggled(self, widget, *args):
1630 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1632 def on_cbLimitDownloads_toggled(self, widget, *args):
1633 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1635 def episode_new_status_changed(self, urls):
1636 self.update_podcast_list_model()
1637 self.update_episode_list_icons(urls)
1639 def update_podcast_list_model(self, urls=None, selected=False, select_url=None):
1640 """Update the podcast list treeview model
1642 If urls is given, it should list the URLs of each
1643 podcast that has to be updated in the list.
1645 If selected is True, only update the model contents
1646 for the currently-selected podcast - nothing more.
1648 The caller can optionally specify "select_url",
1649 which is the URL of the podcast that is to be
1650 selected in the list after the update is complete.
1651 This only works if the podcast list has to be
1652 reloaded; i.e. something has been added or removed
1653 since the last update of the podcast list).
1655 selection = self.treeChannels.get_selection()
1656 model, iter = selection.get_selected()
1658 if selected:
1659 # very cheap! only update selected channel
1660 if iter is not None:
1661 self.podcast_list_model.update_by_filter_iter(iter)
1662 elif not self.channel_list_changed:
1663 # we can keep the model, but have to update some
1664 if urls is None:
1665 # still cheaper than reloading the whole list
1666 self.podcast_list_model.update_all()
1667 else:
1668 # ok, we got a bunch of urls to update
1669 self.podcast_list_model.update_by_urls(urls)
1670 else:
1671 if model and iter and select_url is None:
1672 # Get the URL of the currently-selected podcast
1673 select_url = model.get_value(iter, PodcastListModel.C_URL)
1675 # Update the podcast list model with new channels
1676 self.podcast_list_model.set_channels(self.channels)
1678 try:
1679 selected_iter = model.get_iter_first()
1680 # Find the previously-selected URL in the new
1681 # model if we have an URL (else select first)
1682 if select_url is not None:
1683 pos = model.get_iter_first()
1684 while pos is not None:
1685 url = model.get_value(pos, PodcastListModel.C_URL)
1686 if url == select_url:
1687 selected_iter = pos
1688 break
1689 pos = model.iter_next(pos)
1691 if not gpodder.ui.fremantle:
1692 if selected_iter is not None:
1693 selection.select_iter(selected_iter)
1694 self.on_treeChannels_cursor_changed(self.treeChannels)
1695 except:
1696 log('Cannot select podcast in list', traceback=True, sender=self)
1697 self.channel_list_changed = False
1699 def episode_is_downloading(self, episode):
1700 """Returns True if the given episode is being downloaded at the moment"""
1701 if episode is None:
1702 return False
1704 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
1706 def update_episode_list_model(self):
1707 if self.channels and self.active_channel is not None:
1708 if gpodder.ui.diablo:
1709 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Loading episodes'))
1710 else:
1711 banner = None
1713 if gpodder.ui.fremantle:
1714 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, True)
1716 self.currently_updating = True
1717 self.episode_list_model.clear()
1718 def do_update_episode_list_model():
1719 self.episode_list_model.add_from_channel(\
1720 self.active_channel, \
1721 self.episode_is_downloading, \
1722 self.config.episode_list_descriptions \
1723 and gpodder.ui.desktop)
1725 def on_episode_list_model_updated():
1726 if banner is not None:
1727 banner.destroy()
1728 if gpodder.ui.fremantle:
1729 hildon.hildon_gtk_window_set_progress_indicator(self.episodes_window.main_window, False)
1730 self.treeAvailable.columns_autosize()
1731 self.currently_updating = False
1732 self.play_or_download()
1733 util.idle_add(on_episode_list_model_updated)
1734 threading.Thread(target=do_update_episode_list_model).start()
1735 else:
1736 self.episode_list_model.clear()
1738 def offer_new_episodes(self, channels=None):
1739 new_episodes = self.get_new_episodes(channels)
1740 if new_episodes:
1741 self.new_episodes_show(new_episodes)
1742 return True
1743 return False
1745 def add_podcast_list(self, urls, auth_tokens=None):
1746 """Subscribe to a list of podcast given their URLs
1748 If auth_tokens is given, it should be a dictionary
1749 mapping URLs to (username, password) tuples."""
1751 if auth_tokens is None:
1752 auth_tokens = {}
1754 # Sort and split the URL list into five buckets
1755 queued, failed, existing, worked, authreq = [], [], [], [], []
1756 for input_url in urls:
1757 url = util.normalize_feed_url(input_url)
1758 if url is None:
1759 # Fail this one because the URL is not valid
1760 failed.append(input_url)
1761 elif self.podcast_list_model.get_filter_path_from_url(url) is not None:
1762 # A podcast already exists in the list for this URL
1763 existing.append(url)
1764 else:
1765 # This URL has survived the first round - queue for add
1766 queued.append(url)
1767 if url != input_url and input_url in auth_tokens:
1768 auth_tokens[url] = auth_tokens[input_url]
1770 error_messages = {}
1771 redirections = {}
1773 progress = ProgressIndicator(_('Adding podcasts'), \
1774 _('Please wait while episode information is downloaded.'), \
1775 parent=self.main_window)
1777 def on_after_update():
1778 progress.on_finished()
1779 # Report already-existing subscriptions to the user
1780 if existing:
1781 title = _('Existing subscriptions skipped')
1782 message = _('You are already subscribed to these podcasts:') \
1783 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
1784 self.show_message(message, title, widget=self.treeChannels)
1786 # Report subscriptions that require authentication
1787 if authreq:
1788 retry_podcasts = {}
1789 for url in authreq:
1790 title = _('Podcast requires authentication')
1791 message = _('Please login to %s:') % (saxutils.escape(url),)
1792 success, auth_tokens = self.show_login_dialog(title, message)
1793 if success:
1794 retry_podcasts[url] = auth_tokens
1795 else:
1796 # Stop asking the user for more login data
1797 retry_podcasts = {}
1798 for url in authreq:
1799 error_messages[url] = _('Authentication failed')
1800 failed.append(url)
1801 break
1803 # If we have authentication data to retry, do so here
1804 if retry_podcasts:
1805 self.add_podcast_list(retry_podcasts.keys(), retry_podcasts)
1807 # Report website redirections
1808 for url in redirections:
1809 title = _('Website redirection detected')
1810 message = _('The URL %s redirects to %s.') \
1811 + '\n\n' + _('Do you want to visit the website now?')
1812 message = message % (url, redirections[url])
1813 if self.show_confirmation(message, title):
1814 util.open_website(error.data)
1815 else:
1816 break
1818 # Report failed subscriptions to the user
1819 if failed:
1820 title = _('Could not add some podcasts')
1821 message = _('Some podcasts could not be added to your list:') \
1822 + '\n\n' + '\n'.join(saxutils.escape('%s: %s' % (url, \
1823 error_messages.get(url, _('Unknown')))) for url in failed)
1824 self.show_message(message, title, important=True)
1826 # If at least one podcast has been added, save and update all
1827 if self.channel_list_changed:
1828 self.save_channels_opml()
1830 # If only one podcast was added, select it after the update
1831 if len(worked) == 1:
1832 url = worked[0]
1833 else:
1834 url = None
1836 # Update the list of subscribed podcasts
1837 self.update_feed_cache(force_update=False, select_url_afterwards=url)
1838 self.update_podcasts_tab()
1840 # Offer to download new episodes
1841 self.offer_new_episodes(channels=[c for c in self.channels if c.url in worked])
1843 def thread_proc():
1844 # After the initial sorting and splitting, try all queued podcasts
1845 length = len(queued)
1846 for index, url in enumerate(queued):
1847 progress.on_progress(float(index)/float(length))
1848 progress.on_message(url)
1849 log('QUEUE RUNNER: %s', url, sender=self)
1850 try:
1851 # The URL is valid and does not exist already - subscribe!
1852 channel = PodcastChannel.load(self.db, url=url, create=True, \
1853 authentication_tokens=auth_tokens.get(url, None), \
1854 max_episodes=self.config.max_episodes_per_feed, \
1855 download_dir=self.config.download_dir)
1857 try:
1858 username, password = util.username_password_from_url(url)
1859 except ValueError, ve:
1860 username, password = (None, None)
1862 if username is not None and channel.username is None and \
1863 password is not None and channel.password is None:
1864 channel.username = username
1865 channel.password = password
1866 channel.save()
1868 self._update_cover(channel)
1869 except feedcore.AuthenticationRequired:
1870 if url in auth_tokens:
1871 # Fail for wrong authentication data
1872 error_messages[url] = _('Authentication failed')
1873 failed.append(url)
1874 else:
1875 # Queue for login dialog later
1876 authreq.append(url)
1877 continue
1878 except feedcore.WifiLogin, error:
1879 redirections[url] = error.data
1880 failed.append(url)
1881 error_messages[url] = _('Redirection detected')
1882 continue
1883 except Exception, e:
1884 log('Subscription error: %s', e, traceback=True, sender=self)
1885 error_messages[url] = str(e)
1886 failed.append(url)
1887 continue
1889 assert channel is not None
1890 worked.append(channel.url)
1891 self.channels.append(channel)
1892 self.channel_list_changed = True
1893 util.idle_add(on_after_update)
1894 threading.Thread(target=thread_proc).start()
1896 def save_channels_opml(self):
1897 exporter = opml.Exporter(gpodder.subscription_file)
1898 return exporter.write(self.channels)
1900 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
1901 self.db.commit()
1902 self.updating_feed_cache = False
1904 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
1905 self.channel_list_changed = True
1906 self.update_podcast_list_model(select_url=select_url_afterwards)
1908 # Only search for new episodes in podcasts that have been
1909 # updated, not in other podcasts (for single-feed updates)
1910 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
1912 if gpodder.ui.fremantle:
1913 if self._fremantle_update_banner is not None:
1914 self._fremantle_update_banner.destroy()
1915 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, False)
1916 hildon.hildon_gtk_window_set_progress_indicator(self.podcasts_window.main_window, False)
1917 if episodes:
1918 self.new_episodes_show(episodes)
1919 else:
1920 self.show_message(_('No new episodes. Please check for new episodes later.'), important=True)
1921 return
1923 if self.tray_icon:
1924 self.tray_icon.set_status()
1926 if self.feed_cache_update_cancelled:
1927 # The user decided to abort the feed update
1928 self.show_update_feeds_buttons()
1929 elif not episodes:
1930 # Nothing new here - but inform the user
1931 self.pbFeedUpdate.set_fraction(1.0)
1932 self.pbFeedUpdate.set_text(_('No new episodes'))
1933 self.feed_cache_update_cancelled = True
1934 self.btnCancelFeedUpdate.show()
1935 self.btnCancelFeedUpdate.set_sensitive(True)
1936 if gpodder.ui.maemo:
1937 # btnCancelFeedUpdate is a ToolButton on Maemo
1938 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
1939 else:
1940 # btnCancelFeedUpdate is a normal gtk.Button
1941 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
1942 else:
1943 # New episodes are available
1944 self.pbFeedUpdate.set_fraction(1.0)
1945 # Are we minimized and should we auto download?
1946 if (self.is_iconified() and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
1947 self.download_episode_list(episodes)
1948 if len(episodes) == 1:
1949 title = _('Downloading one new episode.')
1950 else:
1951 title = _('Downloading %d new episodes.') % len(episodes)
1953 if not gpodder.ui.fremantle:
1954 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
1955 self.show_update_feeds_buttons()
1956 else:
1957 self.show_update_feeds_buttons()
1958 # New episodes are available and we are not minimized
1959 if not self.config.do_not_show_new_episodes_dialog:
1960 self.new_episodes_show(episodes, notification=True)
1961 else:
1962 if len(episodes) == 1:
1963 message = _('One new episode is available for download')
1964 else:
1965 message = _('%i new episodes are available for download' % len(episodes))
1967 self.pbFeedUpdate.set_text(message)
1969 def _update_cover(self, channel):
1970 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
1971 self.cover_downloader.request_cover(channel)
1973 def update_feed_cache_proc(self, channels, select_url_afterwards):
1974 total = len(channels)
1976 for updated, channel in enumerate(channels):
1977 if not self.feed_cache_update_cancelled:
1978 try:
1979 # Update if timeout is not reached or we update a single podcast or skipping is disabled
1980 if channel.query_automatic_update() or total == 1 or not self.config.feed_update_skipping:
1981 channel.update(max_episodes=self.config.max_episodes_per_feed)
1982 else:
1983 log('Skipping update of %s (see feed_update_skipping)', channel.title, sender=self)
1984 self._update_cover(channel)
1985 except Exception, e:
1986 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)
1987 log('Error: %s', str(e), sender=self, traceback=True)
1989 if self.feed_cache_update_cancelled:
1990 break
1992 if gpodder.ui.fremantle:
1993 if self._fremantle_update_banner is not None:
1994 progression = _('%d of %d podcasts updated') % (updated, total)
1995 util.idle_add(self._fremantle_update_banner.set_text, progression)
1996 util.idle_add(self._fremantle_update_banner.show)
1997 continue
1999 # By the time we get here the update may have already been cancelled
2000 if not self.feed_cache_update_cancelled:
2001 def update_progress():
2002 progression = _('Updated %s (%d/%d)') % (channel.title, updated, total)
2003 self.pbFeedUpdate.set_text(progression)
2004 if self.tray_icon:
2005 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
2006 self.pbFeedUpdate.set_fraction(float(updated)/float(total))
2007 util.idle_add(update_progress)
2009 updated_urls = [c.url for c in channels]
2010 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
2012 def show_update_feeds_buttons(self):
2013 # Make sure that the buttons for updating feeds
2014 # appear - this should happen after a feed update
2015 if gpodder.ui.maemo:
2016 self.btnUpdateSelectedFeed.show()
2017 self.toolFeedUpdateProgress.hide()
2018 self.btnCancelFeedUpdate.hide()
2019 self.btnCancelFeedUpdate.set_is_important(False)
2020 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
2021 self.toolbarSpacer.set_expand(True)
2022 self.toolbarSpacer.set_draw(False)
2023 else:
2024 self.hboxUpdateFeeds.hide()
2025 self.btnUpdateFeeds.show()
2026 self.itemUpdate.set_sensitive(True)
2027 self.itemUpdateChannel.set_sensitive(True)
2029 def on_btnCancelFeedUpdate_clicked(self, widget):
2030 if not self.feed_cache_update_cancelled:
2031 self.pbFeedUpdate.set_text(_('Cancelling...'))
2032 self.feed_cache_update_cancelled = True
2033 self.btnCancelFeedUpdate.set_sensitive(False)
2034 else:
2035 self.show_update_feeds_buttons()
2037 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
2038 if self.updating_feed_cache:
2039 return
2041 if not force_update:
2042 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
2043 self.channel_list_changed = True
2044 self.update_podcast_list_model(select_url=select_url_afterwards)
2045 return
2047 self.updating_feed_cache = True
2049 if channels is None:
2050 channels = self.channels
2052 if gpodder.ui.fremantle:
2053 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, True)
2054 hildon.hildon_gtk_window_set_progress_indicator(self.podcasts_window.main_window, True)
2055 self._fremantle_update_banner = hildon.hildon_banner_show_animation(self.main_window, \
2056 '', _('Updating podcast feeds'))
2057 else:
2058 self.itemUpdate.set_sensitive(False)
2059 self.itemUpdateChannel.set_sensitive(False)
2061 if self.tray_icon:
2062 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
2064 if len(channels) == 1:
2065 text = _('Updating "%s"...') % channels[0].title
2066 else:
2067 text = _('Updating %d feeds...') % len(channels)
2068 self.pbFeedUpdate.set_text(text)
2069 self.pbFeedUpdate.set_fraction(0)
2071 self.feed_cache_update_cancelled = False
2072 self.btnCancelFeedUpdate.show()
2073 self.btnCancelFeedUpdate.set_sensitive(True)
2074 if gpodder.ui.maemo:
2075 self.toolbarSpacer.set_expand(False)
2076 self.toolbarSpacer.set_draw(True)
2077 self.btnUpdateSelectedFeed.hide()
2078 self.toolFeedUpdateProgress.show_all()
2079 else:
2080 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
2081 self.hboxUpdateFeeds.show_all()
2082 self.btnUpdateFeeds.hide()
2084 args = (channels, select_url_afterwards)
2085 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
2087 def on_gPodder_delete_event(self, widget, *args):
2088 """Called when the GUI wants to close the window
2089 Displays a confirmation dialog (and closes/hides gPodder)
2092 downloading = self.download_status_model.are_downloads_in_progress()
2094 # Only iconify if we are using the window's "X" button,
2095 # but not when we are using "Quit" in the menu or toolbar
2096 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'):
2097 self.iconify_main_window()
2098 elif self.config.on_quit_ask or downloading:
2099 if gpodder.ui.maemo:
2100 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
2101 if result:
2102 self.close_gpodder()
2103 else:
2104 return True
2105 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2106 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2107 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
2109 title = _('Quit gPodder')
2110 if downloading:
2111 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2112 else:
2113 message = _('Do you really want to quit gPodder now?')
2115 dialog.set_title(title)
2116 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2117 if not downloading:
2118 cb_ask = gtk.CheckButton(_("Don't ask me again"))
2119 dialog.vbox.pack_start(cb_ask)
2120 cb_ask.show_all()
2122 quit_button.grab_focus()
2123 result = dialog.run()
2124 dialog.destroy()
2126 if result == gtk.RESPONSE_CLOSE:
2127 if not downloading and cb_ask.get_active() == True:
2128 self.config.on_quit_ask = False
2129 self.close_gpodder()
2130 else:
2131 self.close_gpodder()
2133 return True
2135 def close_gpodder(self):
2136 """ clean everything and exit properly
2138 if self.channels:
2139 if self.save_channels_opml():
2140 if self.config.my_gpodder_autoupload:
2141 log('Uploading to my.gpodder.org on close', sender=self)
2142 util.idle_add(self.on_upload_to_mygpo, None)
2143 else:
2144 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
2146 self.gPodder.hide()
2148 if self.tray_icon is not None:
2149 self.tray_icon.set_visible(False)
2151 # Notify all tasks to to carry out any clean-up actions
2152 self.download_status_model.tell_all_tasks_to_quit()
2154 while gtk.events_pending():
2155 gtk.main_iteration(False)
2157 self.db.close()
2159 self.quit()
2160 sys.exit(0)
2162 def get_old_episodes(self):
2163 episodes = []
2164 for channel in self.channels:
2165 for episode in channel.get_downloaded_episodes():
2166 if episode.age_in_days() > self.config.episode_old_age and \
2167 not episode.is_locked and episode.is_played:
2168 episodes.append(episode)
2169 return episodes
2171 def delete_episode_list(self, episodes, confirm=True):
2172 if not episodes:
2173 return
2175 count = len(episodes)
2177 if count == 1:
2178 episode = episodes[0]
2179 if episode.is_locked:
2180 title = _('%s is locked') % saxutils.escape(episode.title)
2181 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2182 self.notification(message, title, widget=self.treeAvailable)
2183 return
2185 title = _('Remove %s?') % saxutils.escape(episode.title)
2186 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.")
2187 else:
2188 title = _('Remove %d episodes?') % count
2189 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.')
2191 locked_count = sum(int(e.is_locked) for e in episodes if e.is_locked is not None)
2193 if count == locked_count:
2194 title = _('Episodes are locked')
2195 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2196 self.notification(message, title, widget=self.treeAvailable)
2197 return
2198 elif locked_count > 0:
2199 title = _('Remove %d out of %d episodes?') % (count-locked_count, count)
2200 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.')
2202 if confirm and not self.show_confirmation(message, title):
2203 return
2205 episode_urls = set()
2206 channel_urls = set()
2207 for episode in episodes:
2208 if episode.is_locked:
2209 log('Not deleting episode (is locked): %s', episode.title)
2210 else:
2211 log('Deleting episode: %s', episode.title)
2212 episode.delete_from_disk()
2213 episode_urls.add(episode.url)
2214 channel_urls.add(episode.channel.url)
2216 # Tell the shownotes window that we have removed the episode
2217 if self.episode_shownotes_window is not None and \
2218 self.episode_shownotes_window.episode is not None and \
2219 self.episode_shownotes_window.episode.url == episode.url:
2220 self.episode_shownotes_window._download_status_changed(None)
2222 # Episodes have been deleted - persist the database
2223 self.db.commit()
2225 self.update_episode_list_icons(episode_urls)
2226 self.update_podcast_list_model(channel_urls)
2227 self.play_or_download()
2229 def on_itemRemoveOldEpisodes_activate( self, widget):
2230 if gpodder.ui.maemo:
2231 columns = (
2232 ('maemo_remove_markup', None, None, _('Episode')),
2234 else:
2235 columns = (
2236 ('title_markup', None, None, _('Episode')),
2237 ('channel_prop', None, None, _('Podcast')),
2238 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2239 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2240 ('played_prop', None, None, _('Status')),
2241 ('age_prop', None, None, _('Downloaded')),
2244 selection_buttons = {
2245 _('Select played'): lambda episode: episode.is_played,
2246 _('Select older than %d days') % self.config.episode_old_age: lambda episode: episode.age_in_days() > self.config.episode_old_age,
2249 instructions = _('Select the episodes you want to delete:')
2251 episodes = []
2252 selected = []
2253 for channel in self.channels:
2254 for episode in channel.get_downloaded_episodes():
2255 if not episode.is_locked:
2256 episodes.append(episode)
2257 selected.append(episode.is_played)
2259 gPodderEpisodeSelector(self.gPodder, title = _('Remove old episodes'), instructions = instructions, \
2260 episodes = episodes, selected = selected, columns = columns, \
2261 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2262 selection_buttons = selection_buttons, _config=self.config)
2264 def on_selected_episodes_status_changed(self):
2265 self.update_episode_list_icons(selected=True)
2266 self.update_podcast_list_model(selected=True)
2267 self.db.commit()
2269 def mark_selected_episodes_new(self):
2270 for episode in self.get_selected_episodes():
2271 episode.mark_new()
2272 self.on_selected_episodes_status_changed()
2274 def mark_selected_episodes_old(self):
2275 for episode in self.get_selected_episodes():
2276 episode.mark_old()
2277 self.on_selected_episodes_status_changed()
2279 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2280 for episode in self.get_selected_episodes():
2281 if toggle:
2282 episode.mark(is_played=not episode.is_played)
2283 else:
2284 episode.mark(is_played=new_value)
2285 self.on_selected_episodes_status_changed()
2287 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2288 for episode in self.get_selected_episodes():
2289 if toggle:
2290 episode.mark(is_locked=not episode.is_locked)
2291 else:
2292 episode.mark(is_locked=new_value)
2293 self.on_selected_episodes_status_changed()
2295 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2296 if self.active_channel is None:
2297 return
2299 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2300 self.active_channel.update_channel_lock()
2302 for episode in self.active_channel.get_all_episodes():
2303 episode.mark(is_locked=self.active_channel.channel_is_locked)
2305 self.update_podcast_list_model(selected=True)
2306 self.update_episode_list_icons(all=True)
2308 def send_subscriptions(self):
2309 try:
2310 subprocess.Popen(['xdg-email', '--subject', _('My podcast subscriptions'),
2311 '--attach', gpodder.subscription_file])
2312 except:
2313 return False
2315 return True
2317 def on_item_email_subscriptions_activate(self, widget):
2318 if not self.channels:
2319 self.show_message(_('Your subscription list is empty. Add some podcasts first.'), _('Could not send list'), widget=self.treeChannels)
2320 elif not self.send_subscriptions():
2321 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'), important=True)
2323 def on_itemUpdateChannel_activate(self, widget=None):
2324 if self.active_channel is None:
2325 title = _('No podcast selected')
2326 message = _('Please select a podcast in the podcasts list to update.')
2327 self.show_message( message, title, widget=self.treeChannels)
2328 return
2330 self.update_feed_cache(channels=[self.active_channel])
2332 def on_itemUpdate_activate(self, widget=None):
2333 if self.channels:
2334 self.update_feed_cache()
2335 else:
2336 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)
2338 def download_episode_list_paused(self, episodes):
2339 self.download_episode_list(episodes, True)
2341 def download_episode_list(self, episodes, add_paused=False):
2342 for episode in episodes:
2343 log('Downloading episode: %s', episode.title, sender = self)
2344 if not episode.was_downloaded(and_exists=True):
2345 task_exists = False
2346 for task in self.download_tasks_seen:
2347 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2348 self.download_queue_manager.add_task(task)
2349 self.enable_download_list_update()
2350 task_exists = True
2351 continue
2353 if task_exists:
2354 continue
2356 try:
2357 task = download.DownloadTask(episode, self.config)
2358 except Exception, e:
2359 self.show_message(_('Download error while downloading %s:\n\n%s') % (episode.title, str(e)), _('Download error'), important=True)
2360 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
2361 continue
2363 if add_paused:
2364 task.status = task.PAUSED
2365 else:
2366 self.download_queue_manager.add_task(task)
2368 self.download_status_model.register_task(task)
2369 self.enable_download_list_update()
2371 def cancel_task_list(self, tasks):
2372 if not tasks:
2373 return
2375 for task in tasks:
2376 if task.status in (task.QUEUED, task.DOWNLOADING):
2377 task.status = task.CANCELLED
2378 elif task.status == task.PAUSED:
2379 task.status = task.CANCELLED
2380 # Call run, so the partial file gets deleted
2381 task.run()
2383 self.update_episode_list_icons([task.url for task in tasks])
2384 self.play_or_download()
2386 # Update the tab title and downloads list
2387 self.update_downloads_list()
2389 def new_episodes_show(self, episodes, notification=False):
2390 if gpodder.ui.maemo:
2391 columns = (
2392 ('maemo_markup', None, None, _('Episode')),
2394 show_notification = notification
2395 else:
2396 columns = (
2397 ('title_markup', None, None, _('Episode')),
2398 ('channel_prop', None, None, _('Podcast')),
2399 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2400 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2402 show_notification = False
2404 instructions = _('Select the episodes you want to download:')
2406 gPodderEpisodeSelector(self.gPodder, title=_('New episodes available'), instructions=instructions, \
2407 episodes=episodes, columns=columns, selected_default=True, \
2408 stock_ok_button = 'gpodder-download', \
2409 callback=self.download_episode_list, \
2410 remove_callback=lambda e: e.mark_old(), \
2411 remove_action=_('Mark as old'), \
2412 remove_finished=self.episode_new_status_changed, \
2413 _config=self.config, \
2414 show_notification=show_notification)
2416 def on_itemDownloadAllNew_activate(self, widget, *args):
2417 if not self.offer_new_episodes():
2418 self.show_message(_('Please check for new episodes later.'), \
2419 _('No new episodes available'), widget=self.btnUpdateFeeds)
2421 def get_new_episodes(self, channels=None):
2422 if channels is None:
2423 channels = self.channels
2424 episodes = []
2425 for channel in channels:
2426 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
2427 episodes.append(episode)
2429 return episodes
2431 def on_sync_to_ipod_activate(self, widget, episodes=None):
2432 self.sync_ui.on_synchronize_episodes(self.channels, episodes)
2433 # The sync process might have updated the status of episodes,
2434 # therefore persist the database here to avoid losing data
2435 self.db.commit()
2437 def on_cleanup_ipod_activate(self, widget, *args):
2438 self.sync_ui.on_cleanup_device()
2440 def on_manage_device_playlist(self, widget):
2441 self.sync_ui.on_manage_device_playlist()
2443 def show_hide_tray_icon(self):
2444 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2445 self.tray_icon = GPodderStatusIcon(self, gpodder.icon_file, self.config)
2446 elif not self.config.display_tray_icon and self.tray_icon is not None:
2447 self.tray_icon.set_visible(False)
2448 del self.tray_icon
2449 self.tray_icon = None
2451 if self.config.minimize_to_tray and self.tray_icon:
2452 self.tray_icon.set_visible(self.is_iconified())
2453 elif self.tray_icon:
2454 self.tray_icon.set_visible(True)
2456 def on_itemShowToolbar_activate(self, widget):
2457 self.config.show_toolbar = self.itemShowToolbar.get_active()
2459 def on_itemShowDescription_activate(self, widget):
2460 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
2462 def on_item_view_hide_boring_podcasts_toggled(self, toggleaction):
2463 self.config.podcast_list_hide_boring = toggleaction.get_active()
2464 if self.config.podcast_list_hide_boring:
2465 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
2466 else:
2467 self.podcast_list_model.set_view_mode(-1)
2469 def on_item_view_podcasts_changed(self, radioaction, current):
2470 # Only on Fremantle
2471 if current == self.item_view_podcasts_all:
2472 self.podcast_list_model.set_view_mode(-1)
2473 elif current == self.item_view_podcasts_downloaded:
2474 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
2475 elif current == self.item_view_podcasts_unplayed:
2476 self.podcast_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
2478 self.config.podcast_list_view_mode = self.podcast_list_model.get_view_mode()
2480 def on_item_view_episodes_changed(self, radioaction, current):
2481 if current == self.item_view_episodes_all:
2482 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_ALL)
2483 elif current == self.item_view_episodes_undeleted:
2484 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNDELETED)
2485 elif current == self.item_view_episodes_downloaded:
2486 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
2487 elif current == self.item_view_episodes_unplayed:
2488 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNPLAYED)
2490 self.config.episode_list_view_mode = self.episode_list_model.get_view_mode()
2492 if self.config.podcast_list_hide_boring and not gpodder.ui.fremantle:
2493 self.podcast_list_model.set_view_mode(self.config.episode_list_view_mode)
2495 def update_item_device( self):
2496 if not gpodder.ui.fremantle:
2497 if self.config.device_type != 'none':
2498 self.itemDevice.set_visible(True)
2499 self.itemDevice.label = self.get_device_name()
2500 else:
2501 self.itemDevice.set_visible(False)
2503 def properties_closed( self):
2504 self.show_hide_tray_icon()
2505 self.update_item_device()
2506 if gpodder.ui.maemo:
2507 selection = self.treeAvailable.get_selection()
2508 if self.config.maemo_enable_gestures or \
2509 self.config.enable_fingerscroll:
2510 selection.set_mode(gtk.SELECTION_SINGLE)
2511 else:
2512 selection.set_mode(gtk.SELECTION_MULTIPLE)
2514 def on_itemPreferences_activate(self, widget, *args):
2515 gPodderPreferences(self.gPodder, _config=self.config, \
2516 callback_finished=self.properties_closed, \
2517 user_apps_reader=self.user_apps_reader)
2519 def on_itemDependencies_activate(self, widget):
2520 gPodderDependencyManager(self.gPodder)
2522 def require_my_gpodder_authentication(self):
2523 if not self.config.my_gpodder_username or not self.config.my_gpodder_password:
2524 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'))
2525 if success and authentication[0] and authentication[1]:
2526 self.config.my_gpodder_username, self.config.my_gpodder_password = authentication
2527 return True
2528 else:
2529 return False
2531 return True
2533 def my_gpodder_offer_autoupload(self):
2534 if not self.config.my_gpodder_autoupload:
2535 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')):
2536 self.config.my_gpodder_autoupload = True
2538 def on_download_from_mygpo(self, widget):
2539 if self.require_my_gpodder_authentication():
2540 client = my.MygPodderClient(self.config.my_gpodder_username, self.config.my_gpodder_password)
2541 opml_data = client.download_subscriptions()
2542 if len(opml_data) > 0:
2543 fp = open(gpodder.subscription_file, 'w')
2544 fp.write(opml_data)
2545 fp.close()
2546 (added, skipped) = (0, 0)
2547 i = opml.Importer(gpodder.subscription_file)
2549 existing = [c.url for c in self.channels]
2550 urls = [item['url'] for item in i.items if item['url'] not in existing]
2552 skipped = len(i.items) - len(urls)
2553 added = len(urls)
2555 self.add_podcast_list(urls)
2557 self.my_gpodder_offer_autoupload()
2558 if added > 0:
2559 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'), widget=self.treeChannels)
2560 elif widget is not None:
2561 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget=self.treeChannels)
2562 else:
2563 self.config.my_gpodder_password = ''
2564 self.on_download_from_mygpo(widget)
2565 else:
2566 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
2568 def on_upload_to_mygpo(self, widget):
2569 if self.require_my_gpodder_authentication():
2570 client = my.MygPodderClient(self.config.my_gpodder_username, self.config.my_gpodder_password)
2571 self.save_channels_opml()
2572 success, messages = client.upload_subscriptions(gpodder.subscription_file)
2573 if widget is not None:
2574 if not success:
2575 self.show_message('\n'.join(messages), _('Results of upload'), important=True)
2576 self.config.my_gpodder_password = ''
2577 self.on_upload_to_mygpo(widget)
2578 else:
2579 self.my_gpodder_offer_autoupload()
2580 self.show_message('\n'.join(messages), _('Results of upload'), widget=self.treeChannels)
2581 elif not success:
2582 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2583 elif widget is not None:
2584 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
2586 def on_itemAddChannel_activate(self, widget, *args):
2587 gPodderAddPodcast(self.gPodder, \
2588 add_urls_callback=self.add_podcast_list)
2590 def on_itemEditChannel_activate(self, widget, *args):
2591 if self.active_channel is None:
2592 title = _('No podcast selected')
2593 message = _('Please select a podcast in the podcasts list to edit.')
2594 self.show_message( message, title, widget=self.treeChannels)
2595 return
2597 callback_closed = lambda: self.update_podcast_list_model(selected=True)
2598 gPodderChannel(self.main_window, \
2599 channel=self.active_channel, \
2600 callback_closed=callback_closed, \
2601 cover_downloader=self.cover_downloader)
2603 def on_itemRemoveChannel_activate(self, widget, *args):
2604 if self.active_channel is None:
2605 title = _('No podcast selected')
2606 message = _('Please select a podcast in the podcasts list to remove.')
2607 self.show_message( message, title, widget=self.treeChannels)
2608 return
2610 try:
2611 if gpodder.ui.desktop:
2612 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2613 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2614 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2616 title = _('Remove podcast and episodes?')
2617 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2619 dialog.set_title(title)
2620 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2622 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2623 dialog.vbox.pack_start(cb_ask)
2624 cb_ask.show_all()
2625 result = (dialog.run() == gtk.RESPONSE_YES)
2626 keep_episodes = cb_ask.get_active()
2627 dialog.destroy()
2628 elif gpodder.ui.diablo:
2629 result = self.show_confirmation(_('Do you really want to remove this podcast and all downloaded episodes?'))
2630 keep_episodes = False
2631 elif gpodder.ui.fremantle:
2632 result = True
2633 keep_episodes = False
2635 if result:
2636 # delete downloaded episodes only if checkbox is unchecked
2637 if keep_episodes:
2638 log('Not removing downloaded episodes', sender=self)
2639 else:
2640 self.active_channel.remove_downloaded()
2642 # Clean up downloads and download directories
2643 self.clean_up_downloads()
2645 # cancel any active downloads from this channel
2646 for episode in self.active_channel.get_all_episodes():
2647 self.download_status_model.cancel_by_url(episode.url)
2649 # get the URL of the podcast we want to select next
2650 position = self.channels.index(self.active_channel)
2651 if position == len(self.channels)-1:
2652 # this is the last podcast, so select the URL
2653 # of the item before this one (i.e. the "new last")
2654 select_url = self.channels[position-1].url
2655 else:
2656 # there is a podcast after the deleted one, so
2657 # we simply select the one that comes after it
2658 select_url = self.channels[position+1].url
2660 title = self.active_channel.title
2662 # Remove the channel
2663 self.active_channel.delete(purge=not keep_episodes)
2664 self.channels.remove(self.active_channel)
2665 self.channel_list_changed = True
2666 self.save_channels_opml()
2668 if gpodder.ui.fremantle:
2669 self.show_message(_('Podcast removed: %s') % title)
2671 # Re-load the channels and select the desired new channel
2672 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2673 except:
2674 log('There has been an error removing the channel.', traceback=True, sender=self)
2675 self.update_podcasts_tab()
2677 def get_opml_filter(self):
2678 filter = gtk.FileFilter()
2679 filter.add_pattern('*.opml')
2680 filter.add_pattern('*.xml')
2681 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2682 return filter
2684 def on_item_import_from_file_activate(self, widget, filename=None):
2685 if filename is None:
2686 if gpodder.ui.desktop or gpodder.ui.fremantle:
2687 # FIXME: Hildonization on Fremantle
2688 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2689 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2690 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2691 elif gpodder.ui.diablo:
2692 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2693 dlg.set_filter(self.get_opml_filter())
2694 response = dlg.run()
2695 filename = None
2696 if response == gtk.RESPONSE_OK:
2697 filename = dlg.get_filename()
2698 dlg.destroy()
2700 if filename is not None:
2701 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
2702 custom_title=_('Import podcasts from OPML file'), \
2703 add_urls_callback=self.add_podcast_list, \
2704 hide_url_entry=True)
2705 dir.download_opml_file(filename)
2707 def on_itemExportChannels_activate(self, widget, *args):
2708 if not self.channels:
2709 title = _('Nothing to export')
2710 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2711 self.show_message(message, title, widget=self.treeChannels)
2712 return
2714 if gpodder.ui.desktop or gpodder.ui.fremantle:
2715 # FIXME: Hildonization on Fremantle
2716 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2717 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2718 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2719 elif gpodder.ui.diablo:
2720 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2721 dlg.set_filter(self.get_opml_filter())
2722 response = dlg.run()
2723 if response == gtk.RESPONSE_OK:
2724 filename = dlg.get_filename()
2725 dlg.destroy()
2726 exporter = opml.Exporter( filename)
2727 if exporter.write(self.channels):
2728 if len(self.channels) == 1:
2729 title = _('One subscription exported')
2730 else:
2731 title = _('%d subscriptions exported') % len(self.channels)
2732 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
2733 else:
2734 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
2735 else:
2736 dlg.destroy()
2738 def on_itemImportChannels_activate(self, widget, *args):
2739 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
2740 add_urls_callback=self.add_podcast_list)
2741 if not gpodder.ui.fremantle:
2742 util.idle_add(dir.download_opml_file, self.config.opml_url)
2744 def on_homepage_activate(self, widget, *args):
2745 util.open_website(gpodder.__url__)
2747 def on_wiki_activate(self, widget, *args):
2748 util.open_website('http://wiki.gpodder.org/')
2750 def on_bug_tracker_activate(self, widget, *args):
2751 if gpodder.ui.maemo:
2752 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
2753 else:
2754 util.open_website('http://bugs.gpodder.org/')
2756 def on_shop_activate(self, widget, *args):
2757 util.open_website('http://gpodder.org/shop')
2759 def on_wishlist_activate(self, widget, *args):
2760 util.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
2762 def on_itemAbout_activate(self, widget, *args):
2763 dlg = gtk.AboutDialog()
2764 dlg.set_name('gPodder')
2765 dlg.set_version(gpodder.__version__)
2766 dlg.set_copyright(gpodder.__copyright__)
2767 dlg.set_website(gpodder.__url__)
2768 dlg.set_translator_credits( _('translator-credits'))
2769 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2771 if gpodder.ui.desktop:
2772 # For the "GUI" version, we add some more
2773 # items to the about dialog (credits and logo)
2774 app_authors = [
2775 _('Maintainer:'),
2776 'Thomas Perl <thpinfo.com>',
2779 if os.path.exists(gpodder.credits_file):
2780 credits = open(gpodder.credits_file).read().strip().split('\n')
2781 app_authors += ['', _('Patches, bug reports and donations by:')]
2782 app_authors += credits
2784 dlg.set_authors(app_authors)
2785 try:
2786 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
2787 except:
2788 dlg.set_logo_icon_name('gpodder')
2790 dlg.run()
2792 def on_wNotebook_switch_page(self, widget, *args):
2793 page_num = args[1]
2794 if gpodder.ui.maemo:
2795 self.tool_downloads.set_active(page_num == 1)
2796 page = self.wNotebook.get_nth_page(page_num)
2797 tab_label = self.wNotebook.get_tab_label(page).get_text()
2798 if page_num == 0 and self.active_channel is not None:
2799 self.set_title(self.active_channel.title)
2800 else:
2801 self.set_title(tab_label)
2802 if page_num == 0:
2803 self.play_or_download()
2804 self.menuChannels.set_sensitive(True)
2805 self.menuSubscriptions.set_sensitive(True)
2806 # The message area in the downloads tab should be hidden
2807 # when the user switches away from the downloads tab
2808 if self.message_area is not None:
2809 self.message_area.hide()
2810 self.message_area = None
2811 else:
2812 self.menuChannels.set_sensitive(False)
2813 self.menuSubscriptions.set_sensitive(False)
2814 if gpodder.ui.desktop:
2815 self.toolDownload.set_sensitive(False)
2816 self.toolPlay.set_sensitive(False)
2817 self.toolTransfer.set_sensitive(False)
2818 self.toolCancel.set_sensitive(False)
2820 def on_treeChannels_row_activated(self, widget, path, *args):
2821 # double-click action of the podcast list or enter
2822 self.treeChannels.set_cursor(path)
2824 def on_treeChannels_cursor_changed(self, widget, *args):
2825 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2827 if model is not None and iter is not None:
2828 old_active_channel = self.active_channel
2829 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
2831 if self.active_channel == old_active_channel:
2832 return
2834 if gpodder.ui.maemo:
2835 self.set_title(self.active_channel.title)
2836 self.itemEditChannel.set_visible(True)
2837 self.itemRemoveChannel.set_visible(True)
2838 else:
2839 self.active_channel = None
2840 self.itemEditChannel.set_visible(False)
2841 self.itemRemoveChannel.set_visible(False)
2843 self.update_episode_list_model()
2845 def on_btnEditChannel_clicked(self, widget, *args):
2846 self.on_itemEditChannel_activate( widget, args)
2848 def get_selected_episodes(self):
2849 """Get a list of selected episodes from treeAvailable"""
2850 selection = self.treeAvailable.get_selection()
2851 model, paths = selection.get_selected_rows()
2853 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
2854 return episodes
2856 def on_transfer_selected_episodes(self, widget):
2857 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
2859 def on_playback_selected_episodes(self, widget):
2860 self.playback_episodes(self.get_selected_episodes())
2862 def on_shownotes_selected_episodes(self, widget):
2863 episodes = self.get_selected_episodes()
2864 if episodes:
2865 episode = episodes.pop(0)
2866 self.show_episode_shownotes(episode)
2867 else:
2868 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
2870 def on_download_selected_episodes(self, widget):
2871 episodes = self.get_selected_episodes()
2872 self.download_episode_list(episodes)
2873 self.update_episode_list_icons([episode.url for episode in episodes])
2874 self.play_or_download()
2876 def on_treeAvailable_row_activated(self, widget, path, view_column):
2877 """Double-click/enter action handler for treeAvailable"""
2878 # We should only have one one selected as it was double clicked!
2879 e = self.get_selected_episodes()[0]
2881 if (self.config.double_click_episode_action == 'download'):
2882 # If the episode has already been downloaded and exists then play it
2883 if e.was_downloaded(and_exists=True):
2884 self.playback_episodes(self.get_selected_episodes())
2885 # else download it if it is not already downloading
2886 elif not self.episode_is_downloading(e):
2887 self.download_episode_list([e])
2888 self.update_episode_list_icons([e.url])
2889 self.play_or_download()
2890 elif (self.config.double_click_episode_action == 'stream'):
2891 # If we happen to have downloaded this episode simple play it
2892 if e.was_downloaded(and_exists=True):
2893 self.playback_episodes(self.get_selected_episodes())
2894 # else if streaming is possible stream it
2895 elif self.streaming_possible():
2896 self.playback_episodes(self.get_selected_episodes())
2897 else:
2898 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
2899 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
2900 else:
2901 # default action is to display show notes
2902 self.on_shownotes_selected_episodes(widget)
2904 def show_episode_shownotes(self, episode):
2905 if self.episode_shownotes_window is None:
2906 log('First-time use of episode window --- creating', sender=self)
2907 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
2908 _download_episode_list=self.download_episode_list, \
2909 _playback_episodes=self.playback_episodes, \
2910 _delete_episode_list=self.delete_episode_list, \
2911 _episode_list_status_changed=self.episode_list_status_changed, \
2912 _cancel_task_list=self.cancel_task_list, \
2913 _episode_is_downloading=self.episode_is_downloading)
2914 self.episode_shownotes_window.show(episode)
2915 if self.episode_is_downloading(episode):
2916 self.update_downloads_list()
2918 def auto_update_procedure(self, first_run=False):
2919 log('auto_update_procedure() got called', sender=self)
2920 if not first_run and self.config.auto_update_feeds and self.is_iconified():
2921 self.update_feed_cache(force_update=True)
2923 next_update = 60*1000*self.config.auto_update_frequency
2924 gobject.timeout_add(next_update, self.auto_update_procedure)
2925 return False
2927 def on_treeDownloads_row_activated(self, widget, *args):
2928 # Use the standard way of working on the treeview
2929 selection = self.treeDownloads.get_selection()
2930 (model, paths) = selection.get_selected_rows()
2931 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
2933 for tree_row_reference, task in selected_tasks:
2934 if task.status in (task.DOWNLOADING, task.QUEUED):
2935 task.status = task.PAUSED
2936 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
2937 self.download_queue_manager.add_task(task)
2938 self.enable_download_list_update()
2939 elif task.status == task.DONE:
2940 model.remove(model.get_iter(tree_row_reference.get_path()))
2942 self.play_or_download()
2944 # Update the tab title and downloads list
2945 self.update_downloads_list()
2947 def on_item_cancel_download_activate(self, widget):
2948 if self.wNotebook.get_current_page() == 0:
2949 selection = self.treeAvailable.get_selection()
2950 (model, paths) = selection.get_selected_rows()
2951 urls = [model.get_value(model.get_iter(path), \
2952 self.episode_list_model.C_URL) for path in paths]
2953 selected_tasks = [task for task in self.download_tasks_seen \
2954 if task.url in urls]
2955 else:
2956 selection = self.treeDownloads.get_selection()
2957 (model, paths) = selection.get_selected_rows()
2958 selected_tasks = [model.get_value(model.get_iter(path), \
2959 self.download_status_model.C_TASK) for path in paths]
2960 self.cancel_task_list(selected_tasks)
2962 def on_btnCancelAll_clicked(self, widget, *args):
2963 self.cancel_task_list(self.download_tasks_seen)
2965 def on_btnDownloadedDelete_clicked(self, widget, *args):
2966 if self.wNotebook.get_current_page() == 1:
2967 # Downloads tab visibile - skip (for now)
2968 return
2970 episodes = self.get_selected_episodes()
2971 self.delete_episode_list(episodes)
2973 def on_key_press(self, widget, event):
2974 # Allow tab switching with Ctrl + PgUp/PgDown
2975 if event.state & gtk.gdk.CONTROL_MASK:
2976 if event.keyval == gtk.keysyms.Page_Up:
2977 self.wNotebook.prev_page()
2978 return True
2979 elif event.keyval == gtk.keysyms.Page_Down:
2980 self.wNotebook.next_page()
2981 return True
2983 # After this code we only handle Maemo hardware keys,
2984 # so if we are not a Maemo app, we don't do anything
2985 if not gpodder.ui.maemo:
2986 return False
2988 diff = 0
2989 if event.keyval == gtk.keysyms.F7: #plus
2990 diff = 1
2991 elif event.keyval == gtk.keysyms.F8: #minus
2992 diff = -1
2994 if diff != 0 and not self.currently_updating:
2995 selection = self.treeChannels.get_selection()
2996 (model, iter) = selection.get_selected()
2997 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
2998 selection.select_path(new_path)
2999 self.treeChannels.set_cursor(new_path)
3000 return True
3002 return False
3004 def on_iconify(self):
3005 if self.tray_icon:
3006 self.gPodder.set_skip_taskbar_hint(True)
3007 if self.config.minimize_to_tray:
3008 self.tray_icon.set_visible(True)
3009 else:
3010 self.gPodder.set_skip_taskbar_hint(False)
3012 def on_uniconify(self):
3013 if self.tray_icon:
3014 self.gPodder.set_skip_taskbar_hint(False)
3015 if self.config.minimize_to_tray:
3016 self.tray_icon.set_visible(False)
3017 else:
3018 self.gPodder.set_skip_taskbar_hint(False)
3020 def uniconify_main_window(self):
3021 if self.is_iconified():
3022 self.gPodder.present()
3024 def iconify_main_window(self):
3025 if not self.is_iconified():
3026 self.gPodder.iconify()
3028 def update_podcasts_tab(self):
3029 if len(self.channels):
3030 if gpodder.ui.fremantle:
3031 self.button_podcasts.set_value(_('%d subscriptions') % len(self.channels))
3032 else:
3033 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3034 else:
3035 if gpodder.ui.fremantle:
3036 self.button_podcasts.set_value(_('No subscriptions'))
3037 else:
3038 self.label2.set_text(_('Podcasts'))
3040 @dbus.service.method(gpodder.dbus_interface)
3041 def show_gui_window(self):
3042 self.gPodder.present()
3044 @dbus.service.method(gpodder.dbus_interface)
3045 def subscribe_to_url(self, url):
3046 gPodderAddPodcast(self.gPodder,
3047 add_urls_callback=self.add_podcast_list,
3048 preset_url=url)
3050 @dbus.service.method(gpodder.dbus_interface)
3051 def mark_episode_played(self, filename):
3052 if filename is None:
3053 return False
3055 for channel in self.channels:
3056 for episode in channel.get_all_episodes():
3057 fn = episode.local_filename(create=False, check_only=True)
3058 if fn == filename:
3059 episode.mark(is_played=True)
3060 self.db.commit()
3061 self.update_episode_list_icons([episode.url])
3062 self.update_podcast_list_model([episode.channel.url])
3063 return True
3065 return False
3068 def main(options=None):
3069 gobject.threads_init()
3070 gobject.set_application_name('gPodder')
3072 if gpodder.ui.diablo:
3073 # Try to enable the custom icon theme for gPodder on Maemo
3074 settings = gtk.settings_get_default()
3075 settings.set_string_property('gtk-icon-theme-name', \
3076 'gpodder', __file__)
3078 gtk.window_set_default_icon_name('gpodder')
3079 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
3081 try:
3082 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
3083 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
3084 except dbus.exceptions.DBusException, dbe:
3085 log('Warning: Cannot get "on the bus".', traceback=True)
3086 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
3087 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
3088 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
3089 dlg.set_title('gPodder')
3090 dlg.run()
3091 dlg.destroy()
3092 sys.exit(0)
3094 util.make_directory(gpodder.home)
3095 config = UIConfig(gpodder.config_file)
3097 if gpodder.ui.diablo:
3098 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3099 # folder exists there (allow moving "gpodder" between SD cards or USB)
3100 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3101 if not os.path.exists(config.download_dir):
3102 log('Downloads might have been moved. Trying to locate them...')
3103 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user/MyDocs']:
3104 dir = os.path.join(basedir, 'gpodder')
3105 if os.path.exists(dir):
3106 log('Downloads found in: %s', dir)
3107 config.download_dir = dir
3108 break
3109 else:
3110 log('Downloads NOT FOUND in %s', dir)
3112 if config.enable_fingerscroll:
3113 BuilderWidget.use_fingerscroll = True
3114 elif gpodder.ui.fremantle:
3115 # FIXME: Move download_dir from ~/gPodder-Podcasts to default setting
3116 pass
3118 gp = gPodder(bus_name, config)
3120 # Handle options
3121 if options.subscribe:
3122 util.idle_add(gp.subscribe_to_url, options.subscribe)
3124 gp.run()