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/>.
37 from xml
.sax
import saxutils
47 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
50 def __init__(self
, *args
, **kwargs
):
52 def add_signal_receiver(self
, *args
, **kwargs
):
56 def __init__(self
, *args
, **kwargs
):
60 def method(*args
, **kwargs
):
63 def __init__(self
, *args
, **kwargs
):
66 def __init__(self
, *args
, **kwargs
):
70 from gpodder
import feedcore
71 from gpodder
import util
72 from gpodder
import opml
73 from gpodder
import download
74 from gpodder
import my
75 from gpodder
import youtube
76 from gpodder
import player
77 from gpodder
.liblogger
import log
82 from gpodder
.model
import PodcastChannel
83 from gpodder
.model
import PodcastEpisode
84 from gpodder
.dbsqlite
import Database
86 from gpodder
.gtkui
.model
import PodcastListModel
87 from gpodder
.gtkui
.model
import EpisodeListModel
88 from gpodder
.gtkui
.config
import UIConfig
89 from gpodder
.gtkui
.services
import CoverDownloader
90 from gpodder
.gtkui
.widgets
import SimpleMessageArea
91 from gpodder
.gtkui
.desktopfile
import UserAppsReader
93 from gpodder
.gtkui
.draw
import draw_text_box_centered
95 from gpodder
.gtkui
.interface
.common
import BuilderWidget
96 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
97 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
99 if gpodder
.ui
.desktop
:
100 from gpodder
.gtkui
.download
import DownloadStatusModel
102 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
104 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
105 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
106 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
107 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
108 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
109 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
110 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
112 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
114 except Exception, exc
:
115 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
116 log('Warning: This probably means your PyGTK installation is too old!')
117 have_trayicon
= False
118 elif gpodder
.ui
.diablo
:
119 from gpodder
.gtkui
.download
import DownloadStatusModel
121 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
122 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
123 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
124 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
125 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
126 from gpodder
.gtkui
.maemo
.mygpodder
import MygPodderSettings
127 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
128 have_trayicon
= False
129 elif gpodder
.ui
.fremantle
:
130 from gpodder
.gtkui
.frmntl
.model
import DownloadStatusModel
131 from gpodder
.gtkui
.frmntl
.model
import EpisodeListModel
132 from gpodder
.gtkui
.frmntl
.model
import PodcastListModel
134 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
135 from gpodder
.gtkui
.frmntl
.preferences
import gPodderPreferences
136 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
137 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
138 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
139 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
140 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
141 from gpodder
.gtkui
.frmntl
.progress
import ProgressIndicator
142 from gpodder
.gtkui
.frmntl
.widgets
import FancyProgressBar
143 have_trayicon
= False
145 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
146 from gpodder
.gtkui
.frmntl
.mafw
import MafwPlaybackMonitor
147 from gpodder
.gtkui
.frmntl
.hints
import HINT_STRINGS
149 from gpodder
.gtkui
.interface
.common
import Orientation
151 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
156 from gpodder
.dbusproxy
import DBusPodcastsProxy
157 from gpodder
import hooks
159 class gPodder(BuilderWidget
, dbus
.service
.Object
):
160 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear', 'label2', 'labelDownloads', 'btnUpdateFeeds']
162 ICON_GENERAL_ADD
= 'general_add'
163 ICON_GENERAL_REFRESH
= 'general_refresh'
165 # Delay until live search is started after typing stop
166 LIVE_SEARCH_DELAY
= 200
168 def __init__(self
, bus_name
, config
):
169 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
170 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
171 self
.on_itemUpdate_activate
, \
172 self
.playback_episodes
, \
173 self
.download_episode_list
, \
174 self
.episode_object_by_uri
, \
176 self
.db
= Database(gpodder
.database_file
)
178 BuilderWidget
.__init
__(self
, None)
181 if gpodder
.ui
.diablo
:
183 self
.app
= hildon
.Program()
184 self
.app
.add_window(self
.main_window
)
185 self
.main_window
.add_toolbar(self
.toolbar
)
187 for child
in self
.main_menu
.get_children():
189 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
190 self
._last
_orientation
= Orientation
.LANDSCAPE
191 elif gpodder
.ui
.fremantle
:
193 self
.app
= hildon
.Program()
194 self
.app
.add_window(self
.main_window
)
196 appmenu
= hildon
.AppMenu()
198 for filter in (self
.item_view_podcasts_all
, \
199 self
.item_view_podcasts_downloaded
, \
200 self
.item_view_podcasts_unplayed
):
201 button
= gtk
.ToggleButton()
202 filter.connect_proxy(button
)
203 appmenu
.add_filter(button
)
205 for action
in (self
.itemPreferences
, \
206 self
.item_downloads
, \
207 self
.itemRemoveOldEpisodes
, \
208 self
.item_unsubscribe
, \
210 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
211 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
212 action
.connect_proxy(button
)
213 if action
== self
.item_downloads
:
214 button
.set_title(_('Downloads'))
215 button
.set_value(_('Idle'))
216 self
.button_downloads
= button
217 appmenu
.append(button
)
219 def show_hint(button
):
220 self
.show_message(random
.choice(HINT_STRINGS
), important
=True)
222 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
223 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
224 button
.set_title(_('Hint of the day'))
225 button
.connect('clicked', show_hint
)
226 appmenu
.append(button
)
229 self
.main_window
.set_app_menu(appmenu
)
231 # Initialize portrait mode / rotation manager
232 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
234 gpodder
.__version
__, \
235 self
.config
.rotation_mode
)
237 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
238 util
.idle_add(self
.on_window_orientation_changed
, \
239 Orientation
.PORTRAIT
)
240 self
._last
_orientation
= Orientation
.PORTRAIT
242 self
._last
_orientation
= Orientation
.LANDSCAPE
244 # Flag set when a notification is being shown (Maemo bug 11235)
245 self
._fremantle
_notification
_visible
= False
247 self
._last
_orientation
= Orientation
.LANDSCAPE
248 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
250 self
.bluetooth_available
= util
.bluetooth_available()
252 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
253 if not gpodder
.ui
.fremantle
:
254 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
255 self
.main_window
.show()
257 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
259 if gpodder
.ui
.fremantle
:
260 # Create a D-Bus monitoring object that takes care of
261 # tracking MAFW (Nokia Media Player) playback events
262 # and sends episode playback status events via D-Bus
263 self
.mafw_monitor
= MafwPlaybackMonitor(gpodder
.dbus_session_bus
)
265 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
267 self
.preferences_dialog
= None
268 self
.config
.add_observer(self
.on_config_changed
)
270 self
.tray_icon
= None
271 self
.episode_shownotes_window
= None
272 self
.new_episodes_window
= None
274 if gpodder
.ui
.desktop
:
275 # Mac OS X-specific UI tweaks: Native main menu integration
276 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
277 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
279 import igemacintegration
as igemi
281 # Move the menu bar from the window to the Mac menu bar
283 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
285 # Reparent some items to the "Application" menu
286 for widget
in ('/mainMenu/menuHelp/itemAbout', \
287 '/mainMenu/menuPodcasts/itemPreferences'):
288 item
= self
.uimanager1
.get_widget(widget
)
289 group
= igemi
.ige_mac_menu_add_app_menu_group()
290 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
292 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
293 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
294 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
296 print >>sys
.stderr
, """
297 Warning: ige-mac-integration not found - no native menus.
300 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
301 self
.main_window
, self
.show_confirmation
, \
302 self
.update_episode_list_icons
, \
303 self
.update_podcast_list_model
, self
.toolPreferences
, \
304 gPodderEpisodeSelector
, \
305 self
.commit_changes_to_database
)
309 self
.download_status_model
= DownloadStatusModel()
310 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
312 if gpodder
.ui
.desktop
:
313 self
.show_hide_tray_icon()
314 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
315 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
316 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
318 if not gpodder
.ui
.fremantle
:
319 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
320 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
321 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
322 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
324 # When the amount of maximum downloads changes, notify the queue manager
325 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
326 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
328 self
.default_title
= 'gPodder'
329 if gpodder
.__version
__.rfind('git') != -1:
330 self
.set_title('gPodder %s' % gpodder
.__version
__)
332 title
= self
.gPodder
.get_title()
333 if title
is not None:
334 self
.set_title(title
)
336 self
.set_title(_('gPodder'))
338 self
.cover_downloader
= CoverDownloader()
340 # Generate list models for podcasts and their episodes
341 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
343 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
344 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
346 if gpodder
.ui
.fremantle
:
347 # Work around Maemo bug #4718
348 self
.button_refresh
.set_name('HildonButton-finger')
349 self
.button_subscribe
.set_name('HildonButton-finger')
351 self
.button_refresh
.set_sensitive(False)
352 self
.button_subscribe
.set_sensitive(False)
354 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
355 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
356 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
357 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
359 # Make the button scroll together with the TreeView contents
360 action_area_box
= self
.treeChannels
.get_action_area_box()
361 for child
in self
.buttonbox
:
362 child
.reparent(action_area_box
)
363 self
.vbox
.remove(self
.buttonbox
)
364 self
.treeChannels
.set_action_area_visible(True)
366 # Set up a very nice progress bar setup
367 self
.fancy_progress_bar
= FancyProgressBar(self
.main_window
, \
368 self
.on_btnCancelFeedUpdate_clicked
)
369 self
.pbFeedUpdate
= self
.fancy_progress_bar
.progress_bar
370 self
.pbFeedUpdate
.set_ellipsize(pango
.ELLIPSIZE_MIDDLE
)
371 self
.vbox
.pack_start(self
.fancy_progress_bar
.event_box
, False)
373 from gpodder
.gtkui
.frmntl
import style
374 sub_font
= style
.get_font_desc('SmallSystemFont')
375 sub_color
= style
.get_color('SecondaryTextColor')
376 sub
= (sub_font
.to_string(), sub_color
.to_string())
377 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
378 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
380 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
381 while gtk
.events_pending():
382 gtk
.main_iteration(False)
385 # Try to get the real package version from dpkg
386 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
387 version
, _stderr
= p
.communicate()
391 version
= gpodder
.__version
__
392 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
393 self
.label_footer
.hide()
395 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
396 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
397 show_episode_shownotes
=self
.show_episode_shownotes
, \
398 update_podcast_list_model
=self
.update_podcast_list_model
, \
399 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
400 item_view_episodes_all
=self
.item_view_episodes_all
, \
401 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
402 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
403 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
404 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
405 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
406 hide_episode_search
=self
.hide_episode_search
, \
407 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
408 playback_episodes
=self
.playback_episodes
, \
409 delete_episode_list
=self
.delete_episode_list
, \
410 episode_list_status_changed
=self
.episode_list_status_changed
, \
411 download_episode_list
=self
.download_episode_list
, \
412 episode_is_downloading
=self
.episode_is_downloading
, \
413 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
414 add_download_task_monitor
=self
.add_download_task_monitor
, \
415 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
416 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
417 on_itemUpdate_activate
=self
.on_itemUpdate_activate
, \
418 show_delete_episodes_window
=self
.show_delete_episodes_window
, \
419 cover_downloader
=self
.cover_downloader
)
421 # Expose objects for episode list type-ahead find
422 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
423 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
424 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
426 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
427 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
428 cleanup_downloads
=self
.cleanup_downloads
, \
429 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
430 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
433 self
.treeAvailable
= self
.episodes_window
.treeview
434 self
.treeDownloads
= self
.downloads_window
.treeview
436 # Source IDs for timeouts for search-as-you-type
437 self
._podcast
_list
_search
_timeout
= None
438 self
._episode
_list
_search
_timeout
= None
440 # Init the treeviews that we use
441 self
.init_podcast_list_treeview()
442 self
.init_episode_list_treeview()
443 self
.init_download_list_treeview()
445 if self
.config
.podcast_list_hide_boring
:
446 self
.item_view_hide_boring_podcasts
.set_active(True)
448 self
.currently_updating
= False
450 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
451 self
.context_menu_mouse_button
= 1
453 self
.context_menu_mouse_button
= 3
455 if self
.config
.start_iconified
:
456 self
.iconify_main_window()
458 self
.download_tasks_seen
= set()
459 self
.download_list_update_enabled
= False
460 self
.download_task_monitors
= set()
462 # Subscribed channels
463 self
.active_channel
= None
464 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
465 self
.channel_list_changed
= True
466 self
.update_podcasts_tab()
468 # load list of user applications for audio playback
469 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
470 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
472 # Set the "Device" menu item for the first time
473 if gpodder
.ui
.desktop
:
474 self
.update_item_device()
476 # Set up the first instance of MygPoClient
477 self
.mygpo_client
= my
.MygPoClient(self
.config
)
479 # Now, update the feed cache, when everything's in place
480 if not gpodder
.ui
.fremantle
:
481 self
.btnUpdateFeeds
.show()
482 self
.updating_feed_cache
= False
483 self
.feed_cache_update_cancelled
= False
484 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
486 self
.message_area
= None
488 def find_partial_downloads():
489 # Look for partial file downloads
490 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
491 count
= len(partial_files
)
492 resumable_episodes
= []
494 if not gpodder
.ui
.fremantle
:
495 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
496 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
497 _('Some episodes have not finished downloading in a previous session.'), \
498 False, self
.get_dialog_parent())
499 indicator
.on_message(N_('%(count)d partial file', '%(count)d partial files', count
) % {'count':count
})
501 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
504 for c
in self
.channels
:
505 for e
in c
.get_all_episodes():
506 filename
= e
.local_filename(create
=False, check_only
=True)
507 if filename
in candidates
:
508 log('Found episode: %s', e
.title
, sender
=self
)
510 indicator
.on_message(e
.title
)
511 indicator
.on_progress(float(found
)/count
)
512 candidates
.remove(filename
)
513 partial_files
.remove(filename
+'.partial')
514 resumable_episodes
.append(e
)
522 for f
in partial_files
:
523 log('Partial file without episode: %s', f
, sender
=self
)
526 util
.idle_add(indicator
.on_finished
)
528 if len(resumable_episodes
):
529 def offer_resuming():
530 self
.download_episode_list_paused(resumable_episodes
)
531 if not gpodder
.ui
.fremantle
:
532 resume_all
= gtk
.Button(_('Resume all'))
533 #resume_all.set_border_width(0)
534 def on_resume_all(button
):
535 selection
= self
.treeDownloads
.get_selection()
536 selection
.select_all()
537 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
538 selection
.unselect_all()
539 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
540 self
.message_area
.hide()
541 resume_all
.connect('clicked', on_resume_all
)
543 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
544 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
545 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
546 self
.message_area
.show_all()
547 self
.clean_up_downloads(delete_partial
=False)
548 util
.idle_add(offer_resuming
)
549 elif not gpodder
.ui
.fremantle
:
550 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
552 util
.idle_add(self
.clean_up_downloads
, True)
553 threading
.Thread(target
=find_partial_downloads
).start()
555 # Start the auto-update procedure
556 self
._auto
_update
_timer
_source
_id
= None
557 if self
.config
.auto_update_feeds
:
558 self
.restart_auto_update_timer()
560 # Delete old episodes if the user wishes to
561 if self
.config
.auto_remove_played_episodes
and \
562 self
.config
.episode_old_age
> 0:
563 old_episodes
= list(self
.get_expired_episodes())
564 if len(old_episodes
) > 0:
565 self
.delete_episode_list(old_episodes
, confirm
=False)
566 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
568 if gpodder
.ui
.fremantle
:
569 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
570 self
.button_refresh
.set_sensitive(True)
571 self
.button_subscribe
.set_sensitive(True)
572 self
.main_window
.set_title(_('gPodder'))
573 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
575 # Do the initial sync with the web service
576 util
.idle_add(self
.mygpo_client
.flush
, True)
578 # First-time users should be asked if they want to see the OPML
579 if not self
.channels
and not gpodder
.ui
.fremantle
:
580 util
.idle_add(self
.on_itemUpdate_activate
)
582 def episode_object_by_uri(self
, uri
):
583 """Get an episode object given a local or remote URI
585 This can be used to quickly access an episode object
586 when all we have is its download filename or episode
587 URL (e.g. from external D-Bus calls / signals, etc..)
589 if uri
.startswith('/'):
590 uri
= 'file://' + uri
592 prefix
= 'file://' + self
.config
.download_dir
594 if uri
.startswith(prefix
):
595 # File is on the local filesystem in the download folder
596 filename
= urllib
.unquote(uri
[len(prefix
):])
597 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
599 if len(file_parts
) == 2:
600 dir_name
, filename
= file_parts
601 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
602 if len(channels
) == 1:
603 channel
= channels
[0]
604 return channel
.get_episode_by_filename(filename
)
606 # Possibly remote file - search the database for a podcast
607 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
609 if channel_id
is not None:
610 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
611 if len(channels
) == 1:
612 channel
= channels
[0]
613 return channel
.get_episode_by_url(uri
)
617 def on_played(self
, start
, end
, total
, file_uri
):
618 """Handle the "played" signal from a media player"""
619 if start
== 0 and end
== 0 and total
== 0:
620 # Ignore bogus play event
622 elif end
< start
+ 5:
623 # Ignore "less than five seconds" segments,
624 # as they can happen with seeking, etc...
627 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
628 episode
= self
.episode_object_by_uri(file_uri
)
630 if episode
is not None:
631 file_type
= episode
.file_type()
632 # Automatically enable D-Bus played status mode
633 if file_type
== 'audio':
634 self
.config
.audio_played_dbus
= True
635 elif file_type
== 'video':
636 self
.config
.video_played_dbus
= True
640 episode
.total_time
= total
642 # Assume the episode's total time for the action
643 total
= episode
.total_time
644 if episode
.current_position_updated
is None or \
645 now
> episode
.current_position_updated
:
646 episode
.current_position
= end
647 episode
.current_position_updated
= now
648 episode
.mark(is_played
=True)
651 self
.update_episode_list_icons([episode
.url
])
652 self
.update_podcast_list_model([episode
.channel
.url
])
654 # Submit this action to the webservice
655 self
.mygpo_client
.on_playback_full(episode
, \
658 def on_add_remove_podcasts_mygpo(self
):
659 actions
= self
.mygpo_client
.get_received_actions()
663 existing_urls
= [c
.url
for c
in self
.channels
]
665 # Columns for the episode selector window - just one...
667 ('description', None, None, _('Action')),
670 # A list of actions that have to be chosen from
673 # Actions that are ignored (already carried out)
676 for action
in actions
:
677 if action
.is_add
and action
.url
not in existing_urls
:
678 changes
.append(my
.Change(action
))
679 elif action
.is_remove
and action
.url
in existing_urls
:
680 podcast_object
= None
681 for podcast
in self
.channels
:
682 if podcast
.url
== action
.url
:
683 podcast_object
= podcast
685 changes
.append(my
.Change(action
, podcast_object
))
687 log('Ignoring action: %s', action
, sender
=self
)
688 ignored
.append(action
)
690 # Confirm all ignored changes
691 self
.mygpo_client
.confirm_received_actions(ignored
)
693 def execute_podcast_actions(selected
):
694 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
695 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
697 # Apply the accepted changes locally
698 self
.add_podcast_list(add_list
)
699 self
.remove_podcast_list(remove_list
, confirm
=False)
701 # All selected items are now confirmed
702 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
704 # Revert the changes on the server
705 rejected
= [c
.action
for c
in changes
if c
not in selected
]
706 self
.mygpo_client
.reject_received_actions(rejected
)
709 # We're abusing the Episode Selector again ;) -- thp
710 gPodderEpisodeSelector(self
.main_window
, \
711 title
=_('Confirm changes from gpodder.net'), \
712 instructions
=_('Select the actions you want to carry out.'), \
715 size_attribute
=None, \
716 stock_ok_button
=gtk
.STOCK_APPLY
, \
717 callback
=execute_podcast_actions
, \
720 # There are some actions that need the user's attention
725 # We have no remaining actions - no selection happens
728 def rewrite_urls_mygpo(self
):
729 # Check if we have to rewrite URLs since the last add
730 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
732 for rewritten_url
in rewritten_urls
:
733 if not rewritten_url
.new_url
:
736 for channel
in self
.channels
:
737 if channel
.url
== rewritten_url
.old_url
:
738 log('Updating URL of %s to %s', channel
, \
739 rewritten_url
.new_url
, sender
=self
)
740 channel
.url
= rewritten_url
.new_url
742 self
.channel_list_changed
= True
743 util
.idle_add(self
.update_episode_list_model
)
746 def on_send_full_subscriptions(self
):
747 # Send the full subscription list to the gpodder.net client
748 # (this will overwrite the subscription list on the server)
749 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
750 _('Your subscriptions are being uploaded to the server.'), \
751 False, self
.get_dialog_parent())
754 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
755 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
760 message
= e
.__class
__.__name
__
761 self
.show_message(message
, \
762 _('Error while uploading'), \
764 util
.idle_add(show_error
, e
)
766 util
.idle_add(indicator
.on_finished
)
768 def on_podcast_selected(self
, treeview
, path
, column
):
770 model
= treeview
.get_model()
771 channel
= model
.get_value(model
.get_iter(path
), \
772 PodcastListModel
.C_CHANNEL
)
773 self
.active_channel
= channel
774 self
.update_episode_list_model()
775 self
.episodes_window
.channel
= self
.active_channel
776 self
.episodes_window
.show()
778 def on_button_subscribe_clicked(self
, button
):
779 self
.on_itemImportChannels_activate(button
)
781 def on_button_downloads_clicked(self
, widget
):
782 self
.downloads_window
.show()
784 def show_episode_in_download_manager(self
, episode
):
785 self
.downloads_window
.show()
786 model
= self
.treeDownloads
.get_model()
787 selection
= self
.treeDownloads
.get_selection()
788 selection
.unselect_all()
789 it
= model
.get_iter_first()
790 while it
is not None:
791 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
792 if task
.episode
.url
== episode
.url
:
793 selection
.select_iter(it
)
794 # FIXME: Scroll to selection in pannable area
796 it
= model
.iter_next(it
)
798 def for_each_episode_set_task_status(self
, episodes
, status
):
799 episode_urls
= set(episode
.url
for episode
in episodes
)
800 model
= self
.treeDownloads
.get_model()
801 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
802 model
.get_value(row
.iter, \
803 DownloadStatusModel
.C_TASK
)) for row
in model \
804 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
806 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
808 def on_window_orientation_changed(self
, orientation
):
809 self
._last
_orientation
= orientation
810 if self
.preferences_dialog
is not None:
811 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
813 treeview
= self
.treeChannels
814 if orientation
== Orientation
.PORTRAIT
:
815 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
816 # Work around Maemo bug #4718
817 self
.button_subscribe
.set_name('HildonButton-thumb')
818 self
.button_refresh
.set_name('HildonButton-thumb')
820 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
821 # Work around Maemo bug #4718
822 self
.button_subscribe
.set_name('HildonButton-finger')
823 self
.button_refresh
.set_name('HildonButton-finger')
825 if gpodder
.ui
.fremantle
:
826 self
.fancy_progress_bar
.relayout()
828 def on_treeview_podcasts_selection_changed(self
, selection
):
829 model
, iter = selection
.get_selected()
831 self
.active_channel
= None
832 self
.episode_list_model
.clear()
834 def on_treeview_button_pressed(self
, treeview
, event
):
835 if event
.window
!= treeview
.get_bin_window():
838 TreeViewHelper
.save_button_press_event(treeview
, event
)
840 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
841 TreeViewHelper
.ROLE_PODCASTS
:
842 return self
.currently_updating
844 return event
.button
== self
.context_menu_mouse_button
and \
847 def on_treeview_podcasts_button_released(self
, treeview
, event
):
848 if event
.window
!= treeview
.get_bin_window():
852 return self
.treeview_channels_handle_gestures(treeview
, event
)
853 return self
.treeview_channels_show_context_menu(treeview
, event
)
855 def on_treeview_episodes_button_released(self
, treeview
, event
):
856 if event
.window
!= treeview
.get_bin_window():
859 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
860 return self
.treeview_available_handle_gestures(treeview
, event
)
862 return self
.treeview_available_show_context_menu(treeview
, event
)
864 def on_treeview_downloads_button_released(self
, treeview
, event
):
865 if event
.window
!= treeview
.get_bin_window():
868 return self
.treeview_downloads_show_context_menu(treeview
, event
)
870 def on_entry_search_podcasts_changed(self
, editable
):
871 if self
.hbox_search_podcasts
.get_property('visible'):
872 def set_search_term(self
, text
):
873 self
.podcast_list_model
.set_search_term(text
)
874 self
._podcast
_list
_search
_timeout
= None
877 if self
._podcast
_list
_search
_timeout
is not None:
878 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
879 self
._podcast
_list
_search
_timeout
= gobject
.timeout_add(\
880 self
.LIVE_SEARCH_DELAY
, \
881 set_search_term
, self
, editable
.get_chars(0, -1))
883 def on_entry_search_podcasts_key_press(self
, editable
, event
):
884 if event
.keyval
== gtk
.keysyms
.Escape
:
885 self
.hide_podcast_search()
888 def hide_podcast_search(self
, *args
):
889 if self
._podcast
_list
_search
_timeout
is not None:
890 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
891 self
._podcast
_list
_search
_timeout
= None
892 self
.hbox_search_podcasts
.hide()
893 self
.entry_search_podcasts
.set_text('')
894 self
.podcast_list_model
.set_search_term(None)
895 self
.treeChannels
.grab_focus()
897 def show_podcast_search(self
, input_char
):
898 self
.hbox_search_podcasts
.show()
899 self
.entry_search_podcasts
.insert_text(input_char
, -1)
900 self
.entry_search_podcasts
.grab_focus()
901 self
.entry_search_podcasts
.set_position(-1)
903 def init_podcast_list_treeview(self
):
904 # Set up podcast channel tree view widget
905 if gpodder
.ui
.fremantle
:
906 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
907 self
.item_view_podcasts_downloaded
.set_active(True)
908 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
909 self
.item_view_podcasts_unplayed
.set_active(True)
911 self
.item_view_podcasts_all
.set_active(True)
912 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
914 iconcolumn
= gtk
.TreeViewColumn('')
915 iconcell
= gtk
.CellRendererPixbuf()
916 iconcolumn
.pack_start(iconcell
, False)
917 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
918 self
.treeChannels
.append_column(iconcolumn
)
920 namecolumn
= gtk
.TreeViewColumn('')
921 namecell
= gtk
.CellRendererText()
922 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
923 namecolumn
.pack_start(namecell
, True)
924 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
926 if gpodder
.ui
.fremantle
:
927 countcell
= gtk
.CellRendererText()
928 from gpodder
.gtkui
.frmntl
import style
929 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
930 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
931 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
932 countcell
.set_property('xalign', 1.)
933 countcell
.set_property('xpad', 5)
934 namecolumn
.pack_start(countcell
, False)
935 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
936 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
938 iconcell
= gtk
.CellRendererPixbuf()
939 iconcell
.set_property('xalign', 1.0)
940 namecolumn
.pack_start(iconcell
, False)
941 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
942 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
944 self
.treeChannels
.append_column(namecolumn
)
946 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
948 # When no podcast is selected, clear the episode list model
949 selection
= self
.treeChannels
.get_selection()
950 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
952 # Set up type-ahead find for the podcast list
953 def on_key_press(treeview
, event
):
954 if event
.keyval
== gtk
.keysyms
.Escape
:
955 self
.hide_podcast_search()
956 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
957 self
.hide_podcast_search()
958 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
959 # Don't handle type-ahead when control is pressed (so shortcuts
960 # with the Ctrl key still work, e.g. Ctrl+A, ...)
963 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
964 if unicode_char_id
== 0:
966 input_char
= unichr(unicode_char_id
)
967 self
.show_podcast_search(input_char
)
969 self
.treeChannels
.connect('key-press-event', on_key_press
)
971 # Enable separators to the podcast list to separate special podcasts
972 # from others (this is used for the "all episodes" view)
973 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
975 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
977 def on_entry_search_episodes_changed(self
, editable
):
978 if self
.hbox_search_episodes
.get_property('visible'):
979 def set_search_term(self
, text
):
980 self
.episode_list_model
.set_search_term(text
)
981 self
._episode
_list
_search
_timeout
= None
984 if self
._episode
_list
_search
_timeout
is not None:
985 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
986 self
._episode
_list
_search
_timeout
= gobject
.timeout_add(\
987 self
.LIVE_SEARCH_DELAY
, \
988 set_search_term
, self
, editable
.get_chars(0, -1))
990 def on_entry_search_episodes_key_press(self
, editable
, event
):
991 if event
.keyval
== gtk
.keysyms
.Escape
:
992 self
.hide_episode_search()
995 def hide_episode_search(self
, *args
):
996 if self
._episode
_list
_search
_timeout
is not None:
997 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
998 self
._episode
_list
_search
_timeout
= None
999 self
.hbox_search_episodes
.hide()
1000 self
.entry_search_episodes
.set_text('')
1001 self
.episode_list_model
.set_search_term(None)
1002 self
.treeAvailable
.grab_focus()
1004 def show_episode_search(self
, input_char
):
1005 self
.hbox_search_episodes
.show()
1006 self
.entry_search_episodes
.insert_text(input_char
, -1)
1007 self
.entry_search_episodes
.grab_focus()
1008 self
.entry_search_episodes
.set_position(-1)
1010 def init_episode_list_treeview(self
):
1011 # For loading the list model
1012 self
.episode_list_model
= EpisodeListModel(self
.on_episode_list_filter_changed
)
1014 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
1015 self
.item_view_episodes_undeleted
.set_active(True)
1016 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
1017 self
.item_view_episodes_downloaded
.set_active(True)
1018 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
1019 self
.item_view_episodes_unplayed
.set_active(True)
1021 self
.item_view_episodes_all
.set_active(True)
1023 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
1025 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
1027 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
1029 iconcell
= gtk
.CellRendererPixbuf()
1030 iconcell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1031 if gpodder
.ui
.maemo
:
1032 iconcell
.set_fixed_size(50, 50)
1034 iconcell
.set_fixed_size(40, -1)
1036 namecell
= gtk
.CellRendererText()
1037 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1038 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
1039 namecolumn
.pack_start(iconcell
, False)
1040 namecolumn
.add_attribute(iconcell
, 'icon-name', EpisodeListModel
.C_STATUS_ICON
)
1041 namecolumn
.pack_start(namecell
, True)
1042 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1043 if gpodder
.ui
.fremantle
:
1044 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
1046 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1047 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1048 namecolumn
.set_resizable(True)
1049 namecolumn
.set_expand(True)
1051 if gpodder
.ui
.fremantle
:
1052 from gpodder
.gtkui
.frmntl
import style
1053 timecell
= gtk
.CellRendererText()
1054 timecell
.set_property('font-desc', style
.get_font_desc('SmallSystemFont'))
1055 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1056 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1057 timecell
.set_property('xalign', 1.)
1058 timecell
.set_property('xpad', 5)
1059 timecell
.set_property('yalign', .85)
1060 namecolumn
.pack_start(timecell
, False)
1061 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1062 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME_VISIBLE
)
1064 lockcell
= gtk
.CellRendererPixbuf()
1065 lockcell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1066 if gpodder
.ui
.fremantle
:
1067 lockcell
.set_property('icon-name', 'general_locked')
1069 lockcell
.set_property('icon-name', 'emblem-readonly')
1071 namecolumn
.pack_start(lockcell
, False)
1072 namecolumn
.add_attribute(lockcell
, 'visible', EpisodeListModel
.C_LOCKED
)
1074 sizecell
= gtk
.CellRendererText()
1075 sizecell
.set_property('xalign', 1)
1076 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1077 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1079 releasecell
= gtk
.CellRendererText()
1080 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1081 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1083 namecolumn
.set_reorderable(True)
1084 self
.treeAvailable
.append_column(namecolumn
)
1086 if not gpodder
.ui
.maemo
:
1087 for itemcolumn
in (sizecolumn
, releasecolumn
):
1088 itemcolumn
.set_reorderable(True)
1089 self
.treeAvailable
.append_column(itemcolumn
)
1091 # Set up type-ahead find for the episode list
1092 def on_key_press(treeview
, event
):
1093 if event
.keyval
== gtk
.keysyms
.Escape
:
1094 self
.hide_episode_search()
1095 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1096 self
.hide_episode_search()
1097 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1098 # Don't handle type-ahead when control is pressed (so shortcuts
1099 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1102 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1103 if unicode_char_id
== 0:
1105 input_char
= unichr(unicode_char_id
)
1106 self
.show_episode_search(input_char
)
1108 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1110 if gpodder
.ui
.desktop
and not self
.config
.enable_fingerscroll
:
1111 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1112 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1113 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1114 if self
.config
.on_drag_mark_played
:
1115 for episode
in self
.get_selected_episodes():
1116 episode
.mark(is_played
=True)
1117 self
.on_selected_episodes_status_changed()
1118 uris
= ['file://'+e
.local_filename(create
=False) \
1119 for e
in self
.get_selected_episodes() \
1120 if e
.was_downloaded(and_exists
=True)]
1121 uris
.append('') # for the trailing '\r\n'
1122 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1123 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1125 selection
= self
.treeAvailable
.get_selection()
1126 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1127 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1128 elif gpodder
.ui
.fremantle
:
1129 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1131 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1132 # Update the sensitivity of the toolbar buttons on the Desktop
1133 selection
.connect('changed', lambda s
: self
.play_or_download())
1135 if gpodder
.ui
.diablo
:
1136 # Set up the tap-and-hold context menu for podcasts
1138 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1139 menu
.append(self
.itemEditChannel
.create_menu_item())
1140 menu
.append(gtk
.SeparatorMenuItem())
1141 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1142 menu
.append(gtk
.SeparatorMenuItem())
1143 item
= gtk
.ImageMenuItem(_('Close this menu'))
1144 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1145 gtk
.ICON_SIZE_MENU
))
1148 menu
= self
.set_finger_friendly(menu
)
1149 self
.treeChannels
.tap_and_hold_setup(menu
)
1152 def init_download_list_treeview(self
):
1153 # enable multiple selection support
1154 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1155 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1157 # columns and renderers for "download progress" tab
1158 # First column: [ICON] Episodename
1159 column
= gtk
.TreeViewColumn(_('Episode'))
1161 cell
= gtk
.CellRendererPixbuf()
1162 if gpodder
.ui
.maemo
:
1163 cell
.set_fixed_size(50, 50)
1164 cell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1165 column
.pack_start(cell
, expand
=False)
1166 column
.add_attribute(cell
, 'icon-name', \
1167 DownloadStatusModel
.C_ICON_NAME
)
1169 cell
= gtk
.CellRendererText()
1170 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1171 column
.pack_start(cell
, expand
=True)
1172 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1173 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1174 column
.set_expand(True)
1175 self
.treeDownloads
.append_column(column
)
1177 # Second column: Progress
1178 cell
= gtk
.CellRendererProgress()
1179 cell
.set_property('yalign', .5)
1180 cell
.set_property('ypad', 6)
1181 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1182 value
=DownloadStatusModel
.C_PROGRESS
, \
1183 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1184 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1185 column
.set_expand(False)
1186 self
.treeDownloads
.append_column(column
)
1187 if gpodder
.ui
.maemo
:
1188 column
.set_property('min-width', 200)
1189 column
.set_property('max-width', 200)
1191 column
.set_property('min-width', 150)
1192 column
.set_property('max-width', 150)
1194 self
.treeDownloads
.set_model(self
.download_status_model
)
1195 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1197 def on_treeview_expose_event(self
, treeview
, event
):
1198 if event
.window
== treeview
.get_bin_window():
1199 model
= treeview
.get_model()
1200 if (model
is not None and model
.get_iter_first() is not None):
1203 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1207 ctx
= event
.window
.cairo_create()
1208 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1209 event
.area
.width
, event
.area
.height
)
1212 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1215 if role
== TreeViewHelper
.ROLE_EPISODES
:
1216 if self
.currently_updating
:
1217 text
= _('Loading episodes')
1218 elif self
.config
.episode_list_view_mode
!= \
1219 EpisodeListModel
.VIEW_ALL
:
1220 text
= _('No episodes in current view')
1222 text
= _('No episodes available')
1223 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1224 if self
.config
.episode_list_view_mode
!= \
1225 EpisodeListModel
.VIEW_ALL
and \
1226 self
.config
.podcast_list_hide_boring
and \
1227 len(self
.channels
) > 0:
1228 text
= _('No podcasts in this view')
1230 text
= _('No subscriptions')
1231 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1232 text
= _('No active downloads')
1234 raise Exception('on_treeview_expose_event: unknown role')
1236 if gpodder
.ui
.fremantle
:
1237 from gpodder
.gtkui
.frmntl
import style
1238 font_desc
= style
.get_font_desc('LargeSystemFont')
1242 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1246 def enable_download_list_update(self
):
1247 if not self
.download_list_update_enabled
:
1248 self
.update_downloads_list()
1249 gobject
.timeout_add(1500, self
.update_downloads_list
)
1250 self
.download_list_update_enabled
= True
1252 def cleanup_downloads(self
):
1253 model
= self
.download_status_model
1255 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1256 changed_episode_urls
= set()
1257 for row_reference
, task
in all_tasks
:
1258 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1259 model
.remove(model
.get_iter(row_reference
.get_path()))
1261 # We don't "see" this task anymore - remove it;
1262 # this is needed, so update_episode_list_icons()
1263 # below gets the correct list of "seen" tasks
1264 self
.download_tasks_seen
.remove(task
)
1265 except KeyError, key_error
:
1266 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1267 changed_episode_urls
.add(task
.url
)
1268 # Tell the task that it has been removed (so it can clean up)
1269 task
.removed_from_list()
1271 # Tell the podcasts tab to update icons for our removed podcasts
1272 self
.update_episode_list_icons(changed_episode_urls
)
1274 # Tell the shownotes window that we have removed the episode
1275 if self
.episode_shownotes_window
is not None and \
1276 self
.episode_shownotes_window
.episode
is not None and \
1277 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1278 self
.episode_shownotes_window
._download
_status
_changed
(None)
1280 # Update the downloads list one more time
1281 self
.update_downloads_list(can_call_cleanup
=False)
1283 def on_tool_downloads_toggled(self
, toolbutton
):
1284 if toolbutton
.get_active():
1285 self
.wNotebook
.set_current_page(1)
1287 self
.wNotebook
.set_current_page(0)
1289 def add_download_task_monitor(self
, monitor
):
1290 self
.download_task_monitors
.add(monitor
)
1291 model
= self
.download_status_model
1295 task
= row
[self
.download_status_model
.C_TASK
]
1296 monitor
.task_updated(task
)
1298 def remove_download_task_monitor(self
, monitor
):
1299 self
.download_task_monitors
.remove(monitor
)
1301 def update_downloads_list(self
, can_call_cleanup
=True):
1303 model
= self
.download_status_model
1305 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1306 total_speed
, total_size
, done_size
= 0, 0, 0
1308 # Keep a list of all download tasks that we've seen
1309 download_tasks_seen
= set()
1311 # Remember the DownloadTask object for the episode that
1312 # has been opened in the episode shownotes dialog (if any)
1313 if self
.episode_shownotes_window
is not None:
1314 shownotes_episode
= self
.episode_shownotes_window
.episode
1315 shownotes_task
= None
1317 shownotes_episode
= None
1318 shownotes_task
= None
1320 # Do not go through the list of the model is not (yet) available
1324 failed_downloads
= []
1326 self
.download_status_model
.request_update(row
.iter)
1328 task
= row
[self
.download_status_model
.C_TASK
]
1329 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1331 # Let the download task monitors know of changes
1332 for monitor
in self
.download_task_monitors
:
1333 monitor
.task_updated(task
)
1336 done_size
+= size
*progress
1338 if shownotes_episode
is not None and \
1339 shownotes_episode
.url
== task
.episode
.url
:
1340 shownotes_task
= task
1342 download_tasks_seen
.add(task
)
1344 if status
== download
.DownloadTask
.DOWNLOADING
:
1346 total_speed
+= speed
1347 elif status
== download
.DownloadTask
.FAILED
:
1348 failed_downloads
.append(task
)
1350 elif status
== download
.DownloadTask
.DONE
:
1352 elif status
== download
.DownloadTask
.QUEUED
:
1354 elif status
== download
.DownloadTask
.PAUSED
:
1359 # Remember which tasks we have seen after this run
1360 self
.download_tasks_seen
= download_tasks_seen
1362 if gpodder
.ui
.desktop
:
1363 text
= [_('Downloads')]
1364 if downloading
+ failed
+ queued
> 0:
1367 s
.append(N_('%(count)d active', '%(count)d active', downloading
) % {'count':downloading
})
1369 s
.append(N_('%(count)d failed', '%(count)d failed', failed
) % {'count':failed
})
1371 s
.append(N_('%(count)d queued', '%(count)d queued', queued
) % {'count':queued
})
1372 text
.append(' (' + ', '.join(s
)+')')
1373 self
.labelDownloads
.set_text(''.join(text
))
1374 elif gpodder
.ui
.diablo
:
1375 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1377 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1379 self
.tool_downloads
.set_label(_('Downloads'))
1380 elif gpodder
.ui
.fremantle
:
1381 if downloading
+ queued
> 0:
1382 self
.button_downloads
.set_value(N_('%(count)d active', '%(count)d active', downloading
+queued
) % {'count':(downloading
+queued
)})
1384 self
.button_downloads
.set_value(N_('%(count)d failed', '%(count)d failed', failed
) % {'count':failed
})
1386 self
.button_downloads
.set_value(N_('%(count)d paused', '%(count)d paused', paused
) % {'count':paused
})
1388 self
.button_downloads
.set_value(_('Idle'))
1390 title
= [self
.default_title
]
1392 # We have to update all episodes/channels for which the status has
1393 # changed. Accessing task.status_changed has the side effect of
1394 # re-setting the changed flag, so we need to get the "changed" list
1395 # of tuples first and split it into two lists afterwards
1396 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1397 self
.download_tasks_seen
if task
.status_changed
]
1398 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1399 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1401 count
= downloading
+ queued
1403 title
.append(N_('downloading %(count)d file', 'downloading %(count)d files', count
) % {'count':count
})
1406 percentage
= 100.0*done_size
/total_size
1409 total_speed
= util
.format_filesize(total_speed
)
1410 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1411 if self
.tray_icon
is not None:
1412 # Update the tray icon status and progress bar
1413 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1414 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1416 if self
.tray_icon
is not None:
1417 # Update the tray icon status
1418 self
.tray_icon
.set_status()
1419 if gpodder
.ui
.desktop
:
1420 self
.downloads_finished(self
.download_tasks_seen
)
1421 if gpodder
.ui
.diablo
:
1422 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1423 log('All downloads have finished.', sender
=self
)
1424 if self
.config
.cmd_all_downloads_complete
:
1425 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1427 if gpodder
.ui
.fremantle
and failed
:
1428 message
= '\n'.join(['%s: %s' % (str(task
), \
1429 task
.error_message
) for task
in failed_downloads
])
1430 self
.show_message(message
, _('Downloads failed'), important
=True)
1432 # Remove finished episodes
1433 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1434 self
.cleanup_downloads()
1436 # Stop updating the download list here
1437 self
.download_list_update_enabled
= False
1439 if not gpodder
.ui
.fremantle
:
1440 self
.gPodder
.set_title(' - '.join(title
))
1442 self
.update_episode_list_icons(episode_urls
)
1443 if self
.episode_shownotes_window
is not None:
1444 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1445 shownotes_task
!= self
.episode_shownotes_window
.task
:
1446 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1447 self
.episode_shownotes_window
._download
_status
_progress
()
1448 self
.play_or_download()
1450 self
.update_podcast_list_model(channel_urls
)
1452 return self
.download_list_update_enabled
1453 except Exception, e
:
1454 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1455 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1456 # We return False here, so the update loop won't be called again,
1457 # that's why we require the restart of gPodder in the message.
1460 def on_config_changed(self
, *args
):
1461 util
.idle_add(self
._on
_config
_changed
, *args
)
1463 def _on_config_changed(self
, name
, old_value
, new_value
):
1464 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1465 self
.toolbar
.set_property('visible', new_value
)
1466 elif name
== 'videoplayer':
1467 self
.config
.video_played_dbus
= False
1468 elif name
== 'player':
1469 self
.config
.audio_played_dbus
= False
1470 elif name
== 'episode_list_descriptions':
1471 self
.update_episode_list_model()
1472 elif name
== 'episode_list_thumbnails':
1473 self
.update_episode_list_icons(all
=True)
1474 elif name
== 'rotation_mode':
1475 self
._fremantle
_rotation
.set_mode(new_value
)
1476 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1477 self
.restart_auto_update_timer()
1478 elif name
== 'podcast_list_view_all':
1479 # Force a update of the podcast list model
1480 self
.channel_list_changed
= True
1481 if gpodder
.ui
.fremantle
:
1482 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1483 while gtk
.events_pending():
1484 gtk
.main_iteration(False)
1485 self
.update_podcast_list_model()
1486 if gpodder
.ui
.fremantle
:
1487 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1489 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1490 # With get_bin_window, we get the window that contains the rows without
1491 # the header. The Y coordinate of this window will be the height of the
1492 # treeview header. This is the amount we have to subtract from the
1493 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1494 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1497 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1499 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or x
> 50 or (column
is not None and column
!= treeview
.get_columns()[0]):
1500 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1503 if path
is not None:
1504 model
= treeview
.get_model()
1505 iter = model
.get_iter(path
)
1506 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1508 if role
== TreeViewHelper
.ROLE_EPISODES
:
1509 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1510 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1511 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1513 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1514 if last_tooltip
is not None and last_tooltip
!= id:
1515 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1517 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1519 if role
== TreeViewHelper
.ROLE_EPISODES
:
1520 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1522 tooltip
.set_text(description
)
1525 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1526 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1529 channel
.request_save_dir_size()
1530 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1531 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1533 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1534 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1535 table
= gtk
.Table(rows
=3, columns
=3)
1536 table
.set_row_spacings(5)
1537 table
.set_col_spacings(5)
1538 table
.set_border_width(5)
1540 heading
= gtk
.Label()
1541 heading
.set_alignment(0, 1)
1542 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1543 table
.attach(heading
, 0, 1, 0, 1)
1544 size_info
= gtk
.Label()
1545 size_info
.set_alignment(1, 1)
1546 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1547 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1548 table
.attach(size_info
, 2, 3, 0, 1)
1550 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1552 if len(channel
.description
) < 500:
1553 description
= channel
.description
1555 pos
= channel
.description
.find('\n\n')
1556 if pos
== -1 or pos
> 500:
1557 description
= channel
.description
[:498]+'[...]'
1559 description
= channel
.description
[:pos
]
1561 description
= gtk
.Label(description
)
1563 description
.set_markup(error_str
)
1564 description
.set_alignment(0, 0)
1565 description
.set_line_wrap(True)
1566 table
.attach(description
, 0, 3, 2, 3)
1569 tooltip
.set_custom(table
)
1573 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1576 def treeview_allow_tooltips(self
, treeview
, allow
):
1577 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1579 def update_m3u_playlist_clicked(self
, widget
):
1580 if self
.active_channel
is not None:
1581 self
.active_channel
.update_m3u_playlist()
1582 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1584 def treeview_handle_context_menu_click(self
, treeview
, event
):
1585 x
, y
= int(event
.x
), int(event
.y
)
1586 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1588 selection
= treeview
.get_selection()
1589 model
, paths
= selection
.get_selected_rows()
1591 if path
is None or (path
not in paths
and \
1592 event
.button
== self
.context_menu_mouse_button
):
1593 # We have right-clicked, but not into the selection,
1594 # assume we don't want to operate on the selection
1597 if path
is not None and not paths
and \
1598 event
.button
== self
.context_menu_mouse_button
:
1599 # No selection or clicked outside selection;
1600 # select the single item where we clicked
1601 treeview
.grab_focus()
1602 treeview
.set_cursor(path
, column
, 0)
1606 # Unselect any remaining items (clicked elsewhere)
1607 if hasattr(treeview
, 'is_rubber_banding_active'):
1608 if not treeview
.is_rubber_banding_active():
1609 selection
.unselect_all()
1611 selection
.unselect_all()
1615 def downloads_list_get_selection(self
, model
=None, paths
=None):
1616 if model
is None and paths
is None:
1617 selection
= self
.treeDownloads
.get_selection()
1618 model
, paths
= selection
.get_selected_rows()
1620 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1621 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1622 model
.get_value(model
.get_iter(path
), \
1623 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1625 for row_reference
, task
in selected_tasks
:
1626 if task
.status
!= download
.DownloadTask
.QUEUED
:
1628 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1629 download
.DownloadTask
.FAILED
, \
1630 download
.DownloadTask
.CANCELLED
):
1632 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1633 download
.DownloadTask
.QUEUED
, \
1634 download
.DownloadTask
.DOWNLOADING
, \
1635 download
.DownloadTask
.FAILED
):
1637 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1638 download
.DownloadTask
.DOWNLOADING
):
1640 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1641 download
.DownloadTask
.FAILED
, \
1642 download
.DownloadTask
.DONE
):
1645 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1647 def downloads_finished(self
, download_tasks_seen
):
1648 # FIXME: Filter all tasks that have already been reported
1649 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1650 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1652 if finished_downloads
and failed_downloads
:
1653 message
= self
.format_episode_list(finished_downloads
, 5)
1654 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1655 message
+= self
.format_episode_list(failed_downloads
, 5)
1656 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1657 elif finished_downloads
:
1658 message
= self
.format_episode_list(finished_downloads
)
1659 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1660 elif failed_downloads
:
1661 message
= self
.format_episode_list(failed_downloads
)
1662 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1664 # Open torrent files right after download (bug 1029)
1665 if self
.config
.open_torrent_after_download
:
1666 for task
in download_tasks_seen
:
1667 if task
.status
!= task
.DONE
:
1670 episode
= task
.episode
1671 if episode
.mimetype
!= 'application/x-bittorrent':
1674 self
.playback_episodes([episode
])
1677 def format_episode_list(self
, episode_list
, max_episodes
=10):
1679 Format a list of episode names for notifications
1681 Will truncate long episode names and limit the amount of
1682 episodes displayed (max_episodes=10).
1684 The episode_list parameter should be a list of strings.
1686 MAX_TITLE_LENGTH
= 100
1689 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1690 if len(title
) > MAX_TITLE_LENGTH
:
1691 middle
= (MAX_TITLE_LENGTH
/2)-2
1692 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1693 result
.append(saxutils
.escape(title
))
1696 more_episodes
= len(episode_list
) - max_episodes
1697 if more_episodes
> 0:
1698 result
.append('(...')
1699 result
.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes
) % {'count':more_episodes
})
1700 result
.append('...)')
1702 return (''.join(result
)).strip()
1704 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1705 episode_urls
= set()
1706 model
= self
.treeDownloads
.get_model()
1707 for row_reference
, task
in tasks
:
1708 if status
== download
.DownloadTask
.QUEUED
:
1709 # Only queue task when its paused/failed/cancelled (or forced)
1710 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1711 self
.download_queue_manager
.add_task(task
, force_start
)
1712 self
.enable_download_list_update()
1713 elif status
== download
.DownloadTask
.CANCELLED
:
1714 # Cancelling a download allowed when downloading/queued
1715 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1716 task
.status
= status
1717 # Cancelling paused/failed downloads requires a call to .run()
1718 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1719 task
.status
= status
1720 # Call run, so the partial file gets deleted
1722 elif status
== download
.DownloadTask
.PAUSED
:
1723 # Pausing a download only when queued/downloading
1724 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1725 task
.status
= status
1726 elif status
is None:
1727 # Remove the selected task - cancel downloading/queued tasks
1728 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1729 task
.status
= task
.CANCELLED
1730 model
.remove(model
.get_iter(row_reference
.get_path()))
1731 # Remember the URL, so we can tell the UI to update
1733 # We don't "see" this task anymore - remove it;
1734 # this is needed, so update_episode_list_icons()
1735 # below gets the correct list of "seen" tasks
1736 self
.download_tasks_seen
.remove(task
)
1737 except KeyError, key_error
:
1738 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1739 episode_urls
.add(task
.url
)
1740 # Tell the task that it has been removed (so it can clean up)
1741 task
.removed_from_list()
1743 # We can (hopefully) simply set the task status here
1744 task
.status
= status
1745 # Tell the podcasts tab to update icons for our removed podcasts
1746 self
.update_episode_list_icons(episode_urls
)
1747 # Update the tab title and downloads list
1748 self
.update_downloads_list()
1750 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1751 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1753 if not hasattr(treeview
, 'is_rubber_banding_active'):
1756 return not treeview
.is_rubber_banding_active()
1758 if event
.button
== self
.context_menu_mouse_button
:
1759 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1760 self
.downloads_list_get_selection(model
, paths
)
1762 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1763 # This creates a menu item for selection-wide actions
1764 item
= gtk
.ImageMenuItem(label
)
1765 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1766 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1767 item
.set_sensitive(sensitive
)
1768 return self
.set_finger_friendly(item
)
1772 item
= gtk
.ImageMenuItem(_('Episode details'))
1773 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1774 if len(selected_tasks
) == 1:
1775 row_reference
, task
= selected_tasks
[0]
1776 episode
= task
.episode
1777 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1779 item
.set_sensitive(False)
1780 menu
.append(self
.set_finger_friendly(item
))
1781 menu
.append(gtk
.SeparatorMenuItem())
1783 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1785 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1786 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1787 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1788 menu
.append(gtk
.SeparatorMenuItem())
1789 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1791 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
1792 # Because we open the popup on left-click for Maemo,
1793 # we also include a non-action to close the menu
1794 menu
.append(gtk
.SeparatorMenuItem())
1795 item
= gtk
.ImageMenuItem(_('Close this menu'))
1796 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1798 menu
.append(self
.set_finger_friendly(item
))
1801 menu
.popup(None, None, None, event
.button
, event
.time
)
1804 def treeview_channels_show_context_menu(self
, treeview
, event
):
1805 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1809 # Check for valid channel id, if there's no id then
1810 # assume that it is a proxy channel or equivalent
1811 # and cannot be operated with right click
1812 if self
.active_channel
.id is None:
1815 if event
.button
== 3:
1820 item
= gtk
.ImageMenuItem( _('Update podcast'))
1821 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1822 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1823 item
.set_sensitive(not self
.updating_feed_cache
)
1826 menu
.append(gtk
.SeparatorMenuItem())
1828 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1829 item
.set_active(self
.active_channel
.channel_is_locked
)
1830 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1831 menu
.append(self
.set_finger_friendly(item
))
1833 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1834 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1835 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1838 if self
.config
.device_type
!= 'none':
1839 item
= gtk
.MenuItem(_('Synchronize to device'))
1840 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1843 menu
.append( gtk
.SeparatorMenuItem())
1845 item
= gtk
.ImageMenuItem(_('Podcast details'))
1846 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1847 item
.connect('activate', self
.on_itemEditChannel_activate
)
1851 # Disable tooltips while we are showing the menu, so
1852 # the tooltip will not appear over the menu
1853 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1854 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1855 menu
.popup( None, None, None, event
.button
, event
.time
)
1859 def on_itemClose_activate(self
, widget
):
1860 if self
.tray_icon
is not None:
1861 self
.iconify_main_window()
1863 self
.on_gPodder_delete_event(widget
)
1865 def cover_file_removed(self
, channel_url
):
1867 The Cover Downloader calls this when a previously-
1868 available cover has been removed from the disk. We
1869 have to update our model to reflect this change.
1871 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1873 def cover_download_finished(self
, channel
, pixbuf
):
1875 The Cover Downloader calls this when it has finished
1876 downloading (or registering, if already downloaded)
1877 a new channel cover, which is ready for displaying.
1879 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1881 def save_episodes_as_file(self
, episodes
):
1882 for episode
in episodes
:
1883 self
.save_episode_as_file(episode
)
1885 def save_episode_as_file(self
, episode
):
1886 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1887 if episode
.was_downloaded(and_exists
=True):
1888 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1889 copy_from
= episode
.local_filename(create
=False)
1890 assert copy_from
is not None
1891 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1892 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1893 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1895 def copy_episodes_bluetooth(self
, episodes
):
1896 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1898 if gpodder
.ui
.maemo
:
1899 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1900 for e
in episodes_to_copy
])
1903 def convert_and_send_thread(episode
):
1904 for episode
in episodes
:
1905 filename
= episode
.local_filename(create
=False)
1906 assert filename
is not None
1907 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1908 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1909 (base
, ext
) = os
.path
.splitext(filename
)
1910 if not destfile
.endswith(ext
):
1914 shutil
.copyfile(filename
, destfile
)
1915 util
.bluetooth_send_file(destfile
)
1917 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1918 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1920 util
.delete_file(destfile
)
1922 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1924 def get_device_name(self
):
1925 if self
.config
.device_type
== 'ipod':
1927 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1928 return _('MP3 player')
1930 return '(unknown device)'
1932 def _treeview_button_released(self
, treeview
, event
):
1933 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1934 dy
= int(abs(event
.y
-ypos
))
1935 dx
= int(event
.x
-xpos
)
1937 selection
= treeview
.get_selection()
1938 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1939 if path
is None or dy
> 30:
1940 return (False, dx
, dy
)
1942 path
, column
, x
, y
= path
1943 selection
.select_path(path
)
1944 treeview
.set_cursor(path
)
1945 treeview
.grab_focus()
1947 return (True, dx
, dy
)
1949 def treeview_channels_handle_gestures(self
, treeview
, event
):
1950 if self
.currently_updating
:
1953 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1956 if self
.config
.maemo_enable_gestures
:
1958 self
.on_itemUpdateChannel_activate()
1960 self
.on_itemEditChannel_activate(treeview
)
1964 def treeview_available_handle_gestures(self
, treeview
, event
):
1965 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1968 if self
.config
.maemo_enable_gestures
:
1970 self
.on_playback_selected_episodes(None)
1973 self
.on_shownotes_selected_episodes(None)
1976 # Pass the event to the context menu handler for treeAvailable
1977 self
.treeview_available_show_context_menu(treeview
, event
)
1981 def treeview_available_show_context_menu(self
, treeview
, event
):
1982 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1984 if not hasattr(treeview
, 'is_rubber_banding_active'):
1987 return not treeview
.is_rubber_banding_active()
1989 if event
.button
== self
.context_menu_mouse_button
:
1990 episodes
= self
.get_selected_episodes()
1991 any_locked
= any(e
.is_locked
for e
in episodes
)
1992 any_played
= any(e
.is_played
for e
in episodes
)
1993 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1994 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1995 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1999 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
2001 if open_instead_of_play
:
2002 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
2004 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
2006 item
= gtk
.ImageMenuItem(_('Stream'))
2007 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
2009 item
.set_sensitive(can_play
and not downloading
)
2010 item
.connect('activate', self
.on_playback_selected_episodes
)
2011 menu
.append(self
.set_finger_friendly(item
))
2014 item
= gtk
.ImageMenuItem(_('Download'))
2015 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
2016 item
.set_sensitive(can_download
)
2017 item
.connect('activate', self
.on_download_selected_episodes
)
2018 menu
.append(self
.set_finger_friendly(item
))
2020 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
2021 item
.connect('activate', self
.on_item_cancel_download_activate
)
2022 menu
.append(self
.set_finger_friendly(item
))
2024 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
2025 item
.set_sensitive(can_delete
)
2026 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
2027 menu
.append(self
.set_finger_friendly(item
))
2031 # Ok, this probably makes sense to only display for downloaded files
2033 menu
.append(gtk
.SeparatorMenuItem())
2034 share_item
= gtk
.MenuItem(_('Send to'))
2035 menu
.append(self
.set_finger_friendly(share_item
))
2036 share_menu
= gtk
.Menu()
2038 item
= gtk
.ImageMenuItem(_('Local folder'))
2039 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
2040 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
2041 share_menu
.append(self
.set_finger_friendly(item
))
2042 if self
.bluetooth_available
:
2043 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
2044 if gpodder
.ui
.maemo
:
2045 icon_name
= ICON('qgn_list_filesys_bluetooth')
2047 icon_name
= ICON('bluetooth')
2048 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2049 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2050 share_menu
.append(self
.set_finger_friendly(item
))
2052 item
= gtk
.ImageMenuItem(self
.get_device_name())
2053 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2054 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2055 share_menu
.append(self
.set_finger_friendly(item
))
2057 share_item
.set_submenu(share_menu
)
2059 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2060 menu
.append(gtk
.SeparatorMenuItem())
2062 item
= gtk
.CheckMenuItem(_('New'))
2063 item
.set_active(True)
2064 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2065 menu
.append(self
.set_finger_friendly(item
))
2067 item
= gtk
.CheckMenuItem(_('New'))
2068 item
.set_active(False)
2069 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2070 menu
.append(self
.set_finger_friendly(item
))
2073 item
= gtk
.CheckMenuItem(_('Played'))
2074 item
.set_active(any_played
)
2075 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2076 menu
.append(self
.set_finger_friendly(item
))
2078 item
= gtk
.CheckMenuItem(_('Keep episode'))
2079 item
.set_active(any_locked
)
2080 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2081 menu
.append(self
.set_finger_friendly(item
))
2083 menu
.append(gtk
.SeparatorMenuItem())
2084 # Single item, add episode information menu item
2085 item
= gtk
.ImageMenuItem(_('Episode details'))
2086 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2087 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2088 menu
.append(self
.set_finger_friendly(item
))
2090 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
2091 # Because we open the popup on left-click for Maemo,
2092 # we also include a non-action to close the menu
2093 menu
.append(gtk
.SeparatorMenuItem())
2094 item
= gtk
.ImageMenuItem(_('Close this menu'))
2095 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2096 menu
.append(self
.set_finger_friendly(item
))
2099 # Disable tooltips while we are showing the menu, so
2100 # the tooltip will not appear over the menu
2101 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2102 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2103 menu
.popup( None, None, None, event
.button
, event
.time
)
2107 def set_title(self
, new_title
):
2108 if not gpodder
.ui
.fremantle
:
2109 self
.default_title
= new_title
2110 self
.gPodder
.set_title(new_title
)
2112 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2114 Updates the status icons in the episode list.
2116 If urls is given, it should be a list of URLs
2117 of episodes that should be updated.
2119 If urls is None, set ONE OF selected, all to
2120 True (the former updates just the selected
2121 episodes and the latter updates all episodes).
2123 additional_args
= (self
.episode_is_downloading
, \
2124 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2125 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2127 if urls
is not None:
2128 # We have a list of URLs to walk through
2129 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2130 elif selected
and not all
:
2131 # We should update all selected episodes
2132 selection
= self
.treeAvailable
.get_selection()
2133 model
, paths
= selection
.get_selected_rows()
2134 for path
in reversed(paths
):
2135 iter = model
.get_iter(path
)
2136 self
.episode_list_model
.update_by_filter_iter(iter, \
2138 elif all
and not selected
:
2139 # We update all (even the filter-hidden) episodes
2140 self
.episode_list_model
.update_all(*additional_args
)
2142 # Wrong/invalid call - have to specify at least one parameter
2143 raise ValueError('Invalid call to update_episode_list_icons')
2145 def episode_list_status_changed(self
, episodes
):
2146 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2147 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2150 def clean_up_downloads(self
, delete_partial
=False):
2151 # Clean up temporary files left behind by old gPodder versions
2152 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2155 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2157 for tempfile
in temporary_files
:
2158 util
.delete_file(tempfile
)
2160 # Clean up empty download folders and abandoned download folders
2161 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2162 for ddir
in download_dirs
:
2163 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2164 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2165 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2166 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2167 shutil
.rmtree(ddir
, ignore_errors
=True)
2169 def streaming_possible(self
):
2170 if gpodder
.ui
.desktop
:
2171 # User has to have a media player set on the Desktop, or else we
2172 # would probably open the browser when giving a URL to xdg-open..
2173 return (self
.config
.player
and self
.config
.player
!= 'default')
2174 elif gpodder
.ui
.maemo
:
2175 # On Maemo, the default is to use the Nokia Media Player, which is
2176 # already able to deal with HTTP URLs the right way, so we
2177 # unconditionally enable streaming always on Maemo
2182 def playback_episodes_for_real(self
, episodes
):
2183 groups
= collections
.defaultdict(list)
2184 for episode
in episodes
:
2185 file_type
= episode
.file_type()
2186 if file_type
== 'video' and self
.config
.videoplayer
and \
2187 self
.config
.videoplayer
!= 'default':
2188 player
= self
.config
.videoplayer
2189 if gpodder
.ui
.diablo
:
2190 # Use the wrapper script if it's installed to crop 3GP YouTube
2191 # videos to fit the screen (looks much nicer than w/ black border)
2192 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2193 player
= 'gpodder-mplayer'
2194 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2195 player
= 'mplayer -fs %F'
2196 elif file_type
== 'audio' and self
.config
.player
and \
2197 self
.config
.player
!= 'default':
2198 player
= self
.config
.player
2202 if file_type
not in ('audio', 'video') or \
2203 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2204 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2205 # Mark episode as played in the database
2206 episode
.mark(is_played
=True)
2207 self
.mygpo_client
.on_playback([episode
])
2209 filename
= episode
.local_filename(create
=False)
2210 if filename
is None or not os
.path
.exists(filename
):
2211 filename
= episode
.url
2212 if youtube
.is_video_link(filename
):
2213 fmt_id
= self
.config
.youtube_preferred_fmt_id
2214 if gpodder
.ui
.fremantle
:
2216 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2218 # Determine the playback resume position - if the file
2219 # was played 100%, we simply start from the beginning
2220 resume_position
= episode
.current_position
2221 if resume_position
== episode
.total_time
:
2224 if gpodder
.ui
.fremantle
:
2225 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2227 # If Panucci is configured, use D-Bus on Maemo to call it
2228 if player
== 'panucci':
2230 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2231 PANUCCI_PATH
= '/panucciInterface'
2232 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2233 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2234 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2236 def on_reply(*args
):
2239 def error_handler(filename
, err
):
2240 log('Exception in D-Bus call: %s', str(err
), \
2243 # Fallback: use the command line client
2244 for command
in util
.format_desktop_command('panucci', \
2246 log('Executing: %s', repr(command
), sender
=self
)
2247 subprocess
.Popen(command
)
2249 on_error
= lambda err
: error_handler(filename
, err
)
2251 # This method only exists in Panucci > 0.9 ('new Panucci')
2252 i
.playback_from(filename
, resume_position
, \
2253 reply_handler
=on_reply
, error_handler
=on_error
)
2255 continue # This file was handled by the D-Bus call
2256 except Exception, e
:
2257 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2258 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2260 MEDIABOX_NAME
= 'de.pycage.mediabox'
2261 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2262 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2263 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2264 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2266 def on_reply(*args
):
2270 log('Exception in D-Bus call: %s', str(err
), \
2273 i
.load(filename
, '%s/x-unknown' % file_type
, \
2274 reply_handler
=on_reply
, error_handler
=on_error
)
2276 continue # This file was handled by the D-Bus call
2277 except Exception, e
:
2278 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2280 groups
[player
].append(filename
)
2282 # Open episodes with system default player
2283 if 'default' in groups
:
2284 # Special-casing for a single episode when the object is a PDF
2285 # file - this is needed on Maemo 5, so we only use gui_open()
2286 # for single PDF files, but still use the built-in media player
2287 # with an M3U file for single audio/video files. (The Maemo 5
2288 # media player behaves differently when opening a single-file
2289 # M3U playlist compared to opening the single file directly.)
2290 if len(groups
['default']) == 1:
2291 fn
= groups
['default'][0]
2292 # The list of extensions is taken from gui_open in util.py
2293 # where all special-cases of Maemo apps are listed
2294 for extension
in ('.pdf', '.jpg', '.jpeg', '.png'):
2295 if fn
.lower().endswith(extension
):
2297 groups
['default'] = []
2300 if gpodder
.ui
.maemo
and groups
['default']:
2301 # The Nokia Media Player app does not support receiving multiple
2302 # file names via D-Bus, so we simply place all file names into a
2303 # temporary M3U playlist and open that with the Media Player.
2304 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2308 return 'file://' + urllib
.quote(os
.path
.abspath(x
))
2311 util
.write_m3u_playlist(m3u_filename
, \
2312 map(to_url
, groups
['default']), \
2314 util
.gui_open(m3u_filename
)
2316 for filename
in groups
['default']:
2317 log('Opening with system default: %s', filename
, sender
=self
)
2318 util
.gui_open(filename
)
2319 del groups
['default']
2320 elif gpodder
.ui
.maemo
and groups
:
2321 # When on Maemo and not opening with default, show a notification
2322 # (no startup notification for Panucci / MPlayer yet...)
2323 if len(episodes
) == 1:
2324 text
= _('Opening %s') % episodes
[0].title
2326 count
= len(episodes
)
2327 text
= N_('Opening %(count)d episode', 'Opening %(count)d episodes', count
) % {'count':count
}
2329 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2331 def destroy_banner_later(banner
):
2334 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2336 # For each type now, go and create play commands
2337 for group
in groups
:
2338 for command
in util
.format_desktop_command(group
, groups
[group
]):
2339 log('Executing: %s', repr(command
), sender
=self
)
2340 subprocess
.Popen(command
)
2342 # Persist episode status changes to the database
2345 # Flush updated episode status
2346 self
.mygpo_client
.flush()
2348 def playback_episodes(self
, episodes
):
2349 # We need to create a list, because we run through it more than once
2350 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2351 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2354 self
.playback_episodes_for_real(episodes
)
2355 except Exception, e
:
2356 log('Error in playback!', sender
=self
, traceback
=True)
2357 if gpodder
.ui
.desktop
:
2358 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2359 _('Error opening player'), widget
=self
.toolPreferences
)
2361 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2363 channel_urls
= set()
2364 episode_urls
= set()
2365 for episode
in episodes
:
2366 channel_urls
.add(episode
.channel
.url
)
2367 episode_urls
.add(episode
.url
)
2368 self
.update_episode_list_icons(episode_urls
)
2369 self
.update_podcast_list_model(channel_urls
)
2371 def play_or_download(self
):
2372 if not gpodder
.ui
.fremantle
:
2373 if self
.wNotebook
.get_current_page() > 0:
2374 if gpodder
.ui
.desktop
:
2375 self
.toolCancel
.set_sensitive(True)
2378 if self
.currently_updating
:
2379 return (False, False, False, False, False, False)
2381 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2382 ( is_played
, is_locked
) = (False,)*2
2384 open_instead_of_play
= False
2386 selection
= self
.treeAvailable
.get_selection()
2387 if selection
.count_selected_rows() > 0:
2388 (model
, paths
) = selection
.get_selected_rows()
2392 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2393 except TypeError, te
:
2394 log('Invalid episode at path %s', str(path
), sender
=self
)
2397 if episode
.file_type() not in ('audio', 'video'):
2398 open_instead_of_play
= True
2400 if episode
.was_downloaded():
2401 can_play
= episode
.was_downloaded(and_exists
=True)
2402 is_played
= episode
.is_played
2403 is_locked
= episode
.is_locked
2407 if self
.episode_is_downloading(episode
):
2412 can_download
= can_download
and not can_cancel
2413 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2414 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2415 can_delete
= not can_cancel
2417 if gpodder
.ui
.desktop
:
2418 if open_instead_of_play
:
2419 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2421 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2422 self
.toolPlay
.set_sensitive( can_play
)
2423 self
.toolDownload
.set_sensitive( can_download
)
2424 self
.toolTransfer
.set_sensitive( can_transfer
)
2425 self
.toolCancel
.set_sensitive( can_cancel
)
2427 if not gpodder
.ui
.fremantle
:
2428 self
.item_cancel_download
.set_sensitive(can_cancel
)
2429 self
.itemDownloadSelected
.set_sensitive(can_download
)
2430 self
.itemOpenSelected
.set_sensitive(can_play
)
2431 self
.itemPlaySelected
.set_sensitive(can_play
)
2432 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2433 self
.item_toggle_played
.set_sensitive(can_play
)
2434 self
.item_toggle_lock
.set_sensitive(can_play
)
2435 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2436 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2438 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2440 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2441 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2443 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2444 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2446 def episode_new_status_changed(self
, urls
):
2447 self
.update_podcast_list_model()
2448 self
.update_episode_list_icons(urls
)
2450 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2451 """Update the podcast list treeview model
2453 If urls is given, it should list the URLs of each
2454 podcast that has to be updated in the list.
2456 If selected is True, only update the model contents
2457 for the currently-selected podcast - nothing more.
2459 The caller can optionally specify "select_url",
2460 which is the URL of the podcast that is to be
2461 selected in the list after the update is complete.
2462 This only works if the podcast list has to be
2463 reloaded; i.e. something has been added or removed
2464 since the last update of the podcast list).
2466 selection
= self
.treeChannels
.get_selection()
2467 model
, iter = selection
.get_selected()
2469 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2470 # Update "all episodes" view in any case (if enabled)
2471 self
.podcast_list_model
.update_first_row()
2474 # very cheap! only update selected channel
2475 if iter is not None:
2476 # If we have selected the "all episodes" view, we have
2477 # to update all channels for selected episodes:
2478 if self
.config
.podcast_list_view_all
and \
2479 self
.podcast_list_model
.iter_is_first_row(iter):
2480 urls
= self
.get_podcast_urls_from_selected_episodes()
2481 self
.podcast_list_model
.update_by_urls(urls
)
2483 # Otherwise just update the selected row (a podcast)
2484 self
.podcast_list_model
.update_by_filter_iter(iter)
2485 elif not self
.channel_list_changed
:
2486 # we can keep the model, but have to update some
2488 # still cheaper than reloading the whole list
2489 self
.podcast_list_model
.update_all()
2491 # ok, we got a bunch of urls to update
2492 self
.podcast_list_model
.update_by_urls(urls
)
2494 if model
and iter and select_url
is None:
2495 # Get the URL of the currently-selected podcast
2496 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2498 # Update the podcast list model with new channels
2499 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2502 selected_iter
= model
.get_iter_first()
2503 # Find the previously-selected URL in the new
2504 # model if we have an URL (else select first)
2505 if select_url
is not None:
2506 pos
= model
.get_iter_first()
2507 while pos
is not None:
2508 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2509 if url
== select_url
:
2512 pos
= model
.iter_next(pos
)
2514 if not gpodder
.ui
.maemo
:
2515 if selected_iter
is not None:
2516 selection
.select_iter(selected_iter
)
2517 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2519 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2520 self
.channel_list_changed
= False
2522 def episode_is_downloading(self
, episode
):
2523 """Returns True if the given episode is being downloaded at the moment"""
2527 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2529 def on_episode_list_filter_changed(self
, has_episodes
):
2530 if gpodder
.ui
.fremantle
:
2532 self
.episodes_window
.empty_label
.hide()
2533 self
.episodes_window
.pannablearea
.show()
2535 if self
.config
.episode_list_view_mode
!= \
2536 EpisodeListModel
.VIEW_ALL
:
2537 text
= _('No episodes in current view')
2539 text
= _('No episodes available')
2540 self
.episodes_window
.empty_label
.set_text(text
)
2541 self
.episodes_window
.pannablearea
.hide()
2542 self
.episodes_window
.empty_label
.show()
2544 def update_episode_list_model(self
):
2545 if self
.channels
and self
.active_channel
is not None:
2546 if gpodder
.ui
.fremantle
:
2547 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2549 self
.currently_updating
= True
2550 self
.episode_list_model
.clear()
2551 if gpodder
.ui
.fremantle
:
2552 self
.episodes_window
.pannablearea
.hide()
2553 self
.episodes_window
.empty_label
.set_text(_('Loading episodes'))
2554 self
.episodes_window
.empty_label
.show()
2557 additional_args
= (self
.episode_is_downloading
, \
2558 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2559 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2560 self
.episode_list_model
.replace_from_channel(self
.active_channel
, *additional_args
)
2562 self
.treeAvailable
.get_selection().unselect_all()
2563 self
.treeAvailable
.scroll_to_point(0, 0)
2565 self
.currently_updating
= False
2566 self
.play_or_download()
2568 if gpodder
.ui
.fremantle
:
2569 hildon
.hildon_gtk_window_set_progress_indicator(\
2570 self
.episodes_window
.main_window
, False)
2572 util
.idle_add(update
)
2574 self
.episode_list_model
.clear()
2576 @dbus.service
.method(gpodder
.dbus_interface
)
2577 def offer_new_episodes(self
, channels
=None):
2578 if gpodder
.ui
.fremantle
:
2579 # Assume that when this function is called that the
2580 # notification is not shown anymore (Maemo bug 11345)
2581 self
._fremantle
_notification
_visible
= False
2583 new_episodes
= self
.get_new_episodes(channels
)
2585 self
.new_episodes_show(new_episodes
)
2589 def add_podcast_list(self
, urls
, auth_tokens
=None):
2590 """Subscribe to a list of podcast given their URLs
2592 If auth_tokens is given, it should be a dictionary
2593 mapping URLs to (username, password) tuples."""
2595 if auth_tokens
is None:
2598 # Sort and split the URL list into five buckets
2599 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2600 for input_url
in urls
:
2601 url
= util
.normalize_feed_url(input_url
)
2603 # Fail this one because the URL is not valid
2604 failed
.append(input_url
)
2605 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2606 # A podcast already exists in the list for this URL
2607 existing
.append(url
)
2609 # This URL has survived the first round - queue for add
2611 if url
!= input_url
and input_url
in auth_tokens
:
2612 auth_tokens
[url
] = auth_tokens
[input_url
]
2617 progress
= ProgressIndicator(_('Adding podcasts'), \
2618 _('Please wait while episode information is downloaded.'), \
2619 parent
=self
.get_dialog_parent())
2621 def on_after_update():
2622 progress
.on_finished()
2623 # Report already-existing subscriptions to the user
2625 title
= _('Existing subscriptions skipped')
2626 message
= _('You are already subscribed to these podcasts:') \
2627 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2628 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2630 # Report subscriptions that require authentication
2634 title
= _('Podcast requires authentication')
2635 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2636 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2638 retry_podcasts
[url
] = auth_tokens
2640 # Stop asking the user for more login data
2643 error_messages
[url
] = _('Authentication failed')
2647 # If we have authentication data to retry, do so here
2649 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2651 # Report website redirections
2652 for url
in redirections
:
2653 title
= _('Website redirection detected')
2654 message
= _('The URL %(url)s redirects to %(target)s.') \
2655 + '\n\n' + _('Do you want to visit the website now?')
2656 message
= message
% {'url': url
, 'target': redirections
[url
]}
2657 if self
.show_confirmation(message
, title
):
2658 util
.open_website(url
)
2662 # Report failed subscriptions to the user
2664 title
= _('Could not add some podcasts')
2665 message
= _('Some podcasts could not be added to your list:') \
2666 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2667 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2668 self
.show_message(message
, title
, important
=True)
2670 # Upload subscription changes to gpodder.net
2671 self
.mygpo_client
.on_subscribe(worked
)
2673 # If at least one podcast has been added, save and update all
2674 if self
.channel_list_changed
:
2675 # Fix URLs if mygpo has rewritten them
2676 self
.rewrite_urls_mygpo()
2678 self
.save_channels_opml()
2680 # If only one podcast was added, select it after the update
2681 if len(worked
) == 1:
2686 # Update the list of subscribed podcasts
2687 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2688 self
.update_podcasts_tab()
2690 # Offer to download new episodes
2692 for podcast
in self
.channels
:
2693 if podcast
.url
in worked
:
2694 episodes
.extend(podcast
.get_all_episodes())
2697 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2699 self
.new_episodes_show(episodes
, \
2700 selected
=[e
.check_is_new() for e
in episodes
])
2704 # After the initial sorting and splitting, try all queued podcasts
2705 length
= len(queued
)
2706 for index
, url
in enumerate(queued
):
2707 progress
.on_progress(float(index
)/float(length
))
2708 progress
.on_message(url
)
2709 log('QUEUE RUNNER: %s', url
, sender
=self
)
2711 # The URL is valid and does not exist already - subscribe!
2712 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2713 authentication_tokens
=auth_tokens
.get(url
, None), \
2714 max_episodes
=self
.config
.max_episodes_per_feed
, \
2715 download_dir
=self
.config
.download_dir
, \
2716 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2717 mimetype_prefs
=self
.config
.mimetype_prefs
)
2720 username
, password
= util
.username_password_from_url(url
)
2721 except ValueError, ve
:
2722 username
, password
= (None, None)
2724 if username
is not None and channel
.username
is None and \
2725 password
is not None and channel
.password
is None:
2726 channel
.username
= username
2727 channel
.password
= password
2730 self
._update
_cover
(channel
)
2731 except feedcore
.AuthenticationRequired
:
2732 if url
in auth_tokens
:
2733 # Fail for wrong authentication data
2734 error_messages
[url
] = _('Authentication failed')
2737 # Queue for login dialog later
2740 except feedcore
.WifiLogin
, error
:
2741 redirections
[url
] = error
.data
2743 error_messages
[url
] = _('Redirection detected')
2745 except Exception, e
:
2746 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2747 error_messages
[url
] = str(e
)
2751 assert channel
is not None
2752 worked
.append(channel
.url
)
2753 self
.channels
.append(channel
)
2754 self
.channel_list_changed
= True
2755 util
.idle_add(on_after_update
)
2756 threading
.Thread(target
=thread_proc
).start()
2758 def save_channels_opml(self
):
2759 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2760 return exporter
.write(self
.channels
)
2762 def find_episode(self
, podcast_url
, episode_url
):
2763 """Find an episode given its podcast and episode URL
2765 The function will return a PodcastEpisode object if
2766 the episode is found, or None if it's not found.
2768 for podcast
in self
.channels
:
2769 if podcast_url
== podcast
.url
:
2770 for episode
in podcast
.get_all_episodes():
2771 if episode_url
== episode
.url
:
2776 def process_received_episode_actions(self
, updated_urls
):
2777 """Process/merge episode actions from gpodder.net
2779 This function will merge all changes received from
2780 the server to the local database and update the
2781 status of the affected episodes as necessary.
2783 indicator
= ProgressIndicator(_('Merging episode actions'), \
2784 _('Episode actions from gpodder.net are merged.'), \
2785 False, self
.get_dialog_parent())
2787 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2788 if action
.action
== 'play':
2789 episode
= self
.find_episode(action
.podcast_url
, \
2792 if episode
is not None:
2793 log('Play action for %s', episode
.url
, sender
=self
)
2794 episode
.mark(is_played
=True)
2796 if action
.timestamp
> episode
.current_position_updated
and \
2797 action
.position
is not None:
2798 log('Updating position for %s', episode
.url
, sender
=self
)
2799 episode
.current_position
= action
.position
2800 episode
.current_position_updated
= action
.timestamp
2803 log('Updating total time for %s', episode
.url
, sender
=self
)
2804 episode
.total_time
= action
.total
2807 elif action
.action
== 'delete':
2808 episode
= self
.find_episode(action
.podcast_url
, \
2811 if episode
is not None:
2812 if not episode
.was_downloaded(and_exists
=True):
2813 # Set the episode to a "deleted" state
2814 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2815 episode
.delete_from_disk()
2818 indicator
.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx
) % {'count':idx
})
2819 gtk
.main_iteration(False)
2821 indicator
.on_finished()
2825 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2827 self
.updating_feed_cache
= False
2829 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2831 # Process received episode actions for all updated URLs
2832 self
.process_received_episode_actions(updated_urls
)
2834 self
.channel_list_changed
= True
2835 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2837 # Only search for new episodes in podcasts that have been
2838 # updated, not in other podcasts (for single-feed updates)
2839 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2841 if gpodder
.ui
.fremantle
:
2842 self
.fancy_progress_bar
.hide()
2843 self
.button_subscribe
.set_sensitive(True)
2844 self
.button_refresh
.set_sensitive(True)
2845 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2846 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2847 self
.update_podcasts_tab()
2848 self
.update_episode_list_model()
2849 if self
.feed_cache_update_cancelled
:
2852 def application_in_foreground():
2854 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2855 except Exception, e
:
2856 log('Could not determine is-topmost', traceback
=True)
2857 # When in doubt, assume not in foreground
2861 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2862 # New episodes found, but we should do nothing
2863 self
.show_message(_('New episodes are available.'))
2864 elif self
.config
.auto_download
== 'always':
2865 count
= len(episodes
)
2866 title
= N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count
) % {'count':count
}
2867 self
.show_message(title
)
2868 self
.download_episode_list(episodes
)
2869 elif self
.config
.auto_download
== 'queue':
2870 self
.show_message(_('New episodes have been added to the download list.'))
2871 self
.download_episode_list_paused(episodes
)
2872 elif application_in_foreground():
2873 if not self
._fremantle
_notification
_visible
:
2874 self
.new_episodes_show(episodes
)
2875 elif not self
._fremantle
_notification
_visible
:
2878 pynotify
.init('gPodder')
2879 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2880 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2881 n
.set_hint('dbus-callback-default', ' '.join([
2882 gpodder
.dbus_bus_name
,
2883 gpodder
.dbus_gui_object_path
,
2884 gpodder
.dbus_interface
,
2885 'offer_new_episodes',
2887 n
.set_category('gpodder-new-episodes')
2889 self
._fremantle
_notification
_visible
= True
2890 except Exception, e
:
2891 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2892 self
.new_episodes_show(episodes
)
2893 self
._fremantle
_notification
_visible
= False
2894 elif not self
.config
.auto_update_feeds
:
2895 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2899 self
.tray_icon
.set_status()
2901 if self
.feed_cache_update_cancelled
:
2902 # The user decided to abort the feed update
2903 self
.show_update_feeds_buttons()
2905 # Nothing new here - but inform the user
2906 self
.pbFeedUpdate
.set_fraction(1.0)
2907 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2908 self
.feed_cache_update_cancelled
= True
2909 self
.btnCancelFeedUpdate
.show()
2910 self
.btnCancelFeedUpdate
.set_sensitive(True)
2911 self
.itemUpdate
.set_sensitive(True)
2912 if gpodder
.ui
.maemo
:
2913 # btnCancelFeedUpdate is a ToolButton on Maemo
2914 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2916 # btnCancelFeedUpdate is a normal gtk.Button
2917 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2919 count
= len(episodes
)
2920 # New episodes are available
2921 self
.pbFeedUpdate
.set_fraction(1.0)
2922 # Are we minimized and should we auto download?
2923 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2924 self
.download_episode_list(episodes
)
2925 title
= N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count
) % {'count':count
}
2926 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2927 self
.show_update_feeds_buttons()
2928 elif self
.config
.auto_download
== 'queue':
2929 self
.download_episode_list_paused(episodes
)
2930 title
= N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count
) % {'count':count
}
2931 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2932 self
.show_update_feeds_buttons()
2934 self
.show_update_feeds_buttons()
2935 # New episodes are available and we are not minimized
2936 if not self
.config
.do_not_show_new_episodes_dialog
:
2937 self
.new_episodes_show(episodes
, notification
=True)
2939 message
= N_('%(count)d new episode available', '%(count)d new episodes available', count
) % {'count':count
}
2940 self
.pbFeedUpdate
.set_text(message
)
2942 def _update_cover(self
, channel
):
2943 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2944 self
.cover_downloader
.request_cover(channel
)
2946 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2947 total
= len(channels
)
2949 for updated
, channel
in enumerate(channels
):
2950 if not self
.feed_cache_update_cancelled
:
2952 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2953 mimetype_prefs
=self
.config
.mimetype_prefs
)
2954 self
._update
_cover
(channel
)
2955 except Exception, e
:
2956 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2958 message
= _('Error while updating %(url)s: %(message)s')
2960 message
= _('The feed at %(url)s could not be updated.')
2961 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2962 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2964 if self
.feed_cache_update_cancelled
:
2967 # By the time we get here the update may have already been cancelled
2968 if not self
.feed_cache_update_cancelled
:
2969 def update_progress():
2970 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2971 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2972 self
.pbFeedUpdate
.set_text(progression
)
2974 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2975 self
.pbFeedUpdate
.set_fraction(float(updated
+1)/float(total
))
2976 util
.idle_add(update_progress
)
2978 updated_urls
= [c
.url
for c
in channels
]
2979 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2981 def show_update_feeds_buttons(self
):
2982 # Make sure that the buttons for updating feeds
2983 # appear - this should happen after a feed update
2984 if gpodder
.ui
.maemo
:
2985 self
.btnUpdateSelectedFeed
.show()
2986 self
.toolFeedUpdateProgress
.hide()
2987 self
.btnCancelFeedUpdate
.hide()
2988 self
.btnCancelFeedUpdate
.set_is_important(False)
2989 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2990 self
.toolbarSpacer
.set_expand(True)
2991 self
.toolbarSpacer
.set_draw(False)
2993 self
.hboxUpdateFeeds
.hide()
2994 self
.btnUpdateFeeds
.show()
2995 self
.itemUpdate
.set_sensitive(True)
2996 self
.itemUpdateChannel
.set_sensitive(True)
2998 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2999 if not self
.feed_cache_update_cancelled
:
3000 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
3001 self
.feed_cache_update_cancelled
= True
3002 if not gpodder
.ui
.fremantle
:
3003 self
.btnCancelFeedUpdate
.set_sensitive(False)
3004 elif not gpodder
.ui
.fremantle
:
3005 self
.show_update_feeds_buttons()
3007 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
3008 if self
.updating_feed_cache
:
3009 if gpodder
.ui
.fremantle
:
3010 self
.feed_cache_update_cancelled
= True
3013 if not force_update
:
3014 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
3015 self
.channel_list_changed
= True
3016 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
3019 # Fix URLs if mygpo has rewritten them
3020 self
.rewrite_urls_mygpo()
3022 self
.updating_feed_cache
= True
3024 if channels
is None:
3025 # Only update podcasts for which updates are enabled
3026 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
3028 if gpodder
.ui
.fremantle
:
3029 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
3030 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
3031 self
.fancy_progress_bar
.show()
3032 self
.button_subscribe
.set_sensitive(False)
3033 self
.button_refresh
.set_sensitive(False)
3034 self
.feed_cache_update_cancelled
= False
3036 self
.itemUpdate
.set_sensitive(False)
3037 self
.itemUpdateChannel
.set_sensitive(False)
3040 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
3042 self
.feed_cache_update_cancelled
= False
3043 self
.btnCancelFeedUpdate
.show()
3044 self
.btnCancelFeedUpdate
.set_sensitive(True)
3045 if gpodder
.ui
.maemo
:
3046 self
.toolbarSpacer
.set_expand(False)
3047 self
.toolbarSpacer
.set_draw(True)
3048 self
.btnUpdateSelectedFeed
.hide()
3049 self
.toolFeedUpdateProgress
.show_all()
3051 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
3052 self
.hboxUpdateFeeds
.show_all()
3053 self
.btnUpdateFeeds
.hide()
3055 if len(channels
) == 1:
3056 text
= _('Updating "%s"...') % channels
[0].title
3058 count
= len(channels
)
3059 text
= N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count
) % {'count':count
}
3060 self
.pbFeedUpdate
.set_text(text
)
3061 self
.pbFeedUpdate
.set_fraction(0)
3063 args
= (channels
, select_url_afterwards
)
3064 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
3066 def on_gPodder_delete_event(self
, widget
, *args
):
3067 """Called when the GUI wants to close the window
3068 Displays a confirmation dialog (and closes/hides gPodder)
3071 downloading
= self
.download_status_model
.are_downloads_in_progress()
3073 # Only iconify if we are using the window's "X" button,
3074 # but not when we are using "Quit" in the menu or toolbar
3075 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
3076 self
.iconify_main_window()
3078 if gpodder
.ui
.fremantle
:
3079 self
.close_gpodder()
3080 elif gpodder
.ui
.diablo
:
3081 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
3083 self
.close_gpodder()
3086 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
3087 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3088 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
3090 title
= _('Quit gPodder')
3091 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3093 dialog
.set_title(title
)
3094 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3096 quit_button
.grab_focus()
3097 result
= dialog
.run()
3100 if result
== gtk
.RESPONSE_CLOSE
:
3101 self
.close_gpodder()
3103 self
.close_gpodder()
3107 def close_gpodder(self
):
3108 """ clean everything and exit properly
3111 if self
.save_channels_opml():
3112 pass # FIXME: Add mygpo synchronization here
3114 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3118 if self
.tray_icon
is not None:
3119 self
.tray_icon
.set_visible(False)
3121 # Notify all tasks to to carry out any clean-up actions
3122 self
.download_status_model
.tell_all_tasks_to_quit()
3124 while gtk
.events_pending():
3125 gtk
.main_iteration(False)
3132 def get_expired_episodes(self
):
3133 for channel
in self
.channels
:
3134 for episode
in channel
.get_downloaded_episodes():
3135 # Never consider locked episodes as old
3136 if episode
.is_locked
:
3139 # Never consider fresh episodes as old
3140 if episode
.age_in_days() < self
.config
.episode_old_age
:
3143 # Do not delete played episodes (except if configured)
3144 if episode
.is_played
:
3145 if not self
.config
.auto_remove_played_episodes
:
3148 # Do not delete unplayed episodes (except if configured)
3149 if not episode
.is_played
:
3150 if not self
.config
.auto_remove_unplayed_episodes
:
3155 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3160 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3163 title
= _('Episodes are locked')
3164 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3165 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3168 count
= len(episodes
)
3169 title
= N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count
) % {'count':count
}
3170 message
= _('Deleting episodes removes downloaded files.')
3172 if gpodder
.ui
.fremantle
:
3173 message
= '\n'.join([title
, message
])
3175 if confirm
and not self
.show_confirmation(message
, title
):
3178 progress
= ProgressIndicator(_('Deleting episodes'), \
3179 _('Please wait while episodes are deleted'), \
3180 parent
=self
.get_dialog_parent())
3182 def finish_deletion(episode_urls
, channel_urls
):
3183 progress
.on_finished()
3185 # Episodes have been deleted - persist the database
3188 self
.update_episode_list_icons(episode_urls
)
3189 self
.update_podcast_list_model(channel_urls
)
3190 self
.play_or_download()
3193 episode_urls
= set()
3194 channel_urls
= set()
3196 episodes_status_update
= []
3197 for idx
, episode
in enumerate(episodes
):
3198 progress
.on_progress(float(idx
)/float(len(episodes
)))
3199 if episode
.is_locked
and skip_locked
:
3200 log('Not deleting episode (is locked): %s', episode
.title
)
3202 log('Deleting episode: %s', episode
.title
)
3203 progress
.on_message(episode
.title
)
3204 episode
.delete_from_disk()
3205 episode_urls
.add(episode
.url
)
3206 channel_urls
.add(episode
.channel
.url
)
3207 episodes_status_update
.append(episode
)
3209 # Tell the shownotes window that we have removed the episode
3210 if self
.episode_shownotes_window
is not None and \
3211 self
.episode_shownotes_window
.episode
is not None and \
3212 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3213 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3215 # Notify the web service about the status update + upload
3216 self
.mygpo_client
.on_delete(episodes_status_update
)
3217 self
.mygpo_client
.flush()
3219 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3221 threading
.Thread(target
=thread_proc
).start()
3225 def on_itemRemoveOldEpisodes_activate(self
, widget
):
3226 self
.show_delete_episodes_window()
3228 def show_delete_episodes_window(self
, channel
=None):
3229 """Offer deletion of episodes
3231 If channel is None, offer deletion of all episodes.
3232 Otherwise only offer deletion of episodes in the channel.
3234 if gpodder
.ui
.maemo
:
3236 ('maemo_remove_markup', None, None, _('Episode')),
3240 ('title_markup', None, None, _('Episode')),
3241 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3242 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3243 ('played_prop', None, None, _('Status')),
3244 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3247 msg_older_than
= N_('Select older than %(count)d day', 'Select older than %(count)d days', self
.config
.episode_old_age
)
3248 selection_buttons
= {
3249 _('Select played'): lambda episode
: episode
.is_played
,
3250 _('Select finished'): lambda episode
: episode
.is_finished(),
3251 msg_older_than
% {'count':self
.config
.episode_old_age
}: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3254 instructions
= _('Select the episodes you want to delete:')
3257 channels
= self
.channels
3259 channels
= [channel
]
3262 for channel
in channels
:
3263 for episode
in channel
.get_downloaded_episodes():
3264 # Disallow deletion of locked episodes that still exist
3265 if not episode
.is_locked
or not episode
.file_exists():
3266 episodes
.append(episode
)
3268 selected
= [e
.is_played
or not e
.file_exists() for e
in episodes
]
3270 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3271 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3272 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3273 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3274 show_episode_shownotes
=self
.show_episode_shownotes
)
3276 def on_selected_episodes_status_changed(self
):
3277 # The order of the updates here is important! When "All episodes" is
3278 # selected, the update of the podcast list model depends on the episode
3279 # list selection to determine which podcasts are affected. Updating
3280 # the episode list could remove the selection if a filter is active.
3281 self
.update_podcast_list_model(selected
=True)
3282 self
.update_episode_list_icons(selected
=True)
3285 def mark_selected_episodes_new(self
):
3286 for episode
in self
.get_selected_episodes():
3288 self
.on_selected_episodes_status_changed()
3290 def mark_selected_episodes_old(self
):
3291 for episode
in self
.get_selected_episodes():
3293 self
.on_selected_episodes_status_changed()
3295 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3296 for episode
in self
.get_selected_episodes():
3298 episode
.mark(is_played
=not episode
.is_played
)
3300 episode
.mark(is_played
=new_value
)
3301 self
.on_selected_episodes_status_changed()
3303 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3304 for episode
in self
.get_selected_episodes():
3306 episode
.mark(is_locked
=not episode
.is_locked
)
3308 episode
.mark(is_locked
=new_value
)
3309 self
.on_selected_episodes_status_changed()
3311 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3312 if self
.active_channel
is None:
3315 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3316 self
.active_channel
.update_channel_lock()
3318 for episode
in self
.active_channel
.get_all_episodes():
3319 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3321 self
.update_podcast_list_model(selected
=True)
3322 self
.update_episode_list_icons(all
=True)
3324 def on_itemUpdateChannel_activate(self
, widget
=None):
3325 if self
.active_channel
is None:
3326 title
= _('No podcast selected')
3327 message
= _('Please select a podcast in the podcasts list to update.')
3328 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3331 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3332 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3333 self
.update_feed_cache()
3335 self
.update_feed_cache(channels
=[self
.active_channel
])
3337 def on_itemUpdate_activate(self
, widget
=None):
3338 # Check if we have outstanding subscribe/unsubscribe actions
3339 if self
.on_add_remove_podcasts_mygpo():
3340 log('Update cancelled (received server changes)', sender
=self
)
3344 self
.update_feed_cache()
3346 gPodderWelcome(self
.gPodder
,
3347 center_on_widget
=self
.gPodder
,
3348 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3349 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3351 def download_episode_list_paused(self
, episodes
):
3352 self
.download_episode_list(episodes
, True)
3354 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3355 enable_update
= False
3357 for episode
in episodes
:
3358 log('Downloading episode: %s', episode
.title
, sender
= self
)
3359 if not episode
.was_downloaded(and_exists
=True):
3361 for task
in self
.download_tasks_seen
:
3362 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3363 self
.download_queue_manager
.add_task(task
, force_start
)
3364 enable_update
= True
3372 task
= download
.DownloadTask(episode
, self
.config
)
3373 except Exception, e
:
3374 d
= {'episode': episode
.title
, 'message': str(e
)}
3375 message
= _('Download error while downloading %(episode)s: %(message)s')
3376 self
.show_message(message
% d
, _('Download error'), important
=True)
3377 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3381 task
.status
= task
.PAUSED
3383 self
.mygpo_client
.on_download([task
.episode
])
3384 self
.download_queue_manager
.add_task(task
, force_start
)
3386 self
.download_status_model
.register_task(task
)
3387 enable_update
= True
3390 self
.enable_download_list_update()
3392 # Flush updated episode status
3393 self
.mygpo_client
.flush()
3395 def cancel_task_list(self
, tasks
):
3400 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3401 task
.status
= task
.CANCELLED
3402 elif task
.status
== task
.PAUSED
:
3403 task
.status
= task
.CANCELLED
3404 # Call run, so the partial file gets deleted
3407 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3408 self
.play_or_download()
3410 # Update the tab title and downloads list
3411 self
.update_downloads_list()
3413 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3414 if gpodder
.ui
.maemo
:
3416 ('maemo_markup', None, None, _('Episode')),
3418 show_notification
= notification
3421 ('title_markup', None, None, _('Episode')),
3422 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3423 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3425 show_notification
= False
3427 instructions
= _('Select the episodes you want to download:')
3429 if self
.new_episodes_window
is not None:
3430 self
.new_episodes_window
.main_window
.destroy()
3431 self
.new_episodes_window
= None
3433 def download_episodes_callback(episodes
):
3434 self
.new_episodes_window
= None
3435 self
.download_episode_list(episodes
)
3437 if selected
is None:
3438 # Select all by default
3439 selected
= [True]*len(episodes
)
3441 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3442 title
=_('New episodes available'), \
3443 instructions
=instructions
, \
3444 episodes
=episodes
, \
3446 selected
=selected
, \
3447 stock_ok_button
= 'gpodder-download', \
3448 callback
=download_episodes_callback
, \
3449 remove_callback
=lambda e
: e
.mark_old(), \
3450 remove_action
=_('Mark as old'), \
3451 remove_finished
=self
.episode_new_status_changed
, \
3452 _config
=self
.config
, \
3453 show_notification
=show_notification
, \
3454 show_episode_shownotes
=self
.show_episode_shownotes
)
3456 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3457 if not self
.offer_new_episodes():
3458 self
.show_message(_('Please check for new episodes later.'), \
3459 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3461 def get_new_episodes(self
, channels
=None):
3462 if channels
is None:
3463 channels
= self
.channels
3465 for channel
in channels
:
3466 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3467 episodes
.append(episode
)
3471 @dbus.service
.method(gpodder
.dbus_interface
)
3472 def start_device_synchronization(self
):
3473 """Public D-Bus API for starting Device sync (Desktop only)
3475 This method can be called to initiate a synchronization with
3476 a configured protable media player. This only works for the
3477 Desktop version of gPodder and does nothing on Maemo.
3479 if gpodder
.ui
.desktop
:
3480 self
.on_sync_to_ipod_activate(None)
3485 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3486 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3488 def commit_changes_to_database(self
):
3489 """This will be called after the sync process is finished"""
3492 def on_cleanup_ipod_activate(self
, widget
, *args
):
3493 self
.sync_ui
.on_cleanup_device()
3495 def on_manage_device_playlist(self
, widget
):
3496 self
.sync_ui
.on_manage_device_playlist()
3498 def show_hide_tray_icon(self
):
3499 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3500 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3501 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3502 self
.tray_icon
.set_visible(False)
3504 self
.tray_icon
= None
3506 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3507 self
.tray_icon
.set_visible(self
.is_iconified())
3508 elif self
.tray_icon
:
3509 self
.tray_icon
.set_visible(True)
3511 def on_itemShowAllEpisodes_activate(self
, widget
):
3512 self
.config
.podcast_list_view_all
= widget
.get_active()
3514 def on_itemShowToolbar_activate(self
, widget
):
3515 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3517 def on_itemShowDescription_activate(self
, widget
):
3518 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3520 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3521 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3522 if self
.config
.podcast_list_hide_boring
:
3523 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3525 self
.podcast_list_model
.set_view_mode(-1)
3527 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3529 if current
== self
.item_view_podcasts_all
:
3530 self
.podcast_list_model
.set_view_mode(-1)
3531 elif current
== self
.item_view_podcasts_downloaded
:
3532 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3533 elif current
== self
.item_view_podcasts_unplayed
:
3534 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3536 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3538 def on_item_view_episodes_changed(self
, radioaction
, current
):
3539 if current
== self
.item_view_episodes_all
:
3540 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_ALL
3541 elif current
== self
.item_view_episodes_undeleted
:
3542 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNDELETED
3543 elif current
== self
.item_view_episodes_downloaded
:
3544 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_DOWNLOADED
3545 elif current
== self
.item_view_episodes_unplayed
:
3546 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNPLAYED
3548 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3550 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3551 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3553 def update_item_device( self
):
3554 if not gpodder
.ui
.fremantle
:
3555 if self
.config
.device_type
!= 'none':
3556 self
.itemDevice
.set_visible(True)
3557 self
.itemDevice
.label
= self
.get_device_name()
3559 self
.itemDevice
.set_visible(False)
3561 def properties_closed( self
):
3562 self
.preferences_dialog
= None
3563 self
.show_hide_tray_icon()
3564 self
.update_item_device()
3565 if gpodder
.ui
.maemo
:
3566 selection
= self
.treeAvailable
.get_selection()
3567 if self
.config
.maemo_enable_gestures
or \
3568 self
.config
.enable_fingerscroll
:
3569 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3571 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3573 def on_itemPreferences_activate(self
, widget
, *args
):
3574 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3575 _config
=self
.config
, \
3576 callback_finished
=self
.properties_closed
, \
3577 user_apps_reader
=self
.user_apps_reader
, \
3578 parent_window
=self
.main_window
, \
3579 mygpo_client
=self
.mygpo_client
, \
3580 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3582 # Initial message to relayout window (in case it's opened in portrait mode
3583 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3585 def on_itemDependencies_activate(self
, widget
):
3586 gPodderDependencyManager(self
.gPodder
)
3588 def on_goto_mygpo(self
, widget
):
3589 self
.mygpo_client
.open_website()
3591 def on_download_subscriptions_from_mygpo(self
, action
=None):
3592 title
= _('Login to gpodder.net')
3593 message
= _('Please login to download your subscriptions.')
3594 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3595 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3599 self
.config
.mygpo_username
= username
3600 self
.config
.mygpo_password
= password
3602 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3603 custom_title
=_('Subscriptions on gpodder.net'), \
3604 add_urls_callback
=self
.add_podcast_list
, \
3605 hide_url_entry
=True)
3607 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3608 # we do not have to hardcode the URL here
3609 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3610 url
= util
.url_add_authentication(OPML_URL
, \
3611 self
.config
.mygpo_username
, \
3612 self
.config
.mygpo_password
)
3613 dir.download_opml_file(url
)
3615 def on_mygpo_settings_activate(self
, action
=None):
3616 # This dialog is only used for Maemo 4
3617 if not gpodder
.ui
.diablo
:
3620 settings
= MygPodderSettings(self
.main_window
, \
3621 config
=self
.config
, \
3622 mygpo_client
=self
.mygpo_client
, \
3623 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3625 def on_itemAddChannel_activate(self
, widget
=None):
3626 gPodderAddPodcast(self
.gPodder
, \
3627 add_urls_callback
=self
.add_podcast_list
)
3629 def on_itemEditChannel_activate(self
, widget
, *args
):
3630 if self
.active_channel
is None:
3631 title
= _('No podcast selected')
3632 message
= _('Please select a podcast in the podcasts list to edit.')
3633 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3636 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3637 gPodderChannel(self
.main_window
, \
3638 channel
=self
.active_channel
, \
3639 callback_closed
=callback_closed
, \
3640 cover_downloader
=self
.cover_downloader
)
3642 def on_itemMassUnsubscribe_activate(self
, item
=None):
3644 ('title', None, None, _('Podcast')),
3647 # We're abusing the Episode Selector for selecting Podcasts here,
3648 # but it works and looks good, so why not? -- thp
3649 gPodderEpisodeSelector(self
.main_window
, \
3650 title
=_('Remove podcasts'), \
3651 instructions
=_('Select the podcast you want to remove.'), \
3652 episodes
=self
.channels
, \
3654 size_attribute
=None, \
3655 stock_ok_button
=_('Remove'), \
3656 callback
=self
.remove_podcast_list
, \
3657 _config
=self
.config
)
3659 def remove_podcast_list(self
, channels
, confirm
=True):
3661 log('No podcasts selected for deletion', sender
=self
)
3664 if len(channels
) == 1:
3665 title
= _('Removing podcast')
3666 info
= _('Please wait while the podcast is removed')
3667 message
= _('Do you really want to remove this podcast and its episodes?')
3669 title
= _('Removing podcasts')
3670 info
= _('Please wait while the podcasts are removed')
3671 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3673 if confirm
and not self
.show_confirmation(message
, title
):
3676 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3678 def finish_deletion(select_url
):
3679 # Upload subscription list changes to the web service
3680 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3682 # Re-load the channels and select the desired new channel
3683 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3684 progress
.on_finished()
3685 self
.update_podcasts_tab()
3690 for idx
, channel
in enumerate(channels
):
3691 # Update the UI for correct status messages
3692 progress
.on_progress(float(idx
)/float(len(channels
)))
3693 progress
.on_message(channel
.title
)
3695 # Delete downloaded episodes
3696 channel
.remove_downloaded()
3698 # cancel any active downloads from this channel
3699 for episode
in channel
.get_all_episodes():
3700 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3703 if len(channels
) == 1:
3704 # get the URL of the podcast we want to select next
3705 if channel
in self
.channels
:
3706 position
= self
.channels
.index(channel
)
3710 if position
== len(self
.channels
)-1:
3711 # this is the last podcast, so select the URL
3712 # of the item before this one (i.e. the "new last")
3713 select_url
= self
.channels
[position
-1].url
3715 # there is a podcast after the deleted one, so
3716 # we simply select the one that comes after it
3717 select_url
= self
.channels
[position
+1].url
3719 # Remove the channel and clean the database entries
3721 self
.channels
.remove(channel
)
3723 # Clean up downloads and download directories
3724 self
.clean_up_downloads()
3726 self
.channel_list_changed
= True
3727 self
.save_channels_opml()
3729 # The remaining stuff is to be done in the GTK main thread
3730 util
.idle_add(finish_deletion
, select_url
)
3732 threading
.Thread(target
=thread_proc
).start()
3734 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3735 if self
.active_channel
is None:
3736 title
= _('No podcast selected')
3737 message
= _('Please select a podcast in the podcasts list to remove.')
3738 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3741 self
.remove_podcast_list([self
.active_channel
])
3743 def get_opml_filter(self
):
3744 filter = gtk
.FileFilter()
3745 filter.add_pattern('*.opml')
3746 filter.add_pattern('*.xml')
3747 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3750 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3751 if filename
is None:
3752 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3753 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), \
3754 parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3755 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3756 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3757 elif gpodder
.ui
.diablo
:
3758 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3759 dlg
.set_filter(self
.get_opml_filter())
3760 response
= dlg
.run()
3762 if response
== gtk
.RESPONSE_OK
:
3763 filename
= dlg
.get_filename()
3766 if filename
is not None:
3767 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3768 custom_title
=_('Import podcasts from OPML file'), \
3769 add_urls_callback
=self
.add_podcast_list
, \
3770 hide_url_entry
=True)
3771 dir.download_opml_file(filename
)
3773 def on_itemExportChannels_activate(self
, widget
, *args
):
3774 if not self
.channels
:
3775 title
= _('Nothing to export')
3776 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3777 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3780 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3781 # FIXME: Hildonization on Fremantle
3782 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3783 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3784 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3785 elif gpodder
.ui
.diablo
:
3786 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3787 dlg
.set_filter(self
.get_opml_filter())
3788 response
= dlg
.run()
3789 if response
== gtk
.RESPONSE_OK
:
3790 filename
= dlg
.get_filename()
3792 exporter
= opml
.Exporter( filename
)
3793 if exporter
.write(self
.channels
):
3794 count
= len(self
.channels
)
3795 title
= N_('%(count)d subscription exported', '%(count)d subscriptions exported', count
) % {'count':count
}
3796 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3798 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3802 def on_itemImportChannels_activate(self
, widget
, *args
):
3803 if gpodder
.ui
.fremantle
:
3804 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3805 self
.config
.toplist_url
, \
3806 self
.config
.opml_url
, \
3807 self
.add_podcast_list
, \
3808 self
.on_itemAddChannel_activate
, \
3809 self
.on_download_subscriptions_from_mygpo
, \
3810 self
.show_text_edit_dialog
)
3812 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3813 add_urls_callback
=self
.add_podcast_list
)
3814 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3816 def on_homepage_activate(self
, widget
, *args
):
3817 util
.open_website(gpodder
.__url
__)
3819 def on_wiki_activate(self
, widget
, *args
):
3820 util
.open_website('http://gpodder.org/wiki/User_Manual')
3822 def on_bug_tracker_activate(self
, widget
, *args
):
3823 if gpodder
.ui
.maemo
:
3824 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3826 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3828 def on_item_support_activate(self
, widget
):
3829 util
.open_website('http://gpodder.org/donate')
3831 def on_itemAbout_activate(self
, widget
, *args
):
3832 if gpodder
.ui
.fremantle
:
3833 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3834 HeAboutDialog
.present(self
.main_window
,
3837 gpodder
.__version
__,
3838 _('A podcast client with focus on usability'),
3839 gpodder
.__copyright
__,
3841 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3842 'http://gpodder.org/donate')
3845 dlg
= gtk
.AboutDialog()
3846 dlg
.set_transient_for(self
.main_window
)
3847 dlg
.set_name('gPodder')
3848 dlg
.set_version(gpodder
.__version
__)
3849 dlg
.set_copyright(gpodder
.__copyright
__)
3850 dlg
.set_comments(_('A podcast client with focus on usability'))
3851 dlg
.set_website(gpodder
.__url
__)
3852 dlg
.set_translator_credits( _('translator-credits'))
3853 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3855 if gpodder
.ui
.desktop
:
3856 # For the "GUI" version, we add some more
3857 # items to the about dialog (credits and logo)
3860 'Thomas Perl <thp.io>',
3863 if os
.path
.exists(gpodder
.credits_file
):
3864 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3865 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3866 app_authors
+= credits
3868 dlg
.set_authors(app_authors
)
3870 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3872 dlg
.set_logo_icon_name('gpodder')
3876 def on_wNotebook_switch_page(self
, widget
, *args
):
3878 if gpodder
.ui
.maemo
:
3879 self
.tool_downloads
.set_active(page_num
== 1)
3880 page
= self
.wNotebook
.get_nth_page(page_num
)
3881 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3882 if page_num
== 0 and self
.active_channel
is not None:
3883 self
.set_title(self
.active_channel
.title
)
3885 self
.set_title(tab_label
)
3887 self
.play_or_download()
3888 self
.menuChannels
.set_sensitive(True)
3889 self
.menuSubscriptions
.set_sensitive(True)
3890 # The message area in the downloads tab should be hidden
3891 # when the user switches away from the downloads tab
3892 if self
.message_area
is not None:
3893 self
.message_area
.hide()
3894 self
.message_area
= None
3896 self
.menuChannels
.set_sensitive(False)
3897 self
.menuSubscriptions
.set_sensitive(False)
3898 if gpodder
.ui
.desktop
:
3899 self
.toolDownload
.set_sensitive(False)
3900 self
.toolPlay
.set_sensitive(False)
3901 self
.toolTransfer
.set_sensitive(False)
3902 self
.toolCancel
.set_sensitive(False)
3904 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3905 # double-click action of the podcast list or enter
3906 self
.treeChannels
.set_cursor(path
)
3908 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3909 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3911 if model
is not None and iter is not None:
3912 old_active_channel
= self
.active_channel
3913 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3915 if self
.active_channel
== old_active_channel
:
3918 if gpodder
.ui
.maemo
:
3919 self
.set_title(self
.active_channel
.title
)
3921 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3922 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3923 self
.itemEditChannel
.set_visible(False)
3924 self
.itemRemoveChannel
.set_visible(False)
3926 self
.itemEditChannel
.set_visible(True)
3927 self
.itemRemoveChannel
.set_visible(True)
3929 self
.active_channel
= None
3930 self
.itemEditChannel
.set_visible(False)
3931 self
.itemRemoveChannel
.set_visible(False)
3933 self
.update_episode_list_model()
3935 def on_btnEditChannel_clicked(self
, widget
, *args
):
3936 self
.on_itemEditChannel_activate( widget
, args
)
3938 def get_podcast_urls_from_selected_episodes(self
):
3939 """Get a set of podcast URLs based on the selected episodes"""
3940 return set(episode
.channel
.url
for episode
in \
3941 self
.get_selected_episodes())
3943 def get_selected_episodes(self
):
3944 """Get a list of selected episodes from treeAvailable"""
3945 selection
= self
.treeAvailable
.get_selection()
3946 model
, paths
= selection
.get_selected_rows()
3948 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3951 def on_transfer_selected_episodes(self
, widget
):
3952 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3954 def on_playback_selected_episodes(self
, widget
):
3955 self
.playback_episodes(self
.get_selected_episodes())
3957 def on_shownotes_selected_episodes(self
, widget
):
3958 episodes
= self
.get_selected_episodes()
3960 episode
= episodes
.pop(0)
3961 self
.show_episode_shownotes(episode
)
3963 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3965 def on_download_selected_episodes(self
, widget
):
3966 episodes
= self
.get_selected_episodes()
3967 self
.download_episode_list(episodes
)
3968 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3969 self
.play_or_download()
3971 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3972 """Double-click/enter action handler for treeAvailable"""
3973 # We should only have one one selected as it was double clicked!
3974 e
= self
.get_selected_episodes()[0]
3976 if (self
.config
.double_click_episode_action
== 'download'):
3977 # If the episode has already been downloaded and exists then play it
3978 if e
.was_downloaded(and_exists
=True):
3979 self
.playback_episodes(self
.get_selected_episodes())
3980 # else download it if it is not already downloading
3981 elif not self
.episode_is_downloading(e
):
3982 self
.download_episode_list([e
])
3983 self
.update_episode_list_icons([e
.url
])
3984 self
.play_or_download()
3985 elif (self
.config
.double_click_episode_action
== 'stream'):
3986 # If we happen to have downloaded this episode simple play it
3987 if e
.was_downloaded(and_exists
=True):
3988 self
.playback_episodes(self
.get_selected_episodes())
3989 # else if streaming is possible stream it
3990 elif self
.streaming_possible():
3991 self
.playback_episodes(self
.get_selected_episodes())
3993 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3994 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3996 # default action is to display show notes
3997 self
.on_shownotes_selected_episodes(widget
)
3999 def show_episode_shownotes(self
, episode
):
4000 if self
.episode_shownotes_window
is None:
4001 log('First-time use of episode window --- creating', sender
=self
)
4002 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
4003 _download_episode_list
=self
.download_episode_list
, \
4004 _playback_episodes
=self
.playback_episodes
, \
4005 _delete_episode_list
=self
.delete_episode_list
, \
4006 _episode_list_status_changed
=self
.episode_list_status_changed
, \
4007 _cancel_task_list
=self
.cancel_task_list
, \
4008 _episode_is_downloading
=self
.episode_is_downloading
, \
4009 _streaming_possible
=self
.streaming_possible())
4010 self
.episode_shownotes_window
.show(episode
)
4011 if self
.episode_is_downloading(episode
):
4012 self
.update_downloads_list()
4014 def restart_auto_update_timer(self
):
4015 if self
._auto
_update
_timer
_source
_id
is not None:
4016 log('Removing existing auto update timer.', sender
=self
)
4017 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
4018 self
._auto
_update
_timer
_source
_id
= None
4020 if self
.config
.auto_update_feeds
and \
4021 self
.config
.auto_update_frequency
:
4022 interval
= 60*1000*self
.config
.auto_update_frequency
4023 log('Setting up auto update timer with interval %d.', \
4024 self
.config
.auto_update_frequency
, sender
=self
)
4025 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
4026 interval
, self
._on
_auto
_update
_timer
)
4028 def _on_auto_update_timer(self
):
4029 log('Auto update timer fired.', sender
=self
)
4030 self
.update_feed_cache(force_update
=True)
4032 # Ask web service for sub changes (if enabled)
4033 self
.mygpo_client
.flush()
4037 def on_treeDownloads_row_activated(self
, widget
, *args
):
4038 # Use the standard way of working on the treeview
4039 selection
= self
.treeDownloads
.get_selection()
4040 (model
, paths
) = selection
.get_selected_rows()
4041 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
4043 for tree_row_reference
, task
in selected_tasks
:
4044 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
4045 task
.status
= task
.PAUSED
4046 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
4047 self
.download_queue_manager
.add_task(task
)
4048 self
.enable_download_list_update()
4049 elif task
.status
== task
.DONE
:
4050 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
4052 self
.play_or_download()
4054 # Update the tab title and downloads list
4055 self
.update_downloads_list()
4057 def on_item_cancel_download_activate(self
, widget
):
4058 if self
.wNotebook
.get_current_page() == 0:
4059 selection
= self
.treeAvailable
.get_selection()
4060 (model
, paths
) = selection
.get_selected_rows()
4061 urls
= [model
.get_value(model
.get_iter(path
), \
4062 self
.episode_list_model
.C_URL
) for path
in paths
]
4063 selected_tasks
= [task
for task
in self
.download_tasks_seen \
4064 if task
.url
in urls
]
4066 selection
= self
.treeDownloads
.get_selection()
4067 (model
, paths
) = selection
.get_selected_rows()
4068 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
4069 self
.download_status_model
.C_TASK
) for path
in paths
]
4070 self
.cancel_task_list(selected_tasks
)
4072 def on_btnCancelAll_clicked(self
, widget
, *args
):
4073 self
.cancel_task_list(self
.download_tasks_seen
)
4075 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
4076 episodes
= self
.get_selected_episodes()
4077 if len(episodes
) == 1:
4078 self
.delete_episode_list(episodes
, skip_locked
=False)
4080 self
.delete_episode_list(episodes
)
4082 def on_key_press(self
, widget
, event
):
4083 # Allow tab switching with Ctrl + PgUp/PgDown
4084 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
4085 if event
.keyval
== gtk
.keysyms
.Page_Up
:
4086 self
.wNotebook
.prev_page()
4088 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
4089 self
.wNotebook
.next_page()
4092 # After this code we only handle Maemo hardware keys,
4093 # so if we are not a Maemo app, we don't do anything
4094 if not gpodder
.ui
.maemo
:
4098 if event
.keyval
== gtk
.keysyms
.F7
: #plus
4100 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
4103 if diff
!= 0 and not self
.currently_updating
:
4104 selection
= self
.treeChannels
.get_selection()
4105 (model
, iter) = selection
.get_selected()
4106 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4107 selection
.select_path(new_path
)
4108 self
.treeChannels
.set_cursor(new_path
)
4113 def on_iconify(self
):
4115 self
.gPodder
.set_skip_taskbar_hint(True)
4116 if self
.config
.minimize_to_tray
:
4117 self
.tray_icon
.set_visible(True)
4119 self
.gPodder
.set_skip_taskbar_hint(False)
4121 def on_uniconify(self
):
4123 self
.gPodder
.set_skip_taskbar_hint(False)
4124 if self
.config
.minimize_to_tray
:
4125 self
.tray_icon
.set_visible(False)
4127 self
.gPodder
.set_skip_taskbar_hint(False)
4129 def uniconify_main_window(self
):
4130 if self
.is_iconified():
4131 # We need to hide and then show the window in WMs like Metacity
4132 # or KWin4 to move the window to the active workspace
4133 # (see http://gpodder.org/bug/1125)
4136 self
.gPodder
.present()
4138 def iconify_main_window(self
):
4139 if not self
.is_iconified():
4140 self
.gPodder
.iconify()
4142 def update_podcasts_tab(self
):
4143 if len(self
.channels
):
4144 if gpodder
.ui
.fremantle
:
4145 self
.button_refresh
.set_title(_('Check for new episodes'))
4146 self
.button_refresh
.show()
4148 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4150 if gpodder
.ui
.fremantle
:
4151 self
.button_refresh
.hide()
4153 self
.label2
.set_text(_('Podcasts'))
4155 @dbus.service
.method(gpodder
.dbus_interface
)
4156 def show_gui_window(self
):
4157 parent
= self
.get_dialog_parent()
4160 @dbus.service
.method(gpodder
.dbus_interface
)
4161 def subscribe_to_url(self
, url
):
4162 gPodderAddPodcast(self
.gPodder
,
4163 add_urls_callback
=self
.add_podcast_list
,
4166 @dbus.service
.method(gpodder
.dbus_interface
)
4167 def mark_episode_played(self
, filename
):
4168 if filename
is None:
4171 for channel
in self
.channels
:
4172 for episode
in channel
.get_all_episodes():
4173 fn
= episode
.local_filename(create
=False, check_only
=True)
4175 episode
.mark(is_played
=True)
4177 self
.update_episode_list_icons([episode
.url
])
4178 self
.update_podcast_list_model([episode
.channel
.url
])
4184 def main(options
=None):
4185 gobject
.threads_init()
4186 gobject
.set_application_name('gPodder')
4188 if gpodder
.ui
.maemo
:
4189 # Try to enable the custom icon theme for gPodder on Maemo
4190 settings
= gtk
.settings_get_default()
4191 settings
.set_string_property('gtk-icon-theme-name', \
4192 'gpodder', __file__
)
4193 # Extend the search path for the optified icon theme (Maemo 5)
4194 icon_theme
= gtk
.icon_theme_get_default()
4195 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4197 gtk
.window_set_default_icon_name('gpodder')
4198 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4201 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4202 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4204 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4205 except dbus
.exceptions
.DBusException
, dbe
:
4206 log('Warning: Cannot get "on the bus".', traceback
=True)
4207 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4208 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4209 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4210 dlg
.set_title('gPodder')
4215 util
.make_directory(gpodder
.home
)
4216 gpodder
.load_plugins()
4218 config
= UIConfig(gpodder
.config_file
)
4220 # Load hook modules and install the hook manager globally
4221 # if modules have been found an instantiated by the manager
4222 user_hooks
= hooks
.HookManager()
4223 if user_hooks
.has_modules():
4224 gpodder
.user_hooks
= user_hooks
4226 if gpodder
.ui
.diablo
:
4227 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4228 # folder exists there (allow moving "gpodder" between SD cards or USB)
4229 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4230 if not os
.path
.exists(config
.download_dir
):
4231 log('Downloads might have been moved. Trying to locate them...')
4232 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4233 dir = os
.path
.join(basedir
, 'gpodder')
4234 if os
.path
.exists(dir):
4235 log('Downloads found in: %s', dir)
4236 config
.download_dir
= dir
4239 log('Downloads NOT FOUND in %s', dir)
4241 if config
.enable_fingerscroll
:
4242 BuilderWidget
.use_fingerscroll
= True
4244 config
.mygpo_device_type
= util
.detect_device_type()
4246 gp
= gPodder(bus_name
, config
)
4249 if options
.subscribe
:
4250 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4253 # handle "subscribe to podcast" events from firefox
4254 if platform
.system() == 'Darwin':
4255 from gpodder
import gpodderosx
4256 gpodderosx
.register_handlers(gp
)
4257 # end mac OS X stuff