1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 from xml
.sax
import saxutils
45 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
48 def __init__(self
, *args
, **kwargs
):
52 def __init__(self
, *args
, **kwargs
):
56 def method(*args
, **kwargs
):
59 def __init__(self
, *args
, **kwargs
):
62 def __init__(self
, *args
, **kwargs
):
66 from gpodder
import feedcore
67 from gpodder
import util
68 from gpodder
import opml
69 from gpodder
import download
70 from gpodder
import my
71 from gpodder
import youtube
72 from gpodder
import player
73 from gpodder
.liblogger
import log
78 from gpodder
.model
import PodcastChannel
79 from gpodder
.model
import PodcastEpisode
80 from gpodder
.dbsqlite
import Database
82 from gpodder
.gtkui
.model
import PodcastListModel
83 from gpodder
.gtkui
.model
import EpisodeListModel
84 from gpodder
.gtkui
.config
import UIConfig
85 from gpodder
.gtkui
.services
import CoverDownloader
86 from gpodder
.gtkui
.widgets
import SimpleMessageArea
87 from gpodder
.gtkui
.desktopfile
import UserAppsReader
89 from gpodder
.gtkui
.draw
import draw_text_box_centered
91 from gpodder
.gtkui
.interface
.common
import BuilderWidget
92 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
93 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
95 if gpodder
.ui
.desktop
:
96 from gpodder
.gtkui
.download
import DownloadStatusModel
98 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
100 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
101 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
102 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
103 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
104 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
105 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
106 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
108 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
110 except Exception, exc
:
111 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
112 log('Warning: This probably means your PyGTK installation is too old!')
113 have_trayicon
= False
114 elif gpodder
.ui
.diablo
:
115 from gpodder
.gtkui
.download
import DownloadStatusModel
117 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
118 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
119 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
120 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
121 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
122 from gpodder
.gtkui
.maemo
.mygpodder
import MygPodderSettings
123 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
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 from gpodder
.gtkui
.frmntl
.progress
import ProgressIndicator
138 have_trayicon
= False
140 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
141 from gpodder
.gtkui
.frmntl
.mafw
import MafwPlaybackMonitor
143 from gpodder
.gtkui
.interface
.common
import Orientation
145 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
150 from gpodder
.dbusproxy
import DBusPodcastsProxy
151 from gpodder
import hooks
153 class gPodder(BuilderWidget
, dbus
.service
.Object
):
154 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear']
156 ICON_GENERAL_ADD
= 'general_add'
157 ICON_GENERAL_REFRESH
= 'general_refresh'
158 ICON_GENERAL_CLOSE
= 'general_close'
160 def __init__(self
, bus_name
, config
):
161 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
162 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
163 self
.on_itemUpdate_activate
, \
164 self
.playback_episodes
, \
165 self
.download_episode_list
, \
166 self
.episode_object_by_uri
, \
168 self
.db
= Database(gpodder
.database_file
)
170 BuilderWidget
.__init
__(self
, None)
173 if gpodder
.ui
.diablo
:
175 self
.app
= hildon
.Program()
176 self
.app
.add_window(self
.main_window
)
177 self
.main_window
.add_toolbar(self
.toolbar
)
179 for child
in self
.main_menu
.get_children():
181 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
182 self
._last
_orientation
= Orientation
.LANDSCAPE
183 elif gpodder
.ui
.fremantle
:
185 self
.app
= hildon
.Program()
186 self
.app
.add_window(self
.main_window
)
188 appmenu
= hildon
.AppMenu()
190 for filter in (self
.item_view_podcasts_all
, \
191 self
.item_view_podcasts_downloaded
, \
192 self
.item_view_podcasts_unplayed
):
193 button
= gtk
.ToggleButton()
194 filter.connect_proxy(button
)
195 appmenu
.add_filter(button
)
197 for action
in (self
.itemPreferences
, \
198 self
.item_downloads
, \
199 self
.itemRemoveOldEpisodes
, \
200 self
.item_unsubscribe
, \
202 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
203 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
204 action
.connect_proxy(button
)
205 if action
== self
.item_downloads
:
206 button
.set_title(_('Downloads'))
207 button
.set_value(_('Idle'))
208 self
.button_downloads
= button
209 appmenu
.append(button
)
211 self
.main_window
.set_app_menu(appmenu
)
213 # Initialize portrait mode / rotation manager
214 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
216 gpodder
.__version
__, \
217 self
.config
.rotation_mode
)
219 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
220 util
.idle_add(self
.on_window_orientation_changed
, \
221 Orientation
.PORTRAIT
)
222 self
._last
_orientation
= Orientation
.PORTRAIT
224 self
._last
_orientation
= Orientation
.LANDSCAPE
226 self
._last
_orientation
= Orientation
.LANDSCAPE
227 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
229 self
.bluetooth_available
= util
.bluetooth_available()
231 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
232 if not gpodder
.ui
.fremantle
:
233 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
234 self
.main_window
.show()
236 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
238 if gpodder
.ui
.fremantle
:
239 # Create a D-Bus monitoring object that takes care of
240 # tracking MAFW (Nokia Media Player) playback events
241 # and sends episode playback status events via D-Bus
242 self
.mafw_monitor
= MafwPlaybackMonitor(gpodder
.dbus_session_bus
)
244 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
246 self
.preferences_dialog
= None
247 self
.config
.add_observer(self
.on_config_changed
)
249 self
.tray_icon
= None
250 self
.episode_shownotes_window
= None
251 self
.new_episodes_window
= None
253 if gpodder
.ui
.desktop
:
254 # Mac OS X-specific UI tweaks: Native main menu integration
255 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
256 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
258 import igemacintegration
as igemi
260 # Move the menu bar from the window to the Mac menu bar
262 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
264 # Reparent some items to the "Application" menu
265 for widget
in ('/mainMenu/menuHelp/itemAbout', \
266 '/mainMenu/menuPodcasts/itemPreferences'):
267 item
= self
.uimanager1
.get_widget(widget
)
268 group
= igemi
.ige_mac_menu_add_app_menu_group()
269 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
271 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
272 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
273 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
275 print >>sys
.stderr
, """
276 Warning: ige-mac-integration not found - no native menus.
279 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
280 self
.main_window
, self
.show_confirmation
, \
281 self
.update_episode_list_icons
, \
282 self
.update_podcast_list_model
, self
.toolPreferences
, \
283 gPodderEpisodeSelector
, \
284 self
.commit_changes_to_database
)
288 self
.download_status_model
= DownloadStatusModel()
289 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
291 if gpodder
.ui
.desktop
:
292 self
.show_hide_tray_icon()
293 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
294 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
295 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
297 if not gpodder
.ui
.fremantle
:
298 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
299 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
300 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
301 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
303 # When the amount of maximum downloads changes, notify the queue manager
304 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
305 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
307 self
.default_title
= 'gPodder'
308 if gpodder
.__version
__.rfind('git') != -1:
309 self
.set_title('gPodder %s' % gpodder
.__version
__)
311 title
= self
.gPodder
.get_title()
312 if title
is not None:
313 self
.set_title(title
)
315 self
.set_title(_('gPodder'))
317 self
.cover_downloader
= CoverDownloader()
319 # Generate list models for podcasts and their episodes
320 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
322 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
323 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
325 if gpodder
.ui
.fremantle
:
326 # Work around Maemo bug #4718
327 self
.button_refresh
.set_name('HildonButton-finger')
328 self
.button_subscribe
.set_name('HildonButton-finger')
330 self
.button_refresh
.set_sensitive(False)
331 self
.button_subscribe
.set_sensitive(False)
333 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
334 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
335 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
336 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
338 # Make the button scroll together with the TreeView contents
339 action_area_box
= self
.treeChannels
.get_action_area_box()
340 for child
in self
.buttonbox
:
341 child
.reparent(action_area_box
)
342 self
.vbox
.remove(self
.buttonbox
)
343 action_area_box
.set_spacing(2)
344 action_area_box
.set_border_width(3)
345 self
.treeChannels
.set_action_area_visible(True)
347 from gpodder
.gtkui
.frmntl
import style
348 sub_font
= style
.get_font_desc('SmallSystemFont')
349 sub_color
= style
.get_color('SecondaryTextColor')
350 sub
= (sub_font
.to_string(), sub_color
.to_string())
351 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
352 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
354 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
355 while gtk
.events_pending():
356 gtk
.main_iteration(False)
359 # Try to get the real package version from dpkg
360 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
361 version
, _stderr
= p
.communicate()
365 version
= gpodder
.__version
__
366 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
367 self
.label_footer
.hide()
369 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
370 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
371 show_episode_shownotes
=self
.show_episode_shownotes
, \
372 update_podcast_list_model
=self
.update_podcast_list_model
, \
373 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
374 item_view_episodes_all
=self
.item_view_episodes_all
, \
375 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
376 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
377 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
378 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
379 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
380 hide_episode_search
=self
.hide_episode_search
, \
381 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
382 playback_episodes
=self
.playback_episodes
, \
383 delete_episode_list
=self
.delete_episode_list
, \
384 episode_list_status_changed
=self
.episode_list_status_changed
, \
385 download_episode_list
=self
.download_episode_list
, \
386 episode_is_downloading
=self
.episode_is_downloading
, \
387 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
388 add_download_task_monitor
=self
.add_download_task_monitor
, \
389 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
390 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
391 on_delete_episodes_button_clicked
=self
.on_itemRemoveOldEpisodes_activate
, \
392 on_itemUpdate_activate
=self
.on_itemUpdate_activate
)
394 # Expose objects for episode list type-ahead find
395 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
396 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
397 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
399 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
400 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
401 cleanup_downloads
=self
.cleanup_downloads
, \
402 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
403 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
406 self
.treeAvailable
= self
.episodes_window
.treeview
407 self
.treeDownloads
= self
.downloads_window
.treeview
409 # Init the treeviews that we use
410 self
.init_podcast_list_treeview()
411 self
.init_episode_list_treeview()
412 self
.init_download_list_treeview()
414 if self
.config
.podcast_list_hide_boring
:
415 self
.item_view_hide_boring_podcasts
.set_active(True)
417 self
.currently_updating
= False
420 self
.context_menu_mouse_button
= 1
422 self
.context_menu_mouse_button
= 3
424 if self
.config
.start_iconified
:
425 self
.iconify_main_window()
427 self
.download_tasks_seen
= set()
428 self
.download_list_update_enabled
= False
429 self
.download_task_monitors
= set()
431 # Subscribed channels
432 self
.active_channel
= None
433 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
434 self
.channel_list_changed
= True
435 self
.update_podcasts_tab()
437 # load list of user applications for audio playback
438 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
439 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
441 # Set the "Device" menu item for the first time
442 if gpodder
.ui
.desktop
:
443 self
.update_item_device()
445 # Set up the first instance of MygPoClient
446 self
.mygpo_client
= my
.MygPoClient(self
.config
)
448 # Now, update the feed cache, when everything's in place
449 if not gpodder
.ui
.fremantle
:
450 self
.btnUpdateFeeds
.show()
451 self
.updating_feed_cache
= False
452 self
.feed_cache_update_cancelled
= False
453 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
455 self
.message_area
= None
457 def find_partial_downloads():
458 # Look for partial file downloads
459 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
460 count
= len(partial_files
)
461 resumable_episodes
= []
463 if not gpodder
.ui
.fremantle
:
464 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
465 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
466 _('Some episodes have not finished downloading in a previous session.'), \
467 False, self
.get_dialog_parent())
468 indicator
.on_message(N_('%d partial file', '%d partial files', count
) % count
)
470 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
473 for c
in self
.channels
:
474 for e
in c
.get_all_episodes():
475 filename
= e
.local_filename(create
=False, check_only
=True)
476 if filename
in candidates
:
477 log('Found episode: %s', e
.title
, sender
=self
)
479 indicator
.on_message(e
.title
)
480 indicator
.on_progress(float(found
)/count
)
481 candidates
.remove(filename
)
482 partial_files
.remove(filename
+'.partial')
483 resumable_episodes
.append(e
)
491 for f
in partial_files
:
492 log('Partial file without episode: %s', f
, sender
=self
)
495 util
.idle_add(indicator
.on_finished
)
497 if len(resumable_episodes
):
498 def offer_resuming():
499 self
.download_episode_list_paused(resumable_episodes
)
500 if not gpodder
.ui
.fremantle
:
501 resume_all
= gtk
.Button(_('Resume all'))
502 #resume_all.set_border_width(0)
503 def on_resume_all(button
):
504 selection
= self
.treeDownloads
.get_selection()
505 selection
.select_all()
506 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
507 selection
.unselect_all()
508 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
509 self
.message_area
.hide()
510 resume_all
.connect('clicked', on_resume_all
)
512 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
513 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
514 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
515 self
.message_area
.show_all()
516 self
.clean_up_downloads(delete_partial
=False)
517 util
.idle_add(offer_resuming
)
518 elif not gpodder
.ui
.fremantle
:
519 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
521 util
.idle_add(self
.clean_up_downloads
, True)
522 threading
.Thread(target
=find_partial_downloads
).start()
524 # Start the auto-update procedure
525 self
._auto
_update
_timer
_source
_id
= None
526 if self
.config
.auto_update_feeds
:
527 self
.restart_auto_update_timer()
529 # Delete old episodes if the user wishes to
530 if self
.config
.auto_remove_played_episodes
and \
531 self
.config
.episode_old_age
> 0:
532 old_episodes
= list(self
.get_expired_episodes())
533 if len(old_episodes
) > 0:
534 self
.delete_episode_list(old_episodes
, confirm
=False)
535 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
537 if gpodder
.ui
.fremantle
:
538 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
539 self
.button_refresh
.set_sensitive(True)
540 self
.button_subscribe
.set_sensitive(True)
541 self
.main_window
.set_title(_('gPodder'))
542 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
544 # Do the initial sync with the web service
545 util
.idle_add(self
.mygpo_client
.flush
, True)
547 # First-time users should be asked if they want to see the OPML
548 if not self
.channels
and not gpodder
.ui
.fremantle
:
549 util
.idle_add(self
.on_itemUpdate_activate
)
551 def episode_object_by_uri(self
, uri
):
552 """Get an episode object given a local or remote URI
554 This can be used to quickly access an episode object
555 when all we have is its download filename or episode
556 URL (e.g. from external D-Bus calls / signals, etc..)
558 if uri
.startswith('/'):
559 uri
= 'file://' + uri
561 prefix
= 'file://' + self
.config
.download_dir
563 if uri
.startswith(prefix
):
564 # File is on the local filesystem in the download folder
565 filename
= uri
[len(prefix
):]
566 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
568 if len(file_parts
) == 2:
569 dir_name
, filename
= file_parts
570 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
571 if len(channels
) == 1:
572 channel
= channels
[0]
573 return channel
.get_episode_by_filename(filename
)
575 # Possibly remote file - search the database for a podcast
576 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
578 if channel_id
is not None:
579 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
580 if len(channels
) == 1:
581 channel
= channels
[0]
582 return channel
.get_episode_by_url(uri
)
586 def on_played(self
, start
, end
, total
, file_uri
):
587 """Handle the "played" signal from a media player"""
588 if start
== 0 and end
== 0 and total
== 0:
589 # Ignore bogus play event
591 elif end
< start
+ 5:
592 # Ignore "less than five seconds" segments,
593 # as they can happen with seeking, etc...
596 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
597 episode
= self
.episode_object_by_uri(file_uri
)
599 if episode
is not None:
600 file_type
= episode
.file_type()
601 # Automatically enable D-Bus played status mode
602 if file_type
== 'audio':
603 self
.config
.audio_played_dbus
= True
604 elif file_type
== 'video':
605 self
.config
.video_played_dbus
= True
609 episode
.total_time
= total
611 # Assume the episode's total time for the action
612 total
= episode
.total_time
613 if episode
.current_position_updated
is None or \
614 now
> episode
.current_position_updated
:
615 episode
.current_position
= end
616 episode
.current_position_updated
= now
617 episode
.mark(is_played
=True)
620 self
.update_episode_list_icons([episode
.url
])
621 self
.update_podcast_list_model([episode
.channel
.url
])
623 # Submit this action to the webservice
624 self
.mygpo_client
.on_playback_full(episode
, \
627 def on_add_remove_podcasts_mygpo(self
):
628 actions
= self
.mygpo_client
.get_received_actions()
632 existing_urls
= [c
.url
for c
in self
.channels
]
634 # Columns for the episode selector window - just one...
636 ('description', None, None, _('Action')),
639 # A list of actions that have to be chosen from
642 # Actions that are ignored (already carried out)
645 for action
in actions
:
646 if action
.is_add
and action
.url
not in existing_urls
:
647 changes
.append(my
.Change(action
))
648 elif action
.is_remove
and action
.url
in existing_urls
:
649 podcast_object
= None
650 for podcast
in self
.channels
:
651 if podcast
.url
== action
.url
:
652 podcast_object
= podcast
654 changes
.append(my
.Change(action
, podcast_object
))
656 log('Ignoring action: %s', action
, sender
=self
)
657 ignored
.append(action
)
659 # Confirm all ignored changes
660 self
.mygpo_client
.confirm_received_actions(ignored
)
662 def execute_podcast_actions(selected
):
663 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
664 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
666 # Apply the accepted changes locally
667 self
.add_podcast_list(add_list
)
668 self
.remove_podcast_list(remove_list
, confirm
=False)
670 # All selected items are now confirmed
671 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
673 # Revert the changes on the server
674 rejected
= [c
.action
for c
in changes
if c
not in selected
]
675 self
.mygpo_client
.reject_received_actions(rejected
)
678 # We're abusing the Episode Selector again ;) -- thp
679 gPodderEpisodeSelector(self
.main_window
, \
680 title
=_('Confirm changes from gpodder.net'), \
681 instructions
=_('Select the actions you want to carry out.'), \
684 size_attribute
=None, \
685 stock_ok_button
=gtk
.STOCK_APPLY
, \
686 callback
=execute_podcast_actions
, \
689 # There are some actions that need the user's attention
694 # We have no remaining actions - no selection happens
697 def rewrite_urls_mygpo(self
):
698 # Check if we have to rewrite URLs since the last add
699 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
701 for rewritten_url
in rewritten_urls
:
702 if not rewritten_url
.new_url
:
705 for channel
in self
.channels
:
706 if channel
.url
== rewritten_url
.old_url
:
707 log('Updating URL of %s to %s', channel
, \
708 rewritten_url
.new_url
, sender
=self
)
709 channel
.url
= rewritten_url
.new_url
711 self
.channel_list_changed
= True
712 util
.idle_add(self
.update_episode_list_model
)
715 def on_send_full_subscriptions(self
):
716 # Send the full subscription list to the gpodder.net client
717 # (this will overwrite the subscription list on the server)
718 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
719 _('Your subscriptions are being uploaded to the server.'), \
720 False, self
.get_dialog_parent())
723 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
724 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
729 message
= e
.__class
__.__name
__
730 self
.show_message(message
, \
731 _('Error while uploading'), \
733 util
.idle_add(show_error
, e
)
735 util
.idle_add(indicator
.on_finished
)
737 def on_podcast_selected(self
, treeview
, path
, column
):
739 model
= treeview
.get_model()
740 channel
= model
.get_value(model
.get_iter(path
), \
741 PodcastListModel
.C_CHANNEL
)
742 self
.active_channel
= channel
743 self
.update_episode_list_model()
744 self
.episodes_window
.channel
= self
.active_channel
745 self
.episodes_window
.show()
747 def on_button_subscribe_clicked(self
, button
):
748 self
.on_itemImportChannels_activate(button
)
750 def on_button_downloads_clicked(self
, widget
):
751 self
.downloads_window
.show()
753 def show_episode_in_download_manager(self
, episode
):
754 self
.downloads_window
.show()
755 model
= self
.treeDownloads
.get_model()
756 selection
= self
.treeDownloads
.get_selection()
757 selection
.unselect_all()
758 it
= model
.get_iter_first()
759 while it
is not None:
760 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
761 if task
.episode
.url
== episode
.url
:
762 selection
.select_iter(it
)
763 # FIXME: Scroll to selection in pannable area
765 it
= model
.iter_next(it
)
767 def for_each_episode_set_task_status(self
, episodes
, status
):
768 episode_urls
= set(episode
.url
for episode
in episodes
)
769 model
= self
.treeDownloads
.get_model()
770 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
771 model
.get_value(row
.iter, \
772 DownloadStatusModel
.C_TASK
)) for row
in model \
773 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
775 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
777 def on_window_orientation_changed(self
, orientation
):
778 self
._last
_orientation
= orientation
779 if self
.preferences_dialog
is not None:
780 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
782 treeview
= self
.treeChannels
783 if orientation
== Orientation
.PORTRAIT
:
784 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
785 # Work around Maemo bug #4718
786 self
.button_subscribe
.set_name('HildonButton-thumb')
787 self
.button_refresh
.set_name('HildonButton-thumb')
789 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
790 # Work around Maemo bug #4718
791 self
.button_subscribe
.set_name('HildonButton-finger')
792 self
.button_refresh
.set_name('HildonButton-finger')
794 def on_treeview_podcasts_selection_changed(self
, selection
):
795 model
, iter = selection
.get_selected()
797 self
.active_channel
= None
798 self
.episode_list_model
.clear()
800 def on_treeview_button_pressed(self
, treeview
, event
):
801 if event
.window
!= treeview
.get_bin_window():
804 TreeViewHelper
.save_button_press_event(treeview
, event
)
806 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
807 TreeViewHelper
.ROLE_PODCASTS
:
808 return self
.currently_updating
810 return event
.button
== self
.context_menu_mouse_button
and \
813 def on_treeview_podcasts_button_released(self
, treeview
, event
):
814 if event
.window
!= treeview
.get_bin_window():
818 return self
.treeview_channels_handle_gestures(treeview
, event
)
819 return self
.treeview_channels_show_context_menu(treeview
, event
)
821 def on_treeview_episodes_button_released(self
, treeview
, event
):
822 if event
.window
!= treeview
.get_bin_window():
826 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
827 return self
.treeview_available_handle_gestures(treeview
, event
)
829 return self
.treeview_available_show_context_menu(treeview
, event
)
831 def on_treeview_downloads_button_released(self
, treeview
, event
):
832 if event
.window
!= treeview
.get_bin_window():
835 return self
.treeview_downloads_show_context_menu(treeview
, event
)
837 def on_entry_search_podcasts_changed(self
, editable
):
838 if self
.hbox_search_podcasts
.get_property('visible'):
839 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
841 def on_entry_search_podcasts_key_press(self
, editable
, event
):
842 if event
.keyval
== gtk
.keysyms
.Escape
:
843 self
.hide_podcast_search()
846 def hide_podcast_search(self
, *args
):
847 self
.hbox_search_podcasts
.hide()
848 self
.entry_search_podcasts
.set_text('')
849 self
.podcast_list_model
.set_search_term(None)
850 self
.treeChannels
.grab_focus()
852 def show_podcast_search(self
, input_char
):
853 self
.hbox_search_podcasts
.show()
854 self
.entry_search_podcasts
.insert_text(input_char
, -1)
855 self
.entry_search_podcasts
.grab_focus()
856 self
.entry_search_podcasts
.set_position(-1)
858 def init_podcast_list_treeview(self
):
859 # Set up podcast channel tree view widget
860 if gpodder
.ui
.fremantle
:
861 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
862 self
.item_view_podcasts_downloaded
.set_active(True)
863 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
864 self
.item_view_podcasts_unplayed
.set_active(True)
866 self
.item_view_podcasts_all
.set_active(True)
867 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
869 iconcolumn
= gtk
.TreeViewColumn('')
870 iconcell
= gtk
.CellRendererPixbuf()
871 iconcolumn
.pack_start(iconcell
, False)
872 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
873 self
.treeChannels
.append_column(iconcolumn
)
875 namecolumn
= gtk
.TreeViewColumn('')
876 namecell
= gtk
.CellRendererText()
877 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
878 namecolumn
.pack_start(namecell
, True)
879 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
881 iconcell
= gtk
.CellRendererPixbuf()
882 iconcell
.set_property('xalign', 1.0)
883 namecolumn
.pack_start(iconcell
, False)
884 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
885 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
886 self
.treeChannels
.append_column(namecolumn
)
888 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
890 # When no podcast is selected, clear the episode list model
891 selection
= self
.treeChannels
.get_selection()
892 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
894 # Set up type-ahead find for the podcast list
895 def on_key_press(treeview
, event
):
896 if event
.keyval
== gtk
.keysyms
.Escape
:
897 self
.hide_podcast_search()
898 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
899 self
.hide_podcast_search()
900 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
901 # Don't handle type-ahead when control is pressed (so shortcuts
902 # with the Ctrl key still work, e.g. Ctrl+A, ...)
905 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
906 if unicode_char_id
== 0:
908 input_char
= unichr(unicode_char_id
)
909 self
.show_podcast_search(input_char
)
911 self
.treeChannels
.connect('key-press-event', on_key_press
)
913 # Enable separators to the podcast list to separate special podcasts
914 # from others (this is used for the "all episodes" view)
915 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
917 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
919 def on_entry_search_episodes_changed(self
, editable
):
920 if self
.hbox_search_episodes
.get_property('visible'):
921 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
923 def on_entry_search_episodes_key_press(self
, editable
, event
):
924 if event
.keyval
== gtk
.keysyms
.Escape
:
925 self
.hide_episode_search()
928 def hide_episode_search(self
, *args
):
929 self
.hbox_search_episodes
.hide()
930 self
.entry_search_episodes
.set_text('')
931 self
.episode_list_model
.set_search_term(None)
932 self
.treeAvailable
.grab_focus()
934 def show_episode_search(self
, input_char
):
935 self
.hbox_search_episodes
.show()
936 self
.entry_search_episodes
.insert_text(input_char
, -1)
937 self
.entry_search_episodes
.grab_focus()
938 self
.entry_search_episodes
.set_position(-1)
940 def init_episode_list_treeview(self
):
941 # For loading the list model
942 self
.empty_episode_list_model
= EpisodeListModel()
943 self
.episode_list_model
= EpisodeListModel()
945 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
946 self
.item_view_episodes_undeleted
.set_active(True)
947 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
948 self
.item_view_episodes_downloaded
.set_active(True)
949 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
950 self
.item_view_episodes_unplayed
.set_active(True)
952 self
.item_view_episodes_all
.set_active(True)
954 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
956 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
958 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
960 iconcell
= gtk
.CellRendererPixbuf()
962 iconcell
.set_fixed_size(50, 50)
963 status_column_label
= ''
965 status_column_label
= _('Status')
966 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
968 namecell
= gtk
.CellRendererText()
969 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
970 namecolumn
= gtk
.TreeViewColumn(_('Episode'), namecell
, markup
=EpisodeListModel
.C_DESCRIPTION
)
971 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
972 namecolumn
.set_resizable(True)
973 namecolumn
.set_expand(True)
975 sizecell
= gtk
.CellRendererText()
976 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
978 releasecell
= gtk
.CellRendererText()
979 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
981 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
982 itemcolumn
.set_reorderable(True)
983 self
.treeAvailable
.append_column(itemcolumn
)
986 sizecolumn
.set_visible(False)
987 releasecolumn
.set_visible(False)
989 # Set up type-ahead find for the episode list
990 def on_key_press(treeview
, event
):
991 if event
.keyval
== gtk
.keysyms
.Escape
:
992 self
.hide_episode_search()
993 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
994 self
.hide_episode_search()
995 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
996 # Don't handle type-ahead when control is pressed (so shortcuts
997 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1000 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1001 if unicode_char_id
== 0:
1003 input_char
= unichr(unicode_char_id
)
1004 self
.show_episode_search(input_char
)
1006 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1008 if gpodder
.ui
.desktop
:
1009 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1010 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1011 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1012 if self
.config
.on_drag_mark_played
:
1013 for episode
in self
.get_selected_episodes():
1014 episode
.mark(is_played
=True)
1015 self
.on_selected_episodes_status_changed()
1016 uris
= ['file://'+e
.local_filename(create
=False) \
1017 for e
in self
.get_selected_episodes() \
1018 if e
.was_downloaded(and_exists
=True)]
1019 uris
.append('') # for the trailing '\r\n'
1020 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1021 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1023 selection
= self
.treeAvailable
.get_selection()
1024 if gpodder
.ui
.diablo
:
1025 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1026 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1028 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1029 elif gpodder
.ui
.fremantle
:
1030 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1032 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1033 # Update the sensitivity of the toolbar buttons on the Desktop
1034 selection
.connect('changed', lambda s
: self
.play_or_download())
1036 if gpodder
.ui
.diablo
:
1037 # Set up the tap-and-hold context menu for podcasts
1039 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1040 menu
.append(self
.itemEditChannel
.create_menu_item())
1041 menu
.append(gtk
.SeparatorMenuItem())
1042 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1043 menu
.append(gtk
.SeparatorMenuItem())
1044 item
= gtk
.ImageMenuItem(_('Close this menu'))
1045 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1046 gtk
.ICON_SIZE_MENU
))
1049 menu
= self
.set_finger_friendly(menu
)
1050 self
.treeChannels
.tap_and_hold_setup(menu
)
1053 def init_download_list_treeview(self
):
1054 # enable multiple selection support
1055 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1056 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1058 # columns and renderers for "download progress" tab
1059 # First column: [ICON] Episodename
1060 column
= gtk
.TreeViewColumn(_('Episode'))
1062 cell
= gtk
.CellRendererPixbuf()
1063 if gpodder
.ui
.maemo
:
1064 cell
.set_fixed_size(50, 50)
1065 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1066 column
.pack_start(cell
, expand
=False)
1067 column
.add_attribute(cell
, 'stock-id', \
1068 DownloadStatusModel
.C_ICON_NAME
)
1070 cell
= gtk
.CellRendererText()
1071 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1072 column
.pack_start(cell
, expand
=True)
1073 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1074 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1075 column
.set_expand(True)
1076 self
.treeDownloads
.append_column(column
)
1078 # Second column: Progress
1079 cell
= gtk
.CellRendererProgress()
1080 cell
.set_property('yalign', .5)
1081 cell
.set_property('ypad', 6)
1082 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1083 value
=DownloadStatusModel
.C_PROGRESS
, \
1084 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1085 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1086 column
.set_expand(False)
1087 self
.treeDownloads
.append_column(column
)
1088 column
.set_property('min-width', 150)
1089 column
.set_property('max-width', 150)
1091 self
.treeDownloads
.set_model(self
.download_status_model
)
1092 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1094 def on_treeview_expose_event(self
, treeview
, event
):
1095 if event
.window
== treeview
.get_bin_window():
1096 model
= treeview
.get_model()
1097 if (model
is not None and model
.get_iter_first() is not None):
1100 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1104 ctx
= event
.window
.cairo_create()
1105 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1106 event
.area
.width
, event
.area
.height
)
1109 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1112 if role
== TreeViewHelper
.ROLE_EPISODES
:
1113 if self
.currently_updating
:
1114 text
= _('Loading episodes')
1115 progress
= self
.episode_list_model
.get_update_progress()
1116 elif self
.config
.episode_list_view_mode
!= \
1117 EpisodeListModel
.VIEW_ALL
:
1118 text
= _('No episodes in current view')
1120 text
= _('No episodes available')
1121 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1122 if self
.config
.episode_list_view_mode
!= \
1123 EpisodeListModel
.VIEW_ALL
and \
1124 self
.config
.podcast_list_hide_boring
and \
1125 len(self
.channels
) > 0:
1126 text
= _('No podcasts in this view')
1128 text
= _('No subscriptions')
1129 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1130 text
= _('No active downloads')
1132 raise Exception('on_treeview_expose_event: unknown role')
1134 if gpodder
.ui
.fremantle
:
1135 from gpodder
.gtkui
.frmntl
import style
1136 font_desc
= style
.get_font_desc('LargeSystemFont')
1140 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1144 def enable_download_list_update(self
):
1145 if not self
.download_list_update_enabled
:
1146 self
.update_downloads_list()
1147 gobject
.timeout_add(1500, self
.update_downloads_list
)
1148 self
.download_list_update_enabled
= True
1150 def cleanup_downloads(self
):
1151 model
= self
.download_status_model
1153 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1154 changed_episode_urls
= set()
1155 for row_reference
, task
in all_tasks
:
1156 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1157 model
.remove(model
.get_iter(row_reference
.get_path()))
1159 # We don't "see" this task anymore - remove it;
1160 # this is needed, so update_episode_list_icons()
1161 # below gets the correct list of "seen" tasks
1162 self
.download_tasks_seen
.remove(task
)
1163 except KeyError, key_error
:
1164 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1165 changed_episode_urls
.add(task
.url
)
1166 # Tell the task that it has been removed (so it can clean up)
1167 task
.removed_from_list()
1169 # Tell the podcasts tab to update icons for our removed podcasts
1170 self
.update_episode_list_icons(changed_episode_urls
)
1172 # Tell the shownotes window that we have removed the episode
1173 if self
.episode_shownotes_window
is not None and \
1174 self
.episode_shownotes_window
.episode
is not None and \
1175 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1176 self
.episode_shownotes_window
._download
_status
_changed
(None)
1178 # Update the downloads list one more time
1179 self
.update_downloads_list(can_call_cleanup
=False)
1181 def on_tool_downloads_toggled(self
, toolbutton
):
1182 if toolbutton
.get_active():
1183 self
.wNotebook
.set_current_page(1)
1185 self
.wNotebook
.set_current_page(0)
1187 def add_download_task_monitor(self
, monitor
):
1188 self
.download_task_monitors
.add(monitor
)
1189 model
= self
.download_status_model
1193 task
= row
[self
.download_status_model
.C_TASK
]
1194 monitor
.task_updated(task
)
1196 def remove_download_task_monitor(self
, monitor
):
1197 self
.download_task_monitors
.remove(monitor
)
1199 def update_downloads_list(self
, can_call_cleanup
=True):
1201 model
= self
.download_status_model
1203 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1204 total_speed
, total_size
, done_size
= 0, 0, 0
1206 # Keep a list of all download tasks that we've seen
1207 download_tasks_seen
= set()
1209 # Remember the DownloadTask object for the episode that
1210 # has been opened in the episode shownotes dialog (if any)
1211 if self
.episode_shownotes_window
is not None:
1212 shownotes_episode
= self
.episode_shownotes_window
.episode
1213 shownotes_task
= None
1215 shownotes_episode
= None
1216 shownotes_task
= None
1218 # Do not go through the list of the model is not (yet) available
1222 failed_downloads
= []
1224 self
.download_status_model
.request_update(row
.iter)
1226 task
= row
[self
.download_status_model
.C_TASK
]
1227 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1229 # Let the download task monitors know of changes
1230 for monitor
in self
.download_task_monitors
:
1231 monitor
.task_updated(task
)
1234 done_size
+= size
*progress
1236 if shownotes_episode
is not None and \
1237 shownotes_episode
.url
== task
.episode
.url
:
1238 shownotes_task
= task
1240 download_tasks_seen
.add(task
)
1242 if status
== download
.DownloadTask
.DOWNLOADING
:
1244 total_speed
+= speed
1245 elif status
== download
.DownloadTask
.FAILED
:
1246 failed_downloads
.append(task
)
1248 elif status
== download
.DownloadTask
.DONE
:
1250 elif status
== download
.DownloadTask
.QUEUED
:
1252 elif status
== download
.DownloadTask
.PAUSED
:
1257 # Remember which tasks we have seen after this run
1258 self
.download_tasks_seen
= download_tasks_seen
1260 if gpodder
.ui
.desktop
:
1261 text
= [_('Downloads')]
1262 if downloading
+ failed
+ queued
> 0:
1265 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1267 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1269 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1270 text
.append(' (' + ', '.join(s
)+')')
1271 self
.labelDownloads
.set_text(''.join(text
))
1272 elif gpodder
.ui
.diablo
:
1273 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1275 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1277 self
.tool_downloads
.set_label(_('Downloads'))
1278 elif gpodder
.ui
.fremantle
:
1279 if downloading
+ queued
> 0:
1280 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1282 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1284 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1286 self
.button_downloads
.set_value(_('Idle'))
1288 title
= [self
.default_title
]
1290 # We have to update all episodes/channels for which the status has
1291 # changed. Accessing task.status_changed has the side effect of
1292 # re-setting the changed flag, so we need to get the "changed" list
1293 # of tuples first and split it into two lists afterwards
1294 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1295 self
.download_tasks_seen
if task
.status_changed
]
1296 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1297 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1299 count
= downloading
+ queued
1301 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1304 percentage
= 100.0*done_size
/total_size
1307 total_speed
= util
.format_filesize(total_speed
)
1308 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1309 if self
.tray_icon
is not None:
1310 # Update the tray icon status and progress bar
1311 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1312 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1314 if self
.tray_icon
is not None:
1315 # Update the tray icon status
1316 self
.tray_icon
.set_status()
1317 if gpodder
.ui
.desktop
:
1318 self
.downloads_finished(self
.download_tasks_seen
)
1319 if gpodder
.ui
.diablo
:
1320 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1321 log('All downloads have finished.', sender
=self
)
1322 if self
.config
.cmd_all_downloads_complete
:
1323 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1325 if gpodder
.ui
.fremantle
and failed
:
1326 message
= '\n'.join(['%s: %s' % (str(task
), \
1327 task
.error_message
) for task
in failed_downloads
])
1328 self
.show_message(message
, _('Downloads failed'), important
=True)
1330 # Remove finished episodes
1331 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1332 self
.cleanup_downloads()
1334 # Stop updating the download list here
1335 self
.download_list_update_enabled
= False
1337 if not gpodder
.ui
.fremantle
:
1338 self
.gPodder
.set_title(' - '.join(title
))
1340 self
.update_episode_list_icons(episode_urls
)
1341 if self
.episode_shownotes_window
is not None:
1342 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1343 shownotes_task
!= self
.episode_shownotes_window
.task
:
1344 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1345 self
.episode_shownotes_window
._download
_status
_progress
()
1346 self
.play_or_download()
1348 self
.update_podcast_list_model(channel_urls
)
1350 return self
.download_list_update_enabled
1351 except Exception, e
:
1352 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1353 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1354 # We return False here, so the update loop won't be called again,
1355 # that's why we require the restart of gPodder in the message.
1358 def on_config_changed(self
, *args
):
1359 util
.idle_add(self
._on
_config
_changed
, *args
)
1361 def _on_config_changed(self
, name
, old_value
, new_value
):
1362 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1363 self
.toolbar
.set_property('visible', new_value
)
1364 elif name
== 'videoplayer':
1365 self
.config
.video_played_dbus
= False
1366 elif name
== 'player':
1367 self
.config
.audio_played_dbus
= False
1368 elif name
== 'episode_list_descriptions':
1369 self
.update_episode_list_model()
1370 elif name
== 'episode_list_thumbnails':
1371 self
.update_episode_list_icons(all
=True)
1372 elif name
== 'rotation_mode':
1373 self
._fremantle
_rotation
.set_mode(new_value
)
1374 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1375 self
.restart_auto_update_timer()
1376 elif name
== 'podcast_list_view_all':
1377 # Force a update of the podcast list model
1378 self
.channel_list_changed
= True
1379 if gpodder
.ui
.fremantle
:
1380 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1381 while gtk
.events_pending():
1382 gtk
.main_iteration(False)
1383 self
.update_podcast_list_model()
1384 if gpodder
.ui
.fremantle
:
1385 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1387 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1388 # With get_bin_window, we get the window that contains the rows without
1389 # the header. The Y coordinate of this window will be the height of the
1390 # treeview header. This is the amount we have to subtract from the
1391 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1392 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1395 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1397 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1398 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1401 if path
is not None:
1402 model
= treeview
.get_model()
1403 iter = model
.get_iter(path
)
1404 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1406 if role
== TreeViewHelper
.ROLE_EPISODES
:
1407 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1408 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1409 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1411 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1412 if last_tooltip
is not None and last_tooltip
!= id:
1413 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1415 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1417 if role
== TreeViewHelper
.ROLE_EPISODES
:
1418 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1420 tooltip
.set_text(description
)
1423 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1424 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1427 channel
.request_save_dir_size()
1428 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1429 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1431 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1432 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1433 table
= gtk
.Table(rows
=3, columns
=3)
1434 table
.set_row_spacings(5)
1435 table
.set_col_spacings(5)
1436 table
.set_border_width(5)
1438 heading
= gtk
.Label()
1439 heading
.set_alignment(0, 1)
1440 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1441 table
.attach(heading
, 0, 1, 0, 1)
1442 size_info
= gtk
.Label()
1443 size_info
.set_alignment(1, 1)
1444 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1445 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1446 table
.attach(size_info
, 2, 3, 0, 1)
1448 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1450 if len(channel
.description
) < 500:
1451 description
= channel
.description
1453 pos
= channel
.description
.find('\n\n')
1454 if pos
== -1 or pos
> 500:
1455 description
= channel
.description
[:498]+'[...]'
1457 description
= channel
.description
[:pos
]
1459 description
= gtk
.Label(description
)
1461 description
.set_markup(error_str
)
1462 description
.set_alignment(0, 0)
1463 description
.set_line_wrap(True)
1464 table
.attach(description
, 0, 3, 2, 3)
1467 tooltip
.set_custom(table
)
1471 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1474 def treeview_allow_tooltips(self
, treeview
, allow
):
1475 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1477 def update_m3u_playlist_clicked(self
, widget
):
1478 if self
.active_channel
is not None:
1479 self
.active_channel
.update_m3u_playlist()
1480 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1482 def treeview_handle_context_menu_click(self
, treeview
, event
):
1483 x
, y
= int(event
.x
), int(event
.y
)
1484 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1486 selection
= treeview
.get_selection()
1487 model
, paths
= selection
.get_selected_rows()
1489 if path
is None or (path
not in paths
and \
1490 event
.button
== self
.context_menu_mouse_button
):
1491 # We have right-clicked, but not into the selection,
1492 # assume we don't want to operate on the selection
1495 if path
is not None and not paths
and \
1496 event
.button
== self
.context_menu_mouse_button
:
1497 # No selection or clicked outside selection;
1498 # select the single item where we clicked
1499 treeview
.grab_focus()
1500 treeview
.set_cursor(path
, column
, 0)
1504 # Unselect any remaining items (clicked elsewhere)
1505 if hasattr(treeview
, 'is_rubber_banding_active'):
1506 if not treeview
.is_rubber_banding_active():
1507 selection
.unselect_all()
1509 selection
.unselect_all()
1513 def downloads_list_get_selection(self
, model
=None, paths
=None):
1514 if model
is None and paths
is None:
1515 selection
= self
.treeDownloads
.get_selection()
1516 model
, paths
= selection
.get_selected_rows()
1518 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1519 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1520 model
.get_value(model
.get_iter(path
), \
1521 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1523 for row_reference
, task
in selected_tasks
:
1524 if task
.status
!= download
.DownloadTask
.QUEUED
:
1526 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1527 download
.DownloadTask
.FAILED
, \
1528 download
.DownloadTask
.CANCELLED
):
1530 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1531 download
.DownloadTask
.QUEUED
, \
1532 download
.DownloadTask
.DOWNLOADING
):
1534 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1535 download
.DownloadTask
.DOWNLOADING
):
1537 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1538 download
.DownloadTask
.FAILED
, \
1539 download
.DownloadTask
.DONE
):
1542 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1544 def downloads_finished(self
, download_tasks_seen
):
1545 # FIXME: Filter all tasks that have already been reported
1546 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1547 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1549 if finished_downloads
and failed_downloads
:
1550 message
= self
.format_episode_list(finished_downloads
, 5)
1551 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1552 message
+= self
.format_episode_list(failed_downloads
, 5)
1553 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1554 elif finished_downloads
:
1555 message
= self
.format_episode_list(finished_downloads
)
1556 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1557 elif failed_downloads
:
1558 message
= self
.format_episode_list(failed_downloads
)
1559 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1561 # Open torrent files right after download (bug 1029)
1562 if self
.config
.open_torrent_after_download
:
1563 for task
in download_tasks_seen
:
1564 if task
.status
!= task
.DONE
:
1567 episode
= task
.episode
1568 if episode
.mimetype
!= 'application/x-bittorrent':
1571 self
.playback_episodes([episode
])
1574 def format_episode_list(self
, episode_list
, max_episodes
=10):
1576 Format a list of episode names for notifications
1578 Will truncate long episode names and limit the amount of
1579 episodes displayed (max_episodes=10).
1581 The episode_list parameter should be a list of strings.
1583 MAX_TITLE_LENGTH
= 100
1586 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1587 if len(title
) > MAX_TITLE_LENGTH
:
1588 middle
= (MAX_TITLE_LENGTH
/2)-2
1589 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1590 result
.append(saxutils
.escape(title
))
1593 more_episodes
= len(episode_list
) - max_episodes
1594 if more_episodes
> 0:
1595 result
.append('(...')
1596 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1597 result
.append('...)')
1599 return (''.join(result
)).strip()
1601 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1602 episode_urls
= set()
1603 model
= self
.treeDownloads
.get_model()
1604 for row_reference
, task
in tasks
:
1605 if status
== download
.DownloadTask
.QUEUED
:
1606 # Only queue task when its paused/failed/cancelled (or forced)
1607 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1608 self
.download_queue_manager
.add_task(task
, force_start
)
1609 self
.enable_download_list_update()
1610 elif status
== download
.DownloadTask
.CANCELLED
:
1611 # Cancelling a download allowed when downloading/queued
1612 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1613 task
.status
= status
1614 # Cancelling paused downloads requires a call to .run()
1615 elif task
.status
== task
.PAUSED
:
1616 task
.status
= status
1617 # Call run, so the partial file gets deleted
1619 elif status
== download
.DownloadTask
.PAUSED
:
1620 # Pausing a download only when queued/downloading
1621 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1622 task
.status
= status
1623 elif status
is None:
1624 # Remove the selected task - cancel downloading/queued tasks
1625 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1626 task
.status
= task
.CANCELLED
1627 model
.remove(model
.get_iter(row_reference
.get_path()))
1628 # Remember the URL, so we can tell the UI to update
1630 # We don't "see" this task anymore - remove it;
1631 # this is needed, so update_episode_list_icons()
1632 # below gets the correct list of "seen" tasks
1633 self
.download_tasks_seen
.remove(task
)
1634 except KeyError, key_error
:
1635 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1636 episode_urls
.add(task
.url
)
1637 # Tell the task that it has been removed (so it can clean up)
1638 task
.removed_from_list()
1640 # We can (hopefully) simply set the task status here
1641 task
.status
= status
1642 # Tell the podcasts tab to update icons for our removed podcasts
1643 self
.update_episode_list_icons(episode_urls
)
1644 # Update the tab title and downloads list
1645 self
.update_downloads_list()
1647 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1648 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1650 if not hasattr(treeview
, 'is_rubber_banding_active'):
1653 return not treeview
.is_rubber_banding_active()
1655 if event
.button
== self
.context_menu_mouse_button
:
1656 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1657 self
.downloads_list_get_selection(model
, paths
)
1659 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1660 # This creates a menu item for selection-wide actions
1661 item
= gtk
.ImageMenuItem(label
)
1662 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1663 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1664 item
.set_sensitive(sensitive
)
1665 return self
.set_finger_friendly(item
)
1669 item
= gtk
.ImageMenuItem(_('Episode details'))
1670 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1671 if len(selected_tasks
) == 1:
1672 row_reference
, task
= selected_tasks
[0]
1673 episode
= task
.episode
1674 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1676 item
.set_sensitive(False)
1677 menu
.append(self
.set_finger_friendly(item
))
1678 menu
.append(gtk
.SeparatorMenuItem())
1680 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1682 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1683 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1684 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1685 menu
.append(gtk
.SeparatorMenuItem())
1686 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1688 if gpodder
.ui
.maemo
:
1689 # Because we open the popup on left-click for Maemo,
1690 # we also include a non-action to close the menu
1691 menu
.append(gtk
.SeparatorMenuItem())
1692 item
= gtk
.ImageMenuItem(_('Close this menu'))
1693 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1695 menu
.append(self
.set_finger_friendly(item
))
1698 menu
.popup(None, None, None, event
.button
, event
.time
)
1701 def treeview_channels_show_context_menu(self
, treeview
, event
):
1702 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1706 # Check for valid channel id, if there's no id then
1707 # assume that it is a proxy channel or equivalent
1708 # and cannot be operated with right click
1709 if self
.active_channel
.id is None:
1712 if event
.button
== 3:
1717 item
= gtk
.ImageMenuItem( _('Update podcast'))
1718 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1719 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1720 item
.set_sensitive(not self
.updating_feed_cache
)
1723 menu
.append(gtk
.SeparatorMenuItem())
1725 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1726 item
.set_active(self
.active_channel
.channel_is_locked
)
1727 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1728 menu
.append(self
.set_finger_friendly(item
))
1730 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1731 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1732 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1735 if self
.config
.device_type
!= 'none':
1736 item
= gtk
.MenuItem(_('Synchronize to device'))
1737 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1740 menu
.append( gtk
.SeparatorMenuItem())
1742 item
= gtk
.ImageMenuItem(_('Podcast details'))
1743 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1744 item
.connect('activate', self
.on_itemEditChannel_activate
)
1748 # Disable tooltips while we are showing the menu, so
1749 # the tooltip will not appear over the menu
1750 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1751 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1752 menu
.popup( None, None, None, event
.button
, event
.time
)
1756 def on_itemClose_activate(self
, widget
):
1757 if self
.tray_icon
is not None:
1758 self
.iconify_main_window()
1760 self
.on_gPodder_delete_event(widget
)
1762 def cover_file_removed(self
, channel_url
):
1764 The Cover Downloader calls this when a previously-
1765 available cover has been removed from the disk. We
1766 have to update our model to reflect this change.
1768 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1770 def cover_download_finished(self
, channel_url
, pixbuf
):
1772 The Cover Downloader calls this when it has finished
1773 downloading (or registering, if already downloaded)
1774 a new channel cover, which is ready for displaying.
1776 self
.podcast_list_model
.add_cover_by_url(channel_url
, pixbuf
)
1778 def save_episodes_as_file(self
, episodes
):
1779 for episode
in episodes
:
1780 self
.save_episode_as_file(episode
)
1782 def save_episode_as_file(self
, episode
):
1783 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1784 if episode
.was_downloaded(and_exists
=True):
1785 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1786 copy_from
= episode
.local_filename(create
=False)
1787 assert copy_from
is not None
1788 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1789 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1790 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1792 def copy_episodes_bluetooth(self
, episodes
):
1793 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1795 if gpodder
.ui
.maemo
:
1796 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1797 for e
in episodes_to_copy
])
1800 def convert_and_send_thread(episode
):
1801 for episode
in episodes
:
1802 filename
= episode
.local_filename(create
=False)
1803 assert filename
is not None
1804 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1805 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1806 (base
, ext
) = os
.path
.splitext(filename
)
1807 if not destfile
.endswith(ext
):
1811 shutil
.copyfile(filename
, destfile
)
1812 util
.bluetooth_send_file(destfile
)
1814 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1815 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1817 util
.delete_file(destfile
)
1819 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1821 def get_device_name(self
):
1822 if self
.config
.device_type
== 'ipod':
1824 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1825 return _('MP3 player')
1827 return '(unknown device)'
1829 def _treeview_button_released(self
, treeview
, event
):
1830 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1831 dy
= int(abs(event
.y
-ypos
))
1832 dx
= int(event
.x
-xpos
)
1834 selection
= treeview
.get_selection()
1835 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1836 if path
is None or dy
> 30:
1837 return (False, dx
, dy
)
1839 path
, column
, x
, y
= path
1840 selection
.select_path(path
)
1841 treeview
.set_cursor(path
)
1842 treeview
.grab_focus()
1844 return (True, dx
, dy
)
1846 def treeview_channels_handle_gestures(self
, treeview
, event
):
1847 if self
.currently_updating
:
1850 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1853 if self
.config
.maemo_enable_gestures
:
1855 self
.on_itemUpdateChannel_activate()
1857 self
.on_itemEditChannel_activate(treeview
)
1861 def treeview_available_handle_gestures(self
, treeview
, event
):
1862 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1865 if self
.config
.maemo_enable_gestures
:
1867 self
.on_playback_selected_episodes(None)
1870 self
.on_shownotes_selected_episodes(None)
1873 # Pass the event to the context menu handler for treeAvailable
1874 self
.treeview_available_show_context_menu(treeview
, event
)
1878 def treeview_available_show_context_menu(self
, treeview
, event
):
1879 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1881 if not hasattr(treeview
, 'is_rubber_banding_active'):
1884 return not treeview
.is_rubber_banding_active()
1886 if event
.button
== self
.context_menu_mouse_button
:
1887 episodes
= self
.get_selected_episodes()
1888 any_locked
= any(e
.is_locked
for e
in episodes
)
1889 any_played
= any(e
.is_played
for e
in episodes
)
1890 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1891 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1892 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1896 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1898 if open_instead_of_play
:
1899 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1901 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1903 item
= gtk
.ImageMenuItem(_('Stream'))
1904 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1906 item
.set_sensitive(can_play
and not downloading
)
1907 item
.connect('activate', self
.on_playback_selected_episodes
)
1908 menu
.append(self
.set_finger_friendly(item
))
1911 item
= gtk
.ImageMenuItem(_('Download'))
1912 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1913 item
.set_sensitive(can_download
)
1914 item
.connect('activate', self
.on_download_selected_episodes
)
1915 menu
.append(self
.set_finger_friendly(item
))
1917 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1918 item
.connect('activate', self
.on_item_cancel_download_activate
)
1919 menu
.append(self
.set_finger_friendly(item
))
1921 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1922 item
.set_sensitive(can_delete
)
1923 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1924 menu
.append(self
.set_finger_friendly(item
))
1928 # Ok, this probably makes sense to only display for downloaded files
1930 menu
.append(gtk
.SeparatorMenuItem())
1931 share_item
= gtk
.MenuItem(_('Send to'))
1932 menu
.append(self
.set_finger_friendly(share_item
))
1933 share_menu
= gtk
.Menu()
1935 item
= gtk
.ImageMenuItem(_('Local folder'))
1936 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
1937 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
1938 share_menu
.append(self
.set_finger_friendly(item
))
1939 if self
.bluetooth_available
:
1940 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
1941 if gpodder
.ui
.maemo
:
1942 icon_name
= ICON('qgn_list_filesys_bluetooth')
1944 icon_name
= ICON('bluetooth')
1945 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
1946 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
1947 share_menu
.append(self
.set_finger_friendly(item
))
1949 item
= gtk
.ImageMenuItem(self
.get_device_name())
1950 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
1951 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
1952 share_menu
.append(self
.set_finger_friendly(item
))
1954 share_item
.set_submenu(share_menu
)
1956 if (downloaded
or one_is_new
or can_download
) and not downloading
:
1957 menu
.append(gtk
.SeparatorMenuItem())
1959 item
= gtk
.CheckMenuItem(_('New'))
1960 item
.set_active(True)
1961 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
1962 menu
.append(self
.set_finger_friendly(item
))
1964 item
= gtk
.CheckMenuItem(_('New'))
1965 item
.set_active(False)
1966 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
1967 menu
.append(self
.set_finger_friendly(item
))
1970 item
= gtk
.CheckMenuItem(_('Played'))
1971 item
.set_active(any_played
)
1972 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
1973 menu
.append(self
.set_finger_friendly(item
))
1975 item
= gtk
.CheckMenuItem(_('Keep episode'))
1976 item
.set_active(any_locked
)
1977 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
1978 menu
.append(self
.set_finger_friendly(item
))
1980 menu
.append(gtk
.SeparatorMenuItem())
1981 # Single item, add episode information menu item
1982 item
= gtk
.ImageMenuItem(_('Episode details'))
1983 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1984 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
1985 menu
.append(self
.set_finger_friendly(item
))
1987 if gpodder
.ui
.maemo
:
1988 # Because we open the popup on left-click for Maemo,
1989 # we also include a non-action to close the menu
1990 menu
.append(gtk
.SeparatorMenuItem())
1991 item
= gtk
.ImageMenuItem(_('Close this menu'))
1992 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1993 menu
.append(self
.set_finger_friendly(item
))
1996 # Disable tooltips while we are showing the menu, so
1997 # the tooltip will not appear over the menu
1998 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
1999 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2000 menu
.popup( None, None, None, event
.button
, event
.time
)
2004 def set_title(self
, new_title
):
2005 if not gpodder
.ui
.fremantle
:
2006 self
.default_title
= new_title
2007 self
.gPodder
.set_title(new_title
)
2009 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2011 Updates the status icons in the episode list.
2013 If urls is given, it should be a list of URLs
2014 of episodes that should be updated.
2016 If urls is None, set ONE OF selected, all to
2017 True (the former updates just the selected
2018 episodes and the latter updates all episodes).
2020 additional_args
= (self
.episode_is_downloading
, \
2021 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2022 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2024 if urls
is not None:
2025 # We have a list of URLs to walk through
2026 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2027 elif selected
and not all
:
2028 # We should update all selected episodes
2029 selection
= self
.treeAvailable
.get_selection()
2030 model
, paths
= selection
.get_selected_rows()
2031 for path
in reversed(paths
):
2032 iter = model
.get_iter(path
)
2033 self
.episode_list_model
.update_by_filter_iter(iter, \
2035 elif all
and not selected
:
2036 # We update all (even the filter-hidden) episodes
2037 self
.episode_list_model
.update_all(*additional_args
)
2039 # Wrong/invalid call - have to specify at least one parameter
2040 raise ValueError('Invalid call to update_episode_list_icons')
2042 def episode_list_status_changed(self
, episodes
):
2043 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2044 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2047 def clean_up_downloads(self
, delete_partial
=False):
2048 # Clean up temporary files left behind by old gPodder versions
2049 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2052 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2054 for tempfile
in temporary_files
:
2055 util
.delete_file(tempfile
)
2057 # Clean up empty download folders and abandoned download folders
2058 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2059 for ddir
in download_dirs
:
2060 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2061 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2062 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2063 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2064 shutil
.rmtree(ddir
, ignore_errors
=True)
2066 def streaming_possible(self
):
2067 if gpodder
.ui
.desktop
:
2068 # User has to have a media player set on the Desktop, or else we
2069 # would probably open the browser when giving a URL to xdg-open..
2070 return (self
.config
.player
and self
.config
.player
!= 'default')
2071 elif gpodder
.ui
.maemo
:
2072 # On Maemo, the default is to use the Nokia Media Player, which is
2073 # already able to deal with HTTP URLs the right way, so we
2074 # unconditionally enable streaming always on Maemo
2079 def playback_episodes_for_real(self
, episodes
):
2080 groups
= collections
.defaultdict(list)
2081 for episode
in episodes
:
2082 file_type
= episode
.file_type()
2083 if file_type
== 'video' and self
.config
.videoplayer
and \
2084 self
.config
.videoplayer
!= 'default':
2085 player
= self
.config
.videoplayer
2086 if gpodder
.ui
.diablo
:
2087 # Use the wrapper script if it's installed to crop 3GP YouTube
2088 # videos to fit the screen (looks much nicer than w/ black border)
2089 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2090 player
= 'gpodder-mplayer'
2091 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2092 player
= 'mplayer -fs %F'
2093 elif file_type
== 'audio' and self
.config
.player
and \
2094 self
.config
.player
!= 'default':
2095 player
= self
.config
.player
2099 if file_type
not in ('audio', 'video') or \
2100 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2101 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2102 # Mark episode as played in the database
2103 episode
.mark(is_played
=True)
2104 self
.mygpo_client
.on_playback([episode
])
2106 filename
= episode
.local_filename(create
=False)
2107 if filename
is None or not os
.path
.exists(filename
):
2108 filename
= episode
.url
2109 if youtube
.is_video_link(filename
):
2110 fmt_id
= self
.config
.youtube_preferred_fmt_id
2111 if gpodder
.ui
.fremantle
:
2113 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2115 # Determine the playback resume position - if the file
2116 # was played 100%, we simply start from the beginning
2117 resume_position
= episode
.current_position
2118 if resume_position
== episode
.total_time
:
2121 if gpodder
.ui
.fremantle
:
2122 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2124 # If Panucci is configured, use D-Bus on Maemo to call it
2125 if player
== 'panucci':
2127 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2128 PANUCCI_PATH
= '/panucciInterface'
2129 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2130 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2131 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2133 def on_reply(*args
):
2137 log('Exception in D-Bus call: %s', str(err
), \
2140 # This method only exists in Panucci > 0.9 ('new Panucci')
2141 i
.playback_from(filename
, resume_position
, \
2142 reply_handler
=on_reply
, error_handler
=on_error
)
2144 continue # This file was handled by the D-Bus call
2145 except Exception, e
:
2146 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2147 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2149 MEDIABOX_NAME
= 'de.pycage.mediabox'
2150 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2151 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2152 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2153 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2155 def on_reply(*args
):
2159 log('Exception in D-Bus call: %s', str(err
), \
2162 i
.load(filename
, '%s/x-unknown' % file_type
, \
2163 reply_handler
=on_reply
, error_handler
=on_error
)
2165 continue # This file was handled by the D-Bus call
2166 except Exception, e
:
2167 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2169 groups
[player
].append(filename
)
2171 # Open episodes with system default player
2172 if 'default' in groups
:
2173 if gpodder
.ui
.maemo
:
2174 # The Nokia Media Player app does not support receiving multiple
2175 # file names via D-Bus, so we simply place all file names into a
2176 # temporary M3U playlist and open that with the Media Player.
2177 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2178 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2179 util
.gui_open(m3u_filename
)
2181 for filename
in groups
['default']:
2182 log('Opening with system default: %s', filename
, sender
=self
)
2183 util
.gui_open(filename
)
2184 del groups
['default']
2185 elif gpodder
.ui
.maemo
and groups
:
2186 # When on Maemo and not opening with default, show a notification
2187 # (no startup notification for Panucci / MPlayer yet...)
2188 if len(episodes
) == 1:
2189 text
= _('Opening %s') % episodes
[0].title
2191 count
= len(episodes
)
2192 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2194 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2196 def destroy_banner_later(banner
):
2199 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2201 # For each type now, go and create play commands
2202 for group
in groups
:
2203 for command
in util
.format_desktop_command(group
, groups
[group
]):
2204 log('Executing: %s', repr(command
), sender
=self
)
2205 subprocess
.Popen(command
)
2207 # Persist episode status changes to the database
2210 # Flush updated episode status
2211 self
.mygpo_client
.flush()
2213 def playback_episodes(self
, episodes
):
2214 # We need to create a list, because we run through it more than once
2215 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2216 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2219 self
.playback_episodes_for_real(episodes
)
2220 except Exception, e
:
2221 log('Error in playback!', sender
=self
, traceback
=True)
2222 if gpodder
.ui
.desktop
:
2223 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2224 _('Error opening player'), widget
=self
.toolPreferences
)
2226 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2228 channel_urls
= set()
2229 episode_urls
= set()
2230 for episode
in episodes
:
2231 channel_urls
.add(episode
.channel
.url
)
2232 episode_urls
.add(episode
.url
)
2233 self
.update_episode_list_icons(episode_urls
)
2234 self
.update_podcast_list_model(channel_urls
)
2236 def play_or_download(self
):
2237 if not gpodder
.ui
.fremantle
:
2238 if self
.wNotebook
.get_current_page() > 0:
2239 if gpodder
.ui
.desktop
:
2240 self
.toolCancel
.set_sensitive(True)
2243 if self
.currently_updating
:
2244 return (False, False, False, False, False, False)
2246 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2247 ( is_played
, is_locked
) = (False,)*2
2249 open_instead_of_play
= False
2251 selection
= self
.treeAvailable
.get_selection()
2252 if selection
.count_selected_rows() > 0:
2253 (model
, paths
) = selection
.get_selected_rows()
2257 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2258 except TypeError, te
:
2259 log('Invalid episode at path %s', str(path
), sender
=self
)
2262 if episode
.file_type() not in ('audio', 'video'):
2263 open_instead_of_play
= True
2265 if episode
.was_downloaded():
2266 can_play
= episode
.was_downloaded(and_exists
=True)
2267 is_played
= episode
.is_played
2268 is_locked
= episode
.is_locked
2272 if self
.episode_is_downloading(episode
):
2277 can_download
= can_download
and not can_cancel
2278 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2279 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2280 can_delete
= not can_cancel
2282 if gpodder
.ui
.desktop
:
2283 if open_instead_of_play
:
2284 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2286 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2287 self
.toolPlay
.set_sensitive( can_play
)
2288 self
.toolDownload
.set_sensitive( can_download
)
2289 self
.toolTransfer
.set_sensitive( can_transfer
)
2290 self
.toolCancel
.set_sensitive( can_cancel
)
2292 if not gpodder
.ui
.fremantle
:
2293 self
.item_cancel_download
.set_sensitive(can_cancel
)
2294 self
.itemDownloadSelected
.set_sensitive(can_download
)
2295 self
.itemOpenSelected
.set_sensitive(can_play
)
2296 self
.itemPlaySelected
.set_sensitive(can_play
)
2297 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2298 self
.item_toggle_played
.set_sensitive(can_play
)
2299 self
.item_toggle_lock
.set_sensitive(can_play
)
2300 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2301 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2303 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2305 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2306 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2308 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2309 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2311 def episode_new_status_changed(self
, urls
):
2312 self
.update_podcast_list_model()
2313 self
.update_episode_list_icons(urls
)
2315 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2316 """Update the podcast list treeview model
2318 If urls is given, it should list the URLs of each
2319 podcast that has to be updated in the list.
2321 If selected is True, only update the model contents
2322 for the currently-selected podcast - nothing more.
2324 The caller can optionally specify "select_url",
2325 which is the URL of the podcast that is to be
2326 selected in the list after the update is complete.
2327 This only works if the podcast list has to be
2328 reloaded; i.e. something has been added or removed
2329 since the last update of the podcast list).
2331 selection
= self
.treeChannels
.get_selection()
2332 model
, iter = selection
.get_selected()
2334 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2335 # Update "all episodes" view in any case (if enabled)
2336 self
.podcast_list_model
.update_first_row()
2339 # very cheap! only update selected channel
2340 if iter is not None:
2341 # If we have selected the "all episodes" view, we have
2342 # to update all channels for selected episodes:
2343 if self
.config
.podcast_list_view_all
and \
2344 self
.podcast_list_model
.iter_is_first_row(iter):
2345 urls
= self
.get_podcast_urls_from_selected_episodes()
2346 self
.podcast_list_model
.update_by_urls(urls
)
2348 # Otherwise just update the selected row (a podcast)
2349 self
.podcast_list_model
.update_by_filter_iter(iter)
2350 elif not self
.channel_list_changed
:
2351 # we can keep the model, but have to update some
2353 # still cheaper than reloading the whole list
2354 self
.podcast_list_model
.update_all()
2356 # ok, we got a bunch of urls to update
2357 self
.podcast_list_model
.update_by_urls(urls
)
2359 if model
and iter and select_url
is None:
2360 # Get the URL of the currently-selected podcast
2361 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2363 # Update the podcast list model with new channels
2364 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2367 selected_iter
= model
.get_iter_first()
2368 # Find the previously-selected URL in the new
2369 # model if we have an URL (else select first)
2370 if select_url
is not None:
2371 pos
= model
.get_iter_first()
2372 while pos
is not None:
2373 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2374 if url
== select_url
:
2377 pos
= model
.iter_next(pos
)
2379 if not gpodder
.ui
.fremantle
:
2380 if selected_iter
is not None:
2381 selection
.select_iter(selected_iter
)
2382 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2384 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2385 self
.channel_list_changed
= False
2387 def episode_is_downloading(self
, episode
):
2388 """Returns True if the given episode is being downloaded at the moment"""
2392 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2394 def update_episode_list_model(self
):
2395 if self
.channels
and self
.active_channel
is not None:
2396 if gpodder
.ui
.fremantle
:
2397 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2399 self
.currently_updating
= True
2400 self
.episode_list_model
.clear()
2401 self
.episode_list_model
.reset_update_progress()
2402 self
.treeAvailable
.set_model(self
.empty_episode_list_model
)
2403 def do_update_episode_list_model():
2404 additional_args
= (self
.episode_is_downloading
, \
2405 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2406 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
, \
2408 self
.episode_list_model
.add_from_channel(self
.active_channel
, *additional_args
)
2410 def on_episode_list_model_updated():
2411 if gpodder
.ui
.fremantle
:
2412 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2413 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
2414 self
.treeAvailable
.columns_autosize()
2415 self
.currently_updating
= False
2416 self
.play_or_download()
2417 util
.idle_add(on_episode_list_model_updated
)
2418 threading
.Thread(target
=do_update_episode_list_model
).start()
2420 self
.episode_list_model
.clear()
2422 @dbus.service
.method(gpodder
.dbus_interface
)
2423 def offer_new_episodes(self
, channels
=None):
2424 new_episodes
= self
.get_new_episodes(channels
)
2426 self
.new_episodes_show(new_episodes
)
2430 def add_podcast_list(self
, urls
, auth_tokens
=None):
2431 """Subscribe to a list of podcast given their URLs
2433 If auth_tokens is given, it should be a dictionary
2434 mapping URLs to (username, password) tuples."""
2436 if auth_tokens
is None:
2439 # Sort and split the URL list into five buckets
2440 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2441 for input_url
in urls
:
2442 url
= util
.normalize_feed_url(input_url
)
2444 # Fail this one because the URL is not valid
2445 failed
.append(input_url
)
2446 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2447 # A podcast already exists in the list for this URL
2448 existing
.append(url
)
2450 # This URL has survived the first round - queue for add
2452 if url
!= input_url
and input_url
in auth_tokens
:
2453 auth_tokens
[url
] = auth_tokens
[input_url
]
2458 progress
= ProgressIndicator(_('Adding podcasts'), \
2459 _('Please wait while episode information is downloaded.'), \
2460 parent
=self
.get_dialog_parent())
2462 def on_after_update():
2463 progress
.on_finished()
2464 # Report already-existing subscriptions to the user
2466 title
= _('Existing subscriptions skipped')
2467 message
= _('You are already subscribed to these podcasts:') \
2468 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2469 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2471 # Report subscriptions that require authentication
2475 title
= _('Podcast requires authentication')
2476 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2477 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2479 retry_podcasts
[url
] = auth_tokens
2481 # Stop asking the user for more login data
2484 error_messages
[url
] = _('Authentication failed')
2488 # If we have authentication data to retry, do so here
2490 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2492 # Report website redirections
2493 for url
in redirections
:
2494 title
= _('Website redirection detected')
2495 message
= _('The URL %(url)s redirects to %(target)s.') \
2496 + '\n\n' + _('Do you want to visit the website now?')
2497 message
= message
% {'url': url
, 'target': redirections
[url
]}
2498 if self
.show_confirmation(message
, title
):
2499 util
.open_website(url
)
2503 # Report failed subscriptions to the user
2505 title
= _('Could not add some podcasts')
2506 message
= _('Some podcasts could not be added to your list:') \
2507 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2508 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2509 self
.show_message(message
, title
, important
=True)
2511 # Upload subscription changes to gpodder.net
2512 self
.mygpo_client
.on_subscribe(worked
)
2514 # If at least one podcast has been added, save and update all
2515 if self
.channel_list_changed
:
2516 # Fix URLs if mygpo has rewritten them
2517 self
.rewrite_urls_mygpo()
2519 self
.save_channels_opml()
2521 # If only one podcast was added, select it after the update
2522 if len(worked
) == 1:
2527 # Update the list of subscribed podcasts
2528 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2529 self
.update_podcasts_tab()
2531 # Offer to download new episodes
2532 self
.offer_new_episodes(channels
=[c
for c
in self
.channels
if c
.url
in worked
])
2535 # After the initial sorting and splitting, try all queued podcasts
2536 length
= len(queued
)
2537 for index
, url
in enumerate(queued
):
2538 progress
.on_progress(float(index
)/float(length
))
2539 progress
.on_message(url
)
2540 log('QUEUE RUNNER: %s', url
, sender
=self
)
2542 # The URL is valid and does not exist already - subscribe!
2543 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2544 authentication_tokens
=auth_tokens
.get(url
, None), \
2545 max_episodes
=self
.config
.max_episodes_per_feed
, \
2546 download_dir
=self
.config
.download_dir
, \
2547 allow_empty_feeds
=self
.config
.allow_empty_feeds
)
2550 username
, password
= util
.username_password_from_url(url
)
2551 except ValueError, ve
:
2552 username
, password
= (None, None)
2554 if username
is not None and channel
.username
is None and \
2555 password
is not None and channel
.password
is None:
2556 channel
.username
= username
2557 channel
.password
= password
2560 self
._update
_cover
(channel
)
2561 except feedcore
.AuthenticationRequired
:
2562 if url
in auth_tokens
:
2563 # Fail for wrong authentication data
2564 error_messages
[url
] = _('Authentication failed')
2567 # Queue for login dialog later
2570 except feedcore
.WifiLogin
, error
:
2571 redirections
[url
] = error
.data
2573 error_messages
[url
] = _('Redirection detected')
2575 except Exception, e
:
2576 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2577 error_messages
[url
] = str(e
)
2581 assert channel
is not None
2582 worked
.append(channel
.url
)
2583 self
.channels
.append(channel
)
2584 self
.channel_list_changed
= True
2585 util
.idle_add(on_after_update
)
2586 threading
.Thread(target
=thread_proc
).start()
2588 def save_channels_opml(self
):
2589 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2590 return exporter
.write(self
.channels
)
2592 def find_episode(self
, podcast_url
, episode_url
):
2593 """Find an episode given its podcast and episode URL
2595 The function will return a PodcastEpisode object if
2596 the episode is found, or None if it's not found.
2598 for podcast
in self
.channels
:
2599 if podcast_url
== podcast
.url
:
2600 for episode
in podcast
.get_all_episodes():
2601 if episode_url
== episode
.url
:
2606 def process_received_episode_actions(self
, updated_urls
):
2607 """Process/merge episode actions from gpodder.net
2609 This function will merge all changes received from
2610 the server to the local database and update the
2611 status of the affected episodes as necessary.
2613 indicator
= ProgressIndicator(_('Merging episode actions'), \
2614 _('Episode actions from gpodder.net are merged.'), \
2615 False, self
.get_dialog_parent())
2617 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2618 if action
.action
== 'play':
2619 episode
= self
.find_episode(action
.podcast_url
, \
2622 if episode
is not None:
2623 log('Play action for %s', episode
.url
, sender
=self
)
2624 episode
.mark(is_played
=True)
2626 if action
.timestamp
> episode
.current_position_updated
:
2627 log('Updating position for %s', episode
.url
, sender
=self
)
2628 episode
.current_position
= action
.position
2629 episode
.current_position_updated
= action
.timestamp
2632 log('Updating total time for %s', episode
.url
, sender
=self
)
2633 episode
.total_time
= action
.total
2636 elif action
.action
== 'delete':
2637 episode
= self
.find_episode(action
.podcast_url
, \
2640 if episode
is not None:
2641 if not episode
.was_downloaded(and_exists
=True):
2642 # Set the episode to a "deleted" state
2643 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2644 episode
.delete_from_disk()
2647 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2648 gtk
.main_iteration(False)
2650 indicator
.on_finished()
2654 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2656 self
.updating_feed_cache
= False
2658 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2660 # Process received episode actions for all updated URLs
2661 self
.process_received_episode_actions(updated_urls
)
2663 self
.channel_list_changed
= True
2664 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2666 # Only search for new episodes in podcasts that have been
2667 # updated, not in other podcasts (for single-feed updates)
2668 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2670 if gpodder
.ui
.fremantle
:
2671 self
.button_subscribe
.set_sensitive(True)
2672 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2673 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
2674 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2675 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2676 self
.update_podcasts_tab()
2677 self
.update_episode_list_model()
2678 if self
.feed_cache_update_cancelled
:
2682 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2683 # New episodes found, but we should do nothing
2684 self
.show_message(_('New episodes are available.'))
2685 elif self
.config
.auto_download
== 'always':
2686 count
= len(episodes
)
2687 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2688 self
.show_message(title
)
2689 self
.download_episode_list(episodes
)
2690 elif self
.config
.auto_download
== 'queue':
2691 self
.show_message(_('New episodes have been added to the download list.'))
2692 self
.download_episode_list_paused(episodes
)
2696 pynotify
.init('gPodder')
2697 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2698 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2699 n
.set_hint('dbus-callback-default', ' '.join([
2700 gpodder
.dbus_bus_name
,
2701 gpodder
.dbus_gui_object_path
,
2702 gpodder
.dbus_interface
,
2703 'offer_new_episodes',
2705 n
.set_category('gpodder-new-episodes')
2707 except Exception, e
:
2708 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2709 self
.new_episodes_show(episodes
)
2710 elif not self
.config
.auto_update_feeds
:
2711 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2715 self
.tray_icon
.set_status()
2717 if self
.feed_cache_update_cancelled
:
2718 # The user decided to abort the feed update
2719 self
.show_update_feeds_buttons()
2721 # Nothing new here - but inform the user
2722 self
.pbFeedUpdate
.set_fraction(1.0)
2723 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2724 self
.feed_cache_update_cancelled
= True
2725 self
.btnCancelFeedUpdate
.show()
2726 self
.btnCancelFeedUpdate
.set_sensitive(True)
2727 if gpodder
.ui
.maemo
:
2728 # btnCancelFeedUpdate is a ToolButton on Maemo
2729 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2731 # btnCancelFeedUpdate is a normal gtk.Button
2732 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2734 count
= len(episodes
)
2735 # New episodes are available
2736 self
.pbFeedUpdate
.set_fraction(1.0)
2737 # Are we minimized and should we auto download?
2738 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2739 self
.download_episode_list(episodes
)
2740 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2741 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2742 self
.show_update_feeds_buttons()
2743 elif self
.config
.auto_download
== 'queue':
2744 self
.download_episode_list_paused(episodes
)
2745 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2746 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2747 self
.show_update_feeds_buttons()
2749 self
.show_update_feeds_buttons()
2750 # New episodes are available and we are not minimized
2751 if not self
.config
.do_not_show_new_episodes_dialog
:
2752 self
.new_episodes_show(episodes
, notification
=True)
2754 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2755 self
.pbFeedUpdate
.set_text(message
)
2757 def _update_cover(self
, channel
):
2758 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2759 self
.cover_downloader
.request_cover(channel
)
2761 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2762 total
= len(channels
)
2764 for updated
, channel
in enumerate(channels
):
2765 if not self
.feed_cache_update_cancelled
:
2767 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
)
2768 self
._update
_cover
(channel
)
2769 except Exception, e
:
2770 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2772 message
= _('Error while updating %(url)s: %(message)s')
2774 message
= _('The feed at %(url)s could not be updated.')
2775 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2776 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2778 if self
.feed_cache_update_cancelled
:
2781 if gpodder
.ui
.fremantle
:
2782 util
.idle_add(self
.button_refresh
.set_title
, \
2783 _('%(position)d/%(total)d updated') % {'position': updated
, 'total': total
})
2786 # By the time we get here the update may have already been cancelled
2787 if not self
.feed_cache_update_cancelled
:
2788 def update_progress():
2789 d
= {'podcast': channel
.title
, 'position': updated
, 'total': total
}
2790 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2791 self
.pbFeedUpdate
.set_text(progression
)
2793 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2794 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2795 util
.idle_add(update_progress
)
2797 updated_urls
= [c
.url
for c
in channels
]
2798 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2800 def show_update_feeds_buttons(self
):
2801 # Make sure that the buttons for updating feeds
2802 # appear - this should happen after a feed update
2803 if gpodder
.ui
.maemo
:
2804 self
.btnUpdateSelectedFeed
.show()
2805 self
.toolFeedUpdateProgress
.hide()
2806 self
.btnCancelFeedUpdate
.hide()
2807 self
.btnCancelFeedUpdate
.set_is_important(False)
2808 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2809 self
.toolbarSpacer
.set_expand(True)
2810 self
.toolbarSpacer
.set_draw(False)
2812 self
.hboxUpdateFeeds
.hide()
2813 self
.btnUpdateFeeds
.show()
2814 self
.itemUpdate
.set_sensitive(True)
2815 self
.itemUpdateChannel
.set_sensitive(True)
2817 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2818 if not self
.feed_cache_update_cancelled
:
2819 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2820 self
.feed_cache_update_cancelled
= True
2821 self
.btnCancelFeedUpdate
.set_sensitive(False)
2823 self
.show_update_feeds_buttons()
2825 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2826 if self
.updating_feed_cache
:
2827 if gpodder
.ui
.fremantle
:
2828 self
.feed_cache_update_cancelled
= True
2831 if not force_update
:
2832 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2833 self
.channel_list_changed
= True
2834 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2837 # Fix URLs if mygpo has rewritten them
2838 self
.rewrite_urls_mygpo()
2840 self
.updating_feed_cache
= True
2842 if channels
is None:
2843 channels
= self
.channels
2845 if gpodder
.ui
.fremantle
:
2846 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2847 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2848 self
.button_refresh
.set_title(_('Updating...'))
2849 self
.button_subscribe
.set_sensitive(False)
2850 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2851 self
.ICON_GENERAL_CLOSE
, gtk
.ICON_SIZE_BUTTON
))
2852 self
.feed_cache_update_cancelled
= False
2854 self
.itemUpdate
.set_sensitive(False)
2855 self
.itemUpdateChannel
.set_sensitive(False)
2858 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2860 if len(channels
) == 1:
2861 text
= _('Updating "%s"...') % channels
[0].title
2863 count
= len(channels
)
2864 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2865 self
.pbFeedUpdate
.set_text(text
)
2866 self
.pbFeedUpdate
.set_fraction(0)
2868 self
.feed_cache_update_cancelled
= False
2869 self
.btnCancelFeedUpdate
.show()
2870 self
.btnCancelFeedUpdate
.set_sensitive(True)
2871 if gpodder
.ui
.maemo
:
2872 self
.toolbarSpacer
.set_expand(False)
2873 self
.toolbarSpacer
.set_draw(True)
2874 self
.btnUpdateSelectedFeed
.hide()
2875 self
.toolFeedUpdateProgress
.show_all()
2877 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2878 self
.hboxUpdateFeeds
.show_all()
2879 self
.btnUpdateFeeds
.hide()
2881 args
= (channels
, select_url_afterwards
)
2882 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2884 def on_gPodder_delete_event(self
, widget
, *args
):
2885 """Called when the GUI wants to close the window
2886 Displays a confirmation dialog (and closes/hides gPodder)
2889 downloading
= self
.download_status_model
.are_downloads_in_progress()
2891 # Only iconify if we are using the window's "X" button,
2892 # but not when we are using "Quit" in the menu or toolbar
2893 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
2894 self
.iconify_main_window()
2895 elif self
.config
.on_quit_ask
or downloading
:
2896 if gpodder
.ui
.fremantle
:
2897 self
.close_gpodder()
2898 elif gpodder
.ui
.diablo
:
2899 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2901 self
.close_gpodder()
2904 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2905 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2906 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2908 title
= _('Quit gPodder')
2910 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2912 message
= _('Do you really want to quit gPodder now?')
2914 dialog
.set_title(title
)
2915 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2917 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
2918 dialog
.vbox
.pack_start(cb_ask
)
2921 quit_button
.grab_focus()
2922 result
= dialog
.run()
2925 if result
== gtk
.RESPONSE_CLOSE
:
2926 if not downloading
and cb_ask
.get_active() == True:
2927 self
.config
.on_quit_ask
= False
2928 self
.close_gpodder()
2930 self
.close_gpodder()
2934 def close_gpodder(self
):
2935 """ clean everything and exit properly
2938 if self
.save_channels_opml():
2939 pass # FIXME: Add mygpo synchronization here
2941 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
2945 if self
.tray_icon
is not None:
2946 self
.tray_icon
.set_visible(False)
2948 # Notify all tasks to to carry out any clean-up actions
2949 self
.download_status_model
.tell_all_tasks_to_quit()
2951 while gtk
.events_pending():
2952 gtk
.main_iteration(False)
2959 def get_expired_episodes(self
):
2960 for channel
in self
.channels
:
2961 for episode
in channel
.get_downloaded_episodes():
2962 # Never consider locked episodes as old
2963 if episode
.is_locked
:
2966 # Never consider fresh episodes as old
2967 if episode
.age_in_days() < self
.config
.episode_old_age
:
2970 # Do not delete played episodes (except if configured)
2971 if episode
.is_played
:
2972 if not self
.config
.auto_remove_played_episodes
:
2975 # Do not delete unplayed episodes (except if configured)
2976 if not episode
.is_played
:
2977 if not self
.config
.auto_remove_unplayed_episodes
:
2982 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
2987 episodes
= [e
for e
in episodes
if not e
.is_locked
]
2990 title
= _('Episodes are locked')
2991 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2992 self
.notification(message
, title
, widget
=self
.treeAvailable
)
2995 count
= len(episodes
)
2996 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
2997 message
= _('Deleting episodes removes downloaded files.')
2999 if gpodder
.ui
.fremantle
:
3000 message
= '\n'.join([title
, message
])
3002 if confirm
and not self
.show_confirmation(message
, title
):
3005 progress
= ProgressIndicator(_('Deleting episodes'), \
3006 _('Please wait while episodes are deleted'), \
3007 parent
=self
.get_dialog_parent())
3009 def finish_deletion(episode_urls
, channel_urls
):
3010 progress
.on_finished()
3012 # Episodes have been deleted - persist the database
3015 self
.update_episode_list_icons(episode_urls
)
3016 self
.update_podcast_list_model(channel_urls
)
3017 self
.play_or_download()
3020 episode_urls
= set()
3021 channel_urls
= set()
3023 episodes_status_update
= []
3024 for idx
, episode
in enumerate(episodes
):
3025 progress
.on_progress(float(idx
)/float(len(episodes
)))
3026 if episode
.is_locked
and skip_locked
:
3027 log('Not deleting episode (is locked): %s', episode
.title
)
3029 log('Deleting episode: %s', episode
.title
)
3030 progress
.on_message(episode
.title
)
3031 episode
.delete_from_disk()
3032 episode_urls
.add(episode
.url
)
3033 channel_urls
.add(episode
.channel
.url
)
3034 episodes_status_update
.append(episode
)
3036 # Tell the shownotes window that we have removed the episode
3037 if self
.episode_shownotes_window
is not None and \
3038 self
.episode_shownotes_window
.episode
is not None and \
3039 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3040 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3042 # Notify the web service about the status update + upload
3043 self
.mygpo_client
.on_delete(episodes_status_update
)
3044 self
.mygpo_client
.flush()
3046 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3048 threading
.Thread(target
=thread_proc
).start()
3052 def on_itemRemoveOldEpisodes_activate( self
, widget
):
3053 if gpodder
.ui
.maemo
:
3055 ('maemo_remove_markup', None, None, _('Episode')),
3059 ('title_markup', None, None, _('Episode')),
3060 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3061 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3062 ('played_prop', None, None, _('Status')),
3063 ('age_prop', None, None, _('Downloaded')),
3066 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3067 selection_buttons
= {
3068 _('Select played'): lambda episode
: episode
.is_played
,
3069 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3072 instructions
= _('Select the episodes you want to delete:')
3076 for channel
in self
.channels
:
3077 for episode
in channel
.get_downloaded_episodes():
3078 # Disallow deletion of locked episodes that still exist
3079 if not episode
.is_locked
or not episode
.file_exists():
3080 episodes
.append(episode
)
3081 # Automatically select played and file-less episodes
3082 selected
.append(episode
.is_played
or \
3083 not episode
.file_exists())
3085 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3086 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3087 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3088 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3089 show_episode_shownotes
=self
.show_episode_shownotes
)
3091 def on_selected_episodes_status_changed(self
):
3092 self
.update_episode_list_icons(selected
=True)
3093 self
.update_podcast_list_model(selected
=True)
3096 def mark_selected_episodes_new(self
):
3097 for episode
in self
.get_selected_episodes():
3099 self
.on_selected_episodes_status_changed()
3101 def mark_selected_episodes_old(self
):
3102 for episode
in self
.get_selected_episodes():
3104 self
.on_selected_episodes_status_changed()
3106 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3107 for episode
in self
.get_selected_episodes():
3109 episode
.mark(is_played
=not episode
.is_played
)
3111 episode
.mark(is_played
=new_value
)
3112 self
.on_selected_episodes_status_changed()
3114 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3115 for episode
in self
.get_selected_episodes():
3117 episode
.mark(is_locked
=not episode
.is_locked
)
3119 episode
.mark(is_locked
=new_value
)
3120 self
.on_selected_episodes_status_changed()
3122 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3123 if self
.active_channel
is None:
3126 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3127 self
.active_channel
.update_channel_lock()
3129 for episode
in self
.active_channel
.get_all_episodes():
3130 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3132 self
.update_podcast_list_model(selected
=True)
3133 self
.update_episode_list_icons(all
=True)
3135 def on_itemUpdateChannel_activate(self
, widget
=None):
3136 if self
.active_channel
is None:
3137 title
= _('No podcast selected')
3138 message
= _('Please select a podcast in the podcasts list to update.')
3139 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3142 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3143 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3144 self
.update_feed_cache()
3146 self
.update_feed_cache(channels
=[self
.active_channel
])
3148 def on_itemUpdate_activate(self
, widget
=None):
3149 # Check if we have outstanding subscribe/unsubscribe actions
3150 if self
.on_add_remove_podcasts_mygpo():
3151 log('Update cancelled (received server changes)', sender
=self
)
3155 self
.update_feed_cache()
3157 gPodderWelcome(self
.gPodder
,
3158 center_on_widget
=self
.gPodder
,
3159 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3160 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3162 def download_episode_list_paused(self
, episodes
):
3163 self
.download_episode_list(episodes
, True)
3165 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3166 enable_update
= False
3168 for episode
in episodes
:
3169 log('Downloading episode: %s', episode
.title
, sender
= self
)
3170 if not episode
.was_downloaded(and_exists
=True):
3172 for task
in self
.download_tasks_seen
:
3173 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3174 self
.download_queue_manager
.add_task(task
, force_start
)
3175 enable_update
= True
3183 task
= download
.DownloadTask(episode
, self
.config
)
3184 except Exception, e
:
3185 d
= {'episode': episode
.title
, 'message': str(e
)}
3186 message
= _('Download error while downloading %(episode)s: %(message)s')
3187 self
.show_message(message
% d
, _('Download error'), important
=True)
3188 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3192 task
.status
= task
.PAUSED
3194 self
.mygpo_client
.on_download([task
.episode
])
3195 self
.download_queue_manager
.add_task(task
, force_start
)
3197 self
.download_status_model
.register_task(task
)
3198 enable_update
= True
3201 self
.enable_download_list_update()
3203 # Flush updated episode status
3204 self
.mygpo_client
.flush()
3206 def cancel_task_list(self
, tasks
):
3211 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3212 task
.status
= task
.CANCELLED
3213 elif task
.status
== task
.PAUSED
:
3214 task
.status
= task
.CANCELLED
3215 # Call run, so the partial file gets deleted
3218 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3219 self
.play_or_download()
3221 # Update the tab title and downloads list
3222 self
.update_downloads_list()
3224 def new_episodes_show(self
, episodes
, notification
=False):
3225 if gpodder
.ui
.maemo
:
3227 ('maemo_markup', None, None, _('Episode')),
3229 show_notification
= notification
3232 ('title_markup', None, None, _('Episode')),
3233 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3234 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3236 show_notification
= False
3238 instructions
= _('Select the episodes you want to download:')
3240 if self
.new_episodes_window
is not None:
3241 self
.new_episodes_window
.main_window
.destroy()
3242 self
.new_episodes_window
= None
3244 def download_episodes_callback(episodes
):
3245 self
.new_episodes_window
= None
3246 self
.download_episode_list(episodes
)
3248 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3249 title
=_('New episodes available'), \
3250 instructions
=instructions
, \
3251 episodes
=episodes
, \
3253 selected_default
=True, \
3254 stock_ok_button
= 'gpodder-download', \
3255 callback
=download_episodes_callback
, \
3256 remove_callback
=lambda e
: e
.mark_old(), \
3257 remove_action
=_('Mark as old'), \
3258 remove_finished
=self
.episode_new_status_changed
, \
3259 _config
=self
.config
, \
3260 show_notification
=show_notification
, \
3261 show_episode_shownotes
=self
.show_episode_shownotes
)
3263 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3264 if not self
.offer_new_episodes():
3265 self
.show_message(_('Please check for new episodes later.'), \
3266 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3268 def get_new_episodes(self
, channels
=None):
3269 if channels
is None:
3270 channels
= self
.channels
3272 for channel
in channels
:
3273 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3274 episodes
.append(episode
)
3278 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3279 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3281 def commit_changes_to_database(self
):
3282 """This will be called after the sync process is finished"""
3285 def on_cleanup_ipod_activate(self
, widget
, *args
):
3286 self
.sync_ui
.on_cleanup_device()
3288 def on_manage_device_playlist(self
, widget
):
3289 self
.sync_ui
.on_manage_device_playlist()
3291 def show_hide_tray_icon(self
):
3292 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3293 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3294 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3295 self
.tray_icon
.set_visible(False)
3297 self
.tray_icon
= None
3299 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3300 self
.tray_icon
.set_visible(self
.is_iconified())
3301 elif self
.tray_icon
:
3302 self
.tray_icon
.set_visible(True)
3304 def on_itemShowAllEpisodes_activate(self
, widget
):
3305 self
.config
.podcast_list_view_all
= widget
.get_active()
3307 def on_itemShowToolbar_activate(self
, widget
):
3308 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3310 def on_itemShowDescription_activate(self
, widget
):
3311 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3313 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3314 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3315 if self
.config
.podcast_list_hide_boring
:
3316 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3318 self
.podcast_list_model
.set_view_mode(-1)
3320 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3322 if current
== self
.item_view_podcasts_all
:
3323 self
.podcast_list_model
.set_view_mode(-1)
3324 elif current
== self
.item_view_podcasts_downloaded
:
3325 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3326 elif current
== self
.item_view_podcasts_unplayed
:
3327 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3329 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3331 def on_item_view_episodes_changed(self
, radioaction
, current
):
3332 if current
== self
.item_view_episodes_all
:
3333 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3334 elif current
== self
.item_view_episodes_undeleted
:
3335 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3336 elif current
== self
.item_view_episodes_downloaded
:
3337 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3338 elif current
== self
.item_view_episodes_unplayed
:
3339 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3341 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3343 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3344 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3346 def update_item_device( self
):
3347 if not gpodder
.ui
.fremantle
:
3348 if self
.config
.device_type
!= 'none':
3349 self
.itemDevice
.set_visible(True)
3350 self
.itemDevice
.label
= self
.get_device_name()
3352 self
.itemDevice
.set_visible(False)
3354 def properties_closed( self
):
3355 self
.preferences_dialog
= None
3356 self
.show_hide_tray_icon()
3357 self
.update_item_device()
3358 if gpodder
.ui
.maemo
:
3359 selection
= self
.treeAvailable
.get_selection()
3360 if self
.config
.maemo_enable_gestures
or \
3361 self
.config
.enable_fingerscroll
:
3362 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3364 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3366 def on_itemPreferences_activate(self
, widget
, *args
):
3367 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3368 _config
=self
.config
, \
3369 callback_finished
=self
.properties_closed
, \
3370 user_apps_reader
=self
.user_apps_reader
, \
3371 parent_window
=self
.main_window
, \
3372 mygpo_client
=self
.mygpo_client
, \
3373 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3375 # Initial message to relayout window (in case it's opened in portrait mode
3376 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3378 def on_itemDependencies_activate(self
, widget
):
3379 gPodderDependencyManager(self
.gPodder
)
3381 def on_goto_mygpo(self
, widget
):
3382 self
.mygpo_client
.open_website()
3384 def on_download_subscriptions_from_mygpo(self
, action
=None):
3385 title
= _('Login to gpodder.net')
3386 message
= _('Please login to download your subscriptions.')
3387 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3388 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3392 self
.config
.mygpo_username
= username
3393 self
.config
.mygpo_password
= password
3395 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3396 custom_title
=_('Subscriptions on gpodder.net'), \
3397 add_urls_callback
=self
.add_podcast_list
, \
3398 hide_url_entry
=True)
3400 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3401 # we do not have to hardcode the URL here
3402 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3403 url
= util
.url_add_authentication(OPML_URL
, \
3404 self
.config
.mygpo_username
, \
3405 self
.config
.mygpo_password
)
3406 dir.download_opml_file(url
)
3408 def on_mygpo_settings_activate(self
, action
=None):
3409 # This dialog is only used for Maemo 4
3410 if not gpodder
.ui
.diablo
:
3413 settings
= MygPodderSettings(self
.main_window
, \
3414 config
=self
.config
, \
3415 mygpo_client
=self
.mygpo_client
, \
3416 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3418 def on_itemAddChannel_activate(self
, widget
=None):
3419 gPodderAddPodcast(self
.gPodder
, \
3420 add_urls_callback
=self
.add_podcast_list
)
3422 def on_itemEditChannel_activate(self
, widget
, *args
):
3423 if self
.active_channel
is None:
3424 title
= _('No podcast selected')
3425 message
= _('Please select a podcast in the podcasts list to edit.')
3426 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3429 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3430 gPodderChannel(self
.main_window
, \
3431 channel
=self
.active_channel
, \
3432 callback_closed
=callback_closed
, \
3433 cover_downloader
=self
.cover_downloader
)
3435 def on_itemMassUnsubscribe_activate(self
, item
=None):
3437 ('title', None, None, _('Podcast')),
3440 # We're abusing the Episode Selector for selecting Podcasts here,
3441 # but it works and looks good, so why not? -- thp
3442 gPodderEpisodeSelector(self
.main_window
, \
3443 title
=_('Remove podcasts'), \
3444 instructions
=_('Select the podcast you want to remove.'), \
3445 episodes
=self
.channels
, \
3447 size_attribute
=None, \
3448 stock_ok_button
=_('Remove'), \
3449 callback
=self
.remove_podcast_list
, \
3450 _config
=self
.config
)
3452 def remove_podcast_list(self
, channels
, confirm
=True):
3454 log('No podcasts selected for deletion', sender
=self
)
3457 if len(channels
) == 1:
3458 title
= _('Removing podcast')
3459 info
= _('Please wait while the podcast is removed')
3460 message
= _('Do you really want to remove this podcast and its episodes?')
3462 title
= _('Removing podcasts')
3463 info
= _('Please wait while the podcasts are removed')
3464 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3466 if confirm
and not self
.show_confirmation(message
, title
):
3469 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3471 def finish_deletion(select_url
):
3472 # Upload subscription list changes to the web service
3473 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3475 # Re-load the channels and select the desired new channel
3476 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3477 progress
.on_finished()
3478 self
.update_podcasts_tab()
3483 for idx
, channel
in enumerate(channels
):
3484 # Update the UI for correct status messages
3485 progress
.on_progress(float(idx
)/float(len(channels
)))
3486 progress
.on_message(channel
.title
)
3488 # Delete downloaded episodes
3489 channel
.remove_downloaded()
3491 # cancel any active downloads from this channel
3492 for episode
in channel
.get_all_episodes():
3493 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3496 if len(channels
) == 1:
3497 # get the URL of the podcast we want to select next
3498 if channel
in self
.channels
:
3499 position
= self
.channels
.index(channel
)
3503 if position
== len(self
.channels
)-1:
3504 # this is the last podcast, so select the URL
3505 # of the item before this one (i.e. the "new last")
3506 select_url
= self
.channels
[position
-1].url
3508 # there is a podcast after the deleted one, so
3509 # we simply select the one that comes after it
3510 select_url
= self
.channels
[position
+1].url
3512 # Remove the channel and clean the database entries
3514 self
.channels
.remove(channel
)
3516 # Clean up downloads and download directories
3517 self
.clean_up_downloads()
3519 self
.channel_list_changed
= True
3520 self
.save_channels_opml()
3522 # The remaining stuff is to be done in the GTK main thread
3523 util
.idle_add(finish_deletion
, select_url
)
3525 threading
.Thread(target
=thread_proc
).start()
3527 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3528 if self
.active_channel
is None:
3529 title
= _('No podcast selected')
3530 message
= _('Please select a podcast in the podcasts list to remove.')
3531 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3534 self
.remove_podcast_list([self
.active_channel
])
3536 def get_opml_filter(self
):
3537 filter = gtk
.FileFilter()
3538 filter.add_pattern('*.opml')
3539 filter.add_pattern('*.xml')
3540 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3543 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3544 if filename
is None:
3545 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3546 # FIXME: Hildonization on Fremantle
3547 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3548 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3549 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3550 elif gpodder
.ui
.diablo
:
3551 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3552 dlg
.set_filter(self
.get_opml_filter())
3553 response
= dlg
.run()
3555 if response
== gtk
.RESPONSE_OK
:
3556 filename
= dlg
.get_filename()
3559 if filename
is not None:
3560 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3561 custom_title
=_('Import podcasts from OPML file'), \
3562 add_urls_callback
=self
.add_podcast_list
, \
3563 hide_url_entry
=True)
3564 dir.download_opml_file(filename
)
3566 def on_itemExportChannels_activate(self
, widget
, *args
):
3567 if not self
.channels
:
3568 title
= _('Nothing to export')
3569 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3570 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3573 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3574 # FIXME: Hildonization on Fremantle
3575 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3576 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3577 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3578 elif gpodder
.ui
.diablo
:
3579 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3580 dlg
.set_filter(self
.get_opml_filter())
3581 response
= dlg
.run()
3582 if response
== gtk
.RESPONSE_OK
:
3583 filename
= dlg
.get_filename()
3585 exporter
= opml
.Exporter( filename
)
3586 if exporter
.write(self
.channels
):
3587 count
= len(self
.channels
)
3588 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3589 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3591 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3595 def on_itemImportChannels_activate(self
, widget
, *args
):
3596 if gpodder
.ui
.fremantle
:
3597 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3598 self
.config
.toplist_url
, \
3599 self
.config
.opml_url
, \
3600 self
.add_podcast_list
, \
3601 self
.on_itemAddChannel_activate
, \
3602 self
.on_download_subscriptions_from_mygpo
, \
3603 self
.show_text_edit_dialog
)
3605 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3606 add_urls_callback
=self
.add_podcast_list
)
3607 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3609 def on_homepage_activate(self
, widget
, *args
):
3610 util
.open_website(gpodder
.__url
__)
3612 def on_wiki_activate(self
, widget
, *args
):
3613 util
.open_website('http://gpodder.org/wiki/User_Manual')
3615 def on_bug_tracker_activate(self
, widget
, *args
):
3616 if gpodder
.ui
.maemo
:
3617 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3619 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3621 def on_item_support_activate(self
, widget
):
3622 util
.open_website('http://gpodder.org/donate')
3624 def on_itemAbout_activate(self
, widget
, *args
):
3625 if gpodder
.ui
.fremantle
:
3626 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3627 HeAboutDialog
.present(self
.main_window
,
3630 gpodder
.__version
__,
3631 _('A podcast client with focus on usability'),
3632 gpodder
.__copyright
__,
3634 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3635 'http://gpodder.org/donate')
3638 dlg
= gtk
.AboutDialog()
3639 dlg
.set_transient_for(self
.main_window
)
3640 dlg
.set_name('gPodder')
3641 dlg
.set_version(gpodder
.__version
__)
3642 dlg
.set_copyright(gpodder
.__copyright
__)
3643 dlg
.set_comments(_('A podcast client with focus on usability'))
3644 dlg
.set_website(gpodder
.__url
__)
3645 dlg
.set_translator_credits( _('translator-credits'))
3646 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3648 if gpodder
.ui
.desktop
:
3649 # For the "GUI" version, we add some more
3650 # items to the about dialog (credits and logo)
3653 'Thomas Perl <thpinfo.com>',
3656 if os
.path
.exists(gpodder
.credits_file
):
3657 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3658 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3659 app_authors
+= credits
3661 dlg
.set_authors(app_authors
)
3663 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3665 dlg
.set_logo_icon_name('gpodder')
3669 def on_wNotebook_switch_page(self
, widget
, *args
):
3671 if gpodder
.ui
.maemo
:
3672 self
.tool_downloads
.set_active(page_num
== 1)
3673 page
= self
.wNotebook
.get_nth_page(page_num
)
3674 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3675 if page_num
== 0 and self
.active_channel
is not None:
3676 self
.set_title(self
.active_channel
.title
)
3678 self
.set_title(tab_label
)
3680 self
.play_or_download()
3681 self
.menuChannels
.set_sensitive(True)
3682 self
.menuSubscriptions
.set_sensitive(True)
3683 # The message area in the downloads tab should be hidden
3684 # when the user switches away from the downloads tab
3685 if self
.message_area
is not None:
3686 self
.message_area
.hide()
3687 self
.message_area
= None
3689 self
.menuChannels
.set_sensitive(False)
3690 self
.menuSubscriptions
.set_sensitive(False)
3691 if gpodder
.ui
.desktop
:
3692 self
.toolDownload
.set_sensitive(False)
3693 self
.toolPlay
.set_sensitive(False)
3694 self
.toolTransfer
.set_sensitive(False)
3695 self
.toolCancel
.set_sensitive(False)
3697 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3698 # double-click action of the podcast list or enter
3699 self
.treeChannels
.set_cursor(path
)
3701 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3702 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3704 if model
is not None and iter is not None:
3705 old_active_channel
= self
.active_channel
3706 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3708 if self
.active_channel
== old_active_channel
:
3711 if gpodder
.ui
.maemo
:
3712 self
.set_title(self
.active_channel
.title
)
3714 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3715 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3716 self
.itemEditChannel
.set_visible(False)
3717 self
.itemRemoveChannel
.set_visible(False)
3719 self
.itemEditChannel
.set_visible(True)
3720 self
.itemRemoveChannel
.set_visible(True)
3722 self
.active_channel
= None
3723 self
.itemEditChannel
.set_visible(False)
3724 self
.itemRemoveChannel
.set_visible(False)
3726 self
.update_episode_list_model()
3728 def on_btnEditChannel_clicked(self
, widget
, *args
):
3729 self
.on_itemEditChannel_activate( widget
, args
)
3731 def get_podcast_urls_from_selected_episodes(self
):
3732 """Get a set of podcast URLs based on the selected episodes"""
3733 return set(episode
.channel
.url
for episode
in \
3734 self
.get_selected_episodes())
3736 def get_selected_episodes(self
):
3737 """Get a list of selected episodes from treeAvailable"""
3738 selection
= self
.treeAvailable
.get_selection()
3739 model
, paths
= selection
.get_selected_rows()
3741 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3744 def on_transfer_selected_episodes(self
, widget
):
3745 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3747 def on_playback_selected_episodes(self
, widget
):
3748 self
.playback_episodes(self
.get_selected_episodes())
3750 def on_shownotes_selected_episodes(self
, widget
):
3751 episodes
= self
.get_selected_episodes()
3753 episode
= episodes
.pop(0)
3754 self
.show_episode_shownotes(episode
)
3756 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3758 def on_download_selected_episodes(self
, widget
):
3759 episodes
= self
.get_selected_episodes()
3760 self
.download_episode_list(episodes
)
3761 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3762 self
.play_or_download()
3764 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3765 """Double-click/enter action handler for treeAvailable"""
3766 # We should only have one one selected as it was double clicked!
3767 e
= self
.get_selected_episodes()[0]
3769 if (self
.config
.double_click_episode_action
== 'download'):
3770 # If the episode has already been downloaded and exists then play it
3771 if e
.was_downloaded(and_exists
=True):
3772 self
.playback_episodes(self
.get_selected_episodes())
3773 # else download it if it is not already downloading
3774 elif not self
.episode_is_downloading(e
):
3775 self
.download_episode_list([e
])
3776 self
.update_episode_list_icons([e
.url
])
3777 self
.play_or_download()
3778 elif (self
.config
.double_click_episode_action
== 'stream'):
3779 # If we happen to have downloaded this episode simple play it
3780 if e
.was_downloaded(and_exists
=True):
3781 self
.playback_episodes(self
.get_selected_episodes())
3782 # else if streaming is possible stream it
3783 elif self
.streaming_possible():
3784 self
.playback_episodes(self
.get_selected_episodes())
3786 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3787 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3789 # default action is to display show notes
3790 self
.on_shownotes_selected_episodes(widget
)
3792 def show_episode_shownotes(self
, episode
):
3793 if self
.episode_shownotes_window
is None:
3794 log('First-time use of episode window --- creating', sender
=self
)
3795 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3796 _download_episode_list
=self
.download_episode_list
, \
3797 _playback_episodes
=self
.playback_episodes
, \
3798 _delete_episode_list
=self
.delete_episode_list
, \
3799 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3800 _cancel_task_list
=self
.cancel_task_list
, \
3801 _episode_is_downloading
=self
.episode_is_downloading
, \
3802 _streaming_possible
=self
.streaming_possible())
3803 self
.episode_shownotes_window
.show(episode
)
3804 if self
.episode_is_downloading(episode
):
3805 self
.update_downloads_list()
3807 def restart_auto_update_timer(self
):
3808 if self
._auto
_update
_timer
_source
_id
is not None:
3809 log('Removing existing auto update timer.', sender
=self
)
3810 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3811 self
._auto
_update
_timer
_source
_id
= None
3813 if self
.config
.auto_update_feeds
and \
3814 self
.config
.auto_update_frequency
:
3815 interval
= 60*1000*self
.config
.auto_update_frequency
3816 log('Setting up auto update timer with interval %d.', \
3817 self
.config
.auto_update_frequency
, sender
=self
)
3818 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3819 interval
, self
._on
_auto
_update
_timer
)
3821 def _on_auto_update_timer(self
):
3822 log('Auto update timer fired.', sender
=self
)
3823 self
.update_feed_cache(force_update
=True)
3825 # Ask web service for sub changes (if enabled)
3826 self
.mygpo_client
.flush()
3830 def on_treeDownloads_row_activated(self
, widget
, *args
):
3831 # Use the standard way of working on the treeview
3832 selection
= self
.treeDownloads
.get_selection()
3833 (model
, paths
) = selection
.get_selected_rows()
3834 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3836 for tree_row_reference
, task
in selected_tasks
:
3837 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3838 task
.status
= task
.PAUSED
3839 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3840 self
.download_queue_manager
.add_task(task
)
3841 self
.enable_download_list_update()
3842 elif task
.status
== task
.DONE
:
3843 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3845 self
.play_or_download()
3847 # Update the tab title and downloads list
3848 self
.update_downloads_list()
3850 def on_item_cancel_download_activate(self
, widget
):
3851 if self
.wNotebook
.get_current_page() == 0:
3852 selection
= self
.treeAvailable
.get_selection()
3853 (model
, paths
) = selection
.get_selected_rows()
3854 urls
= [model
.get_value(model
.get_iter(path
), \
3855 self
.episode_list_model
.C_URL
) for path
in paths
]
3856 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3857 if task
.url
in urls
]
3859 selection
= self
.treeDownloads
.get_selection()
3860 (model
, paths
) = selection
.get_selected_rows()
3861 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3862 self
.download_status_model
.C_TASK
) for path
in paths
]
3863 self
.cancel_task_list(selected_tasks
)
3865 def on_btnCancelAll_clicked(self
, widget
, *args
):
3866 self
.cancel_task_list(self
.download_tasks_seen
)
3868 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3869 episodes
= self
.get_selected_episodes()
3870 if len(episodes
) == 1:
3871 self
.delete_episode_list(episodes
, skip_locked
=False)
3873 self
.delete_episode_list(episodes
)
3875 def on_key_press(self
, widget
, event
):
3876 # Allow tab switching with Ctrl + PgUp/PgDown
3877 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3878 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3879 self
.wNotebook
.prev_page()
3881 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3882 self
.wNotebook
.next_page()
3885 # After this code we only handle Maemo hardware keys,
3886 # so if we are not a Maemo app, we don't do anything
3887 if not gpodder
.ui
.maemo
:
3891 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3893 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3896 if diff
!= 0 and not self
.currently_updating
:
3897 selection
= self
.treeChannels
.get_selection()
3898 (model
, iter) = selection
.get_selected()
3899 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
3900 selection
.select_path(new_path
)
3901 self
.treeChannels
.set_cursor(new_path
)
3906 def on_iconify(self
):
3908 self
.gPodder
.set_skip_taskbar_hint(True)
3909 if self
.config
.minimize_to_tray
:
3910 self
.tray_icon
.set_visible(True)
3912 self
.gPodder
.set_skip_taskbar_hint(False)
3914 def on_uniconify(self
):
3916 self
.gPodder
.set_skip_taskbar_hint(False)
3917 if self
.config
.minimize_to_tray
:
3918 self
.tray_icon
.set_visible(False)
3920 self
.gPodder
.set_skip_taskbar_hint(False)
3922 def uniconify_main_window(self
):
3923 if self
.is_iconified():
3924 self
.gPodder
.present()
3926 def iconify_main_window(self
):
3927 if not self
.is_iconified():
3928 self
.gPodder
.iconify()
3930 def update_podcasts_tab(self
):
3931 if len(self
.channels
):
3932 if gpodder
.ui
.fremantle
:
3933 self
.button_refresh
.set_title(_('Check for new episodes'))
3934 self
.button_refresh
.show()
3936 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
3938 if gpodder
.ui
.fremantle
:
3939 self
.button_refresh
.hide()
3941 self
.label2
.set_text(_('Podcasts'))
3943 @dbus.service
.method(gpodder
.dbus_interface
)
3944 def show_gui_window(self
):
3945 parent
= self
.get_dialog_parent()
3948 @dbus.service
.method(gpodder
.dbus_interface
)
3949 def subscribe_to_url(self
, url
):
3950 gPodderAddPodcast(self
.gPodder
,
3951 add_urls_callback
=self
.add_podcast_list
,
3954 @dbus.service
.method(gpodder
.dbus_interface
)
3955 def mark_episode_played(self
, filename
):
3956 if filename
is None:
3959 for channel
in self
.channels
:
3960 for episode
in channel
.get_all_episodes():
3961 fn
= episode
.local_filename(create
=False, check_only
=True)
3963 episode
.mark(is_played
=True)
3965 self
.update_episode_list_icons([episode
.url
])
3966 self
.update_podcast_list_model([episode
.channel
.url
])
3972 def main(options
=None):
3973 gobject
.threads_init()
3974 gobject
.set_application_name('gPodder')
3976 if gpodder
.ui
.maemo
:
3977 # Try to enable the custom icon theme for gPodder on Maemo
3978 settings
= gtk
.settings_get_default()
3979 settings
.set_string_property('gtk-icon-theme-name', \
3980 'gpodder', __file__
)
3981 # Extend the search path for the optified icon theme (Maemo 5)
3982 icon_theme
= gtk
.icon_theme_get_default()
3983 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
3985 gtk
.window_set_default_icon_name('gpodder')
3986 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
3989 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
3990 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
3992 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
3993 except dbus
.exceptions
.DBusException
, dbe
:
3994 log('Warning: Cannot get "on the bus".', traceback
=True)
3995 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
3996 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
3997 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
3998 dlg
.set_title('gPodder')
4003 util
.make_directory(gpodder
.home
)
4004 gpodder
.load_plugins()
4006 config
= UIConfig(gpodder
.config_file
)
4008 # Load hook modules and install the hook manager globally
4009 # if modules have been found an instantiated by the manager
4010 user_hooks
= hooks
.HookManager()
4011 if user_hooks
.has_modules():
4012 gpodder
.user_hooks
= user_hooks
4014 if gpodder
.ui
.diablo
:
4015 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4016 # folder exists there (allow moving "gpodder" between SD cards or USB)
4017 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4018 if not os
.path
.exists(config
.download_dir
):
4019 log('Downloads might have been moved. Trying to locate them...')
4020 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4021 dir = os
.path
.join(basedir
, 'gpodder')
4022 if os
.path
.exists(dir):
4023 log('Downloads found in: %s', dir)
4024 config
.download_dir
= dir
4027 log('Downloads NOT FOUND in %s', dir)
4029 if config
.enable_fingerscroll
:
4030 BuilderWidget
.use_fingerscroll
= True
4031 elif gpodder
.ui
.fremantle
:
4032 config
.on_quit_ask
= False
4034 config
.mygpo_device_type
= util
.detect_device_type()
4036 gp
= gPodder(bus_name
, config
)
4039 if options
.subscribe
:
4040 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4043 # handle "subscribe to podcast" events from firefox
4044 if platform
.system() == 'Darwin':
4045 from gpodder
import gpodderosx
4046 gpodderosx
.register_handlers(gp
)
4047 # end mac OS X stuff