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 def on_treeview_podcasts_selection_changed(self
, selection
):
807 model
, iter = selection
.get_selected()
809 self
.active_channel
= None
810 self
.episode_list_model
.clear()
812 def on_treeview_button_pressed(self
, treeview
, event
):
813 if event
.window
!= treeview
.get_bin_window():
816 TreeViewHelper
.save_button_press_event(treeview
, event
)
818 if getattr(treeview
, TreeViewHelper
.ROLE
) == \
819 TreeViewHelper
.ROLE_PODCASTS
:
820 return self
.currently_updating
822 return event
.button
== self
.context_menu_mouse_button
and \
825 def on_treeview_podcasts_button_released(self
, treeview
, event
):
826 if event
.window
!= treeview
.get_bin_window():
830 return self
.treeview_channels_handle_gestures(treeview
, event
)
831 return self
.treeview_channels_show_context_menu(treeview
, event
)
833 def on_treeview_episodes_button_released(self
, treeview
, event
):
834 if event
.window
!= treeview
.get_bin_window():
838 if self
.config
.enable_fingerscroll
or self
.config
.maemo_enable_gestures
:
839 return self
.treeview_available_handle_gestures(treeview
, event
)
841 return self
.treeview_available_show_context_menu(treeview
, event
)
843 def on_treeview_downloads_button_released(self
, treeview
, event
):
844 if event
.window
!= treeview
.get_bin_window():
847 return self
.treeview_downloads_show_context_menu(treeview
, event
)
849 def on_entry_search_podcasts_changed(self
, editable
):
850 if self
.hbox_search_podcasts
.get_property('visible'):
851 self
.podcast_list_model
.set_search_term(editable
.get_chars(0, -1))
853 def on_entry_search_podcasts_key_press(self
, editable
, event
):
854 if event
.keyval
== gtk
.keysyms
.Escape
:
855 self
.hide_podcast_search()
858 def hide_podcast_search(self
, *args
):
859 self
.hbox_search_podcasts
.hide()
860 self
.entry_search_podcasts
.set_text('')
861 self
.podcast_list_model
.set_search_term(None)
862 self
.treeChannels
.grab_focus()
864 def show_podcast_search(self
, input_char
):
865 self
.hbox_search_podcasts
.show()
866 self
.entry_search_podcasts
.insert_text(input_char
, -1)
867 self
.entry_search_podcasts
.grab_focus()
868 self
.entry_search_podcasts
.set_position(-1)
870 def init_podcast_list_treeview(self
):
871 # Set up podcast channel tree view widget
872 if gpodder
.ui
.fremantle
:
873 if self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
874 self
.item_view_podcasts_downloaded
.set_active(True)
875 elif self
.config
.podcast_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
876 self
.item_view_podcasts_unplayed
.set_active(True)
878 self
.item_view_podcasts_all
.set_active(True)
879 self
.podcast_list_model
.set_view_mode(self
.config
.podcast_list_view_mode
)
881 iconcolumn
= gtk
.TreeViewColumn('')
882 iconcell
= gtk
.CellRendererPixbuf()
883 iconcolumn
.pack_start(iconcell
, False)
884 iconcolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_COVER
)
885 self
.treeChannels
.append_column(iconcolumn
)
887 namecolumn
= gtk
.TreeViewColumn('')
888 namecell
= gtk
.CellRendererText()
889 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
890 namecolumn
.pack_start(namecell
, True)
891 namecolumn
.add_attribute(namecell
, 'markup', PodcastListModel
.C_DESCRIPTION
)
893 if gpodder
.ui
.fremantle
:
894 countcell
= gtk
.CellRendererText()
895 from gpodder
.gtkui
.frmntl
import style
896 countcell
.set_property('font-desc', style
.get_font_desc('EmpSystemFont'))
897 countcell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
898 countcell
.set_property('alignment', pango
.ALIGN_RIGHT
)
899 countcell
.set_property('xalign', 1.)
900 countcell
.set_property('xpad', 5)
901 namecolumn
.pack_start(countcell
, False)
902 namecolumn
.add_attribute(countcell
, 'text', PodcastListModel
.C_DOWNLOADS
)
903 namecolumn
.add_attribute(countcell
, 'visible', PodcastListModel
.C_DOWNLOADS
)
905 iconcell
= gtk
.CellRendererPixbuf()
906 iconcell
.set_property('xalign', 1.0)
907 namecolumn
.pack_start(iconcell
, False)
908 namecolumn
.add_attribute(iconcell
, 'pixbuf', PodcastListModel
.C_PILL
)
909 namecolumn
.add_attribute(iconcell
, 'visible', PodcastListModel
.C_PILL_VISIBLE
)
911 self
.treeChannels
.append_column(namecolumn
)
913 self
.treeChannels
.set_model(self
.podcast_list_model
.get_filtered_model())
915 # When no podcast is selected, clear the episode list model
916 selection
= self
.treeChannels
.get_selection()
917 selection
.connect('changed', self
.on_treeview_podcasts_selection_changed
)
919 # Set up type-ahead find for the podcast list
920 def on_key_press(treeview
, event
):
921 if event
.keyval
== gtk
.keysyms
.Escape
:
922 self
.hide_podcast_search()
923 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
924 self
.hide_podcast_search()
925 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
926 # Don't handle type-ahead when control is pressed (so shortcuts
927 # with the Ctrl key still work, e.g. Ctrl+A, ...)
930 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
931 if unicode_char_id
== 0:
933 input_char
= unichr(unicode_char_id
)
934 self
.show_podcast_search(input_char
)
936 self
.treeChannels
.connect('key-press-event', on_key_press
)
938 # Enable separators to the podcast list to separate special podcasts
939 # from others (this is used for the "all episodes" view)
940 self
.treeChannels
.set_row_separator_func(PodcastListModel
.row_separator_func
)
942 TreeViewHelper
.set(self
.treeChannels
, TreeViewHelper
.ROLE_PODCASTS
)
944 def on_entry_search_episodes_changed(self
, editable
):
945 if self
.hbox_search_episodes
.get_property('visible'):
946 self
.episode_list_model
.set_search_term(editable
.get_chars(0, -1))
948 def on_entry_search_episodes_key_press(self
, editable
, event
):
949 if event
.keyval
== gtk
.keysyms
.Escape
:
950 self
.hide_episode_search()
953 def hide_episode_search(self
, *args
):
954 self
.hbox_search_episodes
.hide()
955 self
.entry_search_episodes
.set_text('')
956 self
.episode_list_model
.set_search_term(None)
957 self
.treeAvailable
.grab_focus()
959 def show_episode_search(self
, input_char
):
960 self
.hbox_search_episodes
.show()
961 self
.entry_search_episodes
.insert_text(input_char
, -1)
962 self
.entry_search_episodes
.grab_focus()
963 self
.entry_search_episodes
.set_position(-1)
965 def init_episode_list_treeview(self
):
966 # For loading the list model
967 self
.empty_episode_list_model
= EpisodeListModel()
968 self
.episode_list_model
= EpisodeListModel()
970 if self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNDELETED
:
971 self
.item_view_episodes_undeleted
.set_active(True)
972 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_DOWNLOADED
:
973 self
.item_view_episodes_downloaded
.set_active(True)
974 elif self
.config
.episode_list_view_mode
== EpisodeListModel
.VIEW_UNPLAYED
:
975 self
.item_view_episodes_unplayed
.set_active(True)
977 self
.item_view_episodes_all
.set_active(True)
979 self
.episode_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
981 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
983 TreeViewHelper
.set(self
.treeAvailable
, TreeViewHelper
.ROLE_EPISODES
)
985 iconcell
= gtk
.CellRendererPixbuf()
987 iconcell
.set_fixed_size(50, 50)
988 status_column_label
= ''
990 status_column_label
= _('Status')
991 iconcolumn
= gtk
.TreeViewColumn(status_column_label
, iconcell
, pixbuf
=EpisodeListModel
.C_STATUS_ICON
)
993 namecell
= gtk
.CellRendererText()
994 namecell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
995 namecolumn
= gtk
.TreeViewColumn(_('Episode'))
996 namecolumn
.pack_start(namecell
, True)
997 namecolumn
.add_attribute(namecell
, 'markup', EpisodeListModel
.C_DESCRIPTION
)
998 namecolumn
.set_sort_column_id(EpisodeListModel
.C_DESCRIPTION
)
999 namecolumn
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1000 namecolumn
.set_resizable(True)
1001 namecolumn
.set_expand(True)
1003 if gpodder
.ui
.fremantle
:
1004 from gpodder
.gtkui
.frmntl
import style
1005 timecell
= gtk
.CellRendererText()
1006 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1007 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1008 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1009 timecell
.set_property('xalign', 1.)
1010 timecell
.set_property('xpad', 5)
1011 namecolumn
.pack_start(timecell
, False)
1012 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1013 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME1_VISIBLE
)
1015 # Add another cell renderer to fix a sizing issue (one renderer
1016 # only renders short text and the other one longer text to avoid
1017 # having titles of episodes unnecessarily cut off)
1018 timecell
= gtk
.CellRendererText()
1019 timecell
.set_property('font-desc', style
.get_font_desc('SystemFont'))
1020 timecell
.set_property('foreground-gdk', style
.get_color('SecondaryTextColor'))
1021 timecell
.set_property('alignment', pango
.ALIGN_RIGHT
)
1022 timecell
.set_property('xalign', 1.)
1023 timecell
.set_property('xpad', 5)
1024 namecolumn
.pack_start(timecell
, False)
1025 namecolumn
.add_attribute(timecell
, 'text', EpisodeListModel
.C_TIME
)
1026 namecolumn
.add_attribute(timecell
, 'visible', EpisodeListModel
.C_TIME2_VISIBLE
)
1028 sizecell
= gtk
.CellRendererText()
1029 sizecolumn
= gtk
.TreeViewColumn(_('Size'), sizecell
, text
=EpisodeListModel
.C_FILESIZE_TEXT
)
1030 sizecolumn
.set_sort_column_id(EpisodeListModel
.C_FILESIZE
)
1032 releasecell
= gtk
.CellRendererText()
1033 releasecolumn
= gtk
.TreeViewColumn(_('Released'), releasecell
, text
=EpisodeListModel
.C_PUBLISHED_TEXT
)
1034 releasecolumn
.set_sort_column_id(EpisodeListModel
.C_PUBLISHED
)
1036 for itemcolumn
in (iconcolumn
, namecolumn
, sizecolumn
, releasecolumn
):
1037 itemcolumn
.set_reorderable(True)
1038 self
.treeAvailable
.append_column(itemcolumn
)
1040 if gpodder
.ui
.maemo
:
1041 sizecolumn
.set_visible(False)
1042 releasecolumn
.set_visible(False)
1044 # Set up type-ahead find for the episode list
1045 def on_key_press(treeview
, event
):
1046 if event
.keyval
== gtk
.keysyms
.Escape
:
1047 self
.hide_episode_search()
1048 elif gpodder
.ui
.fremantle
and event
.keyval
== gtk
.keysyms
.BackSpace
:
1049 self
.hide_episode_search()
1050 elif event
.state
& gtk
.gdk
.CONTROL_MASK
:
1051 # Don't handle type-ahead when control is pressed (so shortcuts
1052 # with the Ctrl key still work, e.g. Ctrl+A, ...)
1055 unicode_char_id
= gtk
.gdk
.keyval_to_unicode(event
.keyval
)
1056 if unicode_char_id
== 0:
1058 input_char
= unichr(unicode_char_id
)
1059 self
.show_episode_search(input_char
)
1061 self
.treeAvailable
.connect('key-press-event', on_key_press
)
1063 if gpodder
.ui
.desktop
:
1064 self
.treeAvailable
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, \
1065 (('text/uri-list', 0, 0),), gtk
.gdk
.ACTION_COPY
)
1066 def drag_data_get(tree
, context
, selection_data
, info
, timestamp
):
1067 if self
.config
.on_drag_mark_played
:
1068 for episode
in self
.get_selected_episodes():
1069 episode
.mark(is_played
=True)
1070 self
.on_selected_episodes_status_changed()
1071 uris
= ['file://'+e
.local_filename(create
=False) \
1072 for e
in self
.get_selected_episodes() \
1073 if e
.was_downloaded(and_exists
=True)]
1074 uris
.append('') # for the trailing '\r\n'
1075 selection_data
.set(selection_data
.target
, 8, '\r\n'.join(uris
))
1076 self
.treeAvailable
.connect('drag-data-get', drag_data_get
)
1078 selection
= self
.treeAvailable
.get_selection()
1079 if gpodder
.ui
.diablo
:
1080 if self
.config
.maemo_enable_gestures
or self
.config
.enable_fingerscroll
:
1081 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1083 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1084 elif gpodder
.ui
.fremantle
:
1085 selection
.set_mode(gtk
.SELECTION_SINGLE
)
1087 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
1088 # Update the sensitivity of the toolbar buttons on the Desktop
1089 selection
.connect('changed', lambda s
: self
.play_or_download())
1091 if gpodder
.ui
.diablo
:
1092 # Set up the tap-and-hold context menu for podcasts
1094 menu
.append(self
.itemUpdateChannel
.create_menu_item())
1095 menu
.append(self
.itemEditChannel
.create_menu_item())
1096 menu
.append(gtk
.SeparatorMenuItem())
1097 menu
.append(self
.itemRemoveChannel
.create_menu_item())
1098 menu
.append(gtk
.SeparatorMenuItem())
1099 item
= gtk
.ImageMenuItem(_('Close this menu'))
1100 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, \
1101 gtk
.ICON_SIZE_MENU
))
1104 menu
= self
.set_finger_friendly(menu
)
1105 self
.treeChannels
.tap_and_hold_setup(menu
)
1108 def init_download_list_treeview(self
):
1109 # enable multiple selection support
1110 self
.treeDownloads
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
1111 self
.treeDownloads
.set_search_equal_func(TreeViewHelper
.make_search_equal_func(DownloadStatusModel
))
1113 # columns and renderers for "download progress" tab
1114 # First column: [ICON] Episodename
1115 column
= gtk
.TreeViewColumn(_('Episode'))
1117 cell
= gtk
.CellRendererPixbuf()
1118 if gpodder
.ui
.maemo
:
1119 cell
.set_fixed_size(50, 50)
1120 cell
.set_property('stock-size', gtk
.ICON_SIZE_MENU
)
1121 column
.pack_start(cell
, expand
=False)
1122 column
.add_attribute(cell
, 'stock-id', \
1123 DownloadStatusModel
.C_ICON_NAME
)
1125 cell
= gtk
.CellRendererText()
1126 cell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1127 column
.pack_start(cell
, expand
=True)
1128 column
.add_attribute(cell
, 'markup', DownloadStatusModel
.C_NAME
)
1129 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1130 column
.set_expand(True)
1131 self
.treeDownloads
.append_column(column
)
1133 # Second column: Progress
1134 cell
= gtk
.CellRendererProgress()
1135 cell
.set_property('yalign', .5)
1136 cell
.set_property('ypad', 6)
1137 column
= gtk
.TreeViewColumn(_('Progress'), cell
,
1138 value
=DownloadStatusModel
.C_PROGRESS
, \
1139 text
=DownloadStatusModel
.C_PROGRESS_TEXT
)
1140 column
.set_sizing(gtk
.TREE_VIEW_COLUMN_AUTOSIZE
)
1141 column
.set_expand(False)
1142 self
.treeDownloads
.append_column(column
)
1143 column
.set_property('min-width', 150)
1144 column
.set_property('max-width', 150)
1146 self
.treeDownloads
.set_model(self
.download_status_model
)
1147 TreeViewHelper
.set(self
.treeDownloads
, TreeViewHelper
.ROLE_DOWNLOADS
)
1149 def on_treeview_expose_event(self
, treeview
, event
):
1150 if event
.window
== treeview
.get_bin_window():
1151 model
= treeview
.get_model()
1152 if (model
is not None and model
.get_iter_first() is not None):
1155 role
= getattr(treeview
, TreeViewHelper
.ROLE
, None)
1159 ctx
= event
.window
.cairo_create()
1160 ctx
.rectangle(event
.area
.x
, event
.area
.y
,
1161 event
.area
.width
, event
.area
.height
)
1164 x
, y
, width
, height
, depth
= event
.window
.get_geometry()
1167 if role
== TreeViewHelper
.ROLE_EPISODES
:
1168 if self
.currently_updating
:
1169 text
= _('Loading episodes')
1170 progress
= self
.episode_list_model
.get_update_progress()
1171 elif self
.config
.episode_list_view_mode
!= \
1172 EpisodeListModel
.VIEW_ALL
:
1173 text
= _('No episodes in current view')
1175 text
= _('No episodes available')
1176 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1177 if self
.config
.episode_list_view_mode
!= \
1178 EpisodeListModel
.VIEW_ALL
and \
1179 self
.config
.podcast_list_hide_boring
and \
1180 len(self
.channels
) > 0:
1181 text
= _('No podcasts in this view')
1183 text
= _('No subscriptions')
1184 elif role
== TreeViewHelper
.ROLE_DOWNLOADS
:
1185 text
= _('No active downloads')
1187 raise Exception('on_treeview_expose_event: unknown role')
1189 if gpodder
.ui
.fremantle
:
1190 from gpodder
.gtkui
.frmntl
import style
1191 font_desc
= style
.get_font_desc('LargeSystemFont')
1195 draw_text_box_centered(ctx
, treeview
, width
, height
, text
, font_desc
, progress
)
1199 def enable_download_list_update(self
):
1200 if not self
.download_list_update_enabled
:
1201 self
.update_downloads_list()
1202 gobject
.timeout_add(1500, self
.update_downloads_list
)
1203 self
.download_list_update_enabled
= True
1205 def cleanup_downloads(self
):
1206 model
= self
.download_status_model
1208 all_tasks
= [(gtk
.TreeRowReference(model
, row
.path
), row
[0]) for row
in model
]
1209 changed_episode_urls
= set()
1210 for row_reference
, task
in all_tasks
:
1211 if task
.status
in (task
.DONE
, task
.CANCELLED
):
1212 model
.remove(model
.get_iter(row_reference
.get_path()))
1214 # We don't "see" this task anymore - remove it;
1215 # this is needed, so update_episode_list_icons()
1216 # below gets the correct list of "seen" tasks
1217 self
.download_tasks_seen
.remove(task
)
1218 except KeyError, key_error
:
1219 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1220 changed_episode_urls
.add(task
.url
)
1221 # Tell the task that it has been removed (so it can clean up)
1222 task
.removed_from_list()
1224 # Tell the podcasts tab to update icons for our removed podcasts
1225 self
.update_episode_list_icons(changed_episode_urls
)
1227 # Tell the shownotes window that we have removed the episode
1228 if self
.episode_shownotes_window
is not None and \
1229 self
.episode_shownotes_window
.episode
is not None and \
1230 self
.episode_shownotes_window
.episode
.url
in changed_episode_urls
:
1231 self
.episode_shownotes_window
._download
_status
_changed
(None)
1233 # Update the downloads list one more time
1234 self
.update_downloads_list(can_call_cleanup
=False)
1236 def on_tool_downloads_toggled(self
, toolbutton
):
1237 if toolbutton
.get_active():
1238 self
.wNotebook
.set_current_page(1)
1240 self
.wNotebook
.set_current_page(0)
1242 def add_download_task_monitor(self
, monitor
):
1243 self
.download_task_monitors
.add(monitor
)
1244 model
= self
.download_status_model
1248 task
= row
[self
.download_status_model
.C_TASK
]
1249 monitor
.task_updated(task
)
1251 def remove_download_task_monitor(self
, monitor
):
1252 self
.download_task_monitors
.remove(monitor
)
1254 def update_downloads_list(self
, can_call_cleanup
=True):
1256 model
= self
.download_status_model
1258 downloading
, failed
, finished
, queued
, paused
, others
= 0, 0, 0, 0, 0, 0
1259 total_speed
, total_size
, done_size
= 0, 0, 0
1261 # Keep a list of all download tasks that we've seen
1262 download_tasks_seen
= set()
1264 # Remember the DownloadTask object for the episode that
1265 # has been opened in the episode shownotes dialog (if any)
1266 if self
.episode_shownotes_window
is not None:
1267 shownotes_episode
= self
.episode_shownotes_window
.episode
1268 shownotes_task
= None
1270 shownotes_episode
= None
1271 shownotes_task
= None
1273 # Do not go through the list of the model is not (yet) available
1277 failed_downloads
= []
1279 self
.download_status_model
.request_update(row
.iter)
1281 task
= row
[self
.download_status_model
.C_TASK
]
1282 speed
, size
, status
, progress
= task
.speed
, task
.total_size
, task
.status
, task
.progress
1284 # Let the download task monitors know of changes
1285 for monitor
in self
.download_task_monitors
:
1286 monitor
.task_updated(task
)
1289 done_size
+= size
*progress
1291 if shownotes_episode
is not None and \
1292 shownotes_episode
.url
== task
.episode
.url
:
1293 shownotes_task
= task
1295 download_tasks_seen
.add(task
)
1297 if status
== download
.DownloadTask
.DOWNLOADING
:
1299 total_speed
+= speed
1300 elif status
== download
.DownloadTask
.FAILED
:
1301 failed_downloads
.append(task
)
1303 elif status
== download
.DownloadTask
.DONE
:
1305 elif status
== download
.DownloadTask
.QUEUED
:
1307 elif status
== download
.DownloadTask
.PAUSED
:
1312 # Remember which tasks we have seen after this run
1313 self
.download_tasks_seen
= download_tasks_seen
1315 if gpodder
.ui
.desktop
:
1316 text
= [_('Downloads')]
1317 if downloading
+ failed
+ queued
> 0:
1320 s
.append(N_('%d active', '%d active', downloading
) % downloading
)
1322 s
.append(N_('%d failed', '%d failed', failed
) % failed
)
1324 s
.append(N_('%d queued', '%d queued', queued
) % queued
)
1325 text
.append(' (' + ', '.join(s
)+')')
1326 self
.labelDownloads
.set_text(''.join(text
))
1327 elif gpodder
.ui
.diablo
:
1328 sum = downloading
+ failed
+ finished
+ queued
+ paused
+ others
1330 self
.tool_downloads
.set_label(_('Downloads (%d)') % sum)
1332 self
.tool_downloads
.set_label(_('Downloads'))
1333 elif gpodder
.ui
.fremantle
:
1334 if downloading
+ queued
> 0:
1335 self
.button_downloads
.set_value(N_('%d active', '%d active', downloading
+queued
) % (downloading
+queued
))
1337 self
.button_downloads
.set_value(N_('%d failed', '%d failed', failed
) % failed
)
1339 self
.button_downloads
.set_value(N_('%d paused', '%d paused', paused
) % paused
)
1341 self
.button_downloads
.set_value(_('Idle'))
1343 title
= [self
.default_title
]
1345 # We have to update all episodes/channels for which the status has
1346 # changed. Accessing task.status_changed has the side effect of
1347 # re-setting the changed flag, so we need to get the "changed" list
1348 # of tuples first and split it into two lists afterwards
1349 changed
= [(task
.url
, task
.podcast_url
) for task
in \
1350 self
.download_tasks_seen
if task
.status_changed
]
1351 episode_urls
= [episode_url
for episode_url
, channel_url
in changed
]
1352 channel_urls
= [channel_url
for episode_url
, channel_url
in changed
]
1354 count
= downloading
+ queued
1356 title
.append(N_('downloading %d file', 'downloading %d files', count
) % count
)
1359 percentage
= 100.0*done_size
/total_size
1362 total_speed
= util
.format_filesize(total_speed
)
1363 title
[1] += ' (%d%%, %s/s)' % (percentage
, total_speed
)
1364 if self
.tray_icon
is not None:
1365 # Update the tray icon status and progress bar
1366 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_DOWNLOAD_IN_PROGRESS
, title
[1])
1367 self
.tray_icon
.draw_progress_bar(percentage
/100.)
1369 if self
.tray_icon
is not None:
1370 # Update the tray icon status
1371 self
.tray_icon
.set_status()
1372 if gpodder
.ui
.desktop
:
1373 self
.downloads_finished(self
.download_tasks_seen
)
1374 if gpodder
.ui
.diablo
:
1375 hildon
.hildon_banner_show_information(self
.gPodder
, '', 'gPodder: %s' % _('All downloads finished'))
1376 log('All downloads have finished.', sender
=self
)
1377 if self
.config
.cmd_all_downloads_complete
:
1378 util
.run_external_command(self
.config
.cmd_all_downloads_complete
)
1380 if gpodder
.ui
.fremantle
and failed
:
1381 message
= '\n'.join(['%s: %s' % (str(task
), \
1382 task
.error_message
) for task
in failed_downloads
])
1383 self
.show_message(message
, _('Downloads failed'), important
=True)
1385 # Remove finished episodes
1386 if self
.config
.auto_cleanup_downloads
and can_call_cleanup
:
1387 self
.cleanup_downloads()
1389 # Stop updating the download list here
1390 self
.download_list_update_enabled
= False
1392 if not gpodder
.ui
.fremantle
:
1393 self
.gPodder
.set_title(' - '.join(title
))
1395 self
.update_episode_list_icons(episode_urls
)
1396 if self
.episode_shownotes_window
is not None:
1397 if (shownotes_task
and shownotes_task
.url
in episode_urls
) or \
1398 shownotes_task
!= self
.episode_shownotes_window
.task
:
1399 self
.episode_shownotes_window
._download
_status
_changed
(shownotes_task
)
1400 self
.episode_shownotes_window
._download
_status
_progress
()
1401 self
.play_or_download()
1403 self
.update_podcast_list_model(channel_urls
)
1405 return self
.download_list_update_enabled
1406 except Exception, e
:
1407 log('Exception happened while updating download list.', sender
=self
, traceback
=True)
1408 self
.show_message('%s\n\n%s' % (_('Please report this problem and restart gPodder:'), str(e
)), _('Unhandled exception'), important
=True)
1409 # We return False here, so the update loop won't be called again,
1410 # that's why we require the restart of gPodder in the message.
1413 def on_config_changed(self
, *args
):
1414 util
.idle_add(self
._on
_config
_changed
, *args
)
1416 def _on_config_changed(self
, name
, old_value
, new_value
):
1417 if name
== 'show_toolbar' and gpodder
.ui
.desktop
:
1418 self
.toolbar
.set_property('visible', new_value
)
1419 elif name
== 'videoplayer':
1420 self
.config
.video_played_dbus
= False
1421 elif name
== 'player':
1422 self
.config
.audio_played_dbus
= False
1423 elif name
== 'episode_list_descriptions':
1424 self
.update_episode_list_model()
1425 elif name
== 'episode_list_thumbnails':
1426 self
.update_episode_list_icons(all
=True)
1427 elif name
== 'rotation_mode':
1428 self
._fremantle
_rotation
.set_mode(new_value
)
1429 elif name
in ('auto_update_feeds', 'auto_update_frequency'):
1430 self
.restart_auto_update_timer()
1431 elif name
== 'podcast_list_view_all':
1432 # Force a update of the podcast list model
1433 self
.channel_list_changed
= True
1434 if gpodder
.ui
.fremantle
:
1435 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
1436 while gtk
.events_pending():
1437 gtk
.main_iteration(False)
1438 self
.update_podcast_list_model()
1439 if gpodder
.ui
.fremantle
:
1440 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
1442 def on_treeview_query_tooltip(self
, treeview
, x
, y
, keyboard_tooltip
, tooltip
):
1443 # With get_bin_window, we get the window that contains the rows without
1444 # the header. The Y coordinate of this window will be the height of the
1445 # treeview header. This is the amount we have to subtract from the
1446 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
1447 (x_bin
, y_bin
) = treeview
.get_bin_window().get_position()
1450 (path
, column
, rx
, ry
) = treeview
.get_path_at_pos( x
, y
) or (None,)*4
1452 if not getattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
) or (column
is not None and column
!= treeview
.get_columns()[0]):
1453 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1456 if path
is not None:
1457 model
= treeview
.get_model()
1458 iter = model
.get_iter(path
)
1459 role
= getattr(treeview
, TreeViewHelper
.ROLE
)
1461 if role
== TreeViewHelper
.ROLE_EPISODES
:
1462 id = model
.get_value(iter, EpisodeListModel
.C_URL
)
1463 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1464 id = model
.get_value(iter, PodcastListModel
.C_URL
)
1466 last_tooltip
= getattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
)
1467 if last_tooltip
is not None and last_tooltip
!= id:
1468 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1470 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, id)
1472 if role
== TreeViewHelper
.ROLE_EPISODES
:
1473 description
= model
.get_value(iter, EpisodeListModel
.C_TOOLTIP
)
1475 tooltip
.set_text(description
)
1478 elif role
== TreeViewHelper
.ROLE_PODCASTS
:
1479 channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
1482 channel
.request_save_dir_size()
1483 diskspace_str
= util
.format_filesize(channel
.save_dir_size
, 0)
1484 error_str
= model
.get_value(iter, PodcastListModel
.C_ERROR
)
1486 error_str
= _('Feedparser error: %s') % saxutils
.escape(error_str
.strip())
1487 error_str
= '<span foreground="#ff0000">%s</span>' % error_str
1488 table
= gtk
.Table(rows
=3, columns
=3)
1489 table
.set_row_spacings(5)
1490 table
.set_col_spacings(5)
1491 table
.set_border_width(5)
1493 heading
= gtk
.Label()
1494 heading
.set_alignment(0, 1)
1495 heading
.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils
.escape(channel
.title
), saxutils
.escape(channel
.url
)))
1496 table
.attach(heading
, 0, 1, 0, 1)
1497 size_info
= gtk
.Label()
1498 size_info
.set_alignment(1, 1)
1499 size_info
.set_justify(gtk
.JUSTIFY_RIGHT
)
1500 size_info
.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str
, _('disk usage')))
1501 table
.attach(size_info
, 2, 3, 0, 1)
1503 table
.attach(gtk
.HSeparator(), 0, 3, 1, 2)
1505 if len(channel
.description
) < 500:
1506 description
= channel
.description
1508 pos
= channel
.description
.find('\n\n')
1509 if pos
== -1 or pos
> 500:
1510 description
= channel
.description
[:498]+'[...]'
1512 description
= channel
.description
[:pos
]
1514 description
= gtk
.Label(description
)
1516 description
.set_markup(error_str
)
1517 description
.set_alignment(0, 0)
1518 description
.set_line_wrap(True)
1519 table
.attach(description
, 0, 3, 2, 3)
1522 tooltip
.set_custom(table
)
1526 setattr(treeview
, TreeViewHelper
.LAST_TOOLTIP
, None)
1529 def treeview_allow_tooltips(self
, treeview
, allow
):
1530 setattr(treeview
, TreeViewHelper
.CAN_TOOLTIP
, allow
)
1532 def update_m3u_playlist_clicked(self
, widget
):
1533 if self
.active_channel
is not None:
1534 self
.active_channel
.update_m3u_playlist()
1535 self
.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'), widget
=self
.treeChannels
)
1537 def treeview_handle_context_menu_click(self
, treeview
, event
):
1538 x
, y
= int(event
.x
), int(event
.y
)
1539 path
, column
, rx
, ry
= treeview
.get_path_at_pos(x
, y
) or (None,)*4
1541 selection
= treeview
.get_selection()
1542 model
, paths
= selection
.get_selected_rows()
1544 if path
is None or (path
not in paths
and \
1545 event
.button
== self
.context_menu_mouse_button
):
1546 # We have right-clicked, but not into the selection,
1547 # assume we don't want to operate on the selection
1550 if path
is not None and not paths
and \
1551 event
.button
== self
.context_menu_mouse_button
:
1552 # No selection or clicked outside selection;
1553 # select the single item where we clicked
1554 treeview
.grab_focus()
1555 treeview
.set_cursor(path
, column
, 0)
1559 # Unselect any remaining items (clicked elsewhere)
1560 if hasattr(treeview
, 'is_rubber_banding_active'):
1561 if not treeview
.is_rubber_banding_active():
1562 selection
.unselect_all()
1564 selection
.unselect_all()
1568 def downloads_list_get_selection(self
, model
=None, paths
=None):
1569 if model
is None and paths
is None:
1570 selection
= self
.treeDownloads
.get_selection()
1571 model
, paths
= selection
.get_selected_rows()
1573 can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= (True,)*5
1574 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), \
1575 model
.get_value(model
.get_iter(path
), \
1576 DownloadStatusModel
.C_TASK
)) for path
in paths
]
1578 for row_reference
, task
in selected_tasks
:
1579 if task
.status
!= download
.DownloadTask
.QUEUED
:
1581 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1582 download
.DownloadTask
.FAILED
, \
1583 download
.DownloadTask
.CANCELLED
):
1585 if task
.status
not in (download
.DownloadTask
.PAUSED
, \
1586 download
.DownloadTask
.QUEUED
, \
1587 download
.DownloadTask
.DOWNLOADING
, \
1588 download
.DownloadTask
.FAILED
):
1590 if task
.status
not in (download
.DownloadTask
.QUEUED
, \
1591 download
.DownloadTask
.DOWNLOADING
):
1593 if task
.status
not in (download
.DownloadTask
.CANCELLED
, \
1594 download
.DownloadTask
.FAILED
, \
1595 download
.DownloadTask
.DONE
):
1598 return selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
1600 def downloads_finished(self
, download_tasks_seen
):
1601 # FIXME: Filter all tasks that have already been reported
1602 finished_downloads
= [str(task
) for task
in download_tasks_seen
if task
.status
== task
.DONE
]
1603 failed_downloads
= [str(task
)+' ('+task
.error_message
+')' for task
in download_tasks_seen
if task
.status
== task
.FAILED
]
1605 if finished_downloads
and failed_downloads
:
1606 message
= self
.format_episode_list(finished_downloads
, 5)
1607 message
+= '\n\n<i>%s</i>\n' % _('These downloads failed:')
1608 message
+= self
.format_episode_list(failed_downloads
, 5)
1609 self
.show_message(message
, _('Downloads finished'), True, widget
=self
.labelDownloads
)
1610 elif finished_downloads
:
1611 message
= self
.format_episode_list(finished_downloads
)
1612 self
.show_message(message
, _('Downloads finished'), widget
=self
.labelDownloads
)
1613 elif failed_downloads
:
1614 message
= self
.format_episode_list(failed_downloads
)
1615 self
.show_message(message
, _('Downloads failed'), True, widget
=self
.labelDownloads
)
1617 # Open torrent files right after download (bug 1029)
1618 if self
.config
.open_torrent_after_download
:
1619 for task
in download_tasks_seen
:
1620 if task
.status
!= task
.DONE
:
1623 episode
= task
.episode
1624 if episode
.mimetype
!= 'application/x-bittorrent':
1627 self
.playback_episodes([episode
])
1630 def format_episode_list(self
, episode_list
, max_episodes
=10):
1632 Format a list of episode names for notifications
1634 Will truncate long episode names and limit the amount of
1635 episodes displayed (max_episodes=10).
1637 The episode_list parameter should be a list of strings.
1639 MAX_TITLE_LENGTH
= 100
1642 for title
in episode_list
[:min(len(episode_list
), max_episodes
)]:
1643 if len(title
) > MAX_TITLE_LENGTH
:
1644 middle
= (MAX_TITLE_LENGTH
/2)-2
1645 title
= '%s...%s' % (title
[0:middle
], title
[-middle
:])
1646 result
.append(saxutils
.escape(title
))
1649 more_episodes
= len(episode_list
) - max_episodes
1650 if more_episodes
> 0:
1651 result
.append('(...')
1652 result
.append(N_('%d more episode', '%d more episodes', more_episodes
) % more_episodes
)
1653 result
.append('...)')
1655 return (''.join(result
)).strip()
1657 def _for_each_task_set_status(self
, tasks
, status
, force_start
=False):
1658 episode_urls
= set()
1659 model
= self
.treeDownloads
.get_model()
1660 for row_reference
, task
in tasks
:
1661 if status
== download
.DownloadTask
.QUEUED
:
1662 # Only queue task when its paused/failed/cancelled (or forced)
1663 if task
.status
in (task
.PAUSED
, task
.FAILED
, task
.CANCELLED
) or force_start
:
1664 self
.download_queue_manager
.add_task(task
, force_start
)
1665 self
.enable_download_list_update()
1666 elif status
== download
.DownloadTask
.CANCELLED
:
1667 # Cancelling a download allowed when downloading/queued
1668 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1669 task
.status
= status
1670 # Cancelling paused/failed downloads requires a call to .run()
1671 elif task
.status
in (task
.PAUSED
, task
.FAILED
):
1672 task
.status
= status
1673 # Call run, so the partial file gets deleted
1675 elif status
== download
.DownloadTask
.PAUSED
:
1676 # Pausing a download only when queued/downloading
1677 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
1678 task
.status
= status
1679 elif status
is None:
1680 # Remove the selected task - cancel downloading/queued tasks
1681 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
1682 task
.status
= task
.CANCELLED
1683 model
.remove(model
.get_iter(row_reference
.get_path()))
1684 # Remember the URL, so we can tell the UI to update
1686 # We don't "see" this task anymore - remove it;
1687 # this is needed, so update_episode_list_icons()
1688 # below gets the correct list of "seen" tasks
1689 self
.download_tasks_seen
.remove(task
)
1690 except KeyError, key_error
:
1691 log('Cannot remove task from "seen" list: %s', task
, sender
=self
)
1692 episode_urls
.add(task
.url
)
1693 # Tell the task that it has been removed (so it can clean up)
1694 task
.removed_from_list()
1696 # We can (hopefully) simply set the task status here
1697 task
.status
= status
1698 # Tell the podcasts tab to update icons for our removed podcasts
1699 self
.update_episode_list_icons(episode_urls
)
1700 # Update the tab title and downloads list
1701 self
.update_downloads_list()
1703 def treeview_downloads_show_context_menu(self
, treeview
, event
):
1704 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1706 if not hasattr(treeview
, 'is_rubber_banding_active'):
1709 return not treeview
.is_rubber_banding_active()
1711 if event
.button
== self
.context_menu_mouse_button
:
1712 selected_tasks
, can_queue
, can_cancel
, can_pause
, can_remove
, can_force
= \
1713 self
.downloads_list_get_selection(model
, paths
)
1715 def make_menu_item(label
, stock_id
, tasks
, status
, sensitive
, force_start
=False):
1716 # This creates a menu item for selection-wide actions
1717 item
= gtk
.ImageMenuItem(label
)
1718 item
.set_image(gtk
.image_new_from_stock(stock_id
, gtk
.ICON_SIZE_MENU
))
1719 item
.connect('activate', lambda item
: self
._for
_each
_task
_set
_status
(tasks
, status
, force_start
))
1720 item
.set_sensitive(sensitive
)
1721 return self
.set_finger_friendly(item
)
1725 item
= gtk
.ImageMenuItem(_('Episode details'))
1726 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1727 if len(selected_tasks
) == 1:
1728 row_reference
, task
= selected_tasks
[0]
1729 episode
= task
.episode
1730 item
.connect('activate', lambda item
: self
.show_episode_shownotes(episode
))
1732 item
.set_sensitive(False)
1733 menu
.append(self
.set_finger_friendly(item
))
1734 menu
.append(gtk
.SeparatorMenuItem())
1736 menu
.append(make_menu_item(_('Start download now'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, True, True))
1738 menu
.append(make_menu_item(_('Download'), gtk
.STOCK_GO_DOWN
, selected_tasks
, download
.DownloadTask
.QUEUED
, can_queue
, False))
1739 menu
.append(make_menu_item(_('Cancel'), gtk
.STOCK_CANCEL
, selected_tasks
, download
.DownloadTask
.CANCELLED
, can_cancel
))
1740 menu
.append(make_menu_item(_('Pause'), gtk
.STOCK_MEDIA_PAUSE
, selected_tasks
, download
.DownloadTask
.PAUSED
, can_pause
))
1741 menu
.append(gtk
.SeparatorMenuItem())
1742 menu
.append(make_menu_item(_('Remove from list'), gtk
.STOCK_REMOVE
, selected_tasks
, None, can_remove
))
1744 if gpodder
.ui
.maemo
:
1745 # Because we open the popup on left-click for Maemo,
1746 # we also include a non-action to close the menu
1747 menu
.append(gtk
.SeparatorMenuItem())
1748 item
= gtk
.ImageMenuItem(_('Close this menu'))
1749 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
1751 menu
.append(self
.set_finger_friendly(item
))
1754 menu
.popup(None, None, None, event
.button
, event
.time
)
1757 def treeview_channels_show_context_menu(self
, treeview
, event
):
1758 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1762 # Check for valid channel id, if there's no id then
1763 # assume that it is a proxy channel or equivalent
1764 # and cannot be operated with right click
1765 if self
.active_channel
.id is None:
1768 if event
.button
== 3:
1773 item
= gtk
.ImageMenuItem( _('Update podcast'))
1774 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_REFRESH
, gtk
.ICON_SIZE_MENU
))
1775 item
.connect('activate', self
.on_itemUpdateChannel_activate
)
1776 item
.set_sensitive(not self
.updating_feed_cache
)
1779 menu
.append(gtk
.SeparatorMenuItem())
1781 item
= gtk
.CheckMenuItem(_('Keep episodes'))
1782 item
.set_active(self
.active_channel
.channel_is_locked
)
1783 item
.connect('activate', self
.on_channel_toggle_lock_activate
)
1784 menu
.append(self
.set_finger_friendly(item
))
1786 item
= gtk
.ImageMenuItem(_('Remove podcast'))
1787 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
1788 item
.connect( 'activate', self
.on_itemRemoveChannel_activate
)
1791 if self
.config
.device_type
!= 'none':
1792 item
= gtk
.MenuItem(_('Synchronize to device'))
1793 item
.connect('activate', lambda item
: self
.on_sync_to_ipod_activate(item
, self
.active_channel
.get_downloaded_episodes()))
1796 menu
.append( gtk
.SeparatorMenuItem())
1798 item
= gtk
.ImageMenuItem(_('Podcast details'))
1799 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
1800 item
.connect('activate', self
.on_itemEditChannel_activate
)
1804 # Disable tooltips while we are showing the menu, so
1805 # the tooltip will not appear over the menu
1806 self
.treeview_allow_tooltips(self
.treeChannels
, False)
1807 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeChannels
, True))
1808 menu
.popup( None, None, None, event
.button
, event
.time
)
1812 def on_itemClose_activate(self
, widget
):
1813 if self
.tray_icon
is not None:
1814 self
.iconify_main_window()
1816 self
.on_gPodder_delete_event(widget
)
1818 def cover_file_removed(self
, channel_url
):
1820 The Cover Downloader calls this when a previously-
1821 available cover has been removed from the disk. We
1822 have to update our model to reflect this change.
1824 self
.podcast_list_model
.delete_cover_by_url(channel_url
)
1826 def cover_download_finished(self
, channel
, pixbuf
):
1828 The Cover Downloader calls this when it has finished
1829 downloading (or registering, if already downloaded)
1830 a new channel cover, which is ready for displaying.
1832 self
.podcast_list_model
.add_cover_by_channel(channel
, pixbuf
)
1834 def save_episodes_as_file(self
, episodes
):
1835 for episode
in episodes
:
1836 self
.save_episode_as_file(episode
)
1838 def save_episode_as_file(self
, episode
):
1839 PRIVATE_FOLDER_ATTRIBUTE
= '_save_episodes_as_file_folder'
1840 if episode
.was_downloaded(and_exists
=True):
1841 folder
= getattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, None)
1842 copy_from
= episode
.local_filename(create
=False)
1843 assert copy_from
is not None
1844 copy_to
= episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)
1845 (result
, folder
) = self
.show_copy_dialog(src_filename
=copy_from
, dst_filename
=copy_to
, dst_directory
=folder
)
1846 setattr(self
, PRIVATE_FOLDER_ATTRIBUTE
, folder
)
1848 def copy_episodes_bluetooth(self
, episodes
):
1849 episodes_to_copy
= [e
for e
in episodes
if e
.was_downloaded(and_exists
=True)]
1851 if gpodder
.ui
.maemo
:
1852 util
.bluetooth_send_files_maemo([e
.local_filename(create
=False) \
1853 for e
in episodes_to_copy
])
1856 def convert_and_send_thread(episode
):
1857 for episode
in episodes
:
1858 filename
= episode
.local_filename(create
=False)
1859 assert filename
is not None
1860 destfile
= os
.path
.join(tempfile
.gettempdir(), \
1861 util
.sanitize_filename(episode
.sync_filename(self
.config
.custom_sync_name_enabled
, self
.config
.custom_sync_name
)))
1862 (base
, ext
) = os
.path
.splitext(filename
)
1863 if not destfile
.endswith(ext
):
1867 shutil
.copyfile(filename
, destfile
)
1868 util
.bluetooth_send_file(destfile
)
1870 log('Cannot copy "%s" to "%s".', filename
, destfile
, sender
=self
)
1871 self
.notification(_('Error converting file.'), _('Bluetooth file transfer'), important
=True)
1873 util
.delete_file(destfile
)
1875 threading
.Thread(target
=convert_and_send_thread
, args
=[episodes_to_copy
]).start()
1877 def get_device_name(self
):
1878 if self
.config
.device_type
== 'ipod':
1880 elif self
.config
.device_type
in ('filesystem', 'mtp'):
1881 return _('MP3 player')
1883 return '(unknown device)'
1885 def _treeview_button_released(self
, treeview
, event
):
1886 xpos
, ypos
= TreeViewHelper
.get_button_press_event(treeview
)
1887 dy
= int(abs(event
.y
-ypos
))
1888 dx
= int(event
.x
-xpos
)
1890 selection
= treeview
.get_selection()
1891 path
= treeview
.get_path_at_pos(int(event
.x
), int(event
.y
))
1892 if path
is None or dy
> 30:
1893 return (False, dx
, dy
)
1895 path
, column
, x
, y
= path
1896 selection
.select_path(path
)
1897 treeview
.set_cursor(path
)
1898 treeview
.grab_focus()
1900 return (True, dx
, dy
)
1902 def treeview_channels_handle_gestures(self
, treeview
, event
):
1903 if self
.currently_updating
:
1906 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1909 if self
.config
.maemo_enable_gestures
:
1911 self
.on_itemUpdateChannel_activate()
1913 self
.on_itemEditChannel_activate(treeview
)
1917 def treeview_available_handle_gestures(self
, treeview
, event
):
1918 selected
, dx
, dy
= self
._treeview
_button
_released
(treeview
, event
)
1921 if self
.config
.maemo_enable_gestures
:
1923 self
.on_playback_selected_episodes(None)
1926 self
.on_shownotes_selected_episodes(None)
1929 # Pass the event to the context menu handler for treeAvailable
1930 self
.treeview_available_show_context_menu(treeview
, event
)
1934 def treeview_available_show_context_menu(self
, treeview
, event
):
1935 model
, paths
= self
.treeview_handle_context_menu_click(treeview
, event
)
1937 if not hasattr(treeview
, 'is_rubber_banding_active'):
1940 return not treeview
.is_rubber_banding_active()
1942 if event
.button
== self
.context_menu_mouse_button
:
1943 episodes
= self
.get_selected_episodes()
1944 any_locked
= any(e
.is_locked
for e
in episodes
)
1945 any_played
= any(e
.is_played
for e
in episodes
)
1946 one_is_new
= any(e
.state
== gpodder
.STATE_NORMAL
and not e
.is_played
for e
in episodes
)
1947 downloaded
= all(e
.was_downloaded(and_exists
=True) for e
in episodes
)
1948 downloading
= any(self
.episode_is_downloading(e
) for e
in episodes
)
1952 (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
) = self
.play_or_download()
1954 if open_instead_of_play
:
1955 item
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
1957 item
= gtk
.ImageMenuItem(gtk
.STOCK_MEDIA_PLAY
)
1959 item
= gtk
.ImageMenuItem(_('Stream'))
1960 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_MENU
))
1962 item
.set_sensitive(can_play
and not downloading
)
1963 item
.connect('activate', self
.on_playback_selected_episodes
)
1964 menu
.append(self
.set_finger_friendly(item
))
1967 item
= gtk
.ImageMenuItem(_('Download'))
1968 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_GO_DOWN
, gtk
.ICON_SIZE_MENU
))
1969 item
.set_sensitive(can_download
)
1970 item
.connect('activate', self
.on_download_selected_episodes
)
1971 menu
.append(self
.set_finger_friendly(item
))
1973 item
= gtk
.ImageMenuItem(gtk
.STOCK_CANCEL
)
1974 item
.connect('activate', self
.on_item_cancel_download_activate
)
1975 menu
.append(self
.set_finger_friendly(item
))
1977 item
= gtk
.ImageMenuItem(gtk
.STOCK_DELETE
)
1978 item
.set_sensitive(can_delete
)
1979 item
.connect('activate', self
.on_btnDownloadedDelete_clicked
)
1980 menu
.append(self
.set_finger_friendly(item
))
1984 # Ok, this probably makes sense to only display for downloaded files
1986 menu
.append(gtk
.SeparatorMenuItem())
1987 share_item
= gtk
.MenuItem(_('Send to'))
1988 menu
.append(self
.set_finger_friendly(share_item
))
1989 share_menu
= gtk
.Menu()
1991 item
= gtk
.ImageMenuItem(_('Local folder'))
1992 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
))
1993 item
.connect('button-press-event', lambda w
, ee
: self
.save_episodes_as_file(episodes
))
1994 share_menu
.append(self
.set_finger_friendly(item
))
1995 if self
.bluetooth_available
:
1996 item
= gtk
.ImageMenuItem(_('Bluetooth device'))
1997 if gpodder
.ui
.maemo
:
1998 icon_name
= ICON('qgn_list_filesys_bluetooth')
2000 icon_name
= ICON('bluetooth')
2001 item
.set_image(gtk
.image_new_from_icon_name(icon_name
, gtk
.ICON_SIZE_MENU
))
2002 item
.connect('button-press-event', lambda w
, ee
: self
.copy_episodes_bluetooth(episodes
))
2003 share_menu
.append(self
.set_finger_friendly(item
))
2005 item
= gtk
.ImageMenuItem(self
.get_device_name())
2006 item
.set_image(gtk
.image_new_from_icon_name(ICON('multimedia-player'), gtk
.ICON_SIZE_MENU
))
2007 item
.connect('button-press-event', lambda w
, ee
: self
.on_sync_to_ipod_activate(w
, episodes
))
2008 share_menu
.append(self
.set_finger_friendly(item
))
2010 share_item
.set_submenu(share_menu
)
2012 if (downloaded
or one_is_new
or can_download
) and not downloading
:
2013 menu
.append(gtk
.SeparatorMenuItem())
2015 item
= gtk
.CheckMenuItem(_('New'))
2016 item
.set_active(True)
2017 item
.connect('activate', lambda w
: self
.mark_selected_episodes_old())
2018 menu
.append(self
.set_finger_friendly(item
))
2020 item
= gtk
.CheckMenuItem(_('New'))
2021 item
.set_active(False)
2022 item
.connect('activate', lambda w
: self
.mark_selected_episodes_new())
2023 menu
.append(self
.set_finger_friendly(item
))
2026 item
= gtk
.CheckMenuItem(_('Played'))
2027 item
.set_active(any_played
)
2028 item
.connect( 'activate', lambda w
: self
.on_item_toggle_played_activate( w
, False, not any_played
))
2029 menu
.append(self
.set_finger_friendly(item
))
2031 item
= gtk
.CheckMenuItem(_('Keep episode'))
2032 item
.set_active(any_locked
)
2033 item
.connect('activate', lambda w
: self
.on_item_toggle_lock_activate( w
, False, not any_locked
))
2034 menu
.append(self
.set_finger_friendly(item
))
2036 menu
.append(gtk
.SeparatorMenuItem())
2037 # Single item, add episode information menu item
2038 item
= gtk
.ImageMenuItem(_('Episode details'))
2039 item
.set_image(gtk
.image_new_from_stock( gtk
.STOCK_INFO
, gtk
.ICON_SIZE_MENU
))
2040 item
.connect('activate', lambda w
: self
.show_episode_shownotes(episodes
[0]))
2041 menu
.append(self
.set_finger_friendly(item
))
2043 if gpodder
.ui
.maemo
:
2044 # Because we open the popup on left-click for Maemo,
2045 # we also include a non-action to close the menu
2046 menu
.append(gtk
.SeparatorMenuItem())
2047 item
= gtk
.ImageMenuItem(_('Close this menu'))
2048 item
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_CLOSE
, gtk
.ICON_SIZE_MENU
))
2049 menu
.append(self
.set_finger_friendly(item
))
2052 # Disable tooltips while we are showing the menu, so
2053 # the tooltip will not appear over the menu
2054 self
.treeview_allow_tooltips(self
.treeAvailable
, False)
2055 menu
.connect('deactivate', lambda menushell
: self
.treeview_allow_tooltips(self
.treeAvailable
, True))
2056 menu
.popup( None, None, None, event
.button
, event
.time
)
2060 def set_title(self
, new_title
):
2061 if not gpodder
.ui
.fremantle
:
2062 self
.default_title
= new_title
2063 self
.gPodder
.set_title(new_title
)
2065 def update_episode_list_icons(self
, urls
=None, selected
=False, all
=False):
2067 Updates the status icons in the episode list.
2069 If urls is given, it should be a list of URLs
2070 of episodes that should be updated.
2072 If urls is None, set ONE OF selected, all to
2073 True (the former updates just the selected
2074 episodes and the latter updates all episodes).
2076 additional_args
= (self
.episode_is_downloading
, \
2077 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2078 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
)
2080 if urls
is not None:
2081 # We have a list of URLs to walk through
2082 self
.episode_list_model
.update_by_urls(urls
, *additional_args
)
2083 elif selected
and not all
:
2084 # We should update all selected episodes
2085 selection
= self
.treeAvailable
.get_selection()
2086 model
, paths
= selection
.get_selected_rows()
2087 for path
in reversed(paths
):
2088 iter = model
.get_iter(path
)
2089 self
.episode_list_model
.update_by_filter_iter(iter, \
2091 elif all
and not selected
:
2092 # We update all (even the filter-hidden) episodes
2093 self
.episode_list_model
.update_all(*additional_args
)
2095 # Wrong/invalid call - have to specify at least one parameter
2096 raise ValueError('Invalid call to update_episode_list_icons')
2098 def episode_list_status_changed(self
, episodes
):
2099 self
.update_episode_list_icons(set(e
.url
for e
in episodes
))
2100 self
.update_podcast_list_model(set(e
.channel
.url
for e
in episodes
))
2103 def clean_up_downloads(self
, delete_partial
=False):
2104 # Clean up temporary files left behind by old gPodder versions
2105 temporary_files
= glob
.glob('%s/*/.tmp-*' % self
.config
.download_dir
)
2108 temporary_files
+= glob
.glob('%s/*/*.partial' % self
.config
.download_dir
)
2110 for tempfile
in temporary_files
:
2111 util
.delete_file(tempfile
)
2113 # Clean up empty download folders and abandoned download folders
2114 download_dirs
= glob
.glob(os
.path
.join(self
.config
.download_dir
, '*'))
2115 for ddir
in download_dirs
:
2116 if os
.path
.isdir(ddir
) and False: # FIXME not db.channel_foldername_exists(os.path.basename(ddir)):
2117 globr
= glob
.glob(os
.path
.join(ddir
, '*'))
2118 if len(globr
) == 0 or (len(globr
) == 1 and globr
[0].endswith('/cover')):
2119 log('Stale download directory found: %s', os
.path
.basename(ddir
), sender
=self
)
2120 shutil
.rmtree(ddir
, ignore_errors
=True)
2122 def streaming_possible(self
):
2123 if gpodder
.ui
.desktop
:
2124 # User has to have a media player set on the Desktop, or else we
2125 # would probably open the browser when giving a URL to xdg-open..
2126 return (self
.config
.player
and self
.config
.player
!= 'default')
2127 elif gpodder
.ui
.maemo
:
2128 # On Maemo, the default is to use the Nokia Media Player, which is
2129 # already able to deal with HTTP URLs the right way, so we
2130 # unconditionally enable streaming always on Maemo
2135 def playback_episodes_for_real(self
, episodes
):
2136 groups
= collections
.defaultdict(list)
2137 for episode
in episodes
:
2138 file_type
= episode
.file_type()
2139 if file_type
== 'video' and self
.config
.videoplayer
and \
2140 self
.config
.videoplayer
!= 'default':
2141 player
= self
.config
.videoplayer
2142 if gpodder
.ui
.diablo
:
2143 # Use the wrapper script if it's installed to crop 3GP YouTube
2144 # videos to fit the screen (looks much nicer than w/ black border)
2145 if player
== 'mplayer' and util
.find_command('gpodder-mplayer'):
2146 player
= 'gpodder-mplayer'
2147 elif gpodder
.ui
.fremantle
and player
== 'mplayer':
2148 player
= 'mplayer -fs %F'
2149 elif file_type
== 'audio' and self
.config
.player
and \
2150 self
.config
.player
!= 'default':
2151 player
= self
.config
.player
2155 if file_type
not in ('audio', 'video') or \
2156 (file_type
== 'audio' and not self
.config
.audio_played_dbus
) or \
2157 (file_type
== 'video' and not self
.config
.video_played_dbus
):
2158 # Mark episode as played in the database
2159 episode
.mark(is_played
=True)
2160 self
.mygpo_client
.on_playback([episode
])
2162 filename
= episode
.local_filename(create
=False)
2163 if filename
is None or not os
.path
.exists(filename
):
2164 filename
= episode
.url
2165 if youtube
.is_video_link(filename
):
2166 fmt_id
= self
.config
.youtube_preferred_fmt_id
2167 if gpodder
.ui
.fremantle
:
2169 filename
= youtube
.get_real_download_url(filename
, fmt_id
)
2171 # Determine the playback resume position - if the file
2172 # was played 100%, we simply start from the beginning
2173 resume_position
= episode
.current_position
2174 if resume_position
== episode
.total_time
:
2177 if gpodder
.ui
.fremantle
:
2178 self
.mafw_monitor
.set_resume_point(filename
, resume_position
)
2180 # If Panucci is configured, use D-Bus on Maemo to call it
2181 if player
== 'panucci':
2183 PANUCCI_NAME
= 'org.panucci.panucciInterface'
2184 PANUCCI_PATH
= '/panucciInterface'
2185 PANUCCI_INTF
= 'org.panucci.panucciInterface'
2186 o
= gpodder
.dbus_session_bus
.get_object(PANUCCI_NAME
, PANUCCI_PATH
)
2187 i
= dbus
.Interface(o
, PANUCCI_INTF
)
2189 def on_reply(*args
):
2192 def error_handler(filename
, err
):
2193 log('Exception in D-Bus call: %s', str(err
), \
2196 # Fallback: use the command line client
2197 for command
in util
.format_desktop_command('panucci', \
2199 log('Executing: %s', repr(command
), sender
=self
)
2200 subprocess
.Popen(command
)
2202 on_error
= lambda err
: error_handler(filename
, err
)
2204 # This method only exists in Panucci > 0.9 ('new Panucci')
2205 i
.playback_from(filename
, resume_position
, \
2206 reply_handler
=on_reply
, error_handler
=on_error
)
2208 continue # This file was handled by the D-Bus call
2209 except Exception, e
:
2210 log('Error calling Panucci using D-Bus', sender
=self
, traceback
=True)
2211 elif player
== 'MediaBox' and gpodder
.ui
.maemo
:
2213 MEDIABOX_NAME
= 'de.pycage.mediabox'
2214 MEDIABOX_PATH
= '/de/pycage/mediabox/control'
2215 MEDIABOX_INTF
= 'de.pycage.mediabox.control'
2216 o
= gpodder
.dbus_session_bus
.get_object(MEDIABOX_NAME
, MEDIABOX_PATH
)
2217 i
= dbus
.Interface(o
, MEDIABOX_INTF
)
2219 def on_reply(*args
):
2223 log('Exception in D-Bus call: %s', str(err
), \
2226 i
.load(filename
, '%s/x-unknown' % file_type
, \
2227 reply_handler
=on_reply
, error_handler
=on_error
)
2229 continue # This file was handled by the D-Bus call
2230 except Exception, e
:
2231 log('Error calling MediaBox using D-Bus', sender
=self
, traceback
=True)
2233 groups
[player
].append(filename
)
2235 # Open episodes with system default player
2236 if 'default' in groups
:
2237 if gpodder
.ui
.maemo
:
2238 # The Nokia Media Player app does not support receiving multiple
2239 # file names via D-Bus, so we simply place all file names into a
2240 # temporary M3U playlist and open that with the Media Player.
2241 m3u_filename
= os
.path
.join(gpodder
.home
, 'gpodder_open_with.m3u')
2242 util
.write_m3u_playlist(m3u_filename
, groups
['default'], extm3u
=False)
2243 util
.gui_open(m3u_filename
)
2245 for filename
in groups
['default']:
2246 log('Opening with system default: %s', filename
, sender
=self
)
2247 util
.gui_open(filename
)
2248 del groups
['default']
2249 elif gpodder
.ui
.maemo
and groups
:
2250 # When on Maemo and not opening with default, show a notification
2251 # (no startup notification for Panucci / MPlayer yet...)
2252 if len(episodes
) == 1:
2253 text
= _('Opening %s') % episodes
[0].title
2255 count
= len(episodes
)
2256 text
= N_('Opening %d episode', 'Opening %d episodes', count
) % count
2258 banner
= hildon
.hildon_banner_show_animation(self
.gPodder
, '', text
)
2260 def destroy_banner_later(banner
):
2263 gobject
.timeout_add(5000, destroy_banner_later
, banner
)
2265 # For each type now, go and create play commands
2266 for group
in groups
:
2267 for command
in util
.format_desktop_command(group
, groups
[group
]):
2268 log('Executing: %s', repr(command
), sender
=self
)
2269 subprocess
.Popen(command
)
2271 # Persist episode status changes to the database
2274 # Flush updated episode status
2275 self
.mygpo_client
.flush()
2277 def playback_episodes(self
, episodes
):
2278 # We need to create a list, because we run through it more than once
2279 episodes
= list(PodcastEpisode
.sort_by_pubdate(e
for e
in episodes
if \
2280 e
.was_downloaded(and_exists
=True) or self
.streaming_possible()))
2283 self
.playback_episodes_for_real(episodes
)
2284 except Exception, e
:
2285 log('Error in playback!', sender
=self
, traceback
=True)
2286 if gpodder
.ui
.desktop
:
2287 self
.show_message(_('Please check your media player settings in the preferences dialog.'), \
2288 _('Error opening player'), widget
=self
.toolPreferences
)
2290 self
.show_message(_('Please check your media player settings in the preferences dialog.'))
2292 channel_urls
= set()
2293 episode_urls
= set()
2294 for episode
in episodes
:
2295 channel_urls
.add(episode
.channel
.url
)
2296 episode_urls
.add(episode
.url
)
2297 self
.update_episode_list_icons(episode_urls
)
2298 self
.update_podcast_list_model(channel_urls
)
2300 def play_or_download(self
):
2301 if not gpodder
.ui
.fremantle
:
2302 if self
.wNotebook
.get_current_page() > 0:
2303 if gpodder
.ui
.desktop
:
2304 self
.toolCancel
.set_sensitive(True)
2307 if self
.currently_updating
:
2308 return (False, False, False, False, False, False)
2310 ( can_play
, can_download
, can_transfer
, can_cancel
, can_delete
) = (False,)*5
2311 ( is_played
, is_locked
) = (False,)*2
2313 open_instead_of_play
= False
2315 selection
= self
.treeAvailable
.get_selection()
2316 if selection
.count_selected_rows() > 0:
2317 (model
, paths
) = selection
.get_selected_rows()
2321 episode
= model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
)
2322 except TypeError, te
:
2323 log('Invalid episode at path %s', str(path
), sender
=self
)
2326 if episode
.file_type() not in ('audio', 'video'):
2327 open_instead_of_play
= True
2329 if episode
.was_downloaded():
2330 can_play
= episode
.was_downloaded(and_exists
=True)
2331 is_played
= episode
.is_played
2332 is_locked
= episode
.is_locked
2336 if self
.episode_is_downloading(episode
):
2341 can_download
= can_download
and not can_cancel
2342 can_play
= self
.streaming_possible() or (can_play
and not can_cancel
and not can_download
)
2343 can_transfer
= can_play
and self
.config
.device_type
!= 'none' and not can_cancel
and not can_download
and not open_instead_of_play
2344 can_delete
= not can_cancel
2346 if gpodder
.ui
.desktop
:
2347 if open_instead_of_play
:
2348 self
.toolPlay
.set_stock_id(gtk
.STOCK_OPEN
)
2350 self
.toolPlay
.set_stock_id(gtk
.STOCK_MEDIA_PLAY
)
2351 self
.toolPlay
.set_sensitive( can_play
)
2352 self
.toolDownload
.set_sensitive( can_download
)
2353 self
.toolTransfer
.set_sensitive( can_transfer
)
2354 self
.toolCancel
.set_sensitive( can_cancel
)
2356 if not gpodder
.ui
.fremantle
:
2357 self
.item_cancel_download
.set_sensitive(can_cancel
)
2358 self
.itemDownloadSelected
.set_sensitive(can_download
)
2359 self
.itemOpenSelected
.set_sensitive(can_play
)
2360 self
.itemPlaySelected
.set_sensitive(can_play
)
2361 self
.itemDeleteSelected
.set_sensitive(can_delete
)
2362 self
.item_toggle_played
.set_sensitive(can_play
)
2363 self
.item_toggle_lock
.set_sensitive(can_play
)
2364 self
.itemOpenSelected
.set_visible(open_instead_of_play
)
2365 self
.itemPlaySelected
.set_visible(not open_instead_of_play
)
2367 return (can_play
, can_download
, can_transfer
, can_cancel
, can_delete
, open_instead_of_play
)
2369 def on_cbMaxDownloads_toggled(self
, widget
, *args
):
2370 self
.spinMaxDownloads
.set_sensitive(self
.cbMaxDownloads
.get_active())
2372 def on_cbLimitDownloads_toggled(self
, widget
, *args
):
2373 self
.spinLimitDownloads
.set_sensitive(self
.cbLimitDownloads
.get_active())
2375 def episode_new_status_changed(self
, urls
):
2376 self
.update_podcast_list_model()
2377 self
.update_episode_list_icons(urls
)
2379 def update_podcast_list_model(self
, urls
=None, selected
=False, select_url
=None):
2380 """Update the podcast list treeview model
2382 If urls is given, it should list the URLs of each
2383 podcast that has to be updated in the list.
2385 If selected is True, only update the model contents
2386 for the currently-selected podcast - nothing more.
2388 The caller can optionally specify "select_url",
2389 which is the URL of the podcast that is to be
2390 selected in the list after the update is complete.
2391 This only works if the podcast list has to be
2392 reloaded; i.e. something has been added or removed
2393 since the last update of the podcast list).
2395 selection
= self
.treeChannels
.get_selection()
2396 model
, iter = selection
.get_selected()
2398 if self
.config
.podcast_list_view_all
and not self
.channel_list_changed
:
2399 # Update "all episodes" view in any case (if enabled)
2400 self
.podcast_list_model
.update_first_row()
2403 # very cheap! only update selected channel
2404 if iter is not None:
2405 # If we have selected the "all episodes" view, we have
2406 # to update all channels for selected episodes:
2407 if self
.config
.podcast_list_view_all
and \
2408 self
.podcast_list_model
.iter_is_first_row(iter):
2409 urls
= self
.get_podcast_urls_from_selected_episodes()
2410 self
.podcast_list_model
.update_by_urls(urls
)
2412 # Otherwise just update the selected row (a podcast)
2413 self
.podcast_list_model
.update_by_filter_iter(iter)
2414 elif not self
.channel_list_changed
:
2415 # we can keep the model, but have to update some
2417 # still cheaper than reloading the whole list
2418 self
.podcast_list_model
.update_all()
2420 # ok, we got a bunch of urls to update
2421 self
.podcast_list_model
.update_by_urls(urls
)
2423 if model
and iter and select_url
is None:
2424 # Get the URL of the currently-selected podcast
2425 select_url
= model
.get_value(iter, PodcastListModel
.C_URL
)
2427 # Update the podcast list model with new channels
2428 self
.podcast_list_model
.set_channels(self
.db
, self
.config
, self
.channels
)
2431 selected_iter
= model
.get_iter_first()
2432 # Find the previously-selected URL in the new
2433 # model if we have an URL (else select first)
2434 if select_url
is not None:
2435 pos
= model
.get_iter_first()
2436 while pos
is not None:
2437 url
= model
.get_value(pos
, PodcastListModel
.C_URL
)
2438 if url
== select_url
:
2441 pos
= model
.iter_next(pos
)
2443 if not gpodder
.ui
.fremantle
:
2444 if selected_iter
is not None:
2445 selection
.select_iter(selected_iter
)
2446 self
.on_treeChannels_cursor_changed(self
.treeChannels
)
2448 log('Cannot select podcast in list', traceback
=True, sender
=self
)
2449 self
.channel_list_changed
= False
2451 def episode_is_downloading(self
, episode
):
2452 """Returns True if the given episode is being downloaded at the moment"""
2456 return episode
.url
in (task
.url
for task
in self
.download_tasks_seen
if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
, task
.PAUSED
))
2458 def update_episode_list_model(self
):
2459 if self
.channels
and self
.active_channel
is not None:
2460 if gpodder
.ui
.fremantle
:
2461 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2463 self
.currently_updating
= True
2464 self
.episode_list_model
.clear()
2465 self
.episode_list_model
.reset_update_progress()
2466 self
.treeAvailable
.set_model(self
.empty_episode_list_model
)
2467 def do_update_episode_list_model():
2468 additional_args
= (self
.episode_is_downloading
, \
2469 self
.config
.episode_list_descriptions
and gpodder
.ui
.desktop
, \
2470 self
.config
.episode_list_thumbnails
and gpodder
.ui
.desktop
, \
2472 self
.episode_list_model
.add_from_channel(self
.active_channel
, *additional_args
)
2474 def on_episode_list_model_updated():
2475 if gpodder
.ui
.fremantle
:
2476 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2477 self
.treeAvailable
.set_model(self
.episode_list_model
.get_filtered_model())
2478 self
.treeAvailable
.columns_autosize()
2479 self
.currently_updating
= False
2480 self
.play_or_download()
2481 util
.idle_add(on_episode_list_model_updated
)
2482 threading
.Thread(target
=do_update_episode_list_model
).start()
2484 self
.episode_list_model
.clear()
2486 @dbus.service
.method(gpodder
.dbus_interface
)
2487 def offer_new_episodes(self
, channels
=None):
2488 if gpodder
.ui
.fremantle
:
2489 # Assume that when this function is called that the
2490 # notification is not shown anymore (Maemo bug 11345)
2491 self
._fremantle
_notification
_visible
= False
2493 new_episodes
= self
.get_new_episodes(channels
)
2495 self
.new_episodes_show(new_episodes
)
2499 def add_podcast_list(self
, urls
, auth_tokens
=None):
2500 """Subscribe to a list of podcast given their URLs
2502 If auth_tokens is given, it should be a dictionary
2503 mapping URLs to (username, password) tuples."""
2505 if auth_tokens
is None:
2508 # Sort and split the URL list into five buckets
2509 queued
, failed
, existing
, worked
, authreq
= [], [], [], [], []
2510 for input_url
in urls
:
2511 url
= util
.normalize_feed_url(input_url
)
2513 # Fail this one because the URL is not valid
2514 failed
.append(input_url
)
2515 elif self
.podcast_list_model
.get_filter_path_from_url(url
) is not None:
2516 # A podcast already exists in the list for this URL
2517 existing
.append(url
)
2519 # This URL has survived the first round - queue for add
2521 if url
!= input_url
and input_url
in auth_tokens
:
2522 auth_tokens
[url
] = auth_tokens
[input_url
]
2527 progress
= ProgressIndicator(_('Adding podcasts'), \
2528 _('Please wait while episode information is downloaded.'), \
2529 parent
=self
.get_dialog_parent())
2531 def on_after_update():
2532 progress
.on_finished()
2533 # Report already-existing subscriptions to the user
2535 title
= _('Existing subscriptions skipped')
2536 message
= _('You are already subscribed to these podcasts:') \
2537 + '\n\n' + '\n'.join(saxutils
.escape(url
) for url
in existing
)
2538 self
.show_message(message
, title
, widget
=self
.treeChannels
)
2540 # Report subscriptions that require authentication
2544 title
= _('Podcast requires authentication')
2545 message
= _('Please login to %s:') % (saxutils
.escape(url
),)
2546 success
, auth_tokens
= self
.show_login_dialog(title
, message
)
2548 retry_podcasts
[url
] = auth_tokens
2550 # Stop asking the user for more login data
2553 error_messages
[url
] = _('Authentication failed')
2557 # If we have authentication data to retry, do so here
2559 self
.add_podcast_list(retry_podcasts
.keys(), retry_podcasts
)
2561 # Report website redirections
2562 for url
in redirections
:
2563 title
= _('Website redirection detected')
2564 message
= _('The URL %(url)s redirects to %(target)s.') \
2565 + '\n\n' + _('Do you want to visit the website now?')
2566 message
= message
% {'url': url
, 'target': redirections
[url
]}
2567 if self
.show_confirmation(message
, title
):
2568 util
.open_website(url
)
2572 # Report failed subscriptions to the user
2574 title
= _('Could not add some podcasts')
2575 message
= _('Some podcasts could not be added to your list:') \
2576 + '\n\n' + '\n'.join(saxutils
.escape('%s: %s' % (url
, \
2577 error_messages
.get(url
, _('Unknown')))) for url
in failed
)
2578 self
.show_message(message
, title
, important
=True)
2580 # Upload subscription changes to gpodder.net
2581 self
.mygpo_client
.on_subscribe(worked
)
2583 # If at least one podcast has been added, save and update all
2584 if self
.channel_list_changed
:
2585 # Fix URLs if mygpo has rewritten them
2586 self
.rewrite_urls_mygpo()
2588 self
.save_channels_opml()
2590 # If only one podcast was added, select it after the update
2591 if len(worked
) == 1:
2596 # Update the list of subscribed podcasts
2597 self
.update_feed_cache(force_update
=False, select_url_afterwards
=url
)
2598 self
.update_podcasts_tab()
2600 # Offer to download new episodes
2602 for podcast
in self
.channels
:
2603 if podcast
.url
in worked
:
2604 episodes
.extend(podcast
.get_all_episodes())
2607 episodes
= list(PodcastEpisode
.sort_by_pubdate(episodes
, \
2609 self
.new_episodes_show(episodes
, \
2610 selected
=[e
.check_is_new() for e
in episodes
])
2614 # After the initial sorting and splitting, try all queued podcasts
2615 length
= len(queued
)
2616 for index
, url
in enumerate(queued
):
2617 progress
.on_progress(float(index
)/float(length
))
2618 progress
.on_message(url
)
2619 log('QUEUE RUNNER: %s', url
, sender
=self
)
2621 # The URL is valid and does not exist already - subscribe!
2622 channel
= PodcastChannel
.load(self
.db
, url
=url
, create
=True, \
2623 authentication_tokens
=auth_tokens
.get(url
, None), \
2624 max_episodes
=self
.config
.max_episodes_per_feed
, \
2625 download_dir
=self
.config
.download_dir
, \
2626 allow_empty_feeds
=self
.config
.allow_empty_feeds
, \
2627 mimetype_prefs
=self
.config
.mimetype_prefs
)
2630 username
, password
= util
.username_password_from_url(url
)
2631 except ValueError, ve
:
2632 username
, password
= (None, None)
2634 if username
is not None and channel
.username
is None and \
2635 password
is not None and channel
.password
is None:
2636 channel
.username
= username
2637 channel
.password
= password
2640 self
._update
_cover
(channel
)
2641 except feedcore
.AuthenticationRequired
:
2642 if url
in auth_tokens
:
2643 # Fail for wrong authentication data
2644 error_messages
[url
] = _('Authentication failed')
2647 # Queue for login dialog later
2650 except feedcore
.WifiLogin
, error
:
2651 redirections
[url
] = error
.data
2653 error_messages
[url
] = _('Redirection detected')
2655 except Exception, e
:
2656 log('Subscription error: %s', e
, traceback
=True, sender
=self
)
2657 error_messages
[url
] = str(e
)
2661 assert channel
is not None
2662 worked
.append(channel
.url
)
2663 self
.channels
.append(channel
)
2664 self
.channel_list_changed
= True
2665 util
.idle_add(on_after_update
)
2666 threading
.Thread(target
=thread_proc
).start()
2668 def save_channels_opml(self
):
2669 exporter
= opml
.Exporter(gpodder
.subscription_file
)
2670 return exporter
.write(self
.channels
)
2672 def find_episode(self
, podcast_url
, episode_url
):
2673 """Find an episode given its podcast and episode URL
2675 The function will return a PodcastEpisode object if
2676 the episode is found, or None if it's not found.
2678 for podcast
in self
.channels
:
2679 if podcast_url
== podcast
.url
:
2680 for episode
in podcast
.get_all_episodes():
2681 if episode_url
== episode
.url
:
2686 def process_received_episode_actions(self
, updated_urls
):
2687 """Process/merge episode actions from gpodder.net
2689 This function will merge all changes received from
2690 the server to the local database and update the
2691 status of the affected episodes as necessary.
2693 indicator
= ProgressIndicator(_('Merging episode actions'), \
2694 _('Episode actions from gpodder.net are merged.'), \
2695 False, self
.get_dialog_parent())
2697 for idx
, action
in enumerate(self
.mygpo_client
.get_episode_actions(updated_urls
)):
2698 if action
.action
== 'play':
2699 episode
= self
.find_episode(action
.podcast_url
, \
2702 if episode
is not None:
2703 log('Play action for %s', episode
.url
, sender
=self
)
2704 episode
.mark(is_played
=True)
2706 if action
.timestamp
> episode
.current_position_updated
:
2707 log('Updating position for %s', episode
.url
, sender
=self
)
2708 episode
.current_position
= action
.position
2709 episode
.current_position_updated
= action
.timestamp
2712 log('Updating total time for %s', episode
.url
, sender
=self
)
2713 episode
.total_time
= action
.total
2716 elif action
.action
== 'delete':
2717 episode
= self
.find_episode(action
.podcast_url
, \
2720 if episode
is not None:
2721 if not episode
.was_downloaded(and_exists
=True):
2722 # Set the episode to a "deleted" state
2723 log('Marking as deleted: %s', episode
.url
, sender
=self
)
2724 episode
.delete_from_disk()
2727 indicator
.on_message(N_('%d action processed', '%d actions processed', idx
) % idx
)
2728 gtk
.main_iteration(False)
2730 indicator
.on_finished()
2734 def update_feed_cache_finish_callback(self
, updated_urls
=None, select_url_afterwards
=None):
2736 self
.updating_feed_cache
= False
2738 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2740 # Process received episode actions for all updated URLs
2741 self
.process_received_episode_actions(updated_urls
)
2743 self
.channel_list_changed
= True
2744 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2746 # Only search for new episodes in podcasts that have been
2747 # updated, not in other podcasts (for single-feed updates)
2748 episodes
= self
.get_new_episodes([c
for c
in self
.channels
if c
.url
in updated_urls
])
2750 if gpodder
.ui
.fremantle
:
2751 self
.fancy_progress_bar
.hide()
2752 self
.button_subscribe
.set_sensitive(True)
2753 self
.button_refresh
.set_sensitive(True)
2754 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, False)
2755 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, False)
2756 self
.update_podcasts_tab()
2757 self
.update_episode_list_model()
2758 if self
.feed_cache_update_cancelled
:
2761 def application_in_foreground():
2763 return any(w
.get_property('is-topmost') for w
in hildon
.WindowStack
.get_default().get_windows())
2764 except Exception, e
:
2765 log('Could not determine is-topmost', traceback
=True)
2766 # When in doubt, assume not in foreground
2770 if self
.config
.auto_download
== 'quiet' and not self
.config
.auto_update_feeds
:
2771 # New episodes found, but we should do nothing
2772 self
.show_message(_('New episodes are available.'))
2773 elif self
.config
.auto_download
== 'always':
2774 count
= len(episodes
)
2775 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2776 self
.show_message(title
)
2777 self
.download_episode_list(episodes
)
2778 elif self
.config
.auto_download
== 'queue':
2779 self
.show_message(_('New episodes have been added to the download list.'))
2780 self
.download_episode_list_paused(episodes
)
2781 elif application_in_foreground():
2782 if not self
._fremantle
_notification
_visible
:
2783 self
.new_episodes_show(episodes
)
2784 elif not self
._fremantle
_notification
_visible
:
2787 pynotify
.init('gPodder')
2788 n
= pynotify
.Notification('gPodder', _('New episodes available'), 'gpodder')
2789 n
.set_urgency(pynotify
.URGENCY_CRITICAL
)
2790 n
.set_hint('dbus-callback-default', ' '.join([
2791 gpodder
.dbus_bus_name
,
2792 gpodder
.dbus_gui_object_path
,
2793 gpodder
.dbus_interface
,
2794 'offer_new_episodes',
2796 n
.set_category('gpodder-new-episodes')
2798 self
._fremantle
_notification
_visible
= True
2799 except Exception, e
:
2800 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2801 self
.new_episodes_show(episodes
)
2802 self
._fremantle
_notification
_visible
= False
2803 elif not self
.config
.auto_update_feeds
:
2804 self
.show_message(_('No new episodes. Please check for new episodes later.'))
2808 self
.tray_icon
.set_status()
2810 if self
.feed_cache_update_cancelled
:
2811 # The user decided to abort the feed update
2812 self
.show_update_feeds_buttons()
2814 # Nothing new here - but inform the user
2815 self
.pbFeedUpdate
.set_fraction(1.0)
2816 self
.pbFeedUpdate
.set_text(_('No new episodes'))
2817 self
.feed_cache_update_cancelled
= True
2818 self
.btnCancelFeedUpdate
.show()
2819 self
.btnCancelFeedUpdate
.set_sensitive(True)
2820 if gpodder
.ui
.maemo
:
2821 # btnCancelFeedUpdate is a ToolButton on Maemo
2822 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_APPLY
)
2824 # btnCancelFeedUpdate is a normal gtk.Button
2825 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_APPLY
, gtk
.ICON_SIZE_BUTTON
))
2827 count
= len(episodes
)
2828 # New episodes are available
2829 self
.pbFeedUpdate
.set_fraction(1.0)
2830 # Are we minimized and should we auto download?
2831 if (self
.is_iconified() and (self
.config
.auto_download
== 'minimized')) or (self
.config
.auto_download
== 'always'):
2832 self
.download_episode_list(episodes
)
2833 title
= N_('Downloading %d new episode.', 'Downloading %d new episodes.', count
) % count
2834 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2835 self
.show_update_feeds_buttons()
2836 elif self
.config
.auto_download
== 'queue':
2837 self
.download_episode_list_paused(episodes
)
2838 title
= N_('%d new episode added to download list.', '%d new episodes added to download list.', count
) % count
2839 self
.show_message(title
, _('New episodes available'), widget
=self
.labelDownloads
)
2840 self
.show_update_feeds_buttons()
2842 self
.show_update_feeds_buttons()
2843 # New episodes are available and we are not minimized
2844 if not self
.config
.do_not_show_new_episodes_dialog
:
2845 self
.new_episodes_show(episodes
, notification
=True)
2847 message
= N_('%d new episode available', '%d new episodes available', count
) % count
2848 self
.pbFeedUpdate
.set_text(message
)
2850 def _update_cover(self
, channel
):
2851 if channel
is not None and not os
.path
.exists(channel
.cover_file
) and channel
.image
:
2852 self
.cover_downloader
.request_cover(channel
)
2854 def update_feed_cache_proc(self
, channels
, select_url_afterwards
):
2855 total
= len(channels
)
2857 for updated
, channel
in enumerate(channels
):
2858 if not self
.feed_cache_update_cancelled
:
2860 channel
.update(max_episodes
=self
.config
.max_episodes_per_feed
, \
2861 mimetype_prefs
=self
.config
.mimetype_prefs
)
2862 self
._update
_cover
(channel
)
2863 except Exception, e
:
2864 d
= {'url': saxutils
.escape(channel
.url
), 'message': saxutils
.escape(str(e
))}
2866 message
= _('Error while updating %(url)s: %(message)s')
2868 message
= _('The feed at %(url)s could not be updated.')
2869 self
.notification(message
% d
, _('Error while updating feed'), widget
=self
.treeChannels
)
2870 log('Error: %s', str(e
), sender
=self
, traceback
=True)
2872 if self
.feed_cache_update_cancelled
:
2875 # By the time we get here the update may have already been cancelled
2876 if not self
.feed_cache_update_cancelled
:
2877 def update_progress():
2878 d
= {'podcast': channel
.title
, 'position': updated
+1, 'total': total
}
2879 progression
= _('Updated %(podcast)s (%(position)d/%(total)d)') % d
2880 self
.pbFeedUpdate
.set_text(progression
)
2882 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
, progression
)
2883 self
.pbFeedUpdate
.set_fraction(float(updated
)/float(total
))
2884 util
.idle_add(update_progress
)
2886 updated_urls
= [c
.url
for c
in channels
]
2887 util
.idle_add(self
.update_feed_cache_finish_callback
, updated_urls
, select_url_afterwards
)
2889 def show_update_feeds_buttons(self
):
2890 # Make sure that the buttons for updating feeds
2891 # appear - this should happen after a feed update
2892 if gpodder
.ui
.maemo
:
2893 self
.btnUpdateSelectedFeed
.show()
2894 self
.toolFeedUpdateProgress
.hide()
2895 self
.btnCancelFeedUpdate
.hide()
2896 self
.btnCancelFeedUpdate
.set_is_important(False)
2897 self
.btnCancelFeedUpdate
.set_stock_id(gtk
.STOCK_CLOSE
)
2898 self
.toolbarSpacer
.set_expand(True)
2899 self
.toolbarSpacer
.set_draw(False)
2901 self
.hboxUpdateFeeds
.hide()
2902 self
.btnUpdateFeeds
.show()
2903 self
.itemUpdate
.set_sensitive(True)
2904 self
.itemUpdateChannel
.set_sensitive(True)
2906 def on_btnCancelFeedUpdate_clicked(self
, widget
):
2907 if not self
.feed_cache_update_cancelled
:
2908 self
.pbFeedUpdate
.set_text(_('Cancelling...'))
2909 self
.feed_cache_update_cancelled
= True
2910 self
.btnCancelFeedUpdate
.set_sensitive(False)
2912 self
.show_update_feeds_buttons()
2914 def update_feed_cache(self
, channels
=None, force_update
=True, select_url_afterwards
=None):
2915 if self
.updating_feed_cache
:
2916 if gpodder
.ui
.fremantle
:
2917 self
.feed_cache_update_cancelled
= True
2920 if not force_update
:
2921 self
.channels
= PodcastChannel
.load_from_db(self
.db
, self
.config
.download_dir
)
2922 self
.channel_list_changed
= True
2923 self
.update_podcast_list_model(select_url
=select_url_afterwards
)
2926 # Fix URLs if mygpo has rewritten them
2927 self
.rewrite_urls_mygpo()
2929 self
.updating_feed_cache
= True
2931 if channels
is None:
2932 # Only update podcasts for which updates are enabled
2933 channels
= [c
for c
in self
.channels
if c
.feed_update_enabled
]
2935 if gpodder
.ui
.fremantle
:
2936 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, True)
2937 hildon
.hildon_gtk_window_set_progress_indicator(self
.episodes_window
.main_window
, True)
2938 self
.fancy_progress_bar
.show()
2939 self
.button_subscribe
.set_sensitive(False)
2940 self
.button_refresh
.set_sensitive(False)
2941 self
.feed_cache_update_cancelled
= False
2943 self
.itemUpdate
.set_sensitive(False)
2944 self
.itemUpdateChannel
.set_sensitive(False)
2947 self
.tray_icon
.set_status(self
.tray_icon
.STATUS_UPDATING_FEED_CACHE
)
2949 self
.feed_cache_update_cancelled
= False
2950 self
.btnCancelFeedUpdate
.show()
2951 self
.btnCancelFeedUpdate
.set_sensitive(True)
2952 if gpodder
.ui
.maemo
:
2953 self
.toolbarSpacer
.set_expand(False)
2954 self
.toolbarSpacer
.set_draw(True)
2955 self
.btnUpdateSelectedFeed
.hide()
2956 self
.toolFeedUpdateProgress
.show_all()
2958 self
.btnCancelFeedUpdate
.set_image(gtk
.image_new_from_stock(gtk
.STOCK_STOP
, gtk
.ICON_SIZE_BUTTON
))
2959 self
.hboxUpdateFeeds
.show_all()
2960 self
.btnUpdateFeeds
.hide()
2962 if len(channels
) == 1:
2963 text
= _('Updating "%s"...') % channels
[0].title
2965 count
= len(channels
)
2966 text
= N_('Updating %d feed...', 'Updating %d feeds...', count
) % count
2967 self
.pbFeedUpdate
.set_text(text
)
2968 self
.pbFeedUpdate
.set_fraction(0)
2970 args
= (channels
, select_url_afterwards
)
2971 threading
.Thread(target
=self
.update_feed_cache_proc
, args
=args
).start()
2973 def on_gPodder_delete_event(self
, widget
, *args
):
2974 """Called when the GUI wants to close the window
2975 Displays a confirmation dialog (and closes/hides gPodder)
2978 downloading
= self
.download_status_model
.are_downloads_in_progress()
2980 # Only iconify if we are using the window's "X" button,
2981 # but not when we are using "Quit" in the menu or toolbar
2982 if self
.config
.on_quit_systray
and self
.tray_icon
and widget
.get_name() not in ('toolQuit', 'itemQuit'):
2983 self
.iconify_main_window()
2984 elif self
.config
.on_quit_ask
or downloading
:
2985 if gpodder
.ui
.fremantle
:
2986 self
.close_gpodder()
2987 elif gpodder
.ui
.diablo
:
2988 result
= self
.show_confirmation(_('Do you really want to quit gPodder now?'))
2990 self
.close_gpodder()
2993 dialog
= gtk
.MessageDialog(self
.gPodder
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_NONE
)
2994 dialog
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
2995 quit_button
= dialog
.add_button(gtk
.STOCK_QUIT
, gtk
.RESPONSE_CLOSE
)
2997 title
= _('Quit gPodder')
2999 message
= _('You are downloading episodes. You can resume downloads the next time you start gPodder. Do you want to quit now?')
3001 message
= _('Do you really want to quit gPodder now?')
3003 dialog
.set_title(title
)
3004 dialog
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title
, message
))
3006 cb_ask
= gtk
.CheckButton(_("Don't ask me again"))
3007 dialog
.vbox
.pack_start(cb_ask
)
3010 quit_button
.grab_focus()
3011 result
= dialog
.run()
3014 if result
== gtk
.RESPONSE_CLOSE
:
3015 if not downloading
and cb_ask
.get_active() == True:
3016 self
.config
.on_quit_ask
= False
3017 self
.close_gpodder()
3019 self
.close_gpodder()
3023 def close_gpodder(self
):
3024 """ clean everything and exit properly
3027 if self
.save_channels_opml():
3028 pass # FIXME: Add mygpo synchronization here
3030 self
.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'), important
=True)
3034 if self
.tray_icon
is not None:
3035 self
.tray_icon
.set_visible(False)
3037 # Notify all tasks to to carry out any clean-up actions
3038 self
.download_status_model
.tell_all_tasks_to_quit()
3040 while gtk
.events_pending():
3041 gtk
.main_iteration(False)
3048 def get_expired_episodes(self
):
3049 for channel
in self
.channels
:
3050 for episode
in channel
.get_downloaded_episodes():
3051 # Never consider locked episodes as old
3052 if episode
.is_locked
:
3055 # Never consider fresh episodes as old
3056 if episode
.age_in_days() < self
.config
.episode_old_age
:
3059 # Do not delete played episodes (except if configured)
3060 if episode
.is_played
:
3061 if not self
.config
.auto_remove_played_episodes
:
3064 # Do not delete unplayed episodes (except if configured)
3065 if not episode
.is_played
:
3066 if not self
.config
.auto_remove_unplayed_episodes
:
3071 def delete_episode_list(self
, episodes
, confirm
=True, skip_locked
=True):
3076 episodes
= [e
for e
in episodes
if not e
.is_locked
]
3079 title
= _('Episodes are locked')
3080 message
= _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
3081 self
.notification(message
, title
, widget
=self
.treeAvailable
)
3084 count
= len(episodes
)
3085 title
= N_('Delete %d episode?', 'Delete %d episodes?', count
) % count
3086 message
= _('Deleting episodes removes downloaded files.')
3088 if gpodder
.ui
.fremantle
:
3089 message
= '\n'.join([title
, message
])
3091 if confirm
and not self
.show_confirmation(message
, title
):
3094 progress
= ProgressIndicator(_('Deleting episodes'), \
3095 _('Please wait while episodes are deleted'), \
3096 parent
=self
.get_dialog_parent())
3098 def finish_deletion(episode_urls
, channel_urls
):
3099 progress
.on_finished()
3101 # Episodes have been deleted - persist the database
3104 self
.update_episode_list_icons(episode_urls
)
3105 self
.update_podcast_list_model(channel_urls
)
3106 self
.play_or_download()
3109 episode_urls
= set()
3110 channel_urls
= set()
3112 episodes_status_update
= []
3113 for idx
, episode
in enumerate(episodes
):
3114 progress
.on_progress(float(idx
)/float(len(episodes
)))
3115 if episode
.is_locked
and skip_locked
:
3116 log('Not deleting episode (is locked): %s', episode
.title
)
3118 log('Deleting episode: %s', episode
.title
)
3119 progress
.on_message(episode
.title
)
3120 episode
.delete_from_disk()
3121 episode_urls
.add(episode
.url
)
3122 channel_urls
.add(episode
.channel
.url
)
3123 episodes_status_update
.append(episode
)
3125 # Tell the shownotes window that we have removed the episode
3126 if self
.episode_shownotes_window
is not None and \
3127 self
.episode_shownotes_window
.episode
is not None and \
3128 self
.episode_shownotes_window
.episode
.url
== episode
.url
:
3129 util
.idle_add(self
.episode_shownotes_window
._download
_status
_changed
, None)
3131 # Notify the web service about the status update + upload
3132 self
.mygpo_client
.on_delete(episodes_status_update
)
3133 self
.mygpo_client
.flush()
3135 util
.idle_add(finish_deletion
, episode_urls
, channel_urls
)
3137 threading
.Thread(target
=thread_proc
).start()
3141 def on_itemRemoveOldEpisodes_activate( self
, widget
):
3142 if gpodder
.ui
.maemo
:
3144 ('maemo_remove_markup', None, None, _('Episode')),
3148 ('title_markup', None, None, _('Episode')),
3149 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3150 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3151 ('played_prop', None, None, _('Status')),
3152 ('age_prop', 'age_int_prop', gobject
.TYPE_INT
, _('Downloaded')),
3155 msg_older_than
= N_('Select older than %d day', 'Select older than %d days', self
.config
.episode_old_age
)
3156 selection_buttons
= {
3157 _('Select played'): lambda episode
: episode
.is_played
,
3158 msg_older_than
% self
.config
.episode_old_age
: lambda episode
: episode
.age_in_days() > self
.config
.episode_old_age
,
3161 instructions
= _('Select the episodes you want to delete:')
3165 for channel
in self
.channels
:
3166 for episode
in channel
.get_downloaded_episodes():
3167 # Disallow deletion of locked episodes that still exist
3168 if not episode
.is_locked
or not episode
.file_exists():
3169 episodes
.append(episode
)
3170 # Automatically select played and file-less episodes
3171 selected
.append(episode
.is_played
or \
3172 not episode
.file_exists())
3174 gPodderEpisodeSelector(self
.gPodder
, title
= _('Delete episodes'), instructions
= instructions
, \
3175 episodes
= episodes
, selected
= selected
, columns
= columns
, \
3176 stock_ok_button
= gtk
.STOCK_DELETE
, callback
= self
.delete_episode_list
, \
3177 selection_buttons
= selection_buttons
, _config
=self
.config
, \
3178 show_episode_shownotes
=self
.show_episode_shownotes
)
3180 def on_selected_episodes_status_changed(self
):
3181 # The order of the updates here is important! When "All episodes" is
3182 # selected, the update of the podcast list model depends on the episode
3183 # list selection to determine which podcasts are affected. Updating
3184 # the episode list could remove the selection if a filter is active.
3185 self
.update_podcast_list_model(selected
=True)
3186 self
.update_episode_list_icons(selected
=True)
3189 def mark_selected_episodes_new(self
):
3190 for episode
in self
.get_selected_episodes():
3192 self
.on_selected_episodes_status_changed()
3194 def mark_selected_episodes_old(self
):
3195 for episode
in self
.get_selected_episodes():
3197 self
.on_selected_episodes_status_changed()
3199 def on_item_toggle_played_activate( self
, widget
, toggle
= True, new_value
= False):
3200 for episode
in self
.get_selected_episodes():
3202 episode
.mark(is_played
=not episode
.is_played
)
3204 episode
.mark(is_played
=new_value
)
3205 self
.on_selected_episodes_status_changed()
3207 def on_item_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3208 for episode
in self
.get_selected_episodes():
3210 episode
.mark(is_locked
=not episode
.is_locked
)
3212 episode
.mark(is_locked
=new_value
)
3213 self
.on_selected_episodes_status_changed()
3215 def on_channel_toggle_lock_activate(self
, widget
, toggle
=True, new_value
=False):
3216 if self
.active_channel
is None:
3219 self
.active_channel
.channel_is_locked
= not self
.active_channel
.channel_is_locked
3220 self
.active_channel
.update_channel_lock()
3222 for episode
in self
.active_channel
.get_all_episodes():
3223 episode
.mark(is_locked
=self
.active_channel
.channel_is_locked
)
3225 self
.update_podcast_list_model(selected
=True)
3226 self
.update_episode_list_icons(all
=True)
3228 def on_itemUpdateChannel_activate(self
, widget
=None):
3229 if self
.active_channel
is None:
3230 title
= _('No podcast selected')
3231 message
= _('Please select a podcast in the podcasts list to update.')
3232 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3235 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3236 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3237 self
.update_feed_cache()
3239 self
.update_feed_cache(channels
=[self
.active_channel
])
3241 def on_itemUpdate_activate(self
, widget
=None):
3242 # Check if we have outstanding subscribe/unsubscribe actions
3243 if self
.on_add_remove_podcasts_mygpo():
3244 log('Update cancelled (received server changes)', sender
=self
)
3248 self
.update_feed_cache()
3250 gPodderWelcome(self
.gPodder
,
3251 center_on_widget
=self
.gPodder
,
3252 show_example_podcasts_callback
=self
.on_itemImportChannels_activate
,
3253 setup_my_gpodder_callback
=self
.on_download_subscriptions_from_mygpo
)
3255 def download_episode_list_paused(self
, episodes
):
3256 self
.download_episode_list(episodes
, True)
3258 def download_episode_list(self
, episodes
, add_paused
=False, force_start
=False):
3259 enable_update
= False
3261 for episode
in episodes
:
3262 log('Downloading episode: %s', episode
.title
, sender
= self
)
3263 if not episode
.was_downloaded(and_exists
=True):
3265 for task
in self
.download_tasks_seen
:
3266 if episode
.url
== task
.url
and task
.status
not in (task
.DOWNLOADING
, task
.QUEUED
):
3267 self
.download_queue_manager
.add_task(task
, force_start
)
3268 enable_update
= True
3276 task
= download
.DownloadTask(episode
, self
.config
)
3277 except Exception, e
:
3278 d
= {'episode': episode
.title
, 'message': str(e
)}
3279 message
= _('Download error while downloading %(episode)s: %(message)s')
3280 self
.show_message(message
% d
, _('Download error'), important
=True)
3281 log('Download error while downloading %s', episode
.title
, sender
=self
, traceback
=True)
3285 task
.status
= task
.PAUSED
3287 self
.mygpo_client
.on_download([task
.episode
])
3288 self
.download_queue_manager
.add_task(task
, force_start
)
3290 self
.download_status_model
.register_task(task
)
3291 enable_update
= True
3294 self
.enable_download_list_update()
3296 # Flush updated episode status
3297 self
.mygpo_client
.flush()
3299 def cancel_task_list(self
, tasks
):
3304 if task
.status
in (task
.QUEUED
, task
.DOWNLOADING
):
3305 task
.status
= task
.CANCELLED
3306 elif task
.status
== task
.PAUSED
:
3307 task
.status
= task
.CANCELLED
3308 # Call run, so the partial file gets deleted
3311 self
.update_episode_list_icons([task
.url
for task
in tasks
])
3312 self
.play_or_download()
3314 # Update the tab title and downloads list
3315 self
.update_downloads_list()
3317 def new_episodes_show(self
, episodes
, notification
=False, selected
=None):
3318 if gpodder
.ui
.maemo
:
3320 ('maemo_markup', None, None, _('Episode')),
3322 show_notification
= notification
3325 ('title_markup', None, None, _('Episode')),
3326 ('filesize_prop', 'length', gobject
.TYPE_INT
, _('Size')),
3327 ('pubdate_prop', 'pubDate', gobject
.TYPE_INT
, _('Released')),
3329 show_notification
= False
3331 instructions
= _('Select the episodes you want to download:')
3333 if self
.new_episodes_window
is not None:
3334 self
.new_episodes_window
.main_window
.destroy()
3335 self
.new_episodes_window
= None
3337 def download_episodes_callback(episodes
):
3338 self
.new_episodes_window
= None
3339 self
.download_episode_list(episodes
)
3341 if selected
is None:
3342 # Select all by default
3343 selected
= [True]*len(episodes
)
3345 self
.new_episodes_window
= gPodderEpisodeSelector(self
.gPodder
, \
3346 title
=_('New episodes available'), \
3347 instructions
=instructions
, \
3348 episodes
=episodes
, \
3350 selected
=selected
, \
3351 stock_ok_button
= 'gpodder-download', \
3352 callback
=download_episodes_callback
, \
3353 remove_callback
=lambda e
: e
.mark_old(), \
3354 remove_action
=_('Mark as old'), \
3355 remove_finished
=self
.episode_new_status_changed
, \
3356 _config
=self
.config
, \
3357 show_notification
=show_notification
, \
3358 show_episode_shownotes
=self
.show_episode_shownotes
)
3360 def on_itemDownloadAllNew_activate(self
, widget
, *args
):
3361 if not self
.offer_new_episodes():
3362 self
.show_message(_('Please check for new episodes later.'), \
3363 _('No new episodes available'), widget
=self
.btnUpdateFeeds
)
3365 def get_new_episodes(self
, channels
=None):
3366 if channels
is None:
3367 channels
= self
.channels
3369 for channel
in channels
:
3370 for episode
in channel
.get_new_episodes(downloading
=self
.episode_is_downloading
):
3371 episodes
.append(episode
)
3375 def on_sync_to_ipod_activate(self
, widget
, episodes
=None):
3376 self
.sync_ui
.on_synchronize_episodes(self
.channels
, episodes
)
3378 def commit_changes_to_database(self
):
3379 """This will be called after the sync process is finished"""
3382 def on_cleanup_ipod_activate(self
, widget
, *args
):
3383 self
.sync_ui
.on_cleanup_device()
3385 def on_manage_device_playlist(self
, widget
):
3386 self
.sync_ui
.on_manage_device_playlist()
3388 def show_hide_tray_icon(self
):
3389 if self
.config
.display_tray_icon
and have_trayicon
and self
.tray_icon
is None:
3390 self
.tray_icon
= GPodderStatusIcon(self
, gpodder
.icon_file
, self
.config
)
3391 elif not self
.config
.display_tray_icon
and self
.tray_icon
is not None:
3392 self
.tray_icon
.set_visible(False)
3394 self
.tray_icon
= None
3396 if self
.config
.minimize_to_tray
and self
.tray_icon
:
3397 self
.tray_icon
.set_visible(self
.is_iconified())
3398 elif self
.tray_icon
:
3399 self
.tray_icon
.set_visible(True)
3401 def on_itemShowAllEpisodes_activate(self
, widget
):
3402 self
.config
.podcast_list_view_all
= widget
.get_active()
3404 def on_itemShowToolbar_activate(self
, widget
):
3405 self
.config
.show_toolbar
= self
.itemShowToolbar
.get_active()
3407 def on_itemShowDescription_activate(self
, widget
):
3408 self
.config
.episode_list_descriptions
= self
.itemShowDescription
.get_active()
3410 def on_item_view_hide_boring_podcasts_toggled(self
, toggleaction
):
3411 self
.config
.podcast_list_hide_boring
= toggleaction
.get_active()
3412 if self
.config
.podcast_list_hide_boring
:
3413 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3415 self
.podcast_list_model
.set_view_mode(-1)
3417 def on_item_view_podcasts_changed(self
, radioaction
, current
):
3419 if current
== self
.item_view_podcasts_all
:
3420 self
.podcast_list_model
.set_view_mode(-1)
3421 elif current
== self
.item_view_podcasts_downloaded
:
3422 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3423 elif current
== self
.item_view_podcasts_unplayed
:
3424 self
.podcast_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3426 self
.config
.podcast_list_view_mode
= self
.podcast_list_model
.get_view_mode()
3428 def on_item_view_episodes_changed(self
, radioaction
, current
):
3429 if current
== self
.item_view_episodes_all
:
3430 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_ALL
)
3431 elif current
== self
.item_view_episodes_undeleted
:
3432 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNDELETED
)
3433 elif current
== self
.item_view_episodes_downloaded
:
3434 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_DOWNLOADED
)
3435 elif current
== self
.item_view_episodes_unplayed
:
3436 self
.episode_list_model
.set_view_mode(EpisodeListModel
.VIEW_UNPLAYED
)
3438 self
.config
.episode_list_view_mode
= self
.episode_list_model
.get_view_mode()
3440 if self
.config
.podcast_list_hide_boring
and not gpodder
.ui
.fremantle
:
3441 self
.podcast_list_model
.set_view_mode(self
.config
.episode_list_view_mode
)
3443 def update_item_device( self
):
3444 if not gpodder
.ui
.fremantle
:
3445 if self
.config
.device_type
!= 'none':
3446 self
.itemDevice
.set_visible(True)
3447 self
.itemDevice
.label
= self
.get_device_name()
3449 self
.itemDevice
.set_visible(False)
3451 def properties_closed( self
):
3452 self
.preferences_dialog
= None
3453 self
.show_hide_tray_icon()
3454 self
.update_item_device()
3455 if gpodder
.ui
.maemo
:
3456 selection
= self
.treeAvailable
.get_selection()
3457 if self
.config
.maemo_enable_gestures
or \
3458 self
.config
.enable_fingerscroll
:
3459 selection
.set_mode(gtk
.SELECTION_SINGLE
)
3461 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
3463 def on_itemPreferences_activate(self
, widget
, *args
):
3464 self
.preferences_dialog
= gPodderPreferences(self
.main_window
, \
3465 _config
=self
.config
, \
3466 callback_finished
=self
.properties_closed
, \
3467 user_apps_reader
=self
.user_apps_reader
, \
3468 parent_window
=self
.main_window
, \
3469 mygpo_client
=self
.mygpo_client
, \
3470 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3472 # Initial message to relayout window (in case it's opened in portrait mode
3473 self
.preferences_dialog
.on_window_orientation_changed(self
._last
_orientation
)
3475 def on_itemDependencies_activate(self
, widget
):
3476 gPodderDependencyManager(self
.gPodder
)
3478 def on_goto_mygpo(self
, widget
):
3479 self
.mygpo_client
.open_website()
3481 def on_download_subscriptions_from_mygpo(self
, action
=None):
3482 title
= _('Login to gpodder.net')
3483 message
= _('Please login to download your subscriptions.')
3484 success
, (username
, password
) = self
.show_login_dialog(title
, message
, \
3485 self
.config
.mygpo_username
, self
.config
.mygpo_password
)
3489 self
.config
.mygpo_username
= username
3490 self
.config
.mygpo_password
= password
3492 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3493 custom_title
=_('Subscriptions on gpodder.net'), \
3494 add_urls_callback
=self
.add_podcast_list
, \
3495 hide_url_entry
=True)
3497 # TODO: Refactor this into "gpodder.my" or mygpoclient, so that
3498 # we do not have to hardcode the URL here
3499 OPML_URL
= 'http://gpodder.net/subscriptions/%s.opml' % self
.config
.mygpo_username
3500 url
= util
.url_add_authentication(OPML_URL
, \
3501 self
.config
.mygpo_username
, \
3502 self
.config
.mygpo_password
)
3503 dir.download_opml_file(url
)
3505 def on_mygpo_settings_activate(self
, action
=None):
3506 # This dialog is only used for Maemo 4
3507 if not gpodder
.ui
.diablo
:
3510 settings
= MygPodderSettings(self
.main_window
, \
3511 config
=self
.config
, \
3512 mygpo_client
=self
.mygpo_client
, \
3513 on_send_full_subscriptions
=self
.on_send_full_subscriptions
)
3515 def on_itemAddChannel_activate(self
, widget
=None):
3516 gPodderAddPodcast(self
.gPodder
, \
3517 add_urls_callback
=self
.add_podcast_list
)
3519 def on_itemEditChannel_activate(self
, widget
, *args
):
3520 if self
.active_channel
is None:
3521 title
= _('No podcast selected')
3522 message
= _('Please select a podcast in the podcasts list to edit.')
3523 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3526 callback_closed
= lambda: self
.update_podcast_list_model(selected
=True)
3527 gPodderChannel(self
.main_window
, \
3528 channel
=self
.active_channel
, \
3529 callback_closed
=callback_closed
, \
3530 cover_downloader
=self
.cover_downloader
)
3532 def on_itemMassUnsubscribe_activate(self
, item
=None):
3534 ('title', None, None, _('Podcast')),
3537 # We're abusing the Episode Selector for selecting Podcasts here,
3538 # but it works and looks good, so why not? -- thp
3539 gPodderEpisodeSelector(self
.main_window
, \
3540 title
=_('Remove podcasts'), \
3541 instructions
=_('Select the podcast you want to remove.'), \
3542 episodes
=self
.channels
, \
3544 size_attribute
=None, \
3545 stock_ok_button
=_('Remove'), \
3546 callback
=self
.remove_podcast_list
, \
3547 _config
=self
.config
)
3549 def remove_podcast_list(self
, channels
, confirm
=True):
3551 log('No podcasts selected for deletion', sender
=self
)
3554 if len(channels
) == 1:
3555 title
= _('Removing podcast')
3556 info
= _('Please wait while the podcast is removed')
3557 message
= _('Do you really want to remove this podcast and its episodes?')
3559 title
= _('Removing podcasts')
3560 info
= _('Please wait while the podcasts are removed')
3561 message
= _('Do you really want to remove the selected podcasts and their episodes?')
3563 if confirm
and not self
.show_confirmation(message
, title
):
3566 progress
= ProgressIndicator(title
, info
, parent
=self
.get_dialog_parent())
3568 def finish_deletion(select_url
):
3569 # Upload subscription list changes to the web service
3570 self
.mygpo_client
.on_unsubscribe([c
.url
for c
in channels
])
3572 # Re-load the channels and select the desired new channel
3573 self
.update_feed_cache(force_update
=False, select_url_afterwards
=select_url
)
3574 progress
.on_finished()
3575 self
.update_podcasts_tab()
3580 for idx
, channel
in enumerate(channels
):
3581 # Update the UI for correct status messages
3582 progress
.on_progress(float(idx
)/float(len(channels
)))
3583 progress
.on_message(channel
.title
)
3585 # Delete downloaded episodes
3586 channel
.remove_downloaded()
3588 # cancel any active downloads from this channel
3589 for episode
in channel
.get_all_episodes():
3590 util
.idle_add(self
.download_status_model
.cancel_by_url
,
3593 if len(channels
) == 1:
3594 # get the URL of the podcast we want to select next
3595 if channel
in self
.channels
:
3596 position
= self
.channels
.index(channel
)
3600 if position
== len(self
.channels
)-1:
3601 # this is the last podcast, so select the URL
3602 # of the item before this one (i.e. the "new last")
3603 select_url
= self
.channels
[position
-1].url
3605 # there is a podcast after the deleted one, so
3606 # we simply select the one that comes after it
3607 select_url
= self
.channels
[position
+1].url
3609 # Remove the channel and clean the database entries
3611 self
.channels
.remove(channel
)
3613 # Clean up downloads and download directories
3614 self
.clean_up_downloads()
3616 self
.channel_list_changed
= True
3617 self
.save_channels_opml()
3619 # The remaining stuff is to be done in the GTK main thread
3620 util
.idle_add(finish_deletion
, select_url
)
3622 threading
.Thread(target
=thread_proc
).start()
3624 def on_itemRemoveChannel_activate(self
, widget
, *args
):
3625 if self
.active_channel
is None:
3626 title
= _('No podcast selected')
3627 message
= _('Please select a podcast in the podcasts list to remove.')
3628 self
.show_message( message
, title
, widget
=self
.treeChannels
)
3631 self
.remove_podcast_list([self
.active_channel
])
3633 def get_opml_filter(self
):
3634 filter = gtk
.FileFilter()
3635 filter.add_pattern('*.opml')
3636 filter.add_pattern('*.xml')
3637 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
3640 def on_item_import_from_file_activate(self
, widget
, filename
=None):
3641 if filename
is None:
3642 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3643 # FIXME: Hildonization on Fremantle
3644 dlg
= gtk
.FileChooserDialog(title
=_('Import from OPML'), parent
=None, action
=gtk
.FILE_CHOOSER_ACTION_OPEN
)
3645 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3646 dlg
.add_button(gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)
3647 elif gpodder
.ui
.diablo
:
3648 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
3649 dlg
.set_filter(self
.get_opml_filter())
3650 response
= dlg
.run()
3652 if response
== gtk
.RESPONSE_OK
:
3653 filename
= dlg
.get_filename()
3656 if filename
is not None:
3657 dir = gPodderPodcastDirectory(self
.gPodder
, _config
=self
.config
, \
3658 custom_title
=_('Import podcasts from OPML file'), \
3659 add_urls_callback
=self
.add_podcast_list
, \
3660 hide_url_entry
=True)
3661 dir.download_opml_file(filename
)
3663 def on_itemExportChannels_activate(self
, widget
, *args
):
3664 if not self
.channels
:
3665 title
= _('Nothing to export')
3666 message
= _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
3667 self
.show_message(message
, title
, widget
=self
.treeChannels
)
3670 if gpodder
.ui
.desktop
or gpodder
.ui
.fremantle
:
3671 # FIXME: Hildonization on Fremantle
3672 dlg
= gtk
.FileChooserDialog(title
=_('Export to OPML'), parent
=self
.gPodder
, action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
3673 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
3674 dlg
.add_button(gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)
3675 elif gpodder
.ui
.diablo
:
3676 dlg
= hildon
.FileChooserDialog(self
.gPodder
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
3677 dlg
.set_filter(self
.get_opml_filter())
3678 response
= dlg
.run()
3679 if response
== gtk
.RESPONSE_OK
:
3680 filename
= dlg
.get_filename()
3682 exporter
= opml
.Exporter( filename
)
3683 if exporter
.write(self
.channels
):
3684 count
= len(self
.channels
)
3685 title
= N_('%d subscription exported', '%d subscriptions exported', count
) % count
3686 self
.show_message(_('Your podcast list has been successfully exported.'), title
, widget
=self
.treeChannels
)
3688 self
.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'), important
=True)
3692 def on_itemImportChannels_activate(self
, widget
, *args
):
3693 if gpodder
.ui
.fremantle
:
3694 gPodderPodcastDirectory
.show_add_podcast_picker(self
.main_window
, \
3695 self
.config
.toplist_url
, \
3696 self
.config
.opml_url
, \
3697 self
.add_podcast_list
, \
3698 self
.on_itemAddChannel_activate
, \
3699 self
.on_download_subscriptions_from_mygpo
, \
3700 self
.show_text_edit_dialog
)
3702 dir = gPodderPodcastDirectory(self
.main_window
, _config
=self
.config
, \
3703 add_urls_callback
=self
.add_podcast_list
)
3704 util
.idle_add(dir.download_opml_file
, self
.config
.opml_url
)
3706 def on_homepage_activate(self
, widget
, *args
):
3707 util
.open_website(gpodder
.__url
__)
3709 def on_wiki_activate(self
, widget
, *args
):
3710 util
.open_website('http://gpodder.org/wiki/User_Manual')
3712 def on_bug_tracker_activate(self
, widget
, *args
):
3713 if gpodder
.ui
.maemo
:
3714 util
.open_website('http://bugs.maemo.org/enter_bug.cgi?product=gPodder')
3716 util
.open_website('https://bugs.gpodder.org/enter_bug.cgi?product=gPodder')
3718 def on_item_support_activate(self
, widget
):
3719 util
.open_website('http://gpodder.org/donate')
3721 def on_itemAbout_activate(self
, widget
, *args
):
3722 if gpodder
.ui
.fremantle
:
3723 from gpodder
.gtkui
.frmntl
.about
import HeAboutDialog
3724 HeAboutDialog
.present(self
.main_window
,
3727 gpodder
.__version
__,
3728 _('A podcast client with focus on usability'),
3729 gpodder
.__copyright
__,
3731 'http://bugs.maemo.org/enter_bug.cgi?product=gPodder',
3732 'http://gpodder.org/donate')
3735 dlg
= gtk
.AboutDialog()
3736 dlg
.set_transient_for(self
.main_window
)
3737 dlg
.set_name('gPodder')
3738 dlg
.set_version(gpodder
.__version
__)
3739 dlg
.set_copyright(gpodder
.__copyright
__)
3740 dlg
.set_comments(_('A podcast client with focus on usability'))
3741 dlg
.set_website(gpodder
.__url
__)
3742 dlg
.set_translator_credits( _('translator-credits'))
3743 dlg
.connect( 'response', lambda dlg
, response
: dlg
.destroy())
3745 if gpodder
.ui
.desktop
:
3746 # For the "GUI" version, we add some more
3747 # items to the about dialog (credits and logo)
3750 'Thomas Perl <thpinfo.com>',
3753 if os
.path
.exists(gpodder
.credits_file
):
3754 credits
= open(gpodder
.credits_file
).read().strip().split('\n')
3755 app_authors
+= ['', _('Patches, bug reports and donations by:')]
3756 app_authors
+= credits
3758 dlg
.set_authors(app_authors
)
3760 dlg
.set_logo(gtk
.gdk
.pixbuf_new_from_file(gpodder
.icon_file
))
3762 dlg
.set_logo_icon_name('gpodder')
3766 def on_wNotebook_switch_page(self
, widget
, *args
):
3768 if gpodder
.ui
.maemo
:
3769 self
.tool_downloads
.set_active(page_num
== 1)
3770 page
= self
.wNotebook
.get_nth_page(page_num
)
3771 tab_label
= self
.wNotebook
.get_tab_label(page
).get_text()
3772 if page_num
== 0 and self
.active_channel
is not None:
3773 self
.set_title(self
.active_channel
.title
)
3775 self
.set_title(tab_label
)
3777 self
.play_or_download()
3778 self
.menuChannels
.set_sensitive(True)
3779 self
.menuSubscriptions
.set_sensitive(True)
3780 # The message area in the downloads tab should be hidden
3781 # when the user switches away from the downloads tab
3782 if self
.message_area
is not None:
3783 self
.message_area
.hide()
3784 self
.message_area
= None
3786 self
.menuChannels
.set_sensitive(False)
3787 self
.menuSubscriptions
.set_sensitive(False)
3788 if gpodder
.ui
.desktop
:
3789 self
.toolDownload
.set_sensitive(False)
3790 self
.toolPlay
.set_sensitive(False)
3791 self
.toolTransfer
.set_sensitive(False)
3792 self
.toolCancel
.set_sensitive(False)
3794 def on_treeChannels_row_activated(self
, widget
, path
, *args
):
3795 # double-click action of the podcast list or enter
3796 self
.treeChannels
.set_cursor(path
)
3798 def on_treeChannels_cursor_changed(self
, widget
, *args
):
3799 ( model
, iter ) = self
.treeChannels
.get_selection().get_selected()
3801 if model
is not None and iter is not None:
3802 old_active_channel
= self
.active_channel
3803 self
.active_channel
= model
.get_value(iter, PodcastListModel
.C_CHANNEL
)
3805 if self
.active_channel
== old_active_channel
:
3808 if gpodder
.ui
.maemo
:
3809 self
.set_title(self
.active_channel
.title
)
3811 # Dirty hack to check for "All episodes" (see gpodder.gtkui.model)
3812 if getattr(self
.active_channel
, 'ALL_EPISODES_PROXY', False):
3813 self
.itemEditChannel
.set_visible(False)
3814 self
.itemRemoveChannel
.set_visible(False)
3816 self
.itemEditChannel
.set_visible(True)
3817 self
.itemRemoveChannel
.set_visible(True)
3819 self
.active_channel
= None
3820 self
.itemEditChannel
.set_visible(False)
3821 self
.itemRemoveChannel
.set_visible(False)
3823 self
.update_episode_list_model()
3825 def on_btnEditChannel_clicked(self
, widget
, *args
):
3826 self
.on_itemEditChannel_activate( widget
, args
)
3828 def get_podcast_urls_from_selected_episodes(self
):
3829 """Get a set of podcast URLs based on the selected episodes"""
3830 return set(episode
.channel
.url
for episode
in \
3831 self
.get_selected_episodes())
3833 def get_selected_episodes(self
):
3834 """Get a list of selected episodes from treeAvailable"""
3835 selection
= self
.treeAvailable
.get_selection()
3836 model
, paths
= selection
.get_selected_rows()
3838 episodes
= [model
.get_value(model
.get_iter(path
), EpisodeListModel
.C_EPISODE
) for path
in paths
]
3841 def on_transfer_selected_episodes(self
, widget
):
3842 self
.on_sync_to_ipod_activate(widget
, self
.get_selected_episodes())
3844 def on_playback_selected_episodes(self
, widget
):
3845 self
.playback_episodes(self
.get_selected_episodes())
3847 def on_shownotes_selected_episodes(self
, widget
):
3848 episodes
= self
.get_selected_episodes()
3850 episode
= episodes
.pop(0)
3851 self
.show_episode_shownotes(episode
)
3853 self
.show_message(_('Please select an episode from the episode list to display shownotes.'), _('No episode selected'), widget
=self
.treeAvailable
)
3855 def on_download_selected_episodes(self
, widget
):
3856 episodes
= self
.get_selected_episodes()
3857 self
.download_episode_list(episodes
)
3858 self
.update_episode_list_icons([episode
.url
for episode
in episodes
])
3859 self
.play_or_download()
3861 def on_treeAvailable_row_activated(self
, widget
, path
, view_column
):
3862 """Double-click/enter action handler for treeAvailable"""
3863 # We should only have one one selected as it was double clicked!
3864 e
= self
.get_selected_episodes()[0]
3866 if (self
.config
.double_click_episode_action
== 'download'):
3867 # If the episode has already been downloaded and exists then play it
3868 if e
.was_downloaded(and_exists
=True):
3869 self
.playback_episodes(self
.get_selected_episodes())
3870 # else download it if it is not already downloading
3871 elif not self
.episode_is_downloading(e
):
3872 self
.download_episode_list([e
])
3873 self
.update_episode_list_icons([e
.url
])
3874 self
.play_or_download()
3875 elif (self
.config
.double_click_episode_action
== 'stream'):
3876 # If we happen to have downloaded this episode simple play it
3877 if e
.was_downloaded(and_exists
=True):
3878 self
.playback_episodes(self
.get_selected_episodes())
3879 # else if streaming is possible stream it
3880 elif self
.streaming_possible():
3881 self
.playback_episodes(self
.get_selected_episodes())
3883 log('Unable to stream episode - default media player selected!', sender
=self
, traceback
=True)
3884 self
.show_message(_('Please check your media player settings in the preferences dialog.'), _('Unable to stream episode'), widget
=self
.toolPreferences
)
3886 # default action is to display show notes
3887 self
.on_shownotes_selected_episodes(widget
)
3889 def show_episode_shownotes(self
, episode
):
3890 if self
.episode_shownotes_window
is None:
3891 log('First-time use of episode window --- creating', sender
=self
)
3892 self
.episode_shownotes_window
= gPodderShownotes(self
.gPodder
, _config
=self
.config
, \
3893 _download_episode_list
=self
.download_episode_list
, \
3894 _playback_episodes
=self
.playback_episodes
, \
3895 _delete_episode_list
=self
.delete_episode_list
, \
3896 _episode_list_status_changed
=self
.episode_list_status_changed
, \
3897 _cancel_task_list
=self
.cancel_task_list
, \
3898 _episode_is_downloading
=self
.episode_is_downloading
, \
3899 _streaming_possible
=self
.streaming_possible())
3900 self
.episode_shownotes_window
.show(episode
)
3901 if self
.episode_is_downloading(episode
):
3902 self
.update_downloads_list()
3904 def restart_auto_update_timer(self
):
3905 if self
._auto
_update
_timer
_source
_id
is not None:
3906 log('Removing existing auto update timer.', sender
=self
)
3907 gobject
.source_remove(self
._auto
_update
_timer
_source
_id
)
3908 self
._auto
_update
_timer
_source
_id
= None
3910 if self
.config
.auto_update_feeds
and \
3911 self
.config
.auto_update_frequency
:
3912 interval
= 60*1000*self
.config
.auto_update_frequency
3913 log('Setting up auto update timer with interval %d.', \
3914 self
.config
.auto_update_frequency
, sender
=self
)
3915 self
._auto
_update
_timer
_source
_id
= gobject
.timeout_add(\
3916 interval
, self
._on
_auto
_update
_timer
)
3918 def _on_auto_update_timer(self
):
3919 log('Auto update timer fired.', sender
=self
)
3920 self
.update_feed_cache(force_update
=True)
3922 # Ask web service for sub changes (if enabled)
3923 self
.mygpo_client
.flush()
3927 def on_treeDownloads_row_activated(self
, widget
, *args
):
3928 # Use the standard way of working on the treeview
3929 selection
= self
.treeDownloads
.get_selection()
3930 (model
, paths
) = selection
.get_selected_rows()
3931 selected_tasks
= [(gtk
.TreeRowReference(model
, path
), model
.get_value(model
.get_iter(path
), 0)) for path
in paths
]
3933 for tree_row_reference
, task
in selected_tasks
:
3934 if task
.status
in (task
.DOWNLOADING
, task
.QUEUED
):
3935 task
.status
= task
.PAUSED
3936 elif task
.status
in (task
.CANCELLED
, task
.PAUSED
, task
.FAILED
):
3937 self
.download_queue_manager
.add_task(task
)
3938 self
.enable_download_list_update()
3939 elif task
.status
== task
.DONE
:
3940 model
.remove(model
.get_iter(tree_row_reference
.get_path()))
3942 self
.play_or_download()
3944 # Update the tab title and downloads list
3945 self
.update_downloads_list()
3947 def on_item_cancel_download_activate(self
, widget
):
3948 if self
.wNotebook
.get_current_page() == 0:
3949 selection
= self
.treeAvailable
.get_selection()
3950 (model
, paths
) = selection
.get_selected_rows()
3951 urls
= [model
.get_value(model
.get_iter(path
), \
3952 self
.episode_list_model
.C_URL
) for path
in paths
]
3953 selected_tasks
= [task
for task
in self
.download_tasks_seen \
3954 if task
.url
in urls
]
3956 selection
= self
.treeDownloads
.get_selection()
3957 (model
, paths
) = selection
.get_selected_rows()
3958 selected_tasks
= [model
.get_value(model
.get_iter(path
), \
3959 self
.download_status_model
.C_TASK
) for path
in paths
]
3960 self
.cancel_task_list(selected_tasks
)
3962 def on_btnCancelAll_clicked(self
, widget
, *args
):
3963 self
.cancel_task_list(self
.download_tasks_seen
)
3965 def on_btnDownloadedDelete_clicked(self
, widget
, *args
):
3966 episodes
= self
.get_selected_episodes()
3967 if len(episodes
) == 1:
3968 self
.delete_episode_list(episodes
, skip_locked
=False)
3970 self
.delete_episode_list(episodes
)
3972 def on_key_press(self
, widget
, event
):
3973 # Allow tab switching with Ctrl + PgUp/PgDown
3974 if event
.state
& gtk
.gdk
.CONTROL_MASK
:
3975 if event
.keyval
== gtk
.keysyms
.Page_Up
:
3976 self
.wNotebook
.prev_page()
3978 elif event
.keyval
== gtk
.keysyms
.Page_Down
:
3979 self
.wNotebook
.next_page()
3982 # After this code we only handle Maemo hardware keys,
3983 # so if we are not a Maemo app, we don't do anything
3984 if not gpodder
.ui
.maemo
:
3988 if event
.keyval
== gtk
.keysyms
.F7
: #plus
3990 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
3993 if diff
!= 0 and not self
.currently_updating
:
3994 selection
= self
.treeChannels
.get_selection()
3995 (model
, iter) = selection
.get_selected()
3996 new_path
= ((model
.get_path(iter)[0]+diff
)%len(model
),)
3997 selection
.select_path(new_path
)
3998 self
.treeChannels
.set_cursor(new_path
)
4003 def on_iconify(self
):
4005 self
.gPodder
.set_skip_taskbar_hint(True)
4006 if self
.config
.minimize_to_tray
:
4007 self
.tray_icon
.set_visible(True)
4009 self
.gPodder
.set_skip_taskbar_hint(False)
4011 def on_uniconify(self
):
4013 self
.gPodder
.set_skip_taskbar_hint(False)
4014 if self
.config
.minimize_to_tray
:
4015 self
.tray_icon
.set_visible(False)
4017 self
.gPodder
.set_skip_taskbar_hint(False)
4019 def uniconify_main_window(self
):
4020 if self
.is_iconified():
4021 # We need to hide and then show the window in WMs like Metacity
4022 # or KWin4 to move the window to the active workspace
4023 # (see http://gpodder.org/bug/1125)
4026 self
.gPodder
.present()
4028 def iconify_main_window(self
):
4029 if not self
.is_iconified():
4030 self
.gPodder
.iconify()
4032 def update_podcasts_tab(self
):
4033 if len(self
.channels
):
4034 if gpodder
.ui
.fremantle
:
4035 self
.button_refresh
.set_title(_('Check for new episodes'))
4036 self
.button_refresh
.show()
4038 self
.label2
.set_text(_('Podcasts (%d)') % len(self
.channels
))
4040 if gpodder
.ui
.fremantle
:
4041 self
.button_refresh
.hide()
4043 self
.label2
.set_text(_('Podcasts'))
4045 @dbus.service
.method(gpodder
.dbus_interface
)
4046 def show_gui_window(self
):
4047 parent
= self
.get_dialog_parent()
4050 @dbus.service
.method(gpodder
.dbus_interface
)
4051 def subscribe_to_url(self
, url
):
4052 gPodderAddPodcast(self
.gPodder
,
4053 add_urls_callback
=self
.add_podcast_list
,
4056 @dbus.service
.method(gpodder
.dbus_interface
)
4057 def mark_episode_played(self
, filename
):
4058 if filename
is None:
4061 for channel
in self
.channels
:
4062 for episode
in channel
.get_all_episodes():
4063 fn
= episode
.local_filename(create
=False, check_only
=True)
4065 episode
.mark(is_played
=True)
4067 self
.update_episode_list_icons([episode
.url
])
4068 self
.update_podcast_list_model([episode
.channel
.url
])
4074 def main(options
=None):
4075 gobject
.threads_init()
4076 gobject
.set_application_name('gPodder')
4078 if gpodder
.ui
.maemo
:
4079 # Try to enable the custom icon theme for gPodder on Maemo
4080 settings
= gtk
.settings_get_default()
4081 settings
.set_string_property('gtk-icon-theme-name', \
4082 'gpodder', __file__
)
4083 # Extend the search path for the optified icon theme (Maemo 5)
4084 icon_theme
= gtk
.icon_theme_get_default()
4085 icon_theme
.prepend_search_path('/opt/gpodder-icon-theme/')
4087 gtk
.window_set_default_icon_name('gpodder')
4088 gtk
.about_dialog_set_url_hook(lambda dlg
, link
, data
: util
.open_website(link
), None)
4091 dbus_main_loop
= dbus
.glib
.DBusGMainLoop(set_as_default
=True)
4092 gpodder
.dbus_session_bus
= dbus
.SessionBus(dbus_main_loop
)
4094 bus_name
= dbus
.service
.BusName(gpodder
.dbus_bus_name
, bus
=gpodder
.dbus_session_bus
)
4095 except dbus
.exceptions
.DBusException
, dbe
:
4096 log('Warning: Cannot get "on the bus".', traceback
=True)
4097 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, \
4098 gtk
.BUTTONS_CLOSE
, _('Cannot start gPodder'))
4099 dlg
.format_secondary_markup(_('D-Bus error: %s') % (str(dbe
),))
4100 dlg
.set_title('gPodder')
4105 util
.make_directory(gpodder
.home
)
4106 gpodder
.load_plugins()
4108 config
= UIConfig(gpodder
.config_file
)
4110 # Load hook modules and install the hook manager globally
4111 # if modules have been found an instantiated by the manager
4112 user_hooks
= hooks
.HookManager()
4113 if user_hooks
.has_modules():
4114 gpodder
.user_hooks
= user_hooks
4116 if gpodder
.ui
.diablo
:
4117 # Detect changing of SD cards between mmc1/mmc2 if a gpodder
4118 # folder exists there (allow moving "gpodder" between SD cards or USB)
4119 # Also allow moving "gpodder" to home folder (e.g. rootfs on SD)
4120 if not os
.path
.exists(config
.download_dir
):
4121 log('Downloads might have been moved. Trying to locate them...')
4122 for basedir
in ['/media/mmc1', '/media/mmc2']+glob
.glob('/media/usb/*')+['/home/user/MyDocs']:
4123 dir = os
.path
.join(basedir
, 'gpodder')
4124 if os
.path
.exists(dir):
4125 log('Downloads found in: %s', dir)
4126 config
.download_dir
= dir
4129 log('Downloads NOT FOUND in %s', dir)
4131 if config
.enable_fingerscroll
:
4132 BuilderWidget
.use_fingerscroll
= True
4133 elif gpodder
.ui
.fremantle
:
4134 config
.on_quit_ask
= False
4136 config
.mygpo_device_type
= util
.detect_device_type()
4138 gp
= gPodder(bus_name
, config
)
4141 if options
.subscribe
:
4142 util
.idle_add(gp
.subscribe_to_url
, options
.subscribe
)
4145 # handle "subscribe to podcast" events from firefox
4146 if platform
.system() == 'Darwin':
4147 from gpodder
import gpodderosx
4148 gpodderosx
.register_handlers(gp
)
4149 # end mac OS X stuff