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 from gpodder
.gtkui
.frmntl
.widgets
import FancyProgressBar
141 have_trayicon
= False
143 from gpodder
.gtkui
.frmntl
.portrait
import FremantleRotation
144 from gpodder
.gtkui
.frmntl
.mafw
import MafwPlaybackMonitor
146 from gpodder
.gtkui
.interface
.common
import Orientation
148 from gpodder
.gtkui
.interface
.welcome
import gPodderWelcome
153 from gpodder
.dbusproxy
import DBusPodcastsProxy
154 from gpodder
import hooks
156 class gPodder(BuilderWidget
, dbus
.service
.Object
):
157 finger_friendly_widgets
= ['btnCleanUpDownloads', 'button_search_episodes_clear']
159 ICON_GENERAL_ADD
= 'general_add'
160 ICON_GENERAL_REFRESH
= 'general_refresh'
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 # Set up a very nice progress bar setup
353 self
.fancy_progress_bar
= FancyProgressBar(self
.main_window
, \
354 self
.on_btnCancelFeedUpdate_clicked
)
355 self
.pbFeedUpdate
= self
.fancy_progress_bar
.progress_bar
356 self
.pbFeedUpdate
.set_ellipsize(pango
.ELLIPSIZE_MIDDLE
)
357 self
.vbox
.pack_start(self
.fancy_progress_bar
.event_box
, False)
359 from gpodder
.gtkui
.frmntl
import style
360 sub_font
= style
.get_font_desc('SmallSystemFont')
361 sub_color
= style
.get_color('SecondaryTextColor')
362 sub
= (sub_font
.to_string(), sub_color
.to_string())
363 sub
= '<span font_desc="%s" foreground="%s">%%s</span>' % sub
364 self
.label_footer
.set_markup(sub
% gpodder
.__copyright
__)
366 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
367 while gtk
.events_pending():
368 gtk
.main_iteration(False)
371 # Try to get the real package version from dpkg
372 p
= subprocess
.Popen(['dpkg-query', '-W', '-f=${Version}', 'gpodder'], stdout
=subprocess
.PIPE
)
373 version
, _stderr
= p
.communicate()
377 version
= gpodder
.__version
__
378 self
.label_footer
.set_markup(sub
% ('v %s' % version
))
379 self
.label_footer
.hide()
381 self
.episodes_window
= gPodderEpisodes(self
.main_window
, \
382 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
383 show_episode_shownotes
=self
.show_episode_shownotes
, \
384 update_podcast_list_model
=self
.update_podcast_list_model
, \
385 on_itemRemoveChannel_activate
=self
.on_itemRemoveChannel_activate
, \
386 item_view_episodes_all
=self
.item_view_episodes_all
, \
387 item_view_episodes_unplayed
=self
.item_view_episodes_unplayed
, \
388 item_view_episodes_downloaded
=self
.item_view_episodes_downloaded
, \
389 item_view_episodes_undeleted
=self
.item_view_episodes_undeleted
, \
390 on_entry_search_episodes_changed
=self
.on_entry_search_episodes_changed
, \
391 on_entry_search_episodes_key_press
=self
.on_entry_search_episodes_key_press
, \
392 hide_episode_search
=self
.hide_episode_search
, \
393 on_itemUpdateChannel_activate
=self
.on_itemUpdateChannel_activate
, \
394 playback_episodes
=self
.playback_episodes
, \
395 delete_episode_list
=self
.delete_episode_list
, \
396 episode_list_status_changed
=self
.episode_list_status_changed
, \
397 download_episode_list
=self
.download_episode_list
, \
398 episode_is_downloading
=self
.episode_is_downloading
, \
399 show_episode_in_download_manager
=self
.show_episode_in_download_manager
, \
400 add_download_task_monitor
=self
.add_download_task_monitor
, \
401 remove_download_task_monitor
=self
.remove_download_task_monitor
, \
402 for_each_episode_set_task_status
=self
.for_each_episode_set_task_status
, \
403 on_delete_episodes_button_clicked
=self
.on_itemRemoveOldEpisodes_activate
, \
404 on_itemUpdate_activate
=self
.on_itemUpdate_activate
)
406 # Expose objects for episode list type-ahead find
407 self
.hbox_search_episodes
= self
.episodes_window
.hbox_search_episodes
408 self
.entry_search_episodes
= self
.episodes_window
.entry_search_episodes
409 self
.button_search_episodes_clear
= self
.episodes_window
.button_search_episodes_clear
411 self
.downloads_window
= gPodderDownloads(self
.main_window
, \
412 on_treeview_expose_event
=self
.on_treeview_expose_event
, \
413 cleanup_downloads
=self
.cleanup_downloads
, \
414 _for_each_task_set_status
=self
._for
_each
_task
_set
_status
, \
415 downloads_list_get_selection
=self
.downloads_list_get_selection
, \
418 self
.treeAvailable
= self
.episodes_window
.treeview
419 self
.treeDownloads
= self
.downloads_window
.treeview
421 # Init the treeviews that we use
422 self
.init_podcast_list_treeview()
423 self
.init_episode_list_treeview()
424 self
.init_download_list_treeview()
426 if self
.config
.podcast_list_hide_boring
:
427 self
.item_view_hide_boring_podcasts
.set_active(True)
429 self
.currently_updating
= False
432 self
.context_menu_mouse_button
= 1
434 self
.context_menu_mouse_button
= 3
436 if self
.config
.start_iconified
:
437 self
.iconify_main_window()
439 self
.download_tasks_seen
= set()
440 self
.download_list_update_enabled
= False
441 self
.download_task_monitors
= set()
443 # Subscribed channels
444 self
.active_channel
= None
445 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
446 self
.channel_list_changed
= True
447 self
.update_podcasts_tab()
449 # load list of user applications for audio playback
450 self
.user_apps_reader
= UserAppsReader(['audio', 'video'])
451 threading
.Thread(target
=self
.user_apps_reader
.read
).start()
453 # Set the "Device" menu item for the first time
454 if gpodder
.ui
.desktop
:
455 self
.update_item_device()
457 # Set up the first instance of MygPoClient
458 self
.mygpo_client
= my
.MygPoClient(self
.config
)
460 # Now, update the feed cache, when everything's in place
461 if not gpodder
.ui
.fremantle
:
462 self
.btnUpdateFeeds
.show()
463 self
.updating_feed_cache
= False
464 self
.feed_cache_update_cancelled
= False
465 self
.update_feed_cache(force_update
=self
.config
.update_on_startup
)
467 self
.message_area
= None
469 def find_partial_downloads():
470 # Look for partial file downloads
471 partial_files
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*', '*.partial'))
472 count
= len(partial_files
)
473 resumable_episodes
= []
475 if not gpodder
.ui
.fremantle
:
476 util
.idle_add(self
.wNotebook
.set_current_page
, 1)
477 indicator
= ProgressIndicator(_('Loading incomplete downloads'), \
478 _('Some episodes have not finished downloading in a previous session.'), \
479 False, self
.get_dialog_parent())
480 indicator
.on_message(N_('%d partial file', '%d partial files', count
) % count
)
482 candidates
= [f
[:-len('.partial')] for f
in partial_files
]
485 for c
in self
.channels
:
486 for e
in c
.get_all_episodes():
487 filename
= e
.local_filename(create
=False, check_only
=True)
488 if filename
in candidates
:
489 log('Found episode: %s', e
.title
, sender
=self
)
491 indicator
.on_message(e
.title
)
492 indicator
.on_progress(float(found
)/count
)
493 candidates
.remove(filename
)
494 partial_files
.remove(filename
+'.partial')
495 resumable_episodes
.append(e
)
503 for f
in partial_files
:
504 log('Partial file without episode: %s', f
, sender
=self
)
507 util
.idle_add(indicator
.on_finished
)
509 if len(resumable_episodes
):
510 def offer_resuming():
511 self
.download_episode_list_paused(resumable_episodes
)
512 if not gpodder
.ui
.fremantle
:
513 resume_all
= gtk
.Button(_('Resume all'))
514 #resume_all.set_border_width(0)
515 def on_resume_all(button
):
516 selection
= self
.treeDownloads
.get_selection()
517 selection
.select_all()
518 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= self
.downloads_list_get_selection()
519 selection
.unselect_all()
520 self
._for
_each
_task
_set
_status
(selected_tasks
, download
.DownloadTask
.QUEUED
)
521 self
.message_area
.hide()
522 resume_all
.connect('clicked', on_resume_all
)
524 self
.message_area
= SimpleMessageArea(_('Incomplete downloads from a previous session were found.'), (resume_all
,))
525 self
.vboxDownloadStatusWidgets
.pack_start(self
.message_area
, expand
=False)
526 self
.vboxDownloadStatusWidgets
.reorder_child(self
.message_area
, 0)
527 self
.message_area
.show_all()
528 self
.clean_up_downloads(delete_partial
=False)
529 util
.idle_add(offer_resuming
)
530 elif not gpodder
.ui
.fremantle
:
531 util
.idle_add(self
.wNotebook
.set_current_page
, 0)
533 util
.idle_add(self
.clean_up_downloads
, True)
534 threading
.Thread(target
=find_partial_downloads
).start()
536 # Start the auto-update procedure
537 self
._auto
_update
_timer
_source
_id
= None
538 if self
.config
.auto_update_feeds
:
539 self
.restart_auto_update_timer()
541 # Delete old episodes if the user wishes to
542 if self
.config
.auto_remove_played_episodes
and \
543 self
.config
.episode_old_age
> 0:
544 old_episodes
= list(self
.get_expired_episodes())
545 if len(old_episodes
) > 0:
546 self
.delete_episode_list(old_episodes
, confirm
=False)
547 self
.update_podcast_list_model(set(e
.channel
.url
for e
in old_episodes
))
549 if gpodder
.ui
.fremantle
:
550 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
551 self
.button_refresh
.set_sensitive(True)
552 self
.button_subscribe
.set_sensitive(True)
553 self
.main_window
.set_title(_('gPodder'))
554 hildon
.hildon_gtk_window_take_screenshot(self
.main_window
, True)
556 # Do the initial sync with the web service
557 util
.idle_add(self
.mygpo_client
.flush
, True)
559 # First-time users should be asked if they want to see the OPML
560 if not self
.channels
and not gpodder
.ui
.fremantle
:
561 util
.idle_add(self
.on_itemUpdate_activate
)
563 def episode_object_by_uri(self
, uri
):
564 """Get an episode object given a local or remote URI
566 This can be used to quickly access an episode object
567 when all we have is its download filename or episode
568 URL (e.g. from external D-Bus calls / signals, etc..)
570 if uri
.startswith('/'):
571 uri
= 'file://' + uri
573 prefix
= 'file://' + self
.config
.download_dir
575 if uri
.startswith(prefix
):
576 # File is on the local filesystem in the download folder
577 filename
= uri
[len(prefix
):]
578 file_parts
= [x
for x
in filename
.split(os
.sep
) if x
]
580 if len(file_parts
) == 2:
581 dir_name
, filename
= file_parts
582 channels
= [c
for c
in self
.channels
if c
.foldername
== dir_name
]
583 if len(channels
) == 1:
584 channel
= channels
[0]
585 return channel
.get_episode_by_filename(filename
)
587 # Possibly remote file - search the database for a podcast
588 channel_id
= self
.db
.get_channel_id_from_episode_url(uri
)
590 if channel_id
is not None:
591 channels
= [c
for c
in self
.channels
if c
.id == channel_id
]
592 if len(channels
) == 1:
593 channel
= channels
[0]
594 return channel
.get_episode_by_url(uri
)
598 def on_played(self
, start
, end
, total
, file_uri
):
599 """Handle the "played" signal from a media player"""
600 if start
== 0 and end
== 0 and total
== 0:
601 # Ignore bogus play event
603 elif end
< start
+ 5:
604 # Ignore "less than five seconds" segments,
605 # as they can happen with seeking, etc...
608 log('Received play action: %s (%d, %d, %d)', file_uri
, start
, end
, total
, sender
=self
)
609 episode
= self
.episode_object_by_uri(file_uri
)
611 if episode
is not None:
612 file_type
= episode
.file_type()
613 # Automatically enable D-Bus played status mode
614 if file_type
== 'audio':
615 self
.config
.audio_played_dbus
= True
616 elif file_type
== 'video':
617 self
.config
.video_played_dbus
= True
621 episode
.total_time
= total
623 # Assume the episode's total time for the action
624 total
= episode
.total_time
625 if episode
.current_position_updated
is None or \
626 now
> episode
.current_position_updated
:
627 episode
.current_position
= end
628 episode
.current_position_updated
= now
629 episode
.mark(is_played
=True)
632 self
.update_episode_list_icons([episode
.url
])
633 self
.update_podcast_list_model([episode
.channel
.url
])
635 # Submit this action to the webservice
636 self
.mygpo_client
.on_playback_full(episode
, \
639 def on_add_remove_podcasts_mygpo(self
):
640 actions
= self
.mygpo_client
.get_received_actions()
644 existing_urls
= [c
.url
for c
in self
.channels
]
646 # Columns for the episode selector window - just one...
648 ('description', None, None, _('Action')),
651 # A list of actions that have to be chosen from
654 # Actions that are ignored (already carried out)
657 for action
in actions
:
658 if action
.is_add
and action
.url
not in existing_urls
:
659 changes
.append(my
.Change(action
))
660 elif action
.is_remove
and action
.url
in existing_urls
:
661 podcast_object
= None
662 for podcast
in self
.channels
:
663 if podcast
.url
== action
.url
:
664 podcast_object
= podcast
666 changes
.append(my
.Change(action
, podcast_object
))
668 log('Ignoring action: %s', action
, sender
=self
)
669 ignored
.append(action
)
671 # Confirm all ignored changes
672 self
.mygpo_client
.confirm_received_actions(ignored
)
674 def execute_podcast_actions(selected
):
675 add_list
= [c
.action
.url
for c
in selected
if c
.action
.is_add
]
676 remove_list
= [c
.podcast
for c
in selected
if c
.action
.is_remove
]
678 # Apply the accepted changes locally
679 self
.add_podcast_list(add_list
)
680 self
.remove_podcast_list(remove_list
, confirm
=False)
682 # All selected items are now confirmed
683 self
.mygpo_client
.confirm_received_actions(c
.action
for c
in selected
)
685 # Revert the changes on the server
686 rejected
= [c
.action
for c
in changes
if c
not in selected
]
687 self
.mygpo_client
.reject_received_actions(rejected
)
690 # We're abusing the Episode Selector again ;) -- thp
691 gPodderEpisodeSelector(self
.main_window
, \
692 title
=_('Confirm changes from gpodder.net'), \
693 instructions
=_('Select the actions you want to carry out.'), \
696 size_attribute
=None, \
697 stock_ok_button
=gtk
.STOCK_APPLY
, \
698 callback
=execute_podcast_actions
, \
701 # There are some actions that need the user's attention
706 # We have no remaining actions - no selection happens
709 def rewrite_urls_mygpo(self
):
710 # Check if we have to rewrite URLs since the last add
711 rewritten_urls
= self
.mygpo_client
.get_rewritten_urls()
713 for rewritten_url
in rewritten_urls
:
714 if not rewritten_url
.new_url
:
717 for channel
in self
.channels
:
718 if channel
.url
== rewritten_url
.old_url
:
719 log('Updating URL of %s to %s', channel
, \
720 rewritten_url
.new_url
, sender
=self
)
721 channel
.url
= rewritten_url
.new_url
723 self
.channel_list_changed
= True
724 util
.idle_add(self
.update_episode_list_model
)
727 def on_send_full_subscriptions(self
):
728 # Send the full subscription list to the gpodder.net client
729 # (this will overwrite the subscription list on the server)
730 indicator
= ProgressIndicator(_('Uploading subscriptions'), \
731 _('Your subscriptions are being uploaded to the server.'), \
732 False, self
.get_dialog_parent())
735 self
.mygpo_client
.set_subscriptions([c
.url
for c
in self
.channels
])
736 util
.idle_add(self
.show_message
, _('List uploaded successfully.'))
741 message
= e
.__class
__.__name
__
742 self
.show_message(message
, \
743 _('Error while uploading'), \
745 util
.idle_add(show_error
, e
)
747 util
.idle_add(indicator
.on_finished
)
749 def on_podcast_selected(self
, treeview
, path
, column
):
751 model
= treeview
.get_model()
752 channel
= model
.get_value(model
.get_iter(path
), \
753 PodcastListModel
.C_CHANNEL
)
754 self
.active_channel
= channel
755 self
.update_episode_list_model()
756 self
.episodes_window
.channel
= self
.active_channel
757 self
.episodes_window
.show()
759 def on_button_subscribe_clicked(self
, button
):
760 self
.on_itemImportChannels_activate(button
)
762 def on_button_downloads_clicked(self
, widget
):
763 self
.downloads_window
.show()
765 def show_episode_in_download_manager(self
, episode
):
766 self
.downloads_window
.show()
767 model
= self
.treeDownloads
.get_model()
768 selection
= self
.treeDownloads
.get_selection()
769 selection
.unselect_all()
770 it
= model
.get_iter_first()
771 while it
is not None:
772 task
= model
.get_value(it
, DownloadStatusModel
.C_TASK
)
773 if task
.episode
.url
== episode
.url
:
774 selection
.select_iter(it
)
775 # FIXME: Scroll to selection in pannable area
777 it
= model
.iter_next(it
)
779 def for_each_episode_set_task_status(self
, episodes
, status
):
780 episode_urls
= set(episode
.url
for episode
in episodes
)
781 model
= self
.treeDownloads
.get_model()
782 selected_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), \
783 model
.get_value(row
.iter, \
784 DownloadStatusModel
.C_TASK
)) for row
in model \
785 if model
.get_value(row
.iter, DownloadStatusModel
.C_TASK
).url \
787 self
._for
_each
_task
_set
_status
(selected_tasks
, status
)
789 def on_window_orientation_changed(self
, orientation
):
790 self
._last
_orientation
= orientation
791 if self
.preferences_dialog
is not None:
792 self
.preferences_dialog
.on_window_orientation_changed(orientation
)
794 treeview
= self
.treeChannels
795 if orientation
== Orientation
.PORTRAIT
:
796 treeview
.set_action_area_orientation(gtk
.ORIENTATION_VERTICAL
)
797 # Work around Maemo bug #4718
798 self
.button_subscribe
.set_name('HildonButton-thumb')
799 self
.button_refresh
.set_name('HildonButton-thumb')
801 treeview
.set_action_area_orientation(gtk
.ORIENTATION_HORIZONTAL
)
802 # Work around Maemo bug #4718
803 self
.button_subscribe
.set_name('HildonButton-finger')
804 self
.button_refresh
.set_name('HildonButton-finger')
806 if gpodder
.ui
.fremantle
:
807 self
.fancy_progress_bar
.relayout()
809 def on_treeview_podcasts_selection_changed(self
, selection
):
810 model
, iter = selection
.get_selected()
812 self
.active_channel
= None
813 self
.episode_list_model
.clear()
815 def on_treeview_button_pressed(self
, treeview
, event
):
816 if event
.window
!= treeview
.get_bin_window():
819 TreeViewHelper
.save_button_press_event(treeview
, event
)
821 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
822 TreeViewHelper
.ROLE_PODCASTS
:
823 return self
.currently_updating
825 return event
.button
== self
.context_menu_mouse_button
and \
828 def on_treeview_podcasts_button_released(self
, treeview
, event
):
829 if event
.window
!= treeview
.get_bin_window():
833 return self
.treeview_channels_handle_gestures(treeview
, event
)
834 return self
.treeview_channels_show_context_menu(treeview
, event
)
836 def on_treeview_episodes_button_released(self
, treeview
, event
):
837 if event
.window
!= treeview
.get_bin_window():
841 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
842 return self
.treeview_available_handle_gestures(treeview
, event
)
844 return self
.treeview_available_show_context_menu(treeview
, event
)
846 def on_treeview_downloads_button_released(self
, treeview
, event
):
847 if event
.window
!= treeview
.get_bin_window():
850 return self
.treeview_downloads_show_context_menu(treeview
, event
)
852 def on_entry_search_podcasts_changed(self
, editable
):
853 if self
.hbox_search_podcasts
.get_property('visible'):
854 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
856 def on_entry_search_podcasts_key_press(self
, editable
, event
):
857 if event
.keyval
== gtk
.keysyms
.Escape
:
858 self
.hide_podcast_search()
861 def hide_podcast_search(self
, *args
):
862 self
.hbox_search_podcasts
.hide()
863 self
.entry_search_podcasts
.set_text('')
864 self
.podcast_list_model
.set_search_term(None)
865 self
.treeChannels
.grab_focus()
867 def show_podcast_search(self
, input_char
):
868 self
.hbox_search_podcasts
.show()
869 self
.entry_search_podcasts
.insert_text(input_char
, -1)
870 self
.entry_search_podcasts
.grab_focus()
871 self
.entry_search_podcasts
.set_position(-1)
873 def init_podcast_list_treeview(self
):
874 # Set up podcast channel tree view widget
875 if gpodder
.ui
.fremantle
:
876 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
877 self
.item_view_podcasts_downloaded
.set_active(True)
878 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
879 self
.item_view_podcasts_unplayed
.set_active(True)
881 self
.item_view_podcasts_all
.set_active(True)
882 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
884 iconcolumn
= gtk
.TreeViewColumn('')
885 iconcell
= gtk
.CellRendererPixbuf()
886 iconcolumn
.pack_start(iconcell
, False)
887 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
888 self
.treeChannels
.append_column(iconcolumn
)
890 namecolumn
= gtk
.TreeViewColumn('')
891 namecell
= gtk
.CellRendererText()
892 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
893 namecolumn
.pack_start(namecell
, True)
894 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
896 if gpodder
.ui
.fremantle
:
897 countcell
= gtk
.CellRendererText()
898 from gpodder
.gtkui
.frmntl
import style
899 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
900 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
901 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
902 countcell
.set_property('xalign', 1.)
903 countcell
.set_property('xpad', 5)
904 namecolumn
.pack_start(countcell
, False)
905 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
906 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
908 iconcell
= gtk
.CellRendererPixbuf()
909 iconcell
.set_property('xalign', 1.0)
910 namecolumn
.pack_start(iconcell
, False)
911 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
912 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
914 self
.treeChannels
.append_column(namecolumn
)
916 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
918 # When no podcast is selected, clear the episode list model
919 selection
= self
.treeChannels
.get_selection()
920 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
922 # Set up type-ahead find for the podcast list
923 def on_key_press(treeview
, event
):
924 if event
.keyval
== gtk
.keysyms
.Escape
:
925 self
.hide_podcast_search()
926 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
927 self
.hide_podcast_search()
928 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
929 # Don't handle type-ahead when control is pressed (so shortcuts
930 # with the Ctrl key still work, e.g. Ctrl+A, ...)
933 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
934 if unicode_char_id
== 0:
936 input_char
= unichr(unicode_char_id
)
937 self
.show_podcast_search(input_char
)
939 self
.treeChannels
.connect('key-press-event', on_key_press
)
941 # Enable separators to the podcast list to separate special podcasts
942 # from others (this is used for the "all episodes" view)
943 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
945 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
947 def on_entry_search_episodes_changed(self
, editable
):
948 if self
.hbox_search_episodes
.get_property('visible'):
949 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
951 def on_entry_search_episodes_key_press(self
, editable
, event
):
952 if event
.keyval
== gtk
.keysyms
.Escape
:
953 self
.hide_episode_search()
956 def hide_episode_search(self
, *args
):
957 self
.hbox_search_episodes
.hide()
958 self
.entry_search_episodes
.set_text('')
959 self
.episode_list_model
.set_search_term(None)
960 self
.treeAvailable
.grab_focus()
962 def show_episode_search(self
, input_char
):
963 self
.hbox_search_episodes
.show()
964 self
.entry_search_episodes
.insert_text(input_char
, -1)
965 self
.entry_search_episodes
.grab_focus()
966 self
.entry_search_episodes
.set_position(-1)
968 def init_episode_list_treeview(self
):
969 # For loading the list model
970 self
.empty_episode_list_model
= EpisodeListModel()
971 self
.episode_list_model
= EpisodeListModel()
973 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
974 self
.item_view_episodes_undeleted
.set_active(True)
975 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
976 self
.item_view_episodes_downloaded
.set_active(True)
977 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
978 self
.item_view_episodes_unplayed
.set_active(True)
980 self
.item_view_episodes_all
.set_active(True)
982 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
984 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
986 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
988 iconcell
= gtk
.CellRendererPixbuf()
990 iconcell
.set_fixed_size(50, 50)
991 status_column_label
= ''
993 status_column_label
= _('Status')
994 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
996 namecell
= gtk
.CellRendererText()
997 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
998 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
999 namecolumn
.pack_start(namecell
, True)
1000 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
1001 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
1002 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1003 namecolumn
.set_resizable(True)
1004 namecolumn
.set_expand(True)
1006 if gpodder
.ui
.fremantle
:
1007 from gpodder
.gtkui
.frmntl
import style
1008 timecell
= gtk
.CellRendererText()
1009 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1010 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1011 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1012 timecell
.set_property('xalign', 1.)
1013 timecell
.set_property('xpad', 5)
1014 namecolumn
.pack_start(timecell
, False)
1015 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1016 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME1_VISIBLE
)
1018 # Add another cell renderer to fix a sizing issue (one renderer
1019 # only renders short text and the other one longer text to avoid
1020 # having titles of episodes unnecessarily cut off)
1021 timecell
= gtk
.CellRendererText()
1022 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1023 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1024 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1025 timecell
.set_property('xalign', 1.)
1026 timecell
.set_property('xpad', 5)
1027 namecolumn
.pack_start(timecell
, False)
1028 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1029 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME2_VISIBLE
)
1031 sizecell
= gtk
.CellRendererText()
1032 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1033 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1035 releasecell
= gtk
.CellRendererText()
1036 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1037 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1039 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
1040 itemcolumn
.set_reorderable(True)
1041 self
.treeAvailable
.append_column(itemcolumn
)
1043 if gpodder
.ui
.maemo
:
1044 sizecolumn
.set_visible(False)
1045 releasecolumn
.set_visible(False)
1047 # Set up type-ahead find for the episode list
1048 def on_key_press(treeview
, event
):
1049 if event
.keyval
== gtk
.keysyms
.Escape
:
1050 self
.hide_episode_search()
1051 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1052 self
.hide_episode_search()
1053 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1054 # Don't handle type-ahead when control is pressed (so shortcuts
1055 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1058 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1059 if unicode_char_id
== 0:
1061 input_char
= unichr(unicode_char_id
)
1062 self
.show_episode_search(input_char
)
1064 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1066 if gpodder
.ui
.desktop
:
1067 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1068 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1069 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1070 if self
.config
.on_drag_mark_played
:
1071 for episode
in self
.get_selected_episodes():
1072 episode
.mark(is_played
=True)
1073 self
.on_selected_episodes_status_changed()
1074 uris
= ['file://'+e
.local_filename(create
=False) \
1075 for e
in self
.get_selected_episodes() \
1076 if e
.was_downloaded(and_exists
=True)]
1077 uris
.append('') # for the trailing '\r\n'
1078 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1079 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1081 selection
= self
.treeAvailable
.get_selection()
1082 if gpodder
.ui
.diablo
:
1083 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1084 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1086 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1087 elif gpodder
.ui
.fremantle
:
1088 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1090 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1091 # Update the sensitivity of the toolbar buttons on the Desktop
1092 selection
.connect('changed', lambda s
: self
.play_or_download())
1094 if gpodder
.ui
.diablo
:
1095 # Set up the tap-and-hold context menu for podcasts
1097 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1098 menu
.append(self
.itemEditChannel
.create_menu_item())
1099 menu
.append(gtk
.SeparatorMenuItem())
1100 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1101 menu
.append(gtk
.SeparatorMenuItem())
1102 item
= gtk
.ImageMenuItem(_('Close this menu'))
1103 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1104 gtk
.ICON_SIZE_MENU
))
1107 menu
= self
.set_finger_friendly(menu
)
1108 self
.treeChannels
.tap_and_hold_setup(menu
)
1111 def init_download_list_treeview(self
):
1112 # enable multiple selection support
1113 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1114 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1116 # columns and renderers for "download progress" tab
1117 # First column: [ICON] Episodename
1118 column
= gtk
.TreeViewColumn(_('Episode'))
1120 cell
= gtk
.CellRendererPixbuf()
1121 if gpodder
.ui
.maemo
:
1122 cell
.set_fixed_size(50, 50)
1123 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1124 column
.pack_start(cell
, expand
=False)
1125 column
.add_attribute(cell
, 'stock-id', \
1126 DownloadStatusModel
.C_ICON_NAME
)
1128 cell
= gtk
.CellRendererText()
1129 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1130 column
.pack_start(cell
, expand
=True)
1131 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1132 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1133 column
.set_expand(True)
1134 self
.treeDownloads
.append_column(column
)
1136 # Second column: Progress
1137 cell
= gtk
.CellRendererProgress()
1138 cell
.set_property('yalign', .5)
1139 cell
.set_property('ypad', 6)
1140 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1141 value
=DownloadStatusModel
.C_PROGRESS
, \
1142 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1143 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1144 column
.set_expand(False)
1145 self
.treeDownloads
.append_column(column
)
1146 column
.set_property('min-width', 150)
1147 column
.set_property('max-width', 150)
1149 self
.treeDownloads
.set_model(self
.download_status_model
)
1150 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1152 def on_treeview_expose_event(self
, treeview
, event
):
1153 if event
.window
== treeview
.get_bin_window():
1154 model
= treeview
.get_model()
1155 if (model
is not None and model
.get_iter_first() is not None):
1158 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1162 ctx
= event
.window
.cairo_create()
1163 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1164 event
.area
.width
, event
.area
.height
)
1167 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1170 if role
== TreeViewHelper
.ROLE_EPISODES
:
1171 if self
.currently_updating
:
1172 text
= _('Loading episodes')
1173 progress
= self
.episode_list_model
.get_update_progress()
1174 elif self
.config
.episode_list_view_mode
!= \
1175 EpisodeListModel
.VIEW_ALL
:
1176 text
= _('No episodes in current view')
1178 text
= _('No episodes available')
1179 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1180 if self
.config
.episode_list_view_mode
!= \
1181 EpisodeListModel
.VIEW_ALL
and \
1182 self
.config
.podcast_list_hide_boring
and \
1183 len(self
.channels
) > 0:
1184 text
= _('No podcasts in this view')
1186 text
= _('No subscriptions')
1187 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1188 text
= _('No active downloads')
1190 raise Exception('on_treeview_expose_event: unknown role')
1192 if gpodder
.ui
.fremantle
:
1193 from gpodder
.gtkui
.frmntl
import style
1194 font_desc
= style
.get_font_desc('LargeSystemFont')
1198 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1202 def enable_download_list_update(self
):
1203 if not self
.download_list_update_enabled
:
1204 self
.update_downloads_list()
1205 gobject
.timeout_add(1500, self
.update_downloads_list
)
1206 self
.download_list_update_enabled
= True
1208 def cleanup_downloads(self
):
1209 model
= self
.download_status_model
1211 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1212 changed_episode_urls
= set()
1213 for row_reference
, task
in all_tasks
:
1214 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1215 model
.remove(model
.get_iter(row_reference
.get_path()))
1217 # We don't "see" this task anymore - remove it;
1218 # this is needed, so update_episode_list_icons()
1219 # below gets the correct list of "seen" tasks
1220 self
.download_tasks_seen
.remove(task
)
1221 except KeyError, key_error
:
1222 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1223 changed_episode_urls
.add(task
.url
)
1224 # Tell the task that it has been removed (so it can clean up)
1225 task
.removed_from_list()
1227 # Tell the podcasts tab to update icons for our removed podcasts
1228 self
.update_episode_list_icons(changed_episode_urls
)
1230 # Tell the shownotes window that we have removed the episode
1231 if self
.episode_shownotes_window
is not None and \
1232 self
.episode_shownotes_window
.episode
is not None and \
1233 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1234 self
.episode_shownotes_window
._download
_status
_changed
(None)
1236 # Update the downloads list one more time
1237 self
.update_downloads_list(can_call_cleanup
=False)
1239 def on_tool_downloads_toggled(self
, toolbutton
):
1240 if toolbutton
.get_active():
1241 self
.wNotebook
.set_current_page(1)
1243 self
.wNotebook
.set_current_page(0)
1245 def add_download_task_monitor(self
, monitor
):
1246 self
.download_task_monitors
.add(monitor
)
1247 model
= self
.download_status_model
1251 task
= row
[self
.download_status_model
.C_TASK
]
1252 monitor
.task_updated(task
)
1254 def remove_download_task_monitor(self
, monitor
):
1255 self
.download_task_monitors
.remove(monitor
)
1257 def update_downloads_list(self
, can_call_cleanup
=True):
1259 model
= self
.download_status_model
1261 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1262 total_speed
, total_size
, done_size
= 0, 0, 0
1264 # Keep a list of all download tasks that we've seen
1265 download_tasks_seen
= set()
1267 # Remember the DownloadTask object for the episode that
1268 # has been opened in the episode shownotes dialog (if any)
1269 if self
.episode_shownotes_window
is not None:
1270 shownotes_episode
= self
.episode_shownotes_window
.episode
1271 shownotes_task
= None
1273 shownotes_episode
= None
1274 shownotes_task
= None
1276 # Do not go through the list of the model is not (yet) available
1280 failed_downloads
= []
1282 self
.download_status_model
.request_update(row
.iter)
1284 task
= row
[self
.download_status_model
.C_TASK
]
1285 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1287 # Let the download task monitors know of changes
1288 for monitor
in self
.download_task_monitors
:
1289 monitor
.task_updated(task
)
1292 done_size
+= size
*progress
1294 if shownotes_episode
is not None and \
1295 shownotes_episode
.url
== task
.episode
.url
:
1296 shownotes_task
= task
1298 download_tasks_seen
.add(task
)
1300 if status
== download
.DownloadTask
.DOWNLOADING
:
1302 total_speed
+= speed
1303 elif status
== download
.DownloadTask
.FAILED
:
1304 failed_downloads
.append(task
)
1306 elif status
== download
.DownloadTask
.DONE
:
1308 elif status
== download
.DownloadTask
.QUEUED
:
1310 elif status
== download
.DownloadTask
.PAUSED
:
1315 # Remember which tasks we have seen after this run
1316 self
.download_tasks_seen
= download_tasks_seen
1318 if gpodder
.ui
.desktop
:
1319 text
= [_('Downloads')]
1320 if downloading
+ failed
+ queued
> 0:
1323 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1325 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1327 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1328 text
.append(' (' + ', '.join(s
)+')')
1329 self
.labelDownloads
.set_text(''.join(text
))
1330 elif gpodder
.ui
.diablo
:
1331 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1333 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1335 self
.tool_downloads
.set_label(_('Downloads'))
1336 elif gpodder
.ui
.fremantle
:
1337 if downloading
+ queued
> 0:
1338 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1340 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1342 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1344 self
.button_downloads
.set_value(_('Idle'))
1346 title
= [self
.default_title
]
1348 # We have to update all episodes/channels for which the status has
1349 # changed. Accessing task.status_changed has the side effect of
1350 # re-setting the changed flag, so we need to get the "changed" list
1351 # of tuples first and split it into two lists afterwards
1352 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1353 self
.download_tasks_seen
if task
.status_changed
]
1354 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1355 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1357 count
= downloading
+ queued
1359 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1362 percentage
= 100.0*done_size
/total_size
1365 total_speed
= util
.format_filesize(total_speed
)
1366 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1367 if self
.tray_icon
is not None:
1368 # Update the tray icon status and progress bar
1369 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1370 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1372 if self
.tray_icon
is not None:
1373 # Update the tray icon status
1374 self
.tray_icon
.set_status()
1375 if gpodder
.ui
.desktop
:
1376 self
.downloads_finished(self
.download_tasks_seen
)
1377 if gpodder
.ui
.diablo
:
1378 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1379 log('All downloads have finished.', sender
=self
)
1380 if self
.config
.cmd_all_downloads_complete
:
1381 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1383 if gpodder
.ui
.fremantle
and failed
:
1384 message
= '\n'.join(['%s: %s' % (str(task
), \
1385 task
.error_message
) for task
in failed_downloads
])
1386 self
.show_message(message
, _('Downloads failed'), important
=True)
1388 # Remove finished episodes
1389 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1390 self
.cleanup_downloads()
1392 # Stop updating the download list here
1393 self
.download_list_update_enabled
= False
1395 if not gpodder
.ui
.fremantle
:
1396 self
.gPodder
.set_title(' - '.join(title
))
1398 self
.update_episode_list_icons(episode_urls
)
1399 if self
.episode_shownotes_window
is not None:
1400 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1401 shownotes_task
!= self
.episode_shownotes_window
.task
:
1402 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1403 self
.episode_shownotes_window
._download
_status
_progress
()
1404 self
.play_or_download()
1406 self
.update_podcast_list_model(channel_urls
)
1408 return self
.download_list_update_enabled
1409 except Exception, e
:
1410 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1411 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1412 # We return False here, so the update loop won't be called again,
1413 # that's why we require the restart of gPodder in the message.
1416 def on_config_changed(self
, *args
):
1417 util
.idle_add(self
._on
_config
_changed
, *args
)
1419 def _on_config_changed(self
, name
, old_value
, new_value
):
1420 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1421 self
.toolbar
.set_property('visible', new_value
)
1422 elif name
== 'videoplayer':
1423 self
.config
.video_played_dbus
= False
1424 elif name
== 'player':
1425 self
.config
.audio_played_dbus
= False
1426 elif name
== 'episode_list_descriptions':
1427 self
.update_episode_list_model()
1428 elif name
== 'episode_list_thumbnails':
1429 self
.update_episode_list_icons(all
=True)
1430 elif name
== 'rotation_mode':
1431 self
._fremantle
_rotation
.set_mode(new_value
)
1432 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1433 self
.restart_auto_update_timer()
1434 elif name
== 'podcast_list_view_all':
1435 # Force a update of the podcast list model
1436 self
.channel_list_changed
= True
1437 if gpodder
.ui
.fremantle
:
1438 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1439 while gtk
.events_pending():
1440 gtk
.main_iteration(False)
1441 self
.update_podcast_list_model()
1442 if gpodder
.ui
.fremantle
:
1443 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1445 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1446 # With get_bin_window, we get the window that contains the rows without
1447 # the header. The Y coordinate of this window will be the height of the
1448 # treeview header. This is the amount we have to subtract from the
1449 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1450 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1453 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1455 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1456 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1459 if path
is not None:
1460 model
= treeview
.get_model()
1461 iter = model
.get_iter(path
)
1462 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1464 if role
== TreeViewHelper
.ROLE_EPISODES
:
1465 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1466 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1467 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1469 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1470 if last_tooltip
is not None and last_tooltip
!= id:
1471 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1473 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1475 if role
== TreeViewHelper
.ROLE_EPISODES
:
1476 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1478 tooltip
.set_text(description
)
1481 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1482 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1485 channel
.request_save_dir_size()
1486 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1487 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1489 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1490 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1491 table
= gtk
.Table(rows
=3, columns
=3)
1492 table
.set_row_spacings(5)
1493 table
.set_col_spacings(5)
1494 table
.set_border_width(5)
1496 heading
= gtk
.Label()
1497 heading
.set_alignment(0, 1)
1498 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1499 table
.attach(heading
, 0, 1, 0, 1)
1500 size_info
= gtk
.Label()
1501 size_info
.set_alignment(1, 1)
1502 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1503 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1504 table
.attach(size_info
, 2, 3, 0, 1)
1506 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1508 if len(channel
.description
) < 500:
1509 description
= channel
.description
1511 pos
= channel
.description
.find('\n\n')
1512 if pos
== -1 or pos
> 500:
1513 description
= channel
.description
[:498]+'[...]'
1515 description
= channel
.description
[:pos
]
1517 description
= gtk
.Label(description
)
1519 description
.set_markup(error_str
)
1520 description
.set_alignment(0, 0)
1521 description
.set_line_wrap(True)
1522 table
.attach(description
, 0, 3, 2, 3)
1525 tooltip
.set_custom(table
)
1529 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1532 def treeview_allow_tooltips(self
, treeview
, allow
):
1533 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1535 def update_m3u_playlist_clicked(self
, widget
):
1536 if self
.active_channel
is not None:
1537 self
.active_channel
.update_m3u_playlist()
1538 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1540 def treeview_handle_context_menu_click(self
, treeview
, event
):
1541 x
, y
= int(event
.x
), int(event
.y
)
1542 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1544 selection
= treeview
.get_selection()
1545 model
, paths
= selection
.get_selected_rows()
1547 if path
is None or (path
not in paths
and \
1548 event
.button
== self
.context_menu_mouse_button
):
1549 # We have right-clicked, but not into the selection,
1550 # assume we don't want to operate on the selection
1553 if path
is not None and not paths
and \
1554 event
.button
== self
.context_menu_mouse_button
:
1555 # No selection or clicked outside selection;
1556 # select the single item where we clicked
1557 treeview
.grab_focus()
1558 treeview
.set_cursor(path
, column
, 0)
1562 # Unselect any remaining items (clicked elsewhere)
1563 if hasattr(treeview
, 'is_rubber_banding_active'):
1564 if not treeview
.is_rubber_banding_active():
1565 selection
.unselect_all()
1567 selection
.unselect_all()
1571 def downloads_list_get_selection(self
, model
=None, paths
=None):
1572 if model
is None and paths
is None:
1573 selection
= self
.treeDownloads
.get_selection()
1574 model
, paths
= selection
.get_selected_rows()
1576 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1577 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1578 model
.get_value(model
.get_iter(path
), \
1579 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1581 for row_reference
, task
in selected_tasks
:
1582 if task
.status
!= download
.DownloadTask
.QUEUED
:
1584 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1585 download
.DownloadTask
.FAILED
, \
1586 download
.DownloadTask
.CANCELLED
):
1588 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1589 download
.DownloadTask
.QUEUED
, \
1590 download
.DownloadTask
.DOWNLOADING
, \
1591 download
.DownloadTask
.FAILED
):
1593 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1594 download
.DownloadTask
.DOWNLOADING
):
1596 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1597 download
.DownloadTask
.FAILED
, \
1598 download
.DownloadTask
.DONE
):
1601 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1603 def downloads_finished(self
, download_tasks_seen
):
1604 # FIXME: Filter all tasks that have already been reported
1605 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1606 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1608 if finished_downloads
and failed_downloads
:
1609 message
= self
.format_episode_list(finished_downloads
, 5)
1610 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1611 message
+= self
.format_episode_list(failed_downloads
, 5)
1612 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1613 elif finished_downloads
:
1614 message
= self
.format_episode_list(finished_downloads
)
1615 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1616 elif failed_downloads
:
1617 message
= self
.format_episode_list(failed_downloads
)
1618 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1620 # Open torrent files right after download (bug 1029)
1621 if self
.config
.open_torrent_after_download
:
1622 for task
in download_tasks_seen
:
1623 if task
.status
!= task
.DONE
:
1626 episode
= task
.episode
1627 if episode
.mimetype
!= 'application/x-bittorrent':
1630 self
.playback_episodes([episode
])
1633 def format_episode_list(self
, episode_list
, max_episodes
=10):
1635 Format a list of episode names for notifications
1637 Will truncate long episode names and limit the amount of
1638 episodes displayed (max_episodes=10).
1640 The episode_list parameter should be a list of strings.
1642 MAX_TITLE_LENGTH
= 100
1645 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1646 if len(title
) > MAX_TITLE_LENGTH
:
1647 middle
= (MAX_TITLE_LENGTH
/2)-2
1648 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1649 result
.append(saxutils
.escape(title
))
1652 more_episodes
= len(episode_list
) - max_episodes
1653 if more_episodes
> 0:
1654 result
.append('(...')
1655 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1656 result
.append('...)')
1658 return (''.join(result
)).strip()
1660 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1661 episode_urls
= set()
1662 model
= self
.treeDownloads
.get_model()
1663 for row_reference
, task
in tasks
:
1664 if status
== download
.DownloadTask
.QUEUED
:
1665 # Only queue task when its paused/failed/cancelled (or forced)
1666 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1667 self
.download_queue_manager
.add_task(task
, force_start
)
1668 self
.enable_download_list_update()
1669 elif status
== download
.DownloadTask
.CANCELLED
:
1670 # Cancelling a download allowed when downloading/queued
1671 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1672 task
.status
= status
1673 # Cancelling paused/failed downloads requires a call to .run()
1674 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1675 task
.status
= status
1676 # Call run, so the partial file gets deleted
1678 elif status
== download
.DownloadTask
.PAUSED
:
1679 # Pausing a download only when queued/downloading
1680 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1681 task
.status
= status
1682 elif status
is None:
1683 # Remove the selected task - cancel downloading/queued tasks
1684 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1685 task
.status
= task
.CANCELLED
1686 model
.remove(model
.get_iter(row_reference
.get_path()))
1687 # Remember the URL, so we can tell the UI to update
1689 # We don't "see" this task anymore - remove it;
1690 # this is needed, so update_episode_list_icons()
1691 # below gets the correct list of "seen" tasks
1692 self
.download_tasks_seen
.remove(task
)
1693 except KeyError, key_error
:
1694 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1695 episode_urls
.add(task
.url
)
1696 # Tell the task that it has been removed (so it can clean up)
1697 task
.removed_from_list()
1699 # We can (hopefully) simply set the task status here
1700 task
.status
= status
1701 # Tell the podcasts tab to update icons for our removed podcasts
1702 self
.update_episode_list_icons(episode_urls
)
1703 # Update the tab title and downloads list
1704 self
.update_downloads_list()
1706 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1707 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1709 if not hasattr(treeview
, 'is_rubber_banding_active'):
1712 return not treeview
.is_rubber_banding_active()
1714 if event
.button
== self
.context_menu_mouse_button
:
1715 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1716 self
.downloads_list_get_selection(model
, paths
)
1718 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1719 # This creates a menu item for selection-wide actions
1720 item
= gtk
.ImageMenuItem(label
)
1721 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1722 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1723 item
.set_sensitive(sensitive
)
1724 return self
.set_finger_friendly(item
)
1728 item
= gtk
.ImageMenuItem(_('Episode details'))
1729 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1730 if len(selected_tasks
) == 1:
1731 row_reference
, task
= selected_tasks
[0]
1732 episode
= task
.episode
1733 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1735 item
.set_sensitive(False)
1736 menu
.append(self
.set_finger_friendly(item
))
1737 menu
.append(gtk
.SeparatorMenuItem())
1739 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1741 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1742 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1743 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1744 menu
.append(gtk
.SeparatorMenuItem())
1745 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1747 if gpodder
.ui
.maemo
:
1748 # Because we open the popup on left-click for Maemo,
1749 # we also include a non-action to close the menu
1750 menu
.append(gtk
.SeparatorMenuItem())
1751 item
= gtk
.ImageMenuItem(_('Close this menu'))
1752 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1754 menu
.append(self
.set_finger_friendly(item
))
1757 menu
.popup(None, None, None, event
.button
, event
.time
)
1760 def treeview_channels_show_context_menu(self
, treeview
, event
):
1761 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1765 # Check for valid channel id, if there's no id then
1766 # assume that it is a proxy channel or equivalent
1767 # and cannot be operated with right click
1768 if self
.active_channel
.id is None:
1771 if event
.button
== 3:
1776 item
= gtk
.ImageMenuItem( _('Update podcast'))
1777 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1778 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1779 item
.set_sensitive(not self
.updating_feed_cache
)
1782 menu
.append(gtk
.SeparatorMenuItem())
1784 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1785 item
.set_active(self
.active_channel
.channel_is_locked
)
1786 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1787 menu
.append(self
.set_finger_friendly(item
))
1789 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1790 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1791 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1794 if self
.config
.device_type
!= 'none':
1795 item
= gtk
.MenuItem(_('Synchronize to device'))
1796 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1799 menu
.append( gtk
.SeparatorMenuItem())
1801 item
= gtk
.ImageMenuItem(_('Podcast details'))
1802 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1803 item
.connect('activate', self
.on_itemEditChannel_activate
)
1807 # Disable tooltips while we are showing the menu, so
1808 # the tooltip will not appear over the menu
1809 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1810 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1811 menu
.popup( None, None, None, event
.button
, event
.time
)
1815 def on_itemClose_activate(self
, widget
):
1816 if self
.tray_icon
is not None:
1817 self
.iconify_main_window()
1819 self
.on_gPodder_delete_event(widget
)
1821 def cover_file_removed(self
, channel_url
):
1823 The Cover Downloader calls this when a previously-
1824 available cover has been removed from the disk. We
1825 have to update our model to reflect this change.
1827 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1829 def cover_download_finished(self
, channel
, pixbuf
):
1831 The Cover Downloader calls this when it has finished
1832 downloading (or registering, if already downloaded)
1833 a new channel cover, which is ready for displaying.
1835 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1837 def save_episodes_as_file(self
, episodes
):
1838 for episode
in episodes
:
1839 self
.save_episode_as_file(episode
)
1841 def save_episode_as_file(self
, episode
):
1842 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1843 if episode
.was_downloaded(and_exists
=True):
1844 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1845 copy_from
= episode
.local_filename(create
=False)
1846 assert copy_from
is not None
1847 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1848 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1849 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1851 def copy_episodes_bluetooth(self
, episodes
):
1852 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1854 if gpodder
.ui
.maemo
:
1855 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1856 for e
in episodes_to_copy
])
1859 def convert_and_send_thread(episode
):
1860 for episode
in episodes
:
1861 filename
= episode
.local_filename(create
=False)
1862 assert filename
is not None
1863 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1864 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1865 (base
, ext
) = os
.path
.splitext(filename
)
1866 if not destfile
.endswith(ext
):
1870 shutil
.copyfile(filename
, destfile
)
1871 util
.bluetooth_send_file(destfile
)
1873 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1874 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1876 util
.delete_file(destfile
)
1878 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1880 def get_device_name(self
):
1881 if self
.config
.device_type
== 'ipod':
1883 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1884 return _('MP3 player')
1886 return '(unknown device)'
1888 def _treeview_button_released(self
, treeview
, event
):
1889 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1890 dy
= int(abs(event
.y
-ypos
))
1891 dx
= int(event
.x
-xpos
)
1893 selection
= treeview
.get_selection()
1894 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1895 if path
is None or dy
> 30:
1896 return (False, dx
, dy
)
1898 path
, column
, x
, y
= path
1899 selection
.select_path(path
)
1900 treeview
.set_cursor(path
)
1901 treeview
.grab_focus()
1903 return (True, dx
, dy
)
1905 def treeview_channels_handle_gestures(self
, treeview
, event
):
1906 if self
.currently_updating
:
1909 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1912 if self
.config
.maemo_enable_gestures
:
1914 self
.on_itemUpdateChannel_activate()
1916 self
.on_itemEditChannel_activate(treeview
)
1920 def treeview_available_handle_gestures(self
, treeview
, event
):
1921 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1924 if self
.config
.maemo_enable_gestures
:
1926 self
.on_playback_selected_episodes(None)
1929 self
.on_shownotes_selected_episodes(None)
1932 # Pass the event to the context menu handler for treeAvailable
1933 self
.treeview_available_show_context_menu(treeview
, event
)
1937 def treeview_available_show_context_menu(self
, treeview
, event
):
1938 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1940 if not hasattr(treeview
, 'is_rubber_banding_active'):
1943 return not treeview
.is_rubber_banding_active()
1945 if event
.button
== self
.context_menu_mouse_button
:
1946 episodes
= self
.get_selected_episodes()
1947 any_locked
= any(e
.is_locked
for e
in episodes
)
1948 any_played
= any(e
.is_played
for e
in episodes
)
1949 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1950 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1951 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1955 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1957 if open_instead_of_play
:
1958 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1960 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1962 item
= gtk
.ImageMenuItem(_('Stream'))
1963 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1965 item
.set_sensitive(can_play
and not downloading
)
1966 item
.connect('activate', self
.on_playback_selected_episodes
)
1967 menu
.append(self
.set_finger_friendly(item
))
1970 item
= gtk
.ImageMenuItem(_('Download'))
1971 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1972 item
.set_sensitive(can_download
)
1973 item
.connect('activate', self
.on_download_selected_episodes
)
1974 menu
.append(self
.set_finger_friendly(item
))
1976 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1977 item
.connect('activate', self
.on_item_cancel_download_activate
)
1978 menu
.append(self
.set_finger_friendly(item
))
1980 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1981 item
.set_sensitive(can_delete
)
1982 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1983 menu
.append(self
.set_finger_friendly(item
))
1987 # Ok, this probably makes sense to only display for downloaded files
1989 menu
.append(gtk
.SeparatorMenuItem())
1990 share_item
= gtk
.MenuItem(_('Send to'))
1991 menu
.append(self
.set_finger_friendly(share_item
))
1992 share_menu
= gtk
.Menu()
1994 item
= gtk
.ImageMenuItem(_('Local folder'))
1995 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
1996 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
1997 share_menu
.append(self
.set_finger_friendly(item
))
1998 if self
.bluetooth_available
:
1999 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
2000 if gpodder
.ui
.maemo
:
2001 icon_name
= ICON('qgn_list_filesys_bluetooth')
2003 icon_name
= ICON('bluetooth')
2004 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2005 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2006 share_menu
.append(self
.set_finger_friendly(item
))
2008 item
= gtk
.ImageMenuItem(self
.get_device_name())
2009 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2010 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2011 share_menu
.append(self
.set_finger_friendly(item
))
2013 share_item
.set_submenu(share_menu
)
2015 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2016 menu
.append(gtk
.SeparatorMenuItem())
2018 item
= gtk
.CheckMenuItem(_('New'))
2019 item
.set_active(True)
2020 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2021 menu
.append(self
.set_finger_friendly(item
))
2023 item
= gtk
.CheckMenuItem(_('New'))
2024 item
.set_active(False)
2025 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2026 menu
.append(self
.set_finger_friendly(item
))
2029 item
= gtk
.CheckMenuItem(_('Played'))
2030 item
.set_active(any_played
)
2031 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2032 menu
.append(self
.set_finger_friendly(item
))
2034 item
= gtk
.CheckMenuItem(_('Keep episode'))
2035 item
.set_active(any_locked
)
2036 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2037 menu
.append(self
.set_finger_friendly(item
))
2039 menu
.append(gtk
.SeparatorMenuItem())
2040 # Single item, add episode information menu item
2041 item
= gtk
.ImageMenuItem(_('Episode details'))
2042 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2043 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2044 menu
.append(self
.set_finger_friendly(item
))
2046 if gpodder
.ui
.maemo
:
2047 # Because we open the popup on left-click for Maemo,
2048 # we also include a non-action to close the menu
2049 menu
.append(gtk
.SeparatorMenuItem())
2050 item
= gtk
.ImageMenuItem(_('Close this menu'))
2051 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2052 menu
.append(self
.set_finger_friendly(item
))
2055 # Disable tooltips while we are showing the menu, so
2056 # the tooltip will not appear over the menu
2057 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2058 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2059 menu
.popup( None, None, None, event
.button
, event
.time
)
2063 def set_title(self
, new_title
):
2064 if not gpodder
.ui
.fremantle
:
2065 self
.default_title
= new_title
2066 self
.gPodder
.set_title(new_title
)
2068 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2070 Updates the status icons in the episode list.
2072 If urls is given, it should be a list of URLs
2073 of episodes that should be updated.
2075 If urls is None, set ONE OF selected, all to
2076 True (the former updates just the selected
2077 episodes and the latter updates all episodes).
2079 additional_args
= (self
.episode_is_downloading
, \
2080 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2081 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2083 if urls
is not None:
2084 # We have a list of URLs to walk through
2085 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2086 elif selected
and not all
:
2087 # We should update all selected episodes
2088 selection
= self
.treeAvailable
.get_selection()
2089 model
, paths
= selection
.get_selected_rows()
2090 for path
in reversed(paths
):
2091 iter = model
.get_iter(path
)
2092 self
.episode_list_model
.update_by_filter_iter(iter, \
2094 elif all
and not selected
:
2095 # We update all (even the filter-hidden) episodes
2096 self
.episode_list_model
.update_all(*additional_args
)
2098 # Wrong/invalid call - have to specify at least one parameter
2099 raise ValueError('Invalid call to update_episode_list_icons')
2101 def episode_list_status_changed(self
, episodes
):
2102 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2103 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2106 def clean_up_downloads(self
, delete_partial
=False):
2107 # Clean up temporary files left behind by old gPodder versions
2108 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2111 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2113 for tempfile
in temporary_files
:
2114 util
.delete_file(tempfile
)
2116 # Clean up empty download folders and abandoned download folders
2117 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2118 for ddir
in download_dirs
:
2119 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2120 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2121 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2122 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2123 shutil
.rmtree(ddir
, ignore_errors
=True)
2125 def streaming_possible(self
):
2126 if gpodder
.ui
.desktop
:
2127 # User has to have a media player set on the Desktop, or else we
2128 # would probably open the browser when giving a URL to xdg-open..
2129 return (self
.config
.player
and self
.config
.player
!= 'default')
2130 elif gpodder
.ui
.maemo
:
2131 # On Maemo, the default is to use the Nokia Media Player, which is
2132 # already able to deal with HTTP URLs the right way, so we
2133 # unconditionally enable streaming always on Maemo
2138 def playback_episodes_for_real(self
, episodes
):
2139 groups
= collections
.defaultdict(list)
2140 for episode
in episodes
:
2141 file_type
= episode
.file_type()
2142 if file_type
== 'video' and self
.config
.videoplayer
and \
2143 self
.config
.videoplayer
!= 'default':
2144 player
= self
.config
.videoplayer
2145 if gpodder
.ui
.diablo
:
2146 # Use the wrapper script if it's installed to crop 3GP YouTube
2147 # videos to fit the screen (looks much nicer than w/ black border)
2148 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2149 player
= 'gpodder-mplayer'
2150 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2151 player
= 'mplayer -fs %F'
2152 elif file_type
== 'audio' and self
.config
.player
and \
2153 self
.config
.player
!= 'default':
2154 player
= self
.config
.player
2158 if file_type
not in ('audio', 'video') or \
2159 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2160 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2161 # Mark episode as played in the database
2162 episode
.mark(is_played
=True)
2163 self
.mygpo_client
.on_playback([episode
])
2165 filename
= episode
.local_filename(create
=False)
2166 if filename
is None or not os
.path
.exists(filename
):
2167 filename
= episode
.url
2168 if youtube
.is_video_link(filename
):
2169 fmt_id
= self
.config
.youtube_preferred_fmt_id
2170 if gpodder
.ui
.fremantle
:
2172 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2174 # Determine the playback resume position - if the file
2175 # was played 100%, we simply start from the beginning
2176 resume_position
= episode
.current_position
2177 if resume_position
== episode
.total_time
:
2180 if gpodder
.ui
.fremantle
:
2181 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2183 # If Panucci is configured, use D-Bus on Maemo to call it
2184 if player
== 'panucci':
2186 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2187 PANUCCI_PATH
= '/panucciInterface'
2188 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2189 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2190 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2192 def on_reply(*args
):
2195 def error_handler(filename
, err
):
2196 log('Exception in D-Bus call: %s', str(err
), \
2199 # Fallback: use the command line client
2200 for command
in util
.format_desktop_command('panucci', \
2202 log('Executing: %s', repr(command
), sender
=self
)
2203 subprocess
.Popen(command
)
2205 on_error
= lambda err
: error_handler(filename
, err
)
2207 # This method only exists in Panucci > 0.9 ('new Panucci')
2208 i
.playback_from(filename
, resume_position
, \
2209 reply_handler
=on_reply
, error_handler
=on_error
)
2211 continue # This file was handled by the D-Bus call
2212 except Exception, e
:
2213 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2214 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2216 MEDIABOX_NAME
= 'de.pycage.mediabox'
2217 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2218 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2219 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2220 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2222 def on_reply(*args
):
2226 log('Exception in D-Bus call: %s', str(err
), \
2229 i
.load(filename
, '%s/x-unknown' % file_type
, \
2230 reply_handler
=on_reply
, error_handler
=on_error
)
2232 continue # This file was handled by the D-Bus call
2233 except Exception, e
:
2234 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2236 groups
[player
].append(filename
)
2238 # Open episodes with system default player
2239 if 'default' in groups
:
2240 if gpodder
.ui
.maemo
:
2241 # The Nokia Media Player app does not support receiving multiple
2242 # file names via D-Bus, so we simply place all file names into a
2243 # temporary M3U playlist and open that with the Media Player.
2244 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2245 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2246 util
.gui_open(m3u_filename
)
2248 for filename
in groups
['default']:
2249 log('Opening with system default: %s', filename
, sender
=self
)
2250 util
.gui_open(filename
)
2251 del groups
['default']
2252 elif gpodder
.ui
.maemo
and groups
:
2253 # When on Maemo and not opening with default, show a notification
2254 # (no startup notification for Panucci / MPlayer yet...)
2255 if len(episodes
) == 1:
2256 text
= _('Opening %s') % episodes
[0].title
2258 count
= len(episodes
)
2259 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2261 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2263 def destroy_banner_later(banner
):
2266 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2268 # For each type now, go and create play commands
2269 for group
in groups
:
2270 for command
in util
.format_desktop_command(group
, groups
[group
]):
2271 log('Executing: %s', repr(command
), sender
=self
)
2272 subprocess
.Popen(command
)
2274 # Persist episode status changes to the database
2277 # Flush updated episode status
2278 self
.mygpo_client
.flush()
2280 def playback_episodes(self
, episodes
):
2281 # We need to create a list, because we run through it more than once
2282 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2283 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2286 self
.playback_episodes_for_real(episodes
)
2287 except Exception, e
:
2288 log('Error in playback!', sender
=self
, traceback
=True)
2289 if gpodder
.ui
.desktop
:
2290 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2291 _('Error opening player'), widget
=self
.toolPreferences
)
2293 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2295 channel_urls
= set()
2296 episode_urls
= set()
2297 for episode
in episodes
:
2298 channel_urls
.add(episode
.channel
.url
)
2299 episode_urls
.add(episode
.url
)
2300 self
.update_episode_list_icons(episode_urls
)
2301 self
.update_podcast_list_model(channel_urls
)
2303 def play_or_download(self
):
2304 if not gpodder
.ui
.fremantle
:
2305 if self
.wNotebook
.get_current_page() > 0:
2306 if gpodder
.ui
.desktop
:
2307 self
.toolCancel
.set_sensitive(True)
2310 if self
.currently_updating
:
2311 return (False, False, False, False, False, False)
2313 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2314 ( is_played
, is_locked
) = (False,)*2
2316 open_instead_of_play
= False
2318 selection
= self
.treeAvailable
.get_selection()
2319 if selection
.count_selected_rows() > 0:
2320 (model
, paths
) = selection
.get_selected_rows()
2324 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2325 except TypeError, te
:
2326 log('Invalid episode at path %s', str(path
), sender
=self
)
2329 if episode
.file_type() not in ('audio', 'video'):
2330 open_instead_of_play
= True
2332 if episode
.was_downloaded():
2333 can_play
= episode
.was_downloaded(and_exists
=True)
2334 is_played
= episode
.is_played
2335 is_locked
= episode
.is_locked
2339 if self
.episode_is_downloading(episode
):
2344 can_download
= can_download
and not can_cancel
2345 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2346 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2347 can_delete
= not can_cancel
2349 if gpodder
.ui
.desktop
:
2350 if open_instead_of_play
:
2351 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2353 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2354 self
.toolPlay
.set_sensitive( can_play
)
2355 self
.toolDownload
.set_sensitive( can_download
)
2356 self
.toolTransfer
.set_sensitive( can_transfer
)
2357 self
.toolCancel
.set_sensitive( can_cancel
)
2359 if not gpodder
.ui
.fremantle
:
2360 self
.item_cancel_download
.set_sensitive(can_cancel
)
2361 self
.itemDownloadSelected
.set_sensitive(can_download
)
2362 self
.itemOpenSelected
.set_sensitive(can_play
)
2363 self
.itemPlaySelected
.set_sensitive(can_play
)
2364 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2365 self
.item_toggle_played
.set_sensitive(can_play
)
2366 self
.item_toggle_lock
.set_sensitive(can_play
)
2367 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2368 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2370 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2372 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2373 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2375 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2376 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2378 def episode_new_status_changed(self
, urls
):
2379 self
.update_podcast_list_model()
2380 self
.update_episode_list_icons(urls
)
2382 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2383 """Update the podcast list treeview model
2385 If urls is given, it should list the URLs of each
2386 podcast that has to be updated in the list.
2388 If selected is True, only update the model contents
2389 for the currently-selected podcast - nothing more.
2391 The caller can optionally specify "select_url",
2392 which is the URL of the podcast that is to be
2393 selected in the list after the update is complete.
2394 This only works if the podcast list has to be
2395 reloaded; i.e. something has been added or removed
2396 since the last update of the podcast list).
2398 selection
= self
.treeChannels
.get_selection()
2399 model
, iter = selection
.get_selected()
2401 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2402 # Update "all episodes" view in any case (if enabled)
2403 self
.podcast_list_model
.update_first_row()
2406 # very cheap! only update selected channel
2407 if iter is not None:
2408 # If we have selected the "all episodes" view, we have
2409 # to update all channels for selected episodes:
2410 if self
.config
.podcast_list_view_all
and \
2411 self
.podcast_list_model
.iter_is_first_row(iter):
2412 urls
= self
.get_podcast_urls_from_selected_episodes()
2413 self
.podcast_list_model
.update_by_urls(urls
)
2415 # Otherwise just update the selected row (a podcast)
2416 self
.podcast_list_model
.update_by_filter_iter(iter)
2417 elif not self
.channel_list_changed
:
2418 # we can keep the model, but have to update some
2420 # still cheaper than reloading the whole list
2421 self
.podcast_list_model
.update_all()
2423 # ok, we got a bunch of urls to update
2424 self
.podcast_list_model
.update_by_urls(urls
)
2426 if model
and iter and select_url
is None:
2427 # Get the URL of the currently-selected podcast
2428 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2430 # Update the podcast list model with new channels
2431 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2434 selected_iter
= model
.get_iter_first()
2435 # Find the previously-selected URL in the new
2436 # model if we have an URL (else select first)
2437 if select_url
is not None:
2438 pos
= model
.get_iter_first()
2439 while pos
is not None:
2440 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2441 if url
== select_url
:
2444 pos
= model
.iter_next(pos
)
2446 if not gpodder
.ui
.fremantle
:
2447 if selected_iter
is not None:
2448 selection
.select_iter(selected_iter
)
2449 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2451 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2452 self
.channel_list_changed
= False
2454 def episode_is_downloading(self
, episode
):
2455 """Returns True if the given episode is being downloaded at the moment"""
2459 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2461 def update_episode_list_model(self
):
2462 if self
.channels
and self
.active_channel
is not None:
2463 if gpodder
.ui
.fremantle
:
2464 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2466 self
.currently_updating
= True
2467 self
.episode_list_model
.clear()
2468 self
.episode_list_model
.reset_update_progress()
2469 self
.treeAvailable
.set_model(self
.empty_episode_list_model
)
2470 def do_update_episode_list_model():
2471 additional_args
= (self
.episode_is_downloading
, \
2472 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2473 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
, \
2475 self
.episode_list_model
.add_from_channel(self
.active_channel
, *additional_args
)
2477 def on_episode_list_model_updated():
2478 if gpodder
.ui
.fremantle
:
2479 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2480 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
2481 self
.treeAvailable
.columns_autosize()
2482 self
.currently_updating
= False
2483 self
.play_or_download()
2484 util
.idle_add(on_episode_list_model_updated
)
2485 threading
.Thread(target
=do_update_episode_list_model
).start()
2487 self
.episode_list_model
.clear()
2489 @dbus.service
.method(gpodder
.dbus_interface
)
2490 def offer_new_episodes(self
, channels
=None):
2491 if gpodder
.ui
.fremantle
:
2492 # Assume that when this function is called that the
2493 # notification is not shown anymore (Maemo bug 11345)
2494 self
._fremantle
_notification
_visible
= False
2496 new_episodes
= self
.get_new_episodes(channels
)
2498 self
.new_episodes_show(new_episodes
)
2502 def add_podcast_list(self
, urls
, auth_tokens
=None):
2503 """Subscribe to a list of podcast given their URLs
2505 If auth_tokens is given, it should be a dictionary
2506 mapping URLs to (username, password) tuples."""
2508 if auth_tokens
is None:
2511 # Sort and split the URL list into five buckets
2512 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2513 for input_url
in urls
:
2514 url
= util
.normalize_feed_url(input_url
)
2516 # Fail this one because the URL is not valid
2517 failed
.append(input_url
)
2518 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2519 # A podcast already exists in the list for this URL
2520 existing
.append(url
)
2522 # This URL has survived the first round - queue for add
2524 if url
!= input_url
and input_url
in auth_tokens
:
2525 auth_tokens
[url
] = auth_tokens
[input_url
]
2530 progress
= ProgressIndicator(_('Adding podcasts'), \
2531 _('Please wait while episode information is downloaded.'), \
2532 parent
=self
.get_dialog_parent())
2534 def on_after_update():
2535 progress
.on_finished()
2536 # Report already-existing subscriptions to the user
2538 title
= _('Existing subscriptions skipped')
2539 message
= _('You are already subscribed to these podcasts:') \
2540 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2541 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2543 # Report subscriptions that require authentication
2547 title
= _('Podcast requires authentication')
2548 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2549 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2551 retry_podcasts
[url
] = auth_tokens
2553 # Stop asking the user for more login data
2556 error_messages
[url
] = _('Authentication failed')
2560 # If we have authentication data to retry, do so here
2562 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2564 # Report website redirections
2565 for url
in redirections
:
2566 title
= _('Website redirection detected')
2567 message
= _('The URL %(url)s redirects to %(target)s.') \
2568 + '\n\n' + _('Do you want to visit the website now?')
2569 message
= message
% {'url': url
, 'target': redirections
[url
]}
2570 if self
.show_confirmation(message
, title
):
2571 util
.open_website(url
)
2575 # Report failed subscriptions to the user
2577 title
= _('Could not add some podcasts')
2578 message
= _('Some podcasts could not be added to your list:') \
2579 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2580 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2581 self
.show_message(message
, title
, important
=True)
2583 # Upload subscription changes to gpodder.net
2584 self
.mygpo_client
.on_subscribe(worked
)
2586 # If at least one podcast has been added, save and update all
2587 if self
.channel_list_changed
:
2588 # Fix URLs if mygpo has rewritten them
2589 self
.rewrite_urls_mygpo()
2591 self
.save_channels_opml()
2593 # If only one podcast was added, select it after the update
2594 if len(worked
) == 1:
2599 # Update the list of subscribed podcasts
2600 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2601 self
.update_podcasts_tab()
2603 # Offer to download new episodes
2605 for podcast
in self
.channels
:
2606 if podcast
.url
in worked
:
2607 episodes
.extend(podcast
.get_all_episodes())
2610 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2612 self
.new_episodes_show(episodes
, \
2613 selected
=[e
.check_is_new() for e
in episodes
])
2617 # After the initial sorting and splitting, try all queued podcasts
2618 length
= len(queued
)
2619 for index
, url
in enumerate(queued
):
2620 progress
.on_progress(float(index
)/float(length
))
2621 progress
.on_message(url
)
2622 log('QUEUE RUNNER: %s', url
, sender
=self
)
2624 # The URL is valid and does not exist already - subscribe!
2625 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2626 authentication_tokens
=auth_tokens
.get(url
, None), \
2627 max_episodes
=self
.config
.max_episodes_per_feed
, \
2628 download_dir
=self
.config
.download_dir
, \
2629 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2630 mimetype_prefs
=self
.config
.mimetype_prefs
)
2633 username
, password
= util
.username_password_from_url(url
)
2634 except ValueError, ve
:
2635 username
, password
= (None, None)
2637 if username
is not None and channel
.username
is None and \
2638 password
is not None and channel
.password
is None:
2639 channel
.username
= username
2640 channel
.password
= password
2643 self
._update
_cover
(channel
)
2644 except feedcore
.AuthenticationRequired
:
2645 if url
in auth_tokens
:
2646 # Fail for wrong authentication data
2647 error_messages
[url
] = _('Authentication failed')
2650 # Queue for login dialog later
2653 except feedcore
.WifiLogin
, error
:
2654 redirections
[url
] = error
.data
2656 error_messages
[url
] = _('Redirection detected')
2658 except Exception, e
:
2659 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2660 error_messages
[url
] = str(e
)
2664 assert channel
is not None
2665 worked
.append(channel
.url
)
2666 self
.channels
.append(channel
)
2667 self
.channel_list_changed
= True
2668 util
.idle_add(on_after_update
)
2669 threading
.Thread(target
=thread_proc
).start()
2671 def save_channels_opml(self
):
2672 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2673 return exporter
.write(self
.channels
)
2675 def find_episode(self
, podcast_url
, episode_url
):
2676 """Find an episode given its podcast and episode URL
2678 The function will return a PodcastEpisode object if
2679 the episode is found, or None if it's not found.
2681 for podcast
in self
.channels
:
2682 if podcast_url
== podcast
.url
:
2683 for episode
in podcast
.get_all_episodes():
2684 if episode_url
== episode
.url
:
2689 def process_received_episode_actions(self
, updated_urls
):
2690 """Process/merge episode actions from gpodder.net
2692 This function will merge all changes received from
2693 the server to the local database and update the
2694 status of the affected episodes as necessary.
2696 indicator
= ProgressIndicator(_('Merging episode actions'), \
2697 _('Episode actions from gpodder.net are merged.'), \
2698 False, self
.get_dialog_parent())
2700 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2701 if action
.action
== 'play':
2702 episode
= self
.find_episode(action
.podcast_url
, \
2705 if episode
is not None:
2706 log('Play action for %s', episode
.url
, sender
=self
)
2707 episode
.mark(is_played
=True)
2709 if action
.timestamp
> episode
.current_position_updated
:
2710 log('Updating position for %s', episode
.url
, sender
=self
)
2711 episode
.current_position
= action
.position
2712 episode
.current_position_updated
= action
.timestamp
2715 log('Updating total time for %s', episode
.url
, sender
=self
)
2716 episode
.total_time
= action
.total
2719 elif action
.action
== 'delete':
2720 episode
= self
.find_episode(action
.podcast_url
, \
2723 if episode
is not None:
2724 if not episode
.was_downloaded(and_exists
=True):
2725 # Set the episode to a "deleted" state
2726 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2727 episode
.delete_from_disk()
2730 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2731 gtk
.main_iteration(False)
2733 indicator
.on_finished()
2737 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2739 self
.updating_feed_cache
= False
2741 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2743 # Process received episode actions for all updated URLs
2744 self
.process_received_episode_actions(updated_urls
)
2746 self
.channel_list_changed
= True
2747 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2749 # Only search for new episodes in podcasts that have been
2750 # updated, not in other podcasts (for single-feed updates)
2751 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2753 if gpodder
.ui
.fremantle
:
2754 self
.fancy_progress_bar
.hide()
2755 self
.button_subscribe
.set_sensitive(True)
2756 self
.button_refresh
.set_sensitive(True)
2757 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2758 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2759 self
.update_podcasts_tab()
2760 self
.update_episode_list_model()
2761 if self
.feed_cache_update_cancelled
:
2764 def application_in_foreground():
2766 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2767 except Exception, e
:
2768 log('Could not determine is-topmost', traceback
=True)
2769 # When in doubt, assume not in foreground
2773 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2774 # New episodes found, but we should do nothing
2775 self
.show_message(_('New episodes are available.'))
2776 elif self
.config
.auto_download
== 'always':
2777 count
= len(episodes
)
2778 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2779 self
.show_message(title
)
2780 self
.download_episode_list(episodes
)
2781 elif self
.config
.auto_download
== 'queue':
2782 self
.show_message(_('New episodes have been added to the download list.'))
2783 self
.download_episode_list_paused(episodes
)
2784 elif application_in_foreground():
2785 if not self
._fremantle
_notification
_visible
:
2786 self
.new_episodes_show(episodes
)
2787 elif not self
._fremantle
_notification
_visible
:
2790 pynotify
.init('gPodder')
2791 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2792 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2793 n
.set_hint('dbus-callback-default', ' '.join([
2794 gpodder
.dbus_bus_name
,
2795 gpodder
.dbus_gui_object_path
,
2796 gpodder
.dbus_interface
,
2797 'offer_new_episodes',
2799 n
.set_category('gpodder-new-episodes')
2801 self
._fremantle
_notification
_visible
= True
2802 except Exception, e
:
2803 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2804 self
.new_episodes_show(episodes
)
2805 self
._fremantle
_notification
_visible
= False
2806 elif not self
.config
.auto_update_feeds
:
2807 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2811 self
.tray_icon
.set_status()
2813 if self
.feed_cache_update_cancelled
:
2814 # The user decided to abort the feed update
2815 self
.show_update_feeds_buttons()
2817 # Nothing new here - but inform the user
2818 self
.pbFeedUpdate
.set_fraction(1.0)
2819 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2820 self
.feed_cache_update_cancelled
= True
2821 self
.btnCancelFeedUpdate
.show()
2822 self
.btnCancelFeedUpdate
.set_sensitive(True)
2823 if gpodder
.ui
.maemo
:
2824 # btnCancelFeedUpdate is a ToolButton on Maemo
2825 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2827 # btnCancelFeedUpdate is a normal gtk.Button
2828 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2830 count
= len(episodes
)
2831 # New episodes are available
2832 self
.pbFeedUpdate
.set_fraction(1.0)
2833 # Are we minimized and should we auto download?
2834 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2835 self
.download_episode_list(episodes
)
2836 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2837 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2838 self
.show_update_feeds_buttons()
2839 elif self
.config
.auto_download
== 'queue':
2840 self
.download_episode_list_paused(episodes
)
2841 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2842 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2843 self
.show_update_feeds_buttons()
2845 self
.show_update_feeds_buttons()
2846 # New episodes are available and we are not minimized
2847 if not self
.config
.do_not_show_new_episodes_dialog
:
2848 self
.new_episodes_show(episodes
, notification
=True)
2850 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2851 self
.pbFeedUpdate
.set_text(message
)
2853 def _update_cover(self
, channel
):
2854 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2855 self
.cover_downloader
.request_cover(channel
)
2857 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2858 total
= len(channels
)
2860 for updated
, channel
in enumerate(channels
):
2861 if not self
.feed_cache_update_cancelled
:
2863 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2864 mimetype_prefs
=self
.config
.mimetype_prefs
)
2865 self
._update
_cover
(channel
)
2866 except Exception, e
:
2867 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2869 message
= _('Error while updating %(url)s: %(message)s')
2871 message
= _('The feed at %(url)s could not be updated.')
2872 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2873 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2875 if self
.feed_cache_update_cancelled
:
2878 # By the time we get here the update may have already been cancelled
2879 if not self
.feed_cache_update_cancelled
:
2880 def update_progress():
2881 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2882 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2883 self
.pbFeedUpdate
.set_text(progression
)
2885 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2886 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2887 util
.idle_add(update_progress
)
2889 updated_urls
= [c
.url
for c
in channels
]
2890 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2892 def show_update_feeds_buttons(self
):
2893 # Make sure that the buttons for updating feeds
2894 # appear - this should happen after a feed update
2895 if gpodder
.ui
.maemo
:
2896 self
.btnUpdateSelectedFeed
.show()
2897 self
.toolFeedUpdateProgress
.hide()
2898 self
.btnCancelFeedUpdate
.hide()
2899 self
.btnCancelFeedUpdate
.set_is_important(False)
2900 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2901 self
.toolbarSpacer
.set_expand(True)
2902 self
.toolbarSpacer
.set_draw(False)
2904 self
.hboxUpdateFeeds
.hide()
2905 self
.btnUpdateFeeds
.show()
2906 self
.itemUpdate
.set_sensitive(True)
2907 self
.itemUpdateChannel
.set_sensitive(True)
2909 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2910 if not self
.feed_cache_update_cancelled
:
2911 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2912 self
.feed_cache_update_cancelled
= True
2913 if not gpodder
.ui
.fremantle
:
2914 self
.btnCancelFeedUpdate
.set_sensitive(False)
2916 self
.show_update_feeds_buttons()
2918 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2919 if self
.updating_feed_cache
:
2920 if gpodder
.ui
.fremantle
:
2921 self
.feed_cache_update_cancelled
= True
2924 if not force_update
:
2925 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2926 self
.channel_list_changed
= True
2927 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2930 # Fix URLs if mygpo has rewritten them
2931 self
.rewrite_urls_mygpo()
2933 self
.updating_feed_cache
= True
2935 if channels
is None:
2936 # Only update podcasts for which updates are enabled
2937 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
2939 if gpodder
.ui
.fremantle
:
2940 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2941 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2942 self
.fancy_progress_bar
.show()
2943 self
.button_subscribe
.set_sensitive(False)
2944 self
.button_refresh
.set_sensitive(False)
2945 self
.feed_cache_update_cancelled
= False
2947 self
.itemUpdate
.set_sensitive(False)
2948 self
.itemUpdateChannel
.set_sensitive(False)
2951 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2953 self
.feed_cache_update_cancelled
= False
2954 self
.btnCancelFeedUpdate
.show()
2955 self
.btnCancelFeedUpdate
.set_sensitive(True)
2956 if gpodder
.ui
.maemo
:
2957 self
.toolbarSpacer
.set_expand(False)
2958 self
.toolbarSpacer
.set_draw(True)
2959 self
.btnUpdateSelectedFeed
.hide()
2960 self
.toolFeedUpdateProgress
.show_all()
2962 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2963 self
.hboxUpdateFeeds
.show_all()
2964 self
.btnUpdateFeeds
.hide()
2966 if len(channels
) == 1:
2967 text
= _('Updating "%s"...') % channels
[0].title
2969 count
= len(channels
)
2970 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2971 self
.pbFeedUpdate
.set_text(text
)
2972 self
.pbFeedUpdate
.set_fraction(0)
2974 args
= (channels
, select_url_afterwards
)
2975 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2977 def on_gPodder_delete_event(self
, widget
, *args
):
2978 """Called when the GUI wants to close the window
2979 Displays a confirmation dialog (and closes/hides gPodder)
2982 downloading
= self
.download_status_model
.are_downloads_in_progress()
2984 # Only iconify if we are using the window's "X" button,
2985 # but not when we are using "Quit" in the menu or toolbar
2986 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
2987 self
.iconify_main_window()
2988 elif self
.config
.on_quit_ask
or downloading
:
2989 if gpodder
.ui
.fremantle
:
2990 self
.close_gpodder()
2991 elif gpodder
.ui
.diablo
:
2992 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2994 self
.close_gpodder()
2997 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2998 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2999 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
3001 title
= _('Quit gPodder')
3003 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3005 message
= _('Do you really want to quit gPodder now?')
3007 dialog
.set_title(title
)
3008 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3010 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
3011 dialog
.vbox
.pack_start(cb_ask
)
3014 quit_button
.grab_focus()
3015 result
= dialog
.run()
3018 if result
== gtk
.RESPONSE_CLOSE
:
3019 if not downloading
and cb_ask
.get_active() == True:
3020 self
.config
.on_quit_ask
= False
3021 self
.close_gpodder()
3023 self
.close_gpodder()
3027 def close_gpodder(self
):
3028 """ clean everything and exit properly
3031 if self
.save_channels_opml():
3032 pass # FIXME: Add mygpo synchronization here
3034 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3038 if self
.tray_icon
is not None:
3039 self
.tray_icon
.set_visible(False)
3041 # Notify all tasks to to carry out any clean-up actions
3042 self
.download_status_model
.tell_all_tasks_to_quit()
3044 while gtk
.events_pending():
3045 gtk
.main_iteration(False)
3052 def get_expired_episodes(self
):
3053 for channel
in self
.channels
:
3054 for episode
in channel
.get_downloaded_episodes():
3055 # Never consider locked episodes as old
3056 if episode
.is_locked
:
3059 # Never consider fresh episodes as old
3060 if episode
.age_in_days() < self
.config
.episode_old_age
:
3063 # Do not delete played episodes (except if configured)
3064 if episode
.is_played
:
3065 if not self
.config
.auto_remove_played_episodes
:
3068 # Do not delete unplayed episodes (except if configured)
3069 if not episode
.is_played
:
3070 if not self
.config
.auto_remove_unplayed_episodes
:
3075 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3080 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3083 title
= _('Episodes are locked')
3084 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3085 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3088 count
= len(episodes
)
3089 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
3090 message
= _('Deleting episodes removes downloaded files.')
3092 if gpodder
.ui
.fremantle
:
3093 message
= '\n'.join([title
, message
])
3095 if confirm
and not self
.show_confirmation(message
, title
):
3098 progress
= ProgressIndicator(_('Deleting episodes'), \
3099 _('Please wait while episodes are deleted'), \
3100 parent
=self
.get_dialog_parent())
3102 def finish_deletion(episode_urls
, channel_urls
):
3103 progress
.on_finished()
3105 # Episodes have been deleted - persist the database
3108 self
.update_episode_list_icons(episode_urls
)
3109 self
.update_podcast_list_model(channel_urls
)
3110 self
.play_or_download()
3113 episode_urls
= set()
3114 channel_urls
= set()
3116 episodes_status_update
= []
3117 for idx
, episode
in enumerate(episodes
):
3118 progress
.on_progress(float(idx
)/float(len(episodes
)))
3119 if episode
.is_locked
and skip_locked
:
3120 log('Not deleting episode (is locked): %s', episode
.title
)
3122 log('Deleting episode: %s', episode
.title
)
3123 progress
.on_message(episode
.title
)
3124 episode
.delete_from_disk()
3125 episode_urls
.add(episode
.url
)
3126 channel_urls
.add(episode
.channel
.url
)
3127 episodes_status_update
.append(episode
)
3129 # Tell the shownotes window that we have removed the episode
3130 if self
.episode_shownotes_window
is not None and \
3131 self
.episode_shownotes_window
.episode
is not None and \
3132 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3133 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3135 # Notify the web service about the status update + upload
3136 self
.mygpo_client
.on_delete(episodes_status_update
)
3137 self
.mygpo_client
.flush()
3139 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3141 threading
.Thread(target
=thread_proc
).start()
3145 def on_itemRemoveOldEpisodes_activate( self
, widget
):
3146 if gpodder
.ui
.maemo
:
3148 ('maemo_remove_markup', None, None, _('Episode')),
3152 ('title_markup', None, None, _('Episode')),
3153 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3154 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3155 ('played_prop', None, None, _('Status')),
3156 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3159 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3160 selection_buttons
= {
3161 _('Select played'): lambda episode
: episode
.is_played
,
3162 _('Select finished'): lambda episode
: episode
.is_finished(),
3163 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3166 instructions
= _('Select the episodes you want to delete:')
3170 for channel
in self
.channels
:
3171 for episode
in channel
.get_downloaded_episodes():
3172 # Disallow deletion of locked episodes that still exist
3173 if not episode
.is_locked
or not episode
.file_exists():
3174 episodes
.append(episode
)
3175 # Automatically select played and file-less episodes
3176 selected
.append(episode
.is_played
or \
3177 not episode
.file_exists())
3179 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3180 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3181 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3182 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3183 show_episode_shownotes
=self
.show_episode_shownotes
)
3185 def on_selected_episodes_status_changed(self
):
3186 # The order of the updates here is important! When "All episodes" is
3187 # selected, the update of the podcast list model depends on the episode
3188 # list selection to determine which podcasts are affected. Updating
3189 # the episode list could remove the selection if a filter is active.
3190 self
.update_podcast_list_model(selected
=True)
3191 self
.update_episode_list_icons(selected
=True)
3194 def mark_selected_episodes_new(self
):
3195 for episode
in self
.get_selected_episodes():
3197 self
.on_selected_episodes_status_changed()
3199 def mark_selected_episodes_old(self
):
3200 for episode
in self
.get_selected_episodes():
3202 self
.on_selected_episodes_status_changed()
3204 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3205 for episode
in self
.get_selected_episodes():
3207 episode
.mark(is_played
=not episode
.is_played
)
3209 episode
.mark(is_played
=new_value
)
3210 self
.on_selected_episodes_status_changed()
3212 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3213 for episode
in self
.get_selected_episodes():
3215 episode
.mark(is_locked
=not episode
.is_locked
)
3217 episode
.mark(is_locked
=new_value
)
3218 self
.on_selected_episodes_status_changed()
3220 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3221 if self
.active_channel
is None:
3224 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3225 self
.active_channel
.update_channel_lock()
3227 for episode
in self
.active_channel
.get_all_episodes():
3228 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3230 self
.update_podcast_list_model(selected
=True)
3231 self
.update_episode_list_icons(all
=True)
3233 def on_itemUpdateChannel_activate(self
, widget
=None):
3234 if self
.active_channel
is None:
3235 title
= _('No podcast selected')
3236 message
= _('Please select a podcast in the podcasts list to update.')
3237 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3240 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3241 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3242 self
.update_feed_cache()
3244 self
.update_feed_cache(channels
=[self
.active_channel
])
3246 def on_itemUpdate_activate(self
, widget
=None):
3247 # Check if we have outstanding subscribe/unsubscribe actions
3248 if self
.on_add_remove_podcasts_mygpo():
3249 log('Update cancelled (received server changes)', sender
=self
)
3253 self
.update_feed_cache()
3255 gPodderWelcome(self
.gPodder
,
3256 center_on_widget
=self
.gPodder
,
3257 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3258 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3260 def download_episode_list_paused(self
, episodes
):
3261 self
.download_episode_list(episodes
, True)
3263 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3264 enable_update
= False
3266 for episode
in episodes
:
3267 log('Downloading episode: %s', episode
.title
, sender
= self
)
3268 if not episode
.was_downloaded(and_exists
=True):
3270 for task
in self
.download_tasks_seen
:
3271 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3272 self
.download_queue_manager
.add_task(task
, force_start
)
3273 enable_update
= True
3281 task
= download
.DownloadTask(episode
, self
.config
)
3282 except Exception, e
:
3283 d
= {'episode': episode
.title
, 'message': str(e
)}
3284 message
= _('Download error while downloading %(episode)s: %(message)s')
3285 self
.show_message(message
% d
, _('Download error'), important
=True)
3286 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3290 task
.status
= task
.PAUSED
3292 self
.mygpo_client
.on_download([task
.episode
])
3293 self
.download_queue_manager
.add_task(task
, force_start
)
3295 self
.download_status_model
.register_task(task
)
3296 enable_update
= True
3299 self
.enable_download_list_update()
3301 # Flush updated episode status
3302 self
.mygpo_client
.flush()
3304 def cancel_task_list(self
, tasks
):
3309 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3310 task
.status
= task
.CANCELLED
3311 elif task
.status
== task
.PAUSED
:
3312 task
.status
= task
.CANCELLED
3313 # Call run, so the partial file gets deleted
3316 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3317 self
.play_or_download()
3319 # Update the tab title and downloads list
3320 self
.update_downloads_list()
3322 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3323 if gpodder
.ui
.maemo
:
3325 ('maemo_markup', None, None, _('Episode')),
3327 show_notification
= notification
3330 ('title_markup', None, None, _('Episode')),
3331 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3332 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3334 show_notification
= False
3336 instructions
= _('Select the episodes you want to download:')
3338 if self
.new_episodes_window
is not None:
3339 self
.new_episodes_window
.main_window
.destroy()
3340 self
.new_episodes_window
= None
3342 def download_episodes_callback(episodes
):
3343 self
.new_episodes_window
= None
3344 self
.download_episode_list(episodes
)
3346 if selected
is None:
3347 # Select all by default
3348 selected
= [True]*len(episodes
)
3350 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3351 title
=_('New episodes available'), \
3352 instructions
=instructions
, \
3353 episodes
=episodes
, \
3355 selected
=selected
, \
3356 stock_ok_button
= 'gpodder-download', \
3357 callback
=download_episodes_callback
, \
3358 remove_callback
=lambda e
: e
.mark_old(), \
3359 remove_action
=_('Mark as old'), \
3360 remove_finished
=self
.episode_new_status_changed
, \
3361 _config
=self
.config
, \
3362 show_notification
=show_notification
, \
3363 show_episode_shownotes
=self
.show_episode_shownotes
)
3365 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3366 if not self
.offer_new_episodes():
3367 self
.show_message(_('Please check for new episodes later.'), \
3368 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3370 def get_new_episodes(self
, channels
=None):
3371 if channels
is None:
3372 channels
= self
.channels
3374 for channel
in channels
:
3375 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3376 episodes
.append(episode
)
3380 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3381 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3383 def commit_changes_to_database(self
):
3384 """This will be called after the sync process is finished"""
3387 def on_cleanup_ipod_activate(self
, widget
, *args
):
3388 self
.sync_ui
.on_cleanup_device()
3390 def on_manage_device_playlist(self
, widget
):
3391 self
.sync_ui
.on_manage_device_playlist()
3393 def show_hide_tray_icon(self
):
3394 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3395 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3396 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3397 self
.tray_icon
.set_visible(False)
3399 self
.tray_icon
= None
3401 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3402 self
.tray_icon
.set_visible(self
.is_iconified())
3403 elif self
.tray_icon
:
3404 self
.tray_icon
.set_visible(True)
3406 def on_itemShowAllEpisodes_activate(self
, widget
):
3407 self
.config
.podcast_list_view_all
= widget
.get_active()
3409 def on_itemShowToolbar_activate(self
, widget
):
3410 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3412 def on_itemShowDescription_activate(self
, widget
):
3413 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3415 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3416 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3417 if self
.config
.podcast_list_hide_boring
:
3418 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3420 self
.podcast_list_model
.set_view_mode(-1)
3422 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3424 if current
== self
.item_view_podcasts_all
:
3425 self
.podcast_list_model
.set_view_mode(-1)
3426 elif current
== self
.item_view_podcasts_downloaded
:
3427 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3428 elif current
== self
.item_view_podcasts_unplayed
:
3429 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3431 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3433 def on_item_view_episodes_changed(self
, radioaction
, current
):
3434 if current
== self
.item_view_episodes_all
:
3435 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3436 elif current
== self
.item_view_episodes_undeleted
:
3437 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3438 elif current
== self
.item_view_episodes_downloaded
:
3439 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3440 elif current
== self
.item_view_episodes_unplayed
:
3441 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3443 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3445 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3446 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3448 def update_item_device( self
):
3449 if not gpodder
.ui
.fremantle
:
3450 if self
.config
.device_type
!= 'none':
3451 self
.itemDevice
.set_visible(True)
3452 self
.itemDevice
.label
= self
.get_device_name()
3454 self
.itemDevice
.set_visible(False)
3456 def properties_closed( self
):
3457 self
.preferences_dialog
= None
3458 self
.show_hide_tray_icon()
3459 self
.update_item_device()
3460 if gpodder
.ui
.maemo
:
3461 selection
= self
.treeAvailable
.get_selection()
3462 if self
.config
.maemo_enable_gestures
or \
3463 self
.config
.enable_fingerscroll
:
3464 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3466 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3468 def on_itemPreferences_activate(self
, widget
, *args
):
3469 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3470 _config
=self
.config
, \
3471 callback_finished
=self
.properties_closed
, \
3472 user_apps_reader
=self
.user_apps_reader
, \
3473 parent_window
=self
.main_window
, \
3474 mygpo_client
=self
.mygpo_client
, \
3475 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3477 # Initial message to relayout window (in case it's opened in portrait mode
3478 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3480 def on_itemDependencies_activate(self
, widget
):
3481 gPodderDependencyManager(self
.gPodder
)
3483 def on_goto_mygpo(self
, widget
):
3484 self
.mygpo_client
.open_website()
3486 def on_download_subscriptions_from_mygpo(self
, action
=None):
3487 title
= _('Login to gpodder.net')
3488 message
= _('Please login to download your subscriptions.')
3489 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3490 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3494 self
.config
.mygpo_username
= username
3495 self
.config
.mygpo_password
= password
3497 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3498 custom_title
=_('Subscriptions on gpodder.net'), \
3499 add_urls_callback
=self
.add_podcast_list
, \
3500 hide_url_entry
=True)
3502 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3503 # we do not have to hardcode the URL here
3504 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3505 url
= util
.url_add_authentication(OPML_URL
, \
3506 self
.config
.mygpo_username
, \
3507 self
.config
.mygpo_password
)
3508 dir.download_opml_file(url
)
3510 def on_mygpo_settings_activate(self
, action
=None):
3511 # This dialog is only used for Maemo 4
3512 if not gpodder
.ui
.diablo
:
3515 settings
= MygPodderSettings(self
.main_window
, \
3516 config
=self
.config
, \
3517 mygpo_client
=self
.mygpo_client
, \
3518 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3520 def on_itemAddChannel_activate(self
, widget
=None):
3521 gPodderAddPodcast(self
.gPodder
, \
3522 add_urls_callback
=self
.add_podcast_list
)
3524 def on_itemEditChannel_activate(self
, widget
, *args
):
3525 if self
.active_channel
is None:
3526 title
= _('No podcast selected')
3527 message
= _('Please select a podcast in the podcasts list to edit.')
3528 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3531 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3532 gPodderChannel(self
.main_window
, \
3533 channel
=self
.active_channel
, \
3534 callback_closed
=callback_closed
, \
3535 cover_downloader
=self
.cover_downloader
)
3537 def on_itemMassUnsubscribe_activate(self
, item
=None):
3539 ('title', None, None, _('Podcast')),
3542 # We're abusing the Episode Selector for selecting Podcasts here,
3543 # but it works and looks good, so why not? -- thp
3544 gPodderEpisodeSelector(self
.main_window
, \
3545 title
=_('Remove podcasts'), \
3546 instructions
=_('Select the podcast you want to remove.'), \
3547 episodes
=self
.channels
, \
3549 size_attribute
=None, \
3550 stock_ok_button
=_('Remove'), \
3551 callback
=self
.remove_podcast_list
, \
3552 _config
=self
.config
)
3554 def remove_podcast_list(self
, channels
, confirm
=True):
3556 log('No podcasts selected for deletion', sender
=self
)
3559 if len(channels
) == 1:
3560 title
= _('Removing podcast')
3561 info
= _('Please wait while the podcast is removed')
3562 message
= _('Do you really want to remove this podcast and its episodes?')
3564 title
= _('Removing podcasts')
3565 info
= _('Please wait while the podcasts are removed')
3566 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3568 if confirm
and not self
.show_confirmation(message
, title
):
3571 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3573 def finish_deletion(select_url
):
3574 # Upload subscription list changes to the web service
3575 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3577 # Re-load the channels and select the desired new channel
3578 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3579 progress
.on_finished()
3580 self
.update_podcasts_tab()
3585 for idx
, channel
in enumerate(channels
):
3586 # Update the UI for correct status messages
3587 progress
.on_progress(float(idx
)/float(len(channels
)))
3588 progress
.on_message(channel
.title
)
3590 # Delete downloaded episodes
3591 channel
.remove_downloaded()
3593 # cancel any active downloads from this channel
3594 for episode
in channel
.get_all_episodes():
3595 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3598 if len(channels
) == 1:
3599 # get the URL of the podcast we want to select next
3600 if channel
in self
.channels
:
3601 position
= self
.channels
.index(channel
)
3605 if position
== len(self
.channels
)-1:
3606 # this is the last podcast, so select the URL
3607 # of the item before this one (i.e. the "new last")
3608 select_url
= self
.channels
[position
-1].url
3610 # there is a podcast after the deleted one, so
3611 # we simply select the one that comes after it
3612 select_url
= self
.channels
[position
+1].url
3614 # Remove the channel and clean the database entries
3616 self
.channels
.remove(channel
)
3618 # Clean up downloads and download directories
3619 self
.clean_up_downloads()
3621 self
.channel_list_changed
= True
3622 self
.save_channels_opml()
3624 # The remaining stuff is to be done in the GTK main thread
3625 util
.idle_add(finish_deletion
, select_url
)
3627 threading
.Thread(target
=thread_proc
).start()
3629 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3630 if self
.active_channel
is None:
3631 title
= _('No podcast selected')
3632 message
= _('Please select a podcast in the podcasts list to remove.')
3633 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3636 self
.remove_podcast_list([self
.active_channel
])
3638 def get_opml_filter(self
):
3639 filter = gtk
.FileFilter()
3640 filter.add_pattern('*.opml')
3641 filter.add_pattern('*.xml')
3642 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3645 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3646 if filename
is None:
3647 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3648 # FIXME: Hildonization on Fremantle
3649 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3650 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3651 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3652 elif gpodder
.ui
.diablo
:
3653 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3654 dlg
.set_filter(self
.get_opml_filter())
3655 response
= dlg
.run()
3657 if response
== gtk
.RESPONSE_OK
:
3658 filename
= dlg
.get_filename()
3661 if filename
is not None:
3662 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3663 custom_title
=_('Import podcasts from OPML file'), \
3664 add_urls_callback
=self
.add_podcast_list
, \
3665 hide_url_entry
=True)
3666 dir.download_opml_file(filename
)
3668 def on_itemExportChannels_activate(self
, widget
, *args
):
3669 if not self
.channels
:
3670 title
= _('Nothing to export')
3671 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3672 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3675 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3676 # FIXME: Hildonization on Fremantle
3677 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3678 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3679 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3680 elif gpodder
.ui
.diablo
:
3681 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3682 dlg
.set_filter(self
.get_opml_filter())
3683 response
= dlg
.run()
3684 if response
== gtk
.RESPONSE_OK
:
3685 filename
= dlg
.get_filename()
3687 exporter
= opml
.Exporter( filename
)
3688 if exporter
.write(self
.channels
):
3689 count
= len(self
.channels
)
3690 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3691 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3693 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3697 def on_itemImportChannels_activate(self
, widget
, *args
):
3698 if gpodder
.ui
.fremantle
:
3699 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3700 self
.config
.toplist_url
, \
3701 self
.config
.opml_url
, \
3702 self
.add_podcast_list
, \
3703 self
.on_itemAddChannel_activate
, \
3704 self
.on_download_subscriptions_from_mygpo
, \
3705 self
.show_text_edit_dialog
)
3707 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3708 add_urls_callback
=self
.add_podcast_list
)
3709 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3711 def on_homepage_activate(self
, widget
, *args
):
3712 util
.open_website(gpodder
.__url
__)
3714 def on_wiki_activate(self
, widget
, *args
):
3715 util
.open_website('http://gpodder.org/wiki/User_Manual')
3717 def on_bug_tracker_activate(self
, widget
, *args
):
3718 if gpodder
.ui
.maemo
:
3719 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3721 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3723 def on_item_support_activate(self
, widget
):
3724 util
.open_website('http://gpodder.org/donate')
3726 def on_itemAbout_activate(self
, widget
, *args
):
3727 if gpodder
.ui
.fremantle
:
3728 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3729 HeAboutDialog
.present(self
.main_window
,
3732 gpodder
.__version
__,
3733 _('A podcast client with focus on usability'),
3734 gpodder
.__copyright
__,
3736 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3737 'http://gpodder.org/donate')
3740 dlg
= gtk
.AboutDialog()
3741 dlg
.set_transient_for(self
.main_window
)
3742 dlg
.set_name('gPodder')
3743 dlg
.set_version(gpodder
.__version
__)
3744 dlg
.set_copyright(gpodder
.__copyright
__)
3745 dlg
.set_comments(_('A podcast client with focus on usability'))
3746 dlg
.set_website(gpodder
.__url
__)
3747 dlg
.set_translator_credits( _('translator-credits'))
3748 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3750 if gpodder
.ui
.desktop
:
3751 # For the "GUI" version, we add some more
3752 # items to the about dialog (credits and logo)
3755 'Thomas Perl <thpinfo.com>',
3758 if os
.path
.exists(gpodder
.credits_file
):
3759 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3760 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3761 app_authors
+= credits
3763 dlg
.set_authors(app_authors
)
3765 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3767 dlg
.set_logo_icon_name('gpodder')
3771 def on_wNotebook_switch_page(self
, widget
, *args
):
3773 if gpodder
.ui
.maemo
:
3774 self
.tool_downloads
.set_active(page_num
== 1)
3775 page
= self
.wNotebook
.get_nth_page(page_num
)
3776 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3777 if page_num
== 0 and self
.active_channel
is not None:
3778 self
.set_title(self
.active_channel
.title
)
3780 self
.set_title(tab_label
)
3782 self
.play_or_download()
3783 self
.menuChannels
.set_sensitive(True)
3784 self
.menuSubscriptions
.set_sensitive(True)
3785 # The message area in the downloads tab should be hidden
3786 # when the user switches away from the downloads tab
3787 if self
.message_area
is not None:
3788 self
.message_area
.hide()
3789 self
.message_area
= None
3791 self
.menuChannels
.set_sensitive(False)
3792 self
.menuSubscriptions
.set_sensitive(False)
3793 if gpodder
.ui
.desktop
:
3794 self
.toolDownload
.set_sensitive(False)
3795 self
.toolPlay
.set_sensitive(False)
3796 self
.toolTransfer
.set_sensitive(False)
3797 self
.toolCancel
.set_sensitive(False)
3799 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3800 # double-click action of the podcast list or enter
3801 self
.treeChannels
.set_cursor(path
)
3803 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3804 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3806 if model
is not None and iter is not None:
3807 old_active_channel
= self
.active_channel
3808 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3810 if self
.active_channel
== old_active_channel
:
3813 if gpodder
.ui
.maemo
:
3814 self
.set_title(self
.active_channel
.title
)
3816 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3817 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3818 self
.itemEditChannel
.set_visible(False)
3819 self
.itemRemoveChannel
.set_visible(False)
3821 self
.itemEditChannel
.set_visible(True)
3822 self
.itemRemoveChannel
.set_visible(True)
3824 self
.active_channel
= None
3825 self
.itemEditChannel
.set_visible(False)
3826 self
.itemRemoveChannel
.set_visible(False)
3828 self
.update_episode_list_model()
3830 def on_btnEditChannel_clicked(self
, widget
, *args
):
3831 self
.on_itemEditChannel_activate( widget
, args
)
3833 def get_podcast_urls_from_selected_episodes(self
):
3834 """Get a set of podcast URLs based on the selected episodes"""
3835 return set(episode
.channel
.url
for episode
in \
3836 self
.get_selected_episodes())
3838 def get_selected_episodes(self
):
3839 """Get a list of selected episodes from treeAvailable"""
3840 selection
= self
.treeAvailable
.get_selection()
3841 model
, paths
= selection
.get_selected_rows()
3843 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3846 def on_transfer_selected_episodes(self
, widget
):
3847 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3849 def on_playback_selected_episodes(self
, widget
):
3850 self
.playback_episodes(self
.get_selected_episodes())
3852 def on_shownotes_selected_episodes(self
, widget
):
3853 episodes
= self
.get_selected_episodes()
3855 episode
= episodes
.pop(0)
3856 self
.show_episode_shownotes(episode
)
3858 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3860 def on_download_selected_episodes(self
, widget
):
3861 episodes
= self
.get_selected_episodes()
3862 self
.download_episode_list(episodes
)
3863 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3864 self
.play_or_download()
3866 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3867 """Double-click/enter action handler for treeAvailable"""
3868 # We should only have one one selected as it was double clicked!
3869 e
= self
.get_selected_episodes()[0]
3871 if (self
.config
.double_click_episode_action
== 'download'):
3872 # If the episode has already been downloaded and exists then play it
3873 if e
.was_downloaded(and_exists
=True):
3874 self
.playback_episodes(self
.get_selected_episodes())
3875 # else download it if it is not already downloading
3876 elif not self
.episode_is_downloading(e
):
3877 self
.download_episode_list([e
])
3878 self
.update_episode_list_icons([e
.url
])
3879 self
.play_or_download()
3880 elif (self
.config
.double_click_episode_action
== 'stream'):
3881 # If we happen to have downloaded this episode simple play it
3882 if e
.was_downloaded(and_exists
=True):
3883 self
.playback_episodes(self
.get_selected_episodes())
3884 # else if streaming is possible stream it
3885 elif self
.streaming_possible():
3886 self
.playback_episodes(self
.get_selected_episodes())
3888 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3889 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3891 # default action is to display show notes
3892 self
.on_shownotes_selected_episodes(widget
)
3894 def show_episode_shownotes(self
, episode
):
3895 if self
.episode_shownotes_window
is None:
3896 log('First-time use of episode window --- creating', sender
=self
)
3897 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3898 _download_episode_list
=self
.download_episode_list
, \
3899 _playback_episodes
=self
.playback_episodes
, \
3900 _delete_episode_list
=self
.delete_episode_list
, \
3901 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3902 _cancel_task_list
=self
.cancel_task_list
, \
3903 _episode_is_downloading
=self
.episode_is_downloading
, \
3904 _streaming_possible
=self
.streaming_possible())
3905 self
.episode_shownotes_window
.show(episode
)
3906 if self
.episode_is_downloading(episode
):
3907 self
.update_downloads_list()
3909 def restart_auto_update_timer(self
):
3910 if self
._auto
_update
_timer
_source
_id
is not None:
3911 log('Removing existing auto update timer.', sender
=self
)
3912 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3913 self
._auto
_update
_timer
_source
_id
= None
3915 if self
.config
.auto_update_feeds
and \
3916 self
.config
.auto_update_frequency
:
3917 interval
= 60*1000*self
.config
.auto_update_frequency
3918 log('Setting up auto update timer with interval %d.', \
3919 self
.config
.auto_update_frequency
, sender
=self
)
3920 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3921 interval
, self
._on
_auto
_update
_timer
)
3923 def _on_auto_update_timer(self
):
3924 log('Auto update timer fired.', sender
=self
)
3925 self
.update_feed_cache(force_update
=True)
3927 # Ask web service for sub changes (if enabled)
3928 self
.mygpo_client
.flush()
3932 def on_treeDownloads_row_activated(self
, widget
, *args
):
3933 # Use the standard way of working on the treeview
3934 selection
= self
.treeDownloads
.get_selection()
3935 (model
, paths
) = selection
.get_selected_rows()
3936 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3938 for tree_row_reference
, task
in selected_tasks
:
3939 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3940 task
.status
= task
.PAUSED
3941 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3942 self
.download_queue_manager
.add_task(task
)
3943 self
.enable_download_list_update()
3944 elif task
.status
== task
.DONE
:
3945 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3947 self
.play_or_download()
3949 # Update the tab title and downloads list
3950 self
.update_downloads_list()
3952 def on_item_cancel_download_activate(self
, widget
):
3953 if self
.wNotebook
.get_current_page() == 0:
3954 selection
= self
.treeAvailable
.get_selection()
3955 (model
, paths
) = selection
.get_selected_rows()
3956 urls
= [model
.get_value(model
.get_iter(path
), \
3957 self
.episode_list_model
.C_URL
) for path
in paths
]
3958 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3959 if task
.url
in urls
]
3961 selection
= self
.treeDownloads
.get_selection()
3962 (model
, paths
) = selection
.get_selected_rows()
3963 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3964 self
.download_status_model
.C_TASK
) for path
in paths
]
3965 self
.cancel_task_list(selected_tasks
)
3967 def on_btnCancelAll_clicked(self
, widget
, *args
):
3968 self
.cancel_task_list(self
.download_tasks_seen
)
3970 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3971 episodes
= self
.get_selected_episodes()
3972 if len(episodes
) == 1:
3973 self
.delete_episode_list(episodes
, skip_locked
=False)
3975 self
.delete_episode_list(episodes
)
3977 def on_key_press(self
, widget
, event
):
3978 # Allow tab switching with Ctrl + PgUp/PgDown
3979 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3980 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3981 self
.wNotebook
.prev_page()
3983 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3984 self
.wNotebook
.next_page()
3987 # After this code we only handle Maemo hardware keys,
3988 # so if we are not a Maemo app, we don't do anything
3989 if not gpodder
.ui
.maemo
:
3993 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3995 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3998 if diff
!= 0 and not self
.currently_updating
:
3999 selection
= self
.treeChannels
.get_selection()
4000 (model
, iter) = selection
.get_selected()
4001 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
4002 selection
.select_path(new_path
)
4003 self
.treeChannels
.set_cursor(new_path
)
4008 def on_iconify(self
):
4010 self
.gPodder
.set_skip_taskbar_hint(True)
4011 if self
.config
.minimize_to_tray
:
4012 self
.tray_icon
.set_visible(True)
4014 self
.gPodder
.set_skip_taskbar_hint(False)
4016 def on_uniconify(self
):
4018 self
.gPodder
.set_skip_taskbar_hint(False)
4019 if self
.config
.minimize_to_tray
:
4020 self
.tray_icon
.set_visible(False)
4022 self
.gPodder
.set_skip_taskbar_hint(False)
4024 def uniconify_main_window(self
):
4025 if self
.is_iconified():
4026 # We need to hide and then show the window in WMs like Metacity
4027 # or KWin4 to move the window to the active workspace
4028 # (see http://gpodder.org/bug/1125)
4031 self
.gPodder
.present()
4033 def iconify_main_window(self
):
4034 if not self
.is_iconified():
4035 self
.gPodder
.iconify()
4037 def update_podcasts_tab(self
):
4038 if len(self
.channels
):
4039 if gpodder
.ui
.fremantle
:
4040 self
.button_refresh
.set_title(_('Check for new episodes'))
4041 self
.button_refresh
.show()
4043 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4045 if gpodder
.ui
.fremantle
:
4046 self
.button_refresh
.hide()
4048 self
.label2
.set_text(_('Podcasts'))
4050 @dbus.service
.method(gpodder
.dbus_interface
)
4051 def show_gui_window(self
):
4052 parent
= self
.get_dialog_parent()
4055 @dbus.service
.method(gpodder
.dbus_interface
)
4056 def subscribe_to_url(self
, url
):
4057 gPodderAddPodcast(self
.gPodder
,
4058 add_urls_callback
=self
.add_podcast_list
,
4061 @dbus.service
.method(gpodder
.dbus_interface
)
4062 def mark_episode_played(self
, filename
):
4063 if filename
is None:
4066 for channel
in self
.channels
:
4067 for episode
in channel
.get_all_episodes():
4068 fn
= episode
.local_filename(create
=False, check_only
=True)
4070 episode
.mark(is_played
=True)
4072 self
.update_episode_list_icons([episode
.url
])
4073 self
.update_podcast_list_model([episode
.channel
.url
])
4079 def main(options
=None):
4080 gobject
.threads_init()
4081 gobject
.set_application_name('gPodder')
4083 if gpodder
.ui
.maemo
:
4084 # Try to enable the custom icon theme for gPodder on Maemo
4085 settings
= gtk
.settings_get_default()
4086 settings
.set_string_property('gtk-icon-theme-name', \
4087 'gpodder', __file__
)
4088 # Extend the search path for the optified icon theme (Maemo 5)
4089 icon_theme
= gtk
.icon_theme_get_default()
4090 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4092 gtk
.window_set_default_icon_name('gpodder')
4093 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4096 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4097 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4099 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4100 except dbus
.exceptions
.DBusException
, dbe
:
4101 log('Warning: Cannot get "on the bus".', traceback
=True)
4102 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4103 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4104 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4105 dlg
.set_title('gPodder')
4110 util
.make_directory(gpodder
.home
)
4111 gpodder
.load_plugins()
4113 config
= UIConfig(gpodder
.config_file
)
4115 # Load hook modules and install the hook manager globally
4116 # if modules have been found an instantiated by the manager
4117 user_hooks
= hooks
.HookManager()
4118 if user_hooks
.has_modules():
4119 gpodder
.user_hooks
= user_hooks
4121 if gpodder
.ui
.diablo
:
4122 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4123 # folder exists there (allow moving "gpodder" between SD cards or USB)
4124 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4125 if not os
.path
.exists(config
.download_dir
):
4126 log('Downloads might have been moved. Trying to locate them...')
4127 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4128 dir = os
.path
.join(basedir
, 'gpodder')
4129 if os
.path
.exists(dir):
4130 log('Downloads found in: %s', dir)
4131 config
.download_dir
= dir
4134 log('Downloads NOT FOUND in %s', dir)
4136 if config
.enable_fingerscroll
:
4137 BuilderWidget
.use_fingerscroll
= True
4138 elif gpodder
.ui
.fremantle
:
4139 config
.on_quit_ask
= False
4141 config
.mygpo_device_type
= util
.detect_device_type()
4143 gp
= gPodder(bus_name
, config
)
4146 if options
.subscribe
:
4147 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4150 # handle "subscribe to podcast" events from firefox
4151 if platform
.system() == 'Darwin':
4152 from gpodder
import gpodderosx
4153 gpodderosx
.register_handlers(gp
)
4154 # end mac OS X stuff