Episode list filtering (all, hide deleted, downloaded)
[gpodder.git] / src / gpodder / gui.py
blob07a8cf98393c9d349093455ae939646abd6a2f80
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 sync
70 from gpodder import download
71 from gpodder import my
72 from gpodder.liblogger import log
74 _ = gpodder.gettext
76 try:
77 from gpodder import trayicon
78 have_trayicon = True
79 except Exception, exc:
80 log('Warning: Could not import gpodder.trayicon.', traceback=True)
81 log('Warning: This probably means your PyGTK installation is too old!')
82 have_trayicon = False
84 from gpodder.model import PodcastChannel
85 from gpodder.dbsqlite import Database
87 from gpodder.gtkui.model import PodcastListModel
88 from gpodder.gtkui.model import EpisodeListModel
89 from gpodder.gtkui.config import UIConfig
90 from gpodder.gtkui.download import DownloadStatusModel
91 from gpodder.gtkui.services import CoverDownloader
92 from gpodder.gtkui.widgets import SimpleMessageArea
93 from gpodder.gtkui.desktopfile import UserAppsReader
95 from gpodder.gtkui.interface.common import BuilderWidget
96 from gpodder.gtkui.interface.channel import gPodderChannel
97 from gpodder.gtkui.interface.addpodcast import gPodderAddPodcast
99 if gpodder.interface == gpodder.GUI:
100 from gpodder.gtkui.interface.preferences import gPodderPreferences
101 from gpodder.gtkui.interface.syncprogress import gPodderSyncProgress
102 from gpodder.gtkui.interface.deviceplaylist import gPodderDevicePlaylist
103 else:
104 from gpodder.gtkui.maemo.preferences import gPodderDiabloPreferences as gPodderPreferences
106 from gpodder.gtkui.interface.shownotes import gPodderShownotes
107 from gpodder.gtkui.interface.podcastdirectory import gPodderPodcastDirectory
108 from gpodder.gtkui.interface.episodeselector import gPodderEpisodeSelector
109 from gpodder.gtkui.interface.dependencymanager import gPodderDependencyManager
110 from gpodder.gtkui.interface.welcome import gPodderWelcome
112 if gpodder.interface == gpodder.MAEMO:
113 import hildon
115 class gPodder(BuilderWidget, dbus.service.Object):
116 finger_friendly_widgets = ['btnCancelFeedUpdate', 'label2', 'labelDownloads', 'btnCleanUpDownloads']
117 ENTER_URL_TEXT = _('Enter podcast URL...')
118 APPMENU_ACTIONS = ('itemUpdate', 'itemDownloadAllNew', 'itemPreferences')
119 TREEVIEW_WIDGETS = ('treeAvailable', 'treeChannels', 'treeDownloads')
121 def __init__(self, bus_name, config):
122 dbus.service.Object.__init__(self, object_path=gpodder.dbus_gui_object_path, bus_name=bus_name)
123 self.db = Database(gpodder.database_file)
124 self.config = config
125 BuilderWidget.__init__(self, None)
127 def new(self):
128 if gpodder.interface == gpodder.MAEMO:
129 # Maemo-specific changes to the UI
130 gpodder.icon_file = gpodder.icon_file.replace('.svg', '.png')
132 self.app = hildon.Program()
133 gtk.set_application_name('gPodder')
134 self.window = hildon.Window()
135 self.window.connect('delete-event', self.on_gPodder_delete_event)
136 self.window.connect('window-state-event', self.window_state_event)
138 self.itemUpdateChannel.set_visible(True)
140 # Remove old toolbar from its parent widget
141 self.toolbar.get_parent().remove(self.toolbar)
143 toolbar = gtk.Toolbar()
144 toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
146 self.btnUpdateFeeds.get_parent().remove(self.btnUpdateFeeds)
148 self.btnUpdateFeeds = gtk.ToolButton(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_SMALL_TOOLBAR), _('Update all'))
149 self.btnUpdateFeeds.set_is_important(True)
150 self.btnUpdateFeeds.connect('clicked', self.on_itemUpdate_activate)
151 toolbar.insert(self.btnUpdateFeeds, -1)
152 self.btnUpdateFeeds.show_all()
154 self.btnUpdateSelectedFeed = gtk.ToolButton(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_SMALL_TOOLBAR), _('Update selected'))
155 self.btnUpdateSelectedFeed.set_is_important(True)
156 self.btnUpdateSelectedFeed.connect('clicked', self.on_itemUpdateChannel_activate)
157 toolbar.insert(self.btnUpdateSelectedFeed, -1)
158 self.btnUpdateSelectedFeed.show_all()
160 self.toolFeedUpdateProgress = gtk.ToolItem()
161 self.pbFeedUpdate.reparent(self.toolFeedUpdateProgress)
162 self.toolFeedUpdateProgress.set_expand(True)
163 toolbar.insert(self.toolFeedUpdateProgress, -1)
164 self.toolFeedUpdateProgress.hide()
166 self.btnCancelFeedUpdate = gtk.ToolButton(gtk.STOCK_CLOSE)
167 self.btnCancelFeedUpdate.connect('clicked', self.on_btnCancelFeedUpdate_clicked)
168 toolbar.insert(self.btnCancelFeedUpdate, -1)
169 self.btnCancelFeedUpdate.hide()
171 self.toolbarSpacer = gtk.SeparatorToolItem()
172 self.toolbarSpacer.set_draw(False)
173 self.toolbarSpacer.set_expand(True)
174 toolbar.insert(self.toolbarSpacer, -1)
175 self.toolbarSpacer.show()
177 self.wNotebook.set_show_tabs(False)
178 self.tool_downloads = gtk.ToggleToolButton(gtk.STOCK_GO_DOWN)
179 self.tool_downloads.connect('toggled', self.on_tool_downloads_toggled)
180 self.tool_downloads.set_label(_('Downloads'))
181 self.tool_downloads.set_is_important(True)
182 toolbar.insert(self.tool_downloads, -1)
183 self.tool_downloads.show_all()
185 self.toolPreferences = gtk.ToolButton(gtk.STOCK_PREFERENCES)
186 self.toolPreferences.connect('clicked', self.on_itemPreferences_activate)
187 toolbar.insert(self.toolPreferences, -1)
188 self.toolPreferences.show()
190 self.toolQuit = gtk.ToolButton(gtk.STOCK_QUIT)
191 self.toolQuit.connect('clicked', self.on_gPodder_delete_event)
192 toolbar.insert(self.toolQuit, -1)
193 self.toolQuit.show()
195 # Add and replace toolbar with our new one
196 toolbar.show()
197 self.window.add_toolbar(toolbar)
198 self.toolbar = toolbar
200 self.app.add_window(self.window)
201 self.vMain.reparent(self.window)
202 self.gPodder = self.window
204 # Reparent the main menu
205 menu = gtk.Menu()
206 for child in self.mainMenu.get_children():
207 child.get_parent().remove(child)
208 menu.append(self.set_finger_friendly(child))
209 menu.append(self.set_finger_friendly(self.itemQuit.create_menu_item()))
211 if hasattr(hildon, 'AppMenu'):
212 # Maemo 5 - use the new AppMenu with Buttons
213 self.appmenu = hildon.AppMenu()
214 for action_name in self.APPMENU_ACTIONS:
215 action = getattr(self, action_name)
216 b = gtk.Button('')
217 action.connect_proxy(b)
218 self.appmenu.append(b)
219 b = gtk.Button(_('Classic menu'))
220 b.connect('clicked', lambda b: menu.popup(None, None, None, 1, 0))
221 self.appmenu.append(b)
222 self.window.set_app_menu(self.appmenu)
223 else:
224 # Maemo 4 - just "reparent" the menu to the hildon window
225 self.window.set_menu(menu)
227 self.mainMenu.destroy()
228 self.window.show()
230 # do some widget hiding
231 self.itemTransferSelected.set_visible(False)
232 self.item_email_subscriptions.set_visible(False)
233 self.menuView.set_visible(False)
235 # get screen real estate
236 self.hboxContainer.set_border_width(0)
238 # Offer importing of videocenter podcasts
239 if os.path.exists(os.path.expanduser('~/videocenter')):
240 self.item_upgrade_from_videocenter.set_visible(True)
242 self.gPodder.connect('key-press-event', self.on_key_press)
243 self.bluetooth_available = util.bluetooth_available()
245 if gpodder.win32:
246 # FIXME: Implement e-mail sending of list in win32
247 self.item_email_subscriptions.set_sensitive(False)
249 if not gpodder.interface == gpodder.MAEMO and not self.config.show_toolbar:
250 self.toolbar.hide()
252 self.config.add_observer(self.on_config_changed)
254 self.uar = None
255 self.tray_icon = None
256 self.episode_shownotes_window = None
258 self.download_status_model = DownloadStatusModel()
259 self.download_queue_manager = download.DownloadQueueManager(self.config)
261 self.fullscreen = False
262 self.minimized = False
263 self.gPodder.connect('window-state-event', self.window_state_event)
265 self.show_hide_tray_icon()
267 self.itemShowToolbar.set_active(self.config.show_toolbar)
268 self.itemShowDescription.set_active(self.config.episode_list_descriptions)
270 self.config.connect_gtk_window(self.gPodder, 'main_window')
271 self.config.connect_gtk_paned( 'paned_position', self.channelPaned)
273 self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
274 self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
275 self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
276 self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
278 # Then the amount of maximum downloads changes, notify the queue manager
279 changed_cb = lambda spinbutton: self.download_queue_manager.spawn_and_retire_threads()
280 self.spinMaxDownloads.connect('value-changed', changed_cb)
282 self.default_title = None
283 if gpodder.__version__.rfind('git') != -1:
284 self.set_title('gPodder %s' % gpodder.__version__)
285 else:
286 title = self.gPodder.get_title()
287 if title is not None:
288 self.set_title(title)
289 else:
290 self.set_title(_('gPodder'))
292 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
294 # Set up podcast channel tree view widget
295 self.treeChannels.set_enable_search(True)
296 self.treeChannels.set_search_column(PodcastListModel.C_TITLE)
297 self.treeChannels.set_headers_visible(False)
299 iconcolumn = gtk.TreeViewColumn('')
300 iconcell = gtk.CellRendererPixbuf()
301 iconcolumn.pack_start(iconcell, False)
302 iconcolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
303 self.treeChannels.append_column(iconcolumn)
305 namecolumn = gtk.TreeViewColumn('')
306 namecell = gtk.CellRendererText()
307 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
308 namecolumn.pack_start(namecell, True)
309 namecolumn.add_attribute(namecell, 'markup', PodcastListModel.C_DESCRIPTION)
311 iconcell = gtk.CellRendererPixbuf()
312 iconcell.set_property('xalign', 1.0)
313 namecolumn.pack_start(iconcell, False)
314 namecolumn.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
315 namecolumn.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
316 self.treeChannels.append_column(namecolumn)
318 self.cover_downloader = CoverDownloader()
320 # Generate list models for podcasts and their episodes
321 self.podcast_list_model = PodcastListModel(self.config.podcast_list_icon_size, self.cover_downloader)
322 self.treeChannels.set_model(self.podcast_list_model)
324 self.episode_list_model = EpisodeListModel()
326 if self.config.episode_list_view_mode == EpisodeListModel.VIEW_UNDELETED:
327 self.item_view_episodes_undeleted.set_active(True)
328 elif self.config.episode_list_view_mode == EpisodeListModel.VIEW_DOWNLOADED:
329 self.item_view_episodes_downloaded.set_active(True)
330 else:
331 self.item_view_episodes_all.set_active(True)
333 self.treeAvailable.set_model(self.episode_list_model.get_filtered_model())
335 # enable alternating colors hint
336 self.treeAvailable.set_rules_hint( True)
337 self.treeChannels.set_rules_hint( True)
339 # connect to tooltip signals
340 try:
341 self.treeChannels.set_property('has-tooltip', True)
342 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
343 self.treeAvailable.set_property('has-tooltip', True)
344 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
345 except:
346 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
347 self.last_tooltip_channel = None
348 self.last_tooltip_episode = None
349 self.podcast_list_can_tooltip = True
350 self.episode_list_can_tooltip = True
352 self.currently_updating = False
354 # Add our context menu to treeAvailable
355 if gpodder.interface == gpodder.MAEMO:
356 self.treeview_available_buttonpress = (0, 0)
357 self.treeAvailable.connect('button-press-event', self.treeview_button_savepos)
358 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
360 self.treeview_channels_buttonpress = (0, 0)
361 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
362 self.treeChannels.connect('button-release-event', self.treeview_channels_button_released)
363 else:
364 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
365 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
367 self.treeDownloads.connect('button-press-event', self.treeview_downloads_button_pressed)
369 iconcell = gtk.CellRendererPixbuf()
370 if gpodder.interface == gpodder.MAEMO:
371 iconcell.set_fixed_size(-1, 52)
372 status_column_label = ''
373 else:
374 status_column_label = _('Status')
375 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=EpisodeListModel.C_STATUS_ICON)
377 namecell = gtk.CellRendererText()
378 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
379 namecolumn = gtk.TreeViewColumn(_('Episode'), namecell, markup=EpisodeListModel.C_DESCRIPTION)
380 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
381 namecolumn.set_resizable(True)
382 namecolumn.set_expand(True)
384 sizecell = gtk.CellRendererText()
385 sizecolumn = gtk.TreeViewColumn(_('Size'), sizecell, text=EpisodeListModel.C_FILESIZE_TEXT)
387 releasecell = gtk.CellRendererText()
388 releasecolumn = gtk.TreeViewColumn(_('Released'), releasecell, text=EpisodeListModel.C_PUBLISHED_TEXT)
390 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
391 itemcolumn.set_reorderable(gpodder.interface != gpodder.MAEMO)
392 self.treeAvailable.append_column(itemcolumn)
394 if gpodder.interface == gpodder.MAEMO:
395 # Due to screen space contraints, we
396 # hide these columns here by default
397 self.column_size = sizecolumn
398 self.column_released = releasecolumn
399 self.column_released.set_visible(False)
400 self.column_size.set_visible(False)
402 # enable search in treeavailable
403 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
405 # on Maemo 5, we need to set hildon-ui-mode of TreeView widgets to 1
406 if gpodder.interface == gpodder.MAEMO:
407 HUIM = 'hildon-ui-mode'
408 if HUIM in [p.name for p in gobject.list_properties(gtk.TreeView)]:
409 for treeview_name in self.TREEVIEW_WIDGETS:
410 treeview = getattr(self, treeview_name)
411 treeview.set_property(HUIM, 1)
413 # enable multiple selection support
414 if gpodder.interface == gpodder.MAEMO:
415 self.treeAvailable.get_selection().set_mode(gtk.SELECTION_SINGLE)
416 else:
417 self.treeAvailable.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
418 self.treeDownloads.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
420 if hasattr(self.treeDownloads, 'set_rubber_banding'):
421 # Available in PyGTK 2.10 and above
422 self.treeDownloads.set_rubber_banding(True)
424 # columns and renderers for "download progress" tab
425 # First column: [ICON] Episodename
426 column = gtk.TreeViewColumn(_('Episode'))
428 cell = gtk.CellRendererPixbuf()
429 if gpodder.interface == gpodder.MAEMO:
430 cell.set_property('stock-size', gtk.ICON_SIZE_DIALOG)
431 else:
432 cell.set_property('stock-size', gtk.ICON_SIZE_MENU)
433 column.pack_start(cell, expand=False)
434 column.add_attribute(cell, 'stock-id', \
435 DownloadStatusModel.C_ICON_NAME)
437 cell = gtk.CellRendererText()
438 cell.set_property('ellipsize', pango.ELLIPSIZE_END)
439 column.pack_start(cell, expand=True)
440 column.add_attribute(cell, 'text', DownloadStatusModel.C_NAME)
442 column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
443 column.set_resizable(True)
444 column.set_expand(True)
445 self.treeDownloads.append_column(column)
447 # Second column: Progress
448 column = gtk.TreeViewColumn(_('Progress'), gtk.CellRendererProgress(),
449 value=DownloadStatusModel.C_PROGRESS, \
450 text=DownloadStatusModel.C_PROGRESS_TEXT)
451 self.treeDownloads.append_column(column)
453 # Third column: Size
454 if gpodder.interface != gpodder.MAEMO:
455 column = gtk.TreeViewColumn(_('Size'), gtk.CellRendererText(),
456 text=DownloadStatusModel.C_SIZE_TEXT)
457 self.treeDownloads.append_column(column)
459 # Fourth column: Speed
460 column = gtk.TreeViewColumn(_('Speed'), gtk.CellRendererText(),
461 text=DownloadStatusModel.C_SPEED_TEXT)
462 self.treeDownloads.append_column(column)
464 # Fifth column: Status
465 column = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(),
466 text=DownloadStatusModel.C_STATUS_TEXT)
467 self.treeDownloads.append_column(column)
469 # After we've set up most of the window, show it :)
470 if not gpodder.interface == gpodder.MAEMO:
471 self.gPodder.show()
473 if self.config.start_iconified:
474 self.iconify_main_window()
475 if self.tray_icon and self.config.minimize_to_tray:
476 self.tray_icon.set_visible(False)
478 self.cover_downloader.register('cover-available', self.cover_download_finished)
479 self.cover_downloader.register('cover-removed', self.cover_file_removed)
481 self.treeDownloads.set_model(self.download_status_model)
482 self.download_tasks_seen = set()
483 self.download_list_update_enabled = False
484 self.last_download_count = 0
486 #Add Drag and Drop Support
487 flags = gtk.DEST_DEFAULT_ALL
488 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
489 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
490 self.treeChannels.drag_dest_set( flags, targets, actions)
491 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
493 # Subscribed channels
494 self.active_channel = None
495 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
496 self.channel_list_changed = True
497 self.update_podcasts_tab()
499 # load list of user applications for audio playback
500 self.user_apps_reader = UserAppsReader(['audio', 'video'])
501 threading.Thread(target=self.read_apps).start()
503 # Set the "Device" menu item for the first time
504 self.update_item_device()
506 # Last folder used for saving episodes
507 self.folder_for_saving_episodes = None
509 # Now, update the feed cache, when everything's in place
510 self.btnUpdateFeeds.show()
511 self.updating_feed_cache = False
512 self.feed_cache_update_cancelled = False
513 self.update_feed_cache(force_update=self.config.update_on_startup)
515 # Look for partial file downloads
516 partial_files = glob.glob(os.path.join(self.config.download_dir, '*', '*.partial'))
518 # Message area
519 self.message_area = None
521 resumable_episodes = []
522 if len(partial_files) > 0:
523 for f in partial_files:
524 correct_name = f[:-len('.partial')] # strip ".partial"
525 log('Searching episode for file: %s', correct_name, sender=self)
526 found_episode = False
527 for c in self.channels:
528 for e in c.get_all_episodes():
529 if e.local_filename(create=False, check_only=True) == correct_name:
530 log('Found episode: %s', e.title, sender=self)
531 resumable_episodes.append(e)
532 found_episode = True
533 if found_episode:
534 break
535 if found_episode:
536 break
537 if not found_episode:
538 log('Partial file without episode: %s', f, sender=self)
539 util.delete_file(f)
541 if len(resumable_episodes):
542 self.download_episode_list_paused(resumable_episodes)
543 self.message_area = SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
544 self.vboxDownloadStatusWidgets.pack_start(self.message_area, expand=False)
545 self.vboxDownloadStatusWidgets.reorder_child(self.message_area, 0)
546 self.message_area.show_all()
547 self.wNotebook.set_current_page(1)
549 self.clean_up_downloads(delete_partial=False)
550 else:
551 self.clean_up_downloads(delete_partial=True)
553 # Start the auto-update procedure
554 self.auto_update_procedure(first_run=True)
556 # Delete old episodes if the user wishes to
557 if self.config.auto_remove_old_episodes:
558 old_episodes = self.get_old_episodes()
559 if len(old_episodes) > 0:
560 self.delete_episode_list(old_episodes, confirm=False)
561 self.updateComboBox()
563 # First-time users should be asked if they want to see the OPML
564 if len(self.channels) == 0:
565 util.idle_add(self.on_itemUpdate_activate)
567 def enable_download_list_update(self):
568 if not self.download_list_update_enabled:
569 gobject.timeout_add(1500, self.update_downloads_list)
570 self.download_list_update_enabled = True
572 def on_btnCleanUpDownloads_clicked(self, button):
573 model = self.download_status_model
575 all_tasks = [(gtk.TreeRowReference(model, row.path), row[0]) for row in model]
576 changed_episode_urls = []
577 for row_reference, task in all_tasks:
578 if task.status in (task.DONE, task.CANCELLED, task.FAILED):
579 model.remove(model.get_iter(row_reference.get_path()))
580 try:
581 # We don't "see" this task anymore - remove it;
582 # this is needed, so update_episode_list_icons()
583 # below gets the correct list of "seen" tasks
584 self.download_tasks_seen.remove(task)
585 except KeyError, key_error:
586 log('Cannot remove task from "seen" list: %s', task, sender=self)
587 changed_episode_urls.append(task.url)
588 # Tell the task that it has been removed (so it can clean up)
589 task.removed_from_list()
591 # Tell the podcasts tab to update icons for our removed podcasts
592 self.update_episode_list_icons(changed_episode_urls)
594 # Update the tab title and downloads list
595 self.update_downloads_list()
597 def on_tool_downloads_toggled(self, toolbutton):
598 if toolbutton.get_active():
599 self.wNotebook.set_current_page(1)
600 else:
601 self.wNotebook.set_current_page(0)
603 def update_downloads_list(self):
604 try:
605 model = self.download_status_model
607 downloading, failed, finished, queued, others = 0, 0, 0, 0, 0
608 total_speed, total_size, done_size = 0, 0, 0
610 # Keep a list of all download tasks that we've seen
611 download_tasks_seen = set()
613 # Remember the progress and speed for the episode that
614 # has been opened in the episode shownotes dialog (if any)
615 if self.episode_shownotes_window is not None:
616 episode_window_episode = self.episode_shownotes_window.episode
617 episode_window_progress = 0.0
618 episode_window_speed = 0.0
619 else:
620 episode_window_episode = None
622 # Do not go through the list of the model is not (yet) available
623 if model is None:
624 model = ()
626 for row in model:
627 self.download_status_model.request_update(row.iter)
629 task = row[self.download_status_model.C_TASK]
630 speed, size, status, progress = task.speed, task.total_size, task.status, task.progress
632 total_size += size
633 done_size += size*progress
635 if episode_window_episode is not None and \
636 episode_window_episode.url == task.url:
637 episode_window_progress = progress
638 episode_window_speed = speed
640 download_tasks_seen.add(task)
642 if status == download.DownloadTask.DOWNLOADING:
643 downloading += 1
644 total_speed += speed
645 elif status == download.DownloadTask.FAILED:
646 failed += 1
647 elif status == download.DownloadTask.DONE:
648 finished += 1
649 elif status == download.DownloadTask.QUEUED:
650 queued += 1
651 else:
652 others += 1
654 # Remember which tasks we have seen after this run
655 self.download_tasks_seen = download_tasks_seen
657 text = [_('Downloads')]
658 if downloading + failed + finished + queued > 0:
659 s = []
660 if downloading > 0:
661 s.append(_('%d active') % downloading)
662 if failed > 0:
663 s.append(_('%d failed') % failed)
664 if finished > 0:
665 s.append(_('%d done') % finished)
666 if queued > 0:
667 s.append(_('%d queued') % queued)
668 text.append(' (' + ', '.join(s)+')')
669 self.labelDownloads.set_text(''.join(text))
671 if gpodder.interface == gpodder.MAEMO:
672 sum = downloading + failed + finished + queued + others
673 if sum:
674 self.tool_downloads.set_label(_('Downloads (%d)') % sum)
675 else:
676 self.tool_downloads.set_label(_('Downloads'))
678 title = [self.default_title]
680 # We have to update all episodes/channels for which the status has
681 # changed. Accessing task.status_changed has the side effect of
682 # re-setting the changed flag, so we need to get the "changed" list
683 # of tuples first and split it into two lists afterwards
684 changed = [(task.url, task.podcast_url) for task in \
685 self.download_tasks_seen if task.status_changed]
686 episode_urls = [episode_url for episode_url, channel_url in changed]
687 channel_urls = [channel_url for episode_url, channel_url in changed]
689 count = downloading + queued
690 if count > 0:
691 if count == 1:
692 title.append( _('downloading one file'))
693 elif count > 1:
694 title.append( _('downloading %d files') % count)
696 if total_size > 0:
697 percentage = 100.0*done_size/total_size
698 else:
699 percentage = 0.0
700 total_speed = util.format_filesize(total_speed)
701 title[1] += ' (%d%%, %s/s)' % (percentage, total_speed)
702 if self.tray_icon is not None:
703 # Update the tray icon status and progress bar
704 self.tray_icon.set_status(self.tray_icon.STATUS_DOWNLOAD_IN_PROGRESS, title[1])
705 self.tray_icon.draw_progress_bar(percentage/100.)
706 elif self.last_download_count > 0:
707 if self.tray_icon is not None:
708 # Update the tray icon status
709 self.tray_icon.set_status()
710 self.tray_icon.downloads_finished(self.download_tasks_seen)
711 if gpodder.interface == gpodder.MAEMO:
712 hildon.hildon_banner_show_information(self.gPodder, None, 'gPodder: %s' % _('All downloads finished'))
713 log('All downloads have finished.', sender=self)
714 if self.config.cmd_all_downloads_complete:
715 util.run_external_command(self.config.cmd_all_downloads_complete)
716 self.last_download_count = count
718 self.gPodder.set_title(' - '.join(title))
720 self.update_episode_list_icons(episode_urls)
721 if self.episode_shownotes_window is not None and \
722 self.episode_shownotes_window.gPodderShownotes.get_property('visible'):
723 self.episode_shownotes_window.download_status_changed(episode_urls)
724 self.episode_shownotes_window.download_status_progress(episode_window_progress, episode_window_speed)
725 self.play_or_download()
726 if channel_urls:
727 self.updateComboBox(only_these_urls=channel_urls)
729 if not self.download_queue_manager.are_queued_or_active_tasks():
730 self.download_list_update_enabled = False
732 return self.download_list_update_enabled
733 except Exception, e:
734 log('Exception happened while updating download list.', sender=self, traceback=True)
735 self.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e)), _('Unhandled exception'), important=True)
736 # We return False here, so the update loop won't be called again,
737 # that's why we require the restart of gPodder in the message.
738 return False
740 def on_config_changed(self, name, old_value, new_value):
741 if name == 'show_toolbar' and gpodder.interface != gpodder.MAEMO:
742 if new_value:
743 self.toolbar.show()
744 else:
745 self.toolbar.hide()
746 elif name == 'episode_list_descriptions' and gpodder.interface != gpodder.MAEMO:
747 self.updateTreeView()
749 def read_apps(self):
750 time.sleep(3) # give other parts of gpodder a chance to start up
751 self.user_apps_reader.read()
752 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
753 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
755 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
756 # With get_bin_window, we get the window that contains the rows without
757 # the header. The Y coordinate of this window will be the height of the
758 # treeview header. This is the amount we have to subtract from the
759 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
760 (x_bin, y_bin) = treeview.get_bin_window().get_position()
761 y -= x_bin
762 y -= y_bin
763 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
765 if not self.episode_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
766 self.last_tooltip_episode = None
767 return False
769 if path is not None:
770 model = treeview.get_model()
771 iter = model.get_iter(path)
772 url = model.get_value(iter, EpisodeListModel.C_URL)
773 description = model.get_value(iter, EpisodeListModel.C_DESCRIPTION_STRIPPED)
774 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
775 self.last_tooltip_episode = None
776 return False
777 self.last_tooltip_episode = url
779 if len(description) > 400:
780 description = description[:398]+'[...]'
782 tooltip.set_text(description)
783 return True
785 self.last_tooltip_episode = None
786 return False
788 def podcast_list_allow_tooltips(self):
789 self.podcast_list_can_tooltip = True
791 def episode_list_allow_tooltips(self):
792 self.episode_list_can_tooltip = True
794 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
795 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
797 if not self.podcast_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
798 self.last_tooltip_channel = None
799 return False
801 if path is not None:
802 model = treeview.get_model()
803 iter = model.get_iter(path)
804 url = model.get_value(iter, PodcastListModel.C_URL)
805 channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
807 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
808 self.last_tooltip_channel = None
809 return False
810 self.last_tooltip_channel = channel
811 channel.request_save_dir_size()
812 diskspace_str = util.format_filesize(channel.save_dir_size, 0)
813 error_str = model.get_value(iter, PodcastListModel.C_ERROR)
814 if error_str:
815 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
816 error_str = '<span foreground="#ff0000">%s</span>' % error_str
817 table = gtk.Table(rows=3, columns=3)
818 table.set_row_spacings(5)
819 table.set_col_spacings(5)
820 table.set_border_width(5)
822 heading = gtk.Label()
823 heading.set_alignment(0, 1)
824 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
825 table.attach(heading, 0, 1, 0, 1)
826 size_info = gtk.Label()
827 size_info.set_alignment(1, 1)
828 size_info.set_justify(gtk.JUSTIFY_RIGHT)
829 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
830 table.attach(size_info, 2, 3, 0, 1)
832 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
834 if len(channel.description) < 500:
835 description = channel.description
836 else:
837 pos = channel.description.find('\n\n')
838 if pos == -1 or pos > 500:
839 description = channel.description[:498]+'[...]'
840 else:
841 description = channel.description[:pos]
843 description = gtk.Label(description)
844 if error_str:
845 description.set_markup(error_str)
846 description.set_alignment(0, 0)
847 description.set_line_wrap(True)
848 table.attach(description, 0, 3, 2, 3)
850 table.show_all()
851 tooltip.set_custom(table)
853 return True
855 self.last_tooltip_channel = None
856 return False
858 def update_m3u_playlist_clicked(self, widget):
859 self.active_channel.update_m3u_playlist()
860 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget=self.treeChannels)
862 def treeview_downloads_button_pressed(self, treeview, event):
863 if event.button == 1:
864 # Catch left mouse button presses, and if we there is no
865 # path at the given position, deselect all items
866 (x, y) = (int(event.x), int(event.y))
867 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
868 if path is None:
869 treeview.get_selection().unselect_all()
871 # Use right-click for the Desktop version and left-click for Maemo
872 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
873 (event.button == 3 and gpodder.interface == gpodder.GUI):
874 (x, y) = (int(event.x), int(event.y))
875 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
877 paths = []
878 # Did the user right-click into a selection?
879 selection = treeview.get_selection()
880 if selection.count_selected_rows() and path:
881 (model, paths) = selection.get_selected_rows()
882 if path not in paths:
883 # We have right-clicked, but not into the
884 # selection, assume we don't want to operate
885 # on the selection
886 paths = []
888 # No selection or right click not in selection:
889 # Select the single item where we clicked
890 if not paths and path:
891 treeview.grab_focus()
892 treeview.set_cursor( path, column, 0)
893 (model, paths) = (treeview.get_model(), [path])
895 # We did not find a selection, and the user didn't
896 # click on an item to select -- don't show the menu
897 if not paths:
898 return True
900 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
902 def make_menu_item(label, stock_id, tasks, status):
903 # This creates a menu item for selection-wide actions
904 def for_each_task_set_status(tasks, status):
905 changed_episode_urls = []
906 for row_reference, task in tasks:
907 if status is not None:
908 if status == download.DownloadTask.QUEUED:
909 # Only queue task when its paused/failed/cancelled
910 if task.status in (task.PAUSED, task.FAILED, task.CANCELLED):
911 self.download_queue_manager.add_task(task)
912 self.enable_download_list_update()
913 elif status == download.DownloadTask.CANCELLED:
914 # Cancelling a download only allows when paused/downloading/queued
915 if task.status in (task.QUEUED, task.DOWNLOADING, task.PAUSED):
916 task.status = status
917 elif status == download.DownloadTask.PAUSED:
918 # Pausing a download only when queued/downloading
919 if task.status in (task.DOWNLOADING, task.QUEUED):
920 task.status = status
921 else:
922 # We (hopefully) can simply set the task status here
923 task.status = status
924 else:
925 # Remove the selected task - cancel downloading/queued tasks
926 if task.status in (task.QUEUED, task.DOWNLOADING):
927 task.status = task.CANCELLED
928 model.remove(model.get_iter(row_reference.get_path()))
929 # Remember the URL, so we can tell the UI to update
930 try:
931 # We don't "see" this task anymore - remove it;
932 # this is needed, so update_episode_list_icons()
933 # below gets the correct list of "seen" tasks
934 self.download_tasks_seen.remove(task)
935 except KeyError, key_error:
936 log('Cannot remove task from "seen" list: %s', task, sender=self)
937 changed_episode_urls.append(task.url)
938 # Tell the task that it has been removed (so it can clean up)
939 task.removed_from_list()
940 # Tell the podcasts tab to update icons for our removed podcasts
941 self.update_episode_list_icons(changed_episode_urls)
942 # Update the tab title and downloads list
943 self.update_downloads_list()
944 return True
945 item = gtk.ImageMenuItem(label)
946 item.set_image(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU))
947 item.connect('activate', lambda item: for_each_task_set_status(tasks, status))
949 # Determine if we should disable this menu item
950 for row_reference, task in tasks:
951 if status == download.DownloadTask.QUEUED:
952 if task.status not in (download.DownloadTask.PAUSED, \
953 download.DownloadTask.FAILED, \
954 download.DownloadTask.CANCELLED):
955 item.set_sensitive(False)
956 break
957 elif status == download.DownloadTask.CANCELLED:
958 if task.status not in (download.DownloadTask.PAUSED, \
959 download.DownloadTask.QUEUED, \
960 download.DownloadTask.DOWNLOADING):
961 item.set_sensitive(False)
962 break
963 elif status == download.DownloadTask.PAUSED:
964 if task.status not in (download.DownloadTask.QUEUED, \
965 download.DownloadTask.DOWNLOADING):
966 item.set_sensitive(False)
967 break
968 elif status is None:
969 if task.status not in (download.DownloadTask.CANCELLED, \
970 download.DownloadTask.FAILED, \
971 download.DownloadTask.DONE):
972 item.set_sensitive(False)
973 break
975 return self.set_finger_friendly(item)
977 menu = gtk.Menu()
979 item = gtk.ImageMenuItem(_('Episode details'))
980 item.set_image(gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
981 if len(selected_tasks) == 1:
982 row_reference, task = selected_tasks[0]
983 episode = task.episode
984 item.connect('activate', lambda item: self.show_episode_shownotes(episode))
985 else:
986 item.set_sensitive(False)
987 menu.append(item)
988 menu.append(gtk.SeparatorMenuItem())
989 menu.append(make_menu_item(_('Download'), gtk.STOCK_GO_DOWN, selected_tasks, download.DownloadTask.QUEUED))
990 menu.append(make_menu_item(_('Cancel'), gtk.STOCK_CANCEL, selected_tasks, download.DownloadTask.CANCELLED))
991 menu.append(make_menu_item(_('Pause'), gtk.STOCK_MEDIA_PAUSE, selected_tasks, download.DownloadTask.PAUSED))
992 menu.append(gtk.SeparatorMenuItem())
993 menu.append(make_menu_item(_('Remove from list'), gtk.STOCK_REMOVE, selected_tasks, None))
995 if gpodder.interface == gpodder.MAEMO:
996 # Because we open the popup on left-click for Maemo,
997 # we also include a non-action to close the menu
998 menu.append(gtk.SeparatorMenuItem())
999 item = gtk.ImageMenuItem(_('Close this menu'))
1000 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1001 menu.append(self.set_finger_friendly(item))
1003 menu.show_all()
1004 menu.popup(None, None, None, event.button, event.time)
1005 return True
1007 def treeview_channels_button_pressed( self, treeview, event):
1008 if gpodder.interface == gpodder.MAEMO:
1009 self.treeview_channels_buttonpress = (event.x, event.y)
1010 return True
1012 if event.button == 3:
1013 ( x, y ) = ( int(event.x), int(event.y) )
1014 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
1016 paths = []
1018 # Did the user right-click into a selection?
1019 selection = treeview.get_selection()
1020 if selection.count_selected_rows() and path:
1021 ( model, paths ) = selection.get_selected_rows()
1022 if path not in paths:
1023 # We have right-clicked, but not into the
1024 # selection, assume we don't want to operate
1025 # on the selection
1026 paths = []
1028 # No selection or right click not in selection:
1029 # Select the single item where we clicked
1030 if not len( paths) and path:
1031 treeview.grab_focus()
1032 treeview.set_cursor( path, column, 0)
1034 ( model, paths ) = ( treeview.get_model(), [ path ] )
1036 # We did not find a selection, and the user didn't
1037 # click on an item to select -- don't show the menu
1038 if not len( paths):
1039 return True
1041 menu = gtk.Menu()
1043 item = gtk.ImageMenuItem( _('Open download folder'))
1044 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
1045 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
1046 menu.append( item)
1048 item = gtk.ImageMenuItem( _('Update Feed'))
1049 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
1050 item.connect('activate', self.on_itemUpdateChannel_activate )
1051 item.set_sensitive( not self.updating_feed_cache )
1052 menu.append( item)
1054 item = gtk.ImageMenuItem(_('Update M3U playlist'))
1055 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
1056 item.connect('activate', self.update_m3u_playlist_clicked)
1057 menu.append(item)
1059 if self.active_channel.link:
1060 item = gtk.ImageMenuItem(_('Visit website'))
1061 item.set_image(gtk.image_new_from_icon_name('web-browser', gtk.ICON_SIZE_MENU))
1062 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
1063 menu.append(item)
1065 if self.active_channel.channel_is_locked:
1066 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
1067 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1068 item.connect('activate', self.on_channel_toggle_lock_activate)
1069 menu.append(self.set_finger_friendly(item))
1070 else:
1071 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
1072 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1073 item.connect('activate', self.on_channel_toggle_lock_activate)
1074 menu.append(self.set_finger_friendly(item))
1077 menu.append( gtk.SeparatorMenuItem())
1079 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
1080 item.connect( 'activate', self.on_itemEditChannel_activate)
1081 menu.append( item)
1083 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1084 item.connect( 'activate', self.on_itemRemoveChannel_activate)
1085 menu.append( item)
1087 menu.show_all()
1088 # Disable tooltips while we are showing the menu, so
1089 # the tooltip will not appear over the menu
1090 self.podcast_list_can_tooltip = False
1091 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
1092 menu.popup( None, None, None, event.button, event.time)
1094 return True
1096 def on_itemClose_activate(self, widget):
1097 if self.tray_icon is not None:
1098 if gpodder.interface == gpodder.MAEMO:
1099 self.gPodder.set_property('visible', False)
1100 else:
1101 self.iconify_main_window()
1102 else:
1103 self.on_gPodder_delete_event(widget)
1105 def cover_file_removed(self, channel_url):
1107 The Cover Downloader calls this when a previously-
1108 available cover has been removed from the disk. We
1109 have to update our model to reflect this change.
1111 self.podcast_list_model.delete_cover_by_url(channel_url)
1113 def cover_download_finished(self, channel_url, pixbuf):
1115 The Cover Downloader calls this when it has finished
1116 downloading (or registering, if already downloaded)
1117 a new channel cover, which is ready for displaying.
1119 self.podcast_list_model.add_cover_by_url(channel_url, pixbuf)
1121 def save_episode_as_file(self, episode):
1122 if episode.was_downloaded(and_exists=True):
1123 folder = self.folder_for_saving_episodes
1124 copy_from = episode.local_filename(create=False)
1125 assert copy_from is not None
1126 copy_to = episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)
1127 (result, folder) = self.show_copy_dialog(src_filename=copy_from, dst_filename=copy_to, dst_directory=folder)
1128 self.folder_for_saving_episodes = folder
1130 def copy_episodes_bluetooth(self, episodes):
1131 episodes_to_copy = [e for e in episodes if e.was_downloaded(and_exists=True)]
1133 def convert_and_send_thread(episode):
1134 for episode in episodes:
1135 filename = episode.local_filename(create=False)
1136 assert filename is not None
1137 destfile = os.path.join(tempfile.gettempdir(), \
1138 util.sanitize_filename(episode.sync_filename(self.config.custom_sync_name_enabled, self.config.custom_sync_name)))
1139 (base, ext) = os.path.splitext(filename)
1140 if not destfile.endswith(ext):
1141 destfile += ext
1143 try:
1144 shutil.copyfile(filename, destfile)
1145 util.bluetooth_send_file(destfile)
1146 except:
1147 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
1148 self.notification(_('Error converting file.'), _('Bluetooth file transfer'), important=True)
1150 util.delete_file(destfile)
1152 threading.Thread(target=convert_and_send_thread, args=[episodes_to_copy]).start()
1154 def treeview_button_savepos(self, treeview, event):
1155 if gpodder.interface == gpodder.MAEMO and event.button == 1:
1156 self.treeview_available_buttonpress = (event.x, event.y)
1157 return True
1159 def treeview_channels_button_released(self, treeview, event):
1160 if gpodder.interface == gpodder.MAEMO and event.button == 1:
1161 selection = self.treeChannels.get_selection()
1162 pathatpos = self.treeChannels.get_path_at_pos(int(event.x), int(event.y))
1163 if self.currently_updating:
1164 log('do not handle press while updating', sender=self)
1165 return True
1166 if pathatpos is None:
1167 return False
1168 else:
1169 ydistance = int(abs(event.y-self.treeview_channels_buttonpress[1]))
1170 xdistance = int(event.x-self.treeview_channels_buttonpress[0])
1171 if ydistance < 30:
1172 (path, column, x, y) = pathatpos
1173 selection.select_path(path)
1174 self.treeChannels.set_cursor(path)
1175 self.treeChannels.grab_focus()
1176 # Emulate the cursor changed signal to force an update
1177 self.on_treeChannels_cursor_changed(self.treeChannels)
1178 return True
1180 def get_device_name(self):
1181 if self.config.device_type == 'ipod':
1182 return _('iPod')
1183 elif self.config.device_type in ('filesystem', 'mtp'):
1184 return _('MP3 player')
1185 else:
1186 return '(unknown device)'
1188 def treeview_button_pressed( self, treeview, event):
1189 if gpodder.interface == gpodder.MAEMO:
1190 ydistance = int(abs(event.y-self.treeview_available_buttonpress[1]))
1191 xdistance = int(event.x-self.treeview_available_buttonpress[0])
1193 selection = self.treeAvailable.get_selection()
1194 pathatpos = self.treeAvailable.get_path_at_pos(int(event.x), int(event.y))
1195 if pathatpos is None:
1196 # No item at the current cursor position
1197 return False
1198 elif ydistance < 30:
1199 # Item under the cursor, and no scrolling done
1200 (path, column, x, y) = pathatpos
1201 selection.select_path(path)
1202 self.treeAvailable.set_cursor(path)
1203 self.treeAvailable.grab_focus()
1204 if self.config.maemo_enable_gestures and xdistance > 70:
1205 self.on_playback_selected_episodes(None)
1206 return True
1207 elif self.config.maemo_enable_gestures and xdistance < -70:
1208 self.on_shownotes_selected_episodes(None)
1209 return True
1210 else:
1211 # Scrolling has been done
1212 return True
1214 # Use right-click for the Desktop version and left-click for Maemo
1215 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
1216 (event.button == 3 and gpodder.interface == gpodder.GUI):
1217 ( x, y ) = ( int(event.x), int(event.y) )
1218 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
1220 paths = []
1222 # Did the user right-click into a selection?
1223 selection = self.treeAvailable.get_selection()
1224 if selection.count_selected_rows() and path:
1225 ( model, paths ) = selection.get_selected_rows()
1226 if path not in paths:
1227 # We have right-clicked, but not into the
1228 # selection, assume we don't want to operate
1229 # on the selection
1230 paths = []
1232 # No selection or right click not in selection:
1233 # Select the single item where we clicked
1234 if not len( paths) and path:
1235 treeview.grab_focus()
1236 treeview.set_cursor( path, column, 0)
1238 ( model, paths ) = ( treeview.get_model(), [ path ] )
1240 # We did not find a selection, and the user didn't
1241 # click on an item to select -- don't show the menu
1242 if not len( paths):
1243 return True
1245 episodes = self.get_selected_episodes()
1246 any_locked = any(e.is_locked for e in episodes)
1247 any_played = any(e.is_played for e in episodes)
1248 one_is_new = any(e.state == gpodder.STATE_NORMAL and not e.is_played for e in episodes)
1250 menu = gtk.Menu()
1252 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1254 if open_instead_of_play:
1255 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1256 else:
1257 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1259 item.set_sensitive(can_play)
1260 item.connect('activate', self.on_playback_selected_episodes)
1261 menu.append(self.set_finger_friendly(item))
1263 if not can_cancel:
1264 item = gtk.ImageMenuItem(_('Download'))
1265 item.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1266 item.set_sensitive(can_download)
1267 item.connect('activate', self.on_download_selected_episodes)
1268 menu.append(self.set_finger_friendly(item))
1269 else:
1270 item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
1271 item.connect('activate', lambda w: self.on_treeDownloads_row_activated(self.toolCancel))
1272 menu.append(self.set_finger_friendly(item))
1274 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1275 item.set_sensitive(can_delete)
1276 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1277 menu.append(self.set_finger_friendly(item))
1279 if one_is_new:
1280 item = gtk.ImageMenuItem(_('Do not download'))
1281 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1282 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1283 menu.append(self.set_finger_friendly(item))
1284 elif can_download:
1285 item = gtk.ImageMenuItem(_('Mark as new'))
1286 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1287 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1288 menu.append(self.set_finger_friendly(item))
1290 # Ok, this probably makes sense to only display for downloaded files
1291 if can_play and not can_download:
1292 menu.append( gtk.SeparatorMenuItem())
1293 item = gtk.ImageMenuItem(_('Save to disk'))
1294 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1295 item.connect('activate', lambda w: [self.save_episode_as_file(e) for e in episodes])
1296 menu.append(self.set_finger_friendly(item))
1297 if self.bluetooth_available:
1298 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1299 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
1300 item.connect('activate', lambda w: self.copy_episodes_bluetooth(episodes))
1301 menu.append(self.set_finger_friendly(item))
1302 if can_transfer:
1303 item = gtk.ImageMenuItem(_('Transfer to %s') % self.get_device_name())
1304 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
1305 item.connect('activate', lambda w: self.on_sync_to_ipod_activate(w, episodes))
1306 menu.append(self.set_finger_friendly(item))
1308 if can_play:
1309 menu.append( gtk.SeparatorMenuItem())
1310 if any_played:
1311 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1312 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1313 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1314 menu.append(self.set_finger_friendly(item))
1315 else:
1316 item = gtk.ImageMenuItem(_('Mark as played'))
1317 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1318 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1319 menu.append(self.set_finger_friendly(item))
1321 if any_locked:
1322 item = gtk.ImageMenuItem(_('Allow deletion'))
1323 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1324 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, False))
1325 menu.append(self.set_finger_friendly(item))
1326 else:
1327 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1328 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1329 item.connect('activate', lambda w: self.on_item_toggle_lock_activate( w, False, True))
1330 menu.append(self.set_finger_friendly(item))
1332 menu.append(gtk.SeparatorMenuItem())
1333 # Single item, add episode information menu item
1334 item = gtk.ImageMenuItem(_('Episode details'))
1335 item.set_image(gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1336 item.connect('activate', lambda w: self.show_episode_shownotes(episodes[0]))
1337 menu.append(self.set_finger_friendly(item))
1339 # If we have it, also add episode website link
1340 if episodes[0].link and episodes[0].link != episodes[0].url:
1341 item = gtk.ImageMenuItem(_('Visit website'))
1342 item.set_image(gtk.image_new_from_icon_name('web-browser', gtk.ICON_SIZE_MENU))
1343 item.connect('activate', lambda w: util.open_website(episodes[0].link))
1344 menu.append(self.set_finger_friendly(item))
1346 if gpodder.interface == gpodder.MAEMO:
1347 # Because we open the popup on left-click for Maemo,
1348 # we also include a non-action to close the menu
1349 menu.append(gtk.SeparatorMenuItem())
1350 item = gtk.ImageMenuItem(_('Close this menu'))
1351 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1352 menu.append(self.set_finger_friendly(item))
1354 menu.show_all()
1355 # Disable tooltips while we are showing the menu, so
1356 # the tooltip will not appear over the menu
1357 self.episode_list_can_tooltip = False
1358 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1359 menu.popup( None, None, None, event.button, event.time)
1361 return True
1363 def set_title(self, new_title):
1364 self.default_title = new_title
1365 self.gPodder.set_title(new_title)
1367 def update_selected_episode_list_icons(self):
1369 Updates the status icons in the episode list
1371 selection = self.treeAvailable.get_selection()
1372 (model, paths) = selection.get_selected_rows()
1373 for path in reversed(paths):
1374 iter = model.get_iter(path)
1375 self.episode_list_model.update_by_filter_iter(iter, \
1376 self.episode_is_downloading, \
1377 self.config.episode_list_descriptions and gpodder.interface != gpodder.MAEMO)
1379 def update_episode_list_icons(self, urls):
1381 Updates the status icons in the episode list
1382 Only update the episodes that have an URL in
1383 the "urls" iterable object (e.g. a list of URLs)
1385 if self.active_channel is None or not urls:
1386 return
1388 self.episode_list_model.update_by_urls(urls, \
1389 self.episode_is_downloading, \
1390 self.config.episode_list_descriptions and gpodder.interface != gpodder.MAEMO)
1392 def clean_up_downloads(self, delete_partial=False):
1393 # Clean up temporary files left behind by old gPodder versions
1394 temporary_files = glob.glob('%s/*/.tmp-*' % self.config.download_dir)
1396 if delete_partial:
1397 temporary_files += glob.glob('%s/*/*.partial' % self.config.download_dir)
1399 for tempfile in temporary_files:
1400 util.delete_file(tempfile)
1402 # Clean up empty download folders and abandoned download folders
1403 download_dirs = glob.glob(os.path.join(self.config.download_dir, '*'))
1404 for ddir in download_dirs:
1405 if os.path.isdir(ddir) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1406 globr = glob.glob(os.path.join(ddir, '*'))
1407 if len(globr) == 0 or (len(globr) == 1 and globr[0].endswith('/cover')):
1408 log('Stale download directory found: %s', os.path.basename(ddir), sender=self)
1409 shutil.rmtree(ddir, ignore_errors=True)
1411 def streaming_possible(self):
1412 return self.config.player and self.config.player != 'default'
1414 def playback_episodes_for_real(self, episodes):
1415 groups = collections.defaultdict(list)
1416 for episode in episodes:
1417 # Mark episode as played in the database
1418 episode.mark(is_played=True)
1420 file_type = episode.file_type()
1421 if file_type == 'video' and self.config.videoplayer and \
1422 self.config.videoplayer != 'default':
1423 player = self.config.videoplayer
1424 elif file_type == 'audio' and self.config.player and \
1425 self.config.player != 'default':
1426 player = self.config.player
1427 else:
1428 player = 'default'
1430 filename = episode.local_filename(create=False)
1431 if filename is None or not os.path.exists(filename):
1432 filename = episode.url
1433 groups[player].append(filename)
1435 # Open episodes with system default player
1436 if 'default' in groups:
1437 for filename in groups['default']:
1438 log('Opening with system default: %s', filename, sender=self)
1439 util.gui_open(filename)
1440 del groups['default']
1442 # For each type now, go and create play commands
1443 for group in groups:
1444 for command in util.format_desktop_command(group, groups[group]):
1445 log('Executing: %s', repr(command), sender=self)
1446 subprocess.Popen(command)
1448 def playback_episodes(self, episodes):
1449 if gpodder.interface == gpodder.MAEMO:
1450 if len(episodes) == 1:
1451 text = _('Opening %s') % saxutils.escape(episodes[0].title)
1452 else:
1453 text = _('Opening %d episodes') % len(episodes)
1454 banner = hildon.hildon_banner_show_animation(self.gPodder, None, text)
1455 def destroy_banner_later(banner):
1456 banner.destroy()
1457 return False
1458 gobject.timeout_add(5000, destroy_banner_later, banner)
1460 episodes = [e for e in episodes if \
1461 e.was_downloaded(and_exists=True) or self.streaming_possible()]
1463 try:
1464 self.playback_episodes_for_real(episodes)
1465 except Exception, e:
1466 log('Error in playback!', sender=self, traceback=True)
1467 self.show_message( _('Please check your media player settings in the preferences dialog.'), _('Error opening player'), widget=self.toolPreferences)
1469 self.update_selected_episode_list_icons()
1470 self.updateComboBox(only_selected_channel=True)
1472 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1473 if model is None:
1474 return True
1476 key = key.lower()
1478 for column in (EpisodeListModel.C_TITLE, EpisodeListModel.C_DESCRIPTION_STRIPPED):
1479 value = model.get_value( iter, column).lower()
1480 if value.find( key) != -1:
1481 return False
1483 return True
1485 def play_or_download(self):
1486 if self.wNotebook.get_current_page() > 0:
1487 return
1489 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1490 ( is_played, is_locked ) = (False,)*2
1492 open_instead_of_play = False
1494 selection = self.treeAvailable.get_selection()
1495 if selection.count_selected_rows() > 0:
1496 (model, paths) = selection.get_selected_rows()
1498 for path in paths:
1499 episode = model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE)
1501 if episode.file_type() not in ('audio', 'video'):
1502 open_instead_of_play = True
1504 if episode.was_downloaded():
1505 can_play = episode.was_downloaded(and_exists=True)
1506 can_delete = True
1507 is_played = episode.is_played
1508 is_locked = episode.is_locked
1509 if not can_play:
1510 can_download = True
1511 else:
1512 if self.episode_is_downloading(episode):
1513 can_cancel = True
1514 else:
1515 can_download = True
1517 can_download = can_download and not can_cancel
1518 can_play = self.streaming_possible() or (can_play and not can_cancel and not can_download)
1519 can_transfer = can_play and self.config.device_type != 'none' and not can_cancel and not can_download
1521 if open_instead_of_play:
1522 if gpodder.interface != gpodder.MAEMO:
1523 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1524 can_transfer = False
1525 else:
1526 if gpodder.interface != gpodder.MAEMO:
1527 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1529 self.toolPlay.set_sensitive( can_play)
1530 self.toolDownload.set_sensitive( can_download)
1531 self.toolTransfer.set_sensitive( can_transfer)
1532 self.toolCancel.set_sensitive( can_cancel)
1534 self.item_cancel_download.set_sensitive(can_cancel)
1535 self.itemDownloadSelected.set_sensitive(can_download)
1536 self.itemOpenSelected.set_sensitive(can_play)
1537 self.itemPlaySelected.set_sensitive(can_play)
1538 self.itemDeleteSelected.set_sensitive(can_play and not can_download)
1539 self.item_toggle_played.set_sensitive(can_play)
1540 self.item_toggle_lock.set_sensitive(can_play)
1542 self.itemOpenSelected.set_visible(open_instead_of_play)
1543 self.itemPlaySelected.set_visible(not open_instead_of_play)
1545 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1547 def on_cbMaxDownloads_toggled(self, widget, *args):
1548 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1550 def on_cbLimitDownloads_toggled(self, widget, *args):
1551 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1553 def episode_new_status_changed(self, urls):
1554 self.updateComboBox()
1555 self.update_episode_list_icons(urls)
1557 def updateComboBox(self, selected_url=None, only_selected_channel=False, only_these_urls=None):
1558 selection = self.treeChannels.get_selection()
1559 (model, iter) = selection.get_selected()
1561 if only_selected_channel:
1562 # very cheap! only update selected channel
1563 if iter and self.active_channel is not None:
1564 model.update_by_iter(iter)
1565 elif not self.channel_list_changed:
1566 # we can keep the model, but have to update some
1567 if only_these_urls is None:
1568 # still cheaper than reloading the whole list
1569 iter = model.get_iter_first()
1570 while iter is not None:
1571 model.update_by_iter(iter)
1572 iter = model.iter_next(iter)
1573 else:
1574 # ok, we got a bunch of urls to update
1575 model.update_by_urls(only_these_urls)
1576 else:
1577 if model and iter and selected_url is None:
1578 # Get the URL of the currently-selected podcast
1579 selected_url = model.get_value(iter, 0)
1581 # Update the podcast list model with new channels
1582 self.podcast_list_model.set_channels(self.channels)
1584 try:
1585 selected_path = (0,)
1586 # Find the previously-selected URL in the new
1587 # model if we have an URL (else select first)
1588 if selected_url is not None:
1589 pos = model.get_iter_first()
1590 while pos is not None:
1591 url = model.get_value(pos, 0)
1592 if url == selected_url:
1593 selected_path = model.get_path(pos)
1594 break
1595 pos = model.iter_next(pos)
1597 self.treeChannels.get_selection().select_path(selected_path)
1598 except:
1599 log( 'Cannot set selection on treeChannels', sender = self)
1600 self.on_treeChannels_cursor_changed( self.treeChannels)
1601 self.channel_list_changed = False
1603 def episode_is_downloading(self, episode):
1604 """Returns True if the given episode is being downloaded at the moment"""
1605 if episode is None:
1606 return False
1608 return episode.url in (task.url for task in self.download_tasks_seen if task.status in (task.DOWNLOADING, task.QUEUED, task.PAUSED))
1610 def on_episode_list_model_updated(self, banner=None):
1611 if banner is not None:
1612 banner.destroy()
1613 self.treeAvailable.columns_autosize()
1614 self.play_or_download()
1615 self.currently_updating = False
1617 def updateTreeView(self):
1618 if self.channels and self.active_channel is not None:
1619 if gpodder.interface == gpodder.MAEMO:
1620 banner = hildon.hildon_banner_show_animation(self.gPodder, None, _('Loading episodes for %s') % saxutils.escape(self.active_channel.title))
1621 else:
1622 banner = None
1624 self.currently_updating = True
1625 self.episode_list_model.update_from_channel(self.active_channel, \
1626 self.episode_is_downloading, \
1627 self.config.episode_list_descriptions and gpodder.interface != gpodder.MAEMO, \
1628 lambda: self.on_episode_list_model_updated(banner))
1629 else:
1630 self.episode_list_model.clear()
1632 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1633 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1635 dnd_channel = None
1636 if path is not None:
1637 model = self.treeChannels.get_model()
1638 iter = model.get_iter(path)
1639 url = model.get_value(iter, 0)
1640 for channel in self.channels:
1641 if channel.url == url:
1642 dnd_channel = channel
1643 break
1645 result = sel.data
1646 rl = result.strip().lower()
1647 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1648 self.cover_downloader.replace_cover(dnd_channel, result)
1649 else:
1650 self.add_podcast_list([result])
1652 def offer_new_episodes(self):
1653 new_episodes = self.get_new_episodes()
1654 if new_episodes:
1655 self.new_episodes_show(new_episodes)
1656 return True
1657 return False
1659 def add_podcast_list(self, urls):
1660 """Subscribe to a list of podcast given their URLs"""
1662 # Sort and split the URL list into three buckets
1663 queued, failed, existing = [], [], []
1664 for input_url in urls:
1665 url = util.normalize_feed_url(input_url)
1666 if url is None:
1667 # Fail this one because the URL is not valid
1668 failed.append(input_url)
1669 elif self.podcast_list_model.get_path_from_url(url) is not None:
1670 # A podcast already exists in the list for this URL
1671 existing.append(url)
1672 else:
1673 # This URL has survived the first round - queue for add
1674 queued.append(url)
1676 # After the initial sorting and splitting, try all queued podcasts
1677 for url in queued:
1678 log('QUEUE RUNNER: %s', url, sender=self)
1679 channel = self._add_new_channel(url)
1680 if channel is None:
1681 failed.append(url)
1682 else:
1683 self.channels.append(channel)
1684 self.channel_list_changed = True
1686 # Report already-existing subscriptions to the user
1687 if existing:
1688 title = _('Existing subscriptions skipped')
1689 message = _('You are already subscribed to these podcasts:') \
1690 + '\n\n' + '\n'.join(saxutils.escape(url) for url in existing)
1691 self.show_message(message, title, widget=self.treeChannels)
1693 # Report failed subscriptions to the user
1694 if failed:
1695 title = _('Could not add some podcasts')
1696 message = _('Some podcasts could not be added to your list:') \
1697 + '\n\n' + '\n'.join(saxutils.escape(url) for url in failed)
1698 self.show_message(message, title, important=True)
1700 # If at least one podcast has been added, save and update all
1701 if self.channel_list_changed:
1702 self.save_channels_opml()
1704 # Update the list of subscribed podcasts
1705 self.update_feed_cache(force_update=False)
1706 self.update_podcasts_tab()
1708 # If only one podcast was added, select it
1709 if len(urls) == 1:
1710 path = self.podcast_list_model.get_path_from_url(urls[0])
1711 if path is not None:
1712 selection = self.treeChannels.get_selection()
1713 selection.select_path(path)
1714 self.on_treeChannels_cursor_changed(self.treeChannels)
1716 # Offer to download new episodes
1717 self.offer_new_episodes()
1719 def _add_new_channel(self, url, authentication_tokens=None):
1720 # The URL is valid and does not exist already - subscribe!
1721 try:
1722 channel = PodcastChannel.load(self.db, url=url, create=True, \
1723 authentication_tokens=authentication_tokens, \
1724 max_episodes=self.config.max_episodes_per_feed, \
1725 download_dir=self.config.download_dir)
1726 except feedcore.AuthenticationRequired:
1727 title = _('Feed requires authentication')
1728 message = _('Please enter your username and password.')
1729 success, auth_tokens = self.show_login_dialog(title, message)
1730 if success:
1731 return self._add_new_channel(url, \
1732 authentication_tokens=auth_tokens)
1733 except feedcore.WifiLogin, error:
1734 title = _('Website redirection detected')
1735 message = _('The URL you are trying to add redirects to %s.') \
1736 + _('Do you want to visit the website now?')
1737 message = message % saxutils.escape(error.data)
1738 if self.show_confirmation(message, title):
1739 util.open_website(error.data)
1740 return None
1741 except Exception, e:
1742 self.show_message(saxutils.escape(str(e)), \
1743 _('Cannot subscribe to podcast'), important=True)
1744 log('Subscription error: %s', e, traceback=True, sender=self)
1745 return None
1747 try:
1748 username, password = util.username_password_from_url(url)
1749 except ValueError, ve:
1750 username, password = (None, None)
1752 if username is not None and channel.username is None and \
1753 password is not None and channel.password is None:
1754 channel.username = username
1755 channel.password = password
1756 channel.save()
1758 self._update_cover(channel)
1759 return channel
1761 def save_channels_opml(self):
1762 exporter = opml.Exporter(gpodder.subscription_file)
1763 return exporter.write(self.channels)
1765 def update_feed_cache_finish_callback(self, updated_urls=None, select_url_afterwards=None):
1766 self.db.commit()
1767 self.updating_feed_cache = False
1769 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
1770 self.channel_list_changed = True
1771 self.updateComboBox(selected_url=select_url_afterwards)
1773 # Only search for new episodes in podcasts that have been
1774 # updated, not in other podcasts (for single-feed updates)
1775 episodes = self.get_new_episodes([c for c in self.channels if c.url in updated_urls])
1777 if self.tray_icon:
1778 self.tray_icon.set_status()
1780 if self.feed_cache_update_cancelled:
1781 # The user decided to abort the feed update
1782 self.show_update_feeds_buttons()
1783 elif not episodes:
1784 # Nothing new here - but inform the user
1785 self.pbFeedUpdate.set_fraction(1.0)
1786 self.pbFeedUpdate.set_text(_('No new episodes'))
1787 self.feed_cache_update_cancelled = True
1788 self.btnCancelFeedUpdate.show()
1789 self.btnCancelFeedUpdate.set_sensitive(True)
1790 if gpodder.interface == gpodder.MAEMO:
1791 # btnCancelFeedUpdate is a ToolButton on Maemo
1792 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_APPLY)
1793 else:
1794 # btnCancelFeedUpdate is a normal gtk.Button
1795 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON))
1796 else:
1797 # New episodes are available
1798 self.pbFeedUpdate.set_fraction(1.0)
1799 # Are we minimized and should we auto download?
1800 if (self.minimized and (self.config.auto_download == 'minimized')) or (self.config.auto_download == 'always'):
1801 self.download_episode_list(episodes)
1802 if len(episodes) == 1:
1803 title = _('Downloading one new episode.')
1804 else:
1805 title = _('Downloading %d new episodes.') % len(episodes)
1807 self.show_message(title, _('New episodes available'), widget=self.labelDownloads)
1808 self.show_update_feeds_buttons()
1809 else:
1810 self.show_update_feeds_buttons()
1811 # New episodes are available and we are not minimized
1812 if not self.config.do_not_show_new_episodes_dialog:
1813 self.new_episodes_show(episodes)
1814 else:
1815 if len(episodes) == 1:
1816 message = _('One new episode is available for download')
1817 else:
1818 message = _('%i new episodes are available for download' % len(episodes))
1820 self.pbFeedUpdate.set_text(message)
1822 def _update_cover(self, channel):
1823 if channel is not None and not os.path.exists(channel.cover_file) and channel.image:
1824 self.cover_downloader.request_cover(channel)
1826 def update_feed_cache_proc(self, channels, select_url_afterwards):
1827 total = len(channels)
1829 for updated, channel in enumerate(channels):
1830 if not self.feed_cache_update_cancelled:
1831 try:
1832 # Update if timeout is not reached or we update a single podcast or skipping is disabled
1833 if channel.query_automatic_update() or total == 1 or not self.config.feed_update_skipping:
1834 channel.update(max_episodes=self.config.max_episodes_per_feed)
1835 else:
1836 log('Skipping update of %s (see feed_update_skipping)', channel.title, sender=self)
1837 self._update_cover(channel)
1838 except Exception, e:
1839 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)
1840 log('Error: %s', str(e), sender=self, traceback=True)
1842 # By the time we get here the update may have already been cancelled
1843 if not self.feed_cache_update_cancelled:
1844 def update_progress():
1845 progression = _('Updated %s (%d/%d)') % (channel.title, updated, total)
1846 self.pbFeedUpdate.set_text(progression)
1847 if self.tray_icon:
1848 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
1849 self.pbFeedUpdate.set_fraction(float(updated)/float(total))
1850 util.idle_add(update_progress)
1852 if self.feed_cache_update_cancelled:
1853 break
1855 updated_urls = [c.url for c in channels]
1856 util.idle_add(self.update_feed_cache_finish_callback, updated_urls, select_url_afterwards)
1858 def show_update_feeds_buttons(self):
1859 # Make sure that the buttons for updating feeds
1860 # appear - this should happen after a feed update
1861 if gpodder.interface == gpodder.MAEMO:
1862 self.btnUpdateSelectedFeed.show()
1863 self.toolFeedUpdateProgress.hide()
1864 self.btnCancelFeedUpdate.hide()
1865 self.btnCancelFeedUpdate.set_is_important(False)
1866 self.btnCancelFeedUpdate.set_stock_id(gtk.STOCK_CLOSE)
1867 self.toolbarSpacer.set_expand(True)
1868 self.toolbarSpacer.set_draw(False)
1869 else:
1870 self.hboxUpdateFeeds.hide()
1871 self.btnUpdateFeeds.show()
1872 self.itemUpdate.set_sensitive(True)
1873 self.itemUpdateChannel.set_sensitive(True)
1875 def on_btnCancelFeedUpdate_clicked(self, widget):
1876 if not self.feed_cache_update_cancelled:
1877 self.pbFeedUpdate.set_text(_('Cancelling...'))
1878 self.feed_cache_update_cancelled = True
1879 self.btnCancelFeedUpdate.set_sensitive(False)
1880 else:
1881 self.show_update_feeds_buttons()
1883 def update_feed_cache(self, channels=None, force_update=True, select_url_afterwards=None):
1884 if self.updating_feed_cache:
1885 return
1887 if not force_update:
1888 self.channels = PodcastChannel.load_from_db(self.db, self.config.download_dir)
1889 self.channel_list_changed = True
1890 self.updateComboBox(selected_url=select_url_afterwards)
1891 return
1893 self.updating_feed_cache = True
1894 self.itemUpdate.set_sensitive(False)
1895 self.itemUpdateChannel.set_sensitive(False)
1897 if self.tray_icon:
1898 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
1900 if channels is None:
1901 channels = self.channels
1903 if len(channels) == 1:
1904 text = _('Updating "%s"...') % channels[0].title
1905 else:
1906 text = _('Updating %d feeds...') % len(channels)
1907 self.pbFeedUpdate.set_text(text)
1908 self.pbFeedUpdate.set_fraction(0)
1910 self.feed_cache_update_cancelled = False
1911 self.btnCancelFeedUpdate.show()
1912 self.btnCancelFeedUpdate.set_sensitive(True)
1913 if gpodder.interface == gpodder.MAEMO:
1914 self.toolbarSpacer.set_expand(False)
1915 self.toolbarSpacer.set_draw(True)
1916 self.btnUpdateSelectedFeed.hide()
1917 self.toolFeedUpdateProgress.show_all()
1918 else:
1919 self.btnCancelFeedUpdate.set_image(gtk.image_new_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON))
1920 self.hboxUpdateFeeds.show_all()
1921 self.btnUpdateFeeds.hide()
1923 args = (channels, select_url_afterwards)
1924 threading.Thread(target=self.update_feed_cache_proc, args=args).start()
1926 def on_gPodder_delete_event(self, widget, *args):
1927 """Called when the GUI wants to close the window
1928 Displays a confirmation dialog (and closes/hides gPodder)
1931 downloading = self.download_status_model.are_downloads_in_progress()
1933 # Only iconify if we are using the window's "X" button,
1934 # but not when we are using "Quit" in the menu or toolbar
1935 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'):
1936 self.iconify_main_window()
1937 elif self.config.on_quit_ask or downloading:
1938 if gpodder.interface == gpodder.MAEMO:
1939 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1940 if result:
1941 self.close_gpodder()
1942 else:
1943 return True
1944 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1945 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1946 quit_button = dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1948 title = _('Quit gPodder')
1949 if downloading:
1950 message = _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
1951 else:
1952 message = _('Do you really want to quit gPodder now?')
1954 dialog.set_title(title)
1955 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1956 if not downloading:
1957 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1958 dialog.vbox.pack_start(cb_ask)
1959 cb_ask.show_all()
1961 quit_button.grab_focus()
1962 result = dialog.run()
1963 dialog.destroy()
1965 if result == gtk.RESPONSE_CLOSE:
1966 if not downloading and cb_ask.get_active() == True:
1967 self.config.on_quit_ask = False
1968 self.close_gpodder()
1969 else:
1970 self.close_gpodder()
1972 return True
1974 def close_gpodder(self):
1975 """ clean everything and exit properly
1977 if self.channels:
1978 if self.save_channels_opml():
1979 if self.config.my_gpodder_autoupload:
1980 log('Uploading to my.gpodder.org on close', sender=self)
1981 util.idle_add(self.on_upload_to_mygpo, None)
1982 else:
1983 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important=True)
1985 self.gPodder.hide()
1987 if self.tray_icon is not None:
1988 self.tray_icon.set_visible(False)
1990 # Notify all tasks to to carry out any clean-up actions
1991 self.download_status_model.tell_all_tasks_to_quit()
1993 while gtk.events_pending():
1994 gtk.main_iteration(False)
1996 self.db.close()
1998 self.quit()
1999 sys.exit(0)
2001 def get_old_episodes(self):
2002 episodes = []
2003 for channel in self.channels:
2004 for episode in channel.get_downloaded_episodes():
2005 if episode.age_in_days() > self.config.episode_old_age and \
2006 not episode.is_locked and episode.is_played:
2007 episodes.append(episode)
2008 return episodes
2010 def delete_episode_list( self, episodes, confirm = True):
2011 if len(episodes) == 0:
2012 return
2014 if len(episodes) == 1:
2015 message = _('Do you really want to delete this episode?')
2016 else:
2017 message = _('Do you really want to delete %d episodes?') % len(episodes)
2019 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
2020 return
2022 episode_urls = set()
2023 channel_urls = set()
2024 for episode in episodes:
2025 log('Deleting episode: %s', episode.title, sender = self)
2026 episode.delete_from_disk()
2027 episode_urls.add(episode.url)
2028 channel_urls.add(episode.channel.url)
2030 # Episodes have been deleted - persist the database
2031 self.db.commit()
2033 self.update_episode_list_icons(episode_urls)
2034 self.updateComboBox(only_these_urls=channel_urls)
2036 def on_itemRemoveOldEpisodes_activate( self, widget):
2037 columns = (
2038 ('title_markup', None, None, _('Episode')),
2039 ('channel_prop', None, None, _('Podcast')),
2040 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2041 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2042 ('played_prop', None, None, _('Status')),
2043 ('age_prop', None, None, _('Downloaded')),
2046 selection_buttons = {
2047 _('Select played'): lambda episode: episode.is_played,
2048 _('Select older than %d days') % self.config.episode_old_age: lambda episode: episode.age_in_days() > self.config.episode_old_age,
2051 instructions = _('Select the episodes you want to delete from your hard disk.')
2053 episodes = []
2054 selected = []
2055 for channel in self.channels:
2056 for episode in channel.get_downloaded_episodes():
2057 if not episode.is_locked:
2058 episodes.append(episode)
2059 selected.append(episode.is_played)
2061 gPodderEpisodeSelector(self.gPodder, title = _('Remove old episodes'), instructions = instructions, \
2062 episodes = episodes, selected = selected, columns = columns, \
2063 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
2064 selection_buttons = selection_buttons, _config=self.config)
2066 def on_selected_episodes_status_changed(self):
2067 self.update_selected_episode_list_icons()
2068 self.updateComboBox(only_selected_channel=True)
2069 self.db.commit()
2071 def mark_selected_episodes_new(self):
2072 for episode in self.get_selected_episodes():
2073 episode.mark_new()
2074 self.on_selected_episodes_status_changed()
2076 def mark_selected_episodes_old(self):
2077 for episode in self.get_selected_episodes():
2078 episode.mark_old()
2079 self.on_selected_episodes_status_changed()
2081 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
2082 for episode in self.get_selected_episodes():
2083 if toggle:
2084 episode.mark(is_played=not episode.is_played)
2085 else:
2086 episode.mark(is_played=new_value)
2087 self.on_selected_episodes_status_changed()
2089 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2090 for episode in self.get_selected_episodes():
2091 if toggle:
2092 episode.mark(is_locked=not episode.is_locked)
2093 else:
2094 episode.mark(is_locked=new_value)
2095 self.on_selected_episodes_status_changed()
2097 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
2098 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
2099 self.active_channel.update_channel_lock()
2101 for episode in self.active_channel.get_all_episodes():
2102 episode.mark(is_locked=self.active_channel.channel_is_locked)
2104 self.updateComboBox(only_selected_channel=True)
2105 self.update_episode_list_icons([e.url for e in self.active_channel.get_all_episodes()])
2107 def send_subscriptions(self):
2108 try:
2109 subprocess.Popen(['xdg-email', '--subject', _('My podcast subscriptions'),
2110 '--attach', gpodder.subscription_file])
2111 except:
2112 return False
2114 return True
2116 def on_item_email_subscriptions_activate(self, widget):
2117 if not self.channels:
2118 self.show_message(_('Your subscription list is empty. Add some podcasts first.'), _('Could not send list'), widget=self.treeChannels)
2119 elif not self.send_subscriptions():
2120 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'), important=True)
2122 def on_itemUpdateChannel_activate(self, widget=None):
2123 self.update_feed_cache(channels=[self.active_channel,])
2125 def on_itemUpdate_activate(self, widget=None):
2126 if self.channels:
2127 self.update_feed_cache()
2128 else:
2129 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)
2131 def download_episode_list_paused(self, episodes):
2132 self.download_episode_list(episodes, True)
2134 def download_episode_list(self, episodes, add_paused=False):
2135 for episode in episodes:
2136 log('Downloading episode: %s', episode.title, sender = self)
2137 if not episode.was_downloaded(and_exists=True):
2138 task_exists = False
2139 for task in self.download_tasks_seen:
2140 if episode.url == task.url and task.status not in (task.DOWNLOADING, task.QUEUED):
2141 self.download_queue_manager.add_task(task)
2142 self.enable_download_list_update()
2143 task_exists = True
2144 continue
2146 if task_exists:
2147 continue
2149 try:
2150 task = download.DownloadTask(episode, self.config)
2151 except Exception, e:
2152 self.show_message(_('Download error while downloading %s:\n\n%s') % (episode.title, str(e)), _('Download error'), important=True)
2153 log('Download error while downloading %s', episode.title, sender=self, traceback=True)
2154 continue
2156 if add_paused:
2157 task.status = task.PAUSED
2158 else:
2159 self.download_queue_manager.add_task(task)
2161 self.download_status_model.register_task(task)
2162 self.enable_download_list_update()
2164 def new_episodes_show(self, episodes):
2165 columns = (
2166 ('title_markup', None, None, _('Episode')),
2167 ('channel_prop', None, None, _('Podcast')),
2168 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
2169 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
2172 instructions = _('Select the episodes you want to download now.')
2174 gPodderEpisodeSelector(self.gPodder, title=_('New episodes available'), instructions=instructions, \
2175 episodes=episodes, columns=columns, selected_default=True, \
2176 stock_ok_button = 'gpodder-download', \
2177 callback=self.download_episode_list, \
2178 remove_callback=lambda e: e.mark_old(), \
2179 remove_action=_('Never download'), \
2180 remove_finished=self.episode_new_status_changed, \
2181 _config=self.config)
2183 def on_itemDownloadAllNew_activate(self, widget, *args):
2184 if not self.offer_new_episodes():
2185 self.show_message(_('Please check for new episodes later.'), \
2186 _('No new episodes available'), widget=self.btnUpdateFeeds)
2188 def get_new_episodes(self, channels=None):
2189 if channels is None:
2190 channels = self.channels
2191 episodes = []
2192 for channel in channels:
2193 for episode in channel.get_new_episodes(downloading=self.episode_is_downloading):
2194 episodes.append(episode)
2196 return episodes
2198 def get_all_episodes(self, exclude_nonsignificant=True ):
2199 """'exclude_nonsignificant' will exclude non-downloaded episodes
2200 and all episodes from channels that are set to skip when syncing"""
2201 episode_list = []
2202 for channel in self.channels:
2203 if not channel.sync_to_devices and exclude_nonsignificant:
2204 log('Skipping channel: %s', channel.title, sender=self)
2205 continue
2206 for episode in channel.get_all_episodes():
2207 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
2208 episode_list.append(episode)
2209 return episode_list
2211 def ipod_delete_played(self, device):
2212 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
2213 episodes_on_device = device.get_all_tracks()
2214 for local_episode in all_episodes:
2215 device_episode = device.episode_on_device(local_episode)
2216 if device_episode and ( local_episode.is_played and not local_episode.is_locked
2217 or local_episode.state == gpodder.STATE_DELETED ):
2218 log("mp3_player_delete_played: removing %s" % device_episode.title)
2219 device.remove_track(device_episode)
2221 def on_sync_to_ipod_activate(self, widget, episodes=None):
2222 # make sure gpod is available before even trying to sync
2223 if self.config.device_type == 'ipod' and not sync.gpod_available:
2224 title = _('Cannot Sync To iPod')
2225 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
2226 self.notification(message, title, important=True)
2227 return
2228 elif self.config.device_type == 'mtp' and not sync.pymtp_available:
2229 title = _('Cannot sync to MTP device')
2230 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
2231 self.notification(message, title, important=True)
2232 return
2234 device = sync.open_device(self.config)
2235 if device is not None:
2236 device.register( 'post-done', self.sync_to_ipod_completed )
2238 if device is None:
2239 title = _('No device configured')
2240 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2241 self.notification(message, title, widget=self.toolPreferences)
2242 return
2244 if not device.open():
2245 title = _('Cannot open device')
2246 message = _('There has been an error opening the device. Please check the settings in the preferences dialog.')
2247 self.notification(message, title, widget=self.toolPreferences)
2248 return
2250 if self.config.device_type == 'ipod':
2251 #update played episodes and delete if requested
2252 for channel in self.channels:
2253 if channel.sync_to_devices:
2254 allepisodes = [ episode for episode in channel.get_all_episodes() if episode.was_downloaded(and_exists=True) ]
2255 device.update_played_or_delete(channel, allepisodes, self.config.ipod_delete_played_from_db)
2257 if self.config.ipod_purge_old_episodes:
2258 device.purge()
2260 sync_all_episodes = not bool(episodes)
2262 if episodes is None:
2263 episodes = self.get_all_episodes()
2265 # make sure we have enough space on the device
2266 can_sync = True
2267 total_size = 0
2268 free_space = max(device.get_free_space(), 0)
2269 for episode in episodes:
2270 if not device.episode_on_device(episode) and not (sync_all_episodes and self.config.only_sync_not_played and episode.is_played):
2271 filename = episode.local_filename(create=False)
2272 if filename is not None:
2273 total_size += util.calculate_size(str(filename))
2275 if total_size > free_space:
2276 title = _('Not enough space left on device')
2277 message = _('You need to free up %s.\nDo you want to continue?') % (util.format_filesize(total_size-free_space),)
2278 can_sync = self.show_confirmation(message, title)
2280 if self.tray_icon:
2281 self.tray_icon.set_synchronisation_device(device)
2283 if can_sync:
2284 gPodderSyncProgress(self.gPodder, device=device, gPodder=self)
2285 threading.Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
2286 else:
2287 device.close()
2289 # The sync process might have updated the status of episodes,
2290 # therefore persist the database here to avoid losing data
2291 self.db.commit()
2293 def sync_to_ipod_completed(self, device, successful_sync):
2294 device.unregister( 'post-done', self.sync_to_ipod_completed )
2296 if self.tray_icon:
2297 self.tray_icon.release_synchronisation_device()
2299 if successful_sync:
2300 title = _('Device synchronized')
2301 message = _('Your device has been synchronized with gPodder.')
2302 self.notification(message, title)
2303 else:
2304 title = _('Error closing device')
2305 message = _('There has been an error closing your device.')
2306 self.notification(message, title, important=True)
2308 # update model for played state updates after sync
2309 util.idle_add(self.updateComboBox)
2311 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
2312 if sync_all_episodes:
2313 device.add_tracks(episodes)
2314 # 'only_sync_not_played' must be used or else all the played
2315 # tracks will be copied then immediately deleted
2316 if self.config.mp3_player_delete_played and self.config.only_sync_not_played:
2317 self.ipod_delete_played(device)
2318 else:
2319 device.add_tracks(episodes, force_played=True)
2320 device.close()
2321 self.update_selected_episode_list_icons()
2323 def ipod_cleanup_callback(self, device, tracks):
2324 title = _('Delete podcasts from device?')
2325 message = _('The selected episodes will be removed from your device. This cannot be undone. Files in your gPodder library will be unaffected. Do you really want to delete these episodes from your device?')
2326 if len(tracks) > 0 and self.show_confirmation(message, title):
2327 gPodderSyncProgress(self.gPodder, device=device, gPodder=self)
2328 threading.Thread(target=self.ipod_cleanup_thread, args=[device, tracks]).start()
2330 def ipod_cleanup_thread(self, device, tracks):
2331 device.remove_tracks(tracks)
2333 if not device.close():
2334 title = _('Error closing device')
2335 message = _('There has been an error closing your device.')
2336 self.notification(message, title, important=True)
2338 def on_cleanup_ipod_activate(self, widget, *args):
2339 columns = (
2340 ('title', None, None, _('Episode')),
2341 ('podcast', None, None, _('Podcast')),
2342 ('filesize', None, None, _('Size')),
2343 ('modified', 'modified_sort', gobject.TYPE_INT, _('Copied')),
2344 ('playcount', None, None, _('Play count')),
2345 ('released', None, None, _('Released')),
2348 device = sync.open_device(self.config)
2350 if device is None:
2351 title = _('No device configured')
2352 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2353 self.show_message(message, title, widget=self.toolPreferences)
2354 return
2356 if not device.open():
2357 title = _('Cannot open device')
2358 message = _('There has been an error opening the device. Please check the settings in the preferences dialog.')
2359 self.show_message(message, title, widget=self.toolPreferences)
2360 return
2362 tracks = device.get_all_tracks()
2363 if len(tracks) > 0:
2364 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
2365 wanted_columns = []
2366 for key, sort_name, sort_type, caption in columns:
2367 want_this_column = False
2368 for track in tracks:
2369 if getattr(track, key) is not None:
2370 want_this_column = True
2371 break
2373 if want_this_column:
2374 wanted_columns.append((key, sort_name, sort_type, caption))
2375 title = _('Remove podcasts from device')
2376 instructions = _('Select the podcast episodes you want to remove from your device.')
2377 gPodderEpisodeSelector(self.gPodder, title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
2378 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None, \
2379 _config=self.config)
2380 else:
2381 title = _('No files on device')
2382 message = _('The devices contains no files to be removed.')
2383 self.show_message(message, title)
2384 device.close()
2386 def on_manage_device_playlist(self, widget):
2387 # make sure gpod is available before even trying to sync
2388 if self.config.device_type == 'ipod' and not sync.gpod_available:
2389 title = _('Cannot manage iPod playlist')
2390 message = _('This feature is not available for iPods.')
2391 self.notification(message, title)
2392 return
2393 elif self.config.device_type == 'mtp' and not sync.pymtp_available:
2394 title = _('Cannot manage MTP device playlist')
2395 message = _('This feature is not available for MTP devices.')
2396 self.notification(message, title)
2397 return
2399 device = sync.open_device(self.config)
2401 if device is None:
2402 title = _('No device configured')
2403 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2404 self.notification(message, title, widget=self.toolPreferences)
2405 return
2407 if not device.open():
2408 title = _('Cannot open device')
2409 message = _('There has been an error opening the device. Please check the settings in the preferences dialog.')
2410 self.notification(message, title, widget=self.toolPreferences)
2411 return
2413 gPodderDevicePlaylist(self.gPodder, device=device, gPodder=self, _config=self.config)
2414 device.close()
2416 def show_hide_tray_icon(self):
2417 if self.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2418 self.tray_icon = trayicon.GPodderStatusIcon(self, gpodder.icon_file, self.config)
2419 elif not self.config.display_tray_icon and self.tray_icon is not None:
2420 self.tray_icon.set_visible(False)
2421 del self.tray_icon
2422 self.tray_icon = None
2424 if self.config.minimize_to_tray and self.tray_icon:
2425 self.tray_icon.set_visible(self.minimized)
2426 elif self.tray_icon:
2427 self.tray_icon.set_visible(True)
2429 def on_itemShowToolbar_activate(self, widget):
2430 self.config.show_toolbar = self.itemShowToolbar.get_active()
2432 def on_itemShowDescription_activate(self, widget):
2433 self.config.episode_list_descriptions = self.itemShowDescription.get_active()
2435 def on_item_view_episodes_changed(self, radioaction, current):
2436 if current == self.item_view_episodes_all:
2437 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_ALL)
2438 elif current == self.item_view_episodes_undeleted:
2439 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_UNDELETED)
2440 elif current == self.item_view_episodes_downloaded:
2441 self.episode_list_model.set_view_mode(EpisodeListModel.VIEW_DOWNLOADED)
2443 self.config.episode_list_view_mode = self.episode_list_model.get_view_mode()
2445 def update_item_device( self):
2446 if self.config.device_type != 'none':
2447 self.itemDevice.set_visible(True)
2448 self.itemDevice.label = self.get_device_name()
2449 else:
2450 self.itemDevice.set_visible(False)
2452 def properties_closed( self):
2453 self.show_hide_tray_icon()
2454 self.update_item_device()
2455 self.updateComboBox()
2457 def on_itemPreferences_activate(self, widget, *args):
2458 gPodderPreferences(self.gPodder, _config=self.config, \
2459 callback_finished=self.properties_closed, \
2460 user_apps_reader=self.user_apps_reader)
2462 def on_itemDependencies_activate(self, widget):
2463 gPodderDependencyManager(self.gPodder)
2465 def on_upgrade_from_videocenter(self, widget):
2466 from gpodder import nokiavideocenter
2467 vc = nokiavideocenter.UpgradeFromVideocenter()
2468 if vc.db2opml():
2469 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
2470 custom_title=_('Import podcasts from Video Center'), \
2471 add_urls_callback=self.add_podcast_list, \
2472 hide_url_entry=True)
2473 dir.download_opml_file(vc.opmlfile)
2474 else:
2475 self.show_message(_('Have you installed Video Center on your tablet?'), _('Cannot find Video Center subscriptions'), important=True)
2477 def require_my_gpodder_authentication(self):
2478 if not self.config.my_gpodder_username or not self.config.my_gpodder_password:
2479 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'))
2480 if success and authentication[0] and authentication[1]:
2481 self.config.my_gpodder_username, self.config.my_gpodder_password = authentication
2482 return True
2483 else:
2484 return False
2486 return True
2488 def my_gpodder_offer_autoupload(self):
2489 if not self.config.my_gpodder_autoupload:
2490 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')):
2491 self.config.my_gpodder_autoupload = True
2493 def on_download_from_mygpo(self, widget):
2494 if self.require_my_gpodder_authentication():
2495 client = my.MygPodderClient(self.config.my_gpodder_username, self.config.my_gpodder_password)
2496 opml_data = client.download_subscriptions()
2497 if len(opml_data) > 0:
2498 fp = open(gpodder.subscription_file, 'w')
2499 fp.write(opml_data)
2500 fp.close()
2501 (added, skipped) = (0, 0)
2502 i = opml.Importer(gpodder.subscription_file)
2504 existing = [c.url for c in self.channels]
2505 urls = [item['url'] for item in i.items if item['url'] not in existing]
2507 skipped = len(i.items) - len(urls)
2508 added = len(urls)
2510 self.add_podcast_list(urls)
2512 self.my_gpodder_offer_autoupload()
2513 if added > 0:
2514 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'), widget=self.treeChannels)
2515 elif widget is not None:
2516 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget=self.treeChannels)
2517 else:
2518 self.config.my_gpodder_password = ''
2519 self.on_download_from_mygpo(widget)
2520 else:
2521 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
2523 def on_upload_to_mygpo(self, widget):
2524 if self.require_my_gpodder_authentication():
2525 client = my.MygPodderClient(self.config.my_gpodder_username, self.config.my_gpodder_password)
2526 self.save_channels_opml()
2527 success, messages = client.upload_subscriptions(gpodder.subscription_file)
2528 if widget is not None:
2529 if not success:
2530 self.show_message('\n'.join(messages), _('Results of upload'), important=True)
2531 self.config.my_gpodder_password = ''
2532 self.on_upload_to_mygpo(widget)
2533 else:
2534 self.my_gpodder_offer_autoupload()
2535 self.show_message('\n'.join(messages), _('Results of upload'), widget=self.treeChannels)
2536 elif not success:
2537 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2538 elif widget is not None:
2539 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important=True)
2541 def on_itemAddChannel_activate(self, widget, *args):
2542 gPodderAddPodcast(self.gPodder, \
2543 add_urls_callback=self.add_podcast_list)
2545 def on_itemEditChannel_activate(self, widget, *args):
2546 if self.active_channel is None:
2547 title = _('No podcast selected')
2548 message = _('Please select a podcast in the podcasts list to edit.')
2549 self.show_message( message, title, widget=self.treeChannels)
2550 return
2552 gPodderChannel(self.main_window, channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), cover_downloader=self.cover_downloader)
2554 def on_itemRemoveChannel_activate(self, widget, *args):
2555 try:
2556 if gpodder.interface == gpodder.GUI:
2557 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2558 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2559 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2561 title = _('Remove podcast and episodes?')
2562 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2564 dialog.set_title(title)
2565 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2567 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2568 dialog.vbox.pack_start(cb_ask)
2569 cb_ask.show_all()
2570 affirmative = gtk.RESPONSE_YES
2571 elif gpodder.interface == gpodder.MAEMO:
2572 cb_ask = gtk.CheckButton('') # dummy check button
2573 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2574 affirmative = gtk.RESPONSE_OK
2576 result = dialog.run()
2577 dialog.destroy()
2579 if result == affirmative:
2580 keep_episodes = cb_ask.get_active()
2581 # delete downloaded episodes only if checkbox is unchecked
2582 if keep_episodes:
2583 log('Not removing downloaded episodes', sender=self)
2584 else:
2585 self.active_channel.remove_downloaded()
2587 # Clean up downloads and download directories
2588 self.clean_up_downloads()
2590 # cancel any active downloads from this channel
2591 for episode in self.active_channel.get_all_episodes():
2592 self.download_status_model.cancel_by_url(episode.url)
2594 # get the URL of the podcast we want to select next
2595 position = self.channels.index(self.active_channel)
2596 if position == len(self.channels)-1:
2597 # this is the last podcast, so select the URL
2598 # of the item before this one (i.e. the "new last")
2599 select_url = self.channels[position-1].url
2600 else:
2601 # there is a podcast after the deleted one, so
2602 # we simply select the one that comes after it
2603 select_url = self.channels[position+1].url
2605 # Remove the channel
2606 self.active_channel.delete(purge=not keep_episodes)
2607 self.channels.remove(self.active_channel)
2608 self.channel_list_changed = True
2609 self.save_channels_opml()
2611 # Re-load the channels and select the desired new channel
2612 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2613 except:
2614 log('There has been an error removing the channel.', traceback=True, sender=self)
2615 self.update_podcasts_tab()
2617 def get_opml_filter(self):
2618 filter = gtk.FileFilter()
2619 filter.add_pattern('*.opml')
2620 filter.add_pattern('*.xml')
2621 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2622 return filter
2624 def on_item_import_from_file_activate(self, widget, filename=None):
2625 if filename is None:
2626 if gpodder.interface == gpodder.GUI:
2627 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2628 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2629 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2630 elif gpodder.interface == gpodder.MAEMO:
2631 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2632 dlg.set_filter(self.get_opml_filter())
2633 response = dlg.run()
2634 filename = None
2635 if response == gtk.RESPONSE_OK:
2636 filename = dlg.get_filename()
2637 dlg.destroy()
2639 if filename is not None:
2640 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
2641 custom_title=_('Import podcasts from OPML file'), \
2642 add_urls_callback=self.add_podcast_list, \
2643 hide_url_entry=True)
2644 dir.download_opml_file(filename)
2646 def on_itemExportChannels_activate(self, widget, *args):
2647 if not self.channels:
2648 title = _('Nothing to export')
2649 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2650 self.show_message(message, title, widget=self.treeChannels)
2651 return
2653 if gpodder.interface == gpodder.GUI:
2654 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2655 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2656 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2657 elif gpodder.interface == gpodder.MAEMO:
2658 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2659 dlg.set_filter(self.get_opml_filter())
2660 response = dlg.run()
2661 if response == gtk.RESPONSE_OK:
2662 filename = dlg.get_filename()
2663 dlg.destroy()
2664 exporter = opml.Exporter( filename)
2665 if exporter.write(self.channels):
2666 if len(self.channels) == 1:
2667 title = _('One subscription exported')
2668 else:
2669 title = _('%d subscriptions exported') % len(self.channels)
2670 self.show_message(_('Your podcast list has been successfully exported.'), title, widget=self.treeChannels)
2671 else:
2672 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important=True)
2673 else:
2674 dlg.destroy()
2676 def on_itemImportChannels_activate(self, widget, *args):
2677 dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
2678 add_urls_callback=self.add_podcast_list)
2679 dir.download_opml_file(self.config.opml_url)
2681 def on_homepage_activate(self, widget, *args):
2682 util.open_website(gpodder.__url__)
2684 def on_wiki_activate(self, widget, *args):
2685 util.open_website('http://wiki.gpodder.org/')
2687 def on_bug_tracker_activate(self, widget, *args):
2688 if gpodder.interface == gpodder.MAEMO:
2689 util.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
2690 else:
2691 util.open_website('http://bugs.gpodder.org/')
2693 def on_shop_activate(self, widget, *args):
2694 util.open_website('http://gpodder.org/shop')
2696 def on_wishlist_activate(self, widget, *args):
2697 util.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
2699 def on_itemAbout_activate(self, widget, *args):
2700 dlg = gtk.AboutDialog()
2701 dlg.set_name('gPodder')
2702 dlg.set_version(gpodder.__version__)
2703 dlg.set_copyright(gpodder.__copyright__)
2704 dlg.set_website(gpodder.__url__)
2705 dlg.set_translator_credits( _('translator-credits'))
2706 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2708 if gpodder.interface == gpodder.GUI:
2709 # For the "GUI" version, we add some more
2710 # items to the about dialog (credits and logo)
2711 app_authors = [
2712 _('Maintainer:'),
2713 'Thomas Perl <thpinfo.com>',
2716 if os.path.exists(gpodder.credits_file):
2717 credits = open(gpodder.credits_file).read().strip().split('\n')
2718 app_authors += ['', _('Patches, bug reports and donations by:')]
2719 app_authors += credits
2721 dlg.set_authors(app_authors)
2722 try:
2723 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(gpodder.icon_file))
2724 except:
2725 dlg.set_logo_icon_name('gpodder')
2727 dlg.run()
2729 def on_wNotebook_switch_page(self, widget, *args):
2730 page_num = args[1]
2731 if gpodder.interface == gpodder.MAEMO:
2732 self.tool_downloads.set_active(page_num == 1)
2733 page = self.wNotebook.get_nth_page(page_num)
2734 tab_label = self.wNotebook.get_tab_label(page).get_text()
2735 if page_num == 0 and self.active_channel is not None:
2736 self.set_title(self.active_channel.title)
2737 else:
2738 self.set_title(tab_label)
2739 if page_num == 0:
2740 self.play_or_download()
2741 self.menuChannels.set_sensitive(True)
2742 self.menuSubscriptions.set_sensitive(True)
2743 # The message area in the downloads tab should be hidden
2744 # when the user switches away from the downloads tab
2745 if self.message_area is not None:
2746 self.message_area.hide()
2747 self.message_area = None
2748 else:
2749 self.menuChannels.set_sensitive(False)
2750 self.menuSubscriptions.set_sensitive(False)
2751 self.toolDownload.set_sensitive(False)
2752 self.toolPlay.set_sensitive(False)
2753 self.toolTransfer.set_sensitive(False)
2754 self.toolCancel.set_sensitive(False)
2756 def on_treeChannels_row_activated(self, widget, path, *args):
2757 # double-click action of the podcast list or enter
2758 self.treeChannels.set_cursor(path)
2760 def on_treeChannels_cursor_changed(self, widget, *args):
2761 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2763 if model is not None and iter is not None:
2764 old_active_channel = self.active_channel
2765 self.active_channel = model.get_value(iter, PodcastListModel.C_CHANNEL)
2767 if self.active_channel == old_active_channel:
2768 return
2770 if gpodder.interface == gpodder.MAEMO:
2771 self.set_title(self.active_channel.title)
2772 self.itemEditChannel.set_visible(True)
2773 self.itemRemoveChannel.set_visible(True)
2774 else:
2775 self.active_channel = None
2776 self.itemEditChannel.set_visible(False)
2777 self.itemRemoveChannel.set_visible(False)
2779 self.updateTreeView()
2781 def on_btnEditChannel_clicked(self, widget, *args):
2782 self.on_itemEditChannel_activate( widget, args)
2784 def get_selected_episodes(self):
2785 """Get a list of selected episodes from treeAvailable"""
2786 selection = self.treeAvailable.get_selection()
2787 model, paths = selection.get_selected_rows()
2789 episodes = [model.get_value(model.get_iter(path), EpisodeListModel.C_EPISODE) for path in paths]
2790 return episodes
2792 def on_transfer_selected_episodes(self, widget):
2793 self.on_sync_to_ipod_activate(widget, self.get_selected_episodes())
2795 def on_playback_selected_episodes(self, widget):
2796 self.playback_episodes(self.get_selected_episodes())
2798 def on_shownotes_selected_episodes(self, widget):
2799 episodes = self.get_selected_episodes()
2800 if episodes:
2801 episode = episodes.pop(0)
2802 self.show_episode_shownotes(episode)
2803 else:
2804 self.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget=self.treeAvailable)
2806 def on_download_selected_episodes(self, widget):
2807 episodes = self.get_selected_episodes()
2808 self.download_episode_list(episodes)
2809 self.update_episode_list_icons([episode.url for episode in episodes])
2810 self.play_or_download()
2812 def on_treeAvailable_row_activated(self, widget, path, view_column):
2813 """Double-click/enter action handler for treeAvailable"""
2814 # We should only have one one selected as it was double clicked!
2815 e = self.get_selected_episodes()[0]
2817 if (self.config.double_click_episode_action == 'download'):
2818 # If the episode has already been downloaded and exists then play it
2819 if e.was_downloaded(and_exists=True):
2820 self.playback_episodes(self.get_selected_episodes())
2821 # else download it if it is not already downloading
2822 elif not self.episode_is_downloading(e):
2823 self.download_episode_list([e])
2824 self.update_episode_list_icons([e.url])
2825 self.play_or_download()
2826 elif (self.config.double_click_episode_action == 'stream'):
2827 # If we happen to have downloaded this episode simple play it
2828 if e.was_downloaded(and_exists=True):
2829 self.playback_episodes(self.get_selected_episodes())
2830 # else if streaming is possible stream it
2831 elif self.streaming_possible():
2832 self.playback_episodes(self.get_selected_episodes())
2833 else:
2834 log('Unable to stream episode - default media player selected!', sender=self, traceback=True)
2835 self.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget=self.toolPreferences)
2836 else:
2837 # default action is to display show notes
2838 self.on_shownotes_selected_episodes(widget)
2840 def show_episode_shownotes(self, episode):
2841 play_callback = lambda: self.playback_episodes([episode])
2842 def download_callback():
2843 self.download_episode_list([episode])
2844 self.play_or_download()
2845 if self.episode_shownotes_window is None:
2846 log('First-time use of episode window --- creating', sender=self)
2847 self.episode_shownotes_window = gPodderShownotes(self.gPodder, _config=self.config, \
2848 download_status_model=self.download_status_model, \
2849 episode_is_downloading=self.episode_is_downloading)
2850 self.episode_shownotes_window.show(episode=episode, download_callback=download_callback, play_callback=play_callback)
2852 def on_treeAvailable_button_release_event(self, widget, *args):
2853 self.play_or_download()
2855 def auto_update_procedure(self, first_run=False):
2856 log('auto_update_procedure() got called', sender=self)
2857 if not first_run and self.config.auto_update_feeds and self.minimized:
2858 self.update_feed_cache(force_update=True)
2860 next_update = 60*1000*self.config.auto_update_frequency
2861 gobject.timeout_add(next_update, self.auto_update_procedure)
2863 def on_treeDownloads_row_activated(self, widget, *args):
2864 if self.wNotebook.get_current_page() == 0:
2865 # Use the available podcasts treeview + model
2866 selection = self.treeAvailable.get_selection()
2867 (model, paths) = selection.get_selected_rows()
2868 urls = [model.get_value(model.get_iter(path), 0) for path in paths]
2869 selected_tasks = [task for task in self.download_tasks_seen if task.url in urls]
2870 for task in selected_tasks:
2871 task.status = task.CANCELLED
2872 self.update_selected_episode_list_icons()
2873 self.play_or_download()
2874 return
2876 # Use the standard way of working on the treeview
2877 selection = self.treeDownloads.get_selection()
2878 (model, paths) = selection.get_selected_rows()
2879 selected_tasks = [(gtk.TreeRowReference(model, path), model.get_value(model.get_iter(path), 0)) for path in paths]
2881 for tree_row_reference, task in selected_tasks:
2882 if task.status in (task.DOWNLOADING, task.QUEUED):
2883 task.status = task.PAUSED
2884 elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
2885 self.download_queue_manager.add_task(task)
2886 self.enable_download_list_update()
2887 elif task.status == task.DONE:
2888 model.remove(model.get_iter(tree_row_reference.get_path()))
2890 self.play_or_download()
2892 # Update the tab title and downloads list
2893 self.update_downloads_list()
2895 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
2896 self.on_treeDownloads_row_activated( widget, None)
2898 def on_btnCancelAll_clicked(self, widget, *args):
2899 self.treeDownloads.get_selection().select_all()
2900 self.on_treeDownloads_row_activated( self.toolCancel, None)
2901 self.treeDownloads.get_selection().unselect_all()
2903 # Update the tab title and downloads list
2904 self.update_downloads_list()
2906 def on_btnDownloadedDelete_clicked(self, widget, *args):
2907 if self.active_channel is None:
2908 return
2910 if self.wNotebook.get_current_page() == 1:
2911 # Downloads tab visible - no action!
2912 return
2914 episodes = self.get_selected_episodes()
2916 if not episodes:
2917 log('Nothing selected - will not remove any downloaded episode.')
2918 return
2920 if len(episodes) == 1:
2921 episode = episodes[0]
2922 if episode.is_locked:
2923 title = _('%s is locked') % saxutils.escape(episode.title)
2924 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2925 self.notification(message, title, widget=self.treeAvailable)
2926 return
2928 title = _('Remove %s?') % saxutils.escape(episode.title)
2929 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.")
2930 else:
2931 title = _('Remove %d episodes?') % len(episodes)
2932 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.')
2934 locked_count = sum(int(e.is_locked) for e in episodes if e.is_locked is not None)
2936 if len(episodes) == locked_count:
2937 title = _('Episodes are locked')
2938 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2939 self.notification(message, title, widget=self.treeAvailable)
2940 return
2941 elif locked_count > 0:
2942 title = _('Remove %d out of %d episodes?') % (len(episodes)-locked_count, len(episodes))
2943 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.')
2945 # if user confirms deletion, let's remove some stuff ;)
2946 if self.show_confirmation(message, title):
2947 for episode in episodes:
2948 if not episode.is_locked:
2949 episode.delete_from_disk()
2950 self.updateComboBox(only_selected_channel=True)
2952 # only delete partial files if we do not have any downloads in progress
2953 self.clean_up_downloads(False)
2954 self.update_selected_episode_list_icons()
2955 self.play_or_download()
2957 def on_key_press(self, widget, event):
2958 # Allow tab switching with Ctrl + PgUp/PgDown
2959 if event.state & gtk.gdk.CONTROL_MASK:
2960 if event.keyval == gtk.keysyms.Page_Up:
2961 self.wNotebook.prev_page()
2962 return True
2963 elif event.keyval == gtk.keysyms.Page_Down:
2964 self.wNotebook.next_page()
2965 return True
2967 # After this code we only handle Maemo hardware keys,
2968 # so if we are not a Maemo app, we don't do anything
2969 if gpodder.interface != gpodder.MAEMO:
2970 return False
2972 if event.keyval == gtk.keysyms.F6:
2973 if self.fullscreen:
2974 self.window.unfullscreen()
2975 else:
2976 self.window.fullscreen()
2977 if event.keyval == gtk.keysyms.Escape:
2978 new_visibility = not self.vboxChannelNavigator.get_property('visible')
2979 self.vboxChannelNavigator.set_property('visible', new_visibility)
2980 self.column_size.set_visible(not new_visibility)
2981 self.column_released.set_visible(not new_visibility)
2983 diff = 0
2984 if event.keyval == gtk.keysyms.F7: #plus
2985 diff = 1
2986 elif event.keyval == gtk.keysyms.F8: #minus
2987 diff = -1
2989 if diff != 0 and not self.currently_updating:
2990 selection = self.treeChannels.get_selection()
2991 (model, iter) = selection.get_selected()
2992 new_path = ((model.get_path(iter)[0]+diff)%len(model),)
2993 selection.select_path(new_path)
2994 self.treeChannels.set_cursor(new_path)
2995 return True
2997 return False
2999 def window_state_event(self, widget, event):
3000 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
3001 self.fullscreen = True
3002 else:
3003 self.fullscreen = False
3005 old_minimized = self.minimized
3007 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
3008 if gpodder.interface == gpodder.MAEMO:
3009 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
3011 if old_minimized != self.minimized and self.tray_icon:
3012 self.gPodder.set_skip_taskbar_hint(self.minimized)
3013 elif not self.tray_icon:
3014 self.gPodder.set_skip_taskbar_hint(False)
3016 if self.config.minimize_to_tray and self.tray_icon:
3017 self.tray_icon.set_visible(self.minimized)
3019 def uniconify_main_window(self):
3020 if self.minimized:
3021 self.gPodder.present()
3023 def iconify_main_window(self):
3024 if not self.minimized:
3025 self.gPodder.iconify()
3027 def update_podcasts_tab(self):
3028 if len(self.channels):
3029 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
3030 else:
3031 self.label2.set_text(_('Podcasts'))
3033 @dbus.service.method(gpodder.dbus_interface)
3034 def show_gui_window(self):
3035 self.gPodder.present()
3037 @dbus.service.method(gpodder.dbus_interface)
3038 def subscribe_to_url(self, url):
3039 gPodderAddPodcast(self.gPodder,
3040 add_urls_callback=self.add_podcast_list,
3041 preset_url=url)
3044 def main(options=None):
3045 gobject.threads_init()
3046 gtk.window_set_default_icon_name( 'gpodder')
3048 try:
3049 session_bus = dbus.SessionBus(mainloop=dbus.glib.DBusGMainLoop())
3050 bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=session_bus)
3051 except dbus.exceptions.DBusException, dbe:
3052 log('Warning: Cannot get "on the bus".', traceback=True)
3053 dlg = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, \
3054 gtk.BUTTONS_CLOSE, _('Cannot start gPodder'))
3055 dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
3056 dlg.set_title('gPodder')
3057 dlg.run()
3058 dlg.destroy()
3059 sys.exit(0)
3061 util.make_directory(gpodder.home)
3062 config = UIConfig(gpodder.config_file)
3064 if gpodder.interface == gpodder.MAEMO:
3065 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3066 # folder exists there (allow moving "gpodder" between SD cards or USB)
3067 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3068 if not os.path.exists(config.download_dir):
3069 log('Downloads might have been moved. Trying to locate them...')
3070 for basedir in ['/media/mmc1', '/media/mmc2']+glob.glob('/media/usb/*')+['/home/user']:
3071 dir = os.path.join(basedir, 'gpodder')
3072 if os.path.exists(dir):
3073 log('Downloads found in: %s', dir)
3074 config.download_dir = dir
3075 break
3076 else:
3077 log('Downloads NOT FOUND in %s', dir)
3079 if not config.disable_fingerscroll:
3080 BuilderWidget.use_fingerscroll = True
3082 gp = gPodder(bus_name, config)
3084 # Handle options
3085 if options.subscribe:
3086 util.idle_add(gp.subscribe_to_url, options.subscribe)
3088 gp.run()