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/>.
38 from xml
.sax
import saxutils
48 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
51 def __init__(self
, *args
, **kwargs
):
58 def method(*args
, **kwargs
):
61 def __init__(self
, *args
, **kwargs
):
64 def __init__(self
, *args
, **kwargs
):
68 from gpodder
import feedcore
69 from gpodder
import util
70 from gpodder
import opml
71 from gpodder
import download
72 from gpodder
import my
73 from gpodder
import youtube
74 from gpodder
import player
75 from gpodder
.liblogger
import log
80 from gpodder
.model
import PodcastChannel
81 from gpodder
.model
import PodcastEpisode
82 from gpodder
.dbsqlite
import Database
84 from gpodder
.gtkui
.model
import PodcastListModel
85 from gpodder
.gtkui
.model
import EpisodeListModel
86 from gpodder
.gtkui
.config
import UIConfig
87 from gpodder
.gtkui
.services
import CoverDownloader
88 from gpodder
.gtkui
.widgets
import SimpleMessageArea
89 from gpodder
.gtkui
.desktopfile
import UserAppsReader
91 from gpodder
.gtkui
.draw
import draw_text_box_centered
93 from gpodder
.gtkui
.interface
.common
import BuilderWidget
94 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
95 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
96 from gpodder
.gtkui
.mygpodder
import MygPodderSettings
98 if gpodder
.ui
.desktop
:
99 from gpodder
.gtkui
.download
import DownloadStatusModel
101 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
103 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
104 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
105 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
106 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
107 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
108 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
110 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
112 except Exception, exc
:
113 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
114 log('Warning: This probably means your PyGTK installation is too old!')
115 have_trayicon
= False
116 elif gpodder
.ui
.diablo
:
117 from gpodder
.gtkui
.download
import DownloadStatusModel
119 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
120 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
121 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
122 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
123 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
124 have_trayicon
= False
125 elif gpodder
.ui
.fremantle
:
126 from gpodder
.gtkui
.frmntl
.model
import DownloadStatusModel
127 from gpodder
.gtkui
.frmntl
.model
import EpisodeListModel
128 from gpodder
.gtkui
.frmntl
.model
import PodcastListModel
130 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
131 from gpodder
.gtkui
.frmntl
.preferences
import gPodderPreferences
132 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
133 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
134 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
135 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
136 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
137 have_trayicon
= False
139 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
141 from gpodder
.gtkui
.interface
.common
import Orientation
143 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
144 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
149 from gpodder
.dbusproxy
import DBusPodcastsProxy
151 class gPodder(BuilderWidget
, dbus
.service
.Object
):
152 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear']
154 ICON_GENERAL_ADD
= 'general_add'
155 ICON_GENERAL_REFRESH
= 'general_refresh'
156 ICON_GENERAL_CLOSE
= 'general_close'
158 def __init__(self
, bus_name
, config
):
159 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
160 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
161 self
.on_itemUpdate_activate
, \
162 self
.playback_episodes
, \
163 self
.download_episode_list
, \
165 self
.db
= Database(gpodder
.database_file
)
167 BuilderWidget
.__init
__(self
, None)
170 if gpodder
.ui
.diablo
:
172 self
.app
= hildon
.Program()
173 self
.app
.add_window(self
.main_window
)
174 self
.main_window
.add_toolbar(self
.toolbar
)
176 for child
in self
.main_menu
.get_children():
178 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
179 self
.bluetooth_available
= False
180 elif gpodder
.ui
.fremantle
:
182 self
.app
= hildon
.Program()
183 self
.app
.add_window(self
.main_window
)
185 appmenu
= hildon
.AppMenu()
187 for filter in (self
.item_view_podcasts_all
, \
188 self
.item_view_podcasts_downloaded
, \
189 self
.item_view_podcasts_unplayed
):
190 button
= gtk
.ToggleButton()
191 filter.connect_proxy(button
)
192 appmenu
.add_filter(button
)
194 for action
in (self
.itemPreferences
, \
195 self
.item_downloads
, \
196 self
.itemRemoveOldEpisodes
, \
197 self
.item_unsubscribe
, \
199 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
200 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
201 action
.connect_proxy(button
)
202 if action
== self
.item_downloads
:
203 button
.set_title(_('Downloads'))
204 button
.set_value(_('Idle'))
205 self
.button_downloads
= button
206 appmenu
.append(button
)
208 self
.main_window
.set_app_menu(appmenu
)
210 # Initialize portrait mode / rotation manager
211 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
213 gpodder
.__version
__, \
214 self
.config
.rotation_mode
)
216 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
217 util
.idle_add(self
.on_window_orientation_changed
, \
218 Orientation
.PORTRAIT
)
219 self
._last
_orientation
= Orientation
.PORTRAIT
221 self
._last
_orientation
= Orientation
.LANDSCAPE
223 self
.bluetooth_available
= False
225 self
._last
_orientation
= Orientation
.LANDSCAPE
226 self
.bluetooth_available
= util
.bluetooth_available()
227 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
229 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
230 if not gpodder
.ui
.fremantle
:
231 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
232 self
.main_window
.show()
234 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
236 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
238 self
.preferences_dialog
= None
239 self
.config
.add_observer(self
.on_config_changed
)
241 self
.tray_icon
= None
242 self
.episode_shownotes_window
= None
243 self
.new_episodes_window
= None
245 if gpodder
.ui
.desktop
:
246 # Mac OS X-specific UI tweaks: Native main menu integration
247 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
248 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
250 import igemacintegration
as igemi
252 # Move the menu bar from the window to the Mac menu bar
254 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
256 # Reparent some items to the "Application" menu
257 for widget
in ('/mainMenu/menuHelp/itemAbout', \
258 '/mainMenu/menuPodcasts/itemPreferences'):
259 item
= self
.uimanager1
.get_widget(widget
)
260 group
= igemi
.ige_mac_menu_add_app_menu_group()
261 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
263 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
264 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
265 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
267 print >>sys
.stderr
, """
268 Warning: ige-mac-integration not found - no native menus.
271 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
272 self
.main_window
, self
.show_confirmation
, \
273 self
.update_episode_list_icons
, \
274 self
.update_podcast_list_model
, self
.toolPreferences
, \
275 gPodderEpisodeSelector
, \
276 self
.commit_changes_to_database
)
280 self
.download_status_model
= DownloadStatusModel()
281 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
283 if gpodder
.ui
.desktop
:
284 self
.show_hide_tray_icon()
285 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
286 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
287 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
289 if not gpodder
.ui
.fremantle
:
290 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
291 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
292 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
293 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
295 # When the amount of maximum downloads changes, notify the queue manager
296 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
297 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
299 self
.default_title
= 'gPodder'
300 if gpodder
.__version
__.rfind('git') != -1:
301 self
.set_title('gPodder %s' % gpodder
.__version
__)
303 title
= self
.gPodder
.get_title()
304 if title
is not None:
305 self
.set_title(title
)
307 self
.set_title(_('gPodder'))
309 self
.cover_downloader
= CoverDownloader()
311 # Generate list models for podcasts and their episodes
312 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
314 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
315 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
317 if gpodder
.ui
.fremantle
:
318 # Work around Maemo bug #4718
319 self
.button_refresh
.set_name('HildonButton-finger')
320 self
.button_subscribe
.set_name('HildonButton-finger')
322 self
.button_refresh
.set_sensitive(False)
323 self
.button_subscribe
.set_sensitive(False)
325 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
326 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
327 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
328 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
330 # Make the button scroll together with the TreeView contents
331 action_area_box
= self
.treeChannels
.get_action_area_box()
332 for child
in self
.buttonbox
:
333 child
.reparent(action_area_box
)
334 self
.vbox
.remove(self
.buttonbox
)
335 action_area_box
.set_spacing(2)
336 action_area_box
.set_border_width(3)
337 self
.treeChannels
.set_action_area_visible(True)
339 from gpodder
.gtkui
.frmntl
import style
340 sub_font
= style
.get_font_desc('SmallSystemFont')
341 sub_color
= style
.get_color('SecondaryTextColor')
342 sub
= (sub_font
.to_string(), sub_color
.to_string())
343 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
344 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
346 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
347 while gtk
.events_pending():
348 gtk
.main_iteration(False)
351 # Try to get the real package version from dpkg
352 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
353 version
, _stderr
= p
.communicate()
357 version
= gpodder
.__version
__
358 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
359 self
.label_footer
.hide()
361 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
362 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
363 show_episode_shownotes
=self
.show_episode_shownotes
, \
364 update_podcast_list_model
=self
.update_podcast_list_model
, \
365 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
366 item_view_episodes_all
=self
.item_view_episodes_all
, \
367 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
368 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
369 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
370 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
371 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
372 hide_episode_search
=self
.hide_episode_search
, \
373 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
374 playback_episodes
=self
.playback_episodes
, \
375 delete_episode_list
=self
.delete_episode_list
, \
376 episode_list_status_changed
=self
.episode_list_status_changed
, \
377 download_episode_list
=self
.download_episode_list
, \
378 episode_is_downloading
=self
.episode_is_downloading
, \
379 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
380 add_download_task_monitor
=self
.add_download_task_monitor
, \
381 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
382 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
383 on_delete_episodes_button_clicked
=self
.on_itemRemoveOldEpisodes_activate
, \
384 on_itemUpdate_activate
=self
.on_itemUpdate_activate
)
386 # Expose objects for episode list type-ahead find
387 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
388 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
389 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
391 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
392 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
393 on_btnCleanUpDownloads_clicked
=self
.on_btnCleanUpDownloads_clicked
, \
394 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
395 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
398 self
.treeAvailable
= self
.episodes_window
.treeview
399 self
.treeDownloads
= self
.downloads_window
.treeview
401 # Init the treeviews that we use
402 self
.init_podcast_list_treeview()
403 self
.init_episode_list_treeview()
404 self
.init_download_list_treeview()
406 if self
.config
.podcast_list_hide_boring
:
407 self
.item_view_hide_boring_podcasts
.set_active(True)
409 self
.currently_updating
= False
412 self
.context_menu_mouse_button
= 1
414 self
.context_menu_mouse_button
= 3
416 if self
.config
.start_iconified
:
417 self
.iconify_main_window()
419 self
.download_tasks_seen
= set()
420 self
.download_list_update_enabled
= False
421 self
.last_download_count
= 0
422 self
.download_task_monitors
= set()
424 # Subscribed channels
425 self
.active_channel
= None
426 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
427 self
.channel_list_changed
= True
428 self
.update_podcasts_tab()
430 # load list of user applications for audio playback
431 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
432 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
434 # Set the "Device" menu item for the first time
435 if gpodder
.ui
.desktop
:
436 self
.update_item_device()
438 # Set up the first instance of MygPoClient
439 self
.mygpo_client
= my
.MygPoClient(self
.config
)
441 # Now, update the feed cache, when everything's in place
442 if not gpodder
.ui
.fremantle
:
443 self
.btnUpdateFeeds
.show()
444 self
.updating_feed_cache
= False
445 self
.feed_cache_update_cancelled
= False
446 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
448 self
.message_area
= None
450 def find_partial_downloads():
451 # Look for partial file downloads
452 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
453 count
= len(partial_files
)
454 resumable_episodes
= []
456 if not gpodder
.ui
.fremantle
:
457 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
458 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
459 _('Some episodes have not finished downloading in a previous session.'), \
460 False, self
.main_window
)
461 indicator
.on_message(N_('%d partial file', '%d partial files', count
) % count
)
463 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
466 for c
in self
.channels
:
467 for e
in c
.get_all_episodes():
468 filename
= e
.local_filename(create
=False, check_only
=True)
469 if filename
in candidates
:
470 log('Found episode: %s', e
.title
, sender
=self
)
472 indicator
.on_message(e
.title
)
473 indicator
.on_progress(float(found
)/count
)
474 candidates
.remove(filename
)
475 partial_files
.remove(filename
+'.partial')
476 resumable_episodes
.append(e
)
484 for f
in partial_files
:
485 log('Partial file without episode: %s', f
, sender
=self
)
488 util
.idle_add(indicator
.on_finished
)
490 if len(resumable_episodes
):
491 def offer_resuming():
492 self
.download_episode_list_paused(resumable_episodes
)
493 if not gpodder
.ui
.fremantle
:
494 resume_all
= gtk
.Button(_('Resume all'))
495 #resume_all.set_border_width(0)
496 def on_resume_all(button
):
497 selection
= self
.treeDownloads
.get_selection()
498 selection
.select_all()
499 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
500 selection
.unselect_all()
501 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
502 self
.message_area
.hide()
503 resume_all
.connect('clicked', on_resume_all
)
505 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
506 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
507 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
508 self
.message_area
.show_all()
509 self
.clean_up_downloads(delete_partial
=False)
510 util
.idle_add(offer_resuming
)
511 elif not gpodder
.ui
.fremantle
:
512 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
514 util
.idle_add(self
.clean_up_downloads
, True)
515 threading
.Thread(target
=find_partial_downloads
).start()
517 # Start the auto-update procedure
518 self
._auto
_update
_timer
_source
_id
= None
519 if self
.config
.auto_update_feeds
:
520 self
.restart_auto_update_timer()
522 # Delete old episodes if the user wishes to
523 if self
.config
.auto_remove_played_episodes
and \
524 self
.config
.episode_old_age
> 0:
525 old_episodes
= list(self
.get_expired_episodes())
526 if len(old_episodes
) > 0:
527 self
.delete_episode_list(old_episodes
, confirm
=False)
528 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
530 if gpodder
.ui
.fremantle
:
531 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
532 self
.button_refresh
.set_sensitive(True)
533 self
.button_subscribe
.set_sensitive(True)
534 self
.main_window
.set_title(_('gPodder'))
535 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
537 # Do the initial sync with the web service
538 util
.idle_add(self
.mygpo_client
.flush
, True)
540 # First-time users should be asked if they want to see the OPML
541 if not self
.channels
and not gpodder
.ui
.fremantle
:
542 util
.idle_add(self
.on_itemUpdate_activate
)
544 def on_played(self
, start
, end
, total
, file_uri
):
545 """Handle the "played" signal from a media player"""
546 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
547 filename
= file_uri
[len('file://'):]
548 # FIXME: Optimize this by querying the database more directly
549 for channel
in self
.channels
:
550 for episode
in channel
.get_all_episodes():
551 fn
= episode
.local_filename(create
=False, check_only
=True)
553 file_type
= episode
.file_type()
554 # Automatically enable D-Bus played status mode
555 if file_type
== 'audio':
556 self
.config
.audio_played_dbus
= True
557 elif file_type
== 'video':
558 self
.config
.video_played_dbus
= True
562 episode
.total_time
= total
563 if episode
.current_position_updated
is None or \
564 now
> episode
.current_position_updated
:
565 episode
.current_position
= end
566 episode
.current_position_updated
= now
567 episode
.mark(is_played
=True)
570 self
.update_episode_list_icons([episode
.url
])
571 self
.update_podcast_list_model([episode
.channel
.url
])
573 # Submit this action to the webservice
574 self
.mygpo_client
.on_playback_full(episode
, \
578 def on_add_remove_podcasts_mygpo(self
):
579 actions
= self
.mygpo_client
.get_received_actions()
583 existing_urls
= [c
.url
for c
in self
.channels
]
585 # Columns for the episode selector window - just one...
587 ('description', None, None, _('Action')),
590 # A list of actions that have to be chosen from
593 # Actions that are ignored (already carried out)
596 for action
in actions
:
597 if action
.is_add
and action
.url
not in existing_urls
:
598 changes
.append(my
.Change(action
))
599 elif action
.is_remove
and action
.url
in existing_urls
:
600 podcast_object
= None
601 for podcast
in self
.channels
:
602 if podcast
.url
== action
.url
:
603 podcast_object
= podcast
605 changes
.append(my
.Change(action
, podcast_object
))
607 log('Ignoring action: %s', action
, sender
=self
)
608 ignored
.append(action
)
610 # Confirm all ignored changes
611 self
.mygpo_client
.confirm_received_actions(ignored
)
613 def execute_podcast_actions(selected
):
614 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
615 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
617 # Apply the accepted changes locally
618 self
.add_podcast_list(add_list
)
619 self
.remove_podcast_list(remove_list
, confirm
=False)
621 # All selected items are now confirmed
622 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
624 # Revert the changes on the server
625 rejected
= [c
.action
for c
in changes
if c
not in selected
]
626 self
.mygpo_client
.reject_received_actions(rejected
)
629 # We're abusing the Episode Selector again ;) -- thp
630 gPodderEpisodeSelector(self
.main_window
, \
631 title
=_('Confirm changes from gpodder.net'), \
632 instructions
=_('Select the actions you want to carry out.'), \
635 size_attribute
=None, \
636 stock_ok_button
=gtk
.STOCK_APPLY
, \
637 callback
=execute_podcast_actions
, \
640 # There are some actions that need the user's attention
645 # We have no remaining actions - no selection happens
648 def rewrite_urls_mygpo(self
):
649 # Check if we have to rewrite URLs since the last add
650 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
652 for rewritten_url
in rewritten_urls
:
653 if not rewritten_url
.new_url
:
656 for channel
in self
.channels
:
657 if channel
.url
== rewritten_url
.old_url
:
658 log('Updating URL of %s to %s', channel
, \
659 rewritten_url
.new_url
, sender
=self
)
660 channel
.url
= rewritten_url
.new_url
662 self
.channel_list_changed
= True
663 util
.idle_add(self
.update_episode_list_model
)
666 def on_send_full_subscriptions(self
):
667 # Send the full subscription list to the gpodder.net client
668 # (this will overwrite the subscription list on the server)
669 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
670 _('Your subscriptions are being uploaded to the server.'), \
671 False, self
.main_window
)
674 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
675 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
680 message
= e
.__class
__.__name
__
681 self
.show_message(message
, \
682 _('Error while uploading'), \
684 util
.idle_add(show_error
, e
)
686 util
.idle_add(indicator
.on_finished
)
688 def on_podcast_selected(self
, treeview
, path
, column
):
690 model
= treeview
.get_model()
691 channel
= model
.get_value(model
.get_iter(path
), \
692 PodcastListModel
.C_CHANNEL
)
693 self
.active_channel
= channel
694 self
.update_episode_list_model()
695 self
.episodes_window
.channel
= self
.active_channel
696 self
.episodes_window
.show()
698 def on_button_subscribe_clicked(self
, button
):
699 self
.on_itemImportChannels_activate(button
)
701 def on_button_downloads_clicked(self
, widget
):
702 self
.downloads_window
.show()
704 def show_episode_in_download_manager(self
, episode
):
705 self
.downloads_window
.show()
706 model
= self
.treeDownloads
.get_model()
707 selection
= self
.treeDownloads
.get_selection()
708 selection
.unselect_all()
709 it
= model
.get_iter_first()
710 while it
is not None:
711 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
712 if task
.episode
.url
== episode
.url
:
713 selection
.select_iter(it
)
714 # FIXME: Scroll to selection in pannable area
716 it
= model
.iter_next(it
)
718 def for_each_episode_set_task_status(self
, episodes
, status
):
719 episode_urls
= set(episode
.url
for episode
in episodes
)
720 model
= self
.treeDownloads
.get_model()
721 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
722 model
.get_value(row
.iter, \
723 DownloadStatusModel
.C_TASK
)) for row
in model \
724 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
726 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
728 def on_window_orientation_changed(self
, orientation
):
729 self
._last
_orientation
= orientation
730 if self
.preferences_dialog
is not None:
731 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
733 treeview
= self
.treeChannels
734 if orientation
== Orientation
.PORTRAIT
:
735 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
736 # Work around Maemo bug #4718
737 self
.button_subscribe
.set_name('HildonButton-thumb')
738 self
.button_refresh
.set_name('HildonButton-thumb')
740 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
741 # Work around Maemo bug #4718
742 self
.button_subscribe
.set_name('HildonButton-finger')
743 self
.button_refresh
.set_name('HildonButton-finger')
745 def on_treeview_podcasts_selection_changed(self
, selection
):
746 model
, iter = selection
.get_selected()
748 self
.active_channel
= None
749 self
.episode_list_model
.clear()
751 def on_treeview_button_pressed(self
, treeview
, event
):
752 if event
.window
!= treeview
.get_bin_window():
755 TreeViewHelper
.save_button_press_event(treeview
, event
)
757 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
758 TreeViewHelper
.ROLE_PODCASTS
:
759 return self
.currently_updating
761 return event
.button
== self
.context_menu_mouse_button
and \
764 def on_treeview_podcasts_button_released(self
, treeview
, event
):
765 if event
.window
!= treeview
.get_bin_window():
769 return self
.treeview_channels_handle_gestures(treeview
, event
)
770 return self
.treeview_channels_show_context_menu(treeview
, event
)
772 def on_treeview_episodes_button_released(self
, treeview
, event
):
773 if event
.window
!= treeview
.get_bin_window():
777 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
778 return self
.treeview_available_handle_gestures(treeview
, event
)
780 return self
.treeview_available_show_context_menu(treeview
, event
)
782 def on_treeview_downloads_button_released(self
, treeview
, event
):
783 if event
.window
!= treeview
.get_bin_window():
786 return self
.treeview_downloads_show_context_menu(treeview
, event
)
788 def on_entry_search_podcasts_changed(self
, editable
):
789 if self
.hbox_search_podcasts
.get_property('visible'):
790 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
792 def on_entry_search_podcasts_key_press(self
, editable
, event
):
793 if event
.keyval
== gtk
.keysyms
.Escape
:
794 self
.hide_podcast_search()
797 def hide_podcast_search(self
, *args
):
798 self
.hbox_search_podcasts
.hide()
799 self
.entry_search_podcasts
.set_text('')
800 self
.podcast_list_model
.set_search_term(None)
801 self
.treeChannels
.grab_focus()
803 def show_podcast_search(self
, input_char
):
804 self
.hbox_search_podcasts
.show()
805 self
.entry_search_podcasts
.insert_text(input_char
, -1)
806 self
.entry_search_podcasts
.grab_focus()
807 self
.entry_search_podcasts
.set_position(-1)
809 def init_podcast_list_treeview(self
):
810 # Set up podcast channel tree view widget
811 if gpodder
.ui
.fremantle
:
812 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
813 self
.item_view_podcasts_downloaded
.set_active(True)
814 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
815 self
.item_view_podcasts_unplayed
.set_active(True)
817 self
.item_view_podcasts_all
.set_active(True)
818 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
820 iconcolumn
= gtk
.TreeViewColumn('')
821 iconcell
= gtk
.CellRendererPixbuf()
822 iconcolumn
.pack_start(iconcell
, False)
823 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
824 self
.treeChannels
.append_column(iconcolumn
)
826 namecolumn
= gtk
.TreeViewColumn('')
827 namecell
= gtk
.CellRendererText()
828 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
829 namecolumn
.pack_start(namecell
, True)
830 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
832 iconcell
= gtk
.CellRendererPixbuf()
833 iconcell
.set_property('xalign', 1.0)
834 namecolumn
.pack_start(iconcell
, False)
835 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
836 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
837 self
.treeChannels
.append_column(namecolumn
)
839 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
841 # When no podcast is selected, clear the episode list model
842 selection
= self
.treeChannels
.get_selection()
843 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
845 # Set up type-ahead find for the podcast list
846 def on_key_press(treeview
, event
):
847 if event
.keyval
== gtk
.keysyms
.Escape
:
848 self
.hide_podcast_search()
849 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
850 self
.hide_podcast_search()
851 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
852 # Don't handle type-ahead when control is pressed (so shortcuts
853 # with the Ctrl key still work, e.g. Ctrl+A, ...)
856 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
857 if unicode_char_id
== 0:
859 input_char
= unichr(unicode_char_id
)
860 self
.show_podcast_search(input_char
)
862 self
.treeChannels
.connect('key-press-event', on_key_press
)
864 # Enable separators to the podcast list to separate special podcasts
865 # from others (this is used for the "all episodes" view)
866 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
868 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
870 def on_entry_search_episodes_changed(self
, editable
):
871 if self
.hbox_search_episodes
.get_property('visible'):
872 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
874 def on_entry_search_episodes_key_press(self
, editable
, event
):
875 if event
.keyval
== gtk
.keysyms
.Escape
:
876 self
.hide_episode_search()
879 def hide_episode_search(self
, *args
):
880 self
.hbox_search_episodes
.hide()
881 self
.entry_search_episodes
.set_text('')
882 self
.episode_list_model
.set_search_term(None)
883 self
.treeAvailable
.grab_focus()
885 def show_episode_search(self
, input_char
):
886 self
.hbox_search_episodes
.show()
887 self
.entry_search_episodes
.insert_text(input_char
, -1)
888 self
.entry_search_episodes
.grab_focus()
889 self
.entry_search_episodes
.set_position(-1)
891 def init_episode_list_treeview(self
):
892 # For loading the list model
893 self
.empty_episode_list_model
= EpisodeListModel()
894 self
.episode_list_model
= EpisodeListModel()
896 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
897 self
.item_view_episodes_undeleted
.set_active(True)
898 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
899 self
.item_view_episodes_downloaded
.set_active(True)
900 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
901 self
.item_view_episodes_unplayed
.set_active(True)
903 self
.item_view_episodes_all
.set_active(True)
905 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
907 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
909 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
911 iconcell
= gtk
.CellRendererPixbuf()
913 iconcell
.set_fixed_size(50, 50)
914 status_column_label
= ''
916 status_column_label
= _('Status')
917 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
919 namecell
= gtk
.CellRendererText()
920 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
921 namecolumn
= gtk
.TreeViewColumn(_('Episode'), namecell
, markup
=EpisodeListModel
.C_DESCRIPTION
)
922 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
923 namecolumn
.set_resizable(True)
924 namecolumn
.set_expand(True)
926 sizecell
= gtk
.CellRendererText()
927 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
929 releasecell
= gtk
.CellRendererText()
930 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
932 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
933 itemcolumn
.set_reorderable(True)
934 self
.treeAvailable
.append_column(itemcolumn
)
937 sizecolumn
.set_visible(False)
938 releasecolumn
.set_visible(False)
940 # Set up type-ahead find for the episode list
941 def on_key_press(treeview
, event
):
942 if event
.keyval
== gtk
.keysyms
.Escape
:
943 self
.hide_episode_search()
944 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
945 self
.hide_episode_search()
946 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
947 # Don't handle type-ahead when control is pressed (so shortcuts
948 # with the Ctrl key still work, e.g. Ctrl+A, ...)
951 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
952 if unicode_char_id
== 0:
954 input_char
= unichr(unicode_char_id
)
955 self
.show_episode_search(input_char
)
957 self
.treeAvailable
.connect('key-press-event', on_key_press
)
959 if gpodder
.ui
.desktop
:
960 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
961 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
962 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
963 if self
.config
.on_drag_mark_played
:
964 for episode
in self
.get_selected_episodes():
965 episode
.mark(is_played
=True)
966 self
.on_selected_episodes_status_changed()
967 uris
= ['file://'+e
.local_filename(create
=False) \
968 for e
in self
.get_selected_episodes() \
969 if e
.was_downloaded(and_exists
=True)]
970 uris
.append('') # for the trailing '\r\n'
971 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
972 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
974 selection
= self
.treeAvailable
.get_selection()
975 if gpodder
.ui
.diablo
:
976 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
977 selection
.set_mode(gtk
.SELECTION_SINGLE
)
979 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
980 elif gpodder
.ui
.fremantle
:
981 selection
.set_mode(gtk
.SELECTION_SINGLE
)
983 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
984 # Update the sensitivity of the toolbar buttons on the Desktop
985 selection
.connect('changed', lambda s
: self
.play_or_download())
987 if gpodder
.ui
.diablo
:
988 # Set up the tap-and-hold context menu for podcasts
990 menu
.append(self
.itemUpdateChannel
.create_menu_item())
991 menu
.append(self
.itemEditChannel
.create_menu_item())
992 menu
.append(gtk
.SeparatorMenuItem())
993 menu
.append(self
.itemRemoveChannel
.create_menu_item())
994 menu
.append(gtk
.SeparatorMenuItem())
995 item
= gtk
.ImageMenuItem(_('Close this menu'))
996 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1000 menu
= self
.set_finger_friendly(menu
)
1001 self
.treeChannels
.tap_and_hold_setup(menu
)
1004 def init_download_list_treeview(self
):
1005 # enable multiple selection support
1006 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1007 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1009 # columns and renderers for "download progress" tab
1010 # First column: [ICON] Episodename
1011 column
= gtk
.TreeViewColumn(_('Episode'))
1013 cell
= gtk
.CellRendererPixbuf()
1014 if gpodder
.ui
.maemo
:
1015 cell
.set_fixed_size(50, 50)
1016 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1017 column
.pack_start(cell
, expand
=False)
1018 column
.add_attribute(cell
, 'stock-id', \
1019 DownloadStatusModel
.C_ICON_NAME
)
1021 cell
= gtk
.CellRendererText()
1022 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1023 column
.pack_start(cell
, expand
=True)
1024 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1025 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1026 column
.set_expand(True)
1027 self
.treeDownloads
.append_column(column
)
1029 # Second column: Progress
1030 cell
= gtk
.CellRendererProgress()
1031 cell
.set_property('yalign', .5)
1032 cell
.set_property('ypad', 6)
1033 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1034 value
=DownloadStatusModel
.C_PROGRESS
, \
1035 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1036 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1037 column
.set_expand(False)
1038 self
.treeDownloads
.append_column(column
)
1039 column
.set_property('min-width', 150)
1040 column
.set_property('max-width', 150)
1042 self
.treeDownloads
.set_model(self
.download_status_model
)
1043 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1045 def on_treeview_expose_event(self
, treeview
, event
):
1046 if event
.window
== treeview
.get_bin_window():
1047 model
= treeview
.get_model()
1048 if (model
is not None and model
.get_iter_first() is not None):
1051 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1052 ctx
= event
.window
.cairo_create()
1053 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1054 event
.area
.width
, event
.area
.height
)
1057 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1060 if role
== TreeViewHelper
.ROLE_EPISODES
:
1061 if self
.currently_updating
:
1062 text
= _('Loading episodes')
1063 progress
= self
.episode_list_model
.get_update_progress()
1064 elif self
.config
.episode_list_view_mode
!= \
1065 EpisodeListModel
.VIEW_ALL
:
1066 text
= _('No episodes in current view')
1068 text
= _('No episodes available')
1069 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1070 if self
.config
.episode_list_view_mode
!= \
1071 EpisodeListModel
.VIEW_ALL
and \
1072 self
.config
.podcast_list_hide_boring
and \
1073 len(self
.channels
) > 0:
1074 text
= _('No podcasts in this view')
1076 text
= _('No subscriptions')
1077 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1078 text
= _('No active downloads')
1080 raise Exception('on_treeview_expose_event: unknown role')
1082 if gpodder
.ui
.fremantle
:
1083 from gpodder
.gtkui
.frmntl
import style
1084 font_desc
= style
.get_font_desc('LargeSystemFont')
1088 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1092 def enable_download_list_update(self
):
1093 if not self
.download_list_update_enabled
:
1094 gobject
.timeout_add(1500, self
.update_downloads_list
)
1095 self
.download_list_update_enabled
= True
1097 def on_btnCleanUpDownloads_clicked(self
, button
=None):
1098 model
= self
.download_status_model
1100 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1101 changed_episode_urls
= set()
1102 for row_reference
, task
in all_tasks
:
1103 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1104 model
.remove(model
.get_iter(row_reference
.get_path()))
1106 # We don't "see" this task anymore - remove it;
1107 # this is needed, so update_episode_list_icons()
1108 # below gets the correct list of "seen" tasks
1109 self
.download_tasks_seen
.remove(task
)
1110 except KeyError, key_error
:
1111 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1112 changed_episode_urls
.add(task
.url
)
1113 # Tell the task that it has been removed (so it can clean up)
1114 task
.removed_from_list()
1116 # Tell the podcasts tab to update icons for our removed podcasts
1117 self
.update_episode_list_icons(changed_episode_urls
)
1119 # Tell the shownotes window that we have removed the episode
1120 if self
.episode_shownotes_window
is not None and \
1121 self
.episode_shownotes_window
.episode
is not None and \
1122 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1123 self
.episode_shownotes_window
._download
_status
_changed
(None)
1125 # Update the tab title and downloads list
1126 self
.update_downloads_list()
1128 def on_tool_downloads_toggled(self
, toolbutton
):
1129 if toolbutton
.get_active():
1130 self
.wNotebook
.set_current_page(1)
1132 self
.wNotebook
.set_current_page(0)
1134 def add_download_task_monitor(self
, monitor
):
1135 self
.download_task_monitors
.add(monitor
)
1136 model
= self
.download_status_model
1140 task
= row
[self
.download_status_model
.C_TASK
]
1141 monitor
.task_updated(task
)
1143 def remove_download_task_monitor(self
, monitor
):
1144 self
.download_task_monitors
.remove(monitor
)
1146 def update_downloads_list(self
):
1148 model
= self
.download_status_model
1150 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1151 total_speed
, total_size
, done_size
= 0, 0, 0
1153 # Keep a list of all download tasks that we've seen
1154 download_tasks_seen
= set()
1156 # Remember the DownloadTask object for the episode that
1157 # has been opened in the episode shownotes dialog (if any)
1158 if self
.episode_shownotes_window
is not None:
1159 shownotes_episode
= self
.episode_shownotes_window
.episode
1160 shownotes_task
= None
1162 shownotes_episode
= None
1163 shownotes_task
= None
1165 # Do not go through the list of the model is not (yet) available
1169 failed_downloads
= []
1171 self
.download_status_model
.request_update(row
.iter)
1173 task
= row
[self
.download_status_model
.C_TASK
]
1174 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1176 # Let the download task monitors know of changes
1177 for monitor
in self
.download_task_monitors
:
1178 monitor
.task_updated(task
)
1181 done_size
+= size
*progress
1183 if shownotes_episode
is not None and \
1184 shownotes_episode
.url
== task
.episode
.url
:
1185 shownotes_task
= task
1187 download_tasks_seen
.add(task
)
1189 if status
== download
.DownloadTask
.DOWNLOADING
:
1191 total_speed
+= speed
1192 elif status
== download
.DownloadTask
.FAILED
:
1193 failed_downloads
.append(task
)
1195 elif status
== download
.DownloadTask
.DONE
:
1197 elif status
== download
.DownloadTask
.QUEUED
:
1199 elif status
== download
.DownloadTask
.PAUSED
:
1204 # Remember which tasks we have seen after this run
1205 self
.download_tasks_seen
= download_tasks_seen
1207 if gpodder
.ui
.desktop
:
1208 text
= [_('Downloads')]
1209 if downloading
+ failed
+ queued
> 0:
1212 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1214 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1216 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1217 text
.append(' (' + ', '.join(s
)+')')
1218 self
.labelDownloads
.set_text(''.join(text
))
1219 elif gpodder
.ui
.diablo
:
1220 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1222 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1224 self
.tool_downloads
.set_label(_('Downloads'))
1225 elif gpodder
.ui
.fremantle
:
1226 if downloading
+ queued
> 0:
1227 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1229 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1231 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1233 self
.button_downloads
.set_value(_('Idle'))
1235 title
= [self
.default_title
]
1237 # We have to update all episodes/channels for which the status has
1238 # changed. Accessing task.status_changed has the side effect of
1239 # re-setting the changed flag, so we need to get the "changed" list
1240 # of tuples first and split it into two lists afterwards
1241 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1242 self
.download_tasks_seen
if task
.status_changed
]
1243 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1244 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1246 count
= downloading
+ queued
1248 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1251 percentage
= 100.0*done_size
/total_size
1254 total_speed
= util
.format_filesize(total_speed
)
1255 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1256 if self
.tray_icon
is not None:
1257 # Update the tray icon status and progress bar
1258 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1259 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1260 elif self
.last_download_count
> 0:
1261 if self
.tray_icon
is not None:
1262 # Update the tray icon status
1263 self
.tray_icon
.set_status()
1264 if gpodder
.ui
.desktop
:
1265 self
.downloads_finished(self
.download_tasks_seen
)
1266 if gpodder
.ui
.diablo
:
1267 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1268 log('All downloads have finished.', sender
=self
)
1269 if self
.config
.cmd_all_downloads_complete
:
1270 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1272 if gpodder
.ui
.fremantle
and failed
:
1273 message
= '\n'.join(['%s: %s' % (str(task
), \
1274 task
.error_message
) for task
in failed_downloads
])
1275 self
.show_message(message
, _('Downloads failed'), important
=True)
1276 self
.last_download_count
= count
1278 if not gpodder
.ui
.fremantle
:
1279 self
.gPodder
.set_title(' - '.join(title
))
1281 self
.update_episode_list_icons(episode_urls
)
1282 if self
.episode_shownotes_window
is not None:
1283 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1284 shownotes_task
!= self
.episode_shownotes_window
.task
:
1285 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1286 self
.episode_shownotes_window
._download
_status
_progress
()
1287 self
.play_or_download()
1289 self
.update_podcast_list_model(channel_urls
)
1291 if not self
.download_queue_manager
.are_queued_or_active_tasks():
1292 self
.download_list_update_enabled
= False
1294 return self
.download_list_update_enabled
1295 except Exception, e
:
1296 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1297 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1298 # We return False here, so the update loop won't be called again,
1299 # that's why we require the restart of gPodder in the message.
1302 def on_config_changed(self
, *args
):
1303 util
.idle_add(self
._on
_config
_changed
, *args
)
1305 def _on_config_changed(self
, name
, old_value
, new_value
):
1306 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1307 self
.toolbar
.set_property('visible', new_value
)
1308 elif name
== 'videoplayer':
1309 self
.config
.video_played_dbus
= False
1310 elif name
== 'player':
1311 self
.config
.audio_played_dbus
= False
1312 elif name
== 'episode_list_descriptions':
1313 self
.update_episode_list_model()
1314 elif name
== 'episode_list_thumbnails':
1315 self
.update_episode_list_icons(all
=True)
1316 elif name
== 'rotation_mode':
1317 self
._fremantle
_rotation
.set_mode(new_value
)
1318 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1319 self
.restart_auto_update_timer()
1320 elif name
== 'podcast_list_view_all':
1321 # Force a update of the podcast list model
1322 self
.channel_list_changed
= True
1323 if gpodder
.ui
.fremantle
:
1324 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1325 while gtk
.events_pending():
1326 gtk
.main_iteration(False)
1327 self
.update_podcast_list_model()
1328 if gpodder
.ui
.fremantle
:
1329 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1331 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1332 # With get_bin_window, we get the window that contains the rows without
1333 # the header. The Y coordinate of this window will be the height of the
1334 # treeview header. This is the amount we have to subtract from the
1335 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1336 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1339 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1341 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1342 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1345 if path
is not None:
1346 model
= treeview
.get_model()
1347 iter = model
.get_iter(path
)
1348 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1350 if role
== TreeViewHelper
.ROLE_EPISODES
:
1351 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1352 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1353 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1355 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1356 if last_tooltip
is not None and last_tooltip
!= id:
1357 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1359 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1361 if role
== TreeViewHelper
.ROLE_EPISODES
:
1362 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1364 tooltip
.set_text(description
)
1367 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1368 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1371 channel
.request_save_dir_size()
1372 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1373 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1375 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1376 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1377 table
= gtk
.Table(rows
=3, columns
=3)
1378 table
.set_row_spacings(5)
1379 table
.set_col_spacings(5)
1380 table
.set_border_width(5)
1382 heading
= gtk
.Label()
1383 heading
.set_alignment(0, 1)
1384 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1385 table
.attach(heading
, 0, 1, 0, 1)
1386 size_info
= gtk
.Label()
1387 size_info
.set_alignment(1, 1)
1388 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1389 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1390 table
.attach(size_info
, 2, 3, 0, 1)
1392 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1394 if len(channel
.description
) < 500:
1395 description
= channel
.description
1397 pos
= channel
.description
.find('\n\n')
1398 if pos
== -1 or pos
> 500:
1399 description
= channel
.description
[:498]+'[...]'
1401 description
= channel
.description
[:pos
]
1403 description
= gtk
.Label(description
)
1405 description
.set_markup(error_str
)
1406 description
.set_alignment(0, 0)
1407 description
.set_line_wrap(True)
1408 table
.attach(description
, 0, 3, 2, 3)
1411 tooltip
.set_custom(table
)
1415 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1418 def treeview_allow_tooltips(self
, treeview
, allow
):
1419 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1421 def update_m3u_playlist_clicked(self
, widget
):
1422 if self
.active_channel
is not None:
1423 self
.active_channel
.update_m3u_playlist()
1424 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1426 def treeview_handle_context_menu_click(self
, treeview
, event
):
1427 x
, y
= int(event
.x
), int(event
.y
)
1428 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1430 selection
= treeview
.get_selection()
1431 model
, paths
= selection
.get_selected_rows()
1433 if path
is None or (path
not in paths
and \
1434 event
.button
== self
.context_menu_mouse_button
):
1435 # We have right-clicked, but not into the selection,
1436 # assume we don't want to operate on the selection
1439 if path
is not None and not paths
and \
1440 event
.button
== self
.context_menu_mouse_button
:
1441 # No selection or clicked outside selection;
1442 # select the single item where we clicked
1443 treeview
.grab_focus()
1444 treeview
.set_cursor(path
, column
, 0)
1448 # Unselect any remaining items (clicked elsewhere)
1449 if hasattr(treeview
, 'is_rubber_banding_active'):
1450 if not treeview
.is_rubber_banding_active():
1451 selection
.unselect_all()
1453 selection
.unselect_all()
1457 def downloads_list_get_selection(self
, model
=None, paths
=None):
1458 if model
is None and paths
is None:
1459 selection
= self
.treeDownloads
.get_selection()
1460 model
, paths
= selection
.get_selected_rows()
1462 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1463 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1464 model
.get_value(model
.get_iter(path
), \
1465 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1467 for row_reference
, task
in selected_tasks
:
1468 if task
.status
!= download
.DownloadTask
.QUEUED
:
1470 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1471 download
.DownloadTask
.FAILED
, \
1472 download
.DownloadTask
.CANCELLED
):
1474 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1475 download
.DownloadTask
.QUEUED
, \
1476 download
.DownloadTask
.DOWNLOADING
):
1478 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1479 download
.DownloadTask
.DOWNLOADING
):
1481 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1482 download
.DownloadTask
.FAILED
, \
1483 download
.DownloadTask
.DONE
):
1486 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1488 def downloads_finished(self
, download_tasks_seen
):
1489 # FIXME: Filter all tasks that have already been reported
1490 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1491 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1493 if finished_downloads
and failed_downloads
:
1494 message
= self
.format_episode_list(finished_downloads
, 5)
1495 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1496 message
+= self
.format_episode_list(failed_downloads
, 5)
1497 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1498 elif finished_downloads
:
1499 message
= self
.format_episode_list(finished_downloads
)
1500 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1501 elif failed_downloads
:
1502 message
= self
.format_episode_list(failed_downloads
)
1503 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1505 def format_episode_list(self
, episode_list
, max_episodes
=10):
1507 Format a list of episode names for notifications
1509 Will truncate long episode names and limit the amount of
1510 episodes displayed (max_episodes=10).
1512 The episode_list parameter should be a list of strings.
1514 MAX_TITLE_LENGTH
= 100
1517 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1518 if len(title
) > MAX_TITLE_LENGTH
:
1519 middle
= (MAX_TITLE_LENGTH
/2)-2
1520 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1521 result
.append(saxutils
.escape(title
))
1524 more_episodes
= len(episode_list
) - max_episodes
1525 if more_episodes
> 0:
1526 result
.append('(...')
1527 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1528 result
.append('...)')
1530 return (''.join(result
)).strip()
1532 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1533 episode_urls
= set()
1534 model
= self
.treeDownloads
.get_model()
1535 for row_reference
, task
in tasks
:
1536 if status
== download
.DownloadTask
.QUEUED
:
1537 # Only queue task when its paused/failed/cancelled (or forced)
1538 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1539 self
.download_queue_manager
.add_task(task
, force_start
)
1540 self
.enable_download_list_update()
1541 elif status
== download
.DownloadTask
.CANCELLED
:
1542 # Cancelling a download allowed when downloading/queued
1543 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1544 task
.status
= status
1545 # Cancelling paused downloads requires a call to .run()
1546 elif task
.status
== task
.PAUSED
:
1547 task
.status
= status
1548 # Call run, so the partial file gets deleted
1550 elif status
== download
.DownloadTask
.PAUSED
:
1551 # Pausing a download only when queued/downloading
1552 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1553 task
.status
= status
1554 elif status
is None:
1555 # Remove the selected task - cancel downloading/queued tasks
1556 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1557 task
.status
= task
.CANCELLED
1558 model
.remove(model
.get_iter(row_reference
.get_path()))
1559 # Remember the URL, so we can tell the UI to update
1561 # We don't "see" this task anymore - remove it;
1562 # this is needed, so update_episode_list_icons()
1563 # below gets the correct list of "seen" tasks
1564 self
.download_tasks_seen
.remove(task
)
1565 except KeyError, key_error
:
1566 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1567 episode_urls
.add(task
.url
)
1568 # Tell the task that it has been removed (so it can clean up)
1569 task
.removed_from_list()
1571 # We can (hopefully) simply set the task status here
1572 task
.status
= status
1573 # Tell the podcasts tab to update icons for our removed podcasts
1574 self
.update_episode_list_icons(episode_urls
)
1575 # Update the tab title and downloads list
1576 self
.update_downloads_list()
1578 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1579 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1581 if not hasattr(treeview
, 'is_rubber_banding_active'):
1584 return not treeview
.is_rubber_banding_active()
1586 if event
.button
== self
.context_menu_mouse_button
:
1587 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1588 self
.downloads_list_get_selection(model
, paths
)
1590 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1591 # This creates a menu item for selection-wide actions
1592 item
= gtk
.ImageMenuItem(label
)
1593 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1594 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1595 item
.set_sensitive(sensitive
)
1596 return self
.set_finger_friendly(item
)
1600 item
= gtk
.ImageMenuItem(_('Episode details'))
1601 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1602 if len(selected_tasks
) == 1:
1603 row_reference
, task
= selected_tasks
[0]
1604 episode
= task
.episode
1605 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1607 item
.set_sensitive(False)
1608 menu
.append(self
.set_finger_friendly(item
))
1609 menu
.append(gtk
.SeparatorMenuItem())
1611 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1613 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1614 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1615 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1616 menu
.append(gtk
.SeparatorMenuItem())
1617 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1619 if gpodder
.ui
.maemo
:
1620 # Because we open the popup on left-click for Maemo,
1621 # we also include a non-action to close the menu
1622 menu
.append(gtk
.SeparatorMenuItem())
1623 item
= gtk
.ImageMenuItem(_('Close this menu'))
1624 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1626 menu
.append(self
.set_finger_friendly(item
))
1629 menu
.popup(None, None, None, event
.button
, event
.time
)
1632 def treeview_channels_show_context_menu(self
, treeview
, event
):
1633 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1637 # Check for valid channel id, if there's no id then
1638 # assume that it is a proxy channel or equivalent
1639 # and cannot be operated with right click
1640 if self
.active_channel
.id is None:
1643 if event
.button
== 3:
1648 item
= gtk
.ImageMenuItem( _('Open download folder'))
1649 item
.set_image( gtk
.image_new_from_icon_name(ICON('folder-open'), gtk
.ICON_SIZE_MENU
))
1650 item
.connect('activate', lambda x
: util
.gui_open(self
.active_channel
.save_dir
))
1653 item
= gtk
.ImageMenuItem( _('Update Feed'))
1654 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1655 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1656 item
.set_sensitive( not self
.updating_feed_cache
)
1659 item
= gtk
.ImageMenuItem(_('Update M3U playlist'))
1660 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1661 item
.connect('activate', self
.update_m3u_playlist_clicked
)
1664 if self
.active_channel
.link
:
1665 item
= gtk
.ImageMenuItem(_('Visit website'))
1666 item
.set_image(gtk
.image_new_from_icon_name(ICON('web-browser'), gtk
.ICON_SIZE_MENU
))
1667 item
.connect('activate', lambda w
: util
.open_website(self
.active_channel
.link
))
1670 if self
.active_channel
.channel_is_locked
:
1671 item
= gtk
.ImageMenuItem(_('Allow deletion of all episodes'))
1672 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1673 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1674 menu
.append(self
.set_finger_friendly(item
))
1676 item
= gtk
.ImageMenuItem(_('Prohibit deletion of all episodes'))
1677 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIALOG_AUTHENTICATION
, gtk
.ICON_SIZE_MENU
))
1678 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1679 menu
.append(self
.set_finger_friendly(item
))
1682 menu
.append( gtk
.SeparatorMenuItem())
1684 item
= gtk
.ImageMenuItem(gtk
.STOCK_EDIT
)
1685 item
.connect( 'activate', self
.on_itemEditChannel_activate
)
1688 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1689 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1693 # Disable tooltips while we are showing the menu, so
1694 # the tooltip will not appear over the menu
1695 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1696 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1697 menu
.popup( None, None, None, event
.button
, event
.time
)
1701 def on_itemClose_activate(self
, widget
):
1702 if self
.tray_icon
is not None:
1703 self
.iconify_main_window()
1705 self
.on_gPodder_delete_event(widget
)
1707 def cover_file_removed(self
, channel_url
):
1709 The Cover Downloader calls this when a previously-
1710 available cover has been removed from the disk. We
1711 have to update our model to reflect this change.
1713 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1715 def cover_download_finished(self
, channel_url
, pixbuf
):
1717 The Cover Downloader calls this when it has finished
1718 downloading (or registering, if already downloaded)
1719 a new channel cover, which is ready for displaying.
1721 self
.podcast_list_model
.add_cover_by_url(channel_url
, pixbuf
)
1723 def save_episodes_as_file(self
, episodes
):
1724 for episode
in episodes
:
1725 self
.save_episode_as_file(episode
)
1727 def save_episode_as_file(self
, episode
):
1728 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1729 if episode
.was_downloaded(and_exists
=True):
1730 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1731 copy_from
= episode
.local_filename(create
=False)
1732 assert copy_from
is not None
1733 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1734 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1735 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1737 def copy_episodes_bluetooth(self
, episodes
):
1738 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1740 def convert_and_send_thread(episode
):
1741 for episode
in episodes
:
1742 filename
= episode
.local_filename(create
=False)
1743 assert filename
is not None
1744 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1745 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1746 (base
, ext
) = os
.path
.splitext(filename
)
1747 if not destfile
.endswith(ext
):
1751 shutil
.copyfile(filename
, destfile
)
1752 util
.bluetooth_send_file(destfile
)
1754 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1755 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1757 util
.delete_file(destfile
)
1759 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1761 def get_device_name(self
):
1762 if self
.config
.device_type
== 'ipod':
1764 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1765 return _('MP3 player')
1767 return '(unknown device)'
1769 def _treeview_button_released(self
, treeview
, event
):
1770 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1771 dy
= int(abs(event
.y
-ypos
))
1772 dx
= int(event
.x
-xpos
)
1774 selection
= treeview
.get_selection()
1775 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1776 if path
is None or dy
> 30:
1777 return (False, dx
, dy
)
1779 path
, column
, x
, y
= path
1780 selection
.select_path(path
)
1781 treeview
.set_cursor(path
)
1782 treeview
.grab_focus()
1784 return (True, dx
, dy
)
1786 def treeview_channels_handle_gestures(self
, treeview
, event
):
1787 if self
.currently_updating
:
1790 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1793 if self
.config
.maemo_enable_gestures
:
1795 self
.on_itemUpdateChannel_activate()
1797 self
.on_itemEditChannel_activate(treeview
)
1801 def treeview_available_handle_gestures(self
, treeview
, event
):
1802 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1805 if self
.config
.maemo_enable_gestures
:
1807 self
.on_playback_selected_episodes(None)
1810 self
.on_shownotes_selected_episodes(None)
1813 # Pass the event to the context menu handler for treeAvailable
1814 self
.treeview_available_show_context_menu(treeview
, event
)
1818 def treeview_available_show_context_menu(self
, treeview
, event
):
1819 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1821 if not hasattr(treeview
, 'is_rubber_banding_active'):
1824 return not treeview
.is_rubber_banding_active()
1826 if event
.button
== self
.context_menu_mouse_button
:
1827 episodes
= self
.get_selected_episodes()
1828 any_locked
= any(e
.is_locked
for e
in episodes
)
1829 any_played
= any(e
.is_played
for e
in episodes
)
1830 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1831 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1832 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1836 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1838 if open_instead_of_play
:
1839 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1841 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1843 item
= gtk
.ImageMenuItem(_('Stream'))
1844 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1846 item
.set_sensitive(can_play
and not downloading
)
1847 item
.connect('activate', self
.on_playback_selected_episodes
)
1848 menu
.append(self
.set_finger_friendly(item
))
1851 item
= gtk
.ImageMenuItem(_('Download'))
1852 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1853 item
.set_sensitive(can_download
)
1854 item
.connect('activate', self
.on_download_selected_episodes
)
1855 menu
.append(self
.set_finger_friendly(item
))
1857 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1858 item
.connect('activate', self
.on_item_cancel_download_activate
)
1859 menu
.append(self
.set_finger_friendly(item
))
1861 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1862 item
.set_sensitive(can_delete
)
1863 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1864 menu
.append(self
.set_finger_friendly(item
))
1868 # Ok, this probably makes sense to only display for downloaded files
1870 menu
.append(gtk
.SeparatorMenuItem())
1871 share_item
= gtk
.MenuItem(_('Send to'))
1872 menu
.append(share_item
)
1873 share_menu
= gtk
.Menu()
1875 item
= gtk
.ImageMenuItem(_('Local folder'))
1876 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
1877 item
.connect('activate', lambda w
, ee
: self
.save_episodes_as_file(ee
), episodes
)
1878 share_menu
.append(self
.set_finger_friendly(item
))
1879 if self
.bluetooth_available
:
1880 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
1881 item
.set_image(gtk
.image_new_from_icon_name(ICON('bluetooth'), gtk
.ICON_SIZE_MENU
))
1882 item
.connect('activate', lambda w
, ee
: self
.copy_episodes_bluetooth(ee
), episodes
)
1883 share_menu
.append(self
.set_finger_friendly(item
))
1885 item
= gtk
.ImageMenuItem(self
.get_device_name())
1886 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
1887 item
.connect('activate', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, ee
), episodes
)
1888 share_menu
.append(self
.set_finger_friendly(item
))
1890 share_item
.set_submenu(share_menu
)
1892 if (downloaded
or one_is_new
or can_download
) and not downloading
:
1893 menu
.append(gtk
.SeparatorMenuItem())
1895 item
= gtk
.CheckMenuItem(_('New'))
1896 item
.set_active(True)
1897 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1898 menu
.append(self
.set_finger_friendly(item
))
1900 item
= gtk
.CheckMenuItem(_('New'))
1901 item
.set_active(False)
1902 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
1903 menu
.append(self
.set_finger_friendly(item
))
1906 item
= gtk
.CheckMenuItem(_('Played'))
1907 item
.set_active(any_played
)
1908 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
1909 menu
.append(self
.set_finger_friendly(item
))
1911 item
= gtk
.CheckMenuItem(_('Keep episode'))
1912 item
.set_active(any_locked
)
1913 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
1914 menu
.append(self
.set_finger_friendly(item
))
1916 menu
.append(gtk
.SeparatorMenuItem())
1917 # Single item, add episode information menu item
1918 item
= gtk
.ImageMenuItem(_('Episode details'))
1919 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1920 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
1921 menu
.append(self
.set_finger_friendly(item
))
1923 if gpodder
.ui
.maemo
:
1924 # Because we open the popup on left-click for Maemo,
1925 # we also include a non-action to close the menu
1926 menu
.append(gtk
.SeparatorMenuItem())
1927 item
= gtk
.ImageMenuItem(_('Close this menu'))
1928 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1929 menu
.append(self
.set_finger_friendly(item
))
1932 # Disable tooltips while we are showing the menu, so
1933 # the tooltip will not appear over the menu
1934 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
1935 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
1936 menu
.popup( None, None, None, event
.button
, event
.time
)
1940 def set_title(self
, new_title
):
1941 if not gpodder
.ui
.fremantle
:
1942 self
.default_title
= new_title
1943 self
.gPodder
.set_title(new_title
)
1945 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
1947 Updates the status icons in the episode list.
1949 If urls is given, it should be a list of URLs
1950 of episodes that should be updated.
1952 If urls is None, set ONE OF selected, all to
1953 True (the former updates just the selected
1954 episodes and the latter updates all episodes).
1956 additional_args
= (self
.episode_is_downloading
, \
1957 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
1958 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
1960 if urls
is not None:
1961 # We have a list of URLs to walk through
1962 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
1963 elif selected
and not all
:
1964 # We should update all selected episodes
1965 selection
= self
.treeAvailable
.get_selection()
1966 model
, paths
= selection
.get_selected_rows()
1967 for path
in reversed(paths
):
1968 iter = model
.get_iter(path
)
1969 self
.episode_list_model
.update_by_filter_iter(iter, \
1971 elif all
and not selected
:
1972 # We update all (even the filter-hidden) episodes
1973 self
.episode_list_model
.update_all(*additional_args
)
1975 # Wrong/invalid call - have to specify at least one parameter
1976 raise ValueError('Invalid call to update_episode_list_icons')
1978 def episode_list_status_changed(self
, episodes
):
1979 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
1980 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
1983 def clean_up_downloads(self
, delete_partial
=False):
1984 # Clean up temporary files left behind by old gPodder versions
1985 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
1988 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
1990 for tempfile
in temporary_files
:
1991 util
.delete_file(tempfile
)
1993 # Clean up empty download folders and abandoned download folders
1994 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
1995 for ddir
in download_dirs
:
1996 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
1997 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
1998 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
1999 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2000 shutil
.rmtree(ddir
, ignore_errors
=True)
2002 def streaming_possible(self
):
2003 if gpodder
.ui
.desktop
:
2004 # User has to have a media player set on the Desktop, or else we
2005 # would probably open the browser when giving a URL to xdg-open..
2006 return (self
.config
.player
and self
.config
.player
!= 'default')
2007 elif gpodder
.ui
.maemo
:
2008 # On Maemo, the default is to use the Nokia Media Player, which is
2009 # already able to deal with HTTP URLs the right way, so we
2010 # unconditionally enable streaming always on Maemo
2015 def playback_episodes_for_real(self
, episodes
):
2016 groups
= collections
.defaultdict(list)
2017 for episode
in episodes
:
2018 file_type
= episode
.file_type()
2019 if file_type
== 'video' and self
.config
.videoplayer
and \
2020 self
.config
.videoplayer
!= 'default':
2021 player
= self
.config
.videoplayer
2022 if gpodder
.ui
.diablo
:
2023 # Use the wrapper script if it's installed to crop 3GP YouTube
2024 # videos to fit the screen (looks much nicer than w/ black border)
2025 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2026 player
= 'gpodder-mplayer'
2027 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2028 player
= 'mplayer -fs %F'
2029 elif file_type
== 'audio' and self
.config
.player
and \
2030 self
.config
.player
!= 'default':
2031 player
= self
.config
.player
2035 if file_type
not in ('audio', 'video') or \
2036 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2037 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2038 # Mark episode as played in the database
2039 episode
.mark(is_played
=True)
2040 self
.mygpo_client
.on_playback([episode
])
2042 filename
= episode
.local_filename(create
=False)
2043 if filename
is None or not os
.path
.exists(filename
):
2044 filename
= episode
.url
2045 if youtube
.is_video_link(filename
):
2046 fmt_id
= self
.config
.youtube_preferred_fmt_id
2047 if gpodder
.ui
.fremantle
:
2049 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2050 groups
[player
].append(filename
)
2052 # Open episodes with system default player
2053 if 'default' in groups
:
2054 for filename
in groups
['default']:
2055 log('Opening with system default: %s', filename
, sender
=self
)
2056 util
.gui_open(filename
)
2057 del groups
['default']
2058 elif gpodder
.ui
.maemo
:
2059 # When on Maemo and not opening with default, show a notification
2060 # (no startup notification for Panucci / MPlayer yet...)
2061 if len(episodes
) == 1:
2062 text
= _('Opening %s') % episodes
[0].title
2064 count
= len(episodes
)
2065 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2067 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2069 def destroy_banner_later(banner
):
2072 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2074 # For each type now, go and create play commands
2075 for group
in groups
:
2076 for command
in util
.format_desktop_command(group
, groups
[group
]):
2077 log('Executing: %s', repr(command
), sender
=self
)
2078 subprocess
.Popen(command
)
2080 # Persist episode status changes to the database
2083 # Flush updated episode status
2084 self
.mygpo_client
.flush()
2086 def playback_episodes(self
, episodes
):
2087 # We need to create a list, because we run through it more than once
2088 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2089 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2092 self
.playback_episodes_for_real(episodes
)
2093 except Exception, e
:
2094 log('Error in playback!', sender
=self
, traceback
=True)
2095 if gpodder
.ui
.desktop
:
2096 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2097 _('Error opening player'), widget
=self
.toolPreferences
)
2099 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2101 channel_urls
= set()
2102 episode_urls
= set()
2103 for episode
in episodes
:
2104 channel_urls
.add(episode
.channel
.url
)
2105 episode_urls
.add(episode
.url
)
2106 self
.update_episode_list_icons(episode_urls
)
2107 self
.update_podcast_list_model(channel_urls
)
2109 def play_or_download(self
):
2110 if not gpodder
.ui
.fremantle
:
2111 if self
.wNotebook
.get_current_page() > 0:
2112 if gpodder
.ui
.desktop
:
2113 self
.toolCancel
.set_sensitive(True)
2116 if self
.currently_updating
:
2117 return (False, False, False, False, False, False)
2119 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2120 ( is_played
, is_locked
) = (False,)*2
2122 open_instead_of_play
= False
2124 selection
= self
.treeAvailable
.get_selection()
2125 if selection
.count_selected_rows() > 0:
2126 (model
, paths
) = selection
.get_selected_rows()
2129 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2131 if episode
.file_type() not in ('audio', 'video'):
2132 open_instead_of_play
= True
2134 if episode
.was_downloaded():
2135 can_play
= episode
.was_downloaded(and_exists
=True)
2136 is_played
= episode
.is_played
2137 is_locked
= episode
.is_locked
2141 if self
.episode_is_downloading(episode
):
2146 can_download
= can_download
and not can_cancel
2147 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2148 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2149 can_delete
= not can_cancel
2151 if gpodder
.ui
.desktop
:
2152 if open_instead_of_play
:
2153 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2155 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2156 self
.toolPlay
.set_sensitive( can_play
)
2157 self
.toolDownload
.set_sensitive( can_download
)
2158 self
.toolTransfer
.set_sensitive( can_transfer
)
2159 self
.toolCancel
.set_sensitive( can_cancel
)
2161 if not gpodder
.ui
.fremantle
:
2162 self
.item_cancel_download
.set_sensitive(can_cancel
)
2163 self
.itemDownloadSelected
.set_sensitive(can_download
)
2164 self
.itemOpenSelected
.set_sensitive(can_play
)
2165 self
.itemPlaySelected
.set_sensitive(can_play
)
2166 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2167 self
.item_toggle_played
.set_sensitive(can_play
)
2168 self
.item_toggle_lock
.set_sensitive(can_play
)
2169 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2170 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2172 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2174 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2175 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2177 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2178 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2180 def episode_new_status_changed(self
, urls
):
2181 self
.update_podcast_list_model()
2182 self
.update_episode_list_icons(urls
)
2184 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2185 """Update the podcast list treeview model
2187 If urls is given, it should list the URLs of each
2188 podcast that has to be updated in the list.
2190 If selected is True, only update the model contents
2191 for the currently-selected podcast - nothing more.
2193 The caller can optionally specify "select_url",
2194 which is the URL of the podcast that is to be
2195 selected in the list after the update is complete.
2196 This only works if the podcast list has to be
2197 reloaded; i.e. something has been added or removed
2198 since the last update of the podcast list).
2200 selection
= self
.treeChannels
.get_selection()
2201 model
, iter = selection
.get_selected()
2203 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2204 # Update "all episodes" view in any case (if enabled)
2205 self
.podcast_list_model
.update_first_row()
2208 # very cheap! only update selected channel
2209 if iter is not None:
2210 # If we have selected the "all episodes" view, we have
2211 # to update all channels for selected episodes:
2212 if self
.config
.podcast_list_view_all
and \
2213 self
.podcast_list_model
.iter_is_first_row(iter):
2214 urls
= self
.get_podcast_urls_from_selected_episodes()
2215 self
.podcast_list_model
.update_by_urls(urls
)
2217 # Otherwise just update the selected row (a podcast)
2218 self
.podcast_list_model
.update_by_filter_iter(iter)
2219 elif not self
.channel_list_changed
:
2220 # we can keep the model, but have to update some
2222 # still cheaper than reloading the whole list
2223 self
.podcast_list_model
.update_all()
2225 # ok, we got a bunch of urls to update
2226 self
.podcast_list_model
.update_by_urls(urls
)
2228 if model
and iter and select_url
is None:
2229 # Get the URL of the currently-selected podcast
2230 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2232 # Update the podcast list model with new channels
2233 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2236 selected_iter
= model
.get_iter_first()
2237 # Find the previously-selected URL in the new
2238 # model if we have an URL (else select first)
2239 if select_url
is not None:
2240 pos
= model
.get_iter_first()
2241 while pos
is not None:
2242 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2243 if url
== select_url
:
2246 pos
= model
.iter_next(pos
)
2248 if not gpodder
.ui
.fremantle
:
2249 if selected_iter
is not None:
2250 selection
.select_iter(selected_iter
)
2251 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2253 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2254 self
.channel_list_changed
= False
2256 def episode_is_downloading(self
, episode
):
2257 """Returns True if the given episode is being downloaded at the moment"""
2261 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2263 def update_episode_list_model(self
):
2264 if self
.channels
and self
.active_channel
is not None:
2265 if gpodder
.ui
.fremantle
:
2266 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2268 self
.currently_updating
= True
2269 self
.episode_list_model
.clear()
2270 self
.episode_list_model
.reset_update_progress()
2271 self
.treeAvailable
.set_model(self
.empty_episode_list_model
)
2272 def do_update_episode_list_model():
2273 additional_args
= (self
.episode_is_downloading
, \
2274 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2275 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
, \
2277 self
.episode_list_model
.add_from_channel(self
.active_channel
, *additional_args
)
2279 def on_episode_list_model_updated():
2280 if gpodder
.ui
.fremantle
:
2281 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2282 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
2283 self
.treeAvailable
.columns_autosize()
2284 self
.currently_updating
= False
2285 self
.play_or_download()
2286 util
.idle_add(on_episode_list_model_updated
)
2287 threading
.Thread(target
=do_update_episode_list_model
).start()
2289 self
.episode_list_model
.clear()
2291 def offer_new_episodes(self
, channels
=None):
2292 new_episodes
= self
.get_new_episodes(channels
)
2294 self
.new_episodes_show(new_episodes
)
2298 def add_podcast_list(self
, urls
, auth_tokens
=None):
2299 """Subscribe to a list of podcast given their URLs
2301 If auth_tokens is given, it should be a dictionary
2302 mapping URLs to (username, password) tuples."""
2304 if auth_tokens
is None:
2307 # Sort and split the URL list into five buckets
2308 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2309 for input_url
in urls
:
2310 url
= util
.normalize_feed_url(input_url
)
2312 # Fail this one because the URL is not valid
2313 failed
.append(input_url
)
2314 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2315 # A podcast already exists in the list for this URL
2316 existing
.append(url
)
2318 # This URL has survived the first round - queue for add
2320 if url
!= input_url
and input_url
in auth_tokens
:
2321 auth_tokens
[url
] = auth_tokens
[input_url
]
2326 progress
= ProgressIndicator(_('Adding podcasts'), \
2327 _('Please wait while episode information is downloaded.'), \
2328 parent
=self
.main_window
)
2330 def on_after_update():
2331 progress
.on_finished()
2332 # Report already-existing subscriptions to the user
2334 title
= _('Existing subscriptions skipped')
2335 message
= _('You are already subscribed to these podcasts:') \
2336 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2337 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2339 # Report subscriptions that require authentication
2343 title
= _('Podcast requires authentication')
2344 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2345 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2347 retry_podcasts
[url
] = auth_tokens
2349 # Stop asking the user for more login data
2352 error_messages
[url
] = _('Authentication failed')
2356 # If we have authentication data to retry, do so here
2358 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2360 # Report website redirections
2361 for url
in redirections
:
2362 title
= _('Website redirection detected')
2363 message
= _('The URL %(url)s redirects to %(target)s.') \
2364 + '\n\n' + _('Do you want to visit the website now?')
2365 message
= message
% {'url': url
, 'target': redirections
[url
]}
2366 if self
.show_confirmation(message
, title
):
2367 util
.open_website(url
)
2371 # Report failed subscriptions to the user
2373 title
= _('Could not add some podcasts')
2374 message
= _('Some podcasts could not be added to your list:') \
2375 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2376 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2377 self
.show_message(message
, title
, important
=True)
2379 # Upload subscription changes to gpodder.net
2380 self
.mygpo_client
.on_subscribe(worked
)
2382 # If at least one podcast has been added, save and update all
2383 if self
.channel_list_changed
:
2384 # Fix URLs if mygpo has rewritten them
2385 self
.rewrite_urls_mygpo()
2387 self
.save_channels_opml()
2389 # If only one podcast was added, select it after the update
2390 if len(worked
) == 1:
2395 # Update the list of subscribed podcasts
2396 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2397 self
.update_podcasts_tab()
2399 # Offer to download new episodes
2400 self
.offer_new_episodes(channels
=[c
for c
in self
.channels
if c
.url
in worked
])
2403 # After the initial sorting and splitting, try all queued podcasts
2404 length
= len(queued
)
2405 for index
, url
in enumerate(queued
):
2406 progress
.on_progress(float(index
)/float(length
))
2407 progress
.on_message(url
)
2408 log('QUEUE RUNNER: %s', url
, sender
=self
)
2410 # The URL is valid and does not exist already - subscribe!
2411 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2412 authentication_tokens
=auth_tokens
.get(url
, None), \
2413 max_episodes
=self
.config
.max_episodes_per_feed
, \
2414 download_dir
=self
.config
.download_dir
, \
2415 allow_empty_feeds
=self
.config
.allow_empty_feeds
)
2418 username
, password
= util
.username_password_from_url(url
)
2419 except ValueError, ve
:
2420 username
, password
= (None, None)
2422 if username
is not None and channel
.username
is None and \
2423 password
is not None and channel
.password
is None:
2424 channel
.username
= username
2425 channel
.password
= password
2428 self
._update
_cover
(channel
)
2429 except feedcore
.AuthenticationRequired
:
2430 if url
in auth_tokens
:
2431 # Fail for wrong authentication data
2432 error_messages
[url
] = _('Authentication failed')
2435 # Queue for login dialog later
2438 except feedcore
.WifiLogin
, error
:
2439 redirections
[url
] = error
.data
2441 error_messages
[url
] = _('Redirection detected')
2443 except Exception, e
:
2444 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2445 error_messages
[url
] = str(e
)
2449 assert channel
is not None
2450 worked
.append(channel
.url
)
2451 self
.channels
.append(channel
)
2452 self
.channel_list_changed
= True
2453 util
.idle_add(on_after_update
)
2454 threading
.Thread(target
=thread_proc
).start()
2456 def save_channels_opml(self
):
2457 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2458 return exporter
.write(self
.channels
)
2460 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2462 self
.updating_feed_cache
= False
2464 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2465 self
.channel_list_changed
= True
2466 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2468 # Only search for new episodes in podcasts that have been
2469 # updated, not in other podcasts (for single-feed updates)
2470 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2472 if gpodder
.ui
.fremantle
:
2473 self
.button_subscribe
.set_sensitive(True)
2474 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2475 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
2476 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2477 self
.update_podcasts_tab()
2478 if self
.feed_cache_update_cancelled
:
2482 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2483 # New episodes found, but we should do nothing
2484 self
.show_message(_('New episodes are available.'))
2485 elif self
.config
.auto_download
== 'always':
2486 count
= len(episodes
)
2487 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2488 self
.show_message(title
)
2489 self
.download_episode_list(episodes
)
2490 elif self
.config
.auto_download
== 'queue':
2491 self
.show_message(_('New episodes have been added to the download list.'))
2492 self
.download_episode_list_paused(episodes
)
2494 self
.new_episodes_show(episodes
)
2495 elif not self
.config
.auto_update_feeds
:
2496 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2500 self
.tray_icon
.set_status()
2502 if self
.feed_cache_update_cancelled
:
2503 # The user decided to abort the feed update
2504 self
.show_update_feeds_buttons()
2506 # Nothing new here - but inform the user
2507 self
.pbFeedUpdate
.set_fraction(1.0)
2508 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2509 self
.feed_cache_update_cancelled
= True
2510 self
.btnCancelFeedUpdate
.show()
2511 self
.btnCancelFeedUpdate
.set_sensitive(True)
2512 if gpodder
.ui
.maemo
:
2513 # btnCancelFeedUpdate is a ToolButton on Maemo
2514 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2516 # btnCancelFeedUpdate is a normal gtk.Button
2517 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2519 count
= len(episodes
)
2520 # New episodes are available
2521 self
.pbFeedUpdate
.set_fraction(1.0)
2522 # Are we minimized and should we auto download?
2523 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2524 self
.download_episode_list(episodes
)
2525 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2526 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2527 self
.show_update_feeds_buttons()
2528 elif self
.config
.auto_download
== 'queue':
2529 self
.download_episode_list_paused(episodes
)
2530 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2531 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2532 self
.show_update_feeds_buttons()
2534 self
.show_update_feeds_buttons()
2535 # New episodes are available and we are not minimized
2536 if not self
.config
.do_not_show_new_episodes_dialog
:
2537 self
.new_episodes_show(episodes
, notification
=True)
2539 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2540 self
.pbFeedUpdate
.set_text(message
)
2542 def _update_cover(self
, channel
):
2543 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2544 self
.cover_downloader
.request_cover(channel
)
2546 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2547 total
= len(channels
)
2549 for updated
, channel
in enumerate(channels
):
2550 if not self
.feed_cache_update_cancelled
:
2552 # Update if timeout is not reached or we update a single podcast or skipping is disabled
2553 if channel
.query_automatic_update() or total
== 1 or not self
.config
.feed_update_skipping
:
2554 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
)
2556 log('Skipping update of %s (see feed_update_skipping)', channel
.title
, sender
=self
)
2557 self
._update
_cover
(channel
)
2558 except Exception, e
:
2559 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2561 message
= _('Error while updating %(url)s: %(message)s')
2563 message
= _('The feed at %(url)s could not be updated.')
2564 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2565 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2567 if self
.feed_cache_update_cancelled
:
2570 if gpodder
.ui
.fremantle
:
2571 util
.idle_add(self
.button_refresh
.set_title
, \
2572 _('%(position)d/%(total)d updated') % {'position': updated
, 'total': total
})
2575 # By the time we get here the update may have already been cancelled
2576 if not self
.feed_cache_update_cancelled
:
2577 def update_progress():
2578 d
= {'podcast': channel
.title
, 'position': updated
, 'total': total
}
2579 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2580 self
.pbFeedUpdate
.set_text(progression
)
2582 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2583 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2584 util
.idle_add(update_progress
)
2586 updated_urls
= [c
.url
for c
in channels
]
2587 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2589 def show_update_feeds_buttons(self
):
2590 # Make sure that the buttons for updating feeds
2591 # appear - this should happen after a feed update
2592 if gpodder
.ui
.maemo
:
2593 self
.btnUpdateSelectedFeed
.show()
2594 self
.toolFeedUpdateProgress
.hide()
2595 self
.btnCancelFeedUpdate
.hide()
2596 self
.btnCancelFeedUpdate
.set_is_important(False)
2597 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2598 self
.toolbarSpacer
.set_expand(True)
2599 self
.toolbarSpacer
.set_draw(False)
2601 self
.hboxUpdateFeeds
.hide()
2602 self
.btnUpdateFeeds
.show()
2603 self
.itemUpdate
.set_sensitive(True)
2604 self
.itemUpdateChannel
.set_sensitive(True)
2606 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2607 if not self
.feed_cache_update_cancelled
:
2608 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2609 self
.feed_cache_update_cancelled
= True
2610 self
.btnCancelFeedUpdate
.set_sensitive(False)
2612 self
.show_update_feeds_buttons()
2614 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2615 if self
.updating_feed_cache
:
2616 if gpodder
.ui
.fremantle
:
2617 self
.feed_cache_update_cancelled
= True
2620 if not force_update
:
2621 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2622 self
.channel_list_changed
= True
2623 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2626 # Fix URLs if mygpo has rewritten them
2627 self
.rewrite_urls_mygpo()
2629 self
.updating_feed_cache
= True
2631 if channels
is None:
2632 channels
= self
.channels
2634 if gpodder
.ui
.fremantle
:
2635 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2636 self
.button_refresh
.set_title(_('Updating...'))
2637 self
.button_subscribe
.set_sensitive(False)
2638 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2639 self
.ICON_GENERAL_CLOSE
, gtk
.ICON_SIZE_BUTTON
))
2640 self
.feed_cache_update_cancelled
= False
2642 self
.itemUpdate
.set_sensitive(False)
2643 self
.itemUpdateChannel
.set_sensitive(False)
2646 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2648 if len(channels
) == 1:
2649 text
= _('Updating "%s"...') % channels
[0].title
2651 count
= len(channels
)
2652 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2653 self
.pbFeedUpdate
.set_text(text
)
2654 self
.pbFeedUpdate
.set_fraction(0)
2656 self
.feed_cache_update_cancelled
= False
2657 self
.btnCancelFeedUpdate
.show()
2658 self
.btnCancelFeedUpdate
.set_sensitive(True)
2659 if gpodder
.ui
.maemo
:
2660 self
.toolbarSpacer
.set_expand(False)
2661 self
.toolbarSpacer
.set_draw(True)
2662 self
.btnUpdateSelectedFeed
.hide()
2663 self
.toolFeedUpdateProgress
.show_all()
2665 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2666 self
.hboxUpdateFeeds
.show_all()
2667 self
.btnUpdateFeeds
.hide()
2669 args
= (channels
, select_url_afterwards
)
2670 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2672 def on_gPodder_delete_event(self
, widget
, *args
):
2673 """Called when the GUI wants to close the window
2674 Displays a confirmation dialog (and closes/hides gPodder)
2677 downloading
= self
.download_status_model
.are_downloads_in_progress()
2679 # Only iconify if we are using the window's "X" button,
2680 # but not when we are using "Quit" in the menu or toolbar
2681 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
2682 self
.iconify_main_window()
2683 elif self
.config
.on_quit_ask
or downloading
:
2684 if gpodder
.ui
.fremantle
:
2685 self
.close_gpodder()
2686 elif gpodder
.ui
.diablo
:
2687 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2689 self
.close_gpodder()
2692 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2693 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2694 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2696 title
= _('Quit gPodder')
2698 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2700 message
= _('Do you really want to quit gPodder now?')
2702 dialog
.set_title(title
)
2703 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2705 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
2706 dialog
.vbox
.pack_start(cb_ask
)
2709 quit_button
.grab_focus()
2710 result
= dialog
.run()
2713 if result
== gtk
.RESPONSE_CLOSE
:
2714 if not downloading
and cb_ask
.get_active() == True:
2715 self
.config
.on_quit_ask
= False
2716 self
.close_gpodder()
2718 self
.close_gpodder()
2722 def close_gpodder(self
):
2723 """ clean everything and exit properly
2726 if self
.save_channels_opml():
2727 pass # FIXME: Add mygpo synchronization here
2729 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
2733 if self
.tray_icon
is not None:
2734 self
.tray_icon
.set_visible(False)
2736 # Notify all tasks to to carry out any clean-up actions
2737 self
.download_status_model
.tell_all_tasks_to_quit()
2739 while gtk
.events_pending():
2740 gtk
.main_iteration(False)
2747 def get_expired_episodes(self
):
2748 for channel
in self
.channels
:
2749 for episode
in channel
.get_downloaded_episodes():
2750 # Never consider locked episodes as old
2751 if episode
.is_locked
:
2754 # Never consider fresh episodes as old
2755 if episode
.age_in_days() < self
.config
.episode_old_age
:
2758 # Do not delete played episodes (except if configured)
2759 if episode
.is_played
:
2760 if not self
.config
.auto_remove_played_episodes
:
2763 # Do not delete unplayed episodes (except if configured)
2764 if not episode
.is_played
:
2765 if not self
.config
.auto_remove_unplayed_episodes
:
2770 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
2775 episodes
= [e
for e
in episodes
if not e
.is_locked
]
2778 title
= _('Episodes are locked')
2779 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2780 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2783 count
= len(episodes
)
2784 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
2785 message
= _('Deleting episodes removes downloaded files.')
2787 if gpodder
.ui
.fremantle
:
2788 message
= '\n'.join([title
, message
])
2790 if confirm
and not self
.show_confirmation(message
, title
):
2793 progress
= ProgressIndicator(_('Deleting episodes'), \
2794 _('Please wait while episodes are deleted'), \
2795 parent
=self
.main_window
)
2797 def finish_deletion(episode_urls
, channel_urls
):
2798 progress
.on_finished()
2800 # Episodes have been deleted - persist the database
2803 self
.update_episode_list_icons(episode_urls
)
2804 self
.update_podcast_list_model(channel_urls
)
2805 self
.play_or_download()
2808 episode_urls
= set()
2809 channel_urls
= set()
2811 episodes_status_update
= []
2812 for idx
, episode
in enumerate(episodes
):
2813 progress
.on_progress(float(idx
)/float(len(episodes
)))
2814 if episode
.is_locked
:
2815 log('Not deleting episode (is locked): %s', episode
.title
)
2817 log('Deleting episode: %s', episode
.title
)
2818 progress
.on_message(episode
.title
)
2819 episode
.delete_from_disk()
2820 episode_urls
.add(episode
.url
)
2821 channel_urls
.add(episode
.channel
.url
)
2822 episodes_status_update
.append(episode
)
2824 # Tell the shownotes window that we have removed the episode
2825 if self
.episode_shownotes_window
is not None and \
2826 self
.episode_shownotes_window
.episode
is not None and \
2827 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
2828 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
2830 # Notify the web service about the status update + upload
2831 self
.mygpo_client
.on_delete(episodes_status_update
)
2832 self
.mygpo_client
.flush()
2834 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
2836 threading
.Thread(target
=thread_proc
).start()
2840 def on_itemRemoveOldEpisodes_activate( self
, widget
):
2841 if gpodder
.ui
.maemo
:
2843 ('maemo_remove_markup', None, None, _('Episode')),
2847 ('title_markup', None, None, _('Episode')),
2848 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
2849 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
2850 ('played_prop', None, None, _('Status')),
2851 ('age_prop', None, None, _('Downloaded')),
2854 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
2855 selection_buttons
= {
2856 _('Select played'): lambda episode
: episode
.is_played
,
2857 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
2860 instructions
= _('Select the episodes you want to delete:')
2864 for channel
in self
.channels
:
2865 for episode
in channel
.get_downloaded_episodes():
2866 # Disallow deletion of locked episodes that still exist
2867 if not episode
.is_locked
or not episode
.file_exists():
2868 episodes
.append(episode
)
2869 # Automatically select played and file-less episodes
2870 selected
.append(episode
.is_played
or \
2871 not episode
.file_exists())
2873 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
2874 episodes
= episodes
, selected
= selected
, columns
= columns
, \
2875 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
2876 selection_buttons
= selection_buttons
, _config
=self
.config
, \
2877 show_episode_shownotes
=self
.show_episode_shownotes
)
2879 def on_selected_episodes_status_changed(self
):
2880 self
.update_episode_list_icons(selected
=True)
2881 self
.update_podcast_list_model(selected
=True)
2884 def mark_selected_episodes_new(self
):
2885 for episode
in self
.get_selected_episodes():
2887 self
.on_selected_episodes_status_changed()
2889 def mark_selected_episodes_old(self
):
2890 for episode
in self
.get_selected_episodes():
2892 self
.on_selected_episodes_status_changed()
2894 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
2895 for episode
in self
.get_selected_episodes():
2897 episode
.mark(is_played
=not episode
.is_played
)
2899 episode
.mark(is_played
=new_value
)
2900 self
.on_selected_episodes_status_changed()
2902 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2903 for episode
in self
.get_selected_episodes():
2905 episode
.mark(is_locked
=not episode
.is_locked
)
2907 episode
.mark(is_locked
=new_value
)
2908 self
.on_selected_episodes_status_changed()
2910 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
2911 if self
.active_channel
is None:
2914 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
2915 self
.active_channel
.update_channel_lock()
2917 for episode
in self
.active_channel
.get_all_episodes():
2918 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
2920 self
.update_podcast_list_model(selected
=True)
2921 self
.update_episode_list_icons(all
=True)
2923 def on_itemUpdateChannel_activate(self
, widget
=None):
2924 if self
.active_channel
is None:
2925 title
= _('No podcast selected')
2926 message
= _('Please select a podcast in the podcasts list to update.')
2927 self
.show_message( message
, title
, widget
=self
.treeChannels
)
2930 self
.update_feed_cache(channels
=[self
.active_channel
])
2932 def on_itemUpdate_activate(self
, widget
=None):
2933 # Check if we have outstanding subscribe/unsubscribe actions
2934 if self
.on_add_remove_podcasts_mygpo():
2935 log('Update cancelled (received server changes)', sender
=self
)
2939 self
.update_feed_cache()
2941 gPodderWelcome(self
.gPodder
,
2942 center_on_widget
=self
.gPodder
,
2943 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
2944 setup_my_gpodder_callback
=self
.on_mygpo_settings_activate
)
2946 def download_episode_list_paused(self
, episodes
):
2947 self
.download_episode_list(episodes
, True)
2949 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
2950 for episode
in episodes
:
2951 log('Downloading episode: %s', episode
.title
, sender
= self
)
2952 if not episode
.was_downloaded(and_exists
=True):
2954 for task
in self
.download_tasks_seen
:
2955 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
2956 self
.download_queue_manager
.add_task(task
, force_start
)
2957 self
.enable_download_list_update()
2965 task
= download
.DownloadTask(episode
, self
.config
)
2966 except Exception, e
:
2967 d
= {'episode': episode
.title
, 'message': str(e
)}
2968 message
= _('Download error while downloading %(episode)s: %(message)s')
2969 self
.show_message(message
% d
, _('Download error'), important
=True)
2970 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
2974 task
.status
= task
.PAUSED
2976 self
.mygpo_client
.on_download([task
.episode
])
2977 self
.download_queue_manager
.add_task(task
, force_start
)
2979 self
.download_status_model
.register_task(task
)
2980 self
.enable_download_list_update()
2982 # Flush updated episode status
2983 self
.mygpo_client
.flush()
2985 def cancel_task_list(self
, tasks
):
2990 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
2991 task
.status
= task
.CANCELLED
2992 elif task
.status
== task
.PAUSED
:
2993 task
.status
= task
.CANCELLED
2994 # Call run, so the partial file gets deleted
2997 self
.update_episode_list_icons([task
.url
for task
in tasks
])
2998 self
.play_or_download()
3000 # Update the tab title and downloads list
3001 self
.update_downloads_list()
3003 def new_episodes_show(self
, episodes
, notification
=False):
3004 if gpodder
.ui
.maemo
:
3006 ('maemo_markup', None, None, _('Episode')),
3008 show_notification
= notification
3011 ('title_markup', None, None, _('Episode')),
3012 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3013 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3015 show_notification
= False
3017 instructions
= _('Select the episodes you want to download:')
3019 if self
.new_episodes_window
is not None:
3020 self
.new_episodes_window
.main_window
.destroy()
3021 self
.new_episodes_window
= None
3023 def download_episodes_callback(episodes
):
3024 self
.new_episodes_window
= None
3025 self
.download_episode_list(episodes
)
3027 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3028 title
=_('New episodes available'), \
3029 instructions
=instructions
, \
3030 episodes
=episodes
, \
3032 selected_default
=True, \
3033 stock_ok_button
= 'gpodder-download', \
3034 callback
=download_episodes_callback
, \
3035 remove_callback
=lambda e
: e
.mark_old(), \
3036 remove_action
=_('Mark as old'), \
3037 remove_finished
=self
.episode_new_status_changed
, \
3038 _config
=self
.config
, \
3039 show_notification
=show_notification
, \
3040 show_episode_shownotes
=self
.show_episode_shownotes
)
3042 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3043 if not self
.offer_new_episodes():
3044 self
.show_message(_('Please check for new episodes later.'), \
3045 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3047 def get_new_episodes(self
, channels
=None):
3048 if channels
is None:
3049 channels
= self
.channels
3051 for channel
in channels
:
3052 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3053 episodes
.append(episode
)
3057 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3058 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3060 def commit_changes_to_database(self
):
3061 """This will be called after the sync process is finished"""
3064 def on_cleanup_ipod_activate(self
, widget
, *args
):
3065 self
.sync_ui
.on_cleanup_device()
3067 def on_manage_device_playlist(self
, widget
):
3068 self
.sync_ui
.on_manage_device_playlist()
3070 def show_hide_tray_icon(self
):
3071 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3072 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3073 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3074 self
.tray_icon
.set_visible(False)
3076 self
.tray_icon
= None
3078 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3079 self
.tray_icon
.set_visible(self
.is_iconified())
3080 elif self
.tray_icon
:
3081 self
.tray_icon
.set_visible(True)
3083 def on_itemShowAllEpisodes_activate(self
, widget
):
3084 self
.config
.podcast_list_view_all
= widget
.get_active()
3086 def on_itemShowToolbar_activate(self
, widget
):
3087 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3089 def on_itemShowDescription_activate(self
, widget
):
3090 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3092 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3093 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3094 if self
.config
.podcast_list_hide_boring
:
3095 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3097 self
.podcast_list_model
.set_view_mode(-1)
3099 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3101 if current
== self
.item_view_podcasts_all
:
3102 self
.podcast_list_model
.set_view_mode(-1)
3103 elif current
== self
.item_view_podcasts_downloaded
:
3104 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3105 elif current
== self
.item_view_podcasts_unplayed
:
3106 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3108 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3110 def on_item_view_episodes_changed(self
, radioaction
, current
):
3111 if current
== self
.item_view_episodes_all
:
3112 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3113 elif current
== self
.item_view_episodes_undeleted
:
3114 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3115 elif current
== self
.item_view_episodes_downloaded
:
3116 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3117 elif current
== self
.item_view_episodes_unplayed
:
3118 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3120 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3122 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3123 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3125 def update_item_device( self
):
3126 if not gpodder
.ui
.fremantle
:
3127 if self
.config
.device_type
!= 'none':
3128 self
.itemDevice
.set_visible(True)
3129 self
.itemDevice
.label
= self
.get_device_name()
3131 self
.itemDevice
.set_visible(False)
3133 def properties_closed( self
):
3134 self
.preferences_dialog
= None
3135 self
.show_hide_tray_icon()
3136 self
.update_item_device()
3137 if gpodder
.ui
.maemo
:
3138 selection
= self
.treeAvailable
.get_selection()
3139 if self
.config
.maemo_enable_gestures
or \
3140 self
.config
.enable_fingerscroll
:
3141 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3143 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3145 def on_itemPreferences_activate(self
, widget
, *args
):
3146 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3147 _config
=self
.config
, \
3148 callback_finished
=self
.properties_closed
, \
3149 user_apps_reader
=self
.user_apps_reader
, \
3150 mygpo_login
=self
.on_mygpo_settings_activate
, \
3151 parent_window
=self
.main_window
, \
3152 mygpo_client
=self
.mygpo_client
, \
3153 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3155 # Initial message to relayout window (in case it's opened in portrait mode
3156 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3158 def on_itemDependencies_activate(self
, widget
):
3159 gPodderDependencyManager(self
.gPodder
)
3161 def on_goto_mygpo(self
, widget
):
3162 self
.mygpo_client
.open_website()
3164 def on_mygpo_settings_activate(self
, action
=None):
3165 settings
= MygPodderSettings(self
.main_window
, \
3166 config
=self
.config
, \
3167 mygpo_client
=self
.mygpo_client
, \
3168 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3170 def on_itemAddChannel_activate(self
, widget
=None):
3171 gPodderAddPodcast(self
.gPodder
, \
3172 add_urls_callback
=self
.add_podcast_list
)
3174 def on_itemEditChannel_activate(self
, widget
, *args
):
3175 if self
.active_channel
is None:
3176 title
= _('No podcast selected')
3177 message
= _('Please select a podcast in the podcasts list to edit.')
3178 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3181 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3182 gPodderChannel(self
.main_window
, \
3183 channel
=self
.active_channel
, \
3184 callback_closed
=callback_closed
, \
3185 cover_downloader
=self
.cover_downloader
)
3187 def on_itemMassUnsubscribe_activate(self
, item
=None):
3189 ('title', None, None, _('Podcast')),
3192 # We're abusing the Episode Selector for selecting Podcasts here,
3193 # but it works and looks good, so why not? -- thp
3194 gPodderEpisodeSelector(self
.main_window
, \
3195 title
=_('Remove podcasts'), \
3196 instructions
=_('Select the podcast you want to remove.'), \
3197 episodes
=self
.channels
, \
3199 size_attribute
=None, \
3200 stock_ok_button
=gtk
.STOCK_DELETE
, \
3201 callback
=self
.remove_podcast_list
, \
3202 _config
=self
.config
)
3204 def remove_podcast_list(self
, channels
, confirm
=True):
3206 log('No podcasts selected for deletion', sender
=self
)
3209 if len(channels
) == 1:
3210 title
= _('Removing podcast')
3211 info
= _('Please wait while the podcast is removed')
3212 message
= _('Do you really want to remove this podcast and its episodes?')
3214 title
= _('Removing podcasts')
3215 info
= _('Please wait while the podcasts are removed')
3216 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3218 if confirm
and not self
.show_confirmation(message
, title
):
3221 progress
= ProgressIndicator(title
, info
, parent
=self
.main_window
)
3223 def finish_deletion(select_url
):
3224 # Upload subscription list changes to the web service
3225 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3227 # Re-load the channels and select the desired new channel
3228 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3229 progress
.on_finished()
3230 self
.update_podcasts_tab()
3235 for idx
, channel
in enumerate(channels
):
3236 # Update the UI for correct status messages
3237 progress
.on_progress(float(idx
)/float(len(channels
)))
3238 progress
.on_message(channel
.title
)
3240 # Delete downloaded episodes
3241 channel
.remove_downloaded()
3243 # cancel any active downloads from this channel
3244 for episode
in channel
.get_all_episodes():
3245 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3248 if len(channels
) == 1:
3249 # get the URL of the podcast we want to select next
3250 if channel
in self
.channels
:
3251 position
= self
.channels
.index(channel
)
3255 if position
== len(self
.channels
)-1:
3256 # this is the last podcast, so select the URL
3257 # of the item before this one (i.e. the "new last")
3258 select_url
= self
.channels
[position
-1].url
3260 # there is a podcast after the deleted one, so
3261 # we simply select the one that comes after it
3262 select_url
= self
.channels
[position
+1].url
3264 # Remove the channel and clean the database entries
3266 self
.channels
.remove(channel
)
3268 # Clean up downloads and download directories
3269 self
.clean_up_downloads()
3271 self
.channel_list_changed
= True
3272 self
.save_channels_opml()
3274 # The remaining stuff is to be done in the GTK main thread
3275 util
.idle_add(finish_deletion
, select_url
)
3277 threading
.Thread(target
=thread_proc
).start()
3279 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3280 if self
.active_channel
is None:
3281 title
= _('No podcast selected')
3282 message
= _('Please select a podcast in the podcasts list to remove.')
3283 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3286 self
.remove_podcast_list([self
.active_channel
])
3288 def get_opml_filter(self
):
3289 filter = gtk
.FileFilter()
3290 filter.add_pattern('*.opml')
3291 filter.add_pattern('*.xml')
3292 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3295 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3296 if filename
is None:
3297 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3298 # FIXME: Hildonization on Fremantle
3299 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3300 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3301 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3302 elif gpodder
.ui
.diablo
:
3303 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3304 dlg
.set_filter(self
.get_opml_filter())
3305 response
= dlg
.run()
3307 if response
== gtk
.RESPONSE_OK
:
3308 filename
= dlg
.get_filename()
3311 if filename
is not None:
3312 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3313 custom_title
=_('Import podcasts from OPML file'), \
3314 add_urls_callback
=self
.add_podcast_list
, \
3315 hide_url_entry
=True)
3316 dir.download_opml_file(filename
)
3318 def on_itemExportChannels_activate(self
, widget
, *args
):
3319 if not self
.channels
:
3320 title
= _('Nothing to export')
3321 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3322 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3325 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3326 # FIXME: Hildonization on Fremantle
3327 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3328 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3329 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3330 elif gpodder
.ui
.diablo
:
3331 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3332 dlg
.set_filter(self
.get_opml_filter())
3333 response
= dlg
.run()
3334 if response
== gtk
.RESPONSE_OK
:
3335 filename
= dlg
.get_filename()
3337 exporter
= opml
.Exporter( filename
)
3338 if exporter
.write(self
.channels
):
3339 count
= len(self
.channels
)
3340 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3341 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3343 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3347 def on_itemImportChannels_activate(self
, widget
, *args
):
3348 if gpodder
.ui
.fremantle
:
3349 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3350 self
.config
.toplist_url
, \
3351 self
.config
.opml_url
, \
3352 self
.add_podcast_list
, \
3353 self
.on_itemAddChannel_activate
, \
3354 self
.on_mygpo_settings_activate
, \
3355 self
.show_text_edit_dialog
)
3357 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3358 add_urls_callback
=self
.add_podcast_list
)
3359 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3361 def on_homepage_activate(self
, widget
, *args
):
3362 util
.open_website(gpodder
.__url
__)
3364 def on_wiki_activate(self
, widget
, *args
):
3365 util
.open_website('http://gpodder.org/wiki/User_Manual')
3367 def on_bug_tracker_activate(self
, widget
, *args
):
3368 if gpodder
.ui
.maemo
:
3369 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3371 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3373 def on_item_support_activate(self
, widget
):
3374 util
.open_website('http://gpodder.org/donate')
3376 def on_itemAbout_activate(self
, widget
, *args
):
3377 if gpodder
.ui
.fremantle
:
3378 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3379 HeAboutDialog
.present(self
.main_window
,
3382 gpodder
.__version
__,
3383 _('A podcast client with focus on usability'),
3384 gpodder
.__copyright
__,
3386 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3387 'http://gpodder.org/donate')
3390 dlg
= gtk
.AboutDialog()
3391 dlg
.set_transient_for(self
.main_window
)
3392 dlg
.set_name('gPodder')
3393 dlg
.set_version(gpodder
.__version
__)
3394 dlg
.set_copyright(gpodder
.__copyright
__)
3395 dlg
.set_comments(_('A podcast client with focus on usability'))
3396 dlg
.set_website(gpodder
.__url
__)
3397 dlg
.set_translator_credits( _('translator-credits'))
3398 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3400 if gpodder
.ui
.desktop
:
3401 # For the "GUI" version, we add some more
3402 # items to the about dialog (credits and logo)
3405 'Thomas Perl <thpinfo.com>',
3408 if os
.path
.exists(gpodder
.credits_file
):
3409 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3410 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3411 app_authors
+= credits
3413 dlg
.set_authors(app_authors
)
3415 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3417 dlg
.set_logo_icon_name('gpodder')
3421 def on_wNotebook_switch_page(self
, widget
, *args
):
3423 if gpodder
.ui
.maemo
:
3424 self
.tool_downloads
.set_active(page_num
== 1)
3425 page
= self
.wNotebook
.get_nth_page(page_num
)
3426 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3427 if page_num
== 0 and self
.active_channel
is not None:
3428 self
.set_title(self
.active_channel
.title
)
3430 self
.set_title(tab_label
)
3432 self
.play_or_download()
3433 self
.menuChannels
.set_sensitive(True)
3434 self
.menuSubscriptions
.set_sensitive(True)
3435 # The message area in the downloads tab should be hidden
3436 # when the user switches away from the downloads tab
3437 if self
.message_area
is not None:
3438 self
.message_area
.hide()
3439 self
.message_area
= None
3441 # Remove finished episodes
3442 if self
.config
.auto_cleanup_downloads
:
3443 self
.on_btnCleanUpDownloads_clicked()
3445 self
.menuChannels
.set_sensitive(False)
3446 self
.menuSubscriptions
.set_sensitive(False)
3447 if gpodder
.ui
.desktop
:
3448 self
.toolDownload
.set_sensitive(False)
3449 self
.toolPlay
.set_sensitive(False)
3450 self
.toolTransfer
.set_sensitive(False)
3451 self
.toolCancel
.set_sensitive(False)
3453 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3454 # double-click action of the podcast list or enter
3455 self
.treeChannels
.set_cursor(path
)
3457 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3458 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3460 if model
is not None and iter is not None:
3461 old_active_channel
= self
.active_channel
3462 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3464 if self
.active_channel
== old_active_channel
:
3467 if gpodder
.ui
.maemo
:
3468 self
.set_title(self
.active_channel
.title
)
3469 self
.itemEditChannel
.set_visible(True)
3470 self
.itemRemoveChannel
.set_visible(True)
3472 self
.active_channel
= None
3473 self
.itemEditChannel
.set_visible(False)
3474 self
.itemRemoveChannel
.set_visible(False)
3476 self
.update_episode_list_model()
3478 def on_btnEditChannel_clicked(self
, widget
, *args
):
3479 self
.on_itemEditChannel_activate( widget
, args
)
3481 def get_podcast_urls_from_selected_episodes(self
):
3482 """Get a set of podcast URLs based on the selected episodes"""
3483 return set(episode
.channel
.url
for episode
in \
3484 self
.get_selected_episodes())
3486 def get_selected_episodes(self
):
3487 """Get a list of selected episodes from treeAvailable"""
3488 selection
= self
.treeAvailable
.get_selection()
3489 model
, paths
= selection
.get_selected_rows()
3491 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3494 def on_transfer_selected_episodes(self
, widget
):
3495 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3497 def on_playback_selected_episodes(self
, widget
):
3498 self
.playback_episodes(self
.get_selected_episodes())
3500 def on_shownotes_selected_episodes(self
, widget
):
3501 episodes
= self
.get_selected_episodes()
3503 episode
= episodes
.pop(0)
3504 self
.show_episode_shownotes(episode
)
3506 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3508 def on_download_selected_episodes(self
, widget
):
3509 episodes
= self
.get_selected_episodes()
3510 self
.download_episode_list(episodes
)
3511 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3512 self
.play_or_download()
3514 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3515 """Double-click/enter action handler for treeAvailable"""
3516 # We should only have one one selected as it was double clicked!
3517 e
= self
.get_selected_episodes()[0]
3519 if (self
.config
.double_click_episode_action
== 'download'):
3520 # If the episode has already been downloaded and exists then play it
3521 if e
.was_downloaded(and_exists
=True):
3522 self
.playback_episodes(self
.get_selected_episodes())
3523 # else download it if it is not already downloading
3524 elif not self
.episode_is_downloading(e
):
3525 self
.download_episode_list([e
])
3526 self
.update_episode_list_icons([e
.url
])
3527 self
.play_or_download()
3528 elif (self
.config
.double_click_episode_action
== 'stream'):
3529 # If we happen to have downloaded this episode simple play it
3530 if e
.was_downloaded(and_exists
=True):
3531 self
.playback_episodes(self
.get_selected_episodes())
3532 # else if streaming is possible stream it
3533 elif self
.streaming_possible():
3534 self
.playback_episodes(self
.get_selected_episodes())
3536 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3537 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3539 # default action is to display show notes
3540 self
.on_shownotes_selected_episodes(widget
)
3542 def show_episode_shownotes(self
, episode
):
3543 if self
.episode_shownotes_window
is None:
3544 log('First-time use of episode window --- creating', sender
=self
)
3545 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3546 _download_episode_list
=self
.download_episode_list
, \
3547 _playback_episodes
=self
.playback_episodes
, \
3548 _delete_episode_list
=self
.delete_episode_list
, \
3549 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3550 _cancel_task_list
=self
.cancel_task_list
, \
3551 _episode_is_downloading
=self
.episode_is_downloading
, \
3552 _streaming_possible
=self
.streaming_possible())
3553 self
.episode_shownotes_window
.show(episode
)
3554 if self
.episode_is_downloading(episode
):
3555 self
.update_downloads_list()
3557 def restart_auto_update_timer(self
):
3558 if self
._auto
_update
_timer
_source
_id
is not None:
3559 log('Removing existing auto update timer.', sender
=self
)
3560 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3561 self
._auto
_update
_timer
_source
_id
= None
3563 if self
.config
.auto_update_feeds
and \
3564 self
.config
.auto_update_frequency
:
3565 interval
= 60*1000*self
.config
.auto_update_frequency
3566 log('Setting up auto update timer with interval %d.', \
3567 self
.config
.auto_update_frequency
, sender
=self
)
3568 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3569 interval
, self
._on
_auto
_update
_timer
)
3571 def _on_auto_update_timer(self
):
3572 log('Auto update timer fired.', sender
=self
)
3573 self
.update_feed_cache(force_update
=True)
3575 # Ask web service for sub changes (if enabled)
3576 self
.mygpo_client
.flush()
3580 def on_treeDownloads_row_activated(self
, widget
, *args
):
3581 # Use the standard way of working on the treeview
3582 selection
= self
.treeDownloads
.get_selection()
3583 (model
, paths
) = selection
.get_selected_rows()
3584 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3586 for tree_row_reference
, task
in selected_tasks
:
3587 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3588 task
.status
= task
.PAUSED
3589 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3590 self
.download_queue_manager
.add_task(task
)
3591 self
.enable_download_list_update()
3592 elif task
.status
== task
.DONE
:
3593 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3595 self
.play_or_download()
3597 # Update the tab title and downloads list
3598 self
.update_downloads_list()
3600 def on_item_cancel_download_activate(self
, widget
):
3601 if self
.wNotebook
.get_current_page() == 0:
3602 selection
= self
.treeAvailable
.get_selection()
3603 (model
, paths
) = selection
.get_selected_rows()
3604 urls
= [model
.get_value(model
.get_iter(path
), \
3605 self
.episode_list_model
.C_URL
) for path
in paths
]
3606 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3607 if task
.url
in urls
]
3609 selection
= self
.treeDownloads
.get_selection()
3610 (model
, paths
) = selection
.get_selected_rows()
3611 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3612 self
.download_status_model
.C_TASK
) for path
in paths
]
3613 self
.cancel_task_list(selected_tasks
)
3615 def on_btnCancelAll_clicked(self
, widget
, *args
):
3616 self
.cancel_task_list(self
.download_tasks_seen
)
3618 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3619 episodes
= self
.get_selected_episodes()
3620 if len(episodes
) == 1:
3621 self
.delete_episode_list(episodes
, skip_locked
=False)
3623 self
.delete_episode_list(episodes
)
3625 def on_key_press(self
, widget
, event
):
3626 # Allow tab switching with Ctrl + PgUp/PgDown
3627 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3628 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3629 self
.wNotebook
.prev_page()
3631 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3632 self
.wNotebook
.next_page()
3635 # After this code we only handle Maemo hardware keys,
3636 # so if we are not a Maemo app, we don't do anything
3637 if not gpodder
.ui
.maemo
:
3641 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3643 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3646 if diff
!= 0 and not self
.currently_updating
:
3647 selection
= self
.treeChannels
.get_selection()
3648 (model
, iter) = selection
.get_selected()
3649 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
3650 selection
.select_path(new_path
)
3651 self
.treeChannels
.set_cursor(new_path
)
3656 def on_iconify(self
):
3658 self
.gPodder
.set_skip_taskbar_hint(True)
3659 if self
.config
.minimize_to_tray
:
3660 self
.tray_icon
.set_visible(True)
3662 self
.gPodder
.set_skip_taskbar_hint(False)
3664 def on_uniconify(self
):
3666 self
.gPodder
.set_skip_taskbar_hint(False)
3667 if self
.config
.minimize_to_tray
:
3668 self
.tray_icon
.set_visible(False)
3670 self
.gPodder
.set_skip_taskbar_hint(False)
3672 def uniconify_main_window(self
):
3673 if self
.is_iconified():
3674 self
.gPodder
.present()
3676 def iconify_main_window(self
):
3677 if not self
.is_iconified():
3678 self
.gPodder
.iconify()
3680 def update_podcasts_tab(self
):
3681 if len(self
.channels
):
3682 if gpodder
.ui
.fremantle
:
3683 self
.button_refresh
.set_title(_('Check for new episodes'))
3684 self
.button_refresh
.show()
3686 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
3688 if gpodder
.ui
.fremantle
:
3689 self
.button_refresh
.hide()
3691 self
.label2
.set_text(_('Podcasts'))
3693 @dbus.service
.method(gpodder
.dbus_interface
)
3694 def show_gui_window(self
):
3695 self
.gPodder
.present()
3697 @dbus.service
.method(gpodder
.dbus_interface
)
3698 def subscribe_to_url(self
, url
):
3699 gPodderAddPodcast(self
.gPodder
,
3700 add_urls_callback
=self
.add_podcast_list
,
3703 @dbus.service
.method(gpodder
.dbus_interface
)
3704 def mark_episode_played(self
, filename
):
3705 if filename
is None:
3708 for channel
in self
.channels
:
3709 for episode
in channel
.get_all_episodes():
3710 fn
= episode
.local_filename(create
=False, check_only
=True)
3712 episode
.mark(is_played
=True)
3714 self
.update_episode_list_icons([episode
.url
])
3715 self
.update_podcast_list_model([episode
.channel
.url
])
3721 def main(options
=None):
3722 gobject
.threads_init()
3723 gobject
.set_application_name('gPodder')
3725 if gpodder
.ui
.maemo
:
3726 # Try to enable the custom icon theme for gPodder on Maemo
3727 settings
= gtk
.settings_get_default()
3728 settings
.set_string_property('gtk-icon-theme-name', \
3729 'gpodder', __file__
)
3730 # Extend the search path for the optified icon theme (Maemo 5)
3731 icon_theme
= gtk
.icon_theme_get_default()
3732 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
3734 gtk
.window_set_default_icon_name('gpodder')
3735 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
3738 session_bus
= dbus
.SessionBus(mainloop
=dbus
.glib
.DBusGMainLoop())
3739 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=session_bus
)
3740 except dbus
.exceptions
.DBusException
, dbe
:
3741 log('Warning: Cannot get "on the bus".', traceback
=True)
3742 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
3743 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
3744 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
3745 dlg
.set_title('gPodder')
3750 util
.make_directory(gpodder
.home
)
3751 gpodder
.load_plugins()
3753 config
= UIConfig(gpodder
.config_file
)
3755 if gpodder
.ui
.diablo
:
3756 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
3757 # folder exists there (allow moving "gpodder" between SD cards or USB)
3758 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
3759 if not os
.path
.exists(config
.download_dir
):
3760 log('Downloads might have been moved. Trying to locate them...')
3761 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
3762 dir = os
.path
.join(basedir
, 'gpodder')
3763 if os
.path
.exists(dir):
3764 log('Downloads found in: %s', dir)
3765 config
.download_dir
= dir
3768 log('Downloads NOT FOUND in %s', dir)
3770 if config
.enable_fingerscroll
:
3771 BuilderWidget
.use_fingerscroll
= True
3772 elif gpodder
.ui
.fremantle
:
3773 config
.on_quit_ask
= False
3774 config
.feed_update_skipping
= False
3776 config
.mygpo_device_type
= util
.detect_device_type()
3778 gp
= gPodder(bus_name
, config
)
3781 if options
.subscribe
:
3782 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
3785 # handle "subscribe to podcast" events from firefox
3786 if platform
.system() == 'Darwin':
3787 from gpodder
import gpodderosx
3788 gpodderosx
.register_handlers(gp
)
3789 # end mac OS X stuff