1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 from xml
.sax
import saxutils
45 # Mock the required D-Bus interfaces with no-ops (ugly? maybe.)
48 def __init__(self
, *args
, **kwargs
):
50 def add_signal_receiver(self
, *args
, **kwargs
):
54 def __init__(self
, *args
, **kwargs
):
58 def method(*args
, **kwargs
):
61 def __init__(self
, *args
, **kwargs
):
64 def __init__(self
, *args
, **kwargs
):
68 from gpodder
import feedcore
69 from gpodder
import util
70 from gpodder
import opml
71 from gpodder
import download
72 from gpodder
import my
73 from gpodder
import youtube
74 from gpodder
import player
75 from gpodder
.liblogger
import log
80 from gpodder
.model
import PodcastChannel
81 from gpodder
.model
import PodcastEpisode
82 from gpodder
.dbsqlite
import Database
84 from gpodder
.gtkui
.model
import PodcastListModel
85 from gpodder
.gtkui
.model
import EpisodeListModel
86 from gpodder
.gtkui
.config
import UIConfig
87 from gpodder
.gtkui
.services
import CoverDownloader
88 from gpodder
.gtkui
.widgets
import SimpleMessageArea
89 from gpodder
.gtkui
.desktopfile
import UserAppsReader
91 from gpodder
.gtkui
.draw
import draw_text_box_centered
93 from gpodder
.gtkui
.interface
.common
import BuilderWidget
94 from gpodder
.gtkui
.interface
.common
import TreeViewHelper
95 from gpodder
.gtkui
.interface
.addpodcast
import gPodderAddPodcast
97 if gpodder
.ui
.desktop
:
98 from gpodder
.gtkui
.download
import DownloadStatusModel
100 from gpodder
.gtkui
.desktop
.sync
import gPodderSyncUI
102 from gpodder
.gtkui
.desktop
.channel
import gPodderChannel
103 from gpodder
.gtkui
.desktop
.preferences
import gPodderPreferences
104 from gpodder
.gtkui
.desktop
.shownotes
import gPodderShownotes
105 from gpodder
.gtkui
.desktop
.episodeselector
import gPodderEpisodeSelector
106 from gpodder
.gtkui
.desktop
.podcastdirectory
import gPodderPodcastDirectory
107 from gpodder
.gtkui
.desktop
.dependencymanager
import gPodderDependencyManager
108 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
110 from gpodder
.gtkui
.desktop
.trayicon
import GPodderStatusIcon
112 except Exception, exc
:
113 log('Warning: Could not import gpodder.trayicon.', traceback
=True)
114 log('Warning: This probably means your PyGTK installation is too old!')
115 have_trayicon
= False
116 elif gpodder
.ui
.diablo
:
117 from gpodder
.gtkui
.download
import DownloadStatusModel
119 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
120 from gpodder
.gtkui
.maemo
.preferences
import gPodderPreferences
121 from gpodder
.gtkui
.maemo
.shownotes
import gPodderShownotes
122 from gpodder
.gtkui
.maemo
.episodeselector
import gPodderEpisodeSelector
123 from gpodder
.gtkui
.maemo
.podcastdirectory
import gPodderPodcastDirectory
124 from gpodder
.gtkui
.maemo
.mygpodder
import MygPodderSettings
125 from gpodder
.gtkui
.interface
.progress
import ProgressIndicator
126 have_trayicon
= False
127 elif gpodder
.ui
.fremantle
:
128 from gpodder
.gtkui
.frmntl
.model
import DownloadStatusModel
129 from gpodder
.gtkui
.frmntl
.model
import EpisodeListModel
130 from gpodder
.gtkui
.frmntl
.model
import PodcastListModel
132 from gpodder
.gtkui
.maemo
.channel
import gPodderChannel
133 from gpodder
.gtkui
.frmntl
.preferences
import gPodderPreferences
134 from gpodder
.gtkui
.frmntl
.shownotes
import gPodderShownotes
135 from gpodder
.gtkui
.frmntl
.episodeselector
import gPodderEpisodeSelector
136 from gpodder
.gtkui
.frmntl
.podcastdirectory
import gPodderPodcastDirectory
137 from gpodder
.gtkui
.frmntl
.episodes
import gPodderEpisodes
138 from gpodder
.gtkui
.frmntl
.downloads
import gPodderDownloads
139 from gpodder
.gtkui
.frmntl
.progress
import ProgressIndicator
140 have_trayicon
= False
142 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
143 from gpodder
.gtkui
.frmntl
.mafw
import MafwPlaybackMonitor
145 from gpodder
.gtkui
.interface
.common
import Orientation
147 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
152 from gpodder
.dbusproxy
import DBusPodcastsProxy
153 from gpodder
import hooks
155 class gPodder(BuilderWidget
, dbus
.service
.Object
):
156 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear']
158 ICON_GENERAL_ADD
= 'general_add'
159 ICON_GENERAL_REFRESH
= 'general_refresh'
160 ICON_GENERAL_CLOSE
= 'general_close'
162 def __init__(self
, bus_name
, config
):
163 dbus
.service
.Object
.__init
__(self
, object_path
=gpodder
.dbus_gui_object_path
, bus_name
=bus_name
)
164 self
.podcasts_proxy
= DBusPodcastsProxy(lambda: self
.channels
, \
165 self
.on_itemUpdate_activate
, \
166 self
.playback_episodes
, \
167 self
.download_episode_list
, \
168 self
.episode_object_by_uri
, \
170 self
.db
= Database(gpodder
.database_file
)
172 BuilderWidget
.__init
__(self
, None)
175 if gpodder
.ui
.diablo
:
177 self
.app
= hildon
.Program()
178 self
.app
.add_window(self
.main_window
)
179 self
.main_window
.add_toolbar(self
.toolbar
)
181 for child
in self
.main_menu
.get_children():
183 self
.main_window
.set_menu(self
.set_finger_friendly(menu
))
184 self
._last
_orientation
= Orientation
.LANDSCAPE
185 elif gpodder
.ui
.fremantle
:
187 self
.app
= hildon
.Program()
188 self
.app
.add_window(self
.main_window
)
190 appmenu
= hildon
.AppMenu()
192 for filter in (self
.item_view_podcasts_all
, \
193 self
.item_view_podcasts_downloaded
, \
194 self
.item_view_podcasts_unplayed
):
195 button
= gtk
.ToggleButton()
196 filter.connect_proxy(button
)
197 appmenu
.add_filter(button
)
199 for action
in (self
.itemPreferences
, \
200 self
.item_downloads
, \
201 self
.itemRemoveOldEpisodes
, \
202 self
.item_unsubscribe
, \
204 button
= hildon
.Button(gtk
.HILDON_SIZE_AUTO
,\
205 hildon
.BUTTON_ARRANGEMENT_HORIZONTAL
)
206 action
.connect_proxy(button
)
207 if action
== self
.item_downloads
:
208 button
.set_title(_('Downloads'))
209 button
.set_value(_('Idle'))
210 self
.button_downloads
= button
211 appmenu
.append(button
)
213 self
.main_window
.set_app_menu(appmenu
)
215 # Initialize portrait mode / rotation manager
216 self
._fremantle
_rotation
= FremantleRotation('gPodder', \
218 gpodder
.__version
__, \
219 self
.config
.rotation_mode
)
221 if self
.config
.rotation_mode
== FremantleRotation
.ALWAYS
:
222 util
.idle_add(self
.on_window_orientation_changed
, \
223 Orientation
.PORTRAIT
)
224 self
._last
_orientation
= Orientation
.PORTRAIT
226 self
._last
_orientation
= Orientation
.LANDSCAPE
228 # Flag set when a notification is being shown (Maemo bug 11235)
229 self
._fremantle
_notification
_visible
= False
231 self
._last
_orientation
= Orientation
.LANDSCAPE
232 self
.toolbar
.set_property('visible', self
.config
.show_toolbar
)
234 self
.bluetooth_available
= util
.bluetooth_available()
236 self
.config
.connect_gtk_window(self
.gPodder
, 'main_window')
237 if not gpodder
.ui
.fremantle
:
238 self
.config
.connect_gtk_paned('paned_position', self
.channelPaned
)
239 self
.main_window
.show()
241 self
.player_receiver
= player
.MediaPlayerDBusReceiver(self
.on_played
)
243 if gpodder
.ui
.fremantle
:
244 # Create a D-Bus monitoring object that takes care of
245 # tracking MAFW (Nokia Media Player) playback events
246 # and sends episode playback status events via D-Bus
247 self
.mafw_monitor
= MafwPlaybackMonitor(gpodder
.dbus_session_bus
)
249 self
.gPodder
.connect('key-press-event', self
.on_key_press
)
251 self
.preferences_dialog
= None
252 self
.config
.add_observer(self
.on_config_changed
)
254 self
.tray_icon
= None
255 self
.episode_shownotes_window
= None
256 self
.new_episodes_window
= None
258 if gpodder
.ui
.desktop
:
259 # Mac OS X-specific UI tweaks: Native main menu integration
260 # http://sourceforge.net/apps/trac/gtk-osx/wiki/Integrate
261 if getattr(gtk
.gdk
, 'WINDOWING', 'x11') == 'quartz':
263 import igemacintegration
as igemi
265 # Move the menu bar from the window to the Mac menu bar
267 igemi
.ige_mac_menu_set_menu_bar(self
.mainMenu
)
269 # Reparent some items to the "Application" menu
270 for widget
in ('/mainMenu/menuHelp/itemAbout', \
271 '/mainMenu/menuPodcasts/itemPreferences'):
272 item
= self
.uimanager1
.get_widget(widget
)
273 group
= igemi
.ige_mac_menu_add_app_menu_group()
274 igemi
.ige_mac_menu_add_app_menu_item(group
, item
, None)
276 quit_widget
= '/mainMenu/menuPodcasts/itemQuit'
277 quit_item
= self
.uimanager1
.get_widget(quit_widget
)
278 igemi
.ige_mac_menu_set_quit_menu_item(quit_item
)
280 print >>sys
.stderr
, """
281 Warning: ige-mac-integration not found - no native menus.
284 self
.sync_ui
= gPodderSyncUI(self
.config
, self
.notification
, \
285 self
.main_window
, self
.show_confirmation
, \
286 self
.update_episode_list_icons
, \
287 self
.update_podcast_list_model
, self
.toolPreferences
, \
288 gPodderEpisodeSelector
, \
289 self
.commit_changes_to_database
)
293 self
.download_status_model
= DownloadStatusModel()
294 self
.download_queue_manager
= download
.DownloadQueueManager(self
.config
)
296 if gpodder
.ui
.desktop
:
297 self
.show_hide_tray_icon()
298 self
.itemShowAllEpisodes
.set_active(self
.config
.podcast_list_view_all
)
299 self
.itemShowToolbar
.set_active(self
.config
.show_toolbar
)
300 self
.itemShowDescription
.set_active(self
.config
.episode_list_descriptions
)
302 if not gpodder
.ui
.fremantle
:
303 self
.config
.connect_gtk_spinbutton('max_downloads', self
.spinMaxDownloads
)
304 self
.config
.connect_gtk_togglebutton('max_downloads_enabled', self
.cbMaxDownloads
)
305 self
.config
.connect_gtk_spinbutton('limit_rate_value', self
.spinLimitDownloads
)
306 self
.config
.connect_gtk_togglebutton('limit_rate', self
.cbLimitDownloads
)
308 # When the amount of maximum downloads changes, notify the queue manager
309 changed_cb
= lambda spinbutton
: self
.download_queue_manager
.spawn_threads()
310 self
.spinMaxDownloads
.connect('value-changed', changed_cb
)
312 self
.default_title
= 'gPodder'
313 if gpodder
.__version
__.rfind('git') != -1:
314 self
.set_title('gPodder %s' % gpodder
.__version
__)
316 title
= self
.gPodder
.get_title()
317 if title
is not None:
318 self
.set_title(title
)
320 self
.set_title(_('gPodder'))
322 self
.cover_downloader
= CoverDownloader()
324 # Generate list models for podcasts and their episodes
325 self
.podcast_list_model
= PodcastListModel(self
.cover_downloader
)
327 self
.cover_downloader
.register('cover-available', self
.cover_download_finished
)
328 self
.cover_downloader
.register('cover-removed', self
.cover_file_removed
)
330 if gpodder
.ui
.fremantle
:
331 # Work around Maemo bug #4718
332 self
.button_refresh
.set_name('HildonButton-finger')
333 self
.button_subscribe
.set_name('HildonButton-finger')
335 self
.button_refresh
.set_sensitive(False)
336 self
.button_subscribe
.set_sensitive(False)
338 self
.button_subscribe
.set_image(gtk
.image_new_from_icon_name(\
339 self
.ICON_GENERAL_ADD
, gtk
.ICON_SIZE_BUTTON
))
340 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
341 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
343 # Make the button scroll together with the TreeView contents
344 action_area_box
= self
.treeChannels
.get_action_area_box()
345 for child
in self
.buttonbox
:
346 child
.reparent(action_area_box
)
347 self
.vbox
.remove(self
.buttonbox
)
348 action_area_box
.set_spacing(2)
349 action_area_box
.set_border_width(3)
350 self
.treeChannels
.set_action_area_visible(True)
352 from gpodder
.gtkui
.frmntl
import style
353 sub_font
= style
.get_font_desc('SmallSystemFont')
354 sub_color
= style
.get_color('SecondaryTextColor')
355 sub
= (sub_font
.to_string(), sub_color
.to_string())
356 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
357 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
359 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
360 while gtk
.events_pending():
361 gtk
.main_iteration(False)
364 # Try to get the real package version from dpkg
365 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
366 version
, _stderr
= p
.communicate()
370 version
= gpodder
.__version
__
371 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
372 self
.label_footer
.hide()
374 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
375 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
376 show_episode_shownotes
=self
.show_episode_shownotes
, \
377 update_podcast_list_model
=self
.update_podcast_list_model
, \
378 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
379 item_view_episodes_all
=self
.item_view_episodes_all
, \
380 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
381 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
382 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
383 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
384 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
385 hide_episode_search
=self
.hide_episode_search
, \
386 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
387 playback_episodes
=self
.playback_episodes
, \
388 delete_episode_list
=self
.delete_episode_list
, \
389 episode_list_status_changed
=self
.episode_list_status_changed
, \
390 download_episode_list
=self
.download_episode_list
, \
391 episode_is_downloading
=self
.episode_is_downloading
, \
392 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
393 add_download_task_monitor
=self
.add_download_task_monitor
, \
394 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
395 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
396 on_delete_episodes_button_clicked
=self
.on_itemRemoveOldEpisodes_activate
, \
397 on_itemUpdate_activate
=self
.on_itemUpdate_activate
)
399 # Expose objects for episode list type-ahead find
400 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
401 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
402 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
404 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
405 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
406 cleanup_downloads
=self
.cleanup_downloads
, \
407 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
408 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
411 self
.treeAvailable
= self
.episodes_window
.treeview
412 self
.treeDownloads
= self
.downloads_window
.treeview
414 # Init the treeviews that we use
415 self
.init_podcast_list_treeview()
416 self
.init_episode_list_treeview()
417 self
.init_download_list_treeview()
419 if self
.config
.podcast_list_hide_boring
:
420 self
.item_view_hide_boring_podcasts
.set_active(True)
422 self
.currently_updating
= False
425 self
.context_menu_mouse_button
= 1
427 self
.context_menu_mouse_button
= 3
429 if self
.config
.start_iconified
:
430 self
.iconify_main_window()
432 self
.download_tasks_seen
= set()
433 self
.download_list_update_enabled
= False
434 self
.download_task_monitors
= set()
436 # Subscribed channels
437 self
.active_channel
= None
438 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
439 self
.channel_list_changed
= True
440 self
.update_podcasts_tab()
442 # load list of user applications for audio playback
443 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
444 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
446 # Set the "Device" menu item for the first time
447 if gpodder
.ui
.desktop
:
448 self
.update_item_device()
450 # Set up the first instance of MygPoClient
451 self
.mygpo_client
= my
.MygPoClient(self
.config
)
453 # Now, update the feed cache, when everything's in place
454 if not gpodder
.ui
.fremantle
:
455 self
.btnUpdateFeeds
.show()
456 self
.updating_feed_cache
= False
457 self
.feed_cache_update_cancelled
= False
458 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
460 self
.message_area
= None
462 def find_partial_downloads():
463 # Look for partial file downloads
464 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
465 count
= len(partial_files
)
466 resumable_episodes
= []
468 if not gpodder
.ui
.fremantle
:
469 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
470 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
471 _('Some episodes have not finished downloading in a previous session.'), \
472 False, self
.get_dialog_parent())
473 indicator
.on_message(N_('%d partial file', '%d partial files', count
) % count
)
475 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
478 for c
in self
.channels
:
479 for e
in c
.get_all_episodes():
480 filename
= e
.local_filename(create
=False, check_only
=True)
481 if filename
in candidates
:
482 log('Found episode: %s', e
.title
, sender
=self
)
484 indicator
.on_message(e
.title
)
485 indicator
.on_progress(float(found
)/count
)
486 candidates
.remove(filename
)
487 partial_files
.remove(filename
+'.partial')
488 resumable_episodes
.append(e
)
496 for f
in partial_files
:
497 log('Partial file without episode: %s', f
, sender
=self
)
500 util
.idle_add(indicator
.on_finished
)
502 if len(resumable_episodes
):
503 def offer_resuming():
504 self
.download_episode_list_paused(resumable_episodes
)
505 if not gpodder
.ui
.fremantle
:
506 resume_all
= gtk
.Button(_('Resume all'))
507 #resume_all.set_border_width(0)
508 def on_resume_all(button
):
509 selection
= self
.treeDownloads
.get_selection()
510 selection
.select_all()
511 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
512 selection
.unselect_all()
513 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
514 self
.message_area
.hide()
515 resume_all
.connect('clicked', on_resume_all
)
517 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
518 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
519 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
520 self
.message_area
.show_all()
521 self
.clean_up_downloads(delete_partial
=False)
522 util
.idle_add(offer_resuming
)
523 elif not gpodder
.ui
.fremantle
:
524 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
526 util
.idle_add(self
.clean_up_downloads
, True)
527 threading
.Thread(target
=find_partial_downloads
).start()
529 # Start the auto-update procedure
530 self
._auto
_update
_timer
_source
_id
= None
531 if self
.config
.auto_update_feeds
:
532 self
.restart_auto_update_timer()
534 # Delete old episodes if the user wishes to
535 if self
.config
.auto_remove_played_episodes
and \
536 self
.config
.episode_old_age
> 0:
537 old_episodes
= list(self
.get_expired_episodes())
538 if len(old_episodes
) > 0:
539 self
.delete_episode_list(old_episodes
, confirm
=False)
540 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
542 if gpodder
.ui
.fremantle
:
543 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
544 self
.button_refresh
.set_sensitive(True)
545 self
.button_subscribe
.set_sensitive(True)
546 self
.main_window
.set_title(_('gPodder'))
547 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
549 # Do the initial sync with the web service
550 util
.idle_add(self
.mygpo_client
.flush
, True)
552 # First-time users should be asked if they want to see the OPML
553 if not self
.channels
and not gpodder
.ui
.fremantle
:
554 util
.idle_add(self
.on_itemUpdate_activate
)
556 def episode_object_by_uri(self
, uri
):
557 """Get an episode object given a local or remote URI
559 This can be used to quickly access an episode object
560 when all we have is its download filename or episode
561 URL (e.g. from external D-Bus calls / signals, etc..)
563 if uri
.startswith('/'):
564 uri
= 'file://' + uri
566 prefix
= 'file://' + self
.config
.download_dir
568 if uri
.startswith(prefix
):
569 # File is on the local filesystem in the download folder
570 filename
= uri
[len(prefix
):]
571 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
573 if len(file_parts
) == 2:
574 dir_name
, filename
= file_parts
575 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
576 if len(channels
) == 1:
577 channel
= channels
[0]
578 return channel
.get_episode_by_filename(filename
)
580 # Possibly remote file - search the database for a podcast
581 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
583 if channel_id
is not None:
584 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
585 if len(channels
) == 1:
586 channel
= channels
[0]
587 return channel
.get_episode_by_url(uri
)
591 def on_played(self
, start
, end
, total
, file_uri
):
592 """Handle the "played" signal from a media player"""
593 if start
== 0 and end
== 0 and total
== 0:
594 # Ignore bogus play event
596 elif end
< start
+ 5:
597 # Ignore "less than five seconds" segments,
598 # as they can happen with seeking, etc...
601 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
602 episode
= self
.episode_object_by_uri(file_uri
)
604 if episode
is not None:
605 file_type
= episode
.file_type()
606 # Automatically enable D-Bus played status mode
607 if file_type
== 'audio':
608 self
.config
.audio_played_dbus
= True
609 elif file_type
== 'video':
610 self
.config
.video_played_dbus
= True
614 episode
.total_time
= total
616 # Assume the episode's total time for the action
617 total
= episode
.total_time
618 if episode
.current_position_updated
is None or \
619 now
> episode
.current_position_updated
:
620 episode
.current_position
= end
621 episode
.current_position_updated
= now
622 episode
.mark(is_played
=True)
625 self
.update_episode_list_icons([episode
.url
])
626 self
.update_podcast_list_model([episode
.channel
.url
])
628 # Submit this action to the webservice
629 self
.mygpo_client
.on_playback_full(episode
, \
632 def on_add_remove_podcasts_mygpo(self
):
633 actions
= self
.mygpo_client
.get_received_actions()
637 existing_urls
= [c
.url
for c
in self
.channels
]
639 # Columns for the episode selector window - just one...
641 ('description', None, None, _('Action')),
644 # A list of actions that have to be chosen from
647 # Actions that are ignored (already carried out)
650 for action
in actions
:
651 if action
.is_add
and action
.url
not in existing_urls
:
652 changes
.append(my
.Change(action
))
653 elif action
.is_remove
and action
.url
in existing_urls
:
654 podcast_object
= None
655 for podcast
in self
.channels
:
656 if podcast
.url
== action
.url
:
657 podcast_object
= podcast
659 changes
.append(my
.Change(action
, podcast_object
))
661 log('Ignoring action: %s', action
, sender
=self
)
662 ignored
.append(action
)
664 # Confirm all ignored changes
665 self
.mygpo_client
.confirm_received_actions(ignored
)
667 def execute_podcast_actions(selected
):
668 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
669 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
671 # Apply the accepted changes locally
672 self
.add_podcast_list(add_list
)
673 self
.remove_podcast_list(remove_list
, confirm
=False)
675 # All selected items are now confirmed
676 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
678 # Revert the changes on the server
679 rejected
= [c
.action
for c
in changes
if c
not in selected
]
680 self
.mygpo_client
.reject_received_actions(rejected
)
683 # We're abusing the Episode Selector again ;) -- thp
684 gPodderEpisodeSelector(self
.main_window
, \
685 title
=_('Confirm changes from gpodder.net'), \
686 instructions
=_('Select the actions you want to carry out.'), \
689 size_attribute
=None, \
690 stock_ok_button
=gtk
.STOCK_APPLY
, \
691 callback
=execute_podcast_actions
, \
694 # There are some actions that need the user's attention
699 # We have no remaining actions - no selection happens
702 def rewrite_urls_mygpo(self
):
703 # Check if we have to rewrite URLs since the last add
704 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
706 for rewritten_url
in rewritten_urls
:
707 if not rewritten_url
.new_url
:
710 for channel
in self
.channels
:
711 if channel
.url
== rewritten_url
.old_url
:
712 log('Updating URL of %s to %s', channel
, \
713 rewritten_url
.new_url
, sender
=self
)
714 channel
.url
= rewritten_url
.new_url
716 self
.channel_list_changed
= True
717 util
.idle_add(self
.update_episode_list_model
)
720 def on_send_full_subscriptions(self
):
721 # Send the full subscription list to the gpodder.net client
722 # (this will overwrite the subscription list on the server)
723 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
724 _('Your subscriptions are being uploaded to the server.'), \
725 False, self
.get_dialog_parent())
728 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
729 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
734 message
= e
.__class
__.__name
__
735 self
.show_message(message
, \
736 _('Error while uploading'), \
738 util
.idle_add(show_error
, e
)
740 util
.idle_add(indicator
.on_finished
)
742 def on_podcast_selected(self
, treeview
, path
, column
):
744 model
= treeview
.get_model()
745 channel
= model
.get_value(model
.get_iter(path
), \
746 PodcastListModel
.C_CHANNEL
)
747 self
.active_channel
= channel
748 self
.update_episode_list_model()
749 self
.episodes_window
.channel
= self
.active_channel
750 self
.episodes_window
.show()
752 def on_button_subscribe_clicked(self
, button
):
753 self
.on_itemImportChannels_activate(button
)
755 def on_button_downloads_clicked(self
, widget
):
756 self
.downloads_window
.show()
758 def show_episode_in_download_manager(self
, episode
):
759 self
.downloads_window
.show()
760 model
= self
.treeDownloads
.get_model()
761 selection
= self
.treeDownloads
.get_selection()
762 selection
.unselect_all()
763 it
= model
.get_iter_first()
764 while it
is not None:
765 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
766 if task
.episode
.url
== episode
.url
:
767 selection
.select_iter(it
)
768 # FIXME: Scroll to selection in pannable area
770 it
= model
.iter_next(it
)
772 def for_each_episode_set_task_status(self
, episodes
, status
):
773 episode_urls
= set(episode
.url
for episode
in episodes
)
774 model
= self
.treeDownloads
.get_model()
775 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
776 model
.get_value(row
.iter, \
777 DownloadStatusModel
.C_TASK
)) for row
in model \
778 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
780 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
782 def on_window_orientation_changed(self
, orientation
):
783 self
._last
_orientation
= orientation
784 if self
.preferences_dialog
is not None:
785 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
787 treeview
= self
.treeChannels
788 if orientation
== Orientation
.PORTRAIT
:
789 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
790 # Work around Maemo bug #4718
791 self
.button_subscribe
.set_name('HildonButton-thumb')
792 self
.button_refresh
.set_name('HildonButton-thumb')
794 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
795 # Work around Maemo bug #4718
796 self
.button_subscribe
.set_name('HildonButton-finger')
797 self
.button_refresh
.set_name('HildonButton-finger')
799 def on_treeview_podcasts_selection_changed(self
, selection
):
800 model
, iter = selection
.get_selected()
802 self
.active_channel
= None
803 self
.episode_list_model
.clear()
805 def on_treeview_button_pressed(self
, treeview
, event
):
806 if event
.window
!= treeview
.get_bin_window():
809 TreeViewHelper
.save_button_press_event(treeview
, event
)
811 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
812 TreeViewHelper
.ROLE_PODCASTS
:
813 return self
.currently_updating
815 return event
.button
== self
.context_menu_mouse_button
and \
818 def on_treeview_podcasts_button_released(self
, treeview
, event
):
819 if event
.window
!= treeview
.get_bin_window():
823 return self
.treeview_channels_handle_gestures(treeview
, event
)
824 return self
.treeview_channels_show_context_menu(treeview
, event
)
826 def on_treeview_episodes_button_released(self
, treeview
, event
):
827 if event
.window
!= treeview
.get_bin_window():
831 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
832 return self
.treeview_available_handle_gestures(treeview
, event
)
834 return self
.treeview_available_show_context_menu(treeview
, event
)
836 def on_treeview_downloads_button_released(self
, treeview
, event
):
837 if event
.window
!= treeview
.get_bin_window():
840 return self
.treeview_downloads_show_context_menu(treeview
, event
)
842 def on_entry_search_podcasts_changed(self
, editable
):
843 if self
.hbox_search_podcasts
.get_property('visible'):
844 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
846 def on_entry_search_podcasts_key_press(self
, editable
, event
):
847 if event
.keyval
== gtk
.keysyms
.Escape
:
848 self
.hide_podcast_search()
851 def hide_podcast_search(self
, *args
):
852 self
.hbox_search_podcasts
.hide()
853 self
.entry_search_podcasts
.set_text('')
854 self
.podcast_list_model
.set_search_term(None)
855 self
.treeChannels
.grab_focus()
857 def show_podcast_search(self
, input_char
):
858 self
.hbox_search_podcasts
.show()
859 self
.entry_search_podcasts
.insert_text(input_char
, -1)
860 self
.entry_search_podcasts
.grab_focus()
861 self
.entry_search_podcasts
.set_position(-1)
863 def init_podcast_list_treeview(self
):
864 # Set up podcast channel tree view widget
865 if gpodder
.ui
.fremantle
:
866 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
867 self
.item_view_podcasts_downloaded
.set_active(True)
868 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
869 self
.item_view_podcasts_unplayed
.set_active(True)
871 self
.item_view_podcasts_all
.set_active(True)
872 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
874 iconcolumn
= gtk
.TreeViewColumn('')
875 iconcell
= gtk
.CellRendererPixbuf()
876 iconcolumn
.pack_start(iconcell
, False)
877 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
878 self
.treeChannels
.append_column(iconcolumn
)
880 namecolumn
= gtk
.TreeViewColumn('')
881 namecell
= gtk
.CellRendererText()
882 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
883 namecolumn
.pack_start(namecell
, True)
884 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
886 if gpodder
.ui
.fremantle
:
887 countcell
= gtk
.CellRendererText()
888 from gpodder
.gtkui
.frmntl
import style
889 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
890 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
891 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
892 countcell
.set_property('xalign', 1.)
893 countcell
.set_property('xpad', 5)
894 namecolumn
.pack_start(countcell
, False)
895 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
896 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
898 iconcell
= gtk
.CellRendererPixbuf()
899 iconcell
.set_property('xalign', 1.0)
900 namecolumn
.pack_start(iconcell
, False)
901 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
902 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
904 self
.treeChannels
.append_column(namecolumn
)
906 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
908 # When no podcast is selected, clear the episode list model
909 selection
= self
.treeChannels
.get_selection()
910 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
912 # Set up type-ahead find for the podcast list
913 def on_key_press(treeview
, event
):
914 if event
.keyval
== gtk
.keysyms
.Escape
:
915 self
.hide_podcast_search()
916 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
917 self
.hide_podcast_search()
918 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
919 # Don't handle type-ahead when control is pressed (so shortcuts
920 # with the Ctrl key still work, e.g. Ctrl+A, ...)
923 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
924 if unicode_char_id
== 0:
926 input_char
= unichr(unicode_char_id
)
927 self
.show_podcast_search(input_char
)
929 self
.treeChannels
.connect('key-press-event', on_key_press
)
931 # Enable separators to the podcast list to separate special podcasts
932 # from others (this is used for the "all episodes" view)
933 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
935 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
937 def on_entry_search_episodes_changed(self
, editable
):
938 if self
.hbox_search_episodes
.get_property('visible'):
939 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
941 def on_entry_search_episodes_key_press(self
, editable
, event
):
942 if event
.keyval
== gtk
.keysyms
.Escape
:
943 self
.hide_episode_search()
946 def hide_episode_search(self
, *args
):
947 self
.hbox_search_episodes
.hide()
948 self
.entry_search_episodes
.set_text('')
949 self
.episode_list_model
.set_search_term(None)
950 self
.treeAvailable
.grab_focus()
952 def show_episode_search(self
, input_char
):
953 self
.hbox_search_episodes
.show()
954 self
.entry_search_episodes
.insert_text(input_char
, -1)
955 self
.entry_search_episodes
.grab_focus()
956 self
.entry_search_episodes
.set_position(-1)
958 def init_episode_list_treeview(self
):
959 # For loading the list model
960 self
.empty_episode_list_model
= EpisodeListModel()
961 self
.episode_list_model
= EpisodeListModel()
963 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
964 self
.item_view_episodes_undeleted
.set_active(True)
965 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
966 self
.item_view_episodes_downloaded
.set_active(True)
967 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
968 self
.item_view_episodes_unplayed
.set_active(True)
970 self
.item_view_episodes_all
.set_active(True)
972 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
974 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
976 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
978 iconcell
= gtk
.CellRendererPixbuf()
980 iconcell
.set_fixed_size(50, 50)
981 status_column_label
= ''
983 status_column_label
= _('Status')
984 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
986 namecell
= gtk
.CellRendererText()
987 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
988 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
989 namecolumn
.pack_start(namecell
, True)
990 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
991 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
992 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
993 namecolumn
.set_resizable(True)
994 namecolumn
.set_expand(True)
996 if gpodder
.ui
.fremantle
:
997 from gpodder
.gtkui
.frmntl
import style
998 timecell
= gtk
.CellRendererText()
999 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1000 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1001 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1002 timecell
.set_property('xalign', 1.)
1003 timecell
.set_property('xpad', 5)
1004 namecolumn
.pack_start(timecell
, False)
1005 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1006 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME1_VISIBLE
)
1008 # Add another cell renderer to fix a sizing issue (one renderer
1009 # only renders short text and the other one longer text to avoid
1010 # having titles of episodes unnecessarily cut off)
1011 timecell
= gtk
.CellRendererText()
1012 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1013 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1014 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1015 timecell
.set_property('xalign', 1.)
1016 timecell
.set_property('xpad', 5)
1017 namecolumn
.pack_start(timecell
, False)
1018 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1019 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME2_VISIBLE
)
1021 sizecell
= gtk
.CellRendererText()
1022 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1023 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1025 releasecell
= gtk
.CellRendererText()
1026 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1027 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1029 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
1030 itemcolumn
.set_reorderable(True)
1031 self
.treeAvailable
.append_column(itemcolumn
)
1033 if gpodder
.ui
.maemo
:
1034 sizecolumn
.set_visible(False)
1035 releasecolumn
.set_visible(False)
1037 # Set up type-ahead find for the episode list
1038 def on_key_press(treeview
, event
):
1039 if event
.keyval
== gtk
.keysyms
.Escape
:
1040 self
.hide_episode_search()
1041 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1042 self
.hide_episode_search()
1043 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1044 # Don't handle type-ahead when control is pressed (so shortcuts
1045 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1048 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1049 if unicode_char_id
== 0:
1051 input_char
= unichr(unicode_char_id
)
1052 self
.show_episode_search(input_char
)
1054 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1056 if gpodder
.ui
.desktop
:
1057 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1058 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1059 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1060 if self
.config
.on_drag_mark_played
:
1061 for episode
in self
.get_selected_episodes():
1062 episode
.mark(is_played
=True)
1063 self
.on_selected_episodes_status_changed()
1064 uris
= ['file://'+e
.local_filename(create
=False) \
1065 for e
in self
.get_selected_episodes() \
1066 if e
.was_downloaded(and_exists
=True)]
1067 uris
.append('') # for the trailing '\r\n'
1068 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1069 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1071 selection
= self
.treeAvailable
.get_selection()
1072 if gpodder
.ui
.diablo
:
1073 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1074 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1076 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1077 elif gpodder
.ui
.fremantle
:
1078 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1080 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1081 # Update the sensitivity of the toolbar buttons on the Desktop
1082 selection
.connect('changed', lambda s
: self
.play_or_download())
1084 if gpodder
.ui
.diablo
:
1085 # Set up the tap-and-hold context menu for podcasts
1087 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1088 menu
.append(self
.itemEditChannel
.create_menu_item())
1089 menu
.append(gtk
.SeparatorMenuItem())
1090 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1091 menu
.append(gtk
.SeparatorMenuItem())
1092 item
= gtk
.ImageMenuItem(_('Close this menu'))
1093 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1094 gtk
.ICON_SIZE_MENU
))
1097 menu
= self
.set_finger_friendly(menu
)
1098 self
.treeChannels
.tap_and_hold_setup(menu
)
1101 def init_download_list_treeview(self
):
1102 # enable multiple selection support
1103 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1104 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1106 # columns and renderers for "download progress" tab
1107 # First column: [ICON] Episodename
1108 column
= gtk
.TreeViewColumn(_('Episode'))
1110 cell
= gtk
.CellRendererPixbuf()
1111 if gpodder
.ui
.maemo
:
1112 cell
.set_fixed_size(50, 50)
1113 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1114 column
.pack_start(cell
, expand
=False)
1115 column
.add_attribute(cell
, 'stock-id', \
1116 DownloadStatusModel
.C_ICON_NAME
)
1118 cell
= gtk
.CellRendererText()
1119 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1120 column
.pack_start(cell
, expand
=True)
1121 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1122 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1123 column
.set_expand(True)
1124 self
.treeDownloads
.append_column(column
)
1126 # Second column: Progress
1127 cell
= gtk
.CellRendererProgress()
1128 cell
.set_property('yalign', .5)
1129 cell
.set_property('ypad', 6)
1130 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1131 value
=DownloadStatusModel
.C_PROGRESS
, \
1132 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1133 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1134 column
.set_expand(False)
1135 self
.treeDownloads
.append_column(column
)
1136 column
.set_property('min-width', 150)
1137 column
.set_property('max-width', 150)
1139 self
.treeDownloads
.set_model(self
.download_status_model
)
1140 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1142 def on_treeview_expose_event(self
, treeview
, event
):
1143 if event
.window
== treeview
.get_bin_window():
1144 model
= treeview
.get_model()
1145 if (model
is not None and model
.get_iter_first() is not None):
1148 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1152 ctx
= event
.window
.cairo_create()
1153 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1154 event
.area
.width
, event
.area
.height
)
1157 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1160 if role
== TreeViewHelper
.ROLE_EPISODES
:
1161 if self
.currently_updating
:
1162 text
= _('Loading episodes')
1163 progress
= self
.episode_list_model
.get_update_progress()
1164 elif self
.config
.episode_list_view_mode
!= \
1165 EpisodeListModel
.VIEW_ALL
:
1166 text
= _('No episodes in current view')
1168 text
= _('No episodes available')
1169 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1170 if self
.config
.episode_list_view_mode
!= \
1171 EpisodeListModel
.VIEW_ALL
and \
1172 self
.config
.podcast_list_hide_boring
and \
1173 len(self
.channels
) > 0:
1174 text
= _('No podcasts in this view')
1176 text
= _('No subscriptions')
1177 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1178 text
= _('No active downloads')
1180 raise Exception('on_treeview_expose_event: unknown role')
1182 if gpodder
.ui
.fremantle
:
1183 from gpodder
.gtkui
.frmntl
import style
1184 font_desc
= style
.get_font_desc('LargeSystemFont')
1188 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1192 def enable_download_list_update(self
):
1193 if not self
.download_list_update_enabled
:
1194 self
.update_downloads_list()
1195 gobject
.timeout_add(1500, self
.update_downloads_list
)
1196 self
.download_list_update_enabled
= True
1198 def cleanup_downloads(self
):
1199 model
= self
.download_status_model
1201 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1202 changed_episode_urls
= set()
1203 for row_reference
, task
in all_tasks
:
1204 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1205 model
.remove(model
.get_iter(row_reference
.get_path()))
1207 # We don't "see" this task anymore - remove it;
1208 # this is needed, so update_episode_list_icons()
1209 # below gets the correct list of "seen" tasks
1210 self
.download_tasks_seen
.remove(task
)
1211 except KeyError, key_error
:
1212 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1213 changed_episode_urls
.add(task
.url
)
1214 # Tell the task that it has been removed (so it can clean up)
1215 task
.removed_from_list()
1217 # Tell the podcasts tab to update icons for our removed podcasts
1218 self
.update_episode_list_icons(changed_episode_urls
)
1220 # Tell the shownotes window that we have removed the episode
1221 if self
.episode_shownotes_window
is not None and \
1222 self
.episode_shownotes_window
.episode
is not None and \
1223 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1224 self
.episode_shownotes_window
._download
_status
_changed
(None)
1226 # Update the downloads list one more time
1227 self
.update_downloads_list(can_call_cleanup
=False)
1229 def on_tool_downloads_toggled(self
, toolbutton
):
1230 if toolbutton
.get_active():
1231 self
.wNotebook
.set_current_page(1)
1233 self
.wNotebook
.set_current_page(0)
1235 def add_download_task_monitor(self
, monitor
):
1236 self
.download_task_monitors
.add(monitor
)
1237 model
= self
.download_status_model
1241 task
= row
[self
.download_status_model
.C_TASK
]
1242 monitor
.task_updated(task
)
1244 def remove_download_task_monitor(self
, monitor
):
1245 self
.download_task_monitors
.remove(monitor
)
1247 def update_downloads_list(self
, can_call_cleanup
=True):
1249 model
= self
.download_status_model
1251 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1252 total_speed
, total_size
, done_size
= 0, 0, 0
1254 # Keep a list of all download tasks that we've seen
1255 download_tasks_seen
= set()
1257 # Remember the DownloadTask object for the episode that
1258 # has been opened in the episode shownotes dialog (if any)
1259 if self
.episode_shownotes_window
is not None:
1260 shownotes_episode
= self
.episode_shownotes_window
.episode
1261 shownotes_task
= None
1263 shownotes_episode
= None
1264 shownotes_task
= None
1266 # Do not go through the list of the model is not (yet) available
1270 failed_downloads
= []
1272 self
.download_status_model
.request_update(row
.iter)
1274 task
= row
[self
.download_status_model
.C_TASK
]
1275 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1277 # Let the download task monitors know of changes
1278 for monitor
in self
.download_task_monitors
:
1279 monitor
.task_updated(task
)
1282 done_size
+= size
*progress
1284 if shownotes_episode
is not None and \
1285 shownotes_episode
.url
== task
.episode
.url
:
1286 shownotes_task
= task
1288 download_tasks_seen
.add(task
)
1290 if status
== download
.DownloadTask
.DOWNLOADING
:
1292 total_speed
+= speed
1293 elif status
== download
.DownloadTask
.FAILED
:
1294 failed_downloads
.append(task
)
1296 elif status
== download
.DownloadTask
.DONE
:
1298 elif status
== download
.DownloadTask
.QUEUED
:
1300 elif status
== download
.DownloadTask
.PAUSED
:
1305 # Remember which tasks we have seen after this run
1306 self
.download_tasks_seen
= download_tasks_seen
1308 if gpodder
.ui
.desktop
:
1309 text
= [_('Downloads')]
1310 if downloading
+ failed
+ queued
> 0:
1313 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1315 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1317 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1318 text
.append(' (' + ', '.join(s
)+')')
1319 self
.labelDownloads
.set_text(''.join(text
))
1320 elif gpodder
.ui
.diablo
:
1321 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1323 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1325 self
.tool_downloads
.set_label(_('Downloads'))
1326 elif gpodder
.ui
.fremantle
:
1327 if downloading
+ queued
> 0:
1328 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1330 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1332 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1334 self
.button_downloads
.set_value(_('Idle'))
1336 title
= [self
.default_title
]
1338 # We have to update all episodes/channels for which the status has
1339 # changed. Accessing task.status_changed has the side effect of
1340 # re-setting the changed flag, so we need to get the "changed" list
1341 # of tuples first and split it into two lists afterwards
1342 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1343 self
.download_tasks_seen
if task
.status_changed
]
1344 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1345 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1347 count
= downloading
+ queued
1349 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1352 percentage
= 100.0*done_size
/total_size
1355 total_speed
= util
.format_filesize(total_speed
)
1356 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1357 if self
.tray_icon
is not None:
1358 # Update the tray icon status and progress bar
1359 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1360 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1362 if self
.tray_icon
is not None:
1363 # Update the tray icon status
1364 self
.tray_icon
.set_status()
1365 if gpodder
.ui
.desktop
:
1366 self
.downloads_finished(self
.download_tasks_seen
)
1367 if gpodder
.ui
.diablo
:
1368 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1369 log('All downloads have finished.', sender
=self
)
1370 if self
.config
.cmd_all_downloads_complete
:
1371 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1373 if gpodder
.ui
.fremantle
and failed
:
1374 message
= '\n'.join(['%s: %s' % (str(task
), \
1375 task
.error_message
) for task
in failed_downloads
])
1376 self
.show_message(message
, _('Downloads failed'), important
=True)
1378 # Remove finished episodes
1379 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1380 self
.cleanup_downloads()
1382 # Stop updating the download list here
1383 self
.download_list_update_enabled
= False
1385 if not gpodder
.ui
.fremantle
:
1386 self
.gPodder
.set_title(' - '.join(title
))
1388 self
.update_episode_list_icons(episode_urls
)
1389 if self
.episode_shownotes_window
is not None:
1390 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1391 shownotes_task
!= self
.episode_shownotes_window
.task
:
1392 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1393 self
.episode_shownotes_window
._download
_status
_progress
()
1394 self
.play_or_download()
1396 self
.update_podcast_list_model(channel_urls
)
1398 return self
.download_list_update_enabled
1399 except Exception, e
:
1400 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1401 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1402 # We return False here, so the update loop won't be called again,
1403 # that's why we require the restart of gPodder in the message.
1406 def on_config_changed(self
, *args
):
1407 util
.idle_add(self
._on
_config
_changed
, *args
)
1409 def _on_config_changed(self
, name
, old_value
, new_value
):
1410 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1411 self
.toolbar
.set_property('visible', new_value
)
1412 elif name
== 'videoplayer':
1413 self
.config
.video_played_dbus
= False
1414 elif name
== 'player':
1415 self
.config
.audio_played_dbus
= False
1416 elif name
== 'episode_list_descriptions':
1417 self
.update_episode_list_model()
1418 elif name
== 'episode_list_thumbnails':
1419 self
.update_episode_list_icons(all
=True)
1420 elif name
== 'rotation_mode':
1421 self
._fremantle
_rotation
.set_mode(new_value
)
1422 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1423 self
.restart_auto_update_timer()
1424 elif name
== 'podcast_list_view_all':
1425 # Force a update of the podcast list model
1426 self
.channel_list_changed
= True
1427 if gpodder
.ui
.fremantle
:
1428 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1429 while gtk
.events_pending():
1430 gtk
.main_iteration(False)
1431 self
.update_podcast_list_model()
1432 if gpodder
.ui
.fremantle
:
1433 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1435 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1436 # With get_bin_window, we get the window that contains the rows without
1437 # the header. The Y coordinate of this window will be the height of the
1438 # treeview header. This is the amount we have to subtract from the
1439 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1440 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1443 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1445 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1446 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1449 if path
is not None:
1450 model
= treeview
.get_model()
1451 iter = model
.get_iter(path
)
1452 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1454 if role
== TreeViewHelper
.ROLE_EPISODES
:
1455 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1456 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1457 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1459 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1460 if last_tooltip
is not None and last_tooltip
!= id:
1461 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1463 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1465 if role
== TreeViewHelper
.ROLE_EPISODES
:
1466 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1468 tooltip
.set_text(description
)
1471 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1472 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1475 channel
.request_save_dir_size()
1476 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1477 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1479 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1480 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1481 table
= gtk
.Table(rows
=3, columns
=3)
1482 table
.set_row_spacings(5)
1483 table
.set_col_spacings(5)
1484 table
.set_border_width(5)
1486 heading
= gtk
.Label()
1487 heading
.set_alignment(0, 1)
1488 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1489 table
.attach(heading
, 0, 1, 0, 1)
1490 size_info
= gtk
.Label()
1491 size_info
.set_alignment(1, 1)
1492 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1493 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1494 table
.attach(size_info
, 2, 3, 0, 1)
1496 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1498 if len(channel
.description
) < 500:
1499 description
= channel
.description
1501 pos
= channel
.description
.find('\n\n')
1502 if pos
== -1 or pos
> 500:
1503 description
= channel
.description
[:498]+'[...]'
1505 description
= channel
.description
[:pos
]
1507 description
= gtk
.Label(description
)
1509 description
.set_markup(error_str
)
1510 description
.set_alignment(0, 0)
1511 description
.set_line_wrap(True)
1512 table
.attach(description
, 0, 3, 2, 3)
1515 tooltip
.set_custom(table
)
1519 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1522 def treeview_allow_tooltips(self
, treeview
, allow
):
1523 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1525 def update_m3u_playlist_clicked(self
, widget
):
1526 if self
.active_channel
is not None:
1527 self
.active_channel
.update_m3u_playlist()
1528 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1530 def treeview_handle_context_menu_click(self
, treeview
, event
):
1531 x
, y
= int(event
.x
), int(event
.y
)
1532 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1534 selection
= treeview
.get_selection()
1535 model
, paths
= selection
.get_selected_rows()
1537 if path
is None or (path
not in paths
and \
1538 event
.button
== self
.context_menu_mouse_button
):
1539 # We have right-clicked, but not into the selection,
1540 # assume we don't want to operate on the selection
1543 if path
is not None and not paths
and \
1544 event
.button
== self
.context_menu_mouse_button
:
1545 # No selection or clicked outside selection;
1546 # select the single item where we clicked
1547 treeview
.grab_focus()
1548 treeview
.set_cursor(path
, column
, 0)
1552 # Unselect any remaining items (clicked elsewhere)
1553 if hasattr(treeview
, 'is_rubber_banding_active'):
1554 if not treeview
.is_rubber_banding_active():
1555 selection
.unselect_all()
1557 selection
.unselect_all()
1561 def downloads_list_get_selection(self
, model
=None, paths
=None):
1562 if model
is None and paths
is None:
1563 selection
= self
.treeDownloads
.get_selection()
1564 model
, paths
= selection
.get_selected_rows()
1566 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1567 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1568 model
.get_value(model
.get_iter(path
), \
1569 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1571 for row_reference
, task
in selected_tasks
:
1572 if task
.status
!= download
.DownloadTask
.QUEUED
:
1574 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1575 download
.DownloadTask
.FAILED
, \
1576 download
.DownloadTask
.CANCELLED
):
1578 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1579 download
.DownloadTask
.QUEUED
, \
1580 download
.DownloadTask
.DOWNLOADING
):
1582 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1583 download
.DownloadTask
.DOWNLOADING
):
1585 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1586 download
.DownloadTask
.FAILED
, \
1587 download
.DownloadTask
.DONE
):
1590 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1592 def downloads_finished(self
, download_tasks_seen
):
1593 # FIXME: Filter all tasks that have already been reported
1594 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1595 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1597 if finished_downloads
and failed_downloads
:
1598 message
= self
.format_episode_list(finished_downloads
, 5)
1599 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1600 message
+= self
.format_episode_list(failed_downloads
, 5)
1601 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1602 elif finished_downloads
:
1603 message
= self
.format_episode_list(finished_downloads
)
1604 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1605 elif failed_downloads
:
1606 message
= self
.format_episode_list(failed_downloads
)
1607 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1609 # Open torrent files right after download (bug 1029)
1610 if self
.config
.open_torrent_after_download
:
1611 for task
in download_tasks_seen
:
1612 if task
.status
!= task
.DONE
:
1615 episode
= task
.episode
1616 if episode
.mimetype
!= 'application/x-bittorrent':
1619 self
.playback_episodes([episode
])
1622 def format_episode_list(self
, episode_list
, max_episodes
=10):
1624 Format a list of episode names for notifications
1626 Will truncate long episode names and limit the amount of
1627 episodes displayed (max_episodes=10).
1629 The episode_list parameter should be a list of strings.
1631 MAX_TITLE_LENGTH
= 100
1634 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1635 if len(title
) > MAX_TITLE_LENGTH
:
1636 middle
= (MAX_TITLE_LENGTH
/2)-2
1637 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1638 result
.append(saxutils
.escape(title
))
1641 more_episodes
= len(episode_list
) - max_episodes
1642 if more_episodes
> 0:
1643 result
.append('(...')
1644 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1645 result
.append('...)')
1647 return (''.join(result
)).strip()
1649 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1650 episode_urls
= set()
1651 model
= self
.treeDownloads
.get_model()
1652 for row_reference
, task
in tasks
:
1653 if status
== download
.DownloadTask
.QUEUED
:
1654 # Only queue task when its paused/failed/cancelled (or forced)
1655 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1656 self
.download_queue_manager
.add_task(task
, force_start
)
1657 self
.enable_download_list_update()
1658 elif status
== download
.DownloadTask
.CANCELLED
:
1659 # Cancelling a download allowed when downloading/queued
1660 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1661 task
.status
= status
1662 # Cancelling paused downloads requires a call to .run()
1663 elif task
.status
== task
.PAUSED
:
1664 task
.status
= status
1665 # Call run, so the partial file gets deleted
1667 elif status
== download
.DownloadTask
.PAUSED
:
1668 # Pausing a download only when queued/downloading
1669 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1670 task
.status
= status
1671 elif status
is None:
1672 # Remove the selected task - cancel downloading/queued tasks
1673 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1674 task
.status
= task
.CANCELLED
1675 model
.remove(model
.get_iter(row_reference
.get_path()))
1676 # Remember the URL, so we can tell the UI to update
1678 # We don't "see" this task anymore - remove it;
1679 # this is needed, so update_episode_list_icons()
1680 # below gets the correct list of "seen" tasks
1681 self
.download_tasks_seen
.remove(task
)
1682 except KeyError, key_error
:
1683 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1684 episode_urls
.add(task
.url
)
1685 # Tell the task that it has been removed (so it can clean up)
1686 task
.removed_from_list()
1688 # We can (hopefully) simply set the task status here
1689 task
.status
= status
1690 # Tell the podcasts tab to update icons for our removed podcasts
1691 self
.update_episode_list_icons(episode_urls
)
1692 # Update the tab title and downloads list
1693 self
.update_downloads_list()
1695 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1696 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1698 if not hasattr(treeview
, 'is_rubber_banding_active'):
1701 return not treeview
.is_rubber_banding_active()
1703 if event
.button
== self
.context_menu_mouse_button
:
1704 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1705 self
.downloads_list_get_selection(model
, paths
)
1707 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1708 # This creates a menu item for selection-wide actions
1709 item
= gtk
.ImageMenuItem(label
)
1710 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1711 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1712 item
.set_sensitive(sensitive
)
1713 return self
.set_finger_friendly(item
)
1717 item
= gtk
.ImageMenuItem(_('Episode details'))
1718 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1719 if len(selected_tasks
) == 1:
1720 row_reference
, task
= selected_tasks
[0]
1721 episode
= task
.episode
1722 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1724 item
.set_sensitive(False)
1725 menu
.append(self
.set_finger_friendly(item
))
1726 menu
.append(gtk
.SeparatorMenuItem())
1728 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1730 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1731 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1732 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1733 menu
.append(gtk
.SeparatorMenuItem())
1734 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1736 if gpodder
.ui
.maemo
:
1737 # Because we open the popup on left-click for Maemo,
1738 # we also include a non-action to close the menu
1739 menu
.append(gtk
.SeparatorMenuItem())
1740 item
= gtk
.ImageMenuItem(_('Close this menu'))
1741 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1743 menu
.append(self
.set_finger_friendly(item
))
1746 menu
.popup(None, None, None, event
.button
, event
.time
)
1749 def treeview_channels_show_context_menu(self
, treeview
, event
):
1750 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1754 # Check for valid channel id, if there's no id then
1755 # assume that it is a proxy channel or equivalent
1756 # and cannot be operated with right click
1757 if self
.active_channel
.id is None:
1760 if event
.button
== 3:
1765 item
= gtk
.ImageMenuItem( _('Update podcast'))
1766 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1767 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1768 item
.set_sensitive(not self
.updating_feed_cache
)
1771 menu
.append(gtk
.SeparatorMenuItem())
1773 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1774 item
.set_active(self
.active_channel
.channel_is_locked
)
1775 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1776 menu
.append(self
.set_finger_friendly(item
))
1778 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1779 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1780 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1783 if self
.config
.device_type
!= 'none':
1784 item
= gtk
.MenuItem(_('Synchronize to device'))
1785 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1788 menu
.append( gtk
.SeparatorMenuItem())
1790 item
= gtk
.ImageMenuItem(_('Podcast details'))
1791 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1792 item
.connect('activate', self
.on_itemEditChannel_activate
)
1796 # Disable tooltips while we are showing the menu, so
1797 # the tooltip will not appear over the menu
1798 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1799 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1800 menu
.popup( None, None, None, event
.button
, event
.time
)
1804 def on_itemClose_activate(self
, widget
):
1805 if self
.tray_icon
is not None:
1806 self
.iconify_main_window()
1808 self
.on_gPodder_delete_event(widget
)
1810 def cover_file_removed(self
, channel_url
):
1812 The Cover Downloader calls this when a previously-
1813 available cover has been removed from the disk. We
1814 have to update our model to reflect this change.
1816 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1818 def cover_download_finished(self
, channel
, pixbuf
):
1820 The Cover Downloader calls this when it has finished
1821 downloading (or registering, if already downloaded)
1822 a new channel cover, which is ready for displaying.
1824 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1826 def save_episodes_as_file(self
, episodes
):
1827 for episode
in episodes
:
1828 self
.save_episode_as_file(episode
)
1830 def save_episode_as_file(self
, episode
):
1831 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1832 if episode
.was_downloaded(and_exists
=True):
1833 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1834 copy_from
= episode
.local_filename(create
=False)
1835 assert copy_from
is not None
1836 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1837 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1838 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1840 def copy_episodes_bluetooth(self
, episodes
):
1841 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1843 if gpodder
.ui
.maemo
:
1844 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1845 for e
in episodes_to_copy
])
1848 def convert_and_send_thread(episode
):
1849 for episode
in episodes
:
1850 filename
= episode
.local_filename(create
=False)
1851 assert filename
is not None
1852 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1853 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1854 (base
, ext
) = os
.path
.splitext(filename
)
1855 if not destfile
.endswith(ext
):
1859 shutil
.copyfile(filename
, destfile
)
1860 util
.bluetooth_send_file(destfile
)
1862 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1863 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1865 util
.delete_file(destfile
)
1867 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1869 def get_device_name(self
):
1870 if self
.config
.device_type
== 'ipod':
1872 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1873 return _('MP3 player')
1875 return '(unknown device)'
1877 def _treeview_button_released(self
, treeview
, event
):
1878 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1879 dy
= int(abs(event
.y
-ypos
))
1880 dx
= int(event
.x
-xpos
)
1882 selection
= treeview
.get_selection()
1883 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1884 if path
is None or dy
> 30:
1885 return (False, dx
, dy
)
1887 path
, column
, x
, y
= path
1888 selection
.select_path(path
)
1889 treeview
.set_cursor(path
)
1890 treeview
.grab_focus()
1892 return (True, dx
, dy
)
1894 def treeview_channels_handle_gestures(self
, treeview
, event
):
1895 if self
.currently_updating
:
1898 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1901 if self
.config
.maemo_enable_gestures
:
1903 self
.on_itemUpdateChannel_activate()
1905 self
.on_itemEditChannel_activate(treeview
)
1909 def treeview_available_handle_gestures(self
, treeview
, event
):
1910 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1913 if self
.config
.maemo_enable_gestures
:
1915 self
.on_playback_selected_episodes(None)
1918 self
.on_shownotes_selected_episodes(None)
1921 # Pass the event to the context menu handler for treeAvailable
1922 self
.treeview_available_show_context_menu(treeview
, event
)
1926 def treeview_available_show_context_menu(self
, treeview
, event
):
1927 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1929 if not hasattr(treeview
, 'is_rubber_banding_active'):
1932 return not treeview
.is_rubber_banding_active()
1934 if event
.button
== self
.context_menu_mouse_button
:
1935 episodes
= self
.get_selected_episodes()
1936 any_locked
= any(e
.is_locked
for e
in episodes
)
1937 any_played
= any(e
.is_played
for e
in episodes
)
1938 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1939 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1940 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1944 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1946 if open_instead_of_play
:
1947 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1949 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1951 item
= gtk
.ImageMenuItem(_('Stream'))
1952 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1954 item
.set_sensitive(can_play
and not downloading
)
1955 item
.connect('activate', self
.on_playback_selected_episodes
)
1956 menu
.append(self
.set_finger_friendly(item
))
1959 item
= gtk
.ImageMenuItem(_('Download'))
1960 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1961 item
.set_sensitive(can_download
)
1962 item
.connect('activate', self
.on_download_selected_episodes
)
1963 menu
.append(self
.set_finger_friendly(item
))
1965 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1966 item
.connect('activate', self
.on_item_cancel_download_activate
)
1967 menu
.append(self
.set_finger_friendly(item
))
1969 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1970 item
.set_sensitive(can_delete
)
1971 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1972 menu
.append(self
.set_finger_friendly(item
))
1976 # Ok, this probably makes sense to only display for downloaded files
1978 menu
.append(gtk
.SeparatorMenuItem())
1979 share_item
= gtk
.MenuItem(_('Send to'))
1980 menu
.append(self
.set_finger_friendly(share_item
))
1981 share_menu
= gtk
.Menu()
1983 item
= gtk
.ImageMenuItem(_('Local folder'))
1984 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
1985 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
1986 share_menu
.append(self
.set_finger_friendly(item
))
1987 if self
.bluetooth_available
:
1988 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
1989 if gpodder
.ui
.maemo
:
1990 icon_name
= ICON('qgn_list_filesys_bluetooth')
1992 icon_name
= ICON('bluetooth')
1993 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
1994 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
1995 share_menu
.append(self
.set_finger_friendly(item
))
1997 item
= gtk
.ImageMenuItem(self
.get_device_name())
1998 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
1999 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2000 share_menu
.append(self
.set_finger_friendly(item
))
2002 share_item
.set_submenu(share_menu
)
2004 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2005 menu
.append(gtk
.SeparatorMenuItem())
2007 item
= gtk
.CheckMenuItem(_('New'))
2008 item
.set_active(True)
2009 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2010 menu
.append(self
.set_finger_friendly(item
))
2012 item
= gtk
.CheckMenuItem(_('New'))
2013 item
.set_active(False)
2014 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2015 menu
.append(self
.set_finger_friendly(item
))
2018 item
= gtk
.CheckMenuItem(_('Played'))
2019 item
.set_active(any_played
)
2020 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2021 menu
.append(self
.set_finger_friendly(item
))
2023 item
= gtk
.CheckMenuItem(_('Keep episode'))
2024 item
.set_active(any_locked
)
2025 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2026 menu
.append(self
.set_finger_friendly(item
))
2028 menu
.append(gtk
.SeparatorMenuItem())
2029 # Single item, add episode information menu item
2030 item
= gtk
.ImageMenuItem(_('Episode details'))
2031 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2032 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2033 menu
.append(self
.set_finger_friendly(item
))
2035 if gpodder
.ui
.maemo
:
2036 # Because we open the popup on left-click for Maemo,
2037 # we also include a non-action to close the menu
2038 menu
.append(gtk
.SeparatorMenuItem())
2039 item
= gtk
.ImageMenuItem(_('Close this menu'))
2040 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2041 menu
.append(self
.set_finger_friendly(item
))
2044 # Disable tooltips while we are showing the menu, so
2045 # the tooltip will not appear over the menu
2046 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2047 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2048 menu
.popup( None, None, None, event
.button
, event
.time
)
2052 def set_title(self
, new_title
):
2053 if not gpodder
.ui
.fremantle
:
2054 self
.default_title
= new_title
2055 self
.gPodder
.set_title(new_title
)
2057 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2059 Updates the status icons in the episode list.
2061 If urls is given, it should be a list of URLs
2062 of episodes that should be updated.
2064 If urls is None, set ONE OF selected, all to
2065 True (the former updates just the selected
2066 episodes and the latter updates all episodes).
2068 additional_args
= (self
.episode_is_downloading
, \
2069 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2070 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2072 if urls
is not None:
2073 # We have a list of URLs to walk through
2074 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2075 elif selected
and not all
:
2076 # We should update all selected episodes
2077 selection
= self
.treeAvailable
.get_selection()
2078 model
, paths
= selection
.get_selected_rows()
2079 for path
in reversed(paths
):
2080 iter = model
.get_iter(path
)
2081 self
.episode_list_model
.update_by_filter_iter(iter, \
2083 elif all
and not selected
:
2084 # We update all (even the filter-hidden) episodes
2085 self
.episode_list_model
.update_all(*additional_args
)
2087 # Wrong/invalid call - have to specify at least one parameter
2088 raise ValueError('Invalid call to update_episode_list_icons')
2090 def episode_list_status_changed(self
, episodes
):
2091 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2092 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2095 def clean_up_downloads(self
, delete_partial
=False):
2096 # Clean up temporary files left behind by old gPodder versions
2097 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2100 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2102 for tempfile
in temporary_files
:
2103 util
.delete_file(tempfile
)
2105 # Clean up empty download folders and abandoned download folders
2106 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2107 for ddir
in download_dirs
:
2108 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2109 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2110 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2111 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2112 shutil
.rmtree(ddir
, ignore_errors
=True)
2114 def streaming_possible(self
):
2115 if gpodder
.ui
.desktop
:
2116 # User has to have a media player set on the Desktop, or else we
2117 # would probably open the browser when giving a URL to xdg-open..
2118 return (self
.config
.player
and self
.config
.player
!= 'default')
2119 elif gpodder
.ui
.maemo
:
2120 # On Maemo, the default is to use the Nokia Media Player, which is
2121 # already able to deal with HTTP URLs the right way, so we
2122 # unconditionally enable streaming always on Maemo
2127 def playback_episodes_for_real(self
, episodes
):
2128 groups
= collections
.defaultdict(list)
2129 for episode
in episodes
:
2130 file_type
= episode
.file_type()
2131 if file_type
== 'video' and self
.config
.videoplayer
and \
2132 self
.config
.videoplayer
!= 'default':
2133 player
= self
.config
.videoplayer
2134 if gpodder
.ui
.diablo
:
2135 # Use the wrapper script if it's installed to crop 3GP YouTube
2136 # videos to fit the screen (looks much nicer than w/ black border)
2137 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2138 player
= 'gpodder-mplayer'
2139 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2140 player
= 'mplayer -fs %F'
2141 elif file_type
== 'audio' and self
.config
.player
and \
2142 self
.config
.player
!= 'default':
2143 player
= self
.config
.player
2147 if file_type
not in ('audio', 'video') or \
2148 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2149 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2150 # Mark episode as played in the database
2151 episode
.mark(is_played
=True)
2152 self
.mygpo_client
.on_playback([episode
])
2154 filename
= episode
.local_filename(create
=False)
2155 if filename
is None or not os
.path
.exists(filename
):
2156 filename
= episode
.url
2157 if youtube
.is_video_link(filename
):
2158 fmt_id
= self
.config
.youtube_preferred_fmt_id
2159 if gpodder
.ui
.fremantle
:
2161 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2163 # Determine the playback resume position - if the file
2164 # was played 100%, we simply start from the beginning
2165 resume_position
= episode
.current_position
2166 if resume_position
== episode
.total_time
:
2169 if gpodder
.ui
.fremantle
:
2170 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2172 # If Panucci is configured, use D-Bus on Maemo to call it
2173 if player
== 'panucci':
2175 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2176 PANUCCI_PATH
= '/panucciInterface'
2177 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2178 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2179 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2181 def on_reply(*args
):
2184 def error_handler(filename
, err
):
2185 log('Exception in D-Bus call: %s', str(err
), \
2188 # Fallback: use the command line client
2189 for command
in util
.format_desktop_command('panucci', \
2191 log('Executing: %s', repr(command
), sender
=self
)
2192 subprocess
.Popen(command
)
2194 on_error
= lambda err
: error_handler(filename
, err
)
2196 # This method only exists in Panucci > 0.9 ('new Panucci')
2197 i
.playback_from(filename
, resume_position
, \
2198 reply_handler
=on_reply
, error_handler
=on_error
)
2200 continue # This file was handled by the D-Bus call
2201 except Exception, e
:
2202 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2203 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2205 MEDIABOX_NAME
= 'de.pycage.mediabox'
2206 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2207 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2208 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2209 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2211 def on_reply(*args
):
2215 log('Exception in D-Bus call: %s', str(err
), \
2218 i
.load(filename
, '%s/x-unknown' % file_type
, \
2219 reply_handler
=on_reply
, error_handler
=on_error
)
2221 continue # This file was handled by the D-Bus call
2222 except Exception, e
:
2223 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2225 groups
[player
].append(filename
)
2227 # Open episodes with system default player
2228 if 'default' in groups
:
2229 if gpodder
.ui
.maemo
:
2230 # The Nokia Media Player app does not support receiving multiple
2231 # file names via D-Bus, so we simply place all file names into a
2232 # temporary M3U playlist and open that with the Media Player.
2233 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2234 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2235 util
.gui_open(m3u_filename
)
2237 for filename
in groups
['default']:
2238 log('Opening with system default: %s', filename
, sender
=self
)
2239 util
.gui_open(filename
)
2240 del groups
['default']
2241 elif gpodder
.ui
.maemo
and groups
:
2242 # When on Maemo and not opening with default, show a notification
2243 # (no startup notification for Panucci / MPlayer yet...)
2244 if len(episodes
) == 1:
2245 text
= _('Opening %s') % episodes
[0].title
2247 count
= len(episodes
)
2248 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2250 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2252 def destroy_banner_later(banner
):
2255 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2257 # For each type now, go and create play commands
2258 for group
in groups
:
2259 for command
in util
.format_desktop_command(group
, groups
[group
]):
2260 log('Executing: %s', repr(command
), sender
=self
)
2261 subprocess
.Popen(command
)
2263 # Persist episode status changes to the database
2266 # Flush updated episode status
2267 self
.mygpo_client
.flush()
2269 def playback_episodes(self
, episodes
):
2270 # We need to create a list, because we run through it more than once
2271 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2272 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2275 self
.playback_episodes_for_real(episodes
)
2276 except Exception, e
:
2277 log('Error in playback!', sender
=self
, traceback
=True)
2278 if gpodder
.ui
.desktop
:
2279 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2280 _('Error opening player'), widget
=self
.toolPreferences
)
2282 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2284 channel_urls
= set()
2285 episode_urls
= set()
2286 for episode
in episodes
:
2287 channel_urls
.add(episode
.channel
.url
)
2288 episode_urls
.add(episode
.url
)
2289 self
.update_episode_list_icons(episode_urls
)
2290 self
.update_podcast_list_model(channel_urls
)
2292 def play_or_download(self
):
2293 if not gpodder
.ui
.fremantle
:
2294 if self
.wNotebook
.get_current_page() > 0:
2295 if gpodder
.ui
.desktop
:
2296 self
.toolCancel
.set_sensitive(True)
2299 if self
.currently_updating
:
2300 return (False, False, False, False, False, False)
2302 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2303 ( is_played
, is_locked
) = (False,)*2
2305 open_instead_of_play
= False
2307 selection
= self
.treeAvailable
.get_selection()
2308 if selection
.count_selected_rows() > 0:
2309 (model
, paths
) = selection
.get_selected_rows()
2313 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2314 except TypeError, te
:
2315 log('Invalid episode at path %s', str(path
), sender
=self
)
2318 if episode
.file_type() not in ('audio', 'video'):
2319 open_instead_of_play
= True
2321 if episode
.was_downloaded():
2322 can_play
= episode
.was_downloaded(and_exists
=True)
2323 is_played
= episode
.is_played
2324 is_locked
= episode
.is_locked
2328 if self
.episode_is_downloading(episode
):
2333 can_download
= can_download
and not can_cancel
2334 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2335 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2336 can_delete
= not can_cancel
2338 if gpodder
.ui
.desktop
:
2339 if open_instead_of_play
:
2340 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2342 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2343 self
.toolPlay
.set_sensitive( can_play
)
2344 self
.toolDownload
.set_sensitive( can_download
)
2345 self
.toolTransfer
.set_sensitive( can_transfer
)
2346 self
.toolCancel
.set_sensitive( can_cancel
)
2348 if not gpodder
.ui
.fremantle
:
2349 self
.item_cancel_download
.set_sensitive(can_cancel
)
2350 self
.itemDownloadSelected
.set_sensitive(can_download
)
2351 self
.itemOpenSelected
.set_sensitive(can_play
)
2352 self
.itemPlaySelected
.set_sensitive(can_play
)
2353 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2354 self
.item_toggle_played
.set_sensitive(can_play
)
2355 self
.item_toggle_lock
.set_sensitive(can_play
)
2356 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2357 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2359 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2361 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2362 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2364 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2365 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2367 def episode_new_status_changed(self
, urls
):
2368 self
.update_podcast_list_model()
2369 self
.update_episode_list_icons(urls
)
2371 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2372 """Update the podcast list treeview model
2374 If urls is given, it should list the URLs of each
2375 podcast that has to be updated in the list.
2377 If selected is True, only update the model contents
2378 for the currently-selected podcast - nothing more.
2380 The caller can optionally specify "select_url",
2381 which is the URL of the podcast that is to be
2382 selected in the list after the update is complete.
2383 This only works if the podcast list has to be
2384 reloaded; i.e. something has been added or removed
2385 since the last update of the podcast list).
2387 selection
= self
.treeChannels
.get_selection()
2388 model
, iter = selection
.get_selected()
2390 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2391 # Update "all episodes" view in any case (if enabled)
2392 self
.podcast_list_model
.update_first_row()
2395 # very cheap! only update selected channel
2396 if iter is not None:
2397 # If we have selected the "all episodes" view, we have
2398 # to update all channels for selected episodes:
2399 if self
.config
.podcast_list_view_all
and \
2400 self
.podcast_list_model
.iter_is_first_row(iter):
2401 urls
= self
.get_podcast_urls_from_selected_episodes()
2402 self
.podcast_list_model
.update_by_urls(urls
)
2404 # Otherwise just update the selected row (a podcast)
2405 self
.podcast_list_model
.update_by_filter_iter(iter)
2406 elif not self
.channel_list_changed
:
2407 # we can keep the model, but have to update some
2409 # still cheaper than reloading the whole list
2410 self
.podcast_list_model
.update_all()
2412 # ok, we got a bunch of urls to update
2413 self
.podcast_list_model
.update_by_urls(urls
)
2415 if model
and iter and select_url
is None:
2416 # Get the URL of the currently-selected podcast
2417 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2419 # Update the podcast list model with new channels
2420 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2423 selected_iter
= model
.get_iter_first()
2424 # Find the previously-selected URL in the new
2425 # model if we have an URL (else select first)
2426 if select_url
is not None:
2427 pos
= model
.get_iter_first()
2428 while pos
is not None:
2429 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2430 if url
== select_url
:
2433 pos
= model
.iter_next(pos
)
2435 if not gpodder
.ui
.fremantle
:
2436 if selected_iter
is not None:
2437 selection
.select_iter(selected_iter
)
2438 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2440 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2441 self
.channel_list_changed
= False
2443 def episode_is_downloading(self
, episode
):
2444 """Returns True if the given episode is being downloaded at the moment"""
2448 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2450 def update_episode_list_model(self
):
2451 if self
.channels
and self
.active_channel
is not None:
2452 if gpodder
.ui
.fremantle
:
2453 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2455 self
.currently_updating
= True
2456 self
.episode_list_model
.clear()
2457 self
.episode_list_model
.reset_update_progress()
2458 self
.treeAvailable
.set_model(self
.empty_episode_list_model
)
2459 def do_update_episode_list_model():
2460 additional_args
= (self
.episode_is_downloading
, \
2461 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2462 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
, \
2464 self
.episode_list_model
.add_from_channel(self
.active_channel
, *additional_args
)
2466 def on_episode_list_model_updated():
2467 if gpodder
.ui
.fremantle
:
2468 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2469 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
2470 self
.treeAvailable
.columns_autosize()
2471 self
.currently_updating
= False
2472 self
.play_or_download()
2473 util
.idle_add(on_episode_list_model_updated
)
2474 threading
.Thread(target
=do_update_episode_list_model
).start()
2476 self
.episode_list_model
.clear()
2478 @dbus.service
.method(gpodder
.dbus_interface
)
2479 def offer_new_episodes(self
, channels
=None):
2480 if gpodder
.ui
.fremantle
:
2481 # Assume that when this function is called that the
2482 # notification is not shown anymore (Maemo bug 11345)
2483 self
._fremantle
_notification
_visible
= False
2485 new_episodes
= self
.get_new_episodes(channels
)
2487 self
.new_episodes_show(new_episodes
)
2491 def add_podcast_list(self
, urls
, auth_tokens
=None):
2492 """Subscribe to a list of podcast given their URLs
2494 If auth_tokens is given, it should be a dictionary
2495 mapping URLs to (username, password) tuples."""
2497 if auth_tokens
is None:
2500 # Sort and split the URL list into five buckets
2501 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2502 for input_url
in urls
:
2503 url
= util
.normalize_feed_url(input_url
)
2505 # Fail this one because the URL is not valid
2506 failed
.append(input_url
)
2507 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2508 # A podcast already exists in the list for this URL
2509 existing
.append(url
)
2511 # This URL has survived the first round - queue for add
2513 if url
!= input_url
and input_url
in auth_tokens
:
2514 auth_tokens
[url
] = auth_tokens
[input_url
]
2519 progress
= ProgressIndicator(_('Adding podcasts'), \
2520 _('Please wait while episode information is downloaded.'), \
2521 parent
=self
.get_dialog_parent())
2523 def on_after_update():
2524 progress
.on_finished()
2525 # Report already-existing subscriptions to the user
2527 title
= _('Existing subscriptions skipped')
2528 message
= _('You are already subscribed to these podcasts:') \
2529 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2530 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2532 # Report subscriptions that require authentication
2536 title
= _('Podcast requires authentication')
2537 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2538 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2540 retry_podcasts
[url
] = auth_tokens
2542 # Stop asking the user for more login data
2545 error_messages
[url
] = _('Authentication failed')
2549 # If we have authentication data to retry, do so here
2551 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2553 # Report website redirections
2554 for url
in redirections
:
2555 title
= _('Website redirection detected')
2556 message
= _('The URL %(url)s redirects to %(target)s.') \
2557 + '\n\n' + _('Do you want to visit the website now?')
2558 message
= message
% {'url': url
, 'target': redirections
[url
]}
2559 if self
.show_confirmation(message
, title
):
2560 util
.open_website(url
)
2564 # Report failed subscriptions to the user
2566 title
= _('Could not add some podcasts')
2567 message
= _('Some podcasts could not be added to your list:') \
2568 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2569 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2570 self
.show_message(message
, title
, important
=True)
2572 # Upload subscription changes to gpodder.net
2573 self
.mygpo_client
.on_subscribe(worked
)
2575 # If at least one podcast has been added, save and update all
2576 if self
.channel_list_changed
:
2577 # Fix URLs if mygpo has rewritten them
2578 self
.rewrite_urls_mygpo()
2580 self
.save_channels_opml()
2582 # If only one podcast was added, select it after the update
2583 if len(worked
) == 1:
2588 # Update the list of subscribed podcasts
2589 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2590 self
.update_podcasts_tab()
2592 # Offer to download new episodes
2594 for podcast
in self
.channels
:
2595 if podcast
.url
in worked
:
2596 episodes
.extend(podcast
.get_all_episodes())
2599 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2601 self
.new_episodes_show(episodes
, \
2602 selected
=[e
.check_is_new() for e
in episodes
])
2606 # After the initial sorting and splitting, try all queued podcasts
2607 length
= len(queued
)
2608 for index
, url
in enumerate(queued
):
2609 progress
.on_progress(float(index
)/float(length
))
2610 progress
.on_message(url
)
2611 log('QUEUE RUNNER: %s', url
, sender
=self
)
2613 # The URL is valid and does not exist already - subscribe!
2614 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2615 authentication_tokens
=auth_tokens
.get(url
, None), \
2616 max_episodes
=self
.config
.max_episodes_per_feed
, \
2617 download_dir
=self
.config
.download_dir
, \
2618 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2619 mimetype_prefs
=self
.config
.mimetype_prefs
)
2622 username
, password
= util
.username_password_from_url(url
)
2623 except ValueError, ve
:
2624 username
, password
= (None, None)
2626 if username
is not None and channel
.username
is None and \
2627 password
is not None and channel
.password
is None:
2628 channel
.username
= username
2629 channel
.password
= password
2632 self
._update
_cover
(channel
)
2633 except feedcore
.AuthenticationRequired
:
2634 if url
in auth_tokens
:
2635 # Fail for wrong authentication data
2636 error_messages
[url
] = _('Authentication failed')
2639 # Queue for login dialog later
2642 except feedcore
.WifiLogin
, error
:
2643 redirections
[url
] = error
.data
2645 error_messages
[url
] = _('Redirection detected')
2647 except Exception, e
:
2648 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2649 error_messages
[url
] = str(e
)
2653 assert channel
is not None
2654 worked
.append(channel
.url
)
2655 self
.channels
.append(channel
)
2656 self
.channel_list_changed
= True
2657 util
.idle_add(on_after_update
)
2658 threading
.Thread(target
=thread_proc
).start()
2660 def save_channels_opml(self
):
2661 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2662 return exporter
.write(self
.channels
)
2664 def find_episode(self
, podcast_url
, episode_url
):
2665 """Find an episode given its podcast and episode URL
2667 The function will return a PodcastEpisode object if
2668 the episode is found, or None if it's not found.
2670 for podcast
in self
.channels
:
2671 if podcast_url
== podcast
.url
:
2672 for episode
in podcast
.get_all_episodes():
2673 if episode_url
== episode
.url
:
2678 def process_received_episode_actions(self
, updated_urls
):
2679 """Process/merge episode actions from gpodder.net
2681 This function will merge all changes received from
2682 the server to the local database and update the
2683 status of the affected episodes as necessary.
2685 indicator
= ProgressIndicator(_('Merging episode actions'), \
2686 _('Episode actions from gpodder.net are merged.'), \
2687 False, self
.get_dialog_parent())
2689 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2690 if action
.action
== 'play':
2691 episode
= self
.find_episode(action
.podcast_url
, \
2694 if episode
is not None:
2695 log('Play action for %s', episode
.url
, sender
=self
)
2696 episode
.mark(is_played
=True)
2698 if action
.timestamp
> episode
.current_position_updated
:
2699 log('Updating position for %s', episode
.url
, sender
=self
)
2700 episode
.current_position
= action
.position
2701 episode
.current_position_updated
= action
.timestamp
2704 log('Updating total time for %s', episode
.url
, sender
=self
)
2705 episode
.total_time
= action
.total
2708 elif action
.action
== 'delete':
2709 episode
= self
.find_episode(action
.podcast_url
, \
2712 if episode
is not None:
2713 if not episode
.was_downloaded(and_exists
=True):
2714 # Set the episode to a "deleted" state
2715 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2716 episode
.delete_from_disk()
2719 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2720 gtk
.main_iteration(False)
2722 indicator
.on_finished()
2726 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2728 self
.updating_feed_cache
= False
2730 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2732 # Process received episode actions for all updated URLs
2733 self
.process_received_episode_actions(updated_urls
)
2735 self
.channel_list_changed
= True
2736 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2738 # Only search for new episodes in podcasts that have been
2739 # updated, not in other podcasts (for single-feed updates)
2740 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2742 if gpodder
.ui
.fremantle
:
2743 self
.button_subscribe
.set_sensitive(True)
2744 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2745 self
.ICON_GENERAL_REFRESH
, gtk
.ICON_SIZE_BUTTON
))
2746 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2747 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2748 self
.update_podcasts_tab()
2749 self
.update_episode_list_model()
2750 if self
.feed_cache_update_cancelled
:
2754 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2755 # New episodes found, but we should do nothing
2756 self
.show_message(_('New episodes are available.'))
2757 elif self
.config
.auto_download
== 'always':
2758 count
= len(episodes
)
2759 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2760 self
.show_message(title
)
2761 self
.download_episode_list(episodes
)
2762 elif self
.config
.auto_download
== 'queue':
2763 self
.show_message(_('New episodes have been added to the download list.'))
2764 self
.download_episode_list_paused(episodes
)
2765 elif not self
._fremantle
_notification
_visible
:
2768 pynotify
.init('gPodder')
2769 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2770 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2771 n
.set_hint('dbus-callback-default', ' '.join([
2772 gpodder
.dbus_bus_name
,
2773 gpodder
.dbus_gui_object_path
,
2774 gpodder
.dbus_interface
,
2775 'offer_new_episodes',
2777 n
.set_category('gpodder-new-episodes')
2779 self
._fremantle
_notification
_visible
= True
2780 except Exception, e
:
2781 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2782 self
.new_episodes_show(episodes
)
2783 self
._fremantle
_notification
_visible
= False
2784 elif not self
.config
.auto_update_feeds
:
2785 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2789 self
.tray_icon
.set_status()
2791 if self
.feed_cache_update_cancelled
:
2792 # The user decided to abort the feed update
2793 self
.show_update_feeds_buttons()
2795 # Nothing new here - but inform the user
2796 self
.pbFeedUpdate
.set_fraction(1.0)
2797 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2798 self
.feed_cache_update_cancelled
= True
2799 self
.btnCancelFeedUpdate
.show()
2800 self
.btnCancelFeedUpdate
.set_sensitive(True)
2801 if gpodder
.ui
.maemo
:
2802 # btnCancelFeedUpdate is a ToolButton on Maemo
2803 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2805 # btnCancelFeedUpdate is a normal gtk.Button
2806 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2808 count
= len(episodes
)
2809 # New episodes are available
2810 self
.pbFeedUpdate
.set_fraction(1.0)
2811 # Are we minimized and should we auto download?
2812 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2813 self
.download_episode_list(episodes
)
2814 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2815 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2816 self
.show_update_feeds_buttons()
2817 elif self
.config
.auto_download
== 'queue':
2818 self
.download_episode_list_paused(episodes
)
2819 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2820 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2821 self
.show_update_feeds_buttons()
2823 self
.show_update_feeds_buttons()
2824 # New episodes are available and we are not minimized
2825 if not self
.config
.do_not_show_new_episodes_dialog
:
2826 self
.new_episodes_show(episodes
, notification
=True)
2828 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2829 self
.pbFeedUpdate
.set_text(message
)
2831 def _update_cover(self
, channel
):
2832 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2833 self
.cover_downloader
.request_cover(channel
)
2835 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2836 total
= len(channels
)
2838 for updated
, channel
in enumerate(channels
):
2839 if not self
.feed_cache_update_cancelled
:
2841 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2842 mimetype_prefs
=self
.config
.mimetype_prefs
)
2843 self
._update
_cover
(channel
)
2844 except Exception, e
:
2845 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2847 message
= _('Error while updating %(url)s: %(message)s')
2849 message
= _('The feed at %(url)s could not be updated.')
2850 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2851 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2853 if self
.feed_cache_update_cancelled
:
2856 if gpodder
.ui
.fremantle
:
2857 util
.idle_add(self
.button_refresh
.set_title
, \
2858 _('%(position)d/%(total)d updated') % {'position': updated
+1, 'total': total
})
2861 # By the time we get here the update may have already been cancelled
2862 if not self
.feed_cache_update_cancelled
:
2863 def update_progress():
2864 d
= {'podcast': channel
.title
, 'position': updated
, 'total': total
}
2865 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2866 self
.pbFeedUpdate
.set_text(progression
)
2868 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2869 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2870 util
.idle_add(update_progress
)
2872 updated_urls
= [c
.url
for c
in channels
]
2873 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2875 def show_update_feeds_buttons(self
):
2876 # Make sure that the buttons for updating feeds
2877 # appear - this should happen after a feed update
2878 if gpodder
.ui
.maemo
:
2879 self
.btnUpdateSelectedFeed
.show()
2880 self
.toolFeedUpdateProgress
.hide()
2881 self
.btnCancelFeedUpdate
.hide()
2882 self
.btnCancelFeedUpdate
.set_is_important(False)
2883 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2884 self
.toolbarSpacer
.set_expand(True)
2885 self
.toolbarSpacer
.set_draw(False)
2887 self
.hboxUpdateFeeds
.hide()
2888 self
.btnUpdateFeeds
.show()
2889 self
.itemUpdate
.set_sensitive(True)
2890 self
.itemUpdateChannel
.set_sensitive(True)
2892 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2893 if not self
.feed_cache_update_cancelled
:
2894 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2895 self
.feed_cache_update_cancelled
= True
2896 self
.btnCancelFeedUpdate
.set_sensitive(False)
2898 self
.show_update_feeds_buttons()
2900 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2901 if self
.updating_feed_cache
:
2902 if gpodder
.ui
.fremantle
:
2903 self
.feed_cache_update_cancelled
= True
2906 if not force_update
:
2907 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2908 self
.channel_list_changed
= True
2909 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2912 # Fix URLs if mygpo has rewritten them
2913 self
.rewrite_urls_mygpo()
2915 self
.updating_feed_cache
= True
2917 if channels
is None:
2918 # Only update podcasts for which updates are enabled
2919 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
2921 if gpodder
.ui
.fremantle
:
2922 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2923 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2924 self
.button_refresh
.set_title(_('Updating...'))
2925 self
.button_subscribe
.set_sensitive(False)
2926 self
.button_refresh
.set_image(gtk
.image_new_from_icon_name(\
2927 self
.ICON_GENERAL_CLOSE
, gtk
.ICON_SIZE_BUTTON
))
2928 self
.feed_cache_update_cancelled
= False
2930 self
.itemUpdate
.set_sensitive(False)
2931 self
.itemUpdateChannel
.set_sensitive(False)
2934 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2936 if len(channels
) == 1:
2937 text
= _('Updating "%s"...') % channels
[0].title
2939 count
= len(channels
)
2940 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2941 self
.pbFeedUpdate
.set_text(text
)
2942 self
.pbFeedUpdate
.set_fraction(0)
2944 self
.feed_cache_update_cancelled
= False
2945 self
.btnCancelFeedUpdate
.show()
2946 self
.btnCancelFeedUpdate
.set_sensitive(True)
2947 if gpodder
.ui
.maemo
:
2948 self
.toolbarSpacer
.set_expand(False)
2949 self
.toolbarSpacer
.set_draw(True)
2950 self
.btnUpdateSelectedFeed
.hide()
2951 self
.toolFeedUpdateProgress
.show_all()
2953 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2954 self
.hboxUpdateFeeds
.show_all()
2955 self
.btnUpdateFeeds
.hide()
2957 args
= (channels
, select_url_afterwards
)
2958 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2960 def on_gPodder_delete_event(self
, widget
, *args
):
2961 """Called when the GUI wants to close the window
2962 Displays a confirmation dialog (and closes/hides gPodder)
2965 downloading
= self
.download_status_model
.are_downloads_in_progress()
2967 # Only iconify if we are using the window's "X" button,
2968 # but not when we are using "Quit" in the menu or toolbar
2969 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
2970 self
.iconify_main_window()
2971 elif self
.config
.on_quit_ask
or downloading
:
2972 if gpodder
.ui
.fremantle
:
2973 self
.close_gpodder()
2974 elif gpodder
.ui
.diablo
:
2975 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2977 self
.close_gpodder()
2980 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2981 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2982 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2984 title
= _('Quit gPodder')
2986 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
2988 message
= _('Do you really want to quit gPodder now?')
2990 dialog
.set_title(title
)
2991 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
2993 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
2994 dialog
.vbox
.pack_start(cb_ask
)
2997 quit_button
.grab_focus()
2998 result
= dialog
.run()
3001 if result
== gtk
.RESPONSE_CLOSE
:
3002 if not downloading
and cb_ask
.get_active() == True:
3003 self
.config
.on_quit_ask
= False
3004 self
.close_gpodder()
3006 self
.close_gpodder()
3010 def close_gpodder(self
):
3011 """ clean everything and exit properly
3014 if self
.save_channels_opml():
3015 pass # FIXME: Add mygpo synchronization here
3017 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3021 if self
.tray_icon
is not None:
3022 self
.tray_icon
.set_visible(False)
3024 # Notify all tasks to to carry out any clean-up actions
3025 self
.download_status_model
.tell_all_tasks_to_quit()
3027 while gtk
.events_pending():
3028 gtk
.main_iteration(False)
3035 def get_expired_episodes(self
):
3036 for channel
in self
.channels
:
3037 for episode
in channel
.get_downloaded_episodes():
3038 # Never consider locked episodes as old
3039 if episode
.is_locked
:
3042 # Never consider fresh episodes as old
3043 if episode
.age_in_days() < self
.config
.episode_old_age
:
3046 # Do not delete played episodes (except if configured)
3047 if episode
.is_played
:
3048 if not self
.config
.auto_remove_played_episodes
:
3051 # Do not delete unplayed episodes (except if configured)
3052 if not episode
.is_played
:
3053 if not self
.config
.auto_remove_unplayed_episodes
:
3058 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3063 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3066 title
= _('Episodes are locked')
3067 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3068 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3071 count
= len(episodes
)
3072 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
3073 message
= _('Deleting episodes removes downloaded files.')
3075 if gpodder
.ui
.fremantle
:
3076 message
= '\n'.join([title
, message
])
3078 if confirm
and not self
.show_confirmation(message
, title
):
3081 progress
= ProgressIndicator(_('Deleting episodes'), \
3082 _('Please wait while episodes are deleted'), \
3083 parent
=self
.get_dialog_parent())
3085 def finish_deletion(episode_urls
, channel_urls
):
3086 progress
.on_finished()
3088 # Episodes have been deleted - persist the database
3091 self
.update_episode_list_icons(episode_urls
)
3092 self
.update_podcast_list_model(channel_urls
)
3093 self
.play_or_download()
3096 episode_urls
= set()
3097 channel_urls
= set()
3099 episodes_status_update
= []
3100 for idx
, episode
in enumerate(episodes
):
3101 progress
.on_progress(float(idx
)/float(len(episodes
)))
3102 if episode
.is_locked
and skip_locked
:
3103 log('Not deleting episode (is locked): %s', episode
.title
)
3105 log('Deleting episode: %s', episode
.title
)
3106 progress
.on_message(episode
.title
)
3107 episode
.delete_from_disk()
3108 episode_urls
.add(episode
.url
)
3109 channel_urls
.add(episode
.channel
.url
)
3110 episodes_status_update
.append(episode
)
3112 # Tell the shownotes window that we have removed the episode
3113 if self
.episode_shownotes_window
is not None and \
3114 self
.episode_shownotes_window
.episode
is not None and \
3115 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3116 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3118 # Notify the web service about the status update + upload
3119 self
.mygpo_client
.on_delete(episodes_status_update
)
3120 self
.mygpo_client
.flush()
3122 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3124 threading
.Thread(target
=thread_proc
).start()
3128 def on_itemRemoveOldEpisodes_activate( self
, widget
):
3129 if gpodder
.ui
.maemo
:
3131 ('maemo_remove_markup', None, None, _('Episode')),
3135 ('title_markup', None, None, _('Episode')),
3136 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3137 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3138 ('played_prop', None, None, _('Status')),
3139 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3142 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3143 selection_buttons
= {
3144 _('Select played'): lambda episode
: episode
.is_played
,
3145 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3148 instructions
= _('Select the episodes you want to delete:')
3152 for channel
in self
.channels
:
3153 for episode
in channel
.get_downloaded_episodes():
3154 # Disallow deletion of locked episodes that still exist
3155 if not episode
.is_locked
or not episode
.file_exists():
3156 episodes
.append(episode
)
3157 # Automatically select played and file-less episodes
3158 selected
.append(episode
.is_played
or \
3159 not episode
.file_exists())
3161 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3162 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3163 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3164 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3165 show_episode_shownotes
=self
.show_episode_shownotes
)
3167 def on_selected_episodes_status_changed(self
):
3168 # The order of the updates here is important! When "All episodes" is
3169 # selected, the update of the podcast list model depends on the episode
3170 # list selection to determine which podcasts are affected. Updating
3171 # the episode list could remove the selection if a filter is active.
3172 self
.update_podcast_list_model(selected
=True)
3173 self
.update_episode_list_icons(selected
=True)
3176 def mark_selected_episodes_new(self
):
3177 for episode
in self
.get_selected_episodes():
3179 self
.on_selected_episodes_status_changed()
3181 def mark_selected_episodes_old(self
):
3182 for episode
in self
.get_selected_episodes():
3184 self
.on_selected_episodes_status_changed()
3186 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3187 for episode
in self
.get_selected_episodes():
3189 episode
.mark(is_played
=not episode
.is_played
)
3191 episode
.mark(is_played
=new_value
)
3192 self
.on_selected_episodes_status_changed()
3194 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3195 for episode
in self
.get_selected_episodes():
3197 episode
.mark(is_locked
=not episode
.is_locked
)
3199 episode
.mark(is_locked
=new_value
)
3200 self
.on_selected_episodes_status_changed()
3202 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3203 if self
.active_channel
is None:
3206 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3207 self
.active_channel
.update_channel_lock()
3209 for episode
in self
.active_channel
.get_all_episodes():
3210 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3212 self
.update_podcast_list_model(selected
=True)
3213 self
.update_episode_list_icons(all
=True)
3215 def on_itemUpdateChannel_activate(self
, widget
=None):
3216 if self
.active_channel
is None:
3217 title
= _('No podcast selected')
3218 message
= _('Please select a podcast in the podcasts list to update.')
3219 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3222 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3223 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3224 self
.update_feed_cache()
3226 self
.update_feed_cache(channels
=[self
.active_channel
])
3228 def on_itemUpdate_activate(self
, widget
=None):
3229 # Check if we have outstanding subscribe/unsubscribe actions
3230 if self
.on_add_remove_podcasts_mygpo():
3231 log('Update cancelled (received server changes)', sender
=self
)
3235 self
.update_feed_cache()
3237 gPodderWelcome(self
.gPodder
,
3238 center_on_widget
=self
.gPodder
,
3239 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3240 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3242 def download_episode_list_paused(self
, episodes
):
3243 self
.download_episode_list(episodes
, True)
3245 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3246 enable_update
= False
3248 for episode
in episodes
:
3249 log('Downloading episode: %s', episode
.title
, sender
= self
)
3250 if not episode
.was_downloaded(and_exists
=True):
3252 for task
in self
.download_tasks_seen
:
3253 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3254 self
.download_queue_manager
.add_task(task
, force_start
)
3255 enable_update
= True
3263 task
= download
.DownloadTask(episode
, self
.config
)
3264 except Exception, e
:
3265 d
= {'episode': episode
.title
, 'message': str(e
)}
3266 message
= _('Download error while downloading %(episode)s: %(message)s')
3267 self
.show_message(message
% d
, _('Download error'), important
=True)
3268 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3272 task
.status
= task
.PAUSED
3274 self
.mygpo_client
.on_download([task
.episode
])
3275 self
.download_queue_manager
.add_task(task
, force_start
)
3277 self
.download_status_model
.register_task(task
)
3278 enable_update
= True
3281 self
.enable_download_list_update()
3283 # Flush updated episode status
3284 self
.mygpo_client
.flush()
3286 def cancel_task_list(self
, tasks
):
3291 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3292 task
.status
= task
.CANCELLED
3293 elif task
.status
== task
.PAUSED
:
3294 task
.status
= task
.CANCELLED
3295 # Call run, so the partial file gets deleted
3298 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3299 self
.play_or_download()
3301 # Update the tab title and downloads list
3302 self
.update_downloads_list()
3304 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3305 if gpodder
.ui
.maemo
:
3307 ('maemo_markup', None, None, _('Episode')),
3309 show_notification
= notification
3312 ('title_markup', None, None, _('Episode')),
3313 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3314 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3316 show_notification
= False
3318 instructions
= _('Select the episodes you want to download:')
3320 if self
.new_episodes_window
is not None:
3321 self
.new_episodes_window
.main_window
.destroy()
3322 self
.new_episodes_window
= None
3324 def download_episodes_callback(episodes
):
3325 self
.new_episodes_window
= None
3326 self
.download_episode_list(episodes
)
3328 if selected
is None:
3329 # Select all by default
3330 selected
= [True]*len(episodes
)
3332 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3333 title
=_('New episodes available'), \
3334 instructions
=instructions
, \
3335 episodes
=episodes
, \
3337 selected
=selected
, \
3338 stock_ok_button
= 'gpodder-download', \
3339 callback
=download_episodes_callback
, \
3340 remove_callback
=lambda e
: e
.mark_old(), \
3341 remove_action
=_('Mark as old'), \
3342 remove_finished
=self
.episode_new_status_changed
, \
3343 _config
=self
.config
, \
3344 show_notification
=show_notification
, \
3345 show_episode_shownotes
=self
.show_episode_shownotes
)
3347 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3348 if not self
.offer_new_episodes():
3349 self
.show_message(_('Please check for new episodes later.'), \
3350 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3352 def get_new_episodes(self
, channels
=None):
3353 if channels
is None:
3354 channels
= self
.channels
3356 for channel
in channels
:
3357 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3358 episodes
.append(episode
)
3362 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3363 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3365 def commit_changes_to_database(self
):
3366 """This will be called after the sync process is finished"""
3369 def on_cleanup_ipod_activate(self
, widget
, *args
):
3370 self
.sync_ui
.on_cleanup_device()
3372 def on_manage_device_playlist(self
, widget
):
3373 self
.sync_ui
.on_manage_device_playlist()
3375 def show_hide_tray_icon(self
):
3376 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3377 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3378 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3379 self
.tray_icon
.set_visible(False)
3381 self
.tray_icon
= None
3383 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3384 self
.tray_icon
.set_visible(self
.is_iconified())
3385 elif self
.tray_icon
:
3386 self
.tray_icon
.set_visible(True)
3388 def on_itemShowAllEpisodes_activate(self
, widget
):
3389 self
.config
.podcast_list_view_all
= widget
.get_active()
3391 def on_itemShowToolbar_activate(self
, widget
):
3392 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3394 def on_itemShowDescription_activate(self
, widget
):
3395 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3397 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3398 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3399 if self
.config
.podcast_list_hide_boring
:
3400 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3402 self
.podcast_list_model
.set_view_mode(-1)
3404 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3406 if current
== self
.item_view_podcasts_all
:
3407 self
.podcast_list_model
.set_view_mode(-1)
3408 elif current
== self
.item_view_podcasts_downloaded
:
3409 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3410 elif current
== self
.item_view_podcasts_unplayed
:
3411 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3413 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3415 def on_item_view_episodes_changed(self
, radioaction
, current
):
3416 if current
== self
.item_view_episodes_all
:
3417 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3418 elif current
== self
.item_view_episodes_undeleted
:
3419 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3420 elif current
== self
.item_view_episodes_downloaded
:
3421 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3422 elif current
== self
.item_view_episodes_unplayed
:
3423 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3425 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3427 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3428 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3430 def update_item_device( self
):
3431 if not gpodder
.ui
.fremantle
:
3432 if self
.config
.device_type
!= 'none':
3433 self
.itemDevice
.set_visible(True)
3434 self
.itemDevice
.label
= self
.get_device_name()
3436 self
.itemDevice
.set_visible(False)
3438 def properties_closed( self
):
3439 self
.preferences_dialog
= None
3440 self
.show_hide_tray_icon()
3441 self
.update_item_device()
3442 if gpodder
.ui
.maemo
:
3443 selection
= self
.treeAvailable
.get_selection()
3444 if self
.config
.maemo_enable_gestures
or \
3445 self
.config
.enable_fingerscroll
:
3446 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3448 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3450 def on_itemPreferences_activate(self
, widget
, *args
):
3451 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3452 _config
=self
.config
, \
3453 callback_finished
=self
.properties_closed
, \
3454 user_apps_reader
=self
.user_apps_reader
, \
3455 parent_window
=self
.main_window
, \
3456 mygpo_client
=self
.mygpo_client
, \
3457 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3459 # Initial message to relayout window (in case it's opened in portrait mode
3460 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3462 def on_itemDependencies_activate(self
, widget
):
3463 gPodderDependencyManager(self
.gPodder
)
3465 def on_goto_mygpo(self
, widget
):
3466 self
.mygpo_client
.open_website()
3468 def on_download_subscriptions_from_mygpo(self
, action
=None):
3469 title
= _('Login to gpodder.net')
3470 message
= _('Please login to download your subscriptions.')
3471 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3472 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3476 self
.config
.mygpo_username
= username
3477 self
.config
.mygpo_password
= password
3479 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3480 custom_title
=_('Subscriptions on gpodder.net'), \
3481 add_urls_callback
=self
.add_podcast_list
, \
3482 hide_url_entry
=True)
3484 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3485 # we do not have to hardcode the URL here
3486 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3487 url
= util
.url_add_authentication(OPML_URL
, \
3488 self
.config
.mygpo_username
, \
3489 self
.config
.mygpo_password
)
3490 dir.download_opml_file(url
)
3492 def on_mygpo_settings_activate(self
, action
=None):
3493 # This dialog is only used for Maemo 4
3494 if not gpodder
.ui
.diablo
:
3497 settings
= MygPodderSettings(self
.main_window
, \
3498 config
=self
.config
, \
3499 mygpo_client
=self
.mygpo_client
, \
3500 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3502 def on_itemAddChannel_activate(self
, widget
=None):
3503 gPodderAddPodcast(self
.gPodder
, \
3504 add_urls_callback
=self
.add_podcast_list
)
3506 def on_itemEditChannel_activate(self
, widget
, *args
):
3507 if self
.active_channel
is None:
3508 title
= _('No podcast selected')
3509 message
= _('Please select a podcast in the podcasts list to edit.')
3510 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3513 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3514 gPodderChannel(self
.main_window
, \
3515 channel
=self
.active_channel
, \
3516 callback_closed
=callback_closed
, \
3517 cover_downloader
=self
.cover_downloader
)
3519 def on_itemMassUnsubscribe_activate(self
, item
=None):
3521 ('title', None, None, _('Podcast')),
3524 # We're abusing the Episode Selector for selecting Podcasts here,
3525 # but it works and looks good, so why not? -- thp
3526 gPodderEpisodeSelector(self
.main_window
, \
3527 title
=_('Remove podcasts'), \
3528 instructions
=_('Select the podcast you want to remove.'), \
3529 episodes
=self
.channels
, \
3531 size_attribute
=None, \
3532 stock_ok_button
=_('Remove'), \
3533 callback
=self
.remove_podcast_list
, \
3534 _config
=self
.config
)
3536 def remove_podcast_list(self
, channels
, confirm
=True):
3538 log('No podcasts selected for deletion', sender
=self
)
3541 if len(channels
) == 1:
3542 title
= _('Removing podcast')
3543 info
= _('Please wait while the podcast is removed')
3544 message
= _('Do you really want to remove this podcast and its episodes?')
3546 title
= _('Removing podcasts')
3547 info
= _('Please wait while the podcasts are removed')
3548 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3550 if confirm
and not self
.show_confirmation(message
, title
):
3553 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3555 def finish_deletion(select_url
):
3556 # Upload subscription list changes to the web service
3557 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3559 # Re-load the channels and select the desired new channel
3560 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3561 progress
.on_finished()
3562 self
.update_podcasts_tab()
3567 for idx
, channel
in enumerate(channels
):
3568 # Update the UI for correct status messages
3569 progress
.on_progress(float(idx
)/float(len(channels
)))
3570 progress
.on_message(channel
.title
)
3572 # Delete downloaded episodes
3573 channel
.remove_downloaded()
3575 # cancel any active downloads from this channel
3576 for episode
in channel
.get_all_episodes():
3577 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3580 if len(channels
) == 1:
3581 # get the URL of the podcast we want to select next
3582 if channel
in self
.channels
:
3583 position
= self
.channels
.index(channel
)
3587 if position
== len(self
.channels
)-1:
3588 # this is the last podcast, so select the URL
3589 # of the item before this one (i.e. the "new last")
3590 select_url
= self
.channels
[position
-1].url
3592 # there is a podcast after the deleted one, so
3593 # we simply select the one that comes after it
3594 select_url
= self
.channels
[position
+1].url
3596 # Remove the channel and clean the database entries
3598 self
.channels
.remove(channel
)
3600 # Clean up downloads and download directories
3601 self
.clean_up_downloads()
3603 self
.channel_list_changed
= True
3604 self
.save_channels_opml()
3606 # The remaining stuff is to be done in the GTK main thread
3607 util
.idle_add(finish_deletion
, select_url
)
3609 threading
.Thread(target
=thread_proc
).start()
3611 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3612 if self
.active_channel
is None:
3613 title
= _('No podcast selected')
3614 message
= _('Please select a podcast in the podcasts list to remove.')
3615 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3618 self
.remove_podcast_list([self
.active_channel
])
3620 def get_opml_filter(self
):
3621 filter = gtk
.FileFilter()
3622 filter.add_pattern('*.opml')
3623 filter.add_pattern('*.xml')
3624 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3627 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3628 if filename
is None:
3629 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3630 # FIXME: Hildonization on Fremantle
3631 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3632 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3633 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3634 elif gpodder
.ui
.diablo
:
3635 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3636 dlg
.set_filter(self
.get_opml_filter())
3637 response
= dlg
.run()
3639 if response
== gtk
.RESPONSE_OK
:
3640 filename
= dlg
.get_filename()
3643 if filename
is not None:
3644 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3645 custom_title
=_('Import podcasts from OPML file'), \
3646 add_urls_callback
=self
.add_podcast_list
, \
3647 hide_url_entry
=True)
3648 dir.download_opml_file(filename
)
3650 def on_itemExportChannels_activate(self
, widget
, *args
):
3651 if not self
.channels
:
3652 title
= _('Nothing to export')
3653 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3654 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3657 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3658 # FIXME: Hildonization on Fremantle
3659 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3660 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3661 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3662 elif gpodder
.ui
.diablo
:
3663 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3664 dlg
.set_filter(self
.get_opml_filter())
3665 response
= dlg
.run()
3666 if response
== gtk
.RESPONSE_OK
:
3667 filename
= dlg
.get_filename()
3669 exporter
= opml
.Exporter( filename
)
3670 if exporter
.write(self
.channels
):
3671 count
= len(self
.channels
)
3672 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3673 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3675 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3679 def on_itemImportChannels_activate(self
, widget
, *args
):
3680 if gpodder
.ui
.fremantle
:
3681 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3682 self
.config
.toplist_url
, \
3683 self
.config
.opml_url
, \
3684 self
.add_podcast_list
, \
3685 self
.on_itemAddChannel_activate
, \
3686 self
.on_download_subscriptions_from_mygpo
, \
3687 self
.show_text_edit_dialog
)
3689 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3690 add_urls_callback
=self
.add_podcast_list
)
3691 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3693 def on_homepage_activate(self
, widget
, *args
):
3694 util
.open_website(gpodder
.__url
__)
3696 def on_wiki_activate(self
, widget
, *args
):
3697 util
.open_website('http://gpodder.org/wiki/User_Manual')
3699 def on_bug_tracker_activate(self
, widget
, *args
):
3700 if gpodder
.ui
.maemo
:
3701 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3703 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3705 def on_item_support_activate(self
, widget
):
3706 util
.open_website('http://gpodder.org/donate')
3708 def on_itemAbout_activate(self
, widget
, *args
):
3709 if gpodder
.ui
.fremantle
:
3710 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3711 HeAboutDialog
.present(self
.main_window
,
3714 gpodder
.__version
__,
3715 _('A podcast client with focus on usability'),
3716 gpodder
.__copyright
__,
3718 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3719 'http://gpodder.org/donate')
3722 dlg
= gtk
.AboutDialog()
3723 dlg
.set_transient_for(self
.main_window
)
3724 dlg
.set_name('gPodder')
3725 dlg
.set_version(gpodder
.__version
__)
3726 dlg
.set_copyright(gpodder
.__copyright
__)
3727 dlg
.set_comments(_('A podcast client with focus on usability'))
3728 dlg
.set_website(gpodder
.__url
__)
3729 dlg
.set_translator_credits( _('translator-credits'))
3730 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3732 if gpodder
.ui
.desktop
:
3733 # For the "GUI" version, we add some more
3734 # items to the about dialog (credits and logo)
3737 'Thomas Perl <thpinfo.com>',
3740 if os
.path
.exists(gpodder
.credits_file
):
3741 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3742 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3743 app_authors
+= credits
3745 dlg
.set_authors(app_authors
)
3747 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3749 dlg
.set_logo_icon_name('gpodder')
3753 def on_wNotebook_switch_page(self
, widget
, *args
):
3755 if gpodder
.ui
.maemo
:
3756 self
.tool_downloads
.set_active(page_num
== 1)
3757 page
= self
.wNotebook
.get_nth_page(page_num
)
3758 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3759 if page_num
== 0 and self
.active_channel
is not None:
3760 self
.set_title(self
.active_channel
.title
)
3762 self
.set_title(tab_label
)
3764 self
.play_or_download()
3765 self
.menuChannels
.set_sensitive(True)
3766 self
.menuSubscriptions
.set_sensitive(True)
3767 # The message area in the downloads tab should be hidden
3768 # when the user switches away from the downloads tab
3769 if self
.message_area
is not None:
3770 self
.message_area
.hide()
3771 self
.message_area
= None
3773 self
.menuChannels
.set_sensitive(False)
3774 self
.menuSubscriptions
.set_sensitive(False)
3775 if gpodder
.ui
.desktop
:
3776 self
.toolDownload
.set_sensitive(False)
3777 self
.toolPlay
.set_sensitive(False)
3778 self
.toolTransfer
.set_sensitive(False)
3779 self
.toolCancel
.set_sensitive(False)
3781 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3782 # double-click action of the podcast list or enter
3783 self
.treeChannels
.set_cursor(path
)
3785 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3786 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3788 if model
is not None and iter is not None:
3789 old_active_channel
= self
.active_channel
3790 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3792 if self
.active_channel
== old_active_channel
:
3795 if gpodder
.ui
.maemo
:
3796 self
.set_title(self
.active_channel
.title
)
3798 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3799 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3800 self
.itemEditChannel
.set_visible(False)
3801 self
.itemRemoveChannel
.set_visible(False)
3803 self
.itemEditChannel
.set_visible(True)
3804 self
.itemRemoveChannel
.set_visible(True)
3806 self
.active_channel
= None
3807 self
.itemEditChannel
.set_visible(False)
3808 self
.itemRemoveChannel
.set_visible(False)
3810 self
.update_episode_list_model()
3812 def on_btnEditChannel_clicked(self
, widget
, *args
):
3813 self
.on_itemEditChannel_activate( widget
, args
)
3815 def get_podcast_urls_from_selected_episodes(self
):
3816 """Get a set of podcast URLs based on the selected episodes"""
3817 return set(episode
.channel
.url
for episode
in \
3818 self
.get_selected_episodes())
3820 def get_selected_episodes(self
):
3821 """Get a list of selected episodes from treeAvailable"""
3822 selection
= self
.treeAvailable
.get_selection()
3823 model
, paths
= selection
.get_selected_rows()
3825 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3828 def on_transfer_selected_episodes(self
, widget
):
3829 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3831 def on_playback_selected_episodes(self
, widget
):
3832 self
.playback_episodes(self
.get_selected_episodes())
3834 def on_shownotes_selected_episodes(self
, widget
):
3835 episodes
= self
.get_selected_episodes()
3837 episode
= episodes
.pop(0)
3838 self
.show_episode_shownotes(episode
)
3840 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3842 def on_download_selected_episodes(self
, widget
):
3843 episodes
= self
.get_selected_episodes()
3844 self
.download_episode_list(episodes
)
3845 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3846 self
.play_or_download()
3848 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3849 """Double-click/enter action handler for treeAvailable"""
3850 # We should only have one one selected as it was double clicked!
3851 e
= self
.get_selected_episodes()[0]
3853 if (self
.config
.double_click_episode_action
== 'download'):
3854 # If the episode has already been downloaded and exists then play it
3855 if e
.was_downloaded(and_exists
=True):
3856 self
.playback_episodes(self
.get_selected_episodes())
3857 # else download it if it is not already downloading
3858 elif not self
.episode_is_downloading(e
):
3859 self
.download_episode_list([e
])
3860 self
.update_episode_list_icons([e
.url
])
3861 self
.play_or_download()
3862 elif (self
.config
.double_click_episode_action
== 'stream'):
3863 # If we happen to have downloaded this episode simple play it
3864 if e
.was_downloaded(and_exists
=True):
3865 self
.playback_episodes(self
.get_selected_episodes())
3866 # else if streaming is possible stream it
3867 elif self
.streaming_possible():
3868 self
.playback_episodes(self
.get_selected_episodes())
3870 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3871 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3873 # default action is to display show notes
3874 self
.on_shownotes_selected_episodes(widget
)
3876 def show_episode_shownotes(self
, episode
):
3877 if self
.episode_shownotes_window
is None:
3878 log('First-time use of episode window --- creating', sender
=self
)
3879 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3880 _download_episode_list
=self
.download_episode_list
, \
3881 _playback_episodes
=self
.playback_episodes
, \
3882 _delete_episode_list
=self
.delete_episode_list
, \
3883 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3884 _cancel_task_list
=self
.cancel_task_list
, \
3885 _episode_is_downloading
=self
.episode_is_downloading
, \
3886 _streaming_possible
=self
.streaming_possible())
3887 self
.episode_shownotes_window
.show(episode
)
3888 if self
.episode_is_downloading(episode
):
3889 self
.update_downloads_list()
3891 def restart_auto_update_timer(self
):
3892 if self
._auto
_update
_timer
_source
_id
is not None:
3893 log('Removing existing auto update timer.', sender
=self
)
3894 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3895 self
._auto
_update
_timer
_source
_id
= None
3897 if self
.config
.auto_update_feeds
and \
3898 self
.config
.auto_update_frequency
:
3899 interval
= 60*1000*self
.config
.auto_update_frequency
3900 log('Setting up auto update timer with interval %d.', \
3901 self
.config
.auto_update_frequency
, sender
=self
)
3902 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3903 interval
, self
._on
_auto
_update
_timer
)
3905 def _on_auto_update_timer(self
):
3906 log('Auto update timer fired.', sender
=self
)
3907 self
.update_feed_cache(force_update
=True)
3909 # Ask web service for sub changes (if enabled)
3910 self
.mygpo_client
.flush()
3914 def on_treeDownloads_row_activated(self
, widget
, *args
):
3915 # Use the standard way of working on the treeview
3916 selection
= self
.treeDownloads
.get_selection()
3917 (model
, paths
) = selection
.get_selected_rows()
3918 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3920 for tree_row_reference
, task
in selected_tasks
:
3921 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3922 task
.status
= task
.PAUSED
3923 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3924 self
.download_queue_manager
.add_task(task
)
3925 self
.enable_download_list_update()
3926 elif task
.status
== task
.DONE
:
3927 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3929 self
.play_or_download()
3931 # Update the tab title and downloads list
3932 self
.update_downloads_list()
3934 def on_item_cancel_download_activate(self
, widget
):
3935 if self
.wNotebook
.get_current_page() == 0:
3936 selection
= self
.treeAvailable
.get_selection()
3937 (model
, paths
) = selection
.get_selected_rows()
3938 urls
= [model
.get_value(model
.get_iter(path
), \
3939 self
.episode_list_model
.C_URL
) for path
in paths
]
3940 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3941 if task
.url
in urls
]
3943 selection
= self
.treeDownloads
.get_selection()
3944 (model
, paths
) = selection
.get_selected_rows()
3945 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3946 self
.download_status_model
.C_TASK
) for path
in paths
]
3947 self
.cancel_task_list(selected_tasks
)
3949 def on_btnCancelAll_clicked(self
, widget
, *args
):
3950 self
.cancel_task_list(self
.download_tasks_seen
)
3952 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3953 episodes
= self
.get_selected_episodes()
3954 if len(episodes
) == 1:
3955 self
.delete_episode_list(episodes
, skip_locked
=False)
3957 self
.delete_episode_list(episodes
)
3959 def on_key_press(self
, widget
, event
):
3960 # Allow tab switching with Ctrl + PgUp/PgDown
3961 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3962 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3963 self
.wNotebook
.prev_page()
3965 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3966 self
.wNotebook
.next_page()
3969 # After this code we only handle Maemo hardware keys,
3970 # so if we are not a Maemo app, we don't do anything
3971 if not gpodder
.ui
.maemo
:
3975 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3977 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3980 if diff
!= 0 and not self
.currently_updating
:
3981 selection
= self
.treeChannels
.get_selection()
3982 (model
, iter) = selection
.get_selected()
3983 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
3984 selection
.select_path(new_path
)
3985 self
.treeChannels
.set_cursor(new_path
)
3990 def on_iconify(self
):
3992 self
.gPodder
.set_skip_taskbar_hint(True)
3993 if self
.config
.minimize_to_tray
:
3994 self
.tray_icon
.set_visible(True)
3996 self
.gPodder
.set_skip_taskbar_hint(False)
3998 def on_uniconify(self
):
4000 self
.gPodder
.set_skip_taskbar_hint(False)
4001 if self
.config
.minimize_to_tray
:
4002 self
.tray_icon
.set_visible(False)
4004 self
.gPodder
.set_skip_taskbar_hint(False)
4006 def uniconify_main_window(self
):
4007 if self
.is_iconified():
4008 # We need to hide and then show the window in WMs like Metacity
4009 # or KWin4 to move the window to the active workspace
4010 # (see http://gpodder.org/bug/1125)
4013 self
.gPodder
.present()
4015 def iconify_main_window(self
):
4016 if not self
.is_iconified():
4017 self
.gPodder
.iconify()
4019 def update_podcasts_tab(self
):
4020 if len(self
.channels
):
4021 if gpodder
.ui
.fremantle
:
4022 self
.button_refresh
.set_title(_('Check for new episodes'))
4023 self
.button_refresh
.show()
4025 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4027 if gpodder
.ui
.fremantle
:
4028 self
.button_refresh
.hide()
4030 self
.label2
.set_text(_('Podcasts'))
4032 @dbus.service
.method(gpodder
.dbus_interface
)
4033 def show_gui_window(self
):
4034 parent
= self
.get_dialog_parent()
4037 @dbus.service
.method(gpodder
.dbus_interface
)
4038 def subscribe_to_url(self
, url
):
4039 gPodderAddPodcast(self
.gPodder
,
4040 add_urls_callback
=self
.add_podcast_list
,
4043 @dbus.service
.method(gpodder
.dbus_interface
)
4044 def mark_episode_played(self
, filename
):
4045 if filename
is None:
4048 for channel
in self
.channels
:
4049 for episode
in channel
.get_all_episodes():
4050 fn
= episode
.local_filename(create
=False, check_only
=True)
4052 episode
.mark(is_played
=True)
4054 self
.update_episode_list_icons([episode
.url
])
4055 self
.update_podcast_list_model([episode
.channel
.url
])
4061 def main(options
=None):
4062 gobject
.threads_init()
4063 gobject
.set_application_name('gPodder')
4065 if gpodder
.ui
.maemo
:
4066 # Try to enable the custom icon theme for gPodder on Maemo
4067 settings
= gtk
.settings_get_default()
4068 settings
.set_string_property('gtk-icon-theme-name', \
4069 'gpodder', __file__
)
4070 # Extend the search path for the optified icon theme (Maemo 5)
4071 icon_theme
= gtk
.icon_theme_get_default()
4072 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4074 gtk
.window_set_default_icon_name('gpodder')
4075 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4078 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4079 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4081 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4082 except dbus
.exceptions
.DBusException
, dbe
:
4083 log('Warning: Cannot get "on the bus".', traceback
=True)
4084 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4085 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4086 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4087 dlg
.set_title('gPodder')
4092 util
.make_directory(gpodder
.home
)
4093 gpodder
.load_plugins()
4095 config
= UIConfig(gpodder
.config_file
)
4097 # Load hook modules and install the hook manager globally
4098 # if modules have been found an instantiated by the manager
4099 user_hooks
= hooks
.HookManager()
4100 if user_hooks
.has_modules():
4101 gpodder
.user_hooks
= user_hooks
4103 if gpodder
.ui
.diablo
:
4104 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4105 # folder exists there (allow moving "gpodder" between SD cards or USB)
4106 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4107 if not os
.path
.exists(config
.download_dir
):
4108 log('Downloads might have been moved. Trying to locate them...')
4109 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4110 dir = os
.path
.join(basedir
, 'gpodder')
4111 if os
.path
.exists(dir):
4112 log('Downloads found in: %s', dir)
4113 config
.download_dir
= dir
4116 log('Downloads NOT FOUND in %s', dir)
4118 if config
.enable_fingerscroll
:
4119 BuilderWidget
.use_fingerscroll
= True
4120 elif gpodder
.ui
.fremantle
:
4121 config
.on_quit_ask
= False
4123 config
.mygpo_device_type
= util
.detect_device_type()
4125 gp
= gPodder(bus_name
, config
)
4128 if options
.subscribe
:
4129 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4132 # handle "subscribe to podcast" events from firefox
4133 if platform
.system() == 'Darwin':
4134 from gpodder
import gpodderosx
4135 gpodderosx
.register_handlers(gp
)
4136 # end mac OS X stuff