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 cleanup_downloads
=self
.cleanup_downloads
, \
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
.download_task_monitors
= set()
421 # Subscribed channels
422 self
.active_channel
= None
423 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
424 self
.channel_list_changed
= True
425 self
.update_podcasts_tab()
427 # load list of user applications for audio playback
428 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
429 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
431 # Set the "Device" menu item for the first time
432 if gpodder
.ui
.desktop
:
433 self
.update_item_device()
435 # Set up the first instance of MygPoClient
436 self
.mygpo_client
= my
.MygPoClient(self
.config
)
438 # Now, update the feed cache, when everything's in place
439 if not gpodder
.ui
.fremantle
:
440 self
.btnUpdateFeeds
.show()
441 self
.updating_feed_cache
= False
442 self
.feed_cache_update_cancelled
= False
443 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
445 self
.message_area
= None
447 def find_partial_downloads():
448 # Look for partial file downloads
449 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
450 count
= len(partial_files
)
451 resumable_episodes
= []
453 if not gpodder
.ui
.fremantle
:
454 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
455 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
456 _('Some episodes have not finished downloading in a previous session.'), \
457 False, self
.get_dialog_parent())
458 indicator
.on_message(N_('%d partial file', '%d partial files', count
) % count
)
460 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
463 for c
in self
.channels
:
464 for e
in c
.get_all_episodes():
465 filename
= e
.local_filename(create
=False, check_only
=True)
466 if filename
in candidates
:
467 log('Found episode: %s', e
.title
, sender
=self
)
469 indicator
.on_message(e
.title
)
470 indicator
.on_progress(float(found
)/count
)
471 candidates
.remove(filename
)
472 partial_files
.remove(filename
+'.partial')
473 resumable_episodes
.append(e
)
481 for f
in partial_files
:
482 log('Partial file without episode: %s', f
, sender
=self
)
485 util
.idle_add(indicator
.on_finished
)
487 if len(resumable_episodes
):
488 def offer_resuming():
489 self
.download_episode_list_paused(resumable_episodes
)
490 if not gpodder
.ui
.fremantle
:
491 resume_all
= gtk
.Button(_('Resume all'))
492 #resume_all.set_border_width(0)
493 def on_resume_all(button
):
494 selection
= self
.treeDownloads
.get_selection()
495 selection
.select_all()
496 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
497 selection
.unselect_all()
498 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
499 self
.message_area
.hide()
500 resume_all
.connect('clicked', on_resume_all
)
502 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
503 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
504 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
505 self
.message_area
.show_all()
506 self
.clean_up_downloads(delete_partial
=False)
507 util
.idle_add(offer_resuming
)
508 elif not gpodder
.ui
.fremantle
:
509 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
511 util
.idle_add(self
.clean_up_downloads
, True)
512 threading
.Thread(target
=find_partial_downloads
).start()
514 # Start the auto-update procedure
515 self
._auto
_update
_timer
_source
_id
= None
516 if self
.config
.auto_update_feeds
:
517 self
.restart_auto_update_timer()
519 # Delete old episodes if the user wishes to
520 if self
.config
.auto_remove_played_episodes
and \
521 self
.config
.episode_old_age
> 0:
522 old_episodes
= list(self
.get_expired_episodes())
523 if len(old_episodes
) > 0:
524 self
.delete_episode_list(old_episodes
, confirm
=False)
525 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
527 if gpodder
.ui
.fremantle
:
528 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
529 self
.button_refresh
.set_sensitive(True)
530 self
.button_subscribe
.set_sensitive(True)
531 self
.main_window
.set_title(_('gPodder'))
532 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
534 # Do the initial sync with the web service
535 util
.idle_add(self
.mygpo_client
.flush
, True)
537 # First-time users should be asked if they want to see the OPML
538 if not self
.channels
and not gpodder
.ui
.fremantle
:
539 util
.idle_add(self
.on_itemUpdate_activate
)
541 def on_played(self
, start
, end
, total
, file_uri
):
542 """Handle the "played" signal from a media player"""
543 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
544 filename
= file_uri
[len('file://'):]
545 # FIXME: Optimize this by querying the database more directly
546 for channel
in self
.channels
:
547 for episode
in channel
.get_all_episodes():
548 fn
= episode
.local_filename(create
=False, check_only
=True)
549 if fn
== filename
or episode
.url
== file_uri
:
550 file_type
= episode
.file_type()
551 # Automatically enable D-Bus played status mode
552 if file_type
== 'audio':
553 self
.config
.audio_played_dbus
= True
554 elif file_type
== 'video':
555 self
.config
.video_played_dbus
= True
559 episode
.total_time
= total
560 if episode
.current_position_updated
is None or \
561 now
> episode
.current_position_updated
:
562 episode
.current_position
= end
563 episode
.current_position_updated
= now
564 episode
.mark(is_played
=True)
567 self
.update_episode_list_icons([episode
.url
])
568 self
.update_podcast_list_model([episode
.channel
.url
])
570 # Submit this action to the webservice
571 self
.mygpo_client
.on_playback_full(episode
, \
575 def on_add_remove_podcasts_mygpo(self
):
576 actions
= self
.mygpo_client
.get_received_actions()
580 existing_urls
= [c
.url
for c
in self
.channels
]
582 # Columns for the episode selector window - just one...
584 ('description', None, None, _('Action')),
587 # A list of actions that have to be chosen from
590 # Actions that are ignored (already carried out)
593 for action
in actions
:
594 if action
.is_add
and action
.url
not in existing_urls
:
595 changes
.append(my
.Change(action
))
596 elif action
.is_remove
and action
.url
in existing_urls
:
597 podcast_object
= None
598 for podcast
in self
.channels
:
599 if podcast
.url
== action
.url
:
600 podcast_object
= podcast
602 changes
.append(my
.Change(action
, podcast_object
))
604 log('Ignoring action: %s', action
, sender
=self
)
605 ignored
.append(action
)
607 # Confirm all ignored changes
608 self
.mygpo_client
.confirm_received_actions(ignored
)
610 def execute_podcast_actions(selected
):
611 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
612 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
614 # Apply the accepted changes locally
615 self
.add_podcast_list(add_list
)
616 self
.remove_podcast_list(remove_list
, confirm
=False)
618 # All selected items are now confirmed
619 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
621 # Revert the changes on the server
622 rejected
= [c
.action
for c
in changes
if c
not in selected
]
623 self
.mygpo_client
.reject_received_actions(rejected
)
626 # We're abusing the Episode Selector again ;) -- thp
627 gPodderEpisodeSelector(self
.main_window
, \
628 title
=_('Confirm changes from gpodder.net'), \
629 instructions
=_('Select the actions you want to carry out.'), \
632 size_attribute
=None, \
633 stock_ok_button
=gtk
.STOCK_APPLY
, \
634 callback
=execute_podcast_actions
, \
637 # There are some actions that need the user's attention
642 # We have no remaining actions - no selection happens
645 def rewrite_urls_mygpo(self
):
646 # Check if we have to rewrite URLs since the last add
647 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
649 for rewritten_url
in rewritten_urls
:
650 if not rewritten_url
.new_url
:
653 for channel
in self
.channels
:
654 if channel
.url
== rewritten_url
.old_url
:
655 log('Updating URL of %s to %s', channel
, \
656 rewritten_url
.new_url
, sender
=self
)
657 channel
.url
= rewritten_url
.new_url
659 self
.channel_list_changed
= True
660 util
.idle_add(self
.update_episode_list_model
)
663 def on_send_full_subscriptions(self
):
664 # Send the full subscription list to the gpodder.net client
665 # (this will overwrite the subscription list on the server)
666 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
667 _('Your subscriptions are being uploaded to the server.'), \
668 False, self
.get_dialog_parent())
671 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
672 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
677 message
= e
.__class
__.__name
__
678 self
.show_message(message
, \
679 _('Error while uploading'), \
681 util
.idle_add(show_error
, e
)
683 util
.idle_add(indicator
.on_finished
)
685 def on_podcast_selected(self
, treeview
, path
, column
):
687 model
= treeview
.get_model()
688 channel
= model
.get_value(model
.get_iter(path
), \
689 PodcastListModel
.C_CHANNEL
)
690 self
.active_channel
= channel
691 self
.update_episode_list_model()
692 self
.episodes_window
.channel
= self
.active_channel
693 self
.episodes_window
.show()
695 def on_button_subscribe_clicked(self
, button
):
696 self
.on_itemImportChannels_activate(button
)
698 def on_button_downloads_clicked(self
, widget
):
699 self
.downloads_window
.show()
701 def show_episode_in_download_manager(self
, episode
):
702 self
.downloads_window
.show()
703 model
= self
.treeDownloads
.get_model()
704 selection
= self
.treeDownloads
.get_selection()
705 selection
.unselect_all()
706 it
= model
.get_iter_first()
707 while it
is not None:
708 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
709 if task
.episode
.url
== episode
.url
:
710 selection
.select_iter(it
)
711 # FIXME: Scroll to selection in pannable area
713 it
= model
.iter_next(it
)
715 def for_each_episode_set_task_status(self
, episodes
, status
):
716 episode_urls
= set(episode
.url
for episode
in episodes
)
717 model
= self
.treeDownloads
.get_model()
718 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
719 model
.get_value(row
.iter, \
720 DownloadStatusModel
.C_TASK
)) for row
in model \
721 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
723 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
725 def on_window_orientation_changed(self
, orientation
):
726 self
._last
_orientation
= orientation
727 if self
.preferences_dialog
is not None:
728 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
730 treeview
= self
.treeChannels
731 if orientation
== Orientation
.PORTRAIT
:
732 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
733 # Work around Maemo bug #4718
734 self
.button_subscribe
.set_name('HildonButton-thumb')
735 self
.button_refresh
.set_name('HildonButton-thumb')
737 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
738 # Work around Maemo bug #4718
739 self
.button_subscribe
.set_name('HildonButton-finger')
740 self
.button_refresh
.set_name('HildonButton-finger')
742 def on_treeview_podcasts_selection_changed(self
, selection
):
743 model
, iter = selection
.get_selected()
745 self
.active_channel
= None
746 self
.episode_list_model
.clear()
748 def on_treeview_button_pressed(self
, treeview
, event
):
749 if event
.window
!= treeview
.get_bin_window():
752 TreeViewHelper
.save_button_press_event(treeview
, event
)
754 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
755 TreeViewHelper
.ROLE_PODCASTS
:
756 return self
.currently_updating
758 return event
.button
== self
.context_menu_mouse_button
and \
761 def on_treeview_podcasts_button_released(self
, treeview
, event
):
762 if event
.window
!= treeview
.get_bin_window():
766 return self
.treeview_channels_handle_gestures(treeview
, event
)
767 return self
.treeview_channels_show_context_menu(treeview
, event
)
769 def on_treeview_episodes_button_released(self
, treeview
, event
):
770 if event
.window
!= treeview
.get_bin_window():
774 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
775 return self
.treeview_available_handle_gestures(treeview
, event
)
777 return self
.treeview_available_show_context_menu(treeview
, event
)
779 def on_treeview_downloads_button_released(self
, treeview
, event
):
780 if event
.window
!= treeview
.get_bin_window():
783 return self
.treeview_downloads_show_context_menu(treeview
, event
)
785 def on_entry_search_podcasts_changed(self
, editable
):
786 if self
.hbox_search_podcasts
.get_property('visible'):
787 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
789 def on_entry_search_podcasts_key_press(self
, editable
, event
):
790 if event
.keyval
== gtk
.keysyms
.Escape
:
791 self
.hide_podcast_search()
794 def hide_podcast_search(self
, *args
):
795 self
.hbox_search_podcasts
.hide()
796 self
.entry_search_podcasts
.set_text('')
797 self
.podcast_list_model
.set_search_term(None)
798 self
.treeChannels
.grab_focus()
800 def show_podcast_search(self
, input_char
):
801 self
.hbox_search_podcasts
.show()
802 self
.entry_search_podcasts
.insert_text(input_char
, -1)
803 self
.entry_search_podcasts
.grab_focus()
804 self
.entry_search_podcasts
.set_position(-1)
806 def init_podcast_list_treeview(self
):
807 # Set up podcast channel tree view widget
808 if gpodder
.ui
.fremantle
:
809 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
810 self
.item_view_podcasts_downloaded
.set_active(True)
811 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
812 self
.item_view_podcasts_unplayed
.set_active(True)
814 self
.item_view_podcasts_all
.set_active(True)
815 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
817 iconcolumn
= gtk
.TreeViewColumn('')
818 iconcell
= gtk
.CellRendererPixbuf()
819 iconcolumn
.pack_start(iconcell
, False)
820 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
821 self
.treeChannels
.append_column(iconcolumn
)
823 namecolumn
= gtk
.TreeViewColumn('')
824 namecell
= gtk
.CellRendererText()
825 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
826 namecolumn
.pack_start(namecell
, True)
827 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
829 iconcell
= gtk
.CellRendererPixbuf()
830 iconcell
.set_property('xalign', 1.0)
831 namecolumn
.pack_start(iconcell
, False)
832 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
833 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
834 self
.treeChannels
.append_column(namecolumn
)
836 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
838 # When no podcast is selected, clear the episode list model
839 selection
= self
.treeChannels
.get_selection()
840 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
842 # Set up type-ahead find for the podcast list
843 def on_key_press(treeview
, event
):
844 if event
.keyval
== gtk
.keysyms
.Escape
:
845 self
.hide_podcast_search()
846 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
847 self
.hide_podcast_search()
848 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
849 # Don't handle type-ahead when control is pressed (so shortcuts
850 # with the Ctrl key still work, e.g. Ctrl+A, ...)
853 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
854 if unicode_char_id
== 0:
856 input_char
= unichr(unicode_char_id
)
857 self
.show_podcast_search(input_char
)
859 self
.treeChannels
.connect('key-press-event', on_key_press
)
861 # Enable separators to the podcast list to separate special podcasts
862 # from others (this is used for the "all episodes" view)
863 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
865 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
867 def on_entry_search_episodes_changed(self
, editable
):
868 if self
.hbox_search_episodes
.get_property('visible'):
869 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
871 def on_entry_search_episodes_key_press(self
, editable
, event
):
872 if event
.keyval
== gtk
.keysyms
.Escape
:
873 self
.hide_episode_search()
876 def hide_episode_search(self
, *args
):
877 self
.hbox_search_episodes
.hide()
878 self
.entry_search_episodes
.set_text('')
879 self
.episode_list_model
.set_search_term(None)
880 self
.treeAvailable
.grab_focus()
882 def show_episode_search(self
, input_char
):
883 self
.hbox_search_episodes
.show()
884 self
.entry_search_episodes
.insert_text(input_char
, -1)
885 self
.entry_search_episodes
.grab_focus()
886 self
.entry_search_episodes
.set_position(-1)
888 def init_episode_list_treeview(self
):
889 # For loading the list model
890 self
.empty_episode_list_model
= EpisodeListModel()
891 self
.episode_list_model
= EpisodeListModel()
893 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
894 self
.item_view_episodes_undeleted
.set_active(True)
895 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
896 self
.item_view_episodes_downloaded
.set_active(True)
897 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
898 self
.item_view_episodes_unplayed
.set_active(True)
900 self
.item_view_episodes_all
.set_active(True)
902 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
904 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
906 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
908 iconcell
= gtk
.CellRendererPixbuf()
910 iconcell
.set_fixed_size(50, 50)
911 status_column_label
= ''
913 status_column_label
= _('Status')
914 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
916 namecell
= gtk
.CellRendererText()
917 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
918 namecolumn
= gtk
.TreeViewColumn(_('Episode'), namecell
, markup
=EpisodeListModel
.C_DESCRIPTION
)
919 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
920 namecolumn
.set_resizable(True)
921 namecolumn
.set_expand(True)
923 sizecell
= gtk
.CellRendererText()
924 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
926 releasecell
= gtk
.CellRendererText()
927 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
929 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
930 itemcolumn
.set_reorderable(True)
931 self
.treeAvailable
.append_column(itemcolumn
)
934 sizecolumn
.set_visible(False)
935 releasecolumn
.set_visible(False)
937 # Set up type-ahead find for the episode list
938 def on_key_press(treeview
, event
):
939 if event
.keyval
== gtk
.keysyms
.Escape
:
940 self
.hide_episode_search()
941 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
942 self
.hide_episode_search()
943 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
944 # Don't handle type-ahead when control is pressed (so shortcuts
945 # with the Ctrl key still work, e.g. Ctrl+A, ...)
948 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
949 if unicode_char_id
== 0:
951 input_char
= unichr(unicode_char_id
)
952 self
.show_episode_search(input_char
)
954 self
.treeAvailable
.connect('key-press-event', on_key_press
)
956 if gpodder
.ui
.desktop
:
957 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
958 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
959 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
960 if self
.config
.on_drag_mark_played
:
961 for episode
in self
.get_selected_episodes():
962 episode
.mark(is_played
=True)
963 self
.on_selected_episodes_status_changed()
964 uris
= ['file://'+e
.local_filename(create
=False) \
965 for e
in self
.get_selected_episodes() \
966 if e
.was_downloaded(and_exists
=True)]
967 uris
.append('') # for the trailing '\r\n'
968 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
969 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
971 selection
= self
.treeAvailable
.get_selection()
972 if gpodder
.ui
.diablo
:
973 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
974 selection
.set_mode(gtk
.SELECTION_SINGLE
)
976 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
977 elif gpodder
.ui
.fremantle
:
978 selection
.set_mode(gtk
.SELECTION_SINGLE
)
980 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
981 # Update the sensitivity of the toolbar buttons on the Desktop
982 selection
.connect('changed', lambda s
: self
.play_or_download())
984 if gpodder
.ui
.diablo
:
985 # Set up the tap-and-hold context menu for podcasts
987 menu
.append(self
.itemUpdateChannel
.create_menu_item())
988 menu
.append(self
.itemEditChannel
.create_menu_item())
989 menu
.append(gtk
.SeparatorMenuItem())
990 menu
.append(self
.itemRemoveChannel
.create_menu_item())
991 menu
.append(gtk
.SeparatorMenuItem())
992 item
= gtk
.ImageMenuItem(_('Close this menu'))
993 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
997 menu
= self
.set_finger_friendly(menu
)
998 self
.treeChannels
.tap_and_hold_setup(menu
)
1001 def init_download_list_treeview(self
):
1002 # enable multiple selection support
1003 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1004 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1006 # columns and renderers for "download progress" tab
1007 # First column: [ICON] Episodename
1008 column
= gtk
.TreeViewColumn(_('Episode'))
1010 cell
= gtk
.CellRendererPixbuf()
1011 if gpodder
.ui
.maemo
:
1012 cell
.set_fixed_size(50, 50)
1013 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1014 column
.pack_start(cell
, expand
=False)
1015 column
.add_attribute(cell
, 'stock-id', \
1016 DownloadStatusModel
.C_ICON_NAME
)
1018 cell
= gtk
.CellRendererText()
1019 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1020 column
.pack_start(cell
, expand
=True)
1021 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1022 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1023 column
.set_expand(True)
1024 self
.treeDownloads
.append_column(column
)
1026 # Second column: Progress
1027 cell
= gtk
.CellRendererProgress()
1028 cell
.set_property('yalign', .5)
1029 cell
.set_property('ypad', 6)
1030 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1031 value
=DownloadStatusModel
.C_PROGRESS
, \
1032 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1033 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1034 column
.set_expand(False)
1035 self
.treeDownloads
.append_column(column
)
1036 column
.set_property('min-width', 150)
1037 column
.set_property('max-width', 150)
1039 self
.treeDownloads
.set_model(self
.download_status_model
)
1040 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1042 def on_treeview_expose_event(self
, treeview
, event
):
1043 if event
.window
== treeview
.get_bin_window():
1044 model
= treeview
.get_model()
1045 if (model
is not None and model
.get_iter_first() is not None):
1048 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1049 ctx
= event
.window
.cairo_create()
1050 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1051 event
.area
.width
, event
.area
.height
)
1054 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1057 if role
== TreeViewHelper
.ROLE_EPISODES
:
1058 if self
.currently_updating
:
1059 text
= _('Loading episodes')
1060 progress
= self
.episode_list_model
.get_update_progress()
1061 elif self
.config
.episode_list_view_mode
!= \
1062 EpisodeListModel
.VIEW_ALL
:
1063 text
= _('No episodes in current view')
1065 text
= _('No episodes available')
1066 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1067 if self
.config
.episode_list_view_mode
!= \
1068 EpisodeListModel
.VIEW_ALL
and \
1069 self
.config
.podcast_list_hide_boring
and \
1070 len(self
.channels
) > 0:
1071 text
= _('No podcasts in this view')
1073 text
= _('No subscriptions')
1074 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1075 text
= _('No active downloads')
1077 raise Exception('on_treeview_expose_event: unknown role')
1079 if gpodder
.ui
.fremantle
:
1080 from gpodder
.gtkui
.frmntl
import style
1081 font_desc
= style
.get_font_desc('LargeSystemFont')
1085 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1089 def enable_download_list_update(self
):
1090 if not self
.download_list_update_enabled
:
1091 self
.update_downloads_list()
1092 gobject
.timeout_add(1500, self
.update_downloads_list
)
1093 self
.download_list_update_enabled
= True
1095 def cleanup_downloads(self
):
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 downloads list one more time
1124 self
.update_downloads_list(can_call_cleanup
=False)
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
, can_call_cleanup
=True):
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.)
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)
1275 # Remove finished episodes
1276 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1277 self
.cleanup_downloads()
1279 # Stop updating the download list here
1280 self
.download_list_update_enabled
= False
1282 if not gpodder
.ui
.fremantle
:
1283 self
.gPodder
.set_title(' - '.join(title
))
1285 self
.update_episode_list_icons(episode_urls
)
1286 if self
.episode_shownotes_window
is not None:
1287 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1288 shownotes_task
!= self
.episode_shownotes_window
.task
:
1289 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1290 self
.episode_shownotes_window
._download
_status
_progress
()
1291 self
.play_or_download()
1293 self
.update_podcast_list_model(channel_urls
)
1295 return self
.download_list_update_enabled
1296 except Exception, e
:
1297 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1298 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1299 # We return False here, so the update loop won't be called again,
1300 # that's why we require the restart of gPodder in the message.
1303 def on_config_changed(self
, *args
):
1304 util
.idle_add(self
._on
_config
_changed
, *args
)
1306 def _on_config_changed(self
, name
, old_value
, new_value
):
1307 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1308 self
.toolbar
.set_property('visible', new_value
)
1309 elif name
== 'videoplayer':
1310 self
.config
.video_played_dbus
= False
1311 elif name
== 'player':
1312 self
.config
.audio_played_dbus
= False
1313 elif name
== 'episode_list_descriptions':
1314 self
.update_episode_list_model()
1315 elif name
== 'episode_list_thumbnails':
1316 self
.update_episode_list_icons(all
=True)
1317 elif name
== 'rotation_mode':
1318 self
._fremantle
_rotation
.set_mode(new_value
)
1319 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1320 self
.restart_auto_update_timer()
1321 elif name
== 'podcast_list_view_all':
1322 # Force a update of the podcast list model
1323 self
.channel_list_changed
= True
1324 if gpodder
.ui
.fremantle
:
1325 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1326 while gtk
.events_pending():
1327 gtk
.main_iteration(False)
1328 self
.update_podcast_list_model()
1329 if gpodder
.ui
.fremantle
:
1330 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1332 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1333 # With get_bin_window, we get the window that contains the rows without
1334 # the header. The Y coordinate of this window will be the height of the
1335 # treeview header. This is the amount we have to subtract from the
1336 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1337 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1340 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1342 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1343 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1346 if path
is not None:
1347 model
= treeview
.get_model()
1348 iter = model
.get_iter(path
)
1349 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1351 if role
== TreeViewHelper
.ROLE_EPISODES
:
1352 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1353 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1354 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1356 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1357 if last_tooltip
is not None and last_tooltip
!= id:
1358 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1360 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1362 if role
== TreeViewHelper
.ROLE_EPISODES
:
1363 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1365 tooltip
.set_text(description
)
1368 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1369 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1372 channel
.request_save_dir_size()
1373 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1374 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1376 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1377 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1378 table
= gtk
.Table(rows
=3, columns
=3)
1379 table
.set_row_spacings(5)
1380 table
.set_col_spacings(5)
1381 table
.set_border_width(5)
1383 heading
= gtk
.Label()
1384 heading
.set_alignment(0, 1)
1385 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1386 table
.attach(heading
, 0, 1, 0, 1)
1387 size_info
= gtk
.Label()
1388 size_info
.set_alignment(1, 1)
1389 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1390 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1391 table
.attach(size_info
, 2, 3, 0, 1)
1393 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1395 if len(channel
.description
) < 500:
1396 description
= channel
.description
1398 pos
= channel
.description
.find('\n\n')
1399 if pos
== -1 or pos
> 500:
1400 description
= channel
.description
[:498]+'[...]'
1402 description
= channel
.description
[:pos
]
1404 description
= gtk
.Label(description
)
1406 description
.set_markup(error_str
)
1407 description
.set_alignment(0, 0)
1408 description
.set_line_wrap(True)
1409 table
.attach(description
, 0, 3, 2, 3)
1412 tooltip
.set_custom(table
)
1416 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1419 def treeview_allow_tooltips(self
, treeview
, allow
):
1420 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1422 def update_m3u_playlist_clicked(self
, widget
):
1423 if self
.active_channel
is not None:
1424 self
.active_channel
.update_m3u_playlist()
1425 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1427 def treeview_handle_context_menu_click(self
, treeview
, event
):
1428 x
, y
= int(event
.x
), int(event
.y
)
1429 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1431 selection
= treeview
.get_selection()
1432 model
, paths
= selection
.get_selected_rows()
1434 if path
is None or (path
not in paths
and \
1435 event
.button
== self
.context_menu_mouse_button
):
1436 # We have right-clicked, but not into the selection,
1437 # assume we don't want to operate on the selection
1440 if path
is not None and not paths
and \
1441 event
.button
== self
.context_menu_mouse_button
:
1442 # No selection or clicked outside selection;
1443 # select the single item where we clicked
1444 treeview
.grab_focus()
1445 treeview
.set_cursor(path
, column
, 0)
1449 # Unselect any remaining items (clicked elsewhere)
1450 if hasattr(treeview
, 'is_rubber_banding_active'):
1451 if not treeview
.is_rubber_banding_active():
1452 selection
.unselect_all()
1454 selection
.unselect_all()
1458 def downloads_list_get_selection(self
, model
=None, paths
=None):
1459 if model
is None and paths
is None:
1460 selection
= self
.treeDownloads
.get_selection()
1461 model
, paths
= selection
.get_selected_rows()
1463 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1464 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1465 model
.get_value(model
.get_iter(path
), \
1466 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1468 for row_reference
, task
in selected_tasks
:
1469 if task
.status
!= download
.DownloadTask
.QUEUED
:
1471 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1472 download
.DownloadTask
.FAILED
, \
1473 download
.DownloadTask
.CANCELLED
):
1475 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1476 download
.DownloadTask
.QUEUED
, \
1477 download
.DownloadTask
.DOWNLOADING
):
1479 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1480 download
.DownloadTask
.DOWNLOADING
):
1482 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1483 download
.DownloadTask
.FAILED
, \
1484 download
.DownloadTask
.DONE
):
1487 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1489 def downloads_finished(self
, download_tasks_seen
):
1490 # FIXME: Filter all tasks that have already been reported
1491 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1492 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1494 if finished_downloads
and failed_downloads
:
1495 message
= self
.format_episode_list(finished_downloads
, 5)
1496 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1497 message
+= self
.format_episode_list(failed_downloads
, 5)
1498 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1499 elif finished_downloads
:
1500 message
= self
.format_episode_list(finished_downloads
)
1501 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1502 elif failed_downloads
:
1503 message
= self
.format_episode_list(failed_downloads
)
1504 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1506 # Open torrent files right after download (bug 1029)
1507 if self
.config
.open_torrent_after_download
:
1508 for task
in download_tasks_seen
:
1509 if task
.status
!= task
.DONE
:
1512 episode
= task
.episode
1513 if episode
.mimetype
!= 'application/x-bittorrent':
1516 self
.playback_episodes([episode
])
1519 def format_episode_list(self
, episode_list
, max_episodes
=10):
1521 Format a list of episode names for notifications
1523 Will truncate long episode names and limit the amount of
1524 episodes displayed (max_episodes=10).
1526 The episode_list parameter should be a list of strings.
1528 MAX_TITLE_LENGTH
= 100
1531 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1532 if len(title
) > MAX_TITLE_LENGTH
:
1533 middle
= (MAX_TITLE_LENGTH
/2)-2
1534 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1535 result
.append(saxutils
.escape(title
))
1538 more_episodes
= len(episode_list
) - max_episodes
1539 if more_episodes
> 0:
1540 result
.append('(...')
1541 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1542 result
.append('...)')
1544 return (''.join(result
)).strip()
1546 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1547 episode_urls
= set()
1548 model
= self
.treeDownloads
.get_model()
1549 for row_reference
, task
in tasks
:
1550 if status
== download
.DownloadTask
.QUEUED
:
1551 # Only queue task when its paused/failed/cancelled (or forced)
1552 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1553 self
.download_queue_manager
.add_task(task
, force_start
)
1554 self
.enable_download_list_update()
1555 elif status
== download
.DownloadTask
.CANCELLED
:
1556 # Cancelling a download allowed when downloading/queued
1557 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1558 task
.status
= status
1559 # Cancelling paused downloads requires a call to .run()
1560 elif task
.status
== task
.PAUSED
:
1561 task
.status
= status
1562 # Call run, so the partial file gets deleted
1564 elif status
== download
.DownloadTask
.PAUSED
:
1565 # Pausing a download only when queued/downloading
1566 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1567 task
.status
= status
1568 elif status
is None:
1569 # Remove the selected task - cancel downloading/queued tasks
1570 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1571 task
.status
= task
.CANCELLED
1572 model
.remove(model
.get_iter(row_reference
.get_path()))
1573 # Remember the URL, so we can tell the UI to update
1575 # We don't "see" this task anymore - remove it;
1576 # this is needed, so update_episode_list_icons()
1577 # below gets the correct list of "seen" tasks
1578 self
.download_tasks_seen
.remove(task
)
1579 except KeyError, key_error
:
1580 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1581 episode_urls
.add(task
.url
)
1582 # Tell the task that it has been removed (so it can clean up)
1583 task
.removed_from_list()
1585 # We can (hopefully) simply set the task status here
1586 task
.status
= status
1587 # Tell the podcasts tab to update icons for our removed podcasts
1588 self
.update_episode_list_icons(episode_urls
)
1589 # Update the tab title and downloads list
1590 self
.update_downloads_list()
1592 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1593 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1595 if not hasattr(treeview
, 'is_rubber_banding_active'):
1598 return not treeview
.is_rubber_banding_active()
1600 if event
.button
== self
.context_menu_mouse_button
:
1601 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1602 self
.downloads_list_get_selection(model
, paths
)
1604 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1605 # This creates a menu item for selection-wide actions
1606 item
= gtk
.ImageMenuItem(label
)
1607 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1608 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1609 item
.set_sensitive(sensitive
)
1610 return self
.set_finger_friendly(item
)
1614 item
= gtk
.ImageMenuItem(_('Episode details'))
1615 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1616 if len(selected_tasks
) == 1:
1617 row_reference
, task
= selected_tasks
[0]
1618 episode
= task
.episode
1619 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1621 item
.set_sensitive(False)
1622 menu
.append(self
.set_finger_friendly(item
))
1623 menu
.append(gtk
.SeparatorMenuItem())
1625 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1627 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1628 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1629 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1630 menu
.append(gtk
.SeparatorMenuItem())
1631 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1633 if gpodder
.ui
.maemo
:
1634 # Because we open the popup on left-click for Maemo,
1635 # we also include a non-action to close the menu
1636 menu
.append(gtk
.SeparatorMenuItem())
1637 item
= gtk
.ImageMenuItem(_('Close this menu'))
1638 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1640 menu
.append(self
.set_finger_friendly(item
))
1643 menu
.popup(None, None, None, event
.button
, event
.time
)
1646 def treeview_channels_show_context_menu(self
, treeview
, event
):
1647 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1651 # Check for valid channel id, if there's no id then
1652 # assume that it is a proxy channel or equivalent
1653 # and cannot be operated with right click
1654 if self
.active_channel
.id is None:
1657 if event
.button
== 3:
1662 item
= gtk
.ImageMenuItem( _('Open download folder'))
1663 item
.set_image( gtk
.image_new_from_icon_name(ICON('folder-open'), gtk
.ICON_SIZE_MENU
))
1664 item
.connect('activate', lambda x
: util
.gui_open(self
.active_channel
.save_dir
))
1667 item
= gtk
.ImageMenuItem( _('Update Feed'))
1668 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1669 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1670 item
.set_sensitive( not self
.updating_feed_cache
)
1673 item
= gtk
.ImageMenuItem(_('Update M3U playlist'))
1674 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1675 item
.connect('activate', self
.update_m3u_playlist_clicked
)
1678 if self
.active_channel
.link
:
1679 item
= gtk
.ImageMenuItem(_('Visit website'))
1680 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1681 item
.connect('activate', lambda w
: util
.open_website(self
.active_channel
.link
))
1684 if self
.active_channel
.channel_is_locked
:
1685 item
= gtk
.ImageMenuItem(_('Allow deletion of all episodes'))
1686 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1687 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1688 menu
.append(self
.set_finger_friendly(item
))
1690 item
= gtk
.ImageMenuItem(_('Prohibit deletion of all episodes'))
1691 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1692 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1693 menu
.append(self
.set_finger_friendly(item
))
1696 menu
.append( gtk
.SeparatorMenuItem())
1698 item
= gtk
.ImageMenuItem(gtk
.STOCK_EDIT
)
1699 item
.connect( 'activate', self
.on_itemEditChannel_activate
)
1702 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1703 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1707 # Disable tooltips while we are showing the menu, so
1708 # the tooltip will not appear over the menu
1709 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1710 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1711 menu
.popup( None, None, None, event
.button
, event
.time
)
1715 def on_itemClose_activate(self
, widget
):
1716 if self
.tray_icon
is not None:
1717 self
.iconify_main_window()
1719 self
.on_gPodder_delete_event(widget
)
1721 def cover_file_removed(self
, channel_url
):
1723 The Cover Downloader calls this when a previously-
1724 available cover has been removed from the disk. We
1725 have to update our model to reflect this change.
1727 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1729 def cover_download_finished(self
, channel_url
, pixbuf
):
1731 The Cover Downloader calls this when it has finished
1732 downloading (or registering, if already downloaded)
1733 a new channel cover, which is ready for displaying.
1735 self
.podcast_list_model
.add_cover_by_url(channel_url
, pixbuf
)
1737 def save_episodes_as_file(self
, episodes
):
1738 for episode
in episodes
:
1739 self
.save_episode_as_file(episode
)
1741 def save_episode_as_file(self
, episode
):
1742 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1743 if episode
.was_downloaded(and_exists
=True):
1744 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1745 copy_from
= episode
.local_filename(create
=False)
1746 assert copy_from
is not None
1747 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1748 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1749 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1751 def copy_episodes_bluetooth(self
, episodes
):
1752 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1754 def convert_and_send_thread(episode
):
1755 for episode
in episodes
:
1756 filename
= episode
.local_filename(create
=False)
1757 assert filename
is not None
1758 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1759 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1760 (base
, ext
) = os
.path
.splitext(filename
)
1761 if not destfile
.endswith(ext
):
1765 shutil
.copyfile(filename
, destfile
)
1766 util
.bluetooth_send_file(destfile
)
1768 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1769 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1771 util
.delete_file(destfile
)
1773 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1775 def get_device_name(self
):
1776 if self
.config
.device_type
== 'ipod':
1778 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1779 return _('MP3 player')
1781 return '(unknown device)'
1783 def _treeview_button_released(self
, treeview
, event
):
1784 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1785 dy
= int(abs(event
.y
-ypos
))
1786 dx
= int(event
.x
-xpos
)
1788 selection
= treeview
.get_selection()
1789 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1790 if path
is None or dy
> 30:
1791 return (False, dx
, dy
)
1793 path
, column
, x
, y
= path
1794 selection
.select_path(path
)
1795 treeview
.set_cursor(path
)
1796 treeview
.grab_focus()
1798 return (True, dx
, dy
)
1800 def treeview_channels_handle_gestures(self
, treeview
, event
):
1801 if self
.currently_updating
:
1804 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1807 if self
.config
.maemo_enable_gestures
:
1809 self
.on_itemUpdateChannel_activate()
1811 self
.on_itemEditChannel_activate(treeview
)
1815 def treeview_available_handle_gestures(self
, treeview
, event
):
1816 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1819 if self
.config
.maemo_enable_gestures
:
1821 self
.on_playback_selected_episodes(None)
1824 self
.on_shownotes_selected_episodes(None)
1827 # Pass the event to the context menu handler for treeAvailable
1828 self
.treeview_available_show_context_menu(treeview
, event
)
1832 def treeview_available_show_context_menu(self
, treeview
, event
):
1833 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1835 if not hasattr(treeview
, 'is_rubber_banding_active'):
1838 return not treeview
.is_rubber_banding_active()
1840 if event
.button
== self
.context_menu_mouse_button
:
1841 episodes
= self
.get_selected_episodes()
1842 any_locked
= any(e
.is_locked
for e
in episodes
)
1843 any_played
= any(e
.is_played
for e
in episodes
)
1844 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1845 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1846 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1850 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1852 if open_instead_of_play
:
1853 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1855 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1857 item
= gtk
.ImageMenuItem(_('Stream'))
1858 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1860 item
.set_sensitive(can_play
and not downloading
)
1861 item
.connect('activate', self
.on_playback_selected_episodes
)
1862 menu
.append(self
.set_finger_friendly(item
))
1865 item
= gtk
.ImageMenuItem(_('Download'))
1866 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1867 item
.set_sensitive(can_download
)
1868 item
.connect('activate', self
.on_download_selected_episodes
)
1869 menu
.append(self
.set_finger_friendly(item
))
1871 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1872 item
.connect('activate', self
.on_item_cancel_download_activate
)
1873 menu
.append(self
.set_finger_friendly(item
))
1875 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1876 item
.set_sensitive(can_delete
)
1877 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1878 menu
.append(self
.set_finger_friendly(item
))
1882 # Ok, this probably makes sense to only display for downloaded files
1884 menu
.append(gtk
.SeparatorMenuItem())
1885 share_item
= gtk
.MenuItem(_('Send to'))
1886 menu
.append(share_item
)
1887 share_menu
= gtk
.Menu()
1889 item
= gtk
.ImageMenuItem(_('Local folder'))
1890 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
1891 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
1892 share_menu
.append(self
.set_finger_friendly(item
))
1893 if self
.bluetooth_available
:
1894 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
1895 item
.set_image(gtk
.image_new_from_icon_name(ICON('bluetooth'), gtk
.ICON_SIZE_MENU
))
1896 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
1897 share_menu
.append(self
.set_finger_friendly(item
))
1899 item
= gtk
.ImageMenuItem(self
.get_device_name())
1900 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
1901 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
1902 share_menu
.append(self
.set_finger_friendly(item
))
1904 share_item
.set_submenu(share_menu
)
1906 if (downloaded
or one_is_new
or can_download
) and not downloading
:
1907 menu
.append(gtk
.SeparatorMenuItem())
1909 item
= gtk
.CheckMenuItem(_('New'))
1910 item
.set_active(True)
1911 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1912 menu
.append(self
.set_finger_friendly(item
))
1914 item
= gtk
.CheckMenuItem(_('New'))
1915 item
.set_active(False)
1916 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
1917 menu
.append(self
.set_finger_friendly(item
))
1920 item
= gtk
.CheckMenuItem(_('Played'))
1921 item
.set_active(any_played
)
1922 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
1923 menu
.append(self
.set_finger_friendly(item
))
1925 item
= gtk
.CheckMenuItem(_('Keep episode'))
1926 item
.set_active(any_locked
)
1927 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
1928 menu
.append(self
.set_finger_friendly(item
))
1930 menu
.append(gtk
.SeparatorMenuItem())
1931 # Single item, add episode information menu item
1932 item
= gtk
.ImageMenuItem(_('Episode details'))
1933 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1934 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
1935 menu
.append(self
.set_finger_friendly(item
))
1937 if gpodder
.ui
.maemo
:
1938 # Because we open the popup on left-click for Maemo,
1939 # we also include a non-action to close the menu
1940 menu
.append(gtk
.SeparatorMenuItem())
1941 item
= gtk
.ImageMenuItem(_('Close this menu'))
1942 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1943 menu
.append(self
.set_finger_friendly(item
))
1946 # Disable tooltips while we are showing the menu, so
1947 # the tooltip will not appear over the menu
1948 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
1949 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
1950 menu
.popup( None, None, None, event
.button
, event
.time
)
1954 def set_title(self
, new_title
):
1955 if not gpodder
.ui
.fremantle
:
1956 self
.default_title
= new_title
1957 self
.gPodder
.set_title(new_title
)
1959 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
1961 Updates the status icons in the episode list.
1963 If urls is given, it should be a list of URLs
1964 of episodes that should be updated.
1966 If urls is None, set ONE OF selected, all to
1967 True (the former updates just the selected
1968 episodes and the latter updates all episodes).
1970 additional_args
= (self
.episode_is_downloading
, \
1971 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
1972 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
1974 if urls
is not None:
1975 # We have a list of URLs to walk through
1976 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
1977 elif selected
and not all
:
1978 # We should update all selected episodes
1979 selection
= self
.treeAvailable
.get_selection()
1980 model
, paths
= selection
.get_selected_rows()
1981 for path
in reversed(paths
):
1982 iter = model
.get_iter(path
)
1983 self
.episode_list_model
.update_by_filter_iter(iter, \
1985 elif all
and not selected
:
1986 # We update all (even the filter-hidden) episodes
1987 self
.episode_list_model
.update_all(*additional_args
)
1989 # Wrong/invalid call - have to specify at least one parameter
1990 raise ValueError('Invalid call to update_episode_list_icons')
1992 def episode_list_status_changed(self
, episodes
):
1993 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
1994 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
1997 def clean_up_downloads(self
, delete_partial
=False):
1998 # Clean up temporary files left behind by old gPodder versions
1999 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2002 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2004 for tempfile
in temporary_files
:
2005 util
.delete_file(tempfile
)
2007 # Clean up empty download folders and abandoned download folders
2008 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2009 for ddir
in download_dirs
:
2010 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2011 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2012 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2013 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2014 shutil
.rmtree(ddir
, ignore_errors
=True)
2016 def streaming_possible(self
):
2017 if gpodder
.ui
.desktop
:
2018 # User has to have a media player set on the Desktop, or else we
2019 # would probably open the browser when giving a URL to xdg-open..
2020 return (self
.config
.player
and self
.config
.player
!= 'default')
2021 elif gpodder
.ui
.maemo
:
2022 # On Maemo, the default is to use the Nokia Media Player, which is
2023 # already able to deal with HTTP URLs the right way, so we
2024 # unconditionally enable streaming always on Maemo
2029 def playback_episodes_for_real(self
, episodes
):
2030 groups
= collections
.defaultdict(list)
2031 for episode
in episodes
:
2032 file_type
= episode
.file_type()
2033 if file_type
== 'video' and self
.config
.videoplayer
and \
2034 self
.config
.videoplayer
!= 'default':
2035 player
= self
.config
.videoplayer
2036 if gpodder
.ui
.diablo
:
2037 # Use the wrapper script if it's installed to crop 3GP YouTube
2038 # videos to fit the screen (looks much nicer than w/ black border)
2039 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2040 player
= 'gpodder-mplayer'
2041 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2042 player
= 'mplayer -fs %F'
2043 elif file_type
== 'audio' and self
.config
.player
and \
2044 self
.config
.player
!= 'default':
2045 player
= self
.config
.player
2049 if file_type
not in ('audio', 'video') or \
2050 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2051 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2052 # Mark episode as played in the database
2053 episode
.mark(is_played
=True)
2054 self
.mygpo_client
.on_playback([episode
])
2056 filename
= episode
.local_filename(create
=False)
2057 if filename
is None or not os
.path
.exists(filename
):
2058 filename
= episode
.url
2059 if youtube
.is_video_link(filename
):
2060 fmt_id
= self
.config
.youtube_preferred_fmt_id
2061 if gpodder
.ui
.fremantle
:
2063 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2064 groups
[player
].append(filename
)
2066 # Open episodes with system default player
2067 if 'default' in groups
:
2068 for filename
in groups
['default']:
2069 log('Opening with system default: %s', filename
, sender
=self
)
2070 util
.gui_open(filename
)
2071 del groups
['default']
2072 elif gpodder
.ui
.maemo
:
2073 # When on Maemo and not opening with default, show a notification
2074 # (no startup notification for Panucci / MPlayer yet...)
2075 if len(episodes
) == 1:
2076 text
= _('Opening %s') % episodes
[0].title
2078 count
= len(episodes
)
2079 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2081 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2083 def destroy_banner_later(banner
):
2086 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2088 # For each type now, go and create play commands
2089 for group
in groups
:
2090 for command
in util
.format_desktop_command(group
, groups
[group
]):
2091 log('Executing: %s', repr(command
), sender
=self
)
2092 subprocess
.Popen(command
)
2094 # Persist episode status changes to the database
2097 # Flush updated episode status
2098 self
.mygpo_client
.flush()
2100 def playback_episodes(self
, episodes
):
2101 # We need to create a list, because we run through it more than once
2102 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2103 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2106 self
.playback_episodes_for_real(episodes
)
2107 except Exception, e
:
2108 log('Error in playback!', sender
=self
, traceback
=True)
2109 if gpodder
.ui
.desktop
:
2110 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2111 _('Error opening player'), widget
=self
.toolPreferences
)
2113 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2115 channel_urls
= set()
2116 episode_urls
= set()
2117 for episode
in episodes
:
2118 channel_urls
.add(episode
.channel
.url
)
2119 episode_urls
.add(episode
.url
)
2120 self
.update_episode_list_icons(episode_urls
)
2121 self
.update_podcast_list_model(channel_urls
)
2123 def play_or_download(self
):
2124 if not gpodder
.ui
.fremantle
:
2125 if self
.wNotebook
.get_current_page() > 0:
2126 if gpodder
.ui
.desktop
:
2127 self
.toolCancel
.set_sensitive(True)
2130 if self
.currently_updating
:
2131 return (False, False, False, False, False, False)
2133 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2134 ( is_played
, is_locked
) = (False,)*2
2136 open_instead_of_play
= False
2138 selection
= self
.treeAvailable
.get_selection()
2139 if selection
.count_selected_rows() > 0:
2140 (model
, paths
) = selection
.get_selected_rows()
2144 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2145 except TypeError, te
:
2146 log('Invalid episode at path %s', str(path
), sender
=self
)
2149 if episode
.file_type() not in ('audio', 'video'):
2150 open_instead_of_play
= True
2152 if episode
.was_downloaded():
2153 can_play
= episode
.was_downloaded(and_exists
=True)
2154 is_played
= episode
.is_played
2155 is_locked
= episode
.is_locked
2159 if self
.episode_is_downloading(episode
):
2164 can_download
= can_download
and not can_cancel
2165 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2166 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2167 can_delete
= not can_cancel
2169 if gpodder
.ui
.desktop
:
2170 if open_instead_of_play
:
2171 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2173 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2174 self
.toolPlay
.set_sensitive( can_play
)
2175 self
.toolDownload
.set_sensitive( can_download
)
2176 self
.toolTransfer
.set_sensitive( can_transfer
)
2177 self
.toolCancel
.set_sensitive( can_cancel
)
2179 if not gpodder
.ui
.fremantle
:
2180 self
.item_cancel_download
.set_sensitive(can_cancel
)
2181 self
.itemDownloadSelected
.set_sensitive(can_download
)
2182 self
.itemOpenSelected
.set_sensitive(can_play
)
2183 self
.itemPlaySelected
.set_sensitive(can_play
)
2184 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2185 self
.item_toggle_played
.set_sensitive(can_play
)
2186 self
.item_toggle_lock
.set_sensitive(can_play
)
2187 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2188 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2190 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2192 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2193 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2195 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2196 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2198 def episode_new_status_changed(self
, urls
):
2199 self
.update_podcast_list_model()
2200 self
.update_episode_list_icons(urls
)
2202 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2203 """Update the podcast list treeview model
2205 If urls is given, it should list the URLs of each
2206 podcast that has to be updated in the list.
2208 If selected is True, only update the model contents
2209 for the currently-selected podcast - nothing more.
2211 The caller can optionally specify "select_url",
2212 which is the URL of the podcast that is to be
2213 selected in the list after the update is complete.
2214 This only works if the podcast list has to be
2215 reloaded; i.e. something has been added or removed
2216 since the last update of the podcast list).
2218 selection
= self
.treeChannels
.get_selection()
2219 model
, iter = selection
.get_selected()
2221 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2222 # Update "all episodes" view in any case (if enabled)
2223 self
.podcast_list_model
.update_first_row()
2226 # very cheap! only update selected channel
2227 if iter is not None:
2228 # If we have selected the "all episodes" view, we have
2229 # to update all channels for selected episodes:
2230 if self
.config
.podcast_list_view_all
and \
2231 self
.podcast_list_model
.iter_is_first_row(iter):
2232 urls
= self
.get_podcast_urls_from_selected_episodes()
2233 self
.podcast_list_model
.update_by_urls(urls
)
2235 # Otherwise just update the selected row (a podcast)
2236 self
.podcast_list_model
.update_by_filter_iter(iter)
2237 elif not self
.channel_list_changed
:
2238 # we can keep the model, but have to update some
2240 # still cheaper than reloading the whole list
2241 self
.podcast_list_model
.update_all()
2243 # ok, we got a bunch of urls to update
2244 self
.podcast_list_model
.update_by_urls(urls
)
2246 if model
and iter and select_url
is None:
2247 # Get the URL of the currently-selected podcast
2248 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2250 # Update the podcast list model with new channels
2251 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2254 selected_iter
= model
.get_iter_first()
2255 # Find the previously-selected URL in the new
2256 # model if we have an URL (else select first)
2257 if select_url
is not None:
2258 pos
= model
.get_iter_first()
2259 while pos
is not None:
2260 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2261 if url
== select_url
:
2264 pos
= model
.iter_next(pos
)
2266 if not gpodder
.ui
.fremantle
:
2267 if selected_iter
is not None:
2268 selection
.select_iter(selected_iter
)
2269 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2271 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2272 self
.channel_list_changed
= False
2274 def episode_is_downloading(self
, episode
):
2275 """Returns True if the given episode is being downloaded at the moment"""
2279 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2281 def update_episode_list_model(self
):
2282 if self
.channels
and self
.active_channel
is not None:
2283 if gpodder
.ui
.fremantle
:
2284 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2286 self
.currently_updating
= True
2287 self
.episode_list_model
.clear()
2288 self
.episode_list_model
.reset_update_progress()
2289 self
.treeAvailable
.set_model(self
.empty_episode_list_model
)
2290 def do_update_episode_list_model():
2291 additional_args
= (self
.episode_is_downloading
, \
2292 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2293 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
, \
2295 self
.episode_list_model
.add_from_channel(self
.active_channel
, *additional_args
)
2297 def on_episode_list_model_updated():
2298 if gpodder
.ui
.fremantle
:
2299 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2300 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
2301 self
.treeAvailable
.columns_autosize()
2302 self
.currently_updating
= False
2303 self
.play_or_download()
2304 util
.idle_add(on_episode_list_model_updated
)
2305 threading
.Thread(target
=do_update_episode_list_model
).start()
2307 self
.episode_list_model
.clear()
2309 def offer_new_episodes(self
, channels
=None):
2310 new_episodes
= self
.get_new_episodes(channels
)
2312 self
.new_episodes_show(new_episodes
)
2316 def add_podcast_list(self
, urls
, auth_tokens
=None):
2317 """Subscribe to a list of podcast given their URLs
2319 If auth_tokens is given, it should be a dictionary
2320 mapping URLs to (username, password) tuples."""
2322 if auth_tokens
is None:
2325 # Sort and split the URL list into five buckets
2326 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2327 for input_url
in urls
:
2328 url
= util
.normalize_feed_url(input_url
)
2330 # Fail this one because the URL is not valid
2331 failed
.append(input_url
)
2332 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2333 # A podcast already exists in the list for this URL
2334 existing
.append(url
)
2336 # This URL has survived the first round - queue for add
2338 if url
!= input_url
and input_url
in auth_tokens
:
2339 auth_tokens
[url
] = auth_tokens
[input_url
]
2344 progress
= ProgressIndicator(_('Adding podcasts'), \
2345 _('Please wait while episode information is downloaded.'), \
2346 parent
=self
.get_dialog_parent())
2348 def on_after_update():
2349 progress
.on_finished()
2350 # Report already-existing subscriptions to the user
2352 title
= _('Existing subscriptions skipped')
2353 message
= _('You are already subscribed to these podcasts:') \
2354 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2355 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2357 # Report subscriptions that require authentication
2361 title
= _('Podcast requires authentication')
2362 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2363 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2365 retry_podcasts
[url
] = auth_tokens
2367 # Stop asking the user for more login data
2370 error_messages
[url
] = _('Authentication failed')
2374 # If we have authentication data to retry, do so here
2376 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2378 # Report website redirections
2379 for url
in redirections
:
2380 title
= _('Website redirection detected')
2381 message
= _('The URL %(url)s redirects to %(target)s.') \
2382 + '\n\n' + _('Do you want to visit the website now?')
2383 message
= message
% {'url': url
, 'target': redirections
[url
]}
2384 if self
.show_confirmation(message
, title
):
2385 util
.open_website(url
)
2389 # Report failed subscriptions to the user
2391 title
= _('Could not add some podcasts')
2392 message
= _('Some podcasts could not be added to your list:') \
2393 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2394 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2395 self
.show_message(message
, title
, important
=True)
2397 # Upload subscription changes to gpodder.net
2398 self
.mygpo_client
.on_subscribe(worked
)
2400 # If at least one podcast has been added, save and update all
2401 if self
.channel_list_changed
:
2402 # Fix URLs if mygpo has rewritten them
2403 self
.rewrite_urls_mygpo()
2405 self
.save_channels_opml()
2407 # If only one podcast was added, select it after the update
2408 if len(worked
) == 1:
2413 # Update the list of subscribed podcasts
2414 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2415 self
.update_podcasts_tab()
2417 # Offer to download new episodes
2418 self
.offer_new_episodes(channels
=[c
for c
in self
.channels
if c
.url
in worked
])
2421 # After the initial sorting and splitting, try all queued podcasts
2422 length
= len(queued
)
2423 for index
, url
in enumerate(queued
):
2424 progress
.on_progress(float(index
)/float(length
))
2425 progress
.on_message(url
)
2426 log('QUEUE RUNNER: %s', url
, sender
=self
)
2428 # The URL is valid and does not exist already - subscribe!
2429 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2430 authentication_tokens
=auth_tokens
.get(url
, None), \
2431 max_episodes
=self
.config
.max_episodes_per_feed
, \
2432 download_dir
=self
.config
.download_dir
, \
2433 allow_empty_feeds
=self
.config
.allow_empty_feeds
)
2436 username
, password
= util
.username_password_from_url(url
)
2437 except ValueError, ve
:
2438 username
, password
= (None, None)
2440 if username
is not None and channel
.username
is None and \
2441 password
is not None and channel
.password
is None:
2442 channel
.username
= username
2443 channel
.password
= password
2446 self
._update
_cover
(channel
)
2447 except feedcore
.AuthenticationRequired
:
2448 if url
in auth_tokens
:
2449 # Fail for wrong authentication data
2450 error_messages
[url
] = _('Authentication failed')
2453 # Queue for login dialog later
2456 except feedcore
.WifiLogin
, error
:
2457 redirections
[url
] = error
.data
2459 error_messages
[url
] = _('Redirection detected')
2461 except Exception, e
:
2462 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2463 error_messages
[url
] = str(e
)
2467 assert channel
is not None
2468 worked
.append(channel
.url
)
2469 self
.channels
.append(channel
)
2470 self
.channel_list_changed
= True
2471 util
.idle_add(on_after_update
)
2472 threading
.Thread(target
=thread_proc
).start()
2474 def save_channels_opml(self
):
2475 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2476 return exporter
.write(self
.channels
)
2478 def find_episode(self
, podcast_url
, episode_url
):
2479 """Find an episode given its podcast and episode URL
2481 The function will return a PodcastEpisode object if
2482 the episode is found, or None if it's not found.
2484 for podcast
in self
.channels
:
2485 if podcast_url
== podcast
.url
:
2486 for episode
in podcast
.get_all_episodes():
2487 if episode_url
== episode
.url
:
2492 def process_received_episode_actions(self
, updated_urls
):
2493 """Process/merge episode actions from gpodder.net
2495 This function will merge all changes received from
2496 the server to the local database and update the
2497 status of the affected episodes as necessary.
2499 indicator
= ProgressIndicator(_('Merging episode actions'), \
2500 _('Episode actions from gpodder.net are merged.'), \
2501 False, self
.get_dialog_parent())
2503 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2504 if action
.action
== 'play':
2505 episode
= self
.find_episode(action
.podcast_url
, \
2508 if episode
is not None:
2509 log('Play action for %s', episode
.url
, sender
=self
)
2510 episode
.mark(is_played
=True)
2512 if action
.timestamp
> episode
.current_position_updated
:
2513 log('Updating position for %s', episode
.url
, sender
=self
)
2514 episode
.current_position
= action
.position
2515 episode
.current_position_updated
= action
.timestamp
2518 log('Updating total time for %s', episode
.url
, sender
=self
)
2519 episode
.total_time
= action
.total
2522 elif action
.action
== 'delete':
2523 episode
= self
.find_episode(action
.podcast_url
, \
2526 if episode
is not None:
2527 if not episode
.was_downloaded(and_exists
=True):
2528 # Set the episode to a "deleted" state
2529 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2530 episode
.delete_from_disk()
2533 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2534 gtk
.main_iteration(False)
2536 indicator
.on_finished()
2540 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2542 self
.updating_feed_cache
= False
2544 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2546 # Process received episode actions for all updated URLs
2547 self
.process_received_episode_actions(updated_urls
)
2549 self
.channel_list_changed
= True
2550 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2552 # Only search for new episodes in podcasts that have been
2553 # updated, not in other podcasts (for single-feed updates)
2554 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2556 if gpodder
.ui
.fremantle
:
2557 self
.button_subscribe
.set_sensitive(True)
2558 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2559 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
2560 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2561 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2562 self
.update_podcasts_tab()
2563 self
.update_episode_list_model()
2564 if self
.feed_cache_update_cancelled
:
2568 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2569 # New episodes found, but we should do nothing
2570 self
.show_message(_('New episodes are available.'))
2571 elif self
.config
.auto_download
== 'always':
2572 count
= len(episodes
)
2573 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2574 self
.show_message(title
)
2575 self
.download_episode_list(episodes
)
2576 elif self
.config
.auto_download
== 'queue':
2577 self
.show_message(_('New episodes have been added to the download list.'))
2578 self
.download_episode_list_paused(episodes
)
2580 self
.new_episodes_show(episodes
)
2581 elif not self
.config
.auto_update_feeds
:
2582 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2586 self
.tray_icon
.set_status()
2588 if self
.feed_cache_update_cancelled
:
2589 # The user decided to abort the feed update
2590 self
.show_update_feeds_buttons()
2592 # Nothing new here - but inform the user
2593 self
.pbFeedUpdate
.set_fraction(1.0)
2594 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2595 self
.feed_cache_update_cancelled
= True
2596 self
.btnCancelFeedUpdate
.show()
2597 self
.btnCancelFeedUpdate
.set_sensitive(True)
2598 if gpodder
.ui
.maemo
:
2599 # btnCancelFeedUpdate is a ToolButton on Maemo
2600 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2602 # btnCancelFeedUpdate is a normal gtk.Button
2603 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2605 count
= len(episodes
)
2606 # New episodes are available
2607 self
.pbFeedUpdate
.set_fraction(1.0)
2608 # Are we minimized and should we auto download?
2609 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2610 self
.download_episode_list(episodes
)
2611 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2612 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2613 self
.show_update_feeds_buttons()
2614 elif self
.config
.auto_download
== 'queue':
2615 self
.download_episode_list_paused(episodes
)
2616 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2617 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2618 self
.show_update_feeds_buttons()
2620 self
.show_update_feeds_buttons()
2621 # New episodes are available and we are not minimized
2622 if not self
.config
.do_not_show_new_episodes_dialog
:
2623 self
.new_episodes_show(episodes
, notification
=True)
2625 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2626 self
.pbFeedUpdate
.set_text(message
)
2628 def _update_cover(self
, channel
):
2629 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2630 self
.cover_downloader
.request_cover(channel
)
2632 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2633 total
= len(channels
)
2635 for updated
, channel
in enumerate(channels
):
2636 if not self
.feed_cache_update_cancelled
:
2638 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
)
2639 self
._update
_cover
(channel
)
2640 except Exception, e
:
2641 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2643 message
= _('Error while updating %(url)s: %(message)s')
2645 message
= _('The feed at %(url)s could not be updated.')
2646 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2647 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2649 if self
.feed_cache_update_cancelled
:
2652 if gpodder
.ui
.fremantle
:
2653 util
.idle_add(self
.button_refresh
.set_title
, \
2654 _('%(position)d/%(total)d updated') % {'position': updated
, 'total': total
})
2657 # By the time we get here the update may have already been cancelled
2658 if not self
.feed_cache_update_cancelled
:
2659 def update_progress():
2660 d
= {'podcast': channel
.title
, 'position': updated
, 'total': total
}
2661 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2662 self
.pbFeedUpdate
.set_text(progression
)
2664 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2665 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2666 util
.idle_add(update_progress
)
2668 updated_urls
= [c
.url
for c
in channels
]
2669 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2671 def show_update_feeds_buttons(self
):
2672 # Make sure that the buttons for updating feeds
2673 # appear - this should happen after a feed update
2674 if gpodder
.ui
.maemo
:
2675 self
.btnUpdateSelectedFeed
.show()
2676 self
.toolFeedUpdateProgress
.hide()
2677 self
.btnCancelFeedUpdate
.hide()
2678 self
.btnCancelFeedUpdate
.set_is_important(False)
2679 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2680 self
.toolbarSpacer
.set_expand(True)
2681 self
.toolbarSpacer
.set_draw(False)
2683 self
.hboxUpdateFeeds
.hide()
2684 self
.btnUpdateFeeds
.show()
2685 self
.itemUpdate
.set_sensitive(True)
2686 self
.itemUpdateChannel
.set_sensitive(True)
2688 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2689 if not self
.feed_cache_update_cancelled
:
2690 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2691 self
.feed_cache_update_cancelled
= True
2692 self
.btnCancelFeedUpdate
.set_sensitive(False)
2694 self
.show_update_feeds_buttons()
2696 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2697 if self
.updating_feed_cache
:
2698 if gpodder
.ui
.fremantle
:
2699 self
.feed_cache_update_cancelled
= True
2702 if not force_update
:
2703 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2704 self
.channel_list_changed
= True
2705 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2708 # Fix URLs if mygpo has rewritten them
2709 self
.rewrite_urls_mygpo()
2711 self
.updating_feed_cache
= True
2713 if channels
is None:
2714 channels
= self
.channels
2716 if gpodder
.ui
.fremantle
:
2717 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2718 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2719 self
.button_refresh
.set_title(_('Updating...'))
2720 self
.button_subscribe
.set_sensitive(False)
2721 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2722 self
.ICON_GENERAL_CLOSE
, gtk
.ICON_SIZE_BUTTON
))
2723 self
.feed_cache_update_cancelled
= False
2725 self
.itemUpdate
.set_sensitive(False)
2726 self
.itemUpdateChannel
.set_sensitive(False)
2729 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2731 if len(channels
) == 1:
2732 text
= _('Updating "%s"...') % channels
[0].title
2734 count
= len(channels
)
2735 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2736 self
.pbFeedUpdate
.set_text(text
)
2737 self
.pbFeedUpdate
.set_fraction(0)
2739 self
.feed_cache_update_cancelled
= False
2740 self
.btnCancelFeedUpdate
.show()
2741 self
.btnCancelFeedUpdate
.set_sensitive(True)
2742 if gpodder
.ui
.maemo
:
2743 self
.toolbarSpacer
.set_expand(False)
2744 self
.toolbarSpacer
.set_draw(True)
2745 self
.btnUpdateSelectedFeed
.hide()
2746 self
.toolFeedUpdateProgress
.show_all()
2748 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2749 self
.hboxUpdateFeeds
.show_all()
2750 self
.btnUpdateFeeds
.hide()
2752 args
= (channels
, select_url_afterwards
)
2753 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2755 def on_gPodder_delete_event(self
, widget
, *args
):
2756 """Called when the GUI wants to close the window
2757 Displays a confirmation dialog (and closes/hides gPodder)
2760 downloading
= self
.download_status_model
.are_downloads_in_progress()
2762 # Only iconify if we are using the window's "X" button,
2763 # but not when we are using "Quit" in the menu or toolbar
2764 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
2765 self
.iconify_main_window()
2766 elif self
.config
.on_quit_ask
or downloading
:
2767 if gpodder
.ui
.fremantle
:
2768 self
.close_gpodder()
2769 elif gpodder
.ui
.diablo
:
2770 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2772 self
.close_gpodder()
2775 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2776 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2777 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2779 title
= _('Quit gPodder')
2781 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2783 message
= _('Do you really want to quit gPodder now?')
2785 dialog
.set_title(title
)
2786 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2788 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
2789 dialog
.vbox
.pack_start(cb_ask
)
2792 quit_button
.grab_focus()
2793 result
= dialog
.run()
2796 if result
== gtk
.RESPONSE_CLOSE
:
2797 if not downloading
and cb_ask
.get_active() == True:
2798 self
.config
.on_quit_ask
= False
2799 self
.close_gpodder()
2801 self
.close_gpodder()
2805 def close_gpodder(self
):
2806 """ clean everything and exit properly
2809 if self
.save_channels_opml():
2810 pass # FIXME: Add mygpo synchronization here
2812 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
2816 if self
.tray_icon
is not None:
2817 self
.tray_icon
.set_visible(False)
2819 # Notify all tasks to to carry out any clean-up actions
2820 self
.download_status_model
.tell_all_tasks_to_quit()
2822 while gtk
.events_pending():
2823 gtk
.main_iteration(False)
2830 def get_expired_episodes(self
):
2831 for channel
in self
.channels
:
2832 for episode
in channel
.get_downloaded_episodes():
2833 # Never consider locked episodes as old
2834 if episode
.is_locked
:
2837 # Never consider fresh episodes as old
2838 if episode
.age_in_days() < self
.config
.episode_old_age
:
2841 # Do not delete played episodes (except if configured)
2842 if episode
.is_played
:
2843 if not self
.config
.auto_remove_played_episodes
:
2846 # Do not delete unplayed episodes (except if configured)
2847 if not episode
.is_played
:
2848 if not self
.config
.auto_remove_unplayed_episodes
:
2853 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
2858 episodes
= [e
for e
in episodes
if not e
.is_locked
]
2861 title
= _('Episodes are locked')
2862 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2863 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2866 count
= len(episodes
)
2867 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
2868 message
= _('Deleting episodes removes downloaded files.')
2870 if gpodder
.ui
.fremantle
:
2871 message
= '\n'.join([title
, message
])
2873 if confirm
and not self
.show_confirmation(message
, title
):
2876 progress
= ProgressIndicator(_('Deleting episodes'), \
2877 _('Please wait while episodes are deleted'), \
2878 parent
=self
.get_dialog_parent())
2880 def finish_deletion(episode_urls
, channel_urls
):
2881 progress
.on_finished()
2883 # Episodes have been deleted - persist the database
2886 self
.update_episode_list_icons(episode_urls
)
2887 self
.update_podcast_list_model(channel_urls
)
2888 self
.play_or_download()
2891 episode_urls
= set()
2892 channel_urls
= set()
2894 episodes_status_update
= []
2895 for idx
, episode
in enumerate(episodes
):
2896 progress
.on_progress(float(idx
)/float(len(episodes
)))
2897 if episode
.is_locked
and skip_locked
:
2898 log('Not deleting episode (is locked): %s', episode
.title
)
2900 log('Deleting episode: %s', episode
.title
)
2901 progress
.on_message(episode
.title
)
2902 episode
.delete_from_disk()
2903 episode_urls
.add(episode
.url
)
2904 channel_urls
.add(episode
.channel
.url
)
2905 episodes_status_update
.append(episode
)
2907 # Tell the shownotes window that we have removed the episode
2908 if self
.episode_shownotes_window
is not None and \
2909 self
.episode_shownotes_window
.episode
is not None and \
2910 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
2911 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
2913 # Notify the web service about the status update + upload
2914 self
.mygpo_client
.on_delete(episodes_status_update
)
2915 self
.mygpo_client
.flush()
2917 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
2919 threading
.Thread(target
=thread_proc
).start()
2923 def on_itemRemoveOldEpisodes_activate( self
, widget
):
2924 if gpodder
.ui
.maemo
:
2926 ('maemo_remove_markup', None, None, _('Episode')),
2930 ('title_markup', None, None, _('Episode')),
2931 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2932 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2933 ('played_prop', None, None, _('Status')),
2934 ('age_prop', None, None, _('Downloaded')),
2937 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
2938 selection_buttons
= {
2939 _('Select played'): lambda episode
: episode
.is_played
,
2940 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
2943 instructions
= _('Select the episodes you want to delete:')
2947 for channel
in self
.channels
:
2948 for episode
in channel
.get_downloaded_episodes():
2949 # Disallow deletion of locked episodes that still exist
2950 if not episode
.is_locked
or not episode
.file_exists():
2951 episodes
.append(episode
)
2952 # Automatically select played and file-less episodes
2953 selected
.append(episode
.is_played
or \
2954 not episode
.file_exists())
2956 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
2957 episodes
= episodes
, selected
= selected
, columns
= columns
, \
2958 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
2959 selection_buttons
= selection_buttons
, _config
=self
.config
, \
2960 show_episode_shownotes
=self
.show_episode_shownotes
)
2962 def on_selected_episodes_status_changed(self
):
2963 self
.update_episode_list_icons(selected
=True)
2964 self
.update_podcast_list_model(selected
=True)
2967 def mark_selected_episodes_new(self
):
2968 for episode
in self
.get_selected_episodes():
2970 self
.on_selected_episodes_status_changed()
2972 def mark_selected_episodes_old(self
):
2973 for episode
in self
.get_selected_episodes():
2975 self
.on_selected_episodes_status_changed()
2977 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
2978 for episode
in self
.get_selected_episodes():
2980 episode
.mark(is_played
=not episode
.is_played
)
2982 episode
.mark(is_played
=new_value
)
2983 self
.on_selected_episodes_status_changed()
2985 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2986 for episode
in self
.get_selected_episodes():
2988 episode
.mark(is_locked
=not episode
.is_locked
)
2990 episode
.mark(is_locked
=new_value
)
2991 self
.on_selected_episodes_status_changed()
2993 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2994 if self
.active_channel
is None:
2997 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
2998 self
.active_channel
.update_channel_lock()
3000 for episode
in self
.active_channel
.get_all_episodes():
3001 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3003 self
.update_podcast_list_model(selected
=True)
3004 self
.update_episode_list_icons(all
=True)
3006 def on_itemUpdateChannel_activate(self
, widget
=None):
3007 if self
.active_channel
is None:
3008 title
= _('No podcast selected')
3009 message
= _('Please select a podcast in the podcasts list to update.')
3010 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3013 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3014 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3015 self
.update_feed_cache()
3017 self
.update_feed_cache(channels
=[self
.active_channel
])
3019 def on_itemUpdate_activate(self
, widget
=None):
3020 # Check if we have outstanding subscribe/unsubscribe actions
3021 if self
.on_add_remove_podcasts_mygpo():
3022 log('Update cancelled (received server changes)', sender
=self
)
3026 self
.update_feed_cache()
3028 gPodderWelcome(self
.gPodder
,
3029 center_on_widget
=self
.gPodder
,
3030 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3031 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3033 def download_episode_list_paused(self
, episodes
):
3034 self
.download_episode_list(episodes
, True)
3036 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3037 enable_update
= False
3039 for episode
in episodes
:
3040 log('Downloading episode: %s', episode
.title
, sender
= self
)
3041 if not episode
.was_downloaded(and_exists
=True):
3043 for task
in self
.download_tasks_seen
:
3044 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3045 self
.download_queue_manager
.add_task(task
, force_start
)
3046 enable_update
= True
3054 task
= download
.DownloadTask(episode
, self
.config
)
3055 except Exception, e
:
3056 d
= {'episode': episode
.title
, 'message': str(e
)}
3057 message
= _('Download error while downloading %(episode)s: %(message)s')
3058 self
.show_message(message
% d
, _('Download error'), important
=True)
3059 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3063 task
.status
= task
.PAUSED
3065 self
.mygpo_client
.on_download([task
.episode
])
3066 self
.download_queue_manager
.add_task(task
, force_start
)
3068 self
.download_status_model
.register_task(task
)
3069 enable_update
= True
3072 self
.enable_download_list_update()
3074 # Flush updated episode status
3075 self
.mygpo_client
.flush()
3077 def cancel_task_list(self
, tasks
):
3082 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3083 task
.status
= task
.CANCELLED
3084 elif task
.status
== task
.PAUSED
:
3085 task
.status
= task
.CANCELLED
3086 # Call run, so the partial file gets deleted
3089 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3090 self
.play_or_download()
3092 # Update the tab title and downloads list
3093 self
.update_downloads_list()
3095 def new_episodes_show(self
, episodes
, notification
=False):
3096 if gpodder
.ui
.maemo
:
3098 ('maemo_markup', None, None, _('Episode')),
3100 show_notification
= notification
3103 ('title_markup', None, None, _('Episode')),
3104 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3105 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3107 show_notification
= False
3109 instructions
= _('Select the episodes you want to download:')
3111 if self
.new_episodes_window
is not None:
3112 self
.new_episodes_window
.main_window
.destroy()
3113 self
.new_episodes_window
= None
3115 def download_episodes_callback(episodes
):
3116 self
.new_episodes_window
= None
3117 self
.download_episode_list(episodes
)
3119 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3120 title
=_('New episodes available'), \
3121 instructions
=instructions
, \
3122 episodes
=episodes
, \
3124 selected_default
=True, \
3125 stock_ok_button
= 'gpodder-download', \
3126 callback
=download_episodes_callback
, \
3127 remove_callback
=lambda e
: e
.mark_old(), \
3128 remove_action
=_('Mark as old'), \
3129 remove_finished
=self
.episode_new_status_changed
, \
3130 _config
=self
.config
, \
3131 show_notification
=show_notification
, \
3132 show_episode_shownotes
=self
.show_episode_shownotes
)
3134 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3135 if not self
.offer_new_episodes():
3136 self
.show_message(_('Please check for new episodes later.'), \
3137 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3139 def get_new_episodes(self
, channels
=None):
3140 if channels
is None:
3141 channels
= self
.channels
3143 for channel
in channels
:
3144 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3145 episodes
.append(episode
)
3149 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3150 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3152 def commit_changes_to_database(self
):
3153 """This will be called after the sync process is finished"""
3156 def on_cleanup_ipod_activate(self
, widget
, *args
):
3157 self
.sync_ui
.on_cleanup_device()
3159 def on_manage_device_playlist(self
, widget
):
3160 self
.sync_ui
.on_manage_device_playlist()
3162 def show_hide_tray_icon(self
):
3163 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3164 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3165 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3166 self
.tray_icon
.set_visible(False)
3168 self
.tray_icon
= None
3170 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3171 self
.tray_icon
.set_visible(self
.is_iconified())
3172 elif self
.tray_icon
:
3173 self
.tray_icon
.set_visible(True)
3175 def on_itemShowAllEpisodes_activate(self
, widget
):
3176 self
.config
.podcast_list_view_all
= widget
.get_active()
3178 def on_itemShowToolbar_activate(self
, widget
):
3179 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3181 def on_itemShowDescription_activate(self
, widget
):
3182 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3184 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3185 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3186 if self
.config
.podcast_list_hide_boring
:
3187 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3189 self
.podcast_list_model
.set_view_mode(-1)
3191 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3193 if current
== self
.item_view_podcasts_all
:
3194 self
.podcast_list_model
.set_view_mode(-1)
3195 elif current
== self
.item_view_podcasts_downloaded
:
3196 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3197 elif current
== self
.item_view_podcasts_unplayed
:
3198 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3200 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3202 def on_item_view_episodes_changed(self
, radioaction
, current
):
3203 if current
== self
.item_view_episodes_all
:
3204 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3205 elif current
== self
.item_view_episodes_undeleted
:
3206 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3207 elif current
== self
.item_view_episodes_downloaded
:
3208 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3209 elif current
== self
.item_view_episodes_unplayed
:
3210 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3212 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3214 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3215 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3217 def update_item_device( self
):
3218 if not gpodder
.ui
.fremantle
:
3219 if self
.config
.device_type
!= 'none':
3220 self
.itemDevice
.set_visible(True)
3221 self
.itemDevice
.label
= self
.get_device_name()
3223 self
.itemDevice
.set_visible(False)
3225 def properties_closed( self
):
3226 self
.preferences_dialog
= None
3227 self
.show_hide_tray_icon()
3228 self
.update_item_device()
3229 if gpodder
.ui
.maemo
:
3230 selection
= self
.treeAvailable
.get_selection()
3231 if self
.config
.maemo_enable_gestures
or \
3232 self
.config
.enable_fingerscroll
:
3233 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3235 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3237 def on_itemPreferences_activate(self
, widget
, *args
):
3238 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3239 _config
=self
.config
, \
3240 callback_finished
=self
.properties_closed
, \
3241 user_apps_reader
=self
.user_apps_reader
, \
3242 parent_window
=self
.main_window
, \
3243 mygpo_client
=self
.mygpo_client
, \
3244 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3246 # Initial message to relayout window (in case it's opened in portrait mode
3247 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3249 def on_itemDependencies_activate(self
, widget
):
3250 gPodderDependencyManager(self
.gPodder
)
3252 def on_goto_mygpo(self
, widget
):
3253 self
.mygpo_client
.open_website()
3255 def on_download_subscriptions_from_mygpo(self
, action
=None):
3256 title
= _('Login to gpodder.net')
3257 message
= _('Please login to download your subscriptions.')
3258 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3259 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3263 self
.config
.mygpo_username
= username
3264 self
.config
.mygpo_password
= password
3266 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3267 custom_title
=_('Subscriptions on gpodder.net'), \
3268 add_urls_callback
=self
.add_podcast_list
, \
3269 hide_url_entry
=True)
3271 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3272 # we do not have to hardcode the URL here
3273 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3274 url
= util
.url_add_authentication(OPML_URL
, \
3275 self
.config
.mygpo_username
, \
3276 self
.config
.mygpo_password
)
3277 dir.download_opml_file(url
)
3279 def on_mygpo_settings_activate(self
, action
=None):
3280 # This dialog is only used for Maemo 4
3281 if not gpodder
.ui
.diablo
:
3284 settings
= MygPodderSettings(self
.main_window
, \
3285 config
=self
.config
, \
3286 mygpo_client
=self
.mygpo_client
, \
3287 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3289 def on_itemAddChannel_activate(self
, widget
=None):
3290 gPodderAddPodcast(self
.gPodder
, \
3291 add_urls_callback
=self
.add_podcast_list
)
3293 def on_itemEditChannel_activate(self
, widget
, *args
):
3294 if self
.active_channel
is None:
3295 title
= _('No podcast selected')
3296 message
= _('Please select a podcast in the podcasts list to edit.')
3297 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3300 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3301 gPodderChannel(self
.main_window
, \
3302 channel
=self
.active_channel
, \
3303 callback_closed
=callback_closed
, \
3304 cover_downloader
=self
.cover_downloader
)
3306 def on_itemMassUnsubscribe_activate(self
, item
=None):
3308 ('title', None, None, _('Podcast')),
3311 # We're abusing the Episode Selector for selecting Podcasts here,
3312 # but it works and looks good, so why not? -- thp
3313 gPodderEpisodeSelector(self
.main_window
, \
3314 title
=_('Remove podcasts'), \
3315 instructions
=_('Select the podcast you want to remove.'), \
3316 episodes
=self
.channels
, \
3318 size_attribute
=None, \
3319 stock_ok_button
=gtk
.STOCK_DELETE
, \
3320 callback
=self
.remove_podcast_list
, \
3321 _config
=self
.config
)
3323 def remove_podcast_list(self
, channels
, confirm
=True):
3325 log('No podcasts selected for deletion', sender
=self
)
3328 if len(channels
) == 1:
3329 title
= _('Removing podcast')
3330 info
= _('Please wait while the podcast is removed')
3331 message
= _('Do you really want to remove this podcast and its episodes?')
3333 title
= _('Removing podcasts')
3334 info
= _('Please wait while the podcasts are removed')
3335 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3337 if confirm
and not self
.show_confirmation(message
, title
):
3340 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3342 def finish_deletion(select_url
):
3343 # Upload subscription list changes to the web service
3344 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3346 # Re-load the channels and select the desired new channel
3347 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3348 progress
.on_finished()
3349 self
.update_podcasts_tab()
3354 for idx
, channel
in enumerate(channels
):
3355 # Update the UI for correct status messages
3356 progress
.on_progress(float(idx
)/float(len(channels
)))
3357 progress
.on_message(channel
.title
)
3359 # Delete downloaded episodes
3360 channel
.remove_downloaded()
3362 # cancel any active downloads from this channel
3363 for episode
in channel
.get_all_episodes():
3364 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3367 if len(channels
) == 1:
3368 # get the URL of the podcast we want to select next
3369 if channel
in self
.channels
:
3370 position
= self
.channels
.index(channel
)
3374 if position
== len(self
.channels
)-1:
3375 # this is the last podcast, so select the URL
3376 # of the item before this one (i.e. the "new last")
3377 select_url
= self
.channels
[position
-1].url
3379 # there is a podcast after the deleted one, so
3380 # we simply select the one that comes after it
3381 select_url
= self
.channels
[position
+1].url
3383 # Remove the channel and clean the database entries
3385 self
.channels
.remove(channel
)
3387 # Clean up downloads and download directories
3388 self
.clean_up_downloads()
3390 self
.channel_list_changed
= True
3391 self
.save_channels_opml()
3393 # The remaining stuff is to be done in the GTK main thread
3394 util
.idle_add(finish_deletion
, select_url
)
3396 threading
.Thread(target
=thread_proc
).start()
3398 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3399 if self
.active_channel
is None:
3400 title
= _('No podcast selected')
3401 message
= _('Please select a podcast in the podcasts list to remove.')
3402 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3405 self
.remove_podcast_list([self
.active_channel
])
3407 def get_opml_filter(self
):
3408 filter = gtk
.FileFilter()
3409 filter.add_pattern('*.opml')
3410 filter.add_pattern('*.xml')
3411 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3414 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3415 if filename
is None:
3416 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3417 # FIXME: Hildonization on Fremantle
3418 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3419 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3420 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3421 elif gpodder
.ui
.diablo
:
3422 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3423 dlg
.set_filter(self
.get_opml_filter())
3424 response
= dlg
.run()
3426 if response
== gtk
.RESPONSE_OK
:
3427 filename
= dlg
.get_filename()
3430 if filename
is not None:
3431 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3432 custom_title
=_('Import podcasts from OPML file'), \
3433 add_urls_callback
=self
.add_podcast_list
, \
3434 hide_url_entry
=True)
3435 dir.download_opml_file(filename
)
3437 def on_itemExportChannels_activate(self
, widget
, *args
):
3438 if not self
.channels
:
3439 title
= _('Nothing to export')
3440 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3441 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3444 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3445 # FIXME: Hildonization on Fremantle
3446 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3447 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3448 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3449 elif gpodder
.ui
.diablo
:
3450 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3451 dlg
.set_filter(self
.get_opml_filter())
3452 response
= dlg
.run()
3453 if response
== gtk
.RESPONSE_OK
:
3454 filename
= dlg
.get_filename()
3456 exporter
= opml
.Exporter( filename
)
3457 if exporter
.write(self
.channels
):
3458 count
= len(self
.channels
)
3459 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3460 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3462 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3466 def on_itemImportChannels_activate(self
, widget
, *args
):
3467 if gpodder
.ui
.fremantle
:
3468 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3469 self
.config
.toplist_url
, \
3470 self
.config
.opml_url
, \
3471 self
.add_podcast_list
, \
3472 self
.on_itemAddChannel_activate
, \
3473 self
.on_download_subscriptions_from_mygpo
, \
3474 self
.show_text_edit_dialog
)
3476 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3477 add_urls_callback
=self
.add_podcast_list
)
3478 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3480 def on_homepage_activate(self
, widget
, *args
):
3481 util
.open_website(gpodder
.__url
__)
3483 def on_wiki_activate(self
, widget
, *args
):
3484 util
.open_website('http://gpodder.org/wiki/User_Manual')
3486 def on_bug_tracker_activate(self
, widget
, *args
):
3487 if gpodder
.ui
.maemo
:
3488 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3490 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3492 def on_item_support_activate(self
, widget
):
3493 util
.open_website('http://gpodder.org/donate')
3495 def on_itemAbout_activate(self
, widget
, *args
):
3496 if gpodder
.ui
.fremantle
:
3497 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3498 HeAboutDialog
.present(self
.main_window
,
3501 gpodder
.__version
__,
3502 _('A podcast client with focus on usability'),
3503 gpodder
.__copyright
__,
3505 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3506 'http://gpodder.org/donate')
3509 dlg
= gtk
.AboutDialog()
3510 dlg
.set_transient_for(self
.main_window
)
3511 dlg
.set_name('gPodder')
3512 dlg
.set_version(gpodder
.__version
__)
3513 dlg
.set_copyright(gpodder
.__copyright
__)
3514 dlg
.set_comments(_('A podcast client with focus on usability'))
3515 dlg
.set_website(gpodder
.__url
__)
3516 dlg
.set_translator_credits( _('translator-credits'))
3517 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3519 if gpodder
.ui
.desktop
:
3520 # For the "GUI" version, we add some more
3521 # items to the about dialog (credits and logo)
3524 'Thomas Perl <thpinfo.com>',
3527 if os
.path
.exists(gpodder
.credits_file
):
3528 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3529 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3530 app_authors
+= credits
3532 dlg
.set_authors(app_authors
)
3534 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3536 dlg
.set_logo_icon_name('gpodder')
3540 def on_wNotebook_switch_page(self
, widget
, *args
):
3542 if gpodder
.ui
.maemo
:
3543 self
.tool_downloads
.set_active(page_num
== 1)
3544 page
= self
.wNotebook
.get_nth_page(page_num
)
3545 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3546 if page_num
== 0 and self
.active_channel
is not None:
3547 self
.set_title(self
.active_channel
.title
)
3549 self
.set_title(tab_label
)
3551 self
.play_or_download()
3552 self
.menuChannels
.set_sensitive(True)
3553 self
.menuSubscriptions
.set_sensitive(True)
3554 # The message area in the downloads tab should be hidden
3555 # when the user switches away from the downloads tab
3556 if self
.message_area
is not None:
3557 self
.message_area
.hide()
3558 self
.message_area
= None
3560 self
.menuChannels
.set_sensitive(False)
3561 self
.menuSubscriptions
.set_sensitive(False)
3562 if gpodder
.ui
.desktop
:
3563 self
.toolDownload
.set_sensitive(False)
3564 self
.toolPlay
.set_sensitive(False)
3565 self
.toolTransfer
.set_sensitive(False)
3566 self
.toolCancel
.set_sensitive(False)
3568 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3569 # double-click action of the podcast list or enter
3570 self
.treeChannels
.set_cursor(path
)
3572 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3573 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3575 if model
is not None and iter is not None:
3576 old_active_channel
= self
.active_channel
3577 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3579 if self
.active_channel
== old_active_channel
:
3582 if gpodder
.ui
.maemo
:
3583 self
.set_title(self
.active_channel
.title
)
3585 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3586 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3587 self
.itemEditChannel
.set_visible(False)
3588 self
.itemRemoveChannel
.set_visible(False)
3590 self
.itemEditChannel
.set_visible(True)
3591 self
.itemRemoveChannel
.set_visible(True)
3593 self
.active_channel
= None
3594 self
.itemEditChannel
.set_visible(False)
3595 self
.itemRemoveChannel
.set_visible(False)
3597 self
.update_episode_list_model()
3599 def on_btnEditChannel_clicked(self
, widget
, *args
):
3600 self
.on_itemEditChannel_activate( widget
, args
)
3602 def get_podcast_urls_from_selected_episodes(self
):
3603 """Get a set of podcast URLs based on the selected episodes"""
3604 return set(episode
.channel
.url
for episode
in \
3605 self
.get_selected_episodes())
3607 def get_selected_episodes(self
):
3608 """Get a list of selected episodes from treeAvailable"""
3609 selection
= self
.treeAvailable
.get_selection()
3610 model
, paths
= selection
.get_selected_rows()
3612 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3615 def on_transfer_selected_episodes(self
, widget
):
3616 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3618 def on_playback_selected_episodes(self
, widget
):
3619 self
.playback_episodes(self
.get_selected_episodes())
3621 def on_shownotes_selected_episodes(self
, widget
):
3622 episodes
= self
.get_selected_episodes()
3624 episode
= episodes
.pop(0)
3625 self
.show_episode_shownotes(episode
)
3627 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3629 def on_download_selected_episodes(self
, widget
):
3630 episodes
= self
.get_selected_episodes()
3631 self
.download_episode_list(episodes
)
3632 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3633 self
.play_or_download()
3635 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3636 """Double-click/enter action handler for treeAvailable"""
3637 # We should only have one one selected as it was double clicked!
3638 e
= self
.get_selected_episodes()[0]
3640 if (self
.config
.double_click_episode_action
== 'download'):
3641 # If the episode has already been downloaded and exists then play it
3642 if e
.was_downloaded(and_exists
=True):
3643 self
.playback_episodes(self
.get_selected_episodes())
3644 # else download it if it is not already downloading
3645 elif not self
.episode_is_downloading(e
):
3646 self
.download_episode_list([e
])
3647 self
.update_episode_list_icons([e
.url
])
3648 self
.play_or_download()
3649 elif (self
.config
.double_click_episode_action
== 'stream'):
3650 # If we happen to have downloaded this episode simple play it
3651 if e
.was_downloaded(and_exists
=True):
3652 self
.playback_episodes(self
.get_selected_episodes())
3653 # else if streaming is possible stream it
3654 elif self
.streaming_possible():
3655 self
.playback_episodes(self
.get_selected_episodes())
3657 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3658 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3660 # default action is to display show notes
3661 self
.on_shownotes_selected_episodes(widget
)
3663 def show_episode_shownotes(self
, episode
):
3664 if self
.episode_shownotes_window
is None:
3665 log('First-time use of episode window --- creating', sender
=self
)
3666 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3667 _download_episode_list
=self
.download_episode_list
, \
3668 _playback_episodes
=self
.playback_episodes
, \
3669 _delete_episode_list
=self
.delete_episode_list
, \
3670 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3671 _cancel_task_list
=self
.cancel_task_list
, \
3672 _episode_is_downloading
=self
.episode_is_downloading
, \
3673 _streaming_possible
=self
.streaming_possible())
3674 self
.episode_shownotes_window
.show(episode
)
3675 if self
.episode_is_downloading(episode
):
3676 self
.update_downloads_list()
3678 def restart_auto_update_timer(self
):
3679 if self
._auto
_update
_timer
_source
_id
is not None:
3680 log('Removing existing auto update timer.', sender
=self
)
3681 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3682 self
._auto
_update
_timer
_source
_id
= None
3684 if self
.config
.auto_update_feeds
and \
3685 self
.config
.auto_update_frequency
:
3686 interval
= 60*1000*self
.config
.auto_update_frequency
3687 log('Setting up auto update timer with interval %d.', \
3688 self
.config
.auto_update_frequency
, sender
=self
)
3689 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3690 interval
, self
._on
_auto
_update
_timer
)
3692 def _on_auto_update_timer(self
):
3693 log('Auto update timer fired.', sender
=self
)
3694 self
.update_feed_cache(force_update
=True)
3696 # Ask web service for sub changes (if enabled)
3697 self
.mygpo_client
.flush()
3701 def on_treeDownloads_row_activated(self
, widget
, *args
):
3702 # Use the standard way of working on the treeview
3703 selection
= self
.treeDownloads
.get_selection()
3704 (model
, paths
) = selection
.get_selected_rows()
3705 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3707 for tree_row_reference
, task
in selected_tasks
:
3708 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3709 task
.status
= task
.PAUSED
3710 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3711 self
.download_queue_manager
.add_task(task
)
3712 self
.enable_download_list_update()
3713 elif task
.status
== task
.DONE
:
3714 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3716 self
.play_or_download()
3718 # Update the tab title and downloads list
3719 self
.update_downloads_list()
3721 def on_item_cancel_download_activate(self
, widget
):
3722 if self
.wNotebook
.get_current_page() == 0:
3723 selection
= self
.treeAvailable
.get_selection()
3724 (model
, paths
) = selection
.get_selected_rows()
3725 urls
= [model
.get_value(model
.get_iter(path
), \
3726 self
.episode_list_model
.C_URL
) for path
in paths
]
3727 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3728 if task
.url
in urls
]
3730 selection
= self
.treeDownloads
.get_selection()
3731 (model
, paths
) = selection
.get_selected_rows()
3732 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3733 self
.download_status_model
.C_TASK
) for path
in paths
]
3734 self
.cancel_task_list(selected_tasks
)
3736 def on_btnCancelAll_clicked(self
, widget
, *args
):
3737 self
.cancel_task_list(self
.download_tasks_seen
)
3739 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3740 episodes
= self
.get_selected_episodes()
3741 if len(episodes
) == 1:
3742 self
.delete_episode_list(episodes
, skip_locked
=False)
3744 self
.delete_episode_list(episodes
)
3746 def on_key_press(self
, widget
, event
):
3747 # Allow tab switching with Ctrl + PgUp/PgDown
3748 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3749 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3750 self
.wNotebook
.prev_page()
3752 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3753 self
.wNotebook
.next_page()
3756 # After this code we only handle Maemo hardware keys,
3757 # so if we are not a Maemo app, we don't do anything
3758 if not gpodder
.ui
.maemo
:
3762 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3764 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3767 if diff
!= 0 and not self
.currently_updating
:
3768 selection
= self
.treeChannels
.get_selection()
3769 (model
, iter) = selection
.get_selected()
3770 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
3771 selection
.select_path(new_path
)
3772 self
.treeChannels
.set_cursor(new_path
)
3777 def on_iconify(self
):
3779 self
.gPodder
.set_skip_taskbar_hint(True)
3780 if self
.config
.minimize_to_tray
:
3781 self
.tray_icon
.set_visible(True)
3783 self
.gPodder
.set_skip_taskbar_hint(False)
3785 def on_uniconify(self
):
3787 self
.gPodder
.set_skip_taskbar_hint(False)
3788 if self
.config
.minimize_to_tray
:
3789 self
.tray_icon
.set_visible(False)
3791 self
.gPodder
.set_skip_taskbar_hint(False)
3793 def uniconify_main_window(self
):
3794 if self
.is_iconified():
3795 self
.gPodder
.present()
3797 def iconify_main_window(self
):
3798 if not self
.is_iconified():
3799 self
.gPodder
.iconify()
3801 def update_podcasts_tab(self
):
3802 if len(self
.channels
):
3803 if gpodder
.ui
.fremantle
:
3804 self
.button_refresh
.set_title(_('Check for new episodes'))
3805 self
.button_refresh
.show()
3807 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
3809 if gpodder
.ui
.fremantle
:
3810 self
.button_refresh
.hide()
3812 self
.label2
.set_text(_('Podcasts'))
3814 @dbus.service
.method(gpodder
.dbus_interface
)
3815 def show_gui_window(self
):
3816 self
.gPodder
.present()
3818 @dbus.service
.method(gpodder
.dbus_interface
)
3819 def subscribe_to_url(self
, url
):
3820 gPodderAddPodcast(self
.gPodder
,
3821 add_urls_callback
=self
.add_podcast_list
,
3824 @dbus.service
.method(gpodder
.dbus_interface
)
3825 def mark_episode_played(self
, filename
):
3826 if filename
is None:
3829 for channel
in self
.channels
:
3830 for episode
in channel
.get_all_episodes():
3831 fn
= episode
.local_filename(create
=False, check_only
=True)
3833 episode
.mark(is_played
=True)
3835 self
.update_episode_list_icons([episode
.url
])
3836 self
.update_podcast_list_model([episode
.channel
.url
])
3842 def main(options
=None):
3843 gobject
.threads_init()
3844 gobject
.set_application_name('gPodder')
3846 if gpodder
.ui
.maemo
:
3847 # Try to enable the custom icon theme for gPodder on Maemo
3848 settings
= gtk
.settings_get_default()
3849 settings
.set_string_property('gtk-icon-theme-name', \
3850 'gpodder', __file__
)
3851 # Extend the search path for the optified icon theme (Maemo 5)
3852 icon_theme
= gtk
.icon_theme_get_default()
3853 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
3855 gtk
.window_set_default_icon_name('gpodder')
3856 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
3859 session_bus
= dbus
.SessionBus(mainloop
=dbus
.glib
.DBusGMainLoop())
3860 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=session_bus
)
3861 except dbus
.exceptions
.DBusException
, dbe
:
3862 log('Warning: Cannot get "on the bus".', traceback
=True)
3863 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
3864 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
3865 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
3866 dlg
.set_title('gPodder')
3871 util
.make_directory(gpodder
.home
)
3872 gpodder
.load_plugins()
3874 config
= UIConfig(gpodder
.config_file
)
3876 if gpodder
.ui
.diablo
:
3877 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3878 # folder exists there (allow moving "gpodder" between SD cards or USB)
3879 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3880 if not os
.path
.exists(config
.download_dir
):
3881 log('Downloads might have been moved. Trying to locate them...')
3882 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
3883 dir = os
.path
.join(basedir
, 'gpodder')
3884 if os
.path
.exists(dir):
3885 log('Downloads found in: %s', dir)
3886 config
.download_dir
= dir
3889 log('Downloads NOT FOUND in %s', dir)
3891 if config
.enable_fingerscroll
:
3892 BuilderWidget
.use_fingerscroll
= True
3893 elif gpodder
.ui
.fremantle
:
3894 config
.on_quit_ask
= False
3896 config
.mygpo_device_type
= util
.detect_device_type()
3898 gp
= gPodder(bus_name
, config
)
3901 if options
.subscribe
:
3902 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
3905 # handle "subscribe to podcast" events from firefox
3906 if platform
.system() == 'Darwin':
3907 from gpodder
import gpodderosx
3908 gpodderosx
.register_handlers(gp
)
3909 # end mac OS X stuff