1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
36 from xml
.sax
import saxutils
46 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
49 def __init__(self
, *args
, **kwargs
):
56 def method(interface
):
59 def __init__(self
, *args
, **kwargs
):
62 def __init__(self
, *args
, **kwargs
):
66 from gpodder
import feedcore
67 from gpodder
import util
68 from gpodder
import opml
69 from gpodder
import download
70 from gpodder
import my
71 from gpodder
.liblogger
import log
76 from gpodder
.model
import PodcastChannel
77 from gpodder
.dbsqlite
import Database
79 from gpodder
.gtkui
.model
import PodcastListModel
80 from gpodder
.gtkui
.model
import EpisodeListModel
81 from gpodder
.gtkui
.config
import UIConfig
82 from gpodder
.gtkui
.services
import CoverDownloader
83 from gpodder
.gtkui
.widgets
import SimpleMessageArea
84 from gpodder
.gtkui
.desktopfile
import UserAppsReader
86 from gpodder
.gtkui
.draw
import draw_text_box_centered
88 from gpodder
.gtkui
.interface
.common
import BuilderWidget
89 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
90 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
92 if gpodder
.ui
.desktop
:
93 from gpodder
.gtkui
.download
import DownloadStatusModel
95 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
97 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
98 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
99 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
100 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
101 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
102 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
104 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
106 except Exception, exc
:
107 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
108 log('Warning: This probably means your PyGTK installation is too old!')
109 have_trayicon
= False
110 elif gpodder
.ui
.diablo
:
111 from gpodder
.gtkui
.download
import DownloadStatusModel
113 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
114 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
115 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
116 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
117 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
118 have_trayicon
= False
119 elif gpodder
.ui
.fremantle
:
120 from gpodder
.gtkui
.frmntl
.model
import DownloadStatusModel
121 from gpodder
.gtkui
.frmntl
.model
import EpisodeListModel
122 from gpodder
.gtkui
.frmntl
.model
import PodcastListModel
124 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
125 from gpodder
.gtkui
.frmntl
.preferences
import gPodderPreferences
126 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
127 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
128 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
129 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
130 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
131 from gpodder
.gtkui
.interface
.common
import Orientation
132 have_trayicon
= False
134 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
136 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
137 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
142 class gPodder(BuilderWidget
, dbus
.service
.Object
):
143 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear']
145 ICON_GENERAL_ADD
= 'general_add'
146 ICON_GENERAL_REFRESH
= 'general_refresh'
147 ICON_GENERAL_CLOSE
= 'general_close'
149 def __init__(self
, bus_name
, config
):
150 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
151 self
.db
= Database(gpodder
.database_file
)
153 BuilderWidget
.__init
__(self
, None)
156 if gpodder
.ui
.diablo
:
158 self
.app
= hildon
.Program()
159 self
.app
.add_window(self
.main_window
)
160 self
.main_window
.add_toolbar(self
.toolbar
)
162 for child
in self
.main_menu
.get_children():
164 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
165 self
.bluetooth_available
= False
166 elif gpodder
.ui
.fremantle
:
168 self
.app
= hildon
.Program()
169 self
.app
.add_window(self
.main_window
)
171 appmenu
= hildon
.AppMenu()
173 for filter in (self
.item_view_podcasts_all
, \
174 self
.item_view_podcasts_downloaded
, \
175 self
.item_view_podcasts_unplayed
):
176 button
= gtk
.ToggleButton()
177 filter.connect_proxy(button
)
178 appmenu
.add_filter(button
)
180 for action
in (self
.itemPreferences
, \
181 self
.item_downloads
, \
182 self
.itemRemoveOldEpisodes
, \
183 self
.item_unsubscribe
, \
185 self
.item_report_bug
):
186 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
187 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
188 action
.connect_proxy(button
)
189 if action
== self
.item_downloads
:
190 button
.set_title(_('Downloads'))
191 button
.set_value(_('Idle'))
192 self
.button_downloads
= button
193 appmenu
.append(button
)
195 self
.main_window
.set_app_menu(appmenu
)
197 # Initialize portrait mode / rotation manager
198 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
200 gpodder
.__version
__, \
201 self
.config
.rotation_mode
)
203 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
204 util
.idle_add(self
.on_window_orientation_changed
, \
205 Orientation
.PORTRAIT
)
207 self
.bluetooth_available
= False
209 self
.bluetooth_available
= util
.bluetooth_available()
210 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
212 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
213 if not gpodder
.ui
.fremantle
:
214 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
215 self
.main_window
.show()
217 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
219 self
.config
.add_observer(self
.on_config_changed
)
221 self
.tray_icon
= None
222 self
.episode_shownotes_window
= None
223 self
.new_episodes_window
= None
225 if gpodder
.ui
.desktop
:
226 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
227 self
.main_window
, self
.show_confirmation
, \
228 self
.update_episode_list_icons
, \
229 self
.update_podcast_list_model
, self
.toolPreferences
, \
230 gPodderEpisodeSelector
)
234 self
.download_status_model
= DownloadStatusModel()
235 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
237 if gpodder
.ui
.desktop
:
238 self
.show_hide_tray_icon()
239 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
240 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
242 if not gpodder
.ui
.fremantle
:
243 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
244 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
245 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
246 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
248 # When the amount of maximum downloads changes, notify the queue manager
249 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
250 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
252 self
.default_title
= 'gPodder'
253 if gpodder
.__version
__.rfind('git') != -1:
254 self
.set_title('gPodder %s' % gpodder
.__version
__)
256 title
= self
.gPodder
.get_title()
257 if title
is not None:
258 self
.set_title(title
)
260 self
.set_title(_('gPodder'))
262 self
.cover_downloader
= CoverDownloader()
264 # Generate list models for podcasts and their episodes
265 self
.podcast_list_model
= PodcastListModel(self
.config
.podcast_list_icon_size
, self
.cover_downloader
)
267 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
268 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
270 if gpodder
.ui
.fremantle
:
271 # Work around Maemo bug #4718
272 self
.button_refresh
.set_name('HildonButton-finger')
273 self
.button_subscribe
.set_name('HildonButton-finger')
275 self
.button_refresh
.set_sensitive(False)
276 self
.button_subscribe
.set_sensitive(False)
278 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
279 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
280 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
281 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
283 # Make the button scroll together with the TreeView contents
284 action_area_box
= self
.treeChannels
.get_action_area_box()
285 for child
in self
.buttonbox
:
286 child
.reparent(action_area_box
)
287 self
.vbox
.remove(self
.buttonbox
)
288 action_area_box
.set_spacing(2)
289 action_area_box
.set_border_width(3)
290 self
.treeChannels
.set_action_area_visible(True)
292 from gpodder
.gtkui
.frmntl
import style
293 sub_font
= style
.get_font_desc('SmallSystemFont')
294 sub_color
= style
.get_color('SecondaryTextColor')
295 sub
= (sub_font
.to_string(), sub_color
.to_string())
296 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
297 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
299 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
300 while gtk
.events_pending():
301 gtk
.main_iteration(False)
304 # Try to get the real package version from dpkg
305 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
306 version
, _stderr
= p
.communicate()
310 version
= gpodder
.__version
__
311 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
312 self
.label_footer
.hide()
314 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
315 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
316 show_episode_shownotes
=self
.show_episode_shownotes
, \
317 update_podcast_list_model
=self
.update_podcast_list_model
, \
318 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
319 item_view_episodes_all
=self
.item_view_episodes_all
, \
320 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
321 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
322 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
323 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
324 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
325 hide_episode_search
=self
.hide_episode_search
, \
326 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
327 playback_episodes
=self
.playback_episodes
, \
328 delete_episode_list
=self
.delete_episode_list
, \
329 episode_list_status_changed
=self
.episode_list_status_changed
, \
330 download_episode_list
=self
.download_episode_list
, \
331 episode_is_downloading
=self
.episode_is_downloading
, \
332 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
333 add_download_task_monitor
=self
.add_download_task_monitor
, \
334 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
335 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
)
337 # Expose objects for episode list type-ahead find
338 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
339 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
340 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
342 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
343 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
344 on_btnCleanUpDownloads_clicked
=self
.on_btnCleanUpDownloads_clicked
, \
345 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
346 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
349 self
.treeAvailable
= self
.episodes_window
.treeview
350 self
.treeDownloads
= self
.downloads_window
.treeview
352 # Init the treeviews that we use
353 self
.init_podcast_list_treeview()
354 self
.init_episode_list_treeview()
355 self
.init_download_list_treeview()
357 if self
.config
.podcast_list_hide_boring
:
358 self
.item_view_hide_boring_podcasts
.set_active(True)
360 self
.currently_updating
= False
363 self
.context_menu_mouse_button
= 1
365 self
.context_menu_mouse_button
= 3
367 if self
.config
.start_iconified
:
368 self
.iconify_main_window()
370 self
.download_tasks_seen
= set()
371 self
.download_list_update_enabled
= False
372 self
.last_download_count
= 0
373 self
.download_task_monitors
= set()
375 # Subscribed channels
376 self
.active_channel
= None
377 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
378 self
.channel_list_changed
= True
379 self
.update_podcasts_tab()
381 # load list of user applications for audio playback
382 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
384 time
.sleep(3) # give other parts of gpodder a chance to start up
385 self
.user_apps_reader
.read()
386 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'audio', False)
387 util
.idle_add(self
.user_apps_reader
.get_applications_as_model
, 'video', False)
388 threading
.Thread(target
=read_apps
).start()
390 # Set the "Device" menu item for the first time
391 if gpodder
.ui
.desktop
:
392 self
.update_item_device()
394 # Now, update the feed cache, when everything's in place
395 if not gpodder
.ui
.fremantle
:
396 self
.btnUpdateFeeds
.show()
397 self
.updating_feed_cache
= False
398 self
.feed_cache_update_cancelled
= False
399 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
401 # Look for partial file downloads
402 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
405 self
.message_area
= None
407 resumable_episodes
= []
408 if len(partial_files
) > 0:
409 for f
in partial_files
:
410 correct_name
= f
[:-len('.partial')] # strip ".partial"
411 log('Searching episode for file: %s', correct_name
, sender
=self
)
412 found_episode
= False
413 for c
in self
.channels
:
414 for e
in c
.get_all_episodes():
415 if e
.local_filename(create
=False, check_only
=True) == correct_name
:
416 log('Found episode: %s', e
.title
, sender
=self
)
417 resumable_episodes
.append(e
)
423 if not found_episode
:
424 log('Partial file without episode: %s', f
, sender
=self
)
427 if len(resumable_episodes
):
428 self
.download_episode_list_paused(resumable_episodes
)
429 if not gpodder
.ui
.fremantle
:
430 self
.message_area
= SimpleMessageArea(_('There are unfinished downloads from your last session.\nPick the ones you want to continue downloading.'))
431 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
432 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
433 self
.message_area
.show_all()
434 self
.wNotebook
.set_current_page(1)
436 self
.clean_up_downloads(delete_partial
=False)
438 self
.clean_up_downloads(delete_partial
=True)
440 # Start the auto-update procedure
441 self
._auto
_update
_timer
_source
_id
= None
442 if self
.config
.auto_update_feeds
:
443 self
.restart_auto_update_timer()
445 # Connect the auto cleanup button to the configuration
446 if gpodder
.ui
.desktop
or gpodder
.ui
.diablo
:
447 self
.config
.connect_gtk_togglebutton('auto_cleanup_downloads', \
448 self
.btnCleanUpDownloads
)
450 # Delete old episodes if the user wishes to
451 if self
.config
.auto_remove_old_episodes
:
452 old_episodes
= self
.get_old_episodes()
453 if len(old_episodes
) > 0:
454 self
.delete_episode_list(old_episodes
, confirm
=False)
455 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
457 if gpodder
.ui
.fremantle
:
458 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
459 self
.button_refresh
.set_sensitive(True)
460 self
.button_subscribe
.set_sensitive(True)
461 self
.main_window
.set_title(_('gPodder'))
462 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
464 # First-time users should be asked if they want to see the OPML
465 if not self
.channels
and not gpodder
.ui
.fremantle
:
466 util
.idle_add(self
.on_itemUpdate_activate
)
468 def on_podcast_selected(self
, treeview
, path
, column
):
470 model
= treeview
.get_model()
471 channel
= model
.get_value(model
.get_iter(path
), \
472 PodcastListModel
.C_CHANNEL
)
473 self
.active_channel
= channel
474 self
.update_episode_list_model()
475 self
.episodes_window
.channel
= self
.active_channel
476 self
.episodes_window
.show()
478 def on_button_subscribe_clicked(self
, button
):
479 self
.on_itemImportChannels_activate(button
)
481 def on_button_downloads_clicked(self
, widget
):
482 self
.downloads_window
.show()
484 def show_episode_in_download_manager(self
, episode
):
485 self
.downloads_window
.show()
486 model
= self
.treeDownloads
.get_model()
487 selection
= self
.treeDownloads
.get_selection()
488 selection
.unselect_all()
489 it
= model
.get_iter_first()
490 while it
is not None:
491 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
492 if task
.episode
.url
== episode
.url
:
493 selection
.select_iter(it
)
494 # FIXME: Scroll to selection in pannable area
496 it
= model
.iter_next(it
)
498 def for_each_episode_set_task_status(self
, episodes
, status
):
499 episode_urls
= set(episode
.url
for episode
in episodes
)
500 model
= self
.treeDownloads
.get_model()
501 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
502 model
.get_value(row
.iter, \
503 DownloadStatusModel
.C_TASK
)) for row
in model \
504 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
506 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
508 def on_window_orientation_changed(self
, orientation
):
509 treeview
= self
.treeChannels
510 if orientation
== Orientation
.PORTRAIT
:
511 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
512 # Work around Maemo bug #4718
513 self
.button_subscribe
.set_name('HildonButton-thumb')
514 self
.button_refresh
.set_name('HildonButton-thumb')
516 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
517 # Work around Maemo bug #4718
518 self
.button_subscribe
.set_name('HildonButton-finger')
519 self
.button_refresh
.set_name('HildonButton-finger')
521 def on_treeview_podcasts_selection_changed(self
, selection
):
522 model
, iter = selection
.get_selected()
524 self
.active_channel
= None
525 self
.episode_list_model
.clear()
527 def on_treeview_button_pressed(self
, treeview
, event
):
528 if event
.window
!= treeview
.get_bin_window():
531 TreeViewHelper
.save_button_press_event(treeview
, event
)
533 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
534 TreeViewHelper
.ROLE_PODCASTS
:
535 return self
.currently_updating
537 return event
.button
== self
.context_menu_mouse_button
and \
540 def on_treeview_podcasts_button_released(self
, treeview
, event
):
541 if event
.window
!= treeview
.get_bin_window():
545 return self
.treeview_channels_handle_gestures(treeview
, event
)
546 return self
.treeview_channels_show_context_menu(treeview
, event
)
548 def on_treeview_episodes_button_released(self
, treeview
, event
):
549 if event
.window
!= treeview
.get_bin_window():
553 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
554 return self
.treeview_available_handle_gestures(treeview
, event
)
556 return self
.treeview_available_show_context_menu(treeview
, event
)
558 def on_treeview_downloads_button_released(self
, treeview
, event
):
559 if event
.window
!= treeview
.get_bin_window():
562 return self
.treeview_downloads_show_context_menu(treeview
, event
)
564 def on_entry_search_podcasts_changed(self
, editable
):
565 if self
.hbox_search_podcasts
.get_property('visible'):
566 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
568 def on_entry_search_podcasts_key_press(self
, editable
, event
):
569 if event
.keyval
== gtk
.keysyms
.Escape
:
570 self
.hide_podcast_search()
573 def hide_podcast_search(self
, *args
):
574 self
.hbox_search_podcasts
.hide()
575 self
.entry_search_podcasts
.set_text('')
576 self
.podcast_list_model
.set_search_term(None)
577 self
.treeChannels
.grab_focus()
579 def show_podcast_search(self
, input_char
):
580 self
.hbox_search_podcasts
.show()
581 self
.entry_search_podcasts
.insert_text(input_char
, -1)
582 self
.entry_search_podcasts
.grab_focus()
583 self
.entry_search_podcasts
.set_position(-1)
585 def init_podcast_list_treeview(self
):
586 # Set up podcast channel tree view widget
587 if gpodder
.ui
.fremantle
:
588 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
589 self
.item_view_podcasts_downloaded
.set_active(True)
590 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
591 self
.item_view_podcasts_unplayed
.set_active(True)
593 self
.item_view_podcasts_all
.set_active(True)
594 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
596 iconcolumn
= gtk
.TreeViewColumn('')
597 iconcell
= gtk
.CellRendererPixbuf()
598 iconcolumn
.pack_start(iconcell
, False)
599 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
600 self
.treeChannels
.append_column(iconcolumn
)
602 namecolumn
= gtk
.TreeViewColumn('')
603 namecell
= gtk
.CellRendererText()
604 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
605 namecolumn
.pack_start(namecell
, True)
606 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
608 iconcell
= gtk
.CellRendererPixbuf()
609 iconcell
.set_property('xalign', 1.0)
610 namecolumn
.pack_start(iconcell
, False)
611 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
612 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
613 self
.treeChannels
.append_column(namecolumn
)
615 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
617 # When no podcast is selected, clear the episode list model
618 selection
= self
.treeChannels
.get_selection()
619 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
621 # Set up type-ahead find for the podcast list
622 def on_key_press(treeview
, event
):
623 if event
.keyval
== gtk
.keysyms
.Escape
:
624 self
.hide_podcast_search()
625 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
626 self
.hide_podcast_search()
627 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
628 # Don't handle type-ahead when control is pressed (so shortcuts
629 # with the Ctrl key still work, e.g. Ctrl+A, ...)
632 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
633 if unicode_char_id
== 0:
635 input_char
= unichr(unicode_char_id
)
636 self
.show_podcast_search(input_char
)
638 self
.treeChannels
.connect('key-press-event', on_key_press
)
640 # Enable separators to the podcast list to separate special podcasts
641 # from others (this is used for the "all episodes" view)
642 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
644 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
646 def on_entry_search_episodes_changed(self
, editable
):
647 if self
.hbox_search_episodes
.get_property('visible'):
648 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
650 def on_entry_search_episodes_key_press(self
, editable
, event
):
651 if event
.keyval
== gtk
.keysyms
.Escape
:
652 self
.hide_episode_search()
655 def hide_episode_search(self
, *args
):
656 self
.hbox_search_episodes
.hide()
657 self
.entry_search_episodes
.set_text('')
658 self
.episode_list_model
.set_search_term(None)
659 self
.treeAvailable
.grab_focus()
661 def show_episode_search(self
, input_char
):
662 self
.hbox_search_episodes
.show()
663 self
.entry_search_episodes
.insert_text(input_char
, -1)
664 self
.entry_search_episodes
.grab_focus()
665 self
.entry_search_episodes
.set_position(-1)
667 def init_episode_list_treeview(self
):
668 self
.episode_list_model
= EpisodeListModel()
670 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
671 self
.item_view_episodes_undeleted
.set_active(True)
672 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
673 self
.item_view_episodes_downloaded
.set_active(True)
674 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
675 self
.item_view_episodes_unplayed
.set_active(True)
677 self
.item_view_episodes_all
.set_active(True)
679 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
681 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
683 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
685 iconcell
= gtk
.CellRendererPixbuf()
687 iconcell
.set_fixed_size(50, 50)
688 status_column_label
= ''
690 status_column_label
= _('Status')
691 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
693 namecell
= gtk
.CellRendererText()
694 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
695 namecolumn
= gtk
.TreeViewColumn(_('Episode'), namecell
, markup
=EpisodeListModel
.C_DESCRIPTION
)
696 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
697 namecolumn
.set_resizable(True)
698 namecolumn
.set_expand(True)
700 sizecell
= gtk
.CellRendererText()
701 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
703 releasecell
= gtk
.CellRendererText()
704 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
706 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
707 itemcolumn
.set_reorderable(True)
708 self
.treeAvailable
.append_column(itemcolumn
)
711 sizecolumn
.set_visible(False)
712 releasecolumn
.set_visible(False)
714 # Set up type-ahead find for the episode list
715 def on_key_press(treeview
, event
):
716 if event
.keyval
== gtk
.keysyms
.Escape
:
717 self
.hide_episode_search()
718 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
719 self
.hide_episode_search()
720 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
721 # Don't handle type-ahead when control is pressed (so shortcuts
722 # with the Ctrl key still work, e.g. Ctrl+A, ...)
725 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
726 if unicode_char_id
== 0:
728 input_char
= unichr(unicode_char_id
)
729 self
.show_episode_search(input_char
)
731 self
.treeAvailable
.connect('key-press-event', on_key_press
)
733 if gpodder
.ui
.desktop
:
734 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
735 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
736 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
737 if self
.config
.on_drag_mark_played
:
738 for episode
in self
.get_selected_episodes():
739 episode
.mark(is_played
=True)
740 self
.on_selected_episodes_status_changed()
741 uris
= ['file://'+e
.local_filename(create
=False) \
742 for e
in self
.get_selected_episodes() \
743 if e
.was_downloaded(and_exists
=True)]
744 uris
.append('') # for the trailing '\r\n'
745 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
746 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
748 selection
= self
.treeAvailable
.get_selection()
749 if gpodder
.ui
.diablo
:
750 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
751 selection
.set_mode(gtk
.SELECTION_SINGLE
)
753 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
754 elif gpodder
.ui
.fremantle
:
755 selection
.set_mode(gtk
.SELECTION_SINGLE
)
757 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
758 # Update the sensitivity of the toolbar buttons on the Desktop
759 selection
.connect('changed', lambda s
: self
.play_or_download())
761 if gpodder
.ui
.diablo
:
762 # Set up the tap-and-hold context menu for podcasts
764 menu
.append(self
.itemUpdateChannel
.create_menu_item())
765 menu
.append(self
.itemEditChannel
.create_menu_item())
766 menu
.append(gtk
.SeparatorMenuItem())
767 menu
.append(self
.itemRemoveChannel
.create_menu_item())
768 menu
.append(gtk
.SeparatorMenuItem())
769 item
= gtk
.ImageMenuItem(_('Close this menu'))
770 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
774 menu
= self
.set_finger_friendly(menu
)
775 self
.treeChannels
.tap_and_hold_setup(menu
)
778 def init_download_list_treeview(self
):
779 # enable multiple selection support
780 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
781 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
783 # columns and renderers for "download progress" tab
784 # First column: [ICON] Episodename
785 column
= gtk
.TreeViewColumn(_('Episode'))
787 cell
= gtk
.CellRendererPixbuf()
789 cell
.set_fixed_size(50, 50)
790 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
791 column
.pack_start(cell
, expand
=False)
792 column
.add_attribute(cell
, 'stock-id', \
793 DownloadStatusModel
.C_ICON_NAME
)
795 cell
= gtk
.CellRendererText()
796 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
797 column
.pack_start(cell
, expand
=True)
798 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
799 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
800 column
.set_expand(True)
801 self
.treeDownloads
.append_column(column
)
803 # Second column: Progress
804 cell
= gtk
.CellRendererProgress()
805 cell
.set_property('yalign', .5)
806 cell
.set_property('ypad', 6)
807 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
808 value
=DownloadStatusModel
.C_PROGRESS
, \
809 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
810 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
811 column
.set_expand(False)
812 self
.treeDownloads
.append_column(column
)
813 column
.set_property('min-width', 150)
814 column
.set_property('max-width', 150)
816 self
.treeDownloads
.set_model(self
.download_status_model
)
817 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
819 def on_treeview_expose_event(self
, treeview
, event
):
820 if event
.window
== treeview
.get_bin_window():
821 model
= treeview
.get_model()
822 if (model
is not None and model
.get_iter_first() is not None):
825 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
826 ctx
= event
.window
.cairo_create()
827 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
828 event
.area
.width
, event
.area
.height
)
831 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
833 if role
== TreeViewHelper
.ROLE_EPISODES
:
834 if self
.currently_updating
:
835 text
= _('Loading episodes') + '...'
836 elif self
.config
.episode_list_view_mode
!= \
837 EpisodeListModel
.VIEW_ALL
:
838 text
= _('No episodes in current view')
840 text
= _('No episodes available')
841 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
842 if self
.config
.episode_list_view_mode
!= \
843 EpisodeListModel
.VIEW_ALL
and \
844 self
.config
.podcast_list_hide_boring
and \
845 len(self
.channels
) > 0:
846 text
= _('No podcasts in this view')
848 text
= _('No subscriptions')
849 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
850 text
= _('No active downloads')
852 raise Exception('on_treeview_expose_event: unknown role')
854 if gpodder
.ui
.fremantle
:
855 from gpodder
.gtkui
.frmntl
import style
856 font_desc
= style
.get_font_desc('LargeSystemFont')
860 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
)
864 def enable_download_list_update(self
):
865 if not self
.download_list_update_enabled
:
866 gobject
.timeout_add(1500, self
.update_downloads_list
)
867 self
.download_list_update_enabled
= True
869 def on_btnCleanUpDownloads_clicked(self
, button
=None):
870 model
= self
.download_status_model
872 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
873 changed_episode_urls
= []
874 for row_reference
, task
in all_tasks
:
875 if task
.status
in (task
.DONE
, task
.CANCELLED
) or \
876 (task
.status
== task
.FAILED
and gpodder
.ui
.fremantle
):
877 model
.remove(model
.get_iter(row_reference
.get_path()))
879 # We don't "see" this task anymore - remove it;
880 # this is needed, so update_episode_list_icons()
881 # below gets the correct list of "seen" tasks
882 self
.download_tasks_seen
.remove(task
)
883 except KeyError, key_error
:
884 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
885 changed_episode_urls
.append(task
.url
)
886 # Tell the task that it has been removed (so it can clean up)
887 task
.removed_from_list()
889 # Tell the podcasts tab to update icons for our removed podcasts
890 self
.update_episode_list_icons(changed_episode_urls
)
892 # Tell the shownotes window that we have removed the episode
893 if self
.episode_shownotes_window
is not None and \
894 self
.episode_shownotes_window
.episode
is not None and \
895 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
896 self
.episode_shownotes_window
._download
_status
_changed
(None)
898 # Update the tab title and downloads list
899 self
.update_downloads_list(from_cleanup
=True)
901 def on_tool_downloads_toggled(self
, toolbutton
):
902 if toolbutton
.get_active():
903 self
.wNotebook
.set_current_page(1)
905 self
.wNotebook
.set_current_page(0)
907 def add_download_task_monitor(self
, monitor
):
908 self
.download_task_monitors
.add(monitor
)
909 model
= self
.download_status_model
913 task
= row
[self
.download_status_model
.C_TASK
]
914 monitor
.task_updated(task
)
916 def remove_download_task_monitor(self
, monitor
):
917 self
.download_task_monitors
.remove(monitor
)
919 def update_downloads_list(self
, from_cleanup
=False):
921 model
= self
.download_status_model
923 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
924 total_speed
, total_size
, done_size
= 0, 0, 0
926 # Keep a list of all download tasks that we've seen
927 download_tasks_seen
= set()
929 # Remember the DownloadTask object for the episode that
930 # has been opened in the episode shownotes dialog (if any)
931 if self
.episode_shownotes_window
is not None:
932 shownotes_episode
= self
.episode_shownotes_window
.episode
933 shownotes_task
= None
935 shownotes_episode
= None
936 shownotes_task
= None
938 # Do not go through the list of the model is not (yet) available
942 failed_downloads
= []
944 self
.download_status_model
.request_update(row
.iter)
946 task
= row
[self
.download_status_model
.C_TASK
]
947 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
949 # Let the download task monitors know of changes
950 for monitor
in self
.download_task_monitors
:
951 monitor
.task_updated(task
)
954 done_size
+= size
*progress
956 if shownotes_episode
is not None and \
957 shownotes_episode
.url
== task
.episode
.url
:
958 shownotes_task
= task
960 download_tasks_seen
.add(task
)
962 if status
== download
.DownloadTask
.DOWNLOADING
:
965 elif status
== download
.DownloadTask
.FAILED
:
966 failed_downloads
.append(task
)
968 elif status
== download
.DownloadTask
.DONE
:
970 elif status
== download
.DownloadTask
.QUEUED
:
972 elif status
== download
.DownloadTask
.PAUSED
:
977 # Remember which tasks we have seen after this run
978 self
.download_tasks_seen
= download_tasks_seen
980 if gpodder
.ui
.desktop
:
981 text
= [_('Downloads')]
982 if downloading
+ failed
+ finished
+ queued
> 0:
985 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
987 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
989 s
.append(N_('%d done', '%d done', finished
) % finished
)
991 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
992 text
.append(' (' + ', '.join(s
)+')')
993 self
.labelDownloads
.set_text(''.join(text
))
994 elif gpodder
.ui
.diablo
:
995 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
997 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
999 self
.tool_downloads
.set_label(_('Downloads'))
1000 elif gpodder
.ui
.fremantle
:
1001 if downloading
+ queued
> 0:
1002 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1004 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1006 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1008 self
.button_downloads
.set_value(_('Idle'))
1010 title
= [self
.default_title
]
1012 # We have to update all episodes/channels for which the status has
1013 # changed. Accessing task.status_changed has the side effect of
1014 # re-setting the changed flag, so we need to get the "changed" list
1015 # of tuples first and split it into two lists afterwards
1016 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1017 self
.download_tasks_seen
if task
.status_changed
]
1018 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1019 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1021 count
= downloading
+ queued
1023 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1026 percentage
= 100.0*done_size
/total_size
1029 total_speed
= util
.format_filesize(total_speed
)
1030 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1031 if self
.tray_icon
is not None:
1032 # Update the tray icon status and progress bar
1033 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1034 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1035 elif self
.last_download_count
> 0 and not from_cleanup
:
1036 if self
.tray_icon
is not None:
1037 # Update the tray icon status
1038 self
.tray_icon
.set_status()
1039 self
.tray_icon
.downloads_finished(self
.download_tasks_seen
)
1040 if gpodder
.ui
.diablo
:
1041 hildon
.hildon_banner_show_information(self
.gPodder
, None, 'gPodder: %s' % _('All downloads finished'))
1042 log('All downloads have finished.', sender
=self
)
1043 if self
.config
.cmd_all_downloads_complete
:
1044 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1046 if gpodder
.ui
.fremantle
and failed
:
1047 message
= '\n'.join(['%s: %s' % (str(task
), \
1048 task
.error_message
) for task
in failed_downloads
])
1049 self
.show_message(message
, _('Downloads failed'), important
=True)
1051 # Automatically remove finished downloads from the list
1052 if self
.config
.auto_cleanup_downloads
:
1053 self
.on_btnCleanUpDownloads_clicked()
1054 self
.last_download_count
= count
1056 if not gpodder
.ui
.fremantle
:
1057 self
.gPodder
.set_title(' - '.join(title
))
1059 self
.update_episode_list_icons(episode_urls
)
1060 if self
.episode_shownotes_window
is not None:
1061 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1062 shownotes_task
!= self
.episode_shownotes_window
.task
:
1063 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1064 self
.episode_shownotes_window
._download
_status
_progress
()
1065 self
.play_or_download()
1067 self
.update_podcast_list_model(channel_urls
)
1069 if not self
.download_queue_manager
.are_queued_or_active_tasks():
1070 self
.download_list_update_enabled
= False
1072 return self
.download_list_update_enabled
1073 except Exception, e
:
1074 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1075 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1076 # We return False here, so the update loop won't be called again,
1077 # that's why we require the restart of gPodder in the message.
1080 def on_config_changed(self
, name
, old_value
, new_value
):
1081 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1082 self
.toolbar
.set_property('visible', new_value
)
1083 elif name
== 'episode_list_descriptions':
1084 self
.update_episode_list_model()
1085 elif name
== 'rotation_mode':
1086 self
._fremantle
_rotation
.set_mode(new_value
)
1087 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1088 self
.restart_auto_update_timer()
1089 elif name
== 'podcast_list_view_all':
1090 # Force a update of the podcast list model
1091 self
.channel_list_changed
= True
1092 self
.update_podcast_list_model()
1093 elif name
== 'auto_cleanup_downloads' and new_value
:
1094 # Always cleanup when this option is enabled
1095 self
.on_btnCleanUpDownloads_clicked()
1097 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1098 # With get_bin_window, we get the window that contains the rows without
1099 # the header. The Y coordinate of this window will be the height of the
1100 # treeview header. This is the amount we have to subtract from the
1101 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1102 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1105 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1107 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1108 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1111 if path
is not None:
1112 model
= treeview
.get_model()
1113 iter = model
.get_iter(path
)
1114 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1116 if role
== TreeViewHelper
.ROLE_EPISODES
:
1117 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1118 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1119 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1121 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1122 if last_tooltip
is not None and last_tooltip
!= id:
1123 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1125 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1127 if role
== TreeViewHelper
.ROLE_EPISODES
:
1128 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1129 tooltip
.set_text(description
)
1130 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1131 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1134 channel
.request_save_dir_size()
1135 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1136 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1138 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1139 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1140 table
= gtk
.Table(rows
=3, columns
=3)
1141 table
.set_row_spacings(5)
1142 table
.set_col_spacings(5)
1143 table
.set_border_width(5)
1145 heading
= gtk
.Label()
1146 heading
.set_alignment(0, 1)
1147 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1148 table
.attach(heading
, 0, 1, 0, 1)
1149 size_info
= gtk
.Label()
1150 size_info
.set_alignment(1, 1)
1151 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1152 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1153 table
.attach(size_info
, 2, 3, 0, 1)
1155 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1157 if len(channel
.description
) < 500:
1158 description
= channel
.description
1160 pos
= channel
.description
.find('\n\n')
1161 if pos
== -1 or pos
> 500:
1162 description
= channel
.description
[:498]+'[...]'
1164 description
= channel
.description
[:pos
]
1166 description
= gtk
.Label(description
)
1168 description
.set_markup(error_str
)
1169 description
.set_alignment(0, 0)
1170 description
.set_line_wrap(True)
1171 table
.attach(description
, 0, 3, 2, 3)
1174 tooltip
.set_custom(table
)
1178 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1181 def treeview_allow_tooltips(self
, treeview
, allow
):
1182 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1184 def update_m3u_playlist_clicked(self
, widget
):
1185 if self
.active_channel
is not None:
1186 self
.active_channel
.update_m3u_playlist()
1187 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1189 def treeview_handle_context_menu_click(self
, treeview
, event
):
1190 x
, y
= int(event
.x
), int(event
.y
)
1191 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1193 selection
= treeview
.get_selection()
1194 model
, paths
= selection
.get_selected_rows()
1196 if path
is None or (path
not in paths
and \
1197 event
.button
== self
.context_menu_mouse_button
):
1198 # We have right-clicked, but not into the selection,
1199 # assume we don't want to operate on the selection
1202 if path
is not None and not paths
and \
1203 event
.button
== self
.context_menu_mouse_button
:
1204 # No selection or clicked outside selection;
1205 # select the single item where we clicked
1206 treeview
.grab_focus()
1207 treeview
.set_cursor(path
, column
, 0)
1211 # Unselect any remaining items (clicked elsewhere)
1212 if hasattr(treeview
, 'is_rubber_banding_active'):
1213 if not treeview
.is_rubber_banding_active():
1214 selection
.unselect_all()
1216 selection
.unselect_all()
1220 def downloads_list_get_selection(self
, model
=None, paths
=None):
1221 if model
is None and paths
is None:
1222 selection
= self
.treeDownloads
.get_selection()
1223 model
, paths
= selection
.get_selected_rows()
1225 can_queue
, can_cancel
, can_pause
, can_remove
= (True,)*4
1226 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1227 model
.get_value(model
.get_iter(path
), \
1228 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1230 for row_reference
, task
in selected_tasks
:
1231 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1232 download
.DownloadTask
.FAILED
, \
1233 download
.DownloadTask
.CANCELLED
):
1235 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1236 download
.DownloadTask
.QUEUED
, \
1237 download
.DownloadTask
.DOWNLOADING
):
1239 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1240 download
.DownloadTask
.DOWNLOADING
):
1242 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1243 download
.DownloadTask
.FAILED
, \
1244 download
.DownloadTask
.DONE
):
1247 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
1249 def _for_each_task_set_status(self
, tasks
, status
):
1250 episode_urls
= set()
1251 model
= self
.treeDownloads
.get_model()
1252 for row_reference
, task
in tasks
:
1253 if status
== download
.DownloadTask
.QUEUED
:
1254 # Only queue task when its paused/failed/cancelled
1255 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
):
1256 self
.download_queue_manager
.add_task(task
)
1257 self
.enable_download_list_update()
1258 elif status
== download
.DownloadTask
.CANCELLED
:
1259 # Cancelling a download allowed when downloading/queued
1260 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1261 task
.status
= status
1262 # Cancelling paused downloads requires a call to .run()
1263 elif task
.status
== task
.PAUSED
:
1264 task
.status
= status
1265 # Call run, so the partial file gets deleted
1267 elif status
== download
.DownloadTask
.PAUSED
:
1268 # Pausing a download only when queued/downloading
1269 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1270 task
.status
= status
1271 elif status
is None:
1272 # Remove the selected task - cancel downloading/queued tasks
1273 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1274 task
.status
= task
.CANCELLED
1275 model
.remove(model
.get_iter(row_reference
.get_path()))
1276 # Remember the URL, so we can tell the UI to update
1278 # We don't "see" this task anymore - remove it;
1279 # this is needed, so update_episode_list_icons()
1280 # below gets the correct list of "seen" tasks
1281 self
.download_tasks_seen
.remove(task
)
1282 except KeyError, key_error
:
1283 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1284 episode_urls
.add(task
.url
)
1285 # Tell the task that it has been removed (so it can clean up)
1286 task
.removed_from_list()
1288 # We can (hopefully) simply set the task status here
1289 task
.status
= status
1290 # Tell the podcasts tab to update icons for our removed podcasts
1291 self
.update_episode_list_icons(episode_urls
)
1292 # Update the tab title and downloads list
1293 self
.update_downloads_list()
1295 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1296 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1298 if not hasattr(treeview
, 'is_rubber_banding_active'):
1301 return not treeview
.is_rubber_banding_active()
1303 if event
.button
== self
.context_menu_mouse_button
:
1304 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
= \
1305 self
.downloads_list_get_selection(model
, paths
)
1307 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
):
1308 # This creates a menu item for selection-wide actions
1309 item
= gtk
.ImageMenuItem(label
)
1310 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1311 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
))
1312 item
.set_sensitive(sensitive
)
1313 return self
.set_finger_friendly(item
)
1317 item
= gtk
.ImageMenuItem(_('Episode details'))
1318 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1319 if len(selected_tasks
) == 1:
1320 row_reference
, task
= selected_tasks
[0]
1321 episode
= task
.episode
1322 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1324 item
.set_sensitive(False)
1325 menu
.append(self
.set_finger_friendly(item
))
1326 menu
.append(gtk
.SeparatorMenuItem())
1327 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
))
1328 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1329 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1330 menu
.append(gtk
.SeparatorMenuItem())
1331 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1333 if gpodder
.ui
.maemo
:
1334 # Because we open the popup on left-click for Maemo,
1335 # we also include a non-action to close the menu
1336 menu
.append(gtk
.SeparatorMenuItem())
1337 item
= gtk
.ImageMenuItem(_('Close this menu'))
1338 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1340 menu
.append(self
.set_finger_friendly(item
))
1343 menu
.popup(None, None, None, event
.button
, event
.time
)
1346 def treeview_channels_show_context_menu(self
, treeview
, event
):
1347 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1351 # Check for valid channel id, if there's no id then
1352 # assume that it is a proxy channel or equivalent
1353 # and cannot be operated with right click
1354 if self
.active_channel
.id is None:
1357 if event
.button
== 3:
1362 item
= gtk
.ImageMenuItem( _('Open download folder'))
1363 item
.set_image( gtk
.image_new_from_icon_name(ICON('folder-open'), gtk
.ICON_SIZE_MENU
))
1364 item
.connect('activate', lambda x
: util
.gui_open(self
.active_channel
.save_dir
))
1367 item
= gtk
.ImageMenuItem( _('Update Feed'))
1368 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1369 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1370 item
.set_sensitive( not self
.updating_feed_cache
)
1373 item
= gtk
.ImageMenuItem(_('Update M3U playlist'))
1374 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1375 item
.connect('activate', self
.update_m3u_playlist_clicked
)
1378 if self
.active_channel
.link
:
1379 item
= gtk
.ImageMenuItem(_('Visit website'))
1380 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1381 item
.connect('activate', lambda w
: util
.open_website(self
.active_channel
.link
))
1384 if self
.active_channel
.channel_is_locked
:
1385 item
= gtk
.ImageMenuItem(_('Allow deletion of all episodes'))
1386 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1387 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1388 menu
.append(self
.set_finger_friendly(item
))
1390 item
= gtk
.ImageMenuItem(_('Prohibit deletion of all episodes'))
1391 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1392 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1393 menu
.append(self
.set_finger_friendly(item
))
1396 menu
.append( gtk
.SeparatorMenuItem())
1398 item
= gtk
.ImageMenuItem(gtk
.STOCK_EDIT
)
1399 item
.connect( 'activate', self
.on_itemEditChannel_activate
)
1402 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1403 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1407 # Disable tooltips while we are showing the menu, so
1408 # the tooltip will not appear over the menu
1409 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1410 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1411 menu
.popup( None, None, None, event
.button
, event
.time
)
1415 def on_itemClose_activate(self
, widget
):
1416 if self
.tray_icon
is not None:
1417 self
.iconify_main_window()
1419 self
.on_gPodder_delete_event(widget
)
1421 def cover_file_removed(self
, channel_url
):
1423 The Cover Downloader calls this when a previously-
1424 available cover has been removed from the disk. We
1425 have to update our model to reflect this change.
1427 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1429 def cover_download_finished(self
, channel_url
, pixbuf
):
1431 The Cover Downloader calls this when it has finished
1432 downloading (or registering, if already downloaded)
1433 a new channel cover, which is ready for displaying.
1435 self
.podcast_list_model
.add_cover_by_url(channel_url
, pixbuf
)
1437 def save_episode_as_file(self
, episode
):
1438 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1439 if episode
.was_downloaded(and_exists
=True):
1440 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1441 copy_from
= episode
.local_filename(create
=False)
1442 assert copy_from
is not None
1443 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1444 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1445 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1447 def copy_episodes_bluetooth(self
, episodes
):
1448 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1450 def convert_and_send_thread(episode
):
1451 for episode
in episodes
:
1452 filename
= episode
.local_filename(create
=False)
1453 assert filename
is not None
1454 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1455 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1456 (base
, ext
) = os
.path
.splitext(filename
)
1457 if not destfile
.endswith(ext
):
1461 shutil
.copyfile(filename
, destfile
)
1462 util
.bluetooth_send_file(destfile
)
1464 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1465 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1467 util
.delete_file(destfile
)
1469 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1471 def get_device_name(self
):
1472 if self
.config
.device_type
== 'ipod':
1474 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1475 return _('MP3 player')
1477 return '(unknown device)'
1479 def _treeview_button_released(self
, treeview
, event
):
1480 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1481 dy
= int(abs(event
.y
-ypos
))
1482 dx
= int(event
.x
-xpos
)
1484 selection
= treeview
.get_selection()
1485 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1486 if path
is None or dy
> 30:
1487 return (False, dx
, dy
)
1489 path
, column
, x
, y
= path
1490 selection
.select_path(path
)
1491 treeview
.set_cursor(path
)
1492 treeview
.grab_focus()
1494 return (True, dx
, dy
)
1496 def treeview_channels_handle_gestures(self
, treeview
, event
):
1497 if self
.currently_updating
:
1500 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1503 if self
.config
.maemo_enable_gestures
:
1505 self
.on_itemUpdateChannel_activate()
1507 self
.on_itemEditChannel_activate(treeview
)
1511 def treeview_available_handle_gestures(self
, treeview
, event
):
1512 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1515 if self
.config
.maemo_enable_gestures
:
1517 self
.on_playback_selected_episodes(None)
1520 self
.on_shownotes_selected_episodes(None)
1523 # Pass the event to the context menu handler for treeAvailable
1524 self
.treeview_available_show_context_menu(treeview
, event
)
1528 def treeview_available_show_context_menu(self
, treeview
, event
):
1529 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1531 if not hasattr(treeview
, 'is_rubber_banding_active'):
1534 return not treeview
.is_rubber_banding_active()
1536 if event
.button
== self
.context_menu_mouse_button
:
1537 episodes
= self
.get_selected_episodes()
1538 any_locked
= any(e
.is_locked
for e
in episodes
)
1539 any_played
= any(e
.is_played
for e
in episodes
)
1540 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1544 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1546 if open_instead_of_play
:
1547 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1549 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1551 item
.set_sensitive(can_play
)
1552 item
.connect('activate', self
.on_playback_selected_episodes
)
1553 menu
.append(self
.set_finger_friendly(item
))
1556 item
= gtk
.ImageMenuItem(_('Download'))
1557 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1558 item
.set_sensitive(can_download
)
1559 item
.connect('activate', self
.on_download_selected_episodes
)
1560 menu
.append(self
.set_finger_friendly(item
))
1562 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1563 item
.connect('activate', self
.on_item_cancel_download_activate
)
1564 menu
.append(self
.set_finger_friendly(item
))
1566 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1567 item
.set_sensitive(can_delete
)
1568 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1569 menu
.append(self
.set_finger_friendly(item
))
1572 item
= gtk
.ImageMenuItem(_('Do not download'))
1573 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1574 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1575 menu
.append(self
.set_finger_friendly(item
))
1577 item
= gtk
.ImageMenuItem(_('Mark as new'))
1578 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_ABOUT
, gtk
.ICON_SIZE_MENU
))
1579 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
1580 menu
.append(self
.set_finger_friendly(item
))
1584 # Ok, this probably makes sense to only display for downloaded files
1585 if can_play
and not can_download
:
1586 menu
.append( gtk
.SeparatorMenuItem())
1587 item
= gtk
.ImageMenuItem(_('Save to disk'))
1588 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
1589 item
.connect('activate', lambda w
: [self
.save_episode_as_file(e
) for e
in episodes
])
1590 menu
.append(self
.set_finger_friendly(item
))
1591 if self
.bluetooth_available
:
1592 item
= gtk
.ImageMenuItem(_('Send via bluetooth'))
1593 item
.set_image(gtk
.image_new_from_icon_name(ICON('bluetooth'), gtk
.ICON_SIZE_MENU
))
1594 item
.connect('activate', lambda w
: self
.copy_episodes_bluetooth(episodes
))
1595 menu
.append(self
.set_finger_friendly(item
))
1597 item
= gtk
.ImageMenuItem(_('Transfer to %s') % self
.get_device_name())
1598 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
1599 item
.connect('activate', lambda w
: self
.on_sync_to_ipod_activate(w
, episodes
))
1600 menu
.append(self
.set_finger_friendly(item
))
1603 menu
.append( gtk
.SeparatorMenuItem())
1605 item
= gtk
.ImageMenuItem(_('Mark as unplayed'))
1606 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
))
1607 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, False))
1608 menu
.append(self
.set_finger_friendly(item
))
1610 item
= gtk
.ImageMenuItem(_('Mark as played'))
1611 item
.set_image( gtk
.image_new_from_stock( gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_MENU
))
1612 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, True))
1613 menu
.append(self
.set_finger_friendly(item
))
1616 item
= gtk
.ImageMenuItem(_('Allow deletion'))
1617 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1618 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, False))
1619 menu
.append(self
.set_finger_friendly(item
))
1621 item
= gtk
.ImageMenuItem(_('Prohibit deletion'))
1622 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1623 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, True))
1624 menu
.append(self
.set_finger_friendly(item
))
1626 menu
.append(gtk
.SeparatorMenuItem())
1627 # Single item, add episode information menu item
1628 item
= gtk
.ImageMenuItem(_('Episode details'))
1629 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1630 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
1631 menu
.append(self
.set_finger_friendly(item
))
1633 # If we have it, also add episode website link
1634 if episodes
[0].link
and episodes
[0].link
!= episodes
[0].url
:
1635 item
= gtk
.ImageMenuItem(_('Visit website'))
1636 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1637 item
.connect('activate', lambda w
: util
.open_website(episodes
[0].link
))
1638 menu
.append(self
.set_finger_friendly(item
))
1640 if gpodder
.ui
.maemo
:
1641 # Because we open the popup on left-click for Maemo,
1642 # we also include a non-action to close the menu
1643 menu
.append(gtk
.SeparatorMenuItem())
1644 item
= gtk
.ImageMenuItem(_('Close this menu'))
1645 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1646 menu
.append(self
.set_finger_friendly(item
))
1649 # Disable tooltips while we are showing the menu, so
1650 # the tooltip will not appear over the menu
1651 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
1652 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
1653 menu
.popup( None, None, None, event
.button
, event
.time
)
1657 def set_title(self
, new_title
):
1658 if not gpodder
.ui
.fremantle
:
1659 self
.default_title
= new_title
1660 self
.gPodder
.set_title(new_title
)
1662 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
1664 Updates the status icons in the episode list.
1666 If urls is given, it should be a list of URLs
1667 of episodes that should be updated.
1669 If urls is None, set ONE OF selected, all to
1670 True (the former updates just the selected
1671 episodes and the latter updates all episodes).
1673 if urls
is not None:
1674 # We have a list of URLs to walk through
1675 self
.episode_list_model
.update_by_urls(urls
, \
1676 self
.episode_is_downloading
, \
1677 self
.config
.episode_list_descriptions
and \
1679 elif selected
and not all
:
1680 # We should update all selected episodes
1681 selection
= self
.treeAvailable
.get_selection()
1682 model
, paths
= selection
.get_selected_rows()
1683 for path
in reversed(paths
):
1684 iter = model
.get_iter(path
)
1685 self
.episode_list_model
.update_by_filter_iter(iter, \
1686 self
.episode_is_downloading
, \
1687 self
.config
.episode_list_descriptions
and \
1689 elif all
and not selected
:
1690 # We update all (even the filter-hidden) episodes
1691 self
.episode_list_model
.update_all(\
1692 self
.episode_is_downloading
, \
1693 self
.config
.episode_list_descriptions
and \
1696 # Wrong/invalid call - have to specify at least one parameter
1697 raise ValueError('Invalid call to update_episode_list_icons')
1699 def episode_list_status_changed(self
, episodes
):
1700 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
1701 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
1704 def clean_up_downloads(self
, delete_partial
=False):
1705 # Clean up temporary files left behind by old gPodder versions
1706 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
1709 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
1711 for tempfile
in temporary_files
:
1712 util
.delete_file(tempfile
)
1714 # Clean up empty download folders and abandoned download folders
1715 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
1716 for ddir
in download_dirs
:
1717 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1718 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
1719 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
1720 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
1721 shutil
.rmtree(ddir
, ignore_errors
=True)
1723 def streaming_possible(self
):
1724 if gpodder
.ui
.desktop
:
1725 # User has to have a media player set on the Desktop, or else we
1726 # would probably open the browser when giving a URL to xdg-open..
1727 return (self
.config
.player
and self
.config
.player
!= 'default')
1728 elif gpodder
.ui
.maemo
:
1729 # On Maemo, the default is to use the Nokia Media Player, which is
1730 # already able to deal with HTTP URLs the right way, so we
1731 # unconditionally enable streaming always on Maemo
1736 def playback_episodes_for_real(self
, episodes
):
1737 groups
= collections
.defaultdict(list)
1738 for episode
in episodes
:
1739 file_type
= episode
.file_type()
1740 if file_type
== 'video' and self
.config
.videoplayer
and \
1741 self
.config
.videoplayer
!= 'default':
1742 player
= self
.config
.videoplayer
1743 if gpodder
.ui
.diablo
:
1744 # Use the wrapper script if it's installed to crop 3GP YouTube
1745 # videos to fit the screen (looks much nicer than w/ black border)
1746 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
1747 player
= 'gpodder-mplayer'
1748 elif file_type
== 'audio' and self
.config
.player
and \
1749 self
.config
.player
!= 'default':
1750 player
= self
.config
.player
1754 if file_type
not in ('audio', 'video') or \
1755 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
1756 (file_type
== 'video' and not self
.config
.video_played_dbus
):
1757 # Mark episode as played in the database
1758 episode
.mark(is_played
=True)
1760 filename
= episode
.local_filename(create
=False)
1761 if filename
is None or not os
.path
.exists(filename
):
1762 filename
= episode
.url
1763 groups
[player
].append(filename
)
1765 # Open episodes with system default player
1766 if 'default' in groups
:
1767 for filename
in groups
['default']:
1768 log('Opening with system default: %s', filename
, sender
=self
)
1769 util
.gui_open(filename
)
1770 del groups
['default']
1771 elif gpodder
.ui
.maemo
:
1772 # When on Maemo and not opening with default, show a notification
1773 # (no startup notification for Panucci / MPlayer yet...)
1774 if len(episodes
) == 1:
1775 text
= _('Opening %s') % episodes
[0].title
1777 count
= len(episodes
)
1778 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
1780 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
1782 def destroy_banner_later(banner
):
1785 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
1787 # For each type now, go and create play commands
1788 for group
in groups
:
1789 for command
in util
.format_desktop_command(group
, groups
[group
]):
1790 log('Executing: %s', repr(command
), sender
=self
)
1791 subprocess
.Popen(command
)
1793 def playback_episodes(self
, episodes
):
1794 episodes
= [e
for e
in episodes
if \
1795 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()]
1798 self
.playback_episodes_for_real(episodes
)
1799 except Exception, e
:
1800 log('Error in playback!', sender
=self
, traceback
=True)
1801 if gpodder
.ui
.desktop
:
1802 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
1803 _('Error opening player'), widget
=self
.toolPreferences
)
1805 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
1807 channel_urls
= set()
1808 episode_urls
= set()
1809 for episode
in episodes
:
1810 channel_urls
.add(episode
.channel
.url
)
1811 episode_urls
.add(episode
.url
)
1812 self
.update_episode_list_icons(episode_urls
)
1813 self
.update_podcast_list_model(channel_urls
)
1815 def play_or_download(self
):
1816 if not gpodder
.ui
.fremantle
:
1817 if self
.wNotebook
.get_current_page() > 0:
1818 if gpodder
.ui
.desktop
:
1819 self
.toolCancel
.set_sensitive(True)
1822 if self
.currently_updating
:
1823 return (False, False, False, False, False, False)
1825 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
1826 ( is_played
, is_locked
) = (False,)*2
1828 open_instead_of_play
= False
1830 selection
= self
.treeAvailable
.get_selection()
1831 if selection
.count_selected_rows() > 0:
1832 (model
, paths
) = selection
.get_selected_rows()
1835 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
1837 if episode
.file_type() not in ('audio', 'video'):
1838 open_instead_of_play
= True
1840 if episode
.was_downloaded():
1841 can_play
= episode
.was_downloaded(and_exists
=True)
1843 is_played
= episode
.is_played
1844 is_locked
= episode
.is_locked
1848 if self
.episode_is_downloading(episode
):
1853 can_download
= can_download
and not can_cancel
1854 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
1855 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
1857 if gpodder
.ui
.desktop
:
1858 if open_instead_of_play
:
1859 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
1861 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
1862 self
.toolPlay
.set_sensitive( can_play
)
1863 self
.toolDownload
.set_sensitive( can_download
)
1864 self
.toolTransfer
.set_sensitive( can_transfer
)
1865 self
.toolCancel
.set_sensitive( can_cancel
)
1867 if not gpodder
.ui
.fremantle
:
1868 self
.item_cancel_download
.set_sensitive(can_cancel
)
1869 self
.itemDownloadSelected
.set_sensitive(can_download
)
1870 self
.itemOpenSelected
.set_sensitive(can_play
)
1871 self
.itemPlaySelected
.set_sensitive(can_play
)
1872 self
.itemDeleteSelected
.set_sensitive(can_play
and not can_download
)
1873 self
.item_toggle_played
.set_sensitive(can_play
)
1874 self
.item_toggle_lock
.set_sensitive(can_play
)
1875 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
1876 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
1878 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
1880 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
1881 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
1883 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
1884 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
1886 def episode_new_status_changed(self
, urls
):
1887 self
.update_podcast_list_model()
1888 self
.update_episode_list_icons(urls
)
1890 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
1891 """Update the podcast list treeview model
1893 If urls is given, it should list the URLs of each
1894 podcast that has to be updated in the list.
1896 If selected is True, only update the model contents
1897 for the currently-selected podcast - nothing more.
1899 The caller can optionally specify "select_url",
1900 which is the URL of the podcast that is to be
1901 selected in the list after the update is complete.
1902 This only works if the podcast list has to be
1903 reloaded; i.e. something has been added or removed
1904 since the last update of the podcast list).
1906 selection
= self
.treeChannels
.get_selection()
1907 model
, iter = selection
.get_selected()
1910 # very cheap! only update selected channel
1911 if iter is not None:
1912 self
.podcast_list_model
.update_by_filter_iter(iter)
1913 elif not self
.channel_list_changed
:
1914 # we can keep the model, but have to update some
1916 # still cheaper than reloading the whole list
1917 self
.podcast_list_model
.update_all()
1919 # ok, we got a bunch of urls to update
1920 self
.podcast_list_model
.update_by_urls(urls
)
1922 if model
and iter and select_url
is None:
1923 # Get the URL of the currently-selected podcast
1924 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
1926 # Update the podcast list model with new channels
1927 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
1930 selected_iter
= model
.get_iter_first()
1931 # Find the previously-selected URL in the new
1932 # model if we have an URL (else select first)
1933 if select_url
is not None:
1934 pos
= model
.get_iter_first()
1935 while pos
is not None:
1936 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
1937 if url
== select_url
:
1940 pos
= model
.iter_next(pos
)
1942 if not gpodder
.ui
.fremantle
:
1943 if selected_iter
is not None:
1944 selection
.select_iter(selected_iter
)
1945 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
1947 log('Cannot select podcast in list', traceback
=True, sender
=self
)
1948 self
.channel_list_changed
= False
1950 def episode_is_downloading(self
, episode
):
1951 """Returns True if the given episode is being downloaded at the moment"""
1955 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
1957 def update_episode_list_model(self
):
1958 if self
.channels
and self
.active_channel
is not None:
1959 if gpodder
.ui
.diablo
:
1960 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, None, _('Loading episodes'))
1964 if gpodder
.ui
.fremantle
:
1965 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
1967 self
.currently_updating
= True
1968 self
.episode_list_model
.clear()
1969 def do_update_episode_list_model():
1970 self
.episode_list_model
.add_from_channel(\
1971 self
.active_channel
, \
1972 self
.episode_is_downloading
, \
1973 self
.config
.episode_list_descriptions \
1974 and gpodder
.ui
.desktop
)
1976 def on_episode_list_model_updated():
1977 if banner
is not None:
1979 if gpodder
.ui
.fremantle
:
1980 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
1981 self
.treeAvailable
.columns_autosize()
1982 self
.currently_updating
= False
1983 self
.play_or_download()
1984 util
.idle_add(on_episode_list_model_updated
)
1985 threading
.Thread(target
=do_update_episode_list_model
).start()
1987 self
.episode_list_model
.clear()
1989 def offer_new_episodes(self
, channels
=None):
1990 new_episodes
= self
.get_new_episodes(channels
)
1992 self
.new_episodes_show(new_episodes
)
1996 def add_podcast_list(self
, urls
, auth_tokens
=None):
1997 """Subscribe to a list of podcast given their URLs
1999 If auth_tokens is given, it should be a dictionary
2000 mapping URLs to (username, password) tuples."""
2002 if auth_tokens
is None:
2005 # Sort and split the URL list into five buckets
2006 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2007 for input_url
in urls
:
2008 url
= util
.normalize_feed_url(input_url
)
2010 # Fail this one because the URL is not valid
2011 failed
.append(input_url
)
2012 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2013 # A podcast already exists in the list for this URL
2014 existing
.append(url
)
2016 # This URL has survived the first round - queue for add
2018 if url
!= input_url
and input_url
in auth_tokens
:
2019 auth_tokens
[url
] = auth_tokens
[input_url
]
2024 progress
= ProgressIndicator(_('Adding podcasts'), \
2025 _('Please wait while episode information is downloaded.'), \
2026 parent
=self
.main_window
)
2028 def on_after_update():
2029 progress
.on_finished()
2030 # Report already-existing subscriptions to the user
2032 title
= _('Existing subscriptions skipped')
2033 message
= _('You are already subscribed to these podcasts:') \
2034 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2035 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2037 # Report subscriptions that require authentication
2041 title
= _('Podcast requires authentication')
2042 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2043 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2045 retry_podcasts
[url
] = auth_tokens
2047 # Stop asking the user for more login data
2050 error_messages
[url
] = _('Authentication failed')
2054 # If we have authentication data to retry, do so here
2056 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2058 # Report website redirections
2059 for url
in redirections
:
2060 title
= _('Website redirection detected')
2061 message
= _('The URL %s redirects to %s.') \
2062 + '\n\n' + _('Do you want to visit the website now?')
2063 message
= message
% (url
, redirections
[url
])
2064 if self
.show_confirmation(message
, title
):
2065 util
.open_website(url
)
2069 # Report failed subscriptions to the user
2071 title
= _('Could not add some podcasts')
2072 message
= _('Some podcasts could not be added to your list:') \
2073 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2074 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2075 self
.show_message(message
, title
, important
=True)
2077 # If at least one podcast has been added, save and update all
2078 if self
.channel_list_changed
:
2079 self
.save_channels_opml()
2081 # If only one podcast was added, select it after the update
2082 if len(worked
) == 1:
2087 # Update the list of subscribed podcasts
2088 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2089 self
.update_podcasts_tab()
2091 # Offer to download new episodes
2092 self
.offer_new_episodes(channels
=[c
for c
in self
.channels
if c
.url
in worked
])
2095 # After the initial sorting and splitting, try all queued podcasts
2096 length
= len(queued
)
2097 for index
, url
in enumerate(queued
):
2098 progress
.on_progress(float(index
)/float(length
))
2099 progress
.on_message(url
)
2100 log('QUEUE RUNNER: %s', url
, sender
=self
)
2102 # The URL is valid and does not exist already - subscribe!
2103 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2104 authentication_tokens
=auth_tokens
.get(url
, None), \
2105 max_episodes
=self
.config
.max_episodes_per_feed
, \
2106 download_dir
=self
.config
.download_dir
, \
2107 allow_empty_feeds
=self
.config
.allow_empty_feeds
)
2110 username
, password
= util
.username_password_from_url(url
)
2111 except ValueError, ve
:
2112 username
, password
= (None, None)
2114 if username
is not None and channel
.username
is None and \
2115 password
is not None and channel
.password
is None:
2116 channel
.username
= username
2117 channel
.password
= password
2120 self
._update
_cover
(channel
)
2121 except feedcore
.AuthenticationRequired
:
2122 if url
in auth_tokens
:
2123 # Fail for wrong authentication data
2124 error_messages
[url
] = _('Authentication failed')
2127 # Queue for login dialog later
2130 except feedcore
.WifiLogin
, error
:
2131 redirections
[url
] = error
.data
2133 error_messages
[url
] = _('Redirection detected')
2135 except Exception, e
:
2136 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2137 error_messages
[url
] = str(e
)
2141 assert channel
is not None
2142 worked
.append(channel
.url
)
2143 self
.channels
.append(channel
)
2144 self
.channel_list_changed
= True
2145 util
.idle_add(on_after_update
)
2146 threading
.Thread(target
=thread_proc
).start()
2148 def save_channels_opml(self
):
2149 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2150 return exporter
.write(self
.channels
)
2152 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2154 self
.updating_feed_cache
= False
2156 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2157 self
.channel_list_changed
= True
2158 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2160 # Only search for new episodes in podcasts that have been
2161 # updated, not in other podcasts (for single-feed updates)
2162 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2164 if gpodder
.ui
.fremantle
:
2165 self
.button_subscribe
.set_sensitive(True)
2166 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2167 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
2168 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2169 self
.update_podcasts_tab()
2170 if self
.feed_cache_update_cancelled
:
2174 if self
.config
.auto_download
== 'always':
2175 count
= len(episodes
)
2176 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2177 self
.show_message(title
)
2178 self
.download_episode_list(episodes
)
2179 elif self
.config
.auto_download
== 'queue':
2180 self
.show_message(_('New episodes have been added to the download list.'))
2181 self
.download_episode_list_paused(episodes
)
2183 self
.new_episodes_show(episodes
)
2184 elif not self
.config
.auto_update_feeds
:
2185 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2189 self
.tray_icon
.set_status()
2191 if self
.feed_cache_update_cancelled
:
2192 # The user decided to abort the feed update
2193 self
.show_update_feeds_buttons()
2195 # Nothing new here - but inform the user
2196 self
.pbFeedUpdate
.set_fraction(1.0)
2197 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2198 self
.feed_cache_update_cancelled
= True
2199 self
.btnCancelFeedUpdate
.show()
2200 self
.btnCancelFeedUpdate
.set_sensitive(True)
2201 if gpodder
.ui
.maemo
:
2202 # btnCancelFeedUpdate is a ToolButton on Maemo
2203 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2205 # btnCancelFeedUpdate is a normal gtk.Button
2206 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2208 count
= len(episodes
)
2209 # New episodes are available
2210 self
.pbFeedUpdate
.set_fraction(1.0)
2211 # Are we minimized and should we auto download?
2212 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2213 self
.download_episode_list(episodes
)
2214 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2215 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2216 self
.show_update_feeds_buttons()
2218 self
.show_update_feeds_buttons()
2219 # New episodes are available and we are not minimized
2220 if not self
.config
.do_not_show_new_episodes_dialog
:
2221 self
.new_episodes_show(episodes
, notification
=True)
2223 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2224 self
.pbFeedUpdate
.set_text(message
)
2226 def _update_cover(self
, channel
):
2227 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2228 self
.cover_downloader
.request_cover(channel
)
2230 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2231 total
= len(channels
)
2233 for updated
, channel
in enumerate(channels
):
2234 if not self
.feed_cache_update_cancelled
:
2236 # Update if timeout is not reached or we update a single podcast or skipping is disabled
2237 if channel
.query_automatic_update() or total
== 1 or not self
.config
.feed_update_skipping
:
2238 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
)
2240 log('Skipping update of %s (see feed_update_skipping)', channel
.title
, sender
=self
)
2241 self
._update
_cover
(channel
)
2242 except Exception, e
:
2243 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
)
2244 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2246 if self
.feed_cache_update_cancelled
:
2249 if gpodder
.ui
.fremantle
:
2250 util
.idle_add(self
.button_refresh
.set_title
, \
2251 _('%d/%d updated') % (updated
, total
))
2254 # By the time we get here the update may have already been cancelled
2255 if not self
.feed_cache_update_cancelled
:
2256 def update_progress():
2257 progression
= _('Updated %s (%d/%d)') % (channel
.title
, updated
, total
)
2258 self
.pbFeedUpdate
.set_text(progression
)
2260 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2261 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2262 util
.idle_add(update_progress
)
2264 updated_urls
= [c
.url
for c
in channels
]
2265 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2267 def show_update_feeds_buttons(self
):
2268 # Make sure that the buttons for updating feeds
2269 # appear - this should happen after a feed update
2270 if gpodder
.ui
.maemo
:
2271 self
.btnUpdateSelectedFeed
.show()
2272 self
.toolFeedUpdateProgress
.hide()
2273 self
.btnCancelFeedUpdate
.hide()
2274 self
.btnCancelFeedUpdate
.set_is_important(False)
2275 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2276 self
.toolbarSpacer
.set_expand(True)
2277 self
.toolbarSpacer
.set_draw(False)
2279 self
.hboxUpdateFeeds
.hide()
2280 self
.btnUpdateFeeds
.show()
2281 self
.itemUpdate
.set_sensitive(True)
2282 self
.itemUpdateChannel
.set_sensitive(True)
2284 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2285 if not self
.feed_cache_update_cancelled
:
2286 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2287 self
.feed_cache_update_cancelled
= True
2288 self
.btnCancelFeedUpdate
.set_sensitive(False)
2290 self
.show_update_feeds_buttons()
2292 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2293 if self
.updating_feed_cache
:
2294 if gpodder
.ui
.fremantle
:
2295 self
.feed_cache_update_cancelled
= True
2298 if not force_update
:
2299 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2300 self
.channel_list_changed
= True
2301 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2304 self
.updating_feed_cache
= True
2306 if channels
is None:
2307 channels
= self
.channels
2309 if gpodder
.ui
.fremantle
:
2310 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2311 self
.button_refresh
.set_title(_('Updating...'))
2312 self
.button_subscribe
.set_sensitive(False)
2313 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2314 self
.ICON_GENERAL_CLOSE
, gtk
.ICON_SIZE_BUTTON
))
2315 self
.feed_cache_update_cancelled
= False
2317 self
.itemUpdate
.set_sensitive(False)
2318 self
.itemUpdateChannel
.set_sensitive(False)
2321 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2323 if len(channels
) == 1:
2324 text
= _('Updating "%s"...') % channels
[0].title
2326 count
= len(channels
)
2327 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2328 self
.pbFeedUpdate
.set_text(text
)
2329 self
.pbFeedUpdate
.set_fraction(0)
2331 self
.feed_cache_update_cancelled
= False
2332 self
.btnCancelFeedUpdate
.show()
2333 self
.btnCancelFeedUpdate
.set_sensitive(True)
2334 if gpodder
.ui
.maemo
:
2335 self
.toolbarSpacer
.set_expand(False)
2336 self
.toolbarSpacer
.set_draw(True)
2337 self
.btnUpdateSelectedFeed
.hide()
2338 self
.toolFeedUpdateProgress
.show_all()
2340 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2341 self
.hboxUpdateFeeds
.show_all()
2342 self
.btnUpdateFeeds
.hide()
2344 args
= (channels
, select_url_afterwards
)
2345 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2347 def on_gPodder_delete_event(self
, widget
, *args
):
2348 """Called when the GUI wants to close the window
2349 Displays a confirmation dialog (and closes/hides gPodder)
2352 downloading
= self
.download_status_model
.are_downloads_in_progress()
2354 # Only iconify if we are using the window's "X" button,
2355 # but not when we are using "Quit" in the menu or toolbar
2356 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
2357 self
.iconify_main_window()
2358 elif self
.config
.on_quit_ask
or downloading
:
2359 if gpodder
.ui
.fremantle
:
2360 self
.close_gpodder()
2361 elif gpodder
.ui
.diablo
:
2362 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2364 self
.close_gpodder()
2367 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2368 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2369 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2371 title
= _('Quit gPodder')
2373 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2375 message
= _('Do you really want to quit gPodder now?')
2377 dialog
.set_title(title
)
2378 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2380 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
2381 dialog
.vbox
.pack_start(cb_ask
)
2384 quit_button
.grab_focus()
2385 result
= dialog
.run()
2388 if result
== gtk
.RESPONSE_CLOSE
:
2389 if not downloading
and cb_ask
.get_active() == True:
2390 self
.config
.on_quit_ask
= False
2391 self
.close_gpodder()
2393 self
.close_gpodder()
2397 def close_gpodder(self
):
2398 """ clean everything and exit properly
2401 if self
.save_channels_opml():
2402 if self
.config
.my_gpodder_autoupload
:
2403 log('Uploading to my.gpodder.org on close', sender
=self
)
2404 util
.idle_add(self
.on_upload_to_mygpo
, None)
2406 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
2410 if self
.tray_icon
is not None:
2411 self
.tray_icon
.set_visible(False)
2413 # Notify all tasks to to carry out any clean-up actions
2414 self
.download_status_model
.tell_all_tasks_to_quit()
2416 while gtk
.events_pending():
2417 gtk
.main_iteration(False)
2424 def get_old_episodes(self
):
2426 for channel
in self
.channels
:
2427 for episode
in channel
.get_downloaded_episodes():
2428 if episode
.age_in_days() > self
.config
.episode_old_age
and \
2429 not episode
.is_locked
and episode
.is_played
:
2430 episodes
.append(episode
)
2433 def delete_episode_list(self
, episodes
, confirm
=True):
2437 count
= len(episodes
)
2440 episode
= episodes
[0]
2441 if episode
.is_locked
:
2442 title
= _('%s is locked') % saxutils
.escape(episode
.title
)
2443 message
= _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2444 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2447 title
= _('Remove %s?') % saxutils
.escape(episode
.title
)
2448 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.")
2450 title
= N_('Remove %d episode?', 'Remove %d episodes?', count
) % count
2451 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.')
2453 locked_count
= sum(int(e
.is_locked
) for e
in episodes
if e
.is_locked
is not None)
2455 if count
== locked_count
:
2456 title
= _('Episodes are locked')
2457 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2458 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2460 elif locked_count
> 0:
2461 title
= _('Remove %d out of %d episodes?') % (count
-locked_count
, count
)
2462 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.')
2464 if confirm
and not self
.show_confirmation(message
, title
):
2467 progress
= ProgressIndicator(_('Removing episodes'), \
2468 _('Please wait while episodes are deleted'), \
2469 parent
=self
.main_window
)
2471 def finish_deletion(episode_urls
, channel_urls
):
2472 progress
.on_finished()
2474 # Episodes have been deleted - persist the database
2477 self
.update_episode_list_icons(episode_urls
)
2478 self
.update_podcast_list_model(channel_urls
)
2479 self
.play_or_download()
2482 episode_urls
= set()
2483 channel_urls
= set()
2485 for idx
, episode
in enumerate(episodes
):
2486 progress
.on_progress(float(idx
)/float(len(episodes
)))
2487 if episode
.is_locked
:
2488 log('Not deleting episode (is locked): %s', episode
.title
)
2490 log('Deleting episode: %s', episode
.title
)
2491 progress
.on_message(_('Deleting: %s') % episode
.title
)
2492 episode
.delete_from_disk()
2493 episode_urls
.add(episode
.url
)
2494 channel_urls
.add(episode
.channel
.url
)
2496 # Tell the shownotes window that we have removed the episode
2497 if self
.episode_shownotes_window
is not None and \
2498 self
.episode_shownotes_window
.episode
is not None and \
2499 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
2500 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
2502 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
2504 threading
.Thread(target
=thread_proc
).start()
2508 def on_itemRemoveOldEpisodes_activate( self
, widget
):
2509 if gpodder
.ui
.maemo
:
2511 ('maemo_remove_markup', None, None, _('Episode')),
2515 ('title_markup', None, None, _('Episode')),
2516 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2517 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2518 ('played_prop', None, None, _('Status')),
2519 ('age_prop', None, None, _('Downloaded')),
2522 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
2523 selection_buttons
= {
2524 _('Select played'): lambda episode
: episode
.is_played
,
2525 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
2528 instructions
= _('Select the episodes you want to delete:')
2532 for channel
in self
.channels
:
2533 for episode
in channel
.get_downloaded_episodes():
2534 # Disallow deletion of locked episodes that still exist
2535 if not episode
.is_locked
or not episode
.file_exists():
2536 episodes
.append(episode
)
2537 # Automatically select played and file-less episodes
2538 selected
.append(episode
.is_played
or \
2539 not episode
.file_exists())
2541 gPodderEpisodeSelector(self
.gPodder
, title
= _('Remove old episodes'), instructions
= instructions
, \
2542 episodes
= episodes
, selected
= selected
, columns
= columns
, \
2543 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
2544 selection_buttons
= selection_buttons
, _config
=self
.config
)
2546 def on_selected_episodes_status_changed(self
):
2547 self
.update_episode_list_icons(selected
=True)
2548 self
.update_podcast_list_model(selected
=True)
2551 def mark_selected_episodes_new(self
):
2552 for episode
in self
.get_selected_episodes():
2554 self
.on_selected_episodes_status_changed()
2556 def mark_selected_episodes_old(self
):
2557 for episode
in self
.get_selected_episodes():
2559 self
.on_selected_episodes_status_changed()
2561 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
2562 for episode
in self
.get_selected_episodes():
2564 episode
.mark(is_played
=not episode
.is_played
)
2566 episode
.mark(is_played
=new_value
)
2567 self
.on_selected_episodes_status_changed()
2569 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2570 for episode
in self
.get_selected_episodes():
2572 episode
.mark(is_locked
=not episode
.is_locked
)
2574 episode
.mark(is_locked
=new_value
)
2575 self
.on_selected_episodes_status_changed()
2577 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2578 if self
.active_channel
is None:
2581 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
2582 self
.active_channel
.update_channel_lock()
2584 for episode
in self
.active_channel
.get_all_episodes():
2585 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
2587 self
.update_podcast_list_model(selected
=True)
2588 self
.update_episode_list_icons(all
=True)
2590 def on_itemUpdateChannel_activate(self
, widget
=None):
2591 if self
.active_channel
is None:
2592 title
= _('No podcast selected')
2593 message
= _('Please select a podcast in the podcasts list to update.')
2594 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2597 self
.update_feed_cache(channels
=[self
.active_channel
])
2599 def on_itemUpdate_activate(self
, widget
=None):
2601 self
.update_feed_cache()
2603 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
)
2605 def download_episode_list_paused(self
, episodes
):
2606 self
.download_episode_list(episodes
, True)
2608 def download_episode_list(self
, episodes
, add_paused
=False):
2609 for episode
in episodes
:
2610 log('Downloading episode: %s', episode
.title
, sender
= self
)
2611 if not episode
.was_downloaded(and_exists
=True):
2613 for task
in self
.download_tasks_seen
:
2614 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
2615 self
.download_queue_manager
.add_task(task
)
2616 self
.enable_download_list_update()
2624 task
= download
.DownloadTask(episode
, self
.config
)
2625 except Exception, e
:
2626 self
.show_message(_('Download error while downloading %s:\n\n%s') % (episode
.title
, str(e
)), _('Download error'), important
=True)
2627 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
2631 task
.status
= task
.PAUSED
2633 self
.download_queue_manager
.add_task(task
)
2635 self
.download_status_model
.register_task(task
)
2636 self
.enable_download_list_update()
2638 def cancel_task_list(self
, tasks
):
2643 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
2644 task
.status
= task
.CANCELLED
2645 elif task
.status
== task
.PAUSED
:
2646 task
.status
= task
.CANCELLED
2647 # Call run, so the partial file gets deleted
2650 self
.update_episode_list_icons([task
.url
for task
in tasks
])
2651 self
.play_or_download()
2653 # Update the tab title and downloads list
2654 self
.update_downloads_list()
2656 def new_episodes_show(self
, episodes
, notification
=False):
2657 if gpodder
.ui
.maemo
:
2659 ('maemo_markup', None, None, _('Episode')),
2661 show_notification
= notification
2664 ('title_markup', None, None, _('Episode')),
2665 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2666 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2668 show_notification
= False
2670 instructions
= _('Select the episodes you want to download:')
2672 if self
.new_episodes_window
is not None:
2673 self
.new_episodes_window
.main_window
.destroy()
2674 self
.new_episodes_window
= None
2676 def download_episodes_callback(episodes
):
2677 self
.new_episodes_window
= None
2678 self
.download_episode_list(episodes
)
2680 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
2681 title
=_('New episodes available'), \
2682 instructions
=instructions
, \
2683 episodes
=episodes
, \
2685 selected_default
=True, \
2686 stock_ok_button
= 'gpodder-download', \
2687 callback
=download_episodes_callback
, \
2688 remove_callback
=lambda e
: e
.mark_old(), \
2689 remove_action
=_('Mark as old'), \
2690 remove_finished
=self
.episode_new_status_changed
, \
2691 _config
=self
.config
, \
2692 show_notification
=show_notification
)
2694 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
2695 if not self
.offer_new_episodes():
2696 self
.show_message(_('Please check for new episodes later.'), \
2697 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
2699 def get_new_episodes(self
, channels
=None):
2700 if channels
is None:
2701 channels
= self
.channels
2703 for channel
in channels
:
2704 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
2705 episodes
.append(episode
)
2709 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
2710 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
2711 # The sync process might have updated the status of episodes,
2712 # therefore persist the database here to avoid losing data
2715 def on_cleanup_ipod_activate(self
, widget
, *args
):
2716 self
.sync_ui
.on_cleanup_device()
2718 def on_manage_device_playlist(self
, widget
):
2719 self
.sync_ui
.on_manage_device_playlist()
2721 def show_hide_tray_icon(self
):
2722 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
2723 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
2724 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
2725 self
.tray_icon
.set_visible(False)
2727 self
.tray_icon
= None
2729 if self
.config
.minimize_to_tray
and self
.tray_icon
:
2730 self
.tray_icon
.set_visible(self
.is_iconified())
2731 elif self
.tray_icon
:
2732 self
.tray_icon
.set_visible(True)
2734 def on_itemShowToolbar_activate(self
, widget
):
2735 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
2737 def on_itemShowDescription_activate(self
, widget
):
2738 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
2740 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
2741 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
2742 if self
.config
.podcast_list_hide_boring
:
2743 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
2745 self
.podcast_list_model
.set_view_mode(-1)
2747 def on_item_view_podcasts_changed(self
, radioaction
, current
):
2749 if current
== self
.item_view_podcasts_all
:
2750 self
.podcast_list_model
.set_view_mode(-1)
2751 elif current
== self
.item_view_podcasts_downloaded
:
2752 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
2753 elif current
== self
.item_view_podcasts_unplayed
:
2754 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
2756 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
2758 def on_item_view_episodes_changed(self
, radioaction
, current
):
2759 if current
== self
.item_view_episodes_all
:
2760 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
2761 elif current
== self
.item_view_episodes_undeleted
:
2762 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
2763 elif current
== self
.item_view_episodes_downloaded
:
2764 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
2765 elif current
== self
.item_view_episodes_unplayed
:
2766 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
2768 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
2770 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
2771 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
2773 def update_item_device( self
):
2774 if not gpodder
.ui
.fremantle
:
2775 if self
.config
.device_type
!= 'none':
2776 self
.itemDevice
.set_visible(True)
2777 self
.itemDevice
.label
= self
.get_device_name()
2779 self
.itemDevice
.set_visible(False)
2781 def properties_closed( self
):
2782 self
.show_hide_tray_icon()
2783 self
.update_item_device()
2784 if gpodder
.ui
.maemo
:
2785 selection
= self
.treeAvailable
.get_selection()
2786 if self
.config
.maemo_enable_gestures
or \
2787 self
.config
.enable_fingerscroll
:
2788 selection
.set_mode(gtk
.SELECTION_SINGLE
)
2790 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
2792 def on_itemPreferences_activate(self
, widget
, *args
):
2793 gPodderPreferences(self
.gPodder
, _config
=self
.config
, \
2794 callback_finished
=self
.properties_closed
, \
2795 user_apps_reader
=self
.user_apps_reader
, \
2796 mygpo_login
=lambda: self
.require_my_gpodder_authentication(force_dialog
=True))
2798 def on_itemDependencies_activate(self
, widget
):
2799 gPodderDependencyManager(self
.gPodder
)
2801 def require_my_gpodder_authentication(self
, force_dialog
=False):
2802 if force_dialog
or (not self
.config
.my_gpodder_username
or not self
.config
.my_gpodder_password
):
2803 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'))
2805 self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
= authentication
2812 def on_goto_mygpo(self
, widget
):
2813 client
= my
.MygPodderClient(self
.config
.my_gpodder_service
, \
2814 self
.config
.my_gpodder_username
, \
2815 self
.config
.my_gpodder_password
)
2816 client
.open_website()
2818 def on_download_from_mygpo(self
, widget
=None):
2819 if self
.require_my_gpodder_authentication():
2820 client
= my
.MygPodderClient(self
.config
.my_gpodder_service
, \
2821 self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
)
2822 opml_data
= client
.download_subscriptions()
2823 if len(opml_data
) > 0:
2824 fp
= open(gpodder
.subscription_file
, 'w')
2827 (added
, skipped
) = (0, 0)
2828 i
= opml
.Importer(gpodder
.subscription_file
)
2830 existing
= [c
.url
for c
in self
.channels
]
2831 urls
= [item
['url'] for item
in i
.items
if item
['url'] not in existing
]
2833 skipped
= len(i
.items
) - len(urls
)
2836 self
.add_podcast_list(urls
)
2838 self
.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added
, skipped
), _('Result of subscription download'), widget
=self
.treeChannels
)
2839 elif widget
is not None:
2840 self
.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'), widget
=self
.treeChannels
)
2842 self
.config
.my_gpodder_password
= ''
2843 self
.on_download_from_mygpo(widget
)
2845 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important
=True)
2847 def on_upload_to_mygpo(self
, widget
):
2848 if self
.require_my_gpodder_authentication():
2849 client
= my
.MygPodderClient(self
.config
.my_gpodder_service
, \
2850 self
.config
.my_gpodder_username
, self
.config
.my_gpodder_password
)
2851 self
.save_channels_opml()
2852 success
, messages
= client
.upload_subscriptions(gpodder
.subscription_file
)
2853 if widget
is not None:
2855 self
.show_message('\n'.join(messages
), _('Results of upload'), important
=True)
2856 self
.config
.my_gpodder_password
= ''
2857 self
.on_upload_to_mygpo(widget
)
2859 self
.show_message('\n'.join(messages
), _('Results of upload'), widget
=self
.treeChannels
)
2861 log('Upload to my.gpodder.org failed, but widget is None!', sender
=self
)
2862 elif widget
is not None:
2863 self
.show_message(_('Please set up your username and password first.'), _('Username and password needed'), important
=True)
2865 def on_itemAddChannel_activate(self
, widget
=None):
2866 gPodderAddPodcast(self
.gPodder
, \
2867 add_urls_callback
=self
.add_podcast_list
)
2869 def on_itemEditChannel_activate(self
, widget
, *args
):
2870 if self
.active_channel
is None:
2871 title
= _('No podcast selected')
2872 message
= _('Please select a podcast in the podcasts list to edit.')
2873 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2876 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
2877 gPodderChannel(self
.main_window
, \
2878 channel
=self
.active_channel
, \
2879 callback_closed
=callback_closed
, \
2880 cover_downloader
=self
.cover_downloader
)
2882 def on_itemMassUnsubscribe_activate(self
, item
=None):
2884 ('title', None, None, _('Podcast')),
2887 # We're abusing the Episode Selector for selecting Podcasts here,
2888 # but it works and looks good, so why not? -- thp
2889 gPodderEpisodeSelector(self
.main_window
, \
2890 title
=_('Remove podcasts'), \
2891 instructions
=_('Select the podcast you want to remove.'), \
2892 episodes
=self
.channels
, \
2894 size_attribute
=None, \
2895 stock_ok_button
=gtk
.STOCK_DELETE
, \
2896 callback
=self
.remove_podcast_list
, \
2897 _config
=self
.config
)
2899 def remove_podcast_list(self
, channels
, confirm
=True):
2901 log('No podcasts selected for deletion', sender
=self
)
2904 if len(channels
) == 1:
2905 title
= _('Removing podcast')
2906 info
= _('Please wait while the podcast is removed')
2907 message
= _('Do you really want to remove this podcast and its episodes?')
2909 title
= _('Removing podcasts')
2910 info
= _('Please wait while the podcasts are removed')
2911 message
= _('Do you really want to remove the selected podcasts and their episodes?')
2913 if confirm
and not self
.show_confirmation(message
, title
):
2916 progress
= ProgressIndicator(title
, info
, parent
=self
.main_window
)
2918 def finish_deletion(select_url
):
2919 # Re-load the channels and select the desired new channel
2920 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
2921 progress
.on_finished()
2922 self
.update_podcasts_tab()
2927 for idx
, channel
in enumerate(channels
):
2928 # Update the UI for correct status messages
2929 progress
.on_progress(float(idx
)/float(len(channels
)))
2930 progress
.on_message(_('Removing %s') % channel
.title
)
2932 # Delete downloaded episodes
2933 channel
.remove_downloaded()
2935 # cancel any active downloads from this channel
2936 for episode
in channel
.get_all_episodes():
2937 util
.idle_add(self
.download_status_model
.cancel_by_url
,
2940 if len(channels
) == 1:
2941 # get the URL of the podcast we want to select next
2942 position
= self
.channels
.index(channel
)
2943 if position
== len(self
.channels
)-1:
2944 # this is the last podcast, so select the URL
2945 # of the item before this one (i.e. the "new last")
2946 select_url
= self
.channels
[position
-1].url
2948 # there is a podcast after the deleted one, so
2949 # we simply select the one that comes after it
2950 select_url
= self
.channels
[position
+1].url
2952 # Remove the channel and clean the database entries
2953 channel
.delete(purge
=True)
2954 self
.channels
.remove(channel
)
2956 # Clean up downloads and download directories
2957 self
.clean_up_downloads()
2959 self
.channel_list_changed
= True
2960 self
.save_channels_opml()
2962 # The remaining stuff is to be done in the GTK main thread
2963 util
.idle_add(finish_deletion
, select_url
)
2965 threading
.Thread(target
=thread_proc
).start()
2967 def on_itemRemoveChannel_activate(self
, widget
, *args
):
2968 if self
.active_channel
is None:
2969 title
= _('No podcast selected')
2970 message
= _('Please select a podcast in the podcasts list to remove.')
2971 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2974 self
.remove_podcast_list([self
.active_channel
])
2976 def get_opml_filter(self
):
2977 filter = gtk
.FileFilter()
2978 filter.add_pattern('*.opml')
2979 filter.add_pattern('*.xml')
2980 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2983 def on_item_import_from_file_activate(self
, widget
, filename
=None):
2984 if filename
is None:
2985 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
2986 # FIXME: Hildonization on Fremantle
2987 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
2988 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2989 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
2990 elif gpodder
.ui
.diablo
:
2991 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
2992 dlg
.set_filter(self
.get_opml_filter())
2993 response
= dlg
.run()
2995 if response
== gtk
.RESPONSE_OK
:
2996 filename
= dlg
.get_filename()
2999 if filename
is not None:
3000 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3001 custom_title
=_('Import podcasts from OPML file'), \
3002 add_urls_callback
=self
.add_podcast_list
, \
3003 hide_url_entry
=True)
3004 dir.download_opml_file(filename
)
3006 def on_itemExportChannels_activate(self
, widget
, *args
):
3007 if not self
.channels
:
3008 title
= _('Nothing to export')
3009 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3010 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3013 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3014 # FIXME: Hildonization on Fremantle
3015 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3016 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3017 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3018 elif gpodder
.ui
.diablo
:
3019 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3020 dlg
.set_filter(self
.get_opml_filter())
3021 response
= dlg
.run()
3022 if response
== gtk
.RESPONSE_OK
:
3023 filename
= dlg
.get_filename()
3025 exporter
= opml
.Exporter( filename
)
3026 if exporter
.write(self
.channels
):
3027 count
= len(self
.channels
)
3028 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3029 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3031 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3035 def on_itemImportChannels_activate(self
, widget
, *args
):
3036 if gpodder
.ui
.fremantle
:
3037 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3038 self
.config
.toplist_url
, \
3039 self
.config
.opml_url
, \
3040 self
.add_podcast_list
, \
3041 self
.on_itemAddChannel_activate
, \
3042 self
.on_download_from_mygpo
, \
3043 self
.show_text_edit_dialog
)
3045 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3046 add_urls_callback
=self
.add_podcast_list
)
3047 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3049 def on_homepage_activate(self
, widget
, *args
):
3050 util
.open_website(gpodder
.__url
__)
3052 def on_wiki_activate(self
, widget
, *args
):
3053 util
.open_website('http://wiki.gpodder.org/')
3055 def on_bug_tracker_activate(self
, widget
, *args
):
3056 if gpodder
.ui
.maemo
:
3057 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3059 util
.open_website('http://bugs.gpodder.org/')
3061 def on_shop_activate(self
, widget
, *args
):
3062 util
.open_website('http://gpodder.org/shop')
3064 def on_wishlist_activate(self
, widget
, *args
):
3065 util
.open_website('http://amzn.com/w/2L04WZKX274VB')
3067 def on_item_support_activate(self
, widget
):
3068 util
.open_website('http://gpodder.org/donate')
3070 def on_itemAbout_activate(self
, widget
, *args
):
3071 dlg
= gtk
.AboutDialog()
3072 dlg
.set_name('gPodder')
3073 dlg
.set_version(gpodder
.__version
__)
3074 dlg
.set_copyright(gpodder
.__copyright
__)
3075 dlg
.set_comments(_('A podcast client with focus on usability'))
3076 if not gpodder
.ui
.fremantle
:
3077 # Disable the URL label in Fremantle because of style issues
3078 dlg
.set_website(gpodder
.__url
__)
3079 dlg
.set_translator_credits( _('translator-credits'))
3080 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3082 if gpodder
.ui
.desktop
:
3083 # For the "GUI" version, we add some more
3084 # items to the about dialog (credits and logo)
3087 'Thomas Perl <thpinfo.com>',
3090 if os
.path
.exists(gpodder
.credits_file
):
3091 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3092 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3093 app_authors
+= credits
3095 dlg
.set_authors(app_authors
)
3097 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3099 dlg
.set_logo_icon_name('gpodder')
3100 elif gpodder
.ui
.fremantle
:
3101 for parent
in dlg
.vbox
.get_children():
3102 for child
in parent
.get_children():
3103 if isinstance(child
, gtk
.Label
):
3104 child
.set_selectable(False)
3105 child
.set_alignment(0.0, 0.5)
3109 def on_wNotebook_switch_page(self
, widget
, *args
):
3111 if gpodder
.ui
.maemo
:
3112 self
.tool_downloads
.set_active(page_num
== 1)
3113 page
= self
.wNotebook
.get_nth_page(page_num
)
3114 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3115 if page_num
== 0 and self
.active_channel
is not None:
3116 self
.set_title(self
.active_channel
.title
)
3118 self
.set_title(tab_label
)
3120 self
.play_or_download()
3121 self
.menuChannels
.set_sensitive(True)
3122 self
.menuSubscriptions
.set_sensitive(True)
3123 # The message area in the downloads tab should be hidden
3124 # when the user switches away from the downloads tab
3125 if self
.message_area
is not None:
3126 self
.message_area
.hide()
3127 self
.message_area
= None
3129 self
.menuChannels
.set_sensitive(False)
3130 self
.menuSubscriptions
.set_sensitive(False)
3131 if gpodder
.ui
.desktop
:
3132 self
.toolDownload
.set_sensitive(False)
3133 self
.toolPlay
.set_sensitive(False)
3134 self
.toolTransfer
.set_sensitive(False)
3135 self
.toolCancel
.set_sensitive(False)
3137 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3138 # double-click action of the podcast list or enter
3139 self
.treeChannels
.set_cursor(path
)
3141 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3142 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3144 if model
is not None and iter is not None:
3145 old_active_channel
= self
.active_channel
3146 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3148 if self
.active_channel
== old_active_channel
:
3151 if gpodder
.ui
.maemo
:
3152 self
.set_title(self
.active_channel
.title
)
3153 self
.itemEditChannel
.set_visible(True)
3154 self
.itemRemoveChannel
.set_visible(True)
3156 self
.active_channel
= None
3157 self
.itemEditChannel
.set_visible(False)
3158 self
.itemRemoveChannel
.set_visible(False)
3160 self
.update_episode_list_model()
3162 def on_btnEditChannel_clicked(self
, widget
, *args
):
3163 self
.on_itemEditChannel_activate( widget
, args
)
3165 def get_selected_episodes(self
):
3166 """Get a list of selected episodes from treeAvailable"""
3167 selection
= self
.treeAvailable
.get_selection()
3168 model
, paths
= selection
.get_selected_rows()
3170 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3173 def on_transfer_selected_episodes(self
, widget
):
3174 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3176 def on_playback_selected_episodes(self
, widget
):
3177 self
.playback_episodes(self
.get_selected_episodes())
3179 def on_shownotes_selected_episodes(self
, widget
):
3180 episodes
= self
.get_selected_episodes()
3182 episode
= episodes
.pop(0)
3183 self
.show_episode_shownotes(episode
)
3185 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3187 def on_download_selected_episodes(self
, widget
):
3188 episodes
= self
.get_selected_episodes()
3189 self
.download_episode_list(episodes
)
3190 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3191 self
.play_or_download()
3193 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3194 """Double-click/enter action handler for treeAvailable"""
3195 # We should only have one one selected as it was double clicked!
3196 e
= self
.get_selected_episodes()[0]
3198 if (self
.config
.double_click_episode_action
== 'download'):
3199 # If the episode has already been downloaded and exists then play it
3200 if e
.was_downloaded(and_exists
=True):
3201 self
.playback_episodes(self
.get_selected_episodes())
3202 # else download it if it is not already downloading
3203 elif not self
.episode_is_downloading(e
):
3204 self
.download_episode_list([e
])
3205 self
.update_episode_list_icons([e
.url
])
3206 self
.play_or_download()
3207 elif (self
.config
.double_click_episode_action
== 'stream'):
3208 # If we happen to have downloaded this episode simple play it
3209 if e
.was_downloaded(and_exists
=True):
3210 self
.playback_episodes(self
.get_selected_episodes())
3211 # else if streaming is possible stream it
3212 elif self
.streaming_possible():
3213 self
.playback_episodes(self
.get_selected_episodes())
3215 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3216 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3218 # default action is to display show notes
3219 self
.on_shownotes_selected_episodes(widget
)
3221 def show_episode_shownotes(self
, episode
):
3222 if self
.episode_shownotes_window
is None:
3223 log('First-time use of episode window --- creating', sender
=self
)
3224 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3225 _download_episode_list
=self
.download_episode_list
, \
3226 _playback_episodes
=self
.playback_episodes
, \
3227 _delete_episode_list
=self
.delete_episode_list
, \
3228 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3229 _cancel_task_list
=self
.cancel_task_list
, \
3230 _episode_is_downloading
=self
.episode_is_downloading
, \
3231 _streaming_possible
=self
.streaming_possible())
3232 self
.episode_shownotes_window
.show(episode
)
3233 if self
.episode_is_downloading(episode
):
3234 self
.update_downloads_list()
3236 def restart_auto_update_timer(self
):
3237 if self
._auto
_update
_timer
_source
_id
is not None:
3238 log('Removing existing auto update timer.', sender
=self
)
3239 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3240 self
._auto
_update
_timer
_source
_id
= None
3242 if self
.config
.auto_update_feeds
:
3243 interval
= 60*1000*self
.config
.auto_update_frequency
3244 log('Setting up auto update timer with interval %d.', \
3245 self
.config
.auto_update_frequency
, sender
=self
)
3246 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3247 interval
, self
._on
_auto
_update
_timer
)
3249 def _on_auto_update_timer(self
):
3250 log('Auto update timer fired.', sender
=self
)
3251 self
.update_feed_cache(force_update
=True)
3254 def on_treeDownloads_row_activated(self
, widget
, *args
):
3255 # Use the standard way of working on the treeview
3256 selection
= self
.treeDownloads
.get_selection()
3257 (model
, paths
) = selection
.get_selected_rows()
3258 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3260 for tree_row_reference
, task
in selected_tasks
:
3261 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3262 task
.status
= task
.PAUSED
3263 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3264 self
.download_queue_manager
.add_task(task
)
3265 self
.enable_download_list_update()
3266 elif task
.status
== task
.DONE
:
3267 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3269 self
.play_or_download()
3271 # Update the tab title and downloads list
3272 self
.update_downloads_list()
3274 def on_item_cancel_download_activate(self
, widget
):
3275 if self
.wNotebook
.get_current_page() == 0:
3276 selection
= self
.treeAvailable
.get_selection()
3277 (model
, paths
) = selection
.get_selected_rows()
3278 urls
= [model
.get_value(model
.get_iter(path
), \
3279 self
.episode_list_model
.C_URL
) for path
in paths
]
3280 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3281 if task
.url
in urls
]
3283 selection
= self
.treeDownloads
.get_selection()
3284 (model
, paths
) = selection
.get_selected_rows()
3285 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3286 self
.download_status_model
.C_TASK
) for path
in paths
]
3287 self
.cancel_task_list(selected_tasks
)
3289 def on_btnCancelAll_clicked(self
, widget
, *args
):
3290 self
.cancel_task_list(self
.download_tasks_seen
)
3292 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3293 if self
.wNotebook
.get_current_page() == 1:
3294 # Downloads tab visibile - skip (for now)
3297 episodes
= self
.get_selected_episodes()
3298 self
.delete_episode_list(episodes
)
3300 def on_key_press(self
, widget
, event
):
3301 # Allow tab switching with Ctrl + PgUp/PgDown
3302 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3303 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3304 self
.wNotebook
.prev_page()
3306 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3307 self
.wNotebook
.next_page()
3310 # After this code we only handle Maemo hardware keys,
3311 # so if we are not a Maemo app, we don't do anything
3312 if not gpodder
.ui
.maemo
:
3316 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3318 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3321 if diff
!= 0 and not self
.currently_updating
:
3322 selection
= self
.treeChannels
.get_selection()
3323 (model
, iter) = selection
.get_selected()
3324 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
3325 selection
.select_path(new_path
)
3326 self
.treeChannels
.set_cursor(new_path
)
3331 def on_iconify(self
):
3333 self
.gPodder
.set_skip_taskbar_hint(True)
3334 if self
.config
.minimize_to_tray
:
3335 self
.tray_icon
.set_visible(True)
3337 self
.gPodder
.set_skip_taskbar_hint(False)
3339 def on_uniconify(self
):
3341 self
.gPodder
.set_skip_taskbar_hint(False)
3342 if self
.config
.minimize_to_tray
:
3343 self
.tray_icon
.set_visible(False)
3345 self
.gPodder
.set_skip_taskbar_hint(False)
3347 def uniconify_main_window(self
):
3348 if self
.is_iconified():
3349 self
.gPodder
.present()
3351 def iconify_main_window(self
):
3352 if not self
.is_iconified():
3353 self
.gPodder
.iconify()
3355 def update_podcasts_tab(self
):
3356 if len(self
.channels
):
3357 if gpodder
.ui
.fremantle
:
3358 self
.button_refresh
.set_title(_('Check for new episodes'))
3359 self
.button_refresh
.show()
3361 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
3363 if gpodder
.ui
.fremantle
:
3364 self
.button_refresh
.hide()
3366 self
.label2
.set_text(_('Podcasts'))
3368 @dbus.service
.method(gpodder
.dbus_interface
)
3369 def show_gui_window(self
):
3370 self
.gPodder
.present()
3372 @dbus.service
.method(gpodder
.dbus_interface
)
3373 def subscribe_to_url(self
, url
):
3374 gPodderAddPodcast(self
.gPodder
,
3375 add_urls_callback
=self
.add_podcast_list
,
3378 @dbus.service
.method(gpodder
.dbus_interface
)
3379 def mark_episode_played(self
, filename
):
3380 if filename
is None:
3383 for channel
in self
.channels
:
3384 for episode
in channel
.get_all_episodes():
3385 fn
= episode
.local_filename(create
=False, check_only
=True)
3387 episode
.mark(is_played
=True)
3389 self
.update_episode_list_icons([episode
.url
])
3390 self
.update_podcast_list_model([episode
.channel
.url
])
3396 def main(options
=None):
3397 gobject
.threads_init()
3398 gobject
.set_application_name('gPodder')
3400 if gpodder
.ui
.maemo
:
3401 # Try to enable the custom icon theme for gPodder on Maemo
3402 settings
= gtk
.settings_get_default()
3403 settings
.set_string_property('gtk-icon-theme-name', \
3404 'gpodder', __file__
)
3405 # Extend the search path for the optified icon theme (Maemo 5)
3406 icon_theme
= gtk
.icon_theme_get_default()
3407 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
3409 gtk
.window_set_default_icon_name('gpodder')
3410 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
3413 session_bus
= dbus
.SessionBus(mainloop
=dbus
.glib
.DBusGMainLoop())
3414 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=session_bus
)
3415 except dbus
.exceptions
.DBusException
, dbe
:
3416 log('Warning: Cannot get "on the bus".', traceback
=True)
3417 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
3418 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
3419 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
3420 dlg
.set_title('gPodder')
3425 util
.make_directory(gpodder
.home
)
3426 gpodder
.load_plugins()
3428 config
= UIConfig(gpodder
.config_file
)
3430 if gpodder
.ui
.diablo
:
3431 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3432 # folder exists there (allow moving "gpodder" between SD cards or USB)
3433 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3434 if not os
.path
.exists(config
.download_dir
):
3435 log('Downloads might have been moved. Trying to locate them...')
3436 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
3437 dir = os
.path
.join(basedir
, 'gpodder')
3438 if os
.path
.exists(dir):
3439 log('Downloads found in: %s', dir)
3440 config
.download_dir
= dir
3443 log('Downloads NOT FOUND in %s', dir)
3445 if config
.enable_fingerscroll
:
3446 BuilderWidget
.use_fingerscroll
= True
3447 elif gpodder
.ui
.fremantle
:
3448 config
.on_quit_ask
= False
3450 gp
= gPodder(bus_name
, config
)
3453 if options
.subscribe
:
3454 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)