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 action_area_box
.set_spacing(2)
365 action_area_box
.set_border_width(3)
366 self
.treeChannels
.set_action_area_visible(True)
368 # Set up a very nice progress bar setup
369 self
.fancy_progress_bar
= FancyProgressBar(self
.main_window
, \
370 self
.on_btnCancelFeedUpdate_clicked
)
371 self
.pbFeedUpdate
= self
.fancy_progress_bar
.progress_bar
372 self
.pbFeedUpdate
.set_ellipsize(pango
.ELLIPSIZE_MIDDLE
)
373 self
.vbox
.pack_start(self
.fancy_progress_bar
.event_box
, False)
375 from gpodder
.gtkui
.frmntl
import style
376 sub_font
= style
.get_font_desc('SmallSystemFont')
377 sub_color
= style
.get_color('SecondaryTextColor')
378 sub
= (sub_font
.to_string(), sub_color
.to_string())
379 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
380 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
382 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
383 while gtk
.events_pending():
384 gtk
.main_iteration(False)
387 # Try to get the real package version from dpkg
388 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
389 version
, _stderr
= p
.communicate()
393 version
= gpodder
.__version
__
394 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
395 self
.label_footer
.hide()
397 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
398 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
399 show_episode_shownotes
=self
.show_episode_shownotes
, \
400 update_podcast_list_model
=self
.update_podcast_list_model
, \
401 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
402 item_view_episodes_all
=self
.item_view_episodes_all
, \
403 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
404 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
405 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
406 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
407 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
408 hide_episode_search
=self
.hide_episode_search
, \
409 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
410 playback_episodes
=self
.playback_episodes
, \
411 delete_episode_list
=self
.delete_episode_list
, \
412 episode_list_status_changed
=self
.episode_list_status_changed
, \
413 download_episode_list
=self
.download_episode_list
, \
414 episode_is_downloading
=self
.episode_is_downloading
, \
415 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
416 add_download_task_monitor
=self
.add_download_task_monitor
, \
417 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
418 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
419 on_itemUpdate_activate
=self
.on_itemUpdate_activate
, \
420 show_delete_episodes_window
=self
.show_delete_episodes_window
, \
421 cover_downloader
=self
.cover_downloader
)
423 # Expose objects for episode list type-ahead find
424 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
425 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
426 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
428 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
429 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
430 cleanup_downloads
=self
.cleanup_downloads
, \
431 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
432 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
435 self
.treeAvailable
= self
.episodes_window
.treeview
436 self
.treeDownloads
= self
.downloads_window
.treeview
438 # Source IDs for timeouts for search-as-you-type
439 self
._podcast
_list
_search
_timeout
= None
440 self
._episode
_list
_search
_timeout
= None
442 # Init the treeviews that we use
443 self
.init_podcast_list_treeview()
444 self
.init_episode_list_treeview()
445 self
.init_download_list_treeview()
447 if self
.config
.podcast_list_hide_boring
:
448 self
.item_view_hide_boring_podcasts
.set_active(True)
450 self
.currently_updating
= False
452 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
453 self
.context_menu_mouse_button
= 1
455 self
.context_menu_mouse_button
= 3
457 if self
.config
.start_iconified
:
458 self
.iconify_main_window()
460 self
.download_tasks_seen
= set()
461 self
.download_list_update_enabled
= False
462 self
.download_task_monitors
= set()
464 # Subscribed channels
465 self
.active_channel
= None
466 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
467 self
.channel_list_changed
= True
468 self
.update_podcasts_tab()
470 # load list of user applications for audio playback
471 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
472 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
474 # Set the "Device" menu item for the first time
475 if gpodder
.ui
.desktop
:
476 self
.update_item_device()
478 # Set up the first instance of MygPoClient
479 self
.mygpo_client
= my
.MygPoClient(self
.config
)
481 # Now, update the feed cache, when everything's in place
482 if not gpodder
.ui
.fremantle
:
483 self
.btnUpdateFeeds
.show()
484 self
.updating_feed_cache
= False
485 self
.feed_cache_update_cancelled
= False
486 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
488 self
.message_area
= None
490 def find_partial_downloads():
491 # Look for partial file downloads
492 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
493 count
= len(partial_files
)
494 resumable_episodes
= []
496 if not gpodder
.ui
.fremantle
:
497 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
498 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
499 _('Some episodes have not finished downloading in a previous session.'), \
500 False, self
.get_dialog_parent())
501 indicator
.on_message(N_('%(count)d partial file', '%(count)d partial files', count
) % {'count':count
})
503 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
506 for c
in self
.channels
:
507 for e
in c
.get_all_episodes():
508 filename
= e
.local_filename(create
=False, check_only
=True)
509 if filename
in candidates
:
510 log('Found episode: %s', e
.title
, sender
=self
)
512 indicator
.on_message(e
.title
)
513 indicator
.on_progress(float(found
)/count
)
514 candidates
.remove(filename
)
515 partial_files
.remove(filename
+'.partial')
516 resumable_episodes
.append(e
)
524 for f
in partial_files
:
525 log('Partial file without episode: %s', f
, sender
=self
)
528 util
.idle_add(indicator
.on_finished
)
530 if len(resumable_episodes
):
531 def offer_resuming():
532 self
.download_episode_list_paused(resumable_episodes
)
533 if not gpodder
.ui
.fremantle
:
534 resume_all
= gtk
.Button(_('Resume all'))
535 #resume_all.set_border_width(0)
536 def on_resume_all(button
):
537 selection
= self
.treeDownloads
.get_selection()
538 selection
.select_all()
539 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
540 selection
.unselect_all()
541 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
542 self
.message_area
.hide()
543 resume_all
.connect('clicked', on_resume_all
)
545 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
546 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
547 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
548 self
.message_area
.show_all()
549 self
.clean_up_downloads(delete_partial
=False)
550 util
.idle_add(offer_resuming
)
551 elif not gpodder
.ui
.fremantle
:
552 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
554 util
.idle_add(self
.clean_up_downloads
, True)
555 threading
.Thread(target
=find_partial_downloads
).start()
557 # Start the auto-update procedure
558 self
._auto
_update
_timer
_source
_id
= None
559 if self
.config
.auto_update_feeds
:
560 self
.restart_auto_update_timer()
562 # Delete old episodes if the user wishes to
563 if self
.config
.auto_remove_played_episodes
and \
564 self
.config
.episode_old_age
> 0:
565 old_episodes
= list(self
.get_expired_episodes())
566 if len(old_episodes
) > 0:
567 self
.delete_episode_list(old_episodes
, confirm
=False)
568 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
570 if gpodder
.ui
.fremantle
:
571 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
572 self
.button_refresh
.set_sensitive(True)
573 self
.button_subscribe
.set_sensitive(True)
574 self
.main_window
.set_title(_('gPodder'))
575 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
577 # Do the initial sync with the web service
578 util
.idle_add(self
.mygpo_client
.flush
, True)
580 # First-time users should be asked if they want to see the OPML
581 if not self
.channels
and not gpodder
.ui
.fremantle
:
582 util
.idle_add(self
.on_itemUpdate_activate
)
584 def episode_object_by_uri(self
, uri
):
585 """Get an episode object given a local or remote URI
587 This can be used to quickly access an episode object
588 when all we have is its download filename or episode
589 URL (e.g. from external D-Bus calls / signals, etc..)
591 if uri
.startswith('/'):
592 uri
= 'file://' + uri
594 prefix
= 'file://' + self
.config
.download_dir
596 if uri
.startswith(prefix
):
597 # File is on the local filesystem in the download folder
598 filename
= urllib
.unquote(uri
[len(prefix
):])
599 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
601 if len(file_parts
) == 2:
602 dir_name
, filename
= file_parts
603 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
604 if len(channels
) == 1:
605 channel
= channels
[0]
606 return channel
.get_episode_by_filename(filename
)
608 # Possibly remote file - search the database for a podcast
609 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
611 if channel_id
is not None:
612 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
613 if len(channels
) == 1:
614 channel
= channels
[0]
615 return channel
.get_episode_by_url(uri
)
619 def on_played(self
, start
, end
, total
, file_uri
):
620 """Handle the "played" signal from a media player"""
621 if start
== 0 and end
== 0 and total
== 0:
622 # Ignore bogus play event
624 elif end
< start
+ 5:
625 # Ignore "less than five seconds" segments,
626 # as they can happen with seeking, etc...
629 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
630 episode
= self
.episode_object_by_uri(file_uri
)
632 if episode
is not None:
633 file_type
= episode
.file_type()
634 # Automatically enable D-Bus played status mode
635 if file_type
== 'audio':
636 self
.config
.audio_played_dbus
= True
637 elif file_type
== 'video':
638 self
.config
.video_played_dbus
= True
642 episode
.total_time
= total
644 # Assume the episode's total time for the action
645 total
= episode
.total_time
646 if episode
.current_position_updated
is None or \
647 now
> episode
.current_position_updated
:
648 episode
.current_position
= end
649 episode
.current_position_updated
= now
650 episode
.mark(is_played
=True)
653 self
.update_episode_list_icons([episode
.url
])
654 self
.update_podcast_list_model([episode
.channel
.url
])
656 # Submit this action to the webservice
657 self
.mygpo_client
.on_playback_full(episode
, \
660 def on_add_remove_podcasts_mygpo(self
):
661 actions
= self
.mygpo_client
.get_received_actions()
665 existing_urls
= [c
.url
for c
in self
.channels
]
667 # Columns for the episode selector window - just one...
669 ('description', None, None, _('Action')),
672 # A list of actions that have to be chosen from
675 # Actions that are ignored (already carried out)
678 for action
in actions
:
679 if action
.is_add
and action
.url
not in existing_urls
:
680 changes
.append(my
.Change(action
))
681 elif action
.is_remove
and action
.url
in existing_urls
:
682 podcast_object
= None
683 for podcast
in self
.channels
:
684 if podcast
.url
== action
.url
:
685 podcast_object
= podcast
687 changes
.append(my
.Change(action
, podcast_object
))
689 log('Ignoring action: %s', action
, sender
=self
)
690 ignored
.append(action
)
692 # Confirm all ignored changes
693 self
.mygpo_client
.confirm_received_actions(ignored
)
695 def execute_podcast_actions(selected
):
696 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
697 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
699 # Apply the accepted changes locally
700 self
.add_podcast_list(add_list
)
701 self
.remove_podcast_list(remove_list
, confirm
=False)
703 # All selected items are now confirmed
704 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
706 # Revert the changes on the server
707 rejected
= [c
.action
for c
in changes
if c
not in selected
]
708 self
.mygpo_client
.reject_received_actions(rejected
)
711 # We're abusing the Episode Selector again ;) -- thp
712 gPodderEpisodeSelector(self
.main_window
, \
713 title
=_('Confirm changes from gpodder.net'), \
714 instructions
=_('Select the actions you want to carry out.'), \
717 size_attribute
=None, \
718 stock_ok_button
=gtk
.STOCK_APPLY
, \
719 callback
=execute_podcast_actions
, \
722 # There are some actions that need the user's attention
727 # We have no remaining actions - no selection happens
730 def rewrite_urls_mygpo(self
):
731 # Check if we have to rewrite URLs since the last add
732 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
734 for rewritten_url
in rewritten_urls
:
735 if not rewritten_url
.new_url
:
738 for channel
in self
.channels
:
739 if channel
.url
== rewritten_url
.old_url
:
740 log('Updating URL of %s to %s', channel
, \
741 rewritten_url
.new_url
, sender
=self
)
742 channel
.url
= rewritten_url
.new_url
744 self
.channel_list_changed
= True
745 util
.idle_add(self
.update_episode_list_model
)
748 def on_send_full_subscriptions(self
):
749 # Send the full subscription list to the gpodder.net client
750 # (this will overwrite the subscription list on the server)
751 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
752 _('Your subscriptions are being uploaded to the server.'), \
753 False, self
.get_dialog_parent())
756 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
757 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
762 message
= e
.__class
__.__name
__
763 self
.show_message(message
, \
764 _('Error while uploading'), \
766 util
.idle_add(show_error
, e
)
768 util
.idle_add(indicator
.on_finished
)
770 def on_podcast_selected(self
, treeview
, path
, column
):
772 model
= treeview
.get_model()
773 channel
= model
.get_value(model
.get_iter(path
), \
774 PodcastListModel
.C_CHANNEL
)
775 self
.active_channel
= channel
776 self
.update_episode_list_model()
777 self
.episodes_window
.channel
= self
.active_channel
778 self
.episodes_window
.show()
780 def on_button_subscribe_clicked(self
, button
):
781 self
.on_itemImportChannels_activate(button
)
783 def on_button_downloads_clicked(self
, widget
):
784 self
.downloads_window
.show()
786 def show_episode_in_download_manager(self
, episode
):
787 self
.downloads_window
.show()
788 model
= self
.treeDownloads
.get_model()
789 selection
= self
.treeDownloads
.get_selection()
790 selection
.unselect_all()
791 it
= model
.get_iter_first()
792 while it
is not None:
793 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
794 if task
.episode
.url
== episode
.url
:
795 selection
.select_iter(it
)
796 # FIXME: Scroll to selection in pannable area
798 it
= model
.iter_next(it
)
800 def for_each_episode_set_task_status(self
, episodes
, status
):
801 episode_urls
= set(episode
.url
for episode
in episodes
)
802 model
= self
.treeDownloads
.get_model()
803 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
804 model
.get_value(row
.iter, \
805 DownloadStatusModel
.C_TASK
)) for row
in model \
806 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
808 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
810 def on_window_orientation_changed(self
, orientation
):
811 self
._last
_orientation
= orientation
812 if self
.preferences_dialog
is not None:
813 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
815 treeview
= self
.treeChannels
816 if orientation
== Orientation
.PORTRAIT
:
817 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
818 # Work around Maemo bug #4718
819 self
.button_subscribe
.set_name('HildonButton-thumb')
820 self
.button_refresh
.set_name('HildonButton-thumb')
822 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
823 # Work around Maemo bug #4718
824 self
.button_subscribe
.set_name('HildonButton-finger')
825 self
.button_refresh
.set_name('HildonButton-finger')
827 if gpodder
.ui
.fremantle
:
828 self
.fancy_progress_bar
.relayout()
830 def on_treeview_podcasts_selection_changed(self
, selection
):
831 model
, iter = selection
.get_selected()
833 self
.active_channel
= None
834 self
.episode_list_model
.clear()
836 def on_treeview_button_pressed(self
, treeview
, event
):
837 if event
.window
!= treeview
.get_bin_window():
840 TreeViewHelper
.save_button_press_event(treeview
, event
)
842 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
843 TreeViewHelper
.ROLE_PODCASTS
:
844 return self
.currently_updating
846 return event
.button
== self
.context_menu_mouse_button
and \
849 def on_treeview_podcasts_button_released(self
, treeview
, event
):
850 if event
.window
!= treeview
.get_bin_window():
854 return self
.treeview_channels_handle_gestures(treeview
, event
)
855 return self
.treeview_channels_show_context_menu(treeview
, event
)
857 def on_treeview_episodes_button_released(self
, treeview
, event
):
858 if event
.window
!= treeview
.get_bin_window():
861 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
862 return self
.treeview_available_handle_gestures(treeview
, event
)
864 return self
.treeview_available_show_context_menu(treeview
, event
)
866 def on_treeview_downloads_button_released(self
, treeview
, event
):
867 if event
.window
!= treeview
.get_bin_window():
870 return self
.treeview_downloads_show_context_menu(treeview
, event
)
872 def on_entry_search_podcasts_changed(self
, editable
):
873 if self
.hbox_search_podcasts
.get_property('visible'):
874 def set_search_term(self
, text
):
875 self
.podcast_list_model
.set_search_term(text
)
876 self
._podcast
_list
_search
_timeout
= None
879 if self
._podcast
_list
_search
_timeout
is not None:
880 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
881 self
._podcast
_list
_search
_timeout
= gobject
.timeout_add(\
882 self
.LIVE_SEARCH_DELAY
, \
883 set_search_term
, self
, editable
.get_chars(0, -1))
885 def on_entry_search_podcasts_key_press(self
, editable
, event
):
886 if event
.keyval
== gtk
.keysyms
.Escape
:
887 self
.hide_podcast_search()
890 def hide_podcast_search(self
, *args
):
891 if self
._podcast
_list
_search
_timeout
is not None:
892 gobject
.source_remove(self
._podcast
_list
_search
_timeout
)
893 self
._podcast
_list
_search
_timeout
= None
894 self
.hbox_search_podcasts
.hide()
895 self
.entry_search_podcasts
.set_text('')
896 self
.podcast_list_model
.set_search_term(None)
897 self
.treeChannels
.grab_focus()
899 def show_podcast_search(self
, input_char
):
900 self
.hbox_search_podcasts
.show()
901 self
.entry_search_podcasts
.insert_text(input_char
, -1)
902 self
.entry_search_podcasts
.grab_focus()
903 self
.entry_search_podcasts
.set_position(-1)
905 def init_podcast_list_treeview(self
):
906 # Set up podcast channel tree view widget
907 if gpodder
.ui
.fremantle
:
908 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
909 self
.item_view_podcasts_downloaded
.set_active(True)
910 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
911 self
.item_view_podcasts_unplayed
.set_active(True)
913 self
.item_view_podcasts_all
.set_active(True)
914 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
916 iconcolumn
= gtk
.TreeViewColumn('')
917 iconcell
= gtk
.CellRendererPixbuf()
918 iconcolumn
.pack_start(iconcell
, False)
919 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
920 self
.treeChannels
.append_column(iconcolumn
)
922 namecolumn
= gtk
.TreeViewColumn('')
923 namecell
= gtk
.CellRendererText()
924 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
925 namecolumn
.pack_start(namecell
, True)
926 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
928 if gpodder
.ui
.fremantle
:
929 countcell
= gtk
.CellRendererText()
930 from gpodder
.gtkui
.frmntl
import style
931 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
932 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
933 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
934 countcell
.set_property('xalign', 1.)
935 countcell
.set_property('xpad', 5)
936 namecolumn
.pack_start(countcell
, False)
937 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
938 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
940 iconcell
= gtk
.CellRendererPixbuf()
941 iconcell
.set_property('xalign', 1.0)
942 namecolumn
.pack_start(iconcell
, False)
943 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
944 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
946 self
.treeChannels
.append_column(namecolumn
)
948 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
950 # When no podcast is selected, clear the episode list model
951 selection
= self
.treeChannels
.get_selection()
952 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
954 # Set up type-ahead find for the podcast list
955 def on_key_press(treeview
, event
):
956 if event
.keyval
== gtk
.keysyms
.Escape
:
957 self
.hide_podcast_search()
958 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
959 self
.hide_podcast_search()
960 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
961 # Don't handle type-ahead when control is pressed (so shortcuts
962 # with the Ctrl key still work, e.g. Ctrl+A, ...)
965 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
966 if unicode_char_id
== 0:
968 input_char
= unichr(unicode_char_id
)
969 self
.show_podcast_search(input_char
)
971 self
.treeChannels
.connect('key-press-event', on_key_press
)
973 # Enable separators to the podcast list to separate special podcasts
974 # from others (this is used for the "all episodes" view)
975 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
977 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
979 def on_entry_search_episodes_changed(self
, editable
):
980 if self
.hbox_search_episodes
.get_property('visible'):
981 def set_search_term(self
, text
):
982 self
.episode_list_model
.set_search_term(text
)
983 self
._episode
_list
_search
_timeout
= None
986 if self
._episode
_list
_search
_timeout
is not None:
987 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
988 self
._episode
_list
_search
_timeout
= gobject
.timeout_add(\
989 self
.LIVE_SEARCH_DELAY
, \
990 set_search_term
, self
, editable
.get_chars(0, -1))
992 def on_entry_search_episodes_key_press(self
, editable
, event
):
993 if event
.keyval
== gtk
.keysyms
.Escape
:
994 self
.hide_episode_search()
997 def hide_episode_search(self
, *args
):
998 if self
._episode
_list
_search
_timeout
is not None:
999 gobject
.source_remove(self
._episode
_list
_search
_timeout
)
1000 self
._episode
_list
_search
_timeout
= None
1001 self
.hbox_search_episodes
.hide()
1002 self
.entry_search_episodes
.set_text('')
1003 self
.episode_list_model
.set_search_term(None)
1004 self
.treeAvailable
.grab_focus()
1006 def show_episode_search(self
, input_char
):
1007 self
.hbox_search_episodes
.show()
1008 self
.entry_search_episodes
.insert_text(input_char
, -1)
1009 self
.entry_search_episodes
.grab_focus()
1010 self
.entry_search_episodes
.set_position(-1)
1012 def init_episode_list_treeview(self
):
1013 # For loading the list model
1014 self
.episode_list_model
= EpisodeListModel(self
.on_episode_list_filter_changed
)
1016 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
1017 self
.item_view_episodes_undeleted
.set_active(True)
1018 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
1019 self
.item_view_episodes_downloaded
.set_active(True)
1020 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
1021 self
.item_view_episodes_unplayed
.set_active(True)
1023 self
.item_view_episodes_all
.set_active(True)
1025 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
1027 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
1029 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
1031 iconcell
= gtk
.CellRendererPixbuf()
1032 iconcell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1033 if gpodder
.ui
.maemo
:
1034 iconcell
.set_fixed_size(50, 50)
1036 iconcell
.set_fixed_size(40, -1)
1038 namecell
= gtk
.CellRendererText()
1039 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1040 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
1041 namecolumn
.pack_start(iconcell
, False)
1042 namecolumn
.add_attribute(iconcell
, 'icon-name', EpisodeListModel
.C_STATUS_ICON
)
1043 namecolumn
.pack_start(namecell
, True)
1044 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1045 if gpodder
.ui
.fremantle
:
1046 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
1048 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1049 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1050 namecolumn
.set_resizable(True)
1051 namecolumn
.set_expand(True)
1053 if gpodder
.ui
.fremantle
:
1054 from gpodder
.gtkui
.frmntl
import style
1055 timecell
= gtk
.CellRendererText()
1056 timecell
.set_property('font-desc', style
.get_font_desc('SmallSystemFont'))
1057 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1058 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1059 timecell
.set_property('xalign', 1.)
1060 timecell
.set_property('xpad', 5)
1061 timecell
.set_property('yalign', .85)
1062 namecolumn
.pack_start(timecell
, False)
1063 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1064 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME_VISIBLE
)
1066 lockcell
= gtk
.CellRendererPixbuf()
1067 lockcell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1068 if gpodder
.ui
.fremantle
:
1069 lockcell
.set_property('icon-name', 'general_locked')
1071 lockcell
.set_property('icon-name', 'emblem-readonly')
1073 namecolumn
.pack_start(lockcell
, False)
1074 namecolumn
.add_attribute(lockcell
, 'visible', EpisodeListModel
.C_LOCKED
)
1076 sizecell
= gtk
.CellRendererText()
1077 sizecell
.set_property('xalign', 1)
1078 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1079 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1081 releasecell
= gtk
.CellRendererText()
1082 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1083 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1085 namecolumn
.set_reorderable(True)
1086 self
.treeAvailable
.append_column(namecolumn
)
1088 if not gpodder
.ui
.maemo
:
1089 for itemcolumn
in (sizecolumn
, releasecolumn
):
1090 itemcolumn
.set_reorderable(True)
1091 self
.treeAvailable
.append_column(itemcolumn
)
1093 # Set up type-ahead find for the episode list
1094 def on_key_press(treeview
, event
):
1095 if event
.keyval
== gtk
.keysyms
.Escape
:
1096 self
.hide_episode_search()
1097 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1098 self
.hide_episode_search()
1099 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1100 # Don't handle type-ahead when control is pressed (so shortcuts
1101 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1104 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1105 if unicode_char_id
== 0:
1107 input_char
= unichr(unicode_char_id
)
1108 self
.show_episode_search(input_char
)
1110 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1112 if gpodder
.ui
.desktop
and not self
.config
.enable_fingerscroll
:
1113 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1114 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1115 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1116 if self
.config
.on_drag_mark_played
:
1117 for episode
in self
.get_selected_episodes():
1118 episode
.mark(is_played
=True)
1119 self
.on_selected_episodes_status_changed()
1120 uris
= ['file://'+e
.local_filename(create
=False) \
1121 for e
in self
.get_selected_episodes() \
1122 if e
.was_downloaded(and_exists
=True)]
1123 uris
.append('') # for the trailing '\r\n'
1124 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1125 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1127 selection
= self
.treeAvailable
.get_selection()
1128 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1129 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1130 elif gpodder
.ui
.fremantle
:
1131 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1133 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1134 # Update the sensitivity of the toolbar buttons on the Desktop
1135 selection
.connect('changed', lambda s
: self
.play_or_download())
1137 if gpodder
.ui
.diablo
:
1138 # Set up the tap-and-hold context menu for podcasts
1140 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1141 menu
.append(self
.itemEditChannel
.create_menu_item())
1142 menu
.append(gtk
.SeparatorMenuItem())
1143 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1144 menu
.append(gtk
.SeparatorMenuItem())
1145 item
= gtk
.ImageMenuItem(_('Close this menu'))
1146 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1147 gtk
.ICON_SIZE_MENU
))
1150 menu
= self
.set_finger_friendly(menu
)
1151 self
.treeChannels
.tap_and_hold_setup(menu
)
1154 def init_download_list_treeview(self
):
1155 # enable multiple selection support
1156 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1157 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1159 # columns and renderers for "download progress" tab
1160 # First column: [ICON] Episodename
1161 column
= gtk
.TreeViewColumn(_('Episode'))
1163 cell
= gtk
.CellRendererPixbuf()
1164 if gpodder
.ui
.maemo
:
1165 cell
.set_fixed_size(50, 50)
1166 cell
.set_property('stock-size', gtk
.ICON_SIZE_BUTTON
)
1167 column
.pack_start(cell
, expand
=False)
1168 column
.add_attribute(cell
, 'icon-name', \
1169 DownloadStatusModel
.C_ICON_NAME
)
1171 cell
= gtk
.CellRendererText()
1172 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1173 column
.pack_start(cell
, expand
=True)
1174 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1175 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1176 column
.set_expand(True)
1177 self
.treeDownloads
.append_column(column
)
1179 # Second column: Progress
1180 cell
= gtk
.CellRendererProgress()
1181 cell
.set_property('yalign', .5)
1182 cell
.set_property('ypad', 6)
1183 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1184 value
=DownloadStatusModel
.C_PROGRESS
, \
1185 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1186 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1187 column
.set_expand(False)
1188 self
.treeDownloads
.append_column(column
)
1189 if gpodder
.ui
.maemo
:
1190 column
.set_property('min-width', 200)
1191 column
.set_property('max-width', 200)
1193 column
.set_property('min-width', 150)
1194 column
.set_property('max-width', 150)
1196 self
.treeDownloads
.set_model(self
.download_status_model
)
1197 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1199 def on_treeview_expose_event(self
, treeview
, event
):
1200 if event
.window
== treeview
.get_bin_window():
1201 model
= treeview
.get_model()
1202 if (model
is not None and model
.get_iter_first() is not None):
1205 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1209 ctx
= event
.window
.cairo_create()
1210 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1211 event
.area
.width
, event
.area
.height
)
1214 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1217 if role
== TreeViewHelper
.ROLE_EPISODES
:
1218 if self
.currently_updating
:
1219 text
= _('Loading episodes')
1220 elif self
.config
.episode_list_view_mode
!= \
1221 EpisodeListModel
.VIEW_ALL
:
1222 text
= _('No episodes in current view')
1224 text
= _('No episodes available')
1225 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1226 if self
.config
.episode_list_view_mode
!= \
1227 EpisodeListModel
.VIEW_ALL
and \
1228 self
.config
.podcast_list_hide_boring
and \
1229 len(self
.channels
) > 0:
1230 text
= _('No podcasts in this view')
1232 text
= _('No subscriptions')
1233 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1234 text
= _('No active downloads')
1236 raise Exception('on_treeview_expose_event: unknown role')
1238 if gpodder
.ui
.fremantle
:
1239 from gpodder
.gtkui
.frmntl
import style
1240 font_desc
= style
.get_font_desc('LargeSystemFont')
1244 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1248 def enable_download_list_update(self
):
1249 if not self
.download_list_update_enabled
:
1250 self
.update_downloads_list()
1251 gobject
.timeout_add(1500, self
.update_downloads_list
)
1252 self
.download_list_update_enabled
= True
1254 def cleanup_downloads(self
):
1255 model
= self
.download_status_model
1257 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1258 changed_episode_urls
= set()
1259 for row_reference
, task
in all_tasks
:
1260 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1261 model
.remove(model
.get_iter(row_reference
.get_path()))
1263 # We don't "see" this task anymore - remove it;
1264 # this is needed, so update_episode_list_icons()
1265 # below gets the correct list of "seen" tasks
1266 self
.download_tasks_seen
.remove(task
)
1267 except KeyError, key_error
:
1268 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1269 changed_episode_urls
.add(task
.url
)
1270 # Tell the task that it has been removed (so it can clean up)
1271 task
.removed_from_list()
1273 # Tell the podcasts tab to update icons for our removed podcasts
1274 self
.update_episode_list_icons(changed_episode_urls
)
1276 # Tell the shownotes window that we have removed the episode
1277 if self
.episode_shownotes_window
is not None and \
1278 self
.episode_shownotes_window
.episode
is not None and \
1279 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1280 self
.episode_shownotes_window
._download
_status
_changed
(None)
1282 # Update the downloads list one more time
1283 self
.update_downloads_list(can_call_cleanup
=False)
1285 def on_tool_downloads_toggled(self
, toolbutton
):
1286 if toolbutton
.get_active():
1287 self
.wNotebook
.set_current_page(1)
1289 self
.wNotebook
.set_current_page(0)
1291 def add_download_task_monitor(self
, monitor
):
1292 self
.download_task_monitors
.add(monitor
)
1293 model
= self
.download_status_model
1297 task
= row
[self
.download_status_model
.C_TASK
]
1298 monitor
.task_updated(task
)
1300 def remove_download_task_monitor(self
, monitor
):
1301 self
.download_task_monitors
.remove(monitor
)
1303 def update_downloads_list(self
, can_call_cleanup
=True):
1305 model
= self
.download_status_model
1307 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1308 total_speed
, total_size
, done_size
= 0, 0, 0
1310 # Keep a list of all download tasks that we've seen
1311 download_tasks_seen
= set()
1313 # Remember the DownloadTask object for the episode that
1314 # has been opened in the episode shownotes dialog (if any)
1315 if self
.episode_shownotes_window
is not None:
1316 shownotes_episode
= self
.episode_shownotes_window
.episode
1317 shownotes_task
= None
1319 shownotes_episode
= None
1320 shownotes_task
= None
1322 # Do not go through the list of the model is not (yet) available
1326 failed_downloads
= []
1328 self
.download_status_model
.request_update(row
.iter)
1330 task
= row
[self
.download_status_model
.C_TASK
]
1331 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1333 # Let the download task monitors know of changes
1334 for monitor
in self
.download_task_monitors
:
1335 monitor
.task_updated(task
)
1338 done_size
+= size
*progress
1340 if shownotes_episode
is not None and \
1341 shownotes_episode
.url
== task
.episode
.url
:
1342 shownotes_task
= task
1344 download_tasks_seen
.add(task
)
1346 if status
== download
.DownloadTask
.DOWNLOADING
:
1348 total_speed
+= speed
1349 elif status
== download
.DownloadTask
.FAILED
:
1350 failed_downloads
.append(task
)
1352 elif status
== download
.DownloadTask
.DONE
:
1354 elif status
== download
.DownloadTask
.QUEUED
:
1356 elif status
== download
.DownloadTask
.PAUSED
:
1361 # Remember which tasks we have seen after this run
1362 self
.download_tasks_seen
= download_tasks_seen
1364 if gpodder
.ui
.desktop
:
1365 text
= [_('Downloads')]
1366 if downloading
+ failed
+ queued
> 0:
1369 s
.append(N_('%(count)d active', '%(count)d active', downloading
) % {'count':downloading
})
1371 s
.append(N_('%(count)d failed', '%(count)d failed', failed
) % {'count':failed
})
1373 s
.append(N_('%(count)d queued', '%(count)d queued', queued
) % {'count':queued
})
1374 text
.append(' (' + ', '.join(s
)+')')
1375 self
.labelDownloads
.set_text(''.join(text
))
1376 elif gpodder
.ui
.diablo
:
1377 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1379 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1381 self
.tool_downloads
.set_label(_('Downloads'))
1382 elif gpodder
.ui
.fremantle
:
1383 if downloading
+ queued
> 0:
1384 self
.button_downloads
.set_value(N_('%(count)d active', '%(count)d active', downloading
+queued
) % {'count':(downloading
+queued
)})
1386 self
.button_downloads
.set_value(N_('%(count)d failed', '%(count)d failed', failed
) % {'count':failed
})
1388 self
.button_downloads
.set_value(N_('%(count)d paused', '%(count)d paused', paused
) % {'count':paused
})
1390 self
.button_downloads
.set_value(_('Idle'))
1392 title
= [self
.default_title
]
1394 # We have to update all episodes/channels for which the status has
1395 # changed. Accessing task.status_changed has the side effect of
1396 # re-setting the changed flag, so we need to get the "changed" list
1397 # of tuples first and split it into two lists afterwards
1398 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1399 self
.download_tasks_seen
if task
.status_changed
]
1400 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1401 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1403 count
= downloading
+ queued
1405 title
.append(N_('downloading %(count)d file', 'downloading %(count)d files', count
) % {'count':count
})
1408 percentage
= 100.0*done_size
/total_size
1411 total_speed
= util
.format_filesize(total_speed
)
1412 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1413 if self
.tray_icon
is not None:
1414 # Update the tray icon status and progress bar
1415 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1416 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1418 if self
.tray_icon
is not None:
1419 # Update the tray icon status
1420 self
.tray_icon
.set_status()
1421 if gpodder
.ui
.desktop
:
1422 self
.downloads_finished(self
.download_tasks_seen
)
1423 if gpodder
.ui
.diablo
:
1424 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1425 log('All downloads have finished.', sender
=self
)
1426 if self
.config
.cmd_all_downloads_complete
:
1427 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1429 if gpodder
.ui
.fremantle
and failed
:
1430 message
= '\n'.join(['%s: %s' % (str(task
), \
1431 task
.error_message
) for task
in failed_downloads
])
1432 self
.show_message(message
, _('Downloads failed'), important
=True)
1434 # Remove finished episodes
1435 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1436 self
.cleanup_downloads()
1438 # Stop updating the download list here
1439 self
.download_list_update_enabled
= False
1441 if not gpodder
.ui
.fremantle
:
1442 self
.gPodder
.set_title(' - '.join(title
))
1444 self
.update_episode_list_icons(episode_urls
)
1445 if self
.episode_shownotes_window
is not None:
1446 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1447 shownotes_task
!= self
.episode_shownotes_window
.task
:
1448 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1449 self
.episode_shownotes_window
._download
_status
_progress
()
1450 self
.play_or_download()
1452 self
.update_podcast_list_model(channel_urls
)
1454 return self
.download_list_update_enabled
1455 except Exception, e
:
1456 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1457 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1458 # We return False here, so the update loop won't be called again,
1459 # that's why we require the restart of gPodder in the message.
1462 def on_config_changed(self
, *args
):
1463 util
.idle_add(self
._on
_config
_changed
, *args
)
1465 def _on_config_changed(self
, name
, old_value
, new_value
):
1466 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1467 self
.toolbar
.set_property('visible', new_value
)
1468 elif name
== 'videoplayer':
1469 self
.config
.video_played_dbus
= False
1470 elif name
== 'player':
1471 self
.config
.audio_played_dbus
= False
1472 elif name
== 'episode_list_descriptions':
1473 self
.update_episode_list_model()
1474 elif name
== 'episode_list_thumbnails':
1475 self
.update_episode_list_icons(all
=True)
1476 elif name
== 'rotation_mode':
1477 self
._fremantle
_rotation
.set_mode(new_value
)
1478 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1479 self
.restart_auto_update_timer()
1480 elif name
== 'podcast_list_view_all':
1481 # Force a update of the podcast list model
1482 self
.channel_list_changed
= True
1483 if gpodder
.ui
.fremantle
:
1484 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1485 while gtk
.events_pending():
1486 gtk
.main_iteration(False)
1487 self
.update_podcast_list_model()
1488 if gpodder
.ui
.fremantle
:
1489 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1491 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1492 # With get_bin_window, we get the window that contains the rows without
1493 # the header. The Y coordinate of this window will be the height of the
1494 # treeview header. This is the amount we have to subtract from the
1495 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1496 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1499 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1501 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or x
> 50 or (column
is not None and column
!= treeview
.get_columns()[0]):
1502 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1505 if path
is not None:
1506 model
= treeview
.get_model()
1507 iter = model
.get_iter(path
)
1508 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1510 if role
== TreeViewHelper
.ROLE_EPISODES
:
1511 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1512 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1513 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1515 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1516 if last_tooltip
is not None and last_tooltip
!= id:
1517 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1519 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1521 if role
== TreeViewHelper
.ROLE_EPISODES
:
1522 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1524 tooltip
.set_text(description
)
1527 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1528 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1531 channel
.request_save_dir_size()
1532 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1533 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1535 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1536 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1537 table
= gtk
.Table(rows
=3, columns
=3)
1538 table
.set_row_spacings(5)
1539 table
.set_col_spacings(5)
1540 table
.set_border_width(5)
1542 heading
= gtk
.Label()
1543 heading
.set_alignment(0, 1)
1544 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1545 table
.attach(heading
, 0, 1, 0, 1)
1546 size_info
= gtk
.Label()
1547 size_info
.set_alignment(1, 1)
1548 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1549 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1550 table
.attach(size_info
, 2, 3, 0, 1)
1552 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1554 if len(channel
.description
) < 500:
1555 description
= channel
.description
1557 pos
= channel
.description
.find('\n\n')
1558 if pos
== -1 or pos
> 500:
1559 description
= channel
.description
[:498]+'[...]'
1561 description
= channel
.description
[:pos
]
1563 description
= gtk
.Label(description
)
1565 description
.set_markup(error_str
)
1566 description
.set_alignment(0, 0)
1567 description
.set_line_wrap(True)
1568 table
.attach(description
, 0, 3, 2, 3)
1571 tooltip
.set_custom(table
)
1575 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1578 def treeview_allow_tooltips(self
, treeview
, allow
):
1579 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1581 def update_m3u_playlist_clicked(self
, widget
):
1582 if self
.active_channel
is not None:
1583 self
.active_channel
.update_m3u_playlist()
1584 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1586 def treeview_handle_context_menu_click(self
, treeview
, event
):
1587 x
, y
= int(event
.x
), int(event
.y
)
1588 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1590 selection
= treeview
.get_selection()
1591 model
, paths
= selection
.get_selected_rows()
1593 if path
is None or (path
not in paths
and \
1594 event
.button
== self
.context_menu_mouse_button
):
1595 # We have right-clicked, but not into the selection,
1596 # assume we don't want to operate on the selection
1599 if path
is not None and not paths
and \
1600 event
.button
== self
.context_menu_mouse_button
:
1601 # No selection or clicked outside selection;
1602 # select the single item where we clicked
1603 treeview
.grab_focus()
1604 treeview
.set_cursor(path
, column
, 0)
1608 # Unselect any remaining items (clicked elsewhere)
1609 if hasattr(treeview
, 'is_rubber_banding_active'):
1610 if not treeview
.is_rubber_banding_active():
1611 selection
.unselect_all()
1613 selection
.unselect_all()
1617 def downloads_list_get_selection(self
, model
=None, paths
=None):
1618 if model
is None and paths
is None:
1619 selection
= self
.treeDownloads
.get_selection()
1620 model
, paths
= selection
.get_selected_rows()
1622 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1623 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1624 model
.get_value(model
.get_iter(path
), \
1625 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1627 for row_reference
, task
in selected_tasks
:
1628 if task
.status
!= download
.DownloadTask
.QUEUED
:
1630 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1631 download
.DownloadTask
.FAILED
, \
1632 download
.DownloadTask
.CANCELLED
):
1634 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1635 download
.DownloadTask
.QUEUED
, \
1636 download
.DownloadTask
.DOWNLOADING
, \
1637 download
.DownloadTask
.FAILED
):
1639 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1640 download
.DownloadTask
.DOWNLOADING
):
1642 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1643 download
.DownloadTask
.FAILED
, \
1644 download
.DownloadTask
.DONE
):
1647 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1649 def downloads_finished(self
, download_tasks_seen
):
1650 # FIXME: Filter all tasks that have already been reported
1651 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1652 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1654 if finished_downloads
and failed_downloads
:
1655 message
= self
.format_episode_list(finished_downloads
, 5)
1656 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1657 message
+= self
.format_episode_list(failed_downloads
, 5)
1658 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1659 elif finished_downloads
:
1660 message
= self
.format_episode_list(finished_downloads
)
1661 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1662 elif failed_downloads
:
1663 message
= self
.format_episode_list(failed_downloads
)
1664 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1666 # Open torrent files right after download (bug 1029)
1667 if self
.config
.open_torrent_after_download
:
1668 for task
in download_tasks_seen
:
1669 if task
.status
!= task
.DONE
:
1672 episode
= task
.episode
1673 if episode
.mimetype
!= 'application/x-bittorrent':
1676 self
.playback_episodes([episode
])
1679 def format_episode_list(self
, episode_list
, max_episodes
=10):
1681 Format a list of episode names for notifications
1683 Will truncate long episode names and limit the amount of
1684 episodes displayed (max_episodes=10).
1686 The episode_list parameter should be a list of strings.
1688 MAX_TITLE_LENGTH
= 100
1691 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1692 if len(title
) > MAX_TITLE_LENGTH
:
1693 middle
= (MAX_TITLE_LENGTH
/2)-2
1694 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1695 result
.append(saxutils
.escape(title
))
1698 more_episodes
= len(episode_list
) - max_episodes
1699 if more_episodes
> 0:
1700 result
.append('(...')
1701 result
.append(N_('%(count)d more episode', '%(count)d more episodes', more_episodes
) % {'count':more_episodes
})
1702 result
.append('...)')
1704 return (''.join(result
)).strip()
1706 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1707 episode_urls
= set()
1708 model
= self
.treeDownloads
.get_model()
1709 for row_reference
, task
in tasks
:
1710 if status
== download
.DownloadTask
.QUEUED
:
1711 # Only queue task when its paused/failed/cancelled (or forced)
1712 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1713 self
.download_queue_manager
.add_task(task
, force_start
)
1714 self
.enable_download_list_update()
1715 elif status
== download
.DownloadTask
.CANCELLED
:
1716 # Cancelling a download allowed when downloading/queued
1717 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1718 task
.status
= status
1719 # Cancelling paused/failed downloads requires a call to .run()
1720 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1721 task
.status
= status
1722 # Call run, so the partial file gets deleted
1724 elif status
== download
.DownloadTask
.PAUSED
:
1725 # Pausing a download only when queued/downloading
1726 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1727 task
.status
= status
1728 elif status
is None:
1729 # Remove the selected task - cancel downloading/queued tasks
1730 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1731 task
.status
= task
.CANCELLED
1732 model
.remove(model
.get_iter(row_reference
.get_path()))
1733 # Remember the URL, so we can tell the UI to update
1735 # We don't "see" this task anymore - remove it;
1736 # this is needed, so update_episode_list_icons()
1737 # below gets the correct list of "seen" tasks
1738 self
.download_tasks_seen
.remove(task
)
1739 except KeyError, key_error
:
1740 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1741 episode_urls
.add(task
.url
)
1742 # Tell the task that it has been removed (so it can clean up)
1743 task
.removed_from_list()
1745 # We can (hopefully) simply set the task status here
1746 task
.status
= status
1747 # Tell the podcasts tab to update icons for our removed podcasts
1748 self
.update_episode_list_icons(episode_urls
)
1749 # Update the tab title and downloads list
1750 self
.update_downloads_list()
1752 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1753 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1755 if not hasattr(treeview
, 'is_rubber_banding_active'):
1758 return not treeview
.is_rubber_banding_active()
1760 if event
.button
== self
.context_menu_mouse_button
:
1761 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1762 self
.downloads_list_get_selection(model
, paths
)
1764 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1765 # This creates a menu item for selection-wide actions
1766 item
= gtk
.ImageMenuItem(label
)
1767 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1768 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1769 item
.set_sensitive(sensitive
)
1770 return self
.set_finger_friendly(item
)
1774 item
= gtk
.ImageMenuItem(_('Episode details'))
1775 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1776 if len(selected_tasks
) == 1:
1777 row_reference
, task
= selected_tasks
[0]
1778 episode
= task
.episode
1779 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1781 item
.set_sensitive(False)
1782 menu
.append(self
.set_finger_friendly(item
))
1783 menu
.append(gtk
.SeparatorMenuItem())
1785 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1787 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1788 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1789 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1790 menu
.append(gtk
.SeparatorMenuItem())
1791 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1793 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
1794 # Because we open the popup on left-click for Maemo,
1795 # we also include a non-action to close the menu
1796 menu
.append(gtk
.SeparatorMenuItem())
1797 item
= gtk
.ImageMenuItem(_('Close this menu'))
1798 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1800 menu
.append(self
.set_finger_friendly(item
))
1803 menu
.popup(None, None, None, event
.button
, event
.time
)
1806 def treeview_channels_show_context_menu(self
, treeview
, event
):
1807 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1811 # Check for valid channel id, if there's no id then
1812 # assume that it is a proxy channel or equivalent
1813 # and cannot be operated with right click
1814 if self
.active_channel
.id is None:
1817 if event
.button
== 3:
1822 item
= gtk
.ImageMenuItem( _('Update podcast'))
1823 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1824 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1825 item
.set_sensitive(not self
.updating_feed_cache
)
1828 menu
.append(gtk
.SeparatorMenuItem())
1830 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1831 item
.set_active(self
.active_channel
.channel_is_locked
)
1832 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1833 menu
.append(self
.set_finger_friendly(item
))
1835 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1836 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1837 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1840 if self
.config
.device_type
!= 'none':
1841 item
= gtk
.MenuItem(_('Synchronize to device'))
1842 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1845 menu
.append( gtk
.SeparatorMenuItem())
1847 item
= gtk
.ImageMenuItem(_('Podcast details'))
1848 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1849 item
.connect('activate', self
.on_itemEditChannel_activate
)
1853 # Disable tooltips while we are showing the menu, so
1854 # the tooltip will not appear over the menu
1855 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1856 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1857 menu
.popup( None, None, None, event
.button
, event
.time
)
1861 def on_itemClose_activate(self
, widget
):
1862 if self
.tray_icon
is not None:
1863 self
.iconify_main_window()
1865 self
.on_gPodder_delete_event(widget
)
1867 def cover_file_removed(self
, channel_url
):
1869 The Cover Downloader calls this when a previously-
1870 available cover has been removed from the disk. We
1871 have to update our model to reflect this change.
1873 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1875 def cover_download_finished(self
, channel
, pixbuf
):
1877 The Cover Downloader calls this when it has finished
1878 downloading (or registering, if already downloaded)
1879 a new channel cover, which is ready for displaying.
1881 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1883 def save_episodes_as_file(self
, episodes
):
1884 for episode
in episodes
:
1885 self
.save_episode_as_file(episode
)
1887 def save_episode_as_file(self
, episode
):
1888 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1889 if episode
.was_downloaded(and_exists
=True):
1890 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1891 copy_from
= episode
.local_filename(create
=False)
1892 assert copy_from
is not None
1893 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1894 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1895 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1897 def copy_episodes_bluetooth(self
, episodes
):
1898 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1900 if gpodder
.ui
.maemo
:
1901 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1902 for e
in episodes_to_copy
])
1905 def convert_and_send_thread(episode
):
1906 for episode
in episodes
:
1907 filename
= episode
.local_filename(create
=False)
1908 assert filename
is not None
1909 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1910 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1911 (base
, ext
) = os
.path
.splitext(filename
)
1912 if not destfile
.endswith(ext
):
1916 shutil
.copyfile(filename
, destfile
)
1917 util
.bluetooth_send_file(destfile
)
1919 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1920 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1922 util
.delete_file(destfile
)
1924 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1926 def get_device_name(self
):
1927 if self
.config
.device_type
== 'ipod':
1929 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1930 return _('MP3 player')
1932 return '(unknown device)'
1934 def _treeview_button_released(self
, treeview
, event
):
1935 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1936 dy
= int(abs(event
.y
-ypos
))
1937 dx
= int(event
.x
-xpos
)
1939 selection
= treeview
.get_selection()
1940 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1941 if path
is None or dy
> 30:
1942 return (False, dx
, dy
)
1944 path
, column
, x
, y
= path
1945 selection
.select_path(path
)
1946 treeview
.set_cursor(path
)
1947 treeview
.grab_focus()
1949 return (True, dx
, dy
)
1951 def treeview_channels_handle_gestures(self
, treeview
, event
):
1952 if self
.currently_updating
:
1955 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1958 if self
.config
.maemo_enable_gestures
:
1960 self
.on_itemUpdateChannel_activate()
1962 self
.on_itemEditChannel_activate(treeview
)
1966 def treeview_available_handle_gestures(self
, treeview
, event
):
1967 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1970 if self
.config
.maemo_enable_gestures
:
1972 self
.on_playback_selected_episodes(None)
1975 self
.on_shownotes_selected_episodes(None)
1978 # Pass the event to the context menu handler for treeAvailable
1979 self
.treeview_available_show_context_menu(treeview
, event
)
1983 def treeview_available_show_context_menu(self
, treeview
, event
):
1984 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1986 if not hasattr(treeview
, 'is_rubber_banding_active'):
1989 return not treeview
.is_rubber_banding_active()
1991 if event
.button
== self
.context_menu_mouse_button
:
1992 episodes
= self
.get_selected_episodes()
1993 any_locked
= any(e
.is_locked
for e
in episodes
)
1994 any_played
= any(e
.is_played
for e
in episodes
)
1995 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1996 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1997 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
2001 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
2003 if open_instead_of_play
:
2004 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
2006 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
2008 item
= gtk
.ImageMenuItem(_('Stream'))
2009 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
2011 item
.set_sensitive(can_play
and not downloading
)
2012 item
.connect('activate', self
.on_playback_selected_episodes
)
2013 menu
.append(self
.set_finger_friendly(item
))
2016 item
= gtk
.ImageMenuItem(_('Download'))
2017 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
2018 item
.set_sensitive(can_download
)
2019 item
.connect('activate', self
.on_download_selected_episodes
)
2020 menu
.append(self
.set_finger_friendly(item
))
2022 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
2023 item
.connect('activate', self
.on_item_cancel_download_activate
)
2024 menu
.append(self
.set_finger_friendly(item
))
2026 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
2027 item
.set_sensitive(can_delete
)
2028 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
2029 menu
.append(self
.set_finger_friendly(item
))
2033 # Ok, this probably makes sense to only display for downloaded files
2035 menu
.append(gtk
.SeparatorMenuItem())
2036 share_item
= gtk
.MenuItem(_('Send to'))
2037 menu
.append(self
.set_finger_friendly(share_item
))
2038 share_menu
= gtk
.Menu()
2040 item
= gtk
.ImageMenuItem(_('Local folder'))
2041 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
2042 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
2043 share_menu
.append(self
.set_finger_friendly(item
))
2044 if self
.bluetooth_available
:
2045 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
2046 if gpodder
.ui
.maemo
:
2047 icon_name
= ICON('qgn_list_filesys_bluetooth')
2049 icon_name
= ICON('bluetooth')
2050 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2051 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2052 share_menu
.append(self
.set_finger_friendly(item
))
2054 item
= gtk
.ImageMenuItem(self
.get_device_name())
2055 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2056 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2057 share_menu
.append(self
.set_finger_friendly(item
))
2059 share_item
.set_submenu(share_menu
)
2061 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2062 menu
.append(gtk
.SeparatorMenuItem())
2064 item
= gtk
.CheckMenuItem(_('New'))
2065 item
.set_active(True)
2066 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2067 menu
.append(self
.set_finger_friendly(item
))
2069 item
= gtk
.CheckMenuItem(_('New'))
2070 item
.set_active(False)
2071 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2072 menu
.append(self
.set_finger_friendly(item
))
2075 item
= gtk
.CheckMenuItem(_('Played'))
2076 item
.set_active(any_played
)
2077 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2078 menu
.append(self
.set_finger_friendly(item
))
2080 item
= gtk
.CheckMenuItem(_('Keep episode'))
2081 item
.set_active(any_locked
)
2082 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2083 menu
.append(self
.set_finger_friendly(item
))
2085 menu
.append(gtk
.SeparatorMenuItem())
2086 # Single item, add episode information menu item
2087 item
= gtk
.ImageMenuItem(_('Episode details'))
2088 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2089 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2090 menu
.append(self
.set_finger_friendly(item
))
2092 if gpodder
.ui
.maemo
or self
.config
.enable_fingerscroll
:
2093 # Because we open the popup on left-click for Maemo,
2094 # we also include a non-action to close the menu
2095 menu
.append(gtk
.SeparatorMenuItem())
2096 item
= gtk
.ImageMenuItem(_('Close this menu'))
2097 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2098 menu
.append(self
.set_finger_friendly(item
))
2101 # Disable tooltips while we are showing the menu, so
2102 # the tooltip will not appear over the menu
2103 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2104 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2105 menu
.popup( None, None, None, event
.button
, event
.time
)
2109 def set_title(self
, new_title
):
2110 if not gpodder
.ui
.fremantle
:
2111 self
.default_title
= new_title
2112 self
.gPodder
.set_title(new_title
)
2114 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2116 Updates the status icons in the episode list.
2118 If urls is given, it should be a list of URLs
2119 of episodes that should be updated.
2121 If urls is None, set ONE OF selected, all to
2122 True (the former updates just the selected
2123 episodes and the latter updates all episodes).
2125 additional_args
= (self
.episode_is_downloading
, \
2126 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2127 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2129 if urls
is not None:
2130 # We have a list of URLs to walk through
2131 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2132 elif selected
and not all
:
2133 # We should update all selected episodes
2134 selection
= self
.treeAvailable
.get_selection()
2135 model
, paths
= selection
.get_selected_rows()
2136 for path
in reversed(paths
):
2137 iter = model
.get_iter(path
)
2138 self
.episode_list_model
.update_by_filter_iter(iter, \
2140 elif all
and not selected
:
2141 # We update all (even the filter-hidden) episodes
2142 self
.episode_list_model
.update_all(*additional_args
)
2144 # Wrong/invalid call - have to specify at least one parameter
2145 raise ValueError('Invalid call to update_episode_list_icons')
2147 def episode_list_status_changed(self
, episodes
):
2148 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2149 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2152 def clean_up_downloads(self
, delete_partial
=False):
2153 # Clean up temporary files left behind by old gPodder versions
2154 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2157 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2159 for tempfile
in temporary_files
:
2160 util
.delete_file(tempfile
)
2162 # Clean up empty download folders and abandoned download folders
2163 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2164 for ddir
in download_dirs
:
2165 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2166 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2167 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2168 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2169 shutil
.rmtree(ddir
, ignore_errors
=True)
2171 def streaming_possible(self
):
2172 if gpodder
.ui
.desktop
:
2173 # User has to have a media player set on the Desktop, or else we
2174 # would probably open the browser when giving a URL to xdg-open..
2175 return (self
.config
.player
and self
.config
.player
!= 'default')
2176 elif gpodder
.ui
.maemo
:
2177 # On Maemo, the default is to use the Nokia Media Player, which is
2178 # already able to deal with HTTP URLs the right way, so we
2179 # unconditionally enable streaming always on Maemo
2184 def playback_episodes_for_real(self
, episodes
):
2185 groups
= collections
.defaultdict(list)
2186 for episode
in episodes
:
2187 file_type
= episode
.file_type()
2188 if file_type
== 'video' and self
.config
.videoplayer
and \
2189 self
.config
.videoplayer
!= 'default':
2190 player
= self
.config
.videoplayer
2191 if gpodder
.ui
.diablo
:
2192 # Use the wrapper script if it's installed to crop 3GP YouTube
2193 # videos to fit the screen (looks much nicer than w/ black border)
2194 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2195 player
= 'gpodder-mplayer'
2196 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2197 player
= 'mplayer -fs %F'
2198 elif file_type
== 'audio' and self
.config
.player
and \
2199 self
.config
.player
!= 'default':
2200 player
= self
.config
.player
2204 if file_type
not in ('audio', 'video') or \
2205 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2206 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2207 # Mark episode as played in the database
2208 episode
.mark(is_played
=True)
2209 self
.mygpo_client
.on_playback([episode
])
2211 filename
= episode
.local_filename(create
=False)
2212 if filename
is None or not os
.path
.exists(filename
):
2213 filename
= episode
.url
2214 if youtube
.is_video_link(filename
):
2215 fmt_id
= self
.config
.youtube_preferred_fmt_id
2216 if gpodder
.ui
.fremantle
:
2218 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2220 # Determine the playback resume position - if the file
2221 # was played 100%, we simply start from the beginning
2222 resume_position
= episode
.current_position
2223 if resume_position
== episode
.total_time
:
2226 if gpodder
.ui
.fremantle
:
2227 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2229 # If Panucci is configured, use D-Bus on Maemo to call it
2230 if player
== 'panucci':
2232 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2233 PANUCCI_PATH
= '/panucciInterface'
2234 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2235 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2236 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2238 def on_reply(*args
):
2241 def error_handler(filename
, err
):
2242 log('Exception in D-Bus call: %s', str(err
), \
2245 # Fallback: use the command line client
2246 for command
in util
.format_desktop_command('panucci', \
2248 log('Executing: %s', repr(command
), sender
=self
)
2249 subprocess
.Popen(command
)
2251 on_error
= lambda err
: error_handler(filename
, err
)
2253 # This method only exists in Panucci > 0.9 ('new Panucci')
2254 i
.playback_from(filename
, resume_position
, \
2255 reply_handler
=on_reply
, error_handler
=on_error
)
2257 continue # This file was handled by the D-Bus call
2258 except Exception, e
:
2259 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2260 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2262 MEDIABOX_NAME
= 'de.pycage.mediabox'
2263 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2264 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2265 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2266 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2268 def on_reply(*args
):
2272 log('Exception in D-Bus call: %s', str(err
), \
2275 i
.load(filename
, '%s/x-unknown' % file_type
, \
2276 reply_handler
=on_reply
, error_handler
=on_error
)
2278 continue # This file was handled by the D-Bus call
2279 except Exception, e
:
2280 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2282 groups
[player
].append(filename
)
2284 # Open episodes with system default player
2285 if 'default' in groups
:
2286 # Special-casing for a single episode when the object is a PDF
2287 # file - this is needed on Maemo 5, so we only use gui_open()
2288 # for single PDF files, but still use the built-in media player
2289 # with an M3U file for single audio/video files. (The Maemo 5
2290 # media player behaves differently when opening a single-file
2291 # M3U playlist compared to opening the single file directly.)
2292 if len(groups
['default']) == 1:
2293 fn
= groups
['default'][0]
2294 # The list of extensions is taken from gui_open in util.py
2295 # where all special-cases of Maemo apps are listed
2296 for extension
in ('.pdf', '.jpg', '.jpeg', '.png'):
2297 if fn
.lower().endswith(extension
):
2299 groups
['default'] = []
2302 if gpodder
.ui
.maemo
and groups
['default']:
2303 # The Nokia Media Player app does not support receiving multiple
2304 # file names via D-Bus, so we simply place all file names into a
2305 # temporary M3U playlist and open that with the Media Player.
2306 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2310 return 'file://' + urllib
.quote(os
.path
.abspath(x
))
2313 util
.write_m3u_playlist(m3u_filename
, \
2314 map(to_url
, groups
['default']), \
2316 util
.gui_open(m3u_filename
)
2318 for filename
in groups
['default']:
2319 log('Opening with system default: %s', filename
, sender
=self
)
2320 util
.gui_open(filename
)
2321 del groups
['default']
2322 elif gpodder
.ui
.maemo
and groups
:
2323 # When on Maemo and not opening with default, show a notification
2324 # (no startup notification for Panucci / MPlayer yet...)
2325 if len(episodes
) == 1:
2326 text
= _('Opening %s') % episodes
[0].title
2328 count
= len(episodes
)
2329 text
= N_('Opening %(count)d episode', 'Opening %(count)d episodes', count
) % {'count':count
}
2331 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2333 def destroy_banner_later(banner
):
2336 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2338 # For each type now, go and create play commands
2339 for group
in groups
:
2340 for command
in util
.format_desktop_command(group
, groups
[group
]):
2341 log('Executing: %s', repr(command
), sender
=self
)
2342 subprocess
.Popen(command
)
2344 # Persist episode status changes to the database
2347 # Flush updated episode status
2348 self
.mygpo_client
.flush()
2350 def playback_episodes(self
, episodes
):
2351 # We need to create a list, because we run through it more than once
2352 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2353 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2356 self
.playback_episodes_for_real(episodes
)
2357 except Exception, e
:
2358 log('Error in playback!', sender
=self
, traceback
=True)
2359 if gpodder
.ui
.desktop
:
2360 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2361 _('Error opening player'), widget
=self
.toolPreferences
)
2363 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2365 channel_urls
= set()
2366 episode_urls
= set()
2367 for episode
in episodes
:
2368 channel_urls
.add(episode
.channel
.url
)
2369 episode_urls
.add(episode
.url
)
2370 self
.update_episode_list_icons(episode_urls
)
2371 self
.update_podcast_list_model(channel_urls
)
2373 def play_or_download(self
):
2374 if not gpodder
.ui
.fremantle
:
2375 if self
.wNotebook
.get_current_page() > 0:
2376 if gpodder
.ui
.desktop
:
2377 self
.toolCancel
.set_sensitive(True)
2380 if self
.currently_updating
:
2381 return (False, False, False, False, False, False)
2383 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2384 ( is_played
, is_locked
) = (False,)*2
2386 open_instead_of_play
= False
2388 selection
= self
.treeAvailable
.get_selection()
2389 if selection
.count_selected_rows() > 0:
2390 (model
, paths
) = selection
.get_selected_rows()
2394 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2395 except TypeError, te
:
2396 log('Invalid episode at path %s', str(path
), sender
=self
)
2399 if episode
.file_type() not in ('audio', 'video'):
2400 open_instead_of_play
= True
2402 if episode
.was_downloaded():
2403 can_play
= episode
.was_downloaded(and_exists
=True)
2404 is_played
= episode
.is_played
2405 is_locked
= episode
.is_locked
2409 if self
.episode_is_downloading(episode
):
2414 can_download
= can_download
and not can_cancel
2415 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2416 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2417 can_delete
= not can_cancel
2419 if gpodder
.ui
.desktop
:
2420 if open_instead_of_play
:
2421 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2423 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2424 self
.toolPlay
.set_sensitive( can_play
)
2425 self
.toolDownload
.set_sensitive( can_download
)
2426 self
.toolTransfer
.set_sensitive( can_transfer
)
2427 self
.toolCancel
.set_sensitive( can_cancel
)
2429 if not gpodder
.ui
.fremantle
:
2430 self
.item_cancel_download
.set_sensitive(can_cancel
)
2431 self
.itemDownloadSelected
.set_sensitive(can_download
)
2432 self
.itemOpenSelected
.set_sensitive(can_play
)
2433 self
.itemPlaySelected
.set_sensitive(can_play
)
2434 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2435 self
.item_toggle_played
.set_sensitive(can_play
)
2436 self
.item_toggle_lock
.set_sensitive(can_play
)
2437 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2438 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2440 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2442 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2443 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2445 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2446 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2448 def episode_new_status_changed(self
, urls
):
2449 self
.update_podcast_list_model()
2450 self
.update_episode_list_icons(urls
)
2452 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2453 """Update the podcast list treeview model
2455 If urls is given, it should list the URLs of each
2456 podcast that has to be updated in the list.
2458 If selected is True, only update the model contents
2459 for the currently-selected podcast - nothing more.
2461 The caller can optionally specify "select_url",
2462 which is the URL of the podcast that is to be
2463 selected in the list after the update is complete.
2464 This only works if the podcast list has to be
2465 reloaded; i.e. something has been added or removed
2466 since the last update of the podcast list).
2468 selection
= self
.treeChannels
.get_selection()
2469 model
, iter = selection
.get_selected()
2471 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2472 # Update "all episodes" view in any case (if enabled)
2473 self
.podcast_list_model
.update_first_row()
2476 # very cheap! only update selected channel
2477 if iter is not None:
2478 # If we have selected the "all episodes" view, we have
2479 # to update all channels for selected episodes:
2480 if self
.config
.podcast_list_view_all
and \
2481 self
.podcast_list_model
.iter_is_first_row(iter):
2482 urls
= self
.get_podcast_urls_from_selected_episodes()
2483 self
.podcast_list_model
.update_by_urls(urls
)
2485 # Otherwise just update the selected row (a podcast)
2486 self
.podcast_list_model
.update_by_filter_iter(iter)
2487 elif not self
.channel_list_changed
:
2488 # we can keep the model, but have to update some
2490 # still cheaper than reloading the whole list
2491 self
.podcast_list_model
.update_all()
2493 # ok, we got a bunch of urls to update
2494 self
.podcast_list_model
.update_by_urls(urls
)
2496 if model
and iter and select_url
is None:
2497 # Get the URL of the currently-selected podcast
2498 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2500 # Update the podcast list model with new channels
2501 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2504 selected_iter
= model
.get_iter_first()
2505 # Find the previously-selected URL in the new
2506 # model if we have an URL (else select first)
2507 if select_url
is not None:
2508 pos
= model
.get_iter_first()
2509 while pos
is not None:
2510 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2511 if url
== select_url
:
2514 pos
= model
.iter_next(pos
)
2516 if not gpodder
.ui
.maemo
:
2517 if selected_iter
is not None:
2518 selection
.select_iter(selected_iter
)
2519 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2521 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2522 self
.channel_list_changed
= False
2524 def episode_is_downloading(self
, episode
):
2525 """Returns True if the given episode is being downloaded at the moment"""
2529 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2531 def on_episode_list_filter_changed(self
, has_episodes
):
2532 if gpodder
.ui
.fremantle
:
2534 self
.episodes_window
.empty_label
.hide()
2535 self
.episodes_window
.pannablearea
.show()
2537 if self
.config
.episode_list_view_mode
!= \
2538 EpisodeListModel
.VIEW_ALL
:
2539 text
= _('No episodes in current view')
2541 text
= _('No episodes available')
2542 self
.episodes_window
.empty_label
.set_text(text
)
2543 self
.episodes_window
.pannablearea
.hide()
2544 self
.episodes_window
.empty_label
.show()
2546 def update_episode_list_model(self
):
2547 if self
.channels
and self
.active_channel
is not None:
2548 if gpodder
.ui
.fremantle
:
2549 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2551 self
.currently_updating
= True
2552 self
.episode_list_model
.clear()
2553 if gpodder
.ui
.fremantle
:
2554 self
.episodes_window
.pannablearea
.hide()
2555 self
.episodes_window
.empty_label
.set_text(_('Loading episodes'))
2556 self
.episodes_window
.empty_label
.show()
2559 additional_args
= (self
.episode_is_downloading
, \
2560 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2561 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2562 self
.episode_list_model
.replace_from_channel(self
.active_channel
, *additional_args
)
2564 self
.treeAvailable
.get_selection().unselect_all()
2565 self
.treeAvailable
.scroll_to_point(0, 0)
2567 self
.currently_updating
= False
2568 self
.play_or_download()
2570 if gpodder
.ui
.fremantle
:
2571 hildon
.hildon_gtk_window_set_progress_indicator(\
2572 self
.episodes_window
.main_window
, False)
2574 util
.idle_add(update
)
2576 self
.episode_list_model
.clear()
2578 @dbus.service
.method(gpodder
.dbus_interface
)
2579 def offer_new_episodes(self
, channels
=None):
2580 if gpodder
.ui
.fremantle
:
2581 # Assume that when this function is called that the
2582 # notification is not shown anymore (Maemo bug 11345)
2583 self
._fremantle
_notification
_visible
= False
2585 new_episodes
= self
.get_new_episodes(channels
)
2587 self
.new_episodes_show(new_episodes
)
2591 def add_podcast_list(self
, urls
, auth_tokens
=None):
2592 """Subscribe to a list of podcast given their URLs
2594 If auth_tokens is given, it should be a dictionary
2595 mapping URLs to (username, password) tuples."""
2597 if auth_tokens
is None:
2600 # Sort and split the URL list into five buckets
2601 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2602 for input_url
in urls
:
2603 url
= util
.normalize_feed_url(input_url
)
2605 # Fail this one because the URL is not valid
2606 failed
.append(input_url
)
2607 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2608 # A podcast already exists in the list for this URL
2609 existing
.append(url
)
2611 # This URL has survived the first round - queue for add
2613 if url
!= input_url
and input_url
in auth_tokens
:
2614 auth_tokens
[url
] = auth_tokens
[input_url
]
2619 progress
= ProgressIndicator(_('Adding podcasts'), \
2620 _('Please wait while episode information is downloaded.'), \
2621 parent
=self
.get_dialog_parent())
2623 def on_after_update():
2624 progress
.on_finished()
2625 # Report already-existing subscriptions to the user
2627 title
= _('Existing subscriptions skipped')
2628 message
= _('You are already subscribed to these podcasts:') \
2629 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2630 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2632 # Report subscriptions that require authentication
2636 title
= _('Podcast requires authentication')
2637 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2638 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2640 retry_podcasts
[url
] = auth_tokens
2642 # Stop asking the user for more login data
2645 error_messages
[url
] = _('Authentication failed')
2649 # If we have authentication data to retry, do so here
2651 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2653 # Report website redirections
2654 for url
in redirections
:
2655 title
= _('Website redirection detected')
2656 message
= _('The URL %(url)s redirects to %(target)s.') \
2657 + '\n\n' + _('Do you want to visit the website now?')
2658 message
= message
% {'url': url
, 'target': redirections
[url
]}
2659 if self
.show_confirmation(message
, title
):
2660 util
.open_website(url
)
2664 # Report failed subscriptions to the user
2666 title
= _('Could not add some podcasts')
2667 message
= _('Some podcasts could not be added to your list:') \
2668 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2669 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2670 self
.show_message(message
, title
, important
=True)
2672 # Upload subscription changes to gpodder.net
2673 self
.mygpo_client
.on_subscribe(worked
)
2675 # If at least one podcast has been added, save and update all
2676 if self
.channel_list_changed
:
2677 # Fix URLs if mygpo has rewritten them
2678 self
.rewrite_urls_mygpo()
2680 self
.save_channels_opml()
2682 # If only one podcast was added, select it after the update
2683 if len(worked
) == 1:
2688 # Update the list of subscribed podcasts
2689 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2690 self
.update_podcasts_tab()
2692 # Offer to download new episodes
2694 for podcast
in self
.channels
:
2695 if podcast
.url
in worked
:
2696 episodes
.extend(podcast
.get_all_episodes())
2699 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2701 self
.new_episodes_show(episodes
, \
2702 selected
=[e
.check_is_new() for e
in episodes
])
2706 # After the initial sorting and splitting, try all queued podcasts
2707 length
= len(queued
)
2708 for index
, url
in enumerate(queued
):
2709 progress
.on_progress(float(index
)/float(length
))
2710 progress
.on_message(url
)
2711 log('QUEUE RUNNER: %s', url
, sender
=self
)
2713 # The URL is valid and does not exist already - subscribe!
2714 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2715 authentication_tokens
=auth_tokens
.get(url
, None), \
2716 max_episodes
=self
.config
.max_episodes_per_feed
, \
2717 download_dir
=self
.config
.download_dir
, \
2718 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2719 mimetype_prefs
=self
.config
.mimetype_prefs
)
2722 username
, password
= util
.username_password_from_url(url
)
2723 except ValueError, ve
:
2724 username
, password
= (None, None)
2726 if username
is not None and channel
.username
is None and \
2727 password
is not None and channel
.password
is None:
2728 channel
.username
= username
2729 channel
.password
= password
2732 self
._update
_cover
(channel
)
2733 except feedcore
.AuthenticationRequired
:
2734 if url
in auth_tokens
:
2735 # Fail for wrong authentication data
2736 error_messages
[url
] = _('Authentication failed')
2739 # Queue for login dialog later
2742 except feedcore
.WifiLogin
, error
:
2743 redirections
[url
] = error
.data
2745 error_messages
[url
] = _('Redirection detected')
2747 except Exception, e
:
2748 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2749 error_messages
[url
] = str(e
)
2753 assert channel
is not None
2754 worked
.append(channel
.url
)
2755 self
.channels
.append(channel
)
2756 self
.channel_list_changed
= True
2757 util
.idle_add(on_after_update
)
2758 threading
.Thread(target
=thread_proc
).start()
2760 def save_channels_opml(self
):
2761 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2762 return exporter
.write(self
.channels
)
2764 def find_episode(self
, podcast_url
, episode_url
):
2765 """Find an episode given its podcast and episode URL
2767 The function will return a PodcastEpisode object if
2768 the episode is found, or None if it's not found.
2770 for podcast
in self
.channels
:
2771 if podcast_url
== podcast
.url
:
2772 for episode
in podcast
.get_all_episodes():
2773 if episode_url
== episode
.url
:
2778 def process_received_episode_actions(self
, updated_urls
):
2779 """Process/merge episode actions from gpodder.net
2781 This function will merge all changes received from
2782 the server to the local database and update the
2783 status of the affected episodes as necessary.
2785 indicator
= ProgressIndicator(_('Merging episode actions'), \
2786 _('Episode actions from gpodder.net are merged.'), \
2787 False, self
.get_dialog_parent())
2789 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2790 if action
.action
== 'play':
2791 episode
= self
.find_episode(action
.podcast_url
, \
2794 if episode
is not None:
2795 log('Play action for %s', episode
.url
, sender
=self
)
2796 episode
.mark(is_played
=True)
2798 if action
.timestamp
> episode
.current_position_updated
and \
2799 action
.position
is not None:
2800 log('Updating position for %s', episode
.url
, sender
=self
)
2801 episode
.current_position
= action
.position
2802 episode
.current_position_updated
= action
.timestamp
2805 log('Updating total time for %s', episode
.url
, sender
=self
)
2806 episode
.total_time
= action
.total
2809 elif action
.action
== 'delete':
2810 episode
= self
.find_episode(action
.podcast_url
, \
2813 if episode
is not None:
2814 if not episode
.was_downloaded(and_exists
=True):
2815 # Set the episode to a "deleted" state
2816 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2817 episode
.delete_from_disk()
2820 indicator
.on_message(N_('%(count)d action processed', '%(count)d actions processed', idx
) % {'count':idx
})
2821 gtk
.main_iteration(False)
2823 indicator
.on_finished()
2827 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2829 self
.updating_feed_cache
= False
2831 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2833 # Process received episode actions for all updated URLs
2834 self
.process_received_episode_actions(updated_urls
)
2836 self
.channel_list_changed
= True
2837 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2839 # Only search for new episodes in podcasts that have been
2840 # updated, not in other podcasts (for single-feed updates)
2841 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2843 if gpodder
.ui
.fremantle
:
2844 self
.fancy_progress_bar
.hide()
2845 self
.button_subscribe
.set_sensitive(True)
2846 self
.button_refresh
.set_sensitive(True)
2847 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2848 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2849 self
.update_podcasts_tab()
2850 self
.update_episode_list_model()
2851 if self
.feed_cache_update_cancelled
:
2854 def application_in_foreground():
2856 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2857 except Exception, e
:
2858 log('Could not determine is-topmost', traceback
=True)
2859 # When in doubt, assume not in foreground
2863 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2864 # New episodes found, but we should do nothing
2865 self
.show_message(_('New episodes are available.'))
2866 elif self
.config
.auto_download
== 'always':
2867 count
= len(episodes
)
2868 title
= N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count
) % {'count':count
}
2869 self
.show_message(title
)
2870 self
.download_episode_list(episodes
)
2871 elif self
.config
.auto_download
== 'queue':
2872 self
.show_message(_('New episodes have been added to the download list.'))
2873 self
.download_episode_list_paused(episodes
)
2874 elif application_in_foreground():
2875 if not self
._fremantle
_notification
_visible
:
2876 self
.new_episodes_show(episodes
)
2877 elif not self
._fremantle
_notification
_visible
:
2880 pynotify
.init('gPodder')
2881 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2882 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2883 n
.set_hint('dbus-callback-default', ' '.join([
2884 gpodder
.dbus_bus_name
,
2885 gpodder
.dbus_gui_object_path
,
2886 gpodder
.dbus_interface
,
2887 'offer_new_episodes',
2889 n
.set_category('gpodder-new-episodes')
2891 self
._fremantle
_notification
_visible
= True
2892 except Exception, e
:
2893 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2894 self
.new_episodes_show(episodes
)
2895 self
._fremantle
_notification
_visible
= False
2896 elif not self
.config
.auto_update_feeds
:
2897 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2901 self
.tray_icon
.set_status()
2903 if self
.feed_cache_update_cancelled
:
2904 # The user decided to abort the feed update
2905 self
.show_update_feeds_buttons()
2907 # Nothing new here - but inform the user
2908 self
.pbFeedUpdate
.set_fraction(1.0)
2909 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2910 self
.feed_cache_update_cancelled
= True
2911 self
.btnCancelFeedUpdate
.show()
2912 self
.btnCancelFeedUpdate
.set_sensitive(True)
2913 self
.itemUpdate
.set_sensitive(True)
2914 if gpodder
.ui
.maemo
:
2915 # btnCancelFeedUpdate is a ToolButton on Maemo
2916 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2918 # btnCancelFeedUpdate is a normal gtk.Button
2919 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2921 count
= len(episodes
)
2922 # New episodes are available
2923 self
.pbFeedUpdate
.set_fraction(1.0)
2924 # Are we minimized and should we auto download?
2925 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2926 self
.download_episode_list(episodes
)
2927 title
= N_('Downloading %(count)d new episode.', 'Downloading %(count)d new episodes.', count
) % {'count':count
}
2928 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2929 self
.show_update_feeds_buttons()
2930 elif self
.config
.auto_download
== 'queue':
2931 self
.download_episode_list_paused(episodes
)
2932 title
= N_('%(count)d new episode added to download list.', '%(count)d new episodes added to download list.', count
) % {'count':count
}
2933 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2934 self
.show_update_feeds_buttons()
2936 self
.show_update_feeds_buttons()
2937 # New episodes are available and we are not minimized
2938 if not self
.config
.do_not_show_new_episodes_dialog
:
2939 self
.new_episodes_show(episodes
, notification
=True)
2941 message
= N_('%(count)d new episode available', '%(count)d new episodes available', count
) % {'count':count
}
2942 self
.pbFeedUpdate
.set_text(message
)
2944 def _update_cover(self
, channel
):
2945 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2946 self
.cover_downloader
.request_cover(channel
)
2948 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2949 total
= len(channels
)
2951 for updated
, channel
in enumerate(channels
):
2952 if not self
.feed_cache_update_cancelled
:
2954 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2955 mimetype_prefs
=self
.config
.mimetype_prefs
)
2956 self
._update
_cover
(channel
)
2957 except Exception, e
:
2958 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2960 message
= _('Error while updating %(url)s: %(message)s')
2962 message
= _('The feed at %(url)s could not be updated.')
2963 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2964 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2966 if self
.feed_cache_update_cancelled
:
2969 # By the time we get here the update may have already been cancelled
2970 if not self
.feed_cache_update_cancelled
:
2971 def update_progress():
2972 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2973 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2974 self
.pbFeedUpdate
.set_text(progression
)
2976 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2977 self
.pbFeedUpdate
.set_fraction(float(updated
+1)/float(total
))
2978 util
.idle_add(update_progress
)
2980 updated_urls
= [c
.url
for c
in channels
]
2981 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2983 def show_update_feeds_buttons(self
):
2984 # Make sure that the buttons for updating feeds
2985 # appear - this should happen after a feed update
2986 if gpodder
.ui
.maemo
:
2987 self
.btnUpdateSelectedFeed
.show()
2988 self
.toolFeedUpdateProgress
.hide()
2989 self
.btnCancelFeedUpdate
.hide()
2990 self
.btnCancelFeedUpdate
.set_is_important(False)
2991 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2992 self
.toolbarSpacer
.set_expand(True)
2993 self
.toolbarSpacer
.set_draw(False)
2995 self
.hboxUpdateFeeds
.hide()
2996 self
.btnUpdateFeeds
.show()
2997 self
.itemUpdate
.set_sensitive(True)
2998 self
.itemUpdateChannel
.set_sensitive(True)
3000 def on_btnCancelFeedUpdate_clicked(self
, widget
):
3001 if not self
.feed_cache_update_cancelled
:
3002 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
3003 self
.feed_cache_update_cancelled
= True
3004 if not gpodder
.ui
.fremantle
:
3005 self
.btnCancelFeedUpdate
.set_sensitive(False)
3006 elif not gpodder
.ui
.fremantle
:
3007 self
.show_update_feeds_buttons()
3009 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
3010 if self
.updating_feed_cache
:
3011 if gpodder
.ui
.fremantle
:
3012 self
.feed_cache_update_cancelled
= True
3015 if not force_update
:
3016 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
3017 self
.channel_list_changed
= True
3018 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
3021 # Fix URLs if mygpo has rewritten them
3022 self
.rewrite_urls_mygpo()
3024 self
.updating_feed_cache
= True
3026 if channels
is None:
3027 # Only update podcasts for which updates are enabled
3028 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
3030 if gpodder
.ui
.fremantle
:
3031 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
3032 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
3033 self
.fancy_progress_bar
.show()
3034 self
.button_subscribe
.set_sensitive(False)
3035 self
.button_refresh
.set_sensitive(False)
3036 self
.feed_cache_update_cancelled
= False
3038 self
.itemUpdate
.set_sensitive(False)
3039 self
.itemUpdateChannel
.set_sensitive(False)
3042 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
3044 self
.feed_cache_update_cancelled
= False
3045 self
.btnCancelFeedUpdate
.show()
3046 self
.btnCancelFeedUpdate
.set_sensitive(True)
3047 if gpodder
.ui
.maemo
:
3048 self
.toolbarSpacer
.set_expand(False)
3049 self
.toolbarSpacer
.set_draw(True)
3050 self
.btnUpdateSelectedFeed
.hide()
3051 self
.toolFeedUpdateProgress
.show_all()
3053 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
3054 self
.hboxUpdateFeeds
.show_all()
3055 self
.btnUpdateFeeds
.hide()
3057 if len(channels
) == 1:
3058 text
= _('Updating "%s"...') % channels
[0].title
3060 count
= len(channels
)
3061 text
= N_('Updating %(count)d feed...', 'Updating %(count)d feeds...', count
) % {'count':count
}
3062 self
.pbFeedUpdate
.set_text(text
)
3063 self
.pbFeedUpdate
.set_fraction(0)
3065 args
= (channels
, select_url_afterwards
)
3066 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
3068 def on_gPodder_delete_event(self
, widget
, *args
):
3069 """Called when the GUI wants to close the window
3070 Displays a confirmation dialog (and closes/hides gPodder)
3073 downloading
= self
.download_status_model
.are_downloads_in_progress()
3075 # Only iconify if we are using the window's "X" button,
3076 # but not when we are using "Quit" in the menu or toolbar
3077 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
3078 self
.iconify_main_window()
3080 if gpodder
.ui
.fremantle
:
3081 self
.close_gpodder()
3082 elif gpodder
.ui
.diablo
:
3083 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
3085 self
.close_gpodder()
3088 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
3089 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3090 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
3092 title
= _('Quit gPodder')
3093 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3095 dialog
.set_title(title
)
3096 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3098 quit_button
.grab_focus()
3099 result
= dialog
.run()
3102 if result
== gtk
.RESPONSE_CLOSE
:
3103 self
.close_gpodder()
3105 self
.close_gpodder()
3109 def close_gpodder(self
):
3110 """ clean everything and exit properly
3113 if self
.save_channels_opml():
3114 pass # FIXME: Add mygpo synchronization here
3116 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3120 if self
.tray_icon
is not None:
3121 self
.tray_icon
.set_visible(False)
3123 # Notify all tasks to to carry out any clean-up actions
3124 self
.download_status_model
.tell_all_tasks_to_quit()
3126 while gtk
.events_pending():
3127 gtk
.main_iteration(False)
3134 def get_expired_episodes(self
):
3135 for channel
in self
.channels
:
3136 for episode
in channel
.get_downloaded_episodes():
3137 # Never consider locked episodes as old
3138 if episode
.is_locked
:
3141 # Never consider fresh episodes as old
3142 if episode
.age_in_days() < self
.config
.episode_old_age
:
3145 # Do not delete played episodes (except if configured)
3146 if episode
.is_played
:
3147 if not self
.config
.auto_remove_played_episodes
:
3150 # Do not delete unplayed episodes (except if configured)
3151 if not episode
.is_played
:
3152 if not self
.config
.auto_remove_unplayed_episodes
:
3157 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3162 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3165 title
= _('Episodes are locked')
3166 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3167 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3170 count
= len(episodes
)
3171 title
= N_('Delete %(count)d episode?', 'Delete %(count)d episodes?', count
) % {'count':count
}
3172 message
= _('Deleting episodes removes downloaded files.')
3174 if gpodder
.ui
.fremantle
:
3175 message
= '\n'.join([title
, message
])
3177 if confirm
and not self
.show_confirmation(message
, title
):
3180 progress
= ProgressIndicator(_('Deleting episodes'), \
3181 _('Please wait while episodes are deleted'), \
3182 parent
=self
.get_dialog_parent())
3184 def finish_deletion(episode_urls
, channel_urls
):
3185 progress
.on_finished()
3187 # Episodes have been deleted - persist the database
3190 self
.update_episode_list_icons(episode_urls
)
3191 self
.update_podcast_list_model(channel_urls
)
3192 self
.play_or_download()
3195 episode_urls
= set()
3196 channel_urls
= set()
3198 episodes_status_update
= []
3199 for idx
, episode
in enumerate(episodes
):
3200 progress
.on_progress(float(idx
)/float(len(episodes
)))
3201 if episode
.is_locked
and skip_locked
:
3202 log('Not deleting episode (is locked): %s', episode
.title
)
3204 log('Deleting episode: %s', episode
.title
)
3205 progress
.on_message(episode
.title
)
3206 episode
.delete_from_disk()
3207 episode_urls
.add(episode
.url
)
3208 channel_urls
.add(episode
.channel
.url
)
3209 episodes_status_update
.append(episode
)
3211 # Tell the shownotes window that we have removed the episode
3212 if self
.episode_shownotes_window
is not None and \
3213 self
.episode_shownotes_window
.episode
is not None and \
3214 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3215 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3217 # Notify the web service about the status update + upload
3218 self
.mygpo_client
.on_delete(episodes_status_update
)
3219 self
.mygpo_client
.flush()
3221 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3223 threading
.Thread(target
=thread_proc
).start()
3227 def on_itemRemoveOldEpisodes_activate(self
, widget
):
3228 self
.show_delete_episodes_window()
3230 def show_delete_episodes_window(self
, channel
=None):
3231 """Offer deletion of episodes
3233 If channel is None, offer deletion of all episodes.
3234 Otherwise only offer deletion of episodes in the channel.
3236 if gpodder
.ui
.maemo
:
3238 ('maemo_remove_markup', None, None, _('Episode')),
3242 ('title_markup', None, None, _('Episode')),
3243 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3244 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3245 ('played_prop', None, None, _('Status')),
3246 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3249 msg_older_than
= N_('Select older than %(count)d day', 'Select older than %(count)d days', self
.config
.episode_old_age
)
3250 selection_buttons
= {
3251 _('Select played'): lambda episode
: episode
.is_played
,
3252 _('Select finished'): lambda episode
: episode
.is_finished(),
3253 msg_older_than
% {'count':self
.config
.episode_old_age
}: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3256 instructions
= _('Select the episodes you want to delete:')
3259 channels
= self
.channels
3261 channels
= [channel
]
3264 for channel
in channels
:
3265 for episode
in channel
.get_downloaded_episodes():
3266 # Disallow deletion of locked episodes that still exist
3267 if not episode
.is_locked
or not episode
.file_exists():
3268 episodes
.append(episode
)
3270 selected
= [e
.is_played
or not e
.file_exists() for e
in episodes
]
3272 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3273 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3274 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3275 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3276 show_episode_shownotes
=self
.show_episode_shownotes
)
3278 def on_selected_episodes_status_changed(self
):
3279 # The order of the updates here is important! When "All episodes" is
3280 # selected, the update of the podcast list model depends on the episode
3281 # list selection to determine which podcasts are affected. Updating
3282 # the episode list could remove the selection if a filter is active.
3283 self
.update_podcast_list_model(selected
=True)
3284 self
.update_episode_list_icons(selected
=True)
3287 def mark_selected_episodes_new(self
):
3288 for episode
in self
.get_selected_episodes():
3290 self
.on_selected_episodes_status_changed()
3292 def mark_selected_episodes_old(self
):
3293 for episode
in self
.get_selected_episodes():
3295 self
.on_selected_episodes_status_changed()
3297 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3298 for episode
in self
.get_selected_episodes():
3300 episode
.mark(is_played
=not episode
.is_played
)
3302 episode
.mark(is_played
=new_value
)
3303 self
.on_selected_episodes_status_changed()
3305 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3306 for episode
in self
.get_selected_episodes():
3308 episode
.mark(is_locked
=not episode
.is_locked
)
3310 episode
.mark(is_locked
=new_value
)
3311 self
.on_selected_episodes_status_changed()
3313 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3314 if self
.active_channel
is None:
3317 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3318 self
.active_channel
.update_channel_lock()
3320 for episode
in self
.active_channel
.get_all_episodes():
3321 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3323 self
.update_podcast_list_model(selected
=True)
3324 self
.update_episode_list_icons(all
=True)
3326 def on_itemUpdateChannel_activate(self
, widget
=None):
3327 if self
.active_channel
is None:
3328 title
= _('No podcast selected')
3329 message
= _('Please select a podcast in the podcasts list to update.')
3330 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3333 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3334 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3335 self
.update_feed_cache()
3337 self
.update_feed_cache(channels
=[self
.active_channel
])
3339 def on_itemUpdate_activate(self
, widget
=None):
3340 # Check if we have outstanding subscribe/unsubscribe actions
3341 if self
.on_add_remove_podcasts_mygpo():
3342 log('Update cancelled (received server changes)', sender
=self
)
3346 self
.update_feed_cache()
3348 gPodderWelcome(self
.gPodder
,
3349 center_on_widget
=self
.gPodder
,
3350 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3351 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3353 def download_episode_list_paused(self
, episodes
):
3354 self
.download_episode_list(episodes
, True)
3356 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3357 enable_update
= False
3359 for episode
in episodes
:
3360 log('Downloading episode: %s', episode
.title
, sender
= self
)
3361 if not episode
.was_downloaded(and_exists
=True):
3363 for task
in self
.download_tasks_seen
:
3364 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3365 self
.download_queue_manager
.add_task(task
, force_start
)
3366 enable_update
= True
3374 task
= download
.DownloadTask(episode
, self
.config
)
3375 except Exception, e
:
3376 d
= {'episode': episode
.title
, 'message': str(e
)}
3377 message
= _('Download error while downloading %(episode)s: %(message)s')
3378 self
.show_message(message
% d
, _('Download error'), important
=True)
3379 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3383 task
.status
= task
.PAUSED
3385 self
.mygpo_client
.on_download([task
.episode
])
3386 self
.download_queue_manager
.add_task(task
, force_start
)
3388 self
.download_status_model
.register_task(task
)
3389 enable_update
= True
3392 self
.enable_download_list_update()
3394 # Flush updated episode status
3395 self
.mygpo_client
.flush()
3397 def cancel_task_list(self
, tasks
):
3402 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3403 task
.status
= task
.CANCELLED
3404 elif task
.status
== task
.PAUSED
:
3405 task
.status
= task
.CANCELLED
3406 # Call run, so the partial file gets deleted
3409 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3410 self
.play_or_download()
3412 # Update the tab title and downloads list
3413 self
.update_downloads_list()
3415 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3416 if gpodder
.ui
.maemo
:
3418 ('maemo_markup', None, None, _('Episode')),
3420 show_notification
= notification
3423 ('title_markup', None, None, _('Episode')),
3424 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3425 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3427 show_notification
= False
3429 instructions
= _('Select the episodes you want to download:')
3431 if self
.new_episodes_window
is not None:
3432 self
.new_episodes_window
.main_window
.destroy()
3433 self
.new_episodes_window
= None
3435 def download_episodes_callback(episodes
):
3436 self
.new_episodes_window
= None
3437 self
.download_episode_list(episodes
)
3439 if selected
is None:
3440 # Select all by default
3441 selected
= [True]*len(episodes
)
3443 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3444 title
=_('New episodes available'), \
3445 instructions
=instructions
, \
3446 episodes
=episodes
, \
3448 selected
=selected
, \
3449 stock_ok_button
= 'gpodder-download', \
3450 callback
=download_episodes_callback
, \
3451 remove_callback
=lambda e
: e
.mark_old(), \
3452 remove_action
=_('Mark as old'), \
3453 remove_finished
=self
.episode_new_status_changed
, \
3454 _config
=self
.config
, \
3455 show_notification
=show_notification
, \
3456 show_episode_shownotes
=self
.show_episode_shownotes
)
3458 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3459 if not self
.offer_new_episodes():
3460 self
.show_message(_('Please check for new episodes later.'), \
3461 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3463 def get_new_episodes(self
, channels
=None):
3464 if channels
is None:
3465 channels
= self
.channels
3467 for channel
in channels
:
3468 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3469 episodes
.append(episode
)
3473 @dbus.service
.method(gpodder
.dbus_interface
)
3474 def start_device_synchronization(self
):
3475 """Public D-Bus API for starting Device sync (Desktop only)
3477 This method can be called to initiate a synchronization with
3478 a configured protable media player. This only works for the
3479 Desktop version of gPodder and does nothing on Maemo.
3481 if gpodder
.ui
.desktop
:
3482 self
.on_sync_to_ipod_activate(None)
3487 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3488 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3490 def commit_changes_to_database(self
):
3491 """This will be called after the sync process is finished"""
3494 def on_cleanup_ipod_activate(self
, widget
, *args
):
3495 self
.sync_ui
.on_cleanup_device()
3497 def on_manage_device_playlist(self
, widget
):
3498 self
.sync_ui
.on_manage_device_playlist()
3500 def show_hide_tray_icon(self
):
3501 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3502 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3503 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3504 self
.tray_icon
.set_visible(False)
3506 self
.tray_icon
= None
3508 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3509 self
.tray_icon
.set_visible(self
.is_iconified())
3510 elif self
.tray_icon
:
3511 self
.tray_icon
.set_visible(True)
3513 def on_itemShowAllEpisodes_activate(self
, widget
):
3514 self
.config
.podcast_list_view_all
= widget
.get_active()
3516 def on_itemShowToolbar_activate(self
, widget
):
3517 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3519 def on_itemShowDescription_activate(self
, widget
):
3520 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3522 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3523 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3524 if self
.config
.podcast_list_hide_boring
:
3525 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3527 self
.podcast_list_model
.set_view_mode(-1)
3529 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3531 if current
== self
.item_view_podcasts_all
:
3532 self
.podcast_list_model
.set_view_mode(-1)
3533 elif current
== self
.item_view_podcasts_downloaded
:
3534 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3535 elif current
== self
.item_view_podcasts_unplayed
:
3536 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3538 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3540 def on_item_view_episodes_changed(self
, radioaction
, current
):
3541 if current
== self
.item_view_episodes_all
:
3542 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_ALL
3543 elif current
== self
.item_view_episodes_undeleted
:
3544 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNDELETED
3545 elif current
== self
.item_view_episodes_downloaded
:
3546 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_DOWNLOADED
3547 elif current
== self
.item_view_episodes_unplayed
:
3548 self
.config
.episode_list_view_mode
= EpisodeListModel
.VIEW_UNPLAYED
3550 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3552 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3553 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3555 def update_item_device( self
):
3556 if not gpodder
.ui
.fremantle
:
3557 if self
.config
.device_type
!= 'none':
3558 self
.itemDevice
.set_visible(True)
3559 self
.itemDevice
.label
= self
.get_device_name()
3561 self
.itemDevice
.set_visible(False)
3563 def properties_closed( self
):
3564 self
.preferences_dialog
= None
3565 self
.show_hide_tray_icon()
3566 self
.update_item_device()
3567 if gpodder
.ui
.maemo
:
3568 selection
= self
.treeAvailable
.get_selection()
3569 if self
.config
.maemo_enable_gestures
or \
3570 self
.config
.enable_fingerscroll
:
3571 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3573 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3575 def on_itemPreferences_activate(self
, widget
, *args
):
3576 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3577 _config
=self
.config
, \
3578 callback_finished
=self
.properties_closed
, \
3579 user_apps_reader
=self
.user_apps_reader
, \
3580 parent_window
=self
.main_window
, \
3581 mygpo_client
=self
.mygpo_client
, \
3582 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3584 # Initial message to relayout window (in case it's opened in portrait mode
3585 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3587 def on_itemDependencies_activate(self
, widget
):
3588 gPodderDependencyManager(self
.gPodder
)
3590 def on_goto_mygpo(self
, widget
):
3591 self
.mygpo_client
.open_website()
3593 def on_download_subscriptions_from_mygpo(self
, action
=None):
3594 title
= _('Login to gpodder.net')
3595 message
= _('Please login to download your subscriptions.')
3596 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3597 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3601 self
.config
.mygpo_username
= username
3602 self
.config
.mygpo_password
= password
3604 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3605 custom_title
=_('Subscriptions on gpodder.net'), \
3606 add_urls_callback
=self
.add_podcast_list
, \
3607 hide_url_entry
=True)
3609 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3610 # we do not have to hardcode the URL here
3611 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3612 url
= util
.url_add_authentication(OPML_URL
, \
3613 self
.config
.mygpo_username
, \
3614 self
.config
.mygpo_password
)
3615 dir.download_opml_file(url
)
3617 def on_mygpo_settings_activate(self
, action
=None):
3618 # This dialog is only used for Maemo 4
3619 if not gpodder
.ui
.diablo
:
3622 settings
= MygPodderSettings(self
.main_window
, \
3623 config
=self
.config
, \
3624 mygpo_client
=self
.mygpo_client
, \
3625 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3627 def on_itemAddChannel_activate(self
, widget
=None):
3628 gPodderAddPodcast(self
.gPodder
, \
3629 add_urls_callback
=self
.add_podcast_list
)
3631 def on_itemEditChannel_activate(self
, widget
, *args
):
3632 if self
.active_channel
is None:
3633 title
= _('No podcast selected')
3634 message
= _('Please select a podcast in the podcasts list to edit.')
3635 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3638 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3639 gPodderChannel(self
.main_window
, \
3640 channel
=self
.active_channel
, \
3641 callback_closed
=callback_closed
, \
3642 cover_downloader
=self
.cover_downloader
)
3644 def on_itemMassUnsubscribe_activate(self
, item
=None):
3646 ('title', None, None, _('Podcast')),
3649 # We're abusing the Episode Selector for selecting Podcasts here,
3650 # but it works and looks good, so why not? -- thp
3651 gPodderEpisodeSelector(self
.main_window
, \
3652 title
=_('Remove podcasts'), \
3653 instructions
=_('Select the podcast you want to remove.'), \
3654 episodes
=self
.channels
, \
3656 size_attribute
=None, \
3657 stock_ok_button
=_('Remove'), \
3658 callback
=self
.remove_podcast_list
, \
3659 _config
=self
.config
)
3661 def remove_podcast_list(self
, channels
, confirm
=True):
3663 log('No podcasts selected for deletion', sender
=self
)
3666 if len(channels
) == 1:
3667 title
= _('Removing podcast')
3668 info
= _('Please wait while the podcast is removed')
3669 message
= _('Do you really want to remove this podcast and its episodes?')
3671 title
= _('Removing podcasts')
3672 info
= _('Please wait while the podcasts are removed')
3673 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3675 if confirm
and not self
.show_confirmation(message
, title
):
3678 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3680 def finish_deletion(select_url
):
3681 # Upload subscription list changes to the web service
3682 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3684 # Re-load the channels and select the desired new channel
3685 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3686 progress
.on_finished()
3687 self
.update_podcasts_tab()
3692 for idx
, channel
in enumerate(channels
):
3693 # Update the UI for correct status messages
3694 progress
.on_progress(float(idx
)/float(len(channels
)))
3695 progress
.on_message(channel
.title
)
3697 # Delete downloaded episodes
3698 channel
.remove_downloaded()
3700 # cancel any active downloads from this channel
3701 for episode
in channel
.get_all_episodes():
3702 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3705 if len(channels
) == 1:
3706 # get the URL of the podcast we want to select next
3707 if channel
in self
.channels
:
3708 position
= self
.channels
.index(channel
)
3712 if position
== len(self
.channels
)-1:
3713 # this is the last podcast, so select the URL
3714 # of the item before this one (i.e. the "new last")
3715 select_url
= self
.channels
[position
-1].url
3717 # there is a podcast after the deleted one, so
3718 # we simply select the one that comes after it
3719 select_url
= self
.channels
[position
+1].url
3721 # Remove the channel and clean the database entries
3723 self
.channels
.remove(channel
)
3725 # Clean up downloads and download directories
3726 self
.clean_up_downloads()
3728 self
.channel_list_changed
= True
3729 self
.save_channels_opml()
3731 # The remaining stuff is to be done in the GTK main thread
3732 util
.idle_add(finish_deletion
, select_url
)
3734 threading
.Thread(target
=thread_proc
).start()
3736 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3737 if self
.active_channel
is None:
3738 title
= _('No podcast selected')
3739 message
= _('Please select a podcast in the podcasts list to remove.')
3740 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3743 self
.remove_podcast_list([self
.active_channel
])
3745 def get_opml_filter(self
):
3746 filter = gtk
.FileFilter()
3747 filter.add_pattern('*.opml')
3748 filter.add_pattern('*.xml')
3749 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3752 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3753 if filename
is None:
3754 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3755 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), \
3756 parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3757 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3758 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3759 elif gpodder
.ui
.diablo
:
3760 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3761 dlg
.set_filter(self
.get_opml_filter())
3762 response
= dlg
.run()
3764 if response
== gtk
.RESPONSE_OK
:
3765 filename
= dlg
.get_filename()
3768 if filename
is not None:
3769 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3770 custom_title
=_('Import podcasts from OPML file'), \
3771 add_urls_callback
=self
.add_podcast_list
, \
3772 hide_url_entry
=True)
3773 dir.download_opml_file(filename
)
3775 def on_itemExportChannels_activate(self
, widget
, *args
):
3776 if not self
.channels
:
3777 title
= _('Nothing to export')
3778 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3779 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3782 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3783 # FIXME: Hildonization on Fremantle
3784 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3785 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3786 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3787 elif gpodder
.ui
.diablo
:
3788 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3789 dlg
.set_filter(self
.get_opml_filter())
3790 response
= dlg
.run()
3791 if response
== gtk
.RESPONSE_OK
:
3792 filename
= dlg
.get_filename()
3794 exporter
= opml
.Exporter( filename
)
3795 if exporter
.write(self
.channels
):
3796 count
= len(self
.channels
)
3797 title
= N_('%(count)d subscription exported', '%(count)d subscriptions exported', count
) % {'count':count
}
3798 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3800 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3804 def on_itemImportChannels_activate(self
, widget
, *args
):
3805 if gpodder
.ui
.fremantle
:
3806 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3807 self
.config
.toplist_url
, \
3808 self
.config
.opml_url
, \
3809 self
.add_podcast_list
, \
3810 self
.on_itemAddChannel_activate
, \
3811 self
.on_download_subscriptions_from_mygpo
, \
3812 self
.show_text_edit_dialog
)
3814 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3815 add_urls_callback
=self
.add_podcast_list
)
3816 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3818 def on_homepage_activate(self
, widget
, *args
):
3819 util
.open_website(gpodder
.__url
__)
3821 def on_wiki_activate(self
, widget
, *args
):
3822 util
.open_website('http://gpodder.org/wiki/User_Manual')
3824 def on_bug_tracker_activate(self
, widget
, *args
):
3825 if gpodder
.ui
.maemo
:
3826 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3828 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3830 def on_item_support_activate(self
, widget
):
3831 util
.open_website('http://gpodder.org/donate')
3833 def on_itemAbout_activate(self
, widget
, *args
):
3834 if gpodder
.ui
.fremantle
:
3835 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3836 HeAboutDialog
.present(self
.main_window
,
3839 gpodder
.__version
__,
3840 _('A podcast client with focus on usability'),
3841 gpodder
.__copyright
__,
3843 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3844 'http://gpodder.org/donate')
3847 dlg
= gtk
.AboutDialog()
3848 dlg
.set_transient_for(self
.main_window
)
3849 dlg
.set_name('gPodder')
3850 dlg
.set_version(gpodder
.__version
__)
3851 dlg
.set_copyright(gpodder
.__copyright
__)
3852 dlg
.set_comments(_('A podcast client with focus on usability'))
3853 dlg
.set_website(gpodder
.__url
__)
3854 dlg
.set_translator_credits( _('translator-credits'))
3855 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3857 if gpodder
.ui
.desktop
:
3858 # For the "GUI" version, we add some more
3859 # items to the about dialog (credits and logo)
3862 'Thomas Perl <thp.io>',
3865 if os
.path
.exists(gpodder
.credits_file
):
3866 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3867 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3868 app_authors
+= credits
3870 dlg
.set_authors(app_authors
)
3872 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3874 dlg
.set_logo_icon_name('gpodder')
3878 def on_wNotebook_switch_page(self
, widget
, *args
):
3880 if gpodder
.ui
.maemo
:
3881 self
.tool_downloads
.set_active(page_num
== 1)
3882 page
= self
.wNotebook
.get_nth_page(page_num
)
3883 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3884 if page_num
== 0 and self
.active_channel
is not None:
3885 self
.set_title(self
.active_channel
.title
)
3887 self
.set_title(tab_label
)
3889 self
.play_or_download()
3890 self
.menuChannels
.set_sensitive(True)
3891 self
.menuSubscriptions
.set_sensitive(True)
3892 # The message area in the downloads tab should be hidden
3893 # when the user switches away from the downloads tab
3894 if self
.message_area
is not None:
3895 self
.message_area
.hide()
3896 self
.message_area
= None
3898 self
.menuChannels
.set_sensitive(False)
3899 self
.menuSubscriptions
.set_sensitive(False)
3900 if gpodder
.ui
.desktop
:
3901 self
.toolDownload
.set_sensitive(False)
3902 self
.toolPlay
.set_sensitive(False)
3903 self
.toolTransfer
.set_sensitive(False)
3904 self
.toolCancel
.set_sensitive(False)
3906 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3907 # double-click action of the podcast list or enter
3908 self
.treeChannels
.set_cursor(path
)
3910 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3911 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3913 if model
is not None and iter is not None:
3914 old_active_channel
= self
.active_channel
3915 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3917 if self
.active_channel
== old_active_channel
:
3920 if gpodder
.ui
.maemo
:
3921 self
.set_title(self
.active_channel
.title
)
3923 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3924 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3925 self
.itemEditChannel
.set_visible(False)
3926 self
.itemRemoveChannel
.set_visible(False)
3928 self
.itemEditChannel
.set_visible(True)
3929 self
.itemRemoveChannel
.set_visible(True)
3931 self
.active_channel
= None
3932 self
.itemEditChannel
.set_visible(False)
3933 self
.itemRemoveChannel
.set_visible(False)
3935 self
.update_episode_list_model()
3937 def on_btnEditChannel_clicked(self
, widget
, *args
):
3938 self
.on_itemEditChannel_activate( widget
, args
)
3940 def get_podcast_urls_from_selected_episodes(self
):
3941 """Get a set of podcast URLs based on the selected episodes"""
3942 return set(episode
.channel
.url
for episode
in \
3943 self
.get_selected_episodes())
3945 def get_selected_episodes(self
):
3946 """Get a list of selected episodes from treeAvailable"""
3947 selection
= self
.treeAvailable
.get_selection()
3948 model
, paths
= selection
.get_selected_rows()
3950 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3953 def on_transfer_selected_episodes(self
, widget
):
3954 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3956 def on_playback_selected_episodes(self
, widget
):
3957 self
.playback_episodes(self
.get_selected_episodes())
3959 def on_shownotes_selected_episodes(self
, widget
):
3960 episodes
= self
.get_selected_episodes()
3962 episode
= episodes
.pop(0)
3963 self
.show_episode_shownotes(episode
)
3965 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3967 def on_download_selected_episodes(self
, widget
):
3968 episodes
= self
.get_selected_episodes()
3969 self
.download_episode_list(episodes
)
3970 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3971 self
.play_or_download()
3973 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3974 """Double-click/enter action handler for treeAvailable"""
3975 # We should only have one one selected as it was double clicked!
3976 e
= self
.get_selected_episodes()[0]
3978 if (self
.config
.double_click_episode_action
== 'download'):
3979 # If the episode has already been downloaded and exists then play it
3980 if e
.was_downloaded(and_exists
=True):
3981 self
.playback_episodes(self
.get_selected_episodes())
3982 # else download it if it is not already downloading
3983 elif not self
.episode_is_downloading(e
):
3984 self
.download_episode_list([e
])
3985 self
.update_episode_list_icons([e
.url
])
3986 self
.play_or_download()
3987 elif (self
.config
.double_click_episode_action
== 'stream'):
3988 # If we happen to have downloaded this episode simple play it
3989 if e
.was_downloaded(and_exists
=True):
3990 self
.playback_episodes(self
.get_selected_episodes())
3991 # else if streaming is possible stream it
3992 elif self
.streaming_possible():
3993 self
.playback_episodes(self
.get_selected_episodes())
3995 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3996 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3998 # default action is to display show notes
3999 self
.on_shownotes_selected_episodes(widget
)
4001 def show_episode_shownotes(self
, episode
):
4002 if self
.episode_shownotes_window
is None:
4003 log('First-time use of episode window --- creating', sender
=self
)
4004 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
4005 _download_episode_list
=self
.download_episode_list
, \
4006 _playback_episodes
=self
.playback_episodes
, \
4007 _delete_episode_list
=self
.delete_episode_list
, \
4008 _episode_list_status_changed
=self
.episode_list_status_changed
, \
4009 _cancel_task_list
=self
.cancel_task_list
, \
4010 _episode_is_downloading
=self
.episode_is_downloading
, \
4011 _streaming_possible
=self
.streaming_possible())
4012 self
.episode_shownotes_window
.show(episode
)
4013 if self
.episode_is_downloading(episode
):
4014 self
.update_downloads_list()
4016 def restart_auto_update_timer(self
):
4017 if self
._auto
_update
_timer
_source
_id
is not None:
4018 log('Removing existing auto update timer.', sender
=self
)
4019 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
4020 self
._auto
_update
_timer
_source
_id
= None
4022 if self
.config
.auto_update_feeds
and \
4023 self
.config
.auto_update_frequency
:
4024 interval
= 60*1000*self
.config
.auto_update_frequency
4025 log('Setting up auto update timer with interval %d.', \
4026 self
.config
.auto_update_frequency
, sender
=self
)
4027 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
4028 interval
, self
._on
_auto
_update
_timer
)
4030 def _on_auto_update_timer(self
):
4031 log('Auto update timer fired.', sender
=self
)
4032 self
.update_feed_cache(force_update
=True)
4034 # Ask web service for sub changes (if enabled)
4035 self
.mygpo_client
.flush()
4039 def on_treeDownloads_row_activated(self
, widget
, *args
):
4040 # Use the standard way of working on the treeview
4041 selection
= self
.treeDownloads
.get_selection()
4042 (model
, paths
) = selection
.get_selected_rows()
4043 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
4045 for tree_row_reference
, task
in selected_tasks
:
4046 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
4047 task
.status
= task
.PAUSED
4048 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
4049 self
.download_queue_manager
.add_task(task
)
4050 self
.enable_download_list_update()
4051 elif task
.status
== task
.DONE
:
4052 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
4054 self
.play_or_download()
4056 # Update the tab title and downloads list
4057 self
.update_downloads_list()
4059 def on_item_cancel_download_activate(self
, widget
):
4060 if self
.wNotebook
.get_current_page() == 0:
4061 selection
= self
.treeAvailable
.get_selection()
4062 (model
, paths
) = selection
.get_selected_rows()
4063 urls
= [model
.get_value(model
.get_iter(path
), \
4064 self
.episode_list_model
.C_URL
) for path
in paths
]
4065 selected_tasks
= [task
for task
in self
.download_tasks_seen \
4066 if task
.url
in urls
]
4068 selection
= self
.treeDownloads
.get_selection()
4069 (model
, paths
) = selection
.get_selected_rows()
4070 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
4071 self
.download_status_model
.C_TASK
) for path
in paths
]
4072 self
.cancel_task_list(selected_tasks
)
4074 def on_btnCancelAll_clicked(self
, widget
, *args
):
4075 self
.cancel_task_list(self
.download_tasks_seen
)
4077 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
4078 episodes
= self
.get_selected_episodes()
4079 if len(episodes
) == 1:
4080 self
.delete_episode_list(episodes
, skip_locked
=False)
4082 self
.delete_episode_list(episodes
)
4084 def on_key_press(self
, widget
, event
):
4085 # Allow tab switching with Ctrl + PgUp/PgDown
4086 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
4087 if event
.keyval
== gtk
.keysyms
.Page_Up
:
4088 self
.wNotebook
.prev_page()
4090 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
4091 self
.wNotebook
.next_page()
4094 # After this code we only handle Maemo hardware keys,
4095 # so if we are not a Maemo app, we don't do anything
4096 if not gpodder
.ui
.maemo
:
4100 if event
.keyval
== gtk
.keysyms
.F7
: #plus
4102 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
4105 if diff
!= 0 and not self
.currently_updating
:
4106 selection
= self
.treeChannels
.get_selection()
4107 (model
, iter) = selection
.get_selected()
4108 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4109 selection
.select_path(new_path
)
4110 self
.treeChannels
.set_cursor(new_path
)
4115 def on_iconify(self
):
4117 self
.gPodder
.set_skip_taskbar_hint(True)
4118 if self
.config
.minimize_to_tray
:
4119 self
.tray_icon
.set_visible(True)
4121 self
.gPodder
.set_skip_taskbar_hint(False)
4123 def on_uniconify(self
):
4125 self
.gPodder
.set_skip_taskbar_hint(False)
4126 if self
.config
.minimize_to_tray
:
4127 self
.tray_icon
.set_visible(False)
4129 self
.gPodder
.set_skip_taskbar_hint(False)
4131 def uniconify_main_window(self
):
4132 if self
.is_iconified():
4133 # We need to hide and then show the window in WMs like Metacity
4134 # or KWin4 to move the window to the active workspace
4135 # (see http://gpodder.org/bug/1125)
4138 self
.gPodder
.present()
4140 def iconify_main_window(self
):
4141 if not self
.is_iconified():
4142 self
.gPodder
.iconify()
4144 def update_podcasts_tab(self
):
4145 if len(self
.channels
):
4146 if gpodder
.ui
.fremantle
:
4147 self
.button_refresh
.set_title(_('Check for new episodes'))
4148 self
.button_refresh
.show()
4150 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4152 if gpodder
.ui
.fremantle
:
4153 self
.button_refresh
.hide()
4155 self
.label2
.set_text(_('Podcasts'))
4157 @dbus.service
.method(gpodder
.dbus_interface
)
4158 def show_gui_window(self
):
4159 parent
= self
.get_dialog_parent()
4162 @dbus.service
.method(gpodder
.dbus_interface
)
4163 def subscribe_to_url(self
, url
):
4164 gPodderAddPodcast(self
.gPodder
,
4165 add_urls_callback
=self
.add_podcast_list
,
4168 @dbus.service
.method(gpodder
.dbus_interface
)
4169 def mark_episode_played(self
, filename
):
4170 if filename
is None:
4173 for channel
in self
.channels
:
4174 for episode
in channel
.get_all_episodes():
4175 fn
= episode
.local_filename(create
=False, check_only
=True)
4177 episode
.mark(is_played
=True)
4179 self
.update_episode_list_icons([episode
.url
])
4180 self
.update_podcast_list_model([episode
.channel
.url
])
4186 def main(options
=None):
4187 gobject
.threads_init()
4188 gobject
.set_application_name('gPodder')
4190 if gpodder
.ui
.maemo
:
4191 # Try to enable the custom icon theme for gPodder on Maemo
4192 settings
= gtk
.settings_get_default()
4193 settings
.set_string_property('gtk-icon-theme-name', \
4194 'gpodder', __file__
)
4195 # Extend the search path for the optified icon theme (Maemo 5)
4196 icon_theme
= gtk
.icon_theme_get_default()
4197 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4199 gtk
.window_set_default_icon_name('gpodder')
4200 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4203 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4204 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4206 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4207 except dbus
.exceptions
.DBusException
, dbe
:
4208 log('Warning: Cannot get "on the bus".', traceback
=True)
4209 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4210 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4211 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4212 dlg
.set_title('gPodder')
4217 util
.make_directory(gpodder
.home
)
4218 gpodder
.load_plugins()
4220 config
= UIConfig(gpodder
.config_file
)
4222 # Load hook modules and install the hook manager globally
4223 # if modules have been found an instantiated by the manager
4224 user_hooks
= hooks
.HookManager()
4225 if user_hooks
.has_modules():
4226 gpodder
.user_hooks
= user_hooks
4228 if gpodder
.ui
.diablo
:
4229 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4230 # folder exists there (allow moving "gpodder" between SD cards or USB)
4231 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4232 if not os
.path
.exists(config
.download_dir
):
4233 log('Downloads might have been moved. Trying to locate them...')
4234 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4235 dir = os
.path
.join(basedir
, 'gpodder')
4236 if os
.path
.exists(dir):
4237 log('Downloads found in: %s', dir)
4238 config
.download_dir
= dir
4241 log('Downloads NOT FOUND in %s', dir)
4243 if config
.enable_fingerscroll
:
4244 BuilderWidget
.use_fingerscroll
= True
4246 config
.mygpo_device_type
= util
.detect_device_type()
4248 gp
= gPodder(bus_name
, config
)
4251 if options
.subscribe
:
4252 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4255 # handle "subscribe to podcast" events from firefox
4256 if platform
.system() == 'Darwin':
4257 from gpodder
import gpodderosx
4258 gpodderosx
.register_handlers(gp
)
4259 # end mac OS X stuff