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